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