jonah-fleet 1.6.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 (49) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +25 -2
  3. package/dist/commands/daemon.d.ts.map +1 -1
  4. package/dist/commands/init.d.ts +3 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/labels.d.ts +11 -0
  7. package/dist/commands/labels.d.ts.map +1 -0
  8. package/dist/commands/status.d.ts.map +1 -1
  9. package/dist/commands/sync.d.ts.map +1 -1
  10. package/dist/commands/telemetry.d.ts.map +1 -1
  11. package/dist/index.js +2527 -519
  12. package/dist/lib/daemon-keys.d.ts +103 -0
  13. package/dist/lib/daemon-keys.d.ts.map +1 -0
  14. package/dist/lib/daemon.d.ts +8 -2
  15. package/dist/lib/daemon.d.ts.map +1 -1
  16. package/dist/lib/fleet-query.d.ts.map +1 -1
  17. package/dist/lib/labels.d.ts +68 -0
  18. package/dist/lib/labels.d.ts.map +1 -0
  19. package/dist/lib/manifest.d.ts +6 -0
  20. package/dist/lib/manifest.d.ts.map +1 -1
  21. package/dist/lib/presets.d.ts +52 -1
  22. package/dist/lib/presets.d.ts.map +1 -1
  23. package/dist/lib/runner.d.ts +114 -0
  24. package/dist/lib/runner.d.ts.map +1 -1
  25. package/dist/lib/telemetry.d.ts.map +1 -1
  26. package/dist/lib/terminal-card.d.ts +24 -1
  27. package/dist/lib/terminal-card.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/schema.json +74 -1
  30. package/templates/prompts/ORCHESTRATION.md +84 -41
  31. package/templates/prompts/_prompt-template.md +10 -13
  32. package/templates/prompts/analytics-review.md +4 -2
  33. package/templates/prompts/autowork.md +76 -35
  34. package/templates/prompts/dependency-update-security-check.md +4 -2
  35. package/templates/prompts/design-review.md +125 -0
  36. package/templates/prompts/issues-housekeeping.md +10 -8
  37. package/templates/prompts/optimizer.md +31 -12
  38. package/templates/prompts/peer-review.md +42 -7
  39. package/templates/prompts/product-planning.md +14 -10
  40. package/templates/workflows/autowork-cron.yml +137 -38
  41. package/templates/workflows/dependency-check-cron.yml +127 -2
  42. package/templates/workflows/design-review-cron.yml +221 -0
  43. package/templates/workflows/issues-housekeeping-cron.yml +127 -2
  44. package/templates/workflows/prompt-optimizer-cron.yml +134 -9
  45. package/templates/workflows/sync-fleet.yml +4 -0
  46. package/templates/workflows/trigger-autowork-manual.yml +154 -11
  47. package/templates/workflows/trigger-autowork-on-bug.yml +177 -9
  48. package/templates/workflows/trigger-autowork-on-merge.yml +178 -9
  49. package/templates/workflows/trigger-review-routine.yml +162 -22
package/dist/index.js CHANGED
@@ -12,6 +12,35 @@ import fs from "fs";
12
12
  import path from "path";
13
13
 
14
14
  // src/lib/presets.ts
15
+ var DEFAULT_MODELS_CONFIG = {
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
+ };
21
+ var DEFAULT_BUDGETS_CONFIG = {
22
+ weeklyTokens: 875e4,
23
+ timeoutMinutes: {
24
+ autowork: 60,
25
+ "peer-review": 55,
26
+ optimizer: 35,
27
+ "issues-housekeeping": 40,
28
+ "dependency-update-security-check": 25,
29
+ "product-planning": 45,
30
+ "analytics-review": 30,
31
+ "design-review": 50
32
+ },
33
+ maxIterations: {
34
+ autowork: 65,
35
+ "peer-review": 40,
36
+ optimizer: 30,
37
+ "issues-housekeeping": 30,
38
+ "dependency-update-security-check": 20,
39
+ "product-planning": 40,
40
+ "analytics-review": 25,
41
+ "design-review": 30
42
+ }
43
+ };
15
44
  var PRESET_CONFIGS = {
16
45
  minimal: {
17
46
  routines: {
@@ -21,7 +50,8 @@ var PRESET_CONFIGS = {
21
50
  "issues-housekeeping": false,
22
51
  "dependency-update-security-check": false,
23
52
  "product-planning": false,
24
- "analytics-review": false
53
+ "analytics-review": false,
54
+ "design-review": false
25
55
  },
26
56
  skills: [
27
57
  "tdd",
@@ -39,7 +69,8 @@ var PRESET_CONFIGS = {
39
69
  "issues-housekeeping": true,
40
70
  "dependency-update-security-check": true,
41
71
  "product-planning": false,
42
- "analytics-review": false
72
+ "analytics-review": false,
73
+ "design-review": false
43
74
  },
44
75
  skills: [
45
76
  "tdd",
@@ -61,7 +92,8 @@ var PRESET_CONFIGS = {
61
92
  "issues-housekeeping": true,
62
93
  "dependency-update-security-check": true,
63
94
  "product-planning": true,
64
- "analytics-review": true
95
+ "analytics-review": true,
96
+ "design-review": true
65
97
  },
66
98
  skills: [
67
99
  "tdd",
@@ -90,9 +122,10 @@ var ROUTINE_TO_WORKFLOW_MAP = {
90
122
  "issues-housekeeping": ["issues-housekeeping-cron.yml"],
91
123
  "dependency-update-security-check": ["dependency-check-cron.yml"],
92
124
  "product-planning": [],
93
- "analytics-review": []
125
+ "analytics-review": [],
126
+ "design-review": ["design-review-cron.yml"]
94
127
  };
95
- var FLEET_VERSION = "1.6.0";
128
+ var FLEET_VERSION = "1.8.0";
96
129
  var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
97
130
 
98
131
  // src/lib/manifest.ts
@@ -126,9 +159,16 @@ function createDefaultManifest(preset = "standard") {
126
159
  "issues-housekeeping": true,
127
160
  "dependency-update-security-check": true,
128
161
  "product-planning": false,
129
- "analytics-review": false
162
+ "analytics-review": false,
163
+ "design-review": false
130
164
  },
131
165
  skills: PRESET_CONFIGS.standard.skills,
166
+ models: { ...DEFAULT_MODELS_CONFIG },
167
+ budgets: {
168
+ weeklyTokens: DEFAULT_BUDGETS_CONFIG.weeklyTokens,
169
+ timeoutMinutes: { ...DEFAULT_BUDGETS_CONFIG.timeoutMinutes },
170
+ maxIterations: { ...DEFAULT_BUDGETS_CONFIG.maxIterations }
171
+ },
132
172
  autoUpdate: {
133
173
  enabled: true,
134
174
  channel: "stable"
@@ -142,6 +182,12 @@ function createDefaultManifest(preset = "standard") {
142
182
  preset,
143
183
  routines: { ...config.routines },
144
184
  skills: [...config.skills],
185
+ models: { ...DEFAULT_MODELS_CONFIG },
186
+ budgets: {
187
+ weeklyTokens: DEFAULT_BUDGETS_CONFIG.weeklyTokens,
188
+ timeoutMinutes: { ...DEFAULT_BUDGETS_CONFIG.timeoutMinutes },
189
+ maxIterations: { ...DEFAULT_BUDGETS_CONFIG.maxIterations }
190
+ },
145
191
  autoUpdate: {
146
192
  enabled: true,
147
193
  channel: "stable"
@@ -629,235 +675,10 @@ function installFleet(targetDir, manifest, options = {}) {
629
675
  return result;
630
676
  }
631
677
 
632
- // src/commands/init.ts
633
- async function promptQuestion(query, defaultValue) {
634
- const rl = readline.createInterface({
635
- input: process.stdin,
636
- output: process.stdout
637
- });
638
- return new Promise((resolve) => {
639
- rl.question(`${query} [${defaultValue}]: `, (answer) => {
640
- rl.close();
641
- resolve(answer.trim() || defaultValue);
642
- });
643
- });
644
- }
645
- async function runInit(options = {}) {
646
- const cwd = options.cwd || process.cwd();
647
- const preset = options.preset || "standard";
648
- console.log(pc.cyan(`
649
- \u2693 Initializing Jonah Fleet (preset: ${pc.bold(preset)}) in ${cwd}
650
- `));
651
- let detected = detectTechStack(cwd);
652
- console.log(pc.bold("\u{1F50D} Tech Stack Auto-Detection:"));
653
- console.log(pc.cyan(` - Detected Stack: ${pc.bold(detected.name)}`));
654
- console.log(pc.cyan(` - Language: ${detected.language}`));
655
- if (detected.framework) {
656
- console.log(pc.cyan(` - Framework: ${detected.framework}`));
657
- }
658
- console.log(pc.cyan(` - Package Manager: ${detected.packageManager}`));
659
- if (detected.testFramework) {
660
- console.log(pc.cyan(` - Testing: ${detected.testFramework}`));
661
- }
662
- if (detected.commands.test) {
663
- console.log(pc.cyan(` - Test Command: ${detected.commands.test}`));
664
- }
665
- const isInteractive = options.interactive ?? (process.stdin.isTTY && !options.stack && !options.testCmd);
666
- if (isInteractive && process.stdin.isTTY) {
667
- console.log(pc.yellow("\n\u2699\uFE0F Configure project settings (press enter to accept defaults):"));
668
- const stackName = await promptQuestion("Tech Stack Name", detected.name);
669
- const pkgManager = await promptQuestion("Package Manager", detected.packageManager);
670
- const testCmd = await promptQuestion("Test Command", detected.commands.test || "npm test");
671
- const buildCmd = await promptQuestion("Build Command", detected.commands.build || "npm run build");
672
- detected = {
673
- ...detected,
674
- name: stackName,
675
- language: stackName,
676
- framework: void 0,
677
- packageManager: pkgManager,
678
- commands: {
679
- ...detected.commands,
680
- test: testCmd,
681
- build: buildCmd
682
- }
683
- };
684
- }
685
- if (options.stack) {
686
- detected.name = options.stack;
687
- detected.language = options.stack;
688
- detected.framework = void 0;
689
- }
690
- if (options.packageManager) {
691
- detected.packageManager = options.packageManager;
692
- }
693
- if (options.testCmd) {
694
- detected.commands.test = options.testCmd;
695
- }
696
- if (options.buildCmd) {
697
- detected.commands.build = options.buildCmd;
698
- }
699
- let manifest = loadManifest(cwd);
700
- if (manifest && !options.force) {
701
- console.log(pc.yellow(`
702
- \u26A0\uFE0F Found existing agents-manifest.json. Updating with preset '${preset}'...`));
703
- } else {
704
- manifest = createDefaultManifest(preset);
705
- }
706
- saveManifest(cwd, manifest);
707
- console.log(pc.green(`\u2713 Created/Updated agents-manifest.json`));
708
- const result = installFleet(cwd, manifest, { force: options.force, detectedStack: detected });
709
- console.log(pc.bold("\nInstalled components:"));
710
- if (result.promptsInstalled.length > 0) {
711
- console.log(pc.green(` \u{1F4C1} Prompts (.github/prompts/):`));
712
- result.promptsInstalled.forEach((p) => console.log(` - ${p}`));
713
- }
714
- if (result.workflowsInstalled.length > 0) {
715
- console.log(pc.green(` \u2699\uFE0F Workflows (.github/workflows/):`));
716
- result.workflowsInstalled.forEach((w) => console.log(` - ${w}`));
717
- }
718
- if (result.skillsInstalled.length > 0) {
719
- console.log(pc.green(` \u{1F9E0} Skills (.agents/skills/):`));
720
- result.skillsInstalled.forEach((s) => console.log(` - ${s}`));
721
- }
722
- if (result.docsInstalled.length > 0) {
723
- console.log(pc.green(` \u{1F4C4} Documentation:`));
724
- result.docsInstalled.forEach((d) => console.log(` - ${d}`));
725
- }
726
- console.log(pc.bold(pc.green("\n\u{1F389} Jonah Fleet initialization complete!\n")));
727
- console.log(pc.cyan("Next steps for GitHub repository configuration:"));
728
- console.log(" 1. In Settings \u2192 Actions \u2192 General \u2192 Workflow permissions:");
729
- console.log(' Select "Read and write permissions" and check "Allow GitHub Actions to create and approve pull requests".');
730
- console.log(" 2. In Settings \u2192 Actions \u2192 General \u2192 Fork pull request workflows:");
731
- console.log(" Configure workflow approval settings to prevent automated runs from stalling awaiting approval.");
732
- console.log(" 3. Customize project context, build, and test commands in AGENTS.md.\n");
733
- }
734
-
735
- // src/commands/sync.ts
736
- import pc2 from "picocolors";
737
-
738
- // src/lib/diff.ts
739
- import fs4 from "fs";
740
- import path4 from "path";
741
- function checkDrift(targetDir, manifest) {
742
- const templatesDir = getTemplatesDir();
743
- const report = {
744
- missingPrompts: [],
745
- modifiedPrompts: [],
746
- missingWorkflows: [],
747
- modifiedWorkflows: [],
748
- missingSkills: []
749
- };
750
- const targetPromptsDir = path4.join(targetDir, ".github/prompts");
751
- const targetWorkflowsDir = path4.join(targetDir, ".github/workflows");
752
- const targetSkillsDir = path4.join(targetDir, ".agents/skills");
753
- const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
754
- for (const file of basePrompts) {
755
- const src = path4.join(templatesDir, "prompts", file);
756
- const dest = path4.join(targetPromptsDir, file);
757
- if (!fs4.existsSync(dest)) {
758
- report.missingPrompts.push(file);
759
- } else if (fs4.readFileSync(src, "utf8") !== fs4.readFileSync(dest, "utf8")) {
760
- report.modifiedPrompts.push(file);
761
- }
762
- }
763
- for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
764
- if (!isEnabled) continue;
765
- const promptFile = `${routineName}.md`;
766
- const promptSrc = path4.join(templatesDir, "prompts", promptFile);
767
- const promptDest = path4.join(targetPromptsDir, promptFile);
768
- if (!fs4.existsSync(promptDest)) {
769
- report.missingPrompts.push(promptFile);
770
- } else if (fs4.existsSync(promptSrc) && fs4.readFileSync(promptSrc, "utf8") !== fs4.readFileSync(promptDest, "utf8")) {
771
- report.modifiedPrompts.push(promptFile);
772
- }
773
- const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
774
- for (const workflowFile of workflows) {
775
- const wfSrc = path4.join(templatesDir, "workflows", workflowFile);
776
- const wfDest = path4.join(targetWorkflowsDir, workflowFile);
777
- if (!fs4.existsSync(wfDest)) {
778
- report.missingWorkflows.push(workflowFile);
779
- } else if (fs4.existsSync(wfSrc)) {
780
- const rawSrc = fs4.readFileSync(wfSrc, "utf8");
781
- const destContent = fs4.readFileSync(wfDest, "utf8");
782
- const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
783
- const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
784
- if (expectedSrc !== destContent) {
785
- report.modifiedWorkflows.push(workflowFile);
786
- }
787
- }
788
- }
789
- }
790
- if (manifest.autoUpdate?.enabled) {
791
- const syncWfSrc = path4.join(templatesDir, "workflows/sync-fleet.yml");
792
- const syncWfDest = path4.join(targetWorkflowsDir, "sync-fleet.yml");
793
- if (!fs4.existsSync(syncWfDest)) {
794
- report.missingWorkflows.push("sync-fleet.yml");
795
- } else if (fs4.existsSync(syncWfSrc)) {
796
- const rawSrc = fs4.readFileSync(syncWfSrc, "utf8");
797
- const destContent = fs4.readFileSync(syncWfDest, "utf8");
798
- const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
799
- const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
800
- if (expectedSrc !== destContent) {
801
- report.modifiedWorkflows.push("sync-fleet.yml");
802
- }
803
- }
804
- }
805
- for (const skill of manifest.skills) {
806
- const skillDestDir = path4.join(targetSkillsDir, skill);
807
- if (!fs4.existsSync(skillDestDir)) {
808
- report.missingSkills.push(skill);
809
- }
810
- }
811
- return report;
812
- }
813
-
814
- // src/commands/sync.ts
815
- async function runSync(options = {}) {
816
- const cwd = options.cwd || process.cwd();
817
- const manifest = loadManifest(cwd);
818
- if (!manifest) {
819
- console.error(pc2.red(`\u274C No agents-manifest.json found in ${cwd}. Run 'jonah-fleet init' first.`));
820
- process.exit(1);
821
- }
822
- console.log(pc2.cyan(`
823
- \u{1F504} Syncing Jonah Fleet (current: v${manifest.version}, fleet: v${FLEET_VERSION})...
824
- `));
825
- const drift = checkDrift(cwd, manifest);
826
- const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
827
- if (options.check) {
828
- if (!hasDrift && manifest.version === FLEET_VERSION) {
829
- console.log(pc2.green(`\u2713 All routines, workflows, and skills are perfectly in sync with v${FLEET_VERSION}.
830
- `));
831
- return;
832
- }
833
- console.log(pc2.yellow(`\u26A0\uFE0F Drift or updates detected:`));
834
- if (drift.missingPrompts.length > 0) console.log(pc2.red(` Missing prompts: ${drift.missingPrompts.join(", ")}`));
835
- if (drift.modifiedPrompts.length > 0) console.log(pc2.yellow(` Modified prompts: ${drift.modifiedPrompts.join(", ")}`));
836
- if (drift.missingWorkflows.length > 0) console.log(pc2.red(` Missing workflows: ${drift.missingWorkflows.join(", ")}`));
837
- if (drift.modifiedWorkflows.length > 0) console.log(pc2.yellow(` Modified workflows: ${drift.modifiedWorkflows.join(", ")}`));
838
- if (drift.missingSkills.length > 0) console.log(pc2.red(` Missing skills: ${drift.missingSkills.join(", ")}`));
839
- console.log(pc2.cyan(`
840
- Run 'jonah-fleet sync --force' to apply updates.
841
- `));
842
- return;
843
- }
844
- manifest.version = FLEET_VERSION;
845
- saveManifest(cwd, manifest);
846
- const result = installFleet(cwd, manifest, { force: true });
847
- console.log(pc2.green(`\u2713 Synchronized with Jonah Fleet v${FLEET_VERSION}`));
848
- console.log(pc2.green(`\u2713 Updated ${result.promptsInstalled.length} prompts, ${result.workflowsInstalled.length} workflows, and ${result.skillsInstalled.length} skills.
849
- `));
850
- }
851
-
852
- // src/commands/status.ts
853
- import fs7 from "fs";
854
- import path7 from "path";
855
- import pc5 from "picocolors";
856
-
857
678
  // src/lib/fleet-query.ts
858
679
  import { execFile } from "child_process";
859
- import fs5 from "fs";
860
- import path5 from "path";
680
+ import fs4 from "fs";
681
+ import path4 from "path";
861
682
  import { promisify } from "util";
862
683
  var execFileAsync = promisify(execFile);
863
684
  var defaultGhExecutor = async (args) => {
@@ -1036,11 +857,11 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
1036
857
  staleWarnings: []
1037
858
  };
1038
859
  try {
1039
- if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
1040
- const manifestPath = path5.join(repoIdentifier, "agents-manifest.json");
1041
- if (fs5.existsSync(manifestPath)) {
860
+ if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
861
+ const manifestPath = path4.join(repoIdentifier, "agents-manifest.json");
862
+ if (fs4.existsSync(manifestPath)) {
1042
863
  try {
1043
- const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
864
+ const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
1044
865
  result.fleetVersion = raw.version;
1045
866
  result.preset = raw.preset;
1046
867
  } catch {
@@ -1120,47 +941,75 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
1120
941
  if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
1121
942
  }
1122
943
  const logContents = [];
1123
- if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
1124
- const logsDir = path5.join(repoIdentifier, ".github/prompts/logs");
1125
- if (fs5.existsSync(logsDir)) {
1126
- const collectLogs = (dir) => {
1127
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
1128
- for (const entry of entries) {
1129
- const fullPath = path5.join(dir, entry.name);
1130
- if (entry.isDirectory()) {
1131
- collectLogs(fullPath);
1132
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1133
- try {
1134
- logContents.push(fs5.readFileSync(fullPath, "utf8"));
1135
- } catch {
1136
- }
944
+ if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
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 {
1137
958
  }
1138
959
  }
1139
- };
1140
- collectLogs(logsDir);
1141
- }
960
+ }
961
+ };
962
+ collectLogs(runsDir);
963
+ collectLogs(legacyLogsDir);
1142
964
  } else {
1143
965
  try {
1144
- const treeRaw = await executor([
1145
- "api",
1146
- `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"
1147
979
  ]);
1148
- const tree = JSON.parse(treeRaw);
1149
- if (Array.isArray(tree.tree)) {
1150
- const logFiles = tree.tree.filter((node) => node.path && node.path.startsWith(".github/prompts/logs/") && node.path.endsWith(".md")).slice(-15);
1151
- for (const file of logFiles) {
1152
- try {
1153
- const fileRaw = await executor(["api", `repos/${repoIdentifier}/contents/${file.path}`]);
1154
- const parsed = JSON.parse(fileRaw);
1155
- if (parsed.content) {
1156
- logContents.push(Buffer.from(parsed.content, "base64").toString("utf8"));
1157
- }
1158
- } 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);
1159
985
  }
1160
986
  }
1161
987
  }
1162
988
  } catch {
1163
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
+ }
1164
1013
  }
1165
1014
  if (logContents.length > 0) {
1166
1015
  result.tokenUsage = computeTokenSpendFromLogs(logContents, now);
@@ -1251,23 +1100,545 @@ function summarizeFleet(statuses) {
1251
1100
  return summary;
1252
1101
  }
1253
1102
 
1254
- // src/lib/dashboard.ts
1255
- import pc3 from "picocolors";
1256
- function formatTokens(num) {
1257
- if (num >= 1e6) {
1258
- return `${(num / 1e6).toFixed(2)}M`;
1259
- }
1260
- if (num >= 1e3) {
1261
- return `${(num / 1e3).toFixed(1)}k`;
1103
+ // src/lib/labels.ts
1104
+ var DEFAULT_PROTECTED_LABEL_PATTERNS = [
1105
+ "priority/*",
1106
+ "type/*",
1107
+ "size/*",
1108
+ "needs-triage",
1109
+ "ready-for-agent",
1110
+ "needs-human",
1111
+ "needs-info",
1112
+ "needs-design",
1113
+ "wontfix",
1114
+ "measurement",
1115
+ "blocked",
1116
+ "autorelease:*",
1117
+ "dependencies",
1118
+ "security",
1119
+ "routine-log",
1120
+ "routine:*",
1121
+ "status:*",
1122
+ "runner:*",
1123
+ "historical-migration",
1124
+ "needs-attention"
1125
+ ];
1126
+ function isLabelProtected(labelName, protectedPatterns = DEFAULT_PROTECTED_LABEL_PATTERNS) {
1127
+ for (const pattern of protectedPatterns) {
1128
+ if (pattern === labelName) {
1129
+ return true;
1130
+ }
1131
+ if (pattern.includes("*")) {
1132
+ const regexPattern = "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
1133
+ const regex = new RegExp(regexPattern);
1134
+ if (regex.test(labelName)) {
1135
+ return true;
1136
+ }
1137
+ }
1262
1138
  }
1263
- return num.toString();
1139
+ return false;
1264
1140
  }
1265
- function formatCurrency(amount) {
1266
- return `$${amount.toFixed(2)}`;
1141
+ function classifyLabels(labels, protectedPatterns = DEFAULT_PROTECTED_LABEL_PATTERNS) {
1142
+ const classified = {
1143
+ active: [],
1144
+ protectedZeroCount: [],
1145
+ historical: [],
1146
+ prunable: [],
1147
+ all: []
1148
+ };
1149
+ for (const label of labels) {
1150
+ const isZeroTotal = label.totalIssuesCount === 0 && label.totalPullRequestsCount === 0;
1151
+ const hasOpenItems = label.openIssuesCount > 0 || label.openPullRequestsCount > 0;
1152
+ let category;
1153
+ if (isZeroTotal) {
1154
+ if (isLabelProtected(label.name, protectedPatterns)) {
1155
+ category = "protected_zero_count";
1156
+ const item = { ...label, category };
1157
+ classified.protectedZeroCount.push(item);
1158
+ classified.all.push(item);
1159
+ } else {
1160
+ category = "prunable";
1161
+ const item = { ...label, category };
1162
+ classified.prunable.push(item);
1163
+ classified.all.push(item);
1164
+ }
1165
+ } else if (hasOpenItems) {
1166
+ category = "active";
1167
+ const item = { ...label, category };
1168
+ classified.active.push(item);
1169
+ classified.all.push(item);
1170
+ } else {
1171
+ category = "historical";
1172
+ const item = { ...label, category };
1173
+ classified.historical.push(item);
1174
+ classified.all.push(item);
1175
+ }
1176
+ }
1177
+ return classified;
1267
1178
  }
1268
- function renderFleetDashboard(statuses, options = {}) {
1269
- const summary = summarizeFleet(statuses);
1270
- if (options.json) {
1179
+ async function resolveRepoName(repoIdentifier, cwd = process.cwd(), executor = defaultGhExecutor) {
1180
+ if (repoIdentifier && repoIdentifier.includes("/")) {
1181
+ return repoIdentifier;
1182
+ }
1183
+ if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_REPOSITORY.includes("/")) {
1184
+ return process.env.GITHUB_REPOSITORY;
1185
+ }
1186
+ try {
1187
+ const raw = await executor(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]);
1188
+ const trimmed = raw.trim();
1189
+ if (trimmed && trimmed.includes("/")) {
1190
+ return trimmed;
1191
+ }
1192
+ } catch {
1193
+ }
1194
+ return repoIdentifier || "current";
1195
+ }
1196
+ var LABELS_GRAPHQL_QUERY = `
1197
+ query($owner: String!, $repo: String!, $cursor: String) {
1198
+ repository(owner: $owner, name: $repo) {
1199
+ labels(first: 100, after: $cursor) {
1200
+ nodes {
1201
+ id
1202
+ name
1203
+ description
1204
+ color
1205
+ issues(states: [OPEN]) {
1206
+ totalCount
1207
+ }
1208
+ allIssues: issues {
1209
+ totalCount
1210
+ }
1211
+ pullRequests(states: [OPEN]) {
1212
+ totalCount
1213
+ }
1214
+ allPullRequests: pullRequests {
1215
+ totalCount
1216
+ }
1217
+ }
1218
+ pageInfo {
1219
+ hasNextPage
1220
+ endCursor
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ `;
1226
+ async function fetchRepoLabels(repoIdentifier, executor = defaultGhExecutor, cwd = process.cwd()) {
1227
+ const fullRepo = await resolveRepoName(repoIdentifier, cwd, executor);
1228
+ const [owner, repo] = fullRepo.split("/");
1229
+ if (!owner || !repo) {
1230
+ throw new Error(`Invalid repository identifier '${fullRepo}'. Expected format 'owner/repo'.`);
1231
+ }
1232
+ const results = [];
1233
+ let cursor = null;
1234
+ let hasNextPage = true;
1235
+ while (hasNextPage) {
1236
+ const queryArgs = [
1237
+ "api",
1238
+ "graphql",
1239
+ "-f",
1240
+ `query=${LABELS_GRAPHQL_QUERY}`,
1241
+ "-F",
1242
+ `owner=${owner}`,
1243
+ "-F",
1244
+ `repo=${repo}`
1245
+ ];
1246
+ if (cursor) {
1247
+ queryArgs.push("-F", `cursor=${cursor}`);
1248
+ }
1249
+ const raw = await executor(queryArgs);
1250
+ const parsed = JSON.parse(raw);
1251
+ if (parsed.errors && parsed.errors.length > 0) {
1252
+ throw new Error(`GitHub GraphQL query failed: ${parsed.errors[0].message}`);
1253
+ }
1254
+ const labelConnection = parsed.data?.repository?.labels;
1255
+ if (!labelConnection || !Array.isArray(labelConnection.nodes)) {
1256
+ break;
1257
+ }
1258
+ for (const node of labelConnection.nodes) {
1259
+ results.push({
1260
+ id: node.id,
1261
+ name: node.name,
1262
+ description: node.description ?? null,
1263
+ color: node.color,
1264
+ openIssuesCount: node.issues?.totalCount || 0,
1265
+ totalIssuesCount: node.allIssues?.totalCount || 0,
1266
+ openPullRequestsCount: node.pullRequests?.totalCount || 0,
1267
+ totalPullRequestsCount: node.allPullRequests?.totalCount || 0
1268
+ });
1269
+ }
1270
+ hasNextPage = Boolean(labelConnection.pageInfo?.hasNextPage);
1271
+ cursor = labelConnection.pageInfo?.endCursor || null;
1272
+ if (!cursor) {
1273
+ break;
1274
+ }
1275
+ }
1276
+ return results;
1277
+ }
1278
+ async function pruneLabels(options = {}) {
1279
+ const cwd = options.cwd || process.cwd();
1280
+ const executor = options.executor || defaultGhExecutor;
1281
+ const repo = await resolveRepoName(options.repo, cwd, executor);
1282
+ const manifest = loadManifest(cwd);
1283
+ const userProtected = manifest?.labels?.protected || [];
1284
+ const protectedPatterns = options.protectedPatterns || [
1285
+ ...DEFAULT_PROTECTED_LABEL_PATTERNS,
1286
+ ...userProtected
1287
+ ];
1288
+ const rawLabels = await fetchRepoLabels(repo, executor, cwd);
1289
+ const classified = classifyLabels(rawLabels, protectedPatterns);
1290
+ const result = {
1291
+ repo,
1292
+ classified,
1293
+ pruned: [],
1294
+ skipped: [],
1295
+ errors: [],
1296
+ dryRun: Boolean(options.dryRun)
1297
+ };
1298
+ for (const item of classified.prunable) {
1299
+ if (options.dryRun) {
1300
+ result.pruned.push(item.name);
1301
+ } else {
1302
+ try {
1303
+ const deleteArgs = ["label", "delete", item.name, "--yes"];
1304
+ if (repo && repo !== "current") {
1305
+ deleteArgs.push("--repo", repo);
1306
+ }
1307
+ await executor(deleteArgs);
1308
+ result.pruned.push(item.name);
1309
+ } catch (err) {
1310
+ result.errors.push({
1311
+ label: item.name,
1312
+ error: err.message || String(err)
1313
+ });
1314
+ }
1315
+ }
1316
+ }
1317
+ return result;
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
+ }
1379
+
1380
+ // src/commands/init.ts
1381
+ async function promptQuestion(query, defaultValue) {
1382
+ const rl = readline.createInterface({
1383
+ input: process.stdin,
1384
+ output: process.stdout
1385
+ });
1386
+ return new Promise((resolve) => {
1387
+ rl.question(`${query} [${defaultValue}]: `, (answer) => {
1388
+ rl.close();
1389
+ resolve(answer.trim() || defaultValue);
1390
+ });
1391
+ });
1392
+ }
1393
+ async function runInit(options = {}) {
1394
+ const cwd = options.cwd || process.cwd();
1395
+ const preset = options.preset || "standard";
1396
+ console.log(pc.cyan(`
1397
+ \u2693 Initializing Jonah Fleet (preset: ${pc.bold(preset)}) in ${cwd}
1398
+ `));
1399
+ let detected = detectTechStack(cwd);
1400
+ console.log(pc.bold("\u{1F50D} Tech Stack Auto-Detection:"));
1401
+ console.log(pc.cyan(` - Detected Stack: ${pc.bold(detected.name)}`));
1402
+ console.log(pc.cyan(` - Language: ${detected.language}`));
1403
+ if (detected.framework) {
1404
+ console.log(pc.cyan(` - Framework: ${detected.framework}`));
1405
+ }
1406
+ console.log(pc.cyan(` - Package Manager: ${detected.packageManager}`));
1407
+ if (detected.testFramework) {
1408
+ console.log(pc.cyan(` - Testing: ${detected.testFramework}`));
1409
+ }
1410
+ if (detected.commands.test) {
1411
+ console.log(pc.cyan(` - Test Command: ${detected.commands.test}`));
1412
+ }
1413
+ const isInteractive = options.interactive ?? (process.stdin.isTTY && !options.stack && !options.testCmd);
1414
+ if (isInteractive && process.stdin.isTTY) {
1415
+ console.log(pc.yellow("\n\u2699\uFE0F Configure project settings (press enter to accept defaults):"));
1416
+ const stackName = await promptQuestion("Tech Stack Name", detected.name);
1417
+ const pkgManager = await promptQuestion("Package Manager", detected.packageManager);
1418
+ const testCmd = await promptQuestion("Test Command", detected.commands.test || "npm test");
1419
+ const buildCmd = await promptQuestion("Build Command", detected.commands.build || "npm run build");
1420
+ detected = {
1421
+ ...detected,
1422
+ name: stackName,
1423
+ language: stackName,
1424
+ framework: void 0,
1425
+ packageManager: pkgManager,
1426
+ commands: {
1427
+ ...detected.commands,
1428
+ test: testCmd,
1429
+ build: buildCmd
1430
+ }
1431
+ };
1432
+ }
1433
+ if (options.stack) {
1434
+ detected.name = options.stack;
1435
+ detected.language = options.stack;
1436
+ detected.framework = void 0;
1437
+ }
1438
+ if (options.packageManager) {
1439
+ detected.packageManager = options.packageManager;
1440
+ }
1441
+ if (options.testCmd) {
1442
+ detected.commands.test = options.testCmd;
1443
+ }
1444
+ if (options.buildCmd) {
1445
+ detected.commands.build = options.buildCmd;
1446
+ }
1447
+ let manifest = loadManifest(cwd);
1448
+ if (manifest && !options.force) {
1449
+ console.log(pc.yellow(`
1450
+ \u26A0\uFE0F Found existing agents-manifest.json. Updating with preset '${preset}'...`));
1451
+ } else {
1452
+ manifest = createDefaultManifest(preset);
1453
+ }
1454
+ saveManifest(cwd, manifest);
1455
+ console.log(pc.green(`\u2713 Created/Updated agents-manifest.json`));
1456
+ const result = installFleet(cwd, manifest, { force: options.force, detectedStack: detected });
1457
+ console.log(pc.bold("\nInstalled components:"));
1458
+ if (result.promptsInstalled.length > 0) {
1459
+ console.log(pc.green(` \u{1F4C1} Prompts (.github/prompts/):`));
1460
+ result.promptsInstalled.forEach((p) => console.log(` - ${p}`));
1461
+ }
1462
+ if (result.workflowsInstalled.length > 0) {
1463
+ console.log(pc.green(` \u2699\uFE0F Workflows (.github/workflows/):`));
1464
+ result.workflowsInstalled.forEach((w) => console.log(` - ${w}`));
1465
+ }
1466
+ if (result.skillsInstalled.length > 0) {
1467
+ console.log(pc.green(` \u{1F9E0} Skills (.agents/skills/):`));
1468
+ result.skillsInstalled.forEach((s) => console.log(` - ${s}`));
1469
+ }
1470
+ if (result.docsInstalled.length > 0) {
1471
+ console.log(pc.green(` \u{1F4C4} Documentation:`));
1472
+ result.docsInstalled.forEach((d) => console.log(` - ${d}`));
1473
+ }
1474
+ if (options.pruneLabels) {
1475
+ try {
1476
+ console.log(pc.bold("\n\u{1F3F7}\uFE0F Pruning unused boilerplate labels..."));
1477
+ const pruneRes = await pruneLabels({ cwd, yes: true, dryRun: false, executor: options.executor });
1478
+ if (pruneRes.pruned.length > 0) {
1479
+ console.log(pc.green(` \u2713 Pruned ${pruneRes.pruned.length} unused boilerplate label(s): ${pruneRes.pruned.join(", ")}`));
1480
+ } else {
1481
+ console.log(pc.green(" \u2713 No unused boilerplate labels found."));
1482
+ }
1483
+ } catch (err) {
1484
+ console.log(pc.yellow(` \u26A0\uFE0F Could not prune labels: ${err.message}`));
1485
+ }
1486
+ }
1487
+ console.log(pc.bold(pc.green("\n\u{1F389} Jonah Fleet initialization complete!\n")));
1488
+ console.log(pc.cyan("Next steps for GitHub repository configuration:"));
1489
+ console.log(" 1. In Settings \u2192 Actions \u2192 General \u2192 Workflow permissions:");
1490
+ console.log(' Select "Read and write permissions" and check "Allow GitHub Actions to create and approve pull requests".');
1491
+ console.log(" 2. In Settings \u2192 Actions \u2192 General \u2192 Fork pull request workflows:");
1492
+ console.log(" Configure workflow approval settings to prevent automated runs from stalling awaiting approval.");
1493
+ console.log(" 3. Customize project context, build, and test commands in AGENTS.md.\n");
1494
+ }
1495
+
1496
+ // src/commands/sync.ts
1497
+ import pc2 from "picocolors";
1498
+
1499
+ // src/lib/diff.ts
1500
+ import fs5 from "fs";
1501
+ import path5 from "path";
1502
+ function checkDrift(targetDir, manifest) {
1503
+ const templatesDir = getTemplatesDir();
1504
+ const report = {
1505
+ missingPrompts: [],
1506
+ modifiedPrompts: [],
1507
+ missingWorkflows: [],
1508
+ modifiedWorkflows: [],
1509
+ missingSkills: []
1510
+ };
1511
+ const targetPromptsDir = path5.join(targetDir, ".github/prompts");
1512
+ const targetWorkflowsDir = path5.join(targetDir, ".github/workflows");
1513
+ const targetSkillsDir = path5.join(targetDir, ".agents/skills");
1514
+ const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
1515
+ for (const file of basePrompts) {
1516
+ const src = path5.join(templatesDir, "prompts", file);
1517
+ const dest = path5.join(targetPromptsDir, file);
1518
+ if (!fs5.existsSync(dest)) {
1519
+ report.missingPrompts.push(file);
1520
+ } else if (fs5.readFileSync(src, "utf8") !== fs5.readFileSync(dest, "utf8")) {
1521
+ report.modifiedPrompts.push(file);
1522
+ }
1523
+ }
1524
+ for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
1525
+ if (!isEnabled) continue;
1526
+ const promptFile = `${routineName}.md`;
1527
+ const promptSrc = path5.join(templatesDir, "prompts", promptFile);
1528
+ const promptDest = path5.join(targetPromptsDir, promptFile);
1529
+ if (!fs5.existsSync(promptDest)) {
1530
+ report.missingPrompts.push(promptFile);
1531
+ } else if (fs5.existsSync(promptSrc) && fs5.readFileSync(promptSrc, "utf8") !== fs5.readFileSync(promptDest, "utf8")) {
1532
+ report.modifiedPrompts.push(promptFile);
1533
+ }
1534
+ const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
1535
+ for (const workflowFile of workflows) {
1536
+ const wfSrc = path5.join(templatesDir, "workflows", workflowFile);
1537
+ const wfDest = path5.join(targetWorkflowsDir, workflowFile);
1538
+ if (!fs5.existsSync(wfDest)) {
1539
+ report.missingWorkflows.push(workflowFile);
1540
+ } else if (fs5.existsSync(wfSrc)) {
1541
+ const rawSrc = fs5.readFileSync(wfSrc, "utf8");
1542
+ const destContent = fs5.readFileSync(wfDest, "utf8");
1543
+ const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
1544
+ const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
1545
+ if (expectedSrc !== destContent) {
1546
+ report.modifiedWorkflows.push(workflowFile);
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1551
+ if (manifest.autoUpdate?.enabled) {
1552
+ const syncWfSrc = path5.join(templatesDir, "workflows/sync-fleet.yml");
1553
+ const syncWfDest = path5.join(targetWorkflowsDir, "sync-fleet.yml");
1554
+ if (!fs5.existsSync(syncWfDest)) {
1555
+ report.missingWorkflows.push("sync-fleet.yml");
1556
+ } else if (fs5.existsSync(syncWfSrc)) {
1557
+ const rawSrc = fs5.readFileSync(syncWfSrc, "utf8");
1558
+ const destContent = fs5.readFileSync(syncWfDest, "utf8");
1559
+ const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
1560
+ const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
1561
+ if (expectedSrc !== destContent) {
1562
+ report.modifiedWorkflows.push("sync-fleet.yml");
1563
+ }
1564
+ }
1565
+ }
1566
+ for (const skill of manifest.skills) {
1567
+ const skillDestDir = path5.join(targetSkillsDir, skill);
1568
+ if (!fs5.existsSync(skillDestDir)) {
1569
+ report.missingSkills.push(skill);
1570
+ }
1571
+ }
1572
+ return report;
1573
+ }
1574
+
1575
+ // src/commands/sync.ts
1576
+ async function runSync(options = {}) {
1577
+ const cwd = options.cwd || process.cwd();
1578
+ const manifest = loadManifest(cwd);
1579
+ if (!manifest) {
1580
+ console.error(pc2.red(`\u274C No agents-manifest.json found in ${cwd}. Run 'jonah-fleet init' first.`));
1581
+ process.exit(1);
1582
+ }
1583
+ console.log(pc2.cyan(`
1584
+ \u{1F504} Syncing Jonah Fleet (current: v${manifest.version}, fleet: v${FLEET_VERSION})...
1585
+ `));
1586
+ const drift = checkDrift(cwd, manifest);
1587
+ const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
1588
+ if (options.check) {
1589
+ if (!hasDrift && manifest.version === FLEET_VERSION) {
1590
+ console.log(pc2.green(`\u2713 All routines, workflows, and skills are perfectly in sync with v${FLEET_VERSION}.
1591
+ `));
1592
+ return;
1593
+ }
1594
+ console.log(pc2.yellow(`\u26A0\uFE0F Drift or updates detected:`));
1595
+ if (drift.missingPrompts.length > 0) console.log(pc2.red(` Missing prompts: ${drift.missingPrompts.join(", ")}`));
1596
+ if (drift.modifiedPrompts.length > 0) console.log(pc2.yellow(` Modified prompts: ${drift.modifiedPrompts.join(", ")}`));
1597
+ if (drift.missingWorkflows.length > 0) console.log(pc2.red(` Missing workflows: ${drift.missingWorkflows.join(", ")}`));
1598
+ if (drift.modifiedWorkflows.length > 0) console.log(pc2.yellow(` Modified workflows: ${drift.modifiedWorkflows.join(", ")}`));
1599
+ if (drift.missingSkills.length > 0) console.log(pc2.red(` Missing skills: ${drift.missingSkills.join(", ")}`));
1600
+ console.log(pc2.cyan(`
1601
+ Run 'jonah-fleet sync --force' to apply updates.
1602
+ `));
1603
+ return;
1604
+ }
1605
+ manifest.version = FLEET_VERSION;
1606
+ saveManifest(cwd, manifest);
1607
+ const result = installFleet(cwd, manifest, { force: true });
1608
+ console.log(pc2.green(`\u2713 Synchronized with Jonah Fleet v${FLEET_VERSION}`));
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();
1618
+ }
1619
+
1620
+ // src/commands/status.ts
1621
+ import fs7 from "fs";
1622
+ import path7 from "path";
1623
+ import pc5 from "picocolors";
1624
+
1625
+ // src/lib/dashboard.ts
1626
+ import pc3 from "picocolors";
1627
+ function formatTokens(num) {
1628
+ if (num >= 1e6) {
1629
+ return `${(num / 1e6).toFixed(2)}M`;
1630
+ }
1631
+ if (num >= 1e3) {
1632
+ return `${(num / 1e3).toFixed(1)}k`;
1633
+ }
1634
+ return num.toString();
1635
+ }
1636
+ function formatCurrency(amount) {
1637
+ return `$${amount.toFixed(2)}`;
1638
+ }
1639
+ function renderFleetDashboard(statuses, options = {}) {
1640
+ const summary = summarizeFleet(statuses);
1641
+ if (options.json) {
1271
1642
  return JSON.stringify(
1272
1643
  {
1273
1644
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1529,31 +1900,29 @@ async function runStatus(options = {}) {
1529
1900
  }
1530
1901
  const drift = checkDrift(cwd, manifest);
1531
1902
  const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
1532
- const logsDir = path7.join(cwd, ".github/prompts/logs");
1533
- let tokenUsage = void 0;
1534
- if (fs7.existsSync(logsDir)) {
1535
- const logContents = [];
1536
- const collectLogs = (dir) => {
1537
- const entries = fs7.readdirSync(dir, { withFileTypes: true });
1538
- for (const entry of entries) {
1539
- const fullPath = path7.join(dir, entry.name);
1540
- if (entry.isDirectory()) {
1541
- collectLogs(fullPath);
1542
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1543
- try {
1544
- logContents.push(fs7.readFileSync(fullPath, "utf8"));
1545
- } catch {
1546
- }
1903
+ const runsDir = path7.join(cwd, ".jonah-fleet/runs");
1904
+ const legacyLogsDir = path7.join(cwd, ".github/prompts/logs");
1905
+ let tokenUsage = void 0;
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 {
1547
1918
  }
1548
1919
  }
1549
- };
1550
- try {
1551
- collectLogs(logsDir);
1552
- } catch {
1553
- }
1554
- if (logContents.length > 0) {
1555
- tokenUsage = computeTokenSpendFromLogs(logContents);
1556
1920
  }
1921
+ };
1922
+ collectLogs(runsDir);
1923
+ collectLogs(legacyLogsDir);
1924
+ if (logContents.length > 0) {
1925
+ tokenUsage = computeTokenSpendFromLogs(logContents);
1557
1926
  }
1558
1927
  if (options.json) {
1559
1928
  console.log(
@@ -1566,6 +1935,8 @@ async function runStatus(options = {}) {
1566
1935
  autoUpdate: manifest.autoUpdate,
1567
1936
  routines: manifest.routines,
1568
1937
  skills: manifest.skills,
1938
+ models: manifest.models,
1939
+ budgets: manifest.budgets,
1569
1940
  repositories: manifest.repositories || [],
1570
1941
  tokenUsage,
1571
1942
  drift: {
@@ -1593,6 +1964,36 @@ async function runStatus(options = {}) {
1593
1964
  for (const skill of manifest.skills) {
1594
1965
  console.log(` - ${pc5.cyan(skill)}`);
1595
1966
  }
1967
+ if (manifest.models && Object.keys(manifest.models).length > 0) {
1968
+ console.log(pc5.bold("\n Model Profiles:"));
1969
+ for (const [routine, model] of Object.entries(manifest.models)) {
1970
+ if (model) {
1971
+ console.log(` - ${routine.padEnd(35)}: ${pc5.cyan(model)}`);
1972
+ }
1973
+ }
1974
+ }
1975
+ if (manifest.budgets) {
1976
+ console.log(pc5.bold("\n Configured Budgets:"));
1977
+ if (manifest.budgets.weeklyTokens) {
1978
+ console.log(` - Weekly Token Budget: ${pc5.cyan(formatTokens(manifest.budgets.weeklyTokens))}`);
1979
+ }
1980
+ if (manifest.budgets.timeoutMinutes && Object.keys(manifest.budgets.timeoutMinutes).length > 0) {
1981
+ console.log(` - Timeouts:`);
1982
+ for (const [routine, timeout] of Object.entries(manifest.budgets.timeoutMinutes)) {
1983
+ if (timeout !== void 0) {
1984
+ console.log(` \u2022 ${routine}: ${pc5.cyan(timeout + "m")}`);
1985
+ }
1986
+ }
1987
+ }
1988
+ if (manifest.budgets.maxIterations && Object.keys(manifest.budgets.maxIterations).length > 0) {
1989
+ console.log(` - Max Iterations:`);
1990
+ for (const [routine, iter] of Object.entries(manifest.budgets.maxIterations)) {
1991
+ if (iter !== void 0) {
1992
+ console.log(` \u2022 ${routine}: ${pc5.cyan(String(iter))}`);
1993
+ }
1994
+ }
1995
+ }
1996
+ }
1596
1997
  if (manifest.repositories && manifest.repositories.length > 0) {
1597
1998
  console.log(pc5.bold("\n Fleet Repositories:"));
1598
1999
  for (const repo of manifest.repositories) {
@@ -1973,9 +2374,10 @@ async function emitTelemetry(summary, endpoint, customFetch = globalThis.fetch)
1973
2374
  }
1974
2375
  function collectLocalTelemetryLogs(dir, repositoryName = "local") {
1975
2376
  const summaries = [];
1976
- const logsDir = path8.join(dir, ".github/prompts/logs");
1977
- if (!fs8.existsSync(logsDir)) return summaries;
2377
+ const runsDir = path8.join(dir, ".jonah-fleet/runs");
2378
+ const legacyLogsDir = path8.join(dir, ".github/prompts/logs");
1978
2379
  const traverse = (currentDir) => {
2380
+ if (!fs8.existsSync(currentDir)) return;
1979
2381
  const entries = fs8.readdirSync(currentDir, { withFileTypes: true });
1980
2382
  for (const entry of entries) {
1981
2383
  const fullPath = path8.join(currentDir, entry.name);
@@ -1991,7 +2393,12 @@ function collectLocalTelemetryLogs(dir, repositoryName = "local") {
1991
2393
  }
1992
2394
  }
1993
2395
  };
1994
- traverse(logsDir);
2396
+ if (fs8.existsSync(runsDir)) {
2397
+ traverse(runsDir);
2398
+ }
2399
+ if (fs8.existsSync(legacyLogsDir)) {
2400
+ traverse(legacyLogsDir);
2401
+ }
1995
2402
  return summaries;
1996
2403
  }
1997
2404
  async function collectRepoTelemetry(repoIdentifier, executor = defaultGhExecutor, options = {}) {
@@ -1999,6 +2406,35 @@ async function collectRepoTelemetry(repoIdentifier, executor = defaultGhExecutor
1999
2406
  if (fs8.existsSync(repoIdentifier) && fs8.statSync(repoIdentifier).isDirectory()) {
2000
2407
  return collectLocalTelemetryLogs(repoIdentifier, repoIdentifier);
2001
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
+ }
2002
2438
  try {
2003
2439
  const treeRaw = await executor([
2004
2440
  "api",
@@ -2117,11 +2553,16 @@ async function runTelemetry(options = {}) {
2117
2553
  if (options.action === "emit" || options.log) {
2118
2554
  let logPath = options.log;
2119
2555
  if (!logPath) {
2120
- const logsDir = path9.join(cwd, ".github/prompts/logs");
2121
- 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");
2122
2562
  let latestFile = null;
2123
2563
  let latestMtime = 0;
2124
2564
  const findLogs = (dir) => {
2565
+ if (!fs9.existsSync(dir)) return;
2125
2566
  const entries = fs9.readdirSync(dir, { withFileTypes: true });
2126
2567
  for (const entry of entries) {
2127
2568
  const p = path9.join(dir, entry.name);
@@ -2135,7 +2576,8 @@ async function runTelemetry(options = {}) {
2135
2576
  }
2136
2577
  }
2137
2578
  };
2138
- findLogs(logsDir);
2579
+ findLogs(runsDir);
2580
+ findLogs(legacyLogsDir);
2139
2581
  logPath = latestFile || void 0;
2140
2582
  }
2141
2583
  }
@@ -2208,7 +2650,7 @@ async function runTelemetry(options = {}) {
2208
2650
  }
2209
2651
 
2210
2652
  // src/commands/run.ts
2211
- import pc10 from "picocolors";
2653
+ import pc11 from "picocolors";
2212
2654
 
2213
2655
  // src/lib/runner.ts
2214
2656
  import fs12 from "fs";
@@ -2340,11 +2782,46 @@ async function cleanupStaleWorktrees(repoRoot) {
2340
2782
  // src/lib/terminal-card.ts
2341
2783
  import fs11 from "fs";
2342
2784
  import path11 from "path";
2343
- import { execFileSync } from "child_process";
2785
+ import { execFileSync, execFile as execFile3 } from "child_process";
2786
+ import { promisify as promisify3 } from "util";
2344
2787
  import pc9 from "picocolors";
2788
+ var execFileAsync3 = promisify3(execFile3);
2345
2789
  function stripAnsi(text) {
2346
2790
  return text.replace(/\x1b\[[0-9;]*m/g, "");
2347
2791
  }
2792
+ function truncateAnsi(text, maxWidth) {
2793
+ if (maxWidth <= 0) return "";
2794
+ if (stripAnsi(text).length <= maxWidth) return text;
2795
+ let visibleCount = 0;
2796
+ let result = "";
2797
+ let inAnsi = false;
2798
+ let ansiBuffer = "";
2799
+ for (let i = 0; i < text.length; i++) {
2800
+ const char = text[i];
2801
+ if (char === "\x1B") {
2802
+ inAnsi = true;
2803
+ ansiBuffer = char;
2804
+ continue;
2805
+ }
2806
+ if (inAnsi) {
2807
+ ansiBuffer += char;
2808
+ if (char === "m") {
2809
+ inAnsi = false;
2810
+ result += ansiBuffer;
2811
+ ansiBuffer = "";
2812
+ }
2813
+ continue;
2814
+ }
2815
+ if (visibleCount < maxWidth) {
2816
+ result += char;
2817
+ visibleCount++;
2818
+ } else {
2819
+ break;
2820
+ }
2821
+ }
2822
+ result += "\x1B[0m";
2823
+ return result;
2824
+ }
2348
2825
  function wrapText(text, maxWidth) {
2349
2826
  if (maxWidth <= 0) return [text];
2350
2827
  const words = text.split(/\s+/).filter(Boolean);
@@ -2394,6 +2871,31 @@ function fetchTargetTitle(repoRoot, target) {
2394
2871
  }
2395
2872
  return null;
2396
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
+ }
2397
2899
  function sanitizeWorktreePaths(text) {
2398
2900
  let cleaned = text.replace(/file:\/\/\/[^\s"'()]+?\/\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, "");
2399
2901
  cleaned = cleaned.replace(/(?:^|[\s"'(`[])(?:\/[^\s"'()]+?)?\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, (match) => {
@@ -2428,16 +2930,29 @@ function extractExecutionSummary(output) {
2428
2930
  return sanitizeWorktreePaths(summary);
2429
2931
  }
2430
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
+ }
2431
2944
  const logsDir = path11.join(repoRoot, ".github", "prompts", "logs", routine);
2432
- if (!fs11.existsSync(logsDir)) return null;
2433
- try {
2434
- const files = fs11.readdirSync(logsDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
2435
- if (files.length === 0) return null;
2436
- files.sort().reverse();
2437
- return path11.join(logsDir, files[0]);
2438
- } catch {
2439
- 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
+ }
2440
2954
  }
2955
+ return null;
2441
2956
  }
2442
2957
  function parseRunLog(logContent) {
2443
2958
  const summary = {
@@ -2562,14 +3077,130 @@ function detectClaimedIssue(chunk) {
2562
3077
  function detectClaimedPR(chunk) {
2563
3078
  const findingMatch = chunk.match(/(?:addressing\s+review\s+findings|fixing\s+review\s+findings)[^\n#]*?#(\d+)/i);
2564
3079
  if (findingMatch) return `PR #${findingMatch[1]}`;
2565
- 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);
2566
3081
  if (reviewMatch) return `PR #${reviewMatch[1]}`;
2567
3082
  const prMatch = chunk.match(/(?:selected|target|reviewing)\s+(?:target\s+)?PR:?\s*\[?PR\s*#?(\d+)/i);
2568
3083
  if (prMatch) return `PR #${prMatch[1]}`;
2569
- 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);
2570
3085
  if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
2571
3086
  return null;
2572
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
+ }
3112
+ function formatActionDescription(toolName, params) {
3113
+ const name = toolName || "unknown";
3114
+ if (name === "run_command") {
3115
+ const rawCmd = (params?.CommandLine || params?.command || params?.cmd || "").trim();
3116
+ if (!rawCmd) return "Running command";
3117
+ if (/\b(?:vitest|jest)\b/i.test(rawCmd)) return "Running vitest";
3118
+ if (/\bnpm\s+test\b|\bcargo\s+test\b|\bpytest\b/i.test(rawCmd)) return "Running test suite";
3119
+ if (/type-check|\btsc\b/i.test(rawCmd)) return "Running TypeScript type checks";
3120
+ if (/\blint\b|\beslint\b/i.test(rawCmd)) return "Running codebase linter";
3121
+ if (/\bbuild\b|\btsup\b|\bnext\s+build\b/i.test(rawCmd)) return "Running production build";
3122
+ if (/gh\s+pr\s+list/i.test(rawCmd)) return "Listing open PRs (gh pr list)";
3123
+ const prMergeMatch = rawCmd.match(/gh\s+pr\s+merge(?:\s+(\d+))?/i);
3124
+ if (prMergeMatch) {
3125
+ return prMergeMatch[1] ? `Squash-merging PR #${prMergeMatch[1]}` : "Squash-merging pull request";
3126
+ }
3127
+ const prViewMatch = rawCmd.match(/gh\s+pr\s+view(?:\s+(\d+))?/i);
3128
+ if (prViewMatch) {
3129
+ return prViewMatch[1] ? `Viewing PR #${prViewMatch[1]}` : "Viewing pull request";
3130
+ }
3131
+ const prEditMatch = rawCmd.match(/gh\s+pr\s+edit(?:\s+(\d+))?/i);
3132
+ if (prEditMatch) {
3133
+ return prEditMatch[1] ? `Updating PR #${prEditMatch[1]}` : "Updating pull request";
3134
+ }
3135
+ const prReadyMatch = rawCmd.match(/gh\s+pr\s+ready(?:\s+(\d+))?/i);
3136
+ if (prReadyMatch) {
3137
+ return prReadyMatch[1] ? `Marking PR #${prReadyMatch[1]} ready for review` : "Marking PR ready for review";
3138
+ }
3139
+ if (/gh\s+pr\s+create/i.test(rawCmd)) return "Creating pull request";
3140
+ if (/gh\s+issue\s+list/i.test(rawCmd)) return "Listing open issues (gh issue list)";
3141
+ const issueViewMatch = rawCmd.match(/gh\s+issue\s+view(?:\s+(\d+))?/i);
3142
+ if (issueViewMatch) {
3143
+ return issueViewMatch[1] ? `Viewing issue #${issueViewMatch[1]}` : "Viewing issue";
3144
+ }
3145
+ const issueEditMatch = rawCmd.match(/gh\s+issue\s+edit(?:\s+(\d+))?/i);
3146
+ if (issueEditMatch) {
3147
+ return issueEditMatch[1] ? `Updating issue #${issueEditMatch[1]}` : "Updating issue";
3148
+ }
3149
+ const issueCommentMatch = rawCmd.match(/gh\s+issue\s+comment(?:\s+(\d+))?/i);
3150
+ if (issueCommentMatch) {
3151
+ return issueCommentMatch[1] ? `Commenting on issue #${issueCommentMatch[1]}` : "Commenting on issue";
3152
+ }
3153
+ if (/git\s+checkout/i.test(rawCmd)) return "Git: Checking out branch";
3154
+ if (/git\s+status/i.test(rawCmd)) return "Git: Checking status";
3155
+ if (/git\s+diff/i.test(rawCmd)) return "Git: Inspecting diff";
3156
+ if (/git\s+commit/i.test(rawCmd)) return "Git: Committing changes";
3157
+ if (/git\s+push/i.test(rawCmd)) return "Git: Pushing branch";
3158
+ const firstLine = rawCmd.split("\n")[0].trim();
3159
+ return `Running ${firstLine}`;
3160
+ }
3161
+ if (name === "view_file") {
3162
+ const rawPath = params?.AbsolutePath || params?.TargetFile || params?.path || params?.file || "";
3163
+ if (!rawPath) return "Reading file";
3164
+ return `Reading ${path11.basename(rawPath)}`;
3165
+ }
3166
+ if (name === "replace_file_content" || name === "write_to_file" || name === "multi_replace_file_content") {
3167
+ const rawPath = params?.TargetFile || params?.AbsolutePath || params?.path || params?.file || "";
3168
+ if (!rawPath) return "Editing file";
3169
+ return `Editing ${path11.basename(rawPath)}`;
3170
+ }
3171
+ if (name === "grep_search") {
3172
+ const query = params?.Query || params?.query || params?.pattern || "";
3173
+ if (!query) return "Searching codebase";
3174
+ return `Searching codebase for "${query}"`;
3175
+ }
3176
+ if (name === "find_by_name") {
3177
+ const pattern = params?.Pattern || params?.pattern || "";
3178
+ if (!pattern) return "Finding files";
3179
+ return `Finding files matching "${pattern}"`;
3180
+ }
3181
+ if (name === "list_dir") {
3182
+ const dirPath = params?.DirectoryPath || params?.path || "";
3183
+ if (!dirPath) return "Listing directory";
3184
+ const base = path11.basename(dirPath.replace(/[/\\]+$/, "")) || dirPath;
3185
+ return `Listing directory ${base}`;
3186
+ }
3187
+ if (name === "invoke_subagent") {
3188
+ const role = params?.Subagents?.[0]?.Role || params?.Subagents?.[0]?.role || params?.Role || params?.role || params?.TypeName || params?.name || "";
3189
+ if (!role) return "Running subagent";
3190
+ return `Running subagent: ${role}`;
3191
+ }
3192
+ if (name === "search_web") {
3193
+ const query = params?.query || params?.Query || "";
3194
+ if (!query) return "Searching web";
3195
+ return `Searching web for "${query}"`;
3196
+ }
3197
+ if (name === "read_url_content") {
3198
+ const url = params?.Url || params?.url || "";
3199
+ if (!url) return "Reading URL content";
3200
+ return `Reading URL ${url}`;
3201
+ }
3202
+ return `Tool: ${name}`;
3203
+ }
2573
3204
  function renderSummaryCard(options) {
2574
3205
  const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 64), 90);
2575
3206
  const horizontal = "\u2500".repeat(width - 2);
@@ -2828,15 +3459,27 @@ var TerminalSpinner = class {
2828
3459
  `);
2829
3460
  }
2830
3461
  }
2831
- render() {
2832
- if (!this.isRunning || !this.isTTY) return;
3462
+ formatLine(message, maxWidth) {
3463
+ const cols = maxWidth ?? (process.stderr.columns || process.stdout.columns || 80);
2833
3464
  const frame = pc9.cyan(this.frames[this.currentFrame]);
2834
- this.currentFrame = (this.currentFrame + 1) % this.frames.length;
2835
3465
  const elapsedSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
2836
3466
  const mins = Math.floor(elapsedSeconds / 60);
2837
3467
  const secs = elapsedSeconds % 60;
2838
- const timeStr = pc9.dim(`[${mins}m ${secs < 10 ? "0" : ""}${secs}s]`);
2839
- process.stderr.write(`\r\x1B[K ${frame} ${this.message} ${timeStr}`);
3468
+ const timePlain = `[${mins}m ${secs < 10 ? "0" : ""}${secs}s]`;
3469
+ const timeStr = pc9.dim(timePlain);
3470
+ const fixedWidth = 6 + timePlain.length;
3471
+ const availableMsgWidth = Math.max(0, cols - fixedWidth - 1);
3472
+ let truncatedMsg = message;
3473
+ if (stripAnsi(message).length > availableMsgWidth) {
3474
+ truncatedMsg = availableMsgWidth > 3 ? truncateAnsi(message, availableMsgWidth - 3) + "..." : truncateAnsi(message, availableMsgWidth);
3475
+ }
3476
+ return ` ${frame} ${truncatedMsg} ${timeStr}`;
3477
+ }
3478
+ render() {
3479
+ if (!this.isRunning || !this.isTTY) return;
3480
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
3481
+ const line = this.formatLine(this.message);
3482
+ process.stderr.write(`\r\x1B[K${line}`);
2840
3483
  }
2841
3484
  stop() {
2842
3485
  if (!this.isRunning) return;
@@ -2852,6 +3495,97 @@ var TerminalSpinner = class {
2852
3495
  };
2853
3496
 
2854
3497
  // src/lib/runner.ts
3498
+ import pc10 from "picocolors";
3499
+ var LineBufferedStreamParser = class {
3500
+ buffer = "";
3501
+ onLine;
3502
+ constructor(onLine) {
3503
+ this.onLine = onLine;
3504
+ }
3505
+ feed(chunk) {
3506
+ this.buffer += chunk;
3507
+ const lines = this.buffer.split("\n");
3508
+ this.buffer = lines.pop() ?? "";
3509
+ for (const line of lines) {
3510
+ const trimmed = line.trim();
3511
+ if (trimmed.length > 0) {
3512
+ this.onLine(trimmed);
3513
+ }
3514
+ }
3515
+ }
3516
+ flush() {
3517
+ if (this.buffer.trim().length > 0) {
3518
+ this.onLine(this.buffer.trim());
3519
+ this.buffer = "";
3520
+ }
3521
+ }
3522
+ };
3523
+ function parseStreamJsonEvent(line) {
3524
+ if (!line || !line.trim()) return null;
3525
+ try {
3526
+ const parsed = JSON.parse(line);
3527
+ if (parsed && typeof parsed === "object") {
3528
+ return parsed;
3529
+ }
3530
+ return null;
3531
+ } catch {
3532
+ return null;
3533
+ }
3534
+ }
3535
+ function formatVerboseEvent(event) {
3536
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
3537
+ if (event.event === "init") {
3538
+ return `${pc10.dim(`[${time}]`)} ${pc10.cyan("[init]")} Session started (conversation: ${event.conversation_id || "n/a"})`;
3539
+ }
3540
+ if (event.event === "step_update" && event.step_update) {
3541
+ const su = event.step_update;
3542
+ if (su.step_type === "user_input") {
3543
+ return `${pc10.dim(`[${time}]`)} ${pc10.magenta("[user_input]")} Prompt dispatched`;
3544
+ }
3545
+ if (su.step_type === "tool") {
3546
+ const toolName = su.tool_name || su.tool_info?.name || "tool";
3547
+ const params = su.tool_info?.parameters;
3548
+ if (su.state === "ACTIVE") {
3549
+ const desc = formatActionDescription(toolName, params);
3550
+ return `${pc10.dim(`[${time}]`)} ${pc10.blue("[tool:start]")} ${pc10.bold(toolName)} \u2192 ${desc}`;
3551
+ }
3552
+ if (su.state === "DONE") {
3553
+ const dur = su.duration_seconds !== void 0 ? `${su.duration_seconds.toFixed(1)}s` : "done";
3554
+ return `${pc10.dim(`[${time}]`)} ${pc10.green("[tool:done]")} ${pc10.bold(toolName)} (${dur})`;
3555
+ }
3556
+ }
3557
+ if (su.step_type === "agent_response" || su.step_type === "thought") {
3558
+ if (su.text_delta) {
3559
+ return su.text_delta;
3560
+ }
3561
+ if (su.state === "DONE") {
3562
+ const dur = su.duration_seconds !== void 0 ? ` (${su.duration_seconds.toFixed(1)}s)` : "";
3563
+ return `${pc10.dim(`[${time}]`)} ${pc10.cyan("[agent:step]")} Step ${su.step_index ?? 0} finished${dur}`;
3564
+ }
3565
+ }
3566
+ }
3567
+ if (event.event === "result" && event.result) {
3568
+ const res = event.result;
3569
+ const dur = res.duration_seconds !== void 0 ? `${res.duration_seconds.toFixed(1)}s` : "";
3570
+ const tokens = res.usage?.total_tokens ? `${res.usage.total_tokens.toLocaleString()} tokens` : "";
3571
+ const metrics = [dur, tokens].filter(Boolean).join(", ");
3572
+ return `${pc10.dim(`[${time}]`)} ${pc10.bold(pc10.green("[result]"))} ${res.status || "COMPLETED"} (${metrics || "done"})`;
3573
+ }
3574
+ return null;
3575
+ }
3576
+ function buildAgyArgs(prompt, model, printTimeout) {
3577
+ return [
3578
+ "-p",
3579
+ prompt,
3580
+ "--model",
3581
+ model,
3582
+ "--output-format",
3583
+ "stream-json",
3584
+ "--print-timeout",
3585
+ printTimeout,
3586
+ "--dangerously-skip-permissions"
3587
+ ];
3588
+ }
2855
3589
  function discoverSkillsPrompt(targetDir) {
2856
3590
  const skillsDir = path12.join(targetDir, ".agents", "skills");
2857
3591
  if (!fs12.existsSync(skillsDir)) return "";
@@ -2874,24 +3608,144 @@ function discoverSkillsPrompt(targetDir) {
2874
3608
  function buildRoutinePrompt(targetDir, routine, options = {}) {
2875
3609
  const skillsPrompt = discoverSkillsPrompt(targetDir);
2876
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}.` : "";
2877
3613
  if (routine === "autowork") {
2878
3614
  if (options.issue) {
2879
- 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}`;
2880
3616
  }
2881
- 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}`;
2882
3618
  }
2883
3619
  if (routine === "peer-review") {
2884
3620
  if (options.pr) {
2885
- 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
+ }
3676
+ }
3677
+ } catch {
3678
+ }
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
+ }
2886
3740
  }
2887
- 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.`;
3741
+ } catch {
2888
3742
  }
2889
- return `You are the ${routine} routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}`;
3743
+ return null;
2890
3744
  }
2891
3745
  async function runLocalRoutine(options) {
2892
3746
  const targetDir = path12.resolve(options.targetDir);
2893
3747
  const routine = options.routine;
2894
- const model = options.model || "gemini-3.7-flash-high";
3748
+ const model = options.model || "gemini-3.8-flash-high";
2895
3749
  const printTimeout = options.printTimeout || "30m";
2896
3750
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2897
3751
  const hostname = os2.hostname();
@@ -2912,9 +3766,18 @@ async function runLocalRoutine(options) {
2912
3766
  worktreePath = worktreeResult.worktreePath;
2913
3767
  executionDir = worktreePath;
2914
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
+ }
2915
3777
  const prompt = buildRoutinePrompt(executionDir, routine, {
2916
3778
  issue: options.issue,
2917
- pr: options.pr
3779
+ pr: options.pr,
3780
+ routineIssueNumber
2918
3781
  });
2919
3782
  if (options.dryRun) {
2920
3783
  return {
@@ -2936,30 +3799,53 @@ Timeout: ${printTimeout}`,
2936
3799
  TARGET_ISSUE: options.issue ? String(options.issue) : "",
2937
3800
  PR_NUMBER: options.pr ? String(options.pr) : ""
2938
3801
  };
2939
- const args = [
2940
- "-p",
2941
- prompt,
2942
- "--model",
2943
- model,
2944
- "--output-format",
2945
- "text",
2946
- "--print-timeout",
2947
- printTimeout,
2948
- "--dangerously-skip-permissions"
2949
- ];
3802
+ if (routineIssueNumber) {
3803
+ childEnv.ROUTINE_ISSUE_NUMBER = String(routineIssueNumber);
3804
+ }
3805
+ const args = buildAgyArgs(prompt, model, printTimeout);
2950
3806
  let output = "";
3807
+ let finalResponseText = "";
3808
+ let accumulatedOutput = "";
2951
3809
  let exitCode = 0;
2952
3810
  const startTime = Date.now();
2953
3811
  const logDir = path12.join(targetDir, ".jonah-fleet");
2954
3812
  fs12.mkdirSync(logDir, { recursive: true });
2955
3813
  const logFilePath = path12.join(logDir, "daemon.log");
2956
- let targetLabel = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : routine;
2957
- 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
+ }
2958
3830
  let activePhase = "Starting session...";
3831
+ let lastActionDesc = null;
2959
3832
  const spinner = !options.verbose ? new TerminalSpinner() : null;
2960
3833
  if (spinner) {
2961
3834
  spinner.start(`${targetLabel}: ${activePhase}`);
2962
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
+ }
2963
3849
  const cleanup = async () => {
2964
3850
  spinner?.stop();
2965
3851
  if (worktreePath && !options.keepWorktree) {
@@ -2973,39 +3859,143 @@ Timeout: ${printTimeout}`,
2973
3859
  };
2974
3860
  process.once("SIGINT", sigintHandler);
2975
3861
  process.once("SIGTERM", sigintHandler);
2976
- const processChunk = (chunk, isStderr = false) => {
2977
- output += chunk;
2978
- try {
2979
- fs12.appendFileSync(logFilePath, chunk, "utf8");
2980
- } catch {
3862
+ const checkTargetDetection = (text) => {
3863
+ if (dynamicTargetDetected || !text) return;
3864
+ const detected = routine === "peer-review" ? detectClaimedPR(text) : detectClaimedIssue(text);
3865
+ if (detected) {
3866
+ dynamicTargetDetected = true;
3867
+ targetLabel = detected;
3868
+ options.onTargetDetected?.(detected);
3869
+ if (spinner) {
3870
+ spinner.update(`${targetLabel}: ${lastActionDesc || activePhase}`);
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
+ });
2981
3883
  }
2982
- if (!dynamicTargetDetected) {
2983
- const detected = routine === "peer-review" ? detectClaimedPR(chunk) : detectClaimedIssue(chunk);
2984
- if (detected) {
2985
- dynamicTargetDetected = true;
2986
- targetLabel = detected;
2987
- options.onTargetDetected?.(detected);
2988
- if (spinner) {
3884
+ };
3885
+ const stdoutParser = new LineBufferedStreamParser((line) => {
3886
+ const event = parseStreamJsonEvent(line);
3887
+ if (event) {
3888
+ if (event.event === "step_update" && event.step_update) {
3889
+ const su = event.step_update;
3890
+ if (su.step_type === "tool") {
3891
+ const toolName = su.tool_name || su.tool_info?.name || "unknown";
3892
+ const toolParams = su.tool_info?.parameters;
3893
+ if (su.state === "ACTIVE") {
3894
+ const actionDesc = formatActionDescription(toolName, toolParams);
3895
+ lastActionDesc = actionDesc;
3896
+ if (spinner) {
3897
+ spinner.update(`${targetLabel}: ${actionDesc}`);
3898
+ }
3899
+ if (toolParams?.CommandLine) {
3900
+ checkTargetDetection(toolParams.CommandLine);
3901
+ }
3902
+ if (options.verbose) {
3903
+ const formatted = formatVerboseEvent(event);
3904
+ if (formatted) console.log(formatted);
3905
+ }
3906
+ } else if (su.state === "DONE") {
3907
+ lastActionDesc = null;
3908
+ if (su.tool_info?.output) {
3909
+ checkTargetDetection(su.tool_info.output);
3910
+ }
3911
+ if (spinner) {
3912
+ activePhase = "Evaluating tool output...";
3913
+ spinner.update(`${targetLabel}: ${activePhase}`);
3914
+ }
3915
+ if (options.verbose) {
3916
+ const formatted = formatVerboseEvent(event);
3917
+ if (formatted) console.log(formatted);
3918
+ }
3919
+ }
3920
+ } else if (su.step_type === "agent_response" || su.step_type === "thought") {
3921
+ if (su.text_delta) {
3922
+ accumulatedOutput += su.text_delta;
3923
+ checkTargetDetection(su.text_delta);
3924
+ const newPhase = detectActivePhase(su.text_delta, activePhase);
3925
+ if (newPhase !== activePhase || lastActionDesc) {
3926
+ lastActionDesc = null;
3927
+ activePhase = newPhase;
3928
+ if (spinner) {
3929
+ spinner.update(`${targetLabel}: ${activePhase}`);
3930
+ }
3931
+ }
3932
+ if (options.verbose) {
3933
+ process.stdout.write(su.text_delta);
3934
+ }
3935
+ } else if (options.verbose && su.state === "DONE") {
3936
+ const formatted = formatVerboseEvent(event);
3937
+ if (formatted) console.log(formatted);
3938
+ }
3939
+ } else if (options.verbose) {
3940
+ const formatted = formatVerboseEvent(event);
3941
+ if (formatted) console.log(formatted);
3942
+ }
3943
+ } else if (event.event === "result" && event.result) {
3944
+ if (event.result.response) {
3945
+ finalResponseText = event.result.response;
3946
+ checkTargetDetection(event.result.response);
3947
+ }
3948
+ if (options.verbose) {
3949
+ const formatted = formatVerboseEvent(event);
3950
+ if (formatted) console.log(formatted);
3951
+ }
3952
+ } else if (event.event === "init") {
3953
+ if (options.verbose) {
3954
+ const formatted = formatVerboseEvent(event);
3955
+ if (formatted) console.log(formatted);
3956
+ }
3957
+ }
3958
+ } else {
3959
+ accumulatedOutput += line + "\n";
3960
+ checkTargetDetection(line);
3961
+ if (options.verbose) {
3962
+ console.log(line);
3963
+ } else if (spinner) {
3964
+ lastActionDesc = null;
3965
+ const newPhase = detectActivePhase(line, activePhase);
3966
+ if (newPhase !== activePhase) {
3967
+ activePhase = newPhase;
2989
3968
  spinner.update(`${targetLabel}: ${activePhase}`);
2990
3969
  }
2991
3970
  }
2992
3971
  }
2993
- if (options.onLog) {
2994
- options.onLog(chunk);
2995
- }
3972
+ });
3973
+ const stderrParser = new LineBufferedStreamParser((line) => {
3974
+ checkTargetDetection(line);
2996
3975
  if (options.verbose) {
2997
- if (isStderr) {
2998
- process.stderr.write(chunk);
2999
- } else {
3000
- process.stdout.write(chunk);
3001
- }
3976
+ console.error(pc10.dim(`[stderr] ${line}`));
3002
3977
  } else if (spinner) {
3003
- const newPhase = detectActivePhase(chunk, activePhase);
3978
+ lastActionDesc = null;
3979
+ const newPhase = detectActivePhase(line, activePhase);
3004
3980
  if (newPhase !== activePhase) {
3005
3981
  activePhase = newPhase;
3006
3982
  spinner.update(`${targetLabel}: ${activePhase}`);
3007
3983
  }
3008
3984
  }
3985
+ });
3986
+ const processChunk = (chunk, isStderr = false) => {
3987
+ try {
3988
+ fs12.appendFileSync(logFilePath, chunk, "utf8");
3989
+ } catch {
3990
+ }
3991
+ if (options.onLog) {
3992
+ options.onLog(chunk);
3993
+ }
3994
+ if (isStderr) {
3995
+ stderrParser.feed(chunk);
3996
+ } else {
3997
+ stdoutParser.feed(chunk);
3998
+ }
3009
3999
  };
3010
4000
  try {
3011
4001
  exitCode = await new Promise((resolve, reject) => {
@@ -3037,10 +4027,71 @@ Timeout: ${printTimeout}`,
3037
4027
  await cleanup();
3038
4028
  }
3039
4029
  }
4030
+ stdoutParser.flush();
4031
+ stderrParser.flush();
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
+ }
3040
4090
  if (options.showCard !== false && !options.verbose) {
3041
- const durationMs = Date.now() - startTime;
3042
- const effectiveIssue = options.issue || (targetLabel.startsWith("Issue #") ? targetLabel.replace("Issue #", "") : void 0);
3043
- 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);
3044
4095
  if (exitCode === 0) {
3045
4096
  console.log(
3046
4097
  "\n" + renderSummaryCard({
@@ -3049,6 +4100,7 @@ Timeout: ${printTimeout}`,
3049
4100
  repoRoot: targetDir,
3050
4101
  issue: effectiveIssue,
3051
4102
  pr: effectivePR,
4103
+ title: targetTitle,
3052
4104
  durationMs
3053
4105
  }) + "\n"
3054
4106
  );
@@ -3070,7 +4122,8 @@ Timeout: ${printTimeout}`,
3070
4122
  exitCode,
3071
4123
  output,
3072
4124
  worktreePath,
3073
- branchName
4125
+ branchName,
4126
+ issueNumber: routineIssueNumber
3074
4127
  };
3075
4128
  }
3076
4129
 
@@ -3080,31 +4133,31 @@ async function runRoutineCommand(routine, options = {}) {
3080
4133
  const manifest = loadManifest(cwd);
3081
4134
  if (!manifest) {
3082
4135
  console.warn(
3083
- pc10.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
4136
+ pc11.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
3084
4137
  );
3085
4138
  } else if (manifest.routines && manifest.routines[routine] === false) {
3086
4139
  console.warn(
3087
- pc10.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
4140
+ pc11.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
3088
4141
  );
3089
4142
  }
3090
- console.log(pc10.cyan(`
3091
- \u{1F680} Launching local agent session for routine: ${pc10.bold(routine)}`));
4143
+ console.log(pc11.cyan(`
4144
+ \u{1F680} Launching local agent session for routine: ${pc11.bold(routine)}`));
3092
4145
  if (options.issue) {
3093
- console.log(pc10.dim(` Target issue: #${options.issue}`));
4146
+ console.log(pc11.dim(` Target issue: #${options.issue}`));
3094
4147
  }
3095
4148
  if (options.pr) {
3096
- console.log(pc10.dim(` Target pull request: #${options.pr}`));
4149
+ console.log(pc11.dim(` Target pull request: #${options.pr}`));
3097
4150
  }
3098
4151
  if (options.model) {
3099
- console.log(pc10.dim(` Model override: ${options.model}`));
4152
+ console.log(pc11.dim(` Model override: ${options.model}`));
3100
4153
  }
3101
4154
  if (options.verbose) {
3102
- console.log(pc10.dim(` Verbose output: Enabled (streaming raw tokens)`));
4155
+ console.log(pc11.dim(` Verbose output: Enabled (streaming raw tokens)`));
3103
4156
  }
3104
4157
  if (options.worktree !== false) {
3105
- console.log(pc10.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
4158
+ console.log(pc11.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
3106
4159
  } else {
3107
- console.log(pc10.yellow(` Workspace isolation: Disabled (running in current directory)`));
4160
+ console.log(pc11.yellow(` Workspace isolation: Disabled (running in current directory)`));
3108
4161
  }
3109
4162
  console.log("");
3110
4163
  try {
@@ -3121,56 +4174,443 @@ async function runRoutineCommand(routine, options = {}) {
3121
4174
  verbose: options.verbose
3122
4175
  });
3123
4176
  if (options.dryRun) {
3124
- console.log(pc10.green(result.output));
4177
+ console.log(pc11.green(result.output));
3125
4178
  return;
3126
4179
  }
3127
4180
  if (result.success) {
3128
- console.log(pc10.green(`
4181
+ console.log(pc11.green(`
3129
4182
  \u2713 Local agent session for '${routine}' completed successfully.`));
3130
4183
  } else {
3131
- console.error(pc10.red(`
4184
+ console.error(pc11.red(`
3132
4185
  \u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
3133
4186
  process.exit(result.exitCode);
3134
4187
  }
3135
4188
  } catch (error) {
3136
- console.error(pc10.red(`
4189
+ console.error(pc11.red(`
3137
4190
  \u2717 Failed to execute routine '${routine}': ${error.message}`));
3138
4191
  process.exit(1);
3139
4192
  }
3140
4193
  }
3141
4194
 
3142
4195
  // src/commands/daemon.ts
3143
- import pc12 from "picocolors";
4196
+ import pc14 from "picocolors";
3144
4197
 
3145
4198
  // src/lib/daemon.ts
4199
+ import fs14 from "fs";
4200
+ import path14 from "path";
4201
+ import { spawn as spawn2, execFile as execFile4 } from "child_process";
4202
+ import { promisify as promisify4 } from "util";
4203
+
4204
+ // src/lib/daemon-keys.ts
4205
+ import readline2 from "readline";
3146
4206
  import fs13 from "fs";
3147
4207
  import path13 from "path";
3148
- import { spawn as spawn2, execFile as execFile3 } from "child_process";
3149
- import { promisify as promisify3 } from "util";
3150
- import pc11 from "picocolors";
3151
- var execFileAsync3 = promisify3(execFile3);
4208
+ import pc12 from "picocolors";
4209
+ var KeyboardController = class {
4210
+ constructor(options = {}) {
4211
+ this.options = options;
4212
+ this.stdin = options.stdin || process.stdin;
4213
+ }
4214
+ options;
4215
+ stdin;
4216
+ isRaw = false;
4217
+ listening = false;
4218
+ isPaused = false;
4219
+ keypressListener;
4220
+ start() {
4221
+ if (this.listening) return;
4222
+ if (this.stdin && typeof this.stdin.setRawMode === "function" && this.stdin.isTTY) {
4223
+ readline2.emitKeypressEvents(this.stdin);
4224
+ try {
4225
+ this.stdin.setRawMode(true);
4226
+ this.isRaw = true;
4227
+ } catch {
4228
+ this.isRaw = false;
4229
+ }
4230
+ if (typeof this.stdin.resume === "function") {
4231
+ this.stdin.resume();
4232
+ }
4233
+ }
4234
+ this.keypressListener = (str, key) => {
4235
+ this.handleKeypress(str, key);
4236
+ };
4237
+ this.stdin.on("keypress", this.keypressListener);
4238
+ this.listening = true;
4239
+ this.isPaused = false;
4240
+ }
4241
+ pause() {
4242
+ this.isPaused = true;
4243
+ }
4244
+ resume() {
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
+ }
4252
+ }
4253
+ handleKeypress(str, key) {
4254
+ if (this.isPaused) return;
4255
+ const k = key || {};
4256
+ if (k.ctrl && (k.name === "c" || k.name === "C") || str === "") {
4257
+ this.options.onForceStop?.();
4258
+ return;
4259
+ }
4260
+ const isShift = Boolean(k.shift);
4261
+ const keyName = (k.name || "").toLowerCase();
4262
+ if (str === "R" || keyName === "r" && isShift) {
4263
+ this.options.onTargetedReview?.();
4264
+ return;
4265
+ }
4266
+ if (str === "A" || keyName === "a" && isShift) {
4267
+ this.options.onTargetedAutowork?.();
4268
+ return;
4269
+ }
4270
+ if (str === "r" || keyName === "r" && !isShift) {
4271
+ this.options.onReview?.();
4272
+ return;
4273
+ }
4274
+ if (str === "a" || keyName === "a" && !isShift) {
4275
+ this.options.onAutowork?.();
4276
+ return;
4277
+ }
4278
+ if (str === "v" || str === "V" || keyName === "v") {
4279
+ this.options.onToggleVerbose?.();
4280
+ return;
4281
+ }
4282
+ if (str === "l" || str === "L" || keyName === "l") {
4283
+ this.options.onTailLog?.();
4284
+ return;
4285
+ }
4286
+ if (str === "w" || str === "W" || keyName === "w") {
4287
+ this.options.onCleanWorktrees?.();
4288
+ return;
4289
+ }
4290
+ if (str === "p" || str === "P" || keyName === "p") {
4291
+ this.options.onPauseToggle?.();
4292
+ return;
4293
+ }
4294
+ if (str === "s" || str === "S" || keyName === "s") {
4295
+ this.options.onStatus?.();
4296
+ return;
4297
+ }
4298
+ if (str === "q" || str === "Q" || keyName === "q") {
4299
+ this.options.onGracefulStop?.();
4300
+ return;
4301
+ }
4302
+ if (str === "?" || keyName === "h" || str === "h" || str === "H") {
4303
+ this.options.onHelp?.();
4304
+ return;
4305
+ }
4306
+ }
4307
+ stop() {
4308
+ if (!this.listening) return;
4309
+ if (this.keypressListener) {
4310
+ this.stdin.removeListener("keypress", this.keypressListener);
4311
+ }
4312
+ if (this.isRaw && typeof this.stdin.setRawMode === "function") {
4313
+ try {
4314
+ this.stdin.setRawMode(false);
4315
+ } catch {
4316
+ }
4317
+ this.isRaw = false;
4318
+ }
4319
+ if (typeof this.stdin.pause === "function") {
4320
+ try {
4321
+ this.stdin.pause();
4322
+ } catch {
4323
+ }
4324
+ }
4325
+ this.listening = false;
4326
+ this.isPaused = false;
4327
+ }
4328
+ };
4329
+ function parseNumericTarget(input) {
4330
+ if (!input) return null;
4331
+ const trimmed = input.trim();
4332
+ if (!trimmed) return null;
4333
+ const match = trimmed.match(/^(?:(?:PR|Issue|pr|issue)\s*#?)?#?(\d+)$/i);
4334
+ if (match) {
4335
+ const num = parseInt(match[1], 10);
4336
+ return num > 0 ? num : null;
4337
+ }
4338
+ return null;
4339
+ }
4340
+ async function promptTargetedInput(promptMessage, options = {}) {
4341
+ const stdin = options.stdin || process.stdin;
4342
+ const stdout = options.stdout || process.stdout;
4343
+ if (stdout && typeof stdout.write === "function") {
4344
+ stdout.write(promptMessage);
4345
+ }
4346
+ return new Promise((resolve) => {
4347
+ let cleanedUp = false;
4348
+ const wasRaw = Boolean(stdin && stdin.rawMode !== void 0 ? stdin.rawMode : stdin?.isRaw);
4349
+ if (stdin && typeof stdin.setRawMode === "function" && stdin.isTTY) {
4350
+ try {
4351
+ stdin.setRawMode(false);
4352
+ } catch {
4353
+ }
4354
+ }
4355
+ if (stdin && typeof stdin.resume !== "function") {
4356
+ stdin.resume = () => {
4357
+ };
4358
+ }
4359
+ if (stdin && typeof stdin.pause !== "function") {
4360
+ stdin.pause = () => {
4361
+ };
4362
+ }
4363
+ const rl = readline2.createInterface({
4364
+ input: stdin,
4365
+ output: stdout,
4366
+ terminal: Boolean(stdin && stdin.isTTY)
4367
+ });
4368
+ const cleanup = (val) => {
4369
+ if (cleanedUp) return;
4370
+ cleanedUp = true;
4371
+ try {
4372
+ if (stdin && typeof stdin.removeListener === "function") {
4373
+ stdin.removeListener("data", onRawData);
4374
+ }
4375
+ rl.close();
4376
+ } catch {
4377
+ }
4378
+ if (wasRaw && stdin && typeof stdin.setRawMode === "function" && stdin.isTTY) {
4379
+ try {
4380
+ stdin.setRawMode(true);
4381
+ } catch {
4382
+ }
4383
+ }
4384
+ if (stdin && typeof stdin.resume === "function") {
4385
+ try {
4386
+ stdin.resume();
4387
+ } catch {
4388
+ }
4389
+ }
4390
+ resolve(val);
4391
+ };
4392
+ const onRawData = (chunk) => {
4393
+ const str = chunk.toString();
4394
+ if (str === "\x1B" || str === "") {
4395
+ cleanup(null);
4396
+ }
4397
+ };
4398
+ if (stdin && typeof stdin.on === "function") {
4399
+ stdin.on("data", onRawData);
4400
+ }
4401
+ rl.question("", (answer) => {
4402
+ cleanup(answer.trim() || null);
4403
+ });
4404
+ rl.on("close", () => {
4405
+ cleanup(null);
4406
+ });
4407
+ });
4408
+ }
4409
+ function getDaemonLogTail(repoRoot, linesCount = 20) {
4410
+ const logPath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
4411
+ if (!fs13.existsSync(logPath)) return [];
4412
+ try {
4413
+ const content = fs13.readFileSync(logPath, "utf8");
4414
+ const lines = content.split("\n");
4415
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
4416
+ lines.pop();
4417
+ }
4418
+ return lines.slice(-linesCount);
4419
+ } catch {
4420
+ return [];
4421
+ }
4422
+ }
4423
+ function printDaemonLogTail(repoRoot, linesCount = 20) {
4424
+ const lines = getDaemonLogTail(repoRoot, linesCount);
4425
+ const logPath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
4426
+ const relativePath = path13.relative(repoRoot, logPath) || logPath;
4427
+ console.log(pc12.cyan(`
4428
+ \u{1F4C4} Tail of ${relativePath} (last ${linesCount} lines):
4429
+ `));
4430
+ if (lines.length === 0) {
4431
+ console.log(pc12.dim(` (Log file is empty or does not exist yet at ${relativePath})
4432
+ `));
4433
+ return;
4434
+ }
4435
+ for (const line of lines) {
4436
+ console.log(pc12.dim(line));
4437
+ }
4438
+ console.log("");
4439
+ }
4440
+ async function inspectAndCleanWorktrees(repoRoot) {
4441
+ const active = await listActiveWorktrees(repoRoot);
4442
+ const cleaned = await cleanupStaleWorktrees(repoRoot);
4443
+ return { active, cleaned };
4444
+ }
4445
+ function printWorktreesInspection(result) {
4446
+ console.log(pc12.cyan(`
4447
+ \u{1F333} Jonah Fleet Worktree Inspection & Maintenance
4448
+ `));
4449
+ console.log(` Active Worktrees: ${result.active.length}`);
4450
+ if (result.active.length === 0) {
4451
+ console.log(pc12.dim(` (No active routine worktrees found)`));
4452
+ } else {
4453
+ for (const wt of result.active) {
4454
+ const commitShort = wt.commit ? ` (${wt.commit.slice(0, 7)})` : "";
4455
+ console.log(` - [${pc12.bold(wt.branch)}] ${pc12.dim(wt.path)}${commitShort}`);
4456
+ }
4457
+ }
4458
+ console.log(`
4459
+ Cleaned Stale Worktrees: ${result.cleaned}`);
4460
+ console.log("");
4461
+ }
4462
+ function printKeybindingCheatSheet() {
4463
+ console.log(pc12.cyan(`
4464
+ \u2328\uFE0F Jonah Fleet Daemon Keybindings
4465
+ `));
4466
+ console.log(` ${pc12.bold("r")} Trigger peer-review scan immediately`);
4467
+ console.log(` ${pc12.bold("R")} Prompt for PR # and run targeted peer-review`);
4468
+ console.log(` ${pc12.bold("a")} Trigger autowork backlog scan immediately`);
4469
+ console.log(` ${pc12.bold("A")} Prompt for Issue # and run targeted autowork`);
4470
+ console.log(` ${pc12.bold("p")} Pause / resume automated polling intervals`);
4471
+ console.log(` ${pc12.bold("s")} Print current daemon status summary card`);
4472
+ console.log(` ${pc12.bold("v")} Toggle verbose streaming logging live`);
4473
+ console.log(` ${pc12.bold("l")} Tail recent lines from .jonah-fleet/daemon.log`);
4474
+ console.log(` ${pc12.bold("w")} Inspect active worktrees and clean stale ones`);
4475
+ console.log(` ${pc12.bold("q")} Graceful shutdown (waits for active routine to finish)`);
4476
+ console.log(` ${pc12.bold("Ctrl+C")} Immediate force abort`);
4477
+ console.log(` ${pc12.bold("?")} / ${pc12.bold("h")} Show this keybindings cheat-sheet
4478
+ `);
4479
+ }
4480
+ function printDaemonStatusSummary(options) {
4481
+ const { state, pendingRoutine, activeWorktrees = [], verbose } = options;
4482
+ console.log(pc12.cyan(`
4483
+ \u{1F916} Jonah Fleet Local Daemon Status
4484
+ `));
4485
+ if (state) {
4486
+ let statusText;
4487
+ if (state.status === "working") {
4488
+ const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc12.bold(state.activeTarget)})` : "");
4489
+ statusText = pc12.yellow(pc12.bold(`WORKING on ${workingDesc}`));
4490
+ } else if (state.status === "paused") {
4491
+ statusText = pc12.yellow(pc12.bold("PAUSED"));
4492
+ } else {
4493
+ statusText = pc12.green(pc12.bold("RUNNING (IDLE)"));
4494
+ }
4495
+ console.log(` Status: ${statusText}`);
4496
+ console.log(` PID: ${state.pid}`);
4497
+ console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
4498
+ console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
4499
+ console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
4500
+ console.log(` Routines: ${state.routines.join(", ")}`);
4501
+ if (verbose !== void 0) {
4502
+ console.log(
4503
+ ` Verbose Mode: ${verbose ? pc12.green("ENABLED (streaming tokens)") : pc12.gray("DISABLED (compact spinner)")}`
4504
+ );
4505
+ }
4506
+ if (pendingRoutine) {
4507
+ console.log(` Queued Routine: ${pc12.cyan(pc12.bold(pendingRoutine))}`);
4508
+ }
4509
+ if (state.lastReviewCheckAt) {
4510
+ console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
4511
+ }
4512
+ if (state.lastAutoworkCheckAt) {
4513
+ console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
4514
+ }
4515
+ } else {
4516
+ console.log(` Status: ${pc12.gray("STOPPED")}`);
4517
+ console.log(pc12.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
4518
+ }
4519
+ console.log(`
4520
+ Active Worktrees: ${activeWorktrees.length}`);
4521
+ for (const wt of activeWorktrees) {
4522
+ console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
4523
+ }
4524
+ console.log("");
4525
+ }
4526
+ var DAEMON_STATUS_TIPS = [
4527
+ "Tip: press 'r' to run review pass now",
4528
+ "Tip: press 'a' to run autowork scan now",
4529
+ "Tip: press 'R' to review a specific PR #",
4530
+ "Tip: press 'A' to work a specific Issue #",
4531
+ "Tip: press 'p' to pause/resume automatic checks",
4532
+ "Tip: press 's' to view daemon status",
4533
+ "Tip: press 'v' to toggle verbose streaming",
4534
+ "Tip: press 'l' to view recent log tail",
4535
+ "Tip: press 'w' to inspect/clean worktrees",
4536
+ "Tip: press 'q' to stop daemon gracefully",
4537
+ "Tip: press '?' for all keybindings"
4538
+ ];
4539
+ var DAEMON_PAUSED_TIPS = [
4540
+ "Tip: press 'p' to resume scheduled checks",
4541
+ "Tip: press 'r' to run review pass now",
4542
+ "Tip: press 'a' to run autowork scan now",
4543
+ "Tip: press 'R' to review a specific PR #",
4544
+ "Tip: press 'A' to work a specific Issue #",
4545
+ "Tip: press 's' to view daemon status",
4546
+ "Tip: press 'v' to toggle verbose streaming",
4547
+ "Tip: press 'l' to view recent log tail",
4548
+ "Tip: press 'w' to inspect/clean worktrees",
4549
+ "Tip: press 'q' to stop daemon gracefully",
4550
+ "Tip: press '?' for all keybindings"
4551
+ ];
4552
+ function getRotatingTipIndex(nowMs = Date.now(), intervalSeconds = 4, totalTips = DAEMON_STATUS_TIPS.length) {
4553
+ if (totalTips <= 0) return 0;
4554
+ const slot = Math.floor(nowMs / (intervalSeconds * 1e3));
4555
+ return (slot % totalTips + totalTips) % totalTips;
4556
+ }
4557
+ function formatDaemonStatusLine(options = {}) {
4558
+ const nowDate = options.now instanceof Date ? options.now : typeof options.now === "number" ? new Date(options.now) : /* @__PURE__ */ new Date();
4559
+ const nowMs = nowDate.getTime();
4560
+ const timeString = nowDate.toLocaleTimeString();
4561
+ const columns = options.columns !== void 0 ? options.columns : process.stderr.columns || 80;
4562
+ const maxCols = Math.max(20, (columns || 80) - 2);
4563
+ const includeTip = columns >= 55;
4564
+ let core;
4565
+ if (options.isPaused) {
4566
+ const queueStr = options.pendingRoutine ? pc12.cyan(` [Queued: ${options.pendingRoutine}]`) : "";
4567
+ core = `${pc12.dim("[" + timeString + "]")} \u23F8\uFE0F ${pc12.yellow("PAUSED")}${queueStr}`;
4568
+ } else {
4569
+ const nextCheck = options.nextCheckTime !== void 0 ? options.nextCheckTime : nowMs;
4570
+ const diffMs = Math.max(0, nextCheck - nowMs);
4571
+ const remainingSecs = Math.ceil(diffMs / 1e3);
4572
+ const mins = Math.floor(remainingSecs / 60);
4573
+ const secs = remainingSecs % 60;
4574
+ const timeStr = `${mins}m ${secs < 10 ? "0" : ""}${secs}s`;
4575
+ const prStr = options.lastOpenPRCount !== void 0 ? ` (${options.lastOpenPRCount} ready PRs)` : "";
4576
+ const queueStr = options.pendingRoutine ? pc12.cyan(` [Queued: ${options.pendingRoutine}]`) : "";
4577
+ core = `${pc12.dim("[" + timeString + "]")} \u{1F4A4} ${pc12.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}${queueStr}`;
4578
+ }
4579
+ if (!includeTip) {
4580
+ return truncateAnsi(core, maxCols);
4581
+ }
4582
+ const tipsList = options.tips || (options.isPaused ? DAEMON_PAUSED_TIPS : DAEMON_STATUS_TIPS);
4583
+ const tipIdx = options.tipIndex !== void 0 ? options.tipIndex : getRotatingTipIndex(nowMs, 4, tipsList.length);
4584
+ const tipText = tipsList[(tipIdx % tipsList.length + tipsList.length) % tipsList.length] || "";
4585
+ const fullLine = `${core} ${pc12.dim("\xB7")} ${pc12.dim(tipText)}`;
4586
+ return truncateAnsi(fullLine, maxCols);
4587
+ }
4588
+
4589
+ // src/lib/daemon.ts
4590
+ import pc13 from "picocolors";
4591
+ var execFileAsync4 = promisify4(execFile4);
3152
4592
  function getDaemonStatePath(repoRoot) {
3153
- return path13.join(repoRoot, ".jonah-fleet", "daemon.json");
4593
+ return path14.join(repoRoot, ".jonah-fleet", "daemon.json");
3154
4594
  }
3155
4595
  function readDaemonState(repoRoot) {
3156
4596
  const statePath = getDaemonStatePath(repoRoot);
3157
- if (!fs13.existsSync(statePath)) return null;
4597
+ if (!fs14.existsSync(statePath)) return null;
3158
4598
  try {
3159
- return JSON.parse(fs13.readFileSync(statePath, "utf8"));
4599
+ return JSON.parse(fs14.readFileSync(statePath, "utf8"));
3160
4600
  } catch {
3161
4601
  return null;
3162
4602
  }
3163
4603
  }
3164
4604
  function writeDaemonState(repoRoot, state) {
3165
4605
  const statePath = getDaemonStatePath(repoRoot);
3166
- fs13.mkdirSync(path13.dirname(statePath), { recursive: true });
3167
- fs13.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
4606
+ fs14.mkdirSync(path14.dirname(statePath), { recursive: true });
4607
+ fs14.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
3168
4608
  }
3169
4609
  function clearDaemonState(repoRoot) {
3170
4610
  const statePath = getDaemonStatePath(repoRoot);
3171
- if (fs13.existsSync(statePath)) {
4611
+ if (fs14.existsSync(statePath)) {
3172
4612
  try {
3173
- fs13.unlinkSync(statePath);
4613
+ fs14.unlinkSync(statePath);
3174
4614
  } catch {
3175
4615
  }
3176
4616
  }
@@ -3193,7 +4633,7 @@ function filterReviewablePRs(prs) {
3193
4633
  }
3194
4634
  async function getOpenReviewablePRs(repoRoot) {
3195
4635
  try {
3196
- const { stdout } = await execFileAsync3(
4636
+ const { stdout } = await execFileAsync4(
3197
4637
  "gh",
3198
4638
  ["pr", "list", "--state", "open", "--draft=false", "--json", "number,headRefName,title"],
3199
4639
  { cwd: repoRoot }
@@ -3204,10 +4644,6 @@ async function getOpenReviewablePRs(repoRoot) {
3204
4644
  return [];
3205
4645
  }
3206
4646
  }
3207
- async function countOpenReadyPRs(repoRoot) {
3208
- const prs = await getOpenReviewablePRs(repoRoot);
3209
- return prs.length;
3210
- }
3211
4647
  async function startBackgroundDaemon(repoRoot, options = {}) {
3212
4648
  if (isDaemonRunning(repoRoot)) {
3213
4649
  const existing = readDaemonState(repoRoot);
@@ -3216,9 +4652,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
3216
4652
  const reviewInterval = options.reviewInterval || 3;
3217
4653
  const autoworkInterval = options.autoworkInterval || options.interval || 30;
3218
4654
  const routines = options.routines || ["peer-review", "autowork"];
3219
- const logFilePath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
3220
- fs13.mkdirSync(path13.dirname(logFilePath), { recursive: true });
3221
- const logFd = fs13.openSync(logFilePath, "a");
4655
+ const logFilePath = path14.join(repoRoot, ".jonah-fleet", "daemon.log");
4656
+ fs14.mkdirSync(path14.dirname(logFilePath), { recursive: true });
4657
+ const logFd = fs14.openSync(logFilePath, "a");
3222
4658
  const cliPath = process.argv[1];
3223
4659
  const args = [
3224
4660
  "daemon",
@@ -3286,7 +4722,7 @@ async function drainReviewQueue(drainOptions) {
3286
4722
  let reviewablePRs = await getPRs(repoRoot);
3287
4723
  if (reviewablePRs.length === 0) {
3288
4724
  if (options.verbose) {
3289
- console.log(pc11.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
4725
+ console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
3290
4726
  }
3291
4727
  return;
3292
4728
  }
@@ -3296,7 +4732,7 @@ async function drainReviewQueue(drainOptions) {
3296
4732
  if (candidatePRs.length === 0) {
3297
4733
  if (options.verbose) {
3298
4734
  console.log(
3299
- pc11.dim(
4735
+ pc13.dim(
3300
4736
  `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] All ${reviewablePRs.length} remaining ready PR(s) were already evaluated in this drain pass.`
3301
4737
  )
3302
4738
  );
@@ -3313,7 +4749,7 @@ async function drainReviewQueue(drainOptions) {
3313
4749
  writeDaemonState(repoRoot, state);
3314
4750
  }
3315
4751
  console.log(
3316
- pc11.cyan(
4752
+ pc13.cyan(
3317
4753
  `
3318
4754
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Draining PR backlog (${totalRemaining} PR(s) remaining). Starting review session...`
3319
4755
  )
@@ -3341,14 +4777,14 @@ async function drainReviewQueue(drainOptions) {
3341
4777
  onAttempted?.(prNum);
3342
4778
  }
3343
4779
  if (result.success) {
3344
- console.log(pc11.green(`\u2713 Local peer-review completed successfully.
4780
+ console.log(pc13.green(`\u2713 Local peer-review completed successfully.
3345
4781
  `));
3346
4782
  } else {
3347
- console.warn(pc11.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
4783
+ console.warn(pc13.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
3348
4784
  `));
3349
4785
  }
3350
4786
  } catch (err) {
3351
- console.error(pc11.red(`\u2717 Error in peer-review: ${err.message}`));
4787
+ console.error(pc13.red(`\u2717 Error in peer-review: ${err.message}`));
3352
4788
  if (candidatePRs[0]) {
3353
4789
  attemptedPRNumbers.add(candidatePRs[0].number);
3354
4790
  onAttempted?.(candidatePRs[0].number);
@@ -3377,114 +4813,287 @@ async function runDaemonLoop(repoRoot, options = {}) {
3377
4813
  status: "idle"
3378
4814
  };
3379
4815
  writeDaemonState(repoRoot, state);
3380
- console.log(pc11.cyan(`
4816
+ console.log(pc13.cyan(`
3381
4817
  \u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
3382
- console.log(pc11.dim(` PID: ${process.pid}`));
3383
- console.log(pc11.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
3384
- console.log(pc11.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
3385
- console.log(pc11.dim(` Working Directory: ${repoRoot}
4818
+ console.log(pc13.dim(` PID: ${process.pid}`));
4819
+ console.log(pc13.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
4820
+ console.log(pc13.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
4821
+ console.log(pc13.dim(` Working Directory: ${repoRoot}`));
4822
+ console.log(pc13.dim(` Interactive Hotkeys: 'r' (review), 'a' (autowork), 'p' (pause), 's' (status), 'q' (stop), '?' (help)
3386
4823
  `));
3387
4824
  let isStopping = false;
4825
+ let isGracefulStopping = false;
3388
4826
  let isWorking = false;
4827
+ let isPaused = false;
4828
+ let isPrompting = false;
4829
+ let pendingRoutine = null;
4830
+ let keyboard;
4831
+ let tickerInterval;
4832
+ let stopResolve;
3389
4833
  const reviewIntervalMs = reviewInterval * 60 * 1e3;
3390
4834
  const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
3391
4835
  let nextReviewCheckTime = Date.now() + (routines.includes("peer-review") ? reviewIntervalMs : Infinity);
3392
4836
  let nextAutoworkCheckTime = Date.now() + (routines.includes("autowork") ? autoworkIntervalMs : Infinity);
3393
4837
  let lastOpenPRCount = void 0;
4838
+ const getPRsFn = options.getPRs || getOpenReviewablePRs;
4839
+ const runRoutineFn = options.runRoutine || runLocalRoutine;
3394
4840
  const clearTicker = () => {
3395
4841
  if (process.stderr.isTTY && !options.verbose) {
3396
4842
  process.stderr.write("\r\x1B[K");
3397
4843
  }
3398
4844
  };
3399
- const updateTicker = () => {
3400
- if (isStopping || isWorking || options.verbose || !process.stderr.isTTY) return;
3401
- const now = Date.now();
3402
- const nextCheck = Math.min(nextReviewCheckTime, nextAutoworkCheckTime);
3403
- const diffMs = Math.max(0, nextCheck - now);
3404
- const remainingSecs = Math.ceil(diffMs / 1e3);
3405
- const mins = Math.floor(remainingSecs / 60);
3406
- const secs = remainingSecs % 60;
3407
- const timeStr = `${mins}m ${secs < 10 ? "0" : ""}${secs}s`;
3408
- const prStr = lastOpenPRCount !== void 0 ? ` (${lastOpenPRCount} ready PRs)` : "";
3409
- process.stderr.write(
3410
- `\r\x1B[K${pc11.dim("[" + (/* @__PURE__ */ new Date()).toLocaleTimeString() + "]")} \u{1F4A4} ${pc11.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}`
3411
- );
3412
- };
3413
- const tickerInterval = setInterval(updateTicker, 1e3);
3414
- const handleStop = async () => {
3415
- if (isStopping) return;
3416
- isStopping = true;
3417
- clearInterval(tickerInterval);
3418
- clearInterval(reviewTimer);
3419
- clearInterval(autoworkTimer);
3420
- clearTicker();
3421
- console.log(pc11.yellow(`
3422
- Stopping local agent daemon...`));
3423
- clearDaemonState(repoRoot);
3424
- await cleanupStaleWorktrees(repoRoot);
3425
- process.exit(0);
3426
- };
3427
- process.once("SIGINT", handleStop);
3428
- process.once("SIGTERM", handleStop);
3429
- const performReviewDrain = async () => {
4845
+ const updateTicker = () => {
4846
+ if (isStopping || isWorking || isPrompting || options.verbose || !process.stderr.isTTY) return;
4847
+ const line = formatDaemonStatusLine({
4848
+ now: Date.now(),
4849
+ isPaused,
4850
+ nextCheckTime: Math.min(nextReviewCheckTime, nextAutoworkCheckTime),
4851
+ lastOpenPRCount,
4852
+ pendingRoutine,
4853
+ columns: process.stderr.columns
4854
+ });
4855
+ process.stderr.write(`\r\x1B[K${line}`);
4856
+ };
4857
+ const handleStop = async () => {
4858
+ if (isStopping) return;
4859
+ isStopping = true;
4860
+ process.removeListener("SIGINT", handleStop);
4861
+ process.removeListener("SIGTERM", handleStop);
4862
+ keyboard?.stop();
4863
+ if (tickerInterval) {
4864
+ clearInterval(tickerInterval);
4865
+ tickerInterval = void 0;
4866
+ }
4867
+ clearTicker();
4868
+ console.log(pc13.yellow(`
4869
+ Stopping local agent daemon...`));
4870
+ clearDaemonState(repoRoot);
4871
+ await cleanupStaleWorktrees(repoRoot);
4872
+ stopResolve?.();
4873
+ process.exit(0);
4874
+ };
4875
+ process.once("SIGINT", handleStop);
4876
+ process.once("SIGTERM", handleStop);
4877
+ const performReviewDrain = async () => {
4878
+ if (isStopping || isWorking) return;
4879
+ try {
4880
+ isWorking = true;
4881
+ nextReviewCheckTime = Date.now() + reviewIntervalMs;
4882
+ await drainReviewQueue({
4883
+ repoRoot,
4884
+ state,
4885
+ options,
4886
+ isStopping: () => isStopping,
4887
+ clearTicker,
4888
+ getPRs: getPRsFn,
4889
+ runRoutine: runRoutineFn
4890
+ });
4891
+ const prs = await getPRsFn(repoRoot);
4892
+ lastOpenPRCount = prs.length;
4893
+ } finally {
4894
+ isWorking = false;
4895
+ state.status = isPaused ? "paused" : "idle";
4896
+ state.activeRoutine = void 0;
4897
+ state.activeTarget = void 0;
4898
+ writeDaemonState(repoRoot, state);
4899
+ nextReviewCheckTime = Date.now() + reviewIntervalMs;
4900
+ updateTicker();
4901
+ if (isGracefulStopping) {
4902
+ await handleStop();
4903
+ return;
4904
+ }
4905
+ if (pendingRoutine && !isStopping) {
4906
+ const next = pendingRoutine;
4907
+ pendingRoutine = null;
4908
+ console.log(pc13.cyan(`
4909
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
4910
+ if (next === "peer-review") {
4911
+ await performReviewDrain();
4912
+ } else if (next === "autowork") {
4913
+ await runAutoworkCheck();
4914
+ }
4915
+ }
4916
+ }
4917
+ };
4918
+ const runAutoworkCheck = async () => {
4919
+ if (isStopping || isWorking || !routines.includes("autowork")) return;
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) {
4926
+ console.log(
4927
+ pc13.cyan(
4928
+ `
4929
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
4930
+ )
4931
+ );
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
+ }
4953
+ }
4954
+ }
4955
+ state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
4956
+ clearTicker();
4957
+ state.status = "working";
4958
+ state.activeRoutine = "autowork";
4959
+ writeDaemonState(repoRoot, state);
4960
+ console.log(pc13.cyan(`
4961
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
4962
+ await cleanupStaleWorktrees(repoRoot);
4963
+ const result = await runRoutineFn({
4964
+ targetDir: repoRoot,
4965
+ routine: "autowork",
4966
+ model: options.model,
4967
+ verbose: options.verbose,
4968
+ noWorktree: false,
4969
+ onTargetDetected: (target) => {
4970
+ state.activeTarget = target;
4971
+ writeDaemonState(repoRoot, state);
4972
+ }
4973
+ });
4974
+ if (result.success) {
4975
+ console.log(pc13.green(`\u2713 Local autowork completed successfully.
4976
+ `));
4977
+ } else {
4978
+ console.warn(pc13.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
4979
+ `));
4980
+ }
4981
+ } catch (err) {
4982
+ console.error(pc13.red(`\u2717 Error in autowork: ${err.message}`));
4983
+ } finally {
4984
+ isWorking = false;
4985
+ state.status = isPaused ? "paused" : "idle";
4986
+ state.activeRoutine = void 0;
4987
+ state.activeTarget = void 0;
4988
+ writeDaemonState(repoRoot, state);
4989
+ nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
4990
+ updateTicker();
4991
+ if (!isStopping && routines.includes("peer-review")) {
4992
+ const newPRCount = (await getPRsFn(repoRoot)).length;
4993
+ if (newPRCount > 0) {
4994
+ console.log(
4995
+ pc13.cyan(
4996
+ `
4997
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
4998
+ )
4999
+ );
5000
+ await performReviewDrain();
5001
+ }
5002
+ }
5003
+ if (isGracefulStopping) {
5004
+ await handleStop();
5005
+ return;
5006
+ }
5007
+ if (pendingRoutine && !isStopping) {
5008
+ const next = pendingRoutine;
5009
+ pendingRoutine = null;
5010
+ console.log(pc13.cyan(`
5011
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
5012
+ if (next === "peer-review") {
5013
+ await performReviewDrain();
5014
+ } else if (next === "autowork") {
5015
+ await runAutoworkCheck();
5016
+ }
5017
+ }
5018
+ }
5019
+ };
5020
+ const runTargetedReview = async (prNumber) => {
3430
5021
  if (isStopping || isWorking) return;
3431
5022
  try {
3432
5023
  isWorking = true;
3433
- await drainReviewQueue({
3434
- repoRoot,
3435
- state,
3436
- options,
3437
- isStopping: () => isStopping,
3438
- clearTicker
5024
+ clearTicker();
5025
+ state.status = "working";
5026
+ state.activeRoutine = "peer-review";
5027
+ state.activeTarget = `PR #${prNumber}`;
5028
+ writeDaemonState(repoRoot, state);
5029
+ console.log(
5030
+ pc13.cyan(`
5031
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Peer Review: Starting session on PR #${prNumber}...`)
5032
+ );
5033
+ await cleanupStaleWorktrees(repoRoot);
5034
+ const result = await runRoutineFn({
5035
+ targetDir: repoRoot,
5036
+ routine: "peer-review",
5037
+ pr: prNumber,
5038
+ model: options.model,
5039
+ verbose: options.verbose,
5040
+ noWorktree: false,
5041
+ onTargetDetected: (target) => {
5042
+ state.activeTarget = target;
5043
+ writeDaemonState(repoRoot, state);
5044
+ }
3439
5045
  });
3440
- const prs = await getOpenReviewablePRs(repoRoot);
3441
- lastOpenPRCount = prs.length;
5046
+ if (result.success) {
5047
+ console.log(pc13.green(`\u2713 Targeted peer-review on PR #${prNumber} completed successfully.
5048
+ `));
5049
+ } else {
5050
+ console.warn(pc13.yellow(`\u26A0\uFE0F Targeted peer-review on PR #${prNumber} completed with code ${result.exitCode}.
5051
+ `));
5052
+ }
5053
+ } catch (err) {
5054
+ console.error(pc13.red(`\u2717 Error in targeted peer-review: ${err.message}`));
3442
5055
  } finally {
3443
5056
  isWorking = false;
3444
- nextReviewCheckTime = Date.now() + reviewIntervalMs;
5057
+ state.status = isPaused ? "paused" : "idle";
5058
+ state.activeRoutine = void 0;
5059
+ state.activeTarget = void 0;
5060
+ writeDaemonState(repoRoot, state);
3445
5061
  updateTicker();
3446
- }
3447
- };
3448
- const runAutoworkCheck = async () => {
3449
- if (isStopping || isWorking || !routines.includes("autowork")) return;
3450
- if (routines.includes("peer-review")) {
3451
- const pendingPRs = await countOpenReadyPRs(repoRoot);
3452
- if (pendingPRs > 0) {
3453
- console.log(
3454
- pc11.cyan(
3455
- `
3456
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
3457
- )
3458
- );
3459
- await performReviewDrain();
3460
- const remainingPRs = await countOpenReadyPRs(repoRoot);
3461
- if (remainingPRs > 0) {
3462
- console.log(
3463
- pc11.yellow(
3464
- `
3465
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Review backlog still has ${remainingPRs} pending PR(s). Postponing autowork session.`
3466
- )
3467
- );
3468
- nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
3469
- updateTicker();
3470
- return;
5062
+ if (isGracefulStopping) {
5063
+ await handleStop();
5064
+ return;
5065
+ }
5066
+ if (pendingRoutine && !isStopping) {
5067
+ const next = pendingRoutine;
5068
+ pendingRoutine = null;
5069
+ console.log(pc13.cyan(`
5070
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
5071
+ if (next === "peer-review") {
5072
+ await performReviewDrain();
5073
+ } else if (next === "autowork") {
5074
+ await runAutoworkCheck();
3471
5075
  }
3472
5076
  }
3473
5077
  }
3474
- state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
3475
- writeDaemonState(repoRoot, state);
5078
+ };
5079
+ const runTargetedAutowork = async (issueNumber) => {
5080
+ if (isStopping || isWorking) return;
3476
5081
  try {
3477
5082
  isWorking = true;
3478
5083
  clearTicker();
3479
5084
  state.status = "working";
3480
5085
  state.activeRoutine = "autowork";
5086
+ state.activeTarget = `Issue #${issueNumber}`;
3481
5087
  writeDaemonState(repoRoot, state);
3482
- console.log(pc11.cyan(`
3483
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
5088
+ console.log(
5089
+ pc13.cyan(`
5090
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Autowork: Starting session on Issue #${issueNumber}...`)
5091
+ );
3484
5092
  await cleanupStaleWorktrees(repoRoot);
3485
- const result = await runLocalRoutine({
5093
+ const result = await runRoutineFn({
3486
5094
  targetDir: repoRoot,
3487
5095
  routine: "autowork",
5096
+ issue: issueNumber,
3488
5097
  model: options.model,
3489
5098
  verbose: options.verbose,
3490
5099
  noWorktree: false,
@@ -3494,27 +5103,26 @@ Stopping local agent daemon...`));
3494
5103
  }
3495
5104
  });
3496
5105
  if (result.success) {
3497
- console.log(pc11.green(`\u2713 Local autowork completed successfully.
5106
+ console.log(pc13.green(`\u2713 Targeted autowork on Issue #${issueNumber} completed successfully.
3498
5107
  `));
3499
5108
  } else {
3500
- console.warn(pc11.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
5109
+ console.warn(pc13.yellow(`\u26A0\uFE0F Targeted autowork on Issue #${issueNumber} completed with code ${result.exitCode}.
3501
5110
  `));
3502
5111
  }
3503
5112
  } catch (err) {
3504
- console.error(pc11.red(`\u2717 Error in autowork: ${err.message}`));
5113
+ console.error(pc13.red(`\u2717 Error in targeted autowork: ${err.message}`));
3505
5114
  } finally {
3506
5115
  isWorking = false;
3507
- state.status = "idle";
5116
+ state.status = isPaused ? "paused" : "idle";
3508
5117
  state.activeRoutine = void 0;
3509
5118
  state.activeTarget = void 0;
3510
5119
  writeDaemonState(repoRoot, state);
3511
- nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
3512
5120
  updateTicker();
3513
5121
  if (!isStopping && routines.includes("peer-review")) {
3514
- const newPRCount = await countOpenReadyPRs(repoRoot);
5122
+ const newPRCount = (await getPRsFn(repoRoot)).length;
3515
5123
  if (newPRCount > 0) {
3516
5124
  console.log(
3517
- pc11.cyan(
5125
+ pc13.cyan(
3518
5126
  `
3519
5127
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
3520
5128
  )
@@ -3522,17 +5130,269 @@ Stopping local agent daemon...`));
3522
5130
  await performReviewDrain();
3523
5131
  }
3524
5132
  }
5133
+ if (isGracefulStopping) {
5134
+ await handleStop();
5135
+ return;
5136
+ }
5137
+ if (pendingRoutine && !isStopping) {
5138
+ const next = pendingRoutine;
5139
+ pendingRoutine = null;
5140
+ console.log(pc13.cyan(`
5141
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
5142
+ if (next === "peer-review") {
5143
+ await performReviewDrain();
5144
+ } else if (next === "autowork") {
5145
+ await runAutoworkCheck();
5146
+ }
5147
+ }
3525
5148
  }
3526
5149
  };
5150
+ keyboard = new KeyboardController({
5151
+ stdin: options.stdin || process.stdin,
5152
+ onReview: async () => {
5153
+ if (isStopping || isGracefulStopping) return;
5154
+ if (isWorking) {
5155
+ pendingRoutine = "peer-review";
5156
+ clearTicker();
5157
+ console.log(
5158
+ pc13.cyan(`
5159
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Peer Review scan queued (will run after current routine finishes).`)
5160
+ );
5161
+ updateTicker();
5162
+ return;
5163
+ }
5164
+ clearTicker();
5165
+ console.log(pc13.cyan(`
5166
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Triggering immediate Peer Review scan on demand...`));
5167
+ await performReviewDrain();
5168
+ },
5169
+ onAutowork: async () => {
5170
+ if (isStopping || isGracefulStopping) return;
5171
+ if (isWorking) {
5172
+ pendingRoutine = "autowork";
5173
+ clearTicker();
5174
+ console.log(
5175
+ pc13.cyan(`
5176
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork backlog scan queued (will run after current routine finishes).`)
5177
+ );
5178
+ updateTicker();
5179
+ return;
5180
+ }
5181
+ clearTicker();
5182
+ console.log(pc13.cyan(`
5183
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Triggering immediate Autowork scan on demand...`));
5184
+ await runAutoworkCheck();
5185
+ },
5186
+ onTargetedReview: async () => {
5187
+ if (isStopping || isGracefulStopping || isPrompting) return;
5188
+ if (isWorking) {
5189
+ clearTicker();
5190
+ console.log(
5191
+ pc13.yellow(
5192
+ `
5193
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Targeted review prompts require idle state. Use 'r' to queue a scan pass instead.`
5194
+ )
5195
+ );
5196
+ updateTicker();
5197
+ return;
5198
+ }
5199
+ clearTicker();
5200
+ keyboard?.pause();
5201
+ isPrompting = true;
5202
+ let rawInput = null;
5203
+ try {
5204
+ rawInput = await promptTargetedInput(`
5205
+ ${pc13.cyan("Enter PR # to review (Esc/Enter to cancel):")} `, {
5206
+ stdin: options.stdin || process.stdin,
5207
+ stdout: process.stdout
5208
+ });
5209
+ } finally {
5210
+ isPrompting = false;
5211
+ keyboard?.resume();
5212
+ }
5213
+ if (!rawInput) {
5214
+ console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted review cancelled.
5215
+ `));
5216
+ updateTicker();
5217
+ return;
5218
+ }
5219
+ const prNumber = parseNumericTarget(rawInput);
5220
+ if (!prNumber) {
5221
+ console.log(
5222
+ pc13.yellow(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Invalid PR number '${rawInput}'. Operation cancelled.
5223
+ `)
5224
+ );
5225
+ updateTicker();
5226
+ return;
5227
+ }
5228
+ await runTargetedReview(prNumber);
5229
+ },
5230
+ onTargetedAutowork: async () => {
5231
+ if (isStopping || isGracefulStopping || isPrompting) return;
5232
+ if (isWorking) {
5233
+ clearTicker();
5234
+ console.log(
5235
+ pc13.yellow(
5236
+ `
5237
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Targeted autowork prompts require idle state. Use 'a' to queue a scan pass instead.`
5238
+ )
5239
+ );
5240
+ updateTicker();
5241
+ return;
5242
+ }
5243
+ clearTicker();
5244
+ keyboard?.pause();
5245
+ isPrompting = true;
5246
+ let rawInput = null;
5247
+ try {
5248
+ rawInput = await promptTargetedInput(`
5249
+ ${pc13.cyan("Enter Issue # to work (Esc/Enter to cancel):")} `, {
5250
+ stdin: options.stdin || process.stdin,
5251
+ stdout: process.stdout
5252
+ });
5253
+ } finally {
5254
+ isPrompting = false;
5255
+ keyboard?.resume();
5256
+ }
5257
+ if (!rawInput) {
5258
+ console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted autowork cancelled.
5259
+ `));
5260
+ updateTicker();
5261
+ return;
5262
+ }
5263
+ const issueNumber = parseNumericTarget(rawInput);
5264
+ if (!issueNumber) {
5265
+ console.log(
5266
+ pc13.yellow(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Invalid Issue number '${rawInput}'. Operation cancelled.
5267
+ `)
5268
+ );
5269
+ updateTicker();
5270
+ return;
5271
+ }
5272
+ await runTargetedAutowork(issueNumber);
5273
+ },
5274
+ onToggleVerbose: () => {
5275
+ if (isStopping || isGracefulStopping) return;
5276
+ options.verbose = !options.verbose;
5277
+ clearTicker();
5278
+ if (options.verbose) {
5279
+ console.log(
5280
+ pc13.green(`
5281
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50A} Verbose mode ENABLED (streaming tokens directly to terminal).`)
5282
+ );
5283
+ } else {
5284
+ console.log(
5285
+ pc13.yellow(`
5286
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F507} Verbose mode DISABLED (compact terminal spinner active).`)
5287
+ );
5288
+ }
5289
+ updateTicker();
5290
+ },
5291
+ onTailLog: () => {
5292
+ if (isStopping || isGracefulStopping) return;
5293
+ clearTicker();
5294
+ printDaemonLogTail(repoRoot, 20);
5295
+ updateTicker();
5296
+ },
5297
+ onCleanWorktrees: async () => {
5298
+ if (isStopping || isGracefulStopping) return;
5299
+ clearTicker();
5300
+ const result = await inspectAndCleanWorktrees(repoRoot);
5301
+ printWorktreesInspection(result);
5302
+ updateTicker();
5303
+ },
5304
+ onPauseToggle: () => {
5305
+ if (isStopping || isGracefulStopping) return;
5306
+ isPaused = !isPaused;
5307
+ clearTicker();
5308
+ if (isPaused) {
5309
+ if (state.status !== "working") state.status = "paused";
5310
+ writeDaemonState(repoRoot, state);
5311
+ console.log(
5312
+ pc13.yellow(
5313
+ `
5314
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F8\uFE0F Daemon polling paused. Automatic interval sweeps suspended. (Press 'p' to resume)`
5315
+ )
5316
+ );
5317
+ } else {
5318
+ if (state.status !== "working") state.status = "idle";
5319
+ writeDaemonState(repoRoot, state);
5320
+ console.log(
5321
+ pc13.green(`
5322
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u25B6\uFE0F Daemon polling resumed. Automated interval sweeps active.`)
5323
+ );
5324
+ }
5325
+ updateTicker();
5326
+ },
5327
+ onStatus: async () => {
5328
+ clearTicker();
5329
+ const activeWorktrees = await listActiveWorktrees(repoRoot);
5330
+ printDaemonStatusSummary({
5331
+ repoRoot,
5332
+ state,
5333
+ pendingRoutine,
5334
+ activeWorktrees,
5335
+ verbose: options.verbose
5336
+ });
5337
+ updateTicker();
5338
+ },
5339
+ onGracefulStop: async () => {
5340
+ if (isStopping) return;
5341
+ pendingRoutine = null;
5342
+ if (isWorking) {
5343
+ isGracefulStopping = true;
5344
+ clearTicker();
5345
+ console.log(
5346
+ pc13.yellow(
5347
+ `
5348
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F6D1} Graceful stop requested. Waiting for active routine (${state.activeRoutine || "routine"}) to complete before stopping...`
5349
+ )
5350
+ );
5351
+ return;
5352
+ }
5353
+ await handleStop();
5354
+ },
5355
+ onForceStop: async () => {
5356
+ await handleStop();
5357
+ },
5358
+ onHelp: () => {
5359
+ clearTicker();
5360
+ printKeybindingCheatSheet();
5361
+ updateTicker();
5362
+ }
5363
+ });
5364
+ keyboard?.start();
3527
5365
  if (routines.includes("peer-review")) {
3528
5366
  await performReviewDrain();
3529
5367
  }
3530
- if (!isStopping && routines.includes("autowork")) {
5368
+ if (!isStopping && !isGracefulStopping && routines.includes("autowork")) {
3531
5369
  await runAutoworkCheck();
3532
5370
  }
3533
- const reviewTimer = setInterval(performReviewDrain, reviewIntervalMs);
3534
- const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
3535
- await new Promise(() => {
5371
+ if (isStopping) return;
5372
+ let isTicking = false;
5373
+ const tick = async () => {
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
+ }
5387
+ }
5388
+ updateTicker();
5389
+ } finally {
5390
+ isTicking = false;
5391
+ }
5392
+ };
5393
+ tickerInterval = setInterval(tick, 1e3);
5394
+ await new Promise((resolve) => {
5395
+ stopResolve = resolve;
3536
5396
  });
3537
5397
  }
3538
5398
 
@@ -3556,16 +5416,16 @@ async function runDaemonCommand(action, options = {}) {
3556
5416
  }
3557
5417
  try {
3558
5418
  const state2 = await startBackgroundDaemon(cwd, daemonOpts);
3559
- console.log(pc12.green(`
5419
+ console.log(pc14.green(`
3560
5420
  \u2713 Background agent daemon started successfully.`));
3561
- console.log(pc12.dim(` PID: ${state2.pid}`));
3562
- console.log(pc12.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
3563
- console.log(pc12.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
3564
- console.log(pc12.dim(` Routines: ${state2.routines.join(", ")}`));
3565
- console.log(pc12.dim(` Log file: .jonah-fleet/daemon.log`));
3566
- console.log(pc12.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
5421
+ console.log(pc14.dim(` PID: ${state2.pid}`));
5422
+ console.log(pc14.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
5423
+ console.log(pc14.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
5424
+ console.log(pc14.dim(` Routines: ${state2.routines.join(", ")}`));
5425
+ console.log(pc14.dim(` Log file: .jonah-fleet/daemon.log`));
5426
+ console.log(pc14.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
3567
5427
  } catch (err) {
3568
- console.error(pc12.red(`
5428
+ console.error(pc14.red(`
3569
5429
  \u2717 Failed to start daemon: ${err.message}`));
3570
5430
  process.exit(1);
3571
5431
  }
@@ -3573,18 +5433,18 @@ async function runDaemonCommand(action, options = {}) {
3573
5433
  }
3574
5434
  if (act === "stop") {
3575
5435
  if (!isDaemonRunning(cwd)) {
3576
- console.log(pc12.yellow(`
5436
+ console.log(pc14.yellow(`
3577
5437
  \u26A0\uFE0F No local agent daemon is currently running in this repository.`));
3578
5438
  return;
3579
5439
  }
3580
5440
  const state2 = readDaemonState(cwd);
3581
- console.log(pc12.cyan(`
5441
+ console.log(pc14.cyan(`
3582
5442
  Stopping background agent daemon (PID ${state2?.pid})...`));
3583
5443
  const stopped = await stopDaemon(cwd);
3584
5444
  if (stopped) {
3585
- console.log(pc12.green(`\u2713 Local agent daemon stopped successfully.`));
5445
+ console.log(pc14.green(`\u2713 Local agent daemon stopped successfully.`));
3586
5446
  } else {
3587
- console.error(pc12.red(`\u2717 Could not terminate daemon process.`));
5447
+ console.error(pc14.red(`\u2717 Could not terminate daemon process.`));
3588
5448
  process.exit(1);
3589
5449
  }
3590
5450
  return;
@@ -3596,18 +5456,24 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
3596
5456
  const running = isDaemonRunning(cwd);
3597
5457
  const state = readDaemonState(cwd);
3598
5458
  const activeWorktrees = await listActiveWorktrees(cwd);
3599
- console.log(pc12.cyan(`
5459
+ console.log(pc14.cyan(`
3600
5460
  \u{1F916} Jonah Fleet Local Daemon Status
3601
5461
  `));
3602
5462
  if (running && state) {
3603
- console.log(` Status: ${pc12.green(pc12.bold("RUNNING"))}`);
5463
+ console.log(` Status: ${pc14.green(pc14.bold("RUNNING"))}`);
3604
5464
  console.log(` PID: ${state.pid}`);
3605
5465
  console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
3606
5466
  console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
3607
5467
  console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
3608
5468
  console.log(` Routines: ${state.routines.join(", ")}`);
3609
- const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc12.bold(state.activeTarget)})` : "");
3610
- console.log(` Current State: ${state.status === "working" ? pc12.yellow("WORKING on " + workingDesc) : pc12.green("IDLE")}`);
5469
+ let currentStateText = pc14.green("IDLE");
5470
+ if (state.status === "working") {
5471
+ const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc14.bold(state.activeTarget)})` : "");
5472
+ currentStateText = pc14.yellow("WORKING on " + workingDesc);
5473
+ } else if (state.status === "paused") {
5474
+ currentStateText = pc14.yellow(pc14.bold("PAUSED"));
5475
+ }
5476
+ console.log(` Current State: ${currentStateText}`);
3611
5477
  if (state.lastReviewCheckAt) {
3612
5478
  console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
3613
5479
  }
@@ -3615,27 +5481,169 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
3615
5481
  console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
3616
5482
  }
3617
5483
  } else {
3618
- console.log(` Status: ${pc12.gray("STOPPED")}`);
3619
- console.log(pc12.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
5484
+ console.log(` Status: ${pc14.gray("STOPPED")}`);
5485
+ console.log(pc14.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
3620
5486
  }
3621
5487
  console.log(`
3622
5488
  Active Worktrees: ${activeWorktrees.length}`);
3623
5489
  for (const wt of activeWorktrees) {
3624
- console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
5490
+ console.log(pc14.dim(` - [${wt.branch}] ${wt.path}`));
3625
5491
  }
3626
5492
  console.log("");
3627
5493
  }
3628
5494
 
5495
+ // src/commands/labels.ts
5496
+ import pc15 from "picocolors";
5497
+ async function runLabels(action = "audit", options = {}) {
5498
+ const cwd = options.cwd || process.cwd();
5499
+ const executor = options.executor || defaultGhExecutor;
5500
+ const repo = await resolveRepoName(options.repo, cwd, executor);
5501
+ const manifest = loadManifest(cwd);
5502
+ const userProtected = manifest?.labels?.protected || [];
5503
+ const protectedPatterns = [
5504
+ ...DEFAULT_PROTECTED_LABEL_PATTERNS,
5505
+ ...userProtected
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
+ }
5535
+ if (action === "prune") {
5536
+ const isDryRun = Boolean(options.dryRun);
5537
+ const result = await pruneLabels({
5538
+ repo,
5539
+ dryRun: isDryRun,
5540
+ yes: options.yes,
5541
+ cwd,
5542
+ protectedPatterns,
5543
+ executor
5544
+ });
5545
+ if (options.json) {
5546
+ console.log(JSON.stringify(result, null, 2));
5547
+ return;
5548
+ }
5549
+ if (isDryRun) {
5550
+ console.log(pc15.bold(pc15.cyan(`
5551
+ \u{1F50D} Label Prune (Dry-Run) for ${repo}
5552
+ `)));
5553
+ if (result.pruned.length === 0) {
5554
+ console.log(pc15.green(" \u2713 No prunable labels found. Repository labels are clean.\n"));
5555
+ } else {
5556
+ console.log(pc15.yellow(` Found ${result.pruned.length} strictly unused label(s) eligible for pruning:`));
5557
+ for (const label of result.pruned) {
5558
+ console.log(` - ${pc15.yellow(label)} (0 issues, 0 PRs, non-schema)`);
5559
+ }
5560
+ console.log(pc15.gray(`
5561
+ Run 'jonah-fleet labels prune --yes' to delete these labels.
5562
+ `));
5563
+ }
5564
+ return;
5565
+ }
5566
+ console.log(pc15.bold(pc15.cyan(`
5567
+ \u{1F9F9} Label Pruning for ${repo}
5568
+ `)));
5569
+ if (result.pruned.length === 0 && result.errors.length === 0) {
5570
+ console.log(pc15.green(" \u2713 No prunable labels found. Repository labels are clean.\n"));
5571
+ return;
5572
+ }
5573
+ if (result.pruned.length > 0) {
5574
+ console.log(pc15.green(` \u2713 Successfully pruned ${result.pruned.length} unused label(s):`));
5575
+ for (const label of result.pruned) {
5576
+ console.log(` - ${pc15.green(label)}`);
5577
+ }
5578
+ }
5579
+ if (result.errors.length > 0) {
5580
+ console.log(pc15.red(`
5581
+ \u274C Failed to delete ${result.errors.length} label(s):`));
5582
+ for (const err of result.errors) {
5583
+ console.log(` - ${pc15.red(err.label)}: ${err.error}`);
5584
+ }
5585
+ }
5586
+ console.log();
5587
+ return;
5588
+ }
5589
+ const rawLabels = await fetchRepoLabels(repo, executor, cwd);
5590
+ const classified = classifyLabels(rawLabels, protectedPatterns);
5591
+ if (options.json) {
5592
+ console.log(
5593
+ JSON.stringify(
5594
+ {
5595
+ repo,
5596
+ totalCount: rawLabels.length,
5597
+ activeCount: classified.active.length,
5598
+ protectedZeroCountCount: classified.protectedZeroCount.length,
5599
+ historicalCount: classified.historical.length,
5600
+ prunableCount: classified.prunable.length,
5601
+ active: classified.active,
5602
+ protectedZeroCount: classified.protectedZeroCount,
5603
+ historical: classified.historical,
5604
+ prunable: classified.prunable
5605
+ },
5606
+ null,
5607
+ 2
5608
+ )
5609
+ );
5610
+ return;
5611
+ }
5612
+ console.log(pc15.bold(pc15.cyan(`
5613
+ \u{1F3F7}\uFE0F Repository Label Audit for ${repo}
5614
+ `)));
5615
+ console.log(` Total Labels: ${pc15.bold(String(rawLabels.length))}`);
5616
+ console.log(` Active (Open items): ${pc15.green(String(classified.active.length))}`);
5617
+ console.log(` Protected (Zero-count): ${pc15.cyan(String(classified.protectedZeroCount.length))}`);
5618
+ console.log(` Historical (Closed): ${pc15.gray(String(classified.historical.length))}`);
5619
+ console.log(` Prunable (Unused): ${classified.prunable.length > 0 ? pc15.yellow(String(classified.prunable.length)) : pc15.green("0")}`);
5620
+ if (classified.prunable.length > 0) {
5621
+ console.log(pc15.bold(pc15.yellow("\n \u26A0\uFE0F Prunable Labels (0 total issues/PRs, non-protected):")));
5622
+ for (const label of classified.prunable) {
5623
+ console.log(` - ${pc15.yellow(label.name)}`);
5624
+ }
5625
+ console.log(pc15.gray(`
5626
+ Run 'jonah-fleet labels prune' to clean up unused boilerplate.
5627
+ `));
5628
+ } else {
5629
+ console.log(pc15.green("\n \u2713 All labels are either active, historical, or protected fleet taxonomy.\n"));
5630
+ }
5631
+ }
5632
+
3629
5633
  // src/index.ts
3630
5634
  var program = new Command();
3631
5635
  program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
3632
- 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) => {
3633
5637
  await runRoutineCommand(routine, options);
3634
5638
  });
3635
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) => {
3636
5640
  await runDaemonCommand(action, options);
3637
5641
  });
3638
- 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) => {
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";
5644
+ await runLabels(act, options);
5645
+ });
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) => {
3639
5647
  await runInit(options);
3640
5648
  });
3641
5649
  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) => {