jonah-fleet 1.6.0 → 1.8.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 (37) hide show
  1. package/CHANGELOG.md +85 -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/index.js +1905 -412
  10. package/dist/lib/daemon-keys.d.ts +103 -0
  11. package/dist/lib/daemon-keys.d.ts.map +1 -0
  12. package/dist/lib/daemon.d.ts +8 -2
  13. package/dist/lib/daemon.d.ts.map +1 -1
  14. package/dist/lib/labels.d.ts +48 -0
  15. package/dist/lib/labels.d.ts.map +1 -0
  16. package/dist/lib/manifest.d.ts +6 -0
  17. package/dist/lib/manifest.d.ts.map +1 -1
  18. package/dist/lib/presets.d.ts +46 -0
  19. package/dist/lib/presets.d.ts.map +1 -1
  20. package/dist/lib/runner.d.ts +66 -0
  21. package/dist/lib/runner.d.ts.map +1 -1
  22. package/dist/lib/terminal-card.d.ts +9 -0
  23. package/dist/lib/terminal-card.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/schema.json +68 -0
  26. package/templates/prompts/ORCHESTRATION.md +36 -31
  27. package/templates/prompts/autowork.md +33 -23
  28. package/templates/prompts/issues-housekeeping.md +1 -1
  29. package/templates/prompts/peer-review.md +13 -3
  30. package/templates/workflows/autowork-cron.yml +41 -2
  31. package/templates/workflows/dependency-check-cron.yml +41 -2
  32. package/templates/workflows/issues-housekeeping-cron.yml +41 -2
  33. package/templates/workflows/prompt-optimizer-cron.yml +41 -2
  34. package/templates/workflows/trigger-autowork-manual.yml +41 -2
  35. package/templates/workflows/trigger-autowork-on-bug.yml +45 -7
  36. package/templates/workflows/trigger-autowork-on-merge.yml +46 -7
  37. package/templates/workflows/trigger-review-routine.yml +46 -7
package/dist/index.js CHANGED
@@ -12,6 +12,33 @@ 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.7-flash-high",
17
+ "issues-housekeeping": "gemini-3.7-flash",
18
+ "dependency-update-security-check": "gemini-3.7-flash",
19
+ "analytics-review": "gemini-3.7-flash"
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
+ },
32
+ maxIterations: {
33
+ autowork: 65,
34
+ "peer-review": 40,
35
+ optimizer: 30,
36
+ "issues-housekeeping": 30,
37
+ "dependency-update-security-check": 20,
38
+ "product-planning": 40,
39
+ "analytics-review": 25
40
+ }
41
+ };
15
42
  var PRESET_CONFIGS = {
16
43
  minimal: {
17
44
  routines: {
@@ -129,6 +156,12 @@ function createDefaultManifest(preset = "standard") {
129
156
  "analytics-review": false
130
157
  },
131
158
  skills: PRESET_CONFIGS.standard.skills,
159
+ models: { ...DEFAULT_MODELS_CONFIG },
160
+ budgets: {
161
+ weeklyTokens: DEFAULT_BUDGETS_CONFIG.weeklyTokens,
162
+ timeoutMinutes: { ...DEFAULT_BUDGETS_CONFIG.timeoutMinutes },
163
+ maxIterations: { ...DEFAULT_BUDGETS_CONFIG.maxIterations }
164
+ },
132
165
  autoUpdate: {
133
166
  enabled: true,
134
167
  channel: "stable"
@@ -142,6 +175,12 @@ function createDefaultManifest(preset = "standard") {
142
175
  preset,
143
176
  routines: { ...config.routines },
144
177
  skills: [...config.skills],
178
+ models: { ...DEFAULT_MODELS_CONFIG },
179
+ budgets: {
180
+ weeklyTokens: DEFAULT_BUDGETS_CONFIG.weeklyTokens,
181
+ timeoutMinutes: { ...DEFAULT_BUDGETS_CONFIG.timeoutMinutes },
182
+ maxIterations: { ...DEFAULT_BUDGETS_CONFIG.maxIterations }
183
+ },
145
184
  autoUpdate: {
146
185
  enabled: true,
147
186
  channel: "stable"
@@ -629,235 +668,10 @@ function installFleet(targetDir, manifest, options = {}) {
629
668
  return result;
630
669
  }
631
670
 
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
671
  // src/lib/fleet-query.ts
858
672
  import { execFile } from "child_process";
859
- import fs5 from "fs";
860
- import path5 from "path";
673
+ import fs4 from "fs";
674
+ import path4 from "path";
861
675
  import { promisify } from "util";
862
676
  var execFileAsync = promisify(execFile);
863
677
  var defaultGhExecutor = async (args) => {
@@ -1036,11 +850,11 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
1036
850
  staleWarnings: []
1037
851
  };
1038
852
  try {
1039
- if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
1040
- const manifestPath = path5.join(repoIdentifier, "agents-manifest.json");
1041
- if (fs5.existsSync(manifestPath)) {
853
+ if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
854
+ const manifestPath = path4.join(repoIdentifier, "agents-manifest.json");
855
+ if (fs4.existsSync(manifestPath)) {
1042
856
  try {
1043
- const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
857
+ const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
1044
858
  result.fleetVersion = raw.version;
1045
859
  result.preset = raw.preset;
1046
860
  } catch {
@@ -1120,18 +934,18 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
1120
934
  if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
1121
935
  }
1122
936
  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)) {
937
+ if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
938
+ const logsDir = path4.join(repoIdentifier, ".github/prompts/logs");
939
+ if (fs4.existsSync(logsDir)) {
1126
940
  const collectLogs = (dir) => {
1127
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
941
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
1128
942
  for (const entry of entries) {
1129
- const fullPath = path5.join(dir, entry.name);
943
+ const fullPath = path4.join(dir, entry.name);
1130
944
  if (entry.isDirectory()) {
1131
945
  collectLogs(fullPath);
1132
946
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1133
947
  try {
1134
- logContents.push(fs5.readFileSync(fullPath, "utf8"));
948
+ logContents.push(fs4.readFileSync(fullPath, "utf8"));
1135
949
  } catch {
1136
950
  }
1137
951
  }
@@ -1251,47 +1065,496 @@ function summarizeFleet(statuses) {
1251
1065
  return summary;
1252
1066
  }
1253
1067
 
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`;
1068
+ // src/lib/labels.ts
1069
+ var DEFAULT_PROTECTED_LABEL_PATTERNS = [
1070
+ "priority/*",
1071
+ "type/*",
1072
+ "size/*",
1073
+ "needs-triage",
1074
+ "ready-for-agent",
1075
+ "needs-human",
1076
+ "needs-info",
1077
+ "needs-design",
1078
+ "wontfix",
1079
+ "measurement",
1080
+ "blocked",
1081
+ "autorelease:*",
1082
+ "dependencies",
1083
+ "security"
1084
+ ];
1085
+ function isLabelProtected(labelName, protectedPatterns = DEFAULT_PROTECTED_LABEL_PATTERNS) {
1086
+ for (const pattern of protectedPatterns) {
1087
+ if (pattern === labelName) {
1088
+ return true;
1089
+ }
1090
+ if (pattern.includes("*")) {
1091
+ const regexPattern = "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
1092
+ const regex = new RegExp(regexPattern);
1093
+ if (regex.test(labelName)) {
1094
+ return true;
1095
+ }
1096
+ }
1262
1097
  }
1263
- return num.toString();
1098
+ return false;
1264
1099
  }
1265
- function formatCurrency(amount) {
1266
- return `$${amount.toFixed(2)}`;
1100
+ function classifyLabels(labels, protectedPatterns = DEFAULT_PROTECTED_LABEL_PATTERNS) {
1101
+ const classified = {
1102
+ active: [],
1103
+ protectedZeroCount: [],
1104
+ historical: [],
1105
+ prunable: [],
1106
+ all: []
1107
+ };
1108
+ for (const label of labels) {
1109
+ const isZeroTotal = label.totalIssuesCount === 0 && label.totalPullRequestsCount === 0;
1110
+ const hasOpenItems = label.openIssuesCount > 0 || label.openPullRequestsCount > 0;
1111
+ let category;
1112
+ if (isZeroTotal) {
1113
+ if (isLabelProtected(label.name, protectedPatterns)) {
1114
+ category = "protected_zero_count";
1115
+ const item = { ...label, category };
1116
+ classified.protectedZeroCount.push(item);
1117
+ classified.all.push(item);
1118
+ } else {
1119
+ category = "prunable";
1120
+ const item = { ...label, category };
1121
+ classified.prunable.push(item);
1122
+ classified.all.push(item);
1123
+ }
1124
+ } else if (hasOpenItems) {
1125
+ category = "active";
1126
+ const item = { ...label, category };
1127
+ classified.active.push(item);
1128
+ classified.all.push(item);
1129
+ } else {
1130
+ category = "historical";
1131
+ const item = { ...label, category };
1132
+ classified.historical.push(item);
1133
+ classified.all.push(item);
1134
+ }
1135
+ }
1136
+ return classified;
1267
1137
  }
1268
- function renderFleetDashboard(statuses, options = {}) {
1269
- const summary = summarizeFleet(statuses);
1270
- if (options.json) {
1271
- return JSON.stringify(
1272
- {
1273
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1274
- summary,
1275
- repositories: statuses
1276
- },
1277
- null,
1278
- 2
1279
- );
1138
+ async function resolveRepoName(repoIdentifier, cwd = process.cwd(), executor = defaultGhExecutor) {
1139
+ if (repoIdentifier && repoIdentifier.includes("/")) {
1140
+ return repoIdentifier;
1280
1141
  }
1281
- const lines = [];
1282
- lines.push(pc3.bold(pc3.cyan("\n\u{1F4CA} Jonah Fleet Multi-Repo Monitor\n")));
1283
- if (statuses.length === 0) {
1284
- lines.push(pc3.yellow(" No repositories configured in fleet registry."));
1285
- lines.push(pc3.gray(" Use `jonah-fleet monitor --add <owner/repo>` to register repositories.\n"));
1286
- return lines.join("\n");
1142
+ if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_REPOSITORY.includes("/")) {
1143
+ return process.env.GITHUB_REPOSITORY;
1287
1144
  }
1288
- for (const s of statuses) {
1289
- const versionStr = s.fleetVersion ? `v${s.fleetVersion}` : "unmanaged";
1290
- const presetStr = s.preset ? `preset: ${s.preset}` : "";
1291
- const headerInfo = [versionStr, presetStr].filter(Boolean).join(", ");
1292
- lines.push(pc3.bold(`\u{1F4E6} ${pc3.cyan(s.repo)} ${pc3.gray(`(${headerInfo})`)}`));
1293
- if (s.error) {
1294
- lines.push(pc3.red(` \u274C Error: ${s.error}`));
1145
+ try {
1146
+ const raw = await executor(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]);
1147
+ const trimmed = raw.trim();
1148
+ if (trimmed && trimmed.includes("/")) {
1149
+ return trimmed;
1150
+ }
1151
+ } catch {
1152
+ }
1153
+ return repoIdentifier || "current";
1154
+ }
1155
+ var LABELS_GRAPHQL_QUERY = `
1156
+ query($owner: String!, $repo: String!, $cursor: String) {
1157
+ repository(owner: $owner, name: $repo) {
1158
+ labels(first: 100, after: $cursor) {
1159
+ nodes {
1160
+ id
1161
+ name
1162
+ description
1163
+ color
1164
+ issues(states: [OPEN]) {
1165
+ totalCount
1166
+ }
1167
+ allIssues: issues {
1168
+ totalCount
1169
+ }
1170
+ pullRequests(states: [OPEN]) {
1171
+ totalCount
1172
+ }
1173
+ allPullRequests: pullRequests {
1174
+ totalCount
1175
+ }
1176
+ }
1177
+ pageInfo {
1178
+ hasNextPage
1179
+ endCursor
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ `;
1185
+ async function fetchRepoLabels(repoIdentifier, executor = defaultGhExecutor, cwd = process.cwd()) {
1186
+ const fullRepo = await resolveRepoName(repoIdentifier, cwd, executor);
1187
+ const [owner, repo] = fullRepo.split("/");
1188
+ if (!owner || !repo) {
1189
+ throw new Error(`Invalid repository identifier '${fullRepo}'. Expected format 'owner/repo'.`);
1190
+ }
1191
+ const results = [];
1192
+ let cursor = null;
1193
+ let hasNextPage = true;
1194
+ while (hasNextPage) {
1195
+ const queryArgs = [
1196
+ "api",
1197
+ "graphql",
1198
+ "-f",
1199
+ `query=${LABELS_GRAPHQL_QUERY}`,
1200
+ "-F",
1201
+ `owner=${owner}`,
1202
+ "-F",
1203
+ `repo=${repo}`
1204
+ ];
1205
+ if (cursor) {
1206
+ queryArgs.push("-F", `cursor=${cursor}`);
1207
+ }
1208
+ const raw = await executor(queryArgs);
1209
+ const parsed = JSON.parse(raw);
1210
+ if (parsed.errors && parsed.errors.length > 0) {
1211
+ throw new Error(`GitHub GraphQL query failed: ${parsed.errors[0].message}`);
1212
+ }
1213
+ const labelConnection = parsed.data?.repository?.labels;
1214
+ if (!labelConnection || !Array.isArray(labelConnection.nodes)) {
1215
+ break;
1216
+ }
1217
+ for (const node of labelConnection.nodes) {
1218
+ results.push({
1219
+ id: node.id,
1220
+ name: node.name,
1221
+ description: node.description ?? null,
1222
+ color: node.color,
1223
+ openIssuesCount: node.issues?.totalCount || 0,
1224
+ totalIssuesCount: node.allIssues?.totalCount || 0,
1225
+ openPullRequestsCount: node.pullRequests?.totalCount || 0,
1226
+ totalPullRequestsCount: node.allPullRequests?.totalCount || 0
1227
+ });
1228
+ }
1229
+ hasNextPage = Boolean(labelConnection.pageInfo?.hasNextPage);
1230
+ cursor = labelConnection.pageInfo?.endCursor || null;
1231
+ if (!cursor) {
1232
+ break;
1233
+ }
1234
+ }
1235
+ return results;
1236
+ }
1237
+ async function pruneLabels(options = {}) {
1238
+ const cwd = options.cwd || process.cwd();
1239
+ const executor = options.executor || defaultGhExecutor;
1240
+ const repo = await resolveRepoName(options.repo, cwd, executor);
1241
+ const manifest = loadManifest(cwd);
1242
+ const userProtected = manifest?.labels?.protected || [];
1243
+ const protectedPatterns = options.protectedPatterns || [
1244
+ ...DEFAULT_PROTECTED_LABEL_PATTERNS,
1245
+ ...userProtected
1246
+ ];
1247
+ const rawLabels = await fetchRepoLabels(repo, executor, cwd);
1248
+ const classified = classifyLabels(rawLabels, protectedPatterns);
1249
+ const result = {
1250
+ repo,
1251
+ classified,
1252
+ pruned: [],
1253
+ skipped: [],
1254
+ errors: [],
1255
+ dryRun: Boolean(options.dryRun)
1256
+ };
1257
+ for (const item of classified.prunable) {
1258
+ if (options.dryRun) {
1259
+ result.pruned.push(item.name);
1260
+ } else {
1261
+ try {
1262
+ const deleteArgs = ["label", "delete", item.name, "--yes"];
1263
+ if (repo && repo !== "current") {
1264
+ deleteArgs.push("--repo", repo);
1265
+ }
1266
+ await executor(deleteArgs);
1267
+ result.pruned.push(item.name);
1268
+ } catch (err) {
1269
+ result.errors.push({
1270
+ label: item.name,
1271
+ error: err.message || String(err)
1272
+ });
1273
+ }
1274
+ }
1275
+ }
1276
+ return result;
1277
+ }
1278
+
1279
+ // src/commands/init.ts
1280
+ async function promptQuestion(query, defaultValue) {
1281
+ const rl = readline.createInterface({
1282
+ input: process.stdin,
1283
+ output: process.stdout
1284
+ });
1285
+ return new Promise((resolve) => {
1286
+ rl.question(`${query} [${defaultValue}]: `, (answer) => {
1287
+ rl.close();
1288
+ resolve(answer.trim() || defaultValue);
1289
+ });
1290
+ });
1291
+ }
1292
+ async function runInit(options = {}) {
1293
+ const cwd = options.cwd || process.cwd();
1294
+ const preset = options.preset || "standard";
1295
+ console.log(pc.cyan(`
1296
+ \u2693 Initializing Jonah Fleet (preset: ${pc.bold(preset)}) in ${cwd}
1297
+ `));
1298
+ let detected = detectTechStack(cwd);
1299
+ console.log(pc.bold("\u{1F50D} Tech Stack Auto-Detection:"));
1300
+ console.log(pc.cyan(` - Detected Stack: ${pc.bold(detected.name)}`));
1301
+ console.log(pc.cyan(` - Language: ${detected.language}`));
1302
+ if (detected.framework) {
1303
+ console.log(pc.cyan(` - Framework: ${detected.framework}`));
1304
+ }
1305
+ console.log(pc.cyan(` - Package Manager: ${detected.packageManager}`));
1306
+ if (detected.testFramework) {
1307
+ console.log(pc.cyan(` - Testing: ${detected.testFramework}`));
1308
+ }
1309
+ if (detected.commands.test) {
1310
+ console.log(pc.cyan(` - Test Command: ${detected.commands.test}`));
1311
+ }
1312
+ const isInteractive = options.interactive ?? (process.stdin.isTTY && !options.stack && !options.testCmd);
1313
+ if (isInteractive && process.stdin.isTTY) {
1314
+ console.log(pc.yellow("\n\u2699\uFE0F Configure project settings (press enter to accept defaults):"));
1315
+ const stackName = await promptQuestion("Tech Stack Name", detected.name);
1316
+ const pkgManager = await promptQuestion("Package Manager", detected.packageManager);
1317
+ const testCmd = await promptQuestion("Test Command", detected.commands.test || "npm test");
1318
+ const buildCmd = await promptQuestion("Build Command", detected.commands.build || "npm run build");
1319
+ detected = {
1320
+ ...detected,
1321
+ name: stackName,
1322
+ language: stackName,
1323
+ framework: void 0,
1324
+ packageManager: pkgManager,
1325
+ commands: {
1326
+ ...detected.commands,
1327
+ test: testCmd,
1328
+ build: buildCmd
1329
+ }
1330
+ };
1331
+ }
1332
+ if (options.stack) {
1333
+ detected.name = options.stack;
1334
+ detected.language = options.stack;
1335
+ detected.framework = void 0;
1336
+ }
1337
+ if (options.packageManager) {
1338
+ detected.packageManager = options.packageManager;
1339
+ }
1340
+ if (options.testCmd) {
1341
+ detected.commands.test = options.testCmd;
1342
+ }
1343
+ if (options.buildCmd) {
1344
+ detected.commands.build = options.buildCmd;
1345
+ }
1346
+ let manifest = loadManifest(cwd);
1347
+ if (manifest && !options.force) {
1348
+ console.log(pc.yellow(`
1349
+ \u26A0\uFE0F Found existing agents-manifest.json. Updating with preset '${preset}'...`));
1350
+ } else {
1351
+ manifest = createDefaultManifest(preset);
1352
+ }
1353
+ saveManifest(cwd, manifest);
1354
+ console.log(pc.green(`\u2713 Created/Updated agents-manifest.json`));
1355
+ const result = installFleet(cwd, manifest, { force: options.force, detectedStack: detected });
1356
+ console.log(pc.bold("\nInstalled components:"));
1357
+ if (result.promptsInstalled.length > 0) {
1358
+ console.log(pc.green(` \u{1F4C1} Prompts (.github/prompts/):`));
1359
+ result.promptsInstalled.forEach((p) => console.log(` - ${p}`));
1360
+ }
1361
+ if (result.workflowsInstalled.length > 0) {
1362
+ console.log(pc.green(` \u2699\uFE0F Workflows (.github/workflows/):`));
1363
+ result.workflowsInstalled.forEach((w) => console.log(` - ${w}`));
1364
+ }
1365
+ if (result.skillsInstalled.length > 0) {
1366
+ console.log(pc.green(` \u{1F9E0} Skills (.agents/skills/):`));
1367
+ result.skillsInstalled.forEach((s) => console.log(` - ${s}`));
1368
+ }
1369
+ if (result.docsInstalled.length > 0) {
1370
+ console.log(pc.green(` \u{1F4C4} Documentation:`));
1371
+ result.docsInstalled.forEach((d) => console.log(` - ${d}`));
1372
+ }
1373
+ if (options.pruneLabels) {
1374
+ try {
1375
+ console.log(pc.bold("\n\u{1F3F7}\uFE0F Pruning unused boilerplate labels..."));
1376
+ const pruneRes = await pruneLabels({ cwd, yes: true, dryRun: false, executor: options.executor });
1377
+ if (pruneRes.pruned.length > 0) {
1378
+ console.log(pc.green(` \u2713 Pruned ${pruneRes.pruned.length} unused boilerplate label(s): ${pruneRes.pruned.join(", ")}`));
1379
+ } else {
1380
+ console.log(pc.green(" \u2713 No unused boilerplate labels found."));
1381
+ }
1382
+ } catch (err) {
1383
+ console.log(pc.yellow(` \u26A0\uFE0F Could not prune labels: ${err.message}`));
1384
+ }
1385
+ }
1386
+ console.log(pc.bold(pc.green("\n\u{1F389} Jonah Fleet initialization complete!\n")));
1387
+ console.log(pc.cyan("Next steps for GitHub repository configuration:"));
1388
+ console.log(" 1. In Settings \u2192 Actions \u2192 General \u2192 Workflow permissions:");
1389
+ console.log(' Select "Read and write permissions" and check "Allow GitHub Actions to create and approve pull requests".');
1390
+ console.log(" 2. In Settings \u2192 Actions \u2192 General \u2192 Fork pull request workflows:");
1391
+ console.log(" Configure workflow approval settings to prevent automated runs from stalling awaiting approval.");
1392
+ console.log(" 3. Customize project context, build, and test commands in AGENTS.md.\n");
1393
+ }
1394
+
1395
+ // src/commands/sync.ts
1396
+ import pc2 from "picocolors";
1397
+
1398
+ // src/lib/diff.ts
1399
+ import fs5 from "fs";
1400
+ import path5 from "path";
1401
+ function checkDrift(targetDir, manifest) {
1402
+ const templatesDir = getTemplatesDir();
1403
+ const report = {
1404
+ missingPrompts: [],
1405
+ modifiedPrompts: [],
1406
+ missingWorkflows: [],
1407
+ modifiedWorkflows: [],
1408
+ missingSkills: []
1409
+ };
1410
+ const targetPromptsDir = path5.join(targetDir, ".github/prompts");
1411
+ const targetWorkflowsDir = path5.join(targetDir, ".github/workflows");
1412
+ const targetSkillsDir = path5.join(targetDir, ".agents/skills");
1413
+ const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
1414
+ for (const file of basePrompts) {
1415
+ const src = path5.join(templatesDir, "prompts", file);
1416
+ const dest = path5.join(targetPromptsDir, file);
1417
+ if (!fs5.existsSync(dest)) {
1418
+ report.missingPrompts.push(file);
1419
+ } else if (fs5.readFileSync(src, "utf8") !== fs5.readFileSync(dest, "utf8")) {
1420
+ report.modifiedPrompts.push(file);
1421
+ }
1422
+ }
1423
+ for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
1424
+ if (!isEnabled) continue;
1425
+ const promptFile = `${routineName}.md`;
1426
+ const promptSrc = path5.join(templatesDir, "prompts", promptFile);
1427
+ const promptDest = path5.join(targetPromptsDir, promptFile);
1428
+ if (!fs5.existsSync(promptDest)) {
1429
+ report.missingPrompts.push(promptFile);
1430
+ } else if (fs5.existsSync(promptSrc) && fs5.readFileSync(promptSrc, "utf8") !== fs5.readFileSync(promptDest, "utf8")) {
1431
+ report.modifiedPrompts.push(promptFile);
1432
+ }
1433
+ const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
1434
+ for (const workflowFile of workflows) {
1435
+ const wfSrc = path5.join(templatesDir, "workflows", workflowFile);
1436
+ const wfDest = path5.join(targetWorkflowsDir, workflowFile);
1437
+ if (!fs5.existsSync(wfDest)) {
1438
+ report.missingWorkflows.push(workflowFile);
1439
+ } else if (fs5.existsSync(wfSrc)) {
1440
+ const rawSrc = fs5.readFileSync(wfSrc, "utf8");
1441
+ const destContent = fs5.readFileSync(wfDest, "utf8");
1442
+ const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
1443
+ const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
1444
+ if (expectedSrc !== destContent) {
1445
+ report.modifiedWorkflows.push(workflowFile);
1446
+ }
1447
+ }
1448
+ }
1449
+ }
1450
+ if (manifest.autoUpdate?.enabled) {
1451
+ const syncWfSrc = path5.join(templatesDir, "workflows/sync-fleet.yml");
1452
+ const syncWfDest = path5.join(targetWorkflowsDir, "sync-fleet.yml");
1453
+ if (!fs5.existsSync(syncWfDest)) {
1454
+ report.missingWorkflows.push("sync-fleet.yml");
1455
+ } else if (fs5.existsSync(syncWfSrc)) {
1456
+ const rawSrc = fs5.readFileSync(syncWfSrc, "utf8");
1457
+ const destContent = fs5.readFileSync(syncWfDest, "utf8");
1458
+ const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
1459
+ const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
1460
+ if (expectedSrc !== destContent) {
1461
+ report.modifiedWorkflows.push("sync-fleet.yml");
1462
+ }
1463
+ }
1464
+ }
1465
+ for (const skill of manifest.skills) {
1466
+ const skillDestDir = path5.join(targetSkillsDir, skill);
1467
+ if (!fs5.existsSync(skillDestDir)) {
1468
+ report.missingSkills.push(skill);
1469
+ }
1470
+ }
1471
+ return report;
1472
+ }
1473
+
1474
+ // src/commands/sync.ts
1475
+ async function runSync(options = {}) {
1476
+ const cwd = options.cwd || process.cwd();
1477
+ const manifest = loadManifest(cwd);
1478
+ if (!manifest) {
1479
+ console.error(pc2.red(`\u274C No agents-manifest.json found in ${cwd}. Run 'jonah-fleet init' first.`));
1480
+ process.exit(1);
1481
+ }
1482
+ console.log(pc2.cyan(`
1483
+ \u{1F504} Syncing Jonah Fleet (current: v${manifest.version}, fleet: v${FLEET_VERSION})...
1484
+ `));
1485
+ const drift = checkDrift(cwd, manifest);
1486
+ const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
1487
+ if (options.check) {
1488
+ if (!hasDrift && manifest.version === FLEET_VERSION) {
1489
+ console.log(pc2.green(`\u2713 All routines, workflows, and skills are perfectly in sync with v${FLEET_VERSION}.
1490
+ `));
1491
+ return;
1492
+ }
1493
+ console.log(pc2.yellow(`\u26A0\uFE0F Drift or updates detected:`));
1494
+ if (drift.missingPrompts.length > 0) console.log(pc2.red(` Missing prompts: ${drift.missingPrompts.join(", ")}`));
1495
+ if (drift.modifiedPrompts.length > 0) console.log(pc2.yellow(` Modified prompts: ${drift.modifiedPrompts.join(", ")}`));
1496
+ if (drift.missingWorkflows.length > 0) console.log(pc2.red(` Missing workflows: ${drift.missingWorkflows.join(", ")}`));
1497
+ if (drift.modifiedWorkflows.length > 0) console.log(pc2.yellow(` Modified workflows: ${drift.modifiedWorkflows.join(", ")}`));
1498
+ if (drift.missingSkills.length > 0) console.log(pc2.red(` Missing skills: ${drift.missingSkills.join(", ")}`));
1499
+ console.log(pc2.cyan(`
1500
+ Run 'jonah-fleet sync --force' to apply updates.
1501
+ `));
1502
+ return;
1503
+ }
1504
+ manifest.version = FLEET_VERSION;
1505
+ saveManifest(cwd, manifest);
1506
+ const result = installFleet(cwd, manifest, { force: true });
1507
+ console.log(pc2.green(`\u2713 Synchronized with Jonah Fleet v${FLEET_VERSION}`));
1508
+ console.log(pc2.green(`\u2713 Updated ${result.promptsInstalled.length} prompts, ${result.workflowsInstalled.length} workflows, and ${result.skillsInstalled.length} skills.
1509
+ `));
1510
+ }
1511
+
1512
+ // src/commands/status.ts
1513
+ import fs7 from "fs";
1514
+ import path7 from "path";
1515
+ import pc5 from "picocolors";
1516
+
1517
+ // src/lib/dashboard.ts
1518
+ import pc3 from "picocolors";
1519
+ function formatTokens(num) {
1520
+ if (num >= 1e6) {
1521
+ return `${(num / 1e6).toFixed(2)}M`;
1522
+ }
1523
+ if (num >= 1e3) {
1524
+ return `${(num / 1e3).toFixed(1)}k`;
1525
+ }
1526
+ return num.toString();
1527
+ }
1528
+ function formatCurrency(amount) {
1529
+ return `$${amount.toFixed(2)}`;
1530
+ }
1531
+ function renderFleetDashboard(statuses, options = {}) {
1532
+ const summary = summarizeFleet(statuses);
1533
+ if (options.json) {
1534
+ return JSON.stringify(
1535
+ {
1536
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1537
+ summary,
1538
+ repositories: statuses
1539
+ },
1540
+ null,
1541
+ 2
1542
+ );
1543
+ }
1544
+ const lines = [];
1545
+ lines.push(pc3.bold(pc3.cyan("\n\u{1F4CA} Jonah Fleet Multi-Repo Monitor\n")));
1546
+ if (statuses.length === 0) {
1547
+ lines.push(pc3.yellow(" No repositories configured in fleet registry."));
1548
+ lines.push(pc3.gray(" Use `jonah-fleet monitor --add <owner/repo>` to register repositories.\n"));
1549
+ return lines.join("\n");
1550
+ }
1551
+ for (const s of statuses) {
1552
+ const versionStr = s.fleetVersion ? `v${s.fleetVersion}` : "unmanaged";
1553
+ const presetStr = s.preset ? `preset: ${s.preset}` : "";
1554
+ const headerInfo = [versionStr, presetStr].filter(Boolean).join(", ");
1555
+ lines.push(pc3.bold(`\u{1F4E6} ${pc3.cyan(s.repo)} ${pc3.gray(`(${headerInfo})`)}`));
1556
+ if (s.error) {
1557
+ lines.push(pc3.red(` \u274C Error: ${s.error}`));
1295
1558
  }
1296
1559
  lines.push(pc3.bold(" \u{1F512} Active Claims:"));
1297
1560
  if (s.activeClaims.length === 0) {
@@ -1566,6 +1829,8 @@ async function runStatus(options = {}) {
1566
1829
  autoUpdate: manifest.autoUpdate,
1567
1830
  routines: manifest.routines,
1568
1831
  skills: manifest.skills,
1832
+ models: manifest.models,
1833
+ budgets: manifest.budgets,
1569
1834
  repositories: manifest.repositories || [],
1570
1835
  tokenUsage,
1571
1836
  drift: {
@@ -1593,6 +1858,36 @@ async function runStatus(options = {}) {
1593
1858
  for (const skill of manifest.skills) {
1594
1859
  console.log(` - ${pc5.cyan(skill)}`);
1595
1860
  }
1861
+ if (manifest.models && Object.keys(manifest.models).length > 0) {
1862
+ console.log(pc5.bold("\n Model Profiles:"));
1863
+ for (const [routine, model] of Object.entries(manifest.models)) {
1864
+ if (model) {
1865
+ console.log(` - ${routine.padEnd(35)}: ${pc5.cyan(model)}`);
1866
+ }
1867
+ }
1868
+ }
1869
+ if (manifest.budgets) {
1870
+ console.log(pc5.bold("\n Configured Budgets:"));
1871
+ if (manifest.budgets.weeklyTokens) {
1872
+ console.log(` - Weekly Token Budget: ${pc5.cyan(formatTokens(manifest.budgets.weeklyTokens))}`);
1873
+ }
1874
+ if (manifest.budgets.timeoutMinutes && Object.keys(manifest.budgets.timeoutMinutes).length > 0) {
1875
+ console.log(` - Timeouts:`);
1876
+ for (const [routine, timeout] of Object.entries(manifest.budgets.timeoutMinutes)) {
1877
+ if (timeout !== void 0) {
1878
+ console.log(` \u2022 ${routine}: ${pc5.cyan(timeout + "m")}`);
1879
+ }
1880
+ }
1881
+ }
1882
+ if (manifest.budgets.maxIterations && Object.keys(manifest.budgets.maxIterations).length > 0) {
1883
+ console.log(` - Max Iterations:`);
1884
+ for (const [routine, iter] of Object.entries(manifest.budgets.maxIterations)) {
1885
+ if (iter !== void 0) {
1886
+ console.log(` \u2022 ${routine}: ${pc5.cyan(String(iter))}`);
1887
+ }
1888
+ }
1889
+ }
1890
+ }
1596
1891
  if (manifest.repositories && manifest.repositories.length > 0) {
1597
1892
  console.log(pc5.bold("\n Fleet Repositories:"));
1598
1893
  for (const repo of manifest.repositories) {
@@ -2208,7 +2503,7 @@ async function runTelemetry(options = {}) {
2208
2503
  }
2209
2504
 
2210
2505
  // src/commands/run.ts
2211
- import pc10 from "picocolors";
2506
+ import pc11 from "picocolors";
2212
2507
 
2213
2508
  // src/lib/runner.ts
2214
2509
  import fs12 from "fs";
@@ -2345,6 +2640,39 @@ import pc9 from "picocolors";
2345
2640
  function stripAnsi(text) {
2346
2641
  return text.replace(/\x1b\[[0-9;]*m/g, "");
2347
2642
  }
2643
+ function truncateAnsi(text, maxWidth) {
2644
+ if (maxWidth <= 0) return "";
2645
+ if (stripAnsi(text).length <= maxWidth) return text;
2646
+ let visibleCount = 0;
2647
+ let result = "";
2648
+ let inAnsi = false;
2649
+ let ansiBuffer = "";
2650
+ for (let i = 0; i < text.length; i++) {
2651
+ const char = text[i];
2652
+ if (char === "\x1B") {
2653
+ inAnsi = true;
2654
+ ansiBuffer = char;
2655
+ continue;
2656
+ }
2657
+ if (inAnsi) {
2658
+ ansiBuffer += char;
2659
+ if (char === "m") {
2660
+ inAnsi = false;
2661
+ result += ansiBuffer;
2662
+ ansiBuffer = "";
2663
+ }
2664
+ continue;
2665
+ }
2666
+ if (visibleCount < maxWidth) {
2667
+ result += char;
2668
+ visibleCount++;
2669
+ } else {
2670
+ break;
2671
+ }
2672
+ }
2673
+ result += "\x1B[0m";
2674
+ return result;
2675
+ }
2348
2676
  function wrapText(text, maxWidth) {
2349
2677
  if (maxWidth <= 0) return [text];
2350
2678
  const words = text.split(/\s+/).filter(Boolean);
@@ -2570,6 +2898,98 @@ function detectClaimedPR(chunk) {
2570
2898
  if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
2571
2899
  return null;
2572
2900
  }
2901
+ function formatActionDescription(toolName, params) {
2902
+ const name = toolName || "unknown";
2903
+ if (name === "run_command") {
2904
+ const rawCmd = (params?.CommandLine || params?.command || params?.cmd || "").trim();
2905
+ if (!rawCmd) return "Running command";
2906
+ if (/\b(?:vitest|jest)\b/i.test(rawCmd)) return "Running vitest";
2907
+ if (/\bnpm\s+test\b|\bcargo\s+test\b|\bpytest\b/i.test(rawCmd)) return "Running test suite";
2908
+ if (/type-check|\btsc\b/i.test(rawCmd)) return "Running TypeScript type checks";
2909
+ if (/\blint\b|\beslint\b/i.test(rawCmd)) return "Running codebase linter";
2910
+ if (/\bbuild\b|\btsup\b|\bnext\s+build\b/i.test(rawCmd)) return "Running production build";
2911
+ if (/gh\s+pr\s+list/i.test(rawCmd)) return "Listing open PRs (gh pr list)";
2912
+ const prMergeMatch = rawCmd.match(/gh\s+pr\s+merge(?:\s+(\d+))?/i);
2913
+ if (prMergeMatch) {
2914
+ return prMergeMatch[1] ? `Squash-merging PR #${prMergeMatch[1]}` : "Squash-merging pull request";
2915
+ }
2916
+ const prViewMatch = rawCmd.match(/gh\s+pr\s+view(?:\s+(\d+))?/i);
2917
+ if (prViewMatch) {
2918
+ return prViewMatch[1] ? `Viewing PR #${prViewMatch[1]}` : "Viewing pull request";
2919
+ }
2920
+ const prEditMatch = rawCmd.match(/gh\s+pr\s+edit(?:\s+(\d+))?/i);
2921
+ if (prEditMatch) {
2922
+ return prEditMatch[1] ? `Updating PR #${prEditMatch[1]}` : "Updating pull request";
2923
+ }
2924
+ const prReadyMatch = rawCmd.match(/gh\s+pr\s+ready(?:\s+(\d+))?/i);
2925
+ if (prReadyMatch) {
2926
+ return prReadyMatch[1] ? `Marking PR #${prReadyMatch[1]} ready for review` : "Marking PR ready for review";
2927
+ }
2928
+ if (/gh\s+pr\s+create/i.test(rawCmd)) return "Creating pull request";
2929
+ if (/gh\s+issue\s+list/i.test(rawCmd)) return "Listing open issues (gh issue list)";
2930
+ const issueViewMatch = rawCmd.match(/gh\s+issue\s+view(?:\s+(\d+))?/i);
2931
+ if (issueViewMatch) {
2932
+ return issueViewMatch[1] ? `Viewing issue #${issueViewMatch[1]}` : "Viewing issue";
2933
+ }
2934
+ const issueEditMatch = rawCmd.match(/gh\s+issue\s+edit(?:\s+(\d+))?/i);
2935
+ if (issueEditMatch) {
2936
+ return issueEditMatch[1] ? `Updating issue #${issueEditMatch[1]}` : "Updating issue";
2937
+ }
2938
+ const issueCommentMatch = rawCmd.match(/gh\s+issue\s+comment(?:\s+(\d+))?/i);
2939
+ if (issueCommentMatch) {
2940
+ return issueCommentMatch[1] ? `Commenting on issue #${issueCommentMatch[1]}` : "Commenting on issue";
2941
+ }
2942
+ if (/git\s+checkout/i.test(rawCmd)) return "Git: Checking out branch";
2943
+ if (/git\s+status/i.test(rawCmd)) return "Git: Checking status";
2944
+ if (/git\s+diff/i.test(rawCmd)) return "Git: Inspecting diff";
2945
+ if (/git\s+commit/i.test(rawCmd)) return "Git: Committing changes";
2946
+ if (/git\s+push/i.test(rawCmd)) return "Git: Pushing branch";
2947
+ const firstLine = rawCmd.split("\n")[0].trim();
2948
+ return `Running ${firstLine}`;
2949
+ }
2950
+ if (name === "view_file") {
2951
+ const rawPath = params?.AbsolutePath || params?.TargetFile || params?.path || params?.file || "";
2952
+ if (!rawPath) return "Reading file";
2953
+ return `Reading ${path11.basename(rawPath)}`;
2954
+ }
2955
+ if (name === "replace_file_content" || name === "write_to_file" || name === "multi_replace_file_content") {
2956
+ const rawPath = params?.TargetFile || params?.AbsolutePath || params?.path || params?.file || "";
2957
+ if (!rawPath) return "Editing file";
2958
+ return `Editing ${path11.basename(rawPath)}`;
2959
+ }
2960
+ if (name === "grep_search") {
2961
+ const query = params?.Query || params?.query || params?.pattern || "";
2962
+ if (!query) return "Searching codebase";
2963
+ return `Searching codebase for "${query}"`;
2964
+ }
2965
+ if (name === "find_by_name") {
2966
+ const pattern = params?.Pattern || params?.pattern || "";
2967
+ if (!pattern) return "Finding files";
2968
+ return `Finding files matching "${pattern}"`;
2969
+ }
2970
+ if (name === "list_dir") {
2971
+ const dirPath = params?.DirectoryPath || params?.path || "";
2972
+ if (!dirPath) return "Listing directory";
2973
+ const base = path11.basename(dirPath.replace(/[/\\]+$/, "")) || dirPath;
2974
+ return `Listing directory ${base}`;
2975
+ }
2976
+ if (name === "invoke_subagent") {
2977
+ const role = params?.Subagents?.[0]?.Role || params?.Subagents?.[0]?.role || params?.Role || params?.role || params?.TypeName || params?.name || "";
2978
+ if (!role) return "Running subagent";
2979
+ return `Running subagent: ${role}`;
2980
+ }
2981
+ if (name === "search_web") {
2982
+ const query = params?.query || params?.Query || "";
2983
+ if (!query) return "Searching web";
2984
+ return `Searching web for "${query}"`;
2985
+ }
2986
+ if (name === "read_url_content") {
2987
+ const url = params?.Url || params?.url || "";
2988
+ if (!url) return "Reading URL content";
2989
+ return `Reading URL ${url}`;
2990
+ }
2991
+ return `Tool: ${name}`;
2992
+ }
2573
2993
  function renderSummaryCard(options) {
2574
2994
  const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 64), 90);
2575
2995
  const horizontal = "\u2500".repeat(width - 2);
@@ -2828,15 +3248,27 @@ var TerminalSpinner = class {
2828
3248
  `);
2829
3249
  }
2830
3250
  }
2831
- render() {
2832
- if (!this.isRunning || !this.isTTY) return;
3251
+ formatLine(message, maxWidth) {
3252
+ const cols = maxWidth ?? (process.stderr.columns || process.stdout.columns || 80);
2833
3253
  const frame = pc9.cyan(this.frames[this.currentFrame]);
2834
- this.currentFrame = (this.currentFrame + 1) % this.frames.length;
2835
3254
  const elapsedSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
2836
3255
  const mins = Math.floor(elapsedSeconds / 60);
2837
3256
  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}`);
3257
+ const timePlain = `[${mins}m ${secs < 10 ? "0" : ""}${secs}s]`;
3258
+ const timeStr = pc9.dim(timePlain);
3259
+ const fixedWidth = 6 + timePlain.length;
3260
+ const availableMsgWidth = Math.max(0, cols - fixedWidth - 1);
3261
+ let truncatedMsg = message;
3262
+ if (stripAnsi(message).length > availableMsgWidth) {
3263
+ truncatedMsg = availableMsgWidth > 3 ? truncateAnsi(message, availableMsgWidth - 3) + "..." : truncateAnsi(message, availableMsgWidth);
3264
+ }
3265
+ return ` ${frame} ${truncatedMsg} ${timeStr}`;
3266
+ }
3267
+ render() {
3268
+ if (!this.isRunning || !this.isTTY) return;
3269
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
3270
+ const line = this.formatLine(this.message);
3271
+ process.stderr.write(`\r\x1B[K${line}`);
2840
3272
  }
2841
3273
  stop() {
2842
3274
  if (!this.isRunning) return;
@@ -2852,6 +3284,97 @@ var TerminalSpinner = class {
2852
3284
  };
2853
3285
 
2854
3286
  // src/lib/runner.ts
3287
+ import pc10 from "picocolors";
3288
+ var LineBufferedStreamParser = class {
3289
+ buffer = "";
3290
+ onLine;
3291
+ constructor(onLine) {
3292
+ this.onLine = onLine;
3293
+ }
3294
+ feed(chunk) {
3295
+ this.buffer += chunk;
3296
+ const lines = this.buffer.split("\n");
3297
+ this.buffer = lines.pop() ?? "";
3298
+ for (const line of lines) {
3299
+ const trimmed = line.trim();
3300
+ if (trimmed.length > 0) {
3301
+ this.onLine(trimmed);
3302
+ }
3303
+ }
3304
+ }
3305
+ flush() {
3306
+ if (this.buffer.trim().length > 0) {
3307
+ this.onLine(this.buffer.trim());
3308
+ this.buffer = "";
3309
+ }
3310
+ }
3311
+ };
3312
+ function parseStreamJsonEvent(line) {
3313
+ if (!line || !line.trim()) return null;
3314
+ try {
3315
+ const parsed = JSON.parse(line);
3316
+ if (parsed && typeof parsed === "object") {
3317
+ return parsed;
3318
+ }
3319
+ return null;
3320
+ } catch {
3321
+ return null;
3322
+ }
3323
+ }
3324
+ function formatVerboseEvent(event) {
3325
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
3326
+ if (event.event === "init") {
3327
+ return `${pc10.dim(`[${time}]`)} ${pc10.cyan("[init]")} Session started (conversation: ${event.conversation_id || "n/a"})`;
3328
+ }
3329
+ if (event.event === "step_update" && event.step_update) {
3330
+ const su = event.step_update;
3331
+ if (su.step_type === "user_input") {
3332
+ return `${pc10.dim(`[${time}]`)} ${pc10.magenta("[user_input]")} Prompt dispatched`;
3333
+ }
3334
+ if (su.step_type === "tool") {
3335
+ const toolName = su.tool_name || su.tool_info?.name || "tool";
3336
+ const params = su.tool_info?.parameters;
3337
+ if (su.state === "ACTIVE") {
3338
+ const desc = formatActionDescription(toolName, params);
3339
+ return `${pc10.dim(`[${time}]`)} ${pc10.blue("[tool:start]")} ${pc10.bold(toolName)} \u2192 ${desc}`;
3340
+ }
3341
+ if (su.state === "DONE") {
3342
+ const dur = su.duration_seconds !== void 0 ? `${su.duration_seconds.toFixed(1)}s` : "done";
3343
+ return `${pc10.dim(`[${time}]`)} ${pc10.green("[tool:done]")} ${pc10.bold(toolName)} (${dur})`;
3344
+ }
3345
+ }
3346
+ if (su.step_type === "agent_response" || su.step_type === "thought") {
3347
+ if (su.text_delta) {
3348
+ return su.text_delta;
3349
+ }
3350
+ if (su.state === "DONE") {
3351
+ const dur = su.duration_seconds !== void 0 ? ` (${su.duration_seconds.toFixed(1)}s)` : "";
3352
+ return `${pc10.dim(`[${time}]`)} ${pc10.cyan("[agent:step]")} Step ${su.step_index ?? 0} finished${dur}`;
3353
+ }
3354
+ }
3355
+ }
3356
+ if (event.event === "result" && event.result) {
3357
+ const res = event.result;
3358
+ const dur = res.duration_seconds !== void 0 ? `${res.duration_seconds.toFixed(1)}s` : "";
3359
+ const tokens = res.usage?.total_tokens ? `${res.usage.total_tokens.toLocaleString()} tokens` : "";
3360
+ const metrics = [dur, tokens].filter(Boolean).join(", ");
3361
+ return `${pc10.dim(`[${time}]`)} ${pc10.bold(pc10.green("[result]"))} ${res.status || "COMPLETED"} (${metrics || "done"})`;
3362
+ }
3363
+ return null;
3364
+ }
3365
+ function buildAgyArgs(prompt, model, printTimeout) {
3366
+ return [
3367
+ "-p",
3368
+ prompt,
3369
+ "--model",
3370
+ model,
3371
+ "--output-format",
3372
+ "stream-json",
3373
+ "--print-timeout",
3374
+ printTimeout,
3375
+ "--dangerously-skip-permissions"
3376
+ ];
3377
+ }
2855
3378
  function discoverSkillsPrompt(targetDir) {
2856
3379
  const skillsDir = path12.join(targetDir, ".agents", "skills");
2857
3380
  if (!fs12.existsSync(skillsDir)) return "";
@@ -2936,18 +3459,10 @@ Timeout: ${printTimeout}`,
2936
3459
  TARGET_ISSUE: options.issue ? String(options.issue) : "",
2937
3460
  PR_NUMBER: options.pr ? String(options.pr) : ""
2938
3461
  };
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
- ];
3462
+ const args = buildAgyArgs(prompt, model, printTimeout);
2950
3463
  let output = "";
3464
+ let finalResponseText = "";
3465
+ let accumulatedOutput = "";
2951
3466
  let exitCode = 0;
2952
3467
  const startTime = Date.now();
2953
3468
  const logDir = path12.join(targetDir, ".jonah-fleet");
@@ -2973,39 +3488,127 @@ Timeout: ${printTimeout}`,
2973
3488
  };
2974
3489
  process.once("SIGINT", sigintHandler);
2975
3490
  process.once("SIGTERM", sigintHandler);
2976
- const processChunk = (chunk, isStderr = false) => {
2977
- output += chunk;
2978
- try {
2979
- fs12.appendFileSync(logFilePath, chunk, "utf8");
2980
- } catch {
3491
+ const checkTargetDetection = (text) => {
3492
+ if (dynamicTargetDetected || !text) return;
3493
+ const detected = routine === "peer-review" ? detectClaimedPR(text) : detectClaimedIssue(text);
3494
+ if (detected) {
3495
+ dynamicTargetDetected = true;
3496
+ targetLabel = detected;
3497
+ options.onTargetDetected?.(detected);
3498
+ if (spinner) {
3499
+ spinner.update(`${targetLabel}: ${activePhase}`);
3500
+ }
2981
3501
  }
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) {
3502
+ };
3503
+ const stdoutParser = new LineBufferedStreamParser((line) => {
3504
+ const event = parseStreamJsonEvent(line);
3505
+ if (event) {
3506
+ if (event.event === "step_update" && event.step_update) {
3507
+ const su = event.step_update;
3508
+ if (su.step_type === "tool") {
3509
+ const toolName = su.tool_name || su.tool_info?.name || "unknown";
3510
+ const toolParams = su.tool_info?.parameters;
3511
+ if (su.state === "ACTIVE") {
3512
+ const actionDesc = formatActionDescription(toolName, toolParams);
3513
+ if (spinner) {
3514
+ spinner.update(`${targetLabel}: ${actionDesc}`);
3515
+ }
3516
+ if (toolParams?.CommandLine) {
3517
+ checkTargetDetection(toolParams.CommandLine);
3518
+ }
3519
+ if (options.verbose) {
3520
+ const formatted = formatVerboseEvent(event);
3521
+ if (formatted) console.log(formatted);
3522
+ }
3523
+ } else if (su.state === "DONE") {
3524
+ if (su.tool_info?.output) {
3525
+ checkTargetDetection(su.tool_info.output);
3526
+ }
3527
+ if (spinner) {
3528
+ activePhase = "Evaluating tool output...";
3529
+ spinner.update(`${targetLabel}: ${activePhase}`);
3530
+ }
3531
+ if (options.verbose) {
3532
+ const formatted = formatVerboseEvent(event);
3533
+ if (formatted) console.log(formatted);
3534
+ }
3535
+ }
3536
+ } else if (su.step_type === "agent_response" || su.step_type === "thought") {
3537
+ if (su.text_delta) {
3538
+ accumulatedOutput += su.text_delta;
3539
+ checkTargetDetection(su.text_delta);
3540
+ const newPhase = detectActivePhase(su.text_delta, activePhase);
3541
+ if (newPhase !== activePhase) {
3542
+ activePhase = newPhase;
3543
+ if (spinner) {
3544
+ spinner.update(`${targetLabel}: ${activePhase}`);
3545
+ }
3546
+ }
3547
+ if (options.verbose) {
3548
+ process.stdout.write(su.text_delta);
3549
+ }
3550
+ } else if (options.verbose && su.state === "DONE") {
3551
+ const formatted = formatVerboseEvent(event);
3552
+ if (formatted) console.log(formatted);
3553
+ }
3554
+ } else if (options.verbose) {
3555
+ const formatted = formatVerboseEvent(event);
3556
+ if (formatted) console.log(formatted);
3557
+ }
3558
+ } else if (event.event === "result" && event.result) {
3559
+ if (event.result.response) {
3560
+ finalResponseText = event.result.response;
3561
+ checkTargetDetection(event.result.response);
3562
+ }
3563
+ if (options.verbose) {
3564
+ const formatted = formatVerboseEvent(event);
3565
+ if (formatted) console.log(formatted);
3566
+ }
3567
+ } else if (event.event === "init") {
3568
+ if (options.verbose) {
3569
+ const formatted = formatVerboseEvent(event);
3570
+ if (formatted) console.log(formatted);
3571
+ }
3572
+ }
3573
+ } else {
3574
+ accumulatedOutput += line + "\n";
3575
+ checkTargetDetection(line);
3576
+ if (options.verbose) {
3577
+ console.log(line);
3578
+ } else if (spinner) {
3579
+ const newPhase = detectActivePhase(line, activePhase);
3580
+ if (newPhase !== activePhase) {
3581
+ activePhase = newPhase;
2989
3582
  spinner.update(`${targetLabel}: ${activePhase}`);
2990
3583
  }
2991
3584
  }
2992
3585
  }
2993
- if (options.onLog) {
2994
- options.onLog(chunk);
2995
- }
3586
+ });
3587
+ const stderrParser = new LineBufferedStreamParser((line) => {
3588
+ checkTargetDetection(line);
2996
3589
  if (options.verbose) {
2997
- if (isStderr) {
2998
- process.stderr.write(chunk);
2999
- } else {
3000
- process.stdout.write(chunk);
3001
- }
3590
+ console.error(pc10.dim(`[stderr] ${line}`));
3002
3591
  } else if (spinner) {
3003
- const newPhase = detectActivePhase(chunk, activePhase);
3592
+ const newPhase = detectActivePhase(line, activePhase);
3004
3593
  if (newPhase !== activePhase) {
3005
3594
  activePhase = newPhase;
3006
3595
  spinner.update(`${targetLabel}: ${activePhase}`);
3007
3596
  }
3008
3597
  }
3598
+ });
3599
+ const processChunk = (chunk, isStderr = false) => {
3600
+ try {
3601
+ fs12.appendFileSync(logFilePath, chunk, "utf8");
3602
+ } catch {
3603
+ }
3604
+ if (options.onLog) {
3605
+ options.onLog(chunk);
3606
+ }
3607
+ if (isStderr) {
3608
+ stderrParser.feed(chunk);
3609
+ } else {
3610
+ stdoutParser.feed(chunk);
3611
+ }
3009
3612
  };
3010
3613
  try {
3011
3614
  exitCode = await new Promise((resolve, reject) => {
@@ -3037,6 +3640,9 @@ Timeout: ${printTimeout}`,
3037
3640
  await cleanup();
3038
3641
  }
3039
3642
  }
3643
+ stdoutParser.flush();
3644
+ stderrParser.flush();
3645
+ output = finalResponseText || accumulatedOutput;
3040
3646
  if (options.showCard !== false && !options.verbose) {
3041
3647
  const durationMs = Date.now() - startTime;
3042
3648
  const effectiveIssue = options.issue || (targetLabel.startsWith("Issue #") ? targetLabel.replace("Issue #", "") : void 0);
@@ -3080,31 +3686,31 @@ async function runRoutineCommand(routine, options = {}) {
3080
3686
  const manifest = loadManifest(cwd);
3081
3687
  if (!manifest) {
3082
3688
  console.warn(
3083
- pc10.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
3689
+ pc11.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
3084
3690
  );
3085
3691
  } else if (manifest.routines && manifest.routines[routine] === false) {
3086
3692
  console.warn(
3087
- pc10.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
3693
+ pc11.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
3088
3694
  );
3089
3695
  }
3090
- console.log(pc10.cyan(`
3091
- \u{1F680} Launching local agent session for routine: ${pc10.bold(routine)}`));
3696
+ console.log(pc11.cyan(`
3697
+ \u{1F680} Launching local agent session for routine: ${pc11.bold(routine)}`));
3092
3698
  if (options.issue) {
3093
- console.log(pc10.dim(` Target issue: #${options.issue}`));
3699
+ console.log(pc11.dim(` Target issue: #${options.issue}`));
3094
3700
  }
3095
3701
  if (options.pr) {
3096
- console.log(pc10.dim(` Target pull request: #${options.pr}`));
3702
+ console.log(pc11.dim(` Target pull request: #${options.pr}`));
3097
3703
  }
3098
3704
  if (options.model) {
3099
- console.log(pc10.dim(` Model override: ${options.model}`));
3705
+ console.log(pc11.dim(` Model override: ${options.model}`));
3100
3706
  }
3101
3707
  if (options.verbose) {
3102
- console.log(pc10.dim(` Verbose output: Enabled (streaming raw tokens)`));
3708
+ console.log(pc11.dim(` Verbose output: Enabled (streaming raw tokens)`));
3103
3709
  }
3104
3710
  if (options.worktree !== false) {
3105
- console.log(pc10.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
3711
+ console.log(pc11.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
3106
3712
  } else {
3107
- console.log(pc10.yellow(` Workspace isolation: Disabled (running in current directory)`));
3713
+ console.log(pc11.yellow(` Workspace isolation: Disabled (running in current directory)`));
3108
3714
  }
3109
3715
  console.log("");
3110
3716
  try {
@@ -3121,56 +3727,431 @@ async function runRoutineCommand(routine, options = {}) {
3121
3727
  verbose: options.verbose
3122
3728
  });
3123
3729
  if (options.dryRun) {
3124
- console.log(pc10.green(result.output));
3730
+ console.log(pc11.green(result.output));
3125
3731
  return;
3126
3732
  }
3127
3733
  if (result.success) {
3128
- console.log(pc10.green(`
3734
+ console.log(pc11.green(`
3129
3735
  \u2713 Local agent session for '${routine}' completed successfully.`));
3130
3736
  } else {
3131
- console.error(pc10.red(`
3737
+ console.error(pc11.red(`
3132
3738
  \u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
3133
3739
  process.exit(result.exitCode);
3134
3740
  }
3135
3741
  } catch (error) {
3136
- console.error(pc10.red(`
3742
+ console.error(pc11.red(`
3137
3743
  \u2717 Failed to execute routine '${routine}': ${error.message}`));
3138
3744
  process.exit(1);
3139
3745
  }
3140
3746
  }
3141
-
3142
- // src/commands/daemon.ts
3143
- import pc12 from "picocolors";
3747
+
3748
+ // src/commands/daemon.ts
3749
+ import pc14 from "picocolors";
3750
+
3751
+ // src/lib/daemon.ts
3752
+ import fs14 from "fs";
3753
+ import path14 from "path";
3754
+ import { spawn as spawn2, execFile as execFile3 } from "child_process";
3755
+ import { promisify as promisify3 } from "util";
3756
+
3757
+ // src/lib/daemon-keys.ts
3758
+ import readline2 from "readline";
3759
+ import fs13 from "fs";
3760
+ import path13 from "path";
3761
+ import pc12 from "picocolors";
3762
+ var KeyboardController = class {
3763
+ constructor(options = {}) {
3764
+ this.options = options;
3765
+ this.stdin = options.stdin || process.stdin;
3766
+ }
3767
+ options;
3768
+ stdin;
3769
+ isRaw = false;
3770
+ listening = false;
3771
+ isPaused = false;
3772
+ keypressListener;
3773
+ start() {
3774
+ if (this.listening) return;
3775
+ if (this.stdin && typeof this.stdin.setRawMode === "function" && this.stdin.isTTY) {
3776
+ readline2.emitKeypressEvents(this.stdin);
3777
+ try {
3778
+ this.stdin.setRawMode(true);
3779
+ this.isRaw = true;
3780
+ } catch {
3781
+ this.isRaw = false;
3782
+ }
3783
+ if (typeof this.stdin.resume === "function") {
3784
+ this.stdin.resume();
3785
+ }
3786
+ }
3787
+ this.keypressListener = (str, key) => {
3788
+ this.handleKeypress(str, key);
3789
+ };
3790
+ this.stdin.on("keypress", this.keypressListener);
3791
+ this.listening = true;
3792
+ this.isPaused = false;
3793
+ }
3794
+ pause() {
3795
+ this.isPaused = true;
3796
+ }
3797
+ resume() {
3798
+ this.isPaused = false;
3799
+ }
3800
+ handleKeypress(str, key) {
3801
+ if (this.isPaused) return;
3802
+ const k = key || {};
3803
+ if (k.ctrl && (k.name === "c" || k.name === "C") || str === "") {
3804
+ this.options.onForceStop?.();
3805
+ return;
3806
+ }
3807
+ const isShift = Boolean(k.shift);
3808
+ const keyName = (k.name || "").toLowerCase();
3809
+ if (str === "R" || keyName === "r" && isShift) {
3810
+ this.options.onTargetedReview?.();
3811
+ return;
3812
+ }
3813
+ if (str === "A" || keyName === "a" && isShift) {
3814
+ this.options.onTargetedAutowork?.();
3815
+ return;
3816
+ }
3817
+ if (str === "r" || keyName === "r" && !isShift) {
3818
+ this.options.onReview?.();
3819
+ return;
3820
+ }
3821
+ if (str === "a" || keyName === "a" && !isShift) {
3822
+ this.options.onAutowork?.();
3823
+ return;
3824
+ }
3825
+ if (str === "v" || str === "V" || keyName === "v") {
3826
+ this.options.onToggleVerbose?.();
3827
+ return;
3828
+ }
3829
+ if (str === "l" || str === "L" || keyName === "l") {
3830
+ this.options.onTailLog?.();
3831
+ return;
3832
+ }
3833
+ if (str === "w" || str === "W" || keyName === "w") {
3834
+ this.options.onCleanWorktrees?.();
3835
+ return;
3836
+ }
3837
+ if (str === "p" || str === "P" || keyName === "p") {
3838
+ this.options.onPauseToggle?.();
3839
+ return;
3840
+ }
3841
+ if (str === "s" || str === "S" || keyName === "s") {
3842
+ this.options.onStatus?.();
3843
+ return;
3844
+ }
3845
+ if (str === "q" || str === "Q" || keyName === "q") {
3846
+ this.options.onGracefulStop?.();
3847
+ return;
3848
+ }
3849
+ if (str === "?" || keyName === "h" || str === "h" || str === "H") {
3850
+ this.options.onHelp?.();
3851
+ return;
3852
+ }
3853
+ }
3854
+ stop() {
3855
+ if (!this.listening) return;
3856
+ if (this.keypressListener) {
3857
+ this.stdin.removeListener("keypress", this.keypressListener);
3858
+ }
3859
+ if (this.isRaw && typeof this.stdin.setRawMode === "function") {
3860
+ try {
3861
+ this.stdin.setRawMode(false);
3862
+ } catch {
3863
+ }
3864
+ this.isRaw = false;
3865
+ }
3866
+ if (typeof this.stdin.pause === "function") {
3867
+ try {
3868
+ this.stdin.pause();
3869
+ } catch {
3870
+ }
3871
+ }
3872
+ this.listening = false;
3873
+ this.isPaused = false;
3874
+ }
3875
+ };
3876
+ function parseNumericTarget(input) {
3877
+ if (!input) return null;
3878
+ const trimmed = input.trim();
3879
+ if (!trimmed) return null;
3880
+ const match = trimmed.match(/^(?:(?:PR|Issue|pr|issue)\s*#?)?#?(\d+)$/i);
3881
+ if (match) {
3882
+ const num = parseInt(match[1], 10);
3883
+ return num > 0 ? num : null;
3884
+ }
3885
+ return null;
3886
+ }
3887
+ async function promptTargetedInput(promptMessage, options = {}) {
3888
+ const stdin = options.stdin || process.stdin;
3889
+ const stdout = options.stdout || process.stdout;
3890
+ if (stdout && typeof stdout.write === "function") {
3891
+ stdout.write(promptMessage);
3892
+ }
3893
+ return new Promise((resolve) => {
3894
+ let cleanedUp = false;
3895
+ const wasRaw = Boolean(stdin && stdin.rawMode !== void 0 ? stdin.rawMode : stdin?.isRaw);
3896
+ if (stdin && typeof stdin.setRawMode === "function" && stdin.isTTY) {
3897
+ try {
3898
+ stdin.setRawMode(false);
3899
+ } catch {
3900
+ }
3901
+ }
3902
+ if (stdin && typeof stdin.resume !== "function") {
3903
+ stdin.resume = () => {
3904
+ };
3905
+ }
3906
+ if (stdin && typeof stdin.pause !== "function") {
3907
+ stdin.pause = () => {
3908
+ };
3909
+ }
3910
+ const rl = readline2.createInterface({
3911
+ input: stdin,
3912
+ output: stdout,
3913
+ terminal: Boolean(stdin && stdin.isTTY)
3914
+ });
3915
+ const cleanup = (val) => {
3916
+ if (cleanedUp) return;
3917
+ cleanedUp = true;
3918
+ try {
3919
+ if (stdin && typeof stdin.removeListener === "function") {
3920
+ stdin.removeListener("data", onRawData);
3921
+ }
3922
+ rl.close();
3923
+ } catch {
3924
+ }
3925
+ if (wasRaw && stdin && typeof stdin.setRawMode === "function" && stdin.isTTY) {
3926
+ try {
3927
+ stdin.setRawMode(true);
3928
+ } catch {
3929
+ }
3930
+ }
3931
+ resolve(val);
3932
+ };
3933
+ const onRawData = (chunk) => {
3934
+ const str = chunk.toString();
3935
+ if (str === "\x1B" || str === "") {
3936
+ cleanup(null);
3937
+ }
3938
+ };
3939
+ if (stdin && typeof stdin.on === "function") {
3940
+ stdin.on("data", onRawData);
3941
+ }
3942
+ rl.question("", (answer) => {
3943
+ cleanup(answer.trim() || null);
3944
+ });
3945
+ rl.on("close", () => {
3946
+ cleanup(null);
3947
+ });
3948
+ });
3949
+ }
3950
+ function getDaemonLogTail(repoRoot, linesCount = 20) {
3951
+ const logPath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
3952
+ if (!fs13.existsSync(logPath)) return [];
3953
+ try {
3954
+ const content = fs13.readFileSync(logPath, "utf8");
3955
+ const lines = content.split("\n");
3956
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
3957
+ lines.pop();
3958
+ }
3959
+ return lines.slice(-linesCount);
3960
+ } catch {
3961
+ return [];
3962
+ }
3963
+ }
3964
+ function printDaemonLogTail(repoRoot, linesCount = 20) {
3965
+ const lines = getDaemonLogTail(repoRoot, linesCount);
3966
+ const logPath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
3967
+ const relativePath = path13.relative(repoRoot, logPath) || logPath;
3968
+ console.log(pc12.cyan(`
3969
+ \u{1F4C4} Tail of ${relativePath} (last ${linesCount} lines):
3970
+ `));
3971
+ if (lines.length === 0) {
3972
+ console.log(pc12.dim(` (Log file is empty or does not exist yet at ${relativePath})
3973
+ `));
3974
+ return;
3975
+ }
3976
+ for (const line of lines) {
3977
+ console.log(pc12.dim(line));
3978
+ }
3979
+ console.log("");
3980
+ }
3981
+ async function inspectAndCleanWorktrees(repoRoot) {
3982
+ const active = await listActiveWorktrees(repoRoot);
3983
+ const cleaned = await cleanupStaleWorktrees(repoRoot);
3984
+ return { active, cleaned };
3985
+ }
3986
+ function printWorktreesInspection(result) {
3987
+ console.log(pc12.cyan(`
3988
+ \u{1F333} Jonah Fleet Worktree Inspection & Maintenance
3989
+ `));
3990
+ console.log(` Active Worktrees: ${result.active.length}`);
3991
+ if (result.active.length === 0) {
3992
+ console.log(pc12.dim(` (No active routine worktrees found)`));
3993
+ } else {
3994
+ for (const wt of result.active) {
3995
+ const commitShort = wt.commit ? ` (${wt.commit.slice(0, 7)})` : "";
3996
+ console.log(` - [${pc12.bold(wt.branch)}] ${pc12.dim(wt.path)}${commitShort}`);
3997
+ }
3998
+ }
3999
+ console.log(`
4000
+ Cleaned Stale Worktrees: ${result.cleaned}`);
4001
+ console.log("");
4002
+ }
4003
+ function printKeybindingCheatSheet() {
4004
+ console.log(pc12.cyan(`
4005
+ \u2328\uFE0F Jonah Fleet Daemon Keybindings
4006
+ `));
4007
+ console.log(` ${pc12.bold("r")} Trigger peer-review scan immediately`);
4008
+ console.log(` ${pc12.bold("R")} Prompt for PR # and run targeted peer-review`);
4009
+ console.log(` ${pc12.bold("a")} Trigger autowork backlog scan immediately`);
4010
+ console.log(` ${pc12.bold("A")} Prompt for Issue # and run targeted autowork`);
4011
+ console.log(` ${pc12.bold("p")} Pause / resume automated polling intervals`);
4012
+ console.log(` ${pc12.bold("s")} Print current daemon status summary card`);
4013
+ console.log(` ${pc12.bold("v")} Toggle verbose streaming logging live`);
4014
+ console.log(` ${pc12.bold("l")} Tail recent lines from .jonah-fleet/daemon.log`);
4015
+ console.log(` ${pc12.bold("w")} Inspect active worktrees and clean stale ones`);
4016
+ console.log(` ${pc12.bold("q")} Graceful shutdown (waits for active routine to finish)`);
4017
+ console.log(` ${pc12.bold("Ctrl+C")} Immediate force abort`);
4018
+ console.log(` ${pc12.bold("?")} / ${pc12.bold("h")} Show this keybindings cheat-sheet
4019
+ `);
4020
+ }
4021
+ function printDaemonStatusSummary(options) {
4022
+ const { state, pendingRoutine, activeWorktrees = [], verbose } = options;
4023
+ console.log(pc12.cyan(`
4024
+ \u{1F916} Jonah Fleet Local Daemon Status
4025
+ `));
4026
+ if (state) {
4027
+ let statusText;
4028
+ if (state.status === "working") {
4029
+ const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc12.bold(state.activeTarget)})` : "");
4030
+ statusText = pc12.yellow(pc12.bold(`WORKING on ${workingDesc}`));
4031
+ } else if (state.status === "paused") {
4032
+ statusText = pc12.yellow(pc12.bold("PAUSED"));
4033
+ } else {
4034
+ statusText = pc12.green(pc12.bold("RUNNING (IDLE)"));
4035
+ }
4036
+ console.log(` Status: ${statusText}`);
4037
+ console.log(` PID: ${state.pid}`);
4038
+ console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
4039
+ console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
4040
+ console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
4041
+ console.log(` Routines: ${state.routines.join(", ")}`);
4042
+ if (verbose !== void 0) {
4043
+ console.log(
4044
+ ` Verbose Mode: ${verbose ? pc12.green("ENABLED (streaming tokens)") : pc12.gray("DISABLED (compact spinner)")}`
4045
+ );
4046
+ }
4047
+ if (pendingRoutine) {
4048
+ console.log(` Queued Routine: ${pc12.cyan(pc12.bold(pendingRoutine))}`);
4049
+ }
4050
+ if (state.lastReviewCheckAt) {
4051
+ console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
4052
+ }
4053
+ if (state.lastAutoworkCheckAt) {
4054
+ console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
4055
+ }
4056
+ } else {
4057
+ console.log(` Status: ${pc12.gray("STOPPED")}`);
4058
+ console.log(pc12.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
4059
+ }
4060
+ console.log(`
4061
+ Active Worktrees: ${activeWorktrees.length}`);
4062
+ for (const wt of activeWorktrees) {
4063
+ console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
4064
+ }
4065
+ console.log("");
4066
+ }
4067
+ var DAEMON_STATUS_TIPS = [
4068
+ "Tip: press 'r' to run review pass now",
4069
+ "Tip: press 'a' to run autowork scan now",
4070
+ "Tip: press 'R' to review a specific PR #",
4071
+ "Tip: press 'A' to work a specific Issue #",
4072
+ "Tip: press 'p' to pause/resume automatic checks",
4073
+ "Tip: press 's' to view daemon status",
4074
+ "Tip: press 'v' to toggle verbose streaming",
4075
+ "Tip: press 'l' to view recent log tail",
4076
+ "Tip: press 'w' to inspect/clean worktrees",
4077
+ "Tip: press 'q' to stop daemon gracefully",
4078
+ "Tip: press '?' for all keybindings"
4079
+ ];
4080
+ var DAEMON_PAUSED_TIPS = [
4081
+ "Tip: press 'p' to resume scheduled checks",
4082
+ "Tip: press 'r' to run review pass now",
4083
+ "Tip: press 'a' to run autowork scan now",
4084
+ "Tip: press 'R' to review a specific PR #",
4085
+ "Tip: press 'A' to work a specific Issue #",
4086
+ "Tip: press 's' to view daemon status",
4087
+ "Tip: press 'v' to toggle verbose streaming",
4088
+ "Tip: press 'l' to view recent log tail",
4089
+ "Tip: press 'w' to inspect/clean worktrees",
4090
+ "Tip: press 'q' to stop daemon gracefully",
4091
+ "Tip: press '?' for all keybindings"
4092
+ ];
4093
+ function getRotatingTipIndex(nowMs = Date.now(), intervalSeconds = 4, totalTips = DAEMON_STATUS_TIPS.length) {
4094
+ if (totalTips <= 0) return 0;
4095
+ const slot = Math.floor(nowMs / (intervalSeconds * 1e3));
4096
+ return (slot % totalTips + totalTips) % totalTips;
4097
+ }
4098
+ function formatDaemonStatusLine(options = {}) {
4099
+ const nowDate = options.now instanceof Date ? options.now : typeof options.now === "number" ? new Date(options.now) : /* @__PURE__ */ new Date();
4100
+ const nowMs = nowDate.getTime();
4101
+ const timeString = nowDate.toLocaleTimeString();
4102
+ const columns = options.columns !== void 0 ? options.columns : process.stderr.columns || 80;
4103
+ const maxCols = Math.max(20, (columns || 80) - 2);
4104
+ const includeTip = columns >= 55;
4105
+ let core;
4106
+ if (options.isPaused) {
4107
+ const queueStr = options.pendingRoutine ? pc12.cyan(` [Queued: ${options.pendingRoutine}]`) : "";
4108
+ core = `${pc12.dim("[" + timeString + "]")} \u23F8\uFE0F ${pc12.yellow("PAUSED")}${queueStr}`;
4109
+ } else {
4110
+ const nextCheck = options.nextCheckTime !== void 0 ? options.nextCheckTime : nowMs;
4111
+ const diffMs = Math.max(0, nextCheck - nowMs);
4112
+ const remainingSecs = Math.ceil(diffMs / 1e3);
4113
+ const mins = Math.floor(remainingSecs / 60);
4114
+ const secs = remainingSecs % 60;
4115
+ const timeStr = `${mins}m ${secs < 10 ? "0" : ""}${secs}s`;
4116
+ const prStr = options.lastOpenPRCount !== void 0 ? ` (${options.lastOpenPRCount} ready PRs)` : "";
4117
+ const queueStr = options.pendingRoutine ? pc12.cyan(` [Queued: ${options.pendingRoutine}]`) : "";
4118
+ core = `${pc12.dim("[" + timeString + "]")} \u{1F4A4} ${pc12.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}${queueStr}`;
4119
+ }
4120
+ if (!includeTip) {
4121
+ return truncateAnsi(core, maxCols);
4122
+ }
4123
+ const tipsList = options.tips || (options.isPaused ? DAEMON_PAUSED_TIPS : DAEMON_STATUS_TIPS);
4124
+ const tipIdx = options.tipIndex !== void 0 ? options.tipIndex : getRotatingTipIndex(nowMs, 4, tipsList.length);
4125
+ const tipText = tipsList[(tipIdx % tipsList.length + tipsList.length) % tipsList.length] || "";
4126
+ const fullLine = `${core} ${pc12.dim("\xB7")} ${pc12.dim(tipText)}`;
4127
+ return truncateAnsi(fullLine, maxCols);
4128
+ }
3144
4129
 
3145
4130
  // src/lib/daemon.ts
3146
- import fs13 from "fs";
3147
- 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";
4131
+ import pc13 from "picocolors";
3151
4132
  var execFileAsync3 = promisify3(execFile3);
3152
4133
  function getDaemonStatePath(repoRoot) {
3153
- return path13.join(repoRoot, ".jonah-fleet", "daemon.json");
4134
+ return path14.join(repoRoot, ".jonah-fleet", "daemon.json");
3154
4135
  }
3155
4136
  function readDaemonState(repoRoot) {
3156
4137
  const statePath = getDaemonStatePath(repoRoot);
3157
- if (!fs13.existsSync(statePath)) return null;
4138
+ if (!fs14.existsSync(statePath)) return null;
3158
4139
  try {
3159
- return JSON.parse(fs13.readFileSync(statePath, "utf8"));
4140
+ return JSON.parse(fs14.readFileSync(statePath, "utf8"));
3160
4141
  } catch {
3161
4142
  return null;
3162
4143
  }
3163
4144
  }
3164
4145
  function writeDaemonState(repoRoot, state) {
3165
4146
  const statePath = getDaemonStatePath(repoRoot);
3166
- fs13.mkdirSync(path13.dirname(statePath), { recursive: true });
3167
- fs13.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
4147
+ fs14.mkdirSync(path14.dirname(statePath), { recursive: true });
4148
+ fs14.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
3168
4149
  }
3169
4150
  function clearDaemonState(repoRoot) {
3170
4151
  const statePath = getDaemonStatePath(repoRoot);
3171
- if (fs13.existsSync(statePath)) {
4152
+ if (fs14.existsSync(statePath)) {
3172
4153
  try {
3173
- fs13.unlinkSync(statePath);
4154
+ fs14.unlinkSync(statePath);
3174
4155
  } catch {
3175
4156
  }
3176
4157
  }
@@ -3204,10 +4185,6 @@ async function getOpenReviewablePRs(repoRoot) {
3204
4185
  return [];
3205
4186
  }
3206
4187
  }
3207
- async function countOpenReadyPRs(repoRoot) {
3208
- const prs = await getOpenReviewablePRs(repoRoot);
3209
- return prs.length;
3210
- }
3211
4188
  async function startBackgroundDaemon(repoRoot, options = {}) {
3212
4189
  if (isDaemonRunning(repoRoot)) {
3213
4190
  const existing = readDaemonState(repoRoot);
@@ -3216,9 +4193,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
3216
4193
  const reviewInterval = options.reviewInterval || 3;
3217
4194
  const autoworkInterval = options.autoworkInterval || options.interval || 30;
3218
4195
  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");
4196
+ const logFilePath = path14.join(repoRoot, ".jonah-fleet", "daemon.log");
4197
+ fs14.mkdirSync(path14.dirname(logFilePath), { recursive: true });
4198
+ const logFd = fs14.openSync(logFilePath, "a");
3222
4199
  const cliPath = process.argv[1];
3223
4200
  const args = [
3224
4201
  "daemon",
@@ -3286,7 +4263,7 @@ async function drainReviewQueue(drainOptions) {
3286
4263
  let reviewablePRs = await getPRs(repoRoot);
3287
4264
  if (reviewablePRs.length === 0) {
3288
4265
  if (options.verbose) {
3289
- console.log(pc11.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
4266
+ console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
3290
4267
  }
3291
4268
  return;
3292
4269
  }
@@ -3296,7 +4273,7 @@ async function drainReviewQueue(drainOptions) {
3296
4273
  if (candidatePRs.length === 0) {
3297
4274
  if (options.verbose) {
3298
4275
  console.log(
3299
- pc11.dim(
4276
+ pc13.dim(
3300
4277
  `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] All ${reviewablePRs.length} remaining ready PR(s) were already evaluated in this drain pass.`
3301
4278
  )
3302
4279
  );
@@ -3313,7 +4290,7 @@ async function drainReviewQueue(drainOptions) {
3313
4290
  writeDaemonState(repoRoot, state);
3314
4291
  }
3315
4292
  console.log(
3316
- pc11.cyan(
4293
+ pc13.cyan(
3317
4294
  `
3318
4295
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Draining PR backlog (${totalRemaining} PR(s) remaining). Starting review session...`
3319
4296
  )
@@ -3341,14 +4318,14 @@ async function drainReviewQueue(drainOptions) {
3341
4318
  onAttempted?.(prNum);
3342
4319
  }
3343
4320
  if (result.success) {
3344
- console.log(pc11.green(`\u2713 Local peer-review completed successfully.
4321
+ console.log(pc13.green(`\u2713 Local peer-review completed successfully.
3345
4322
  `));
3346
4323
  } else {
3347
- console.warn(pc11.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
4324
+ console.warn(pc13.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
3348
4325
  `));
3349
4326
  }
3350
4327
  } catch (err) {
3351
- console.error(pc11.red(`\u2717 Error in peer-review: ${err.message}`));
4328
+ console.error(pc13.red(`\u2717 Error in peer-review: ${err.message}`));
3352
4329
  if (candidatePRs[0]) {
3353
4330
  attemptedPRNumbers.add(candidatePRs[0].number);
3354
4331
  onAttempted?.(candidatePRs[0].number);
@@ -3377,20 +4354,29 @@ async function runDaemonLoop(repoRoot, options = {}) {
3377
4354
  status: "idle"
3378
4355
  };
3379
4356
  writeDaemonState(repoRoot, state);
3380
- console.log(pc11.cyan(`
4357
+ console.log(pc13.cyan(`
3381
4358
  \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}
4359
+ console.log(pc13.dim(` PID: ${process.pid}`));
4360
+ console.log(pc13.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
4361
+ console.log(pc13.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
4362
+ console.log(pc13.dim(` Working Directory: ${repoRoot}`));
4363
+ console.log(pc13.dim(` Interactive Hotkeys: 'r' (review), 'a' (autowork), 'p' (pause), 's' (status), 'q' (stop), '?' (help)
3386
4364
  `));
3387
4365
  let isStopping = false;
4366
+ let isGracefulStopping = false;
3388
4367
  let isWorking = false;
4368
+ let isPaused = false;
4369
+ let pendingRoutine = null;
4370
+ let keyboard;
4371
+ let tickerInterval;
4372
+ let stopResolve;
3389
4373
  const reviewIntervalMs = reviewInterval * 60 * 1e3;
3390
4374
  const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
3391
4375
  let nextReviewCheckTime = Date.now() + (routines.includes("peer-review") ? reviewIntervalMs : Infinity);
3392
4376
  let nextAutoworkCheckTime = Date.now() + (routines.includes("autowork") ? autoworkIntervalMs : Infinity);
3393
4377
  let lastOpenPRCount = void 0;
4378
+ const getPRsFn = options.getPRs || getOpenReviewablePRs;
4379
+ const runRoutineFn = options.runRoutine || runLocalRoutine;
3394
4380
  const clearTicker = () => {
3395
4381
  if (process.stderr.isTTY && !options.verbose) {
3396
4382
  process.stderr.write("\r\x1B[K");
@@ -3398,30 +4384,32 @@ async function runDaemonLoop(repoRoot, options = {}) {
3398
4384
  };
3399
4385
  const updateTicker = () => {
3400
4386
  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
- );
4387
+ const line = formatDaemonStatusLine({
4388
+ now: Date.now(),
4389
+ isPaused,
4390
+ nextCheckTime: Math.min(nextReviewCheckTime, nextAutoworkCheckTime),
4391
+ lastOpenPRCount,
4392
+ pendingRoutine,
4393
+ columns: process.stderr.columns
4394
+ });
4395
+ process.stderr.write(`\r\x1B[K${line}`);
3412
4396
  };
3413
- const tickerInterval = setInterval(updateTicker, 1e3);
3414
4397
  const handleStop = async () => {
3415
4398
  if (isStopping) return;
3416
4399
  isStopping = true;
3417
- clearInterval(tickerInterval);
3418
- clearInterval(reviewTimer);
3419
- clearInterval(autoworkTimer);
4400
+ process.removeListener("SIGINT", handleStop);
4401
+ process.removeListener("SIGTERM", handleStop);
4402
+ keyboard?.stop();
4403
+ if (tickerInterval) {
4404
+ clearInterval(tickerInterval);
4405
+ tickerInterval = void 0;
4406
+ }
3420
4407
  clearTicker();
3421
- console.log(pc11.yellow(`
4408
+ console.log(pc13.yellow(`
3422
4409
  Stopping local agent daemon...`));
3423
4410
  clearDaemonState(repoRoot);
3424
4411
  await cleanupStaleWorktrees(repoRoot);
4412
+ stopResolve?.();
3425
4413
  process.exit(0);
3426
4414
  };
3427
4415
  process.once("SIGINT", handleStop);
@@ -3435,32 +4423,53 @@ Stopping local agent daemon...`));
3435
4423
  state,
3436
4424
  options,
3437
4425
  isStopping: () => isStopping,
3438
- clearTicker
4426
+ clearTicker,
4427
+ getPRs: getPRsFn,
4428
+ runRoutine: runRoutineFn
3439
4429
  });
3440
- const prs = await getOpenReviewablePRs(repoRoot);
4430
+ const prs = await getPRsFn(repoRoot);
3441
4431
  lastOpenPRCount = prs.length;
3442
4432
  } finally {
3443
4433
  isWorking = false;
4434
+ state.status = isPaused ? "paused" : "idle";
4435
+ state.activeRoutine = void 0;
4436
+ state.activeTarget = void 0;
4437
+ writeDaemonState(repoRoot, state);
3444
4438
  nextReviewCheckTime = Date.now() + reviewIntervalMs;
3445
4439
  updateTicker();
4440
+ if (isGracefulStopping) {
4441
+ await handleStop();
4442
+ return;
4443
+ }
4444
+ if (pendingRoutine && !isStopping) {
4445
+ const next = pendingRoutine;
4446
+ pendingRoutine = null;
4447
+ console.log(pc13.cyan(`
4448
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
4449
+ if (next === "peer-review") {
4450
+ await performReviewDrain();
4451
+ } else if (next === "autowork") {
4452
+ await runAutoworkCheck();
4453
+ }
4454
+ }
3446
4455
  }
3447
4456
  };
3448
4457
  const runAutoworkCheck = async () => {
3449
4458
  if (isStopping || isWorking || !routines.includes("autowork")) return;
3450
4459
  if (routines.includes("peer-review")) {
3451
- const pendingPRs = await countOpenReadyPRs(repoRoot);
4460
+ const pendingPRs = (await getPRsFn(repoRoot)).length;
3452
4461
  if (pendingPRs > 0) {
3453
4462
  console.log(
3454
- pc11.cyan(
4463
+ pc13.cyan(
3455
4464
  `
3456
4465
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
3457
4466
  )
3458
4467
  );
3459
4468
  await performReviewDrain();
3460
- const remainingPRs = await countOpenReadyPRs(repoRoot);
4469
+ const remainingPRs = (await getPRsFn(repoRoot)).length;
3461
4470
  if (remainingPRs > 0) {
3462
4471
  console.log(
3463
- pc11.yellow(
4472
+ pc13.yellow(
3464
4473
  `
3465
4474
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Review backlog still has ${remainingPRs} pending PR(s). Postponing autowork session.`
3466
4475
  )
@@ -3479,10 +4488,10 @@ Stopping local agent daemon...`));
3479
4488
  state.status = "working";
3480
4489
  state.activeRoutine = "autowork";
3481
4490
  writeDaemonState(repoRoot, state);
3482
- console.log(pc11.cyan(`
4491
+ console.log(pc13.cyan(`
3483
4492
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
3484
4493
  await cleanupStaleWorktrees(repoRoot);
3485
- const result = await runLocalRoutine({
4494
+ const result = await runRoutineFn({
3486
4495
  targetDir: repoRoot,
3487
4496
  routine: "autowork",
3488
4497
  model: options.model,
@@ -3494,27 +4503,157 @@ Stopping local agent daemon...`));
3494
4503
  }
3495
4504
  });
3496
4505
  if (result.success) {
3497
- console.log(pc11.green(`\u2713 Local autowork completed successfully.
4506
+ console.log(pc13.green(`\u2713 Local autowork completed successfully.
3498
4507
  `));
3499
4508
  } else {
3500
- console.warn(pc11.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
4509
+ console.warn(pc13.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
3501
4510
  `));
3502
4511
  }
3503
4512
  } catch (err) {
3504
- console.error(pc11.red(`\u2717 Error in autowork: ${err.message}`));
4513
+ console.error(pc13.red(`\u2717 Error in autowork: ${err.message}`));
3505
4514
  } finally {
3506
4515
  isWorking = false;
3507
- state.status = "idle";
4516
+ state.status = isPaused ? "paused" : "idle";
3508
4517
  state.activeRoutine = void 0;
3509
4518
  state.activeTarget = void 0;
3510
4519
  writeDaemonState(repoRoot, state);
3511
4520
  nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
3512
4521
  updateTicker();
3513
4522
  if (!isStopping && routines.includes("peer-review")) {
3514
- const newPRCount = await countOpenReadyPRs(repoRoot);
4523
+ const newPRCount = (await getPRsFn(repoRoot)).length;
4524
+ if (newPRCount > 0) {
4525
+ console.log(
4526
+ pc13.cyan(
4527
+ `
4528
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
4529
+ )
4530
+ );
4531
+ await performReviewDrain();
4532
+ }
4533
+ }
4534
+ if (isGracefulStopping) {
4535
+ await handleStop();
4536
+ return;
4537
+ }
4538
+ if (pendingRoutine && !isStopping) {
4539
+ const next = pendingRoutine;
4540
+ pendingRoutine = null;
4541
+ console.log(pc13.cyan(`
4542
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
4543
+ if (next === "peer-review") {
4544
+ await performReviewDrain();
4545
+ } else if (next === "autowork") {
4546
+ await runAutoworkCheck();
4547
+ }
4548
+ }
4549
+ }
4550
+ };
4551
+ const runTargetedReview = async (prNumber) => {
4552
+ if (isStopping || isWorking) return;
4553
+ try {
4554
+ isWorking = true;
4555
+ clearTicker();
4556
+ state.status = "working";
4557
+ state.activeRoutine = "peer-review";
4558
+ state.activeTarget = `PR #${prNumber}`;
4559
+ writeDaemonState(repoRoot, state);
4560
+ console.log(
4561
+ pc13.cyan(`
4562
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Peer Review: Starting session on PR #${prNumber}...`)
4563
+ );
4564
+ await cleanupStaleWorktrees(repoRoot);
4565
+ const result = await runRoutineFn({
4566
+ targetDir: repoRoot,
4567
+ routine: "peer-review",
4568
+ pr: prNumber,
4569
+ model: options.model,
4570
+ verbose: options.verbose,
4571
+ noWorktree: false,
4572
+ onTargetDetected: (target) => {
4573
+ state.activeTarget = target;
4574
+ writeDaemonState(repoRoot, state);
4575
+ }
4576
+ });
4577
+ if (result.success) {
4578
+ console.log(pc13.green(`\u2713 Targeted peer-review on PR #${prNumber} completed successfully.
4579
+ `));
4580
+ } else {
4581
+ console.warn(pc13.yellow(`\u26A0\uFE0F Targeted peer-review on PR #${prNumber} completed with code ${result.exitCode}.
4582
+ `));
4583
+ }
4584
+ } catch (err) {
4585
+ console.error(pc13.red(`\u2717 Error in targeted peer-review: ${err.message}`));
4586
+ } finally {
4587
+ isWorking = false;
4588
+ state.status = isPaused ? "paused" : "idle";
4589
+ state.activeRoutine = void 0;
4590
+ state.activeTarget = void 0;
4591
+ writeDaemonState(repoRoot, state);
4592
+ updateTicker();
4593
+ if (isGracefulStopping) {
4594
+ await handleStop();
4595
+ return;
4596
+ }
4597
+ if (pendingRoutine && !isStopping) {
4598
+ const next = pendingRoutine;
4599
+ pendingRoutine = null;
4600
+ console.log(pc13.cyan(`
4601
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
4602
+ if (next === "peer-review") {
4603
+ await performReviewDrain();
4604
+ } else if (next === "autowork") {
4605
+ await runAutoworkCheck();
4606
+ }
4607
+ }
4608
+ }
4609
+ };
4610
+ const runTargetedAutowork = async (issueNumber) => {
4611
+ if (isStopping || isWorking) return;
4612
+ try {
4613
+ isWorking = true;
4614
+ clearTicker();
4615
+ state.status = "working";
4616
+ state.activeRoutine = "autowork";
4617
+ state.activeTarget = `Issue #${issueNumber}`;
4618
+ writeDaemonState(repoRoot, state);
4619
+ console.log(
4620
+ pc13.cyan(`
4621
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Autowork: Starting session on Issue #${issueNumber}...`)
4622
+ );
4623
+ await cleanupStaleWorktrees(repoRoot);
4624
+ const result = await runRoutineFn({
4625
+ targetDir: repoRoot,
4626
+ routine: "autowork",
4627
+ issue: issueNumber,
4628
+ model: options.model,
4629
+ verbose: options.verbose,
4630
+ noWorktree: false,
4631
+ onTargetDetected: (target) => {
4632
+ state.activeTarget = target;
4633
+ writeDaemonState(repoRoot, state);
4634
+ }
4635
+ });
4636
+ if (result.success) {
4637
+ console.log(pc13.green(`\u2713 Targeted autowork on Issue #${issueNumber} completed successfully.
4638
+ `));
4639
+ } else {
4640
+ console.warn(pc13.yellow(`\u26A0\uFE0F Targeted autowork on Issue #${issueNumber} completed with code ${result.exitCode}.
4641
+ `));
4642
+ }
4643
+ } catch (err) {
4644
+ console.error(pc13.red(`\u2717 Error in targeted autowork: ${err.message}`));
4645
+ } finally {
4646
+ isWorking = false;
4647
+ state.status = isPaused ? "paused" : "idle";
4648
+ state.activeRoutine = void 0;
4649
+ state.activeTarget = void 0;
4650
+ writeDaemonState(repoRoot, state);
4651
+ updateTicker();
4652
+ if (!isStopping && routines.includes("peer-review")) {
4653
+ const newPRCount = (await getPRsFn(repoRoot)).length;
3515
4654
  if (newPRCount > 0) {
3516
4655
  console.log(
3517
- pc11.cyan(
4656
+ pc13.cyan(
3518
4657
  `
3519
4658
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
3520
4659
  )
@@ -3522,17 +4661,251 @@ Stopping local agent daemon...`));
3522
4661
  await performReviewDrain();
3523
4662
  }
3524
4663
  }
4664
+ if (isGracefulStopping) {
4665
+ await handleStop();
4666
+ return;
4667
+ }
4668
+ if (pendingRoutine && !isStopping) {
4669
+ const next = pendingRoutine;
4670
+ pendingRoutine = null;
4671
+ console.log(pc13.cyan(`
4672
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
4673
+ if (next === "peer-review") {
4674
+ await performReviewDrain();
4675
+ } else if (next === "autowork") {
4676
+ await runAutoworkCheck();
4677
+ }
4678
+ }
3525
4679
  }
3526
4680
  };
4681
+ keyboard = new KeyboardController({
4682
+ stdin: options.stdin || process.stdin,
4683
+ onReview: async () => {
4684
+ if (isStopping || isGracefulStopping) return;
4685
+ if (isWorking) {
4686
+ pendingRoutine = "peer-review";
4687
+ clearTicker();
4688
+ console.log(
4689
+ pc13.cyan(`
4690
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Peer Review scan queued (will run after current routine finishes).`)
4691
+ );
4692
+ updateTicker();
4693
+ return;
4694
+ }
4695
+ clearTicker();
4696
+ console.log(pc13.cyan(`
4697
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Triggering immediate Peer Review scan on demand...`));
4698
+ await performReviewDrain();
4699
+ },
4700
+ onAutowork: async () => {
4701
+ if (isStopping || isGracefulStopping) return;
4702
+ if (isWorking) {
4703
+ pendingRoutine = "autowork";
4704
+ clearTicker();
4705
+ console.log(
4706
+ pc13.cyan(`
4707
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork backlog scan queued (will run after current routine finishes).`)
4708
+ );
4709
+ updateTicker();
4710
+ return;
4711
+ }
4712
+ clearTicker();
4713
+ console.log(pc13.cyan(`
4714
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Triggering immediate Autowork scan on demand...`));
4715
+ await runAutoworkCheck();
4716
+ },
4717
+ onTargetedReview: async () => {
4718
+ if (isStopping || isGracefulStopping) return;
4719
+ if (isWorking) {
4720
+ clearTicker();
4721
+ console.log(
4722
+ pc13.yellow(
4723
+ `
4724
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Targeted review prompts require idle state. Use 'r' to queue a scan pass instead.`
4725
+ )
4726
+ );
4727
+ updateTicker();
4728
+ return;
4729
+ }
4730
+ clearTicker();
4731
+ keyboard?.pause();
4732
+ const rawInput = await promptTargetedInput(`
4733
+ ${pc13.cyan("Enter PR # to review (Esc/Enter to cancel):")} `, {
4734
+ stdin: options.stdin || process.stdin,
4735
+ stdout: process.stdout
4736
+ });
4737
+ keyboard?.resume();
4738
+ if (!rawInput) {
4739
+ console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted review cancelled.
4740
+ `));
4741
+ updateTicker();
4742
+ return;
4743
+ }
4744
+ const prNumber = parseNumericTarget(rawInput);
4745
+ if (!prNumber) {
4746
+ console.log(
4747
+ pc13.yellow(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Invalid PR number '${rawInput}'. Operation cancelled.
4748
+ `)
4749
+ );
4750
+ updateTicker();
4751
+ return;
4752
+ }
4753
+ await runTargetedReview(prNumber);
4754
+ },
4755
+ onTargetedAutowork: async () => {
4756
+ if (isStopping || isGracefulStopping) return;
4757
+ if (isWorking) {
4758
+ clearTicker();
4759
+ console.log(
4760
+ pc13.yellow(
4761
+ `
4762
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Targeted autowork prompts require idle state. Use 'a' to queue a scan pass instead.`
4763
+ )
4764
+ );
4765
+ updateTicker();
4766
+ return;
4767
+ }
4768
+ clearTicker();
4769
+ keyboard?.pause();
4770
+ const rawInput = await promptTargetedInput(`
4771
+ ${pc13.cyan("Enter Issue # to work (Esc/Enter to cancel):")} `, {
4772
+ stdin: options.stdin || process.stdin,
4773
+ stdout: process.stdout
4774
+ });
4775
+ keyboard?.resume();
4776
+ if (!rawInput) {
4777
+ console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted autowork cancelled.
4778
+ `));
4779
+ updateTicker();
4780
+ return;
4781
+ }
4782
+ const issueNumber = parseNumericTarget(rawInput);
4783
+ if (!issueNumber) {
4784
+ console.log(
4785
+ pc13.yellow(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Invalid Issue number '${rawInput}'. Operation cancelled.
4786
+ `)
4787
+ );
4788
+ updateTicker();
4789
+ return;
4790
+ }
4791
+ await runTargetedAutowork(issueNumber);
4792
+ },
4793
+ onToggleVerbose: () => {
4794
+ if (isStopping || isGracefulStopping) return;
4795
+ options.verbose = !options.verbose;
4796
+ clearTicker();
4797
+ if (options.verbose) {
4798
+ console.log(
4799
+ pc13.green(`
4800
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50A} Verbose mode ENABLED (streaming tokens directly to terminal).`)
4801
+ );
4802
+ } else {
4803
+ console.log(
4804
+ pc13.yellow(`
4805
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F507} Verbose mode DISABLED (compact terminal spinner active).`)
4806
+ );
4807
+ }
4808
+ updateTicker();
4809
+ },
4810
+ onTailLog: () => {
4811
+ if (isStopping || isGracefulStopping) return;
4812
+ clearTicker();
4813
+ printDaemonLogTail(repoRoot, 20);
4814
+ updateTicker();
4815
+ },
4816
+ onCleanWorktrees: async () => {
4817
+ if (isStopping || isGracefulStopping) return;
4818
+ clearTicker();
4819
+ const result = await inspectAndCleanWorktrees(repoRoot);
4820
+ printWorktreesInspection(result);
4821
+ updateTicker();
4822
+ },
4823
+ onPauseToggle: () => {
4824
+ if (isStopping || isGracefulStopping) return;
4825
+ isPaused = !isPaused;
4826
+ clearTicker();
4827
+ if (isPaused) {
4828
+ if (state.status !== "working") state.status = "paused";
4829
+ writeDaemonState(repoRoot, state);
4830
+ console.log(
4831
+ pc13.yellow(
4832
+ `
4833
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F8\uFE0F Daemon polling paused. Automatic interval sweeps suspended. (Press 'p' to resume)`
4834
+ )
4835
+ );
4836
+ } else {
4837
+ if (state.status !== "working") state.status = "idle";
4838
+ writeDaemonState(repoRoot, state);
4839
+ console.log(
4840
+ pc13.green(`
4841
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u25B6\uFE0F Daemon polling resumed. Automated interval sweeps active.`)
4842
+ );
4843
+ }
4844
+ updateTicker();
4845
+ },
4846
+ onStatus: async () => {
4847
+ clearTicker();
4848
+ const activeWorktrees = await listActiveWorktrees(repoRoot);
4849
+ printDaemonStatusSummary({
4850
+ repoRoot,
4851
+ state,
4852
+ pendingRoutine,
4853
+ activeWorktrees,
4854
+ verbose: options.verbose
4855
+ });
4856
+ updateTicker();
4857
+ },
4858
+ onGracefulStop: async () => {
4859
+ if (isStopping) return;
4860
+ pendingRoutine = null;
4861
+ if (isWorking) {
4862
+ isGracefulStopping = true;
4863
+ clearTicker();
4864
+ console.log(
4865
+ pc13.yellow(
4866
+ `
4867
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F6D1} Graceful stop requested. Waiting for active routine (${state.activeRoutine || "routine"}) to complete before stopping...`
4868
+ )
4869
+ );
4870
+ return;
4871
+ }
4872
+ await handleStop();
4873
+ },
4874
+ onForceStop: async () => {
4875
+ await handleStop();
4876
+ },
4877
+ onHelp: () => {
4878
+ clearTicker();
4879
+ printKeybindingCheatSheet();
4880
+ updateTicker();
4881
+ }
4882
+ });
4883
+ keyboard?.start();
3527
4884
  if (routines.includes("peer-review")) {
3528
4885
  await performReviewDrain();
3529
4886
  }
3530
- if (!isStopping && routines.includes("autowork")) {
4887
+ if (!isStopping && !isGracefulStopping && routines.includes("autowork")) {
3531
4888
  await runAutoworkCheck();
3532
4889
  }
3533
- const reviewTimer = setInterval(performReviewDrain, reviewIntervalMs);
3534
- const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
3535
- await new Promise(() => {
4890
+ if (isStopping) return;
4891
+ const tick = async () => {
4892
+ if (isStopping || isGracefulStopping || isWorking) return;
4893
+ if (!isPaused) {
4894
+ const now = Date.now();
4895
+ if (routines.includes("peer-review") && now >= nextReviewCheckTime) {
4896
+ await performReviewDrain();
4897
+ return;
4898
+ }
4899
+ if (routines.includes("autowork") && now >= nextAutoworkCheckTime) {
4900
+ await runAutoworkCheck();
4901
+ return;
4902
+ }
4903
+ }
4904
+ updateTicker();
4905
+ };
4906
+ tickerInterval = setInterval(tick, 1e3);
4907
+ await new Promise((resolve) => {
4908
+ stopResolve = resolve;
3536
4909
  });
3537
4910
  }
3538
4911
 
@@ -3556,16 +4929,16 @@ async function runDaemonCommand(action, options = {}) {
3556
4929
  }
3557
4930
  try {
3558
4931
  const state2 = await startBackgroundDaemon(cwd, daemonOpts);
3559
- console.log(pc12.green(`
4932
+ console.log(pc14.green(`
3560
4933
  \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.`));
4934
+ console.log(pc14.dim(` PID: ${state2.pid}`));
4935
+ console.log(pc14.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
4936
+ console.log(pc14.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
4937
+ console.log(pc14.dim(` Routines: ${state2.routines.join(", ")}`));
4938
+ console.log(pc14.dim(` Log file: .jonah-fleet/daemon.log`));
4939
+ console.log(pc14.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
3567
4940
  } catch (err) {
3568
- console.error(pc12.red(`
4941
+ console.error(pc14.red(`
3569
4942
  \u2717 Failed to start daemon: ${err.message}`));
3570
4943
  process.exit(1);
3571
4944
  }
@@ -3573,18 +4946,18 @@ async function runDaemonCommand(action, options = {}) {
3573
4946
  }
3574
4947
  if (act === "stop") {
3575
4948
  if (!isDaemonRunning(cwd)) {
3576
- console.log(pc12.yellow(`
4949
+ console.log(pc14.yellow(`
3577
4950
  \u26A0\uFE0F No local agent daemon is currently running in this repository.`));
3578
4951
  return;
3579
4952
  }
3580
4953
  const state2 = readDaemonState(cwd);
3581
- console.log(pc12.cyan(`
4954
+ console.log(pc14.cyan(`
3582
4955
  Stopping background agent daemon (PID ${state2?.pid})...`));
3583
4956
  const stopped = await stopDaemon(cwd);
3584
4957
  if (stopped) {
3585
- console.log(pc12.green(`\u2713 Local agent daemon stopped successfully.`));
4958
+ console.log(pc14.green(`\u2713 Local agent daemon stopped successfully.`));
3586
4959
  } else {
3587
- console.error(pc12.red(`\u2717 Could not terminate daemon process.`));
4960
+ console.error(pc14.red(`\u2717 Could not terminate daemon process.`));
3588
4961
  process.exit(1);
3589
4962
  }
3590
4963
  return;
@@ -3596,18 +4969,24 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
3596
4969
  const running = isDaemonRunning(cwd);
3597
4970
  const state = readDaemonState(cwd);
3598
4971
  const activeWorktrees = await listActiveWorktrees(cwd);
3599
- console.log(pc12.cyan(`
4972
+ console.log(pc14.cyan(`
3600
4973
  \u{1F916} Jonah Fleet Local Daemon Status
3601
4974
  `));
3602
4975
  if (running && state) {
3603
- console.log(` Status: ${pc12.green(pc12.bold("RUNNING"))}`);
4976
+ console.log(` Status: ${pc14.green(pc14.bold("RUNNING"))}`);
3604
4977
  console.log(` PID: ${state.pid}`);
3605
4978
  console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
3606
4979
  console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
3607
4980
  console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
3608
4981
  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")}`);
4982
+ let currentStateText = pc14.green("IDLE");
4983
+ if (state.status === "working") {
4984
+ const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc14.bold(state.activeTarget)})` : "");
4985
+ currentStateText = pc14.yellow("WORKING on " + workingDesc);
4986
+ } else if (state.status === "paused") {
4987
+ currentStateText = pc14.yellow(pc14.bold("PAUSED"));
4988
+ }
4989
+ console.log(` Current State: ${currentStateText}`);
3611
4990
  if (state.lastReviewCheckAt) {
3612
4991
  console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
3613
4992
  }
@@ -3615,17 +4994,127 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
3615
4994
  console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
3616
4995
  }
3617
4996
  } else {
3618
- console.log(` Status: ${pc12.gray("STOPPED")}`);
3619
- console.log(pc12.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
4997
+ console.log(` Status: ${pc14.gray("STOPPED")}`);
4998
+ console.log(pc14.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
3620
4999
  }
3621
5000
  console.log(`
3622
5001
  Active Worktrees: ${activeWorktrees.length}`);
3623
5002
  for (const wt of activeWorktrees) {
3624
- console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
5003
+ console.log(pc14.dim(` - [${wt.branch}] ${wt.path}`));
3625
5004
  }
3626
5005
  console.log("");
3627
5006
  }
3628
5007
 
5008
+ // src/commands/labels.ts
5009
+ import pc15 from "picocolors";
5010
+ async function runLabels(action = "audit", options = {}) {
5011
+ const cwd = options.cwd || process.cwd();
5012
+ const executor = options.executor || defaultGhExecutor;
5013
+ const repo = await resolveRepoName(options.repo, cwd, executor);
5014
+ const manifest = loadManifest(cwd);
5015
+ const userProtected = manifest?.labels?.protected || [];
5016
+ const protectedPatterns = [
5017
+ ...DEFAULT_PROTECTED_LABEL_PATTERNS,
5018
+ ...userProtected
5019
+ ];
5020
+ if (action === "prune") {
5021
+ const isDryRun = Boolean(options.dryRun);
5022
+ const result = await pruneLabels({
5023
+ repo,
5024
+ dryRun: isDryRun,
5025
+ yes: options.yes,
5026
+ cwd,
5027
+ protectedPatterns,
5028
+ executor
5029
+ });
5030
+ if (options.json) {
5031
+ console.log(JSON.stringify(result, null, 2));
5032
+ return;
5033
+ }
5034
+ if (isDryRun) {
5035
+ console.log(pc15.bold(pc15.cyan(`
5036
+ \u{1F50D} Label Prune (Dry-Run) for ${repo}
5037
+ `)));
5038
+ if (result.pruned.length === 0) {
5039
+ console.log(pc15.green(" \u2713 No prunable labels found. Repository labels are clean.\n"));
5040
+ } else {
5041
+ console.log(pc15.yellow(` Found ${result.pruned.length} strictly unused label(s) eligible for pruning:`));
5042
+ for (const label of result.pruned) {
5043
+ console.log(` - ${pc15.yellow(label)} (0 issues, 0 PRs, non-schema)`);
5044
+ }
5045
+ console.log(pc15.gray(`
5046
+ Run 'jonah-fleet labels prune --yes' to delete these labels.
5047
+ `));
5048
+ }
5049
+ return;
5050
+ }
5051
+ console.log(pc15.bold(pc15.cyan(`
5052
+ \u{1F9F9} Label Pruning for ${repo}
5053
+ `)));
5054
+ if (result.pruned.length === 0 && result.errors.length === 0) {
5055
+ console.log(pc15.green(" \u2713 No prunable labels found. Repository labels are clean.\n"));
5056
+ return;
5057
+ }
5058
+ if (result.pruned.length > 0) {
5059
+ console.log(pc15.green(` \u2713 Successfully pruned ${result.pruned.length} unused label(s):`));
5060
+ for (const label of result.pruned) {
5061
+ console.log(` - ${pc15.green(label)}`);
5062
+ }
5063
+ }
5064
+ if (result.errors.length > 0) {
5065
+ console.log(pc15.red(`
5066
+ \u274C Failed to delete ${result.errors.length} label(s):`));
5067
+ for (const err of result.errors) {
5068
+ console.log(` - ${pc15.red(err.label)}: ${err.error}`);
5069
+ }
5070
+ }
5071
+ console.log();
5072
+ return;
5073
+ }
5074
+ const rawLabels = await fetchRepoLabels(repo, executor, cwd);
5075
+ const classified = classifyLabels(rawLabels, protectedPatterns);
5076
+ if (options.json) {
5077
+ console.log(
5078
+ JSON.stringify(
5079
+ {
5080
+ repo,
5081
+ totalCount: rawLabels.length,
5082
+ activeCount: classified.active.length,
5083
+ protectedZeroCountCount: classified.protectedZeroCount.length,
5084
+ historicalCount: classified.historical.length,
5085
+ prunableCount: classified.prunable.length,
5086
+ active: classified.active,
5087
+ protectedZeroCount: classified.protectedZeroCount,
5088
+ historical: classified.historical,
5089
+ prunable: classified.prunable
5090
+ },
5091
+ null,
5092
+ 2
5093
+ )
5094
+ );
5095
+ return;
5096
+ }
5097
+ console.log(pc15.bold(pc15.cyan(`
5098
+ \u{1F3F7}\uFE0F Repository Label Audit for ${repo}
5099
+ `)));
5100
+ console.log(` Total Labels: ${pc15.bold(String(rawLabels.length))}`);
5101
+ console.log(` Active (Open items): ${pc15.green(String(classified.active.length))}`);
5102
+ console.log(` Protected (Zero-count): ${pc15.cyan(String(classified.protectedZeroCount.length))}`);
5103
+ console.log(` Historical (Closed): ${pc15.gray(String(classified.historical.length))}`);
5104
+ console.log(` Prunable (Unused): ${classified.prunable.length > 0 ? pc15.yellow(String(classified.prunable.length)) : pc15.green("0")}`);
5105
+ if (classified.prunable.length > 0) {
5106
+ console.log(pc15.bold(pc15.yellow("\n \u26A0\uFE0F Prunable Labels (0 total issues/PRs, non-protected):")));
5107
+ for (const label of classified.prunable) {
5108
+ console.log(` - ${pc15.yellow(label.name)}`);
5109
+ }
5110
+ console.log(pc15.gray(`
5111
+ Run 'jonah-fleet labels prune' to clean up unused boilerplate.
5112
+ `));
5113
+ } else {
5114
+ console.log(pc15.green("\n \u2713 All labels are either active, historical, or protected fleet taxonomy.\n"));
5115
+ }
5116
+ }
5117
+
3629
5118
  // src/index.ts
3630
5119
  var program = new Command();
3631
5120
  program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
@@ -3635,7 +5124,11 @@ program.command("run <routine>").description("Run a specific prompt routine loca
3635
5124
  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
5125
  await runDaemonCommand(action, options);
3637
5126
  });
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) => {
5127
+ program.command("labels [action]").description("Audit and prune unused repository labels while protecting fleet taxonomy").option("-d, --dry-run", "Preview prunable labels without deleting them", false).option("-y, --yes", "Confirm automatic deletion of prunable labels", false).option("-r, --repo <repo>", "Target GitHub repository (defaults to current)").option("-j, --json", "Output results as JSON", false).action(async (action, options) => {
5128
+ const act = action === "prune" || action === "list" || action === "audit" ? action : "audit";
5129
+ await runLabels(act, options);
5130
+ });
5131
+ 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
5132
  await runInit(options);
3640
5133
  });
3641
5134
  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) => {