@rotorsoft/gent 1.12.0 → 1.13.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/dist/index.js CHANGED
@@ -14,7 +14,6 @@ import {
14
14
  createIssue,
15
15
  createPullRequest,
16
16
  createSpinner,
17
- extractPriorityFromLabels,
18
17
  extractTypeFromLabels,
19
18
  generateDefaultConfig,
20
19
  getConfigPath,
@@ -26,6 +25,7 @@ import {
26
25
  getWorkflowLabels,
27
26
  isValidIssueNumber,
28
27
  listIssues,
28
+ listOpenPrs,
29
29
  loadAgentInstructions,
30
30
  loadConfig,
31
31
  logger,
@@ -33,10 +33,9 @@ import {
33
33
  sanitizeSlug,
34
34
  setupLabelsCommand,
35
35
  sortByPriority,
36
- updateConfigProvider,
37
36
  updateIssueLabels,
38
37
  withSpinner
39
- } from "./chunk-TLGYE7J2.js";
38
+ } from "./chunk-KLHUMY5L.js";
40
39
 
41
40
  // src/index.ts
42
41
  import { Command } from "commander";
@@ -197,7 +196,7 @@ async function initCommand(options) {
197
196
  }
198
197
  ]);
199
198
  if (setupLabels) {
200
- const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-FLCGVPFY.js");
199
+ const { setupLabelsCommand: setupLabelsCommand2 } = await import("./setup-labels-3ANC76NF.js");
201
200
  await setupLabelsCommand2();
202
201
  }
203
202
  }
@@ -473,7 +472,7 @@ META:type=<type>,priority=<priority>,risk=<risk>,area=<area>
473
472
  Example: META:type=feature,priority=high,risk=low,area=ui`;
474
473
  return basePrompt;
475
474
  }
476
- function buildImplementationPrompt(issue, agentInstructions, progressContent, config, reviewFeedback = null) {
475
+ function buildImplementationPrompt(issue, agentInstructions, progressContent, config, extraContext = null) {
477
476
  const providerName = getProviderDisplayName(config.ai.provider);
478
477
  const providerEmail = getProviderEmail(config.ai.provider);
479
478
  return `GitHub Issue #${issue.number}: ${issue.title}
@@ -488,8 +487,7 @@ ${progressContent ? `## Previous Progress
488
487
  ${progressContent}
489
488
 
490
489
  ` : ""}
491
- ${reviewFeedback ? `## Review Feedback
492
- ${reviewFeedback}
490
+ ${extraContext ? `${extraContext}
493
491
 
494
492
  ` : ""}
495
493
 
@@ -664,11 +662,11 @@ async function createCommand(description, options) {
664
662
  ]);
665
663
  if (!ghAuth) {
666
664
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
667
- process.exit(1);
665
+ return;
668
666
  }
669
667
  if (!aiOk) {
670
668
  logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
671
- process.exit(1);
669
+ return;
672
670
  }
673
671
  const agentInstructions = loadAgentInstructions();
674
672
  let aiOutput;
@@ -810,105 +808,6 @@ Next steps:
810
808
 
811
809
  // src/commands/list.ts
812
810
  import chalk2 from "chalk";
813
- async function listCommand(options) {
814
- const isAuthed = await checkGhAuth();
815
- if (!isAuthed) {
816
- logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
817
- process.exit(1);
818
- }
819
- const config = loadConfig();
820
- const workflowLabels = getWorkflowLabels(config);
821
- const labels = [];
822
- if (options.label) {
823
- labels.push(options.label);
824
- }
825
- if (options.status && options.status !== "all") {
826
- switch (options.status) {
827
- case "ready":
828
- labels.push(workflowLabels.ready);
829
- break;
830
- case "in-progress":
831
- labels.push(workflowLabels.inProgress);
832
- break;
833
- case "completed":
834
- labels.push(workflowLabels.completed);
835
- break;
836
- case "blocked":
837
- labels.push(workflowLabels.blocked);
838
- break;
839
- }
840
- } else if (!options.status) {
841
- labels.push(workflowLabels.ready);
842
- }
843
- let issues;
844
- try {
845
- issues = await listIssues({
846
- labels: labels.length > 0 ? labels : void 0,
847
- state: "open",
848
- limit: options.limit || 20
849
- });
850
- } catch (error) {
851
- logger.error(`Failed to fetch issues: ${error}`);
852
- return;
853
- }
854
- if (issues.length === 0) {
855
- logger.info("No issues found matching the criteria.");
856
- return;
857
- }
858
- sortByPriority(issues);
859
- logger.bold(`Found ${issues.length} issue(s):`);
860
- logger.newline();
861
- for (const issue of issues) {
862
- const type = extractTypeFromLabels(issue.labels);
863
- const priority = extractPriorityFromLabels(issue.labels);
864
- const status = getIssueStatus(issue.labels, workflowLabels);
865
- const priorityColor = getPriorityColor(priority);
866
- const statusColor = getStatusColor(status);
867
- console.log(
868
- ` ${colors.issue(`#${issue.number.toString().padStart(4)}`)} ${priorityColor(`[${priority}]`.padEnd(10))} ${statusColor(`[${status}]`.padEnd(14))} ${colors.label(`[${type}]`.padEnd(10))} ` + issue.title.slice(0, 50) + (issue.title.length > 50 ? "..." : "")
869
- );
870
- }
871
- logger.newline();
872
- logger.dim(`Run ${colors.command("gent run <issue-number>")} to implement an issue`);
873
- logger.dim(`Run ${colors.command("gent run --auto")} to auto-select highest priority`);
874
- }
875
- function getIssueStatus(labels, workflowLabels) {
876
- if (labels.includes(workflowLabels.ready)) return "ready";
877
- if (labels.includes(workflowLabels.inProgress)) return "in-progress";
878
- if (labels.includes(workflowLabels.completed)) return "completed";
879
- if (labels.includes(workflowLabels.blocked)) return "blocked";
880
- return "unknown";
881
- }
882
- function getPriorityColor(priority) {
883
- switch (priority) {
884
- case "critical":
885
- return chalk2.red;
886
- case "high":
887
- return chalk2.yellow;
888
- case "medium":
889
- return chalk2.blue;
890
- case "low":
891
- return chalk2.green;
892
- default:
893
- return chalk2.gray;
894
- }
895
- }
896
- function getStatusColor(status) {
897
- switch (status) {
898
- case "ready":
899
- return chalk2.green;
900
- case "in-progress":
901
- return chalk2.yellow;
902
- case "completed":
903
- return chalk2.blue;
904
- case "blocked":
905
- return chalk2.red;
906
- default:
907
- return chalk2.gray;
908
- }
909
- }
910
-
911
- // src/commands/run.ts
912
811
  import inquirer3 from "inquirer";
913
812
 
914
813
  // src/lib/git.ts
@@ -1030,6 +929,22 @@ async function getLastCommitTimestamp() {
1030
929
  const { stdout } = await execa2("git", ["log", "-1", "--format=%cI"]);
1031
930
  return stdout.trim();
1032
931
  }
932
+ async function listLocalBranches() {
933
+ const { stdout } = await execa2("git", ["branch", "--format=%(refname:short)"]);
934
+ return stdout.trim().split("\n").filter(Boolean);
935
+ }
936
+ async function remoteBranchExists(name) {
937
+ try {
938
+ await execa2("git", ["ls-remote", "--exit-code", "--heads", "origin", name]);
939
+ return true;
940
+ } catch {
941
+ return false;
942
+ }
943
+ }
944
+ async function fetchAndCheckout(name) {
945
+ await execa2("git", ["fetch", "origin", `${name}:${name}`]);
946
+ await execa2("git", ["checkout", name]);
947
+ }
1033
948
 
1034
949
  // src/lib/branch.ts
1035
950
  async function generateBranchName(config, issueNumber, issueTitle, type) {
@@ -1102,7 +1017,308 @@ function extractIssueNumber(branchName) {
1102
1017
  return info?.issueNumber ?? null;
1103
1018
  }
1104
1019
 
1020
+ // src/commands/list.ts
1021
+ function findBranchForIssue(issueNumber, branches) {
1022
+ for (const branch of branches) {
1023
+ const info = parseBranchName(branch);
1024
+ if (info && info.issueNumber === issueNumber) {
1025
+ return branch;
1026
+ }
1027
+ }
1028
+ return null;
1029
+ }
1030
+ function buildTicketChoices(inProgressIssues, readyIssues, openPrs, localBranches) {
1031
+ const choices = [];
1032
+ const seen = /* @__PURE__ */ new Set();
1033
+ const prByIssue = /* @__PURE__ */ new Map();
1034
+ for (const pr of openPrs) {
1035
+ const info = parseBranchName(pr.headRefName);
1036
+ if (info) {
1037
+ prByIssue.set(info.issueNumber, pr);
1038
+ }
1039
+ }
1040
+ for (const issue of inProgressIssues) {
1041
+ if (seen.has(issue.number)) continue;
1042
+ seen.add(issue.number);
1043
+ const branch = findBranchForIssue(issue.number, localBranches);
1044
+ const pr = prByIssue.get(issue.number);
1045
+ choices.push({
1046
+ issueNumber: issue.number,
1047
+ title: issue.title,
1048
+ branch: branch || pr?.headRefName || null,
1049
+ category: pr ? "open-pr" : "in-progress"
1050
+ });
1051
+ }
1052
+ for (const [issueNumber, pr] of prByIssue) {
1053
+ if (seen.has(issueNumber)) continue;
1054
+ seen.add(issueNumber);
1055
+ const issue = [...inProgressIssues, ...readyIssues].find(
1056
+ (i) => i.number === issueNumber
1057
+ );
1058
+ choices.push({
1059
+ issueNumber,
1060
+ title: issue?.title || pr.title,
1061
+ branch: pr.headRefName,
1062
+ category: "open-pr"
1063
+ });
1064
+ }
1065
+ for (const issue of readyIssues) {
1066
+ if (seen.has(issue.number)) continue;
1067
+ seen.add(issue.number);
1068
+ const branch = findBranchForIssue(issue.number, localBranches);
1069
+ choices.push({
1070
+ issueNumber: issue.number,
1071
+ title: issue.title,
1072
+ branch,
1073
+ category: "ready"
1074
+ });
1075
+ }
1076
+ return choices;
1077
+ }
1078
+ function categoryLabel(category) {
1079
+ switch (category) {
1080
+ case "in-progress":
1081
+ return chalk2.yellow("[in progress]");
1082
+ case "open-pr":
1083
+ return chalk2.blue("[open PR]");
1084
+ case "ready":
1085
+ return chalk2.green("[ready]");
1086
+ }
1087
+ }
1088
+ function formatChoice(choice) {
1089
+ const num = colors.issue(`#${choice.issueNumber}`);
1090
+ const cat = categoryLabel(choice.category);
1091
+ const title = choice.title.length > 50 ? choice.title.slice(0, 50) + "..." : choice.title;
1092
+ return `${num} ${cat} ${title}`;
1093
+ }
1094
+ async function listCommand(options) {
1095
+ const isAuthed = await checkGhAuth();
1096
+ if (!isAuthed) {
1097
+ logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1098
+ return;
1099
+ }
1100
+ const config = loadConfig();
1101
+ const workflowLabels = getWorkflowLabels(config);
1102
+ const currentBranch = await getCurrentBranch();
1103
+ const defaultBranch = await getDefaultBranch();
1104
+ const statusFilter = options.status;
1105
+ const limit = options.limit || 20;
1106
+ let inProgressIssues = [];
1107
+ let readyIssues = [];
1108
+ if (statusFilter && statusFilter !== "all") {
1109
+ const labels = [];
1110
+ if (options.label) labels.push(options.label);
1111
+ switch (statusFilter) {
1112
+ case "ready":
1113
+ labels.push(workflowLabels.ready);
1114
+ break;
1115
+ case "in-progress":
1116
+ labels.push(workflowLabels.inProgress);
1117
+ break;
1118
+ case "completed":
1119
+ labels.push(workflowLabels.completed);
1120
+ break;
1121
+ case "blocked":
1122
+ labels.push(workflowLabels.blocked);
1123
+ break;
1124
+ }
1125
+ const [issues, localBranches2] = await withSpinner(
1126
+ "Fetching tickets...",
1127
+ () => Promise.all([
1128
+ listIssues({ labels, state: "open", limit }),
1129
+ listLocalBranches()
1130
+ ])
1131
+ );
1132
+ sortByPriority(issues);
1133
+ if (statusFilter === "in-progress") {
1134
+ inProgressIssues = issues;
1135
+ } else {
1136
+ readyIssues = issues;
1137
+ }
1138
+ const choices2 = buildTicketChoices(
1139
+ inProgressIssues,
1140
+ readyIssues,
1141
+ [],
1142
+ localBranches2
1143
+ );
1144
+ if (choices2.length === 0) {
1145
+ logger.info("No issues found matching the criteria.");
1146
+ return;
1147
+ }
1148
+ await presentSelector(
1149
+ choices2,
1150
+ currentBranch,
1151
+ defaultBranch,
1152
+ config
1153
+ );
1154
+ return;
1155
+ }
1156
+ const labelFilter = options.label ? [options.label] : [];
1157
+ const [inProgress, ready, prs, localBranches] = await withSpinner(
1158
+ "Fetching tickets...",
1159
+ () => Promise.all([
1160
+ listIssues({
1161
+ labels: [workflowLabels.inProgress, ...labelFilter],
1162
+ state: "open",
1163
+ limit
1164
+ }),
1165
+ listIssues({
1166
+ labels: [workflowLabels.ready, ...labelFilter],
1167
+ state: "open",
1168
+ limit
1169
+ }),
1170
+ listOpenPrs(30),
1171
+ listLocalBranches()
1172
+ ])
1173
+ );
1174
+ sortByPriority(inProgress);
1175
+ sortByPriority(ready);
1176
+ const choices = buildTicketChoices(inProgress, ready, prs, localBranches);
1177
+ if (choices.length === 0) {
1178
+ logger.info("No tickets found.");
1179
+ logger.dim(
1180
+ `Create a ticket with ${colors.command("gent create")} or add the '${workflowLabels.ready}' label to an issue.`
1181
+ );
1182
+ return;
1183
+ }
1184
+ await presentSelector(choices, currentBranch, defaultBranch, config);
1185
+ }
1186
+ async function presentSelector(choices, currentBranch, defaultBranch, config) {
1187
+ const dirty = await hasUncommittedChanges();
1188
+ const inquirerChoices = [];
1189
+ inquirerChoices.push({
1190
+ name: `${chalk2.magenta(defaultBranch)}${currentBranch === defaultBranch ? chalk2.dim(" (current)") : ""}`,
1191
+ value: "__main__"
1192
+ });
1193
+ inquirerChoices.push(new inquirer3.Separator("\u2500"));
1194
+ const inProgress = choices.filter((c) => c.category === "in-progress");
1195
+ const openPrChoices = choices.filter((c) => c.category === "open-pr");
1196
+ const ready = choices.filter((c) => c.category === "ready");
1197
+ if (inProgress.length > 0) {
1198
+ inquirerChoices.push(new inquirer3.Separator(chalk2.yellow(" In Progress")));
1199
+ for (const c of inProgress) {
1200
+ inquirerChoices.push({
1201
+ name: formatChoice(c),
1202
+ value: String(c.issueNumber)
1203
+ });
1204
+ }
1205
+ }
1206
+ if (openPrChoices.length > 0) {
1207
+ inquirerChoices.push(new inquirer3.Separator(chalk2.blue(" Open PRs")));
1208
+ for (const c of openPrChoices) {
1209
+ inquirerChoices.push({
1210
+ name: formatChoice(c),
1211
+ value: String(c.issueNumber)
1212
+ });
1213
+ }
1214
+ }
1215
+ if (ready.length > 0) {
1216
+ inquirerChoices.push(new inquirer3.Separator(chalk2.green(" Ready")));
1217
+ for (const c of ready) {
1218
+ inquirerChoices.push({
1219
+ name: formatChoice(c),
1220
+ value: String(c.issueNumber)
1221
+ });
1222
+ }
1223
+ }
1224
+ const { selected } = await inquirer3.prompt([
1225
+ {
1226
+ type: "list",
1227
+ name: "selected",
1228
+ message: "Select a ticket to switch to:",
1229
+ choices: inquirerChoices,
1230
+ pageSize: 20
1231
+ }
1232
+ ]);
1233
+ if (selected === "__main__") {
1234
+ if (currentBranch === defaultBranch) {
1235
+ logger.info(`Already on ${colors.branch(defaultBranch)}`);
1236
+ return;
1237
+ }
1238
+ if (dirty) {
1239
+ const ok = await confirmDirty();
1240
+ if (!ok) return;
1241
+ }
1242
+ await withSpinner(`Switching to ${defaultBranch}...`, async () => {
1243
+ await checkoutBranch(defaultBranch);
1244
+ });
1245
+ logger.success(`Switched to ${colors.branch(defaultBranch)}`);
1246
+ return;
1247
+ }
1248
+ const issueNumber = parseInt(selected, 10);
1249
+ const ticket = choices.find((c) => c.issueNumber === issueNumber);
1250
+ if (!ticket) return;
1251
+ if (dirty) {
1252
+ const ok = await confirmDirty();
1253
+ if (!ok) return;
1254
+ }
1255
+ const targetBranch = ticket.branch;
1256
+ if (targetBranch) {
1257
+ if (await branchExists(targetBranch)) {
1258
+ await withSpinner(`Switching to ${targetBranch}...`, async () => {
1259
+ await checkoutBranch(targetBranch);
1260
+ });
1261
+ logger.success(`Switched to ${colors.branch(targetBranch)}`);
1262
+ } else if (await remoteBranchExists(targetBranch)) {
1263
+ await withSpinner(
1264
+ `Fetching ${targetBranch} from remote...`,
1265
+ async () => {
1266
+ await fetchAndCheckout(targetBranch);
1267
+ }
1268
+ );
1269
+ logger.success(
1270
+ `Fetched and switched to ${colors.branch(targetBranch)}`
1271
+ );
1272
+ } else {
1273
+ logger.warning(
1274
+ `Branch ${colors.branch(targetBranch)} not found locally or on remote.`
1275
+ );
1276
+ await offerCreateBranch(config, issueNumber, ticket.title);
1277
+ }
1278
+ } else {
1279
+ await offerCreateBranch(config, issueNumber, ticket.title);
1280
+ }
1281
+ }
1282
+ async function confirmDirty() {
1283
+ logger.warning("You have uncommitted changes.");
1284
+ const { proceed } = await inquirer3.prompt([
1285
+ {
1286
+ type: "confirm",
1287
+ name: "proceed",
1288
+ message: "Continue anyway? (changes will carry over to the new branch)",
1289
+ default: false
1290
+ }
1291
+ ]);
1292
+ if (!proceed) {
1293
+ logger.info("Aborting. Please commit or stash your changes first.");
1294
+ }
1295
+ return proceed;
1296
+ }
1297
+ async function offerCreateBranch(config, issueNumber, title) {
1298
+ const branchName = await generateBranchName(
1299
+ config,
1300
+ issueNumber,
1301
+ title,
1302
+ "feature"
1303
+ );
1304
+ const { create } = await inquirer3.prompt([
1305
+ {
1306
+ type: "confirm",
1307
+ name: "create",
1308
+ message: `No branch exists. Create ${colors.branch(branchName)}?`,
1309
+ default: true
1310
+ }
1311
+ ]);
1312
+ if (!create) return;
1313
+ const defaultBranch = await getDefaultBranch();
1314
+ await withSpinner(`Creating branch ${branchName}...`, async () => {
1315
+ await createBranch(branchName, defaultBranch);
1316
+ });
1317
+ logger.success(`Created and switched to ${colors.branch(branchName)}`);
1318
+ }
1319
+
1105
1320
  // src/commands/run.ts
1321
+ import inquirer4 from "inquirer";
1106
1322
  async function runCommand(issueNumberArg, options) {
1107
1323
  logger.bold("Running AI implementation workflow...");
1108
1324
  logger.newline();
@@ -1115,16 +1331,16 @@ async function runCommand(issueNumberArg, options) {
1115
1331
  ]);
1116
1332
  if (!ghAuth) {
1117
1333
  logger.error("Not authenticated with GitHub. Run 'gh auth login' first.");
1118
- process.exit(1);
1334
+ return;
1119
1335
  }
1120
1336
  if (!aiOk) {
1121
1337
  logger.error(`${providerName} CLI not found. Please install ${provider} CLI first.`);
1122
- process.exit(1);
1338
+ return;
1123
1339
  }
1124
1340
  const hasChanges = await hasUncommittedChanges();
1125
1341
  if (hasChanges) {
1126
1342
  logger.warning("You have uncommitted changes.");
1127
- const { proceed } = await inquirer3.prompt([
1343
+ const { proceed } = await inquirer4.prompt([
1128
1344
  {
1129
1345
  type: "confirm",
1130
1346
  name: "proceed",
@@ -1134,28 +1350,20 @@ async function runCommand(issueNumberArg, options) {
1134
1350
  ]);
1135
1351
  if (!proceed) {
1136
1352
  logger.info("Aborting. Please commit or stash your changes first.");
1137
- process.exit(0);
1353
+ return;
1138
1354
  }
1139
1355
  }
1140
1356
  const workflowLabels = getWorkflowLabels(config);
1141
1357
  let issueNumber;
1142
- if (options.auto) {
1143
- const autoIssue = await autoSelectIssue(workflowLabels.ready);
1144
- if (!autoIssue) {
1145
- logger.error("No ai-ready issues found.");
1146
- process.exit(1);
1147
- }
1148
- issueNumber = autoIssue.number;
1149
- logger.info(`Auto-selected: ${colors.issue(`#${issueNumber}`)} - ${autoIssue.title}`);
1150
- } else if (issueNumberArg) {
1358
+ if (issueNumberArg) {
1151
1359
  if (!isValidIssueNumber(issueNumberArg)) {
1152
1360
  logger.error("Invalid issue number.");
1153
- process.exit(1);
1361
+ return;
1154
1362
  }
1155
1363
  issueNumber = parseInt(issueNumberArg, 10);
1156
1364
  } else {
1157
- logger.error("Please provide an issue number or use --auto");
1158
- process.exit(1);
1365
+ logger.error("Please provide an issue number. Use 'gent switch' to browse tickets.");
1366
+ return;
1159
1367
  }
1160
1368
  let issue;
1161
1369
  try {
@@ -1168,7 +1376,7 @@ async function runCommand(issueNumberArg, options) {
1168
1376
  }
1169
1377
  if (!issue.labels.includes(workflowLabels.ready)) {
1170
1378
  logger.warning(`Issue #${issueNumber} does not have the '${workflowLabels.ready}' label.`);
1171
- const { proceed } = await inquirer3.prompt([
1379
+ const { proceed } = await inquirer4.prompt([
1172
1380
  {
1173
1381
  type: "confirm",
1174
1382
  name: "proceed",
@@ -1190,7 +1398,7 @@ Labels: ${issue.labels.join(", ")}`);
1190
1398
  const onMain = await isOnMainBranch();
1191
1399
  if (await branchExists(branchName)) {
1192
1400
  logger.info(`Branch ${colors.branch(branchName)} already exists.`);
1193
- const { action } = await inquirer3.prompt([
1401
+ const { action } = await inquirer4.prompt([
1194
1402
  {
1195
1403
  type: "list",
1196
1404
  name: "action",
@@ -1212,7 +1420,7 @@ Labels: ${issue.labels.join(", ")}`);
1212
1420
  } else {
1213
1421
  if (!onMain) {
1214
1422
  logger.warning(`Not on main branch (currently on ${colors.branch(currentBranch)}).`);
1215
- const { fromMain } = await inquirer3.prompt([
1423
+ const { fromMain } = await inquirer4.prompt([
1216
1424
  {
1217
1425
  type: "confirm",
1218
1426
  name: "fromMain",
@@ -1333,31 +1541,9 @@ No commits were created. Please retry later.`
1333
1541
  3. Push branch: ${colors.command("git push -u origin " + branchName)}
1334
1542
  4. Create PR: ${colors.command("gent pr")}`);
1335
1543
  }
1336
- async function autoSelectIssue(readyLabel) {
1337
- let issues = await listIssues({
1338
- labels: [readyLabel, "priority:critical"],
1339
- state: "open",
1340
- limit: 1
1341
- });
1342
- if (issues.length > 0) return issues[0];
1343
- issues = await listIssues({
1344
- labels: [readyLabel, "priority:high"],
1345
- state: "open",
1346
- limit: 1
1347
- });
1348
- if (issues.length > 0) return issues[0];
1349
- issues = await listIssues({
1350
- labels: [readyLabel],
1351
- state: "open",
1352
- limit: 10
1353
- });
1354
- if (issues.length === 0) return null;
1355
- sortByPriority(issues);
1356
- return issues[0];
1357
- }
1358
1544
 
1359
1545
  // src/commands/pr.ts
1360
- import inquirer4 from "inquirer";
1546
+ import inquirer5 from "inquirer";
1361
1547
 
1362
1548
  // src/lib/playwright.ts
1363
1549
  import { execa as execa3 } from "execa";
@@ -1438,7 +1624,7 @@ async function prCommand(options) {
1438
1624
  const hasUnpushed = await getUnpushedCommits();
1439
1625
  if (hasUnpushed) {
1440
1626
  logger.warning("Branch has unpushed commits.");
1441
- const { push } = await inquirer4.prompt([
1627
+ const { push } = await inquirer5.prompt([
1442
1628
  {
1443
1629
  type: "confirm",
1444
1630
  name: "push",
@@ -1575,7 +1761,7 @@ function generateFallbackBody(issue, commits) {
1575
1761
  }
1576
1762
 
1577
1763
  // src/commands/fix.ts
1578
- import inquirer5 from "inquirer";
1764
+ import inquirer6 from "inquirer";
1579
1765
 
1580
1766
  // src/lib/review-feedback.ts
1581
1767
  var ACTIONABLE_KEYWORDS = [
@@ -1766,7 +1952,7 @@ async function fixCommand(options) {
1766
1952
  const hasChanges = await hasUncommittedChanges();
1767
1953
  if (hasChanges) {
1768
1954
  logger.warning("You have uncommitted changes.");
1769
- const { proceed } = await inquirer5.prompt([
1955
+ const { proceed } = await inquirer6.prompt([
1770
1956
  {
1771
1957
  type: "confirm",
1772
1958
  name: "proceed",
@@ -1814,7 +2000,8 @@ async function fixCommand(options) {
1814
2000
  });
1815
2001
  const agentInstructions = loadAgentInstructions();
1816
2002
  const progressContent = readProgress(config);
1817
- const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config, summary);
2003
+ const prompt = buildImplementationPrompt(issue, agentInstructions, progressContent, config, `## Review Feedback
2004
+ ${summary}`);
1818
2005
  logger.newline();
1819
2006
  logger.info(`Starting ${colors.provider(providerName)} fix session...`);
1820
2007
  logger.dim("Review feedback will be appended to the implementation prompt.");
@@ -1894,7 +2081,7 @@ import { homedir } from "os";
1894
2081
  // package.json
1895
2082
  var package_default = {
1896
2083
  name: "@rotorsoft/gent",
1897
- version: "1.12.0",
2084
+ version: "1.13.0",
1898
2085
  description: "AI-powered GitHub workflow CLI - leverage AI (Claude, Gemini, or Codex) to create tickets, implement features, and manage PRs",
1899
2086
  keywords: [
1900
2087
  "cli",
@@ -2304,7 +2491,7 @@ async function statusCommand() {
2304
2491
  }
2305
2492
 
2306
2493
  // src/commands/tui.ts
2307
- import inquirer6 from "inquirer";
2494
+ import inquirer7 from "inquirer";
2308
2495
  import { execa as execa4 } from "execa";
2309
2496
 
2310
2497
  // src/tui/state.ts
@@ -2433,7 +2620,6 @@ function getAvailableActions(state) {
2433
2620
  }
2434
2621
  if (state.isOnMain) {
2435
2622
  actions.push({ id: "create", label: "new", shortcut: "n" });
2436
- actions.push({ id: "run-auto", label: "run next", shortcut: "r" });
2437
2623
  actions.push({ id: "list", label: "list", shortcut: "l" });
2438
2624
  actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
2439
2625
  actions.push({ id: "quit", label: "quit", shortcut: "q" });
@@ -2442,16 +2628,16 @@ function getAvailableActions(state) {
2442
2628
  if (state.hasUncommittedChanges) {
2443
2629
  actions.push({ id: "commit", label: "commit", shortcut: "c" });
2444
2630
  }
2445
- if (state.hasUnpushedCommits) {
2631
+ if (state.hasUnpushedCommits && state.commits.length > 0) {
2446
2632
  actions.push({ id: "push", label: "Push", shortcut: "P" });
2447
2633
  }
2448
2634
  if (!state.pr && state.commits.length > 0) {
2449
2635
  actions.push({ id: "pr", label: "Create pr", shortcut: "C" });
2450
2636
  }
2637
+ if (state.issue) {
2638
+ actions.push({ id: "implement", label: "implement", shortcut: "i" });
2639
+ }
2451
2640
  if (state.pr && state.pr.state === "open") {
2452
- if (state.hasActionableFeedback) {
2453
- actions.push({ id: "fix", label: "fix feedback", shortcut: "f" });
2454
- }
2455
2641
  if (state.hasUIChanges && state.isPlaywrightAvailable && state.config.video.enabled) {
2456
2642
  actions.push({ id: "video", label: "video", shortcut: "v" });
2457
2643
  }
@@ -2459,6 +2645,7 @@ function getAvailableActions(state) {
2459
2645
  if (state.pr && (state.pr.state === "merged" || state.pr.state === "closed")) {
2460
2646
  actions.push({ id: "checkout-main", label: "main", shortcut: "m" });
2461
2647
  }
2648
+ actions.push({ id: "list", label: "list", shortcut: "l" });
2462
2649
  actions.push({ id: "switch-provider", label: "switch", shortcut: "s" });
2463
2650
  actions.push({ id: "quit", label: "quit", shortcut: "q" });
2464
2651
  return actions;
@@ -2602,6 +2789,14 @@ function renderModal(message) {
2602
2789
  for (let i = 0; i < padY; i++) console.log();
2603
2790
  for (const line of lines) console.log(line);
2604
2791
  }
2792
+ function renderActionPanel(title, content) {
2793
+ const w = termWidth();
2794
+ console.log(topRow(title, w));
2795
+ for (const line of content) {
2796
+ console.log(row(line, w));
2797
+ }
2798
+ console.log(botRow(w));
2799
+ }
2605
2800
  function renderSettings(state, w) {
2606
2801
  const provider = getProviderDisplayName(state.config.ai.provider);
2607
2802
  const provTag = state.isAIProviderAvailable ? chalk3.green(provider) : chalk3.red(provider);
@@ -2732,7 +2927,7 @@ function clearScreen() {
2732
2927
  // src/commands/tui.ts
2733
2928
  var CANCEL = /* @__PURE__ */ Symbol("cancel");
2734
2929
  async function confirm(message) {
2735
- const { ok } = await inquirer6.prompt([
2930
+ const { ok } = await inquirer7.prompt([
2736
2931
  {
2737
2932
  type: "confirm",
2738
2933
  name: "ok",
@@ -2773,24 +2968,23 @@ async function waitForKey(validKeys) {
2773
2968
  stdin.on("data", onData);
2774
2969
  });
2775
2970
  }
2776
- async function executeAction(actionId, state) {
2971
+ async function executeAction(actionId, state, providerSetter) {
2777
2972
  switch (actionId) {
2778
2973
  case "quit":
2779
2974
  return false;
2780
- case "list":
2975
+ case "list": {
2781
2976
  clearScreen();
2782
- await listCommand({ status: "ready", limit: 20 });
2977
+ try {
2978
+ await listCommand({});
2979
+ } catch (error) {
2980
+ logger.error(`List failed: ${error}`);
2981
+ }
2783
2982
  await promptContinue();
2784
2983
  return true;
2785
- case "run-auto": {
2786
- clearScreen();
2787
- if (!await confirm("Start AI agent to implement next ticket?")) return true;
2788
- await runCommand(void 0, { auto: true });
2789
- return false;
2790
2984
  }
2791
2985
  case "create": {
2792
2986
  clearScreen();
2793
- const { description } = await inquirer6.prompt([
2987
+ const { description } = await inquirer7.prompt([
2794
2988
  {
2795
2989
  type: "input",
2796
2990
  name: "description",
@@ -2801,7 +2995,11 @@ async function executeAction(actionId, state) {
2801
2995
  logger.info("Cancelled");
2802
2996
  return true;
2803
2997
  }
2804
- await createCommand(description, {});
2998
+ try {
2999
+ await createCommand(description, {});
3000
+ } catch (error) {
3001
+ logger.error(`Create failed: ${error}`);
3002
+ }
2805
3003
  await promptContinue();
2806
3004
  return true;
2807
3005
  }
@@ -2822,10 +3020,20 @@ async function executeAction(actionId, state) {
2822
3020
  await promptContinue();
2823
3021
  return true;
2824
3022
  }
2825
- case "fix": {
3023
+ case "implement": {
2826
3024
  clearScreen();
2827
- if (!await confirm("Start AI agent to fix review feedback?")) return true;
2828
- await fixCommand({});
3025
+ const hasCommits = state.commits.length > 0;
3026
+ const hasFeedback = state.hasActionableFeedback;
3027
+ let msg;
3028
+ if (hasFeedback && hasCommits) {
3029
+ msg = "Start AI agent to address review feedback?";
3030
+ } else if (hasCommits) {
3031
+ msg = "Start AI agent to continue implementation from existing commits?";
3032
+ } else {
3033
+ msg = "Start AI agent to implement this ticket from scratch?";
3034
+ }
3035
+ if (!await confirm(msg)) return true;
3036
+ await handleImplement(state);
2829
3037
  return false;
2830
3038
  }
2831
3039
  case "video": {
@@ -2837,7 +3045,7 @@ async function executeAction(actionId, state) {
2837
3045
  }
2838
3046
  case "switch-provider":
2839
3047
  clearScreen();
2840
- await handleSwitchProvider(state);
3048
+ await handleSwitchProvider(state, providerSetter);
2841
3049
  return true;
2842
3050
  case "checkout-main": {
2843
3051
  clearScreen();
@@ -2910,7 +3118,7 @@ async function generateCommitMessage(diffContent, issueNumber, issueTitle, state
2910
3118
  } catch {
2911
3119
  logger.warning("AI commit message generation failed");
2912
3120
  console.log();
2913
- const { message } = await inquirer6.prompt([
3121
+ const { message } = await inquirer7.prompt([
2914
3122
  {
2915
3123
  type: "input",
2916
3124
  name: "message",
@@ -2920,6 +3128,50 @@ async function generateCommitMessage(diffContent, issueNumber, issueTitle, state
2920
3128
  return message.trim() || CANCEL;
2921
3129
  }
2922
3130
  }
3131
+ async function handleImplement(state) {
3132
+ if (!state.issue) {
3133
+ logger.error("No linked issue found");
3134
+ return;
3135
+ }
3136
+ const agentInstructions = loadAgentInstructions();
3137
+ const progressContent = readProgress(state.config);
3138
+ const contextParts = [];
3139
+ if (state.commits.length > 0) {
3140
+ contextParts.push(
3141
+ `## Current Progress
3142
+ There are ${state.commits.length} existing commit(s) on this branch:
3143
+ ${state.commits.map((c) => `- ${c}`).join("\n")}
3144
+
3145
+ Continue the implementation from where it left off. Review the existing work and complete any remaining tasks.`
3146
+ );
3147
+ }
3148
+ if (state.hasActionableFeedback && state.reviewFeedback.length > 0) {
3149
+ const feedbackLines = state.reviewFeedback.map((f) => `- [${f.source}] ${f.body.slice(0, 200)}`).join("\n");
3150
+ contextParts.push(`## Review Feedback
3151
+ ${feedbackLines}`);
3152
+ }
3153
+ const extraContext = contextParts.length > 0 ? contextParts.join("\n\n") : null;
3154
+ const prompt = buildImplementationPrompt(
3155
+ state.issue,
3156
+ agentInstructions,
3157
+ progressContent,
3158
+ state.config,
3159
+ extraContext
3160
+ );
3161
+ const providerName = getProviderDisplayName(state.config.ai.provider);
3162
+ clearScreen();
3163
+ renderActionPanel(`${providerName} Session`, [
3164
+ `Implementing: #${state.issue.number} ${state.issue.title}`,
3165
+ state.commits.length > 0 ? `Continuing from ${state.commits.length} existing commit(s)` : "Starting fresh implementation",
3166
+ ...state.hasActionableFeedback ? [`Includes ${state.reviewFeedback.length} review feedback item(s)`] : []
3167
+ ]);
3168
+ console.log();
3169
+ try {
3170
+ await invokeAIInteractive(prompt, state.config);
3171
+ } catch (error) {
3172
+ logger.error(`${providerName} session failed: ${error}`);
3173
+ }
3174
+ }
2923
3175
  async function handlePush() {
2924
3176
  try {
2925
3177
  const { stdout: branch } = await execa4("git", ["branch", "--show-current"]);
@@ -2933,9 +3185,9 @@ async function handlePush() {
2933
3185
  }
2934
3186
  }
2935
3187
  var PROVIDERS = ["claude", "gemini", "codex"];
2936
- async function handleSwitchProvider(state) {
3188
+ async function handleSwitchProvider(state, setProvider) {
2937
3189
  const current = state.config.ai.provider;
2938
- const { provider } = await inquirer6.prompt([
3190
+ const { provider } = await inquirer7.prompt([
2939
3191
  {
2940
3192
  type: "list",
2941
3193
  name: "provider",
@@ -2948,12 +3200,8 @@ async function handleSwitchProvider(state) {
2948
3200
  }
2949
3201
  ]);
2950
3202
  if (provider === current) return;
2951
- try {
2952
- updateConfigProvider(provider);
2953
- logger.success(`Provider switched to ${provider}`);
2954
- } catch (error) {
2955
- logger.error(`Failed to switch provider: ${error}`);
2956
- }
3203
+ setProvider(provider);
3204
+ logger.success(`Provider switched to ${provider} (session only)`);
2957
3205
  }
2958
3206
  async function handleCheckoutMain() {
2959
3207
  try {
@@ -2971,6 +3219,13 @@ async function handleVideoCapture(state) {
2971
3219
  logger.error("No linked issue found");
2972
3220
  return;
2973
3221
  }
3222
+ const providerName = getProviderDisplayName(state.config.ai.provider);
3223
+ clearScreen();
3224
+ renderActionPanel("Video Capture", [
3225
+ `Recording: #${state.issue.number} ${state.issue.title}`,
3226
+ `Provider: ${providerName}`
3227
+ ]);
3228
+ console.log();
2974
3229
  try {
2975
3230
  const agentInstructions = loadAgentInstructions();
2976
3231
  const videoPrompt = buildVideoPrompt(
@@ -2987,7 +3242,7 @@ async function handleVideoCapture(state) {
2987
3242
  }
2988
3243
  async function promptContinue() {
2989
3244
  console.log();
2990
- await inquirer6.prompt([
3245
+ await inquirer7.prompt([
2991
3246
  {
2992
3247
  type: "input",
2993
3248
  name: "continue",
@@ -2997,8 +3252,15 @@ async function promptContinue() {
2997
3252
  }
2998
3253
  async function tuiCommand() {
2999
3254
  let running = true;
3255
+ let providerOverride = null;
3256
+ const setProvider = (p) => {
3257
+ providerOverride = p;
3258
+ };
3000
3259
  while (running) {
3001
3260
  const state = await loadState();
3261
+ if (providerOverride) {
3262
+ state.config.ai.provider = providerOverride;
3263
+ }
3002
3264
  clearScreen();
3003
3265
  const actions = getAvailableActions(state);
3004
3266
  let hint;
@@ -3014,7 +3276,7 @@ async function tuiCommand() {
3014
3276
  const key = await waitForKey(validKeys);
3015
3277
  const action = actions.find((a) => a.shortcut === key);
3016
3278
  if (action) {
3017
- running = await executeAction(action.id, state);
3279
+ running = await executeAction(action.id, state, setProvider);
3018
3280
  }
3019
3281
  }
3020
3282
  }
@@ -3046,15 +3308,15 @@ program.command("setup-labels").description("Setup GitHub labels for AI workflow
3046
3308
  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, gemini, or codex)").option("-t, --title <title>", "Override the generated issue title").action(async (description, options) => {
3047
3309
  await createCommand(description, { yes: options.yes, provider: options.provider, title: options.title });
3048
3310
  });
3049
- 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) => {
3311
+ program.command("list").description("List and switch to GitHub issues").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) => {
3050
3312
  await listCommand({
3051
3313
  label: options.label,
3052
3314
  status: options.status,
3053
3315
  limit: parseInt(options.limit, 10)
3054
3316
  });
3055
3317
  });
3056
- 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, gemini, or codex)").action(async (issueNumber, options) => {
3057
- await runCommand(issueNumber, { auto: options.auto, provider: options.provider });
3318
+ program.command("run [issue-number]").description("Run AI to implement a GitHub issue").option("-p, --provider <provider>", "AI provider to use (claude, gemini, or codex)").action(async (issueNumber, options) => {
3319
+ await runCommand(issueNumber, { provider: options.provider });
3058
3320
  });
3059
3321
  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, gemini, or codex)").option("--no-video", "Disable video capture for UI changes").action(async (options) => {
3060
3322
  await prCommand({