jonah-fleet 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +3 -3
  3. package/dist/commands/labels.d.ts +1 -1
  4. package/dist/commands/labels.d.ts.map +1 -1
  5. package/dist/commands/status.d.ts.map +1 -1
  6. package/dist/commands/sync.d.ts.map +1 -1
  7. package/dist/commands/telemetry.d.ts.map +1 -1
  8. package/dist/index.js +666 -151
  9. package/dist/lib/daemon-keys.d.ts.map +1 -1
  10. package/dist/lib/daemon.d.ts.map +1 -1
  11. package/dist/lib/fleet-query.d.ts.map +1 -1
  12. package/dist/lib/labels.d.ts +20 -0
  13. package/dist/lib/labels.d.ts.map +1 -1
  14. package/dist/lib/manifest.d.ts.map +1 -1
  15. package/dist/lib/presets.d.ts +6 -1
  16. package/dist/lib/presets.d.ts.map +1 -1
  17. package/dist/lib/runner.d.ts +48 -0
  18. package/dist/lib/runner.d.ts.map +1 -1
  19. package/dist/lib/telemetry.d.ts.map +1 -1
  20. package/dist/lib/terminal-card.d.ts +15 -1
  21. package/dist/lib/terminal-card.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/schema.json +10 -5
  24. package/templates/prompts/ORCHESTRATION.md +49 -11
  25. package/templates/prompts/_prompt-template.md +10 -13
  26. package/templates/prompts/analytics-review.md +4 -2
  27. package/templates/prompts/autowork.md +43 -12
  28. package/templates/prompts/dependency-update-security-check.md +4 -2
  29. package/templates/prompts/design-review.md +125 -0
  30. package/templates/prompts/issues-housekeeping.md +9 -7
  31. package/templates/prompts/optimizer.md +31 -12
  32. package/templates/prompts/peer-review.md +29 -4
  33. package/templates/prompts/product-planning.md +14 -10
  34. package/templates/workflows/autowork-cron.yml +99 -39
  35. package/templates/workflows/dependency-check-cron.yml +89 -3
  36. package/templates/workflows/design-review-cron.yml +221 -0
  37. package/templates/workflows/issues-housekeeping-cron.yml +89 -3
  38. package/templates/workflows/prompt-optimizer-cron.yml +96 -10
  39. package/templates/workflows/sync-fleet.yml +4 -0
  40. package/templates/workflows/trigger-autowork-manual.yml +116 -12
  41. package/templates/workflows/trigger-autowork-on-bug.yml +135 -5
  42. package/templates/workflows/trigger-autowork-on-merge.yml +135 -5
  43. package/templates/workflows/trigger-review-routine.yml +119 -18
package/dist/index.js CHANGED
@@ -13,10 +13,10 @@ import path from "path";
13
13
 
14
14
  // src/lib/presets.ts
15
15
  var DEFAULT_MODELS_CONFIG = {
16
- default: "gemini-3.7-flash-high",
17
- "issues-housekeeping": "gemini-3.7-flash",
18
- "dependency-update-security-check": "gemini-3.7-flash",
19
- "analytics-review": "gemini-3.7-flash"
16
+ default: "gemini-3.8-flash-high",
17
+ "issues-housekeeping": "gemini-3.8-flash-medium",
18
+ "dependency-update-security-check": "gemini-3.8-flash-medium",
19
+ "analytics-review": "gemini-3.8-flash-medium"
20
20
  };
21
21
  var DEFAULT_BUDGETS_CONFIG = {
22
22
  weeklyTokens: 875e4,
@@ -27,7 +27,8 @@ var DEFAULT_BUDGETS_CONFIG = {
27
27
  "issues-housekeeping": 40,
28
28
  "dependency-update-security-check": 25,
29
29
  "product-planning": 45,
30
- "analytics-review": 30
30
+ "analytics-review": 30,
31
+ "design-review": 50
31
32
  },
32
33
  maxIterations: {
33
34
  autowork: 65,
@@ -36,7 +37,8 @@ var DEFAULT_BUDGETS_CONFIG = {
36
37
  "issues-housekeeping": 30,
37
38
  "dependency-update-security-check": 20,
38
39
  "product-planning": 40,
39
- "analytics-review": 25
40
+ "analytics-review": 25,
41
+ "design-review": 30
40
42
  }
41
43
  };
42
44
  var PRESET_CONFIGS = {
@@ -48,7 +50,8 @@ var PRESET_CONFIGS = {
48
50
  "issues-housekeeping": false,
49
51
  "dependency-update-security-check": false,
50
52
  "product-planning": false,
51
- "analytics-review": false
53
+ "analytics-review": false,
54
+ "design-review": false
52
55
  },
53
56
  skills: [
54
57
  "tdd",
@@ -66,7 +69,8 @@ var PRESET_CONFIGS = {
66
69
  "issues-housekeeping": true,
67
70
  "dependency-update-security-check": true,
68
71
  "product-planning": false,
69
- "analytics-review": false
72
+ "analytics-review": false,
73
+ "design-review": false
70
74
  },
71
75
  skills: [
72
76
  "tdd",
@@ -88,7 +92,8 @@ var PRESET_CONFIGS = {
88
92
  "issues-housekeeping": true,
89
93
  "dependency-update-security-check": true,
90
94
  "product-planning": true,
91
- "analytics-review": true
95
+ "analytics-review": true,
96
+ "design-review": true
92
97
  },
93
98
  skills: [
94
99
  "tdd",
@@ -117,9 +122,10 @@ var ROUTINE_TO_WORKFLOW_MAP = {
117
122
  "issues-housekeeping": ["issues-housekeeping-cron.yml"],
118
123
  "dependency-update-security-check": ["dependency-check-cron.yml"],
119
124
  "product-planning": [],
120
- "analytics-review": []
125
+ "analytics-review": [],
126
+ "design-review": ["design-review-cron.yml"]
121
127
  };
122
- var FLEET_VERSION = "1.6.0";
128
+ var FLEET_VERSION = "1.8.0";
123
129
  var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
124
130
 
125
131
  // src/lib/manifest.ts
@@ -153,7 +159,8 @@ function createDefaultManifest(preset = "standard") {
153
159
  "issues-housekeeping": true,
154
160
  "dependency-update-security-check": true,
155
161
  "product-planning": false,
156
- "analytics-review": false
162
+ "analytics-review": false,
163
+ "design-review": false
157
164
  },
158
165
  skills: PRESET_CONFIGS.standard.skills,
159
166
  models: { ...DEFAULT_MODELS_CONFIG },
@@ -935,46 +942,74 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
935
942
  }
936
943
  const logContents = [];
937
944
  if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
938
- const logsDir = path4.join(repoIdentifier, ".github/prompts/logs");
939
- if (fs4.existsSync(logsDir)) {
940
- const collectLogs = (dir) => {
941
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
942
- for (const entry of entries) {
943
- const fullPath = path4.join(dir, entry.name);
944
- if (entry.isDirectory()) {
945
- collectLogs(fullPath);
946
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
947
- try {
948
- logContents.push(fs4.readFileSync(fullPath, "utf8"));
949
- } catch {
950
- }
945
+ const runsDir = path4.join(repoIdentifier, ".jonah-fleet/runs");
946
+ const legacyLogsDir = path4.join(repoIdentifier, ".github/prompts/logs");
947
+ const collectLogs = (dir) => {
948
+ if (!fs4.existsSync(dir)) return;
949
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
950
+ for (const entry of entries) {
951
+ const fullPath = path4.join(dir, entry.name);
952
+ if (entry.isDirectory()) {
953
+ collectLogs(fullPath);
954
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
955
+ try {
956
+ logContents.push(fs4.readFileSync(fullPath, "utf8"));
957
+ } catch {
951
958
  }
952
959
  }
953
- };
954
- collectLogs(logsDir);
955
- }
960
+ }
961
+ };
962
+ collectLogs(runsDir);
963
+ collectLogs(legacyLogsDir);
956
964
  } else {
957
965
  try {
958
- const treeRaw = await executor([
959
- "api",
960
- `repos/${repoIdentifier}/git/trees/HEAD?recursive=1`
966
+ const issuesRaw = await executor([
967
+ "issue",
968
+ "list",
969
+ "--repo",
970
+ repoIdentifier,
971
+ "--label",
972
+ "routine-log",
973
+ "--state",
974
+ "all",
975
+ "--limit",
976
+ "25",
977
+ "--json",
978
+ "body"
961
979
  ]);
962
- const tree = JSON.parse(treeRaw);
963
- if (Array.isArray(tree.tree)) {
964
- const logFiles = tree.tree.filter((node) => node.path && node.path.startsWith(".github/prompts/logs/") && node.path.endsWith(".md")).slice(-15);
965
- for (const file of logFiles) {
966
- try {
967
- const fileRaw = await executor(["api", `repos/${repoIdentifier}/contents/${file.path}`]);
968
- const parsed = JSON.parse(fileRaw);
969
- if (parsed.content) {
970
- logContents.push(Buffer.from(parsed.content, "base64").toString("utf8"));
971
- }
972
- } catch {
980
+ const issues = JSON.parse(issuesRaw);
981
+ if (Array.isArray(issues) && issues.length > 0) {
982
+ for (const issue of issues) {
983
+ if (issue.body) {
984
+ logContents.push(issue.body);
973
985
  }
974
986
  }
975
987
  }
976
988
  } catch {
977
989
  }
990
+ if (logContents.length === 0) {
991
+ try {
992
+ const treeRaw = await executor([
993
+ "api",
994
+ `repos/${repoIdentifier}/git/trees/HEAD?recursive=1`
995
+ ]);
996
+ const tree = JSON.parse(treeRaw);
997
+ if (Array.isArray(tree.tree)) {
998
+ const logFiles = tree.tree.filter((node) => node.path && node.path.startsWith(".github/prompts/logs/") && node.path.endsWith(".md")).slice(-15);
999
+ for (const file of logFiles) {
1000
+ try {
1001
+ const fileRaw = await executor(["api", `repos/${repoIdentifier}/contents/${file.path}`]);
1002
+ const parsed = JSON.parse(fileRaw);
1003
+ if (parsed.content) {
1004
+ logContents.push(Buffer.from(parsed.content, "base64").toString("utf8"));
1005
+ }
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ }
1010
+ } catch {
1011
+ }
1012
+ }
978
1013
  }
979
1014
  if (logContents.length > 0) {
980
1015
  result.tokenUsage = computeTokenSpendFromLogs(logContents, now);
@@ -1080,7 +1115,13 @@ var DEFAULT_PROTECTED_LABEL_PATTERNS = [
1080
1115
  "blocked",
1081
1116
  "autorelease:*",
1082
1117
  "dependencies",
1083
- "security"
1118
+ "security",
1119
+ "routine-log",
1120
+ "routine:*",
1121
+ "status:*",
1122
+ "runner:*",
1123
+ "historical-migration",
1124
+ "needs-attention"
1084
1125
  ];
1085
1126
  function isLabelProtected(labelName, protectedPatterns = DEFAULT_PROTECTED_LABEL_PATTERNS) {
1086
1127
  for (const pattern of protectedPatterns) {
@@ -1275,6 +1316,66 @@ async function pruneLabels(options = {}) {
1275
1316
  }
1276
1317
  return result;
1277
1318
  }
1319
+ var FLEET_CORE_LABELS = [
1320
+ { name: "routine-log", color: "5319e7", description: "Autonomous routine execution log" },
1321
+ { name: "status:running", color: "fbca04", description: "Routine execution in progress" },
1322
+ { name: "status:success", color: "0e8a16", description: "Routine execution succeeded" },
1323
+ { name: "status:failure", color: "d93f0b", description: "Routine execution failed" },
1324
+ { name: "needs-attention", color: "e11d48", description: "Requires maintainer triage" },
1325
+ { name: "runner:github-actions", color: "1f883d", description: "Executed via GitHub Actions" },
1326
+ { name: "runner:local", color: "bfd4f2", description: "Executed via local machine daemon" },
1327
+ { name: "routine:autowork", color: "0052cc", description: "Routine: autowork" },
1328
+ { name: "routine:peer-review", color: "0052cc", description: "Routine: peer-review" },
1329
+ { name: "routine:optimizer", color: "0052cc", description: "Routine: optimizer" },
1330
+ { name: "routine:issues-housekeeping", color: "0052cc", description: "Routine: issues-housekeeping" },
1331
+ { name: "routine:dependency-check", color: "0052cc", description: "Routine: dependency-check" },
1332
+ { name: "routine:analytics-review", color: "0052cc", description: "Routine: analytics-review" },
1333
+ { name: "routine:product-planning", color: "0052cc", description: "Routine: product-planning" },
1334
+ { name: "routine:design-review", color: "0052cc", description: "Routine: design-review" },
1335
+ { name: "historical-migration", color: "d4c5f9", description: "Migrated from historical git logs" }
1336
+ ];
1337
+ async function provisionLabels(options = {}) {
1338
+ const cwd = options.cwd || process.cwd();
1339
+ const executor = options.executor || defaultGhExecutor;
1340
+ const repo = await resolveRepoName(options.repo, cwd, executor);
1341
+ const existingRaw = await fetchRepoLabels(repo, executor, cwd).catch(() => []);
1342
+ const existingMap = new Set(existingRaw.map((l) => l.name));
1343
+ const result = {
1344
+ repo,
1345
+ created: [],
1346
+ alreadyExists: [],
1347
+ errors: []
1348
+ };
1349
+ for (const def of FLEET_CORE_LABELS) {
1350
+ if (existingMap.has(def.name)) {
1351
+ result.alreadyExists.push(def.name);
1352
+ continue;
1353
+ }
1354
+ try {
1355
+ const args = [
1356
+ "label",
1357
+ "create",
1358
+ def.name,
1359
+ "--color",
1360
+ def.color,
1361
+ "--description",
1362
+ def.description,
1363
+ "--force"
1364
+ ];
1365
+ if (repo && repo !== "current") {
1366
+ args.push("--repo", repo);
1367
+ }
1368
+ await executor(args);
1369
+ result.created.push(def.name);
1370
+ } catch (err) {
1371
+ result.errors.push({
1372
+ label: def.name,
1373
+ error: err.message || String(err)
1374
+ });
1375
+ }
1376
+ }
1377
+ return result;
1378
+ }
1278
1379
 
1279
1380
  // src/commands/init.ts
1280
1381
  async function promptQuestion(query, defaultValue) {
@@ -1505,8 +1606,15 @@ Run 'jonah-fleet sync --force' to apply updates.
1505
1606
  saveManifest(cwd, manifest);
1506
1607
  const result = installFleet(cwd, manifest, { force: true });
1507
1608
  console.log(pc2.green(`\u2713 Synchronized with Jonah Fleet v${FLEET_VERSION}`));
1508
- console.log(pc2.green(`\u2713 Updated ${result.promptsInstalled.length} prompts, ${result.workflowsInstalled.length} workflows, and ${result.skillsInstalled.length} skills.
1509
- `));
1609
+ console.log(pc2.green(`\u2713 Updated ${result.promptsInstalled.length} prompts, ${result.workflowsInstalled.length} workflows, and ${result.skillsInstalled.length} skills.`));
1610
+ try {
1611
+ const labelRes = await provisionLabels({ cwd });
1612
+ if (labelRes.created.length > 0) {
1613
+ console.log(pc2.green(`\u2713 Provisioned ${labelRes.created.length} missing fleet label(s)`));
1614
+ }
1615
+ } catch {
1616
+ }
1617
+ console.log();
1510
1618
  }
1511
1619
 
1512
1620
  // src/commands/status.ts
@@ -1792,31 +1900,29 @@ async function runStatus(options = {}) {
1792
1900
  }
1793
1901
  const drift = checkDrift(cwd, manifest);
1794
1902
  const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
1795
- const logsDir = path7.join(cwd, ".github/prompts/logs");
1903
+ const runsDir = path7.join(cwd, ".jonah-fleet/runs");
1904
+ const legacyLogsDir = path7.join(cwd, ".github/prompts/logs");
1796
1905
  let tokenUsage = void 0;
1797
- if (fs7.existsSync(logsDir)) {
1798
- const logContents = [];
1799
- const collectLogs = (dir) => {
1800
- const entries = fs7.readdirSync(dir, { withFileTypes: true });
1801
- for (const entry of entries) {
1802
- const fullPath = path7.join(dir, entry.name);
1803
- if (entry.isDirectory()) {
1804
- collectLogs(fullPath);
1805
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1806
- try {
1807
- logContents.push(fs7.readFileSync(fullPath, "utf8"));
1808
- } catch {
1809
- }
1906
+ const logContents = [];
1907
+ const collectLogs = (dir) => {
1908
+ if (!fs7.existsSync(dir)) return;
1909
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
1910
+ for (const entry of entries) {
1911
+ const fullPath = path7.join(dir, entry.name);
1912
+ if (entry.isDirectory()) {
1913
+ collectLogs(fullPath);
1914
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1915
+ try {
1916
+ logContents.push(fs7.readFileSync(fullPath, "utf8"));
1917
+ } catch {
1810
1918
  }
1811
1919
  }
1812
- };
1813
- try {
1814
- collectLogs(logsDir);
1815
- } catch {
1816
- }
1817
- if (logContents.length > 0) {
1818
- tokenUsage = computeTokenSpendFromLogs(logContents);
1819
1920
  }
1921
+ };
1922
+ collectLogs(runsDir);
1923
+ collectLogs(legacyLogsDir);
1924
+ if (logContents.length > 0) {
1925
+ tokenUsage = computeTokenSpendFromLogs(logContents);
1820
1926
  }
1821
1927
  if (options.json) {
1822
1928
  console.log(
@@ -2268,9 +2374,10 @@ async function emitTelemetry(summary, endpoint, customFetch = globalThis.fetch)
2268
2374
  }
2269
2375
  function collectLocalTelemetryLogs(dir, repositoryName = "local") {
2270
2376
  const summaries = [];
2271
- const logsDir = path8.join(dir, ".github/prompts/logs");
2272
- if (!fs8.existsSync(logsDir)) return summaries;
2377
+ const runsDir = path8.join(dir, ".jonah-fleet/runs");
2378
+ const legacyLogsDir = path8.join(dir, ".github/prompts/logs");
2273
2379
  const traverse = (currentDir) => {
2380
+ if (!fs8.existsSync(currentDir)) return;
2274
2381
  const entries = fs8.readdirSync(currentDir, { withFileTypes: true });
2275
2382
  for (const entry of entries) {
2276
2383
  const fullPath = path8.join(currentDir, entry.name);
@@ -2286,7 +2393,12 @@ function collectLocalTelemetryLogs(dir, repositoryName = "local") {
2286
2393
  }
2287
2394
  }
2288
2395
  };
2289
- traverse(logsDir);
2396
+ if (fs8.existsSync(runsDir)) {
2397
+ traverse(runsDir);
2398
+ }
2399
+ if (fs8.existsSync(legacyLogsDir)) {
2400
+ traverse(legacyLogsDir);
2401
+ }
2290
2402
  return summaries;
2291
2403
  }
2292
2404
  async function collectRepoTelemetry(repoIdentifier, executor = defaultGhExecutor, options = {}) {
@@ -2294,6 +2406,35 @@ async function collectRepoTelemetry(repoIdentifier, executor = defaultGhExecutor
2294
2406
  if (fs8.existsSync(repoIdentifier) && fs8.statSync(repoIdentifier).isDirectory()) {
2295
2407
  return collectLocalTelemetryLogs(repoIdentifier, repoIdentifier);
2296
2408
  }
2409
+ try {
2410
+ const issuesRaw = await executor([
2411
+ "issue",
2412
+ "list",
2413
+ "--repo",
2414
+ repoIdentifier,
2415
+ "--label",
2416
+ "routine-log",
2417
+ "--state",
2418
+ "all",
2419
+ "--limit",
2420
+ String(options.maxLogs || 25),
2421
+ "--json",
2422
+ "body,number,title,createdAt"
2423
+ ]);
2424
+ const issues = JSON.parse(issuesRaw);
2425
+ if (Array.isArray(issues) && issues.length > 0) {
2426
+ for (const issue of issues) {
2427
+ if (issue.body) {
2428
+ const summary = parseLogToTelemetry(issue.body, { repository: repoIdentifier });
2429
+ if (summary) summaries.push(summary);
2430
+ }
2431
+ }
2432
+ if (summaries.length > 0) {
2433
+ return summaries;
2434
+ }
2435
+ }
2436
+ } catch {
2437
+ }
2297
2438
  try {
2298
2439
  const treeRaw = await executor([
2299
2440
  "api",
@@ -2412,11 +2553,16 @@ async function runTelemetry(options = {}) {
2412
2553
  if (options.action === "emit" || options.log) {
2413
2554
  let logPath = options.log;
2414
2555
  if (!logPath) {
2415
- const logsDir = path9.join(cwd, ".github/prompts/logs");
2416
- if (fs9.existsSync(logsDir)) {
2556
+ const reportFile = path9.join(cwd, ".jonah-fleet/run-report.md");
2557
+ if (fs9.existsSync(reportFile)) {
2558
+ logPath = reportFile;
2559
+ } else {
2560
+ const runsDir = path9.join(cwd, ".jonah-fleet/runs");
2561
+ const legacyLogsDir = path9.join(cwd, ".github/prompts/logs");
2417
2562
  let latestFile = null;
2418
2563
  let latestMtime = 0;
2419
2564
  const findLogs = (dir) => {
2565
+ if (!fs9.existsSync(dir)) return;
2420
2566
  const entries = fs9.readdirSync(dir, { withFileTypes: true });
2421
2567
  for (const entry of entries) {
2422
2568
  const p = path9.join(dir, entry.name);
@@ -2430,7 +2576,8 @@ async function runTelemetry(options = {}) {
2430
2576
  }
2431
2577
  }
2432
2578
  };
2433
- findLogs(logsDir);
2579
+ findLogs(runsDir);
2580
+ findLogs(legacyLogsDir);
2434
2581
  logPath = latestFile || void 0;
2435
2582
  }
2436
2583
  }
@@ -2635,8 +2782,10 @@ async function cleanupStaleWorktrees(repoRoot) {
2635
2782
  // src/lib/terminal-card.ts
2636
2783
  import fs11 from "fs";
2637
2784
  import path11 from "path";
2638
- import { execFileSync } from "child_process";
2785
+ import { execFileSync, execFile as execFile3 } from "child_process";
2786
+ import { promisify as promisify3 } from "util";
2639
2787
  import pc9 from "picocolors";
2788
+ var execFileAsync3 = promisify3(execFile3);
2640
2789
  function stripAnsi(text) {
2641
2790
  return text.replace(/\x1b\[[0-9;]*m/g, "");
2642
2791
  }
@@ -2722,6 +2871,31 @@ function fetchTargetTitle(repoRoot, target) {
2722
2871
  }
2723
2872
  return null;
2724
2873
  }
2874
+ async function fetchTargetTitleAsync(repoRoot, target) {
2875
+ try {
2876
+ const prMatch = target.match(/PR\s*#?(\d+)/i);
2877
+ if (prMatch) {
2878
+ const { stdout } = await execFileAsync3("gh", ["pr", "view", prMatch[1], "--json", "title", "-q", ".title"], {
2879
+ cwd: repoRoot,
2880
+ encoding: "utf8",
2881
+ timeout: 3e3
2882
+ });
2883
+ return stdout.trim() || null;
2884
+ }
2885
+ const issueMatch = target.match(/Issue\s*#?(\d+)/i);
2886
+ if (issueMatch) {
2887
+ const { stdout } = await execFileAsync3("gh", ["issue", "view", issueMatch[1], "--json", "title", "-q", ".title"], {
2888
+ cwd: repoRoot,
2889
+ encoding: "utf8",
2890
+ timeout: 3e3
2891
+ });
2892
+ return stdout.trim() || null;
2893
+ }
2894
+ } catch {
2895
+ return null;
2896
+ }
2897
+ return null;
2898
+ }
2725
2899
  function sanitizeWorktreePaths(text) {
2726
2900
  let cleaned = text.replace(/file:\/\/\/[^\s"'()]+?\/\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, "");
2727
2901
  cleaned = cleaned.replace(/(?:^|[\s"'(`[])(?:\/[^\s"'()]+?)?\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, (match) => {
@@ -2756,16 +2930,29 @@ function extractExecutionSummary(output) {
2756
2930
  return sanitizeWorktreePaths(summary);
2757
2931
  }
2758
2932
  function findLatestRunLog(repoRoot, routine) {
2933
+ const runsDir = path11.join(repoRoot, ".jonah-fleet", "runs");
2934
+ if (fs11.existsSync(runsDir)) {
2935
+ try {
2936
+ const files = fs11.readdirSync(runsDir).filter((f) => f.startsWith(`${routine}-`) && f.endsWith(".md"));
2937
+ if (files.length > 0) {
2938
+ files.sort().reverse();
2939
+ return path11.join(runsDir, files[0]);
2940
+ }
2941
+ } catch {
2942
+ }
2943
+ }
2759
2944
  const logsDir = path11.join(repoRoot, ".github", "prompts", "logs", routine);
2760
- if (!fs11.existsSync(logsDir)) return null;
2761
- try {
2762
- const files = fs11.readdirSync(logsDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
2763
- if (files.length === 0) return null;
2764
- files.sort().reverse();
2765
- return path11.join(logsDir, files[0]);
2766
- } catch {
2767
- return null;
2945
+ if (fs11.existsSync(logsDir)) {
2946
+ try {
2947
+ const files = fs11.readdirSync(logsDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
2948
+ if (files.length > 0) {
2949
+ files.sort().reverse();
2950
+ return path11.join(logsDir, files[0]);
2951
+ }
2952
+ } catch {
2953
+ }
2768
2954
  }
2955
+ return null;
2769
2956
  }
2770
2957
  function parseRunLog(logContent) {
2771
2958
  const summary = {
@@ -2890,14 +3077,38 @@ function detectClaimedIssue(chunk) {
2890
3077
  function detectClaimedPR(chunk) {
2891
3078
  const findingMatch = chunk.match(/(?:addressing\s+review\s+findings|fixing\s+review\s+findings)[^\n#]*?#(\d+)/i);
2892
3079
  if (findingMatch) return `PR #${findingMatch[1]}`;
2893
- const reviewMatch = chunk.match(/Starting\s+review[^\n#]*?#(\d+)/i);
3080
+ const reviewMatch = chunk.match(/Starting\s+review[^\n]*?(?:on\s+|PR\s+)#(\d+)/i) || chunk.match(/Starting\s+review(?![^\n]*?(?:run\s+log|tracking\s+log|log\s+issue))[^\n#]*?#(\d+)/i);
2894
3081
  if (reviewMatch) return `PR #${reviewMatch[1]}`;
2895
3082
  const prMatch = chunk.match(/(?:selected|target|reviewing)\s+(?:target\s+)?PR:?\s*\[?PR\s*#?(\d+)/i);
2896
3083
  if (prMatch) return `PR #${prMatch[1]}`;
2897
- const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review|edit|ready)\s+(\d+)/i);
3084
+ const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review|edit|ready|comment)\s+(\d+)/i);
2898
3085
  if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
2899
3086
  return null;
2900
3087
  }
3088
+ function cleanTargetTitle(title, maxLength = 28) {
3089
+ if (!title) return "";
3090
+ let cleaned = title.trim();
3091
+ cleaned = cleaned.replace(/^(?:feat|fix|chore|docs|refactor|test|perf|style|ci|build)(?:\([^)]+\))?!?!?:\s*/i, "");
3092
+ cleaned = cleaned.replace(/(?:\s*\(\s*#\d+\s*\))+$/, "");
3093
+ cleaned = cleaned.trim();
3094
+ if (!cleaned) return "";
3095
+ if (cleaned.length > maxLength) {
3096
+ const slice = cleaned.slice(0, maxLength);
3097
+ const lastSpace = slice.lastIndexOf(" ");
3098
+ if (lastSpace > maxLength - 8) {
3099
+ return slice.slice(0, lastSpace).trimEnd() + "...";
3100
+ }
3101
+ return slice.trimEnd() + "...";
3102
+ }
3103
+ return cleaned;
3104
+ }
3105
+ function formatTargetLabel(baseLabel, title, maxLength = 28) {
3106
+ if (!title) return baseLabel;
3107
+ const cleanBase = baseLabel.replace(/\s*\([^)]*\)$/, "").trim();
3108
+ const cleanedTitle = cleanTargetTitle(title, maxLength);
3109
+ if (!cleanedTitle) return cleanBase;
3110
+ return `${cleanBase} (${cleanedTitle})`;
3111
+ }
2901
3112
  function formatActionDescription(toolName, params) {
2902
3113
  const name = toolName || "unknown";
2903
3114
  if (name === "run_command") {
@@ -3397,24 +3608,144 @@ function discoverSkillsPrompt(targetDir) {
3397
3608
  function buildRoutinePrompt(targetDir, routine, options = {}) {
3398
3609
  const skillsPrompt = discoverSkillsPrompt(targetDir);
3399
3610
  const promptFile = `.github/prompts/${routine}.md`;
3611
+ const repoContext = `Working repository is located at ${targetDir}. All git, gh, and workspace commands must execute strictly within this repository.`;
3612
+ const logPrompt = options.routineIssueNumber ? ` Tracking run log issue: #${options.routineIssueNumber}.` : "";
3400
3613
  if (routine === "autowork") {
3401
3614
  if (options.issue) {
3402
- 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.`;
3615
+ return `You are the Autowork routine for this repository. ${repoContext} 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.${logPrompt}`;
3403
3616
  }
3404
- 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.`;
3617
+ return `You are the Autowork routine for this repository. ${repoContext} 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.${logPrompt}`;
3405
3618
  }
3406
3619
  if (routine === "peer-review") {
3407
3620
  if (options.pr) {
3408
- 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.`;
3621
+ return `You are the Peer Review routine for this repository. ${repoContext} 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.${logPrompt}`;
3622
+ }
3623
+ return `You are the Peer Review routine for this repository. ${repoContext} 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.${logPrompt}`;
3624
+ }
3625
+ return `You are the ${routine} routine for this repository. ${repoContext} Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}${logPrompt}`;
3626
+ }
3627
+ function tryCreateLocalRunIssue(cwd, routine, timestamp, targetLabel, hostname) {
3628
+ try {
3629
+ const title = `[${routine}] run ${timestamp} (local)`;
3630
+ const body = `### Autonomous Routine Execution in Progress (runner:local)
3631
+ - **Routine**: \`${routine}\`
3632
+ - **Timestamp**: \`${timestamp}\`
3633
+ - **Target**: \`${targetLabel}\`
3634
+ - **Host**: \`${hostname}\`
3635
+
3636
+ _Running via Jonah Fleet CLI runner._`;
3637
+ const labels = `routine-log,routine:${routine},status:running,runner:local`;
3638
+ const out = execSync2(`gh issue create --title ${JSON.stringify(title)} --body ${JSON.stringify(body)} --label ${JSON.stringify(labels)}`, {
3639
+ cwd,
3640
+ encoding: "utf8",
3641
+ stdio: ["ignore", "pipe", "ignore"]
3642
+ }).trim();
3643
+ const match = out.match(/(\d+)$/);
3644
+ return match ? parseInt(match[1], 10) : void 0;
3645
+ } catch {
3646
+ return void 0;
3647
+ }
3648
+ }
3649
+ function formatInterruptionCard(options) {
3650
+ const lines = [
3651
+ `### \u274C Milestone: Run Interrupted / Failed`,
3652
+ `- **Routine**: \`${options.routine}\``,
3653
+ `- **Status**: Routine execution interrupted or failed (\`${options.status}\`)`,
3654
+ `- **Step**: ${options.step || "Execution halted before normal completion"}`
3655
+ ];
3656
+ if (options.logUrl) {
3657
+ lines.push(`- **Action Log**: [View Run Logs](${options.logUrl})`);
3658
+ } else if (options.runner) {
3659
+ lines.push(`- **Runner**: \`${options.runner}\``);
3660
+ }
3661
+ return lines.join("\n");
3662
+ }
3663
+ function tryPostLocalRunMilestone(cwd, issueNumber, cardContent) {
3664
+ try {
3665
+ const tmpFile = path12.join(os2.tmpdir(), `jonah-fleet-milestone-${issueNumber}-${Date.now()}.md`);
3666
+ fs12.writeFileSync(tmpFile, cardContent, "utf8");
3667
+ try {
3668
+ execSync2(`gh issue comment ${issueNumber} --body-file ${JSON.stringify(tmpFile)}`, {
3669
+ cwd,
3670
+ stdio: ["ignore", "ignore", "ignore"]
3671
+ });
3672
+ } finally {
3673
+ if (fs12.existsSync(tmpFile)) {
3674
+ fs12.unlinkSync(tmpFile);
3675
+ }
3409
3676
  }
3410
- 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.`;
3677
+ } catch {
3411
3678
  }
3412
- return `You are the ${routine} routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}`;
3679
+ }
3680
+ function tryReconcileLocalRunIssue(cwd, issueNumber, reportContent, exitCode, hostname, routine) {
3681
+ try {
3682
+ if (exitCode !== 0) {
3683
+ const interruptionCard = formatInterruptionCard({
3684
+ routine: routine || "local-routine",
3685
+ status: `exit code ${exitCode}`,
3686
+ runner: `local (${hostname})`
3687
+ });
3688
+ tryPostLocalRunMilestone(cwd, issueNumber, interruptionCard);
3689
+ }
3690
+ const finalBody = `${reportContent}
3691
+
3692
+ ---
3693
+ _Generated by Jonah Fleet local runner on ${hostname}._`;
3694
+ const tmpFile = path12.join(os2.tmpdir(), `jonah-fleet-run-${issueNumber}-${Date.now()}.md`);
3695
+ fs12.writeFileSync(tmpFile, finalBody, "utf8");
3696
+ try {
3697
+ execSync2(`gh issue edit ${issueNumber} --body-file ${JSON.stringify(tmpFile)}`, {
3698
+ cwd,
3699
+ stdio: ["ignore", "ignore", "ignore"]
3700
+ });
3701
+ if (exitCode === 0) {
3702
+ execSync2(`gh issue edit ${issueNumber} --add-label "status:success" --remove-label "status:running"`, {
3703
+ cwd,
3704
+ stdio: ["ignore", "ignore", "ignore"]
3705
+ });
3706
+ execSync2(`gh issue close ${issueNumber} --reason completed`, {
3707
+ cwd,
3708
+ stdio: ["ignore", "ignore", "ignore"]
3709
+ });
3710
+ } else {
3711
+ execSync2(`gh issue edit ${issueNumber} --add-label "status:failure,needs-attention" --remove-label "status:running"`, {
3712
+ cwd,
3713
+ stdio: ["ignore", "ignore", "ignore"]
3714
+ });
3715
+ }
3716
+ } finally {
3717
+ if (fs12.existsSync(tmpFile)) {
3718
+ fs12.unlinkSync(tmpFile);
3719
+ }
3720
+ }
3721
+ } catch {
3722
+ }
3723
+ }
3724
+ function extractFreshRunReport(executionReportPath, targetReportPath, startTime) {
3725
+ try {
3726
+ if (fs12.existsSync(executionReportPath)) {
3727
+ const stat = fs12.statSync(executionReportPath);
3728
+ if (stat.mtimeMs >= startTime - 1e3) {
3729
+ return fs12.readFileSync(executionReportPath, "utf8");
3730
+ }
3731
+ }
3732
+ } catch {
3733
+ }
3734
+ try {
3735
+ if (executionReportPath !== targetReportPath && fs12.existsSync(targetReportPath)) {
3736
+ const stat = fs12.statSync(targetReportPath);
3737
+ if (stat.mtimeMs >= startTime - 1e3) {
3738
+ return fs12.readFileSync(targetReportPath, "utf8");
3739
+ }
3740
+ }
3741
+ } catch {
3742
+ }
3743
+ return null;
3413
3744
  }
3414
3745
  async function runLocalRoutine(options) {
3415
3746
  const targetDir = path12.resolve(options.targetDir);
3416
3747
  const routine = options.routine;
3417
- const model = options.model || "gemini-3.7-flash-high";
3748
+ const model = options.model || "gemini-3.8-flash-high";
3418
3749
  const printTimeout = options.printTimeout || "30m";
3419
3750
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3420
3751
  const hostname = os2.hostname();
@@ -3435,9 +3766,18 @@ async function runLocalRoutine(options) {
3435
3766
  worktreePath = worktreeResult.worktreePath;
3436
3767
  executionDir = worktreePath;
3437
3768
  }
3769
+ let targetTitle = options.title;
3770
+ const baseTarget = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : void 0;
3771
+ let targetLabel = baseTarget ? formatTargetLabel(baseTarget, targetTitle) : routine;
3772
+ let dynamicTargetDetected = Boolean(options.pr || options.issue);
3773
+ let routineIssueNumber;
3774
+ if (!options.dryRun) {
3775
+ routineIssueNumber = tryCreateLocalRunIssue(targetDir, routine, timestamp, targetLabel, hostname);
3776
+ }
3438
3777
  const prompt = buildRoutinePrompt(executionDir, routine, {
3439
3778
  issue: options.issue,
3440
- pr: options.pr
3779
+ pr: options.pr,
3780
+ routineIssueNumber
3441
3781
  });
3442
3782
  if (options.dryRun) {
3443
3783
  return {
@@ -3459,6 +3799,9 @@ Timeout: ${printTimeout}`,
3459
3799
  TARGET_ISSUE: options.issue ? String(options.issue) : "",
3460
3800
  PR_NUMBER: options.pr ? String(options.pr) : ""
3461
3801
  };
3802
+ if (routineIssueNumber) {
3803
+ childEnv.ROUTINE_ISSUE_NUMBER = String(routineIssueNumber);
3804
+ }
3462
3805
  const args = buildAgyArgs(prompt, model, printTimeout);
3463
3806
  let output = "";
3464
3807
  let finalResponseText = "";
@@ -3468,13 +3811,41 @@ Timeout: ${printTimeout}`,
3468
3811
  const logDir = path12.join(targetDir, ".jonah-fleet");
3469
3812
  fs12.mkdirSync(logDir, { recursive: true });
3470
3813
  const logFilePath = path12.join(logDir, "daemon.log");
3471
- let targetLabel = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : routine;
3472
- let dynamicTargetDetected = Boolean(options.pr || options.issue);
3814
+ const runsDir = path12.join(logDir, "runs");
3815
+ fs12.mkdirSync(runsDir, { recursive: true });
3816
+ const executionReportPath = path12.join(executionDir, ".jonah-fleet", "run-report.md");
3817
+ const targetReportPath = path12.join(targetDir, ".jonah-fleet", "run-report.md");
3818
+ try {
3819
+ if (fs12.existsSync(executionReportPath)) {
3820
+ fs12.unlinkSync(executionReportPath);
3821
+ }
3822
+ } catch {
3823
+ }
3824
+ try {
3825
+ if (fs12.existsSync(targetReportPath)) {
3826
+ fs12.unlinkSync(targetReportPath);
3827
+ }
3828
+ } catch {
3829
+ }
3473
3830
  let activePhase = "Starting session...";
3831
+ let lastActionDesc = null;
3474
3832
  const spinner = !options.verbose ? new TerminalSpinner() : null;
3475
3833
  if (spinner) {
3476
3834
  spinner.start(`${targetLabel}: ${activePhase}`);
3477
3835
  }
3836
+ if (baseTarget && !targetTitle) {
3837
+ fetchTargetTitleAsync(targetDir, baseTarget).then((fetchedTitle) => {
3838
+ if (fetchedTitle && !targetTitle) {
3839
+ targetTitle = fetchedTitle;
3840
+ targetLabel = formatTargetLabel(baseTarget, targetTitle);
3841
+ options.onTargetDetected?.(targetLabel);
3842
+ if (spinner) {
3843
+ spinner.update(`${targetLabel}: ${lastActionDesc || activePhase}`);
3844
+ }
3845
+ }
3846
+ }).catch(() => {
3847
+ });
3848
+ }
3478
3849
  const cleanup = async () => {
3479
3850
  spinner?.stop();
3480
3851
  if (worktreePath && !options.keepWorktree) {
@@ -3496,8 +3867,19 @@ Timeout: ${printTimeout}`,
3496
3867
  targetLabel = detected;
3497
3868
  options.onTargetDetected?.(detected);
3498
3869
  if (spinner) {
3499
- spinner.update(`${targetLabel}: ${activePhase}`);
3870
+ spinner.update(`${targetLabel}: ${lastActionDesc || activePhase}`);
3500
3871
  }
3872
+ fetchTargetTitleAsync(executionDir, detected).then((fetchedTitle) => {
3873
+ if (fetchedTitle) {
3874
+ targetTitle = fetchedTitle;
3875
+ targetLabel = formatTargetLabel(detected, fetchedTitle);
3876
+ options.onTargetDetected?.(targetLabel);
3877
+ if (spinner) {
3878
+ spinner.update(`${targetLabel}: ${lastActionDesc || activePhase}`);
3879
+ }
3880
+ }
3881
+ }).catch(() => {
3882
+ });
3501
3883
  }
3502
3884
  };
3503
3885
  const stdoutParser = new LineBufferedStreamParser((line) => {
@@ -3510,6 +3892,7 @@ Timeout: ${printTimeout}`,
3510
3892
  const toolParams = su.tool_info?.parameters;
3511
3893
  if (su.state === "ACTIVE") {
3512
3894
  const actionDesc = formatActionDescription(toolName, toolParams);
3895
+ lastActionDesc = actionDesc;
3513
3896
  if (spinner) {
3514
3897
  spinner.update(`${targetLabel}: ${actionDesc}`);
3515
3898
  }
@@ -3521,6 +3904,7 @@ Timeout: ${printTimeout}`,
3521
3904
  if (formatted) console.log(formatted);
3522
3905
  }
3523
3906
  } else if (su.state === "DONE") {
3907
+ lastActionDesc = null;
3524
3908
  if (su.tool_info?.output) {
3525
3909
  checkTargetDetection(su.tool_info.output);
3526
3910
  }
@@ -3538,7 +3922,8 @@ Timeout: ${printTimeout}`,
3538
3922
  accumulatedOutput += su.text_delta;
3539
3923
  checkTargetDetection(su.text_delta);
3540
3924
  const newPhase = detectActivePhase(su.text_delta, activePhase);
3541
- if (newPhase !== activePhase) {
3925
+ if (newPhase !== activePhase || lastActionDesc) {
3926
+ lastActionDesc = null;
3542
3927
  activePhase = newPhase;
3543
3928
  if (spinner) {
3544
3929
  spinner.update(`${targetLabel}: ${activePhase}`);
@@ -3576,6 +3961,7 @@ Timeout: ${printTimeout}`,
3576
3961
  if (options.verbose) {
3577
3962
  console.log(line);
3578
3963
  } else if (spinner) {
3964
+ lastActionDesc = null;
3579
3965
  const newPhase = detectActivePhase(line, activePhase);
3580
3966
  if (newPhase !== activePhase) {
3581
3967
  activePhase = newPhase;
@@ -3589,6 +3975,7 @@ Timeout: ${printTimeout}`,
3589
3975
  if (options.verbose) {
3590
3976
  console.error(pc10.dim(`[stderr] ${line}`));
3591
3977
  } else if (spinner) {
3978
+ lastActionDesc = null;
3592
3979
  const newPhase = detectActivePhase(line, activePhase);
3593
3980
  if (newPhase !== activePhase) {
3594
3981
  activePhase = newPhase;
@@ -3643,10 +4030,68 @@ Timeout: ${printTimeout}`,
3643
4030
  stdoutParser.flush();
3644
4031
  stderrParser.flush();
3645
4032
  output = finalResponseText || accumulatedOutput;
4033
+ const durationMs = Date.now() - startTime;
4034
+ const durationSec = Math.round(durationMs / 1e3);
4035
+ let reportContent = extractFreshRunReport(executionReportPath, targetReportPath, startTime) || "";
4036
+ if (!reportContent) {
4037
+ reportContent = `## Run Summary
4038
+
4039
+ | Metric | Value |
4040
+ |---|---|
4041
+ | Routine | \`${routine}\` |
4042
+ | Timestamp | \`${timestamp}\` |
4043
+ | Result | \`${exitCode === 0 ? "SUCCESS" : "FAILURE"}\` |
4044
+ | Exit Code | \`${exitCode}\` |
4045
+ | Host | \`${hostname}\` |
4046
+ | Target | \`${targetLabel}\` |
4047
+ | Duration | \`${durationSec}s\` |
4048
+ `;
4049
+ if (exitCode !== 0 && output.trim()) {
4050
+ const sanitizedOutput = stripAnsi(output.trim()).split("\n").slice(-15).join("\n");
4051
+ reportContent += `
4052
+ ### Error Output
4053
+
4054
+ \`\`\`
4055
+ ${sanitizedOutput}
4056
+ \`\`\`
4057
+ `;
4058
+ }
4059
+ }
4060
+ try {
4061
+ fs12.writeFileSync(targetReportPath, reportContent, "utf8");
4062
+ } catch {
4063
+ }
4064
+ const runReportFile = path12.join(runsDir, `${routine}-${timestamp}.md`);
4065
+ const runMetaFile = path12.join(runsDir, `${routine}-${timestamp}.json`);
4066
+ try {
4067
+ fs12.writeFileSync(runReportFile, reportContent, "utf8");
4068
+ fs12.writeFileSync(
4069
+ runMetaFile,
4070
+ JSON.stringify(
4071
+ {
4072
+ routine,
4073
+ timestamp,
4074
+ exitCode,
4075
+ success: exitCode === 0,
4076
+ target: targetLabel,
4077
+ issueNumber: routineIssueNumber,
4078
+ durationMs
4079
+ },
4080
+ null,
4081
+ 2
4082
+ ),
4083
+ "utf8"
4084
+ );
4085
+ } catch {
4086
+ }
4087
+ if (routineIssueNumber) {
4088
+ tryReconcileLocalRunIssue(targetDir, routineIssueNumber, reportContent, exitCode, hostname, routine);
4089
+ }
3646
4090
  if (options.showCard !== false && !options.verbose) {
3647
- const durationMs = Date.now() - startTime;
3648
- const effectiveIssue = options.issue || (targetLabel.startsWith("Issue #") ? targetLabel.replace("Issue #", "") : void 0);
3649
- const effectivePR = options.pr || (targetLabel.startsWith("PR #") ? targetLabel.replace("PR #", "") : void 0);
4091
+ const prMatch = targetLabel.match(/PR\s*#?(\d+)/i);
4092
+ const issueMatch = targetLabel.match(/Issue\s*#?(\d+)/i);
4093
+ const effectiveIssue = options.issue || (issueMatch ? issueMatch[1] : void 0);
4094
+ const effectivePR = options.pr || (prMatch ? prMatch[1] : void 0);
3650
4095
  if (exitCode === 0) {
3651
4096
  console.log(
3652
4097
  "\n" + renderSummaryCard({
@@ -3655,6 +4100,7 @@ Timeout: ${printTimeout}`,
3655
4100
  repoRoot: targetDir,
3656
4101
  issue: effectiveIssue,
3657
4102
  pr: effectivePR,
4103
+ title: targetTitle,
3658
4104
  durationMs
3659
4105
  }) + "\n"
3660
4106
  );
@@ -3676,7 +4122,8 @@ Timeout: ${printTimeout}`,
3676
4122
  exitCode,
3677
4123
  output,
3678
4124
  worktreePath,
3679
- branchName
4125
+ branchName,
4126
+ issueNumber: routineIssueNumber
3680
4127
  };
3681
4128
  }
3682
4129
 
@@ -3751,8 +4198,8 @@ import pc14 from "picocolors";
3751
4198
  // src/lib/daemon.ts
3752
4199
  import fs14 from "fs";
3753
4200
  import path14 from "path";
3754
- import { spawn as spawn2, execFile as execFile3 } from "child_process";
3755
- import { promisify as promisify3 } from "util";
4201
+ import { spawn as spawn2, execFile as execFile4 } from "child_process";
4202
+ import { promisify as promisify4 } from "util";
3756
4203
 
3757
4204
  // src/lib/daemon-keys.ts
3758
4205
  import readline2 from "readline";
@@ -3796,6 +4243,12 @@ var KeyboardController = class {
3796
4243
  }
3797
4244
  resume() {
3798
4245
  this.isPaused = false;
4246
+ if (this.listening && this.stdin && typeof this.stdin.resume === "function") {
4247
+ try {
4248
+ this.stdin.resume();
4249
+ } catch {
4250
+ }
4251
+ }
3799
4252
  }
3800
4253
  handleKeypress(str, key) {
3801
4254
  if (this.isPaused) return;
@@ -3928,6 +4381,12 @@ async function promptTargetedInput(promptMessage, options = {}) {
3928
4381
  } catch {
3929
4382
  }
3930
4383
  }
4384
+ if (stdin && typeof stdin.resume === "function") {
4385
+ try {
4386
+ stdin.resume();
4387
+ } catch {
4388
+ }
4389
+ }
3931
4390
  resolve(val);
3932
4391
  };
3933
4392
  const onRawData = (chunk) => {
@@ -4129,7 +4588,7 @@ function formatDaemonStatusLine(options = {}) {
4129
4588
 
4130
4589
  // src/lib/daemon.ts
4131
4590
  import pc13 from "picocolors";
4132
- var execFileAsync3 = promisify3(execFile3);
4591
+ var execFileAsync4 = promisify4(execFile4);
4133
4592
  function getDaemonStatePath(repoRoot) {
4134
4593
  return path14.join(repoRoot, ".jonah-fleet", "daemon.json");
4135
4594
  }
@@ -4174,7 +4633,7 @@ function filterReviewablePRs(prs) {
4174
4633
  }
4175
4634
  async function getOpenReviewablePRs(repoRoot) {
4176
4635
  try {
4177
- const { stdout } = await execFileAsync3(
4636
+ const { stdout } = await execFileAsync4(
4178
4637
  "gh",
4179
4638
  ["pr", "list", "--state", "open", "--draft=false", "--json", "number,headRefName,title"],
4180
4639
  { cwd: repoRoot }
@@ -4366,6 +4825,7 @@ async function runDaemonLoop(repoRoot, options = {}) {
4366
4825
  let isGracefulStopping = false;
4367
4826
  let isWorking = false;
4368
4827
  let isPaused = false;
4828
+ let isPrompting = false;
4369
4829
  let pendingRoutine = null;
4370
4830
  let keyboard;
4371
4831
  let tickerInterval;
@@ -4383,7 +4843,7 @@ async function runDaemonLoop(repoRoot, options = {}) {
4383
4843
  }
4384
4844
  };
4385
4845
  const updateTicker = () => {
4386
- if (isStopping || isWorking || options.verbose || !process.stderr.isTTY) return;
4846
+ if (isStopping || isWorking || isPrompting || options.verbose || !process.stderr.isTTY) return;
4387
4847
  const line = formatDaemonStatusLine({
4388
4848
  now: Date.now(),
4389
4849
  isPaused,
@@ -4418,6 +4878,7 @@ Stopping local agent daemon...`));
4418
4878
  if (isStopping || isWorking) return;
4419
4879
  try {
4420
4880
  isWorking = true;
4881
+ nextReviewCheckTime = Date.now() + reviewIntervalMs;
4421
4882
  await drainReviewQueue({
4422
4883
  repoRoot,
4423
4884
  state,
@@ -4456,34 +4917,42 @@ Stopping local agent daemon...`));
4456
4917
  };
4457
4918
  const runAutoworkCheck = async () => {
4458
4919
  if (isStopping || isWorking || !routines.includes("autowork")) return;
4459
- if (routines.includes("peer-review")) {
4460
- const pendingPRs = (await getPRsFn(repoRoot)).length;
4461
- if (pendingPRs > 0) {
4462
- console.log(
4463
- pc13.cyan(
4464
- `
4465
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
4466
- )
4467
- );
4468
- await performReviewDrain();
4469
- const remainingPRs = (await getPRsFn(repoRoot)).length;
4470
- if (remainingPRs > 0) {
4920
+ try {
4921
+ isWorking = true;
4922
+ nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
4923
+ if (routines.includes("peer-review")) {
4924
+ const pendingPRs = (await getPRsFn(repoRoot)).length;
4925
+ if (pendingPRs > 0) {
4471
4926
  console.log(
4472
- pc13.yellow(
4927
+ pc13.cyan(
4473
4928
  `
4474
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Review backlog still has ${remainingPRs} pending PR(s). Postponing autowork session.`
4929
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
4475
4930
  )
4476
4931
  );
4477
- nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
4478
- updateTicker();
4479
- return;
4932
+ await drainReviewQueue({
4933
+ repoRoot,
4934
+ state,
4935
+ options,
4936
+ isStopping: () => isStopping,
4937
+ clearTicker,
4938
+ getPRs: getPRsFn,
4939
+ runRoutine: runRoutineFn
4940
+ });
4941
+ const prs = await getPRsFn(repoRoot);
4942
+ lastOpenPRCount = prs.length;
4943
+ const remainingPRs = (await getPRsFn(repoRoot)).length;
4944
+ if (remainingPRs > 0) {
4945
+ console.log(
4946
+ pc13.yellow(
4947
+ `
4948
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Review backlog still has ${remainingPRs} pending PR(s). Postponing autowork session.`
4949
+ )
4950
+ );
4951
+ return;
4952
+ }
4480
4953
  }
4481
4954
  }
4482
- }
4483
- state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
4484
- writeDaemonState(repoRoot, state);
4485
- try {
4486
- isWorking = true;
4955
+ state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
4487
4956
  clearTicker();
4488
4957
  state.status = "working";
4489
4958
  state.activeRoutine = "autowork";
@@ -4715,7 +5184,7 @@ Stopping local agent daemon...`));
4715
5184
  await runAutoworkCheck();
4716
5185
  },
4717
5186
  onTargetedReview: async () => {
4718
- if (isStopping || isGracefulStopping) return;
5187
+ if (isStopping || isGracefulStopping || isPrompting) return;
4719
5188
  if (isWorking) {
4720
5189
  clearTicker();
4721
5190
  console.log(
@@ -4729,12 +5198,18 @@ Stopping local agent daemon...`));
4729
5198
  }
4730
5199
  clearTicker();
4731
5200
  keyboard?.pause();
4732
- const rawInput = await promptTargetedInput(`
5201
+ isPrompting = true;
5202
+ let rawInput = null;
5203
+ try {
5204
+ rawInput = await promptTargetedInput(`
4733
5205
  ${pc13.cyan("Enter PR # to review (Esc/Enter to cancel):")} `, {
4734
- stdin: options.stdin || process.stdin,
4735
- stdout: process.stdout
4736
- });
4737
- keyboard?.resume();
5206
+ stdin: options.stdin || process.stdin,
5207
+ stdout: process.stdout
5208
+ });
5209
+ } finally {
5210
+ isPrompting = false;
5211
+ keyboard?.resume();
5212
+ }
4738
5213
  if (!rawInput) {
4739
5214
  console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted review cancelled.
4740
5215
  `));
@@ -4753,7 +5228,7 @@ ${pc13.cyan("Enter PR # to review (Esc/Enter to cancel):")} `, {
4753
5228
  await runTargetedReview(prNumber);
4754
5229
  },
4755
5230
  onTargetedAutowork: async () => {
4756
- if (isStopping || isGracefulStopping) return;
5231
+ if (isStopping || isGracefulStopping || isPrompting) return;
4757
5232
  if (isWorking) {
4758
5233
  clearTicker();
4759
5234
  console.log(
@@ -4767,12 +5242,18 @@ ${pc13.cyan("Enter PR # to review (Esc/Enter to cancel):")} `, {
4767
5242
  }
4768
5243
  clearTicker();
4769
5244
  keyboard?.pause();
4770
- const rawInput = await promptTargetedInput(`
5245
+ isPrompting = true;
5246
+ let rawInput = null;
5247
+ try {
5248
+ rawInput = await promptTargetedInput(`
4771
5249
  ${pc13.cyan("Enter Issue # to work (Esc/Enter to cancel):")} `, {
4772
- stdin: options.stdin || process.stdin,
4773
- stdout: process.stdout
4774
- });
4775
- keyboard?.resume();
5250
+ stdin: options.stdin || process.stdin,
5251
+ stdout: process.stdout
5252
+ });
5253
+ } finally {
5254
+ isPrompting = false;
5255
+ keyboard?.resume();
5256
+ }
4776
5257
  if (!rawInput) {
4777
5258
  console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted autowork cancelled.
4778
5259
  `));
@@ -4888,20 +5369,26 @@ ${pc13.cyan("Enter Issue # to work (Esc/Enter to cancel):")} `, {
4888
5369
  await runAutoworkCheck();
4889
5370
  }
4890
5371
  if (isStopping) return;
5372
+ let isTicking = false;
4891
5373
  const tick = async () => {
4892
- if (isStopping || isGracefulStopping || isWorking) return;
4893
- if (!isPaused) {
4894
- const now = Date.now();
4895
- if (routines.includes("peer-review") && now >= nextReviewCheckTime) {
4896
- await performReviewDrain();
4897
- return;
4898
- }
4899
- if (routines.includes("autowork") && now >= nextAutoworkCheckTime) {
4900
- await runAutoworkCheck();
4901
- return;
5374
+ if (isStopping || isGracefulStopping || isWorking || isPrompting || isTicking) return;
5375
+ isTicking = true;
5376
+ try {
5377
+ if (!isPaused) {
5378
+ const now = Date.now();
5379
+ if (routines.includes("peer-review") && now >= nextReviewCheckTime) {
5380
+ await performReviewDrain();
5381
+ return;
5382
+ }
5383
+ if (routines.includes("autowork") && now >= nextAutoworkCheckTime) {
5384
+ await runAutoworkCheck();
5385
+ return;
5386
+ }
4902
5387
  }
5388
+ updateTicker();
5389
+ } finally {
5390
+ isTicking = false;
4903
5391
  }
4904
- updateTicker();
4905
5392
  };
4906
5393
  tickerInterval = setInterval(tick, 1e3);
4907
5394
  await new Promise((resolve) => {
@@ -5017,6 +5504,34 @@ async function runLabels(action = "audit", options = {}) {
5017
5504
  ...DEFAULT_PROTECTED_LABEL_PATTERNS,
5018
5505
  ...userProtected
5019
5506
  ];
5507
+ if (action === "provision" || action === "sync") {
5508
+ const result = await provisionLabels({ repo, cwd, executor });
5509
+ if (options.json) {
5510
+ console.log(JSON.stringify(result, null, 2));
5511
+ return;
5512
+ }
5513
+ console.log(pc15.bold(pc15.cyan(`
5514
+ \u{1F3F7}\uFE0F Fleet Label Provisioning for ${repo}
5515
+ `)));
5516
+ if (result.created.length > 0) {
5517
+ console.log(pc15.green(` \u2713 Created ${result.created.length} missing label(s):`));
5518
+ for (const label of result.created) {
5519
+ console.log(` - ${pc15.green(label)}`);
5520
+ }
5521
+ }
5522
+ if (result.alreadyExists.length > 0) {
5523
+ console.log(pc15.dim(` \u2139 ${result.alreadyExists.length} label(s) already exist.`));
5524
+ }
5525
+ if (result.errors.length > 0) {
5526
+ console.log(pc15.red(`
5527
+ \u274C Failed to create ${result.errors.length} label(s):`));
5528
+ for (const err of result.errors) {
5529
+ console.log(` - ${pc15.red(err.label)}: ${err.error}`);
5530
+ }
5531
+ }
5532
+ console.log();
5533
+ return;
5534
+ }
5020
5535
  if (action === "prune") {
5021
5536
  const isDryRun = Boolean(options.dryRun);
5022
5537
  const result = await pruneLabels({
@@ -5118,14 +5633,14 @@ async function runLabels(action = "audit", options = {}) {
5118
5633
  // src/index.ts
5119
5634
  var program = new Command();
5120
5635
  program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
5121
- 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").option("-v, --verbose", "Stream raw agent tokens and logs directly to stdout").action(async (routine, options) => {
5636
+ 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.8-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").option("-v, --verbose", "Stream raw agent tokens and logs directly to stdout").action(async (routine, options) => {
5122
5637
  await runRoutineCommand(routine, options);
5123
5638
  });
5124
5639
  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").option("-v, --verbose", "Stream raw agent tokens and logs directly to stdout").action(async (action, options) => {
5125
5640
  await runDaemonCommand(action, options);
5126
5641
  });
5127
- program.command("labels [action]").description("Audit and prune unused repository labels while protecting fleet taxonomy").option("-d, --dry-run", "Preview prunable labels without deleting them", false).option("-y, --yes", "Confirm automatic deletion of prunable labels", false).option("-r, --repo <repo>", "Target GitHub repository (defaults to current)").option("-j, --json", "Output results as JSON", false).action(async (action, options) => {
5128
- const act = action === "prune" || action === "list" || action === "audit" ? action : "audit";
5642
+ program.command("labels [action]").description("Audit, prune, or provision repository labels for autonomous fleet taxonomy (audit | prune | provision)").option("-d, --dry-run", "Preview prunable labels without deleting them", false).option("-y, --yes", "Confirm automatic deletion of prunable labels", false).option("-r, --repo <repo>", "Target GitHub repository (defaults to current)").option("-j, --json", "Output results as JSON", false).action(async (action, options) => {
5643
+ const act = action === "prune" || action === "list" || action === "audit" || action === "provision" || action === "sync" ? action : "audit";
5129
5644
  await runLabels(act, options);
5130
5645
  });
5131
5646
  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("--prune-labels", "Prune unused boilerplate labels on initialization", false).option("--interactive", "Force interactive prompts for stack configuration").option("--no-interactive", "Disable interactive prompts").action(async (options) => {