jonah-fleet 1.2.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +9 -2
- package/dist/commands/contribute.d.ts +29 -0
- package/dist/commands/contribute.d.ts.map +1 -0
- package/dist/commands/daemon.d.ts +10 -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 +1781 -120
- package/dist/lib/daemon.d.ts +42 -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 +9 -5
- package/schema.json +44 -1
- package/templates/docs/AGENTS.template.md +15 -0
- package/templates/prompts/ORCHESTRATION.md +74 -3
- 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 +2 -0
- package/templates/prompts/peer-review.md +14 -5
- 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.1";
|
|
84
94
|
var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
|
|
85
95
|
|
|
86
96
|
// src/lib/manifest.ts
|
|
@@ -113,7 +123,8 @@ function createDefaultManifest(preset = "standard") {
|
|
|
113
123
|
optimizer: true,
|
|
114
124
|
"issues-housekeeping": true,
|
|
115
125
|
"dependency-update-security-check": true,
|
|
116
|
-
"product-planning": false
|
|
126
|
+
"product-planning": false,
|
|
127
|
+
"analytics-review": false
|
|
117
128
|
},
|
|
118
129
|
skills: PRESET_CONFIGS.standard.skills,
|
|
119
130
|
autoUpdate: {
|
|
@@ -137,18 +148,393 @@ function createDefaultManifest(preset = "standard") {
|
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
// src/lib/installer.ts
|
|
151
|
+
import fs3 from "fs";
|
|
152
|
+
import path3 from "path";
|
|
153
|
+
import { fileURLToPath } from "url";
|
|
154
|
+
|
|
155
|
+
// src/lib/detector.ts
|
|
140
156
|
import fs2 from "fs";
|
|
141
157
|
import path2 from "path";
|
|
142
|
-
|
|
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/):`));
|
|
@@ -276,8 +734,8 @@ async function runInit(options = {}) {
|
|
|
276
734
|
import pc2 from "picocolors";
|
|
277
735
|
|
|
278
736
|
// src/lib/diff.ts
|
|
279
|
-
import
|
|
280
|
-
import
|
|
737
|
+
import fs4 from "fs";
|
|
738
|
+
import path4 from "path";
|
|
281
739
|
function checkDrift(targetDir, manifest) {
|
|
282
740
|
const templatesDir = getTemplatesDir();
|
|
283
741
|
const report = {
|
|
@@ -287,43 +745,64 @@ function checkDrift(targetDir, manifest) {
|
|
|
287
745
|
modifiedWorkflows: [],
|
|
288
746
|
missingSkills: []
|
|
289
747
|
};
|
|
290
|
-
const targetPromptsDir =
|
|
291
|
-
const targetWorkflowsDir =
|
|
292
|
-
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");
|
|
293
751
|
const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
|
|
294
752
|
for (const file of basePrompts) {
|
|
295
|
-
const src =
|
|
296
|
-
const dest =
|
|
297
|
-
if (!
|
|
753
|
+
const src = path4.join(templatesDir, "prompts", file);
|
|
754
|
+
const dest = path4.join(targetPromptsDir, file);
|
|
755
|
+
if (!fs4.existsSync(dest)) {
|
|
298
756
|
report.missingPrompts.push(file);
|
|
299
|
-
} else if (
|
|
757
|
+
} else if (fs4.readFileSync(src, "utf8") !== fs4.readFileSync(dest, "utf8")) {
|
|
300
758
|
report.modifiedPrompts.push(file);
|
|
301
759
|
}
|
|
302
760
|
}
|
|
303
761
|
for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
|
|
304
762
|
if (!isEnabled) continue;
|
|
305
763
|
const promptFile = `${routineName}.md`;
|
|
306
|
-
const promptSrc =
|
|
307
|
-
const promptDest =
|
|
308
|
-
if (!
|
|
764
|
+
const promptSrc = path4.join(templatesDir, "prompts", promptFile);
|
|
765
|
+
const promptDest = path4.join(targetPromptsDir, promptFile);
|
|
766
|
+
if (!fs4.existsSync(promptDest)) {
|
|
309
767
|
report.missingPrompts.push(promptFile);
|
|
310
|
-
} else if (
|
|
768
|
+
} else if (fs4.existsSync(promptSrc) && fs4.readFileSync(promptSrc, "utf8") !== fs4.readFileSync(promptDest, "utf8")) {
|
|
311
769
|
report.modifiedPrompts.push(promptFile);
|
|
312
770
|
}
|
|
313
771
|
const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
|
|
314
772
|
for (const workflowFile of workflows) {
|
|
315
|
-
const wfSrc =
|
|
316
|
-
const wfDest =
|
|
317
|
-
if (!
|
|
773
|
+
const wfSrc = path4.join(templatesDir, "workflows", workflowFile);
|
|
774
|
+
const wfDest = path4.join(targetWorkflowsDir, workflowFile);
|
|
775
|
+
if (!fs4.existsSync(wfDest)) {
|
|
318
776
|
report.missingWorkflows.push(workflowFile);
|
|
319
|
-
} else if (
|
|
320
|
-
|
|
777
|
+
} else if (fs4.existsSync(wfSrc)) {
|
|
778
|
+
const rawSrc = fs4.readFileSync(wfSrc, "utf8");
|
|
779
|
+
const destContent = fs4.readFileSync(wfDest, "utf8");
|
|
780
|
+
const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
|
|
781
|
+
const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
|
|
782
|
+
if (expectedSrc !== destContent) {
|
|
783
|
+
report.modifiedWorkflows.push(workflowFile);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (manifest.autoUpdate?.enabled) {
|
|
789
|
+
const syncWfSrc = path4.join(templatesDir, "workflows/sync-fleet.yml");
|
|
790
|
+
const syncWfDest = path4.join(targetWorkflowsDir, "sync-fleet.yml");
|
|
791
|
+
if (!fs4.existsSync(syncWfDest)) {
|
|
792
|
+
report.missingWorkflows.push("sync-fleet.yml");
|
|
793
|
+
} else if (fs4.existsSync(syncWfSrc)) {
|
|
794
|
+
const rawSrc = fs4.readFileSync(syncWfSrc, "utf8");
|
|
795
|
+
const destContent = fs4.readFileSync(syncWfDest, "utf8");
|
|
796
|
+
const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
|
|
797
|
+
const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
|
|
798
|
+
if (expectedSrc !== destContent) {
|
|
799
|
+
report.modifiedWorkflows.push("sync-fleet.yml");
|
|
321
800
|
}
|
|
322
801
|
}
|
|
323
802
|
}
|
|
324
803
|
for (const skill of manifest.skills) {
|
|
325
|
-
const skillDestDir =
|
|
326
|
-
if (!
|
|
804
|
+
const skillDestDir = path4.join(targetSkillsDir, skill);
|
|
805
|
+
if (!fs4.existsSync(skillDestDir)) {
|
|
327
806
|
report.missingSkills.push(skill);
|
|
328
807
|
}
|
|
329
808
|
}
|
|
@@ -369,14 +848,14 @@ Run 'jonah-fleet sync --force' to apply updates.
|
|
|
369
848
|
}
|
|
370
849
|
|
|
371
850
|
// src/commands/status.ts
|
|
372
|
-
import
|
|
373
|
-
import
|
|
851
|
+
import fs7 from "fs";
|
|
852
|
+
import path7 from "path";
|
|
374
853
|
import pc5 from "picocolors";
|
|
375
854
|
|
|
376
855
|
// src/lib/fleet-query.ts
|
|
377
856
|
import { execFile } from "child_process";
|
|
378
|
-
import
|
|
379
|
-
import
|
|
857
|
+
import fs5 from "fs";
|
|
858
|
+
import path5 from "path";
|
|
380
859
|
import { promisify } from "util";
|
|
381
860
|
var execFileAsync = promisify(execFile);
|
|
382
861
|
var defaultGhExecutor = async (args) => {
|
|
@@ -555,11 +1034,11 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
|
|
|
555
1034
|
staleWarnings: []
|
|
556
1035
|
};
|
|
557
1036
|
try {
|
|
558
|
-
if (
|
|
559
|
-
const manifestPath =
|
|
560
|
-
if (
|
|
1037
|
+
if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
|
|
1038
|
+
const manifestPath = path5.join(repoIdentifier, "agents-manifest.json");
|
|
1039
|
+
if (fs5.existsSync(manifestPath)) {
|
|
561
1040
|
try {
|
|
562
|
-
const raw = JSON.parse(
|
|
1041
|
+
const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
|
|
563
1042
|
result.fleetVersion = raw.version;
|
|
564
1043
|
result.preset = raw.preset;
|
|
565
1044
|
} catch {
|
|
@@ -639,18 +1118,18 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
|
|
|
639
1118
|
if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
|
|
640
1119
|
}
|
|
641
1120
|
const logContents = [];
|
|
642
|
-
if (
|
|
643
|
-
const logsDir =
|
|
644
|
-
if (
|
|
1121
|
+
if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
|
|
1122
|
+
const logsDir = path5.join(repoIdentifier, ".github/prompts/logs");
|
|
1123
|
+
if (fs5.existsSync(logsDir)) {
|
|
645
1124
|
const collectLogs = (dir) => {
|
|
646
|
-
const entries =
|
|
1125
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
647
1126
|
for (const entry of entries) {
|
|
648
|
-
const fullPath =
|
|
1127
|
+
const fullPath = path5.join(dir, entry.name);
|
|
649
1128
|
if (entry.isDirectory()) {
|
|
650
1129
|
collectLogs(fullPath);
|
|
651
1130
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
652
1131
|
try {
|
|
653
|
-
logContents.push(
|
|
1132
|
+
logContents.push(fs5.readFileSync(fullPath, "utf8"));
|
|
654
1133
|
} catch {
|
|
655
1134
|
}
|
|
656
1135
|
}
|
|
@@ -862,9 +1341,18 @@ function renderFleetDashboard(statuses, options = {}) {
|
|
|
862
1341
|
lines.push(
|
|
863
1342
|
` Repositories: ${pc3.bold(summary.totalRepos.toString())} | Active Claims: ${pc3.bold(summary.activeClaimsCount.toString())} ` + (summary.staleClaimsCount > 0 ? pc3.red(`(${summary.staleClaimsCount} stale)`) : pc3.green("(0 stale)")) + ` | Open PRs: ${pc3.bold(summary.openPRsCount.toString())} ` + pc3.gray(`(${summary.draftPRsCount} draft, ${summary.readyPRsCount} ready)`)
|
|
864
1343
|
);
|
|
1344
|
+
const CEILING_70_PERCENT = 875e4;
|
|
1345
|
+
const ceilingPct = summary.totalTokens7d / CEILING_70_PERCENT * 100;
|
|
1346
|
+
let budgetTag = pc3.green("[HEALTHY]");
|
|
1347
|
+
if (ceilingPct > 100) budgetTag = pc3.red(pc3.bold("[EXCEEDED]"));
|
|
1348
|
+
else if (ceilingPct >= 90) budgetTag = pc3.red(pc3.bold("[CRITICAL]"));
|
|
1349
|
+
else if (ceilingPct >= 70) budgetTag = pc3.yellow("[WARNING]");
|
|
865
1350
|
lines.push(
|
|
866
1351
|
` 7-Day Spend: ${pc3.bold(formatTokens(summary.totalTokens7d))} tokens ` + pc3.gray(`(in: ${formatTokens(summary.totalInputTokens7d)}, out: ${formatTokens(summary.totalOutputTokens7d)})`) + ` | Est. Cost: ${pc3.bold(pc3.green(formatCurrency(summary.totalEstimatedCost7d)))} across ${pc3.bold(summary.totalRuns7d.toString())} runs`
|
|
867
1352
|
);
|
|
1353
|
+
lines.push(
|
|
1354
|
+
` Weekly Budget: ${pc3.bold(formatTokens(summary.totalTokens7d))} / ${formatTokens(CEILING_70_PERCENT)} tokens (${ceilingPct.toFixed(1)}% of 70% ceiling) ${budgetTag}`
|
|
1355
|
+
);
|
|
868
1356
|
if (summary.byRoutine && Object.keys(summary.byRoutine).length > 0 && (options.tokens || options.detailed || statuses.length > 1)) {
|
|
869
1357
|
lines.push(pc3.bold("\n Fleet Spend by Routine:"));
|
|
870
1358
|
const fleetRoutines = Object.values(summary.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
@@ -883,20 +1371,20 @@ function renderFleetDashboard(statuses, options = {}) {
|
|
|
883
1371
|
import pc4 from "picocolors";
|
|
884
1372
|
|
|
885
1373
|
// src/lib/global-config.ts
|
|
886
|
-
import
|
|
887
|
-
import
|
|
1374
|
+
import fs6 from "fs";
|
|
1375
|
+
import path6 from "path";
|
|
888
1376
|
import os from "os";
|
|
889
1377
|
function getDefaultGlobalConfigPath() {
|
|
890
|
-
const baseDir = process.env.JONAH_FLEET_CONFIG_DIR ||
|
|
891
|
-
return
|
|
1378
|
+
const baseDir = process.env.JONAH_FLEET_CONFIG_DIR || path6.join(os.homedir(), ".jonah-fleet");
|
|
1379
|
+
return path6.join(baseDir, "config.json");
|
|
892
1380
|
}
|
|
893
1381
|
function loadGlobalConfig(customPath) {
|
|
894
1382
|
const filePath = customPath || getDefaultGlobalConfigPath();
|
|
895
|
-
if (!
|
|
1383
|
+
if (!fs6.existsSync(filePath)) {
|
|
896
1384
|
return { repositories: [] };
|
|
897
1385
|
}
|
|
898
1386
|
try {
|
|
899
|
-
const raw =
|
|
1387
|
+
const raw = fs6.readFileSync(filePath, "utf8");
|
|
900
1388
|
const parsed = JSON.parse(raw);
|
|
901
1389
|
return {
|
|
902
1390
|
repositories: Array.isArray(parsed.repositories) ? parsed.repositories : []
|
|
@@ -907,11 +1395,11 @@ function loadGlobalConfig(customPath) {
|
|
|
907
1395
|
}
|
|
908
1396
|
function saveGlobalConfig(config, customPath) {
|
|
909
1397
|
const filePath = customPath || getDefaultGlobalConfigPath();
|
|
910
|
-
const dir =
|
|
911
|
-
if (!
|
|
912
|
-
|
|
1398
|
+
const dir = path6.dirname(filePath);
|
|
1399
|
+
if (!fs6.existsSync(dir)) {
|
|
1400
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
913
1401
|
}
|
|
914
|
-
|
|
1402
|
+
fs6.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
915
1403
|
}
|
|
916
1404
|
function addGlobalRepository(repo, customPath) {
|
|
917
1405
|
const config = loadGlobalConfig(customPath);
|
|
@@ -1039,19 +1527,19 @@ async function runStatus(options = {}) {
|
|
|
1039
1527
|
}
|
|
1040
1528
|
const drift = checkDrift(cwd, manifest);
|
|
1041
1529
|
const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
|
|
1042
|
-
const logsDir =
|
|
1530
|
+
const logsDir = path7.join(cwd, ".github/prompts/logs");
|
|
1043
1531
|
let tokenUsage = void 0;
|
|
1044
|
-
if (
|
|
1532
|
+
if (fs7.existsSync(logsDir)) {
|
|
1045
1533
|
const logContents = [];
|
|
1046
1534
|
const collectLogs = (dir) => {
|
|
1047
|
-
const entries =
|
|
1535
|
+
const entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
1048
1536
|
for (const entry of entries) {
|
|
1049
|
-
const fullPath =
|
|
1537
|
+
const fullPath = path7.join(dir, entry.name);
|
|
1050
1538
|
if (entry.isDirectory()) {
|
|
1051
1539
|
collectLogs(fullPath);
|
|
1052
1540
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1053
1541
|
try {
|
|
1054
|
-
logContents.push(
|
|
1542
|
+
logContents.push(fs7.readFileSync(fullPath, "utf8"));
|
|
1055
1543
|
} catch {
|
|
1056
1544
|
}
|
|
1057
1545
|
}
|
|
@@ -1141,40 +1629,1209 @@ async function runStatus(options = {}) {
|
|
|
1141
1629
|
// src/commands/contribute.ts
|
|
1142
1630
|
import { execSync } from "child_process";
|
|
1143
1631
|
import pc6 from "picocolors";
|
|
1632
|
+
function prepareContributionPayload(options = {}) {
|
|
1633
|
+
const repo = options.repo || "juliendurandeu/jonah-fleet";
|
|
1634
|
+
const title = options.title || "fix(prompts): improve orchestrator routine handling";
|
|
1635
|
+
const body = options.body || "Proposed prompt optimization discovered during autonomous execution runs.";
|
|
1636
|
+
const branchName = `contrib/optimize-${Date.now()}`;
|
|
1637
|
+
const prCommand = `gh pr create --repo ${repo} --title "${title}" --body "${body}"`;
|
|
1638
|
+
return {
|
|
1639
|
+
repo,
|
|
1640
|
+
branchName,
|
|
1641
|
+
title,
|
|
1642
|
+
body,
|
|
1643
|
+
prompt: options.prompt,
|
|
1644
|
+
prCommand
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1144
1647
|
async function runContribute(options = {}) {
|
|
1145
1648
|
console.log(pc6.cyan(`
|
|
1146
1649
|
\u{1F680} Jonah Fleet Upstream Contribution Bridge
|
|
1147
1650
|
`));
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
console.log(`
|
|
1151
|
-
console.log(`
|
|
1152
|
-
console.log(`Body: ${pc6.gray(body)}
|
|
1651
|
+
const payload = prepareContributionPayload(options);
|
|
1652
|
+
console.log(`Preparing upstream contribution PR against ${pc6.bold(payload.repo)}...`);
|
|
1653
|
+
console.log(`Title: ${pc6.green(payload.title)}`);
|
|
1654
|
+
console.log(`Body: ${pc6.gray(payload.body)}
|
|
1153
1655
|
`);
|
|
1656
|
+
if (options.dryRun) {
|
|
1657
|
+
console.log(pc6.yellow(`[DRY-RUN] Would create branch: ${payload.branchName}`));
|
|
1658
|
+
console.log(pc6.yellow(`[DRY-RUN] Would execute: ${payload.prCommand}
|
|
1659
|
+
`));
|
|
1660
|
+
return {
|
|
1661
|
+
success: true,
|
|
1662
|
+
dryRun: true,
|
|
1663
|
+
branchName: payload.branchName,
|
|
1664
|
+
title: payload.title
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1154
1667
|
try {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1668
|
+
console.log(`Creating branch ${pc6.cyan(payload.branchName)}...`);
|
|
1669
|
+
if (options.executor) {
|
|
1670
|
+
await options.executor(["auth", "status"]);
|
|
1671
|
+
const prUrl = await options.executor([
|
|
1672
|
+
"pr",
|
|
1673
|
+
"create",
|
|
1674
|
+
"--repo",
|
|
1675
|
+
payload.repo,
|
|
1676
|
+
"--title",
|
|
1677
|
+
payload.title,
|
|
1678
|
+
"--body",
|
|
1679
|
+
payload.body
|
|
1680
|
+
]);
|
|
1681
|
+
console.log(pc6.green(`\u2713 Submitted upstream contribution: ${prUrl}`));
|
|
1682
|
+
return {
|
|
1683
|
+
success: true,
|
|
1684
|
+
branchName: payload.branchName,
|
|
1685
|
+
title: payload.title,
|
|
1686
|
+
prUrl: typeof prUrl === "string" && prUrl.trim() ? prUrl.trim() : void 0
|
|
1687
|
+
};
|
|
1688
|
+
} else {
|
|
1689
|
+
try {
|
|
1690
|
+
execSync("gh auth status", { stdio: "pipe" });
|
|
1691
|
+
} catch {
|
|
1692
|
+
const errorMsg = "GitHub CLI (`gh`) is not authenticated. Run `gh auth login` first.";
|
|
1693
|
+
console.error(pc6.red(`\u274C ${errorMsg}`));
|
|
1694
|
+
if (options.throwOnError) throw new Error(errorMsg);
|
|
1695
|
+
return { success: false, error: errorMsg };
|
|
1696
|
+
}
|
|
1697
|
+
console.log(pc6.green(`\u2713 Ready to package and submit upstream contribution to ${payload.repo}.`));
|
|
1698
|
+
console.log(pc6.cyan(`Command executed by optimizer routine or operator:
|
|
1165
1699
|
`));
|
|
1166
|
-
|
|
1700
|
+
console.log(` ${payload.prCommand}
|
|
1167
1701
|
`);
|
|
1702
|
+
return {
|
|
1703
|
+
success: true,
|
|
1704
|
+
branchName: payload.branchName,
|
|
1705
|
+
title: payload.title
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1168
1708
|
} catch (err) {
|
|
1169
1709
|
console.error(pc6.red(`\u274C Error during contribution preparation: ${err.message}`));
|
|
1710
|
+
if (options.throwOnError) {
|
|
1711
|
+
throw err;
|
|
1712
|
+
}
|
|
1713
|
+
return {
|
|
1714
|
+
success: false,
|
|
1715
|
+
error: err.message
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/commands/telemetry.ts
|
|
1721
|
+
import fs9 from "fs";
|
|
1722
|
+
import path9 from "path";
|
|
1723
|
+
import pc8 from "picocolors";
|
|
1724
|
+
|
|
1725
|
+
// src/lib/telemetry.ts
|
|
1726
|
+
import fs8 from "fs";
|
|
1727
|
+
import path8 from "path";
|
|
1728
|
+
import pc7 from "picocolors";
|
|
1729
|
+
var GLOBAL_WEEKLY_TOKEN_BUDGET = 875e4;
|
|
1730
|
+
function parseLogToTelemetry(content, options = {}) {
|
|
1731
|
+
if (!content || typeof content !== "string") return null;
|
|
1732
|
+
const lines = content.split("\n");
|
|
1733
|
+
const metadata = {};
|
|
1734
|
+
for (const line of lines) {
|
|
1735
|
+
const match = line.match(/^\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/);
|
|
1736
|
+
if (!match) continue;
|
|
1737
|
+
const key = match[1].trim().toLowerCase();
|
|
1738
|
+
const val = match[2].trim().replace(/`/g, "");
|
|
1739
|
+
metadata[key] = val;
|
|
1740
|
+
}
|
|
1741
|
+
if (!metadata["timestamp"] && !metadata["routine"]) {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
const routine = metadata["routine"] || "unknown";
|
|
1745
|
+
const timestamp = metadata["timestamp"] || (/* @__PURE__ */ new Date()).toISOString();
|
|
1746
|
+
const result = metadata["result"] || "UNKNOWN";
|
|
1747
|
+
const errorReasonRaw = metadata["error reason"];
|
|
1748
|
+
const errorReason = errorReasonRaw && errorReasonRaw.toUpperCase() !== "N/A" ? errorReasonRaw : void 0;
|
|
1749
|
+
let failureCategory = metadata["failure category"];
|
|
1750
|
+
if (!failureCategory && result === "FAILURE" && errorReason) {
|
|
1751
|
+
const lower = errorReason.toLowerCase();
|
|
1752
|
+
if (lower.includes("token") || lower.includes("ceiling")) {
|
|
1753
|
+
failureCategory = "token_limit";
|
|
1754
|
+
} else if (lower.includes("build") || lower.includes("type-check")) {
|
|
1755
|
+
failureCategory = "build_error";
|
|
1756
|
+
} else if (lower.includes("infeasible") || lower.includes("blocker")) {
|
|
1757
|
+
failureCategory = "infeasible";
|
|
1758
|
+
} else if (lower.includes("conflict")) {
|
|
1759
|
+
failureCategory = "merge_conflict";
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
const inputTokens = parseInt((metadata["input tokens"] || "0").replace(/[^\d]/g, ""), 10) || 0;
|
|
1763
|
+
const outputTokens = parseInt((metadata["output tokens"] || "0").replace(/[^\d]/g, ""), 10) || 0;
|
|
1764
|
+
const totalTokens = inputTokens + outputTokens;
|
|
1765
|
+
const estimatedCost = parseFloat((metadata["estimated cost"] || "0").replace(/[^0-9.]/g, "")) || 0;
|
|
1766
|
+
let durationSeconds;
|
|
1767
|
+
if (metadata["duration"]) {
|
|
1768
|
+
const durNum = parseInt(metadata["duration"].replace(/[^\d]/g, ""), 10);
|
|
1769
|
+
if (!isNaN(durNum)) durationSeconds = durNum;
|
|
1770
|
+
}
|
|
1771
|
+
let iterationsUsed;
|
|
1772
|
+
let maxIterations;
|
|
1773
|
+
if (metadata["iterations used"]) {
|
|
1774
|
+
const iterMatch = metadata["iterations used"].match(/(\d+)\s*\/\s*(\d+)/);
|
|
1775
|
+
if (iterMatch) {
|
|
1776
|
+
iterationsUsed = parseInt(iterMatch[1], 10);
|
|
1777
|
+
maxIterations = parseInt(iterMatch[2], 10);
|
|
1778
|
+
} else {
|
|
1779
|
+
const singleNum = parseInt(metadata["iterations used"].replace(/[^\d]/g, ""), 10);
|
|
1780
|
+
if (!isNaN(singleNum)) iterationsUsed = singleNum;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
const promptSha = metadata["prompt sha"];
|
|
1784
|
+
return {
|
|
1785
|
+
schemaVersion: "1.0.0",
|
|
1786
|
+
routine,
|
|
1787
|
+
timestamp,
|
|
1788
|
+
repository: options.repository || "local",
|
|
1789
|
+
runId: options.runId,
|
|
1790
|
+
runNumber: options.runNumber,
|
|
1791
|
+
result,
|
|
1792
|
+
errorReason,
|
|
1793
|
+
failureCategory,
|
|
1794
|
+
inputTokens,
|
|
1795
|
+
outputTokens,
|
|
1796
|
+
totalTokens,
|
|
1797
|
+
estimatedCost,
|
|
1798
|
+
durationSeconds,
|
|
1799
|
+
iterationsUsed,
|
|
1800
|
+
maxIterations,
|
|
1801
|
+
promptSha
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function checkWeeklyBudgetLimit(usedTokens, ceilingTokens = GLOBAL_WEEKLY_TOKEN_BUDGET) {
|
|
1805
|
+
const remainingTokens = Math.max(0, ceilingTokens - usedTokens);
|
|
1806
|
+
const utilizationPercentage = ceilingTokens > 0 ? usedTokens / ceilingTokens * 100 : 0;
|
|
1807
|
+
const dailyBurnRate = usedTokens / 7;
|
|
1808
|
+
const projectedExhaustionDays = dailyBurnRate > 0 ? remainingTokens / dailyBurnRate : 999;
|
|
1809
|
+
let status = "HEALTHY";
|
|
1810
|
+
if (usedTokens > ceilingTokens) {
|
|
1811
|
+
status = "EXCEEDED";
|
|
1812
|
+
} else if (utilizationPercentage >= 90) {
|
|
1813
|
+
status = "CRITICAL";
|
|
1814
|
+
} else if (utilizationPercentage >= 70) {
|
|
1815
|
+
status = "WARNING";
|
|
1816
|
+
}
|
|
1817
|
+
return {
|
|
1818
|
+
weeklyCeilingTokens: ceilingTokens,
|
|
1819
|
+
usedTokens,
|
|
1820
|
+
remainingTokens,
|
|
1821
|
+
utilizationPercentage,
|
|
1822
|
+
status,
|
|
1823
|
+
dailyBurnRate,
|
|
1824
|
+
projectedExhaustionDays
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
function aggregateFleetTelemetry(summaries, options = {}) {
|
|
1828
|
+
const budgetCeiling = options.weeklyTokenBudget || GLOBAL_WEEKLY_TOKEN_BUDGET;
|
|
1829
|
+
let totalInputTokens = 0;
|
|
1830
|
+
let totalOutputTokens = 0;
|
|
1831
|
+
let totalCost = 0;
|
|
1832
|
+
let successCount = 0;
|
|
1833
|
+
let failureCount = 0;
|
|
1834
|
+
let bouncedCount = 0;
|
|
1835
|
+
const byRoutine = {};
|
|
1836
|
+
const byRepository = {};
|
|
1837
|
+
const failureCategories = {};
|
|
1838
|
+
for (const s of summaries) {
|
|
1839
|
+
totalInputTokens += s.inputTokens;
|
|
1840
|
+
totalOutputTokens += s.outputTokens;
|
|
1841
|
+
totalCost += s.estimatedCost;
|
|
1842
|
+
if (s.result === "SUCCESS") successCount++;
|
|
1843
|
+
else if (s.result === "FAILURE") failureCount++;
|
|
1844
|
+
else if (s.result === "BOUNCED_TO_DRAFT") bouncedCount++;
|
|
1845
|
+
if (s.failureCategory) {
|
|
1846
|
+
failureCategories[s.failureCategory] = (failureCategories[s.failureCategory] || 0) + 1;
|
|
1847
|
+
}
|
|
1848
|
+
if (!byRoutine[s.routine]) {
|
|
1849
|
+
byRoutine[s.routine] = {
|
|
1850
|
+
routine: s.routine,
|
|
1851
|
+
runCount: 0,
|
|
1852
|
+
totalInputTokens: 0,
|
|
1853
|
+
totalOutputTokens: 0,
|
|
1854
|
+
totalTokens: 0,
|
|
1855
|
+
totalEstimatedCost: 0,
|
|
1856
|
+
successCount: 0,
|
|
1857
|
+
failureCount: 0,
|
|
1858
|
+
bouncedCount: 0,
|
|
1859
|
+
avgDurationSeconds: 0,
|
|
1860
|
+
avgIterationsUsed: 0
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
const r = byRoutine[s.routine];
|
|
1864
|
+
r.runCount++;
|
|
1865
|
+
r.totalInputTokens += s.inputTokens;
|
|
1866
|
+
r.totalOutputTokens += s.outputTokens;
|
|
1867
|
+
r.totalTokens += s.totalTokens;
|
|
1868
|
+
r.totalEstimatedCost += s.estimatedCost;
|
|
1869
|
+
if (s.result === "SUCCESS") r.successCount++;
|
|
1870
|
+
else if (s.result === "FAILURE") r.failureCount++;
|
|
1871
|
+
else if (s.result === "BOUNCED_TO_DRAFT") r.bouncedCount++;
|
|
1872
|
+
if (s.durationSeconds) {
|
|
1873
|
+
r.avgDurationSeconds = (r.avgDurationSeconds * (r.runCount - 1) + s.durationSeconds) / r.runCount;
|
|
1874
|
+
}
|
|
1875
|
+
if (s.iterationsUsed) {
|
|
1876
|
+
r.avgIterationsUsed = (r.avgIterationsUsed * (r.runCount - 1) + s.iterationsUsed) / r.runCount;
|
|
1877
|
+
}
|
|
1878
|
+
if (!byRepository[s.repository]) {
|
|
1879
|
+
byRepository[s.repository] = {
|
|
1880
|
+
repository: s.repository,
|
|
1881
|
+
runCount: 0,
|
|
1882
|
+
totalTokens: 0,
|
|
1883
|
+
totalEstimatedCost: 0,
|
|
1884
|
+
successCount: 0,
|
|
1885
|
+
failureCount: 0
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
const repoObj = byRepository[s.repository];
|
|
1889
|
+
repoObj.runCount++;
|
|
1890
|
+
repoObj.totalTokens += s.totalTokens;
|
|
1891
|
+
repoObj.totalEstimatedCost += s.estimatedCost;
|
|
1892
|
+
if (s.result === "SUCCESS") repoObj.successCount++;
|
|
1893
|
+
else if (s.result === "FAILURE") repoObj.failureCount++;
|
|
1894
|
+
}
|
|
1895
|
+
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
1896
|
+
const budget = checkWeeklyBudgetLimit(totalTokens, budgetCeiling);
|
|
1897
|
+
return {
|
|
1898
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1899
|
+
totalRuns: summaries.length,
|
|
1900
|
+
successCount,
|
|
1901
|
+
failureCount,
|
|
1902
|
+
bouncedCount,
|
|
1903
|
+
totalInputTokens,
|
|
1904
|
+
totalOutputTokens,
|
|
1905
|
+
totalTokens,
|
|
1906
|
+
totalEstimatedCost: totalCost,
|
|
1907
|
+
budget,
|
|
1908
|
+
byRoutine,
|
|
1909
|
+
byRepository,
|
|
1910
|
+
failureCategories,
|
|
1911
|
+
events: summaries
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
async function emitTelemetry(summary, endpoint, customFetch = globalThis.fetch) {
|
|
1915
|
+
if (!endpoint || !endpoint.trim()) {
|
|
1916
|
+
return { success: true };
|
|
1917
|
+
}
|
|
1918
|
+
try {
|
|
1919
|
+
const res = await customFetch(endpoint.trim(), {
|
|
1920
|
+
method: "POST",
|
|
1921
|
+
headers: {
|
|
1922
|
+
"Content-Type": "application/json",
|
|
1923
|
+
"User-Agent": "jonah-fleet-telemetry/1.0"
|
|
1924
|
+
},
|
|
1925
|
+
body: JSON.stringify(summary)
|
|
1926
|
+
});
|
|
1927
|
+
if (!res.ok) {
|
|
1928
|
+
return { success: false, error: `HTTP ${res.status}: ${res.statusText}` };
|
|
1929
|
+
}
|
|
1930
|
+
return { success: true };
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
return { success: false, error: err.message || "Unknown network error" };
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
function collectLocalTelemetryLogs(dir, repositoryName = "local") {
|
|
1936
|
+
const summaries = [];
|
|
1937
|
+
const logsDir = path8.join(dir, ".github/prompts/logs");
|
|
1938
|
+
if (!fs8.existsSync(logsDir)) return summaries;
|
|
1939
|
+
const traverse = (currentDir) => {
|
|
1940
|
+
const entries = fs8.readdirSync(currentDir, { withFileTypes: true });
|
|
1941
|
+
for (const entry of entries) {
|
|
1942
|
+
const fullPath = path8.join(currentDir, entry.name);
|
|
1943
|
+
if (entry.isDirectory()) {
|
|
1944
|
+
traverse(fullPath);
|
|
1945
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1946
|
+
try {
|
|
1947
|
+
const content = fs8.readFileSync(fullPath, "utf8");
|
|
1948
|
+
const summary = parseLogToTelemetry(content, { repository: repositoryName });
|
|
1949
|
+
if (summary) summaries.push(summary);
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
traverse(logsDir);
|
|
1956
|
+
return summaries;
|
|
1957
|
+
}
|
|
1958
|
+
async function collectRepoTelemetry(repoIdentifier, executor = defaultGhExecutor, options = {}) {
|
|
1959
|
+
const summaries = [];
|
|
1960
|
+
if (fs8.existsSync(repoIdentifier) && fs8.statSync(repoIdentifier).isDirectory()) {
|
|
1961
|
+
return collectLocalTelemetryLogs(repoIdentifier, repoIdentifier);
|
|
1962
|
+
}
|
|
1963
|
+
try {
|
|
1964
|
+
const treeRaw = await executor([
|
|
1965
|
+
"api",
|
|
1966
|
+
`repos/${repoIdentifier}/git/trees/HEAD?recursive=1`
|
|
1967
|
+
]);
|
|
1968
|
+
const tree = JSON.parse(treeRaw);
|
|
1969
|
+
if (Array.isArray(tree.tree)) {
|
|
1970
|
+
const logFiles = tree.tree.filter((node) => node.path && node.path.startsWith(".github/prompts/logs/") && node.path.endsWith(".md")).slice(-(options.maxLogs || 25));
|
|
1971
|
+
for (const file of logFiles) {
|
|
1972
|
+
try {
|
|
1973
|
+
const fileRaw = await executor(["api", `repos/${repoIdentifier}/contents/${file.path}`]);
|
|
1974
|
+
const parsed = JSON.parse(fileRaw);
|
|
1975
|
+
if (parsed.content) {
|
|
1976
|
+
const content = Buffer.from(parsed.content, "base64").toString("utf8");
|
|
1977
|
+
const summary = parseLogToTelemetry(content, { repository: repoIdentifier });
|
|
1978
|
+
if (summary) summaries.push(summary);
|
|
1979
|
+
}
|
|
1980
|
+
} catch {
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
} catch {
|
|
1985
|
+
}
|
|
1986
|
+
return summaries;
|
|
1987
|
+
}
|
|
1988
|
+
function formatTokens2(num) {
|
|
1989
|
+
if (num >= 1e6) {
|
|
1990
|
+
return `${(num / 1e6).toFixed(2)}M`;
|
|
1991
|
+
}
|
|
1992
|
+
if (num >= 1e3) {
|
|
1993
|
+
return `${(num / 1e3).toFixed(1)}k`;
|
|
1994
|
+
}
|
|
1995
|
+
return num.toString();
|
|
1996
|
+
}
|
|
1997
|
+
function renderProgressBar(percentage, width = 25) {
|
|
1998
|
+
const clamped = Math.max(0, Math.min(100, percentage));
|
|
1999
|
+
const filledCount = Math.round(clamped / 100 * width);
|
|
2000
|
+
const emptyCount = width - filledCount;
|
|
2001
|
+
const filledBar = "\u2588".repeat(filledCount);
|
|
2002
|
+
const emptyBar = "\u2591".repeat(emptyCount);
|
|
2003
|
+
if (clamped >= 90) return pc7.red(filledBar) + pc7.gray(emptyBar);
|
|
2004
|
+
if (clamped >= 70) return pc7.yellow(filledBar) + pc7.gray(emptyBar);
|
|
2005
|
+
return pc7.green(filledBar) + pc7.gray(emptyBar);
|
|
2006
|
+
}
|
|
2007
|
+
function renderTelemetryDashboard(telemetry, options = {}) {
|
|
2008
|
+
if (options.json) {
|
|
2009
|
+
return JSON.stringify(telemetry, null, 2);
|
|
2010
|
+
}
|
|
2011
|
+
const lines = [];
|
|
2012
|
+
lines.push(pc7.bold(pc7.cyan("\n\u{1F6F0}\uFE0F Jonah Fleet Telemetry Hub & Token Economics\n")));
|
|
2013
|
+
const b = telemetry.budget;
|
|
2014
|
+
let statusBadge = pc7.green("[HEALTHY]");
|
|
2015
|
+
if (b.status === "WARNING") statusBadge = pc7.yellow("[WARNING]");
|
|
2016
|
+
else if (b.status === "CRITICAL") statusBadge = pc7.red(pc7.bold("[CRITICAL]"));
|
|
2017
|
+
else if (b.status === "EXCEEDED") statusBadge = pc7.red(pc7.bold("[BUDGET EXCEEDED]"));
|
|
2018
|
+
lines.push(pc7.bold("\u{1F4C8} Global Weekly Token Budget Ceiling (~70% Fleet Limit):"));
|
|
2019
|
+
lines.push(
|
|
2020
|
+
` ${renderProgressBar(b.utilizationPercentage, 30)} ${pc7.bold(`${b.utilizationPercentage.toFixed(1)}%`)} ${statusBadge}`
|
|
2021
|
+
);
|
|
2022
|
+
lines.push(
|
|
2023
|
+
` Used: ${pc7.bold(formatTokens2(b.usedTokens))} / ${formatTokens2(b.weeklyCeilingTokens)} tokens | Remaining: ${pc7.green(formatTokens2(b.remainingTokens))} | Daily Burn: ${formatTokens2(b.dailyBurnRate)}/day`
|
|
2024
|
+
);
|
|
2025
|
+
lines.push("\n" + pc7.bold("\u{1F310} Fleet Aggregate Spend:"));
|
|
2026
|
+
lines.push(
|
|
2027
|
+
` Total Runs: ${pc7.bold(telemetry.totalRuns.toString())} (${pc7.green(telemetry.successCount + " success")}, ${pc7.red(telemetry.failureCount + " failed")}, ${pc7.yellow(telemetry.bouncedCount + " bounced")})`
|
|
2028
|
+
);
|
|
2029
|
+
lines.push(
|
|
2030
|
+
` Total Tokens: ${pc7.bold(formatTokens2(telemetry.totalTokens))} ` + pc7.gray(`(in: ${formatTokens2(telemetry.totalInputTokens)}, out: ${formatTokens2(telemetry.totalOutputTokens)})`) + ` | Est. Cost: ${pc7.bold(pc7.green(`$${telemetry.totalEstimatedCost.toFixed(2)}`))}`
|
|
2031
|
+
);
|
|
2032
|
+
lines.push("\n" + pc7.bold("\u{1F916} Spend by Agent Routine:"));
|
|
2033
|
+
for (const [routineName, r] of Object.entries(telemetry.byRoutine)) {
|
|
2034
|
+
const costStr = pc7.green(`$${r.totalEstimatedCost.toFixed(2)}`);
|
|
2035
|
+
lines.push(
|
|
2036
|
+
` \u2022 ${pc7.cyan(routineName.padEnd(30))} Runs: ${pc7.bold(r.runCount.toString().padStart(2))} | Tokens: ${pc7.bold(formatTokens2(r.totalTokens).padStart(7))} | Cost: ${costStr.padStart(6)} | Avg Iter: ${r.avgIterationsUsed.toFixed(1)}`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
if (Object.keys(telemetry.byRepository).length > 0) {
|
|
2040
|
+
lines.push("\n" + pc7.bold("\u{1F4E6} Spend by Repository:"));
|
|
2041
|
+
for (const [repoName, repoObj] of Object.entries(telemetry.byRepository)) {
|
|
2042
|
+
lines.push(
|
|
2043
|
+
` \u2022 ${pc7.bold(repoName)}: ${formatTokens2(repoObj.totalTokens)} tokens across ${repoObj.runCount} runs ($${repoObj.totalEstimatedCost.toFixed(2)})`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const failKeys = Object.keys(telemetry.failureCategories);
|
|
2048
|
+
if (failKeys.length > 0) {
|
|
2049
|
+
lines.push("\n" + pc7.bold(pc7.red("\u26A0\uFE0F Failure Categories Breakdown:")));
|
|
2050
|
+
for (const cat of failKeys) {
|
|
2051
|
+
lines.push(` - ${cat}: ${telemetry.failureCategories[cat]} occurrences`);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
lines.push("\n" + pc7.gray("\u2500".repeat(65)) + "\n");
|
|
2055
|
+
return lines.join("\n");
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// src/commands/telemetry.ts
|
|
2059
|
+
async function runTelemetry(options = {}) {
|
|
2060
|
+
const cwd = options.cwd || process.cwd();
|
|
2061
|
+
const executor = options.executor || defaultGhExecutor;
|
|
2062
|
+
const manifest = loadManifest(cwd);
|
|
2063
|
+
const weeklyBudget = typeof options.budget === "number" ? options.budget : typeof options.budget === "string" ? parseInt(options.budget, 10) : manifest?.telemetry?.weeklyTokenBudget || GLOBAL_WEEKLY_TOKEN_BUDGET;
|
|
2064
|
+
if (options.action === "emit" || options.log) {
|
|
2065
|
+
let logPath = options.log;
|
|
2066
|
+
if (!logPath) {
|
|
2067
|
+
const logsDir = path9.join(cwd, ".github/prompts/logs");
|
|
2068
|
+
if (fs9.existsSync(logsDir)) {
|
|
2069
|
+
let latestFile = null;
|
|
2070
|
+
let latestMtime = 0;
|
|
2071
|
+
const findLogs = (dir) => {
|
|
2072
|
+
const entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
2073
|
+
for (const entry of entries) {
|
|
2074
|
+
const p = path9.join(dir, entry.name);
|
|
2075
|
+
if (entry.isDirectory()) findLogs(p);
|
|
2076
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2077
|
+
const stat = fs9.statSync(p);
|
|
2078
|
+
if (stat.mtimeMs > latestMtime) {
|
|
2079
|
+
latestMtime = stat.mtimeMs;
|
|
2080
|
+
latestFile = p;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
findLogs(logsDir);
|
|
2086
|
+
logPath = latestFile || void 0;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
if (!logPath || !fs9.existsSync(logPath)) {
|
|
2090
|
+
if (options.json) {
|
|
2091
|
+
console.log(JSON.stringify({ error: "No log file found to emit", path: logPath }, null, 2));
|
|
2092
|
+
} else {
|
|
2093
|
+
console.log(pc8.yellow(`
|
|
2094
|
+
\u26A0\uFE0F No log file found to emit: ${logPath || "none specified"}
|
|
2095
|
+
`));
|
|
2096
|
+
}
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const content = fs9.readFileSync(logPath, "utf8");
|
|
2100
|
+
const repoName = options.repo || process.env.GITHUB_REPOSITORY || (manifest ? "local" : "unknown");
|
|
2101
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
2102
|
+
const runNumber = process.env.GITHUB_RUN_NUMBER ? parseInt(process.env.GITHUB_RUN_NUMBER, 10) : void 0;
|
|
2103
|
+
const summary = parseLogToTelemetry(content, {
|
|
2104
|
+
repository: repoName,
|
|
2105
|
+
runId,
|
|
2106
|
+
runNumber
|
|
2107
|
+
});
|
|
2108
|
+
if (!summary) {
|
|
2109
|
+
if (options.json) {
|
|
2110
|
+
console.log(JSON.stringify({ error: "Failed to parse metadata from log file", path: logPath }, null, 2));
|
|
2111
|
+
} else {
|
|
2112
|
+
console.log(pc8.red(`
|
|
2113
|
+
\u274C Failed to parse valid telemetry metadata from: ${logPath}
|
|
2114
|
+
`));
|
|
2115
|
+
}
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
const endpoint = options.endpoint || process.env.JONAH_FLEET_TELEMETRY_ENDPOINT || process.env.TELEMETRY_ENDPOINT || manifest?.telemetry?.endpoint;
|
|
2119
|
+
const emitResult = await emitTelemetry(summary, endpoint);
|
|
2120
|
+
if (options.json) {
|
|
2121
|
+
console.log(JSON.stringify({ success: emitResult.success, error: emitResult.error, summary }, null, 2));
|
|
2122
|
+
} else {
|
|
2123
|
+
if (emitResult.success) {
|
|
2124
|
+
if (endpoint) {
|
|
2125
|
+
console.log(pc8.green(`\u2713 Successfully emitted telemetry summary for ${pc8.bold(summary.routine)} to ${endpoint}`));
|
|
2126
|
+
} else {
|
|
2127
|
+
console.log(pc8.gray(`\u2139 Telemetry summary parsed for ${summary.routine} (emission skipped: no endpoint configured)`));
|
|
2128
|
+
}
|
|
2129
|
+
} else {
|
|
2130
|
+
console.log(pc8.red(`\u274C Failed to emit telemetry to ${endpoint}: ${emitResult.error}`));
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
let targetRepos = [];
|
|
2136
|
+
if (options.repos && options.repos.length > 0) {
|
|
2137
|
+
targetRepos = options.repos;
|
|
2138
|
+
} else {
|
|
2139
|
+
targetRepos = getFleetRepositories(cwd);
|
|
2140
|
+
}
|
|
2141
|
+
const allSummaries = [];
|
|
2142
|
+
const localSummaries = collectLocalTelemetryLogs(cwd, process.env.GITHUB_REPOSITORY || "local");
|
|
2143
|
+
allSummaries.push(...localSummaries);
|
|
2144
|
+
for (const repo of targetRepos) {
|
|
2145
|
+
if (repo === "local" || repo === cwd) continue;
|
|
2146
|
+
try {
|
|
2147
|
+
const repoSummaries = await collectRepoTelemetry(repo, executor);
|
|
2148
|
+
allSummaries.push(...repoSummaries);
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
const aggregated = aggregateFleetTelemetry(allSummaries, { weeklyTokenBudget: weeklyBudget });
|
|
2153
|
+
const output = renderTelemetryDashboard(aggregated, { json: options.json });
|
|
2154
|
+
console.log(output);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/commands/run.ts
|
|
2158
|
+
import pc9 from "picocolors";
|
|
2159
|
+
|
|
2160
|
+
// src/lib/runner.ts
|
|
2161
|
+
import fs11 from "fs";
|
|
2162
|
+
import path11 from "path";
|
|
2163
|
+
import os2 from "os";
|
|
2164
|
+
import { spawn } from "child_process";
|
|
2165
|
+
|
|
2166
|
+
// src/lib/worktree.ts
|
|
2167
|
+
import fs10 from "fs";
|
|
2168
|
+
import path10 from "path";
|
|
2169
|
+
import { execFile as execFile2 } from "child_process";
|
|
2170
|
+
import { promisify as promisify2 } from "util";
|
|
2171
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
2172
|
+
function getWorktreesBaseDir(repoRoot) {
|
|
2173
|
+
return path10.join(repoRoot, ".jonah-fleet", "worktrees");
|
|
2174
|
+
}
|
|
2175
|
+
async function createWorktree(repoRoot, options) {
|
|
2176
|
+
const baseDir = getWorktreesBaseDir(repoRoot);
|
|
2177
|
+
fs10.mkdirSync(baseDir, { recursive: true });
|
|
2178
|
+
const sanitizedDirName = options.worktreeDirName || options.branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2179
|
+
const worktreePath = path10.join(baseDir, sanitizedDirName);
|
|
2180
|
+
if (fs10.existsSync(worktreePath)) {
|
|
2181
|
+
try {
|
|
2182
|
+
await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot });
|
|
2183
|
+
} catch {
|
|
2184
|
+
fs10.rmSync(worktreePath, { recursive: true, force: true });
|
|
2185
|
+
await execFileAsync2("git", ["worktree", "prune"], { cwd: repoRoot }).catch(() => {
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
let baseRef = options.baseRef;
|
|
2190
|
+
if (!baseRef) {
|
|
2191
|
+
try {
|
|
2192
|
+
await execFileAsync2("git", ["rev-parse", "--verify", "origin/main"], { cwd: repoRoot });
|
|
2193
|
+
baseRef = "origin/main";
|
|
2194
|
+
} catch {
|
|
2195
|
+
try {
|
|
2196
|
+
await execFileAsync2("git", ["rev-parse", "--verify", "main"], { cwd: repoRoot });
|
|
2197
|
+
baseRef = "main";
|
|
2198
|
+
} catch {
|
|
2199
|
+
baseRef = "HEAD";
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
try {
|
|
2204
|
+
await execFileAsync2("git", ["branch", "-D", options.branchName], { cwd: repoRoot });
|
|
2205
|
+
} catch {
|
|
2206
|
+
}
|
|
2207
|
+
await execFileAsync2("git", ["worktree", "add", worktreePath, "-b", options.branchName, baseRef], {
|
|
2208
|
+
cwd: repoRoot
|
|
2209
|
+
});
|
|
2210
|
+
return { worktreePath, branchName: options.branchName };
|
|
2211
|
+
}
|
|
2212
|
+
async function removeWorktree(repoRoot, worktreePath, options = {}) {
|
|
2213
|
+
try {
|
|
2214
|
+
if (fs10.existsSync(worktreePath)) {
|
|
2215
|
+
await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot });
|
|
2216
|
+
}
|
|
2217
|
+
} catch {
|
|
2218
|
+
if (fs10.existsSync(worktreePath)) {
|
|
2219
|
+
fs10.rmSync(worktreePath, { recursive: true, force: true });
|
|
2220
|
+
}
|
|
2221
|
+
} finally {
|
|
2222
|
+
await execFileAsync2("git", ["worktree", "prune"], { cwd: repoRoot }).catch(() => {
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
if (options.deleteBranch && options.branchName) {
|
|
2226
|
+
await execFileAsync2("git", ["branch", "-D", options.branchName], { cwd: repoRoot }).catch(() => {
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
async function listActiveWorktrees(repoRoot) {
|
|
2231
|
+
try {
|
|
2232
|
+
const { stdout } = await execFileAsync2("git", ["worktree", "list", "--porcelain"], { cwd: repoRoot });
|
|
2233
|
+
const entries = stdout.trim().split("\n\n");
|
|
2234
|
+
const worktrees = [];
|
|
2235
|
+
const baseDir = path10.resolve(getWorktreesBaseDir(repoRoot));
|
|
2236
|
+
for (const entry of entries) {
|
|
2237
|
+
if (!entry.trim()) continue;
|
|
2238
|
+
const lines = entry.split("\n");
|
|
2239
|
+
let currentPath = "";
|
|
2240
|
+
let currentBranch = "";
|
|
2241
|
+
let currentCommit = "";
|
|
2242
|
+
for (const line of lines) {
|
|
2243
|
+
if (line.startsWith("worktree ")) {
|
|
2244
|
+
currentPath = line.substring(9).trim();
|
|
2245
|
+
} else if (line.startsWith("HEAD ")) {
|
|
2246
|
+
currentCommit = line.substring(5).trim();
|
|
2247
|
+
} else if (line.startsWith("branch ")) {
|
|
2248
|
+
currentBranch = line.substring(7).replace("refs/heads/", "").trim();
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
const resolvedPath = path10.resolve(currentPath);
|
|
2252
|
+
if (resolvedPath.startsWith(baseDir)) {
|
|
2253
|
+
worktrees.push({
|
|
2254
|
+
path: resolvedPath,
|
|
2255
|
+
branch: currentBranch || "detached",
|
|
2256
|
+
commit: currentCommit
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return worktrees;
|
|
2261
|
+
} catch {
|
|
2262
|
+
return [];
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
async function cleanupStaleWorktrees(repoRoot) {
|
|
2266
|
+
let cleaned = 0;
|
|
2267
|
+
try {
|
|
2268
|
+
await execFileAsync2("git", ["worktree", "prune"], { cwd: repoRoot });
|
|
2269
|
+
const baseDir = getWorktreesBaseDir(repoRoot);
|
|
2270
|
+
if (fs10.existsSync(baseDir)) {
|
|
2271
|
+
const active = await listActiveWorktrees(repoRoot);
|
|
2272
|
+
const activePaths = new Set(active.map((w) => w.path));
|
|
2273
|
+
const dirs = fs10.readdirSync(baseDir);
|
|
2274
|
+
for (const dir of dirs) {
|
|
2275
|
+
const fullPath = path10.resolve(path10.join(baseDir, dir));
|
|
2276
|
+
if (!activePaths.has(fullPath)) {
|
|
2277
|
+
fs10.rmSync(fullPath, { recursive: true, force: true });
|
|
2278
|
+
cleaned++;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
} catch {
|
|
2283
|
+
}
|
|
2284
|
+
return cleaned;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/lib/runner.ts
|
|
2288
|
+
function discoverSkillsPrompt(targetDir) {
|
|
2289
|
+
const skillsDir = path11.join(targetDir, ".agents", "skills");
|
|
2290
|
+
if (!fs11.existsSync(skillsDir)) return "";
|
|
2291
|
+
let skillsPrompt = "";
|
|
2292
|
+
try {
|
|
2293
|
+
const entries = fs11.readdirSync(skillsDir, { withFileTypes: true });
|
|
2294
|
+
for (const entry of entries) {
|
|
2295
|
+
if (entry.isDirectory()) {
|
|
2296
|
+
const skillPath = path11.join(".agents", "skills", entry.name, "SKILL.md");
|
|
2297
|
+
const fullPath = path11.join(targetDir, skillPath);
|
|
2298
|
+
if (fs11.existsSync(fullPath)) {
|
|
2299
|
+
skillsPrompt += `Read and follow ${skillPath}. `;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
} catch {
|
|
2304
|
+
}
|
|
2305
|
+
return skillsPrompt;
|
|
2306
|
+
}
|
|
2307
|
+
function buildRoutinePrompt(targetDir, routine, options = {}) {
|
|
2308
|
+
const skillsPrompt = discoverSkillsPrompt(targetDir);
|
|
2309
|
+
const promptFile = `.github/prompts/${routine}.md`;
|
|
2310
|
+
if (routine === "autowork") {
|
|
2311
|
+
if (options.issue) {
|
|
2312
|
+
return `You are the Autowork routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}Your target is issue #${options.issue}. You are in Targeted mode: work issue #${options.issue} directly, ahead of Phase 1 convergence and priority scan.`;
|
|
2313
|
+
}
|
|
2314
|
+
return `You are the Autowork routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}You are in Scan mode: check open PRs for review comments to fix, close merged issues, then pick the highest-priority unclaimed issue.`;
|
|
2315
|
+
}
|
|
2316
|
+
if (routine === "peer-review") {
|
|
2317
|
+
if (options.pr) {
|
|
2318
|
+
return `You are the Peer Review routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}Your target is pull request #${options.pr}. You are in Targeted mode: review PR #${options.pr} directly.`;
|
|
2319
|
+
}
|
|
2320
|
+
return `You are the Peer Review routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}You are in Scan mode: check open PRs and select the highest-priority PR to review.`;
|
|
2321
|
+
}
|
|
2322
|
+
return `You are the ${routine} routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}`;
|
|
2323
|
+
}
|
|
2324
|
+
async function runLocalRoutine(options) {
|
|
2325
|
+
const targetDir = path11.resolve(options.targetDir);
|
|
2326
|
+
const routine = options.routine;
|
|
2327
|
+
const model = options.model || "gemini-3.7-flash-high";
|
|
2328
|
+
const printTimeout = options.printTimeout || "30m";
|
|
2329
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2330
|
+
const hostname = os2.hostname();
|
|
2331
|
+
const promptFile = path11.join(targetDir, ".github", "prompts", `${routine}.md`);
|
|
2332
|
+
if (!fs11.existsSync(promptFile)) {
|
|
2333
|
+
throw new Error(`Routine prompt file not found: ${promptFile}`);
|
|
2334
|
+
}
|
|
2335
|
+
let branchName = `agent/${routine}-${timestamp}`;
|
|
2336
|
+
if (options.issue) {
|
|
2337
|
+
branchName = `agent/${routine}-issue-${options.issue}-${timestamp}`;
|
|
2338
|
+
} else if (options.pr) {
|
|
2339
|
+
branchName = `agent/${routine}-pr-${options.pr}-${timestamp}`;
|
|
2340
|
+
}
|
|
2341
|
+
let worktreePath;
|
|
2342
|
+
let executionDir = targetDir;
|
|
2343
|
+
if (!options.noWorktree && !options.dryRun) {
|
|
2344
|
+
const worktreeResult = await createWorktree(targetDir, { branchName });
|
|
2345
|
+
worktreePath = worktreeResult.worktreePath;
|
|
2346
|
+
executionDir = worktreePath;
|
|
2347
|
+
}
|
|
2348
|
+
const prompt = buildRoutinePrompt(executionDir, routine, {
|
|
2349
|
+
issue: options.issue,
|
|
2350
|
+
pr: options.pr
|
|
2351
|
+
});
|
|
2352
|
+
if (options.dryRun) {
|
|
2353
|
+
return {
|
|
2354
|
+
success: true,
|
|
2355
|
+
exitCode: 0,
|
|
2356
|
+
output: `[DRY RUN] Would execute routine '${routine}' in ${options.noWorktree ? "current directory" : "worktree"}:
|
|
2357
|
+
Prompt: ${prompt}
|
|
2358
|
+
Model: ${model}
|
|
2359
|
+
Timeout: ${printTimeout}`,
|
|
2360
|
+
worktreePath,
|
|
2361
|
+
branchName
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
const childEnv = {
|
|
2365
|
+
...process.env,
|
|
2366
|
+
...options.env,
|
|
2367
|
+
LOCAL_AGENT: "true",
|
|
2368
|
+
LOCAL_HOST: hostname,
|
|
2369
|
+
TARGET_ISSUE: options.issue ? String(options.issue) : "",
|
|
2370
|
+
PR_NUMBER: options.pr ? String(options.pr) : ""
|
|
2371
|
+
};
|
|
2372
|
+
const args = [
|
|
2373
|
+
"-p",
|
|
2374
|
+
prompt,
|
|
2375
|
+
"--model",
|
|
2376
|
+
model,
|
|
2377
|
+
"--output-format",
|
|
2378
|
+
"text",
|
|
2379
|
+
"--print-timeout",
|
|
2380
|
+
printTimeout,
|
|
2381
|
+
"--dangerously-skip-permissions"
|
|
2382
|
+
];
|
|
2383
|
+
let output = "";
|
|
2384
|
+
let exitCode = 0;
|
|
2385
|
+
const cleanup = async () => {
|
|
2386
|
+
if (worktreePath && !options.keepWorktree) {
|
|
2387
|
+
await removeWorktree(targetDir, worktreePath, { deleteBranch: false }).catch(() => {
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
const sigintHandler = async () => {
|
|
2392
|
+
await cleanup();
|
|
2393
|
+
process.exit(130);
|
|
2394
|
+
};
|
|
2395
|
+
process.once("SIGINT", sigintHandler);
|
|
2396
|
+
process.once("SIGTERM", sigintHandler);
|
|
2397
|
+
try {
|
|
2398
|
+
exitCode = await new Promise((resolve, reject) => {
|
|
2399
|
+
const child = spawn("agy", args, {
|
|
2400
|
+
cwd: executionDir,
|
|
2401
|
+
env: childEnv,
|
|
2402
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
2403
|
+
});
|
|
2404
|
+
child.stdout?.on("data", (data) => {
|
|
2405
|
+
const chunk = data.toString();
|
|
2406
|
+
output += chunk;
|
|
2407
|
+
if (options.onLog) {
|
|
2408
|
+
options.onLog(chunk);
|
|
2409
|
+
} else {
|
|
2410
|
+
process.stdout.write(chunk);
|
|
2411
|
+
}
|
|
2412
|
+
});
|
|
2413
|
+
child.stderr?.on("data", (data) => {
|
|
2414
|
+
const chunk = data.toString();
|
|
2415
|
+
output += chunk;
|
|
2416
|
+
if (options.onLog) {
|
|
2417
|
+
options.onLog(chunk);
|
|
2418
|
+
} else {
|
|
2419
|
+
process.stderr.write(chunk);
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
child.on("error", (err) => {
|
|
2423
|
+
reject(err);
|
|
2424
|
+
});
|
|
2425
|
+
child.on("close", (code) => {
|
|
2426
|
+
resolve(code ?? 0);
|
|
2427
|
+
});
|
|
2428
|
+
});
|
|
2429
|
+
} finally {
|
|
2430
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
2431
|
+
process.removeListener("SIGTERM", sigintHandler);
|
|
2432
|
+
if (!options.keepWorktree) {
|
|
2433
|
+
await cleanup();
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
return {
|
|
2437
|
+
success: exitCode === 0,
|
|
2438
|
+
exitCode,
|
|
2439
|
+
output,
|
|
2440
|
+
worktreePath,
|
|
2441
|
+
branchName
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/commands/run.ts
|
|
2446
|
+
async function runRoutineCommand(routine, options = {}) {
|
|
2447
|
+
const cwd = process.cwd();
|
|
2448
|
+
const manifest = loadManifest(cwd);
|
|
2449
|
+
if (!manifest) {
|
|
2450
|
+
console.warn(
|
|
2451
|
+
pc9.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
|
|
2452
|
+
);
|
|
2453
|
+
} else if (manifest.routines && manifest.routines[routine] === false) {
|
|
2454
|
+
console.warn(
|
|
2455
|
+
pc9.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
console.log(pc9.cyan(`
|
|
2459
|
+
\u{1F680} Launching local agent session for routine: ${pc9.bold(routine)}`));
|
|
2460
|
+
if (options.issue) {
|
|
2461
|
+
console.log(pc9.dim(` Target issue: #${options.issue}`));
|
|
2462
|
+
}
|
|
2463
|
+
if (options.pr) {
|
|
2464
|
+
console.log(pc9.dim(` Target pull request: #${options.pr}`));
|
|
2465
|
+
}
|
|
2466
|
+
if (options.model) {
|
|
2467
|
+
console.log(pc9.dim(` Model override: ${options.model}`));
|
|
2468
|
+
}
|
|
2469
|
+
if (options.worktree !== false) {
|
|
2470
|
+
console.log(pc9.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
|
|
2471
|
+
} else {
|
|
2472
|
+
console.log(pc9.yellow(` Workspace isolation: Disabled (running in current directory)`));
|
|
2473
|
+
}
|
|
2474
|
+
console.log("");
|
|
2475
|
+
try {
|
|
2476
|
+
const result = await runLocalRoutine({
|
|
2477
|
+
targetDir: cwd,
|
|
2478
|
+
routine,
|
|
2479
|
+
issue: options.issue,
|
|
2480
|
+
pr: options.pr,
|
|
2481
|
+
model: options.model,
|
|
2482
|
+
printTimeout: options.timeout,
|
|
2483
|
+
noWorktree: options.worktree === false,
|
|
2484
|
+
keepWorktree: options.keepWorktree,
|
|
2485
|
+
dryRun: options.dryRun
|
|
2486
|
+
});
|
|
2487
|
+
if (options.dryRun) {
|
|
2488
|
+
console.log(pc9.green(result.output));
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (result.success) {
|
|
2492
|
+
console.log(pc9.green(`
|
|
2493
|
+
\u2713 Local agent session for '${routine}' completed successfully.`));
|
|
2494
|
+
} else {
|
|
2495
|
+
console.error(pc9.red(`
|
|
2496
|
+
\u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
|
|
2497
|
+
process.exit(result.exitCode);
|
|
2498
|
+
}
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
console.error(pc9.red(`
|
|
2501
|
+
\u2717 Failed to execute routine '${routine}': ${error.message}`));
|
|
1170
2502
|
process.exit(1);
|
|
1171
2503
|
}
|
|
1172
2504
|
}
|
|
1173
2505
|
|
|
2506
|
+
// src/commands/daemon.ts
|
|
2507
|
+
import pc11 from "picocolors";
|
|
2508
|
+
|
|
2509
|
+
// src/lib/daemon.ts
|
|
2510
|
+
import fs12 from "fs";
|
|
2511
|
+
import path12 from "path";
|
|
2512
|
+
import { spawn as spawn2, execFile as execFile3 } from "child_process";
|
|
2513
|
+
import { promisify as promisify3 } from "util";
|
|
2514
|
+
import pc10 from "picocolors";
|
|
2515
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2516
|
+
function getDaemonStatePath(repoRoot) {
|
|
2517
|
+
return path12.join(repoRoot, ".jonah-fleet", "daemon.json");
|
|
2518
|
+
}
|
|
2519
|
+
function readDaemonState(repoRoot) {
|
|
2520
|
+
const statePath = getDaemonStatePath(repoRoot);
|
|
2521
|
+
if (!fs12.existsSync(statePath)) return null;
|
|
2522
|
+
try {
|
|
2523
|
+
return JSON.parse(fs12.readFileSync(statePath, "utf8"));
|
|
2524
|
+
} catch {
|
|
2525
|
+
return null;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
function writeDaemonState(repoRoot, state) {
|
|
2529
|
+
const statePath = getDaemonStatePath(repoRoot);
|
|
2530
|
+
fs12.mkdirSync(path12.dirname(statePath), { recursive: true });
|
|
2531
|
+
fs12.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
2532
|
+
}
|
|
2533
|
+
function clearDaemonState(repoRoot) {
|
|
2534
|
+
const statePath = getDaemonStatePath(repoRoot);
|
|
2535
|
+
if (fs12.existsSync(statePath)) {
|
|
2536
|
+
try {
|
|
2537
|
+
fs12.unlinkSync(statePath);
|
|
2538
|
+
} catch {
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
function isDaemonRunning(repoRoot) {
|
|
2543
|
+
const state = readDaemonState(repoRoot);
|
|
2544
|
+
if (!state || !state.pid) return false;
|
|
2545
|
+
try {
|
|
2546
|
+
process.kill(state.pid, 0);
|
|
2547
|
+
return true;
|
|
2548
|
+
} catch {
|
|
2549
|
+
clearDaemonState(repoRoot);
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
async function countOpenReadyPRs(repoRoot) {
|
|
2554
|
+
try {
|
|
2555
|
+
const { stdout } = await execFileAsync3(
|
|
2556
|
+
"gh",
|
|
2557
|
+
["pr", "list", "--state", "open", "--draft=false", "--json", "number", "--jq", "length"],
|
|
2558
|
+
{ cwd: repoRoot }
|
|
2559
|
+
);
|
|
2560
|
+
return parseInt(stdout.trim(), 10) || 0;
|
|
2561
|
+
} catch {
|
|
2562
|
+
return 0;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
async function startBackgroundDaemon(repoRoot, options = {}) {
|
|
2566
|
+
if (isDaemonRunning(repoRoot)) {
|
|
2567
|
+
const existing = readDaemonState(repoRoot);
|
|
2568
|
+
throw new Error(`Daemon is already running with PID ${existing?.pid}`);
|
|
2569
|
+
}
|
|
2570
|
+
const reviewInterval = options.reviewInterval || 3;
|
|
2571
|
+
const autoworkInterval = options.autoworkInterval || options.interval || 30;
|
|
2572
|
+
const routines = options.routines || ["peer-review", "autowork"];
|
|
2573
|
+
const logFilePath = path12.join(repoRoot, ".jonah-fleet", "daemon.log");
|
|
2574
|
+
fs12.mkdirSync(path12.dirname(logFilePath), { recursive: true });
|
|
2575
|
+
const logFd = fs12.openSync(logFilePath, "a");
|
|
2576
|
+
const cliPath = process.argv[1];
|
|
2577
|
+
const args = [
|
|
2578
|
+
"daemon",
|
|
2579
|
+
"--foreground",
|
|
2580
|
+
"--review-interval",
|
|
2581
|
+
String(reviewInterval),
|
|
2582
|
+
"--autowork-interval",
|
|
2583
|
+
String(autoworkInterval),
|
|
2584
|
+
"--routines",
|
|
2585
|
+
routines.join(",")
|
|
2586
|
+
];
|
|
2587
|
+
if (options.model) {
|
|
2588
|
+
args.push("--model", options.model);
|
|
2589
|
+
}
|
|
2590
|
+
const child = spawn2(process.execPath, [cliPath, ...args], {
|
|
2591
|
+
cwd: repoRoot,
|
|
2592
|
+
detached: true,
|
|
2593
|
+
stdio: ["ignore", logFd, logFd],
|
|
2594
|
+
env: { ...process.env, JONAH_FLEET_DAEMON: "true" }
|
|
2595
|
+
});
|
|
2596
|
+
child.unref();
|
|
2597
|
+
const state = {
|
|
2598
|
+
pid: child.pid,
|
|
2599
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2600
|
+
reviewIntervalMinutes: reviewInterval,
|
|
2601
|
+
autoworkIntervalMinutes: autoworkInterval,
|
|
2602
|
+
routines,
|
|
2603
|
+
status: "idle"
|
|
2604
|
+
};
|
|
2605
|
+
writeDaemonState(repoRoot, state);
|
|
2606
|
+
return state;
|
|
2607
|
+
}
|
|
2608
|
+
async function stopDaemon(repoRoot) {
|
|
2609
|
+
const state = readDaemonState(repoRoot);
|
|
2610
|
+
if (!state || !state.pid) return false;
|
|
2611
|
+
try {
|
|
2612
|
+
process.kill(state.pid, "SIGTERM");
|
|
2613
|
+
clearDaemonState(repoRoot);
|
|
2614
|
+
await cleanupStaleWorktrees(repoRoot);
|
|
2615
|
+
return true;
|
|
2616
|
+
} catch {
|
|
2617
|
+
clearDaemonState(repoRoot);
|
|
2618
|
+
return false;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
async function runDaemonLoop(repoRoot, options = {}) {
|
|
2622
|
+
const reviewInterval = options.reviewInterval || 3;
|
|
2623
|
+
const autoworkInterval = options.autoworkInterval || options.interval || 30;
|
|
2624
|
+
const routines = options.routines || ["peer-review", "autowork"];
|
|
2625
|
+
const state = {
|
|
2626
|
+
pid: process.pid,
|
|
2627
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2628
|
+
reviewIntervalMinutes: reviewInterval,
|
|
2629
|
+
autoworkIntervalMinutes: autoworkInterval,
|
|
2630
|
+
routines,
|
|
2631
|
+
status: "idle"
|
|
2632
|
+
};
|
|
2633
|
+
writeDaemonState(repoRoot, state);
|
|
2634
|
+
console.log(pc10.cyan(`
|
|
2635
|
+
\u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
|
|
2636
|
+
console.log(pc10.dim(` PID: ${process.pid}`));
|
|
2637
|
+
console.log(pc10.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
|
|
2638
|
+
console.log(pc10.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
|
|
2639
|
+
console.log(pc10.dim(` Working Directory: ${repoRoot}
|
|
2640
|
+
`));
|
|
2641
|
+
let isStopping = false;
|
|
2642
|
+
let isWorking = false;
|
|
2643
|
+
const handleStop = async () => {
|
|
2644
|
+
if (isStopping) return;
|
|
2645
|
+
isStopping = true;
|
|
2646
|
+
console.log(pc10.yellow(`
|
|
2647
|
+
Stopping local agent daemon...`));
|
|
2648
|
+
clearDaemonState(repoRoot);
|
|
2649
|
+
await cleanupStaleWorktrees(repoRoot);
|
|
2650
|
+
process.exit(0);
|
|
2651
|
+
};
|
|
2652
|
+
process.once("SIGINT", handleStop);
|
|
2653
|
+
process.once("SIGTERM", handleStop);
|
|
2654
|
+
const runReviewCheck = async () => {
|
|
2655
|
+
if (isStopping || isWorking) return;
|
|
2656
|
+
state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2657
|
+
writeDaemonState(repoRoot, state);
|
|
2658
|
+
const openPRCount = await countOpenReadyPRs(repoRoot);
|
|
2659
|
+
if (openPRCount === 0) {
|
|
2660
|
+
console.log(pc10.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
try {
|
|
2664
|
+
isWorking = true;
|
|
2665
|
+
state.status = "working";
|
|
2666
|
+
state.activeRoutine = "peer-review";
|
|
2667
|
+
writeDaemonState(repoRoot, state);
|
|
2668
|
+
console.log(pc10.cyan(`
|
|
2669
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Found ${openPRCount} ready PR(s). Starting review session...`));
|
|
2670
|
+
await cleanupStaleWorktrees(repoRoot);
|
|
2671
|
+
const result = await runLocalRoutine({
|
|
2672
|
+
targetDir: repoRoot,
|
|
2673
|
+
routine: "peer-review",
|
|
2674
|
+
model: options.model,
|
|
2675
|
+
noWorktree: false
|
|
2676
|
+
});
|
|
2677
|
+
if (result.success) {
|
|
2678
|
+
console.log(pc10.green(`\u2713 Local peer-review completed successfully.`));
|
|
2679
|
+
} else {
|
|
2680
|
+
console.warn(pc10.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.`));
|
|
2681
|
+
}
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
console.error(pc10.red(`\u2717 Error in peer-review: ${err.message}`));
|
|
2684
|
+
} finally {
|
|
2685
|
+
isWorking = false;
|
|
2686
|
+
state.status = "idle";
|
|
2687
|
+
state.activeRoutine = void 0;
|
|
2688
|
+
writeDaemonState(repoRoot, state);
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
const runAutoworkCheck = async () => {
|
|
2692
|
+
if (isStopping || isWorking || !routines.includes("autowork")) return;
|
|
2693
|
+
state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2694
|
+
writeDaemonState(repoRoot, state);
|
|
2695
|
+
try {
|
|
2696
|
+
isWorking = true;
|
|
2697
|
+
state.status = "working";
|
|
2698
|
+
state.activeRoutine = "autowork";
|
|
2699
|
+
writeDaemonState(repoRoot, state);
|
|
2700
|
+
console.log(pc10.cyan(`
|
|
2701
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
|
|
2702
|
+
await cleanupStaleWorktrees(repoRoot);
|
|
2703
|
+
const result = await runLocalRoutine({
|
|
2704
|
+
targetDir: repoRoot,
|
|
2705
|
+
routine: "autowork",
|
|
2706
|
+
model: options.model,
|
|
2707
|
+
noWorktree: false
|
|
2708
|
+
});
|
|
2709
|
+
if (result.success) {
|
|
2710
|
+
console.log(pc10.green(`\u2713 Local autowork completed successfully.`));
|
|
2711
|
+
} else {
|
|
2712
|
+
console.warn(pc10.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.`));
|
|
2713
|
+
}
|
|
2714
|
+
} catch (err) {
|
|
2715
|
+
console.error(pc10.red(`\u2717 Error in autowork: ${err.message}`));
|
|
2716
|
+
} finally {
|
|
2717
|
+
isWorking = false;
|
|
2718
|
+
state.status = "idle";
|
|
2719
|
+
state.activeRoutine = void 0;
|
|
2720
|
+
writeDaemonState(repoRoot, state);
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
if (routines.includes("peer-review")) {
|
|
2724
|
+
await runReviewCheck();
|
|
2725
|
+
}
|
|
2726
|
+
if (routines.includes("autowork")) {
|
|
2727
|
+
await runAutoworkCheck();
|
|
2728
|
+
}
|
|
2729
|
+
const reviewIntervalMs = reviewInterval * 60 * 1e3;
|
|
2730
|
+
const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
|
|
2731
|
+
const reviewTimer = setInterval(runReviewCheck, reviewIntervalMs);
|
|
2732
|
+
const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
|
|
2733
|
+
await new Promise(() => {
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// src/commands/daemon.ts
|
|
2738
|
+
async function runDaemonCommand(action, options = {}) {
|
|
2739
|
+
const cwd = process.cwd();
|
|
2740
|
+
const act = action?.toLowerCase() || (options.foreground ? "foreground" : "status");
|
|
2741
|
+
const daemonOpts = {
|
|
2742
|
+
interval: options.interval ? parseInt(options.interval, 10) : void 0,
|
|
2743
|
+
reviewInterval: options.reviewInterval ? parseInt(options.reviewInterval, 10) : void 0,
|
|
2744
|
+
autoworkInterval: options.autoworkInterval ? parseInt(options.autoworkInterval, 10) : options.interval ? parseInt(options.interval, 10) : void 0,
|
|
2745
|
+
routines: options.routines ? options.routines.split(",").map((r) => r.trim()) : void 0,
|
|
2746
|
+
model: options.model,
|
|
2747
|
+
foreground: options.foreground
|
|
2748
|
+
};
|
|
2749
|
+
if (act === "start") {
|
|
2750
|
+
if (options.foreground) {
|
|
2751
|
+
await runDaemonLoop(cwd, daemonOpts);
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
try {
|
|
2755
|
+
const state2 = await startBackgroundDaemon(cwd, daemonOpts);
|
|
2756
|
+
console.log(pc11.green(`
|
|
2757
|
+
\u2713 Background agent daemon started successfully.`));
|
|
2758
|
+
console.log(pc11.dim(` PID: ${state2.pid}`));
|
|
2759
|
+
console.log(pc11.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
|
|
2760
|
+
console.log(pc11.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
|
|
2761
|
+
console.log(pc11.dim(` Routines: ${state2.routines.join(", ")}`));
|
|
2762
|
+
console.log(pc11.dim(` Log file: .jonah-fleet/daemon.log`));
|
|
2763
|
+
console.log(pc11.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
console.error(pc11.red(`
|
|
2766
|
+
\u2717 Failed to start daemon: ${err.message}`));
|
|
2767
|
+
process.exit(1);
|
|
2768
|
+
}
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
if (act === "stop") {
|
|
2772
|
+
if (!isDaemonRunning(cwd)) {
|
|
2773
|
+
console.log(pc11.yellow(`
|
|
2774
|
+
\u26A0\uFE0F No local agent daemon is currently running in this repository.`));
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
const state2 = readDaemonState(cwd);
|
|
2778
|
+
console.log(pc11.cyan(`
|
|
2779
|
+
Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
2780
|
+
const stopped = await stopDaemon(cwd);
|
|
2781
|
+
if (stopped) {
|
|
2782
|
+
console.log(pc11.green(`\u2713 Local agent daemon stopped successfully.`));
|
|
2783
|
+
} else {
|
|
2784
|
+
console.error(pc11.red(`\u2717 Could not terminate daemon process.`));
|
|
2785
|
+
process.exit(1);
|
|
2786
|
+
}
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (act === "foreground") {
|
|
2790
|
+
await runDaemonLoop(cwd, daemonOpts);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
const running = isDaemonRunning(cwd);
|
|
2794
|
+
const state = readDaemonState(cwd);
|
|
2795
|
+
const activeWorktrees = await listActiveWorktrees(cwd);
|
|
2796
|
+
console.log(pc11.cyan(`
|
|
2797
|
+
\u{1F916} Jonah Fleet Local Daemon Status
|
|
2798
|
+
`));
|
|
2799
|
+
if (running && state) {
|
|
2800
|
+
console.log(` Status: ${pc11.green(pc11.bold("RUNNING"))}`);
|
|
2801
|
+
console.log(` PID: ${state.pid}`);
|
|
2802
|
+
console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
|
|
2803
|
+
console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
|
|
2804
|
+
console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
|
|
2805
|
+
console.log(` Routines: ${state.routines.join(", ")}`);
|
|
2806
|
+
console.log(` Current State: ${state.status === "working" ? pc11.yellow("WORKING on " + state.activeRoutine) : pc11.green("IDLE")}`);
|
|
2807
|
+
if (state.lastReviewCheckAt) {
|
|
2808
|
+
console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
|
|
2809
|
+
}
|
|
2810
|
+
if (state.lastAutoworkCheckAt) {
|
|
2811
|
+
console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
|
|
2812
|
+
}
|
|
2813
|
+
} else {
|
|
2814
|
+
console.log(` Status: ${pc11.gray("STOPPED")}`);
|
|
2815
|
+
console.log(pc11.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
|
|
2816
|
+
}
|
|
2817
|
+
console.log(`
|
|
2818
|
+
Active Worktrees: ${activeWorktrees.length}`);
|
|
2819
|
+
for (const wt of activeWorktrees) {
|
|
2820
|
+
console.log(pc11.dim(` - [${wt.branch}] ${wt.path}`));
|
|
2821
|
+
}
|
|
2822
|
+
console.log("");
|
|
2823
|
+
}
|
|
2824
|
+
|
|
1174
2825
|
// src/index.ts
|
|
1175
2826
|
var program = new Command();
|
|
1176
2827
|
program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
|
|
1177
|
-
program.command("
|
|
2828
|
+
program.command("run <routine>").description("Run a specific prompt routine locally in an isolated git worktree").option("-i, --issue <number>", "Targeted issue number for autowork").option("-p, --pr <number>", "Targeted pull request number for peer-review").option("-m, --model <model>", "LLM model override (defaults to gemini-3.7-flash-high)").option("--timeout <duration>", "CLI execution print timeout (default: 30m)").option("--no-worktree", "Execute directly in current directory without creating a git worktree").option("--keep-worktree", "Preserve the git worktree after routine execution completes").option("-d, --dry-run", "Preview prompt and execution parameters without launching agent").action(async (routine, options) => {
|
|
2829
|
+
await runRoutineCommand(routine, options);
|
|
2830
|
+
});
|
|
2831
|
+
program.command("daemon [action]").description("Manage background local worker daemon polling for unclaimed issues and pull requests").option("-i, --interval <minutes>", "Legacy global polling interval in minutes (default: 30)").option("--review-interval <minutes>", "Peer Review watchdog cadence in minutes (default: 3)").option("--autowork-interval <minutes>", "Autowork backlog cadence in minutes (default: 30)").option("-r, --routines <list>", "Comma-separated routines to run (default: peer-review,autowork)").option("-m, --model <model>", "LLM model override").option("--foreground", "Run daemon in foreground with live console logs").action(async (action, options) => {
|
|
2832
|
+
await runDaemonCommand(action, options);
|
|
2833
|
+
});
|
|
2834
|
+
program.command("init").description("Initialize Jonah Fleet configuration, routines, workflows, and skills in the current repo").option("-p, --preset <preset>", "Preset profile to install (minimal | standard | full)", "standard").option("-f, --force", "Force overwrite existing files", false).option("--stack <stack>", "Override detected tech stack name").option("--package-manager <pm>", "Override package manager (npm, pnpm, yarn, bun, uv, poetry, cargo, go)").option("--test-cmd <cmd>", "Override test execution command").option("--build-cmd <cmd>", "Override build execution command").option("--interactive", "Force interactive prompts for stack configuration").option("--no-interactive", "Disable interactive prompts").action(async (options) => {
|
|
1178
2835
|
await runInit(options);
|
|
1179
2836
|
});
|
|
1180
2837
|
program.command("sync").description("Synchronize local prompts, workflows, and skills with the installed fleet version").option("-c, --check", "Check for drift without writing changes", false).option("-f, --force", "Force update all files to match fleet version", false).action(async (options) => {
|
|
@@ -1186,7 +2843,11 @@ program.command("status").description("Check the status, health, and drift of in
|
|
|
1186
2843
|
program.command("monitor [repos...]").description("Monitor health, active claims, PR review loops, and token spend across fleet repositories").option("-t, --tokens", "Display detailed per-agent token and cost breakdown", false).option("--detailed", "Display detailed metrics breakdown", false).option("-j, --json", "Output telemetry as JSON", false).option("-w, --watch", "Live watch and refresh dashboard", false).option("-i, --interval <seconds>", "Refresh interval in seconds for watch mode", "10").option("-a, --all", "Query all registered repositories from config and manifest", false).option("--add <repo>", "Add a repository to the fleet registry").option("--remove <repo>", "Remove a repository from the fleet registry").action(async (repos, options) => {
|
|
1187
2844
|
await runMonitor({ ...options, repos });
|
|
1188
2845
|
});
|
|
1189
|
-
program.command("contribute").description("Submit local prompt improvements back upstream to jonah-fleet").option("-t, --title <title>", "Contribution PR title").option("-b, --body <body>", "Contribution PR description").action(async (options) => {
|
|
2846
|
+
program.command("contribute").description("Submit local prompt improvements back upstream to jonah-fleet").option("-t, --title <title>", "Contribution PR title").option("-b, --body <body>", "Contribution PR description").option("-p, --prompt <prompt>", "Target prompt template being refined").option("-r, --repo <repo>", "Upstream target repository (defaults to juliendurandeu/jonah-fleet)").option("-d, --dry-run", "Preview contribution branch and PR command without executing", false).action(async (options) => {
|
|
1190
2847
|
await runContribute(options);
|
|
1191
2848
|
});
|
|
2849
|
+
program.command("telemetry [repos...]").description("Aggregate and report fleet-wide telemetry, review loop metrics, failure categories, and weekly token spend").option("-l, --log <path>", "Path to markdown log file to emit").option("-e, --endpoint <url>", "Collector endpoint URL for telemetry emission").option("-b, --budget <tokens>", "Weekly token budget ceiling (~8.75M default)").option("-j, --json", "Output telemetry summary as JSON", false).option("--emit", "Emit telemetry event for specified or latest run log").action(async (repos, options) => {
|
|
2850
|
+
const action = options.emit ? "emit" : "aggregate";
|
|
2851
|
+
await runTelemetry({ ...options, repos, action });
|
|
2852
|
+
});
|
|
1192
2853
|
program.parse(process.argv);
|