jonah-fleet 1.5.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -0
- package/README.md +25 -2
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/labels.d.ts +11 -0
- package/dist/commands/labels.d.ts.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/index.js +2292 -476
- 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 +41 -2
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/evals.d.ts +56 -1
- package/dist/lib/evals.d.ts.map +1 -1
- package/dist/lib/labels.d.ts +48 -0
- package/dist/lib/labels.d.ts.map +1 -0
- package/dist/lib/manifest.d.ts +6 -0
- package/dist/lib/manifest.d.ts.map +1 -1
- package/dist/lib/presets.d.ts +47 -1
- package/dist/lib/presets.d.ts.map +1 -1
- package/dist/lib/runner.d.ts +66 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/telemetry.d.ts +13 -0
- package/dist/lib/telemetry.d.ts.map +1 -1
- package/dist/lib/terminal-card.d.ts +23 -0
- package/dist/lib/terminal-card.d.ts.map +1 -1
- package/package.json +4 -4
- package/schema.json +68 -0
- package/templates/evals/ambiguity-benchmark.json +92 -0
- package/templates/prompts/ORCHESTRATION.md +57 -36
- package/templates/prompts/_prompt-template.md +13 -1
- package/templates/prompts/autowork.md +63 -27
- package/templates/prompts/issues-housekeeping.md +1 -1
- package/templates/prompts/optimizer.md +3 -0
- package/templates/prompts/peer-review.md +13 -3
- package/templates/workflows/autowork-cron.yml +74 -2
- package/templates/workflows/dependency-check-cron.yml +41 -2
- package/templates/workflows/issues-housekeeping-cron.yml +41 -2
- package/templates/workflows/prompt-optimizer-cron.yml +41 -2
- package/templates/workflows/trigger-autowork-manual.yml +43 -2
- package/templates/workflows/trigger-autowork-on-bug.yml +47 -7
- package/templates/workflows/trigger-autowork-on-merge.yml +48 -7
- package/templates/workflows/trigger-review-routine.yml +46 -7
package/dist/index.js
CHANGED
|
@@ -12,6 +12,33 @@ import fs from "fs";
|
|
|
12
12
|
import path from "path";
|
|
13
13
|
|
|
14
14
|
// src/lib/presets.ts
|
|
15
|
+
var DEFAULT_MODELS_CONFIG = {
|
|
16
|
+
default: "gemini-3.7-flash-high",
|
|
17
|
+
"issues-housekeeping": "gemini-3.7-flash",
|
|
18
|
+
"dependency-update-security-check": "gemini-3.7-flash",
|
|
19
|
+
"analytics-review": "gemini-3.7-flash"
|
|
20
|
+
};
|
|
21
|
+
var DEFAULT_BUDGETS_CONFIG = {
|
|
22
|
+
weeklyTokens: 875e4,
|
|
23
|
+
timeoutMinutes: {
|
|
24
|
+
autowork: 60,
|
|
25
|
+
"peer-review": 55,
|
|
26
|
+
optimizer: 35,
|
|
27
|
+
"issues-housekeeping": 40,
|
|
28
|
+
"dependency-update-security-check": 25,
|
|
29
|
+
"product-planning": 45,
|
|
30
|
+
"analytics-review": 30
|
|
31
|
+
},
|
|
32
|
+
maxIterations: {
|
|
33
|
+
autowork: 65,
|
|
34
|
+
"peer-review": 40,
|
|
35
|
+
optimizer: 30,
|
|
36
|
+
"issues-housekeeping": 30,
|
|
37
|
+
"dependency-update-security-check": 20,
|
|
38
|
+
"product-planning": 40,
|
|
39
|
+
"analytics-review": 25
|
|
40
|
+
}
|
|
41
|
+
};
|
|
15
42
|
var PRESET_CONFIGS = {
|
|
16
43
|
minimal: {
|
|
17
44
|
routines: {
|
|
@@ -92,7 +119,7 @@ var ROUTINE_TO_WORKFLOW_MAP = {
|
|
|
92
119
|
"product-planning": [],
|
|
93
120
|
"analytics-review": []
|
|
94
121
|
};
|
|
95
|
-
var FLEET_VERSION = "1.
|
|
122
|
+
var FLEET_VERSION = "1.6.0";
|
|
96
123
|
var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
|
|
97
124
|
|
|
98
125
|
// src/lib/manifest.ts
|
|
@@ -129,6 +156,12 @@ function createDefaultManifest(preset = "standard") {
|
|
|
129
156
|
"analytics-review": false
|
|
130
157
|
},
|
|
131
158
|
skills: PRESET_CONFIGS.standard.skills,
|
|
159
|
+
models: { ...DEFAULT_MODELS_CONFIG },
|
|
160
|
+
budgets: {
|
|
161
|
+
weeklyTokens: DEFAULT_BUDGETS_CONFIG.weeklyTokens,
|
|
162
|
+
timeoutMinutes: { ...DEFAULT_BUDGETS_CONFIG.timeoutMinutes },
|
|
163
|
+
maxIterations: { ...DEFAULT_BUDGETS_CONFIG.maxIterations }
|
|
164
|
+
},
|
|
132
165
|
autoUpdate: {
|
|
133
166
|
enabled: true,
|
|
134
167
|
channel: "stable"
|
|
@@ -142,6 +175,12 @@ function createDefaultManifest(preset = "standard") {
|
|
|
142
175
|
preset,
|
|
143
176
|
routines: { ...config.routines },
|
|
144
177
|
skills: [...config.skills],
|
|
178
|
+
models: { ...DEFAULT_MODELS_CONFIG },
|
|
179
|
+
budgets: {
|
|
180
|
+
weeklyTokens: DEFAULT_BUDGETS_CONFIG.weeklyTokens,
|
|
181
|
+
timeoutMinutes: { ...DEFAULT_BUDGETS_CONFIG.timeoutMinutes },
|
|
182
|
+
maxIterations: { ...DEFAULT_BUDGETS_CONFIG.maxIterations }
|
|
183
|
+
},
|
|
145
184
|
autoUpdate: {
|
|
146
185
|
enabled: true,
|
|
147
186
|
channel: "stable"
|
|
@@ -629,235 +668,10 @@ function installFleet(targetDir, manifest, options = {}) {
|
|
|
629
668
|
return result;
|
|
630
669
|
}
|
|
631
670
|
|
|
632
|
-
// src/commands/init.ts
|
|
633
|
-
async function promptQuestion(query, defaultValue) {
|
|
634
|
-
const rl = readline.createInterface({
|
|
635
|
-
input: process.stdin,
|
|
636
|
-
output: process.stdout
|
|
637
|
-
});
|
|
638
|
-
return new Promise((resolve) => {
|
|
639
|
-
rl.question(`${query} [${defaultValue}]: `, (answer) => {
|
|
640
|
-
rl.close();
|
|
641
|
-
resolve(answer.trim() || defaultValue);
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
async function runInit(options = {}) {
|
|
646
|
-
const cwd = options.cwd || process.cwd();
|
|
647
|
-
const preset = options.preset || "standard";
|
|
648
|
-
console.log(pc.cyan(`
|
|
649
|
-
\u2693 Initializing Jonah Fleet (preset: ${pc.bold(preset)}) in ${cwd}
|
|
650
|
-
`));
|
|
651
|
-
let detected = detectTechStack(cwd);
|
|
652
|
-
console.log(pc.bold("\u{1F50D} Tech Stack Auto-Detection:"));
|
|
653
|
-
console.log(pc.cyan(` - Detected Stack: ${pc.bold(detected.name)}`));
|
|
654
|
-
console.log(pc.cyan(` - Language: ${detected.language}`));
|
|
655
|
-
if (detected.framework) {
|
|
656
|
-
console.log(pc.cyan(` - Framework: ${detected.framework}`));
|
|
657
|
-
}
|
|
658
|
-
console.log(pc.cyan(` - Package Manager: ${detected.packageManager}`));
|
|
659
|
-
if (detected.testFramework) {
|
|
660
|
-
console.log(pc.cyan(` - Testing: ${detected.testFramework}`));
|
|
661
|
-
}
|
|
662
|
-
if (detected.commands.test) {
|
|
663
|
-
console.log(pc.cyan(` - Test Command: ${detected.commands.test}`));
|
|
664
|
-
}
|
|
665
|
-
const isInteractive = options.interactive ?? (process.stdin.isTTY && !options.stack && !options.testCmd);
|
|
666
|
-
if (isInteractive && process.stdin.isTTY) {
|
|
667
|
-
console.log(pc.yellow("\n\u2699\uFE0F Configure project settings (press enter to accept defaults):"));
|
|
668
|
-
const stackName = await promptQuestion("Tech Stack Name", detected.name);
|
|
669
|
-
const pkgManager = await promptQuestion("Package Manager", detected.packageManager);
|
|
670
|
-
const testCmd = await promptQuestion("Test Command", detected.commands.test || "npm test");
|
|
671
|
-
const buildCmd = await promptQuestion("Build Command", detected.commands.build || "npm run build");
|
|
672
|
-
detected = {
|
|
673
|
-
...detected,
|
|
674
|
-
name: stackName,
|
|
675
|
-
language: stackName,
|
|
676
|
-
framework: void 0,
|
|
677
|
-
packageManager: pkgManager,
|
|
678
|
-
commands: {
|
|
679
|
-
...detected.commands,
|
|
680
|
-
test: testCmd,
|
|
681
|
-
build: buildCmd
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
if (options.stack) {
|
|
686
|
-
detected.name = options.stack;
|
|
687
|
-
detected.language = options.stack;
|
|
688
|
-
detected.framework = void 0;
|
|
689
|
-
}
|
|
690
|
-
if (options.packageManager) {
|
|
691
|
-
detected.packageManager = options.packageManager;
|
|
692
|
-
}
|
|
693
|
-
if (options.testCmd) {
|
|
694
|
-
detected.commands.test = options.testCmd;
|
|
695
|
-
}
|
|
696
|
-
if (options.buildCmd) {
|
|
697
|
-
detected.commands.build = options.buildCmd;
|
|
698
|
-
}
|
|
699
|
-
let manifest = loadManifest(cwd);
|
|
700
|
-
if (manifest && !options.force) {
|
|
701
|
-
console.log(pc.yellow(`
|
|
702
|
-
\u26A0\uFE0F Found existing agents-manifest.json. Updating with preset '${preset}'...`));
|
|
703
|
-
} else {
|
|
704
|
-
manifest = createDefaultManifest(preset);
|
|
705
|
-
}
|
|
706
|
-
saveManifest(cwd, manifest);
|
|
707
|
-
console.log(pc.green(`\u2713 Created/Updated agents-manifest.json`));
|
|
708
|
-
const result = installFleet(cwd, manifest, { force: options.force, detectedStack: detected });
|
|
709
|
-
console.log(pc.bold("\nInstalled components:"));
|
|
710
|
-
if (result.promptsInstalled.length > 0) {
|
|
711
|
-
console.log(pc.green(` \u{1F4C1} Prompts (.github/prompts/):`));
|
|
712
|
-
result.promptsInstalled.forEach((p) => console.log(` - ${p}`));
|
|
713
|
-
}
|
|
714
|
-
if (result.workflowsInstalled.length > 0) {
|
|
715
|
-
console.log(pc.green(` \u2699\uFE0F Workflows (.github/workflows/):`));
|
|
716
|
-
result.workflowsInstalled.forEach((w) => console.log(` - ${w}`));
|
|
717
|
-
}
|
|
718
|
-
if (result.skillsInstalled.length > 0) {
|
|
719
|
-
console.log(pc.green(` \u{1F9E0} Skills (.agents/skills/):`));
|
|
720
|
-
result.skillsInstalled.forEach((s) => console.log(` - ${s}`));
|
|
721
|
-
}
|
|
722
|
-
if (result.docsInstalled.length > 0) {
|
|
723
|
-
console.log(pc.green(` \u{1F4C4} Documentation:`));
|
|
724
|
-
result.docsInstalled.forEach((d) => console.log(` - ${d}`));
|
|
725
|
-
}
|
|
726
|
-
console.log(pc.bold(pc.green("\n\u{1F389} Jonah Fleet initialization complete!\n")));
|
|
727
|
-
console.log(pc.cyan("Next steps for GitHub repository configuration:"));
|
|
728
|
-
console.log(" 1. In Settings \u2192 Actions \u2192 General \u2192 Workflow permissions:");
|
|
729
|
-
console.log(' Select "Read and write permissions" and check "Allow GitHub Actions to create and approve pull requests".');
|
|
730
|
-
console.log(" 2. In Settings \u2192 Actions \u2192 General \u2192 Fork pull request workflows:");
|
|
731
|
-
console.log(" Configure workflow approval settings to prevent automated runs from stalling awaiting approval.");
|
|
732
|
-
console.log(" 3. Customize project context, build, and test commands in AGENTS.md.\n");
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// src/commands/sync.ts
|
|
736
|
-
import pc2 from "picocolors";
|
|
737
|
-
|
|
738
|
-
// src/lib/diff.ts
|
|
739
|
-
import fs4 from "fs";
|
|
740
|
-
import path4 from "path";
|
|
741
|
-
function checkDrift(targetDir, manifest) {
|
|
742
|
-
const templatesDir = getTemplatesDir();
|
|
743
|
-
const report = {
|
|
744
|
-
missingPrompts: [],
|
|
745
|
-
modifiedPrompts: [],
|
|
746
|
-
missingWorkflows: [],
|
|
747
|
-
modifiedWorkflows: [],
|
|
748
|
-
missingSkills: []
|
|
749
|
-
};
|
|
750
|
-
const targetPromptsDir = path4.join(targetDir, ".github/prompts");
|
|
751
|
-
const targetWorkflowsDir = path4.join(targetDir, ".github/workflows");
|
|
752
|
-
const targetSkillsDir = path4.join(targetDir, ".agents/skills");
|
|
753
|
-
const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
|
|
754
|
-
for (const file of basePrompts) {
|
|
755
|
-
const src = path4.join(templatesDir, "prompts", file);
|
|
756
|
-
const dest = path4.join(targetPromptsDir, file);
|
|
757
|
-
if (!fs4.existsSync(dest)) {
|
|
758
|
-
report.missingPrompts.push(file);
|
|
759
|
-
} else if (fs4.readFileSync(src, "utf8") !== fs4.readFileSync(dest, "utf8")) {
|
|
760
|
-
report.modifiedPrompts.push(file);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
|
|
764
|
-
if (!isEnabled) continue;
|
|
765
|
-
const promptFile = `${routineName}.md`;
|
|
766
|
-
const promptSrc = path4.join(templatesDir, "prompts", promptFile);
|
|
767
|
-
const promptDest = path4.join(targetPromptsDir, promptFile);
|
|
768
|
-
if (!fs4.existsSync(promptDest)) {
|
|
769
|
-
report.missingPrompts.push(promptFile);
|
|
770
|
-
} else if (fs4.existsSync(promptSrc) && fs4.readFileSync(promptSrc, "utf8") !== fs4.readFileSync(promptDest, "utf8")) {
|
|
771
|
-
report.modifiedPrompts.push(promptFile);
|
|
772
|
-
}
|
|
773
|
-
const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
|
|
774
|
-
for (const workflowFile of workflows) {
|
|
775
|
-
const wfSrc = path4.join(templatesDir, "workflows", workflowFile);
|
|
776
|
-
const wfDest = path4.join(targetWorkflowsDir, workflowFile);
|
|
777
|
-
if (!fs4.existsSync(wfDest)) {
|
|
778
|
-
report.missingWorkflows.push(workflowFile);
|
|
779
|
-
} else if (fs4.existsSync(wfSrc)) {
|
|
780
|
-
const rawSrc = fs4.readFileSync(wfSrc, "utf8");
|
|
781
|
-
const destContent = fs4.readFileSync(wfDest, "utf8");
|
|
782
|
-
const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
|
|
783
|
-
const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
|
|
784
|
-
if (expectedSrc !== destContent) {
|
|
785
|
-
report.modifiedWorkflows.push(workflowFile);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
if (manifest.autoUpdate?.enabled) {
|
|
791
|
-
const syncWfSrc = path4.join(templatesDir, "workflows/sync-fleet.yml");
|
|
792
|
-
const syncWfDest = path4.join(targetWorkflowsDir, "sync-fleet.yml");
|
|
793
|
-
if (!fs4.existsSync(syncWfDest)) {
|
|
794
|
-
report.missingWorkflows.push("sync-fleet.yml");
|
|
795
|
-
} else if (fs4.existsSync(syncWfSrc)) {
|
|
796
|
-
const rawSrc = fs4.readFileSync(syncWfSrc, "utf8");
|
|
797
|
-
const destContent = fs4.readFileSync(syncWfDest, "utf8");
|
|
798
|
-
const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
|
|
799
|
-
const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
|
|
800
|
-
if (expectedSrc !== destContent) {
|
|
801
|
-
report.modifiedWorkflows.push("sync-fleet.yml");
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
for (const skill of manifest.skills) {
|
|
806
|
-
const skillDestDir = path4.join(targetSkillsDir, skill);
|
|
807
|
-
if (!fs4.existsSync(skillDestDir)) {
|
|
808
|
-
report.missingSkills.push(skill);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
return report;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// src/commands/sync.ts
|
|
815
|
-
async function runSync(options = {}) {
|
|
816
|
-
const cwd = options.cwd || process.cwd();
|
|
817
|
-
const manifest = loadManifest(cwd);
|
|
818
|
-
if (!manifest) {
|
|
819
|
-
console.error(pc2.red(`\u274C No agents-manifest.json found in ${cwd}. Run 'jonah-fleet init' first.`));
|
|
820
|
-
process.exit(1);
|
|
821
|
-
}
|
|
822
|
-
console.log(pc2.cyan(`
|
|
823
|
-
\u{1F504} Syncing Jonah Fleet (current: v${manifest.version}, fleet: v${FLEET_VERSION})...
|
|
824
|
-
`));
|
|
825
|
-
const drift = checkDrift(cwd, manifest);
|
|
826
|
-
const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
|
|
827
|
-
if (options.check) {
|
|
828
|
-
if (!hasDrift && manifest.version === FLEET_VERSION) {
|
|
829
|
-
console.log(pc2.green(`\u2713 All routines, workflows, and skills are perfectly in sync with v${FLEET_VERSION}.
|
|
830
|
-
`));
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
console.log(pc2.yellow(`\u26A0\uFE0F Drift or updates detected:`));
|
|
834
|
-
if (drift.missingPrompts.length > 0) console.log(pc2.red(` Missing prompts: ${drift.missingPrompts.join(", ")}`));
|
|
835
|
-
if (drift.modifiedPrompts.length > 0) console.log(pc2.yellow(` Modified prompts: ${drift.modifiedPrompts.join(", ")}`));
|
|
836
|
-
if (drift.missingWorkflows.length > 0) console.log(pc2.red(` Missing workflows: ${drift.missingWorkflows.join(", ")}`));
|
|
837
|
-
if (drift.modifiedWorkflows.length > 0) console.log(pc2.yellow(` Modified workflows: ${drift.modifiedWorkflows.join(", ")}`));
|
|
838
|
-
if (drift.missingSkills.length > 0) console.log(pc2.red(` Missing skills: ${drift.missingSkills.join(", ")}`));
|
|
839
|
-
console.log(pc2.cyan(`
|
|
840
|
-
Run 'jonah-fleet sync --force' to apply updates.
|
|
841
|
-
`));
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
manifest.version = FLEET_VERSION;
|
|
845
|
-
saveManifest(cwd, manifest);
|
|
846
|
-
const result = installFleet(cwd, manifest, { force: true });
|
|
847
|
-
console.log(pc2.green(`\u2713 Synchronized with Jonah Fleet v${FLEET_VERSION}`));
|
|
848
|
-
console.log(pc2.green(`\u2713 Updated ${result.promptsInstalled.length} prompts, ${result.workflowsInstalled.length} workflows, and ${result.skillsInstalled.length} skills.
|
|
849
|
-
`));
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/commands/status.ts
|
|
853
|
-
import fs7 from "fs";
|
|
854
|
-
import path7 from "path";
|
|
855
|
-
import pc5 from "picocolors";
|
|
856
|
-
|
|
857
671
|
// src/lib/fleet-query.ts
|
|
858
672
|
import { execFile } from "child_process";
|
|
859
|
-
import
|
|
860
|
-
import
|
|
673
|
+
import fs4 from "fs";
|
|
674
|
+
import path4 from "path";
|
|
861
675
|
import { promisify } from "util";
|
|
862
676
|
var execFileAsync = promisify(execFile);
|
|
863
677
|
var defaultGhExecutor = async (args) => {
|
|
@@ -1036,11 +850,11 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
|
|
|
1036
850
|
staleWarnings: []
|
|
1037
851
|
};
|
|
1038
852
|
try {
|
|
1039
|
-
if (
|
|
1040
|
-
const manifestPath =
|
|
1041
|
-
if (
|
|
853
|
+
if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
|
|
854
|
+
const manifestPath = path4.join(repoIdentifier, "agents-manifest.json");
|
|
855
|
+
if (fs4.existsSync(manifestPath)) {
|
|
1042
856
|
try {
|
|
1043
|
-
const raw = JSON.parse(
|
|
857
|
+
const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
|
|
1044
858
|
result.fleetVersion = raw.version;
|
|
1045
859
|
result.preset = raw.preset;
|
|
1046
860
|
} catch {
|
|
@@ -1120,18 +934,18 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
|
|
|
1120
934
|
if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
|
|
1121
935
|
}
|
|
1122
936
|
const logContents = [];
|
|
1123
|
-
if (
|
|
1124
|
-
const logsDir =
|
|
1125
|
-
if (
|
|
937
|
+
if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
|
|
938
|
+
const logsDir = path4.join(repoIdentifier, ".github/prompts/logs");
|
|
939
|
+
if (fs4.existsSync(logsDir)) {
|
|
1126
940
|
const collectLogs = (dir) => {
|
|
1127
|
-
const entries =
|
|
941
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
1128
942
|
for (const entry of entries) {
|
|
1129
|
-
const fullPath =
|
|
943
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1130
944
|
if (entry.isDirectory()) {
|
|
1131
945
|
collectLogs(fullPath);
|
|
1132
946
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1133
947
|
try {
|
|
1134
|
-
logContents.push(
|
|
948
|
+
logContents.push(fs4.readFileSync(fullPath, "utf8"));
|
|
1135
949
|
} catch {
|
|
1136
950
|
}
|
|
1137
951
|
}
|
|
@@ -1251,47 +1065,496 @@ function summarizeFleet(statuses) {
|
|
|
1251
1065
|
return summary;
|
|
1252
1066
|
}
|
|
1253
1067
|
|
|
1254
|
-
// src/lib/
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1068
|
+
// src/lib/labels.ts
|
|
1069
|
+
var DEFAULT_PROTECTED_LABEL_PATTERNS = [
|
|
1070
|
+
"priority/*",
|
|
1071
|
+
"type/*",
|
|
1072
|
+
"size/*",
|
|
1073
|
+
"needs-triage",
|
|
1074
|
+
"ready-for-agent",
|
|
1075
|
+
"needs-human",
|
|
1076
|
+
"needs-info",
|
|
1077
|
+
"needs-design",
|
|
1078
|
+
"wontfix",
|
|
1079
|
+
"measurement",
|
|
1080
|
+
"blocked",
|
|
1081
|
+
"autorelease:*",
|
|
1082
|
+
"dependencies",
|
|
1083
|
+
"security"
|
|
1084
|
+
];
|
|
1085
|
+
function isLabelProtected(labelName, protectedPatterns = DEFAULT_PROTECTED_LABEL_PATTERNS) {
|
|
1086
|
+
for (const pattern of protectedPatterns) {
|
|
1087
|
+
if (pattern === labelName) {
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
if (pattern.includes("*")) {
|
|
1091
|
+
const regexPattern = "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
|
|
1092
|
+
const regex = new RegExp(regexPattern);
|
|
1093
|
+
if (regex.test(labelName)) {
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1262
1097
|
}
|
|
1263
|
-
return
|
|
1098
|
+
return false;
|
|
1264
1099
|
}
|
|
1265
|
-
function
|
|
1266
|
-
|
|
1100
|
+
function classifyLabels(labels, protectedPatterns = DEFAULT_PROTECTED_LABEL_PATTERNS) {
|
|
1101
|
+
const classified = {
|
|
1102
|
+
active: [],
|
|
1103
|
+
protectedZeroCount: [],
|
|
1104
|
+
historical: [],
|
|
1105
|
+
prunable: [],
|
|
1106
|
+
all: []
|
|
1107
|
+
};
|
|
1108
|
+
for (const label of labels) {
|
|
1109
|
+
const isZeroTotal = label.totalIssuesCount === 0 && label.totalPullRequestsCount === 0;
|
|
1110
|
+
const hasOpenItems = label.openIssuesCount > 0 || label.openPullRequestsCount > 0;
|
|
1111
|
+
let category;
|
|
1112
|
+
if (isZeroTotal) {
|
|
1113
|
+
if (isLabelProtected(label.name, protectedPatterns)) {
|
|
1114
|
+
category = "protected_zero_count";
|
|
1115
|
+
const item = { ...label, category };
|
|
1116
|
+
classified.protectedZeroCount.push(item);
|
|
1117
|
+
classified.all.push(item);
|
|
1118
|
+
} else {
|
|
1119
|
+
category = "prunable";
|
|
1120
|
+
const item = { ...label, category };
|
|
1121
|
+
classified.prunable.push(item);
|
|
1122
|
+
classified.all.push(item);
|
|
1123
|
+
}
|
|
1124
|
+
} else if (hasOpenItems) {
|
|
1125
|
+
category = "active";
|
|
1126
|
+
const item = { ...label, category };
|
|
1127
|
+
classified.active.push(item);
|
|
1128
|
+
classified.all.push(item);
|
|
1129
|
+
} else {
|
|
1130
|
+
category = "historical";
|
|
1131
|
+
const item = { ...label, category };
|
|
1132
|
+
classified.historical.push(item);
|
|
1133
|
+
classified.all.push(item);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return classified;
|
|
1267
1137
|
}
|
|
1268
|
-
function
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
return JSON.stringify(
|
|
1272
|
-
{
|
|
1273
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1274
|
-
summary,
|
|
1275
|
-
repositories: statuses
|
|
1276
|
-
},
|
|
1277
|
-
null,
|
|
1278
|
-
2
|
|
1279
|
-
);
|
|
1138
|
+
async function resolveRepoName(repoIdentifier, cwd = process.cwd(), executor = defaultGhExecutor) {
|
|
1139
|
+
if (repoIdentifier && repoIdentifier.includes("/")) {
|
|
1140
|
+
return repoIdentifier;
|
|
1280
1141
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
if (statuses.length === 0) {
|
|
1284
|
-
lines.push(pc3.yellow(" No repositories configured in fleet registry."));
|
|
1285
|
-
lines.push(pc3.gray(" Use `jonah-fleet monitor --add <owner/repo>` to register repositories.\n"));
|
|
1286
|
-
return lines.join("\n");
|
|
1142
|
+
if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_REPOSITORY.includes("/")) {
|
|
1143
|
+
return process.env.GITHUB_REPOSITORY;
|
|
1287
1144
|
}
|
|
1288
|
-
|
|
1289
|
-
const
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1145
|
+
try {
|
|
1146
|
+
const raw = await executor(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]);
|
|
1147
|
+
const trimmed = raw.trim();
|
|
1148
|
+
if (trimmed && trimmed.includes("/")) {
|
|
1149
|
+
return trimmed;
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
return repoIdentifier || "current";
|
|
1154
|
+
}
|
|
1155
|
+
var LABELS_GRAPHQL_QUERY = `
|
|
1156
|
+
query($owner: String!, $repo: String!, $cursor: String) {
|
|
1157
|
+
repository(owner: $owner, name: $repo) {
|
|
1158
|
+
labels(first: 100, after: $cursor) {
|
|
1159
|
+
nodes {
|
|
1160
|
+
id
|
|
1161
|
+
name
|
|
1162
|
+
description
|
|
1163
|
+
color
|
|
1164
|
+
issues(states: [OPEN]) {
|
|
1165
|
+
totalCount
|
|
1166
|
+
}
|
|
1167
|
+
allIssues: issues {
|
|
1168
|
+
totalCount
|
|
1169
|
+
}
|
|
1170
|
+
pullRequests(states: [OPEN]) {
|
|
1171
|
+
totalCount
|
|
1172
|
+
}
|
|
1173
|
+
allPullRequests: pullRequests {
|
|
1174
|
+
totalCount
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
pageInfo {
|
|
1178
|
+
hasNextPage
|
|
1179
|
+
endCursor
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
`;
|
|
1185
|
+
async function fetchRepoLabels(repoIdentifier, executor = defaultGhExecutor, cwd = process.cwd()) {
|
|
1186
|
+
const fullRepo = await resolveRepoName(repoIdentifier, cwd, executor);
|
|
1187
|
+
const [owner, repo] = fullRepo.split("/");
|
|
1188
|
+
if (!owner || !repo) {
|
|
1189
|
+
throw new Error(`Invalid repository identifier '${fullRepo}'. Expected format 'owner/repo'.`);
|
|
1190
|
+
}
|
|
1191
|
+
const results = [];
|
|
1192
|
+
let cursor = null;
|
|
1193
|
+
let hasNextPage = true;
|
|
1194
|
+
while (hasNextPage) {
|
|
1195
|
+
const queryArgs = [
|
|
1196
|
+
"api",
|
|
1197
|
+
"graphql",
|
|
1198
|
+
"-f",
|
|
1199
|
+
`query=${LABELS_GRAPHQL_QUERY}`,
|
|
1200
|
+
"-F",
|
|
1201
|
+
`owner=${owner}`,
|
|
1202
|
+
"-F",
|
|
1203
|
+
`repo=${repo}`
|
|
1204
|
+
];
|
|
1205
|
+
if (cursor) {
|
|
1206
|
+
queryArgs.push("-F", `cursor=${cursor}`);
|
|
1207
|
+
}
|
|
1208
|
+
const raw = await executor(queryArgs);
|
|
1209
|
+
const parsed = JSON.parse(raw);
|
|
1210
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
1211
|
+
throw new Error(`GitHub GraphQL query failed: ${parsed.errors[0].message}`);
|
|
1212
|
+
}
|
|
1213
|
+
const labelConnection = parsed.data?.repository?.labels;
|
|
1214
|
+
if (!labelConnection || !Array.isArray(labelConnection.nodes)) {
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
for (const node of labelConnection.nodes) {
|
|
1218
|
+
results.push({
|
|
1219
|
+
id: node.id,
|
|
1220
|
+
name: node.name,
|
|
1221
|
+
description: node.description ?? null,
|
|
1222
|
+
color: node.color,
|
|
1223
|
+
openIssuesCount: node.issues?.totalCount || 0,
|
|
1224
|
+
totalIssuesCount: node.allIssues?.totalCount || 0,
|
|
1225
|
+
openPullRequestsCount: node.pullRequests?.totalCount || 0,
|
|
1226
|
+
totalPullRequestsCount: node.allPullRequests?.totalCount || 0
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
hasNextPage = Boolean(labelConnection.pageInfo?.hasNextPage);
|
|
1230
|
+
cursor = labelConnection.pageInfo?.endCursor || null;
|
|
1231
|
+
if (!cursor) {
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return results;
|
|
1236
|
+
}
|
|
1237
|
+
async function pruneLabels(options = {}) {
|
|
1238
|
+
const cwd = options.cwd || process.cwd();
|
|
1239
|
+
const executor = options.executor || defaultGhExecutor;
|
|
1240
|
+
const repo = await resolveRepoName(options.repo, cwd, executor);
|
|
1241
|
+
const manifest = loadManifest(cwd);
|
|
1242
|
+
const userProtected = manifest?.labels?.protected || [];
|
|
1243
|
+
const protectedPatterns = options.protectedPatterns || [
|
|
1244
|
+
...DEFAULT_PROTECTED_LABEL_PATTERNS,
|
|
1245
|
+
...userProtected
|
|
1246
|
+
];
|
|
1247
|
+
const rawLabels = await fetchRepoLabels(repo, executor, cwd);
|
|
1248
|
+
const classified = classifyLabels(rawLabels, protectedPatterns);
|
|
1249
|
+
const result = {
|
|
1250
|
+
repo,
|
|
1251
|
+
classified,
|
|
1252
|
+
pruned: [],
|
|
1253
|
+
skipped: [],
|
|
1254
|
+
errors: [],
|
|
1255
|
+
dryRun: Boolean(options.dryRun)
|
|
1256
|
+
};
|
|
1257
|
+
for (const item of classified.prunable) {
|
|
1258
|
+
if (options.dryRun) {
|
|
1259
|
+
result.pruned.push(item.name);
|
|
1260
|
+
} else {
|
|
1261
|
+
try {
|
|
1262
|
+
const deleteArgs = ["label", "delete", item.name, "--yes"];
|
|
1263
|
+
if (repo && repo !== "current") {
|
|
1264
|
+
deleteArgs.push("--repo", repo);
|
|
1265
|
+
}
|
|
1266
|
+
await executor(deleteArgs);
|
|
1267
|
+
result.pruned.push(item.name);
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
result.errors.push({
|
|
1270
|
+
label: item.name,
|
|
1271
|
+
error: err.message || String(err)
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return result;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/commands/init.ts
|
|
1280
|
+
async function promptQuestion(query, defaultValue) {
|
|
1281
|
+
const rl = readline.createInterface({
|
|
1282
|
+
input: process.stdin,
|
|
1283
|
+
output: process.stdout
|
|
1284
|
+
});
|
|
1285
|
+
return new Promise((resolve) => {
|
|
1286
|
+
rl.question(`${query} [${defaultValue}]: `, (answer) => {
|
|
1287
|
+
rl.close();
|
|
1288
|
+
resolve(answer.trim() || defaultValue);
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async function runInit(options = {}) {
|
|
1293
|
+
const cwd = options.cwd || process.cwd();
|
|
1294
|
+
const preset = options.preset || "standard";
|
|
1295
|
+
console.log(pc.cyan(`
|
|
1296
|
+
\u2693 Initializing Jonah Fleet (preset: ${pc.bold(preset)}) in ${cwd}
|
|
1297
|
+
`));
|
|
1298
|
+
let detected = detectTechStack(cwd);
|
|
1299
|
+
console.log(pc.bold("\u{1F50D} Tech Stack Auto-Detection:"));
|
|
1300
|
+
console.log(pc.cyan(` - Detected Stack: ${pc.bold(detected.name)}`));
|
|
1301
|
+
console.log(pc.cyan(` - Language: ${detected.language}`));
|
|
1302
|
+
if (detected.framework) {
|
|
1303
|
+
console.log(pc.cyan(` - Framework: ${detected.framework}`));
|
|
1304
|
+
}
|
|
1305
|
+
console.log(pc.cyan(` - Package Manager: ${detected.packageManager}`));
|
|
1306
|
+
if (detected.testFramework) {
|
|
1307
|
+
console.log(pc.cyan(` - Testing: ${detected.testFramework}`));
|
|
1308
|
+
}
|
|
1309
|
+
if (detected.commands.test) {
|
|
1310
|
+
console.log(pc.cyan(` - Test Command: ${detected.commands.test}`));
|
|
1311
|
+
}
|
|
1312
|
+
const isInteractive = options.interactive ?? (process.stdin.isTTY && !options.stack && !options.testCmd);
|
|
1313
|
+
if (isInteractive && process.stdin.isTTY) {
|
|
1314
|
+
console.log(pc.yellow("\n\u2699\uFE0F Configure project settings (press enter to accept defaults):"));
|
|
1315
|
+
const stackName = await promptQuestion("Tech Stack Name", detected.name);
|
|
1316
|
+
const pkgManager = await promptQuestion("Package Manager", detected.packageManager);
|
|
1317
|
+
const testCmd = await promptQuestion("Test Command", detected.commands.test || "npm test");
|
|
1318
|
+
const buildCmd = await promptQuestion("Build Command", detected.commands.build || "npm run build");
|
|
1319
|
+
detected = {
|
|
1320
|
+
...detected,
|
|
1321
|
+
name: stackName,
|
|
1322
|
+
language: stackName,
|
|
1323
|
+
framework: void 0,
|
|
1324
|
+
packageManager: pkgManager,
|
|
1325
|
+
commands: {
|
|
1326
|
+
...detected.commands,
|
|
1327
|
+
test: testCmd,
|
|
1328
|
+
build: buildCmd
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
if (options.stack) {
|
|
1333
|
+
detected.name = options.stack;
|
|
1334
|
+
detected.language = options.stack;
|
|
1335
|
+
detected.framework = void 0;
|
|
1336
|
+
}
|
|
1337
|
+
if (options.packageManager) {
|
|
1338
|
+
detected.packageManager = options.packageManager;
|
|
1339
|
+
}
|
|
1340
|
+
if (options.testCmd) {
|
|
1341
|
+
detected.commands.test = options.testCmd;
|
|
1342
|
+
}
|
|
1343
|
+
if (options.buildCmd) {
|
|
1344
|
+
detected.commands.build = options.buildCmd;
|
|
1345
|
+
}
|
|
1346
|
+
let manifest = loadManifest(cwd);
|
|
1347
|
+
if (manifest && !options.force) {
|
|
1348
|
+
console.log(pc.yellow(`
|
|
1349
|
+
\u26A0\uFE0F Found existing agents-manifest.json. Updating with preset '${preset}'...`));
|
|
1350
|
+
} else {
|
|
1351
|
+
manifest = createDefaultManifest(preset);
|
|
1352
|
+
}
|
|
1353
|
+
saveManifest(cwd, manifest);
|
|
1354
|
+
console.log(pc.green(`\u2713 Created/Updated agents-manifest.json`));
|
|
1355
|
+
const result = installFleet(cwd, manifest, { force: options.force, detectedStack: detected });
|
|
1356
|
+
console.log(pc.bold("\nInstalled components:"));
|
|
1357
|
+
if (result.promptsInstalled.length > 0) {
|
|
1358
|
+
console.log(pc.green(` \u{1F4C1} Prompts (.github/prompts/):`));
|
|
1359
|
+
result.promptsInstalled.forEach((p) => console.log(` - ${p}`));
|
|
1360
|
+
}
|
|
1361
|
+
if (result.workflowsInstalled.length > 0) {
|
|
1362
|
+
console.log(pc.green(` \u2699\uFE0F Workflows (.github/workflows/):`));
|
|
1363
|
+
result.workflowsInstalled.forEach((w) => console.log(` - ${w}`));
|
|
1364
|
+
}
|
|
1365
|
+
if (result.skillsInstalled.length > 0) {
|
|
1366
|
+
console.log(pc.green(` \u{1F9E0} Skills (.agents/skills/):`));
|
|
1367
|
+
result.skillsInstalled.forEach((s) => console.log(` - ${s}`));
|
|
1368
|
+
}
|
|
1369
|
+
if (result.docsInstalled.length > 0) {
|
|
1370
|
+
console.log(pc.green(` \u{1F4C4} Documentation:`));
|
|
1371
|
+
result.docsInstalled.forEach((d) => console.log(` - ${d}`));
|
|
1372
|
+
}
|
|
1373
|
+
if (options.pruneLabels) {
|
|
1374
|
+
try {
|
|
1375
|
+
console.log(pc.bold("\n\u{1F3F7}\uFE0F Pruning unused boilerplate labels..."));
|
|
1376
|
+
const pruneRes = await pruneLabels({ cwd, yes: true, dryRun: false, executor: options.executor });
|
|
1377
|
+
if (pruneRes.pruned.length > 0) {
|
|
1378
|
+
console.log(pc.green(` \u2713 Pruned ${pruneRes.pruned.length} unused boilerplate label(s): ${pruneRes.pruned.join(", ")}`));
|
|
1379
|
+
} else {
|
|
1380
|
+
console.log(pc.green(" \u2713 No unused boilerplate labels found."));
|
|
1381
|
+
}
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
console.log(pc.yellow(` \u26A0\uFE0F Could not prune labels: ${err.message}`));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
console.log(pc.bold(pc.green("\n\u{1F389} Jonah Fleet initialization complete!\n")));
|
|
1387
|
+
console.log(pc.cyan("Next steps for GitHub repository configuration:"));
|
|
1388
|
+
console.log(" 1. In Settings \u2192 Actions \u2192 General \u2192 Workflow permissions:");
|
|
1389
|
+
console.log(' Select "Read and write permissions" and check "Allow GitHub Actions to create and approve pull requests".');
|
|
1390
|
+
console.log(" 2. In Settings \u2192 Actions \u2192 General \u2192 Fork pull request workflows:");
|
|
1391
|
+
console.log(" Configure workflow approval settings to prevent automated runs from stalling awaiting approval.");
|
|
1392
|
+
console.log(" 3. Customize project context, build, and test commands in AGENTS.md.\n");
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/commands/sync.ts
|
|
1396
|
+
import pc2 from "picocolors";
|
|
1397
|
+
|
|
1398
|
+
// src/lib/diff.ts
|
|
1399
|
+
import fs5 from "fs";
|
|
1400
|
+
import path5 from "path";
|
|
1401
|
+
function checkDrift(targetDir, manifest) {
|
|
1402
|
+
const templatesDir = getTemplatesDir();
|
|
1403
|
+
const report = {
|
|
1404
|
+
missingPrompts: [],
|
|
1405
|
+
modifiedPrompts: [],
|
|
1406
|
+
missingWorkflows: [],
|
|
1407
|
+
modifiedWorkflows: [],
|
|
1408
|
+
missingSkills: []
|
|
1409
|
+
};
|
|
1410
|
+
const targetPromptsDir = path5.join(targetDir, ".github/prompts");
|
|
1411
|
+
const targetWorkflowsDir = path5.join(targetDir, ".github/workflows");
|
|
1412
|
+
const targetSkillsDir = path5.join(targetDir, ".agents/skills");
|
|
1413
|
+
const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
|
|
1414
|
+
for (const file of basePrompts) {
|
|
1415
|
+
const src = path5.join(templatesDir, "prompts", file);
|
|
1416
|
+
const dest = path5.join(targetPromptsDir, file);
|
|
1417
|
+
if (!fs5.existsSync(dest)) {
|
|
1418
|
+
report.missingPrompts.push(file);
|
|
1419
|
+
} else if (fs5.readFileSync(src, "utf8") !== fs5.readFileSync(dest, "utf8")) {
|
|
1420
|
+
report.modifiedPrompts.push(file);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
|
|
1424
|
+
if (!isEnabled) continue;
|
|
1425
|
+
const promptFile = `${routineName}.md`;
|
|
1426
|
+
const promptSrc = path5.join(templatesDir, "prompts", promptFile);
|
|
1427
|
+
const promptDest = path5.join(targetPromptsDir, promptFile);
|
|
1428
|
+
if (!fs5.existsSync(promptDest)) {
|
|
1429
|
+
report.missingPrompts.push(promptFile);
|
|
1430
|
+
} else if (fs5.existsSync(promptSrc) && fs5.readFileSync(promptSrc, "utf8") !== fs5.readFileSync(promptDest, "utf8")) {
|
|
1431
|
+
report.modifiedPrompts.push(promptFile);
|
|
1432
|
+
}
|
|
1433
|
+
const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
|
|
1434
|
+
for (const workflowFile of workflows) {
|
|
1435
|
+
const wfSrc = path5.join(templatesDir, "workflows", workflowFile);
|
|
1436
|
+
const wfDest = path5.join(targetWorkflowsDir, workflowFile);
|
|
1437
|
+
if (!fs5.existsSync(wfDest)) {
|
|
1438
|
+
report.missingWorkflows.push(workflowFile);
|
|
1439
|
+
} else if (fs5.existsSync(wfSrc)) {
|
|
1440
|
+
const rawSrc = fs5.readFileSync(wfSrc, "utf8");
|
|
1441
|
+
const destContent = fs5.readFileSync(wfDest, "utf8");
|
|
1442
|
+
const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
|
|
1443
|
+
const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
|
|
1444
|
+
if (expectedSrc !== destContent) {
|
|
1445
|
+
report.modifiedWorkflows.push(workflowFile);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (manifest.autoUpdate?.enabled) {
|
|
1451
|
+
const syncWfSrc = path5.join(templatesDir, "workflows/sync-fleet.yml");
|
|
1452
|
+
const syncWfDest = path5.join(targetWorkflowsDir, "sync-fleet.yml");
|
|
1453
|
+
if (!fs5.existsSync(syncWfDest)) {
|
|
1454
|
+
report.missingWorkflows.push("sync-fleet.yml");
|
|
1455
|
+
} else if (fs5.existsSync(syncWfSrc)) {
|
|
1456
|
+
const rawSrc = fs5.readFileSync(syncWfSrc, "utf8");
|
|
1457
|
+
const destContent = fs5.readFileSync(syncWfDest, "utf8");
|
|
1458
|
+
const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
|
|
1459
|
+
const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
|
|
1460
|
+
if (expectedSrc !== destContent) {
|
|
1461
|
+
report.modifiedWorkflows.push("sync-fleet.yml");
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
for (const skill of manifest.skills) {
|
|
1466
|
+
const skillDestDir = path5.join(targetSkillsDir, skill);
|
|
1467
|
+
if (!fs5.existsSync(skillDestDir)) {
|
|
1468
|
+
report.missingSkills.push(skill);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return report;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/commands/sync.ts
|
|
1475
|
+
async function runSync(options = {}) {
|
|
1476
|
+
const cwd = options.cwd || process.cwd();
|
|
1477
|
+
const manifest = loadManifest(cwd);
|
|
1478
|
+
if (!manifest) {
|
|
1479
|
+
console.error(pc2.red(`\u274C No agents-manifest.json found in ${cwd}. Run 'jonah-fleet init' first.`));
|
|
1480
|
+
process.exit(1);
|
|
1481
|
+
}
|
|
1482
|
+
console.log(pc2.cyan(`
|
|
1483
|
+
\u{1F504} Syncing Jonah Fleet (current: v${manifest.version}, fleet: v${FLEET_VERSION})...
|
|
1484
|
+
`));
|
|
1485
|
+
const drift = checkDrift(cwd, manifest);
|
|
1486
|
+
const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
|
|
1487
|
+
if (options.check) {
|
|
1488
|
+
if (!hasDrift && manifest.version === FLEET_VERSION) {
|
|
1489
|
+
console.log(pc2.green(`\u2713 All routines, workflows, and skills are perfectly in sync with v${FLEET_VERSION}.
|
|
1490
|
+
`));
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
console.log(pc2.yellow(`\u26A0\uFE0F Drift or updates detected:`));
|
|
1494
|
+
if (drift.missingPrompts.length > 0) console.log(pc2.red(` Missing prompts: ${drift.missingPrompts.join(", ")}`));
|
|
1495
|
+
if (drift.modifiedPrompts.length > 0) console.log(pc2.yellow(` Modified prompts: ${drift.modifiedPrompts.join(", ")}`));
|
|
1496
|
+
if (drift.missingWorkflows.length > 0) console.log(pc2.red(` Missing workflows: ${drift.missingWorkflows.join(", ")}`));
|
|
1497
|
+
if (drift.modifiedWorkflows.length > 0) console.log(pc2.yellow(` Modified workflows: ${drift.modifiedWorkflows.join(", ")}`));
|
|
1498
|
+
if (drift.missingSkills.length > 0) console.log(pc2.red(` Missing skills: ${drift.missingSkills.join(", ")}`));
|
|
1499
|
+
console.log(pc2.cyan(`
|
|
1500
|
+
Run 'jonah-fleet sync --force' to apply updates.
|
|
1501
|
+
`));
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
manifest.version = FLEET_VERSION;
|
|
1505
|
+
saveManifest(cwd, manifest);
|
|
1506
|
+
const result = installFleet(cwd, manifest, { force: true });
|
|
1507
|
+
console.log(pc2.green(`\u2713 Synchronized with Jonah Fleet v${FLEET_VERSION}`));
|
|
1508
|
+
console.log(pc2.green(`\u2713 Updated ${result.promptsInstalled.length} prompts, ${result.workflowsInstalled.length} workflows, and ${result.skillsInstalled.length} skills.
|
|
1509
|
+
`));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/commands/status.ts
|
|
1513
|
+
import fs7 from "fs";
|
|
1514
|
+
import path7 from "path";
|
|
1515
|
+
import pc5 from "picocolors";
|
|
1516
|
+
|
|
1517
|
+
// src/lib/dashboard.ts
|
|
1518
|
+
import pc3 from "picocolors";
|
|
1519
|
+
function formatTokens(num) {
|
|
1520
|
+
if (num >= 1e6) {
|
|
1521
|
+
return `${(num / 1e6).toFixed(2)}M`;
|
|
1522
|
+
}
|
|
1523
|
+
if (num >= 1e3) {
|
|
1524
|
+
return `${(num / 1e3).toFixed(1)}k`;
|
|
1525
|
+
}
|
|
1526
|
+
return num.toString();
|
|
1527
|
+
}
|
|
1528
|
+
function formatCurrency(amount) {
|
|
1529
|
+
return `$${amount.toFixed(2)}`;
|
|
1530
|
+
}
|
|
1531
|
+
function renderFleetDashboard(statuses, options = {}) {
|
|
1532
|
+
const summary = summarizeFleet(statuses);
|
|
1533
|
+
if (options.json) {
|
|
1534
|
+
return JSON.stringify(
|
|
1535
|
+
{
|
|
1536
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1537
|
+
summary,
|
|
1538
|
+
repositories: statuses
|
|
1539
|
+
},
|
|
1540
|
+
null,
|
|
1541
|
+
2
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
const lines = [];
|
|
1545
|
+
lines.push(pc3.bold(pc3.cyan("\n\u{1F4CA} Jonah Fleet Multi-Repo Monitor\n")));
|
|
1546
|
+
if (statuses.length === 0) {
|
|
1547
|
+
lines.push(pc3.yellow(" No repositories configured in fleet registry."));
|
|
1548
|
+
lines.push(pc3.gray(" Use `jonah-fleet monitor --add <owner/repo>` to register repositories.\n"));
|
|
1549
|
+
return lines.join("\n");
|
|
1550
|
+
}
|
|
1551
|
+
for (const s of statuses) {
|
|
1552
|
+
const versionStr = s.fleetVersion ? `v${s.fleetVersion}` : "unmanaged";
|
|
1553
|
+
const presetStr = s.preset ? `preset: ${s.preset}` : "";
|
|
1554
|
+
const headerInfo = [versionStr, presetStr].filter(Boolean).join(", ");
|
|
1555
|
+
lines.push(pc3.bold(`\u{1F4E6} ${pc3.cyan(s.repo)} ${pc3.gray(`(${headerInfo})`)}`));
|
|
1556
|
+
if (s.error) {
|
|
1557
|
+
lines.push(pc3.red(` \u274C Error: ${s.error}`));
|
|
1295
1558
|
}
|
|
1296
1559
|
lines.push(pc3.bold(" \u{1F512} Active Claims:"));
|
|
1297
1560
|
if (s.activeClaims.length === 0) {
|
|
@@ -1566,6 +1829,8 @@ async function runStatus(options = {}) {
|
|
|
1566
1829
|
autoUpdate: manifest.autoUpdate,
|
|
1567
1830
|
routines: manifest.routines,
|
|
1568
1831
|
skills: manifest.skills,
|
|
1832
|
+
models: manifest.models,
|
|
1833
|
+
budgets: manifest.budgets,
|
|
1569
1834
|
repositories: manifest.repositories || [],
|
|
1570
1835
|
tokenUsage,
|
|
1571
1836
|
drift: {
|
|
@@ -1593,6 +1858,36 @@ async function runStatus(options = {}) {
|
|
|
1593
1858
|
for (const skill of manifest.skills) {
|
|
1594
1859
|
console.log(` - ${pc5.cyan(skill)}`);
|
|
1595
1860
|
}
|
|
1861
|
+
if (manifest.models && Object.keys(manifest.models).length > 0) {
|
|
1862
|
+
console.log(pc5.bold("\n Model Profiles:"));
|
|
1863
|
+
for (const [routine, model] of Object.entries(manifest.models)) {
|
|
1864
|
+
if (model) {
|
|
1865
|
+
console.log(` - ${routine.padEnd(35)}: ${pc5.cyan(model)}`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (manifest.budgets) {
|
|
1870
|
+
console.log(pc5.bold("\n Configured Budgets:"));
|
|
1871
|
+
if (manifest.budgets.weeklyTokens) {
|
|
1872
|
+
console.log(` - Weekly Token Budget: ${pc5.cyan(formatTokens(manifest.budgets.weeklyTokens))}`);
|
|
1873
|
+
}
|
|
1874
|
+
if (manifest.budgets.timeoutMinutes && Object.keys(manifest.budgets.timeoutMinutes).length > 0) {
|
|
1875
|
+
console.log(` - Timeouts:`);
|
|
1876
|
+
for (const [routine, timeout] of Object.entries(manifest.budgets.timeoutMinutes)) {
|
|
1877
|
+
if (timeout !== void 0) {
|
|
1878
|
+
console.log(` \u2022 ${routine}: ${pc5.cyan(timeout + "m")}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (manifest.budgets.maxIterations && Object.keys(manifest.budgets.maxIterations).length > 0) {
|
|
1883
|
+
console.log(` - Max Iterations:`);
|
|
1884
|
+
for (const [routine, iter] of Object.entries(manifest.budgets.maxIterations)) {
|
|
1885
|
+
if (iter !== void 0) {
|
|
1886
|
+
console.log(` \u2022 ${routine}: ${pc5.cyan(String(iter))}`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1596
1891
|
if (manifest.repositories && manifest.repositories.length > 0) {
|
|
1597
1892
|
console.log(pc5.bold("\n Fleet Repositories:"));
|
|
1598
1893
|
for (const repo of manifest.repositories) {
|
|
@@ -1759,6 +2054,8 @@ function parseLogToTelemetry(content, options = {}) {
|
|
|
1759
2054
|
failureCategory = "infeasible";
|
|
1760
2055
|
} else if (lower.includes("conflict")) {
|
|
1761
2056
|
failureCategory = "merge_conflict";
|
|
2057
|
+
} else if (lower.includes("ambiguous") || lower.includes("needs-info") || lower.includes("clarification") || lower.includes("acceptance criteria")) {
|
|
2058
|
+
failureCategory = "ambiguous_spec";
|
|
1762
2059
|
}
|
|
1763
2060
|
}
|
|
1764
2061
|
const inputTokens = parseInt((metadata["input tokens"] || "0").replace(/[^\d]/g, ""), 10) || 0;
|
|
@@ -1783,6 +2080,16 @@ function parseLogToTelemetry(content, options = {}) {
|
|
|
1783
2080
|
}
|
|
1784
2081
|
}
|
|
1785
2082
|
const promptSha = metadata["prompt sha"];
|
|
2083
|
+
const lowerContent = content.toLowerCase();
|
|
2084
|
+
const ambiguityGateTriggered = lowerContent.includes("ambiguity & missing acceptance criteria gate") || lowerContent.includes("ambiguity gate") || lowerContent.includes("clarifications needed before implementation") || lowerContent.includes("needs-info") && (lowerContent.includes("clarification") || lowerContent.includes("questions"));
|
|
2085
|
+
const needsInfoApplied = lowerContent.includes("needs-info");
|
|
2086
|
+
let questionsAskedCount;
|
|
2087
|
+
if (ambiguityGateTriggered) {
|
|
2088
|
+
const questionLines = content.split("\n").filter(
|
|
2089
|
+
(line) => (line.trim().startsWith("1.") || line.trim().startsWith("2.") || line.trim().startsWith("3.") || line.trim().startsWith("-")) && line.includes("?")
|
|
2090
|
+
);
|
|
2091
|
+
questionsAskedCount = questionLines.length > 0 ? questionLines.length : 2;
|
|
2092
|
+
}
|
|
1786
2093
|
return {
|
|
1787
2094
|
schemaVersion: "1.0.0",
|
|
1788
2095
|
routine,
|
|
@@ -1800,7 +2107,10 @@ function parseLogToTelemetry(content, options = {}) {
|
|
|
1800
2107
|
durationSeconds,
|
|
1801
2108
|
iterationsUsed,
|
|
1802
2109
|
maxIterations,
|
|
1803
|
-
promptSha
|
|
2110
|
+
promptSha,
|
|
2111
|
+
ambiguityGateTriggered: ambiguityGateTriggered || void 0,
|
|
2112
|
+
questionsAskedCount,
|
|
2113
|
+
needsInfoApplied: needsInfoApplied || void 0
|
|
1804
2114
|
};
|
|
1805
2115
|
}
|
|
1806
2116
|
function checkWeeklyBudgetLimit(usedTokens, ceilingTokens = GLOBAL_WEEKLY_TOKEN_BUDGET) {
|
|
@@ -1859,7 +2169,10 @@ function aggregateFleetTelemetry(summaries, options = {}) {
|
|
|
1859
2169
|
failureCount: 0,
|
|
1860
2170
|
bouncedCount: 0,
|
|
1861
2171
|
avgDurationSeconds: 0,
|
|
1862
|
-
avgIterationsUsed: 0
|
|
2172
|
+
avgIterationsUsed: 0,
|
|
2173
|
+
ambiguityGatesTriggered: 0,
|
|
2174
|
+
questionsAskedCount: 0,
|
|
2175
|
+
needsInfoAppliedCount: 0
|
|
1863
2176
|
};
|
|
1864
2177
|
}
|
|
1865
2178
|
const r = byRoutine[s.routine];
|
|
@@ -1877,6 +2190,9 @@ function aggregateFleetTelemetry(summaries, options = {}) {
|
|
|
1877
2190
|
if (s.iterationsUsed) {
|
|
1878
2191
|
r.avgIterationsUsed = (r.avgIterationsUsed * (r.runCount - 1) + s.iterationsUsed) / r.runCount;
|
|
1879
2192
|
}
|
|
2193
|
+
if (s.ambiguityGateTriggered) r.ambiguityGatesTriggered++;
|
|
2194
|
+
if (s.questionsAskedCount) r.questionsAskedCount += s.questionsAskedCount;
|
|
2195
|
+
if (s.needsInfoApplied) r.needsInfoAppliedCount++;
|
|
1880
2196
|
if (!byRepository[s.repository]) {
|
|
1881
2197
|
byRepository[s.repository] = {
|
|
1882
2198
|
repository: s.repository,
|
|
@@ -1894,6 +2210,21 @@ function aggregateFleetTelemetry(summaries, options = {}) {
|
|
|
1894
2210
|
if (s.result === "SUCCESS") repoObj.successCount++;
|
|
1895
2211
|
else if (s.result === "FAILURE") repoObj.failureCount++;
|
|
1896
2212
|
}
|
|
2213
|
+
let totalAmbiguityGatesTriggered = 0;
|
|
2214
|
+
let totalQuestionsAsked = 0;
|
|
2215
|
+
let totalNeedsInfoApplied = 0;
|
|
2216
|
+
for (const s of summaries) {
|
|
2217
|
+
if (s.ambiguityGateTriggered) totalAmbiguityGatesTriggered++;
|
|
2218
|
+
if (s.questionsAskedCount) totalQuestionsAsked += s.questionsAskedCount;
|
|
2219
|
+
if (s.needsInfoApplied) totalNeedsInfoApplied++;
|
|
2220
|
+
}
|
|
2221
|
+
const estimatedTokensSaved = totalAmbiguityGatesTriggered * 5e4;
|
|
2222
|
+
const ambiguity = {
|
|
2223
|
+
totalAmbiguityGatesTriggered,
|
|
2224
|
+
totalQuestionsAsked,
|
|
2225
|
+
needsInfoAppliedCount: totalNeedsInfoApplied,
|
|
2226
|
+
estimatedTokensSaved
|
|
2227
|
+
};
|
|
1897
2228
|
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
1898
2229
|
const budget = checkWeeklyBudgetLimit(totalTokens, budgetCeiling);
|
|
1899
2230
|
return {
|
|
@@ -1910,6 +2241,7 @@ function aggregateFleetTelemetry(summaries, options = {}) {
|
|
|
1910
2241
|
byRoutine,
|
|
1911
2242
|
byRepository,
|
|
1912
2243
|
failureCategories,
|
|
2244
|
+
ambiguity,
|
|
1913
2245
|
events: summaries
|
|
1914
2246
|
};
|
|
1915
2247
|
}
|
|
@@ -2046,6 +2378,20 @@ function renderTelemetryDashboard(telemetry, options = {}) {
|
|
|
2046
2378
|
);
|
|
2047
2379
|
}
|
|
2048
2380
|
}
|
|
2381
|
+
const amb = telemetry.ambiguity;
|
|
2382
|
+
lines.push("\n" + pc7.bold("\u2753 Inquisitive Stance & Ambiguity Gate Signals:"));
|
|
2383
|
+
lines.push(
|
|
2384
|
+
` \u2022 Ambiguity Gate Triggers: ${pc7.bold(amb.totalAmbiguityGatesTriggered.toString())} runs stopped to request clarification`
|
|
2385
|
+
);
|
|
2386
|
+
lines.push(
|
|
2387
|
+
` \u2022 Clarifying Questions Posed: ${pc7.bold(amb.totalQuestionsAsked.toString())} targeted questions`
|
|
2388
|
+
);
|
|
2389
|
+
lines.push(
|
|
2390
|
+
` \u2022 Needs-Info Labels Applied: ${pc7.bold(amb.needsInfoAppliedCount.toString())}`
|
|
2391
|
+
);
|
|
2392
|
+
lines.push(
|
|
2393
|
+
` \u2022 Est. Wasted Tokens Averted: ~${pc7.bold(pc7.green(formatTokens2(amb.estimatedTokensSaved)))} tokens (avoided speculative builds)`
|
|
2394
|
+
);
|
|
2049
2395
|
const failKeys = Object.keys(telemetry.failureCategories);
|
|
2050
2396
|
if (failKeys.length > 0) {
|
|
2051
2397
|
lines.push("\n" + pc7.bold(pc7.red("\u26A0\uFE0F Failure Categories Breakdown:")));
|
|
@@ -2157,7 +2503,7 @@ async function runTelemetry(options = {}) {
|
|
|
2157
2503
|
}
|
|
2158
2504
|
|
|
2159
2505
|
// src/commands/run.ts
|
|
2160
|
-
import
|
|
2506
|
+
import pc11 from "picocolors";
|
|
2161
2507
|
|
|
2162
2508
|
// src/lib/runner.ts
|
|
2163
2509
|
import fs12 from "fs";
|
|
@@ -2289,7 +2635,93 @@ async function cleanupStaleWorktrees(repoRoot) {
|
|
|
2289
2635
|
// src/lib/terminal-card.ts
|
|
2290
2636
|
import fs11 from "fs";
|
|
2291
2637
|
import path11 from "path";
|
|
2638
|
+
import { execFileSync } from "child_process";
|
|
2292
2639
|
import pc9 from "picocolors";
|
|
2640
|
+
function stripAnsi(text) {
|
|
2641
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2642
|
+
}
|
|
2643
|
+
function truncateAnsi(text, maxWidth) {
|
|
2644
|
+
if (maxWidth <= 0) return "";
|
|
2645
|
+
if (stripAnsi(text).length <= maxWidth) return text;
|
|
2646
|
+
let visibleCount = 0;
|
|
2647
|
+
let result = "";
|
|
2648
|
+
let inAnsi = false;
|
|
2649
|
+
let ansiBuffer = "";
|
|
2650
|
+
for (let i = 0; i < text.length; i++) {
|
|
2651
|
+
const char = text[i];
|
|
2652
|
+
if (char === "\x1B") {
|
|
2653
|
+
inAnsi = true;
|
|
2654
|
+
ansiBuffer = char;
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
if (inAnsi) {
|
|
2658
|
+
ansiBuffer += char;
|
|
2659
|
+
if (char === "m") {
|
|
2660
|
+
inAnsi = false;
|
|
2661
|
+
result += ansiBuffer;
|
|
2662
|
+
ansiBuffer = "";
|
|
2663
|
+
}
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
if (visibleCount < maxWidth) {
|
|
2667
|
+
result += char;
|
|
2668
|
+
visibleCount++;
|
|
2669
|
+
} else {
|
|
2670
|
+
break;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
result += "\x1B[0m";
|
|
2674
|
+
return result;
|
|
2675
|
+
}
|
|
2676
|
+
function wrapText(text, maxWidth) {
|
|
2677
|
+
if (maxWidth <= 0) return [text];
|
|
2678
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
2679
|
+
if (words.length === 0) return [];
|
|
2680
|
+
const lines = [];
|
|
2681
|
+
let current = "";
|
|
2682
|
+
for (const word of words) {
|
|
2683
|
+
if (!current) {
|
|
2684
|
+
current = word;
|
|
2685
|
+
} else {
|
|
2686
|
+
const proposed = current + " " + word;
|
|
2687
|
+
if (stripAnsi(proposed).length <= maxWidth) {
|
|
2688
|
+
current = proposed;
|
|
2689
|
+
} else {
|
|
2690
|
+
lines.push(current);
|
|
2691
|
+
current = word;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
if (current) lines.push(current);
|
|
2696
|
+
return lines;
|
|
2697
|
+
}
|
|
2698
|
+
function fetchTargetTitle(repoRoot, target) {
|
|
2699
|
+
try {
|
|
2700
|
+
const prMatch = target.match(/PR\s*#?(\d+)/i);
|
|
2701
|
+
if (prMatch) {
|
|
2702
|
+
const stdout = execFileSync("gh", ["pr", "view", prMatch[1], "--json", "title", "-q", ".title"], {
|
|
2703
|
+
cwd: repoRoot,
|
|
2704
|
+
encoding: "utf8",
|
|
2705
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2706
|
+
timeout: 4e3
|
|
2707
|
+
});
|
|
2708
|
+
return stdout.trim() || null;
|
|
2709
|
+
}
|
|
2710
|
+
const issueMatch = target.match(/Issue\s*#?(\d+)/i);
|
|
2711
|
+
if (issueMatch) {
|
|
2712
|
+
const stdout = execFileSync("gh", ["issue", "view", issueMatch[1], "--json", "title", "-q", ".title"], {
|
|
2713
|
+
cwd: repoRoot,
|
|
2714
|
+
encoding: "utf8",
|
|
2715
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2716
|
+
timeout: 4e3
|
|
2717
|
+
});
|
|
2718
|
+
return stdout.trim() || null;
|
|
2719
|
+
}
|
|
2720
|
+
} catch {
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
return null;
|
|
2724
|
+
}
|
|
2293
2725
|
function sanitizeWorktreePaths(text) {
|
|
2294
2726
|
let cleaned = text.replace(/file:\/\/\/[^\s"'()]+?\/\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, "");
|
|
2295
2727
|
cleaned = cleaned.replace(/(?:^|[\s"'(`[])(?:\/[^\s"'()]+?)?\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, (match) => {
|
|
@@ -2300,7 +2732,7 @@ function sanitizeWorktreePaths(text) {
|
|
|
2300
2732
|
return cleaned;
|
|
2301
2733
|
}
|
|
2302
2734
|
function extractExecutionSummary(output) {
|
|
2303
|
-
const summaryHeaderRegex =
|
|
2735
|
+
const summaryHeaderRegex = /#{1,3}\s+([A-Za-z0-9\s_-]*?(?:Execution|Review|Autowork)\s+Summary[\s\S]*)/i;
|
|
2304
2736
|
const match = output.match(summaryHeaderRegex);
|
|
2305
2737
|
if (!match) return null;
|
|
2306
2738
|
let summary = match[1].trim();
|
|
@@ -2308,8 +2740,8 @@ function extractExecutionSummary(output) {
|
|
|
2308
2740
|
"\u2713 Local peer-review completed",
|
|
2309
2741
|
"\u2713 Local autowork completed",
|
|
2310
2742
|
"\u2713 Local agent session",
|
|
2311
|
-
"
|
|
2312
|
-
"
|
|
2743
|
+
"Peer Review Watchdog:",
|
|
2744
|
+
"Autowork Backlog Scan:"
|
|
2313
2745
|
];
|
|
2314
2746
|
for (const sep of trailingSeparators) {
|
|
2315
2747
|
const idx = summary.indexOf(sep);
|
|
@@ -2317,6 +2749,10 @@ function extractExecutionSummary(output) {
|
|
|
2317
2749
|
summary = summary.slice(0, idx).trim();
|
|
2318
2750
|
}
|
|
2319
2751
|
}
|
|
2752
|
+
const timestampMatch = summary.match(/\n\s*\[\d{1,2}:\d{2}:\d{2}\s*(?:AM|PM)?\][\s\S]*/);
|
|
2753
|
+
if (timestampMatch && timestampMatch.index !== void 0) {
|
|
2754
|
+
summary = summary.slice(0, timestampMatch.index).trim();
|
|
2755
|
+
}
|
|
2320
2756
|
return sanitizeWorktreePaths(summary);
|
|
2321
2757
|
}
|
|
2322
2758
|
function findLatestRunLog(repoRoot, routine) {
|
|
@@ -2337,57 +2773,73 @@ function parseRunLog(logContent) {
|
|
|
2337
2773
|
actions: []
|
|
2338
2774
|
};
|
|
2339
2775
|
const lines = logContent.split("\n");
|
|
2340
|
-
|
|
2341
|
-
const trimmed = line.trim();
|
|
2342
|
-
if (trimmed.startsWith("|") && trimmed.includes("|")) {
|
|
2343
|
-
const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
|
|
2344
|
-
if (parts.length >= 2) {
|
|
2345
|
-
const key = parts[0].toLowerCase();
|
|
2346
|
-
const value = parts[1].replace(/`/g, "");
|
|
2347
|
-
if (key.includes("routine")) summary.routine = value;
|
|
2348
|
-
if (key.includes("target pr") || key.includes("target issue")) summary.target = value;
|
|
2349
|
-
if (key.includes("decision")) summary.decision = value;
|
|
2350
|
-
if (key.includes("result")) summary.result = value;
|
|
2351
|
-
if (key.includes("duration")) summary.duration = value;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2776
|
+
let inMetadata = false;
|
|
2355
2777
|
let inDoD = false;
|
|
2356
2778
|
let inFindings = false;
|
|
2357
2779
|
let inActions = false;
|
|
2358
2780
|
for (const line of lines) {
|
|
2359
2781
|
const trimmed = line.trim();
|
|
2360
|
-
if (trimmed.startsWith("##
|
|
2782
|
+
if (trimmed.startsWith("## Metadata")) {
|
|
2783
|
+
inMetadata = true;
|
|
2784
|
+
inDoD = false;
|
|
2785
|
+
inFindings = false;
|
|
2786
|
+
inActions = false;
|
|
2787
|
+
continue;
|
|
2788
|
+
} else if (trimmed.startsWith("## Definition of Done")) {
|
|
2789
|
+
inMetadata = false;
|
|
2361
2790
|
inDoD = true;
|
|
2362
2791
|
inFindings = false;
|
|
2363
2792
|
inActions = false;
|
|
2364
2793
|
continue;
|
|
2365
2794
|
} else if (trimmed.startsWith("## Code Review Findings") || trimmed.startsWith("## Findings")) {
|
|
2795
|
+
inMetadata = false;
|
|
2366
2796
|
inDoD = false;
|
|
2367
2797
|
inFindings = true;
|
|
2368
2798
|
inActions = false;
|
|
2369
2799
|
continue;
|
|
2370
|
-
} else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken")) {
|
|
2800
|
+
} else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken") || trimmed.startsWith("## Artifacts")) {
|
|
2801
|
+
inMetadata = false;
|
|
2371
2802
|
inDoD = false;
|
|
2372
2803
|
inFindings = false;
|
|
2373
|
-
inActions =
|
|
2804
|
+
inActions = trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken");
|
|
2374
2805
|
continue;
|
|
2375
2806
|
} else if (trimmed.startsWith("## ")) {
|
|
2807
|
+
inMetadata = false;
|
|
2376
2808
|
inDoD = false;
|
|
2377
2809
|
inFindings = false;
|
|
2378
2810
|
inActions = false;
|
|
2379
2811
|
}
|
|
2812
|
+
if (inMetadata && trimmed.startsWith("|") && trimmed.includes("|")) {
|
|
2813
|
+
const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
|
|
2814
|
+
if (parts.length >= 2) {
|
|
2815
|
+
const key = parts[0].toLowerCase();
|
|
2816
|
+
const value = parts[1].replace(/[`*]/g, "").trim();
|
|
2817
|
+
if (key === "routine") summary.routine = value;
|
|
2818
|
+
if (key === "target pr" || key === "target issue" || key === "target") {
|
|
2819
|
+
summary.target = value;
|
|
2820
|
+
}
|
|
2821
|
+
if (key === "decision") summary.decision = value;
|
|
2822
|
+
if (key === "result") summary.result = value;
|
|
2823
|
+
if (key === "duration") summary.duration = value;
|
|
2824
|
+
if (key === "title" || key === "pr title" || key === "issue title") summary.title = value;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2380
2827
|
if (inDoD && trimmed.startsWith("|") && !trimmed.includes("Criterion") && !trimmed.includes("---")) {
|
|
2381
2828
|
const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
|
|
2382
2829
|
if (parts.length >= 2) {
|
|
2383
2830
|
const criterion = parts[0];
|
|
2384
|
-
const
|
|
2385
|
-
const
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2831
|
+
const metRaw = parts[1].toUpperCase();
|
|
2832
|
+
const met = metRaw === "YES" || metRaw === "PASS";
|
|
2833
|
+
const evidence = parts[2] ? parts[2].trim() : "";
|
|
2834
|
+
const isConditionalScan = criterion.toLowerCase().startsWith("if in scan mode and no eligible");
|
|
2835
|
+
const isNA = evidence.toLowerCase().includes("n/a") || metRaw === "N/A";
|
|
2836
|
+
if (!isConditionalScan && !isNA) {
|
|
2837
|
+
summary.passes?.push({
|
|
2838
|
+
name: criterion,
|
|
2839
|
+
status: met ? "pass" : "fail",
|
|
2840
|
+
detail: evidence ? ` (${evidence})` : ""
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2391
2843
|
}
|
|
2392
2844
|
}
|
|
2393
2845
|
if (inActions && (trimmed.startsWith("- ") || trimmed.startsWith("* "))) {
|
|
@@ -2398,9 +2850,15 @@ function parseRunLog(logContent) {
|
|
|
2398
2850
|
}
|
|
2399
2851
|
function detectActivePhase(chunk, currentPhase = "Executing routine") {
|
|
2400
2852
|
const lower = chunk.toLowerCase();
|
|
2853
|
+
if (lower.includes("\u{1F512} addressing review findings") || lower.includes("addressing review findings by")) {
|
|
2854
|
+
return "Claimed bounced PR, addressing review findings";
|
|
2855
|
+
}
|
|
2401
2856
|
if (lower.includes("\u{1F512} claimed") || lower.includes("claimed by local autowork") || lower.includes("claimed by autowork")) {
|
|
2402
2857
|
return "Claimed target issue, starting implementation";
|
|
2403
2858
|
}
|
|
2859
|
+
if (lower.includes("addressing review findings") || lower.includes("fixing review findings")) {
|
|
2860
|
+
return "Claimed bounced PR, addressing review findings";
|
|
2861
|
+
}
|
|
2404
2862
|
if (lower.includes("starting review (round")) {
|
|
2405
2863
|
return "Claimed review window, starting review passes";
|
|
2406
2864
|
}
|
|
@@ -2430,16 +2888,110 @@ function detectClaimedIssue(chunk) {
|
|
|
2430
2888
|
return null;
|
|
2431
2889
|
}
|
|
2432
2890
|
function detectClaimedPR(chunk) {
|
|
2891
|
+
const findingMatch = chunk.match(/(?:addressing\s+review\s+findings|fixing\s+review\s+findings)[^\n#]*?#(\d+)/i);
|
|
2892
|
+
if (findingMatch) return `PR #${findingMatch[1]}`;
|
|
2433
2893
|
const reviewMatch = chunk.match(/Starting\s+review[^\n#]*?#(\d+)/i);
|
|
2434
2894
|
if (reviewMatch) return `PR #${reviewMatch[1]}`;
|
|
2435
2895
|
const prMatch = chunk.match(/(?:selected|target|reviewing)\s+(?:target\s+)?PR:?\s*\[?PR\s*#?(\d+)/i);
|
|
2436
2896
|
if (prMatch) return `PR #${prMatch[1]}`;
|
|
2437
|
-
const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review)\s+(\d+)/i);
|
|
2897
|
+
const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review|edit|ready)\s+(\d+)/i);
|
|
2438
2898
|
if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
|
|
2439
2899
|
return null;
|
|
2440
2900
|
}
|
|
2901
|
+
function formatActionDescription(toolName, params) {
|
|
2902
|
+
const name = toolName || "unknown";
|
|
2903
|
+
if (name === "run_command") {
|
|
2904
|
+
const rawCmd = (params?.CommandLine || params?.command || params?.cmd || "").trim();
|
|
2905
|
+
if (!rawCmd) return "Running command";
|
|
2906
|
+
if (/\b(?:vitest|jest)\b/i.test(rawCmd)) return "Running vitest";
|
|
2907
|
+
if (/\bnpm\s+test\b|\bcargo\s+test\b|\bpytest\b/i.test(rawCmd)) return "Running test suite";
|
|
2908
|
+
if (/type-check|\btsc\b/i.test(rawCmd)) return "Running TypeScript type checks";
|
|
2909
|
+
if (/\blint\b|\beslint\b/i.test(rawCmd)) return "Running codebase linter";
|
|
2910
|
+
if (/\bbuild\b|\btsup\b|\bnext\s+build\b/i.test(rawCmd)) return "Running production build";
|
|
2911
|
+
if (/gh\s+pr\s+list/i.test(rawCmd)) return "Listing open PRs (gh pr list)";
|
|
2912
|
+
const prMergeMatch = rawCmd.match(/gh\s+pr\s+merge(?:\s+(\d+))?/i);
|
|
2913
|
+
if (prMergeMatch) {
|
|
2914
|
+
return prMergeMatch[1] ? `Squash-merging PR #${prMergeMatch[1]}` : "Squash-merging pull request";
|
|
2915
|
+
}
|
|
2916
|
+
const prViewMatch = rawCmd.match(/gh\s+pr\s+view(?:\s+(\d+))?/i);
|
|
2917
|
+
if (prViewMatch) {
|
|
2918
|
+
return prViewMatch[1] ? `Viewing PR #${prViewMatch[1]}` : "Viewing pull request";
|
|
2919
|
+
}
|
|
2920
|
+
const prEditMatch = rawCmd.match(/gh\s+pr\s+edit(?:\s+(\d+))?/i);
|
|
2921
|
+
if (prEditMatch) {
|
|
2922
|
+
return prEditMatch[1] ? `Updating PR #${prEditMatch[1]}` : "Updating pull request";
|
|
2923
|
+
}
|
|
2924
|
+
const prReadyMatch = rawCmd.match(/gh\s+pr\s+ready(?:\s+(\d+))?/i);
|
|
2925
|
+
if (prReadyMatch) {
|
|
2926
|
+
return prReadyMatch[1] ? `Marking PR #${prReadyMatch[1]} ready for review` : "Marking PR ready for review";
|
|
2927
|
+
}
|
|
2928
|
+
if (/gh\s+pr\s+create/i.test(rawCmd)) return "Creating pull request";
|
|
2929
|
+
if (/gh\s+issue\s+list/i.test(rawCmd)) return "Listing open issues (gh issue list)";
|
|
2930
|
+
const issueViewMatch = rawCmd.match(/gh\s+issue\s+view(?:\s+(\d+))?/i);
|
|
2931
|
+
if (issueViewMatch) {
|
|
2932
|
+
return issueViewMatch[1] ? `Viewing issue #${issueViewMatch[1]}` : "Viewing issue";
|
|
2933
|
+
}
|
|
2934
|
+
const issueEditMatch = rawCmd.match(/gh\s+issue\s+edit(?:\s+(\d+))?/i);
|
|
2935
|
+
if (issueEditMatch) {
|
|
2936
|
+
return issueEditMatch[1] ? `Updating issue #${issueEditMatch[1]}` : "Updating issue";
|
|
2937
|
+
}
|
|
2938
|
+
const issueCommentMatch = rawCmd.match(/gh\s+issue\s+comment(?:\s+(\d+))?/i);
|
|
2939
|
+
if (issueCommentMatch) {
|
|
2940
|
+
return issueCommentMatch[1] ? `Commenting on issue #${issueCommentMatch[1]}` : "Commenting on issue";
|
|
2941
|
+
}
|
|
2942
|
+
if (/git\s+checkout/i.test(rawCmd)) return "Git: Checking out branch";
|
|
2943
|
+
if (/git\s+status/i.test(rawCmd)) return "Git: Checking status";
|
|
2944
|
+
if (/git\s+diff/i.test(rawCmd)) return "Git: Inspecting diff";
|
|
2945
|
+
if (/git\s+commit/i.test(rawCmd)) return "Git: Committing changes";
|
|
2946
|
+
if (/git\s+push/i.test(rawCmd)) return "Git: Pushing branch";
|
|
2947
|
+
const firstLine = rawCmd.split("\n")[0].trim();
|
|
2948
|
+
return `Running ${firstLine}`;
|
|
2949
|
+
}
|
|
2950
|
+
if (name === "view_file") {
|
|
2951
|
+
const rawPath = params?.AbsolutePath || params?.TargetFile || params?.path || params?.file || "";
|
|
2952
|
+
if (!rawPath) return "Reading file";
|
|
2953
|
+
return `Reading ${path11.basename(rawPath)}`;
|
|
2954
|
+
}
|
|
2955
|
+
if (name === "replace_file_content" || name === "write_to_file" || name === "multi_replace_file_content") {
|
|
2956
|
+
const rawPath = params?.TargetFile || params?.AbsolutePath || params?.path || params?.file || "";
|
|
2957
|
+
if (!rawPath) return "Editing file";
|
|
2958
|
+
return `Editing ${path11.basename(rawPath)}`;
|
|
2959
|
+
}
|
|
2960
|
+
if (name === "grep_search") {
|
|
2961
|
+
const query = params?.Query || params?.query || params?.pattern || "";
|
|
2962
|
+
if (!query) return "Searching codebase";
|
|
2963
|
+
return `Searching codebase for "${query}"`;
|
|
2964
|
+
}
|
|
2965
|
+
if (name === "find_by_name") {
|
|
2966
|
+
const pattern = params?.Pattern || params?.pattern || "";
|
|
2967
|
+
if (!pattern) return "Finding files";
|
|
2968
|
+
return `Finding files matching "${pattern}"`;
|
|
2969
|
+
}
|
|
2970
|
+
if (name === "list_dir") {
|
|
2971
|
+
const dirPath = params?.DirectoryPath || params?.path || "";
|
|
2972
|
+
if (!dirPath) return "Listing directory";
|
|
2973
|
+
const base = path11.basename(dirPath.replace(/[/\\]+$/, "")) || dirPath;
|
|
2974
|
+
return `Listing directory ${base}`;
|
|
2975
|
+
}
|
|
2976
|
+
if (name === "invoke_subagent") {
|
|
2977
|
+
const role = params?.Subagents?.[0]?.Role || params?.Subagents?.[0]?.role || params?.Role || params?.role || params?.TypeName || params?.name || "";
|
|
2978
|
+
if (!role) return "Running subagent";
|
|
2979
|
+
return `Running subagent: ${role}`;
|
|
2980
|
+
}
|
|
2981
|
+
if (name === "search_web") {
|
|
2982
|
+
const query = params?.query || params?.Query || "";
|
|
2983
|
+
if (!query) return "Searching web";
|
|
2984
|
+
return `Searching web for "${query}"`;
|
|
2985
|
+
}
|
|
2986
|
+
if (name === "read_url_content") {
|
|
2987
|
+
const url = params?.Url || params?.url || "";
|
|
2988
|
+
if (!url) return "Reading URL content";
|
|
2989
|
+
return `Reading URL ${url}`;
|
|
2990
|
+
}
|
|
2991
|
+
return `Tool: ${name}`;
|
|
2992
|
+
}
|
|
2441
2993
|
function renderSummaryCard(options) {
|
|
2442
|
-
const width = Math.min(Math.max((process.stdout.columns || 80) - 4,
|
|
2994
|
+
const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 64), 90);
|
|
2443
2995
|
const horizontal = "\u2500".repeat(width - 2);
|
|
2444
2996
|
const rawSummary = options.output ? extractExecutionSummary(options.output) : null;
|
|
2445
2997
|
let parsedFromLog = null;
|
|
@@ -2455,7 +3007,28 @@ function renderSummaryCard(options) {
|
|
|
2455
3007
|
}
|
|
2456
3008
|
}
|
|
2457
3009
|
let target = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : "";
|
|
2458
|
-
if (!target && parsedFromLog?.target
|
|
3010
|
+
if (!target && parsedFromLog?.target && parsedFromLog.target.toUpperCase() !== "YES") {
|
|
3011
|
+
const rawTarget = parsedFromLog.target;
|
|
3012
|
+
target = rawTarget.startsWith("#") ? options.routine === "peer-review" ? `PR ${rawTarget}` : `Issue ${rawTarget}` : rawTarget;
|
|
3013
|
+
}
|
|
3014
|
+
if (!target && options.output) {
|
|
3015
|
+
const targetMatch = options.output.match(/Selected\s+Target\s+PR:?\s*\[?PR\s*#?(\d+)\]?/i) || options.output.match(/Starting\s+review[^\n#]*?#(\d+)/i) || options.output.match(/Target(?:ing)?\s+(?:issue|PR)\s*#?(\d+)/i) || options.output.match(/Candidate\s+issue\s*#?(\d+)/i);
|
|
3016
|
+
if (targetMatch) {
|
|
3017
|
+
target = options.routine === "peer-review" ? `PR #${targetMatch[1]}` : `Issue #${targetMatch[1]}`;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
let title = options.title || parsedFromLog?.title || "";
|
|
3021
|
+
if (!title && rawSummary) {
|
|
3022
|
+
const titleMatch = rawSummary.match(/\[PR\s*#?\d+\s*\((`?[^`)]+`?)\)\]/i) || rawSummary.match(/Selected\s+Target\s+PR:?\s*\[.*?\]\([^)]+\)\s*\(([^)]+)\)/i) || rawSummary.match(/PR\s*#?\d+[:\s]+`?([^`\n]+)`?/i);
|
|
3023
|
+
if (titleMatch) title = titleMatch[1].replace(/[`*]/g, "").trim();
|
|
3024
|
+
}
|
|
3025
|
+
if (!title && options.output) {
|
|
3026
|
+
const titleMatch = options.output.match(/Selected\s+Target\s+PR:?\s*\[PR\s*#?\d+\s*\((`?[^`)]+`?)\)\]/i) || options.output.match(/Selected\s+candidate\s+issue\s*#?\d+[:\s]+`?([^`\n]+)`?/i);
|
|
3027
|
+
if (titleMatch) title = titleMatch[1].replace(/[`*]/g, "").trim();
|
|
3028
|
+
}
|
|
3029
|
+
if (!title && options.repoRoot && target) {
|
|
3030
|
+
title = fetchTargetTitle(options.repoRoot, target) || "";
|
|
3031
|
+
}
|
|
2459
3032
|
let decision = parsedFromLog?.decision || "";
|
|
2460
3033
|
if (!decision && rawSummary) {
|
|
2461
3034
|
const decisionMatch = rawSummary.match(/\*\*Final Action\*\*:\s*([^\n]+)/i);
|
|
@@ -2464,15 +3037,26 @@ function renderSummaryCard(options) {
|
|
|
2464
3037
|
const durationStr = options.durationMs ? `${Math.round(options.durationMs / 1e3)}s` : parsedFromLog?.duration || "";
|
|
2465
3038
|
const lines = [];
|
|
2466
3039
|
lines.push(pc9.cyan(`\u250C${horizontal}\u2510`));
|
|
2467
|
-
const
|
|
3040
|
+
const headerParts = [pc9.bold(pc9.white(options.routine.toUpperCase()))];
|
|
3041
|
+
if (target) headerParts.push(pc9.yellow(target));
|
|
3042
|
+
if (durationStr) headerParts.push(pc9.dim(`(${durationStr})`));
|
|
3043
|
+
const headerContent = headerParts.join(" \xB7 ");
|
|
3044
|
+
const headerPlain = stripAnsi(headerContent);
|
|
2468
3045
|
lines.push(
|
|
2469
|
-
pc9.cyan("\u2502") + ` ${
|
|
2470
|
-
Math.max(
|
|
2471
|
-
1,
|
|
2472
|
-
width - 4 - options.routine.length - target.length - (durationStr ? durationStr.length + 3 : 0)
|
|
2473
|
-
)
|
|
2474
|
-
) + pc9.cyan("\u2502")
|
|
3046
|
+
pc9.cyan("\u2502") + ` ${headerContent}` + " ".repeat(Math.max(1, width - 3 - headerPlain.length)) + pc9.cyan("\u2502")
|
|
2475
3047
|
);
|
|
3048
|
+
if (title) {
|
|
3049
|
+
const titlePrefix = " Title: ";
|
|
3050
|
+
const wrappedTitle = wrapText(title, width - 4 - titlePrefix.length);
|
|
3051
|
+
for (let i = 0; i < wrappedTitle.length; i++) {
|
|
3052
|
+
const prefix = i === 0 ? pc9.dim(titlePrefix) : " ".repeat(titlePrefix.length);
|
|
3053
|
+
const text = wrappedTitle[i];
|
|
3054
|
+
const plainLen = titlePrefix.length + stripAnsi(text).length;
|
|
3055
|
+
lines.push(
|
|
3056
|
+
pc9.cyan("\u2502") + ` ${prefix}${pc9.white(pc9.bold(text))}` + " ".repeat(Math.max(1, width - 3 - plainLen)) + pc9.cyan("\u2502")
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
2476
3060
|
if (decision) {
|
|
2477
3061
|
let decisionBadge = pc9.green(`\u2714 ${decision}`);
|
|
2478
3062
|
if (/bounce|draft|reject|fail/i.test(decision)) {
|
|
@@ -2480,8 +3064,9 @@ function renderSummaryCard(options) {
|
|
|
2480
3064
|
} else if (/escalat/i.test(decision)) {
|
|
2481
3065
|
decisionBadge = pc9.red(`\u{1F6A8} ${decision}`);
|
|
2482
3066
|
}
|
|
3067
|
+
const decisionPlain = ` Action: ${decision}`;
|
|
2483
3068
|
lines.push(
|
|
2484
|
-
pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width -
|
|
3069
|
+
pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width - 3 - decisionPlain.length)) + pc9.cyan("\u2502")
|
|
2485
3070
|
);
|
|
2486
3071
|
}
|
|
2487
3072
|
lines.push(pc9.cyan(`\u251C${horizontal}\u2524`));
|
|
@@ -2503,22 +3088,36 @@ function renderSummaryCard(options) {
|
|
|
2503
3088
|
} else if (line.startsWith("- ") || line.startsWith("* ")) {
|
|
2504
3089
|
const item = sanitizeWorktreePaths(line.slice(2)).trim();
|
|
2505
3090
|
const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
3091
|
+
const wrapped = wrapText(formatted, width - 8);
|
|
3092
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
3093
|
+
const wLine = wrapped[i];
|
|
3094
|
+
const wPlain = stripAnsi(wLine);
|
|
3095
|
+
if (i === 0) {
|
|
3096
|
+
lines.push(
|
|
3097
|
+
pc9.cyan("\u2502") + ` \u2022 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3098
|
+
);
|
|
3099
|
+
} else {
|
|
3100
|
+
lines.push(
|
|
3101
|
+
pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
2512
3104
|
}
|
|
2513
3105
|
} else if (/^[0-9]+\.\s+/.test(line)) {
|
|
2514
3106
|
const item = sanitizeWorktreePaths(line.replace(/^[0-9]+\.\s+/, "")).trim();
|
|
2515
3107
|
const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
|
|
2516
|
-
const
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
3108
|
+
const wrapped = wrapText(formatted, width - 8);
|
|
3109
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
3110
|
+
const wLine = wrapped[i];
|
|
3111
|
+
const wPlain = stripAnsi(wLine);
|
|
3112
|
+
if (i === 0) {
|
|
3113
|
+
lines.push(
|
|
3114
|
+
pc9.cyan("\u2502") + ` \u2714 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3115
|
+
);
|
|
3116
|
+
} else {
|
|
3117
|
+
lines.push(
|
|
3118
|
+
pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
2522
3121
|
}
|
|
2523
3122
|
}
|
|
2524
3123
|
}
|
|
@@ -2526,10 +3125,45 @@ function renderSummaryCard(options) {
|
|
|
2526
3125
|
lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Verification Passes:")}` + " ".repeat(Math.max(1, width - 23)) + pc9.cyan("\u2502"));
|
|
2527
3126
|
for (const pass of parsedFromLog.passes.slice(0, 6)) {
|
|
2528
3127
|
const icon = pass.status === "pass" ? pc9.green("\u2714") : pc9.red("\u2716");
|
|
2529
|
-
|
|
2530
|
-
const
|
|
2531
|
-
|
|
2532
|
-
|
|
3128
|
+
let criterionName = pass.name;
|
|
3129
|
+
const colonIdx = criterionName.indexOf(":");
|
|
3130
|
+
if (colonIdx > 10 && colonIdx < 40) {
|
|
3131
|
+
criterionName = criterionName.slice(0, colonIdx);
|
|
3132
|
+
}
|
|
3133
|
+
const passText = `${criterionName}${pass.detail || ""}`;
|
|
3134
|
+
const wrapped = wrapText(passText, width - 8);
|
|
3135
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
3136
|
+
const wLine = wrapped[i];
|
|
3137
|
+
const wPlain = stripAnsi(wLine);
|
|
3138
|
+
if (i === 0) {
|
|
3139
|
+
lines.push(
|
|
3140
|
+
pc9.cyan("\u2502") + ` ${icon} ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3141
|
+
);
|
|
3142
|
+
} else {
|
|
3143
|
+
lines.push(
|
|
3144
|
+
pc9.cyan("\u2502") + ` ${pc9.dim(wLine)}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
if (parsedFromLog.actions && parsedFromLog.actions.length > 0) {
|
|
3150
|
+
lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Actions Taken:")}` + " ".repeat(Math.max(1, width - 16)) + pc9.cyan("\u2502"));
|
|
3151
|
+
for (const action of parsedFromLog.actions.slice(0, 4)) {
|
|
3152
|
+
const wrapped = wrapText(action, width - 8);
|
|
3153
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
3154
|
+
const wLine = wrapped[i];
|
|
3155
|
+
const wPlain = stripAnsi(wLine);
|
|
3156
|
+
if (i === 0) {
|
|
3157
|
+
lines.push(
|
|
3158
|
+
pc9.cyan("\u2502") + ` \u2022 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3159
|
+
);
|
|
3160
|
+
} else {
|
|
3161
|
+
lines.push(
|
|
3162
|
+
pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
2533
3167
|
}
|
|
2534
3168
|
}
|
|
2535
3169
|
if (parsedFromLog?.logPath) {
|
|
@@ -2614,15 +3248,27 @@ var TerminalSpinner = class {
|
|
|
2614
3248
|
`);
|
|
2615
3249
|
}
|
|
2616
3250
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
3251
|
+
formatLine(message, maxWidth) {
|
|
3252
|
+
const cols = maxWidth ?? (process.stderr.columns || process.stdout.columns || 80);
|
|
2619
3253
|
const frame = pc9.cyan(this.frames[this.currentFrame]);
|
|
2620
|
-
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
|
|
2621
3254
|
const elapsedSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
2622
3255
|
const mins = Math.floor(elapsedSeconds / 60);
|
|
2623
3256
|
const secs = elapsedSeconds % 60;
|
|
2624
|
-
const
|
|
2625
|
-
|
|
3257
|
+
const timePlain = `[${mins}m ${secs < 10 ? "0" : ""}${secs}s]`;
|
|
3258
|
+
const timeStr = pc9.dim(timePlain);
|
|
3259
|
+
const fixedWidth = 6 + timePlain.length;
|
|
3260
|
+
const availableMsgWidth = Math.max(0, cols - fixedWidth - 1);
|
|
3261
|
+
let truncatedMsg = message;
|
|
3262
|
+
if (stripAnsi(message).length > availableMsgWidth) {
|
|
3263
|
+
truncatedMsg = availableMsgWidth > 3 ? truncateAnsi(message, availableMsgWidth - 3) + "..." : truncateAnsi(message, availableMsgWidth);
|
|
3264
|
+
}
|
|
3265
|
+
return ` ${frame} ${truncatedMsg} ${timeStr}`;
|
|
3266
|
+
}
|
|
3267
|
+
render() {
|
|
3268
|
+
if (!this.isRunning || !this.isTTY) return;
|
|
3269
|
+
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
|
|
3270
|
+
const line = this.formatLine(this.message);
|
|
3271
|
+
process.stderr.write(`\r\x1B[K${line}`);
|
|
2626
3272
|
}
|
|
2627
3273
|
stop() {
|
|
2628
3274
|
if (!this.isRunning) return;
|
|
@@ -2638,6 +3284,97 @@ var TerminalSpinner = class {
|
|
|
2638
3284
|
};
|
|
2639
3285
|
|
|
2640
3286
|
// src/lib/runner.ts
|
|
3287
|
+
import pc10 from "picocolors";
|
|
3288
|
+
var LineBufferedStreamParser = class {
|
|
3289
|
+
buffer = "";
|
|
3290
|
+
onLine;
|
|
3291
|
+
constructor(onLine) {
|
|
3292
|
+
this.onLine = onLine;
|
|
3293
|
+
}
|
|
3294
|
+
feed(chunk) {
|
|
3295
|
+
this.buffer += chunk;
|
|
3296
|
+
const lines = this.buffer.split("\n");
|
|
3297
|
+
this.buffer = lines.pop() ?? "";
|
|
3298
|
+
for (const line of lines) {
|
|
3299
|
+
const trimmed = line.trim();
|
|
3300
|
+
if (trimmed.length > 0) {
|
|
3301
|
+
this.onLine(trimmed);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
flush() {
|
|
3306
|
+
if (this.buffer.trim().length > 0) {
|
|
3307
|
+
this.onLine(this.buffer.trim());
|
|
3308
|
+
this.buffer = "";
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
3312
|
+
function parseStreamJsonEvent(line) {
|
|
3313
|
+
if (!line || !line.trim()) return null;
|
|
3314
|
+
try {
|
|
3315
|
+
const parsed = JSON.parse(line);
|
|
3316
|
+
if (parsed && typeof parsed === "object") {
|
|
3317
|
+
return parsed;
|
|
3318
|
+
}
|
|
3319
|
+
return null;
|
|
3320
|
+
} catch {
|
|
3321
|
+
return null;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
function formatVerboseEvent(event) {
|
|
3325
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
3326
|
+
if (event.event === "init") {
|
|
3327
|
+
return `${pc10.dim(`[${time}]`)} ${pc10.cyan("[init]")} Session started (conversation: ${event.conversation_id || "n/a"})`;
|
|
3328
|
+
}
|
|
3329
|
+
if (event.event === "step_update" && event.step_update) {
|
|
3330
|
+
const su = event.step_update;
|
|
3331
|
+
if (su.step_type === "user_input") {
|
|
3332
|
+
return `${pc10.dim(`[${time}]`)} ${pc10.magenta("[user_input]")} Prompt dispatched`;
|
|
3333
|
+
}
|
|
3334
|
+
if (su.step_type === "tool") {
|
|
3335
|
+
const toolName = su.tool_name || su.tool_info?.name || "tool";
|
|
3336
|
+
const params = su.tool_info?.parameters;
|
|
3337
|
+
if (su.state === "ACTIVE") {
|
|
3338
|
+
const desc = formatActionDescription(toolName, params);
|
|
3339
|
+
return `${pc10.dim(`[${time}]`)} ${pc10.blue("[tool:start]")} ${pc10.bold(toolName)} \u2192 ${desc}`;
|
|
3340
|
+
}
|
|
3341
|
+
if (su.state === "DONE") {
|
|
3342
|
+
const dur = su.duration_seconds !== void 0 ? `${su.duration_seconds.toFixed(1)}s` : "done";
|
|
3343
|
+
return `${pc10.dim(`[${time}]`)} ${pc10.green("[tool:done]")} ${pc10.bold(toolName)} (${dur})`;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
if (su.step_type === "agent_response" || su.step_type === "thought") {
|
|
3347
|
+
if (su.text_delta) {
|
|
3348
|
+
return su.text_delta;
|
|
3349
|
+
}
|
|
3350
|
+
if (su.state === "DONE") {
|
|
3351
|
+
const dur = su.duration_seconds !== void 0 ? ` (${su.duration_seconds.toFixed(1)}s)` : "";
|
|
3352
|
+
return `${pc10.dim(`[${time}]`)} ${pc10.cyan("[agent:step]")} Step ${su.step_index ?? 0} finished${dur}`;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
if (event.event === "result" && event.result) {
|
|
3357
|
+
const res = event.result;
|
|
3358
|
+
const dur = res.duration_seconds !== void 0 ? `${res.duration_seconds.toFixed(1)}s` : "";
|
|
3359
|
+
const tokens = res.usage?.total_tokens ? `${res.usage.total_tokens.toLocaleString()} tokens` : "";
|
|
3360
|
+
const metrics = [dur, tokens].filter(Boolean).join(", ");
|
|
3361
|
+
return `${pc10.dim(`[${time}]`)} ${pc10.bold(pc10.green("[result]"))} ${res.status || "COMPLETED"} (${metrics || "done"})`;
|
|
3362
|
+
}
|
|
3363
|
+
return null;
|
|
3364
|
+
}
|
|
3365
|
+
function buildAgyArgs(prompt, model, printTimeout) {
|
|
3366
|
+
return [
|
|
3367
|
+
"-p",
|
|
3368
|
+
prompt,
|
|
3369
|
+
"--model",
|
|
3370
|
+
model,
|
|
3371
|
+
"--output-format",
|
|
3372
|
+
"stream-json",
|
|
3373
|
+
"--print-timeout",
|
|
3374
|
+
printTimeout,
|
|
3375
|
+
"--dangerously-skip-permissions"
|
|
3376
|
+
];
|
|
3377
|
+
}
|
|
2641
3378
|
function discoverSkillsPrompt(targetDir) {
|
|
2642
3379
|
const skillsDir = path12.join(targetDir, ".agents", "skills");
|
|
2643
3380
|
if (!fs12.existsSync(skillsDir)) return "";
|
|
@@ -2722,18 +3459,10 @@ Timeout: ${printTimeout}`,
|
|
|
2722
3459
|
TARGET_ISSUE: options.issue ? String(options.issue) : "",
|
|
2723
3460
|
PR_NUMBER: options.pr ? String(options.pr) : ""
|
|
2724
3461
|
};
|
|
2725
|
-
const args =
|
|
2726
|
-
"-p",
|
|
2727
|
-
prompt,
|
|
2728
|
-
"--model",
|
|
2729
|
-
model,
|
|
2730
|
-
"--output-format",
|
|
2731
|
-
"text",
|
|
2732
|
-
"--print-timeout",
|
|
2733
|
-
printTimeout,
|
|
2734
|
-
"--dangerously-skip-permissions"
|
|
2735
|
-
];
|
|
3462
|
+
const args = buildAgyArgs(prompt, model, printTimeout);
|
|
2736
3463
|
let output = "";
|
|
3464
|
+
let finalResponseText = "";
|
|
3465
|
+
let accumulatedOutput = "";
|
|
2737
3466
|
let exitCode = 0;
|
|
2738
3467
|
const startTime = Date.now();
|
|
2739
3468
|
const logDir = path12.join(targetDir, ".jonah-fleet");
|
|
@@ -2759,39 +3488,127 @@ Timeout: ${printTimeout}`,
|
|
|
2759
3488
|
};
|
|
2760
3489
|
process.once("SIGINT", sigintHandler);
|
|
2761
3490
|
process.once("SIGTERM", sigintHandler);
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
3491
|
+
const checkTargetDetection = (text) => {
|
|
3492
|
+
if (dynamicTargetDetected || !text) return;
|
|
3493
|
+
const detected = routine === "peer-review" ? detectClaimedPR(text) : detectClaimedIssue(text);
|
|
3494
|
+
if (detected) {
|
|
3495
|
+
dynamicTargetDetected = true;
|
|
3496
|
+
targetLabel = detected;
|
|
3497
|
+
options.onTargetDetected?.(detected);
|
|
3498
|
+
if (spinner) {
|
|
3499
|
+
spinner.update(`${targetLabel}: ${activePhase}`);
|
|
3500
|
+
}
|
|
2767
3501
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
if (
|
|
3502
|
+
};
|
|
3503
|
+
const stdoutParser = new LineBufferedStreamParser((line) => {
|
|
3504
|
+
const event = parseStreamJsonEvent(line);
|
|
3505
|
+
if (event) {
|
|
3506
|
+
if (event.event === "step_update" && event.step_update) {
|
|
3507
|
+
const su = event.step_update;
|
|
3508
|
+
if (su.step_type === "tool") {
|
|
3509
|
+
const toolName = su.tool_name || su.tool_info?.name || "unknown";
|
|
3510
|
+
const toolParams = su.tool_info?.parameters;
|
|
3511
|
+
if (su.state === "ACTIVE") {
|
|
3512
|
+
const actionDesc = formatActionDescription(toolName, toolParams);
|
|
3513
|
+
if (spinner) {
|
|
3514
|
+
spinner.update(`${targetLabel}: ${actionDesc}`);
|
|
3515
|
+
}
|
|
3516
|
+
if (toolParams?.CommandLine) {
|
|
3517
|
+
checkTargetDetection(toolParams.CommandLine);
|
|
3518
|
+
}
|
|
3519
|
+
if (options.verbose) {
|
|
3520
|
+
const formatted = formatVerboseEvent(event);
|
|
3521
|
+
if (formatted) console.log(formatted);
|
|
3522
|
+
}
|
|
3523
|
+
} else if (su.state === "DONE") {
|
|
3524
|
+
if (su.tool_info?.output) {
|
|
3525
|
+
checkTargetDetection(su.tool_info.output);
|
|
3526
|
+
}
|
|
3527
|
+
if (spinner) {
|
|
3528
|
+
activePhase = "Evaluating tool output...";
|
|
3529
|
+
spinner.update(`${targetLabel}: ${activePhase}`);
|
|
3530
|
+
}
|
|
3531
|
+
if (options.verbose) {
|
|
3532
|
+
const formatted = formatVerboseEvent(event);
|
|
3533
|
+
if (formatted) console.log(formatted);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
} else if (su.step_type === "agent_response" || su.step_type === "thought") {
|
|
3537
|
+
if (su.text_delta) {
|
|
3538
|
+
accumulatedOutput += su.text_delta;
|
|
3539
|
+
checkTargetDetection(su.text_delta);
|
|
3540
|
+
const newPhase = detectActivePhase(su.text_delta, activePhase);
|
|
3541
|
+
if (newPhase !== activePhase) {
|
|
3542
|
+
activePhase = newPhase;
|
|
3543
|
+
if (spinner) {
|
|
3544
|
+
spinner.update(`${targetLabel}: ${activePhase}`);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
if (options.verbose) {
|
|
3548
|
+
process.stdout.write(su.text_delta);
|
|
3549
|
+
}
|
|
3550
|
+
} else if (options.verbose && su.state === "DONE") {
|
|
3551
|
+
const formatted = formatVerboseEvent(event);
|
|
3552
|
+
if (formatted) console.log(formatted);
|
|
3553
|
+
}
|
|
3554
|
+
} else if (options.verbose) {
|
|
3555
|
+
const formatted = formatVerboseEvent(event);
|
|
3556
|
+
if (formatted) console.log(formatted);
|
|
3557
|
+
}
|
|
3558
|
+
} else if (event.event === "result" && event.result) {
|
|
3559
|
+
if (event.result.response) {
|
|
3560
|
+
finalResponseText = event.result.response;
|
|
3561
|
+
checkTargetDetection(event.result.response);
|
|
3562
|
+
}
|
|
3563
|
+
if (options.verbose) {
|
|
3564
|
+
const formatted = formatVerboseEvent(event);
|
|
3565
|
+
if (formatted) console.log(formatted);
|
|
3566
|
+
}
|
|
3567
|
+
} else if (event.event === "init") {
|
|
3568
|
+
if (options.verbose) {
|
|
3569
|
+
const formatted = formatVerboseEvent(event);
|
|
3570
|
+
if (formatted) console.log(formatted);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
} else {
|
|
3574
|
+
accumulatedOutput += line + "\n";
|
|
3575
|
+
checkTargetDetection(line);
|
|
3576
|
+
if (options.verbose) {
|
|
3577
|
+
console.log(line);
|
|
3578
|
+
} else if (spinner) {
|
|
3579
|
+
const newPhase = detectActivePhase(line, activePhase);
|
|
3580
|
+
if (newPhase !== activePhase) {
|
|
3581
|
+
activePhase = newPhase;
|
|
2775
3582
|
spinner.update(`${targetLabel}: ${activePhase}`);
|
|
2776
3583
|
}
|
|
2777
3584
|
}
|
|
2778
3585
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
3586
|
+
});
|
|
3587
|
+
const stderrParser = new LineBufferedStreamParser((line) => {
|
|
3588
|
+
checkTargetDetection(line);
|
|
2782
3589
|
if (options.verbose) {
|
|
2783
|
-
|
|
2784
|
-
process.stderr.write(chunk);
|
|
2785
|
-
} else {
|
|
2786
|
-
process.stdout.write(chunk);
|
|
2787
|
-
}
|
|
3590
|
+
console.error(pc10.dim(`[stderr] ${line}`));
|
|
2788
3591
|
} else if (spinner) {
|
|
2789
|
-
const newPhase = detectActivePhase(
|
|
3592
|
+
const newPhase = detectActivePhase(line, activePhase);
|
|
2790
3593
|
if (newPhase !== activePhase) {
|
|
2791
3594
|
activePhase = newPhase;
|
|
2792
3595
|
spinner.update(`${targetLabel}: ${activePhase}`);
|
|
2793
3596
|
}
|
|
2794
3597
|
}
|
|
3598
|
+
});
|
|
3599
|
+
const processChunk = (chunk, isStderr = false) => {
|
|
3600
|
+
try {
|
|
3601
|
+
fs12.appendFileSync(logFilePath, chunk, "utf8");
|
|
3602
|
+
} catch {
|
|
3603
|
+
}
|
|
3604
|
+
if (options.onLog) {
|
|
3605
|
+
options.onLog(chunk);
|
|
3606
|
+
}
|
|
3607
|
+
if (isStderr) {
|
|
3608
|
+
stderrParser.feed(chunk);
|
|
3609
|
+
} else {
|
|
3610
|
+
stdoutParser.feed(chunk);
|
|
3611
|
+
}
|
|
2795
3612
|
};
|
|
2796
3613
|
try {
|
|
2797
3614
|
exitCode = await new Promise((resolve, reject) => {
|
|
@@ -2823,6 +3640,9 @@ Timeout: ${printTimeout}`,
|
|
|
2823
3640
|
await cleanup();
|
|
2824
3641
|
}
|
|
2825
3642
|
}
|
|
3643
|
+
stdoutParser.flush();
|
|
3644
|
+
stderrParser.flush();
|
|
3645
|
+
output = finalResponseText || accumulatedOutput;
|
|
2826
3646
|
if (options.showCard !== false && !options.verbose) {
|
|
2827
3647
|
const durationMs = Date.now() - startTime;
|
|
2828
3648
|
const effectiveIssue = options.issue || (targetLabel.startsWith("Issue #") ? targetLabel.replace("Issue #", "") : void 0);
|
|
@@ -2866,31 +3686,31 @@ async function runRoutineCommand(routine, options = {}) {
|
|
|
2866
3686
|
const manifest = loadManifest(cwd);
|
|
2867
3687
|
if (!manifest) {
|
|
2868
3688
|
console.warn(
|
|
2869
|
-
|
|
3689
|
+
pc11.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
|
|
2870
3690
|
);
|
|
2871
3691
|
} else if (manifest.routines && manifest.routines[routine] === false) {
|
|
2872
3692
|
console.warn(
|
|
2873
|
-
|
|
3693
|
+
pc11.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
|
|
2874
3694
|
);
|
|
2875
3695
|
}
|
|
2876
|
-
console.log(
|
|
2877
|
-
\u{1F680} Launching local agent session for routine: ${
|
|
3696
|
+
console.log(pc11.cyan(`
|
|
3697
|
+
\u{1F680} Launching local agent session for routine: ${pc11.bold(routine)}`));
|
|
2878
3698
|
if (options.issue) {
|
|
2879
|
-
console.log(
|
|
3699
|
+
console.log(pc11.dim(` Target issue: #${options.issue}`));
|
|
2880
3700
|
}
|
|
2881
3701
|
if (options.pr) {
|
|
2882
|
-
console.log(
|
|
3702
|
+
console.log(pc11.dim(` Target pull request: #${options.pr}`));
|
|
2883
3703
|
}
|
|
2884
3704
|
if (options.model) {
|
|
2885
|
-
console.log(
|
|
3705
|
+
console.log(pc11.dim(` Model override: ${options.model}`));
|
|
2886
3706
|
}
|
|
2887
3707
|
if (options.verbose) {
|
|
2888
|
-
console.log(
|
|
3708
|
+
console.log(pc11.dim(` Verbose output: Enabled (streaming raw tokens)`));
|
|
2889
3709
|
}
|
|
2890
3710
|
if (options.worktree !== false) {
|
|
2891
|
-
console.log(
|
|
3711
|
+
console.log(pc11.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
|
|
2892
3712
|
} else {
|
|
2893
|
-
console.log(
|
|
3713
|
+
console.log(pc11.yellow(` Workspace isolation: Disabled (running in current directory)`));
|
|
2894
3714
|
}
|
|
2895
3715
|
console.log("");
|
|
2896
3716
|
try {
|
|
@@ -2907,56 +3727,431 @@ async function runRoutineCommand(routine, options = {}) {
|
|
|
2907
3727
|
verbose: options.verbose
|
|
2908
3728
|
});
|
|
2909
3729
|
if (options.dryRun) {
|
|
2910
|
-
console.log(
|
|
3730
|
+
console.log(pc11.green(result.output));
|
|
2911
3731
|
return;
|
|
2912
3732
|
}
|
|
2913
3733
|
if (result.success) {
|
|
2914
|
-
console.log(
|
|
3734
|
+
console.log(pc11.green(`
|
|
2915
3735
|
\u2713 Local agent session for '${routine}' completed successfully.`));
|
|
2916
3736
|
} else {
|
|
2917
|
-
console.error(
|
|
3737
|
+
console.error(pc11.red(`
|
|
2918
3738
|
\u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
|
|
2919
3739
|
process.exit(result.exitCode);
|
|
2920
3740
|
}
|
|
2921
3741
|
} catch (error) {
|
|
2922
|
-
console.error(
|
|
3742
|
+
console.error(pc11.red(`
|
|
2923
3743
|
\u2717 Failed to execute routine '${routine}': ${error.message}`));
|
|
2924
3744
|
process.exit(1);
|
|
2925
3745
|
}
|
|
2926
3746
|
}
|
|
2927
3747
|
|
|
2928
3748
|
// src/commands/daemon.ts
|
|
2929
|
-
import
|
|
3749
|
+
import pc14 from "picocolors";
|
|
2930
3750
|
|
|
2931
3751
|
// src/lib/daemon.ts
|
|
2932
|
-
import
|
|
2933
|
-
import
|
|
3752
|
+
import fs14 from "fs";
|
|
3753
|
+
import path14 from "path";
|
|
2934
3754
|
import { spawn as spawn2, execFile as execFile3 } from "child_process";
|
|
2935
3755
|
import { promisify as promisify3 } from "util";
|
|
2936
|
-
|
|
3756
|
+
|
|
3757
|
+
// src/lib/daemon-keys.ts
|
|
3758
|
+
import readline2 from "readline";
|
|
3759
|
+
import fs13 from "fs";
|
|
3760
|
+
import path13 from "path";
|
|
3761
|
+
import pc12 from "picocolors";
|
|
3762
|
+
var KeyboardController = class {
|
|
3763
|
+
constructor(options = {}) {
|
|
3764
|
+
this.options = options;
|
|
3765
|
+
this.stdin = options.stdin || process.stdin;
|
|
3766
|
+
}
|
|
3767
|
+
options;
|
|
3768
|
+
stdin;
|
|
3769
|
+
isRaw = false;
|
|
3770
|
+
listening = false;
|
|
3771
|
+
isPaused = false;
|
|
3772
|
+
keypressListener;
|
|
3773
|
+
start() {
|
|
3774
|
+
if (this.listening) return;
|
|
3775
|
+
if (this.stdin && typeof this.stdin.setRawMode === "function" && this.stdin.isTTY) {
|
|
3776
|
+
readline2.emitKeypressEvents(this.stdin);
|
|
3777
|
+
try {
|
|
3778
|
+
this.stdin.setRawMode(true);
|
|
3779
|
+
this.isRaw = true;
|
|
3780
|
+
} catch {
|
|
3781
|
+
this.isRaw = false;
|
|
3782
|
+
}
|
|
3783
|
+
if (typeof this.stdin.resume === "function") {
|
|
3784
|
+
this.stdin.resume();
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
this.keypressListener = (str, key) => {
|
|
3788
|
+
this.handleKeypress(str, key);
|
|
3789
|
+
};
|
|
3790
|
+
this.stdin.on("keypress", this.keypressListener);
|
|
3791
|
+
this.listening = true;
|
|
3792
|
+
this.isPaused = false;
|
|
3793
|
+
}
|
|
3794
|
+
pause() {
|
|
3795
|
+
this.isPaused = true;
|
|
3796
|
+
}
|
|
3797
|
+
resume() {
|
|
3798
|
+
this.isPaused = false;
|
|
3799
|
+
}
|
|
3800
|
+
handleKeypress(str, key) {
|
|
3801
|
+
if (this.isPaused) return;
|
|
3802
|
+
const k = key || {};
|
|
3803
|
+
if (k.ctrl && (k.name === "c" || k.name === "C") || str === "") {
|
|
3804
|
+
this.options.onForceStop?.();
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
const isShift = Boolean(k.shift);
|
|
3808
|
+
const keyName = (k.name || "").toLowerCase();
|
|
3809
|
+
if (str === "R" || keyName === "r" && isShift) {
|
|
3810
|
+
this.options.onTargetedReview?.();
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
if (str === "A" || keyName === "a" && isShift) {
|
|
3814
|
+
this.options.onTargetedAutowork?.();
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
if (str === "r" || keyName === "r" && !isShift) {
|
|
3818
|
+
this.options.onReview?.();
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
if (str === "a" || keyName === "a" && !isShift) {
|
|
3822
|
+
this.options.onAutowork?.();
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
if (str === "v" || str === "V" || keyName === "v") {
|
|
3826
|
+
this.options.onToggleVerbose?.();
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
if (str === "l" || str === "L" || keyName === "l") {
|
|
3830
|
+
this.options.onTailLog?.();
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
if (str === "w" || str === "W" || keyName === "w") {
|
|
3834
|
+
this.options.onCleanWorktrees?.();
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
if (str === "p" || str === "P" || keyName === "p") {
|
|
3838
|
+
this.options.onPauseToggle?.();
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
if (str === "s" || str === "S" || keyName === "s") {
|
|
3842
|
+
this.options.onStatus?.();
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
if (str === "q" || str === "Q" || keyName === "q") {
|
|
3846
|
+
this.options.onGracefulStop?.();
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
if (str === "?" || keyName === "h" || str === "h" || str === "H") {
|
|
3850
|
+
this.options.onHelp?.();
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
stop() {
|
|
3855
|
+
if (!this.listening) return;
|
|
3856
|
+
if (this.keypressListener) {
|
|
3857
|
+
this.stdin.removeListener("keypress", this.keypressListener);
|
|
3858
|
+
}
|
|
3859
|
+
if (this.isRaw && typeof this.stdin.setRawMode === "function") {
|
|
3860
|
+
try {
|
|
3861
|
+
this.stdin.setRawMode(false);
|
|
3862
|
+
} catch {
|
|
3863
|
+
}
|
|
3864
|
+
this.isRaw = false;
|
|
3865
|
+
}
|
|
3866
|
+
if (typeof this.stdin.pause === "function") {
|
|
3867
|
+
try {
|
|
3868
|
+
this.stdin.pause();
|
|
3869
|
+
} catch {
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
this.listening = false;
|
|
3873
|
+
this.isPaused = false;
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
function parseNumericTarget(input) {
|
|
3877
|
+
if (!input) return null;
|
|
3878
|
+
const trimmed = input.trim();
|
|
3879
|
+
if (!trimmed) return null;
|
|
3880
|
+
const match = trimmed.match(/^(?:(?:PR|Issue|pr|issue)\s*#?)?#?(\d+)$/i);
|
|
3881
|
+
if (match) {
|
|
3882
|
+
const num = parseInt(match[1], 10);
|
|
3883
|
+
return num > 0 ? num : null;
|
|
3884
|
+
}
|
|
3885
|
+
return null;
|
|
3886
|
+
}
|
|
3887
|
+
async function promptTargetedInput(promptMessage, options = {}) {
|
|
3888
|
+
const stdin = options.stdin || process.stdin;
|
|
3889
|
+
const stdout = options.stdout || process.stdout;
|
|
3890
|
+
if (stdout && typeof stdout.write === "function") {
|
|
3891
|
+
stdout.write(promptMessage);
|
|
3892
|
+
}
|
|
3893
|
+
return new Promise((resolve) => {
|
|
3894
|
+
let cleanedUp = false;
|
|
3895
|
+
const wasRaw = Boolean(stdin && stdin.rawMode !== void 0 ? stdin.rawMode : stdin?.isRaw);
|
|
3896
|
+
if (stdin && typeof stdin.setRawMode === "function" && stdin.isTTY) {
|
|
3897
|
+
try {
|
|
3898
|
+
stdin.setRawMode(false);
|
|
3899
|
+
} catch {
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
if (stdin && typeof stdin.resume !== "function") {
|
|
3903
|
+
stdin.resume = () => {
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3906
|
+
if (stdin && typeof stdin.pause !== "function") {
|
|
3907
|
+
stdin.pause = () => {
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
const rl = readline2.createInterface({
|
|
3911
|
+
input: stdin,
|
|
3912
|
+
output: stdout,
|
|
3913
|
+
terminal: Boolean(stdin && stdin.isTTY)
|
|
3914
|
+
});
|
|
3915
|
+
const cleanup = (val) => {
|
|
3916
|
+
if (cleanedUp) return;
|
|
3917
|
+
cleanedUp = true;
|
|
3918
|
+
try {
|
|
3919
|
+
if (stdin && typeof stdin.removeListener === "function") {
|
|
3920
|
+
stdin.removeListener("data", onRawData);
|
|
3921
|
+
}
|
|
3922
|
+
rl.close();
|
|
3923
|
+
} catch {
|
|
3924
|
+
}
|
|
3925
|
+
if (wasRaw && stdin && typeof stdin.setRawMode === "function" && stdin.isTTY) {
|
|
3926
|
+
try {
|
|
3927
|
+
stdin.setRawMode(true);
|
|
3928
|
+
} catch {
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
resolve(val);
|
|
3932
|
+
};
|
|
3933
|
+
const onRawData = (chunk) => {
|
|
3934
|
+
const str = chunk.toString();
|
|
3935
|
+
if (str === "\x1B" || str === "") {
|
|
3936
|
+
cleanup(null);
|
|
3937
|
+
}
|
|
3938
|
+
};
|
|
3939
|
+
if (stdin && typeof stdin.on === "function") {
|
|
3940
|
+
stdin.on("data", onRawData);
|
|
3941
|
+
}
|
|
3942
|
+
rl.question("", (answer) => {
|
|
3943
|
+
cleanup(answer.trim() || null);
|
|
3944
|
+
});
|
|
3945
|
+
rl.on("close", () => {
|
|
3946
|
+
cleanup(null);
|
|
3947
|
+
});
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
function getDaemonLogTail(repoRoot, linesCount = 20) {
|
|
3951
|
+
const logPath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
|
|
3952
|
+
if (!fs13.existsSync(logPath)) return [];
|
|
3953
|
+
try {
|
|
3954
|
+
const content = fs13.readFileSync(logPath, "utf8");
|
|
3955
|
+
const lines = content.split("\n");
|
|
3956
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
3957
|
+
lines.pop();
|
|
3958
|
+
}
|
|
3959
|
+
return lines.slice(-linesCount);
|
|
3960
|
+
} catch {
|
|
3961
|
+
return [];
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
function printDaemonLogTail(repoRoot, linesCount = 20) {
|
|
3965
|
+
const lines = getDaemonLogTail(repoRoot, linesCount);
|
|
3966
|
+
const logPath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
|
|
3967
|
+
const relativePath = path13.relative(repoRoot, logPath) || logPath;
|
|
3968
|
+
console.log(pc12.cyan(`
|
|
3969
|
+
\u{1F4C4} Tail of ${relativePath} (last ${linesCount} lines):
|
|
3970
|
+
`));
|
|
3971
|
+
if (lines.length === 0) {
|
|
3972
|
+
console.log(pc12.dim(` (Log file is empty or does not exist yet at ${relativePath})
|
|
3973
|
+
`));
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
for (const line of lines) {
|
|
3977
|
+
console.log(pc12.dim(line));
|
|
3978
|
+
}
|
|
3979
|
+
console.log("");
|
|
3980
|
+
}
|
|
3981
|
+
async function inspectAndCleanWorktrees(repoRoot) {
|
|
3982
|
+
const active = await listActiveWorktrees(repoRoot);
|
|
3983
|
+
const cleaned = await cleanupStaleWorktrees(repoRoot);
|
|
3984
|
+
return { active, cleaned };
|
|
3985
|
+
}
|
|
3986
|
+
function printWorktreesInspection(result) {
|
|
3987
|
+
console.log(pc12.cyan(`
|
|
3988
|
+
\u{1F333} Jonah Fleet Worktree Inspection & Maintenance
|
|
3989
|
+
`));
|
|
3990
|
+
console.log(` Active Worktrees: ${result.active.length}`);
|
|
3991
|
+
if (result.active.length === 0) {
|
|
3992
|
+
console.log(pc12.dim(` (No active routine worktrees found)`));
|
|
3993
|
+
} else {
|
|
3994
|
+
for (const wt of result.active) {
|
|
3995
|
+
const commitShort = wt.commit ? ` (${wt.commit.slice(0, 7)})` : "";
|
|
3996
|
+
console.log(` - [${pc12.bold(wt.branch)}] ${pc12.dim(wt.path)}${commitShort}`);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
console.log(`
|
|
4000
|
+
Cleaned Stale Worktrees: ${result.cleaned}`);
|
|
4001
|
+
console.log("");
|
|
4002
|
+
}
|
|
4003
|
+
function printKeybindingCheatSheet() {
|
|
4004
|
+
console.log(pc12.cyan(`
|
|
4005
|
+
\u2328\uFE0F Jonah Fleet Daemon Keybindings
|
|
4006
|
+
`));
|
|
4007
|
+
console.log(` ${pc12.bold("r")} Trigger peer-review scan immediately`);
|
|
4008
|
+
console.log(` ${pc12.bold("R")} Prompt for PR # and run targeted peer-review`);
|
|
4009
|
+
console.log(` ${pc12.bold("a")} Trigger autowork backlog scan immediately`);
|
|
4010
|
+
console.log(` ${pc12.bold("A")} Prompt for Issue # and run targeted autowork`);
|
|
4011
|
+
console.log(` ${pc12.bold("p")} Pause / resume automated polling intervals`);
|
|
4012
|
+
console.log(` ${pc12.bold("s")} Print current daemon status summary card`);
|
|
4013
|
+
console.log(` ${pc12.bold("v")} Toggle verbose streaming logging live`);
|
|
4014
|
+
console.log(` ${pc12.bold("l")} Tail recent lines from .jonah-fleet/daemon.log`);
|
|
4015
|
+
console.log(` ${pc12.bold("w")} Inspect active worktrees and clean stale ones`);
|
|
4016
|
+
console.log(` ${pc12.bold("q")} Graceful shutdown (waits for active routine to finish)`);
|
|
4017
|
+
console.log(` ${pc12.bold("Ctrl+C")} Immediate force abort`);
|
|
4018
|
+
console.log(` ${pc12.bold("?")} / ${pc12.bold("h")} Show this keybindings cheat-sheet
|
|
4019
|
+
`);
|
|
4020
|
+
}
|
|
4021
|
+
function printDaemonStatusSummary(options) {
|
|
4022
|
+
const { state, pendingRoutine, activeWorktrees = [], verbose } = options;
|
|
4023
|
+
console.log(pc12.cyan(`
|
|
4024
|
+
\u{1F916} Jonah Fleet Local Daemon Status
|
|
4025
|
+
`));
|
|
4026
|
+
if (state) {
|
|
4027
|
+
let statusText;
|
|
4028
|
+
if (state.status === "working") {
|
|
4029
|
+
const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc12.bold(state.activeTarget)})` : "");
|
|
4030
|
+
statusText = pc12.yellow(pc12.bold(`WORKING on ${workingDesc}`));
|
|
4031
|
+
} else if (state.status === "paused") {
|
|
4032
|
+
statusText = pc12.yellow(pc12.bold("PAUSED"));
|
|
4033
|
+
} else {
|
|
4034
|
+
statusText = pc12.green(pc12.bold("RUNNING (IDLE)"));
|
|
4035
|
+
}
|
|
4036
|
+
console.log(` Status: ${statusText}`);
|
|
4037
|
+
console.log(` PID: ${state.pid}`);
|
|
4038
|
+
console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
|
|
4039
|
+
console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
|
|
4040
|
+
console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
|
|
4041
|
+
console.log(` Routines: ${state.routines.join(", ")}`);
|
|
4042
|
+
if (verbose !== void 0) {
|
|
4043
|
+
console.log(
|
|
4044
|
+
` Verbose Mode: ${verbose ? pc12.green("ENABLED (streaming tokens)") : pc12.gray("DISABLED (compact spinner)")}`
|
|
4045
|
+
);
|
|
4046
|
+
}
|
|
4047
|
+
if (pendingRoutine) {
|
|
4048
|
+
console.log(` Queued Routine: ${pc12.cyan(pc12.bold(pendingRoutine))}`);
|
|
4049
|
+
}
|
|
4050
|
+
if (state.lastReviewCheckAt) {
|
|
4051
|
+
console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
|
|
4052
|
+
}
|
|
4053
|
+
if (state.lastAutoworkCheckAt) {
|
|
4054
|
+
console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
|
|
4055
|
+
}
|
|
4056
|
+
} else {
|
|
4057
|
+
console.log(` Status: ${pc12.gray("STOPPED")}`);
|
|
4058
|
+
console.log(pc12.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
|
|
4059
|
+
}
|
|
4060
|
+
console.log(`
|
|
4061
|
+
Active Worktrees: ${activeWorktrees.length}`);
|
|
4062
|
+
for (const wt of activeWorktrees) {
|
|
4063
|
+
console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
|
|
4064
|
+
}
|
|
4065
|
+
console.log("");
|
|
4066
|
+
}
|
|
4067
|
+
var DAEMON_STATUS_TIPS = [
|
|
4068
|
+
"Tip: press 'r' to run review pass now",
|
|
4069
|
+
"Tip: press 'a' to run autowork scan now",
|
|
4070
|
+
"Tip: press 'R' to review a specific PR #",
|
|
4071
|
+
"Tip: press 'A' to work a specific Issue #",
|
|
4072
|
+
"Tip: press 'p' to pause/resume automatic checks",
|
|
4073
|
+
"Tip: press 's' to view daemon status",
|
|
4074
|
+
"Tip: press 'v' to toggle verbose streaming",
|
|
4075
|
+
"Tip: press 'l' to view recent log tail",
|
|
4076
|
+
"Tip: press 'w' to inspect/clean worktrees",
|
|
4077
|
+
"Tip: press 'q' to stop daemon gracefully",
|
|
4078
|
+
"Tip: press '?' for all keybindings"
|
|
4079
|
+
];
|
|
4080
|
+
var DAEMON_PAUSED_TIPS = [
|
|
4081
|
+
"Tip: press 'p' to resume scheduled checks",
|
|
4082
|
+
"Tip: press 'r' to run review pass now",
|
|
4083
|
+
"Tip: press 'a' to run autowork scan now",
|
|
4084
|
+
"Tip: press 'R' to review a specific PR #",
|
|
4085
|
+
"Tip: press 'A' to work a specific Issue #",
|
|
4086
|
+
"Tip: press 's' to view daemon status",
|
|
4087
|
+
"Tip: press 'v' to toggle verbose streaming",
|
|
4088
|
+
"Tip: press 'l' to view recent log tail",
|
|
4089
|
+
"Tip: press 'w' to inspect/clean worktrees",
|
|
4090
|
+
"Tip: press 'q' to stop daemon gracefully",
|
|
4091
|
+
"Tip: press '?' for all keybindings"
|
|
4092
|
+
];
|
|
4093
|
+
function getRotatingTipIndex(nowMs = Date.now(), intervalSeconds = 4, totalTips = DAEMON_STATUS_TIPS.length) {
|
|
4094
|
+
if (totalTips <= 0) return 0;
|
|
4095
|
+
const slot = Math.floor(nowMs / (intervalSeconds * 1e3));
|
|
4096
|
+
return (slot % totalTips + totalTips) % totalTips;
|
|
4097
|
+
}
|
|
4098
|
+
function formatDaemonStatusLine(options = {}) {
|
|
4099
|
+
const nowDate = options.now instanceof Date ? options.now : typeof options.now === "number" ? new Date(options.now) : /* @__PURE__ */ new Date();
|
|
4100
|
+
const nowMs = nowDate.getTime();
|
|
4101
|
+
const timeString = nowDate.toLocaleTimeString();
|
|
4102
|
+
const columns = options.columns !== void 0 ? options.columns : process.stderr.columns || 80;
|
|
4103
|
+
const maxCols = Math.max(20, (columns || 80) - 2);
|
|
4104
|
+
const includeTip = columns >= 55;
|
|
4105
|
+
let core;
|
|
4106
|
+
if (options.isPaused) {
|
|
4107
|
+
const queueStr = options.pendingRoutine ? pc12.cyan(` [Queued: ${options.pendingRoutine}]`) : "";
|
|
4108
|
+
core = `${pc12.dim("[" + timeString + "]")} \u23F8\uFE0F ${pc12.yellow("PAUSED")}${queueStr}`;
|
|
4109
|
+
} else {
|
|
4110
|
+
const nextCheck = options.nextCheckTime !== void 0 ? options.nextCheckTime : nowMs;
|
|
4111
|
+
const diffMs = Math.max(0, nextCheck - nowMs);
|
|
4112
|
+
const remainingSecs = Math.ceil(diffMs / 1e3);
|
|
4113
|
+
const mins = Math.floor(remainingSecs / 60);
|
|
4114
|
+
const secs = remainingSecs % 60;
|
|
4115
|
+
const timeStr = `${mins}m ${secs < 10 ? "0" : ""}${secs}s`;
|
|
4116
|
+
const prStr = options.lastOpenPRCount !== void 0 ? ` (${options.lastOpenPRCount} ready PRs)` : "";
|
|
4117
|
+
const queueStr = options.pendingRoutine ? pc12.cyan(` [Queued: ${options.pendingRoutine}]`) : "";
|
|
4118
|
+
core = `${pc12.dim("[" + timeString + "]")} \u{1F4A4} ${pc12.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}${queueStr}`;
|
|
4119
|
+
}
|
|
4120
|
+
if (!includeTip) {
|
|
4121
|
+
return truncateAnsi(core, maxCols);
|
|
4122
|
+
}
|
|
4123
|
+
const tipsList = options.tips || (options.isPaused ? DAEMON_PAUSED_TIPS : DAEMON_STATUS_TIPS);
|
|
4124
|
+
const tipIdx = options.tipIndex !== void 0 ? options.tipIndex : getRotatingTipIndex(nowMs, 4, tipsList.length);
|
|
4125
|
+
const tipText = tipsList[(tipIdx % tipsList.length + tipsList.length) % tipsList.length] || "";
|
|
4126
|
+
const fullLine = `${core} ${pc12.dim("\xB7")} ${pc12.dim(tipText)}`;
|
|
4127
|
+
return truncateAnsi(fullLine, maxCols);
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
// src/lib/daemon.ts
|
|
4131
|
+
import pc13 from "picocolors";
|
|
2937
4132
|
var execFileAsync3 = promisify3(execFile3);
|
|
2938
4133
|
function getDaemonStatePath(repoRoot) {
|
|
2939
|
-
return
|
|
4134
|
+
return path14.join(repoRoot, ".jonah-fleet", "daemon.json");
|
|
2940
4135
|
}
|
|
2941
4136
|
function readDaemonState(repoRoot) {
|
|
2942
4137
|
const statePath = getDaemonStatePath(repoRoot);
|
|
2943
|
-
if (!
|
|
4138
|
+
if (!fs14.existsSync(statePath)) return null;
|
|
2944
4139
|
try {
|
|
2945
|
-
return JSON.parse(
|
|
4140
|
+
return JSON.parse(fs14.readFileSync(statePath, "utf8"));
|
|
2946
4141
|
} catch {
|
|
2947
4142
|
return null;
|
|
2948
4143
|
}
|
|
2949
4144
|
}
|
|
2950
4145
|
function writeDaemonState(repoRoot, state) {
|
|
2951
4146
|
const statePath = getDaemonStatePath(repoRoot);
|
|
2952
|
-
|
|
2953
|
-
|
|
4147
|
+
fs14.mkdirSync(path14.dirname(statePath), { recursive: true });
|
|
4148
|
+
fs14.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
2954
4149
|
}
|
|
2955
4150
|
function clearDaemonState(repoRoot) {
|
|
2956
4151
|
const statePath = getDaemonStatePath(repoRoot);
|
|
2957
|
-
if (
|
|
4152
|
+
if (fs14.existsSync(statePath)) {
|
|
2958
4153
|
try {
|
|
2959
|
-
|
|
4154
|
+
fs14.unlinkSync(statePath);
|
|
2960
4155
|
} catch {
|
|
2961
4156
|
}
|
|
2962
4157
|
}
|
|
@@ -2972,16 +4167,22 @@ function isDaemonRunning(repoRoot) {
|
|
|
2972
4167
|
return false;
|
|
2973
4168
|
}
|
|
2974
4169
|
}
|
|
2975
|
-
|
|
4170
|
+
function filterReviewablePRs(prs) {
|
|
4171
|
+
return (prs || []).filter(
|
|
4172
|
+
(pr) => pr && typeof pr.number === "number" && !pr.headRefName?.startsWith("release-please--") && !pr.title?.startsWith("chore(main): release")
|
|
4173
|
+
);
|
|
4174
|
+
}
|
|
4175
|
+
async function getOpenReviewablePRs(repoRoot) {
|
|
2976
4176
|
try {
|
|
2977
4177
|
const { stdout } = await execFileAsync3(
|
|
2978
4178
|
"gh",
|
|
2979
|
-
["pr", "list", "--state", "open", "--draft=false", "--json", "number
|
|
4179
|
+
["pr", "list", "--state", "open", "--draft=false", "--json", "number,headRefName,title"],
|
|
2980
4180
|
{ cwd: repoRoot }
|
|
2981
4181
|
);
|
|
2982
|
-
|
|
4182
|
+
const prs = JSON.parse(stdout);
|
|
4183
|
+
return filterReviewablePRs(prs);
|
|
2983
4184
|
} catch {
|
|
2984
|
-
return
|
|
4185
|
+
return [];
|
|
2985
4186
|
}
|
|
2986
4187
|
}
|
|
2987
4188
|
async function startBackgroundDaemon(repoRoot, options = {}) {
|
|
@@ -2992,9 +4193,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
|
|
|
2992
4193
|
const reviewInterval = options.reviewInterval || 3;
|
|
2993
4194
|
const autoworkInterval = options.autoworkInterval || options.interval || 30;
|
|
2994
4195
|
const routines = options.routines || ["peer-review", "autowork"];
|
|
2995
|
-
const logFilePath =
|
|
2996
|
-
|
|
2997
|
-
const logFd =
|
|
4196
|
+
const logFilePath = path14.join(repoRoot, ".jonah-fleet", "daemon.log");
|
|
4197
|
+
fs14.mkdirSync(path14.dirname(logFilePath), { recursive: true });
|
|
4198
|
+
const logFd = fs14.openSync(logFilePath, "a");
|
|
2998
4199
|
const cliPath = process.argv[1];
|
|
2999
4200
|
const args = [
|
|
3000
4201
|
"daemon",
|
|
@@ -3043,6 +4244,103 @@ async function stopDaemon(repoRoot) {
|
|
|
3043
4244
|
return false;
|
|
3044
4245
|
}
|
|
3045
4246
|
}
|
|
4247
|
+
async function drainReviewQueue(drainOptions) {
|
|
4248
|
+
const {
|
|
4249
|
+
repoRoot,
|
|
4250
|
+
state,
|
|
4251
|
+
options = {},
|
|
4252
|
+
isStopping = () => false,
|
|
4253
|
+
clearTicker,
|
|
4254
|
+
getPRs = getOpenReviewablePRs,
|
|
4255
|
+
runRoutine = runLocalRoutine,
|
|
4256
|
+
onAttempted
|
|
4257
|
+
} = drainOptions;
|
|
4258
|
+
if (isStopping()) return;
|
|
4259
|
+
if (state) {
|
|
4260
|
+
state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4261
|
+
writeDaemonState(repoRoot, state);
|
|
4262
|
+
}
|
|
4263
|
+
let reviewablePRs = await getPRs(repoRoot);
|
|
4264
|
+
if (reviewablePRs.length === 0) {
|
|
4265
|
+
if (options.verbose) {
|
|
4266
|
+
console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
|
|
4267
|
+
}
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
const attemptedPRNumbers = /* @__PURE__ */ new Set();
|
|
4271
|
+
while (!isStopping() && reviewablePRs.length > 0) {
|
|
4272
|
+
const candidatePRs = reviewablePRs.filter((pr) => !attemptedPRNumbers.has(pr.number));
|
|
4273
|
+
if (candidatePRs.length === 0) {
|
|
4274
|
+
if (options.verbose) {
|
|
4275
|
+
console.log(
|
|
4276
|
+
pc13.dim(
|
|
4277
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] All ${reviewablePRs.length} remaining ready PR(s) were already evaluated in this drain pass.`
|
|
4278
|
+
)
|
|
4279
|
+
);
|
|
4280
|
+
}
|
|
4281
|
+
break;
|
|
4282
|
+
}
|
|
4283
|
+
const totalRemaining = candidatePRs.length;
|
|
4284
|
+
let targetPRStr = void 0;
|
|
4285
|
+
try {
|
|
4286
|
+
if (clearTicker) clearTicker();
|
|
4287
|
+
if (state) {
|
|
4288
|
+
state.status = "working";
|
|
4289
|
+
state.activeRoutine = "peer-review";
|
|
4290
|
+
writeDaemonState(repoRoot, state);
|
|
4291
|
+
}
|
|
4292
|
+
console.log(
|
|
4293
|
+
pc13.cyan(
|
|
4294
|
+
`
|
|
4295
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Draining PR backlog (${totalRemaining} PR(s) remaining). Starting review session...`
|
|
4296
|
+
)
|
|
4297
|
+
);
|
|
4298
|
+
await cleanupStaleWorktrees(repoRoot);
|
|
4299
|
+
const result = await runRoutine({
|
|
4300
|
+
targetDir: repoRoot,
|
|
4301
|
+
routine: "peer-review",
|
|
4302
|
+
model: options.model,
|
|
4303
|
+
verbose: options.verbose,
|
|
4304
|
+
noWorktree: false,
|
|
4305
|
+
onTargetDetected: (target) => {
|
|
4306
|
+
targetPRStr = target;
|
|
4307
|
+
if (state) {
|
|
4308
|
+
state.activeTarget = target;
|
|
4309
|
+
writeDaemonState(repoRoot, state);
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
});
|
|
4313
|
+
const activeTargetStr = targetPRStr;
|
|
4314
|
+
const match = activeTargetStr?.match(/PR\s*#?([0-9]+)/i);
|
|
4315
|
+
const prNum = match ? parseInt(match[1], 10) : candidatePRs[0]?.number;
|
|
4316
|
+
if (typeof prNum === "number") {
|
|
4317
|
+
attemptedPRNumbers.add(prNum);
|
|
4318
|
+
onAttempted?.(prNum);
|
|
4319
|
+
}
|
|
4320
|
+
if (result.success) {
|
|
4321
|
+
console.log(pc13.green(`\u2713 Local peer-review completed successfully.
|
|
4322
|
+
`));
|
|
4323
|
+
} else {
|
|
4324
|
+
console.warn(pc13.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
|
|
4325
|
+
`));
|
|
4326
|
+
}
|
|
4327
|
+
} catch (err) {
|
|
4328
|
+
console.error(pc13.red(`\u2717 Error in peer-review: ${err.message}`));
|
|
4329
|
+
if (candidatePRs[0]) {
|
|
4330
|
+
attemptedPRNumbers.add(candidatePRs[0].number);
|
|
4331
|
+
onAttempted?.(candidatePRs[0].number);
|
|
4332
|
+
}
|
|
4333
|
+
} finally {
|
|
4334
|
+
if (state) {
|
|
4335
|
+
state.status = "idle";
|
|
4336
|
+
state.activeRoutine = void 0;
|
|
4337
|
+
state.activeTarget = void 0;
|
|
4338
|
+
writeDaemonState(repoRoot, state);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
reviewablePRs = await getPRs(repoRoot);
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
3046
4344
|
async function runDaemonLoop(repoRoot, options = {}) {
|
|
3047
4345
|
const reviewInterval = options.reviewInterval || 3;
|
|
3048
4346
|
const autoworkInterval = options.autoworkInterval || options.interval || 30;
|
|
@@ -3056,20 +4354,29 @@ async function runDaemonLoop(repoRoot, options = {}) {
|
|
|
3056
4354
|
status: "idle"
|
|
3057
4355
|
};
|
|
3058
4356
|
writeDaemonState(repoRoot, state);
|
|
3059
|
-
console.log(
|
|
4357
|
+
console.log(pc13.cyan(`
|
|
3060
4358
|
\u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
|
|
3061
|
-
console.log(
|
|
3062
|
-
console.log(
|
|
3063
|
-
console.log(
|
|
3064
|
-
console.log(
|
|
4359
|
+
console.log(pc13.dim(` PID: ${process.pid}`));
|
|
4360
|
+
console.log(pc13.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
|
|
4361
|
+
console.log(pc13.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
|
|
4362
|
+
console.log(pc13.dim(` Working Directory: ${repoRoot}`));
|
|
4363
|
+
console.log(pc13.dim(` Interactive Hotkeys: 'r' (review), 'a' (autowork), 'p' (pause), 's' (status), 'q' (stop), '?' (help)
|
|
3065
4364
|
`));
|
|
3066
4365
|
let isStopping = false;
|
|
4366
|
+
let isGracefulStopping = false;
|
|
3067
4367
|
let isWorking = false;
|
|
4368
|
+
let isPaused = false;
|
|
4369
|
+
let pendingRoutine = null;
|
|
4370
|
+
let keyboard;
|
|
4371
|
+
let tickerInterval;
|
|
4372
|
+
let stopResolve;
|
|
3068
4373
|
const reviewIntervalMs = reviewInterval * 60 * 1e3;
|
|
3069
4374
|
const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
|
|
3070
4375
|
let nextReviewCheckTime = Date.now() + (routines.includes("peer-review") ? reviewIntervalMs : Infinity);
|
|
3071
4376
|
let nextAutoworkCheckTime = Date.now() + (routines.includes("autowork") ? autoworkIntervalMs : Infinity);
|
|
3072
4377
|
let lastOpenPRCount = void 0;
|
|
4378
|
+
const getPRsFn = options.getPRs || getOpenReviewablePRs;
|
|
4379
|
+
const runRoutineFn = options.runRoutine || runLocalRoutine;
|
|
3073
4380
|
const clearTicker = () => {
|
|
3074
4381
|
if (process.stderr.isTTY && !options.verbose) {
|
|
3075
4382
|
process.stderr.write("\r\x1B[K");
|
|
@@ -3077,60 +4384,188 @@ async function runDaemonLoop(repoRoot, options = {}) {
|
|
|
3077
4384
|
};
|
|
3078
4385
|
const updateTicker = () => {
|
|
3079
4386
|
if (isStopping || isWorking || options.verbose || !process.stderr.isTTY) return;
|
|
3080
|
-
const
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
process.stderr.write(
|
|
3089
|
-
`\r\x1B[K${pc11.dim("[" + (/* @__PURE__ */ new Date()).toLocaleTimeString() + "]")} \u{1F4A4} ${pc11.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}`
|
|
3090
|
-
);
|
|
4387
|
+
const line = formatDaemonStatusLine({
|
|
4388
|
+
now: Date.now(),
|
|
4389
|
+
isPaused,
|
|
4390
|
+
nextCheckTime: Math.min(nextReviewCheckTime, nextAutoworkCheckTime),
|
|
4391
|
+
lastOpenPRCount,
|
|
4392
|
+
pendingRoutine,
|
|
4393
|
+
columns: process.stderr.columns
|
|
4394
|
+
});
|
|
4395
|
+
process.stderr.write(`\r\x1B[K${line}`);
|
|
3091
4396
|
};
|
|
3092
|
-
const tickerInterval = setInterval(updateTicker, 1e3);
|
|
3093
4397
|
const handleStop = async () => {
|
|
3094
4398
|
if (isStopping) return;
|
|
3095
4399
|
isStopping = true;
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
4400
|
+
process.removeListener("SIGINT", handleStop);
|
|
4401
|
+
process.removeListener("SIGTERM", handleStop);
|
|
4402
|
+
keyboard?.stop();
|
|
4403
|
+
if (tickerInterval) {
|
|
4404
|
+
clearInterval(tickerInterval);
|
|
4405
|
+
tickerInterval = void 0;
|
|
4406
|
+
}
|
|
3099
4407
|
clearTicker();
|
|
3100
|
-
console.log(
|
|
4408
|
+
console.log(pc13.yellow(`
|
|
3101
4409
|
Stopping local agent daemon...`));
|
|
3102
4410
|
clearDaemonState(repoRoot);
|
|
3103
4411
|
await cleanupStaleWorktrees(repoRoot);
|
|
4412
|
+
stopResolve?.();
|
|
3104
4413
|
process.exit(0);
|
|
3105
4414
|
};
|
|
3106
4415
|
process.once("SIGINT", handleStop);
|
|
3107
4416
|
process.once("SIGTERM", handleStop);
|
|
3108
|
-
const
|
|
4417
|
+
const performReviewDrain = async () => {
|
|
3109
4418
|
if (isStopping || isWorking) return;
|
|
3110
|
-
|
|
4419
|
+
try {
|
|
4420
|
+
isWorking = true;
|
|
4421
|
+
await drainReviewQueue({
|
|
4422
|
+
repoRoot,
|
|
4423
|
+
state,
|
|
4424
|
+
options,
|
|
4425
|
+
isStopping: () => isStopping,
|
|
4426
|
+
clearTicker,
|
|
4427
|
+
getPRs: getPRsFn,
|
|
4428
|
+
runRoutine: runRoutineFn
|
|
4429
|
+
});
|
|
4430
|
+
const prs = await getPRsFn(repoRoot);
|
|
4431
|
+
lastOpenPRCount = prs.length;
|
|
4432
|
+
} finally {
|
|
4433
|
+
isWorking = false;
|
|
4434
|
+
state.status = isPaused ? "paused" : "idle";
|
|
4435
|
+
state.activeRoutine = void 0;
|
|
4436
|
+
state.activeTarget = void 0;
|
|
4437
|
+
writeDaemonState(repoRoot, state);
|
|
4438
|
+
nextReviewCheckTime = Date.now() + reviewIntervalMs;
|
|
4439
|
+
updateTicker();
|
|
4440
|
+
if (isGracefulStopping) {
|
|
4441
|
+
await handleStop();
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
if (pendingRoutine && !isStopping) {
|
|
4445
|
+
const next = pendingRoutine;
|
|
4446
|
+
pendingRoutine = null;
|
|
4447
|
+
console.log(pc13.cyan(`
|
|
4448
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
|
|
4449
|
+
if (next === "peer-review") {
|
|
4450
|
+
await performReviewDrain();
|
|
4451
|
+
} else if (next === "autowork") {
|
|
4452
|
+
await runAutoworkCheck();
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
};
|
|
4457
|
+
const runAutoworkCheck = async () => {
|
|
4458
|
+
if (isStopping || isWorking || !routines.includes("autowork")) return;
|
|
4459
|
+
if (routines.includes("peer-review")) {
|
|
4460
|
+
const pendingPRs = (await getPRsFn(repoRoot)).length;
|
|
4461
|
+
if (pendingPRs > 0) {
|
|
4462
|
+
console.log(
|
|
4463
|
+
pc13.cyan(
|
|
4464
|
+
`
|
|
4465
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
|
|
4466
|
+
)
|
|
4467
|
+
);
|
|
4468
|
+
await performReviewDrain();
|
|
4469
|
+
const remainingPRs = (await getPRsFn(repoRoot)).length;
|
|
4470
|
+
if (remainingPRs > 0) {
|
|
4471
|
+
console.log(
|
|
4472
|
+
pc13.yellow(
|
|
4473
|
+
`
|
|
4474
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Review backlog still has ${remainingPRs} pending PR(s). Postponing autowork session.`
|
|
4475
|
+
)
|
|
4476
|
+
);
|
|
4477
|
+
nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
|
|
4478
|
+
updateTicker();
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3111
4484
|
writeDaemonState(repoRoot, state);
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
4485
|
+
try {
|
|
4486
|
+
isWorking = true;
|
|
4487
|
+
clearTicker();
|
|
4488
|
+
state.status = "working";
|
|
4489
|
+
state.activeRoutine = "autowork";
|
|
4490
|
+
writeDaemonState(repoRoot, state);
|
|
4491
|
+
console.log(pc13.cyan(`
|
|
4492
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
|
|
4493
|
+
await cleanupStaleWorktrees(repoRoot);
|
|
4494
|
+
const result = await runRoutineFn({
|
|
4495
|
+
targetDir: repoRoot,
|
|
4496
|
+
routine: "autowork",
|
|
4497
|
+
model: options.model,
|
|
4498
|
+
verbose: options.verbose,
|
|
4499
|
+
noWorktree: false,
|
|
4500
|
+
onTargetDetected: (target) => {
|
|
4501
|
+
state.activeTarget = target;
|
|
4502
|
+
writeDaemonState(repoRoot, state);
|
|
4503
|
+
}
|
|
4504
|
+
});
|
|
4505
|
+
if (result.success) {
|
|
4506
|
+
console.log(pc13.green(`\u2713 Local autowork completed successfully.
|
|
4507
|
+
`));
|
|
4508
|
+
} else {
|
|
4509
|
+
console.warn(pc13.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
|
|
4510
|
+
`));
|
|
3117
4511
|
}
|
|
3118
|
-
|
|
4512
|
+
} catch (err) {
|
|
4513
|
+
console.error(pc13.red(`\u2717 Error in autowork: ${err.message}`));
|
|
4514
|
+
} finally {
|
|
4515
|
+
isWorking = false;
|
|
4516
|
+
state.status = isPaused ? "paused" : "idle";
|
|
4517
|
+
state.activeRoutine = void 0;
|
|
4518
|
+
state.activeTarget = void 0;
|
|
4519
|
+
writeDaemonState(repoRoot, state);
|
|
4520
|
+
nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
|
|
3119
4521
|
updateTicker();
|
|
3120
|
-
|
|
4522
|
+
if (!isStopping && routines.includes("peer-review")) {
|
|
4523
|
+
const newPRCount = (await getPRsFn(repoRoot)).length;
|
|
4524
|
+
if (newPRCount > 0) {
|
|
4525
|
+
console.log(
|
|
4526
|
+
pc13.cyan(
|
|
4527
|
+
`
|
|
4528
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
|
|
4529
|
+
)
|
|
4530
|
+
);
|
|
4531
|
+
await performReviewDrain();
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
if (isGracefulStopping) {
|
|
4535
|
+
await handleStop();
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4538
|
+
if (pendingRoutine && !isStopping) {
|
|
4539
|
+
const next = pendingRoutine;
|
|
4540
|
+
pendingRoutine = null;
|
|
4541
|
+
console.log(pc13.cyan(`
|
|
4542
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
|
|
4543
|
+
if (next === "peer-review") {
|
|
4544
|
+
await performReviewDrain();
|
|
4545
|
+
} else if (next === "autowork") {
|
|
4546
|
+
await runAutoworkCheck();
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
3121
4549
|
}
|
|
4550
|
+
};
|
|
4551
|
+
const runTargetedReview = async (prNumber) => {
|
|
4552
|
+
if (isStopping || isWorking) return;
|
|
3122
4553
|
try {
|
|
3123
4554
|
isWorking = true;
|
|
3124
4555
|
clearTicker();
|
|
3125
4556
|
state.status = "working";
|
|
3126
4557
|
state.activeRoutine = "peer-review";
|
|
4558
|
+
state.activeTarget = `PR #${prNumber}`;
|
|
3127
4559
|
writeDaemonState(repoRoot, state);
|
|
3128
|
-
console.log(
|
|
3129
|
-
|
|
4560
|
+
console.log(
|
|
4561
|
+
pc13.cyan(`
|
|
4562
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Peer Review: Starting session on PR #${prNumber}...`)
|
|
4563
|
+
);
|
|
3130
4564
|
await cleanupStaleWorktrees(repoRoot);
|
|
3131
|
-
const result = await
|
|
4565
|
+
const result = await runRoutineFn({
|
|
3132
4566
|
targetDir: repoRoot,
|
|
3133
4567
|
routine: "peer-review",
|
|
4568
|
+
pr: prNumber,
|
|
3134
4569
|
model: options.model,
|
|
3135
4570
|
verbose: options.verbose,
|
|
3136
4571
|
noWorktree: false,
|
|
@@ -3140,40 +4575,56 @@ Stopping local agent daemon...`));
|
|
|
3140
4575
|
}
|
|
3141
4576
|
});
|
|
3142
4577
|
if (result.success) {
|
|
3143
|
-
console.log(
|
|
4578
|
+
console.log(pc13.green(`\u2713 Targeted peer-review on PR #${prNumber} completed successfully.
|
|
3144
4579
|
`));
|
|
3145
4580
|
} else {
|
|
3146
|
-
console.warn(
|
|
4581
|
+
console.warn(pc13.yellow(`\u26A0\uFE0F Targeted peer-review on PR #${prNumber} completed with code ${result.exitCode}.
|
|
3147
4582
|
`));
|
|
3148
4583
|
}
|
|
3149
4584
|
} catch (err) {
|
|
3150
|
-
console.error(
|
|
4585
|
+
console.error(pc13.red(`\u2717 Error in targeted peer-review: ${err.message}`));
|
|
3151
4586
|
} finally {
|
|
3152
4587
|
isWorking = false;
|
|
3153
|
-
state.status = "idle";
|
|
4588
|
+
state.status = isPaused ? "paused" : "idle";
|
|
3154
4589
|
state.activeRoutine = void 0;
|
|
3155
4590
|
state.activeTarget = void 0;
|
|
3156
4591
|
writeDaemonState(repoRoot, state);
|
|
3157
|
-
nextReviewCheckTime = Date.now() + reviewIntervalMs;
|
|
3158
4592
|
updateTicker();
|
|
4593
|
+
if (isGracefulStopping) {
|
|
4594
|
+
await handleStop();
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
if (pendingRoutine && !isStopping) {
|
|
4598
|
+
const next = pendingRoutine;
|
|
4599
|
+
pendingRoutine = null;
|
|
4600
|
+
console.log(pc13.cyan(`
|
|
4601
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
|
|
4602
|
+
if (next === "peer-review") {
|
|
4603
|
+
await performReviewDrain();
|
|
4604
|
+
} else if (next === "autowork") {
|
|
4605
|
+
await runAutoworkCheck();
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
3159
4608
|
}
|
|
3160
4609
|
};
|
|
3161
|
-
const
|
|
3162
|
-
if (isStopping || isWorking
|
|
3163
|
-
state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3164
|
-
writeDaemonState(repoRoot, state);
|
|
4610
|
+
const runTargetedAutowork = async (issueNumber) => {
|
|
4611
|
+
if (isStopping || isWorking) return;
|
|
3165
4612
|
try {
|
|
3166
4613
|
isWorking = true;
|
|
3167
4614
|
clearTicker();
|
|
3168
4615
|
state.status = "working";
|
|
3169
4616
|
state.activeRoutine = "autowork";
|
|
4617
|
+
state.activeTarget = `Issue #${issueNumber}`;
|
|
3170
4618
|
writeDaemonState(repoRoot, state);
|
|
3171
|
-
console.log(
|
|
3172
|
-
|
|
4619
|
+
console.log(
|
|
4620
|
+
pc13.cyan(`
|
|
4621
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F3AF} Targeted Autowork: Starting session on Issue #${issueNumber}...`)
|
|
4622
|
+
);
|
|
3173
4623
|
await cleanupStaleWorktrees(repoRoot);
|
|
3174
|
-
const result = await
|
|
4624
|
+
const result = await runRoutineFn({
|
|
3175
4625
|
targetDir: repoRoot,
|
|
3176
4626
|
routine: "autowork",
|
|
4627
|
+
issue: issueNumber,
|
|
3177
4628
|
model: options.model,
|
|
3178
4629
|
verbose: options.verbose,
|
|
3179
4630
|
noWorktree: false,
|
|
@@ -3183,33 +4634,278 @@ Stopping local agent daemon...`));
|
|
|
3183
4634
|
}
|
|
3184
4635
|
});
|
|
3185
4636
|
if (result.success) {
|
|
3186
|
-
console.log(
|
|
4637
|
+
console.log(pc13.green(`\u2713 Targeted autowork on Issue #${issueNumber} completed successfully.
|
|
3187
4638
|
`));
|
|
3188
4639
|
} else {
|
|
3189
|
-
console.warn(
|
|
4640
|
+
console.warn(pc13.yellow(`\u26A0\uFE0F Targeted autowork on Issue #${issueNumber} completed with code ${result.exitCode}.
|
|
3190
4641
|
`));
|
|
3191
4642
|
}
|
|
3192
4643
|
} catch (err) {
|
|
3193
|
-
console.error(
|
|
4644
|
+
console.error(pc13.red(`\u2717 Error in targeted autowork: ${err.message}`));
|
|
3194
4645
|
} finally {
|
|
3195
4646
|
isWorking = false;
|
|
3196
|
-
state.status = "idle";
|
|
4647
|
+
state.status = isPaused ? "paused" : "idle";
|
|
3197
4648
|
state.activeRoutine = void 0;
|
|
3198
4649
|
state.activeTarget = void 0;
|
|
3199
4650
|
writeDaemonState(repoRoot, state);
|
|
3200
|
-
nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
|
|
3201
4651
|
updateTicker();
|
|
4652
|
+
if (!isStopping && routines.includes("peer-review")) {
|
|
4653
|
+
const newPRCount = (await getPRsFn(repoRoot)).length;
|
|
4654
|
+
if (newPRCount > 0) {
|
|
4655
|
+
console.log(
|
|
4656
|
+
pc13.cyan(
|
|
4657
|
+
`
|
|
4658
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
|
|
4659
|
+
)
|
|
4660
|
+
);
|
|
4661
|
+
await performReviewDrain();
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
if (isGracefulStopping) {
|
|
4665
|
+
await handleStop();
|
|
4666
|
+
return;
|
|
4667
|
+
}
|
|
4668
|
+
if (pendingRoutine && !isStopping) {
|
|
4669
|
+
const next = pendingRoutine;
|
|
4670
|
+
pendingRoutine = null;
|
|
4671
|
+
console.log(pc13.cyan(`
|
|
4672
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Executing queued routine: ${next}...`));
|
|
4673
|
+
if (next === "peer-review") {
|
|
4674
|
+
await performReviewDrain();
|
|
4675
|
+
} else if (next === "autowork") {
|
|
4676
|
+
await runAutoworkCheck();
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
3202
4679
|
}
|
|
3203
4680
|
};
|
|
4681
|
+
keyboard = new KeyboardController({
|
|
4682
|
+
stdin: options.stdin || process.stdin,
|
|
4683
|
+
onReview: async () => {
|
|
4684
|
+
if (isStopping || isGracefulStopping) return;
|
|
4685
|
+
if (isWorking) {
|
|
4686
|
+
pendingRoutine = "peer-review";
|
|
4687
|
+
clearTicker();
|
|
4688
|
+
console.log(
|
|
4689
|
+
pc13.cyan(`
|
|
4690
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Peer Review scan queued (will run after current routine finishes).`)
|
|
4691
|
+
);
|
|
4692
|
+
updateTicker();
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
clearTicker();
|
|
4696
|
+
console.log(pc13.cyan(`
|
|
4697
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Triggering immediate Peer Review scan on demand...`));
|
|
4698
|
+
await performReviewDrain();
|
|
4699
|
+
},
|
|
4700
|
+
onAutowork: async () => {
|
|
4701
|
+
if (isStopping || isGracefulStopping) return;
|
|
4702
|
+
if (isWorking) {
|
|
4703
|
+
pendingRoutine = "autowork";
|
|
4704
|
+
clearTicker();
|
|
4705
|
+
console.log(
|
|
4706
|
+
pc13.cyan(`
|
|
4707
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork backlog scan queued (will run after current routine finishes).`)
|
|
4708
|
+
);
|
|
4709
|
+
updateTicker();
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
clearTicker();
|
|
4713
|
+
console.log(pc13.cyan(`
|
|
4714
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A1 Triggering immediate Autowork scan on demand...`));
|
|
4715
|
+
await runAutoworkCheck();
|
|
4716
|
+
},
|
|
4717
|
+
onTargetedReview: async () => {
|
|
4718
|
+
if (isStopping || isGracefulStopping) return;
|
|
4719
|
+
if (isWorking) {
|
|
4720
|
+
clearTicker();
|
|
4721
|
+
console.log(
|
|
4722
|
+
pc13.yellow(
|
|
4723
|
+
`
|
|
4724
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Targeted review prompts require idle state. Use 'r' to queue a scan pass instead.`
|
|
4725
|
+
)
|
|
4726
|
+
);
|
|
4727
|
+
updateTicker();
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
clearTicker();
|
|
4731
|
+
keyboard?.pause();
|
|
4732
|
+
const rawInput = await promptTargetedInput(`
|
|
4733
|
+
${pc13.cyan("Enter PR # to review (Esc/Enter to cancel):")} `, {
|
|
4734
|
+
stdin: options.stdin || process.stdin,
|
|
4735
|
+
stdout: process.stdout
|
|
4736
|
+
});
|
|
4737
|
+
keyboard?.resume();
|
|
4738
|
+
if (!rawInput) {
|
|
4739
|
+
console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted review cancelled.
|
|
4740
|
+
`));
|
|
4741
|
+
updateTicker();
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
const prNumber = parseNumericTarget(rawInput);
|
|
4745
|
+
if (!prNumber) {
|
|
4746
|
+
console.log(
|
|
4747
|
+
pc13.yellow(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Invalid PR number '${rawInput}'. Operation cancelled.
|
|
4748
|
+
`)
|
|
4749
|
+
);
|
|
4750
|
+
updateTicker();
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
await runTargetedReview(prNumber);
|
|
4754
|
+
},
|
|
4755
|
+
onTargetedAutowork: async () => {
|
|
4756
|
+
if (isStopping || isGracefulStopping) return;
|
|
4757
|
+
if (isWorking) {
|
|
4758
|
+
clearTicker();
|
|
4759
|
+
console.log(
|
|
4760
|
+
pc13.yellow(
|
|
4761
|
+
`
|
|
4762
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Targeted autowork prompts require idle state. Use 'a' to queue a scan pass instead.`
|
|
4763
|
+
)
|
|
4764
|
+
);
|
|
4765
|
+
updateTicker();
|
|
4766
|
+
return;
|
|
4767
|
+
}
|
|
4768
|
+
clearTicker();
|
|
4769
|
+
keyboard?.pause();
|
|
4770
|
+
const rawInput = await promptTargetedInput(`
|
|
4771
|
+
${pc13.cyan("Enter Issue # to work (Esc/Enter to cancel):")} `, {
|
|
4772
|
+
stdin: options.stdin || process.stdin,
|
|
4773
|
+
stdout: process.stdout
|
|
4774
|
+
});
|
|
4775
|
+
keyboard?.resume();
|
|
4776
|
+
if (!rawInput) {
|
|
4777
|
+
console.log(pc13.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Targeted autowork cancelled.
|
|
4778
|
+
`));
|
|
4779
|
+
updateTicker();
|
|
4780
|
+
return;
|
|
4781
|
+
}
|
|
4782
|
+
const issueNumber = parseNumericTarget(rawInput);
|
|
4783
|
+
if (!issueNumber) {
|
|
4784
|
+
console.log(
|
|
4785
|
+
pc13.yellow(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Invalid Issue number '${rawInput}'. Operation cancelled.
|
|
4786
|
+
`)
|
|
4787
|
+
);
|
|
4788
|
+
updateTicker();
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
await runTargetedAutowork(issueNumber);
|
|
4792
|
+
},
|
|
4793
|
+
onToggleVerbose: () => {
|
|
4794
|
+
if (isStopping || isGracefulStopping) return;
|
|
4795
|
+
options.verbose = !options.verbose;
|
|
4796
|
+
clearTicker();
|
|
4797
|
+
if (options.verbose) {
|
|
4798
|
+
console.log(
|
|
4799
|
+
pc13.green(`
|
|
4800
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50A} Verbose mode ENABLED (streaming tokens directly to terminal).`)
|
|
4801
|
+
);
|
|
4802
|
+
} else {
|
|
4803
|
+
console.log(
|
|
4804
|
+
pc13.yellow(`
|
|
4805
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F507} Verbose mode DISABLED (compact terminal spinner active).`)
|
|
4806
|
+
);
|
|
4807
|
+
}
|
|
4808
|
+
updateTicker();
|
|
4809
|
+
},
|
|
4810
|
+
onTailLog: () => {
|
|
4811
|
+
if (isStopping || isGracefulStopping) return;
|
|
4812
|
+
clearTicker();
|
|
4813
|
+
printDaemonLogTail(repoRoot, 20);
|
|
4814
|
+
updateTicker();
|
|
4815
|
+
},
|
|
4816
|
+
onCleanWorktrees: async () => {
|
|
4817
|
+
if (isStopping || isGracefulStopping) return;
|
|
4818
|
+
clearTicker();
|
|
4819
|
+
const result = await inspectAndCleanWorktrees(repoRoot);
|
|
4820
|
+
printWorktreesInspection(result);
|
|
4821
|
+
updateTicker();
|
|
4822
|
+
},
|
|
4823
|
+
onPauseToggle: () => {
|
|
4824
|
+
if (isStopping || isGracefulStopping) return;
|
|
4825
|
+
isPaused = !isPaused;
|
|
4826
|
+
clearTicker();
|
|
4827
|
+
if (isPaused) {
|
|
4828
|
+
if (state.status !== "working") state.status = "paused";
|
|
4829
|
+
writeDaemonState(repoRoot, state);
|
|
4830
|
+
console.log(
|
|
4831
|
+
pc13.yellow(
|
|
4832
|
+
`
|
|
4833
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F8\uFE0F Daemon polling paused. Automatic interval sweeps suspended. (Press 'p' to resume)`
|
|
4834
|
+
)
|
|
4835
|
+
);
|
|
4836
|
+
} else {
|
|
4837
|
+
if (state.status !== "working") state.status = "idle";
|
|
4838
|
+
writeDaemonState(repoRoot, state);
|
|
4839
|
+
console.log(
|
|
4840
|
+
pc13.green(`
|
|
4841
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u25B6\uFE0F Daemon polling resumed. Automated interval sweeps active.`)
|
|
4842
|
+
);
|
|
4843
|
+
}
|
|
4844
|
+
updateTicker();
|
|
4845
|
+
},
|
|
4846
|
+
onStatus: async () => {
|
|
4847
|
+
clearTicker();
|
|
4848
|
+
const activeWorktrees = await listActiveWorktrees(repoRoot);
|
|
4849
|
+
printDaemonStatusSummary({
|
|
4850
|
+
repoRoot,
|
|
4851
|
+
state,
|
|
4852
|
+
pendingRoutine,
|
|
4853
|
+
activeWorktrees,
|
|
4854
|
+
verbose: options.verbose
|
|
4855
|
+
});
|
|
4856
|
+
updateTicker();
|
|
4857
|
+
},
|
|
4858
|
+
onGracefulStop: async () => {
|
|
4859
|
+
if (isStopping) return;
|
|
4860
|
+
pendingRoutine = null;
|
|
4861
|
+
if (isWorking) {
|
|
4862
|
+
isGracefulStopping = true;
|
|
4863
|
+
clearTicker();
|
|
4864
|
+
console.log(
|
|
4865
|
+
pc13.yellow(
|
|
4866
|
+
`
|
|
4867
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F6D1} Graceful stop requested. Waiting for active routine (${state.activeRoutine || "routine"}) to complete before stopping...`
|
|
4868
|
+
)
|
|
4869
|
+
);
|
|
4870
|
+
return;
|
|
4871
|
+
}
|
|
4872
|
+
await handleStop();
|
|
4873
|
+
},
|
|
4874
|
+
onForceStop: async () => {
|
|
4875
|
+
await handleStop();
|
|
4876
|
+
},
|
|
4877
|
+
onHelp: () => {
|
|
4878
|
+
clearTicker();
|
|
4879
|
+
printKeybindingCheatSheet();
|
|
4880
|
+
updateTicker();
|
|
4881
|
+
}
|
|
4882
|
+
});
|
|
4883
|
+
keyboard?.start();
|
|
3204
4884
|
if (routines.includes("peer-review")) {
|
|
3205
|
-
await
|
|
4885
|
+
await performReviewDrain();
|
|
3206
4886
|
}
|
|
3207
|
-
if (routines.includes("autowork")) {
|
|
4887
|
+
if (!isStopping && !isGracefulStopping && routines.includes("autowork")) {
|
|
3208
4888
|
await runAutoworkCheck();
|
|
3209
4889
|
}
|
|
3210
|
-
|
|
3211
|
-
const
|
|
3212
|
-
|
|
4890
|
+
if (isStopping) return;
|
|
4891
|
+
const tick = async () => {
|
|
4892
|
+
if (isStopping || isGracefulStopping || isWorking) return;
|
|
4893
|
+
if (!isPaused) {
|
|
4894
|
+
const now = Date.now();
|
|
4895
|
+
if (routines.includes("peer-review") && now >= nextReviewCheckTime) {
|
|
4896
|
+
await performReviewDrain();
|
|
4897
|
+
return;
|
|
4898
|
+
}
|
|
4899
|
+
if (routines.includes("autowork") && now >= nextAutoworkCheckTime) {
|
|
4900
|
+
await runAutoworkCheck();
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
updateTicker();
|
|
4905
|
+
};
|
|
4906
|
+
tickerInterval = setInterval(tick, 1e3);
|
|
4907
|
+
await new Promise((resolve) => {
|
|
4908
|
+
stopResolve = resolve;
|
|
3213
4909
|
});
|
|
3214
4910
|
}
|
|
3215
4911
|
|
|
@@ -3233,16 +4929,16 @@ async function runDaemonCommand(action, options = {}) {
|
|
|
3233
4929
|
}
|
|
3234
4930
|
try {
|
|
3235
4931
|
const state2 = await startBackgroundDaemon(cwd, daemonOpts);
|
|
3236
|
-
console.log(
|
|
4932
|
+
console.log(pc14.green(`
|
|
3237
4933
|
\u2713 Background agent daemon started successfully.`));
|
|
3238
|
-
console.log(
|
|
3239
|
-
console.log(
|
|
3240
|
-
console.log(
|
|
3241
|
-
console.log(
|
|
3242
|
-
console.log(
|
|
3243
|
-
console.log(
|
|
4934
|
+
console.log(pc14.dim(` PID: ${state2.pid}`));
|
|
4935
|
+
console.log(pc14.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
|
|
4936
|
+
console.log(pc14.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
|
|
4937
|
+
console.log(pc14.dim(` Routines: ${state2.routines.join(", ")}`));
|
|
4938
|
+
console.log(pc14.dim(` Log file: .jonah-fleet/daemon.log`));
|
|
4939
|
+
console.log(pc14.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
|
|
3244
4940
|
} catch (err) {
|
|
3245
|
-
console.error(
|
|
4941
|
+
console.error(pc14.red(`
|
|
3246
4942
|
\u2717 Failed to start daemon: ${err.message}`));
|
|
3247
4943
|
process.exit(1);
|
|
3248
4944
|
}
|
|
@@ -3250,18 +4946,18 @@ async function runDaemonCommand(action, options = {}) {
|
|
|
3250
4946
|
}
|
|
3251
4947
|
if (act === "stop") {
|
|
3252
4948
|
if (!isDaemonRunning(cwd)) {
|
|
3253
|
-
console.log(
|
|
4949
|
+
console.log(pc14.yellow(`
|
|
3254
4950
|
\u26A0\uFE0F No local agent daemon is currently running in this repository.`));
|
|
3255
4951
|
return;
|
|
3256
4952
|
}
|
|
3257
4953
|
const state2 = readDaemonState(cwd);
|
|
3258
|
-
console.log(
|
|
4954
|
+
console.log(pc14.cyan(`
|
|
3259
4955
|
Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
3260
4956
|
const stopped = await stopDaemon(cwd);
|
|
3261
4957
|
if (stopped) {
|
|
3262
|
-
console.log(
|
|
4958
|
+
console.log(pc14.green(`\u2713 Local agent daemon stopped successfully.`));
|
|
3263
4959
|
} else {
|
|
3264
|
-
console.error(
|
|
4960
|
+
console.error(pc14.red(`\u2717 Could not terminate daemon process.`));
|
|
3265
4961
|
process.exit(1);
|
|
3266
4962
|
}
|
|
3267
4963
|
return;
|
|
@@ -3273,18 +4969,24 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
|
3273
4969
|
const running = isDaemonRunning(cwd);
|
|
3274
4970
|
const state = readDaemonState(cwd);
|
|
3275
4971
|
const activeWorktrees = await listActiveWorktrees(cwd);
|
|
3276
|
-
console.log(
|
|
4972
|
+
console.log(pc14.cyan(`
|
|
3277
4973
|
\u{1F916} Jonah Fleet Local Daemon Status
|
|
3278
4974
|
`));
|
|
3279
4975
|
if (running && state) {
|
|
3280
|
-
console.log(` Status: ${
|
|
4976
|
+
console.log(` Status: ${pc14.green(pc14.bold("RUNNING"))}`);
|
|
3281
4977
|
console.log(` PID: ${state.pid}`);
|
|
3282
4978
|
console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
|
|
3283
4979
|
console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
|
|
3284
4980
|
console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
|
|
3285
4981
|
console.log(` Routines: ${state.routines.join(", ")}`);
|
|
3286
|
-
|
|
3287
|
-
|
|
4982
|
+
let currentStateText = pc14.green("IDLE");
|
|
4983
|
+
if (state.status === "working") {
|
|
4984
|
+
const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc14.bold(state.activeTarget)})` : "");
|
|
4985
|
+
currentStateText = pc14.yellow("WORKING on " + workingDesc);
|
|
4986
|
+
} else if (state.status === "paused") {
|
|
4987
|
+
currentStateText = pc14.yellow(pc14.bold("PAUSED"));
|
|
4988
|
+
}
|
|
4989
|
+
console.log(` Current State: ${currentStateText}`);
|
|
3288
4990
|
if (state.lastReviewCheckAt) {
|
|
3289
4991
|
console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
|
|
3290
4992
|
}
|
|
@@ -3292,17 +4994,127 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
|
3292
4994
|
console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
|
|
3293
4995
|
}
|
|
3294
4996
|
} else {
|
|
3295
|
-
console.log(` Status: ${
|
|
3296
|
-
console.log(
|
|
4997
|
+
console.log(` Status: ${pc14.gray("STOPPED")}`);
|
|
4998
|
+
console.log(pc14.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
|
|
3297
4999
|
}
|
|
3298
5000
|
console.log(`
|
|
3299
5001
|
Active Worktrees: ${activeWorktrees.length}`);
|
|
3300
5002
|
for (const wt of activeWorktrees) {
|
|
3301
|
-
console.log(
|
|
5003
|
+
console.log(pc14.dim(` - [${wt.branch}] ${wt.path}`));
|
|
3302
5004
|
}
|
|
3303
5005
|
console.log("");
|
|
3304
5006
|
}
|
|
3305
5007
|
|
|
5008
|
+
// src/commands/labels.ts
|
|
5009
|
+
import pc15 from "picocolors";
|
|
5010
|
+
async function runLabels(action = "audit", options = {}) {
|
|
5011
|
+
const cwd = options.cwd || process.cwd();
|
|
5012
|
+
const executor = options.executor || defaultGhExecutor;
|
|
5013
|
+
const repo = await resolveRepoName(options.repo, cwd, executor);
|
|
5014
|
+
const manifest = loadManifest(cwd);
|
|
5015
|
+
const userProtected = manifest?.labels?.protected || [];
|
|
5016
|
+
const protectedPatterns = [
|
|
5017
|
+
...DEFAULT_PROTECTED_LABEL_PATTERNS,
|
|
5018
|
+
...userProtected
|
|
5019
|
+
];
|
|
5020
|
+
if (action === "prune") {
|
|
5021
|
+
const isDryRun = Boolean(options.dryRun);
|
|
5022
|
+
const result = await pruneLabels({
|
|
5023
|
+
repo,
|
|
5024
|
+
dryRun: isDryRun,
|
|
5025
|
+
yes: options.yes,
|
|
5026
|
+
cwd,
|
|
5027
|
+
protectedPatterns,
|
|
5028
|
+
executor
|
|
5029
|
+
});
|
|
5030
|
+
if (options.json) {
|
|
5031
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
if (isDryRun) {
|
|
5035
|
+
console.log(pc15.bold(pc15.cyan(`
|
|
5036
|
+
\u{1F50D} Label Prune (Dry-Run) for ${repo}
|
|
5037
|
+
`)));
|
|
5038
|
+
if (result.pruned.length === 0) {
|
|
5039
|
+
console.log(pc15.green(" \u2713 No prunable labels found. Repository labels are clean.\n"));
|
|
5040
|
+
} else {
|
|
5041
|
+
console.log(pc15.yellow(` Found ${result.pruned.length} strictly unused label(s) eligible for pruning:`));
|
|
5042
|
+
for (const label of result.pruned) {
|
|
5043
|
+
console.log(` - ${pc15.yellow(label)} (0 issues, 0 PRs, non-schema)`);
|
|
5044
|
+
}
|
|
5045
|
+
console.log(pc15.gray(`
|
|
5046
|
+
Run 'jonah-fleet labels prune --yes' to delete these labels.
|
|
5047
|
+
`));
|
|
5048
|
+
}
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
5051
|
+
console.log(pc15.bold(pc15.cyan(`
|
|
5052
|
+
\u{1F9F9} Label Pruning for ${repo}
|
|
5053
|
+
`)));
|
|
5054
|
+
if (result.pruned.length === 0 && result.errors.length === 0) {
|
|
5055
|
+
console.log(pc15.green(" \u2713 No prunable labels found. Repository labels are clean.\n"));
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
5058
|
+
if (result.pruned.length > 0) {
|
|
5059
|
+
console.log(pc15.green(` \u2713 Successfully pruned ${result.pruned.length} unused label(s):`));
|
|
5060
|
+
for (const label of result.pruned) {
|
|
5061
|
+
console.log(` - ${pc15.green(label)}`);
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
if (result.errors.length > 0) {
|
|
5065
|
+
console.log(pc15.red(`
|
|
5066
|
+
\u274C Failed to delete ${result.errors.length} label(s):`));
|
|
5067
|
+
for (const err of result.errors) {
|
|
5068
|
+
console.log(` - ${pc15.red(err.label)}: ${err.error}`);
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
console.log();
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
const rawLabels = await fetchRepoLabels(repo, executor, cwd);
|
|
5075
|
+
const classified = classifyLabels(rawLabels, protectedPatterns);
|
|
5076
|
+
if (options.json) {
|
|
5077
|
+
console.log(
|
|
5078
|
+
JSON.stringify(
|
|
5079
|
+
{
|
|
5080
|
+
repo,
|
|
5081
|
+
totalCount: rawLabels.length,
|
|
5082
|
+
activeCount: classified.active.length,
|
|
5083
|
+
protectedZeroCountCount: classified.protectedZeroCount.length,
|
|
5084
|
+
historicalCount: classified.historical.length,
|
|
5085
|
+
prunableCount: classified.prunable.length,
|
|
5086
|
+
active: classified.active,
|
|
5087
|
+
protectedZeroCount: classified.protectedZeroCount,
|
|
5088
|
+
historical: classified.historical,
|
|
5089
|
+
prunable: classified.prunable
|
|
5090
|
+
},
|
|
5091
|
+
null,
|
|
5092
|
+
2
|
|
5093
|
+
)
|
|
5094
|
+
);
|
|
5095
|
+
return;
|
|
5096
|
+
}
|
|
5097
|
+
console.log(pc15.bold(pc15.cyan(`
|
|
5098
|
+
\u{1F3F7}\uFE0F Repository Label Audit for ${repo}
|
|
5099
|
+
`)));
|
|
5100
|
+
console.log(` Total Labels: ${pc15.bold(String(rawLabels.length))}`);
|
|
5101
|
+
console.log(` Active (Open items): ${pc15.green(String(classified.active.length))}`);
|
|
5102
|
+
console.log(` Protected (Zero-count): ${pc15.cyan(String(classified.protectedZeroCount.length))}`);
|
|
5103
|
+
console.log(` Historical (Closed): ${pc15.gray(String(classified.historical.length))}`);
|
|
5104
|
+
console.log(` Prunable (Unused): ${classified.prunable.length > 0 ? pc15.yellow(String(classified.prunable.length)) : pc15.green("0")}`);
|
|
5105
|
+
if (classified.prunable.length > 0) {
|
|
5106
|
+
console.log(pc15.bold(pc15.yellow("\n \u26A0\uFE0F Prunable Labels (0 total issues/PRs, non-protected):")));
|
|
5107
|
+
for (const label of classified.prunable) {
|
|
5108
|
+
console.log(` - ${pc15.yellow(label.name)}`);
|
|
5109
|
+
}
|
|
5110
|
+
console.log(pc15.gray(`
|
|
5111
|
+
Run 'jonah-fleet labels prune' to clean up unused boilerplate.
|
|
5112
|
+
`));
|
|
5113
|
+
} else {
|
|
5114
|
+
console.log(pc15.green("\n \u2713 All labels are either active, historical, or protected fleet taxonomy.\n"));
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
|
|
3306
5118
|
// src/index.ts
|
|
3307
5119
|
var program = new Command();
|
|
3308
5120
|
program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
|
|
@@ -3312,7 +5124,11 @@ program.command("run <routine>").description("Run a specific prompt routine loca
|
|
|
3312
5124
|
program.command("daemon [action]").description("Manage background local worker daemon polling for unclaimed issues and pull requests").option("-i, --interval <minutes>", "Legacy global polling interval in minutes (default: 30)").option("--review-interval <minutes>", "Peer Review watchdog cadence in minutes (default: 3)").option("--autowork-interval <minutes>", "Autowork backlog cadence in minutes (default: 30)").option("-r, --routines <list>", "Comma-separated routines to run (default: peer-review,autowork)").option("-m, --model <model>", "LLM model override").option("--foreground", "Run daemon in foreground with live console logs").option("-v, --verbose", "Stream raw agent tokens and logs directly to stdout").action(async (action, options) => {
|
|
3313
5125
|
await runDaemonCommand(action, options);
|
|
3314
5126
|
});
|
|
3315
|
-
program.command("
|
|
5127
|
+
program.command("labels [action]").description("Audit and prune unused repository labels while protecting fleet taxonomy").option("-d, --dry-run", "Preview prunable labels without deleting them", false).option("-y, --yes", "Confirm automatic deletion of prunable labels", false).option("-r, --repo <repo>", "Target GitHub repository (defaults to current)").option("-j, --json", "Output results as JSON", false).action(async (action, options) => {
|
|
5128
|
+
const act = action === "prune" || action === "list" || action === "audit" ? action : "audit";
|
|
5129
|
+
await runLabels(act, options);
|
|
5130
|
+
});
|
|
5131
|
+
program.command("init").description("Initialize Jonah Fleet configuration, routines, workflows, and skills in the current repo").option("-p, --preset <preset>", "Preset profile to install (minimal | standard | full)", "standard").option("-f, --force", "Force overwrite existing files", false).option("--stack <stack>", "Override detected tech stack name").option("--package-manager <pm>", "Override package manager (npm, pnpm, yarn, bun, uv, poetry, cargo, go)").option("--test-cmd <cmd>", "Override test execution command").option("--build-cmd <cmd>", "Override build execution command").option("--prune-labels", "Prune unused boilerplate labels on initialization", false).option("--interactive", "Force interactive prompts for stack configuration").option("--no-interactive", "Disable interactive prompts").action(async (options) => {
|
|
3316
5132
|
await runInit(options);
|
|
3317
5133
|
});
|
|
3318
5134
|
program.command("sync").description("Synchronize local prompts, workflows, and skills with the installed fleet version").option("-c, --check", "Check for drift without writing changes", false).option("-f, --force", "Force update all files to match fleet version", false).action(async (options) => {
|