jonah-fleet 1.5.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 (44) hide show
  1. package/CHANGELOG.md +136 -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 +2292 -476
  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 +41 -2
  13. package/dist/lib/daemon.d.ts.map +1 -1
  14. package/dist/lib/evals.d.ts +56 -1
  15. package/dist/lib/evals.d.ts.map +1 -1
  16. package/dist/lib/labels.d.ts +48 -0
  17. package/dist/lib/labels.d.ts.map +1 -0
  18. package/dist/lib/manifest.d.ts +6 -0
  19. package/dist/lib/manifest.d.ts.map +1 -1
  20. package/dist/lib/presets.d.ts +47 -1
  21. package/dist/lib/presets.d.ts.map +1 -1
  22. package/dist/lib/runner.d.ts +66 -0
  23. package/dist/lib/runner.d.ts.map +1 -1
  24. package/dist/lib/telemetry.d.ts +13 -0
  25. package/dist/lib/telemetry.d.ts.map +1 -1
  26. package/dist/lib/terminal-card.d.ts +23 -0
  27. package/dist/lib/terminal-card.d.ts.map +1 -1
  28. package/package.json +4 -4
  29. package/schema.json +68 -0
  30. package/templates/evals/ambiguity-benchmark.json +92 -0
  31. package/templates/prompts/ORCHESTRATION.md +57 -36
  32. package/templates/prompts/_prompt-template.md +13 -1
  33. package/templates/prompts/autowork.md +63 -27
  34. package/templates/prompts/issues-housekeeping.md +1 -1
  35. package/templates/prompts/optimizer.md +3 -0
  36. package/templates/prompts/peer-review.md +13 -3
  37. package/templates/workflows/autowork-cron.yml +74 -2
  38. package/templates/workflows/dependency-check-cron.yml +41 -2
  39. package/templates/workflows/issues-housekeeping-cron.yml +41 -2
  40. package/templates/workflows/prompt-optimizer-cron.yml +41 -2
  41. package/templates/workflows/trigger-autowork-manual.yml +43 -2
  42. package/templates/workflows/trigger-autowork-on-bug.yml +47 -7
  43. package/templates/workflows/trigger-autowork-on-merge.yml +48 -7
  44. 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: {
@@ -92,7 +119,7 @@ var ROUTINE_TO_WORKFLOW_MAP = {
92
119
  "product-planning": [],
93
120
  "analytics-review": []
94
121
  };
95
- var FLEET_VERSION = "1.5.0";
122
+ var FLEET_VERSION = "1.6.0";
96
123
  var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
97
124
 
98
125
  // src/lib/manifest.ts
@@ -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) {
@@ -1759,6 +2054,8 @@ function parseLogToTelemetry(content, options = {}) {
1759
2054
  failureCategory = "infeasible";
1760
2055
  } else if (lower.includes("conflict")) {
1761
2056
  failureCategory = "merge_conflict";
2057
+ } else if (lower.includes("ambiguous") || lower.includes("needs-info") || lower.includes("clarification") || lower.includes("acceptance criteria")) {
2058
+ failureCategory = "ambiguous_spec";
1762
2059
  }
1763
2060
  }
1764
2061
  const inputTokens = parseInt((metadata["input tokens"] || "0").replace(/[^\d]/g, ""), 10) || 0;
@@ -1783,6 +2080,16 @@ function parseLogToTelemetry(content, options = {}) {
1783
2080
  }
1784
2081
  }
1785
2082
  const promptSha = metadata["prompt sha"];
2083
+ const lowerContent = content.toLowerCase();
2084
+ const ambiguityGateTriggered = lowerContent.includes("ambiguity & missing acceptance criteria gate") || lowerContent.includes("ambiguity gate") || lowerContent.includes("clarifications needed before implementation") || lowerContent.includes("needs-info") && (lowerContent.includes("clarification") || lowerContent.includes("questions"));
2085
+ const needsInfoApplied = lowerContent.includes("needs-info");
2086
+ let questionsAskedCount;
2087
+ if (ambiguityGateTriggered) {
2088
+ const questionLines = content.split("\n").filter(
2089
+ (line) => (line.trim().startsWith("1.") || line.trim().startsWith("2.") || line.trim().startsWith("3.") || line.trim().startsWith("-")) && line.includes("?")
2090
+ );
2091
+ questionsAskedCount = questionLines.length > 0 ? questionLines.length : 2;
2092
+ }
1786
2093
  return {
1787
2094
  schemaVersion: "1.0.0",
1788
2095
  routine,
@@ -1800,7 +2107,10 @@ function parseLogToTelemetry(content, options = {}) {
1800
2107
  durationSeconds,
1801
2108
  iterationsUsed,
1802
2109
  maxIterations,
1803
- promptSha
2110
+ promptSha,
2111
+ ambiguityGateTriggered: ambiguityGateTriggered || void 0,
2112
+ questionsAskedCount,
2113
+ needsInfoApplied: needsInfoApplied || void 0
1804
2114
  };
1805
2115
  }
1806
2116
  function checkWeeklyBudgetLimit(usedTokens, ceilingTokens = GLOBAL_WEEKLY_TOKEN_BUDGET) {
@@ -1859,7 +2169,10 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1859
2169
  failureCount: 0,
1860
2170
  bouncedCount: 0,
1861
2171
  avgDurationSeconds: 0,
1862
- avgIterationsUsed: 0
2172
+ avgIterationsUsed: 0,
2173
+ ambiguityGatesTriggered: 0,
2174
+ questionsAskedCount: 0,
2175
+ needsInfoAppliedCount: 0
1863
2176
  };
1864
2177
  }
1865
2178
  const r = byRoutine[s.routine];
@@ -1877,6 +2190,9 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1877
2190
  if (s.iterationsUsed) {
1878
2191
  r.avgIterationsUsed = (r.avgIterationsUsed * (r.runCount - 1) + s.iterationsUsed) / r.runCount;
1879
2192
  }
2193
+ if (s.ambiguityGateTriggered) r.ambiguityGatesTriggered++;
2194
+ if (s.questionsAskedCount) r.questionsAskedCount += s.questionsAskedCount;
2195
+ if (s.needsInfoApplied) r.needsInfoAppliedCount++;
1880
2196
  if (!byRepository[s.repository]) {
1881
2197
  byRepository[s.repository] = {
1882
2198
  repository: s.repository,
@@ -1894,6 +2210,21 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1894
2210
  if (s.result === "SUCCESS") repoObj.successCount++;
1895
2211
  else if (s.result === "FAILURE") repoObj.failureCount++;
1896
2212
  }
2213
+ let totalAmbiguityGatesTriggered = 0;
2214
+ let totalQuestionsAsked = 0;
2215
+ let totalNeedsInfoApplied = 0;
2216
+ for (const s of summaries) {
2217
+ if (s.ambiguityGateTriggered) totalAmbiguityGatesTriggered++;
2218
+ if (s.questionsAskedCount) totalQuestionsAsked += s.questionsAskedCount;
2219
+ if (s.needsInfoApplied) totalNeedsInfoApplied++;
2220
+ }
2221
+ const estimatedTokensSaved = totalAmbiguityGatesTriggered * 5e4;
2222
+ const ambiguity = {
2223
+ totalAmbiguityGatesTriggered,
2224
+ totalQuestionsAsked,
2225
+ needsInfoAppliedCount: totalNeedsInfoApplied,
2226
+ estimatedTokensSaved
2227
+ };
1897
2228
  const totalTokens = totalInputTokens + totalOutputTokens;
1898
2229
  const budget = checkWeeklyBudgetLimit(totalTokens, budgetCeiling);
1899
2230
  return {
@@ -1910,6 +2241,7 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1910
2241
  byRoutine,
1911
2242
  byRepository,
1912
2243
  failureCategories,
2244
+ ambiguity,
1913
2245
  events: summaries
1914
2246
  };
1915
2247
  }
@@ -2046,6 +2378,20 @@ function renderTelemetryDashboard(telemetry, options = {}) {
2046
2378
  );
2047
2379
  }
2048
2380
  }
2381
+ const amb = telemetry.ambiguity;
2382
+ lines.push("\n" + pc7.bold("\u2753 Inquisitive Stance & Ambiguity Gate Signals:"));
2383
+ lines.push(
2384
+ ` \u2022 Ambiguity Gate Triggers: ${pc7.bold(amb.totalAmbiguityGatesTriggered.toString())} runs stopped to request clarification`
2385
+ );
2386
+ lines.push(
2387
+ ` \u2022 Clarifying Questions Posed: ${pc7.bold(amb.totalQuestionsAsked.toString())} targeted questions`
2388
+ );
2389
+ lines.push(
2390
+ ` \u2022 Needs-Info Labels Applied: ${pc7.bold(amb.needsInfoAppliedCount.toString())}`
2391
+ );
2392
+ lines.push(
2393
+ ` \u2022 Est. Wasted Tokens Averted: ~${pc7.bold(pc7.green(formatTokens2(amb.estimatedTokensSaved)))} tokens (avoided speculative builds)`
2394
+ );
2049
2395
  const failKeys = Object.keys(telemetry.failureCategories);
2050
2396
  if (failKeys.length > 0) {
2051
2397
  lines.push("\n" + pc7.bold(pc7.red("\u26A0\uFE0F Failure Categories Breakdown:")));
@@ -2157,7 +2503,7 @@ async function runTelemetry(options = {}) {
2157
2503
  }
2158
2504
 
2159
2505
  // src/commands/run.ts
2160
- import pc10 from "picocolors";
2506
+ import pc11 from "picocolors";
2161
2507
 
2162
2508
  // src/lib/runner.ts
2163
2509
  import fs12 from "fs";
@@ -2289,7 +2635,93 @@ async function cleanupStaleWorktrees(repoRoot) {
2289
2635
  // src/lib/terminal-card.ts
2290
2636
  import fs11 from "fs";
2291
2637
  import path11 from "path";
2638
+ import { execFileSync } from "child_process";
2292
2639
  import pc9 from "picocolors";
2640
+ function stripAnsi(text) {
2641
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
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
+ }
2676
+ function wrapText(text, maxWidth) {
2677
+ if (maxWidth <= 0) return [text];
2678
+ const words = text.split(/\s+/).filter(Boolean);
2679
+ if (words.length === 0) return [];
2680
+ const lines = [];
2681
+ let current = "";
2682
+ for (const word of words) {
2683
+ if (!current) {
2684
+ current = word;
2685
+ } else {
2686
+ const proposed = current + " " + word;
2687
+ if (stripAnsi(proposed).length <= maxWidth) {
2688
+ current = proposed;
2689
+ } else {
2690
+ lines.push(current);
2691
+ current = word;
2692
+ }
2693
+ }
2694
+ }
2695
+ if (current) lines.push(current);
2696
+ return lines;
2697
+ }
2698
+ function fetchTargetTitle(repoRoot, target) {
2699
+ try {
2700
+ const prMatch = target.match(/PR\s*#?(\d+)/i);
2701
+ if (prMatch) {
2702
+ const stdout = execFileSync("gh", ["pr", "view", prMatch[1], "--json", "title", "-q", ".title"], {
2703
+ cwd: repoRoot,
2704
+ encoding: "utf8",
2705
+ stdio: ["ignore", "pipe", "ignore"],
2706
+ timeout: 4e3
2707
+ });
2708
+ return stdout.trim() || null;
2709
+ }
2710
+ const issueMatch = target.match(/Issue\s*#?(\d+)/i);
2711
+ if (issueMatch) {
2712
+ const stdout = execFileSync("gh", ["issue", "view", issueMatch[1], "--json", "title", "-q", ".title"], {
2713
+ cwd: repoRoot,
2714
+ encoding: "utf8",
2715
+ stdio: ["ignore", "pipe", "ignore"],
2716
+ timeout: 4e3
2717
+ });
2718
+ return stdout.trim() || null;
2719
+ }
2720
+ } catch {
2721
+ return null;
2722
+ }
2723
+ return null;
2724
+ }
2293
2725
  function sanitizeWorktreePaths(text) {
2294
2726
  let cleaned = text.replace(/file:\/\/\/[^\s"'()]+?\/\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, "");
2295
2727
  cleaned = cleaned.replace(/(?:^|[\s"'(`[])(?:\/[^\s"'()]+?)?\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, (match) => {
@@ -2300,7 +2732,7 @@ function sanitizeWorktreePaths(text) {
2300
2732
  return cleaned;
2301
2733
  }
2302
2734
  function extractExecutionSummary(output) {
2303
- const summaryHeaderRegex = /#\s+([A-Za-z0-9\s_-]+?Execution\s+Summary[\s\S]*)/i;
2735
+ const summaryHeaderRegex = /#{1,3}\s+([A-Za-z0-9\s_-]*?(?:Execution|Review|Autowork)\s+Summary[\s\S]*)/i;
2304
2736
  const match = output.match(summaryHeaderRegex);
2305
2737
  if (!match) return null;
2306
2738
  let summary = match[1].trim();
@@ -2308,8 +2740,8 @@ function extractExecutionSummary(output) {
2308
2740
  "\u2713 Local peer-review completed",
2309
2741
  "\u2713 Local autowork completed",
2310
2742
  "\u2713 Local agent session",
2311
- "[6:",
2312
- "Peer Review Watchdog:"
2743
+ "Peer Review Watchdog:",
2744
+ "Autowork Backlog Scan:"
2313
2745
  ];
2314
2746
  for (const sep of trailingSeparators) {
2315
2747
  const idx = summary.indexOf(sep);
@@ -2317,6 +2749,10 @@ function extractExecutionSummary(output) {
2317
2749
  summary = summary.slice(0, idx).trim();
2318
2750
  }
2319
2751
  }
2752
+ const timestampMatch = summary.match(/\n\s*\[\d{1,2}:\d{2}:\d{2}\s*(?:AM|PM)?\][\s\S]*/);
2753
+ if (timestampMatch && timestampMatch.index !== void 0) {
2754
+ summary = summary.slice(0, timestampMatch.index).trim();
2755
+ }
2320
2756
  return sanitizeWorktreePaths(summary);
2321
2757
  }
2322
2758
  function findLatestRunLog(repoRoot, routine) {
@@ -2337,57 +2773,73 @@ function parseRunLog(logContent) {
2337
2773
  actions: []
2338
2774
  };
2339
2775
  const lines = logContent.split("\n");
2340
- for (const line of lines) {
2341
- const trimmed = line.trim();
2342
- if (trimmed.startsWith("|") && trimmed.includes("|")) {
2343
- const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
2344
- if (parts.length >= 2) {
2345
- const key = parts[0].toLowerCase();
2346
- const value = parts[1].replace(/`/g, "");
2347
- if (key.includes("routine")) summary.routine = value;
2348
- if (key.includes("target pr") || key.includes("target issue")) summary.target = value;
2349
- if (key.includes("decision")) summary.decision = value;
2350
- if (key.includes("result")) summary.result = value;
2351
- if (key.includes("duration")) summary.duration = value;
2352
- }
2353
- }
2354
- }
2776
+ let inMetadata = false;
2355
2777
  let inDoD = false;
2356
2778
  let inFindings = false;
2357
2779
  let inActions = false;
2358
2780
  for (const line of lines) {
2359
2781
  const trimmed = line.trim();
2360
- if (trimmed.startsWith("## Definition of Done")) {
2782
+ if (trimmed.startsWith("## Metadata")) {
2783
+ inMetadata = true;
2784
+ inDoD = false;
2785
+ inFindings = false;
2786
+ inActions = false;
2787
+ continue;
2788
+ } else if (trimmed.startsWith("## Definition of Done")) {
2789
+ inMetadata = false;
2361
2790
  inDoD = true;
2362
2791
  inFindings = false;
2363
2792
  inActions = false;
2364
2793
  continue;
2365
2794
  } else if (trimmed.startsWith("## Code Review Findings") || trimmed.startsWith("## Findings")) {
2795
+ inMetadata = false;
2366
2796
  inDoD = false;
2367
2797
  inFindings = true;
2368
2798
  inActions = false;
2369
2799
  continue;
2370
- } else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken")) {
2800
+ } else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken") || trimmed.startsWith("## Artifacts")) {
2801
+ inMetadata = false;
2371
2802
  inDoD = false;
2372
2803
  inFindings = false;
2373
- inActions = true;
2804
+ inActions = trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken");
2374
2805
  continue;
2375
2806
  } else if (trimmed.startsWith("## ")) {
2807
+ inMetadata = false;
2376
2808
  inDoD = false;
2377
2809
  inFindings = false;
2378
2810
  inActions = false;
2379
2811
  }
2812
+ if (inMetadata && trimmed.startsWith("|") && trimmed.includes("|")) {
2813
+ const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
2814
+ if (parts.length >= 2) {
2815
+ const key = parts[0].toLowerCase();
2816
+ const value = parts[1].replace(/[`*]/g, "").trim();
2817
+ if (key === "routine") summary.routine = value;
2818
+ if (key === "target pr" || key === "target issue" || key === "target") {
2819
+ summary.target = value;
2820
+ }
2821
+ if (key === "decision") summary.decision = value;
2822
+ if (key === "result") summary.result = value;
2823
+ if (key === "duration") summary.duration = value;
2824
+ if (key === "title" || key === "pr title" || key === "issue title") summary.title = value;
2825
+ }
2826
+ }
2380
2827
  if (inDoD && trimmed.startsWith("|") && !trimmed.includes("Criterion") && !trimmed.includes("---")) {
2381
2828
  const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
2382
2829
  if (parts.length >= 2) {
2383
2830
  const criterion = parts[0];
2384
- const met = parts[1].toUpperCase() === "YES" || parts[1].toUpperCase() === "PASS";
2385
- const evidence = parts[2] ? ` (${parts[2].slice(0, 60)}...)` : "";
2386
- summary.passes?.push({
2387
- name: criterion,
2388
- status: met ? "pass" : "fail",
2389
- detail: evidence
2390
- });
2831
+ const metRaw = parts[1].toUpperCase();
2832
+ const met = metRaw === "YES" || metRaw === "PASS";
2833
+ const evidence = parts[2] ? parts[2].trim() : "";
2834
+ const isConditionalScan = criterion.toLowerCase().startsWith("if in scan mode and no eligible");
2835
+ const isNA = evidence.toLowerCase().includes("n/a") || metRaw === "N/A";
2836
+ if (!isConditionalScan && !isNA) {
2837
+ summary.passes?.push({
2838
+ name: criterion,
2839
+ status: met ? "pass" : "fail",
2840
+ detail: evidence ? ` (${evidence})` : ""
2841
+ });
2842
+ }
2391
2843
  }
2392
2844
  }
2393
2845
  if (inActions && (trimmed.startsWith("- ") || trimmed.startsWith("* "))) {
@@ -2398,9 +2850,15 @@ function parseRunLog(logContent) {
2398
2850
  }
2399
2851
  function detectActivePhase(chunk, currentPhase = "Executing routine") {
2400
2852
  const lower = chunk.toLowerCase();
2853
+ if (lower.includes("\u{1F512} addressing review findings") || lower.includes("addressing review findings by")) {
2854
+ return "Claimed bounced PR, addressing review findings";
2855
+ }
2401
2856
  if (lower.includes("\u{1F512} claimed") || lower.includes("claimed by local autowork") || lower.includes("claimed by autowork")) {
2402
2857
  return "Claimed target issue, starting implementation";
2403
2858
  }
2859
+ if (lower.includes("addressing review findings") || lower.includes("fixing review findings")) {
2860
+ return "Claimed bounced PR, addressing review findings";
2861
+ }
2404
2862
  if (lower.includes("starting review (round")) {
2405
2863
  return "Claimed review window, starting review passes";
2406
2864
  }
@@ -2430,16 +2888,110 @@ function detectClaimedIssue(chunk) {
2430
2888
  return null;
2431
2889
  }
2432
2890
  function detectClaimedPR(chunk) {
2891
+ const findingMatch = chunk.match(/(?:addressing\s+review\s+findings|fixing\s+review\s+findings)[^\n#]*?#(\d+)/i);
2892
+ if (findingMatch) return `PR #${findingMatch[1]}`;
2433
2893
  const reviewMatch = chunk.match(/Starting\s+review[^\n#]*?#(\d+)/i);
2434
2894
  if (reviewMatch) return `PR #${reviewMatch[1]}`;
2435
2895
  const prMatch = chunk.match(/(?:selected|target|reviewing)\s+(?:target\s+)?PR:?\s*\[?PR\s*#?(\d+)/i);
2436
2896
  if (prMatch) return `PR #${prMatch[1]}`;
2437
- const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review)\s+(\d+)/i);
2897
+ const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review|edit|ready)\s+(\d+)/i);
2438
2898
  if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
2439
2899
  return null;
2440
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
+ }
2441
2993
  function renderSummaryCard(options) {
2442
- const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 60), 86);
2994
+ const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 64), 90);
2443
2995
  const horizontal = "\u2500".repeat(width - 2);
2444
2996
  const rawSummary = options.output ? extractExecutionSummary(options.output) : null;
2445
2997
  let parsedFromLog = null;
@@ -2455,7 +3007,28 @@ function renderSummaryCard(options) {
2455
3007
  }
2456
3008
  }
2457
3009
  let target = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : "";
2458
- if (!target && parsedFromLog?.target) target = parsedFromLog.target;
3010
+ if (!target && parsedFromLog?.target && parsedFromLog.target.toUpperCase() !== "YES") {
3011
+ const rawTarget = parsedFromLog.target;
3012
+ target = rawTarget.startsWith("#") ? options.routine === "peer-review" ? `PR ${rawTarget}` : `Issue ${rawTarget}` : rawTarget;
3013
+ }
3014
+ if (!target && options.output) {
3015
+ const targetMatch = options.output.match(/Selected\s+Target\s+PR:?\s*\[?PR\s*#?(\d+)\]?/i) || options.output.match(/Starting\s+review[^\n#]*?#(\d+)/i) || options.output.match(/Target(?:ing)?\s+(?:issue|PR)\s*#?(\d+)/i) || options.output.match(/Candidate\s+issue\s*#?(\d+)/i);
3016
+ if (targetMatch) {
3017
+ target = options.routine === "peer-review" ? `PR #${targetMatch[1]}` : `Issue #${targetMatch[1]}`;
3018
+ }
3019
+ }
3020
+ let title = options.title || parsedFromLog?.title || "";
3021
+ if (!title && rawSummary) {
3022
+ const titleMatch = rawSummary.match(/\[PR\s*#?\d+\s*\((`?[^`)]+`?)\)\]/i) || rawSummary.match(/Selected\s+Target\s+PR:?\s*\[.*?\]\([^)]+\)\s*\(([^)]+)\)/i) || rawSummary.match(/PR\s*#?\d+[:\s]+`?([^`\n]+)`?/i);
3023
+ if (titleMatch) title = titleMatch[1].replace(/[`*]/g, "").trim();
3024
+ }
3025
+ if (!title && options.output) {
3026
+ const titleMatch = options.output.match(/Selected\s+Target\s+PR:?\s*\[PR\s*#?\d+\s*\((`?[^`)]+`?)\)\]/i) || options.output.match(/Selected\s+candidate\s+issue\s*#?\d+[:\s]+`?([^`\n]+)`?/i);
3027
+ if (titleMatch) title = titleMatch[1].replace(/[`*]/g, "").trim();
3028
+ }
3029
+ if (!title && options.repoRoot && target) {
3030
+ title = fetchTargetTitle(options.repoRoot, target) || "";
3031
+ }
2459
3032
  let decision = parsedFromLog?.decision || "";
2460
3033
  if (!decision && rawSummary) {
2461
3034
  const decisionMatch = rawSummary.match(/\*\*Final Action\*\*:\s*([^\n]+)/i);
@@ -2464,15 +3037,26 @@ function renderSummaryCard(options) {
2464
3037
  const durationStr = options.durationMs ? `${Math.round(options.durationMs / 1e3)}s` : parsedFromLog?.duration || "";
2465
3038
  const lines = [];
2466
3039
  lines.push(pc9.cyan(`\u250C${horizontal}\u2510`));
2467
- const title = ` Jonah Fleet Routine: ${pc9.bold(options.routine.toUpperCase())} `;
3040
+ const headerParts = [pc9.bold(pc9.white(options.routine.toUpperCase()))];
3041
+ if (target) headerParts.push(pc9.yellow(target));
3042
+ if (durationStr) headerParts.push(pc9.dim(`(${durationStr})`));
3043
+ const headerContent = headerParts.join(" \xB7 ");
3044
+ const headerPlain = stripAnsi(headerContent);
2468
3045
  lines.push(
2469
- pc9.cyan("\u2502") + ` ${pc9.bold(pc9.white(options.routine.toUpperCase()))}` + (target ? ` \xB7 ${pc9.yellow(target)}` : "") + (durationStr ? pc9.dim(` (${durationStr})`) : "") + " ".repeat(
2470
- Math.max(
2471
- 1,
2472
- width - 4 - options.routine.length - target.length - (durationStr ? durationStr.length + 3 : 0)
2473
- )
2474
- ) + pc9.cyan("\u2502")
3046
+ pc9.cyan("\u2502") + ` ${headerContent}` + " ".repeat(Math.max(1, width - 3 - headerPlain.length)) + pc9.cyan("\u2502")
2475
3047
  );
3048
+ if (title) {
3049
+ const titlePrefix = " Title: ";
3050
+ const wrappedTitle = wrapText(title, width - 4 - titlePrefix.length);
3051
+ for (let i = 0; i < wrappedTitle.length; i++) {
3052
+ const prefix = i === 0 ? pc9.dim(titlePrefix) : " ".repeat(titlePrefix.length);
3053
+ const text = wrappedTitle[i];
3054
+ const plainLen = titlePrefix.length + stripAnsi(text).length;
3055
+ lines.push(
3056
+ pc9.cyan("\u2502") + ` ${prefix}${pc9.white(pc9.bold(text))}` + " ".repeat(Math.max(1, width - 3 - plainLen)) + pc9.cyan("\u2502")
3057
+ );
3058
+ }
3059
+ }
2476
3060
  if (decision) {
2477
3061
  let decisionBadge = pc9.green(`\u2714 ${decision}`);
2478
3062
  if (/bounce|draft|reject|fail/i.test(decision)) {
@@ -2480,8 +3064,9 @@ function renderSummaryCard(options) {
2480
3064
  } else if (/escalat/i.test(decision)) {
2481
3065
  decisionBadge = pc9.red(`\u{1F6A8} ${decision}`);
2482
3066
  }
3067
+ const decisionPlain = ` Action: ${decision}`;
2483
3068
  lines.push(
2484
- pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width - 11 - decision.length)) + pc9.cyan("\u2502")
3069
+ pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width - 3 - decisionPlain.length)) + pc9.cyan("\u2502")
2485
3070
  );
2486
3071
  }
2487
3072
  lines.push(pc9.cyan(`\u251C${horizontal}\u2524`));
@@ -2503,22 +3088,36 @@ function renderSummaryCard(options) {
2503
3088
  } else if (line.startsWith("- ") || line.startsWith("* ")) {
2504
3089
  const item = sanitizeWorktreePaths(line.slice(2)).trim();
2505
3090
  const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
2506
- const plainLen = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").length;
2507
- if (plainLen <= width - 6) {
2508
- lines.push(pc9.cyan("\u2502") + ` \u2022 ${formatted}` + " ".repeat(Math.max(1, width - 5 - plainLen)) + pc9.cyan("\u2502"));
2509
- } else {
2510
- const truncated = formatted.slice(0, width - 10) + "...";
2511
- lines.push(pc9.cyan("\u2502") + ` \u2022 ${truncated}` + " ".repeat(Math.max(1, width - 5 - (width - 7))) + pc9.cyan("\u2502"));
3091
+ const wrapped = wrapText(formatted, width - 8);
3092
+ for (let i = 0; i < wrapped.length; i++) {
3093
+ const wLine = wrapped[i];
3094
+ const wPlain = stripAnsi(wLine);
3095
+ if (i === 0) {
3096
+ lines.push(
3097
+ pc9.cyan("\u2502") + ` \u2022 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3098
+ );
3099
+ } else {
3100
+ lines.push(
3101
+ pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3102
+ );
3103
+ }
2512
3104
  }
2513
3105
  } else if (/^[0-9]+\.\s+/.test(line)) {
2514
3106
  const item = sanitizeWorktreePaths(line.replace(/^[0-9]+\.\s+/, "")).trim();
2515
3107
  const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
2516
- const plainLen = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").length;
2517
- if (plainLen <= width - 6) {
2518
- lines.push(pc9.cyan("\u2502") + ` \u2714 ${formatted}` + " ".repeat(Math.max(1, width - 5 - plainLen)) + pc9.cyan("\u2502"));
2519
- } else {
2520
- const truncated = formatted.slice(0, width - 10) + "...";
2521
- lines.push(pc9.cyan("\u2502") + ` \u2714 ${truncated}` + " ".repeat(Math.max(1, width - 5 - (width - 7))) + pc9.cyan("\u2502"));
3108
+ const wrapped = wrapText(formatted, width - 8);
3109
+ for (let i = 0; i < wrapped.length; i++) {
3110
+ const wLine = wrapped[i];
3111
+ const wPlain = stripAnsi(wLine);
3112
+ if (i === 0) {
3113
+ lines.push(
3114
+ pc9.cyan("\u2502") + ` \u2714 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3115
+ );
3116
+ } else {
3117
+ lines.push(
3118
+ pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3119
+ );
3120
+ }
2522
3121
  }
2523
3122
  }
2524
3123
  }
@@ -2526,10 +3125,45 @@ function renderSummaryCard(options) {
2526
3125
  lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Verification Passes:")}` + " ".repeat(Math.max(1, width - 23)) + pc9.cyan("\u2502"));
2527
3126
  for (const pass of parsedFromLog.passes.slice(0, 6)) {
2528
3127
  const icon = pass.status === "pass" ? pc9.green("\u2714") : pc9.red("\u2716");
2529
- const text = `${pass.name}${pass.detail || ""}`;
2530
- const plainLen = text.length + 4;
2531
- const truncated = plainLen > width - 6 ? text.slice(0, width - 10) + "..." : text;
2532
- lines.push(pc9.cyan("\u2502") + ` ${icon} ${truncated}` + " ".repeat(Math.max(1, width - 5 - truncated.length)) + pc9.cyan("\u2502"));
3128
+ let criterionName = pass.name;
3129
+ const colonIdx = criterionName.indexOf(":");
3130
+ if (colonIdx > 10 && colonIdx < 40) {
3131
+ criterionName = criterionName.slice(0, colonIdx);
3132
+ }
3133
+ const passText = `${criterionName}${pass.detail || ""}`;
3134
+ const wrapped = wrapText(passText, width - 8);
3135
+ for (let i = 0; i < wrapped.length; i++) {
3136
+ const wLine = wrapped[i];
3137
+ const wPlain = stripAnsi(wLine);
3138
+ if (i === 0) {
3139
+ lines.push(
3140
+ pc9.cyan("\u2502") + ` ${icon} ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3141
+ );
3142
+ } else {
3143
+ lines.push(
3144
+ pc9.cyan("\u2502") + ` ${pc9.dim(wLine)}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3145
+ );
3146
+ }
3147
+ }
3148
+ }
3149
+ if (parsedFromLog.actions && parsedFromLog.actions.length > 0) {
3150
+ lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Actions Taken:")}` + " ".repeat(Math.max(1, width - 16)) + pc9.cyan("\u2502"));
3151
+ for (const action of parsedFromLog.actions.slice(0, 4)) {
3152
+ const wrapped = wrapText(action, width - 8);
3153
+ for (let i = 0; i < wrapped.length; i++) {
3154
+ const wLine = wrapped[i];
3155
+ const wPlain = stripAnsi(wLine);
3156
+ if (i === 0) {
3157
+ lines.push(
3158
+ pc9.cyan("\u2502") + ` \u2022 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3159
+ );
3160
+ } else {
3161
+ lines.push(
3162
+ pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
3163
+ );
3164
+ }
3165
+ }
3166
+ }
2533
3167
  }
2534
3168
  }
2535
3169
  if (parsedFromLog?.logPath) {
@@ -2614,15 +3248,27 @@ var TerminalSpinner = class {
2614
3248
  `);
2615
3249
  }
2616
3250
  }
2617
- render() {
2618
- if (!this.isRunning || !this.isTTY) return;
3251
+ formatLine(message, maxWidth) {
3252
+ const cols = maxWidth ?? (process.stderr.columns || process.stdout.columns || 80);
2619
3253
  const frame = pc9.cyan(this.frames[this.currentFrame]);
2620
- this.currentFrame = (this.currentFrame + 1) % this.frames.length;
2621
3254
  const elapsedSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
2622
3255
  const mins = Math.floor(elapsedSeconds / 60);
2623
3256
  const secs = elapsedSeconds % 60;
2624
- const timeStr = pc9.dim(`[${mins}m ${secs < 10 ? "0" : ""}${secs}s]`);
2625
- 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}`);
2626
3272
  }
2627
3273
  stop() {
2628
3274
  if (!this.isRunning) return;
@@ -2638,6 +3284,97 @@ var TerminalSpinner = class {
2638
3284
  };
2639
3285
 
2640
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
+ }
2641
3378
  function discoverSkillsPrompt(targetDir) {
2642
3379
  const skillsDir = path12.join(targetDir, ".agents", "skills");
2643
3380
  if (!fs12.existsSync(skillsDir)) return "";
@@ -2722,18 +3459,10 @@ Timeout: ${printTimeout}`,
2722
3459
  TARGET_ISSUE: options.issue ? String(options.issue) : "",
2723
3460
  PR_NUMBER: options.pr ? String(options.pr) : ""
2724
3461
  };
2725
- const args = [
2726
- "-p",
2727
- prompt,
2728
- "--model",
2729
- model,
2730
- "--output-format",
2731
- "text",
2732
- "--print-timeout",
2733
- printTimeout,
2734
- "--dangerously-skip-permissions"
2735
- ];
3462
+ const args = buildAgyArgs(prompt, model, printTimeout);
2736
3463
  let output = "";
3464
+ let finalResponseText = "";
3465
+ let accumulatedOutput = "";
2737
3466
  let exitCode = 0;
2738
3467
  const startTime = Date.now();
2739
3468
  const logDir = path12.join(targetDir, ".jonah-fleet");
@@ -2759,39 +3488,127 @@ Timeout: ${printTimeout}`,
2759
3488
  };
2760
3489
  process.once("SIGINT", sigintHandler);
2761
3490
  process.once("SIGTERM", sigintHandler);
2762
- const processChunk = (chunk, isStderr = false) => {
2763
- output += chunk;
2764
- try {
2765
- fs12.appendFileSync(logFilePath, chunk, "utf8");
2766
- } 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
+ }
2767
3501
  }
2768
- if (!dynamicTargetDetected) {
2769
- const detected = routine === "peer-review" ? detectClaimedPR(chunk) : detectClaimedIssue(chunk);
2770
- if (detected) {
2771
- dynamicTargetDetected = true;
2772
- targetLabel = detected;
2773
- options.onTargetDetected?.(detected);
2774
- 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;
2775
3582
  spinner.update(`${targetLabel}: ${activePhase}`);
2776
3583
  }
2777
3584
  }
2778
3585
  }
2779
- if (options.onLog) {
2780
- options.onLog(chunk);
2781
- }
3586
+ });
3587
+ const stderrParser = new LineBufferedStreamParser((line) => {
3588
+ checkTargetDetection(line);
2782
3589
  if (options.verbose) {
2783
- if (isStderr) {
2784
- process.stderr.write(chunk);
2785
- } else {
2786
- process.stdout.write(chunk);
2787
- }
3590
+ console.error(pc10.dim(`[stderr] ${line}`));
2788
3591
  } else if (spinner) {
2789
- const newPhase = detectActivePhase(chunk, activePhase);
3592
+ const newPhase = detectActivePhase(line, activePhase);
2790
3593
  if (newPhase !== activePhase) {
2791
3594
  activePhase = newPhase;
2792
3595
  spinner.update(`${targetLabel}: ${activePhase}`);
2793
3596
  }
2794
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
+ }
2795
3612
  };
2796
3613
  try {
2797
3614
  exitCode = await new Promise((resolve, reject) => {
@@ -2823,6 +3640,9 @@ Timeout: ${printTimeout}`,
2823
3640
  await cleanup();
2824
3641
  }
2825
3642
  }
3643
+ stdoutParser.flush();
3644
+ stderrParser.flush();
3645
+ output = finalResponseText || accumulatedOutput;
2826
3646
  if (options.showCard !== false && !options.verbose) {
2827
3647
  const durationMs = Date.now() - startTime;
2828
3648
  const effectiveIssue = options.issue || (targetLabel.startsWith("Issue #") ? targetLabel.replace("Issue #", "") : void 0);
@@ -2866,31 +3686,31 @@ async function runRoutineCommand(routine, options = {}) {
2866
3686
  const manifest = loadManifest(cwd);
2867
3687
  if (!manifest) {
2868
3688
  console.warn(
2869
- 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.`)
2870
3690
  );
2871
3691
  } else if (manifest.routines && manifest.routines[routine] === false) {
2872
3692
  console.warn(
2873
- 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.`)
2874
3694
  );
2875
3695
  }
2876
- console.log(pc10.cyan(`
2877
- \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)}`));
2878
3698
  if (options.issue) {
2879
- console.log(pc10.dim(` Target issue: #${options.issue}`));
3699
+ console.log(pc11.dim(` Target issue: #${options.issue}`));
2880
3700
  }
2881
3701
  if (options.pr) {
2882
- console.log(pc10.dim(` Target pull request: #${options.pr}`));
3702
+ console.log(pc11.dim(` Target pull request: #${options.pr}`));
2883
3703
  }
2884
3704
  if (options.model) {
2885
- console.log(pc10.dim(` Model override: ${options.model}`));
3705
+ console.log(pc11.dim(` Model override: ${options.model}`));
2886
3706
  }
2887
3707
  if (options.verbose) {
2888
- console.log(pc10.dim(` Verbose output: Enabled (streaming raw tokens)`));
3708
+ console.log(pc11.dim(` Verbose output: Enabled (streaming raw tokens)`));
2889
3709
  }
2890
3710
  if (options.worktree !== false) {
2891
- console.log(pc10.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
3711
+ console.log(pc11.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
2892
3712
  } else {
2893
- console.log(pc10.yellow(` Workspace isolation: Disabled (running in current directory)`));
3713
+ console.log(pc11.yellow(` Workspace isolation: Disabled (running in current directory)`));
2894
3714
  }
2895
3715
  console.log("");
2896
3716
  try {
@@ -2907,56 +3727,431 @@ async function runRoutineCommand(routine, options = {}) {
2907
3727
  verbose: options.verbose
2908
3728
  });
2909
3729
  if (options.dryRun) {
2910
- console.log(pc10.green(result.output));
3730
+ console.log(pc11.green(result.output));
2911
3731
  return;
2912
3732
  }
2913
3733
  if (result.success) {
2914
- console.log(pc10.green(`
3734
+ console.log(pc11.green(`
2915
3735
  \u2713 Local agent session for '${routine}' completed successfully.`));
2916
3736
  } else {
2917
- console.error(pc10.red(`
3737
+ console.error(pc11.red(`
2918
3738
  \u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
2919
3739
  process.exit(result.exitCode);
2920
3740
  }
2921
3741
  } catch (error) {
2922
- console.error(pc10.red(`
3742
+ console.error(pc11.red(`
2923
3743
  \u2717 Failed to execute routine '${routine}': ${error.message}`));
2924
3744
  process.exit(1);
2925
3745
  }
2926
3746
  }
2927
3747
 
2928
3748
  // src/commands/daemon.ts
2929
- import pc12 from "picocolors";
3749
+ import pc14 from "picocolors";
2930
3750
 
2931
3751
  // src/lib/daemon.ts
2932
- import fs13 from "fs";
2933
- import path13 from "path";
3752
+ import fs14 from "fs";
3753
+ import path14 from "path";
2934
3754
  import { spawn as spawn2, execFile as execFile3 } from "child_process";
2935
3755
  import { promisify as promisify3 } from "util";
2936
- import pc11 from "picocolors";
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
+ }
4129
+
4130
+ // src/lib/daemon.ts
4131
+ import pc13 from "picocolors";
2937
4132
  var execFileAsync3 = promisify3(execFile3);
2938
4133
  function getDaemonStatePath(repoRoot) {
2939
- return path13.join(repoRoot, ".jonah-fleet", "daemon.json");
4134
+ return path14.join(repoRoot, ".jonah-fleet", "daemon.json");
2940
4135
  }
2941
4136
  function readDaemonState(repoRoot) {
2942
4137
  const statePath = getDaemonStatePath(repoRoot);
2943
- if (!fs13.existsSync(statePath)) return null;
4138
+ if (!fs14.existsSync(statePath)) return null;
2944
4139
  try {
2945
- return JSON.parse(fs13.readFileSync(statePath, "utf8"));
4140
+ return JSON.parse(fs14.readFileSync(statePath, "utf8"));
2946
4141
  } catch {
2947
4142
  return null;
2948
4143
  }
2949
4144
  }
2950
4145
  function writeDaemonState(repoRoot, state) {
2951
4146
  const statePath = getDaemonStatePath(repoRoot);
2952
- fs13.mkdirSync(path13.dirname(statePath), { recursive: true });
2953
- 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");
2954
4149
  }
2955
4150
  function clearDaemonState(repoRoot) {
2956
4151
  const statePath = getDaemonStatePath(repoRoot);
2957
- if (fs13.existsSync(statePath)) {
4152
+ if (fs14.existsSync(statePath)) {
2958
4153
  try {
2959
- fs13.unlinkSync(statePath);
4154
+ fs14.unlinkSync(statePath);
2960
4155
  } catch {
2961
4156
  }
2962
4157
  }
@@ -2972,16 +4167,22 @@ function isDaemonRunning(repoRoot) {
2972
4167
  return false;
2973
4168
  }
2974
4169
  }
2975
- async function countOpenReadyPRs(repoRoot) {
4170
+ function filterReviewablePRs(prs) {
4171
+ return (prs || []).filter(
4172
+ (pr) => pr && typeof pr.number === "number" && !pr.headRefName?.startsWith("release-please--") && !pr.title?.startsWith("chore(main): release")
4173
+ );
4174
+ }
4175
+ async function getOpenReviewablePRs(repoRoot) {
2976
4176
  try {
2977
4177
  const { stdout } = await execFileAsync3(
2978
4178
  "gh",
2979
- ["pr", "list", "--state", "open", "--draft=false", "--json", "number", "--jq", "length"],
4179
+ ["pr", "list", "--state", "open", "--draft=false", "--json", "number,headRefName,title"],
2980
4180
  { cwd: repoRoot }
2981
4181
  );
2982
- return parseInt(stdout.trim(), 10) || 0;
4182
+ const prs = JSON.parse(stdout);
4183
+ return filterReviewablePRs(prs);
2983
4184
  } catch {
2984
- return 0;
4185
+ return [];
2985
4186
  }
2986
4187
  }
2987
4188
  async function startBackgroundDaemon(repoRoot, options = {}) {
@@ -2992,9 +4193,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
2992
4193
  const reviewInterval = options.reviewInterval || 3;
2993
4194
  const autoworkInterval = options.autoworkInterval || options.interval || 30;
2994
4195
  const routines = options.routines || ["peer-review", "autowork"];
2995
- const logFilePath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
2996
- fs13.mkdirSync(path13.dirname(logFilePath), { recursive: true });
2997
- 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");
2998
4199
  const cliPath = process.argv[1];
2999
4200
  const args = [
3000
4201
  "daemon",
@@ -3043,6 +4244,103 @@ async function stopDaemon(repoRoot) {
3043
4244
  return false;
3044
4245
  }
3045
4246
  }
4247
+ async function drainReviewQueue(drainOptions) {
4248
+ const {
4249
+ repoRoot,
4250
+ state,
4251
+ options = {},
4252
+ isStopping = () => false,
4253
+ clearTicker,
4254
+ getPRs = getOpenReviewablePRs,
4255
+ runRoutine = runLocalRoutine,
4256
+ onAttempted
4257
+ } = drainOptions;
4258
+ if (isStopping()) return;
4259
+ if (state) {
4260
+ state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
4261
+ writeDaemonState(repoRoot, state);
4262
+ }
4263
+ let reviewablePRs = await getPRs(repoRoot);
4264
+ if (reviewablePRs.length === 0) {
4265
+ if (options.verbose) {
4266
+ console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
4267
+ }
4268
+ return;
4269
+ }
4270
+ const attemptedPRNumbers = /* @__PURE__ */ new Set();
4271
+ while (!isStopping() && reviewablePRs.length > 0) {
4272
+ const candidatePRs = reviewablePRs.filter((pr) => !attemptedPRNumbers.has(pr.number));
4273
+ if (candidatePRs.length === 0) {
4274
+ if (options.verbose) {
4275
+ console.log(
4276
+ pc13.dim(
4277
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] All ${reviewablePRs.length} remaining ready PR(s) were already evaluated in this drain pass.`
4278
+ )
4279
+ );
4280
+ }
4281
+ break;
4282
+ }
4283
+ const totalRemaining = candidatePRs.length;
4284
+ let targetPRStr = void 0;
4285
+ try {
4286
+ if (clearTicker) clearTicker();
4287
+ if (state) {
4288
+ state.status = "working";
4289
+ state.activeRoutine = "peer-review";
4290
+ writeDaemonState(repoRoot, state);
4291
+ }
4292
+ console.log(
4293
+ pc13.cyan(
4294
+ `
4295
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Draining PR backlog (${totalRemaining} PR(s) remaining). Starting review session...`
4296
+ )
4297
+ );
4298
+ await cleanupStaleWorktrees(repoRoot);
4299
+ const result = await runRoutine({
4300
+ targetDir: repoRoot,
4301
+ routine: "peer-review",
4302
+ model: options.model,
4303
+ verbose: options.verbose,
4304
+ noWorktree: false,
4305
+ onTargetDetected: (target) => {
4306
+ targetPRStr = target;
4307
+ if (state) {
4308
+ state.activeTarget = target;
4309
+ writeDaemonState(repoRoot, state);
4310
+ }
4311
+ }
4312
+ });
4313
+ const activeTargetStr = targetPRStr;
4314
+ const match = activeTargetStr?.match(/PR\s*#?([0-9]+)/i);
4315
+ const prNum = match ? parseInt(match[1], 10) : candidatePRs[0]?.number;
4316
+ if (typeof prNum === "number") {
4317
+ attemptedPRNumbers.add(prNum);
4318
+ onAttempted?.(prNum);
4319
+ }
4320
+ if (result.success) {
4321
+ console.log(pc13.green(`\u2713 Local peer-review completed successfully.
4322
+ `));
4323
+ } else {
4324
+ console.warn(pc13.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
4325
+ `));
4326
+ }
4327
+ } catch (err) {
4328
+ console.error(pc13.red(`\u2717 Error in peer-review: ${err.message}`));
4329
+ if (candidatePRs[0]) {
4330
+ attemptedPRNumbers.add(candidatePRs[0].number);
4331
+ onAttempted?.(candidatePRs[0].number);
4332
+ }
4333
+ } finally {
4334
+ if (state) {
4335
+ state.status = "idle";
4336
+ state.activeRoutine = void 0;
4337
+ state.activeTarget = void 0;
4338
+ writeDaemonState(repoRoot, state);
4339
+ }
4340
+ }
4341
+ reviewablePRs = await getPRs(repoRoot);
4342
+ }
4343
+ }
3046
4344
  async function runDaemonLoop(repoRoot, options = {}) {
3047
4345
  const reviewInterval = options.reviewInterval || 3;
3048
4346
  const autoworkInterval = options.autoworkInterval || options.interval || 30;
@@ -3056,20 +4354,29 @@ async function runDaemonLoop(repoRoot, options = {}) {
3056
4354
  status: "idle"
3057
4355
  };
3058
4356
  writeDaemonState(repoRoot, state);
3059
- console.log(pc11.cyan(`
4357
+ console.log(pc13.cyan(`
3060
4358
  \u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
3061
- console.log(pc11.dim(` PID: ${process.pid}`));
3062
- console.log(pc11.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
3063
- console.log(pc11.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
3064
- 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)
3065
4364
  `));
3066
4365
  let isStopping = false;
4366
+ let isGracefulStopping = false;
3067
4367
  let isWorking = false;
4368
+ let isPaused = false;
4369
+ let pendingRoutine = null;
4370
+ let keyboard;
4371
+ let tickerInterval;
4372
+ let stopResolve;
3068
4373
  const reviewIntervalMs = reviewInterval * 60 * 1e3;
3069
4374
  const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
3070
4375
  let nextReviewCheckTime = Date.now() + (routines.includes("peer-review") ? reviewIntervalMs : Infinity);
3071
4376
  let nextAutoworkCheckTime = Date.now() + (routines.includes("autowork") ? autoworkIntervalMs : Infinity);
3072
4377
  let lastOpenPRCount = void 0;
4378
+ const getPRsFn = options.getPRs || getOpenReviewablePRs;
4379
+ const runRoutineFn = options.runRoutine || runLocalRoutine;
3073
4380
  const clearTicker = () => {
3074
4381
  if (process.stderr.isTTY && !options.verbose) {
3075
4382
  process.stderr.write("\r\x1B[K");
@@ -3077,60 +4384,188 @@ async function runDaemonLoop(repoRoot, options = {}) {
3077
4384
  };
3078
4385
  const updateTicker = () => {
3079
4386
  if (isStopping || isWorking || options.verbose || !process.stderr.isTTY) return;
3080
- const now = Date.now();
3081
- const nextCheck = Math.min(nextReviewCheckTime, nextAutoworkCheckTime);
3082
- const diffMs = Math.max(0, nextCheck - now);
3083
- const remainingSecs = Math.ceil(diffMs / 1e3);
3084
- const mins = Math.floor(remainingSecs / 60);
3085
- const secs = remainingSecs % 60;
3086
- const timeStr = `${mins}m ${secs < 10 ? "0" : ""}${secs}s`;
3087
- const prStr = lastOpenPRCount !== void 0 ? ` (${lastOpenPRCount} ready PRs)` : "";
3088
- process.stderr.write(
3089
- `\r\x1B[K${pc11.dim("[" + (/* @__PURE__ */ new Date()).toLocaleTimeString() + "]")} \u{1F4A4} ${pc11.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}`
3090
- );
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}`);
3091
4396
  };
3092
- const tickerInterval = setInterval(updateTicker, 1e3);
3093
4397
  const handleStop = async () => {
3094
4398
  if (isStopping) return;
3095
4399
  isStopping = true;
3096
- clearInterval(tickerInterval);
3097
- clearInterval(reviewTimer);
3098
- 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
+ }
3099
4407
  clearTicker();
3100
- console.log(pc11.yellow(`
4408
+ console.log(pc13.yellow(`
3101
4409
  Stopping local agent daemon...`));
3102
4410
  clearDaemonState(repoRoot);
3103
4411
  await cleanupStaleWorktrees(repoRoot);
4412
+ stopResolve?.();
3104
4413
  process.exit(0);
3105
4414
  };
3106
4415
  process.once("SIGINT", handleStop);
3107
4416
  process.once("SIGTERM", handleStop);
3108
- const runReviewCheck = async () => {
4417
+ const performReviewDrain = async () => {
3109
4418
  if (isStopping || isWorking) return;
3110
- state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
4419
+ try {
4420
+ isWorking = true;
4421
+ await drainReviewQueue({
4422
+ repoRoot,
4423
+ state,
4424
+ options,
4425
+ isStopping: () => isStopping,
4426
+ clearTicker,
4427
+ getPRs: getPRsFn,
4428
+ runRoutine: runRoutineFn
4429
+ });
4430
+ const prs = await getPRsFn(repoRoot);
4431
+ lastOpenPRCount = prs.length;
4432
+ } finally {
4433
+ isWorking = false;
4434
+ state.status = isPaused ? "paused" : "idle";
4435
+ state.activeRoutine = void 0;
4436
+ state.activeTarget = void 0;
4437
+ writeDaemonState(repoRoot, state);
4438
+ nextReviewCheckTime = Date.now() + reviewIntervalMs;
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
+ }
4455
+ }
4456
+ };
4457
+ const runAutoworkCheck = async () => {
4458
+ if (isStopping || isWorking || !routines.includes("autowork")) return;
4459
+ if (routines.includes("peer-review")) {
4460
+ const pendingPRs = (await getPRsFn(repoRoot)).length;
4461
+ if (pendingPRs > 0) {
4462
+ console.log(
4463
+ pc13.cyan(
4464
+ `
4465
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
4466
+ )
4467
+ );
4468
+ await performReviewDrain();
4469
+ const remainingPRs = (await getPRsFn(repoRoot)).length;
4470
+ if (remainingPRs > 0) {
4471
+ console.log(
4472
+ pc13.yellow(
4473
+ `
4474
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Review backlog still has ${remainingPRs} pending PR(s). Postponing autowork session.`
4475
+ )
4476
+ );
4477
+ nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
4478
+ updateTicker();
4479
+ return;
4480
+ }
4481
+ }
4482
+ }
4483
+ state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
3111
4484
  writeDaemonState(repoRoot, state);
3112
- const openPRCount = await countOpenReadyPRs(repoRoot);
3113
- lastOpenPRCount = openPRCount;
3114
- if (openPRCount === 0) {
3115
- if (options.verbose) {
3116
- console.log(pc11.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
4485
+ try {
4486
+ isWorking = true;
4487
+ clearTicker();
4488
+ state.status = "working";
4489
+ state.activeRoutine = "autowork";
4490
+ writeDaemonState(repoRoot, state);
4491
+ console.log(pc13.cyan(`
4492
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
4493
+ await cleanupStaleWorktrees(repoRoot);
4494
+ const result = await runRoutineFn({
4495
+ targetDir: repoRoot,
4496
+ routine: "autowork",
4497
+ model: options.model,
4498
+ verbose: options.verbose,
4499
+ noWorktree: false,
4500
+ onTargetDetected: (target) => {
4501
+ state.activeTarget = target;
4502
+ writeDaemonState(repoRoot, state);
4503
+ }
4504
+ });
4505
+ if (result.success) {
4506
+ console.log(pc13.green(`\u2713 Local autowork completed successfully.
4507
+ `));
4508
+ } else {
4509
+ console.warn(pc13.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
4510
+ `));
3117
4511
  }
3118
- nextReviewCheckTime = Date.now() + reviewIntervalMs;
4512
+ } catch (err) {
4513
+ console.error(pc13.red(`\u2717 Error in autowork: ${err.message}`));
4514
+ } finally {
4515
+ isWorking = false;
4516
+ state.status = isPaused ? "paused" : "idle";
4517
+ state.activeRoutine = void 0;
4518
+ state.activeTarget = void 0;
4519
+ writeDaemonState(repoRoot, state);
4520
+ nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
3119
4521
  updateTicker();
3120
- return;
4522
+ if (!isStopping && routines.includes("peer-review")) {
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
+ }
3121
4549
  }
4550
+ };
4551
+ const runTargetedReview = async (prNumber) => {
4552
+ if (isStopping || isWorking) return;
3122
4553
  try {
3123
4554
  isWorking = true;
3124
4555
  clearTicker();
3125
4556
  state.status = "working";
3126
4557
  state.activeRoutine = "peer-review";
4558
+ state.activeTarget = `PR #${prNumber}`;
3127
4559
  writeDaemonState(repoRoot, state);
3128
- console.log(pc11.cyan(`
3129
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Found ${openPRCount} ready PR(s). Starting review session...`));
4560
+ console.log(
4561
+ pc13.cyan(`
4562
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Peer Review: Starting session on PR #${prNumber}...`)
4563
+ );
3130
4564
  await cleanupStaleWorktrees(repoRoot);
3131
- const result = await runLocalRoutine({
4565
+ const result = await runRoutineFn({
3132
4566
  targetDir: repoRoot,
3133
4567
  routine: "peer-review",
4568
+ pr: prNumber,
3134
4569
  model: options.model,
3135
4570
  verbose: options.verbose,
3136
4571
  noWorktree: false,
@@ -3140,40 +4575,56 @@ Stopping local agent daemon...`));
3140
4575
  }
3141
4576
  });
3142
4577
  if (result.success) {
3143
- console.log(pc11.green(`\u2713 Local peer-review completed successfully.
4578
+ console.log(pc13.green(`\u2713 Targeted peer-review on PR #${prNumber} completed successfully.
3144
4579
  `));
3145
4580
  } else {
3146
- console.warn(pc11.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
4581
+ console.warn(pc13.yellow(`\u26A0\uFE0F Targeted peer-review on PR #${prNumber} completed with code ${result.exitCode}.
3147
4582
  `));
3148
4583
  }
3149
4584
  } catch (err) {
3150
- console.error(pc11.red(`\u2717 Error in peer-review: ${err.message}`));
4585
+ console.error(pc13.red(`\u2717 Error in targeted peer-review: ${err.message}`));
3151
4586
  } finally {
3152
4587
  isWorking = false;
3153
- state.status = "idle";
4588
+ state.status = isPaused ? "paused" : "idle";
3154
4589
  state.activeRoutine = void 0;
3155
4590
  state.activeTarget = void 0;
3156
4591
  writeDaemonState(repoRoot, state);
3157
- nextReviewCheckTime = Date.now() + reviewIntervalMs;
3158
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
+ }
3159
4608
  }
3160
4609
  };
3161
- const runAutoworkCheck = async () => {
3162
- if (isStopping || isWorking || !routines.includes("autowork")) return;
3163
- state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
3164
- writeDaemonState(repoRoot, state);
4610
+ const runTargetedAutowork = async (issueNumber) => {
4611
+ if (isStopping || isWorking) return;
3165
4612
  try {
3166
4613
  isWorking = true;
3167
4614
  clearTicker();
3168
4615
  state.status = "working";
3169
4616
  state.activeRoutine = "autowork";
4617
+ state.activeTarget = `Issue #${issueNumber}`;
3170
4618
  writeDaemonState(repoRoot, state);
3171
- console.log(pc11.cyan(`
3172
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
4619
+ console.log(
4620
+ pc13.cyan(`
4621
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Autowork: Starting session on Issue #${issueNumber}...`)
4622
+ );
3173
4623
  await cleanupStaleWorktrees(repoRoot);
3174
- const result = await runLocalRoutine({
4624
+ const result = await runRoutineFn({
3175
4625
  targetDir: repoRoot,
3176
4626
  routine: "autowork",
4627
+ issue: issueNumber,
3177
4628
  model: options.model,
3178
4629
  verbose: options.verbose,
3179
4630
  noWorktree: false,
@@ -3183,33 +4634,278 @@ Stopping local agent daemon...`));
3183
4634
  }
3184
4635
  });
3185
4636
  if (result.success) {
3186
- console.log(pc11.green(`\u2713 Local autowork completed successfully.
4637
+ console.log(pc13.green(`\u2713 Targeted autowork on Issue #${issueNumber} completed successfully.
3187
4638
  `));
3188
4639
  } else {
3189
- console.warn(pc11.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
4640
+ console.warn(pc13.yellow(`\u26A0\uFE0F Targeted autowork on Issue #${issueNumber} completed with code ${result.exitCode}.
3190
4641
  `));
3191
4642
  }
3192
4643
  } catch (err) {
3193
- console.error(pc11.red(`\u2717 Error in autowork: ${err.message}`));
4644
+ console.error(pc13.red(`\u2717 Error in targeted autowork: ${err.message}`));
3194
4645
  } finally {
3195
4646
  isWorking = false;
3196
- state.status = "idle";
4647
+ state.status = isPaused ? "paused" : "idle";
3197
4648
  state.activeRoutine = void 0;
3198
4649
  state.activeTarget = void 0;
3199
4650
  writeDaemonState(repoRoot, state);
3200
- nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
3201
4651
  updateTicker();
4652
+ if (!isStopping && routines.includes("peer-review")) {
4653
+ const newPRCount = (await getPRsFn(repoRoot)).length;
4654
+ if (newPRCount > 0) {
4655
+ console.log(
4656
+ pc13.cyan(
4657
+ `
4658
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
4659
+ )
4660
+ );
4661
+ await performReviewDrain();
4662
+ }
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
+ }
3202
4679
  }
3203
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();
3204
4884
  if (routines.includes("peer-review")) {
3205
- await runReviewCheck();
4885
+ await performReviewDrain();
3206
4886
  }
3207
- if (routines.includes("autowork")) {
4887
+ if (!isStopping && !isGracefulStopping && routines.includes("autowork")) {
3208
4888
  await runAutoworkCheck();
3209
4889
  }
3210
- const reviewTimer = setInterval(runReviewCheck, reviewIntervalMs);
3211
- const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
3212
- 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;
3213
4909
  });
3214
4910
  }
3215
4911
 
@@ -3233,16 +4929,16 @@ async function runDaemonCommand(action, options = {}) {
3233
4929
  }
3234
4930
  try {
3235
4931
  const state2 = await startBackgroundDaemon(cwd, daemonOpts);
3236
- console.log(pc12.green(`
4932
+ console.log(pc14.green(`
3237
4933
  \u2713 Background agent daemon started successfully.`));
3238
- console.log(pc12.dim(` PID: ${state2.pid}`));
3239
- console.log(pc12.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
3240
- console.log(pc12.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
3241
- console.log(pc12.dim(` Routines: ${state2.routines.join(", ")}`));
3242
- console.log(pc12.dim(` Log file: .jonah-fleet/daemon.log`));
3243
- 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.`));
3244
4940
  } catch (err) {
3245
- console.error(pc12.red(`
4941
+ console.error(pc14.red(`
3246
4942
  \u2717 Failed to start daemon: ${err.message}`));
3247
4943
  process.exit(1);
3248
4944
  }
@@ -3250,18 +4946,18 @@ async function runDaemonCommand(action, options = {}) {
3250
4946
  }
3251
4947
  if (act === "stop") {
3252
4948
  if (!isDaemonRunning(cwd)) {
3253
- console.log(pc12.yellow(`
4949
+ console.log(pc14.yellow(`
3254
4950
  \u26A0\uFE0F No local agent daemon is currently running in this repository.`));
3255
4951
  return;
3256
4952
  }
3257
4953
  const state2 = readDaemonState(cwd);
3258
- console.log(pc12.cyan(`
4954
+ console.log(pc14.cyan(`
3259
4955
  Stopping background agent daemon (PID ${state2?.pid})...`));
3260
4956
  const stopped = await stopDaemon(cwd);
3261
4957
  if (stopped) {
3262
- console.log(pc12.green(`\u2713 Local agent daemon stopped successfully.`));
4958
+ console.log(pc14.green(`\u2713 Local agent daemon stopped successfully.`));
3263
4959
  } else {
3264
- console.error(pc12.red(`\u2717 Could not terminate daemon process.`));
4960
+ console.error(pc14.red(`\u2717 Could not terminate daemon process.`));
3265
4961
  process.exit(1);
3266
4962
  }
3267
4963
  return;
@@ -3273,18 +4969,24 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
3273
4969
  const running = isDaemonRunning(cwd);
3274
4970
  const state = readDaemonState(cwd);
3275
4971
  const activeWorktrees = await listActiveWorktrees(cwd);
3276
- console.log(pc12.cyan(`
4972
+ console.log(pc14.cyan(`
3277
4973
  \u{1F916} Jonah Fleet Local Daemon Status
3278
4974
  `));
3279
4975
  if (running && state) {
3280
- console.log(` Status: ${pc12.green(pc12.bold("RUNNING"))}`);
4976
+ console.log(` Status: ${pc14.green(pc14.bold("RUNNING"))}`);
3281
4977
  console.log(` PID: ${state.pid}`);
3282
4978
  console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
3283
4979
  console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
3284
4980
  console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
3285
4981
  console.log(` Routines: ${state.routines.join(", ")}`);
3286
- const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc12.bold(state.activeTarget)})` : "");
3287
- 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}`);
3288
4990
  if (state.lastReviewCheckAt) {
3289
4991
  console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
3290
4992
  }
@@ -3292,17 +4994,127 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
3292
4994
  console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
3293
4995
  }
3294
4996
  } else {
3295
- console.log(` Status: ${pc12.gray("STOPPED")}`);
3296
- 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.`));
3297
4999
  }
3298
5000
  console.log(`
3299
5001
  Active Worktrees: ${activeWorktrees.length}`);
3300
5002
  for (const wt of activeWorktrees) {
3301
- console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
5003
+ console.log(pc14.dim(` - [${wt.branch}] ${wt.path}`));
3302
5004
  }
3303
5005
  console.log("");
3304
5006
  }
3305
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
+
3306
5118
  // src/index.ts
3307
5119
  var program = new Command();
3308
5120
  program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
@@ -3312,7 +5124,11 @@ program.command("run <routine>").description("Run a specific prompt routine loca
3312
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) => {
3313
5125
  await runDaemonCommand(action, options);
3314
5126
  });
3315
- 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) => {
3316
5132
  await runInit(options);
3317
5133
  });
3318
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) => {