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