techunter 0.1.11 → 1.0.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.
Files changed (4) hide show
  1. package/README.md +237 -209
  2. package/dist/index.js +1734 -956
  3. package/dist/mcp.js +1049 -378
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -39,12 +39,20 @@ __export(github_exports, {
39
39
  createTask: () => createTask,
40
40
  editTask: () => editTask,
41
41
  embedBaseCommit: () => embedBaseCommit,
42
+ embedTargetBranch: () => embedTargetBranch,
42
43
  ensureLabels: () => ensureLabels,
44
+ ensureRemoteBranch: () => ensureRemoteBranch,
43
45
  extractBaseCommit: () => extractBaseCommit,
46
+ extractTargetBranch: () => extractTargetBranch,
44
47
  formatGuideAsMarkdown: () => formatGuideAsMarkdown,
45
48
  getAuthenticatedUser: () => getAuthenticatedUser,
46
49
  getDefaultBranch: () => getDefaultBranch,
50
+ getIssueNumberFromBranch: () => getIssueNumberFromBranch,
51
+ getOpenSubtasks: () => getOpenSubtasks,
52
+ getRepoFile: () => getRepoFile,
47
53
  getTask: () => getTask,
54
+ getTaskPR: () => getTaskPR,
55
+ getTaskPRDiff: () => getTaskPRDiff,
48
56
  isCollaborator: () => isCollaborator,
49
57
  listComments: () => listComments,
50
58
  listMyTasks: () => listMyTasks,
@@ -54,7 +62,8 @@ __export(github_exports, {
54
62
  mergeWorkerIntoBase: () => mergeWorkerIntoBase,
55
63
  postComment: () => postComment,
56
64
  postGuideComment: () => postGuideComment,
57
- rejectTask: () => rejectTask
65
+ rejectTask: () => rejectTask,
66
+ upsertRepoFile: () => upsertRepoFile
58
67
  });
59
68
  import { Octokit } from "@octokit/rest";
60
69
  import { fetch as undiciFetch } from "undici";
@@ -117,11 +126,22 @@ function extractBaseCommit(body) {
117
126
  const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
118
127
  return match?.[1] ?? null;
119
128
  }
120
- async function createTask(config, title, body, baseCommit) {
129
+ function embedTargetBranch(body, branch) {
130
+ return `${body}
131
+ ${TARGET_BRANCH_MARKER}${branch} -->`;
132
+ }
133
+ function extractTargetBranch(body) {
134
+ if (!body) return null;
135
+ const match = body.match(/<!-- techunter-target:([^\s>]+) -->/);
136
+ return match?.[1] ?? null;
137
+ }
138
+ async function createTask(config, title, body, baseCommit, targetBranch) {
121
139
  const octokit = createOctokit(config.githubToken);
122
140
  const { owner, repo } = config.github;
123
141
  await ensureLabels(config);
124
- const finalBody = baseCommit ? embedBaseCommit(body ?? "", baseCommit) : body;
142
+ let finalBody = body ?? "";
143
+ if (baseCommit) finalBody = embedBaseCommit(finalBody, baseCommit);
144
+ if (targetBranch) finalBody = embedTargetBranch(finalBody, targetBranch);
125
145
  const { data } = await octokit.issues.create({
126
146
  owner,
127
147
  repo,
@@ -134,13 +154,22 @@ async function createTask(config, title, body, baseCommit) {
134
154
  async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
135
155
  const octokit = createOctokit(config.githubToken);
136
156
  const { owner, repo } = config.github;
137
- await octokit.repos.merge({
138
- owner,
139
- repo,
140
- base: baseBranch,
141
- head: workerBranch,
142
- commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
143
- });
157
+ try {
158
+ await octokit.repos.merge({
159
+ owner,
160
+ repo,
161
+ base: baseBranch,
162
+ head: workerBranch,
163
+ commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
164
+ });
165
+ } catch (err) {
166
+ if (err.status === 409) {
167
+ throw new Error(
168
+ `Merge conflict: ${workerBranch} cannot be merged into ${baseBranch} cleanly. Resolve conflicts manually.`
169
+ );
170
+ }
171
+ throw err;
172
+ }
144
173
  }
145
174
  async function claimTask(config, number, username) {
146
175
  const octokit = createOctokit(config.githubToken);
@@ -216,6 +245,23 @@ async function postGuideComment(config, number, guide) {
216
245
  body
217
246
  });
218
247
  }
248
+ async function ensureRemoteBranch(config, branchName, fallbackBase) {
249
+ const octokit = createOctokit(config.githubToken);
250
+ const { owner, repo } = config.github;
251
+ try {
252
+ await octokit.repos.getBranch({ owner, repo, branch: branchName });
253
+ return;
254
+ } catch (err) {
255
+ if (err.status !== 404) throw err;
256
+ }
257
+ const { data: baseRef } = await octokit.repos.getBranch({ owner, repo, branch: fallbackBase });
258
+ await octokit.git.createRef({
259
+ owner,
260
+ repo,
261
+ ref: `refs/heads/${branchName}`,
262
+ sha: baseRef.commit.sha
263
+ });
264
+ }
219
265
  async function createPR(config, title, body, branch, base) {
220
266
  const octokit = createOctokit(config.githubToken);
221
267
  const { owner, repo } = config.github;
@@ -232,14 +278,11 @@ async function createPR(config, title, body, branch, base) {
232
278
  async function markInReview(config, number) {
233
279
  const octokit = createOctokit(config.githubToken);
234
280
  const { owner, repo } = config.github;
235
- try {
236
- await octokit.issues.removeLabel({
237
- owner,
238
- repo,
239
- issue_number: number,
240
- name: LABEL_CLAIMED
241
- });
242
- } catch {
281
+ for (const label of [LABEL_CLAIMED, LABEL_CHANGES_NEEDED]) {
282
+ try {
283
+ await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
284
+ } catch {
285
+ }
243
286
  }
244
287
  await octokit.issues.addLabels({
245
288
  owner,
@@ -354,28 +397,115 @@ async function editTask(config, number, title, body) {
354
397
  const { owner, repo } = config.github;
355
398
  await octokit.issues.update({ owner, repo, issue_number: number, title, body });
356
399
  }
400
+ async function upsertRepoFile(config, filePath, content, message) {
401
+ const octokit = createOctokit(config.githubToken);
402
+ const { owner, repo } = config.github;
403
+ let sha;
404
+ try {
405
+ const { data: data2 } = await octokit.repos.getContent({ owner, repo, path: filePath });
406
+ if (!Array.isArray(data2) && data2.type === "file") {
407
+ sha = data2.sha;
408
+ }
409
+ } catch {
410
+ }
411
+ const { data } = await octokit.repos.createOrUpdateFileContents({
412
+ owner,
413
+ repo,
414
+ path: filePath,
415
+ message,
416
+ content: Buffer.from(content, "utf-8").toString("base64"),
417
+ ...sha ? { sha } : {}
418
+ });
419
+ return data.content?.html_url ?? `https://github.com/${owner}/${repo}/blob/main/${filePath}`;
420
+ }
421
+ async function getRepoFile(config, filePath) {
422
+ const octokit = createOctokit(config.githubToken);
423
+ const { owner, repo } = config.github;
424
+ try {
425
+ const { data } = await octokit.repos.getContent({ owner, repo, path: filePath });
426
+ if (!Array.isArray(data) && data.type === "file" && "content" in data) {
427
+ return Buffer.from(data.content, "base64").toString("utf-8");
428
+ }
429
+ return null;
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
357
434
  async function getDefaultBranch(config) {
358
435
  const octokit = createOctokit(config.githubToken);
359
436
  const { owner, repo } = config.github;
360
437
  const { data } = await octokit.repos.get({ owner, repo });
361
438
  return data.default_branch;
362
439
  }
363
- async function acceptTask(config, issueNumber, headBranch) {
440
+ async function getTaskPR(config, issueNumber) {
364
441
  const octokit = createOctokit(config.githubToken);
365
442
  const { owner, repo } = config.github;
366
443
  const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
367
- const pr = headBranch ? prs.find((p) => p.head.ref === headBranch) : prs.find((p) => p.head.ref.startsWith(`task-${issueNumber}-`) || p.head.ref.startsWith("worker-"));
368
- if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
369
- const { data: merge } = await octokit.pulls.merge({
444
+ const pr = prs.find(
445
+ (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
446
+ );
447
+ if (!pr) return null;
448
+ return { number: pr.number, url: pr.html_url, body: pr.body ?? "", baseBranch: pr.base.ref };
449
+ }
450
+ async function getOpenSubtasks(config, targetBranch) {
451
+ const octokit = createOctokit(config.githubToken);
452
+ const { owner, repo } = config.github;
453
+ const { data } = await octokit.issues.listForRepo({
454
+ owner,
455
+ repo,
456
+ state: "open",
457
+ per_page: 100
458
+ });
459
+ return data.filter((issue) => !issue.pull_request).filter((issue) => extractTargetBranch(issue.body ?? null) === targetBranch).map((issue) => issue.number);
460
+ }
461
+ async function getIssueNumberFromBranch(config, branch) {
462
+ const octokit = createOctokit(config.githubToken);
463
+ const { owner, repo } = config.github;
464
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
465
+ const pr = prs.find((p) => p.head.ref === branch);
466
+ if (!pr) return null;
467
+ const match = (pr.body ?? "").match(/Closes #(\d+)/i);
468
+ if (!match) return null;
469
+ return { issueNumber: parseInt(match[1], 10), prUrl: pr.html_url };
470
+ }
471
+ async function getTaskPRDiff(config, prNumber) {
472
+ const octokit = createOctokit(config.githubToken);
473
+ const { owner, repo } = config.github;
474
+ const response = await octokit.pulls.get({
370
475
  owner,
371
476
  repo,
372
- pull_number: pr.number,
373
- merge_method: "merge"
477
+ pull_number: prNumber,
478
+ mediaType: { format: "diff" }
374
479
  });
375
- await closeTask(config, issueNumber);
376
- return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "" };
480
+ return response.data;
481
+ }
482
+ async function acceptTask(config, issueNumber) {
483
+ const octokit = createOctokit(config.githubToken);
484
+ const { owner, repo } = config.github;
485
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
486
+ const pr = prs.find(
487
+ (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
488
+ );
489
+ if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
490
+ try {
491
+ const { data: merge } = await octokit.pulls.merge({
492
+ owner,
493
+ repo,
494
+ pull_number: pr.number,
495
+ merge_method: "merge"
496
+ });
497
+ await closeTask(config, issueNumber);
498
+ return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "", baseBranch: pr.base.ref };
499
+ } catch (err) {
500
+ if (err.status === 405) {
501
+ throw new Error(
502
+ `PR #${pr.number} cannot be merged \u2014 may have conflicts or is not in a mergeable state.`
503
+ );
504
+ }
505
+ throw err;
506
+ }
377
507
  }
378
- var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER;
508
+ var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER, TARGET_BRANCH_MARKER;
379
509
  var init_github = __esm({
380
510
  "src/lib/github.ts"() {
381
511
  "use strict";
@@ -392,19 +522,20 @@ var init_github = __esm({
392
522
  ];
393
523
  TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
394
524
  BASE_COMMIT_MARKER = "<!-- techunter-base:";
525
+ TARGET_BRANCH_MARKER = "<!-- techunter-target:";
395
526
  }
396
527
  });
397
528
 
398
529
  // src/index.ts
399
- import chalk14 from "chalk";
530
+ import chalk17 from "chalk";
400
531
  import readline from "readline";
401
532
  import { createRequire } from "module";
402
533
 
403
534
  // src/commands/init.ts
404
- import { input, password, select } from "@inquirer/prompts";
405
- import chalk2 from "chalk";
406
- import ora from "ora";
407
- import open from "open";
535
+ import { input, password, select as select11 } from "@inquirer/prompts";
536
+ import chalk13 from "chalk";
537
+ import ora14 from "ora";
538
+ import open2 from "open";
408
539
  import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
409
540
 
410
541
  // src/lib/config.ts
@@ -423,7 +554,8 @@ var configSchema = z.object({
423
554
  }),
424
555
  taskState: z.object({
425
556
  activeIssueNumber: z.number().optional(),
426
- baseCommit: z.string().optional()
557
+ baseCommit: z.string().optional(),
558
+ activeBranch: z.string().optional()
427
559
  }).optional()
428
560
  });
429
561
  var store = new Conf({
@@ -510,14 +642,21 @@ function parseOwnerRepo(remoteUrl) {
510
642
  }
511
643
  return null;
512
644
  }
513
- function makeBranchName(issueNumber, username) {
514
- const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
515
- return `task-${issueNumber}-${slug}`;
516
- }
517
645
  function makeWorkerBranchName(username) {
518
646
  const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
519
647
  return `worker-${slug}`;
520
648
  }
649
+ function makeTaskBranchName(issueNumber, username) {
650
+ const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
651
+ return `task-${issueNumber}-${slug}`;
652
+ }
653
+ function isTaskBranch(branch) {
654
+ return /^task-\d+-/.test(branch);
655
+ }
656
+ function parseIssueNumberFromBranch(branch) {
657
+ const match = branch.match(/^task-(\d+)-/);
658
+ return match ? parseInt(match[1], 10) : null;
659
+ }
521
660
  async function getCurrentCommit() {
522
661
  return (await git.revparse(["HEAD"])).trim();
523
662
  }
@@ -638,16 +777,27 @@ async function getRemoteHeadSha(baseBranch) {
638
777
  await git.fetch("origin", baseBranch);
639
778
  return (await git.revparse([`origin/${baseBranch}`])).trim();
640
779
  }
641
- async function resetOrCreateBranch(branchName, sha) {
642
- const branches = await git.branch();
643
- const localExists = Object.keys(branches.branches).some((b) => b === branchName);
644
- if (localExists) {
780
+ async function checkoutFromCommit(branchName, sha) {
781
+ const branches = await git.branch(["-a"]);
782
+ const exists = Object.keys(branches.branches).some(
783
+ (b) => b === branchName || b === `remotes/origin/${branchName}`
784
+ );
785
+ if (exists) {
645
786
  await git.checkout(branchName);
646
- await git.reset(["--hard", sha]);
647
787
  } else {
648
788
  await git.checkoutBranch(branchName, sha);
649
789
  }
650
790
  }
791
+ async function hasUncommittedChanges() {
792
+ const status = await git.status();
793
+ return !status.isClean();
794
+ }
795
+ async function stash(message) {
796
+ await git.stash(["push", "-u", "-m", message]);
797
+ }
798
+ async function stashPop() {
799
+ await git.stash(["pop"]);
800
+ }
651
801
 
652
802
  // src/lib/client.ts
653
803
  init_proxy();
@@ -665,349 +815,165 @@ function getModel(config) {
665
815
  return config.aiModel ?? DEFAULT_MODEL;
666
816
  }
667
817
 
668
- // src/commands/init.ts
669
- async function getGitHubTokenViaPAT() {
670
- console.log(chalk2.dim("\n Create a token at: https://github.com/settings/tokens/new"));
671
- console.log(chalk2.dim(" Required scopes: repo, read:user\n"));
672
- const token = await password({
673
- message: "GitHub Personal Access Token:",
674
- mask: "*"
675
- });
676
- return { token: token.trim() };
818
+ // src/tools/pick/index.ts
819
+ var pick_exports = {};
820
+ __export(pick_exports, {
821
+ definition: () => definition3,
822
+ execute: () => execute3,
823
+ run: () => run3,
824
+ terminal: () => terminal3
825
+ });
826
+ init_github();
827
+ import chalk5 from "chalk";
828
+ import ora3 from "ora";
829
+ import { select as select3 } from "@inquirer/prompts";
830
+ init_github();
831
+
832
+ // src/lib/markdown.ts
833
+ import { marked } from "marked";
834
+ import { markedTerminal } from "marked-terminal";
835
+ marked.use(markedTerminal({ showSectionPrefix: false }));
836
+ function renderMarkdown(text) {
837
+ return marked(text);
677
838
  }
678
- var OAUTH_CLIENT_ID = "Ov23liW4zJ4r2RdZOsCJ";
679
- async function getGitHubTokenViaDeviceFlow() {
680
- let verificationUri = "";
681
- let userCode = "";
682
- const auth = createOAuthDeviceAuth({
683
- clientType: "oauth-app",
684
- clientId: OAUTH_CLIENT_ID,
685
- scopes: ["repo"],
686
- onVerification(verification) {
687
- verificationUri = verification.verification_uri;
688
- userCode = verification.user_code;
689
- console.log("");
690
- console.log(chalk2.bold(" 1. Open this URL in your browser:"));
691
- console.log(" " + chalk2.cyan(verificationUri));
692
- console.log("");
693
- console.log(chalk2.bold(" 2. Enter this code:"));
694
- console.log(" " + chalk2.yellow.bold(userCode));
695
- console.log("");
696
- open(verificationUri).catch(() => {
697
- });
698
- }
699
- });
700
- const spinner = ora("Waiting for authorization in browser...").start();
701
- let token;
839
+
840
+ // src/lib/display.ts
841
+ init_github();
842
+ import chalk2 from "chalk";
843
+ var LABEL_AVAILABLE2 = "techunter:available";
844
+ var LABEL_CLAIMED2 = "techunter:claimed";
845
+ var LABEL_IN_REVIEW2 = "techunter:in-review";
846
+ var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
847
+ function getStatus(issue) {
848
+ if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
849
+ if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
850
+ if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
851
+ if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
852
+ return "unknown";
853
+ }
854
+ function colorStatus(status) {
855
+ const padded = status.padEnd(14);
856
+ switch (status) {
857
+ case "available":
858
+ return chalk2.green(padded);
859
+ case "claimed":
860
+ return chalk2.yellow(padded);
861
+ case "in-review":
862
+ return chalk2.blue(padded);
863
+ case "changes-needed":
864
+ return chalk2.red(padded);
865
+ default:
866
+ return padded;
867
+ }
868
+ }
869
+ function parentIssueFromBranch(branch) {
870
+ if (!isTaskBranch(branch)) return null;
871
+ const match = branch.match(/^task-(\d+)-/);
872
+ return match ? parseInt(match[1], 10) : null;
873
+ }
874
+ function getParentIssueNumber(issue) {
875
+ const target = extractTargetBranch(issue.body);
876
+ if (!target) return null;
877
+ return parentIssueFromBranch(target);
878
+ }
879
+ function printTaskDetail(issue) {
880
+ const divider = chalk2.dim("\u2500".repeat(70));
881
+ const parentNum = getParentIssueNumber(issue);
882
+ console.log("\n" + divider);
883
+ console.log(
884
+ chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014") + (parentNum ? chalk2.dim(` sub-task of #${parentNum}`) : "")
885
+ );
886
+ console.log(chalk2.bold("\n " + issue.title));
887
+ if (issue.body) {
888
+ console.log("");
889
+ console.log(renderMarkdown(issue.body));
890
+ }
891
+ console.log("\n " + chalk2.dim(issue.htmlUrl));
892
+ console.log(divider + "\n");
893
+ }
894
+ async function printTaskList(config) {
702
895
  try {
703
- const result = await auth({ type: "oauth" });
704
- token = result.token;
705
- spinner.succeed("Authorized!");
896
+ const tasks = await listTasks(config);
897
+ const divider = chalk2.dim("\u2500".repeat(70));
898
+ console.log("");
899
+ console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
900
+ console.log(divider);
901
+ if (tasks.length === 0) {
902
+ console.log(chalk2.dim(" (no tasks)"));
903
+ } else {
904
+ let printTask2 = function(t, indent, connector, isLast) {
905
+ const num = `#${t.number}`.padEnd(5);
906
+ const status = colorStatus(getStatus(t));
907
+ const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
908
+ const fullPrefix = indent + connector;
909
+ const maxTitle = 36 - fullPrefix.length;
910
+ const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
911
+ console.log(` ${num}${status}${assignee}${chalk2.dim(fullPrefix)}${title}`);
912
+ const children = childrenOf.get(t.number) ?? [];
913
+ const childIndent = indent + (isLast ? " " : "\u2502 ");
914
+ for (let i = 0; i < children.length; i++) {
915
+ const childIsLast = i === children.length - 1;
916
+ printTask2(children[i], childIndent, childIsLast ? "\u2514\u2500 " : "\u251C\u2500 ", childIsLast);
917
+ }
918
+ };
919
+ var printTask = printTask2;
920
+ const taskMap = new Map(tasks.map((t) => [t.number, t]));
921
+ const childrenOf = /* @__PURE__ */ new Map();
922
+ for (const t of tasks) {
923
+ const parentNum = getParentIssueNumber(t);
924
+ const key = parentNum !== null && taskMap.has(parentNum) ? parentNum : null;
925
+ if (!childrenOf.has(key)) childrenOf.set(key, []);
926
+ childrenOf.get(key).push(t);
927
+ }
928
+ const roots = childrenOf.get(null) ?? [];
929
+ for (let i = 0; i < roots.length; i++) {
930
+ const isLast = i === roots.length - 1;
931
+ printTask2(roots[i], "", isLast ? "\u2514\u2500 " : "\u251C\u2500 ", isLast);
932
+ }
933
+ }
934
+ console.log(divider);
935
+ return tasks;
706
936
  } catch (err) {
707
- spinner.fail("Authorization failed");
708
- throw err;
937
+ console.log(chalk2.yellow(`(Could not load tasks: ${err.message})`));
938
+ return [];
709
939
  }
710
- return { token, clientId: OAUTH_CLIENT_ID };
711
940
  }
712
- async function initCommand() {
713
- console.log(chalk2.bold.cyan("\nTechunter \u2014 Initial Setup\n"));
714
- let detectedOwner = "";
715
- let detectedRepo = "";
716
- const remoteUrl = await getRemoteUrl();
717
- if (remoteUrl) {
718
- const parsed = parseOwnerRepo(remoteUrl);
719
- if (parsed) {
720
- detectedOwner = parsed.owner;
721
- detectedRepo = parsed.repo;
722
- console.log(chalk2.dim(`Detected GitHub repo: ${detectedOwner}/${detectedRepo}
723
- `));
941
+ async function printMyTasks(config) {
942
+ try {
943
+ const me = await getAuthenticatedUser(config);
944
+ const tasks = await listMyTasks(config, me);
945
+ if (tasks.length === 0) return;
946
+ const divider = chalk2.dim("\u2500".repeat(70));
947
+ console.log("");
948
+ console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
949
+ console.log(divider);
950
+ for (const t of tasks) {
951
+ const num = `#${t.number}`.padEnd(5);
952
+ const status = colorStatus(getStatus(t));
953
+ const parentNum = getParentIssueNumber(t);
954
+ const parentTag = parentNum ? chalk2.dim(` (sub of #${parentNum})`) : "";
955
+ const maxTitle = parentNum ? 34 : 46;
956
+ const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
957
+ console.log(` ${num}${status}${title}${parentTag}`);
724
958
  }
725
- }
726
- const authMethod = await select({
727
- message: "How would you like to authenticate with GitHub?",
728
- choices: [
729
- {
730
- name: "Browser login (OAuth) \u2014 open a URL and click Authorize",
731
- value: "device"
732
- },
733
- {
734
- name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
735
- value: "pat"
736
- }
737
- ]
738
- });
739
- let githubToken;
740
- let githubClientId;
741
- if (authMethod === "device") {
742
- const result = await getGitHubTokenViaDeviceFlow();
743
- githubToken = result.token;
744
- githubClientId = result.clientId;
745
- } else {
746
- const result = await getGitHubTokenViaPAT();
747
- githubToken = result.token;
748
- }
749
- const providerChoice = await select({
750
- message: "AI provider:",
751
- choices: [
752
- { name: `OpenRouter (recommended) ${chalk2.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
753
- { name: "Custom (specify base URL and model)", value: "custom" }
754
- ]
755
- });
756
- let aiBaseUrl;
757
- let aiModel;
758
- if (providerChoice === "custom") {
759
- aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
760
- aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
761
- }
762
- const apiKeyHint = providerChoice === "openrouter" ? chalk2.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk2.dim(" API key for your provider\n");
763
- console.log(apiKeyHint);
764
- const aiApiKey = await password({
765
- message: "API Key:",
766
- mask: "*"
767
- });
768
- let owner = detectedOwner;
769
- let repo = detectedRepo;
770
- if (!owner || !repo) {
771
- owner = await input({
772
- message: "GitHub repo owner (user or org):",
773
- required: true
774
- });
775
- repo = await input({
776
- message: "GitHub repo name:",
777
- required: true
778
- });
779
- }
780
- const config = {
781
- githubToken,
782
- githubClientId,
783
- aiApiKey: aiApiKey.trim(),
784
- ...aiBaseUrl ? { aiBaseUrl } : {},
785
- ...aiModel ? { aiModel } : {},
786
- github: {
787
- owner: owner.trim(),
788
- repo: repo.trim()
789
- }
790
- };
791
- setConfig(config);
792
- const spinner = ora("Setting up GitHub labels...").start();
793
- try {
794
- await ensureLabels(config);
795
- spinner.succeed("GitHub labels created");
796
- } catch (err) {
797
- spinner.fail("Failed to create labels (check token permissions)");
798
- console.error(chalk2.red(String(err)));
799
- }
800
- console.log(chalk2.green("\nSetup complete!"));
801
- console.log(chalk2.dim(`Config saved to: ${getConfigPath()}
802
- `));
803
- }
804
-
805
- // src/commands/config.ts
806
- import { input as input2, password as password2, select as select2 } from "@inquirer/prompts";
807
- import chalk3 from "chalk";
808
- async function configCommand() {
809
- let config;
810
- try {
811
- config = getConfig();
812
- } catch {
813
- console.error(chalk3.red("No config found. Run `tch init` first."));
814
- process.exit(1);
815
- }
816
- console.log(chalk3.bold.cyan("\nTechunter \u2014 Settings\n"));
817
- console.log(chalk3.dim(`Config file: ${getConfigPath()}
818
- `));
819
- const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
820
- const currentModel = config.aiModel ?? DEFAULT_MODEL;
821
- const currentBaseBranch = config.baseBranch ?? "main";
822
- const field = await select2({
823
- message: "Which setting to change?",
824
- choices: [
825
- { name: `GitHub repo ${chalk3.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
826
- { name: `Base branch ${chalk3.dim(currentBaseBranch)}`, value: "baseBranch" },
827
- { name: `AI base URL ${chalk3.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
828
- { name: `AI model ${chalk3.dim(currentModel)}`, value: "aiModel" },
829
- { name: `AI API Key ${chalk3.dim("(hidden)")}`, value: "aiApiKey" },
830
- { name: `GitHub Token ${chalk3.dim("(hidden)")}`, value: "githubToken" },
831
- { name: "Cancel", value: "cancel" }
832
- ]
833
- });
834
- if (field === "cancel") return;
835
- if (field === "baseBranch") {
836
- const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
837
- if (val.trim()) {
838
- setConfig({ baseBranch: val.trim() });
839
- console.log(chalk3.green(`
840
- Base branch set to: ${val.trim()}
841
- `));
842
- }
843
- } else if (field === "repo") {
844
- const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
845
- const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
846
- setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
847
- console.log(chalk3.green(`
848
- Repo set to: ${owner.trim()}/${repo.trim()}
849
- `));
850
- } else if (field === "aiBaseUrl") {
851
- const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
852
- if (val.trim()) {
853
- setConfig({ aiBaseUrl: val.trim() });
854
- console.log(chalk3.green(`
855
- AI base URL set to: ${val.trim()}
856
- `));
857
- }
858
- } else if (field === "aiModel") {
859
- const val = await input2({ message: "AI model name:", default: currentModel });
860
- if (val.trim()) {
861
- setConfig({ aiModel: val.trim() });
862
- console.log(chalk3.green(`
863
- AI model set to: ${val.trim()}
864
- `));
865
- }
866
- } else if (field === "aiApiKey") {
867
- const val = await password2({ message: "New AI API Key:", mask: "*" });
868
- if (val.trim()) {
869
- setConfig({ aiApiKey: val.trim() });
870
- console.log(chalk3.green("\nAI API Key updated.\n"));
871
- }
872
- } else if (field === "githubToken") {
873
- const val = await password2({ message: "New GitHub Token:", mask: "*" });
874
- if (val.trim()) {
875
- setConfig({ githubToken: val.trim() });
876
- console.log(chalk3.green("\nGitHub Token updated.\n"));
877
- }
878
- }
879
- }
880
-
881
- // src/index.ts
882
- init_github();
883
-
884
- // src/lib/agent.ts
885
- import ora14 from "ora";
886
- import chalk13 from "chalk";
887
-
888
- // src/tools/pick/index.ts
889
- var pick_exports = {};
890
- __export(pick_exports, {
891
- definition: () => definition3,
892
- execute: () => execute3,
893
- run: () => run3,
894
- terminal: () => terminal3
895
- });
896
- init_github();
897
- import chalk8 from "chalk";
898
- import ora4 from "ora";
899
- import { select as select5 } from "@inquirer/prompts";
900
- init_github();
901
-
902
- // src/lib/markdown.ts
903
- import { marked } from "marked";
904
- import { markedTerminal } from "marked-terminal";
905
- marked.use(markedTerminal({ showSectionPrefix: false }));
906
- function renderMarkdown(text) {
907
- return marked(text);
908
- }
909
-
910
- // src/lib/display.ts
911
- init_github();
912
- import chalk4 from "chalk";
913
- var LABEL_AVAILABLE2 = "techunter:available";
914
- var LABEL_CLAIMED2 = "techunter:claimed";
915
- var LABEL_IN_REVIEW2 = "techunter:in-review";
916
- var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
917
- function getStatus(issue) {
918
- if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
919
- if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
920
- if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
921
- if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
922
- return "unknown";
923
- }
924
- function colorStatus(status) {
925
- const padded = status.padEnd(14);
926
- switch (status) {
927
- case "available":
928
- return chalk4.green(padded);
929
- case "claimed":
930
- return chalk4.yellow(padded);
931
- case "in-review":
932
- return chalk4.blue(padded);
933
- case "changes-needed":
934
- return chalk4.red(padded);
935
- default:
936
- return padded;
937
- }
938
- }
939
- function printTaskDetail(issue) {
940
- const divider = chalk4.dim("\u2500".repeat(70));
941
- console.log("\n" + divider);
942
- console.log(
943
- chalk4.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk4.dim(issue.assignee ? `@${issue.assignee}` : "\u2014")
944
- );
945
- console.log(chalk4.bold("\n " + issue.title));
946
- if (issue.body) {
947
- console.log("");
948
- console.log(renderMarkdown(issue.body));
949
- }
950
- console.log("\n " + chalk4.dim(issue.htmlUrl));
951
- console.log(divider + "\n");
952
- }
953
- async function printTaskList(config) {
954
- try {
955
- const tasks = await listTasks(config);
956
- const divider = chalk4.dim("\u2500".repeat(70));
957
- console.log("");
958
- console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
959
- console.log(divider);
960
- if (tasks.length === 0) {
961
- console.log(chalk4.dim(" (no tasks)"));
962
- } else {
963
- for (const t of tasks) {
964
- const num = `#${t.number}`.padEnd(5);
965
- const status = colorStatus(getStatus(t));
966
- const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
967
- const title = t.title.length > 36 ? t.title.slice(0, 33) + "..." : t.title;
968
- console.log(` ${num}${status}${assignee}${title}`);
969
- }
970
- }
971
- console.log(divider);
972
- return tasks;
973
- } catch (err) {
974
- console.log(chalk4.yellow(`(Could not load tasks: ${err.message})`));
975
- return [];
976
- }
977
- }
978
- async function printMyTasks(config) {
979
- try {
980
- const me = await getAuthenticatedUser(config);
981
- const tasks = await listMyTasks(config, me);
982
- if (tasks.length === 0) return;
983
- const divider = chalk4.dim("\u2500".repeat(70));
984
- console.log("");
985
- console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
986
- console.log(divider);
987
- for (const t of tasks) {
988
- const num = `#${t.number}`.padEnd(5);
989
- const status = colorStatus(getStatus(t));
990
- const title = t.title.length > 46 ? t.title.slice(0, 43) + "..." : t.title;
991
- console.log(` ${num}${status}${title}`);
992
- }
993
- console.log(divider);
994
- const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
995
- if (rejectedTasks.length > 0) {
996
- let currentBranch = "";
997
- try {
998
- currentBranch = await getCurrentBranch();
999
- } catch {
959
+ console.log(divider);
960
+ const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
961
+ if (rejectedTasks.length > 0) {
962
+ let currentBranch = "";
963
+ try {
964
+ currentBranch = await getCurrentBranch();
965
+ } catch {
1000
966
  }
1001
967
  console.log("");
1002
968
  for (const t of rejectedTasks) {
1003
- const expectedBranch = makeBranchName(t.number, t.title);
1004
- const onCorrectBranch = currentBranch === expectedBranch;
969
+ const taskBranch = t.assignee ? makeTaskBranchName(t.number, t.assignee) : `task-${t.number}`;
970
+ const onCorrectBranch = currentBranch === taskBranch;
1005
971
  console.log(
1006
- chalk4.red.bold(" \u26A0 Changes requested") + chalk4.red(` on #${t.number} "${t.title}"`)
972
+ chalk2.red.bold(" \u26A0 Changes requested") + chalk2.red(` on #${t.number} "${t.title}"`)
1007
973
  );
1008
974
  if (!onCorrectBranch) {
1009
975
  console.log(
1010
- chalk4.dim(" Switch branch: ") + chalk4.cyan(`git checkout ${expectedBranch}`)
976
+ chalk2.dim(" Switch branch: ") + chalk2.cyan(`git checkout ${taskBranch}`)
1011
977
  );
1012
978
  }
1013
979
  }
@@ -1019,7 +985,7 @@ async function printMyTasks(config) {
1019
985
 
1020
986
  // src/lib/launch.ts
1021
987
  import { spawn } from "child_process";
1022
- import chalk5 from "chalk";
988
+ import chalk3 from "chalk";
1023
989
  function buildClaudePrompt(issue, branch) {
1024
990
  const lines = [
1025
991
  `You are working on task #${issue.number}: ${issue.title}`,
@@ -1035,14 +1001,14 @@ function buildClaudePrompt(issue, branch) {
1035
1001
  }
1036
1002
  async function launchClaudeCode(issue, branch) {
1037
1003
  const prompt = buildClaudePrompt(issue, branch);
1038
- console.log(chalk5.dim("\n Launching Claude Code\u2026\n"));
1004
+ console.log(chalk3.dim("\n Launching Claude Code\u2026\n"));
1039
1005
  await new Promise((resolve) => {
1040
1006
  const safePrompt = prompt.replace(/\r?\n/g, " ").replace(/"/g, "'");
1041
1007
  const child = spawn(`claude "${safePrompt}"`, [], { stdio: "inherit", shell: true });
1042
1008
  child.on("close", () => resolve());
1043
1009
  child.on("error", () => {
1044
1010
  console.log(
1045
- chalk5.yellow(
1011
+ chalk3.yellow(
1046
1012
  " Could not launch claude. Make sure Claude Code is installed:\n npm install -g @anthropic-ai/claude-code"
1047
1013
  )
1048
1014
  );
@@ -1060,78 +1026,9 @@ __export(submit_exports, {
1060
1026
  terminal: () => terminal
1061
1027
  });
1062
1028
  init_github();
1063
- import chalk7 from "chalk";
1064
- import ora2 from "ora";
1065
- import { select as select3, input as promptInput } from "@inquirer/prompts";
1066
-
1067
- // src/lib/agent-ui.ts
1068
- import chalk6 from "chalk";
1069
- function formatInput(input3) {
1070
- return Object.entries(input3).map(([k, v]) => {
1071
- if (typeof v === "number") return `${k}=${v}`;
1072
- if (typeof v === "string") {
1073
- if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
1074
- return `${k}="${v}"`;
1075
- }
1076
- return `${k}=${JSON.stringify(v)}`;
1077
- }).join(" ");
1078
- }
1079
- function summarize(result) {
1080
- const first = result.split("\n").find((l) => l.trim()) ?? result;
1081
- return first.length > 100 ? first.slice(0, 97) + "..." : first;
1082
- }
1083
- function printToolCall(name, input3) {
1084
- const params = formatInput(input3);
1085
- console.log(` ${chalk6.cyan("\u2192")} ${chalk6.bold(name)}${params ? " " + chalk6.dim(params) : ""}`);
1086
- }
1087
- function printToolResult(result) {
1088
- const ok = !result.startsWith("Error:");
1089
- const icon = ok ? chalk6.green("\u2713") : chalk6.red("\u2717");
1090
- console.log(` ${icon} ${chalk6.dim(summarize(result))}`);
1091
- }
1092
-
1093
- // src/lib/sub-agent.ts
1094
- async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
1095
- const client = createClient(config);
1096
- const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
1097
- const tools2 = selected.map((m) => m.definition);
1098
- const messages = [
1099
- { role: "system", content: systemPrompt },
1100
- { role: "user", content: userMessage }
1101
- ];
1102
- const MAX_ITERATIONS = 100;
1103
- let iterations = 0;
1104
- for (; ; ) {
1105
- if (++iterations > MAX_ITERATIONS) {
1106
- throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
1107
- }
1108
- const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
1109
- const choice = res.choices[0];
1110
- messages.push({
1111
- role: "assistant",
1112
- content: choice.message.content ?? null,
1113
- ...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
1114
- });
1115
- if (choice.finish_reason === "stop") {
1116
- return choice.message.content ?? "";
1117
- }
1118
- if (choice.finish_reason === "tool_calls") {
1119
- for (const tc of choice.message.tool_calls ?? []) {
1120
- let input3;
1121
- try {
1122
- input3 = JSON.parse(tc.function.arguments);
1123
- } catch {
1124
- input3 = {};
1125
- }
1126
- printToolCall(tc.function.name, input3);
1127
- const mod = selected.find((m) => m.definition.function.name === tc.function.name);
1128
- const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
1129
- printToolResult(result);
1130
- messages.push({ role: "tool", tool_call_id: tc.id, content: result });
1131
- }
1132
- }
1133
- }
1134
- }
1029
+ import chalk4 from "chalk";
1030
+ import ora from "ora";
1031
+ import { select, input as promptInput } from "@inquirer/prompts";
1135
1032
 
1136
1033
  // src/tools/submit/prompts.ts
1137
1034
  var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. The diff provided shows all changes on this worker branch since the task was claimed. The branch is shared across tasks, so the diff may contain changes unrelated to this specific task \u2014 use the task title and acceptance criteria to identify which changes are relevant, and ignore the rest. Use run_command to run tests/lint if needed, and read_file to inspect specific files. Then output your review: for each acceptance criterion mark \u2705 met or \u274C not met with a one-line reason. End with an overall verdict line: Ready to submit / Not ready. Reply in the same language as the task.";
@@ -1169,11 +1066,21 @@ var definition = {
1169
1066
  };
1170
1067
  async function run(_input, config) {
1171
1068
  const taskState = getConfig().taskState;
1172
- const issueNumber = taskState?.activeIssueNumber;
1069
+ const currentBranch = await getCurrentBranch();
1070
+ let issueNumber = taskState?.activeIssueNumber && taskState?.activeBranch && currentBranch === taskState.activeBranch ? taskState.activeIssueNumber : void 0;
1173
1071
  if (!issueNumber) {
1174
- return "No active task found. Claim a task first with /pick.";
1072
+ const fromBranch = parseIssueNumberFromBranch(currentBranch);
1073
+ if (fromBranch) {
1074
+ issueNumber = fromBranch;
1075
+ } else {
1076
+ const found = await getIssueNumberFromBranch(config, currentBranch);
1077
+ if (!found) {
1078
+ return "No active task found. Claim a task first with /pick.";
1079
+ }
1080
+ issueNumber = found.issueNumber;
1081
+ }
1175
1082
  }
1176
- let spinner = ora2("Loading task and diff\u2026").start();
1083
+ let spinner = ora("Loading task and diff\u2026").start();
1177
1084
  const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1178
1085
  const [issue, diff, me] = await Promise.all([
1179
1086
  getTask(config, issueNumber),
@@ -1181,12 +1088,19 @@ async function run(_input, config) {
1181
1088
  getAuthenticatedUser(config)
1182
1089
  ]);
1183
1090
  spinner.stop();
1184
- const workerBranch = makeWorkerBranchName(issue.author ?? me);
1091
+ const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
1185
1092
  const branch = await getCurrentBranch();
1186
1093
  const isSelfSubmit = issue.author !== null && issue.author === me;
1094
+ spinner = ora("Checking for open sub-tasks\u2026").start();
1095
+ const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1096
+ spinner.stop();
1097
+ if (openSubtaskNumbers.length > 0) {
1098
+ return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open:
1099
+ ` + openSubtaskNumbers.map((n) => ` - #${n}`).join("\n") + "\nComplete all sub-tasks before submitting.";
1100
+ }
1187
1101
  let review = "";
1188
1102
  if (!isSelfSubmit) {
1189
- const reviewSpinner = ora2("Reviewing changes\u2026").start();
1103
+ const reviewSpinner = ora("Reviewing changes\u2026").start();
1190
1104
  try {
1191
1105
  review = await reviewChanges(config, issueNumber, issue, diff);
1192
1106
  } catch (err) {
@@ -1194,19 +1108,19 @@ async function run(_input, config) {
1194
1108
  }
1195
1109
  reviewSpinner.stop();
1196
1110
  }
1197
- const divider = chalk7.dim("\u2500".repeat(70));
1111
+ const divider = chalk4.dim("\u2500".repeat(70));
1198
1112
  console.log("\n" + divider);
1199
1113
  if (isSelfSubmit) {
1200
- console.log(chalk7.yellow(` Self-submit detected \u2014 AI review skipped.`));
1114
+ console.log(chalk4.yellow(` Self-submit detected \u2014 AI review skipped.`));
1201
1115
  } else {
1202
- console.log(chalk7.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1116
+ console.log(chalk4.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1203
1117
  console.log(divider);
1204
1118
  console.log(renderMarkdown(review));
1205
1119
  }
1206
1120
  console.log(divider + "\n");
1207
1121
  let shouldProceed;
1208
1122
  try {
1209
- shouldProceed = await select3({
1123
+ shouldProceed = await select({
1210
1124
  message: `Submit task #${issueNumber}?`,
1211
1125
  choices: [
1212
1126
  { name: "Yes, submit", value: true },
@@ -1227,7 +1141,7 @@ async function run(_input, config) {
1227
1141
  return "Submit cancelled.";
1228
1142
  }
1229
1143
  if (!commitMessage.trim()) return "Submit cancelled.";
1230
- spinner = ora2("Committing and pushing\u2026").start();
1144
+ spinner = ora("Committing and pushing\u2026").start();
1231
1145
  try {
1232
1146
  await stageAllAndCommit(commitMessage.trim());
1233
1147
  spinner.stop();
@@ -1236,52 +1150,71 @@ async function run(_input, config) {
1236
1150
  return `Commit failed: ${err.message}`;
1237
1151
  }
1238
1152
  if (isSelfSubmit) {
1239
- spinner = ora2("Closing issue\u2026").start();
1153
+ spinner = ora("Closing issue\u2026").start();
1240
1154
  try {
1241
1155
  await closeTask(config, issueNumber);
1242
1156
  spinner.stop();
1243
1157
  } catch (err) {
1244
1158
  spinner.stop();
1245
- console.error(chalk7.yellow(`Warning: failed to close issue: ${err.message}`));
1159
+ console.error(chalk4.yellow(`Warning: failed to close issue: ${err.message}`));
1246
1160
  }
1247
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1161
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1248
1162
  return `Task #${issueNumber} committed and closed.
1249
1163
  Commit: "${commitMessage.trim()}"`;
1250
1164
  }
1251
- spinner = ora2("Creating pull request\u2026").start();
1165
+ spinner = ora("Checking for existing PR\u2026").start();
1166
+ const existingPR = await getTaskPR(config, issueNumber);
1167
+ spinner.stop();
1252
1168
  let prUrl;
1253
- try {
1254
- const prBody = [
1255
- `Closes #${issueNumber}`,
1256
- issue.body ? `
1169
+ if (existingPR) {
1170
+ prUrl = existingPR.url;
1171
+ console.log(chalk4.dim(` Existing PR found: ${prUrl} \u2014 updating.`));
1172
+ } else {
1173
+ spinner = ora("Creating pull request\u2026").start();
1174
+ try {
1175
+ await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1176
+ const prBody = [
1177
+ `Closes #${issueNumber}`,
1178
+ issue.body ? `
1257
1179
  ${issue.body}` : "",
1258
- review ? `
1180
+ review ? `
1259
1181
  ## AI Review
1260
1182
  ${review}` : ""
1261
- ].join("\n").trim();
1262
- prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
1263
- spinner.stop();
1264
- } catch (err) {
1265
- spinner.stop();
1266
- return `Committed but PR creation failed: ${err.message}`;
1183
+ ].join("\n").trim();
1184
+ prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1185
+ spinner.stop();
1186
+ } catch (err) {
1187
+ spinner.stop();
1188
+ return `Committed but PR creation failed: ${err.message}`;
1189
+ }
1267
1190
  }
1268
- spinner = ora2("Marking as in-review\u2026").start();
1191
+ spinner = ora("Marking as in-review\u2026").start();
1269
1192
  try {
1270
1193
  await markInReview(config, issueNumber);
1271
1194
  spinner.stop();
1272
1195
  } catch (err) {
1273
1196
  spinner.stop();
1274
- return `PR created (${prUrl}) but failed to update label: ${err.message}`;
1197
+ return `PR ${existingPR ? "updated" : "created"} (${prUrl}) but failed to update label: ${err.message}`;
1275
1198
  }
1276
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1277
- return `Task #${issueNumber} submitted.
1199
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1200
+ return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
1278
1201
  Commit: "${commitMessage.trim()}"
1279
1202
  PR: ${prUrl}`;
1280
1203
  }
1281
1204
  async function execute(input3, config) {
1282
1205
  const taskState = getConfig().taskState;
1283
- const issueNumber = taskState?.activeIssueNumber;
1284
- if (!issueNumber) return "No active task found. Claim a task first.";
1206
+ let issueNumber = taskState?.activeIssueNumber;
1207
+ if (!issueNumber) {
1208
+ const currentBranch = await getCurrentBranch();
1209
+ const fromBranch = parseIssueNumberFromBranch(currentBranch);
1210
+ if (fromBranch) {
1211
+ issueNumber = fromBranch;
1212
+ } else {
1213
+ const found = await getIssueNumberFromBranch(config, currentBranch);
1214
+ if (!found) return "No active task found. Claim a task first.";
1215
+ issueNumber = found.issueNumber;
1216
+ }
1217
+ }
1285
1218
  const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1286
1219
  const [issue, diff, branch, me] = await Promise.all([
1287
1220
  getTask(config, issueNumber),
@@ -1289,7 +1222,11 @@ async function execute(input3, config) {
1289
1222
  getCurrentBranch(),
1290
1223
  getAuthenticatedUser(config)
1291
1224
  ]);
1292
- const workerBranch = makeWorkerBranchName(issue.author ?? me);
1225
+ const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
1226
+ const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1227
+ if (openSubtaskNumbers.length > 0) {
1228
+ return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open: ` + openSubtaskNumbers.map((n) => `#${n}`).join(", ");
1229
+ }
1293
1230
  const isSelfSubmit = issue.author !== null && issue.author === me;
1294
1231
  let review = "";
1295
1232
  if (!isSelfSubmit) {
@@ -1310,29 +1247,36 @@ async function execute(input3, config) {
1310
1247
  await closeTask(config, issueNumber);
1311
1248
  } catch {
1312
1249
  }
1313
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1250
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1314
1251
  return `Task #${issueNumber} committed and closed.
1315
1252
  Commit: "${commitMessage}"`;
1316
1253
  }
1254
+ const existingPR = await getTaskPR(config, issueNumber);
1317
1255
  let prUrl;
1318
- try {
1319
- const prBody = [
1320
- `Closes #${issueNumber}`,
1321
- issue.body ? `
1256
+ if (existingPR) {
1257
+ prUrl = existingPR.url;
1258
+ } else {
1259
+ try {
1260
+ await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1261
+ const prBody = [
1262
+ `Closes #${issueNumber}`,
1263
+ issue.body ? `
1322
1264
  ${issue.body}` : "",
1323
- review ? `
1265
+ review ? `
1324
1266
  ## AI Review
1325
1267
  ${review}` : ""
1326
- ].join("\n").trim();
1327
- prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
1328
- } catch (err) {
1329
- return `Committed but PR creation failed: ${err.message}`;
1268
+ ].join("\n").trim();
1269
+ prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1270
+ } catch (err) {
1271
+ return `Committed but PR creation failed: ${err.message}`;
1272
+ }
1330
1273
  }
1331
1274
  try {
1332
1275
  await markInReview(config, issueNumber);
1333
1276
  } catch {
1334
1277
  }
1335
- return `Task #${issueNumber} submitted.
1278
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1279
+ return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
1336
1280
  Review:
1337
1281
  ${review}
1338
1282
  Commit: "${commitMessage}"
@@ -1349,8 +1293,8 @@ __export(close_exports, {
1349
1293
  terminal: () => terminal2
1350
1294
  });
1351
1295
  init_github();
1352
- import { select as select4 } from "@inquirer/prompts";
1353
- import ora3 from "ora";
1296
+ import { select as select2 } from "@inquirer/prompts";
1297
+ import ora2 from "ora";
1354
1298
  var definition2 = {
1355
1299
  type: "function",
1356
1300
  function: {
@@ -1376,7 +1320,7 @@ async function run2(input3, config) {
1376
1320
  }
1377
1321
  if (tasks.length === 0) return "No tasks found.";
1378
1322
  try {
1379
- issueNumber = await select4({
1323
+ issueNumber = await select2({
1380
1324
  message: "Select task to close:",
1381
1325
  choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
1382
1326
  });
@@ -1386,7 +1330,7 @@ async function run2(input3, config) {
1386
1330
  }
1387
1331
  let confirmed;
1388
1332
  try {
1389
- confirmed = await select4({
1333
+ confirmed = await select2({
1390
1334
  message: `Close task #${issueNumber}?`,
1391
1335
  choices: [
1392
1336
  { name: "Yes, close it", value: true },
@@ -1397,7 +1341,7 @@ async function run2(input3, config) {
1397
1341
  return "Cancelled.";
1398
1342
  }
1399
1343
  if (!confirmed) return "Cancelled.";
1400
- const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
1344
+ const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
1401
1345
  try {
1402
1346
  await closeTask(config, issueNumber);
1403
1347
  spinner.stop();
@@ -1409,7 +1353,7 @@ async function run2(input3, config) {
1409
1353
  }
1410
1354
  async function execute2(input3, config) {
1411
1355
  const issueNumber = input3["issue_number"];
1412
- const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
1356
+ const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
1413
1357
  try {
1414
1358
  await closeTask(config, issueNumber);
1415
1359
  spinner.stop();
@@ -1455,7 +1399,7 @@ async function run3(input3, config) {
1455
1399
  }
1456
1400
  if (tasks.length === 0) return "No tasks found.";
1457
1401
  try {
1458
- chosenNumber = await select5({
1402
+ chosenNumber = await select3({
1459
1403
  message: "Select a task:",
1460
1404
  choices: tasks.map((t) => ({
1461
1405
  name: `#${String(t.number).padEnd(4)} ${colorStatus(getStatus(t))} ${t.title}`,
@@ -1479,9 +1423,9 @@ async function run3(input3, config) {
1479
1423
  const comments = await listComments(config, issue.number, 1);
1480
1424
  if (comments.length > 0) {
1481
1425
  const c = comments[0];
1482
- const divider = chalk8.dim("\u2500".repeat(70));
1426
+ const divider = chalk5.dim("\u2500".repeat(70));
1483
1427
  console.log(
1484
- chalk8.red.bold(" Latest rejection feedback") + chalk8.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
1428
+ chalk5.red.bold(" Latest rejection feedback") + chalk5.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
1485
1429
  );
1486
1430
  console.log(divider);
1487
1431
  console.log(renderMarkdown(c.body));
@@ -1491,22 +1435,36 @@ async function run3(input3, config) {
1491
1435
  }
1492
1436
  }
1493
1437
  const actions = [];
1494
- if (status === "available") actions.push({ name: "Claim this task", value: "claim" });
1495
- if (status === "claimed" || status === "changes-needed") actions.push({ name: "Submit this task", value: "submit" });
1438
+ if (status === "available") {
1439
+ actions.push({ name: "Claim this task", value: "claim" });
1440
+ }
1441
+ if (status === "claimed") {
1442
+ actions.push({ name: "Submit this task", value: "submit" });
1443
+ }
1444
+ if (status === "changes-needed") {
1445
+ const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1446
+ const me = await getAuthenticatedUser2(config);
1447
+ const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1448
+ const currentBranch = await getCurrentBranch();
1449
+ if (currentBranch === taskBranch) {
1450
+ actions.push({ name: "Submit this task (fixes done)", value: "submit" });
1451
+ } else {
1452
+ actions.push({ name: `Switch to ${taskBranch} to fix`, value: "switch-fix" });
1453
+ }
1454
+ }
1496
1455
  actions.push({ name: "Close this task", value: "close" });
1497
1456
  actions.push({ name: "Nothing, just viewing", value: "none" });
1498
1457
  let action;
1499
1458
  try {
1500
- action = await select5({ message: "Action:", choices: actions });
1459
+ action = await select3({ message: "Action:", choices: actions });
1501
1460
  } catch {
1502
1461
  return "Cancelled.";
1503
1462
  }
1504
1463
  if (action === "none") return `Viewed task #${issue.number}.`;
1505
1464
  if (action === "claim") {
1506
1465
  try {
1507
- const { getAuthenticatedUser: getAuthenticatedUser2, listMyTasks: listMyTasks2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1508
- const me = await getAuthenticatedUser2(config);
1509
- const myTasks = await listMyTasks2(config, me);
1466
+ const me = await getAuthenticatedUser(config);
1467
+ const myTasks = await listMyTasks(config, me);
1510
1468
  const activeTask = myTasks.find((t) => {
1511
1469
  const labels = t.labels;
1512
1470
  return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
@@ -1515,37 +1473,65 @@ async function run3(input3, config) {
1515
1473
  return `You already have an active task: #${activeTask.number} "${activeTask.title}"
1516
1474
  Finish or submit it before claiming a new one.`;
1517
1475
  }
1518
- let spinner = ora4(`Claiming #${issue.number}\u2026`).start();
1476
+ let stashed = false;
1477
+ if (await hasUncommittedChanges()) {
1478
+ let choice;
1479
+ try {
1480
+ choice = await select3({
1481
+ message: "You have uncommitted changes. What would you like to do?",
1482
+ choices: [
1483
+ { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1484
+ { name: "Cancel", value: "cancel" }
1485
+ ]
1486
+ });
1487
+ } catch {
1488
+ choice = "cancel";
1489
+ }
1490
+ if (choice === "cancel") return "Cancelled.";
1491
+ await stash(`tch: before claiming #${issue.number}`);
1492
+ stashed = true;
1493
+ console.log(chalk5.dim(" Changes stashed. Run `git stash pop` after you finish this task to restore them."));
1494
+ }
1495
+ let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
1519
1496
  await claimTask(config, issue.number, me);
1520
1497
  spinner.stop();
1521
- const workerBranch = makeWorkerBranchName(me);
1498
+ const taskBranch = makeTaskBranchName(issue.number, me);
1522
1499
  const taskBase = extractBaseCommit(issue.body);
1523
- spinner = ora4(`Switching to ${workerBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
1500
+ spinner = ora3(`Creating branch ${taskBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
1524
1501
  try {
1525
1502
  if (taskBase) {
1526
- await resetOrCreateBranch(workerBranch, taskBase);
1503
+ await checkoutFromCommit(taskBranch, taskBase);
1527
1504
  } else {
1528
- await switchToBranchOrCreate(workerBranch);
1505
+ await switchToBranchOrCreate(taskBranch);
1529
1506
  }
1530
1507
  spinner.stop();
1531
- spinner = ora4("Pushing worker branch\u2026").start();
1508
+ spinner = ora3("Pushing task branch\u2026").start();
1532
1509
  try {
1533
- await pushBranch(workerBranch);
1510
+ await pushBranch(taskBranch);
1534
1511
  spinner.stop();
1535
1512
  } catch {
1536
- spinner.warn("Could not push worker branch");
1513
+ spinner.warn("Could not push task branch \u2014 will push on submit");
1537
1514
  }
1538
- } catch {
1539
- spinner.warn(`Could not switch to ${workerBranch}`);
1515
+ } catch (err) {
1516
+ spinner.warn(`Could not switch to ${taskBranch}`);
1517
+ if (stashed) {
1518
+ try {
1519
+ await stashPop();
1520
+ console.log(chalk5.dim(" Restored stashed changes."));
1521
+ } catch {
1522
+ console.log(chalk5.yellow(" Warning: could not restore stash automatically. Run `git stash pop` manually."));
1523
+ }
1524
+ }
1525
+ throw err;
1540
1526
  }
1541
1527
  const baseCommit = await getCurrentCommit();
1542
- setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
1543
- console.log(chalk8.green(`
1544
- Claimed! Branch: ${workerBranch} (base: ${baseCommit.slice(0, 7)})
1528
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1529
+ console.log(chalk5.green(`
1530
+ Claimed! Branch: ${taskBranch} (base: ${baseCommit.slice(0, 7)})
1545
1531
  `));
1546
1532
  let openClaude;
1547
1533
  try {
1548
- openClaude = await select5({
1534
+ openClaude = await select3({
1549
1535
  message: "Open Claude Code for this task?",
1550
1536
  choices: [
1551
1537
  { name: "Yes, start coding now", value: true },
@@ -1555,12 +1541,58 @@ Finish or submit it before claiming a new one.`;
1555
1541
  } catch {
1556
1542
  openClaude = false;
1557
1543
  }
1558
- if (openClaude) await launchClaudeCode(issue, workerBranch);
1559
- return `Task #${issue.number} claimed. Branch: ${workerBranch}`;
1544
+ if (openClaude) await launchClaudeCode(issue, taskBranch);
1545
+ return `Task #${issue.number} claimed. Branch: ${taskBranch}`;
1560
1546
  } catch (err) {
1561
1547
  return `Error claiming task: ${err.message}`;
1562
1548
  }
1563
1549
  }
1550
+ if (action === "switch-fix") {
1551
+ const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1552
+ const me = await getAuthenticatedUser2(config);
1553
+ const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1554
+ let stashed = false;
1555
+ if (await hasUncommittedChanges()) {
1556
+ let choice;
1557
+ try {
1558
+ choice = await select3({
1559
+ message: "You have uncommitted changes. What would you like to do?",
1560
+ choices: [
1561
+ { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1562
+ { name: "Cancel", value: "cancel" }
1563
+ ]
1564
+ });
1565
+ } catch {
1566
+ choice = "cancel";
1567
+ }
1568
+ if (choice === "cancel") return "Cancelled.";
1569
+ await stash(`tch: before switching to ${taskBranch}`);
1570
+ stashed = true;
1571
+ console.log(chalk5.dim(" Changes stashed. Run `git stash pop` to restore them later."));
1572
+ }
1573
+ const spinner = ora3(`Switching to ${taskBranch}\u2026`).start();
1574
+ try {
1575
+ await switchToBranchOrCreate(taskBranch);
1576
+ spinner.stop();
1577
+ } catch (err) {
1578
+ spinner.warn(`Could not switch to ${taskBranch}: ${err.message}`);
1579
+ if (stashed) {
1580
+ try {
1581
+ await stashPop();
1582
+ console.log(chalk5.dim(" Restored stashed changes."));
1583
+ } catch {
1584
+ console.log(chalk5.yellow(" Run `git stash pop` manually to restore your changes."));
1585
+ }
1586
+ }
1587
+ return `Error: ${err.message}`;
1588
+ }
1589
+ const baseCommit = extractBaseCommit(issue.body) ?? await getCurrentCommit();
1590
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1591
+ console.log(chalk5.green(`
1592
+ Switched to ${taskBranch}. Fix the issues then run /submit.
1593
+ `));
1594
+ return `Switched to ${taskBranch} for task #${issue.number}.`;
1595
+ }
1564
1596
  if (action === "submit") return run({}, config);
1565
1597
  if (action === "close") return run2({ issue_number: issue.number }, config);
1566
1598
  return "Cancelled.";
@@ -1589,28 +1621,31 @@ async function execute3(input3, config) {
1589
1621
  if (activeTask) {
1590
1622
  return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
1591
1623
  }
1624
+ if (await hasUncommittedChanges()) {
1625
+ return "Cannot claim: you have uncommitted changes. Commit or stash them first (git stash).";
1626
+ }
1592
1627
  try {
1593
1628
  await claimTask(config, issueNumber, me);
1594
1629
  } catch (err) {
1595
1630
  return `Error claiming task: ${err.message}`;
1596
1631
  }
1597
- const workerBranch = makeWorkerBranchName(me);
1632
+ const taskBranch = makeTaskBranchName(issue.number, me);
1598
1633
  const taskBase = extractBaseCommit(issue.body);
1599
1634
  try {
1600
1635
  if (taskBase) {
1601
- await resetOrCreateBranch(workerBranch, taskBase);
1636
+ await checkoutFromCommit(taskBranch, taskBase);
1602
1637
  } else {
1603
- await switchToBranchOrCreate(workerBranch);
1638
+ await switchToBranchOrCreate(taskBranch);
1604
1639
  }
1605
1640
  } catch {
1606
1641
  }
1607
1642
  try {
1608
- await pushBranch(workerBranch);
1643
+ await pushBranch(taskBranch);
1609
1644
  } catch {
1610
1645
  }
1611
1646
  const baseCommit = await getCurrentCommit();
1612
- setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
1613
- return `Task #${issueNumber} claimed. Branch: ${workerBranch} (base commit: ${baseCommit.slice(0, 7)})`;
1647
+ setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit, activeBranch: taskBranch } });
1648
+ return `Task #${issueNumber} claimed. Branch: ${taskBranch} (base commit: ${baseCommit.slice(0, 7)})`;
1614
1649
  }
1615
1650
  return `Unknown action: ${action}`;
1616
1651
  }
@@ -1625,14 +1660,14 @@ __export(new_task_exports, {
1625
1660
  terminal: () => terminal4
1626
1661
  });
1627
1662
  init_github();
1628
- import { select as select6, input as promptInput2 } from "@inquirer/prompts";
1663
+ import { select as select4, input as promptInput2 } from "@inquirer/prompts";
1629
1664
  import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
1630
1665
  import { spawn as spawn2 } from "child_process";
1631
1666
  import { tmpdir } from "os";
1632
1667
  import path from "path";
1633
- import ora5 from "ora";
1634
- import chalk9 from "chalk";
1635
- import open2 from "open";
1668
+ import ora4 from "ora";
1669
+ import chalk6 from "chalk";
1670
+ import open from "open";
1636
1671
 
1637
1672
  // src/tools/new-task/prompts.ts
1638
1673
  var GUIDE_FORMAT = `
@@ -1685,6 +1720,77 @@ async function openInEditor(content) {
1685
1720
  await rm(dir, { recursive: true, force: true });
1686
1721
  }
1687
1722
  }
1723
+ async function resolveBaseAndTarget(config, me, interactive) {
1724
+ const currentBranch = await getCurrentBranch();
1725
+ if (isTaskBranch(currentBranch)) {
1726
+ if (await hasUncommittedChanges()) {
1727
+ if (!interactive) {
1728
+ throw new Error("Cannot create sub-task: you have uncommitted changes. Commit them first so the executor starts from the correct base.");
1729
+ }
1730
+ const { select: inquirerSelect } = await import("@inquirer/prompts");
1731
+ let choice;
1732
+ try {
1733
+ choice = await inquirerSelect({
1734
+ message: "You have uncommitted changes. The sub-task executor will start from the last commit \u2014 they won't see your current unsaved work.",
1735
+ choices: [
1736
+ { name: "Commit first (cancel and commit manually)", value: "cancel" },
1737
+ { name: "Continue anyway (executor starts without my unsaved changes)", value: "continue" }
1738
+ ]
1739
+ });
1740
+ } catch {
1741
+ choice = "cancel";
1742
+ }
1743
+ if (choice === "cancel") throw new Error("Cancelled. Commit your changes first, then create the sub-task.");
1744
+ }
1745
+ const baseCommit2 = await getCurrentCommit();
1746
+ return { baseCommit: baseCommit2, targetBranch: currentBranch, isSubtask: true };
1747
+ }
1748
+ let stashedForSync = false;
1749
+ if (await hasUncommittedChanges()) {
1750
+ if (!interactive) {
1751
+ throw new Error("Cannot create task: you have uncommitted changes. Commit or stash them first (git stash).");
1752
+ }
1753
+ const { select: inquirerSelect } = await import("@inquirer/prompts");
1754
+ let choice;
1755
+ try {
1756
+ choice = await inquirerSelect({
1757
+ message: "You have uncommitted changes. Syncing with main requires a clean working tree.",
1758
+ choices: [
1759
+ { name: "Stash changes and continue (restore with: git stash pop)", value: "stash" },
1760
+ { name: "Cancel", value: "cancel" }
1761
+ ]
1762
+ });
1763
+ } catch {
1764
+ choice = "cancel";
1765
+ }
1766
+ if (choice === "cancel") throw new Error("Cancelled.");
1767
+ await stash("tch: before creating new task");
1768
+ stashedForSync = true;
1769
+ console.log(chalk6.dim(" Changes stashed. Run `git stash pop` after creating the task."));
1770
+ }
1771
+ const baseBranch = config.baseBranch ?? "main";
1772
+ let baseCommit;
1773
+ const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
1774
+ try {
1775
+ await syncWithBase(baseBranch);
1776
+ baseCommit = await getCurrentCommit();
1777
+ syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
1778
+ } catch {
1779
+ syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
1780
+ try {
1781
+ baseCommit = await getRemoteHeadSha(baseBranch);
1782
+ } catch {
1783
+ }
1784
+ if (stashedForSync) {
1785
+ try {
1786
+ await stashPop();
1787
+ } catch {
1788
+ }
1789
+ throw new Error(`Could not sync with ${baseBranch}. Your changes have been restored from stash.`);
1790
+ }
1791
+ }
1792
+ return { baseCommit, targetBranch: makeWorkerBranchName(me), isSubtask: false };
1793
+ }
1688
1794
  var definition4 = {
1689
1795
  type: "function",
1690
1796
  function: {
@@ -1701,7 +1807,7 @@ var definition4 = {
1701
1807
  }
1702
1808
  };
1703
1809
  async function run4(input3, config) {
1704
- const authSpinner = ora5("Checking permissions\u2026").start();
1810
+ const authSpinner = ora4("Checking permissions\u2026").start();
1705
1811
  let me;
1706
1812
  let allowed;
1707
1813
  try {
@@ -1724,7 +1830,7 @@ async function run4(input3, config) {
1724
1830
  }
1725
1831
  if (!title) return "Cancelled.";
1726
1832
  }
1727
- const spinner = ora5("Scanning project and generating guide\u2026").start();
1833
+ const spinner = ora4("Scanning project and generating guide\u2026").start();
1728
1834
  let guide;
1729
1835
  try {
1730
1836
  guide = await generateGuide(config, title);
@@ -1733,16 +1839,16 @@ async function run4(input3, config) {
1733
1839
  spinner.stop();
1734
1840
  return `Error generating guide: ${err.message}`;
1735
1841
  }
1736
- const divider = chalk9.dim("\u2500".repeat(70));
1842
+ const divider = chalk6.dim("\u2500".repeat(70));
1737
1843
  for (; ; ) {
1738
1844
  console.log("\n" + divider);
1739
- console.log(chalk9.bold(" Generated guide preview"));
1845
+ console.log(chalk6.bold(" Generated guide preview"));
1740
1846
  console.log(divider);
1741
1847
  console.log(renderMarkdown(guide));
1742
1848
  console.log(divider + "\n");
1743
1849
  let action;
1744
1850
  try {
1745
- action = await select6({
1851
+ action = await select4({
1746
1852
  message: "Create this task?",
1747
1853
  choices: [
1748
1854
  { name: "Yes, create task", value: "create" },
@@ -1760,7 +1866,7 @@ async function run4(input3, config) {
1760
1866
  try {
1761
1867
  guide = await openInEditor(guide);
1762
1868
  } catch (err) {
1763
- console.log(chalk9.yellow(` Editor error: ${err.message}`));
1869
+ console.log(chalk6.yellow(` Editor error: ${err.message}`));
1764
1870
  }
1765
1871
  continue;
1766
1872
  }
@@ -1771,35 +1877,32 @@ async function run4(input3, config) {
1771
1877
  return "Cancelled.";
1772
1878
  }
1773
1879
  if (!feedback) continue;
1774
- const reviseSpinner = ora5("Revising guide\u2026").start();
1880
+ const reviseSpinner = ora4("Revising guide\u2026").start();
1775
1881
  try {
1776
1882
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
1777
1883
  reviseSpinner.stop();
1778
1884
  } catch (err) {
1779
1885
  reviseSpinner.stop();
1780
- console.log(chalk9.yellow(` Revision error: ${err.message}`));
1886
+ console.log(chalk6.yellow(` Revision error: ${err.message}`));
1781
1887
  }
1782
1888
  }
1783
- const baseBranch = config.baseBranch ?? "main";
1784
1889
  let baseCommit;
1785
- const syncSpinner = ora5(`Syncing with ${baseBranch}\u2026`).start();
1890
+ let targetBranch;
1891
+ let isSubtask;
1786
1892
  try {
1787
- await syncWithBase(baseBranch);
1788
- baseCommit = await getCurrentCommit();
1789
- syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
1790
- } catch {
1791
- syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
1792
- try {
1793
- baseCommit = await getRemoteHeadSha(baseBranch);
1794
- } catch {
1795
- }
1893
+ ({ baseCommit, targetBranch, isSubtask } = await resolveBaseAndTarget(config, me, true));
1894
+ } catch (err) {
1895
+ return err.message;
1796
1896
  }
1797
- const createSpinner = ora5(`Creating "${title}"\u2026`).start();
1897
+ if (isSubtask) {
1898
+ console.log(chalk6.dim(` Sub-task: will target branch ${chalk6.cyan(targetBranch)} (base: ${baseCommit?.slice(0, 7) ?? "HEAD"})`));
1899
+ }
1900
+ const createSpinner = ora4(`Creating "${title}"\u2026`).start();
1798
1901
  let htmlUrl;
1799
1902
  let issueNumber;
1800
1903
  let issueTitle;
1801
1904
  try {
1802
- const issue = await createTask(config, title, guide, baseCommit);
1905
+ const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1803
1906
  createSpinner.stop();
1804
1907
  htmlUrl = issue.htmlUrl;
1805
1908
  issueNumber = issue.number;
@@ -1808,19 +1911,19 @@ async function run4(input3, config) {
1808
1911
  createSpinner.stop();
1809
1912
  return `Error: ${err.message}`;
1810
1913
  }
1811
- console.log(chalk9.green(`
1914
+ console.log(chalk6.green(`
1812
1915
  Created #${issueNumber} "${issueTitle}"
1813
- ${chalk9.dim(htmlUrl)}
1916
+ ${chalk6.dim(htmlUrl)}
1814
1917
  `));
1815
1918
  try {
1816
- const openBrowser = await select6({
1919
+ const openBrowser = await select4({
1817
1920
  message: "Open issue in browser?",
1818
1921
  choices: [
1819
1922
  { name: "Yes", value: true },
1820
1923
  { name: "No", value: false }
1821
1924
  ]
1822
1925
  });
1823
- if (openBrowser) await open2(htmlUrl);
1926
+ if (openBrowser) await open(htmlUrl);
1824
1927
  } catch {
1825
1928
  }
1826
1929
  return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
@@ -1836,19 +1939,9 @@ async function execute4(input3, config) {
1836
1939
  if (feedback) {
1837
1940
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
1838
1941
  }
1839
- const baseBranch = config.baseBranch ?? "main";
1840
- let baseCommit;
1841
- try {
1842
- await syncWithBase(baseBranch);
1843
- baseCommit = await getCurrentCommit();
1844
- } catch {
1845
- try {
1846
- baseCommit = await getRemoteHeadSha(baseBranch);
1847
- } catch {
1848
- }
1849
- }
1942
+ const { baseCommit, targetBranch } = await resolveBaseAndTarget(config, me, false);
1850
1943
  try {
1851
- const issue = await createTask(config, title, guide, baseCommit);
1944
+ const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1852
1945
  return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
1853
1946
 
1854
1947
  Guide:
@@ -1868,7 +1961,7 @@ __export(my_status_exports, {
1868
1961
  terminal: () => terminal5
1869
1962
  });
1870
1963
  init_github();
1871
- import ora6 from "ora";
1964
+ import ora5 from "ora";
1872
1965
  var definition5 = {
1873
1966
  type: "function",
1874
1967
  function: {
@@ -1878,7 +1971,7 @@ var definition5 = {
1878
1971
  }
1879
1972
  };
1880
1973
  async function run5(_input, config) {
1881
- const spinner = ora6("Fetching your tasks\u2026").start();
1974
+ const spinner = ora5("Fetching your tasks\u2026").start();
1882
1975
  try {
1883
1976
  const me = await getAuthenticatedUser(config);
1884
1977
  const tasks = await listMyTasks(config, me);
@@ -1898,120 +1991,193 @@ var terminal5 = true;
1898
1991
  // src/tools/review/index.ts
1899
1992
  var review_exports = {};
1900
1993
  __export(review_exports, {
1994
+ definition: () => definition8,
1995
+ execute: () => execute8,
1996
+ run: () => run8,
1997
+ terminal: () => terminal8
1998
+ });
1999
+ init_github();
2000
+ import chalk9 from "chalk";
2001
+ import ora8 from "ora";
2002
+ import { select as select7 } from "@inquirer/prompts";
2003
+
2004
+ // src/tools/accept/index.ts
2005
+ var accept_exports = {};
2006
+ __export(accept_exports, {
1901
2007
  definition: () => definition6,
1902
2008
  execute: () => execute6,
1903
2009
  run: () => run6,
1904
2010
  terminal: () => terminal6
1905
2011
  });
1906
2012
  init_github();
1907
- import ora7 from "ora";
2013
+ import chalk7 from "chalk";
2014
+ import { select as select5 } from "@inquirer/prompts";
2015
+ import ora6 from "ora";
1908
2016
  var definition6 = {
1909
2017
  type: "function",
1910
2018
  function: {
1911
- name: "review",
1912
- description: "List tasks waiting for your review (submitted by others, created by you). Equivalent to /review.",
1913
- parameters: { type: "object", properties: {}, required: [] }
2019
+ name: "accept",
2020
+ description: "Accept an in-review task: merges the PR into the target branch and closes the issue.",
2021
+ parameters: {
2022
+ type: "object",
2023
+ properties: {
2024
+ issue_number: { type: "number", description: "GitHub issue number to accept" }
2025
+ },
2026
+ required: ["issue_number"]
2027
+ }
1914
2028
  }
1915
2029
  };
1916
- async function run6(_input, config) {
1917
- const spinner = ora7("Loading tasks for review\u2026").start();
2030
+ async function run6(input3, config) {
2031
+ let issueNumber = input3["issue_number"];
2032
+ if (!issueNumber) {
2033
+ const spinner3 = ora6("Loading tasks for review\u2026").start();
2034
+ let tasks;
2035
+ let me;
2036
+ try {
2037
+ me = await getAuthenticatedUser(config);
2038
+ tasks = await listTasksForReview(config, me);
2039
+ spinner3.stop();
2040
+ } catch (err) {
2041
+ spinner3.stop();
2042
+ return `Error: ${err.message}`;
2043
+ }
2044
+ if (tasks.length === 0) return "No tasks pending review.";
2045
+ try {
2046
+ issueNumber = await select5({
2047
+ message: "Which task to accept?",
2048
+ choices: tasks.map((t) => ({
2049
+ name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
2050
+ value: t.number
2051
+ }))
2052
+ });
2053
+ } catch {
2054
+ return "Cancelled.";
2055
+ }
2056
+ }
2057
+ const spinner2 = ora6("Verifying permissions\u2026").start();
2058
+ let me2;
2059
+ let issue;
1918
2060
  try {
1919
- const me = await getAuthenticatedUser(config);
1920
- const tasks = await listTasksForReview(config, me);
1921
- spinner.stop();
1922
- if (tasks.length === 0) return `No tasks pending review for @${me}.`;
1923
- const lines = tasks.map((t) => ` #${t.number} [in-review] @${t.assignee ?? "\u2014"} ${t.title}`);
1924
- return `Tasks pending review (created by @${me}):
1925
- ${lines.join("\n")}`;
2061
+ [me2, issue] = await Promise.all([
2062
+ getAuthenticatedUser(config),
2063
+ getTask(config, issueNumber)
2064
+ ]);
2065
+ spinner2.stop();
1926
2066
  } catch (err) {
1927
- spinner.stop();
2067
+ spinner2.stop();
1928
2068
  return `Error: ${err.message}`;
1929
2069
  }
1930
- }
1931
- var execute6 = run6;
1932
- var terminal6 = true;
1933
-
1934
- // src/tools/refresh/index.ts
1935
- var refresh_exports = {};
1936
- __export(refresh_exports, {
1937
- definition: () => definition7,
1938
- execute: () => execute7,
1939
- run: () => run7,
1940
- terminal: () => terminal7
1941
- });
1942
- var definition7 = {
1943
- type: "function",
1944
- function: {
1945
- name: "refresh",
1946
- description: "Reload and display the full task list. Equivalent to /refresh.",
1947
- parameters: { type: "object", properties: {}, required: [] }
2070
+ if (issue.author && issue.author !== me2) {
2071
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
1948
2072
  }
1949
- };
1950
- async function run7(_input, config) {
1951
- const tasks = await printTaskList(config);
1952
- if (tasks.length === 0) return "No tasks found.";
1953
- const lines = tasks.map((t) => {
1954
- const status = getStatus(t);
1955
- const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
1956
- return `#${t.number} [${status}] ${assignee} ${t.title}`;
1957
- });
1958
- return `Tasks (${tasks.length}):
1959
- ${lines.join("\n")}`;
1960
- }
1961
- var execute7 = run7;
1962
- var terminal7 = true;
1963
-
1964
- // src/tools/open-code/index.ts
1965
- var open_code_exports = {};
1966
- __export(open_code_exports, {
1967
- definition: () => definition8,
1968
- execute: () => execute8,
1969
- run: () => run8,
1970
- terminal: () => terminal8
1971
- });
1972
- init_github();
1973
- var definition8 = {
1974
- type: "function",
1975
- function: {
1976
- name: "open_code",
1977
- description: "Launch Claude Code for the current task branch. Equivalent to /code.",
1978
- parameters: { type: "object", properties: {}, required: [] }
2073
+ let confirmed;
2074
+ try {
2075
+ confirmed = await select5({
2076
+ message: `Merge PR for #${issueNumber} and close issue?`,
2077
+ choices: [
2078
+ { name: "Yes, accept", value: true },
2079
+ { name: "Cancel", value: false }
2080
+ ]
2081
+ });
2082
+ } catch {
2083
+ return "Cancelled.";
1979
2084
  }
1980
- };
1981
- async function run8(_input, config) {
1982
- let branch;
2085
+ if (!confirmed) return "Cancelled.";
2086
+ const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2087
+ let result;
1983
2088
  try {
1984
- branch = await getCurrentBranch();
2089
+ result = await acceptTask(config, issueNumber);
2090
+ spinner.succeed(`PR #${result.prNumber} merged \u2192 ${chalk7.cyan(result.baseBranch)}`);
1985
2091
  } catch (err) {
2092
+ spinner.fail("Failed");
1986
2093
  return `Error: ${err.message}`;
1987
2094
  }
1988
- const match = branch.match(/^task-(\d+)-/);
1989
- if (!match) return `Not on a task branch (current: ${branch}).`;
1990
- const issueNum = parseInt(match[1], 10);
1991
- let issue;
2095
+ const mergedIntoTaskBranch = isTaskBranch(result.baseBranch);
2096
+ if (!mergedIntoTaskBranch) {
2097
+ const baseBranch = config.baseBranch ?? "main";
2098
+ let pushToMain;
2099
+ try {
2100
+ pushToMain = await select5({
2101
+ message: `Push ${chalk7.cyan(result.baseBranch)} \u2192 ${chalk7.cyan(baseBranch)}?`,
2102
+ choices: [
2103
+ { name: `Yes, push to ${baseBranch}`, value: true },
2104
+ { name: "No, keep in worker branch", value: false }
2105
+ ]
2106
+ });
2107
+ } catch {
2108
+ pushToMain = false;
2109
+ }
2110
+ if (pushToMain) {
2111
+ const mergeSpinner = ora6(`Merging ${result.baseBranch} \u2192 ${baseBranch}\u2026`).start();
2112
+ try {
2113
+ await mergeWorkerIntoBase(config, result.baseBranch, baseBranch);
2114
+ mergeSpinner.succeed(`Merged ${result.baseBranch} \u2192 ${baseBranch}`);
2115
+ } catch (err) {
2116
+ mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
2117
+ }
2118
+ }
2119
+ }
2120
+ let updateWiki = false;
1992
2121
  try {
1993
- issue = await getTask(config, issueNum);
2122
+ updateWiki = await select5({
2123
+ message: "Update TECHUNTER.md project overview?",
2124
+ choices: [
2125
+ { name: "Yes, regenerate", value: true },
2126
+ { name: "No, skip", value: false }
2127
+ ]
2128
+ });
2129
+ } catch {
2130
+ }
2131
+ if (updateWiki) {
2132
+ const wikiSpinner = ora6("Regenerating TECHUNTER.md\u2026").start();
2133
+ try {
2134
+ const content = await generateWiki(config);
2135
+ await upsertRepoFile(config, "TECHUNTER.md", content, "docs: update TECHUNTER.md project overview");
2136
+ wikiSpinner.succeed("TECHUNTER.md updated");
2137
+ } catch (err) {
2138
+ wikiSpinner.fail(`Wiki update failed: ${err.message}`);
2139
+ }
2140
+ }
2141
+ const mergeTarget = mergedIntoTaskBranch ? `${result.baseBranch} (sub-task merged, no push to main)` : result.baseBranch;
2142
+ return `Task #${issueNumber} accepted.
2143
+ PR #${result.prNumber} merged \u2192 ${mergeTarget}
2144
+ Issue closed.`;
2145
+ }
2146
+ async function execute6(input3, config) {
2147
+ const issueNumber = input3["issue_number"];
2148
+ const [me, issue] = await Promise.all([
2149
+ getAuthenticatedUser(config),
2150
+ getTask(config, issueNumber)
2151
+ ]);
2152
+ if (issue.author && issue.author !== me) {
2153
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2154
+ }
2155
+ const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2156
+ try {
2157
+ const result = await acceptTask(config, issueNumber);
2158
+ spinner.stop();
2159
+ return `Task #${issueNumber} accepted.
2160
+ PR #${result.prNumber} merged \u2192 ${result.baseBranch}
2161
+ Issue closed.`;
1994
2162
  } catch (err) {
2163
+ spinner.stop();
1995
2164
  return `Error: ${err.message}`;
1996
2165
  }
1997
- await launchClaudeCode(issue, branch);
1998
- return "Claude Code session ended.";
1999
2166
  }
2000
- var execute8 = run8;
2001
- var terminal8 = true;
2167
+ var terminal6 = true;
2002
2168
 
2003
2169
  // src/tools/reject/index.ts
2004
2170
  var reject_exports = {};
2005
2171
  __export(reject_exports, {
2006
- definition: () => definition9,
2007
- execute: () => execute9,
2008
- run: () => run9,
2009
- terminal: () => terminal9
2172
+ definition: () => definition7,
2173
+ execute: () => execute7,
2174
+ run: () => run7,
2175
+ terminal: () => terminal7
2010
2176
  });
2011
2177
  init_github();
2012
- import chalk10 from "chalk";
2013
- import { select as select7, input as promptInput3 } from "@inquirer/prompts";
2014
- import ora8 from "ora";
2178
+ import chalk8 from "chalk";
2179
+ import { select as select6, input as promptInput3 } from "@inquirer/prompts";
2180
+ import ora7 from "ora";
2015
2181
 
2016
2182
  // src/tools/reject/prompts.ts
2017
2183
  var REJECTION_FORMAT = `
@@ -2042,7 +2208,7 @@ Reviewer feedback: ${userFeedback}`,
2042
2208
  }
2043
2209
 
2044
2210
  // src/tools/reject/index.ts
2045
- var definition9 = {
2211
+ var definition7 = {
2046
2212
  type: "function",
2047
2213
  function: {
2048
2214
  name: "reject",
@@ -2057,7 +2223,7 @@ var definition9 = {
2057
2223
  }
2058
2224
  }
2059
2225
  };
2060
- async function run9(input3, config) {
2226
+ async function run7(input3, config) {
2061
2227
  const issueNumber = input3["issue_number"];
2062
2228
  const [me, issue] = await Promise.all([
2063
2229
  getAuthenticatedUser(config),
@@ -2075,9 +2241,9 @@ async function run9(input3, config) {
2075
2241
  return "Cancelled.";
2076
2242
  }
2077
2243
  if (!feedback.trim()) return "Cancelled.";
2078
- const divider = chalk10.dim("\u2500".repeat(70));
2244
+ const divider = chalk8.dim("\u2500".repeat(70));
2079
2245
  for (; ; ) {
2080
- const spinner = ora8("Generating rejection comment\u2026").start();
2246
+ const spinner = ora7("Generating rejection comment\u2026").start();
2081
2247
  let comment;
2082
2248
  try {
2083
2249
  comment = await generateRejectionComment(config, issueNumber, feedback);
@@ -2087,13 +2253,13 @@ async function run9(input3, config) {
2087
2253
  return `Error generating comment: ${err.message}`;
2088
2254
  }
2089
2255
  console.log("\n" + divider);
2090
- console.log(chalk10.bold(` Rejection preview \u2014 issue #${issueNumber}`));
2256
+ console.log(chalk8.bold(` Rejection preview \u2014 issue #${issueNumber}`));
2091
2257
  console.log(divider);
2092
2258
  console.log(renderMarkdown(comment));
2093
2259
  console.log(divider + "\n");
2094
2260
  let decision;
2095
2261
  try {
2096
- decision = await select7({
2262
+ decision = await select6({
2097
2263
  message: `Post rejection and mark #${issueNumber} as changes-needed?`,
2098
2264
  choices: [
2099
2265
  { name: "Post & Reject", value: "yes" },
@@ -2113,7 +2279,7 @@ async function run9(input3, config) {
2113
2279
  }
2114
2280
  continue;
2115
2281
  }
2116
- let spinner2 = ora8(`Posting rejection comment on #${issueNumber}\u2026`).start();
2282
+ let spinner2 = ora7(`Posting rejection comment on #${issueNumber}\u2026`).start();
2117
2283
  try {
2118
2284
  await postComment(config, issueNumber, comment);
2119
2285
  spinner2.stop();
@@ -2121,7 +2287,7 @@ async function run9(input3, config) {
2121
2287
  spinner2.stop();
2122
2288
  return `Error posting comment: ${err.message}`;
2123
2289
  }
2124
- spinner2 = ora8(`Marking #${issueNumber} as changes-needed\u2026`).start();
2290
+ spinner2 = ora7(`Marking #${issueNumber} as changes-needed\u2026`).start();
2125
2291
  try {
2126
2292
  await rejectTask(config, issueNumber);
2127
2293
  spinner2.stop();
@@ -2132,7 +2298,7 @@ async function run9(input3, config) {
2132
2298
  return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
2133
2299
  }
2134
2300
  }
2135
- async function execute9(input3, config) {
2301
+ async function execute7(input3, config) {
2136
2302
  const issueNumber = input3["issue_number"];
2137
2303
  const feedback = input3["feedback"];
2138
2304
  const [me, issue] = await Promise.all([
@@ -2149,164 +2315,195 @@ async function execute9(input3, config) {
2149
2315
  return `Error generating comment: ${err.message}`;
2150
2316
  }
2151
2317
  try {
2152
- await postComment(config, issueNumber, comment);
2153
- } catch (err) {
2154
- return `Error posting comment: ${err.message}`;
2318
+ await postComment(config, issueNumber, comment);
2319
+ } catch (err) {
2320
+ return `Error posting comment: ${err.message}`;
2321
+ }
2322
+ try {
2323
+ await rejectTask(config, issueNumber);
2324
+ } catch (err) {
2325
+ return `Comment posted but failed to update label: ${err.message}`;
2326
+ }
2327
+ return `Task #${issueNumber} rejected.
2328
+
2329
+ Comment posted:
2330
+ ${comment}`;
2331
+ }
2332
+ var terminal7 = true;
2333
+
2334
+ // src/tools/review/index.ts
2335
+ var definition8 = {
2336
+ type: "function",
2337
+ function: {
2338
+ name: "review",
2339
+ description: "List tasks waiting for your review (submitted by others, created by you), then let you accept or reject one. Equivalent to /review.",
2340
+ parameters: { type: "object", properties: {}, required: [] }
2341
+ }
2342
+ };
2343
+ async function run8(_input, config) {
2344
+ const spinner = ora8("Loading tasks for review\u2026").start();
2345
+ let me;
2346
+ let tasks;
2347
+ try {
2348
+ me = await getAuthenticatedUser(config);
2349
+ tasks = await listTasksForReview(config, me);
2350
+ spinner.stop();
2351
+ } catch (err) {
2352
+ spinner.stop();
2353
+ return `Error: ${err.message}`;
2354
+ }
2355
+ if (tasks.length === 0) return `No tasks pending review for @${me}.`;
2356
+ let issueNumber;
2357
+ try {
2358
+ issueNumber = await select7({
2359
+ message: "Select a task to review:",
2360
+ choices: tasks.map((t) => ({
2361
+ name: `#${String(t.number).padEnd(4)} @${t.assignee ?? "\u2014"} ${t.title}`,
2362
+ value: t.number
2363
+ }))
2364
+ });
2365
+ } catch {
2366
+ return "Cancelled.";
2155
2367
  }
2368
+ const spinner2 = ora8(`Loading #${issueNumber}\u2026`).start();
2369
+ let pr;
2156
2370
  try {
2157
- await rejectTask(config, issueNumber);
2371
+ pr = await getTaskPR(config, issueNumber);
2372
+ spinner2.stop();
2158
2373
  } catch (err) {
2159
- return `Comment posted but failed to update label: ${err.message}`;
2374
+ spinner2.stop();
2375
+ return `Error loading PR: ${err.message}`;
2160
2376
  }
2161
- return `Task #${issueNumber} rejected.
2377
+ const divider = chalk9.dim("\u2500".repeat(70));
2378
+ console.log("\n" + divider);
2379
+ if (pr) {
2380
+ console.log(chalk9.bold(` PR #${pr.number}`) + " " + chalk9.dim(pr.url));
2381
+ console.log(divider);
2382
+ console.log(renderMarkdown(pr.body));
2383
+ } else {
2384
+ console.log(chalk9.yellow(` No open PR found for task #${issueNumber}`));
2385
+ }
2386
+ console.log(divider + "\n");
2387
+ for (; ; ) {
2388
+ let action;
2389
+ try {
2390
+ action = await select7({
2391
+ message: "Review action:",
2392
+ choices: [
2393
+ ...pr ? [{ name: "View diff", value: "diff" }] : [],
2394
+ { name: chalk9.green("Accept") + " \u2014 merge PR and close issue", value: "accept" },
2395
+ { name: chalk9.red("Reject") + " \u2014 request changes", value: "reject" },
2396
+ { name: "Nothing, just viewing", value: "none" }
2397
+ ]
2398
+ });
2399
+ } catch {
2400
+ return "Cancelled.";
2401
+ }
2402
+ if (action === "none") return `Viewed task #${issueNumber}.`;
2403
+ if (action === "accept") return run6({ issue_number: issueNumber }, config);
2404
+ if (action === "reject") return run7({ issue_number: issueNumber }, config);
2405
+ if (action === "diff") {
2406
+ const diffSpinner = ora8("Fetching diff\u2026").start();
2407
+ let diff;
2408
+ try {
2409
+ diff = await getTaskPRDiff(config, pr.number);
2410
+ diffSpinner.stop();
2411
+ } catch (err) {
2412
+ diffSpinner.stop();
2413
+ console.log(chalk9.red(`Error fetching diff: ${err.message}`));
2414
+ continue;
2415
+ }
2416
+ console.log("\n" + divider);
2417
+ for (const line of diff.split("\n")) {
2418
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2419
+ process.stdout.write(chalk9.green(line) + "\n");
2420
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2421
+ process.stdout.write(chalk9.red(line) + "\n");
2422
+ } else if (line.startsWith("@@")) {
2423
+ process.stdout.write(chalk9.cyan(line) + "\n");
2424
+ } else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
2425
+ process.stdout.write(chalk9.bold(line) + "\n");
2426
+ } else {
2427
+ process.stdout.write(line + "\n");
2428
+ }
2429
+ }
2430
+ console.log(divider + "\n");
2431
+ }
2432
+ }
2433
+ }
2434
+ var execute8 = run8;
2435
+ var terminal8 = true;
2162
2436
 
2163
- Comment posted:
2164
- ${comment}`;
2437
+ // src/tools/refresh/index.ts
2438
+ var refresh_exports = {};
2439
+ __export(refresh_exports, {
2440
+ definition: () => definition9,
2441
+ execute: () => execute9,
2442
+ run: () => run9,
2443
+ terminal: () => terminal9
2444
+ });
2445
+ var definition9 = {
2446
+ type: "function",
2447
+ function: {
2448
+ name: "refresh",
2449
+ description: "Reload and display the full task list. Equivalent to /refresh.",
2450
+ parameters: { type: "object", properties: {}, required: [] }
2451
+ }
2452
+ };
2453
+ async function run9(_input, config) {
2454
+ const tasks = await printTaskList(config);
2455
+ if (tasks.length === 0) return "No tasks found.";
2456
+ const lines = tasks.map((t) => {
2457
+ const status = getStatus(t);
2458
+ const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
2459
+ return `#${t.number} [${status}] ${assignee} ${t.title}`;
2460
+ });
2461
+ return `Tasks (${tasks.length}):
2462
+ ${lines.join("\n")}`;
2165
2463
  }
2464
+ var execute9 = run9;
2166
2465
  var terminal9 = true;
2167
2466
 
2168
- // src/tools/accept/index.ts
2169
- var accept_exports = {};
2170
- __export(accept_exports, {
2467
+ // src/tools/open-code/index.ts
2468
+ var open_code_exports = {};
2469
+ __export(open_code_exports, {
2171
2470
  definition: () => definition10,
2172
2471
  execute: () => execute10,
2173
2472
  run: () => run10,
2174
2473
  terminal: () => terminal10
2175
2474
  });
2176
2475
  init_github();
2177
- import chalk11 from "chalk";
2178
- import { select as select8 } from "@inquirer/prompts";
2179
- import ora9 from "ora";
2180
2476
  var definition10 = {
2181
2477
  type: "function",
2182
2478
  function: {
2183
- name: "accept",
2184
- description: "Accept an in-review task: merges the PR into your worker branch and closes the issue.",
2185
- parameters: {
2186
- type: "object",
2187
- properties: {
2188
- issue_number: { type: "number", description: "GitHub issue number to accept" }
2189
- },
2190
- required: ["issue_number"]
2191
- }
2479
+ name: "open_code",
2480
+ description: "Launch Claude Code for the current task branch. Equivalent to /code.",
2481
+ parameters: { type: "object", properties: {}, required: [] }
2192
2482
  }
2193
2483
  };
2194
- async function run10(input3, config) {
2195
- let issueNumber = input3["issue_number"];
2196
- if (!issueNumber) {
2197
- const spinner3 = ora9("Loading tasks for review\u2026").start();
2198
- let tasks;
2199
- let me;
2200
- try {
2201
- me = await getAuthenticatedUser(config);
2202
- tasks = await listTasksForReview(config, me);
2203
- spinner3.stop();
2204
- } catch (err) {
2205
- spinner3.stop();
2206
- return `Error: ${err.message}`;
2207
- }
2208
- if (tasks.length === 0) return "No tasks pending review.";
2209
- try {
2210
- issueNumber = await select8({
2211
- message: "Which task to accept?",
2212
- choices: tasks.map((t) => ({
2213
- name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
2214
- value: t.number
2215
- }))
2216
- });
2217
- } catch {
2218
- return "Cancelled.";
2219
- }
2220
- }
2221
- const spinner2 = ora9("Verifying permissions\u2026").start();
2222
- let me2;
2223
- let issue;
2224
- try {
2225
- [me2, issue] = await Promise.all([
2226
- getAuthenticatedUser(config),
2227
- getTask(config, issueNumber)
2228
- ]);
2229
- spinner2.stop();
2230
- } catch (err) {
2231
- spinner2.stop();
2232
- return `Error: ${err.message}`;
2233
- }
2234
- if (issue.author && issue.author !== me2) {
2235
- return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2236
- }
2237
- const targetBranch = makeWorkerBranchName(me2);
2238
- let confirmed;
2239
- try {
2240
- confirmed = await select8({
2241
- message: `Merge PR for #${issueNumber} into ${chalk11.cyan(targetBranch)} and close issue?`,
2242
- choices: [
2243
- { name: "Yes, accept", value: true },
2244
- { name: "Cancel", value: false }
2245
- ]
2246
- });
2247
- } catch {
2248
- return "Cancelled.";
2249
- }
2250
- if (!confirmed) return "Cancelled.";
2251
- const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
2252
- let result;
2484
+ async function run10(_input, config) {
2485
+ let branch;
2253
2486
  try {
2254
- const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
2255
- result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
2256
- spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
2487
+ branch = await getCurrentBranch();
2257
2488
  } catch (err) {
2258
- spinner.fail("Failed");
2259
2489
  return `Error: ${err.message}`;
2260
2490
  }
2261
- const baseBranch = config.baseBranch ?? "main";
2262
- let pushToMain;
2263
- try {
2264
- pushToMain = await select8({
2265
- message: `Push ${chalk11.cyan(targetBranch)} \u2192 ${chalk11.cyan(baseBranch)}?`,
2266
- choices: [
2267
- { name: `Yes, push to ${baseBranch}`, value: true },
2268
- { name: "No, keep in worker branch", value: false }
2269
- ]
2270
- });
2271
- } catch {
2272
- pushToMain = false;
2273
- }
2274
- if (pushToMain) {
2275
- const mergeSpinner = ora9(`Merging ${targetBranch} \u2192 ${baseBranch}\u2026`).start();
2276
- try {
2277
- await mergeWorkerIntoBase(config, targetBranch, baseBranch);
2278
- mergeSpinner.succeed(`Merged ${targetBranch} \u2192 ${baseBranch}`);
2279
- } catch (err) {
2280
- mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
2281
- }
2282
- }
2283
- return `Task #${issueNumber} accepted.
2284
- PR #${result.prNumber} merged \u2192 ${targetBranch}${pushToMain ? ` \u2192 ${baseBranch}` : ""}
2285
- Issue closed.`;
2286
- }
2287
- async function execute10(input3, config) {
2288
- const issueNumber = input3["issue_number"];
2289
- const [me, issue] = await Promise.all([
2290
- getAuthenticatedUser(config),
2291
- getTask(config, issueNumber)
2292
- ]);
2293
- if (issue.author && issue.author !== me) {
2294
- return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2491
+ let issueNum = getConfig().taskState?.activeIssueNumber;
2492
+ if (!issueNum) {
2493
+ const found = await getIssueNumberFromBranch(config, branch);
2494
+ if (!found) return `No active task found (current branch: ${branch}).`;
2495
+ issueNum = found.issueNumber;
2295
2496
  }
2296
- const targetBranch = makeWorkerBranchName(me);
2297
- const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
2497
+ let issue;
2298
2498
  try {
2299
- const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
2300
- const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
2301
- spinner.stop();
2302
- return `Task #${issueNumber} accepted.
2303
- PR #${result.prNumber} merged \u2192 ${targetBranch}
2304
- Issue closed.`;
2499
+ issue = await getTask(config, issueNum);
2305
2500
  } catch (err) {
2306
- spinner.stop();
2307
2501
  return `Error: ${err.message}`;
2308
2502
  }
2503
+ await launchClaudeCode(issue, branch);
2504
+ return "Claude Code session ended.";
2309
2505
  }
2506
+ var execute10 = run10;
2310
2507
  var terminal10 = true;
2311
2508
 
2312
2509
  // src/tools/edit-task/index.ts
@@ -2318,8 +2515,8 @@ __export(edit_task_exports, {
2318
2515
  terminal: () => terminal11
2319
2516
  });
2320
2517
  init_github();
2321
- import { select as select9, input as promptInput4 } from "@inquirer/prompts";
2322
- import ora10 from "ora";
2518
+ import { select as select8, input as promptInput4 } from "@inquirer/prompts";
2519
+ import ora9 from "ora";
2323
2520
  var definition11 = {
2324
2521
  type: "function",
2325
2522
  function: {
@@ -2347,7 +2544,7 @@ async function run11(input3, config) {
2347
2544
  }
2348
2545
  if (tasks.length === 0) return "No tasks found.";
2349
2546
  try {
2350
- issueNumber = await select9({
2547
+ issueNumber = await select8({
2351
2548
  message: "Select task to edit:",
2352
2549
  choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
2353
2550
  });
@@ -2375,43 +2572,169 @@ async function run11(input3, config) {
2375
2572
  } catch {
2376
2573
  return "Cancelled.";
2377
2574
  }
2378
- if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
2379
- return "No changes made.";
2380
- }
2381
- const spinner = ora10(`Updating #${issueNumber}\u2026`).start();
2575
+ if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
2576
+ return "No changes made.";
2577
+ }
2578
+ const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
2579
+ try {
2580
+ await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
2581
+ spinner.stop();
2582
+ return `Task #${issueNumber} updated.`;
2583
+ } catch (err) {
2584
+ spinner.stop();
2585
+ return `Error: ${err.message}`;
2586
+ }
2587
+ }
2588
+ async function execute11(input3, config) {
2589
+ const issueNumber = input3["issue_number"];
2590
+ const title = input3["title"];
2591
+ const body = input3["body"];
2592
+ const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
2593
+ try {
2594
+ await editTask(config, issueNumber, title, body);
2595
+ spinner.stop();
2596
+ return `Task #${issueNumber} updated.`;
2597
+ } catch (err) {
2598
+ spinner.stop();
2599
+ return `Error: ${err.message}`;
2600
+ }
2601
+ }
2602
+ var terminal11 = true;
2603
+
2604
+ // src/tools/wiki/index.ts
2605
+ var wiki_exports = {};
2606
+ __export(wiki_exports, {
2607
+ definition: () => definition12,
2608
+ execute: () => execute12,
2609
+ run: () => run12,
2610
+ terminal: () => terminal12
2611
+ });
2612
+ import ora10 from "ora";
2613
+ import chalk10 from "chalk";
2614
+ import { readFile as readFile2 } from "fs/promises";
2615
+ import { select as select9 } from "@inquirer/prompts";
2616
+ init_github();
2617
+ var WIKI_PATH = "TECHUNTER.md";
2618
+ var definition12 = {
2619
+ type: "function",
2620
+ function: {
2621
+ name: "update_wiki",
2622
+ description: "Generate or refresh the project overview document (TECHUNTER.md) by scanning the codebase. The document helps new team members understand what the project does, how it is architected, and how to start contributing. Equivalent to /wiki.",
2623
+ parameters: {
2624
+ type: "object",
2625
+ properties: {},
2626
+ required: []
2627
+ }
2628
+ }
2629
+ };
2630
+ async function readWikiContent(config) {
2631
+ try {
2632
+ return await readFile2(WIKI_PATH, "utf-8");
2633
+ } catch {
2634
+ }
2635
+ return getRepoFile(config, WIKI_PATH);
2636
+ }
2637
+ function printWiki(content) {
2638
+ const divider = chalk10.dim("\u2500".repeat(70));
2639
+ console.log("\n" + divider);
2640
+ console.log(chalk10.bold(" TECHUNTER.md"));
2641
+ console.log(divider);
2642
+ console.log(renderMarkdown(content));
2643
+ console.log(divider + "\n");
2644
+ }
2645
+ async function run12(_input, config) {
2646
+ const fetchSpinner = ora10("Checking for existing wiki\u2026").start();
2647
+ const existing = await readWikiContent(config).catch(() => null);
2648
+ fetchSpinner.stop();
2649
+ let action;
2650
+ try {
2651
+ action = await select9({
2652
+ message: "TECHUNTER.md \u2014 what would you like to do?",
2653
+ choices: [
2654
+ ...existing ? [{ name: "View current wiki", value: "view" }] : [],
2655
+ { name: existing ? "Regenerate & commit to repo" : "Generate & commit to repo", value: "generate" },
2656
+ { name: "Cancel", value: "cancel" }
2657
+ ]
2658
+ });
2659
+ } catch {
2660
+ return "Cancelled.";
2661
+ }
2662
+ if (action === "cancel") return "Cancelled.";
2663
+ if (action === "view") {
2664
+ printWiki(existing);
2665
+ return "Displayed TECHUNTER.md.";
2666
+ }
2667
+ const authSpinner = ora10("Checking permissions\u2026").start();
2668
+ let me;
2669
+ let allowed;
2670
+ try {
2671
+ me = await getAuthenticatedUser(config);
2672
+ allowed = await isCollaborator(config, me);
2673
+ authSpinner.stop();
2674
+ } catch (err) {
2675
+ authSpinner.stop();
2676
+ return `Error checking permissions: ${err.message}`;
2677
+ }
2678
+ if (!allowed) {
2679
+ return `Permission denied: only repository collaborators can update the project wiki.`;
2680
+ }
2681
+ const genSpinner = ora10("Analyzing project and generating overview\u2026").start();
2682
+ let content;
2683
+ try {
2684
+ content = await generateWiki(config);
2685
+ genSpinner.stop();
2686
+ } catch (err) {
2687
+ genSpinner.stop();
2688
+ return `Error generating wiki: ${err.message}`;
2689
+ }
2690
+ printWiki(content);
2691
+ let confirm;
2692
+ try {
2693
+ confirm = await select9({
2694
+ message: `Publish to repository as ${WIKI_PATH}?`,
2695
+ choices: [
2696
+ { name: "Yes, commit to repo", value: "publish" },
2697
+ { name: "Cancel", value: "cancel" }
2698
+ ]
2699
+ });
2700
+ } catch {
2701
+ return "Cancelled.";
2702
+ }
2703
+ if (confirm === "cancel") return "Cancelled.";
2704
+ const writeSpinner = ora10(`Writing ${WIKI_PATH}\u2026`).start();
2382
2705
  try {
2383
- await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
2384
- spinner.stop();
2385
- return `Task #${issueNumber} updated.`;
2706
+ const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2707
+ writeSpinner.succeed(`Written: ${url}`);
2708
+ console.log("");
2709
+ return `TECHUNTER.md updated \u2014 ${url}`;
2386
2710
  } catch (err) {
2387
- spinner.stop();
2711
+ writeSpinner.fail(`Failed: ${err.message}`);
2388
2712
  return `Error: ${err.message}`;
2389
2713
  }
2390
2714
  }
2391
- async function execute11(input3, config) {
2392
- const issueNumber = input3["issue_number"];
2393
- const title = input3["title"];
2394
- const body = input3["body"];
2395
- const spinner = ora10(`Updating #${issueNumber}\u2026`).start();
2715
+ async function execute12(_input, config) {
2716
+ const me = await getAuthenticatedUser(config);
2717
+ if (!await isCollaborator(config, me)) {
2718
+ return `Permission denied: only repository collaborators can update the project wiki.`;
2719
+ }
2720
+ const content = await generateWiki(config);
2396
2721
  try {
2397
- await editTask(config, issueNumber, title, body);
2398
- spinner.stop();
2399
- return `Task #${issueNumber} updated.`;
2722
+ const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2723
+ return `TECHUNTER.md updated \u2014 ${url}`;
2400
2724
  } catch (err) {
2401
- spinner.stop();
2402
2725
  return `Error: ${err.message}`;
2403
2726
  }
2404
2727
  }
2405
- var terminal11 = true;
2728
+ var terminal12 = true;
2406
2729
 
2407
2730
  // src/tools/list-tasks/index.ts
2408
2731
  var list_tasks_exports = {};
2409
2732
  __export(list_tasks_exports, {
2410
- definition: () => definition12,
2411
- execute: () => execute12
2733
+ definition: () => definition13,
2734
+ execute: () => execute13
2412
2735
  });
2413
2736
  init_github();
2414
- var definition12 = {
2737
+ var definition13 = {
2415
2738
  type: "function",
2416
2739
  function: {
2417
2740
  name: "list_tasks",
@@ -2423,7 +2746,7 @@ var definition12 = {
2423
2746
  }
2424
2747
  }
2425
2748
  };
2426
- async function execute12(_input, config) {
2749
+ async function execute13(_input, config) {
2427
2750
  const tasks = await listTasks(config);
2428
2751
  if (tasks.length === 0) return "No open tasks.";
2429
2752
  return tasks.map((t) => {
@@ -2436,11 +2759,11 @@ async function execute12(_input, config) {
2436
2759
  // src/tools/get-task/index.ts
2437
2760
  var get_task_exports = {};
2438
2761
  __export(get_task_exports, {
2439
- definition: () => definition13,
2440
- execute: () => execute13
2762
+ definition: () => definition14,
2763
+ execute: () => execute14
2441
2764
  });
2442
2765
  init_github();
2443
- var definition13 = {
2766
+ var definition14 = {
2444
2767
  type: "function",
2445
2768
  function: {
2446
2769
  name: "get_task",
@@ -2454,7 +2777,7 @@ var definition13 = {
2454
2777
  }
2455
2778
  }
2456
2779
  };
2457
- async function execute13(input3, config) {
2780
+ async function execute14(input3, config) {
2458
2781
  const issue = await getTask(config, input3["issue_number"]);
2459
2782
  const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2460
2783
  const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
@@ -2471,12 +2794,12 @@ ${issue.body}`);
2471
2794
  // src/tools/get-comments/index.ts
2472
2795
  var get_comments_exports = {};
2473
2796
  __export(get_comments_exports, {
2474
- definition: () => definition14,
2475
- execute: () => execute14
2797
+ definition: () => definition15,
2798
+ execute: () => execute15
2476
2799
  });
2477
2800
  init_github();
2478
2801
  import ora11 from "ora";
2479
- var definition14 = {
2802
+ var definition15 = {
2480
2803
  type: "function",
2481
2804
  function: {
2482
2805
  name: "get_comments",
@@ -2491,7 +2814,7 @@ var definition14 = {
2491
2814
  }
2492
2815
  }
2493
2816
  };
2494
- async function execute14(input3, config) {
2817
+ async function execute15(input3, config) {
2495
2818
  const issueNumber = input3["issue_number"];
2496
2819
  const limit = input3["limit"] ?? 5;
2497
2820
  const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
@@ -2513,11 +2836,11 @@ ${lines.join("\n\n")}`;
2513
2836
  // src/tools/get-diff/index.ts
2514
2837
  var get_diff_exports = {};
2515
2838
  __export(get_diff_exports, {
2516
- definition: () => definition15,
2517
- execute: () => execute15
2839
+ definition: () => definition16,
2840
+ execute: () => execute16
2518
2841
  });
2519
2842
  import ora12 from "ora";
2520
- var definition15 = {
2843
+ var definition16 = {
2521
2844
  type: "function",
2522
2845
  function: {
2523
2846
  name: "get_diff",
@@ -2525,7 +2848,7 @@ var definition15 = {
2525
2848
  parameters: { type: "object", properties: {}, required: [] }
2526
2849
  }
2527
2850
  };
2528
- async function execute15(_input, _config) {
2851
+ async function execute16(_input, _config) {
2529
2852
  const spinner = ora12("Reading git diff...").start();
2530
2853
  try {
2531
2854
  const diff = await getDiff();
@@ -2540,14 +2863,14 @@ async function execute15(_input, _config) {
2540
2863
  // src/tools/run-command/index.ts
2541
2864
  var run_command_exports = {};
2542
2865
  __export(run_command_exports, {
2543
- definition: () => definition16,
2544
- execute: () => execute16
2866
+ definition: () => definition17,
2867
+ execute: () => execute17
2545
2868
  });
2546
2869
  import { exec } from "child_process";
2547
2870
  import { promisify } from "util";
2548
2871
  import ora13 from "ora";
2549
2872
  var execAsync = promisify(exec);
2550
- var definition16 = {
2873
+ var definition17 = {
2551
2874
  type: "function",
2552
2875
  function: {
2553
2876
  name: "run_command",
@@ -2561,7 +2884,7 @@ var definition16 = {
2561
2884
  }
2562
2885
  }
2563
2886
  };
2564
- async function execute16(input3, _config) {
2887
+ async function execute17(input3, _config) {
2565
2888
  const command = input3["command"];
2566
2889
  const cwd = process.cwd();
2567
2890
  const spinner = ora13(`$ ${command}`).start();
@@ -2583,15 +2906,15 @@ ${out || e.message}`;
2583
2906
  // src/tools/list-files/index.ts
2584
2907
  var list_files_exports = {};
2585
2908
  __export(list_files_exports, {
2586
- definition: () => definition17,
2587
- execute: () => execute17
2909
+ definition: () => definition18,
2910
+ execute: () => execute18
2588
2911
  });
2589
- import { readFile as readFile2 } from "fs/promises";
2912
+ import { readFile as readFile3 } from "fs/promises";
2590
2913
  import { existsSync } from "fs";
2591
2914
  import path2 from "path";
2592
2915
  import { globby } from "globby";
2593
2916
  import ignore from "ignore";
2594
- var definition17 = {
2917
+ var definition18 = {
2595
2918
  type: "function",
2596
2919
  function: {
2597
2920
  name: "list_files",
@@ -2631,13 +2954,13 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2631
2954
  ".sqlite",
2632
2955
  ".lock"
2633
2956
  ]);
2634
- async function execute17(input3, _config) {
2957
+ async function execute18(input3, _config) {
2635
2958
  const glob = input3["glob"] ?? "**/*";
2636
2959
  const cwd = process.cwd();
2637
2960
  const ig = ignore();
2638
2961
  const gitignorePath = path2.join(cwd, ".gitignore");
2639
2962
  if (existsSync(gitignorePath)) {
2640
- ig.add(await readFile2(gitignorePath, "utf-8"));
2963
+ ig.add(await readFile3(gitignorePath, "utf-8"));
2641
2964
  }
2642
2965
  ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
2643
2966
  const files = await globby(glob, { cwd, dot: false, onlyFiles: true, gitignore: false });
@@ -2650,15 +2973,15 @@ ${filtered.join("\n")}`;
2650
2973
  // src/tools/grep-code/index.ts
2651
2974
  var grep_code_exports = {};
2652
2975
  __export(grep_code_exports, {
2653
- definition: () => definition18,
2654
- execute: () => execute18
2976
+ definition: () => definition19,
2977
+ execute: () => execute19
2655
2978
  });
2656
- import { readFile as readFile3 } from "fs/promises";
2979
+ import { readFile as readFile4 } from "fs/promises";
2657
2980
  import { existsSync as existsSync2 } from "fs";
2658
2981
  import path3 from "path";
2659
2982
  import { globby as globby2 } from "globby";
2660
2983
  import ignore2 from "ignore";
2661
- var definition18 = {
2984
+ var definition19 = {
2662
2985
  type: "function",
2663
2986
  function: {
2664
2987
  name: "grep_code",
@@ -2699,7 +3022,7 @@ async function buildIgnore(cwd) {
2699
3022
  const ig = ignore2();
2700
3023
  const gitignorePath = path3.join(cwd, ".gitignore");
2701
3024
  if (existsSync2(gitignorePath)) {
2702
- ig.add(await readFile3(gitignorePath, "utf-8"));
3025
+ ig.add(await readFile4(gitignorePath, "utf-8"));
2703
3026
  }
2704
3027
  ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
2705
3028
  return ig;
@@ -2731,7 +3054,7 @@ function isText(f) {
2731
3054
  return !BINARY_EXTENSIONS2.has(path3.extname(f).toLowerCase());
2732
3055
  }
2733
3056
  var MAX_RANGE_LINES = 300;
2734
- async function execute18(input3, _config) {
3057
+ async function execute19(input3, _config) {
2735
3058
  const pattern = input3["pattern"] ?? "";
2736
3059
  const fileGlob = input3["file_glob"] ?? "**/*";
2737
3060
  const contextLines = Math.min(input3["context_lines"] ?? 2, 5);
@@ -2743,7 +3066,7 @@ async function execute18(input3, _config) {
2743
3066
  const files = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
2744
3067
  if (files.length === 0) return `No file matched: ${fileGlob}`;
2745
3068
  if (files.length > 1) return `file_glob matched ${files.length} files \u2014 narrow it to a single file for read-range mode.`;
2746
- const raw = await readFile3(path3.join(cwd, files[0]), "utf-8");
3069
+ const raw = await readFile4(path3.join(cwd, files[0]), "utf-8");
2747
3070
  const lines = raw.split("\n");
2748
3071
  const total = lines.length;
2749
3072
  const from = Math.max(1, startLine);
@@ -2772,7 +3095,7 @@ ${numbered}
2772
3095
  if (totalMatches >= maxResults) break;
2773
3096
  let content;
2774
3097
  try {
2775
- content = await readFile3(path3.join(cwd, file), "utf-8");
3098
+ content = await readFile4(path3.join(cwd, file), "utf-8");
2776
3099
  } catch {
2777
3100
  continue;
2778
3101
  }
@@ -2810,92 +3133,457 @@ ${snippets.join("\n---\n")}
2810
3133
  \`\`\``);
2811
3134
  }
2812
3135
  }
2813
- if (matches.length === 0) return `No matches found for: ${pattern}`;
2814
- const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
2815
- return [header, ...matches].join("\n\n");
3136
+ if (matches.length === 0) return `No matches found for: ${pattern}`;
3137
+ const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
3138
+ return [header, ...matches].join("\n\n");
3139
+ }
3140
+
3141
+ // src/tools/ask-user/index.ts
3142
+ var ask_user_exports = {};
3143
+ __export(ask_user_exports, {
3144
+ definition: () => definition20,
3145
+ execute: () => execute20
3146
+ });
3147
+ import chalk11 from "chalk";
3148
+ import { select as select10, input as promptInput5 } from "@inquirer/prompts";
3149
+ var definition20 = {
3150
+ type: "function",
3151
+ function: {
3152
+ name: "ask_user",
3153
+ description: "Ask the user to clarify something ambiguous \u2014 scope, expected behaviour, edge cases, or business decisions. Do NOT ask about technical implementation choices. Use at most 3 times per task.",
3154
+ parameters: {
3155
+ type: "object",
3156
+ properties: {
3157
+ question: { type: "string", description: "The question to ask the user" },
3158
+ options: {
3159
+ type: "array",
3160
+ items: { type: "string" },
3161
+ description: "2\u20134 concrete answer choices"
3162
+ }
3163
+ },
3164
+ required: ["question", "options"]
3165
+ }
3166
+ }
3167
+ };
3168
+ async function execute20(input3, _config) {
3169
+ const question = input3["question"];
3170
+ const options = input3["options"];
3171
+ const OTHER = "__other__";
3172
+ console.log("");
3173
+ console.log(chalk11.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
3174
+ console.log(chalk11.dim(" \u2502"));
3175
+ for (const line of question.split("\n")) {
3176
+ console.log(chalk11.dim(" \u2502 ") + line);
3177
+ }
3178
+ console.log(chalk11.dim(" \u2514" + "\u2500".repeat(67)));
3179
+ let answer;
3180
+ try {
3181
+ const chosen = await select10({
3182
+ message: " ",
3183
+ choices: [
3184
+ ...options.map((o) => ({ name: o, value: o })),
3185
+ { name: chalk11.dim("Other (describe below)"), value: OTHER }
3186
+ ]
3187
+ });
3188
+ answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
3189
+ } catch {
3190
+ answer = "User skipped this question \u2014 use your best judgement.";
3191
+ }
3192
+ console.log("");
3193
+ return answer;
3194
+ }
3195
+
3196
+ // src/tools/registry.ts
3197
+ var toolModules = [
3198
+ // Command tools
3199
+ pick_exports,
3200
+ new_task_exports,
3201
+ close_exports,
3202
+ submit_exports,
3203
+ my_status_exports,
3204
+ review_exports,
3205
+ refresh_exports,
3206
+ open_code_exports,
3207
+ reject_exports,
3208
+ accept_exports,
3209
+ edit_task_exports,
3210
+ wiki_exports,
3211
+ // Low-level tools
3212
+ list_tasks_exports,
3213
+ get_task_exports,
3214
+ get_comments_exports,
3215
+ get_diff_exports,
3216
+ run_command_exports,
3217
+ list_files_exports,
3218
+ grep_code_exports,
3219
+ ask_user_exports
3220
+ ];
3221
+
3222
+ // src/lib/agent-ui.ts
3223
+ import chalk12 from "chalk";
3224
+ function formatInput(input3) {
3225
+ return Object.entries(input3).map(([k, v]) => {
3226
+ if (typeof v === "number") return `${k}=${v}`;
3227
+ if (typeof v === "string") {
3228
+ if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
3229
+ return `${k}="${v}"`;
3230
+ }
3231
+ return `${k}=${JSON.stringify(v)}`;
3232
+ }).join(" ");
3233
+ }
3234
+ function summarize(result) {
3235
+ const first = result.split("\n").find((l) => l.trim()) ?? result;
3236
+ return first.length > 100 ? first.slice(0, 97) + "..." : first;
3237
+ }
3238
+ function printToolCall(name, input3) {
3239
+ const params = formatInput(input3);
3240
+ console.log(` ${chalk12.cyan("\u2192")} ${chalk12.bold(name)}${params ? " " + chalk12.dim(params) : ""}`);
3241
+ }
3242
+ function printToolResult(result) {
3243
+ const ok = !result.startsWith("Error:");
3244
+ const icon = ok ? chalk12.green("\u2713") : chalk12.red("\u2717");
3245
+ console.log(` ${icon} ${chalk12.dim(summarize(result))}`);
3246
+ }
3247
+
3248
+ // src/lib/sub-agent.ts
3249
+ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
3250
+ const client = createClient(config);
3251
+ const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
3252
+ const tools2 = selected.map((m) => m.definition);
3253
+ const messages = [
3254
+ { role: "system", content: systemPrompt },
3255
+ { role: "user", content: userMessage }
3256
+ ];
3257
+ const MAX_ITERATIONS = 100;
3258
+ let iterations = 0;
3259
+ for (; ; ) {
3260
+ if (++iterations > MAX_ITERATIONS) {
3261
+ throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
3262
+ }
3263
+ const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
3264
+ const choice = res.choices[0];
3265
+ messages.push({
3266
+ role: "assistant",
3267
+ content: choice.message.content ?? null,
3268
+ ...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
3269
+ });
3270
+ if (choice.finish_reason === "stop") {
3271
+ return choice.message.content ?? "";
3272
+ }
3273
+ if (choice.finish_reason === "tool_calls") {
3274
+ for (const tc of choice.message.tool_calls ?? []) {
3275
+ let input3;
3276
+ try {
3277
+ input3 = JSON.parse(tc.function.arguments);
3278
+ } catch {
3279
+ input3 = {};
3280
+ }
3281
+ printToolCall(tc.function.name, input3);
3282
+ const mod = selected.find((m) => m.definition.function.name === tc.function.name);
3283
+ const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
3284
+ printToolResult(result);
3285
+ messages.push({ role: "tool", tool_call_id: tc.id, content: result });
3286
+ }
3287
+ }
3288
+ }
3289
+ }
3290
+
3291
+ // src/tools/wiki/prompts.ts
3292
+ var WIKI_FORMAT = `
3293
+ The document you produce must be valid Markdown with these exact sections:
3294
+
3295
+ # [Project Name]
3296
+
3297
+ > One-sentence description of what this project does.
3298
+
3299
+ ## What Is This?
3300
+
3301
+ 2-4 paragraphs covering:
3302
+ - The problem this project solves
3303
+ - Who uses it and in what context
3304
+ - Core capabilities / key features
3305
+
3306
+ ## Quick Start
3307
+
3308
+ Numbered steps for a brand-new developer to install, configure, and run the project for the first time.
3309
+
3310
+ ## Architecture
3311
+
3312
+ High-level explanation of how the system is structured:
3313
+ - Key components / layers and their responsibilities
3314
+ - Request or data flow (prose or ASCII diagram)
3315
+ - Noteworthy design decisions
3316
+
3317
+ ## Key Files
3318
+
3319
+ | File / Directory | Purpose |
3320
+ |---|---|
3321
+ | ... | ... |
3322
+
3323
+ (List the 8-15 most important files.)
3324
+
3325
+ ## Development Workflow
3326
+
3327
+ Common day-to-day tasks a contributor will need:
3328
+ - How to build / run locally
3329
+ - How to add a new feature (brief steps)
3330
+ - Any testing or linting commands
3331
+
3332
+ ---
3333
+ *Maintained by Techunter \u2014 run \`tch wiki\` to regenerate*
3334
+ `;
3335
+
3336
+ // src/tools/wiki/wiki-generator.ts
3337
+ async function generateWiki(config) {
3338
+ return runSubAgentLoop(
3339
+ config,
3340
+ "You are a senior engineer writing a project overview document for new team members. Use list_files to understand the project structure, then grep_code and run_command to read key files (e.g. package.json, README, entry points, config files). Be concrete and specific \u2014 reference real file names, commands, and concepts from this codebase. Avoid vague filler. When you have enough context, write the document.\n\n" + WIKI_FORMAT,
3341
+ "Analyze this project thoroughly and produce a comprehensive TECHUNTER.md overview document for new team members.",
3342
+ ["list_files", "grep_code", "run_command"]
3343
+ );
3344
+ }
3345
+
3346
+ // src/commands/init.ts
3347
+ async function getGitHubTokenViaPAT() {
3348
+ console.log(chalk13.dim("\n Create a token at: https://github.com/settings/tokens/new"));
3349
+ console.log(chalk13.dim(" Required scopes: repo, read:user\n"));
3350
+ const token = await password({
3351
+ message: "GitHub Personal Access Token:",
3352
+ mask: "*"
3353
+ });
3354
+ return { token: token.trim() };
3355
+ }
3356
+ var OAUTH_CLIENT_ID = "Ov23liW4zJ4r2RdZOsCJ";
3357
+ async function getGitHubTokenViaDeviceFlow() {
3358
+ let verificationUri = "";
3359
+ let userCode = "";
3360
+ const auth = createOAuthDeviceAuth({
3361
+ clientType: "oauth-app",
3362
+ clientId: OAUTH_CLIENT_ID,
3363
+ scopes: ["repo"],
3364
+ onVerification(verification) {
3365
+ verificationUri = verification.verification_uri;
3366
+ userCode = verification.user_code;
3367
+ console.log("");
3368
+ console.log(chalk13.bold(" 1. Open this URL in your browser:"));
3369
+ console.log(" " + chalk13.cyan(verificationUri));
3370
+ console.log("");
3371
+ console.log(chalk13.bold(" 2. Enter this code:"));
3372
+ console.log(" " + chalk13.yellow.bold(userCode));
3373
+ console.log("");
3374
+ open2(verificationUri).catch(() => {
3375
+ });
3376
+ }
3377
+ });
3378
+ const spinner = ora14("Waiting for authorization in browser...").start();
3379
+ let token;
3380
+ try {
3381
+ const result = await auth({ type: "oauth" });
3382
+ token = result.token;
3383
+ spinner.succeed("Authorized!");
3384
+ } catch (err) {
3385
+ spinner.fail("Authorization failed");
3386
+ throw err;
3387
+ }
3388
+ return { token, clientId: OAUTH_CLIENT_ID };
2816
3389
  }
2817
-
2818
- // src/tools/ask-user/index.ts
2819
- var ask_user_exports = {};
2820
- __export(ask_user_exports, {
2821
- definition: () => definition19,
2822
- execute: () => execute19
2823
- });
2824
- import chalk12 from "chalk";
2825
- import { select as select10, input as promptInput5 } from "@inquirer/prompts";
2826
- var definition19 = {
2827
- type: "function",
2828
- function: {
2829
- name: "ask_user",
2830
- description: "Ask the user to clarify something ambiguous \u2014 scope, expected behaviour, edge cases, or business decisions. Do NOT ask about technical implementation choices. Use at most 3 times per task.",
2831
- parameters: {
2832
- type: "object",
2833
- properties: {
2834
- question: { type: "string", description: "The question to ask the user" },
2835
- options: {
2836
- type: "array",
2837
- items: { type: "string" },
2838
- description: "2\u20134 concrete answer choices"
2839
- }
2840
- },
2841
- required: ["question", "options"]
3390
+ async function initCommand() {
3391
+ console.log(chalk13.bold.cyan("\nTechunter \u2014 Initial Setup\n"));
3392
+ let detectedOwner = "";
3393
+ let detectedRepo = "";
3394
+ const remoteUrl = await getRemoteUrl();
3395
+ if (remoteUrl) {
3396
+ const parsed = parseOwnerRepo(remoteUrl);
3397
+ if (parsed) {
3398
+ detectedOwner = parsed.owner;
3399
+ detectedRepo = parsed.repo;
3400
+ console.log(chalk13.dim(`Detected GitHub repo: ${detectedOwner}/${detectedRepo}
3401
+ `));
2842
3402
  }
2843
3403
  }
2844
- };
2845
- async function execute19(input3, _config) {
2846
- const question = input3["question"];
2847
- const options = input3["options"];
2848
- const OTHER = "__other__";
2849
- console.log("");
2850
- console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
2851
- console.log(chalk12.dim(" \u2502"));
2852
- for (const line of question.split("\n")) {
2853
- console.log(chalk12.dim(" \u2502 ") + line);
3404
+ const authMethod = await select11({
3405
+ message: "How would you like to authenticate with GitHub?",
3406
+ choices: [
3407
+ {
3408
+ name: "Browser login (OAuth) \u2014 open a URL and click Authorize",
3409
+ value: "device"
3410
+ },
3411
+ {
3412
+ name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
3413
+ value: "pat"
3414
+ }
3415
+ ]
3416
+ });
3417
+ let githubToken;
3418
+ let githubClientId;
3419
+ if (authMethod === "device") {
3420
+ const result = await getGitHubTokenViaDeviceFlow();
3421
+ githubToken = result.token;
3422
+ githubClientId = result.clientId;
3423
+ } else {
3424
+ const result = await getGitHubTokenViaPAT();
3425
+ githubToken = result.token;
2854
3426
  }
2855
- console.log(chalk12.dim(" \u2514" + "\u2500".repeat(67)));
2856
- let answer;
3427
+ const providerChoice = await select11({
3428
+ message: "AI provider:",
3429
+ choices: [
3430
+ { name: `OpenRouter (recommended) ${chalk13.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
3431
+ { name: "Custom (specify base URL and model)", value: "custom" }
3432
+ ]
3433
+ });
3434
+ let aiBaseUrl;
3435
+ let aiModel;
3436
+ if (providerChoice === "custom") {
3437
+ aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
3438
+ aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
3439
+ }
3440
+ const apiKeyHint = providerChoice === "openrouter" ? chalk13.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk13.dim(" API key for your provider\n");
3441
+ console.log(apiKeyHint);
3442
+ const aiApiKey = await password({
3443
+ message: "API Key:",
3444
+ mask: "*"
3445
+ });
3446
+ let owner = detectedOwner;
3447
+ let repo = detectedRepo;
3448
+ if (!owner || !repo) {
3449
+ owner = await input({
3450
+ message: "GitHub repo owner (user or org):",
3451
+ required: true
3452
+ });
3453
+ repo = await input({
3454
+ message: "GitHub repo name:",
3455
+ required: true
3456
+ });
3457
+ }
3458
+ const config = {
3459
+ githubToken,
3460
+ githubClientId,
3461
+ aiApiKey: aiApiKey.trim(),
3462
+ ...aiBaseUrl ? { aiBaseUrl } : {},
3463
+ ...aiModel ? { aiModel } : {},
3464
+ github: {
3465
+ owner: owner.trim(),
3466
+ repo: repo.trim()
3467
+ }
3468
+ };
3469
+ setConfig(config);
3470
+ const spinner = ora14("Setting up GitHub labels...").start();
2857
3471
  try {
2858
- const chosen = await select10({
2859
- message: " ",
3472
+ await ensureLabels(config);
3473
+ spinner.succeed("GitHub labels created");
3474
+ } catch (err) {
3475
+ spinner.fail("Failed to create labels (check token permissions)");
3476
+ console.error(chalk13.red(String(err)));
3477
+ }
3478
+ console.log(chalk13.green("\nSetup complete!"));
3479
+ console.log(chalk13.dim(`Config saved to: ${getConfigPath()}
3480
+ `));
3481
+ let genWiki = false;
3482
+ try {
3483
+ genWiki = await select11({
3484
+ message: "Generate TECHUNTER.md project overview for new team members?",
2860
3485
  choices: [
2861
- ...options.map((o) => ({ name: o, value: o })),
2862
- { name: chalk12.dim("Other (describe below)"), value: OTHER }
3486
+ { name: "Yes, generate now", value: true },
3487
+ { name: "No, skip (run /wiki later)", value: false }
2863
3488
  ]
2864
3489
  });
2865
- answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
2866
3490
  } catch {
2867
- answer = "User skipped this question \u2014 use your best judgement.";
2868
3491
  }
2869
- console.log("");
2870
- return answer;
3492
+ if (genWiki) {
3493
+ const wikiSpinner = ora14("Analyzing project and generating TECHUNTER.md\u2026").start();
3494
+ try {
3495
+ const content = await generateWiki(config);
3496
+ await upsertRepoFile(config, "TECHUNTER.md", content, "docs: add TECHUNTER.md project overview");
3497
+ wikiSpinner.succeed("TECHUNTER.md created");
3498
+ } catch (err) {
3499
+ wikiSpinner.fail(`Could not generate wiki: ${err.message}`);
3500
+ }
3501
+ console.log("");
3502
+ }
2871
3503
  }
2872
3504
 
2873
- // src/tools/registry.ts
2874
- var toolModules = [
2875
- // Command tools
2876
- pick_exports,
2877
- new_task_exports,
2878
- close_exports,
2879
- submit_exports,
2880
- my_status_exports,
2881
- review_exports,
2882
- refresh_exports,
2883
- open_code_exports,
2884
- reject_exports,
2885
- accept_exports,
2886
- edit_task_exports,
2887
- // Low-level tools
2888
- list_tasks_exports,
2889
- get_task_exports,
2890
- get_comments_exports,
2891
- get_diff_exports,
2892
- run_command_exports,
2893
- list_files_exports,
2894
- grep_code_exports,
2895
- ask_user_exports
2896
- ];
3505
+ // src/commands/config.ts
3506
+ import { input as input2, password as password2, select as select12 } from "@inquirer/prompts";
3507
+ import chalk14 from "chalk";
3508
+ async function configCommand() {
3509
+ let config;
3510
+ try {
3511
+ config = getConfig();
3512
+ } catch {
3513
+ console.error(chalk14.red("No config found. Run `tch init` first."));
3514
+ process.exit(1);
3515
+ }
3516
+ console.log(chalk14.bold.cyan("\nTechunter \u2014 Settings\n"));
3517
+ console.log(chalk14.dim(`Config file: ${getConfigPath()}
3518
+ `));
3519
+ const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
3520
+ const currentModel = config.aiModel ?? DEFAULT_MODEL;
3521
+ const currentBaseBranch = config.baseBranch ?? "main";
3522
+ const field = await select12({
3523
+ message: "Which setting to change?",
3524
+ choices: [
3525
+ { name: `GitHub repo ${chalk14.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
3526
+ { name: `Base branch ${chalk14.dim(currentBaseBranch)}`, value: "baseBranch" },
3527
+ { name: `AI base URL ${chalk14.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
3528
+ { name: `AI model ${chalk14.dim(currentModel)}`, value: "aiModel" },
3529
+ { name: `AI API Key ${chalk14.dim("(hidden)")}`, value: "aiApiKey" },
3530
+ { name: `GitHub Token ${chalk14.dim("(hidden)")}`, value: "githubToken" },
3531
+ { name: "Cancel", value: "cancel" }
3532
+ ]
3533
+ });
3534
+ if (field === "cancel") return;
3535
+ if (field === "baseBranch") {
3536
+ const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
3537
+ if (val.trim()) {
3538
+ setConfig({ baseBranch: val.trim() });
3539
+ console.log(chalk14.green(`
3540
+ Base branch set to: ${val.trim()}
3541
+ `));
3542
+ }
3543
+ } else if (field === "repo") {
3544
+ const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
3545
+ const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
3546
+ setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
3547
+ console.log(chalk14.green(`
3548
+ Repo set to: ${owner.trim()}/${repo.trim()}
3549
+ `));
3550
+ } else if (field === "aiBaseUrl") {
3551
+ const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
3552
+ if (val.trim()) {
3553
+ setConfig({ aiBaseUrl: val.trim() });
3554
+ console.log(chalk14.green(`
3555
+ AI base URL set to: ${val.trim()}
3556
+ `));
3557
+ }
3558
+ } else if (field === "aiModel") {
3559
+ const val = await input2({ message: "AI model name:", default: currentModel });
3560
+ if (val.trim()) {
3561
+ setConfig({ aiModel: val.trim() });
3562
+ console.log(chalk14.green(`
3563
+ AI model set to: ${val.trim()}
3564
+ `));
3565
+ }
3566
+ } else if (field === "aiApiKey") {
3567
+ const val = await password2({ message: "New AI API Key:", mask: "*" });
3568
+ if (val.trim()) {
3569
+ setConfig({ aiApiKey: val.trim() });
3570
+ console.log(chalk14.green("\nAI API Key updated.\n"));
3571
+ }
3572
+ } else if (field === "githubToken") {
3573
+ const val = await password2({ message: "New GitHub Token:", mask: "*" });
3574
+ if (val.trim()) {
3575
+ setConfig({ githubToken: val.trim() });
3576
+ console.log(chalk14.green("\nGitHub Token updated.\n"));
3577
+ }
3578
+ }
3579
+ }
3580
+
3581
+ // src/index.ts
3582
+ init_github();
2897
3583
 
2898
3584
  // src/lib/agent.ts
3585
+ import ora15 from "ora";
3586
+ import chalk15 from "chalk";
2899
3587
  var tools = toolModules.map((m) => m.definition);
2900
3588
  var HISTORY_KEEP_TURNS = 8;
2901
3589
  function trimHistory(messages) {
@@ -2981,7 +3669,7 @@ async function runAgentLoop(config, messages) {
2981
3669
  throw new Error(`Agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
2982
3670
  }
2983
3671
  trimHistory(messages);
2984
- const spinner = ora14({ text: chalk13.dim("Thinking\u2026"), color: "cyan" }).start();
3672
+ const spinner = ora15({ text: chalk15.dim("Thinking\u2026"), color: "cyan" }).start();
2985
3673
  let response;
2986
3674
  try {
2987
3675
  response = await client.chat.completions.create({
@@ -3026,7 +3714,7 @@ async function runAgentLoop(config, messages) {
3026
3714
  return executeTool(tc.function.name, parsed, config);
3027
3715
  })
3028
3716
  );
3029
- let terminal12 = false;
3717
+ let terminal13 = false;
3030
3718
  for (let i = 0; i < toolCalls.length; i++) {
3031
3719
  printToolResult(results[i]);
3032
3720
  messages.push({
@@ -3035,16 +3723,93 @@ async function runAgentLoop(config, messages) {
3035
3723
  content: results[i]
3036
3724
  });
3037
3725
  if (toolModules.find((m) => m.definition.function.name === toolCalls[i].function.name)?.terminal) {
3038
- terminal12 = true;
3726
+ terminal13 = true;
3039
3727
  }
3040
3728
  }
3041
- if (terminal12) return results[results.length - 1];
3729
+ if (terminal13) return results[results.length - 1];
3042
3730
  } else {
3043
3731
  return choice.message.content ?? "";
3044
3732
  }
3045
3733
  }
3046
3734
  }
3047
3735
 
3736
+ // src/lib/update-check.ts
3737
+ import Conf2 from "conf";
3738
+ import chalk16 from "chalk";
3739
+ import { execFile } from "child_process";
3740
+ var PACKAGE_NAME = "techunter";
3741
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3742
+ var cache = new Conf2({
3743
+ projectName: "techunter-update-cache",
3744
+ defaults: { lastChecked: 0, latestVersion: "" }
3745
+ });
3746
+ async function fetchLatestVersion() {
3747
+ try {
3748
+ const { fetch } = await import("undici");
3749
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
3750
+ signal: AbortSignal.timeout(5e3)
3751
+ });
3752
+ if (!res.ok) return null;
3753
+ const data = await res.json();
3754
+ return data.version ?? null;
3755
+ } catch {
3756
+ return null;
3757
+ }
3758
+ }
3759
+ function isNewer(latest, current) {
3760
+ const parse = (v) => v.split(".").map(Number);
3761
+ const [la, lb, lc] = parse(latest);
3762
+ const [ca, cb, cc] = parse(current);
3763
+ if (la !== ca) return la > ca;
3764
+ if (lb !== cb) return lb > cb;
3765
+ return lc > cc;
3766
+ }
3767
+ async function getAvailableUpdate(currentVersion) {
3768
+ const now = Date.now();
3769
+ const lastChecked = cache.get("lastChecked");
3770
+ let latest = cache.get("latestVersion");
3771
+ if (!latest || now - lastChecked > CHECK_INTERVAL_MS) {
3772
+ const fetched = await fetchLatestVersion();
3773
+ if (fetched) {
3774
+ latest = fetched;
3775
+ cache.set("latestVersion", fetched);
3776
+ cache.set("lastChecked", now);
3777
+ }
3778
+ }
3779
+ return latest && isNewer(latest, currentVersion) ? latest : null;
3780
+ }
3781
+ function installUpdate() {
3782
+ return new Promise((resolve, reject) => {
3783
+ execFile("npm", ["install", "-g", PACKAGE_NAME], { shell: true }, (err, _stdout, stderr) => {
3784
+ if (err) {
3785
+ reject(new Error(stderr.trim() || err.message));
3786
+ } else {
3787
+ resolve(cache.get("latestVersion"));
3788
+ }
3789
+ });
3790
+ });
3791
+ }
3792
+ async function startAutoUpdate(currentVersion) {
3793
+ const latest = await Promise.race([
3794
+ getAvailableUpdate(currentVersion),
3795
+ new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
3796
+ ]);
3797
+ if (!latest) return;
3798
+ console.log(
3799
+ chalk16.cyan("\n \u2191 Auto-updating to v" + latest + "\u2026") + chalk16.dim(" (running in background)\n")
3800
+ );
3801
+ installUpdate().then((installedVersion) => {
3802
+ console.log(
3803
+ "\n" + chalk16.green(" \u2714 Updated to v" + (installedVersion || latest)) + chalk16.dim(" \u2014 restart tch to use the new version\n") + chalk16.cyan(" You \u203A ")
3804
+ // redraw the prompt hint
3805
+ );
3806
+ }).catch((err) => {
3807
+ console.log(
3808
+ "\n" + chalk16.red(" \u2718 Auto-update failed: ") + chalk16.dim(err.message) + "\n" + chalk16.dim(" Run manually: npm install -g techunter\n") + chalk16.cyan(" You \u203A ")
3809
+ );
3810
+ });
3811
+ }
3812
+
3048
3813
  // src/index.ts
3049
3814
  var _require = createRequire(import.meta.url);
3050
3815
  var { version } = _require("../package.json");
@@ -3071,6 +3836,8 @@ var SLASH_NAMES = [
3071
3836
  "/me",
3072
3837
  "/code",
3073
3838
  "/c",
3839
+ "/wiki",
3840
+ "/w",
3074
3841
  "/config",
3075
3842
  "/cfg",
3076
3843
  "/init"
@@ -3087,7 +3854,7 @@ function promptUser() {
3087
3854
  return new Promise((resolve) => {
3088
3855
  if (process.stdin.isPaused()) process.stdin.resume();
3089
3856
  _rl.resume();
3090
- _rl.question(chalk14.cyan("You") + chalk14.dim(" \u203A "), resolve);
3857
+ _rl.question(chalk17.cyan("You") + chalk17.dim(" \u203A "), resolve);
3091
3858
  });
3092
3859
  }
3093
3860
  var COMMANDS = [
@@ -3103,44 +3870,45 @@ var COMMANDS = [
3103
3870
  { cmd: "/config", alias: "/cfg", desc: "Change settings (branch, repo, API keys)" },
3104
3871
  { cmd: "/init", desc: "Re-run setup wizard for this repo" },
3105
3872
  { cmd: "/status", alias: "/me", desc: "Show tasks assigned to you" },
3106
- { cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" }
3873
+ { cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" },
3874
+ { cmd: "/wiki", alias: "/w", desc: "Generate or refresh TECHUNTER.md project overview" }
3107
3875
  ];
3108
3876
  function cmdHelp() {
3109
3877
  console.log("");
3110
- console.log(chalk14.bold(" Commands"));
3111
- console.log(chalk14.dim(" \u2500".repeat(35)));
3878
+ console.log(chalk17.bold(" Commands"));
3879
+ console.log(chalk17.dim(" \u2500".repeat(35)));
3112
3880
  for (const { cmd, alias, desc } of COMMANDS) {
3113
- const left = (cmd + (alias ? ` ${chalk14.dim(alias)}` : "")).padEnd(22);
3114
- console.log(` ${chalk14.cyan(cmd)}${alias ? " " + chalk14.dim(alias) : ""}`.padEnd(30) + chalk14.dim(desc));
3881
+ const left = (cmd + (alias ? ` ${chalk17.dim(alias)}` : "")).padEnd(22);
3882
+ console.log(` ${chalk17.cyan(cmd)}${alias ? " " + chalk17.dim(alias) : ""}`.padEnd(30) + chalk17.dim(desc));
3115
3883
  }
3116
- console.log(chalk14.dim("\n Anything else is sent to the AI agent.\n"));
3884
+ console.log(chalk17.dim("\n Anything else is sent to the AI agent.\n"));
3117
3885
  }
3118
3886
  function printBanner(config) {
3119
3887
  const { owner, repo } = config.github;
3120
- const g = chalk14.cyan;
3121
- const b = chalk14.bold.white;
3122
- const p = chalk14.yellow.bold;
3888
+ const g = chalk17.cyan;
3889
+ const b = chalk17.bold.white;
3890
+ const p = chalk17.yellow.bold;
3123
3891
  console.log("");
3124
3892
  console.log(" " + g("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
3125
3893
  console.log(p("\u25C6") + b("\u2550\u2550\u2550") + g("\u256C") + b(" TECHUNTER ") + g("\u256C") + b("\u2550\u2550\u2550\u25B6"));
3126
3894
  console.log(" " + g("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
3127
3895
  console.log("");
3128
3896
  console.log(
3129
- " " + chalk14.bold("Techunter") + chalk14.dim(` v${version}`) + chalk14.dim(" \xB7 ") + chalk14.cyan(getModel(config)) + chalk14.dim(" \xB7 ") + chalk14.dim(`${owner}/${repo}`)
3897
+ " " + chalk17.bold("Techunter") + chalk17.dim(` v${version}`) + chalk17.dim(" \xB7 ") + chalk17.cyan(getModel(config)) + chalk17.dim(" \xB7 ") + chalk17.dim(`${owner}/${repo}`)
3130
3898
  );
3131
3899
  console.log("");
3132
3900
  }
3133
3901
  async function initNewRepo(config, owner, repo) {
3134
3902
  console.log("");
3135
- console.log(chalk14.bold.cyan(` New repo detected: ${owner}/${repo}`));
3136
- console.log(chalk14.dim(" Setting up Techunter for this repository...\n"));
3903
+ console.log(chalk17.bold.cyan(` New repo detected: ${owner}/${repo}`));
3904
+ console.log(chalk17.dim(" Setting up Techunter for this repository...\n"));
3137
3905
  const newConfig = {
3138
3906
  ...config,
3139
3907
  github: { owner, repo }
3140
3908
  };
3141
3909
  setConfig({ github: newConfig.github });
3142
- const ora15 = (await import("ora")).default;
3143
- const spinner = ora15("Creating Techunter labels...").start();
3910
+ const ora16 = (await import("ora")).default;
3911
+ const spinner = ora16("Creating Techunter labels...").start();
3144
3912
  try {
3145
3913
  await ensureLabels(newConfig);
3146
3914
  spinner.succeed("Labels ready");
@@ -3156,7 +3924,7 @@ async function main() {
3156
3924
  try {
3157
3925
  await configCommand();
3158
3926
  } catch (err) {
3159
- console.error(chalk14.red(`
3927
+ console.error(chalk17.red(`
3160
3928
  Error: ${err.message}`));
3161
3929
  process.exit(1);
3162
3930
  }
@@ -3170,7 +3938,7 @@ Error: ${err.message}`));
3170
3938
  await initCommand();
3171
3939
  config = getConfig();
3172
3940
  } catch (err) {
3173
- console.error(chalk14.red(`
3941
+ console.error(chalk17.red(`
3174
3942
  Setup failed: ${err.message}`));
3175
3943
  process.exit(1);
3176
3944
  return;
@@ -3188,11 +3956,13 @@ Setup failed: ${err.message}`));
3188
3956
  }
3189
3957
  }
3190
3958
  } else if (!config.github.owner) {
3191
- console.error(chalk14.red("\nNo git remote found and no repo configured. Run tch init."));
3959
+ console.error(chalk17.red("\nNo git remote found and no repo configured. Run tch init."));
3192
3960
  process.exit(1);
3193
3961
  }
3194
3962
  printBanner(config);
3195
- console.log(chalk14.dim(" Type /help for commands, or describe what you want.\n"));
3963
+ console.log(chalk17.dim(" Type /help for commands, or describe what you want.\n"));
3964
+ startAutoUpdate(version).catch(() => {
3965
+ });
3196
3966
  await printTaskList(config);
3197
3967
  await printMyTasks(config);
3198
3968
  _rl = readline.createInterface({
@@ -3202,11 +3972,11 @@ Setup failed: ${err.message}`));
3202
3972
  terminal: true
3203
3973
  });
3204
3974
  _rl.on("close", () => {
3205
- console.log(chalk14.gray("\nGoodbye!"));
3975
+ console.log(chalk17.gray("\nGoodbye!"));
3206
3976
  process.exit(0);
3207
3977
  });
3208
3978
  _rl.on("SIGINT", () => {
3209
- console.log(chalk14.gray("\nGoodbye!"));
3979
+ console.log(chalk17.gray("\nGoodbye!"));
3210
3980
  process.exit(0);
3211
3981
  });
3212
3982
  const messages = [];
@@ -3223,7 +3993,7 @@ Setup failed: ${err.message}`));
3223
3993
  break;
3224
3994
  case "/refresh":
3225
3995
  case "/r":
3226
- await run7({}, config);
3996
+ await run9({}, config);
3227
3997
  break;
3228
3998
  case "/pick":
3229
3999
  case "/p": {
@@ -3231,7 +4001,7 @@ Setup failed: ${err.message}`));
3231
4001
  const preselected = arg ? parseInt(arg, 10) : void 0;
3232
4002
  const result = await run3({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
3233
4003
  if (result && result !== "Cancelled.") {
3234
- console.log(chalk14.green(`
4004
+ console.log(chalk17.green(`
3235
4005
  ${result}
3236
4006
  `));
3237
4007
  }
@@ -3241,7 +4011,7 @@ Setup failed: ${err.message}`));
3241
4011
  case "/new":
3242
4012
  case "/n": {
3243
4013
  const result = await run4({}, config);
3244
- console.log(chalk14.green(`
4014
+ console.log(chalk17.green(`
3245
4015
  ${result}
3246
4016
  `));
3247
4017
  await printTaskList(config);
@@ -3259,7 +4029,7 @@ Setup failed: ${err.message}`));
3259
4029
  const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
3260
4030
  const preselected = arg ? parseInt(arg, 10) : void 0;
3261
4031
  const result = await run11({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
3262
- console.log(chalk14.green(`
4032
+ console.log(chalk17.green(`
3263
4033
  ${result}
3264
4034
  `));
3265
4035
  await printTaskList(config);
@@ -3268,7 +4038,7 @@ Setup failed: ${err.message}`));
3268
4038
  case "/close":
3269
4039
  case "/d": {
3270
4040
  const result = await run2({}, config);
3271
- console.log(chalk14.green(`
4041
+ console.log(chalk17.green(`
3272
4042
  ${result}
3273
4043
  `));
3274
4044
  await printTaskList(config);
@@ -3276,7 +4046,7 @@ Setup failed: ${err.message}`));
3276
4046
  }
3277
4047
  case "/review":
3278
4048
  case "/rv": {
3279
- const result = await run6({}, config);
4049
+ const result = await run8({}, config);
3280
4050
  console.log("\n" + renderMarkdown(result));
3281
4051
  break;
3282
4052
  }
@@ -3290,8 +4060,8 @@ Setup failed: ${err.message}`));
3290
4060
  case "/ac": {
3291
4061
  const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
3292
4062
  const preselected = arg ? parseInt(arg, 10) : void 0;
3293
- const result = await run10({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
3294
- console.log(chalk14.green(`
4063
+ const result = await run6({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
4064
+ console.log(chalk17.green(`
3295
4065
  ${result}
3296
4066
  `));
3297
4067
  await printTaskList(config);
@@ -3307,17 +4077,25 @@ Setup failed: ${err.message}`));
3307
4077
  config = getConfig();
3308
4078
  await printTaskList(config);
3309
4079
  } catch (err) {
3310
- console.error(chalk14.red(`
4080
+ console.error(chalk17.red(`
3311
4081
  Init failed: ${err.message}
3312
4082
  `));
3313
4083
  }
3314
4084
  break;
3315
4085
  case "/code":
3316
4086
  case "/c":
3317
- await run8({}, config);
4087
+ await run10({}, config);
3318
4088
  break;
4089
+ case "/wiki":
4090
+ case "/w": {
4091
+ const result = await run12({}, config);
4092
+ console.log(chalk17.green(`
4093
+ ${result}
4094
+ `));
4095
+ break;
4096
+ }
3319
4097
  default:
3320
- console.log(chalk14.yellow(` Unknown command: ${cmd} (try /help)`));
4098
+ console.log(chalk17.yellow(` Unknown command: ${cmd} (try /help)`));
3321
4099
  }
3322
4100
  continue;
3323
4101
  }
@@ -3325,10 +4103,10 @@ Init failed: ${err.message}
3325
4103
  messages.push({ role: "user", content: userInput });
3326
4104
  try {
3327
4105
  const response = await runAgentLoop(config, messages);
3328
- console.log("\n" + chalk14.green("Techunter:") + "\n" + renderMarkdown(response));
4106
+ console.log("\n" + chalk17.green("Techunter:") + "\n" + renderMarkdown(response));
3329
4107
  } catch (err) {
3330
4108
  messages.splice(prevLength);
3331
- console.error(chalk14.red(`
4109
+ console.error(chalk17.red(`
3332
4110
  Error: ${err.message}
3333
4111
  `));
3334
4112
  }
@@ -3336,6 +4114,6 @@ Error: ${err.message}
3336
4114
  }
3337
4115
  }
3338
4116
  main().catch((err) => {
3339
- console.error(chalk14.red(`Fatal: ${err.message}`));
4117
+ console.error(chalk17.red(`Fatal: ${err.message}`));
3340
4118
  process.exit(1);
3341
4119
  });