@rotorsoft/gent 1.1.1 → 1.2.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/README.md +28 -9
- package/dist/{chunk-32TIYLFY.js → chunk-2LGYNV6S.js} +46 -2
- package/dist/chunk-2LGYNV6S.js.map +1 -0
- package/dist/index.js +248 -107
- package/dist/index.js.map +1 -1
- package/dist/{setup-labels-3VZA2QD6.js → setup-labels-3IMSEEKN.js} +2 -2
- package/package.json +1 -1
- package/templates/.gent.yml +11 -0
- package/dist/chunk-32TIYLFY.js.map +0 -1
- /package/dist/{setup-labels-3VZA2QD6.js.map → setup-labels-3IMSEEKN.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
addIssueComment,
|
|
4
4
|
assignIssue,
|
|
5
5
|
buildIssueLabels,
|
|
6
|
+
checkAIProvider,
|
|
6
7
|
checkClaudeCli,
|
|
8
|
+
checkGeminiCli,
|
|
7
9
|
checkGhAuth,
|
|
8
10
|
checkGitRepo,
|
|
9
11
|
colors,
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
sortByPriority,
|
|
29
31
|
updateIssueLabels,
|
|
30
32
|
withSpinner
|
|
31
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-2LGYNV6S.js";
|
|
32
34
|
|
|
33
35
|
// src/index.ts
|
|
34
36
|
import { Command } from "commander";
|
|
@@ -183,7 +185,7 @@ async function initCommand(options) {
|
|
|
183
185
|
}
|
|
184
186
|
]);
|
|
185
187
|
if (setupLabels) {
|
|
186
|
-
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-
|
|
188
|
+
const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-3IMSEEKN.js");
|
|
187
189
|
await setupLabelsCommand2();
|
|
188
190
|
}
|
|
189
191
|
}
|
|
@@ -193,54 +195,7 @@ import inquirer2 from "inquirer";
|
|
|
193
195
|
import chalk from "chalk";
|
|
194
196
|
|
|
195
197
|
// src/lib/claude.ts
|
|
196
|
-
import { spawn } from "child_process";
|
|
197
198
|
import { execa } from "execa";
|
|
198
|
-
async function invokeClaude(options) {
|
|
199
|
-
const args = ["--print"];
|
|
200
|
-
if (options.permissionMode) {
|
|
201
|
-
args.push("--permission-mode", options.permissionMode);
|
|
202
|
-
}
|
|
203
|
-
args.push(options.prompt);
|
|
204
|
-
if (options.printOutput) {
|
|
205
|
-
const subprocess = execa("claude", args, {
|
|
206
|
-
stdio: "inherit"
|
|
207
|
-
});
|
|
208
|
-
await subprocess;
|
|
209
|
-
return "";
|
|
210
|
-
} else if (options.streamOutput) {
|
|
211
|
-
return new Promise((resolve, reject) => {
|
|
212
|
-
const child = spawn("claude", args, {
|
|
213
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
214
|
-
});
|
|
215
|
-
let output = "";
|
|
216
|
-
child.stdout.on("data", (chunk) => {
|
|
217
|
-
const text = chunk.toString();
|
|
218
|
-
output += text;
|
|
219
|
-
process.stdout.write(text);
|
|
220
|
-
});
|
|
221
|
-
child.stderr.on("data", (chunk) => {
|
|
222
|
-
process.stderr.write(chunk);
|
|
223
|
-
});
|
|
224
|
-
child.on("close", (code) => {
|
|
225
|
-
if (code === 0) {
|
|
226
|
-
resolve(output);
|
|
227
|
-
} else {
|
|
228
|
-
reject(new Error(`Claude exited with code ${code}`));
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
child.on("error", reject);
|
|
232
|
-
});
|
|
233
|
-
} else {
|
|
234
|
-
const { stdout } = await execa("claude", args);
|
|
235
|
-
return stdout;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
async function invokeClaudeInteractive(prompt, config) {
|
|
239
|
-
const args = ["--permission-mode", config.claude.permission_mode, prompt];
|
|
240
|
-
return execa("claude", args, {
|
|
241
|
-
stdio: "inherit"
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
199
|
function buildTicketPrompt(description, agentInstructions, additionalHints = null) {
|
|
245
200
|
const basePrompt = `You are creating a GitHub issue for a software project following an AI-assisted development workflow.
|
|
246
201
|
|
|
@@ -254,7 +209,9 @@ ${additionalHints}
|
|
|
254
209
|
|
|
255
210
|
` : ""}
|
|
256
211
|
|
|
257
|
-
Create a detailed GitHub issue following this exact template
|
|
212
|
+
Create a detailed GitHub issue following this exact template.
|
|
213
|
+
|
|
214
|
+
IMPORTANT: Start your output IMMEDIATELY with "## Description" - do not include any preamble, commentary, or introduction before the template.
|
|
258
215
|
|
|
259
216
|
## Description
|
|
260
217
|
[Clear user-facing description of what needs to be done]
|
|
@@ -371,39 +328,188 @@ function parseTicketMeta(output) {
|
|
|
371
328
|
};
|
|
372
329
|
}
|
|
373
330
|
function extractIssueBody(output) {
|
|
374
|
-
|
|
331
|
+
let body = output.replace(/\n?META:type=\w+,priority=\w+,risk=\w+,area=\w+\s*$/, "").trim();
|
|
332
|
+
const descriptionIndex = body.indexOf("## Description");
|
|
333
|
+
if (descriptionIndex > 0) {
|
|
334
|
+
body = body.substring(descriptionIndex);
|
|
335
|
+
}
|
|
336
|
+
return body;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/lib/ai-provider.ts
|
|
340
|
+
import { spawn } from "child_process";
|
|
341
|
+
import { execa as execa2 } from "execa";
|
|
342
|
+
async function invokeAI(options, config, providerOverride) {
|
|
343
|
+
const provider = providerOverride ?? config.ai.provider;
|
|
344
|
+
try {
|
|
345
|
+
const output = provider === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
|
|
346
|
+
return { output, provider };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (isRateLimitError(error, provider)) {
|
|
349
|
+
if (config.ai.auto_fallback && config.ai.fallback_provider && !providerOverride) {
|
|
350
|
+
const fallback = config.ai.fallback_provider;
|
|
351
|
+
logger.warning(`Rate limit reached on ${getProviderDisplayName(provider)}, switching to ${getProviderDisplayName(fallback)}...`);
|
|
352
|
+
const output = fallback === "claude" ? await invokeClaudeInternal(options) : await invokeGeminiInternal(options);
|
|
353
|
+
return { output, provider: fallback };
|
|
354
|
+
}
|
|
355
|
+
const err = error;
|
|
356
|
+
err.message = `Rate limited on ${getProviderDisplayName(provider)}`;
|
|
357
|
+
err.rateLimited = true;
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function invokeAIInteractive(prompt, config, providerOverride) {
|
|
364
|
+
const provider = providerOverride ?? config.ai.provider;
|
|
365
|
+
if (provider === "claude") {
|
|
366
|
+
const args = ["--permission-mode", config.claude.permission_mode, prompt];
|
|
367
|
+
return {
|
|
368
|
+
result: execa2("claude", args, { stdio: "inherit" }),
|
|
369
|
+
provider
|
|
370
|
+
};
|
|
371
|
+
} else {
|
|
372
|
+
return {
|
|
373
|
+
result: execa2("gemini", [prompt], { stdio: "inherit" }),
|
|
374
|
+
provider
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function getProviderDisplayName(provider) {
|
|
379
|
+
return provider === "claude" ? "Claude" : "Gemini";
|
|
380
|
+
}
|
|
381
|
+
function isRateLimitError(error, provider) {
|
|
382
|
+
if (!error || typeof error !== "object") return false;
|
|
383
|
+
if (provider === "claude" && "exitCode" in error && error.exitCode === 2) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if ("message" in error && typeof error.message === "string") {
|
|
387
|
+
const msg = error.message.toLowerCase();
|
|
388
|
+
if (msg.includes("rate limit") || msg.includes("quota exceeded") || msg.includes("too many requests")) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
async function invokeClaudeInternal(options) {
|
|
395
|
+
const args = ["--print"];
|
|
396
|
+
if (options.permissionMode) {
|
|
397
|
+
args.push("--permission-mode", options.permissionMode);
|
|
398
|
+
}
|
|
399
|
+
args.push(options.prompt);
|
|
400
|
+
if (options.printOutput) {
|
|
401
|
+
const subprocess = execa2("claude", args, {
|
|
402
|
+
stdio: "inherit"
|
|
403
|
+
});
|
|
404
|
+
await subprocess;
|
|
405
|
+
return "";
|
|
406
|
+
} else if (options.streamOutput) {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
const child = spawn("claude", args, {
|
|
409
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
410
|
+
});
|
|
411
|
+
let output = "";
|
|
412
|
+
child.stdout.on("data", (chunk) => {
|
|
413
|
+
const text = chunk.toString();
|
|
414
|
+
output += text;
|
|
415
|
+
process.stdout.write(text);
|
|
416
|
+
});
|
|
417
|
+
child.stderr.on("data", (chunk) => {
|
|
418
|
+
process.stderr.write(chunk);
|
|
419
|
+
});
|
|
420
|
+
child.on("close", (code) => {
|
|
421
|
+
if (code === 0) {
|
|
422
|
+
resolve(output);
|
|
423
|
+
} else {
|
|
424
|
+
const error = new Error(`Claude exited with code ${code}`);
|
|
425
|
+
error.exitCode = code ?? 1;
|
|
426
|
+
reject(error);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
child.on("error", reject);
|
|
430
|
+
});
|
|
431
|
+
} else {
|
|
432
|
+
const { stdout } = await execa2("claude", args);
|
|
433
|
+
return stdout;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function invokeGeminiInternal(options) {
|
|
437
|
+
const args = [];
|
|
438
|
+
args.push(options.prompt);
|
|
439
|
+
if (options.printOutput) {
|
|
440
|
+
const subprocess = execa2("gemini", args, {
|
|
441
|
+
stdio: "inherit"
|
|
442
|
+
});
|
|
443
|
+
await subprocess;
|
|
444
|
+
return "";
|
|
445
|
+
} else if (options.streamOutput) {
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
const child = spawn("gemini", args, {
|
|
448
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
449
|
+
});
|
|
450
|
+
let output = "";
|
|
451
|
+
child.stdout.on("data", (chunk) => {
|
|
452
|
+
const text = chunk.toString();
|
|
453
|
+
output += text;
|
|
454
|
+
process.stdout.write(text);
|
|
455
|
+
});
|
|
456
|
+
child.stderr.on("data", (chunk) => {
|
|
457
|
+
process.stderr.write(chunk);
|
|
458
|
+
});
|
|
459
|
+
child.on("close", (code) => {
|
|
460
|
+
if (code === 0) {
|
|
461
|
+
resolve(output);
|
|
462
|
+
} else {
|
|
463
|
+
const error = new Error(`Gemini exited with code ${code}`);
|
|
464
|
+
error.exitCode = code ?? 1;
|
|
465
|
+
reject(error);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
child.on("error", reject);
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
const { stdout } = await execa2("gemini", args);
|
|
472
|
+
return stdout;
|
|
473
|
+
}
|
|
375
474
|
}
|
|
376
475
|
|
|
377
476
|
// src/commands/create.ts
|
|
378
477
|
async function createCommand(description, options) {
|
|
379
478
|
logger.bold("Creating AI-enhanced ticket...");
|
|
380
479
|
logger.newline();
|
|
381
|
-
const
|
|
480
|
+
const config = loadConfig();
|
|
481
|
+
const provider = options.provider ?? config.ai.provider;
|
|
482
|
+
const providerName = getProviderDisplayName(provider);
|
|
483
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
484
|
+
checkGhAuth(),
|
|
485
|
+
checkAIProvider(provider)
|
|
486
|
+
]);
|
|
382
487
|
if (!ghAuth) {
|
|
383
488
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
384
489
|
process.exit(1);
|
|
385
490
|
}
|
|
386
|
-
if (!
|
|
387
|
-
logger.error(
|
|
491
|
+
if (!aiOk) {
|
|
492
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
388
493
|
process.exit(1);
|
|
389
494
|
}
|
|
390
495
|
const agentInstructions = loadAgentInstructions();
|
|
391
|
-
let
|
|
496
|
+
let aiOutput;
|
|
392
497
|
let additionalHints = null;
|
|
393
498
|
while (true) {
|
|
394
499
|
const prompt = buildTicketPrompt(description, agentInstructions, additionalHints);
|
|
395
500
|
try {
|
|
396
|
-
console.log(chalk.dim(
|
|
501
|
+
console.log(chalk.dim(`\u250C\u2500 Generating ticket with ${providerName}... \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
|
|
397
502
|
logger.newline();
|
|
398
|
-
|
|
503
|
+
const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
|
|
504
|
+
aiOutput = result.output;
|
|
399
505
|
logger.newline();
|
|
400
506
|
console.log(chalk.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
401
507
|
logger.newline();
|
|
402
508
|
} catch (error) {
|
|
403
|
-
logger.error(
|
|
509
|
+
logger.error(`${providerName} invocation failed: ${error}`);
|
|
404
510
|
return;
|
|
405
511
|
}
|
|
406
|
-
const meta = parseTicketMeta(
|
|
512
|
+
const meta = parseTicketMeta(aiOutput);
|
|
407
513
|
if (!meta) {
|
|
408
514
|
logger.warning("Could not parse metadata from Claude output. Using defaults.");
|
|
409
515
|
}
|
|
@@ -413,7 +519,7 @@ async function createCommand(description, options) {
|
|
|
413
519
|
risk: "low",
|
|
414
520
|
area: "shared"
|
|
415
521
|
};
|
|
416
|
-
const issueBody = extractIssueBody(
|
|
522
|
+
const issueBody = extractIssueBody(aiOutput);
|
|
417
523
|
const title = description.length > 60 ? description.slice(0, 57) + "..." : description;
|
|
418
524
|
const labels = buildIssueLabels(finalMeta);
|
|
419
525
|
displayTicketPreview(title, finalMeta, issueBody);
|
|
@@ -614,9 +720,9 @@ function getStatusColor(status) {
|
|
|
614
720
|
import inquirer3 from "inquirer";
|
|
615
721
|
|
|
616
722
|
// src/lib/git.ts
|
|
617
|
-
import { execa as
|
|
723
|
+
import { execa as execa3 } from "execa";
|
|
618
724
|
async function getCurrentBranch() {
|
|
619
|
-
const { stdout } = await
|
|
725
|
+
const { stdout } = await execa3("git", ["branch", "--show-current"]);
|
|
620
726
|
return stdout.trim();
|
|
621
727
|
}
|
|
622
728
|
async function isOnMainBranch() {
|
|
@@ -625,14 +731,14 @@ async function isOnMainBranch() {
|
|
|
625
731
|
}
|
|
626
732
|
async function getDefaultBranch() {
|
|
627
733
|
try {
|
|
628
|
-
const { stdout } = await
|
|
734
|
+
const { stdout } = await execa3("git", [
|
|
629
735
|
"symbolic-ref",
|
|
630
736
|
"refs/remotes/origin/HEAD"
|
|
631
737
|
]);
|
|
632
738
|
return stdout.trim().replace("refs/remotes/origin/", "");
|
|
633
739
|
} catch {
|
|
634
740
|
try {
|
|
635
|
-
await
|
|
741
|
+
await execa3("git", ["rev-parse", "--verify", "main"]);
|
|
636
742
|
return "main";
|
|
637
743
|
} catch {
|
|
638
744
|
return "master";
|
|
@@ -641,7 +747,7 @@ async function getDefaultBranch() {
|
|
|
641
747
|
}
|
|
642
748
|
async function branchExists(name) {
|
|
643
749
|
try {
|
|
644
|
-
await
|
|
750
|
+
await execa3("git", ["rev-parse", "--verify", name]);
|
|
645
751
|
return true;
|
|
646
752
|
} catch {
|
|
647
753
|
return false;
|
|
@@ -649,21 +755,21 @@ async function branchExists(name) {
|
|
|
649
755
|
}
|
|
650
756
|
async function createBranch(name, from) {
|
|
651
757
|
if (from) {
|
|
652
|
-
await
|
|
758
|
+
await execa3("git", ["checkout", "-b", name, from]);
|
|
653
759
|
} else {
|
|
654
|
-
await
|
|
760
|
+
await execa3("git", ["checkout", "-b", name]);
|
|
655
761
|
}
|
|
656
762
|
}
|
|
657
763
|
async function checkoutBranch(name) {
|
|
658
|
-
await
|
|
764
|
+
await execa3("git", ["checkout", name]);
|
|
659
765
|
}
|
|
660
766
|
async function hasUncommittedChanges() {
|
|
661
|
-
const { stdout } = await
|
|
767
|
+
const { stdout } = await execa3("git", ["status", "--porcelain"]);
|
|
662
768
|
return stdout.trim().length > 0;
|
|
663
769
|
}
|
|
664
770
|
async function getUnpushedCommits() {
|
|
665
771
|
try {
|
|
666
|
-
const { stdout } = await
|
|
772
|
+
const { stdout } = await execa3("git", [
|
|
667
773
|
"log",
|
|
668
774
|
"@{u}..HEAD",
|
|
669
775
|
"--oneline"
|
|
@@ -675,18 +781,18 @@ async function getUnpushedCommits() {
|
|
|
675
781
|
}
|
|
676
782
|
async function pushBranch(branch) {
|
|
677
783
|
const branchName = branch || await getCurrentBranch();
|
|
678
|
-
await
|
|
784
|
+
await execa3("git", ["push", "-u", "origin", branchName]);
|
|
679
785
|
}
|
|
680
786
|
async function getAuthorInitials() {
|
|
681
787
|
try {
|
|
682
|
-
const { stdout } = await
|
|
788
|
+
const { stdout } = await execa3("git", ["config", "user.initials"]);
|
|
683
789
|
if (stdout.trim()) {
|
|
684
790
|
return stdout.trim();
|
|
685
791
|
}
|
|
686
792
|
} catch {
|
|
687
793
|
}
|
|
688
794
|
try {
|
|
689
|
-
const { stdout } = await
|
|
795
|
+
const { stdout } = await execa3("git", ["config", "user.name"]);
|
|
690
796
|
const name = stdout.trim();
|
|
691
797
|
if (name) {
|
|
692
798
|
const parts = name.split(/\s+/);
|
|
@@ -698,7 +804,7 @@ async function getAuthorInitials() {
|
|
|
698
804
|
}
|
|
699
805
|
async function getCommitsSinceBase(base = "main") {
|
|
700
806
|
try {
|
|
701
|
-
const { stdout } = await
|
|
807
|
+
const { stdout } = await execa3("git", [
|
|
702
808
|
"log",
|
|
703
809
|
`${base}..HEAD`,
|
|
704
810
|
"--pretty=format:%s"
|
|
@@ -710,7 +816,7 @@ async function getCommitsSinceBase(base = "main") {
|
|
|
710
816
|
}
|
|
711
817
|
async function getDiffSummary(base = "main") {
|
|
712
818
|
try {
|
|
713
|
-
const { stdout } = await
|
|
819
|
+
const { stdout } = await execa3("git", [
|
|
714
820
|
"diff",
|
|
715
821
|
`${base}...HEAD`,
|
|
716
822
|
"--stat"
|
|
@@ -721,7 +827,7 @@ async function getDiffSummary(base = "main") {
|
|
|
721
827
|
}
|
|
722
828
|
}
|
|
723
829
|
async function getCurrentCommitSha() {
|
|
724
|
-
const { stdout } = await
|
|
830
|
+
const { stdout } = await execa3("git", ["rev-parse", "HEAD"]);
|
|
725
831
|
return stdout.trim();
|
|
726
832
|
}
|
|
727
833
|
async function hasNewCommits(beforeSha) {
|
|
@@ -804,13 +910,19 @@ function extractIssueNumber(branchName) {
|
|
|
804
910
|
async function runCommand(issueNumberArg, options) {
|
|
805
911
|
logger.bold("Running AI implementation workflow...");
|
|
806
912
|
logger.newline();
|
|
807
|
-
const
|
|
913
|
+
const config = loadConfig();
|
|
914
|
+
const provider = options.provider ?? config.ai.provider;
|
|
915
|
+
const providerName = getProviderDisplayName(provider);
|
|
916
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
917
|
+
checkGhAuth(),
|
|
918
|
+
checkAIProvider(provider)
|
|
919
|
+
]);
|
|
808
920
|
if (!ghAuth) {
|
|
809
921
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
810
922
|
process.exit(1);
|
|
811
923
|
}
|
|
812
|
-
if (!
|
|
813
|
-
logger.error(
|
|
924
|
+
if (!aiOk) {
|
|
925
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
814
926
|
process.exit(1);
|
|
815
927
|
}
|
|
816
928
|
const hasChanges = await hasUncommittedChanges();
|
|
@@ -829,7 +941,6 @@ async function runCommand(issueNumberArg, options) {
|
|
|
829
941
|
process.exit(0);
|
|
830
942
|
}
|
|
831
943
|
}
|
|
832
|
-
const config = loadConfig();
|
|
833
944
|
const workflowLabels = getWorkflowLabels(config);
|
|
834
945
|
let issueNumber;
|
|
835
946
|
if (options.auto) {
|
|
@@ -936,8 +1047,8 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
936
1047
|
const progressContent = readProgress(config);
|
|
937
1048
|
const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config);
|
|
938
1049
|
logger.newline();
|
|
939
|
-
logger.info(
|
|
940
|
-
logger.dim(
|
|
1050
|
+
logger.info(`Starting ${colors.provider(providerName)} implementation session...`);
|
|
1051
|
+
logger.dim(`${providerName} will implement the feature and create a commit.`);
|
|
941
1052
|
logger.dim("Review the changes before pushing.");
|
|
942
1053
|
logger.newline();
|
|
943
1054
|
const beforeSha = await getCurrentCommitSha();
|
|
@@ -947,15 +1058,17 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
947
1058
|
};
|
|
948
1059
|
process.on("SIGINT", handleSignal);
|
|
949
1060
|
process.on("SIGTERM", handleSignal);
|
|
950
|
-
let
|
|
1061
|
+
let aiExitCode;
|
|
1062
|
+
let usedProvider = provider;
|
|
951
1063
|
try {
|
|
952
|
-
const result = await
|
|
953
|
-
|
|
1064
|
+
const { result, provider: actualProvider } = await invokeAIInteractive(prompt, config, options.provider);
|
|
1065
|
+
usedProvider = actualProvider;
|
|
1066
|
+
aiExitCode = result.exitCode ?? void 0;
|
|
954
1067
|
} catch (error) {
|
|
955
1068
|
if (error && typeof error === "object" && "exitCode" in error) {
|
|
956
|
-
|
|
1069
|
+
aiExitCode = error.exitCode;
|
|
957
1070
|
}
|
|
958
|
-
logger.error(
|
|
1071
|
+
logger.error(`${getProviderDisplayName(usedProvider)} session failed: ${error}`);
|
|
959
1072
|
} finally {
|
|
960
1073
|
process.off("SIGINT", handleSignal);
|
|
961
1074
|
process.off("SIGTERM", handleSignal);
|
|
@@ -966,8 +1079,9 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
966
1079
|
logger.warning("Operation was cancelled. Labels unchanged.");
|
|
967
1080
|
return;
|
|
968
1081
|
}
|
|
1082
|
+
const usedProviderName = getProviderDisplayName(usedProvider);
|
|
969
1083
|
if (commitsCreated) {
|
|
970
|
-
logger.success(
|
|
1084
|
+
logger.success(`${usedProviderName} session completed with new commits.`);
|
|
971
1085
|
try {
|
|
972
1086
|
await updateIssueLabels(issueNumber, {
|
|
973
1087
|
add: [workflowLabels.completed],
|
|
@@ -980,7 +1094,7 @@ Labels: ${issue.labels.join(", ")}`);
|
|
|
980
1094
|
try {
|
|
981
1095
|
await addIssueComment(
|
|
982
1096
|
issueNumber,
|
|
983
|
-
`AI implementation completed on branch \`${branchName}
|
|
1097
|
+
`AI implementation completed on branch \`${branchName}\` using ${usedProviderName}.
|
|
984
1098
|
|
|
985
1099
|
Please review the changes and create a PR when ready.`
|
|
986
1100
|
);
|
|
@@ -989,9 +1103,9 @@ Please review the changes and create a PR when ready.`
|
|
|
989
1103
|
logger.warning(`Failed to post comment: ${error}`);
|
|
990
1104
|
}
|
|
991
1105
|
} else {
|
|
992
|
-
const isRateLimited =
|
|
1106
|
+
const isRateLimited = aiExitCode === 2;
|
|
993
1107
|
if (isRateLimited) {
|
|
994
|
-
logger.warning(
|
|
1108
|
+
logger.warning(`${usedProviderName} session ended due to rate limits. No commits were created.`);
|
|
995
1109
|
try {
|
|
996
1110
|
await updateIssueLabels(issueNumber, {
|
|
997
1111
|
add: [workflowLabels.blocked],
|
|
@@ -1004,7 +1118,7 @@ Please review the changes and create a PR when ready.`
|
|
|
1004
1118
|
try {
|
|
1005
1119
|
await addIssueComment(
|
|
1006
1120
|
issueNumber,
|
|
1007
|
-
`AI implementation was blocked due to API rate limits on branch \`${branchName}
|
|
1121
|
+
`AI implementation was blocked due to API rate limits on branch \`${branchName}\` (${usedProviderName}).
|
|
1008
1122
|
|
|
1009
1123
|
No commits were created. Please retry later.`
|
|
1010
1124
|
);
|
|
@@ -1013,7 +1127,7 @@ No commits were created. Please retry later.`
|
|
|
1013
1127
|
logger.warning(`Failed to post comment: ${error}`);
|
|
1014
1128
|
}
|
|
1015
1129
|
} else {
|
|
1016
|
-
logger.warning(
|
|
1130
|
+
logger.warning(`${usedProviderName} session completed but no commits were created. Labels unchanged.`);
|
|
1017
1131
|
}
|
|
1018
1132
|
return;
|
|
1019
1133
|
}
|
|
@@ -1051,13 +1165,19 @@ import inquirer4 from "inquirer";
|
|
|
1051
1165
|
async function prCommand(options) {
|
|
1052
1166
|
logger.bold("Creating AI-enhanced pull request...");
|
|
1053
1167
|
logger.newline();
|
|
1054
|
-
const
|
|
1168
|
+
const config = loadConfig();
|
|
1169
|
+
const provider = options.provider ?? config.ai.provider;
|
|
1170
|
+
const providerName = getProviderDisplayName(provider);
|
|
1171
|
+
const [ghAuth, aiOk] = await Promise.all([
|
|
1172
|
+
checkGhAuth(),
|
|
1173
|
+
checkAIProvider(provider)
|
|
1174
|
+
]);
|
|
1055
1175
|
if (!ghAuth) {
|
|
1056
1176
|
logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
|
|
1057
1177
|
process.exit(1);
|
|
1058
1178
|
}
|
|
1059
|
-
if (!
|
|
1060
|
-
logger.error(
|
|
1179
|
+
if (!aiOk) {
|
|
1180
|
+
logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
|
|
1061
1181
|
process.exit(1);
|
|
1062
1182
|
}
|
|
1063
1183
|
if (await isOnMainBranch()) {
|
|
@@ -1114,12 +1234,13 @@ async function prCommand(options) {
|
|
|
1114
1234
|
const prompt = buildPrPrompt(issue, commits, diffSummary);
|
|
1115
1235
|
let prBody;
|
|
1116
1236
|
try {
|
|
1117
|
-
logger.info(
|
|
1237
|
+
logger.info(`Generating PR description with ${colors.provider(providerName)}...`);
|
|
1118
1238
|
logger.newline();
|
|
1119
|
-
|
|
1239
|
+
const result = await invokeAI({ prompt, streamOutput: true }, config, options.provider);
|
|
1240
|
+
prBody = result.output;
|
|
1120
1241
|
logger.newline();
|
|
1121
1242
|
} catch (error) {
|
|
1122
|
-
logger.warning(
|
|
1243
|
+
logger.warning(`${providerName} invocation failed: ${error}`);
|
|
1123
1244
|
prBody = generateFallbackBody(issue, commits);
|
|
1124
1245
|
}
|
|
1125
1246
|
const prTitle = issue?.title || commits[0] || currentBranch;
|
|
@@ -1199,6 +1320,14 @@ async function statusCommand() {
|
|
|
1199
1320
|
logger.warning(` ${config.progress.file} not found`);
|
|
1200
1321
|
}
|
|
1201
1322
|
logger.newline();
|
|
1323
|
+
logger.bold("AI Provider:");
|
|
1324
|
+
const providerName = getProviderDisplayName(config.ai.provider);
|
|
1325
|
+
logger.info(` Active: ${colors.provider(providerName)}`);
|
|
1326
|
+
if (config.ai.fallback_provider) {
|
|
1327
|
+
const fallbackName = getProviderDisplayName(config.ai.fallback_provider);
|
|
1328
|
+
logger.info(` Fallback: ${fallbackName} (auto: ${config.ai.auto_fallback ? "enabled" : "disabled"})`);
|
|
1329
|
+
}
|
|
1330
|
+
logger.newline();
|
|
1202
1331
|
logger.bold("Prerequisites:");
|
|
1203
1332
|
const ghAuth = await checkGhAuth();
|
|
1204
1333
|
if (ghAuth) {
|
|
@@ -1207,10 +1336,22 @@ async function statusCommand() {
|
|
|
1207
1336
|
logger.error(" GitHub CLI not authenticated");
|
|
1208
1337
|
}
|
|
1209
1338
|
const claudeOk = await checkClaudeCli();
|
|
1339
|
+
const geminiOk = await checkGeminiCli();
|
|
1340
|
+
const getProviderStatus = (provider) => {
|
|
1341
|
+
const isActive = config.ai.provider === provider;
|
|
1342
|
+
const isFallback = config.ai.fallback_provider === provider;
|
|
1343
|
+
const suffix = isActive ? " (active)" : isFallback ? " (fallback)" : "";
|
|
1344
|
+
return suffix;
|
|
1345
|
+
};
|
|
1210
1346
|
if (claudeOk) {
|
|
1211
|
-
logger.success(
|
|
1347
|
+
logger.success(` Claude CLI available${getProviderStatus("claude")}`);
|
|
1348
|
+
} else {
|
|
1349
|
+
logger.error(` Claude CLI not found${getProviderStatus("claude")}`);
|
|
1350
|
+
}
|
|
1351
|
+
if (geminiOk) {
|
|
1352
|
+
logger.success(` Gemini CLI available${getProviderStatus("gemini")}`);
|
|
1212
1353
|
} else {
|
|
1213
|
-
logger.error(
|
|
1354
|
+
logger.error(` Gemini CLI not found${getProviderStatus("gemini")}`);
|
|
1214
1355
|
}
|
|
1215
1356
|
logger.newline();
|
|
1216
1357
|
logger.bold("Git Status:");
|
|
@@ -1306,8 +1447,8 @@ program.command("init").description("Initialize gent workflow in current reposit
|
|
|
1306
1447
|
program.command("setup-labels").description("Setup GitHub labels for AI workflow").action(async () => {
|
|
1307
1448
|
await setupLabelsCommand();
|
|
1308
1449
|
});
|
|
1309
|
-
program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").action(async (description, options) => {
|
|
1310
|
-
await createCommand(description, { yes: options.yes });
|
|
1450
|
+
program.command("create <description>").description("Create an AI-enhanced GitHub issue").option("-y, --yes", "Skip confirmation and create issue immediately").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (description, options) => {
|
|
1451
|
+
await createCommand(description, { yes: options.yes, provider: options.provider });
|
|
1311
1452
|
});
|
|
1312
1453
|
program.command("list").description("List GitHub issues by label/status").option("-l, --label <label>", "Filter by label").option("-s, --status <status>", "Filter by workflow status (ready, in-progress, completed, blocked, all)").option("-n, --limit <number>", "Maximum number of issues to show", "20").action(async (options) => {
|
|
1313
1454
|
await listCommand({
|
|
@@ -1316,11 +1457,11 @@ program.command("list").description("List GitHub issues by label/status").option
|
|
|
1316
1457
|
limit: parseInt(options.limit, 10)
|
|
1317
1458
|
});
|
|
1318
1459
|
});
|
|
1319
|
-
program.command("run [issue-number]").description("Run
|
|
1320
|
-
await runCommand(issueNumber, { auto: options.auto });
|
|
1460
|
+
program.command("run [issue-number]").description("Run AI to implement a GitHub issue").option("-a, --auto", "Auto-select highest priority ai-ready issue").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (issueNumber, options) => {
|
|
1461
|
+
await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
|
|
1321
1462
|
});
|
|
1322
|
-
program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").action(async (options) => {
|
|
1323
|
-
await prCommand({ draft: options.draft });
|
|
1463
|
+
program.command("pr").description("Create an AI-enhanced pull request").option("-d, --draft", "Create as draft PR").option("-p, --provider <provider>", "AI provider to use (claude or gemini)").action(async (options) => {
|
|
1464
|
+
await prCommand({ draft: options.draft, provider: options.provider });
|
|
1324
1465
|
});
|
|
1325
1466
|
program.command("status").description("Show current workflow status").action(async () => {
|
|
1326
1467
|
await statusCommand();
|