techunter 0.1.11 → 1.0.1

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 +135 -107
  2. package/dist/index.js +1960 -1003
  3. package/dist/mcp.js +1212 -382
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -39,12 +39,22 @@ __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,
49
+ getBranchHeadSha: () => getBranchHeadSha,
46
50
  getDefaultBranch: () => getDefaultBranch,
51
+ getIssueNumberFromBranch: () => getIssueNumberFromBranch,
52
+ getOpenSubtasks: () => getOpenSubtasks,
53
+ getRepoFile: () => getRepoFile,
47
54
  getTask: () => getTask,
55
+ getTaskBranch: () => getTaskBranch,
56
+ getTaskPR: () => getTaskPR,
57
+ getTaskPRDiff: () => getTaskPRDiff,
48
58
  isCollaborator: () => isCollaborator,
49
59
  listComments: () => listComments,
50
60
  listMyTasks: () => listMyTasks,
@@ -52,9 +62,11 @@ __export(github_exports, {
52
62
  listTasksForReview: () => listTasksForReview,
53
63
  markInReview: () => markInReview,
54
64
  mergeWorkerIntoBase: () => mergeWorkerIntoBase,
65
+ moveTask: () => moveTask,
55
66
  postComment: () => postComment,
56
67
  postGuideComment: () => postGuideComment,
57
- rejectTask: () => rejectTask
68
+ rejectTask: () => rejectTask,
69
+ upsertRepoFile: () => upsertRepoFile
58
70
  });
59
71
  import { Octokit } from "@octokit/rest";
60
72
  import { fetch as undiciFetch } from "undici";
@@ -117,11 +129,22 @@ function extractBaseCommit(body) {
117
129
  const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
118
130
  return match?.[1] ?? null;
119
131
  }
120
- async function createTask(config, title, body, baseCommit) {
132
+ function embedTargetBranch(body, branch) {
133
+ return `${body}
134
+ ${TARGET_BRANCH_MARKER}${branch} -->`;
135
+ }
136
+ function extractTargetBranch(body) {
137
+ if (!body) return null;
138
+ const match = body.match(/<!-- techunter-target:([^\s>]+) -->/);
139
+ return match?.[1] ?? null;
140
+ }
141
+ async function createTask(config, title, body, baseCommit, targetBranch) {
121
142
  const octokit = createOctokit(config.githubToken);
122
143
  const { owner, repo } = config.github;
123
144
  await ensureLabels(config);
124
- const finalBody = baseCommit ? embedBaseCommit(body ?? "", baseCommit) : body;
145
+ let finalBody = body ?? "";
146
+ if (baseCommit) finalBody = embedBaseCommit(finalBody, baseCommit);
147
+ if (targetBranch) finalBody = embedTargetBranch(finalBody, targetBranch);
125
148
  const { data } = await octokit.issues.create({
126
149
  owner,
127
150
  repo,
@@ -134,13 +157,22 @@ async function createTask(config, title, body, baseCommit) {
134
157
  async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
135
158
  const octokit = createOctokit(config.githubToken);
136
159
  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
- });
160
+ try {
161
+ await octokit.repos.merge({
162
+ owner,
163
+ repo,
164
+ base: baseBranch,
165
+ head: workerBranch,
166
+ commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
167
+ });
168
+ } catch (err) {
169
+ if (err.status === 409) {
170
+ throw new Error(
171
+ `Merge conflict: ${workerBranch} cannot be merged into ${baseBranch} cleanly. Resolve conflicts manually.`
172
+ );
173
+ }
174
+ throw err;
175
+ }
144
176
  }
145
177
  async function claimTask(config, number, username) {
146
178
  const octokit = createOctokit(config.githubToken);
@@ -216,6 +248,23 @@ async function postGuideComment(config, number, guide) {
216
248
  body
217
249
  });
218
250
  }
251
+ async function ensureRemoteBranch(config, branchName, fallbackBase) {
252
+ const octokit = createOctokit(config.githubToken);
253
+ const { owner, repo } = config.github;
254
+ try {
255
+ await octokit.repos.getBranch({ owner, repo, branch: branchName });
256
+ return;
257
+ } catch (err) {
258
+ if (err.status !== 404) throw err;
259
+ }
260
+ const { data: baseRef } = await octokit.repos.getBranch({ owner, repo, branch: fallbackBase });
261
+ await octokit.git.createRef({
262
+ owner,
263
+ repo,
264
+ ref: `refs/heads/${branchName}`,
265
+ sha: baseRef.commit.sha
266
+ });
267
+ }
219
268
  async function createPR(config, title, body, branch, base) {
220
269
  const octokit = createOctokit(config.githubToken);
221
270
  const { owner, repo } = config.github;
@@ -232,14 +281,11 @@ async function createPR(config, title, body, branch, base) {
232
281
  async function markInReview(config, number) {
233
282
  const octokit = createOctokit(config.githubToken);
234
283
  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 {
284
+ for (const label of [LABEL_CLAIMED, LABEL_CHANGES_NEEDED]) {
285
+ try {
286
+ await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
287
+ } catch {
288
+ }
243
289
  }
244
290
  await octokit.issues.addLabels({
245
291
  owner,
@@ -354,28 +400,146 @@ async function editTask(config, number, title, body) {
354
400
  const { owner, repo } = config.github;
355
401
  await octokit.issues.update({ owner, repo, issue_number: number, title, body });
356
402
  }
403
+ async function upsertRepoFile(config, filePath, content, message) {
404
+ const octokit = createOctokit(config.githubToken);
405
+ const { owner, repo } = config.github;
406
+ let sha;
407
+ try {
408
+ const { data: data2 } = await octokit.repos.getContent({ owner, repo, path: filePath });
409
+ if (!Array.isArray(data2) && data2.type === "file") {
410
+ sha = data2.sha;
411
+ }
412
+ } catch {
413
+ }
414
+ const { data } = await octokit.repos.createOrUpdateFileContents({
415
+ owner,
416
+ repo,
417
+ path: filePath,
418
+ message,
419
+ content: Buffer.from(content, "utf-8").toString("base64"),
420
+ ...sha ? { sha } : {}
421
+ });
422
+ return data.content?.html_url ?? `https://github.com/${owner}/${repo}/blob/main/${filePath}`;
423
+ }
424
+ async function getRepoFile(config, filePath) {
425
+ const octokit = createOctokit(config.githubToken);
426
+ const { owner, repo } = config.github;
427
+ try {
428
+ const { data } = await octokit.repos.getContent({ owner, repo, path: filePath });
429
+ if (!Array.isArray(data) && data.type === "file" && "content" in data) {
430
+ return Buffer.from(data.content, "base64").toString("utf-8");
431
+ }
432
+ return null;
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
357
437
  async function getDefaultBranch(config) {
358
438
  const octokit = createOctokit(config.githubToken);
359
439
  const { owner, repo } = config.github;
360
440
  const { data } = await octokit.repos.get({ owner, repo });
361
441
  return data.default_branch;
362
442
  }
363
- async function acceptTask(config, issueNumber, headBranch) {
443
+ async function getTaskBranch(config, issueNumber) {
364
444
  const octokit = createOctokit(config.githubToken);
365
445
  const { owner, repo } = config.github;
366
446
  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({
447
+ const pr = prs.find((p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? ""));
448
+ if (pr) return pr.head.ref;
449
+ const { data: branches } = await octokit.repos.listBranches({ owner, repo, per_page: 100 });
450
+ const taskBranch = branches.find((b) => new RegExp(`^task-${issueNumber}-`).test(b.name));
451
+ return taskBranch?.name ?? null;
452
+ }
453
+ async function getBranchHeadSha(config, branchName) {
454
+ const octokit = createOctokit(config.githubToken);
455
+ const { owner, repo } = config.github;
456
+ try {
457
+ const { data } = await octokit.repos.getBranch({ owner, repo, branch: branchName });
458
+ return data.commit.sha;
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ async function moveTask(config, issueNumber, newTargetBranch, newBaseCommit) {
464
+ const octokit = createOctokit(config.githubToken);
465
+ const { owner, repo } = config.github;
466
+ const { data } = await octokit.issues.get({ owner, repo, issue_number: issueNumber });
467
+ let body = data.body ?? "";
468
+ body = body.replace(/\n*<!-- techunter-base:[a-f0-9]{7,40} -->/g, "");
469
+ body = body.replace(/\n*<!-- techunter-target:[^\s>]+ -->/g, "");
470
+ body = embedBaseCommit(body, newBaseCommit);
471
+ body = embedTargetBranch(body, newTargetBranch);
472
+ await octokit.issues.update({ owner, repo, issue_number: issueNumber, body });
473
+ }
474
+ async function getTaskPR(config, issueNumber) {
475
+ const octokit = createOctokit(config.githubToken);
476
+ const { owner, repo } = config.github;
477
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
478
+ const pr = prs.find(
479
+ (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
480
+ );
481
+ if (!pr) return null;
482
+ return { number: pr.number, url: pr.html_url, body: pr.body ?? "", baseBranch: pr.base.ref };
483
+ }
484
+ async function getOpenSubtasks(config, targetBranch) {
485
+ const octokit = createOctokit(config.githubToken);
486
+ const { owner, repo } = config.github;
487
+ const { data } = await octokit.issues.listForRepo({
488
+ owner,
489
+ repo,
490
+ state: "open",
491
+ per_page: 100
492
+ });
493
+ return data.filter((issue) => !issue.pull_request).filter((issue) => extractTargetBranch(issue.body ?? null) === targetBranch).map((issue) => issue.number);
494
+ }
495
+ async function getIssueNumberFromBranch(config, branch) {
496
+ const octokit = createOctokit(config.githubToken);
497
+ const { owner, repo } = config.github;
498
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
499
+ const pr = prs.find((p) => p.head.ref === branch);
500
+ if (!pr) return null;
501
+ const match = (pr.body ?? "").match(/Closes #(\d+)/i);
502
+ if (!match) return null;
503
+ return { issueNumber: parseInt(match[1], 10), prUrl: pr.html_url };
504
+ }
505
+ async function getTaskPRDiff(config, prNumber) {
506
+ const octokit = createOctokit(config.githubToken);
507
+ const { owner, repo } = config.github;
508
+ const response = await octokit.pulls.get({
370
509
  owner,
371
510
  repo,
372
- pull_number: pr.number,
373
- merge_method: "merge"
511
+ pull_number: prNumber,
512
+ mediaType: { format: "diff" }
374
513
  });
375
- await closeTask(config, issueNumber);
376
- return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "" };
514
+ return response.data;
515
+ }
516
+ async function acceptTask(config, issueNumber) {
517
+ const octokit = createOctokit(config.githubToken);
518
+ const { owner, repo } = config.github;
519
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
520
+ const pr = prs.find(
521
+ (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
522
+ );
523
+ if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
524
+ try {
525
+ const { data: merge } = await octokit.pulls.merge({
526
+ owner,
527
+ repo,
528
+ pull_number: pr.number,
529
+ merge_method: "merge"
530
+ });
531
+ await closeTask(config, issueNumber);
532
+ return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "", baseBranch: pr.base.ref };
533
+ } catch (err) {
534
+ if (err.status === 405) {
535
+ throw new Error(
536
+ `PR #${pr.number} cannot be merged \u2014 may have conflicts or is not in a mergeable state.`
537
+ );
538
+ }
539
+ throw err;
540
+ }
377
541
  }
378
- var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER;
542
+ var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER, TARGET_BRANCH_MARKER;
379
543
  var init_github = __esm({
380
544
  "src/lib/github.ts"() {
381
545
  "use strict";
@@ -392,19 +556,20 @@ var init_github = __esm({
392
556
  ];
393
557
  TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
394
558
  BASE_COMMIT_MARKER = "<!-- techunter-base:";
559
+ TARGET_BRANCH_MARKER = "<!-- techunter-target:";
395
560
  }
396
561
  });
397
562
 
398
563
  // src/index.ts
399
- import chalk14 from "chalk";
564
+ import chalk18 from "chalk";
400
565
  import readline from "readline";
401
566
  import { createRequire } from "module";
402
567
 
403
568
  // 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";
569
+ import { input, password, select as select12 } from "@inquirer/prompts";
570
+ import chalk14 from "chalk";
571
+ import ora15 from "ora";
572
+ import open2 from "open";
408
573
  import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
409
574
 
410
575
  // src/lib/config.ts
@@ -423,7 +588,8 @@ var configSchema = z.object({
423
588
  }),
424
589
  taskState: z.object({
425
590
  activeIssueNumber: z.number().optional(),
426
- baseCommit: z.string().optional()
591
+ baseCommit: z.string().optional(),
592
+ activeBranch: z.string().optional()
427
593
  }).optional()
428
594
  });
429
595
  var store = new Conf({
@@ -510,14 +676,21 @@ function parseOwnerRepo(remoteUrl) {
510
676
  }
511
677
  return null;
512
678
  }
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
679
  function makeWorkerBranchName(username) {
518
680
  const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
519
681
  return `worker-${slug}`;
520
682
  }
683
+ function makeTaskBranchName(issueNumber, username) {
684
+ const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
685
+ return `task-${issueNumber}-${slug}`;
686
+ }
687
+ function isTaskBranch(branch) {
688
+ return /^task-\d+-/.test(branch);
689
+ }
690
+ function parseIssueNumberFromBranch(branch) {
691
+ const match = branch.match(/^task-(\d+)-/);
692
+ return match ? parseInt(match[1], 10) : null;
693
+ }
521
694
  async function getCurrentCommit() {
522
695
  return (await git.revparse(["HEAD"])).trim();
523
696
  }
@@ -638,16 +811,27 @@ async function getRemoteHeadSha(baseBranch) {
638
811
  await git.fetch("origin", baseBranch);
639
812
  return (await git.revparse([`origin/${baseBranch}`])).trim();
640
813
  }
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) {
814
+ async function checkoutFromCommit(branchName, sha) {
815
+ const branches = await git.branch(["-a"]);
816
+ const exists = Object.keys(branches.branches).some(
817
+ (b) => b === branchName || b === `remotes/origin/${branchName}`
818
+ );
819
+ if (exists) {
645
820
  await git.checkout(branchName);
646
- await git.reset(["--hard", sha]);
647
821
  } else {
648
822
  await git.checkoutBranch(branchName, sha);
649
823
  }
650
824
  }
825
+ async function hasUncommittedChanges() {
826
+ const status = await git.status();
827
+ return !status.isClean();
828
+ }
829
+ async function stash(message) {
830
+ await git.stash(["push", "-u", "-m", message]);
831
+ }
832
+ async function stashPop() {
833
+ await git.stash(["pop"]);
834
+ }
651
835
 
652
836
  // src/lib/client.ts
653
837
  init_proxy();
@@ -665,330 +849,146 @@ function getModel(config) {
665
849
  return config.aiModel ?? DEFAULT_MODEL;
666
850
  }
667
851
 
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() };
852
+ // src/tools/pick/index.ts
853
+ var pick_exports = {};
854
+ __export(pick_exports, {
855
+ definition: () => definition3,
856
+ execute: () => execute3,
857
+ run: () => run3,
858
+ terminal: () => terminal3
859
+ });
860
+ init_github();
861
+ import chalk5 from "chalk";
862
+ import ora3 from "ora";
863
+ import { select as select3 } from "@inquirer/prompts";
864
+ init_github();
865
+
866
+ // src/lib/markdown.ts
867
+ import { marked } from "marked";
868
+ import { markedTerminal } from "marked-terminal";
869
+ marked.use(markedTerminal({ showSectionPrefix: false }));
870
+ function renderMarkdown(text) {
871
+ return marked(text);
677
872
  }
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;
873
+
874
+ // src/lib/display.ts
875
+ init_github();
876
+ import chalk2 from "chalk";
877
+ var LABEL_AVAILABLE2 = "techunter:available";
878
+ var LABEL_CLAIMED2 = "techunter:claimed";
879
+ var LABEL_IN_REVIEW2 = "techunter:in-review";
880
+ var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
881
+ function getStatus(issue) {
882
+ if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
883
+ if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
884
+ if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
885
+ if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
886
+ return "unknown";
887
+ }
888
+ function colorStatus(status) {
889
+ const padded = status.padEnd(14);
890
+ switch (status) {
891
+ case "available":
892
+ return chalk2.green(padded);
893
+ case "claimed":
894
+ return chalk2.yellow(padded);
895
+ case "in-review":
896
+ return chalk2.blue(padded);
897
+ case "changes-needed":
898
+ return chalk2.red(padded);
899
+ default:
900
+ return padded;
901
+ }
902
+ }
903
+ function parentIssueFromBranch(branch) {
904
+ if (!isTaskBranch(branch)) return null;
905
+ const match = branch.match(/^task-(\d+)-/);
906
+ return match ? parseInt(match[1], 10) : null;
907
+ }
908
+ function getParentIssueNumber(issue) {
909
+ const target = extractTargetBranch(issue.body);
910
+ if (!target) return null;
911
+ return parentIssueFromBranch(target);
912
+ }
913
+ function printTaskDetail(issue) {
914
+ const divider = chalk2.dim("\u2500".repeat(70));
915
+ const parentNum = getParentIssueNumber(issue);
916
+ console.log("\n" + divider);
917
+ console.log(
918
+ chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014") + (parentNum ? chalk2.dim(` sub-task of #${parentNum}`) : "")
919
+ );
920
+ console.log(chalk2.bold("\n " + issue.title));
921
+ if (issue.body) {
922
+ console.log("");
923
+ console.log(renderMarkdown(issue.body));
924
+ }
925
+ console.log("\n " + chalk2.dim(issue.htmlUrl));
926
+ console.log(divider + "\n");
927
+ }
928
+ async function printTaskList(config) {
702
929
  try {
703
- const result = await auth({ type: "oauth" });
704
- token = result.token;
705
- spinner.succeed("Authorized!");
930
+ const tasks = await listTasks(config);
931
+ const divider = chalk2.dim("\u2500".repeat(70));
932
+ console.log("");
933
+ console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
934
+ console.log(divider);
935
+ if (tasks.length === 0) {
936
+ console.log(chalk2.dim(" (no tasks)"));
937
+ } else {
938
+ let printTask2 = function(t, indent, connector, isLast) {
939
+ const num = `#${t.number}`.padEnd(5);
940
+ const status = colorStatus(getStatus(t));
941
+ const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
942
+ const fullPrefix = indent + connector;
943
+ const maxTitle = 36 - fullPrefix.length;
944
+ const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
945
+ console.log(` ${num}${status}${assignee}${chalk2.dim(fullPrefix)}${title}`);
946
+ const children = childrenOf.get(t.number) ?? [];
947
+ const childIndent = indent + (isLast ? " " : "\u2502 ");
948
+ for (let i = 0; i < children.length; i++) {
949
+ const childIsLast = i === children.length - 1;
950
+ printTask2(children[i], childIndent, childIsLast ? "\u2514\u2500 " : "\u251C\u2500 ", childIsLast);
951
+ }
952
+ };
953
+ var printTask = printTask2;
954
+ const taskMap = new Map(tasks.map((t) => [t.number, t]));
955
+ const childrenOf = /* @__PURE__ */ new Map();
956
+ for (const t of tasks) {
957
+ const parentNum = getParentIssueNumber(t);
958
+ const key = parentNum !== null && taskMap.has(parentNum) ? parentNum : null;
959
+ if (!childrenOf.has(key)) childrenOf.set(key, []);
960
+ childrenOf.get(key).push(t);
961
+ }
962
+ const roots = childrenOf.get(null) ?? [];
963
+ for (let i = 0; i < roots.length; i++) {
964
+ const isLast = i === roots.length - 1;
965
+ printTask2(roots[i], "", isLast ? "\u2514\u2500 " : "\u251C\u2500 ", isLast);
966
+ }
967
+ }
968
+ console.log(divider);
969
+ return tasks;
706
970
  } catch (err) {
707
- spinner.fail("Authorization failed");
708
- throw err;
971
+ console.log(chalk2.yellow(`(Could not load tasks: ${err.message})`));
972
+ return [];
709
973
  }
710
- return { token, clientId: OAUTH_CLIENT_ID };
711
974
  }
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
- `));
724
- }
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}`);
975
+ async function printMyTasks(config) {
976
+ try {
977
+ const me = await getAuthenticatedUser(config);
978
+ const tasks = await listMyTasks(config, me);
979
+ if (tasks.length === 0) return;
980
+ const divider = chalk2.dim("\u2500".repeat(70));
981
+ console.log("");
982
+ console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
983
+ console.log(divider);
984
+ for (const t of tasks) {
985
+ const num = `#${t.number}`.padEnd(5);
986
+ const status = colorStatus(getStatus(t));
987
+ const parentNum = getParentIssueNumber(t);
988
+ const parentTag = parentNum ? chalk2.dim(` (sub of #${parentNum})`) : "";
989
+ const maxTitle = parentNum ? 34 : 46;
990
+ const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
991
+ console.log(` ${num}${status}${title}${parentTag}`);
992
992
  }
993
993
  console.log(divider);
994
994
  const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
@@ -1000,14 +1000,14 @@ async function printMyTasks(config) {
1000
1000
  }
1001
1001
  console.log("");
1002
1002
  for (const t of rejectedTasks) {
1003
- const expectedBranch = makeBranchName(t.number, t.title);
1004
- const onCorrectBranch = currentBranch === expectedBranch;
1003
+ const taskBranch = t.assignee ? makeTaskBranchName(t.number, t.assignee) : `task-${t.number}`;
1004
+ const onCorrectBranch = currentBranch === taskBranch;
1005
1005
  console.log(
1006
- chalk4.red.bold(" \u26A0 Changes requested") + chalk4.red(` on #${t.number} "${t.title}"`)
1006
+ chalk2.red.bold(" \u26A0 Changes requested") + chalk2.red(` on #${t.number} "${t.title}"`)
1007
1007
  );
1008
1008
  if (!onCorrectBranch) {
1009
1009
  console.log(
1010
- chalk4.dim(" Switch branch: ") + chalk4.cyan(`git checkout ${expectedBranch}`)
1010
+ chalk2.dim(" Switch branch: ") + chalk2.cyan(`git checkout ${taskBranch}`)
1011
1011
  );
1012
1012
  }
1013
1013
  }
@@ -1019,7 +1019,7 @@ async function printMyTasks(config) {
1019
1019
 
1020
1020
  // src/lib/launch.ts
1021
1021
  import { spawn } from "child_process";
1022
- import chalk5 from "chalk";
1022
+ import chalk3 from "chalk";
1023
1023
  function buildClaudePrompt(issue, branch) {
1024
1024
  const lines = [
1025
1025
  `You are working on task #${issue.number}: ${issue.title}`,
@@ -1035,14 +1035,14 @@ function buildClaudePrompt(issue, branch) {
1035
1035
  }
1036
1036
  async function launchClaudeCode(issue, branch) {
1037
1037
  const prompt = buildClaudePrompt(issue, branch);
1038
- console.log(chalk5.dim("\n Launching Claude Code\u2026\n"));
1038
+ console.log(chalk3.dim("\n Launching Claude Code\u2026\n"));
1039
1039
  await new Promise((resolve) => {
1040
1040
  const safePrompt = prompt.replace(/\r?\n/g, " ").replace(/"/g, "'");
1041
1041
  const child = spawn(`claude "${safePrompt}"`, [], { stdio: "inherit", shell: true });
1042
1042
  child.on("close", () => resolve());
1043
1043
  child.on("error", () => {
1044
1044
  console.log(
1045
- chalk5.yellow(
1045
+ chalk3.yellow(
1046
1046
  " Could not launch claude. Make sure Claude Code is installed:\n npm install -g @anthropic-ai/claude-code"
1047
1047
  )
1048
1048
  );
@@ -1060,81 +1060,12 @@ __export(submit_exports, {
1060
1060
  terminal: () => terminal
1061
1061
  });
1062
1062
  init_github();
1063
- import chalk7 from "chalk";
1064
- import ora2 from "ora";
1065
- import { select as select3, input as promptInput } from "@inquirer/prompts";
1063
+ import chalk4 from "chalk";
1064
+ import ora from "ora";
1065
+ import { select, input as promptInput } from "@inquirer/prompts";
1066
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
- }
1135
-
1136
- // src/tools/submit/prompts.ts
1137
- 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.";
1067
+ // src/tools/submit/prompts.ts
1068
+ 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.";
1138
1069
 
1139
1070
  // src/tools/submit/reviewer.ts
1140
1071
  async function reviewChanges(config, issueNumber, issue, diff) {
@@ -1169,11 +1100,21 @@ var definition = {
1169
1100
  };
1170
1101
  async function run(_input, config) {
1171
1102
  const taskState = getConfig().taskState;
1172
- const issueNumber = taskState?.activeIssueNumber;
1103
+ const currentBranch = await getCurrentBranch();
1104
+ let issueNumber = taskState?.activeIssueNumber && taskState?.activeBranch && currentBranch === taskState.activeBranch ? taskState.activeIssueNumber : void 0;
1173
1105
  if (!issueNumber) {
1174
- return "No active task found. Claim a task first with /pick.";
1106
+ const fromBranch = parseIssueNumberFromBranch(currentBranch);
1107
+ if (fromBranch) {
1108
+ issueNumber = fromBranch;
1109
+ } else {
1110
+ const found = await getIssueNumberFromBranch(config, currentBranch);
1111
+ if (!found) {
1112
+ return "No active task found. Claim a task first with /pick.";
1113
+ }
1114
+ issueNumber = found.issueNumber;
1115
+ }
1175
1116
  }
1176
- let spinner = ora2("Loading task and diff\u2026").start();
1117
+ let spinner = ora("Loading task and diff\u2026").start();
1177
1118
  const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1178
1119
  const [issue, diff, me] = await Promise.all([
1179
1120
  getTask(config, issueNumber),
@@ -1181,12 +1122,19 @@ async function run(_input, config) {
1181
1122
  getAuthenticatedUser(config)
1182
1123
  ]);
1183
1124
  spinner.stop();
1184
- const workerBranch = makeWorkerBranchName(issue.author ?? me);
1125
+ const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
1185
1126
  const branch = await getCurrentBranch();
1186
1127
  const isSelfSubmit = issue.author !== null && issue.author === me;
1128
+ spinner = ora("Checking for open sub-tasks\u2026").start();
1129
+ const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1130
+ spinner.stop();
1131
+ if (openSubtaskNumbers.length > 0) {
1132
+ return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open:
1133
+ ` + openSubtaskNumbers.map((n) => ` - #${n}`).join("\n") + "\nComplete all sub-tasks before submitting.";
1134
+ }
1187
1135
  let review = "";
1188
1136
  if (!isSelfSubmit) {
1189
- const reviewSpinner = ora2("Reviewing changes\u2026").start();
1137
+ const reviewSpinner = ora("Reviewing changes\u2026").start();
1190
1138
  try {
1191
1139
  review = await reviewChanges(config, issueNumber, issue, diff);
1192
1140
  } catch (err) {
@@ -1194,19 +1142,19 @@ async function run(_input, config) {
1194
1142
  }
1195
1143
  reviewSpinner.stop();
1196
1144
  }
1197
- const divider = chalk7.dim("\u2500".repeat(70));
1145
+ const divider = chalk4.dim("\u2500".repeat(70));
1198
1146
  console.log("\n" + divider);
1199
1147
  if (isSelfSubmit) {
1200
- console.log(chalk7.yellow(` Self-submit detected \u2014 AI review skipped.`));
1148
+ console.log(chalk4.yellow(` Self-submit detected \u2014 AI review skipped.`));
1201
1149
  } else {
1202
- console.log(chalk7.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1150
+ console.log(chalk4.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1203
1151
  console.log(divider);
1204
1152
  console.log(renderMarkdown(review));
1205
1153
  }
1206
1154
  console.log(divider + "\n");
1207
1155
  let shouldProceed;
1208
1156
  try {
1209
- shouldProceed = await select3({
1157
+ shouldProceed = await select({
1210
1158
  message: `Submit task #${issueNumber}?`,
1211
1159
  choices: [
1212
1160
  { name: "Yes, submit", value: true },
@@ -1227,7 +1175,7 @@ async function run(_input, config) {
1227
1175
  return "Submit cancelled.";
1228
1176
  }
1229
1177
  if (!commitMessage.trim()) return "Submit cancelled.";
1230
- spinner = ora2("Committing and pushing\u2026").start();
1178
+ spinner = ora("Committing and pushing\u2026").start();
1231
1179
  try {
1232
1180
  await stageAllAndCommit(commitMessage.trim());
1233
1181
  spinner.stop();
@@ -1236,52 +1184,71 @@ async function run(_input, config) {
1236
1184
  return `Commit failed: ${err.message}`;
1237
1185
  }
1238
1186
  if (isSelfSubmit) {
1239
- spinner = ora2("Closing issue\u2026").start();
1187
+ spinner = ora("Closing issue\u2026").start();
1240
1188
  try {
1241
1189
  await closeTask(config, issueNumber);
1242
1190
  spinner.stop();
1243
1191
  } catch (err) {
1244
1192
  spinner.stop();
1245
- console.error(chalk7.yellow(`Warning: failed to close issue: ${err.message}`));
1193
+ console.error(chalk4.yellow(`Warning: failed to close issue: ${err.message}`));
1246
1194
  }
1247
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1195
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1248
1196
  return `Task #${issueNumber} committed and closed.
1249
1197
  Commit: "${commitMessage.trim()}"`;
1250
1198
  }
1251
- spinner = ora2("Creating pull request\u2026").start();
1199
+ spinner = ora("Checking for existing PR\u2026").start();
1200
+ const existingPR = await getTaskPR(config, issueNumber);
1201
+ spinner.stop();
1252
1202
  let prUrl;
1253
- try {
1254
- const prBody = [
1255
- `Closes #${issueNumber}`,
1256
- issue.body ? `
1203
+ if (existingPR) {
1204
+ prUrl = existingPR.url;
1205
+ console.log(chalk4.dim(` Existing PR found: ${prUrl} \u2014 updating.`));
1206
+ } else {
1207
+ spinner = ora("Creating pull request\u2026").start();
1208
+ try {
1209
+ await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1210
+ const prBody = [
1211
+ `Closes #${issueNumber}`,
1212
+ issue.body ? `
1257
1213
  ${issue.body}` : "",
1258
- review ? `
1214
+ review ? `
1259
1215
  ## AI Review
1260
1216
  ${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}`;
1217
+ ].join("\n").trim();
1218
+ prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1219
+ spinner.stop();
1220
+ } catch (err) {
1221
+ spinner.stop();
1222
+ return `Committed but PR creation failed: ${err.message}`;
1223
+ }
1267
1224
  }
1268
- spinner = ora2("Marking as in-review\u2026").start();
1225
+ spinner = ora("Marking as in-review\u2026").start();
1269
1226
  try {
1270
1227
  await markInReview(config, issueNumber);
1271
1228
  spinner.stop();
1272
1229
  } catch (err) {
1273
1230
  spinner.stop();
1274
- return `PR created (${prUrl}) but failed to update label: ${err.message}`;
1231
+ return `PR ${existingPR ? "updated" : "created"} (${prUrl}) but failed to update label: ${err.message}`;
1275
1232
  }
1276
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1277
- return `Task #${issueNumber} submitted.
1233
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1234
+ return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
1278
1235
  Commit: "${commitMessage.trim()}"
1279
1236
  PR: ${prUrl}`;
1280
1237
  }
1281
1238
  async function execute(input3, config) {
1282
1239
  const taskState = getConfig().taskState;
1283
- const issueNumber = taskState?.activeIssueNumber;
1284
- if (!issueNumber) return "No active task found. Claim a task first.";
1240
+ let issueNumber = taskState?.activeIssueNumber;
1241
+ if (!issueNumber) {
1242
+ const currentBranch = await getCurrentBranch();
1243
+ const fromBranch = parseIssueNumberFromBranch(currentBranch);
1244
+ if (fromBranch) {
1245
+ issueNumber = fromBranch;
1246
+ } else {
1247
+ const found = await getIssueNumberFromBranch(config, currentBranch);
1248
+ if (!found) return "No active task found. Claim a task first.";
1249
+ issueNumber = found.issueNumber;
1250
+ }
1251
+ }
1285
1252
  const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1286
1253
  const [issue, diff, branch, me] = await Promise.all([
1287
1254
  getTask(config, issueNumber),
@@ -1289,7 +1256,11 @@ async function execute(input3, config) {
1289
1256
  getCurrentBranch(),
1290
1257
  getAuthenticatedUser(config)
1291
1258
  ]);
1292
- const workerBranch = makeWorkerBranchName(issue.author ?? me);
1259
+ const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
1260
+ const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1261
+ if (openSubtaskNumbers.length > 0) {
1262
+ return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open: ` + openSubtaskNumbers.map((n) => `#${n}`).join(", ");
1263
+ }
1293
1264
  const isSelfSubmit = issue.author !== null && issue.author === me;
1294
1265
  let review = "";
1295
1266
  if (!isSelfSubmit) {
@@ -1310,29 +1281,36 @@ async function execute(input3, config) {
1310
1281
  await closeTask(config, issueNumber);
1311
1282
  } catch {
1312
1283
  }
1313
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1284
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1314
1285
  return `Task #${issueNumber} committed and closed.
1315
1286
  Commit: "${commitMessage}"`;
1316
1287
  }
1288
+ const existingPR = await getTaskPR(config, issueNumber);
1317
1289
  let prUrl;
1318
- try {
1319
- const prBody = [
1320
- `Closes #${issueNumber}`,
1321
- issue.body ? `
1290
+ if (existingPR) {
1291
+ prUrl = existingPR.url;
1292
+ } else {
1293
+ try {
1294
+ await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1295
+ const prBody = [
1296
+ `Closes #${issueNumber}`,
1297
+ issue.body ? `
1322
1298
  ${issue.body}` : "",
1323
- review ? `
1299
+ review ? `
1324
1300
  ## AI Review
1325
1301
  ${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}`;
1302
+ ].join("\n").trim();
1303
+ prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1304
+ } catch (err) {
1305
+ return `Committed but PR creation failed: ${err.message}`;
1306
+ }
1330
1307
  }
1331
1308
  try {
1332
1309
  await markInReview(config, issueNumber);
1333
1310
  } catch {
1334
1311
  }
1335
- return `Task #${issueNumber} submitted.
1312
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1313
+ return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
1336
1314
  Review:
1337
1315
  ${review}
1338
1316
  Commit: "${commitMessage}"
@@ -1349,8 +1327,8 @@ __export(close_exports, {
1349
1327
  terminal: () => terminal2
1350
1328
  });
1351
1329
  init_github();
1352
- import { select as select4 } from "@inquirer/prompts";
1353
- import ora3 from "ora";
1330
+ import { select as select2 } from "@inquirer/prompts";
1331
+ import ora2 from "ora";
1354
1332
  var definition2 = {
1355
1333
  type: "function",
1356
1334
  function: {
@@ -1376,7 +1354,7 @@ async function run2(input3, config) {
1376
1354
  }
1377
1355
  if (tasks.length === 0) return "No tasks found.";
1378
1356
  try {
1379
- issueNumber = await select4({
1357
+ issueNumber = await select2({
1380
1358
  message: "Select task to close:",
1381
1359
  choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
1382
1360
  });
@@ -1386,7 +1364,7 @@ async function run2(input3, config) {
1386
1364
  }
1387
1365
  let confirmed;
1388
1366
  try {
1389
- confirmed = await select4({
1367
+ confirmed = await select2({
1390
1368
  message: `Close task #${issueNumber}?`,
1391
1369
  choices: [
1392
1370
  { name: "Yes, close it", value: true },
@@ -1397,7 +1375,7 @@ async function run2(input3, config) {
1397
1375
  return "Cancelled.";
1398
1376
  }
1399
1377
  if (!confirmed) return "Cancelled.";
1400
- const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
1378
+ const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
1401
1379
  try {
1402
1380
  await closeTask(config, issueNumber);
1403
1381
  spinner.stop();
@@ -1409,7 +1387,7 @@ async function run2(input3, config) {
1409
1387
  }
1410
1388
  async function execute2(input3, config) {
1411
1389
  const issueNumber = input3["issue_number"];
1412
- const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
1390
+ const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
1413
1391
  try {
1414
1392
  await closeTask(config, issueNumber);
1415
1393
  spinner.stop();
@@ -1455,7 +1433,7 @@ async function run3(input3, config) {
1455
1433
  }
1456
1434
  if (tasks.length === 0) return "No tasks found.";
1457
1435
  try {
1458
- chosenNumber = await select5({
1436
+ chosenNumber = await select3({
1459
1437
  message: "Select a task:",
1460
1438
  choices: tasks.map((t) => ({
1461
1439
  name: `#${String(t.number).padEnd(4)} ${colorStatus(getStatus(t))} ${t.title}`,
@@ -1479,9 +1457,9 @@ async function run3(input3, config) {
1479
1457
  const comments = await listComments(config, issue.number, 1);
1480
1458
  if (comments.length > 0) {
1481
1459
  const c = comments[0];
1482
- const divider = chalk8.dim("\u2500".repeat(70));
1460
+ const divider = chalk5.dim("\u2500".repeat(70));
1483
1461
  console.log(
1484
- chalk8.red.bold(" Latest rejection feedback") + chalk8.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
1462
+ chalk5.red.bold(" Latest rejection feedback") + chalk5.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
1485
1463
  );
1486
1464
  console.log(divider);
1487
1465
  console.log(renderMarkdown(c.body));
@@ -1491,22 +1469,36 @@ async function run3(input3, config) {
1491
1469
  }
1492
1470
  }
1493
1471
  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" });
1472
+ if (status === "available") {
1473
+ actions.push({ name: "Claim this task", value: "claim" });
1474
+ }
1475
+ if (status === "claimed") {
1476
+ actions.push({ name: "Submit this task", value: "submit" });
1477
+ }
1478
+ if (status === "changes-needed") {
1479
+ const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1480
+ const me = await getAuthenticatedUser2(config);
1481
+ const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1482
+ const currentBranch = await getCurrentBranch();
1483
+ if (currentBranch === taskBranch) {
1484
+ actions.push({ name: "Submit this task (fixes done)", value: "submit" });
1485
+ } else {
1486
+ actions.push({ name: `Switch to ${taskBranch} to fix`, value: "switch-fix" });
1487
+ }
1488
+ }
1496
1489
  actions.push({ name: "Close this task", value: "close" });
1497
1490
  actions.push({ name: "Nothing, just viewing", value: "none" });
1498
1491
  let action;
1499
1492
  try {
1500
- action = await select5({ message: "Action:", choices: actions });
1493
+ action = await select3({ message: "Action:", choices: actions });
1501
1494
  } catch {
1502
1495
  return "Cancelled.";
1503
1496
  }
1504
1497
  if (action === "none") return `Viewed task #${issue.number}.`;
1505
1498
  if (action === "claim") {
1506
1499
  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);
1500
+ const me = await getAuthenticatedUser(config);
1501
+ const myTasks = await listMyTasks(config, me);
1510
1502
  const activeTask = myTasks.find((t) => {
1511
1503
  const labels = t.labels;
1512
1504
  return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
@@ -1515,37 +1507,65 @@ async function run3(input3, config) {
1515
1507
  return `You already have an active task: #${activeTask.number} "${activeTask.title}"
1516
1508
  Finish or submit it before claiming a new one.`;
1517
1509
  }
1518
- let spinner = ora4(`Claiming #${issue.number}\u2026`).start();
1510
+ let stashed = false;
1511
+ if (await hasUncommittedChanges()) {
1512
+ let choice;
1513
+ try {
1514
+ choice = await select3({
1515
+ message: "You have uncommitted changes. What would you like to do?",
1516
+ choices: [
1517
+ { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1518
+ { name: "Cancel", value: "cancel" }
1519
+ ]
1520
+ });
1521
+ } catch {
1522
+ choice = "cancel";
1523
+ }
1524
+ if (choice === "cancel") return "Cancelled.";
1525
+ await stash(`tch: before claiming #${issue.number}`);
1526
+ stashed = true;
1527
+ console.log(chalk5.dim(" Changes stashed. Run `git stash pop` after you finish this task to restore them."));
1528
+ }
1529
+ let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
1519
1530
  await claimTask(config, issue.number, me);
1520
1531
  spinner.stop();
1521
- const workerBranch = makeWorkerBranchName(me);
1532
+ const taskBranch = makeTaskBranchName(issue.number, me);
1522
1533
  const taskBase = extractBaseCommit(issue.body);
1523
- spinner = ora4(`Switching to ${workerBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
1534
+ spinner = ora3(`Creating branch ${taskBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
1524
1535
  try {
1525
1536
  if (taskBase) {
1526
- await resetOrCreateBranch(workerBranch, taskBase);
1537
+ await checkoutFromCommit(taskBranch, taskBase);
1527
1538
  } else {
1528
- await switchToBranchOrCreate(workerBranch);
1539
+ await switchToBranchOrCreate(taskBranch);
1529
1540
  }
1530
1541
  spinner.stop();
1531
- spinner = ora4("Pushing worker branch\u2026").start();
1542
+ spinner = ora3("Pushing task branch\u2026").start();
1532
1543
  try {
1533
- await pushBranch(workerBranch);
1544
+ await pushBranch(taskBranch);
1534
1545
  spinner.stop();
1535
1546
  } catch {
1536
- spinner.warn("Could not push worker branch");
1547
+ spinner.warn("Could not push task branch \u2014 will push on submit");
1537
1548
  }
1538
- } catch {
1539
- spinner.warn(`Could not switch to ${workerBranch}`);
1549
+ } catch (err) {
1550
+ spinner.warn(`Could not switch to ${taskBranch}`);
1551
+ if (stashed) {
1552
+ try {
1553
+ await stashPop();
1554
+ console.log(chalk5.dim(" Restored stashed changes."));
1555
+ } catch {
1556
+ console.log(chalk5.yellow(" Warning: could not restore stash automatically. Run `git stash pop` manually."));
1557
+ }
1558
+ }
1559
+ throw err;
1540
1560
  }
1541
1561
  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)})
1562
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1563
+ console.log(chalk5.green(`
1564
+ Claimed! Branch: ${taskBranch} (base: ${baseCommit.slice(0, 7)})
1545
1565
  `));
1546
1566
  let openClaude;
1547
1567
  try {
1548
- openClaude = await select5({
1568
+ openClaude = await select3({
1549
1569
  message: "Open Claude Code for this task?",
1550
1570
  choices: [
1551
1571
  { name: "Yes, start coding now", value: true },
@@ -1555,12 +1575,58 @@ Finish or submit it before claiming a new one.`;
1555
1575
  } catch {
1556
1576
  openClaude = false;
1557
1577
  }
1558
- if (openClaude) await launchClaudeCode(issue, workerBranch);
1559
- return `Task #${issue.number} claimed. Branch: ${workerBranch}`;
1578
+ if (openClaude) await launchClaudeCode(issue, taskBranch);
1579
+ return `Task #${issue.number} claimed. Branch: ${taskBranch}`;
1560
1580
  } catch (err) {
1561
1581
  return `Error claiming task: ${err.message}`;
1562
1582
  }
1563
1583
  }
1584
+ if (action === "switch-fix") {
1585
+ const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1586
+ const me = await getAuthenticatedUser2(config);
1587
+ const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1588
+ let stashed = false;
1589
+ if (await hasUncommittedChanges()) {
1590
+ let choice;
1591
+ try {
1592
+ choice = await select3({
1593
+ message: "You have uncommitted changes. What would you like to do?",
1594
+ choices: [
1595
+ { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1596
+ { name: "Cancel", value: "cancel" }
1597
+ ]
1598
+ });
1599
+ } catch {
1600
+ choice = "cancel";
1601
+ }
1602
+ if (choice === "cancel") return "Cancelled.";
1603
+ await stash(`tch: before switching to ${taskBranch}`);
1604
+ stashed = true;
1605
+ console.log(chalk5.dim(" Changes stashed. Run `git stash pop` to restore them later."));
1606
+ }
1607
+ const spinner = ora3(`Switching to ${taskBranch}\u2026`).start();
1608
+ try {
1609
+ await switchToBranchOrCreate(taskBranch);
1610
+ spinner.stop();
1611
+ } catch (err) {
1612
+ spinner.warn(`Could not switch to ${taskBranch}: ${err.message}`);
1613
+ if (stashed) {
1614
+ try {
1615
+ await stashPop();
1616
+ console.log(chalk5.dim(" Restored stashed changes."));
1617
+ } catch {
1618
+ console.log(chalk5.yellow(" Run `git stash pop` manually to restore your changes."));
1619
+ }
1620
+ }
1621
+ return `Error: ${err.message}`;
1622
+ }
1623
+ const baseCommit = extractBaseCommit(issue.body) ?? await getCurrentCommit();
1624
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1625
+ console.log(chalk5.green(`
1626
+ Switched to ${taskBranch}. Fix the issues then run /submit.
1627
+ `));
1628
+ return `Switched to ${taskBranch} for task #${issue.number}.`;
1629
+ }
1564
1630
  if (action === "submit") return run({}, config);
1565
1631
  if (action === "close") return run2({ issue_number: issue.number }, config);
1566
1632
  return "Cancelled.";
@@ -1589,28 +1655,31 @@ async function execute3(input3, config) {
1589
1655
  if (activeTask) {
1590
1656
  return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
1591
1657
  }
1658
+ if (await hasUncommittedChanges()) {
1659
+ return "Cannot claim: you have uncommitted changes. Commit or stash them first (git stash).";
1660
+ }
1592
1661
  try {
1593
1662
  await claimTask(config, issueNumber, me);
1594
1663
  } catch (err) {
1595
1664
  return `Error claiming task: ${err.message}`;
1596
1665
  }
1597
- const workerBranch = makeWorkerBranchName(me);
1666
+ const taskBranch = makeTaskBranchName(issue.number, me);
1598
1667
  const taskBase = extractBaseCommit(issue.body);
1599
1668
  try {
1600
1669
  if (taskBase) {
1601
- await resetOrCreateBranch(workerBranch, taskBase);
1670
+ await checkoutFromCommit(taskBranch, taskBase);
1602
1671
  } else {
1603
- await switchToBranchOrCreate(workerBranch);
1672
+ await switchToBranchOrCreate(taskBranch);
1604
1673
  }
1605
1674
  } catch {
1606
1675
  }
1607
1676
  try {
1608
- await pushBranch(workerBranch);
1677
+ await pushBranch(taskBranch);
1609
1678
  } catch {
1610
1679
  }
1611
1680
  const baseCommit = await getCurrentCommit();
1612
- setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
1613
- return `Task #${issueNumber} claimed. Branch: ${workerBranch} (base commit: ${baseCommit.slice(0, 7)})`;
1681
+ setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit, activeBranch: taskBranch } });
1682
+ return `Task #${issueNumber} claimed. Branch: ${taskBranch} (base commit: ${baseCommit.slice(0, 7)})`;
1614
1683
  }
1615
1684
  return `Unknown action: ${action}`;
1616
1685
  }
@@ -1625,14 +1694,14 @@ __export(new_task_exports, {
1625
1694
  terminal: () => terminal4
1626
1695
  });
1627
1696
  init_github();
1628
- import { select as select6, input as promptInput2 } from "@inquirer/prompts";
1697
+ import { select as select4, input as promptInput2 } from "@inquirer/prompts";
1629
1698
  import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
1630
1699
  import { spawn as spawn2 } from "child_process";
1631
1700
  import { tmpdir } from "os";
1632
1701
  import path from "path";
1633
- import ora5 from "ora";
1634
- import chalk9 from "chalk";
1635
- import open2 from "open";
1702
+ import ora4 from "ora";
1703
+ import chalk6 from "chalk";
1704
+ import open from "open";
1636
1705
 
1637
1706
  // src/tools/new-task/prompts.ts
1638
1707
  var GUIDE_FORMAT = `
@@ -1685,6 +1754,77 @@ async function openInEditor(content) {
1685
1754
  await rm(dir, { recursive: true, force: true });
1686
1755
  }
1687
1756
  }
1757
+ async function resolveBaseAndTarget(config, me, interactive) {
1758
+ const currentBranch = await getCurrentBranch();
1759
+ if (isTaskBranch(currentBranch)) {
1760
+ if (await hasUncommittedChanges()) {
1761
+ if (!interactive) {
1762
+ throw new Error("Cannot create sub-task: you have uncommitted changes. Commit them first so the executor starts from the correct base.");
1763
+ }
1764
+ const { select: inquirerSelect } = await import("@inquirer/prompts");
1765
+ let choice;
1766
+ try {
1767
+ choice = await inquirerSelect({
1768
+ message: "You have uncommitted changes. The sub-task executor will start from the last commit \u2014 they won't see your current unsaved work.",
1769
+ choices: [
1770
+ { name: "Commit first (cancel and commit manually)", value: "cancel" },
1771
+ { name: "Continue anyway (executor starts without my unsaved changes)", value: "continue" }
1772
+ ]
1773
+ });
1774
+ } catch {
1775
+ choice = "cancel";
1776
+ }
1777
+ if (choice === "cancel") throw new Error("Cancelled. Commit your changes first, then create the sub-task.");
1778
+ }
1779
+ const baseCommit2 = await getCurrentCommit();
1780
+ return { baseCommit: baseCommit2, targetBranch: currentBranch, isSubtask: true };
1781
+ }
1782
+ let stashedForSync = false;
1783
+ if (await hasUncommittedChanges()) {
1784
+ if (!interactive) {
1785
+ throw new Error("Cannot create task: you have uncommitted changes. Commit or stash them first (git stash).");
1786
+ }
1787
+ const { select: inquirerSelect } = await import("@inquirer/prompts");
1788
+ let choice;
1789
+ try {
1790
+ choice = await inquirerSelect({
1791
+ message: "You have uncommitted changes. Syncing with main requires a clean working tree.",
1792
+ choices: [
1793
+ { name: "Stash changes and continue (restore with: git stash pop)", value: "stash" },
1794
+ { name: "Cancel", value: "cancel" }
1795
+ ]
1796
+ });
1797
+ } catch {
1798
+ choice = "cancel";
1799
+ }
1800
+ if (choice === "cancel") throw new Error("Cancelled.");
1801
+ await stash("tch: before creating new task");
1802
+ stashedForSync = true;
1803
+ console.log(chalk6.dim(" Changes stashed. Run `git stash pop` after creating the task."));
1804
+ }
1805
+ const baseBranch = config.baseBranch ?? "main";
1806
+ let baseCommit;
1807
+ const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
1808
+ try {
1809
+ await syncWithBase(baseBranch);
1810
+ baseCommit = await getCurrentCommit();
1811
+ syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
1812
+ } catch {
1813
+ syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
1814
+ try {
1815
+ baseCommit = await getRemoteHeadSha(baseBranch);
1816
+ } catch {
1817
+ }
1818
+ if (stashedForSync) {
1819
+ try {
1820
+ await stashPop();
1821
+ } catch {
1822
+ }
1823
+ throw new Error(`Could not sync with ${baseBranch}. Your changes have been restored from stash.`);
1824
+ }
1825
+ }
1826
+ return { baseCommit, targetBranch: makeWorkerBranchName(me), isSubtask: false };
1827
+ }
1688
1828
  var definition4 = {
1689
1829
  type: "function",
1690
1830
  function: {
@@ -1701,7 +1841,7 @@ var definition4 = {
1701
1841
  }
1702
1842
  };
1703
1843
  async function run4(input3, config) {
1704
- const authSpinner = ora5("Checking permissions\u2026").start();
1844
+ const authSpinner = ora4("Checking permissions\u2026").start();
1705
1845
  let me;
1706
1846
  let allowed;
1707
1847
  try {
@@ -1724,7 +1864,7 @@ async function run4(input3, config) {
1724
1864
  }
1725
1865
  if (!title) return "Cancelled.";
1726
1866
  }
1727
- const spinner = ora5("Scanning project and generating guide\u2026").start();
1867
+ const spinner = ora4("Scanning project and generating guide\u2026").start();
1728
1868
  let guide;
1729
1869
  try {
1730
1870
  guide = await generateGuide(config, title);
@@ -1733,16 +1873,16 @@ async function run4(input3, config) {
1733
1873
  spinner.stop();
1734
1874
  return `Error generating guide: ${err.message}`;
1735
1875
  }
1736
- const divider = chalk9.dim("\u2500".repeat(70));
1876
+ const divider = chalk6.dim("\u2500".repeat(70));
1737
1877
  for (; ; ) {
1738
1878
  console.log("\n" + divider);
1739
- console.log(chalk9.bold(" Generated guide preview"));
1879
+ console.log(chalk6.bold(" Generated guide preview"));
1740
1880
  console.log(divider);
1741
1881
  console.log(renderMarkdown(guide));
1742
1882
  console.log(divider + "\n");
1743
1883
  let action;
1744
1884
  try {
1745
- action = await select6({
1885
+ action = await select4({
1746
1886
  message: "Create this task?",
1747
1887
  choices: [
1748
1888
  { name: "Yes, create task", value: "create" },
@@ -1760,7 +1900,7 @@ async function run4(input3, config) {
1760
1900
  try {
1761
1901
  guide = await openInEditor(guide);
1762
1902
  } catch (err) {
1763
- console.log(chalk9.yellow(` Editor error: ${err.message}`));
1903
+ console.log(chalk6.yellow(` Editor error: ${err.message}`));
1764
1904
  }
1765
1905
  continue;
1766
1906
  }
@@ -1771,35 +1911,32 @@ async function run4(input3, config) {
1771
1911
  return "Cancelled.";
1772
1912
  }
1773
1913
  if (!feedback) continue;
1774
- const reviseSpinner = ora5("Revising guide\u2026").start();
1914
+ const reviseSpinner = ora4("Revising guide\u2026").start();
1775
1915
  try {
1776
1916
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
1777
1917
  reviseSpinner.stop();
1778
1918
  } catch (err) {
1779
1919
  reviseSpinner.stop();
1780
- console.log(chalk9.yellow(` Revision error: ${err.message}`));
1920
+ console.log(chalk6.yellow(` Revision error: ${err.message}`));
1781
1921
  }
1782
1922
  }
1783
- const baseBranch = config.baseBranch ?? "main";
1784
1923
  let baseCommit;
1785
- const syncSpinner = ora5(`Syncing with ${baseBranch}\u2026`).start();
1924
+ let targetBranch;
1925
+ let isSubtask;
1786
1926
  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
- }
1927
+ ({ baseCommit, targetBranch, isSubtask } = await resolveBaseAndTarget(config, me, true));
1928
+ } catch (err) {
1929
+ return err.message;
1930
+ }
1931
+ if (isSubtask) {
1932
+ console.log(chalk6.dim(` Sub-task: will target branch ${chalk6.cyan(targetBranch)} (base: ${baseCommit?.slice(0, 7) ?? "HEAD"})`));
1796
1933
  }
1797
- const createSpinner = ora5(`Creating "${title}"\u2026`).start();
1934
+ const createSpinner = ora4(`Creating "${title}"\u2026`).start();
1798
1935
  let htmlUrl;
1799
1936
  let issueNumber;
1800
1937
  let issueTitle;
1801
1938
  try {
1802
- const issue = await createTask(config, title, guide, baseCommit);
1939
+ const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1803
1940
  createSpinner.stop();
1804
1941
  htmlUrl = issue.htmlUrl;
1805
1942
  issueNumber = issue.number;
@@ -1808,19 +1945,19 @@ async function run4(input3, config) {
1808
1945
  createSpinner.stop();
1809
1946
  return `Error: ${err.message}`;
1810
1947
  }
1811
- console.log(chalk9.green(`
1948
+ console.log(chalk6.green(`
1812
1949
  Created #${issueNumber} "${issueTitle}"
1813
- ${chalk9.dim(htmlUrl)}
1950
+ ${chalk6.dim(htmlUrl)}
1814
1951
  `));
1815
1952
  try {
1816
- const openBrowser = await select6({
1953
+ const openBrowser = await select4({
1817
1954
  message: "Open issue in browser?",
1818
1955
  choices: [
1819
1956
  { name: "Yes", value: true },
1820
1957
  { name: "No", value: false }
1821
1958
  ]
1822
1959
  });
1823
- if (openBrowser) await open2(htmlUrl);
1960
+ if (openBrowser) await open(htmlUrl);
1824
1961
  } catch {
1825
1962
  }
1826
1963
  return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
@@ -1836,19 +1973,9 @@ async function execute4(input3, config) {
1836
1973
  if (feedback) {
1837
1974
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
1838
1975
  }
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
- }
1976
+ const { baseCommit, targetBranch } = await resolveBaseAndTarget(config, me, false);
1850
1977
  try {
1851
- const issue = await createTask(config, title, guide, baseCommit);
1978
+ const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1852
1979
  return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
1853
1980
 
1854
1981
  Guide:
@@ -1868,7 +1995,7 @@ __export(my_status_exports, {
1868
1995
  terminal: () => terminal5
1869
1996
  });
1870
1997
  init_github();
1871
- import ora6 from "ora";
1998
+ import ora5 from "ora";
1872
1999
  var definition5 = {
1873
2000
  type: "function",
1874
2001
  function: {
@@ -1878,7 +2005,7 @@ var definition5 = {
1878
2005
  }
1879
2006
  };
1880
2007
  async function run5(_input, config) {
1881
- const spinner = ora6("Fetching your tasks\u2026").start();
2008
+ const spinner = ora5("Fetching your tasks\u2026").start();
1882
2009
  try {
1883
2010
  const me = await getAuthenticatedUser(config);
1884
2011
  const tasks = await listMyTasks(config, me);
@@ -1898,120 +2025,193 @@ var terminal5 = true;
1898
2025
  // src/tools/review/index.ts
1899
2026
  var review_exports = {};
1900
2027
  __export(review_exports, {
2028
+ definition: () => definition8,
2029
+ execute: () => execute8,
2030
+ run: () => run8,
2031
+ terminal: () => terminal8
2032
+ });
2033
+ init_github();
2034
+ import chalk9 from "chalk";
2035
+ import ora8 from "ora";
2036
+ import { select as select7 } from "@inquirer/prompts";
2037
+
2038
+ // src/tools/accept/index.ts
2039
+ var accept_exports = {};
2040
+ __export(accept_exports, {
1901
2041
  definition: () => definition6,
1902
2042
  execute: () => execute6,
1903
2043
  run: () => run6,
1904
2044
  terminal: () => terminal6
1905
2045
  });
1906
2046
  init_github();
1907
- import ora7 from "ora";
2047
+ import chalk7 from "chalk";
2048
+ import { select as select5 } from "@inquirer/prompts";
2049
+ import ora6 from "ora";
1908
2050
  var definition6 = {
1909
2051
  type: "function",
1910
2052
  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: [] }
2053
+ name: "accept",
2054
+ description: "Accept an in-review task: merges the PR into the target branch and closes the issue.",
2055
+ parameters: {
2056
+ type: "object",
2057
+ properties: {
2058
+ issue_number: { type: "number", description: "GitHub issue number to accept" }
2059
+ },
2060
+ required: ["issue_number"]
2061
+ }
1914
2062
  }
1915
2063
  };
1916
- async function run6(_input, config) {
1917
- const spinner = ora7("Loading tasks for review\u2026").start();
2064
+ async function run6(input3, config) {
2065
+ let issueNumber = input3["issue_number"];
2066
+ if (!issueNumber) {
2067
+ const spinner3 = ora6("Loading tasks for review\u2026").start();
2068
+ let tasks;
2069
+ let me;
2070
+ try {
2071
+ me = await getAuthenticatedUser(config);
2072
+ tasks = await listTasksForReview(config, me);
2073
+ spinner3.stop();
2074
+ } catch (err) {
2075
+ spinner3.stop();
2076
+ return `Error: ${err.message}`;
2077
+ }
2078
+ if (tasks.length === 0) return "No tasks pending review.";
2079
+ try {
2080
+ issueNumber = await select5({
2081
+ message: "Which task to accept?",
2082
+ choices: tasks.map((t) => ({
2083
+ name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
2084
+ value: t.number
2085
+ }))
2086
+ });
2087
+ } catch {
2088
+ return "Cancelled.";
2089
+ }
2090
+ }
2091
+ const spinner2 = ora6("Verifying permissions\u2026").start();
2092
+ let me2;
2093
+ let issue;
1918
2094
  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")}`;
2095
+ [me2, issue] = await Promise.all([
2096
+ getAuthenticatedUser(config),
2097
+ getTask(config, issueNumber)
2098
+ ]);
2099
+ spinner2.stop();
1926
2100
  } catch (err) {
1927
- spinner.stop();
2101
+ spinner2.stop();
1928
2102
  return `Error: ${err.message}`;
1929
2103
  }
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: [] }
2104
+ if (issue.author && issue.author !== me2) {
2105
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
1948
2106
  }
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: [] }
2107
+ let confirmed;
2108
+ try {
2109
+ confirmed = await select5({
2110
+ message: `Merge PR for #${issueNumber} and close issue?`,
2111
+ choices: [
2112
+ { name: "Yes, accept", value: true },
2113
+ { name: "Cancel", value: false }
2114
+ ]
2115
+ });
2116
+ } catch {
2117
+ return "Cancelled.";
1979
2118
  }
1980
- };
1981
- async function run8(_input, config) {
1982
- let branch;
2119
+ if (!confirmed) return "Cancelled.";
2120
+ const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2121
+ let result;
1983
2122
  try {
1984
- branch = await getCurrentBranch();
2123
+ result = await acceptTask(config, issueNumber);
2124
+ spinner.succeed(`PR #${result.prNumber} merged \u2192 ${chalk7.cyan(result.baseBranch)}`);
1985
2125
  } catch (err) {
2126
+ spinner.fail("Failed");
1986
2127
  return `Error: ${err.message}`;
1987
2128
  }
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;
2129
+ const mergedIntoTaskBranch = isTaskBranch(result.baseBranch);
2130
+ if (!mergedIntoTaskBranch) {
2131
+ const baseBranch = config.baseBranch ?? "main";
2132
+ let pushToMain;
2133
+ try {
2134
+ pushToMain = await select5({
2135
+ message: `Push ${chalk7.cyan(result.baseBranch)} \u2192 ${chalk7.cyan(baseBranch)}?`,
2136
+ choices: [
2137
+ { name: `Yes, push to ${baseBranch}`, value: true },
2138
+ { name: "No, keep in worker branch", value: false }
2139
+ ]
2140
+ });
2141
+ } catch {
2142
+ pushToMain = false;
2143
+ }
2144
+ if (pushToMain) {
2145
+ const mergeSpinner = ora6(`Merging ${result.baseBranch} \u2192 ${baseBranch}\u2026`).start();
2146
+ try {
2147
+ await mergeWorkerIntoBase(config, result.baseBranch, baseBranch);
2148
+ mergeSpinner.succeed(`Merged ${result.baseBranch} \u2192 ${baseBranch}`);
2149
+ } catch (err) {
2150
+ mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
2151
+ }
2152
+ }
2153
+ }
2154
+ let updateWiki = false;
1992
2155
  try {
1993
- issue = await getTask(config, issueNum);
2156
+ updateWiki = await select5({
2157
+ message: "Update TECHUNTER.md project overview?",
2158
+ choices: [
2159
+ { name: "Yes, regenerate", value: true },
2160
+ { name: "No, skip", value: false }
2161
+ ]
2162
+ });
2163
+ } catch {
2164
+ }
2165
+ if (updateWiki) {
2166
+ const wikiSpinner = ora6("Regenerating TECHUNTER.md\u2026").start();
2167
+ try {
2168
+ const content = await generateWiki(config);
2169
+ await upsertRepoFile(config, "TECHUNTER.md", content, "docs: update TECHUNTER.md project overview");
2170
+ wikiSpinner.succeed("TECHUNTER.md updated");
2171
+ } catch (err) {
2172
+ wikiSpinner.fail(`Wiki update failed: ${err.message}`);
2173
+ }
2174
+ }
2175
+ const mergeTarget = mergedIntoTaskBranch ? `${result.baseBranch} (sub-task merged, no push to main)` : result.baseBranch;
2176
+ return `Task #${issueNumber} accepted.
2177
+ PR #${result.prNumber} merged \u2192 ${mergeTarget}
2178
+ Issue closed.`;
2179
+ }
2180
+ async function execute6(input3, config) {
2181
+ const issueNumber = input3["issue_number"];
2182
+ const [me, issue] = await Promise.all([
2183
+ getAuthenticatedUser(config),
2184
+ getTask(config, issueNumber)
2185
+ ]);
2186
+ if (issue.author && issue.author !== me) {
2187
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2188
+ }
2189
+ const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2190
+ try {
2191
+ const result = await acceptTask(config, issueNumber);
2192
+ spinner.stop();
2193
+ return `Task #${issueNumber} accepted.
2194
+ PR #${result.prNumber} merged \u2192 ${result.baseBranch}
2195
+ Issue closed.`;
1994
2196
  } catch (err) {
2197
+ spinner.stop();
1995
2198
  return `Error: ${err.message}`;
1996
2199
  }
1997
- await launchClaudeCode(issue, branch);
1998
- return "Claude Code session ended.";
1999
2200
  }
2000
- var execute8 = run8;
2001
- var terminal8 = true;
2201
+ var terminal6 = true;
2002
2202
 
2003
2203
  // src/tools/reject/index.ts
2004
2204
  var reject_exports = {};
2005
2205
  __export(reject_exports, {
2006
- definition: () => definition9,
2007
- execute: () => execute9,
2008
- run: () => run9,
2009
- terminal: () => terminal9
2206
+ definition: () => definition7,
2207
+ execute: () => execute7,
2208
+ run: () => run7,
2209
+ terminal: () => terminal7
2010
2210
  });
2011
2211
  init_github();
2012
- import chalk10 from "chalk";
2013
- import { select as select7, input as promptInput3 } from "@inquirer/prompts";
2014
- import ora8 from "ora";
2212
+ import chalk8 from "chalk";
2213
+ import { select as select6, input as promptInput3 } from "@inquirer/prompts";
2214
+ import ora7 from "ora";
2015
2215
 
2016
2216
  // src/tools/reject/prompts.ts
2017
2217
  var REJECTION_FORMAT = `
@@ -2042,7 +2242,7 @@ Reviewer feedback: ${userFeedback}`,
2042
2242
  }
2043
2243
 
2044
2244
  // src/tools/reject/index.ts
2045
- var definition9 = {
2245
+ var definition7 = {
2046
2246
  type: "function",
2047
2247
  function: {
2048
2248
  name: "reject",
@@ -2057,7 +2257,7 @@ var definition9 = {
2057
2257
  }
2058
2258
  }
2059
2259
  };
2060
- async function run9(input3, config) {
2260
+ async function run7(input3, config) {
2061
2261
  const issueNumber = input3["issue_number"];
2062
2262
  const [me, issue] = await Promise.all([
2063
2263
  getAuthenticatedUser(config),
@@ -2075,9 +2275,9 @@ async function run9(input3, config) {
2075
2275
  return "Cancelled.";
2076
2276
  }
2077
2277
  if (!feedback.trim()) return "Cancelled.";
2078
- const divider = chalk10.dim("\u2500".repeat(70));
2278
+ const divider = chalk8.dim("\u2500".repeat(70));
2079
2279
  for (; ; ) {
2080
- const spinner = ora8("Generating rejection comment\u2026").start();
2280
+ const spinner = ora7("Generating rejection comment\u2026").start();
2081
2281
  let comment;
2082
2282
  try {
2083
2283
  comment = await generateRejectionComment(config, issueNumber, feedback);
@@ -2087,13 +2287,13 @@ async function run9(input3, config) {
2087
2287
  return `Error generating comment: ${err.message}`;
2088
2288
  }
2089
2289
  console.log("\n" + divider);
2090
- console.log(chalk10.bold(` Rejection preview \u2014 issue #${issueNumber}`));
2290
+ console.log(chalk8.bold(` Rejection preview \u2014 issue #${issueNumber}`));
2091
2291
  console.log(divider);
2092
2292
  console.log(renderMarkdown(comment));
2093
2293
  console.log(divider + "\n");
2094
2294
  let decision;
2095
2295
  try {
2096
- decision = await select7({
2296
+ decision = await select6({
2097
2297
  message: `Post rejection and mark #${issueNumber} as changes-needed?`,
2098
2298
  choices: [
2099
2299
  { name: "Post & Reject", value: "yes" },
@@ -2113,7 +2313,7 @@ async function run9(input3, config) {
2113
2313
  }
2114
2314
  continue;
2115
2315
  }
2116
- let spinner2 = ora8(`Posting rejection comment on #${issueNumber}\u2026`).start();
2316
+ let spinner2 = ora7(`Posting rejection comment on #${issueNumber}\u2026`).start();
2117
2317
  try {
2118
2318
  await postComment(config, issueNumber, comment);
2119
2319
  spinner2.stop();
@@ -2121,7 +2321,7 @@ async function run9(input3, config) {
2121
2321
  spinner2.stop();
2122
2322
  return `Error posting comment: ${err.message}`;
2123
2323
  }
2124
- spinner2 = ora8(`Marking #${issueNumber} as changes-needed\u2026`).start();
2324
+ spinner2 = ora7(`Marking #${issueNumber} as changes-needed\u2026`).start();
2125
2325
  try {
2126
2326
  await rejectTask(config, issueNumber);
2127
2327
  spinner2.stop();
@@ -2132,7 +2332,7 @@ async function run9(input3, config) {
2132
2332
  return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
2133
2333
  }
2134
2334
  }
2135
- async function execute9(input3, config) {
2335
+ async function execute7(input3, config) {
2136
2336
  const issueNumber = input3["issue_number"];
2137
2337
  const feedback = input3["feedback"];
2138
2338
  const [me, issue] = await Promise.all([
@@ -2163,255 +2363,536 @@ async function execute9(input3, config) {
2163
2363
  Comment posted:
2164
2364
  ${comment}`;
2165
2365
  }
2366
+ var terminal7 = true;
2367
+
2368
+ // src/tools/review/index.ts
2369
+ var definition8 = {
2370
+ type: "function",
2371
+ function: {
2372
+ name: "review",
2373
+ description: "List tasks waiting for your review (submitted by others, created by you), then let you accept or reject one. Equivalent to /review.",
2374
+ parameters: { type: "object", properties: {}, required: [] }
2375
+ }
2376
+ };
2377
+ async function run8(_input, config) {
2378
+ const spinner = ora8("Loading tasks for review\u2026").start();
2379
+ let me;
2380
+ let tasks;
2381
+ try {
2382
+ me = await getAuthenticatedUser(config);
2383
+ tasks = await listTasksForReview(config, me);
2384
+ spinner.stop();
2385
+ } catch (err) {
2386
+ spinner.stop();
2387
+ return `Error: ${err.message}`;
2388
+ }
2389
+ if (tasks.length === 0) return `No tasks pending review for @${me}.`;
2390
+ let issueNumber;
2391
+ try {
2392
+ issueNumber = await select7({
2393
+ message: "Select a task to review:",
2394
+ choices: tasks.map((t) => ({
2395
+ name: `#${String(t.number).padEnd(4)} @${t.assignee ?? "\u2014"} ${t.title}`,
2396
+ value: t.number
2397
+ }))
2398
+ });
2399
+ } catch {
2400
+ return "Cancelled.";
2401
+ }
2402
+ const spinner2 = ora8(`Loading #${issueNumber}\u2026`).start();
2403
+ let pr;
2404
+ try {
2405
+ pr = await getTaskPR(config, issueNumber);
2406
+ spinner2.stop();
2407
+ } catch (err) {
2408
+ spinner2.stop();
2409
+ return `Error loading PR: ${err.message}`;
2410
+ }
2411
+ const divider = chalk9.dim("\u2500".repeat(70));
2412
+ console.log("\n" + divider);
2413
+ if (pr) {
2414
+ console.log(chalk9.bold(` PR #${pr.number}`) + " " + chalk9.dim(pr.url));
2415
+ console.log(divider);
2416
+ console.log(renderMarkdown(pr.body));
2417
+ } else {
2418
+ console.log(chalk9.yellow(` No open PR found for task #${issueNumber}`));
2419
+ }
2420
+ console.log(divider + "\n");
2421
+ for (; ; ) {
2422
+ let action;
2423
+ try {
2424
+ action = await select7({
2425
+ message: "Review action:",
2426
+ choices: [
2427
+ ...pr ? [{ name: "View diff", value: "diff" }] : [],
2428
+ { name: chalk9.green("Accept") + " \u2014 merge PR and close issue", value: "accept" },
2429
+ { name: chalk9.red("Reject") + " \u2014 request changes", value: "reject" },
2430
+ { name: "Nothing, just viewing", value: "none" }
2431
+ ]
2432
+ });
2433
+ } catch {
2434
+ return "Cancelled.";
2435
+ }
2436
+ if (action === "none") return `Viewed task #${issueNumber}.`;
2437
+ if (action === "accept") return run6({ issue_number: issueNumber }, config);
2438
+ if (action === "reject") return run7({ issue_number: issueNumber }, config);
2439
+ if (action === "diff") {
2440
+ const diffSpinner = ora8("Fetching diff\u2026").start();
2441
+ let diff;
2442
+ try {
2443
+ diff = await getTaskPRDiff(config, pr.number);
2444
+ diffSpinner.stop();
2445
+ } catch (err) {
2446
+ diffSpinner.stop();
2447
+ console.log(chalk9.red(`Error fetching diff: ${err.message}`));
2448
+ continue;
2449
+ }
2450
+ console.log("\n" + divider);
2451
+ for (const line of diff.split("\n")) {
2452
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2453
+ process.stdout.write(chalk9.green(line) + "\n");
2454
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2455
+ process.stdout.write(chalk9.red(line) + "\n");
2456
+ } else if (line.startsWith("@@")) {
2457
+ process.stdout.write(chalk9.cyan(line) + "\n");
2458
+ } else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
2459
+ process.stdout.write(chalk9.bold(line) + "\n");
2460
+ } else {
2461
+ process.stdout.write(line + "\n");
2462
+ }
2463
+ }
2464
+ console.log(divider + "\n");
2465
+ }
2466
+ }
2467
+ }
2468
+ var execute8 = run8;
2469
+ var terminal8 = true;
2470
+
2471
+ // src/tools/refresh/index.ts
2472
+ var refresh_exports = {};
2473
+ __export(refresh_exports, {
2474
+ definition: () => definition9,
2475
+ execute: () => execute9,
2476
+ run: () => run9,
2477
+ terminal: () => terminal9
2478
+ });
2479
+ var definition9 = {
2480
+ type: "function",
2481
+ function: {
2482
+ name: "refresh",
2483
+ description: "Reload and display the full task list. Equivalent to /refresh.",
2484
+ parameters: { type: "object", properties: {}, required: [] }
2485
+ }
2486
+ };
2487
+ async function run9(_input, config) {
2488
+ const tasks = await printTaskList(config);
2489
+ if (tasks.length === 0) return "No tasks found.";
2490
+ const lines = tasks.map((t) => {
2491
+ const status = getStatus(t);
2492
+ const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
2493
+ return `#${t.number} [${status}] ${assignee} ${t.title}`;
2494
+ });
2495
+ return `Tasks (${tasks.length}):
2496
+ ${lines.join("\n")}`;
2497
+ }
2498
+ var execute9 = run9;
2166
2499
  var terminal9 = true;
2167
2500
 
2168
- // src/tools/accept/index.ts
2169
- var accept_exports = {};
2170
- __export(accept_exports, {
2501
+ // src/tools/open-code/index.ts
2502
+ var open_code_exports = {};
2503
+ __export(open_code_exports, {
2171
2504
  definition: () => definition10,
2172
2505
  execute: () => execute10,
2173
2506
  run: () => run10,
2174
2507
  terminal: () => terminal10
2175
2508
  });
2176
2509
  init_github();
2177
- import chalk11 from "chalk";
2178
- import { select as select8 } from "@inquirer/prompts";
2179
- import ora9 from "ora";
2180
2510
  var definition10 = {
2181
2511
  type: "function",
2182
2512
  function: {
2183
- name: "accept",
2184
- description: "Accept an in-review task: merges the PR into your worker branch and closes the issue.",
2513
+ name: "open_code",
2514
+ description: "Launch Claude Code for the current task branch. Equivalent to /code.",
2515
+ parameters: { type: "object", properties: {}, required: [] }
2516
+ }
2517
+ };
2518
+ async function run10(_input, config) {
2519
+ let branch;
2520
+ try {
2521
+ branch = await getCurrentBranch();
2522
+ } catch (err) {
2523
+ return `Error: ${err.message}`;
2524
+ }
2525
+ let issueNum = getConfig().taskState?.activeIssueNumber;
2526
+ if (!issueNum) {
2527
+ const found = await getIssueNumberFromBranch(config, branch);
2528
+ if (!found) return `No active task found (current branch: ${branch}).`;
2529
+ issueNum = found.issueNumber;
2530
+ }
2531
+ let issue;
2532
+ try {
2533
+ issue = await getTask(config, issueNum);
2534
+ } catch (err) {
2535
+ return `Error: ${err.message}`;
2536
+ }
2537
+ await launchClaudeCode(issue, branch);
2538
+ return "Claude Code session ended.";
2539
+ }
2540
+ var execute10 = run10;
2541
+ var terminal10 = true;
2542
+
2543
+ // src/tools/edit-task/index.ts
2544
+ var edit_task_exports = {};
2545
+ __export(edit_task_exports, {
2546
+ definition: () => definition11,
2547
+ execute: () => execute11,
2548
+ run: () => run11,
2549
+ terminal: () => terminal11
2550
+ });
2551
+ init_github();
2552
+ import { select as select8, input as promptInput4 } from "@inquirer/prompts";
2553
+ import ora9 from "ora";
2554
+ var definition11 = {
2555
+ type: "function",
2556
+ function: {
2557
+ name: "edit_task",
2558
+ description: "Edit the title and/or body of an existing task (GitHub Issue). Equivalent to /edit.",
2185
2559
  parameters: {
2186
2560
  type: "object",
2187
2561
  properties: {
2188
- issue_number: { type: "number", description: "GitHub issue number to accept" }
2562
+ issue_number: { type: "number", description: "Issue number to edit." },
2563
+ title: { type: "string", description: "New title." },
2564
+ body: { type: "string", description: "New body/description." }
2189
2565
  },
2190
- required: ["issue_number"]
2566
+ required: ["issue_number", "title", "body"]
2191
2567
  }
2192
2568
  }
2193
2569
  };
2194
- async function run10(input3, config) {
2570
+ async function run11(input3, config) {
2195
2571
  let issueNumber = input3["issue_number"];
2196
2572
  if (!issueNumber) {
2197
- const spinner3 = ora9("Loading tasks for review\u2026").start();
2198
2573
  let tasks;
2199
- let me;
2200
2574
  try {
2201
- me = await getAuthenticatedUser(config);
2202
- tasks = await listTasksForReview(config, me);
2203
- spinner3.stop();
2575
+ tasks = await listTasks(config);
2204
2576
  } catch (err) {
2205
- spinner3.stop();
2206
- return `Error: ${err.message}`;
2577
+ return `Error loading tasks: ${err.message}`;
2207
2578
  }
2208
- if (tasks.length === 0) return "No tasks pending review.";
2579
+ if (tasks.length === 0) return "No tasks found.";
2209
2580
  try {
2210
2581
  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
- }))
2582
+ message: "Select task to edit:",
2583
+ choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
2216
2584
  });
2217
2585
  } catch {
2218
2586
  return "Cancelled.";
2219
2587
  }
2220
2588
  }
2221
- const spinner2 = ora9("Verifying permissions\u2026").start();
2222
- let me2;
2223
2589
  let issue;
2224
2590
  try {
2225
- [me2, issue] = await Promise.all([
2226
- getAuthenticatedUser(config),
2227
- getTask(config, issueNumber)
2228
- ]);
2229
- spinner2.stop();
2591
+ issue = await getTask(config, issueNumber);
2230
2592
  } 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}.`;
2593
+ return `Error loading task: ${err.message}`;
2236
2594
  }
2237
- const targetBranch = makeWorkerBranchName(me2);
2238
- let confirmed;
2595
+ let title;
2596
+ let body;
2239
2597
  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
- ]
2598
+ title = await promptInput4({
2599
+ message: "Title:",
2600
+ default: issue.title
2601
+ });
2602
+ body = await promptInput4({
2603
+ message: "Description:",
2604
+ default: issue.body ?? ""
2246
2605
  });
2247
2606
  } catch {
2248
2607
  return "Cancelled.";
2249
2608
  }
2250
- if (!confirmed) return "Cancelled.";
2251
- const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
2252
- let result;
2609
+ if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
2610
+ return "No changes made.";
2611
+ }
2612
+ const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
2253
2613
  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}`);
2614
+ await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
2615
+ spinner.stop();
2616
+ return `Task #${issueNumber} updated.`;
2257
2617
  } catch (err) {
2258
- spinner.fail("Failed");
2618
+ spinner.stop();
2259
2619
  return `Error: ${err.message}`;
2260
2620
  }
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
2621
  }
2287
- async function execute10(input3, config) {
2622
+ async function execute11(input3, config) {
2288
2623
  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}.`;
2295
- }
2296
- const targetBranch = makeWorkerBranchName(me);
2297
- const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
2624
+ const title = input3["title"];
2625
+ const body = input3["body"];
2626
+ const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
2298
2627
  try {
2299
- const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
2300
- const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
2628
+ await editTask(config, issueNumber, title, body);
2301
2629
  spinner.stop();
2302
- return `Task #${issueNumber} accepted.
2303
- PR #${result.prNumber} merged \u2192 ${targetBranch}
2304
- Issue closed.`;
2630
+ return `Task #${issueNumber} updated.`;
2305
2631
  } catch (err) {
2306
2632
  spinner.stop();
2307
2633
  return `Error: ${err.message}`;
2308
2634
  }
2309
2635
  }
2310
- var terminal10 = true;
2636
+ var terminal11 = true;
2311
2637
 
2312
- // src/tools/edit-task/index.ts
2313
- var edit_task_exports = {};
2314
- __export(edit_task_exports, {
2315
- definition: () => definition11,
2316
- execute: () => execute11,
2317
- run: () => run11,
2318
- terminal: () => terminal11
2638
+ // src/tools/move-task/index.ts
2639
+ var move_task_exports = {};
2640
+ __export(move_task_exports, {
2641
+ definition: () => definition12,
2642
+ execute: () => execute12,
2643
+ run: () => run12,
2644
+ terminal: () => terminal12
2319
2645
  });
2320
2646
  init_github();
2321
- import { select as select9, input as promptInput4 } from "@inquirer/prompts";
2647
+ import { select as select9 } from "@inquirer/prompts";
2322
2648
  import ora10 from "ora";
2323
- var definition11 = {
2649
+ import chalk10 from "chalk";
2650
+ var definition12 = {
2324
2651
  type: "function",
2325
2652
  function: {
2326
- name: "edit_task",
2327
- description: "Edit the title and/or body of an existing task (GitHub Issue). Equivalent to /edit.",
2653
+ name: "move_task",
2654
+ description: "Move one of your own published tasks to be a sub-task of another task. Updates the target branch and base commit so executors sync from the new parent HEAD. Equivalent to /move.",
2328
2655
  parameters: {
2329
2656
  type: "object",
2330
2657
  properties: {
2331
- issue_number: { type: "number", description: "Issue number to edit." },
2332
- title: { type: "string", description: "New title." },
2333
- body: { type: "string", description: "New body/description." }
2658
+ issue_number: { type: "number", description: "Issue number of the task to move." },
2659
+ parent_issue_number: { type: "number", description: "Issue number of the new parent task." }
2334
2660
  },
2335
- required: ["issue_number", "title", "body"]
2661
+ required: ["issue_number", "parent_issue_number"]
2336
2662
  }
2337
2663
  }
2338
2664
  };
2339
- async function run11(input3, config) {
2665
+ async function run12(input3, config) {
2666
+ const me = await getAuthenticatedUser(config);
2667
+ const allTasks = await listTasks(config);
2340
2668
  let issueNumber = input3["issue_number"];
2341
- if (!issueNumber) {
2342
- let tasks;
2669
+ let taskToMove;
2670
+ if (issueNumber) {
2343
2671
  try {
2344
- tasks = await listTasks(config);
2672
+ taskToMove = await getTask(config, issueNumber);
2345
2673
  } catch (err) {
2346
- return `Error loading tasks: ${err.message}`;
2674
+ return `Error loading task #${issueNumber}: ${err.message}`;
2347
2675
  }
2348
- if (tasks.length === 0) return "No tasks found.";
2676
+ if (taskToMove.author !== me) {
2677
+ return `Task #${issueNumber} was not authored by you \u2014 you can only move your own tasks.`;
2678
+ }
2679
+ } else {
2680
+ const myTasks = allTasks.filter((t) => t.author === me);
2681
+ if (myTasks.length === 0) return "No tasks you authored are available to move.";
2349
2682
  try {
2350
2683
  issueNumber = await select9({
2351
- message: "Select task to edit:",
2352
- choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
2684
+ message: "Select task to move:",
2685
+ choices: myTasks.map((t) => ({
2686
+ name: `#${t.number} [${getStatus(t)}] ${t.title}`,
2687
+ value: t.number
2688
+ }))
2353
2689
  });
2354
2690
  } catch {
2355
2691
  return "Cancelled.";
2356
2692
  }
2693
+ taskToMove = myTasks.find((t) => t.number === issueNumber);
2694
+ }
2695
+ const candidates = allTasks.filter((t) => t.number !== taskToMove.number);
2696
+ const resolveSpinner = ora10("Finding parent task branches\u2026").start();
2697
+ const parents = [];
2698
+ for (const t of candidates) {
2699
+ const branch = await getTaskBranch(config, t.number);
2700
+ if (branch) parents.push({ task: t, branch });
2701
+ }
2702
+ resolveSpinner.stop();
2703
+ if (parents.length === 0) {
2704
+ return "No other tasks with known branches are available as a parent.";
2705
+ }
2706
+ let parentIssueNumber = input3["parent_issue_number"];
2707
+ let chosen;
2708
+ if (parentIssueNumber) {
2709
+ const found = parents.find((p) => p.task.number === parentIssueNumber);
2710
+ if (!found) {
2711
+ return `Task #${parentIssueNumber} is not available as a parent (no branch found or not open).`;
2712
+ }
2713
+ chosen = found;
2714
+ } else {
2715
+ try {
2716
+ const selectedBranch = await select9({
2717
+ message: `Move #${taskToMove.number} under which task?`,
2718
+ choices: parents.map((p) => ({
2719
+ name: `#${p.task.number} [${getStatus(p.task)}] ${p.task.title} ${chalk10.dim("\u2192 " + p.branch)}`,
2720
+ value: p.branch
2721
+ }))
2722
+ });
2723
+ chosen = parents.find((p) => p.branch === selectedBranch);
2724
+ } catch {
2725
+ return "Cancelled.";
2726
+ }
2357
2727
  }
2358
- let issue;
2728
+ const sha = await getBranchHeadSha(config, chosen.branch);
2729
+ if (!sha) {
2730
+ return `Could not resolve HEAD of branch ${chosen.branch} \u2014 does it exist on the remote?`;
2731
+ }
2732
+ const spinner = ora10(`Moving #${taskToMove.number} under #${chosen.task.number}\u2026`).start();
2359
2733
  try {
2360
- issue = await getTask(config, issueNumber);
2734
+ await moveTask(config, taskToMove.number, chosen.branch, sha);
2735
+ spinner.succeed(
2736
+ `Task #${taskToMove.number} moved under #${chosen.task.number} "${chosen.task.title}"
2737
+ target: ${chalk10.cyan(chosen.branch)} base: ${chalk10.dim(sha.slice(0, 7))}`
2738
+ );
2739
+ return `Task #${taskToMove.number} moved under #${chosen.task.number} (branch: ${chosen.branch}, base: ${sha.slice(0, 7)})`;
2361
2740
  } catch (err) {
2362
- return `Error loading task: ${err.message}`;
2741
+ spinner.fail(`Failed: ${err.message}`);
2742
+ return `Error: ${err.message}`;
2363
2743
  }
2364
- let title;
2365
- let body;
2744
+ }
2745
+ async function execute12(input3, config) {
2746
+ const me = await getAuthenticatedUser(config);
2747
+ const issueNumber = input3["issue_number"];
2748
+ const parentIssueNumber = input3["parent_issue_number"];
2749
+ const task = await getTask(config, issueNumber);
2750
+ if (task.author !== me) {
2751
+ return `Task #${issueNumber} was not authored by you \u2014 you can only move your own tasks.`;
2752
+ }
2753
+ const branch = await getTaskBranch(config, parentIssueNumber);
2754
+ if (!branch) return `No branch found for parent task #${parentIssueNumber}.`;
2755
+ const sha = await getBranchHeadSha(config, branch);
2756
+ if (!sha) return `Could not resolve HEAD of branch ${branch}.`;
2757
+ await moveTask(config, issueNumber, branch, sha);
2758
+ return `Task #${issueNumber} moved under #${parentIssueNumber} (branch: ${branch}, base: ${sha.slice(0, 7)})`;
2759
+ }
2760
+ var terminal12 = true;
2761
+
2762
+ // src/tools/wiki/index.ts
2763
+ var wiki_exports = {};
2764
+ __export(wiki_exports, {
2765
+ definition: () => definition13,
2766
+ execute: () => execute13,
2767
+ run: () => run13,
2768
+ terminal: () => terminal13
2769
+ });
2770
+ import ora11 from "ora";
2771
+ import chalk11 from "chalk";
2772
+ import { readFile as readFile2 } from "fs/promises";
2773
+ import { select as select10 } from "@inquirer/prompts";
2774
+ init_github();
2775
+ var WIKI_PATH = "TECHUNTER.md";
2776
+ var definition13 = {
2777
+ type: "function",
2778
+ function: {
2779
+ name: "update_wiki",
2780
+ 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.",
2781
+ parameters: {
2782
+ type: "object",
2783
+ properties: {},
2784
+ required: []
2785
+ }
2786
+ }
2787
+ };
2788
+ async function readWikiContent(config) {
2366
2789
  try {
2367
- title = await promptInput4({
2368
- message: "Title:",
2369
- default: issue.title
2370
- });
2371
- body = await promptInput4({
2372
- message: "Description:",
2373
- default: issue.body ?? ""
2790
+ return await readFile2(WIKI_PATH, "utf-8");
2791
+ } catch {
2792
+ }
2793
+ return getRepoFile(config, WIKI_PATH);
2794
+ }
2795
+ function printWiki(content) {
2796
+ const divider = chalk11.dim("\u2500".repeat(70));
2797
+ console.log("\n" + divider);
2798
+ console.log(chalk11.bold(" TECHUNTER.md"));
2799
+ console.log(divider);
2800
+ console.log(renderMarkdown(content));
2801
+ console.log(divider + "\n");
2802
+ }
2803
+ async function run13(_input, config) {
2804
+ const fetchSpinner = ora11("Checking for existing wiki\u2026").start();
2805
+ const existing = await readWikiContent(config).catch(() => null);
2806
+ fetchSpinner.stop();
2807
+ let action;
2808
+ try {
2809
+ action = await select10({
2810
+ message: "TECHUNTER.md \u2014 what would you like to do?",
2811
+ choices: [
2812
+ ...existing ? [{ name: "View current wiki", value: "view" }] : [],
2813
+ { name: existing ? "Regenerate & commit to repo" : "Generate & commit to repo", value: "generate" },
2814
+ { name: "Cancel", value: "cancel" }
2815
+ ]
2374
2816
  });
2375
2817
  } catch {
2376
2818
  return "Cancelled.";
2377
2819
  }
2378
- if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
2379
- return "No changes made.";
2820
+ if (action === "cancel") return "Cancelled.";
2821
+ if (action === "view") {
2822
+ printWiki(existing);
2823
+ return "Displayed TECHUNTER.md.";
2380
2824
  }
2381
- const spinner = ora10(`Updating #${issueNumber}\u2026`).start();
2825
+ const authSpinner = ora11("Checking permissions\u2026").start();
2826
+ let me;
2827
+ let allowed;
2382
2828
  try {
2383
- await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
2384
- spinner.stop();
2385
- return `Task #${issueNumber} updated.`;
2829
+ me = await getAuthenticatedUser(config);
2830
+ allowed = await isCollaborator(config, me);
2831
+ authSpinner.stop();
2386
2832
  } catch (err) {
2387
- spinner.stop();
2833
+ authSpinner.stop();
2834
+ return `Error checking permissions: ${err.message}`;
2835
+ }
2836
+ if (!allowed) {
2837
+ return `Permission denied: only repository collaborators can update the project wiki.`;
2838
+ }
2839
+ const genSpinner = ora11("Analyzing project and generating overview\u2026").start();
2840
+ let content;
2841
+ try {
2842
+ content = await generateWiki(config);
2843
+ genSpinner.stop();
2844
+ } catch (err) {
2845
+ genSpinner.stop();
2846
+ return `Error generating wiki: ${err.message}`;
2847
+ }
2848
+ printWiki(content);
2849
+ let confirm;
2850
+ try {
2851
+ confirm = await select10({
2852
+ message: `Publish to repository as ${WIKI_PATH}?`,
2853
+ choices: [
2854
+ { name: "Yes, commit to repo", value: "publish" },
2855
+ { name: "Cancel", value: "cancel" }
2856
+ ]
2857
+ });
2858
+ } catch {
2859
+ return "Cancelled.";
2860
+ }
2861
+ if (confirm === "cancel") return "Cancelled.";
2862
+ const writeSpinner = ora11(`Writing ${WIKI_PATH}\u2026`).start();
2863
+ try {
2864
+ const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2865
+ writeSpinner.succeed(`Written: ${url}`);
2866
+ console.log("");
2867
+ return `TECHUNTER.md updated \u2014 ${url}`;
2868
+ } catch (err) {
2869
+ writeSpinner.fail(`Failed: ${err.message}`);
2388
2870
  return `Error: ${err.message}`;
2389
2871
  }
2390
2872
  }
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();
2873
+ async function execute13(_input, config) {
2874
+ const me = await getAuthenticatedUser(config);
2875
+ if (!await isCollaborator(config, me)) {
2876
+ return `Permission denied: only repository collaborators can update the project wiki.`;
2877
+ }
2878
+ const content = await generateWiki(config);
2396
2879
  try {
2397
- await editTask(config, issueNumber, title, body);
2398
- spinner.stop();
2399
- return `Task #${issueNumber} updated.`;
2880
+ const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2881
+ return `TECHUNTER.md updated \u2014 ${url}`;
2400
2882
  } catch (err) {
2401
- spinner.stop();
2402
2883
  return `Error: ${err.message}`;
2403
2884
  }
2404
2885
  }
2405
- var terminal11 = true;
2886
+ var terminal13 = true;
2406
2887
 
2407
2888
  // src/tools/list-tasks/index.ts
2408
2889
  var list_tasks_exports = {};
2409
2890
  __export(list_tasks_exports, {
2410
- definition: () => definition12,
2411
- execute: () => execute12
2891
+ definition: () => definition14,
2892
+ execute: () => execute14
2412
2893
  });
2413
2894
  init_github();
2414
- var definition12 = {
2895
+ var definition14 = {
2415
2896
  type: "function",
2416
2897
  function: {
2417
2898
  name: "list_tasks",
@@ -2423,7 +2904,7 @@ var definition12 = {
2423
2904
  }
2424
2905
  }
2425
2906
  };
2426
- async function execute12(_input, config) {
2907
+ async function execute14(_input, config) {
2427
2908
  const tasks = await listTasks(config);
2428
2909
  if (tasks.length === 0) return "No open tasks.";
2429
2910
  return tasks.map((t) => {
@@ -2436,11 +2917,11 @@ async function execute12(_input, config) {
2436
2917
  // src/tools/get-task/index.ts
2437
2918
  var get_task_exports = {};
2438
2919
  __export(get_task_exports, {
2439
- definition: () => definition13,
2440
- execute: () => execute13
2920
+ definition: () => definition15,
2921
+ execute: () => execute15
2441
2922
  });
2442
2923
  init_github();
2443
- var definition13 = {
2924
+ var definition15 = {
2444
2925
  type: "function",
2445
2926
  function: {
2446
2927
  name: "get_task",
@@ -2454,7 +2935,7 @@ var definition13 = {
2454
2935
  }
2455
2936
  }
2456
2937
  };
2457
- async function execute13(input3, config) {
2938
+ async function execute15(input3, config) {
2458
2939
  const issue = await getTask(config, input3["issue_number"]);
2459
2940
  const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2460
2941
  const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
@@ -2471,12 +2952,12 @@ ${issue.body}`);
2471
2952
  // src/tools/get-comments/index.ts
2472
2953
  var get_comments_exports = {};
2473
2954
  __export(get_comments_exports, {
2474
- definition: () => definition14,
2475
- execute: () => execute14
2955
+ definition: () => definition16,
2956
+ execute: () => execute16
2476
2957
  });
2477
2958
  init_github();
2478
- import ora11 from "ora";
2479
- var definition14 = {
2959
+ import ora12 from "ora";
2960
+ var definition16 = {
2480
2961
  type: "function",
2481
2962
  function: {
2482
2963
  name: "get_comments",
@@ -2491,10 +2972,10 @@ var definition14 = {
2491
2972
  }
2492
2973
  }
2493
2974
  };
2494
- async function execute14(input3, config) {
2975
+ async function execute16(input3, config) {
2495
2976
  const issueNumber = input3["issue_number"];
2496
2977
  const limit = input3["limit"] ?? 5;
2497
- const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
2978
+ const spinner = ora12(`Loading comments for #${issueNumber}...`).start();
2498
2979
  try {
2499
2980
  const comments = await listComments(config, issueNumber, limit);
2500
2981
  spinner.stop();
@@ -2513,11 +2994,11 @@ ${lines.join("\n\n")}`;
2513
2994
  // src/tools/get-diff/index.ts
2514
2995
  var get_diff_exports = {};
2515
2996
  __export(get_diff_exports, {
2516
- definition: () => definition15,
2517
- execute: () => execute15
2997
+ definition: () => definition17,
2998
+ execute: () => execute17
2518
2999
  });
2519
- import ora12 from "ora";
2520
- var definition15 = {
3000
+ import ora13 from "ora";
3001
+ var definition17 = {
2521
3002
  type: "function",
2522
3003
  function: {
2523
3004
  name: "get_diff",
@@ -2525,8 +3006,8 @@ var definition15 = {
2525
3006
  parameters: { type: "object", properties: {}, required: [] }
2526
3007
  }
2527
3008
  };
2528
- async function execute15(_input, _config) {
2529
- const spinner = ora12("Reading git diff...").start();
3009
+ async function execute17(_input, _config) {
3010
+ const spinner = ora13("Reading git diff...").start();
2530
3011
  try {
2531
3012
  const diff = await getDiff();
2532
3013
  spinner.stop();
@@ -2540,14 +3021,14 @@ async function execute15(_input, _config) {
2540
3021
  // src/tools/run-command/index.ts
2541
3022
  var run_command_exports = {};
2542
3023
  __export(run_command_exports, {
2543
- definition: () => definition16,
2544
- execute: () => execute16
3024
+ definition: () => definition18,
3025
+ execute: () => execute18
2545
3026
  });
2546
3027
  import { exec } from "child_process";
2547
3028
  import { promisify } from "util";
2548
- import ora13 from "ora";
3029
+ import ora14 from "ora";
2549
3030
  var execAsync = promisify(exec);
2550
- var definition16 = {
3031
+ var definition18 = {
2551
3032
  type: "function",
2552
3033
  function: {
2553
3034
  name: "run_command",
@@ -2561,10 +3042,10 @@ var definition16 = {
2561
3042
  }
2562
3043
  }
2563
3044
  };
2564
- async function execute16(input3, _config) {
3045
+ async function execute18(input3, _config) {
2565
3046
  const command = input3["command"];
2566
3047
  const cwd = process.cwd();
2567
- const spinner = ora13(`$ ${command}`).start();
3048
+ const spinner = ora14(`$ ${command}`).start();
2568
3049
  try {
2569
3050
  const { stdout, stderr } = await execAsync(command, { cwd, timeout: 6e4, maxBuffer: 1024 * 1024 });
2570
3051
  spinner.stop();
@@ -2583,15 +3064,15 @@ ${out || e.message}`;
2583
3064
  // src/tools/list-files/index.ts
2584
3065
  var list_files_exports = {};
2585
3066
  __export(list_files_exports, {
2586
- definition: () => definition17,
2587
- execute: () => execute17
3067
+ definition: () => definition19,
3068
+ execute: () => execute19
2588
3069
  });
2589
- import { readFile as readFile2 } from "fs/promises";
3070
+ import { readFile as readFile3 } from "fs/promises";
2590
3071
  import { existsSync } from "fs";
2591
3072
  import path2 from "path";
2592
3073
  import { globby } from "globby";
2593
3074
  import ignore from "ignore";
2594
- var definition17 = {
3075
+ var definition19 = {
2595
3076
  type: "function",
2596
3077
  function: {
2597
3078
  name: "list_files",
@@ -2631,13 +3112,13 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2631
3112
  ".sqlite",
2632
3113
  ".lock"
2633
3114
  ]);
2634
- async function execute17(input3, _config) {
3115
+ async function execute19(input3, _config) {
2635
3116
  const glob = input3["glob"] ?? "**/*";
2636
3117
  const cwd = process.cwd();
2637
3118
  const ig = ignore();
2638
3119
  const gitignorePath = path2.join(cwd, ".gitignore");
2639
3120
  if (existsSync(gitignorePath)) {
2640
- ig.add(await readFile2(gitignorePath, "utf-8"));
3121
+ ig.add(await readFile3(gitignorePath, "utf-8"));
2641
3122
  }
2642
3123
  ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
2643
3124
  const files = await globby(glob, { cwd, dot: false, onlyFiles: true, gitignore: false });
@@ -2650,15 +3131,15 @@ ${filtered.join("\n")}`;
2650
3131
  // src/tools/grep-code/index.ts
2651
3132
  var grep_code_exports = {};
2652
3133
  __export(grep_code_exports, {
2653
- definition: () => definition18,
2654
- execute: () => execute18
3134
+ definition: () => definition20,
3135
+ execute: () => execute20
2655
3136
  });
2656
- import { readFile as readFile3 } from "fs/promises";
3137
+ import { readFile as readFile4 } from "fs/promises";
2657
3138
  import { existsSync as existsSync2 } from "fs";
2658
3139
  import path3 from "path";
2659
3140
  import { globby as globby2 } from "globby";
2660
3141
  import ignore2 from "ignore";
2661
- var definition18 = {
3142
+ var definition20 = {
2662
3143
  type: "function",
2663
3144
  function: {
2664
3145
  name: "grep_code",
@@ -2699,7 +3180,7 @@ async function buildIgnore(cwd) {
2699
3180
  const ig = ignore2();
2700
3181
  const gitignorePath = path3.join(cwd, ".gitignore");
2701
3182
  if (existsSync2(gitignorePath)) {
2702
- ig.add(await readFile3(gitignorePath, "utf-8"));
3183
+ ig.add(await readFile4(gitignorePath, "utf-8"));
2703
3184
  }
2704
3185
  ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
2705
3186
  return ig;
@@ -2731,7 +3212,7 @@ function isText(f) {
2731
3212
  return !BINARY_EXTENSIONS2.has(path3.extname(f).toLowerCase());
2732
3213
  }
2733
3214
  var MAX_RANGE_LINES = 300;
2734
- async function execute18(input3, _config) {
3215
+ async function execute20(input3, _config) {
2735
3216
  const pattern = input3["pattern"] ?? "";
2736
3217
  const fileGlob = input3["file_glob"] ?? "**/*";
2737
3218
  const contextLines = Math.min(input3["context_lines"] ?? 2, 5);
@@ -2743,7 +3224,7 @@ async function execute18(input3, _config) {
2743
3224
  const files = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
2744
3225
  if (files.length === 0) return `No file matched: ${fileGlob}`;
2745
3226
  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");
3227
+ const raw = await readFile4(path3.join(cwd, files[0]), "utf-8");
2747
3228
  const lines = raw.split("\n");
2748
3229
  const total = lines.length;
2749
3230
  const from = Math.max(1, startLine);
@@ -2756,146 +3237,512 @@ async function execute18(input3, _config) {
2756
3237
  ${numbered}
2757
3238
  \`\`\`${truncNote}`;
2758
3239
  }
2759
- if (!pattern) return "Provide a `pattern` to search, or `start_line` + `end_line` for read-range mode. Use list_files to browse file paths.";
2760
- const ig = await buildIgnore(cwd);
2761
- let regex;
3240
+ if (!pattern) return "Provide a `pattern` to search, or `start_line` + `end_line` for read-range mode. Use list_files to browse file paths.";
3241
+ const ig = await buildIgnore(cwd);
3242
+ let regex;
3243
+ try {
3244
+ regex = new RegExp(pattern, "i");
3245
+ } catch {
3246
+ regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
3247
+ }
3248
+ const allFiles = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
3249
+ const filtered = allFiles.filter((f) => !ig.ignores(f) && isText(f));
3250
+ const matches = [];
3251
+ let totalMatches = 0;
3252
+ for (const file of filtered) {
3253
+ if (totalMatches >= maxResults) break;
3254
+ let content;
3255
+ try {
3256
+ content = await readFile4(path3.join(cwd, file), "utf-8");
3257
+ } catch {
3258
+ continue;
3259
+ }
3260
+ const lines = content.split("\n");
3261
+ const hitLines = [];
3262
+ for (let i = 0; i < lines.length; i++) {
3263
+ if (regex.test(lines[i])) hitLines.push(i);
3264
+ }
3265
+ if (hitLines.length === 0) continue;
3266
+ const ranges = [];
3267
+ for (const hit of hitLines) {
3268
+ const s = Math.max(0, hit - contextLines);
3269
+ const e = Math.min(lines.length - 1, hit + contextLines);
3270
+ if (ranges.length > 0 && s <= ranges[ranges.length - 1][1] + 1) {
3271
+ ranges[ranges.length - 1][1] = e;
3272
+ } else {
3273
+ ranges.push([s, e]);
3274
+ }
3275
+ }
3276
+ const snippets = [];
3277
+ for (const [s, e] of ranges) {
3278
+ if (totalMatches >= maxResults) break;
3279
+ snippets.push(
3280
+ lines.slice(s, e + 1).map((l, i) => {
3281
+ const n = s + i + 1;
3282
+ return `${regex.test(l) ? ">" : " "} ${String(n).padStart(4)}: ${l}`;
3283
+ }).join("\n")
3284
+ );
3285
+ totalMatches += hitLines.filter((h) => h >= s && h <= e).length;
3286
+ }
3287
+ if (snippets.length > 0) {
3288
+ matches.push(`## ${file}
3289
+ \`\`\`
3290
+ ${snippets.join("\n---\n")}
3291
+ \`\`\``);
3292
+ }
3293
+ }
3294
+ if (matches.length === 0) return `No matches found for: ${pattern}`;
3295
+ const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
3296
+ return [header, ...matches].join("\n\n");
3297
+ }
3298
+
3299
+ // src/tools/ask-user/index.ts
3300
+ var ask_user_exports = {};
3301
+ __export(ask_user_exports, {
3302
+ definition: () => definition21,
3303
+ execute: () => execute21
3304
+ });
3305
+ import chalk12 from "chalk";
3306
+ import { select as select11, input as promptInput5 } from "@inquirer/prompts";
3307
+ var definition21 = {
3308
+ type: "function",
3309
+ function: {
3310
+ name: "ask_user",
3311
+ 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.",
3312
+ parameters: {
3313
+ type: "object",
3314
+ properties: {
3315
+ question: { type: "string", description: "The question to ask the user" },
3316
+ options: {
3317
+ type: "array",
3318
+ items: { type: "string" },
3319
+ description: "2\u20134 concrete answer choices"
3320
+ }
3321
+ },
3322
+ required: ["question", "options"]
3323
+ }
3324
+ }
3325
+ };
3326
+ async function execute21(input3, _config) {
3327
+ const question = input3["question"];
3328
+ const options = input3["options"];
3329
+ const OTHER = "__other__";
3330
+ console.log("");
3331
+ console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
3332
+ console.log(chalk12.dim(" \u2502"));
3333
+ for (const line of question.split("\n")) {
3334
+ console.log(chalk12.dim(" \u2502 ") + line);
3335
+ }
3336
+ console.log(chalk12.dim(" \u2514" + "\u2500".repeat(67)));
3337
+ let answer;
3338
+ try {
3339
+ const chosen = await select11({
3340
+ message: " ",
3341
+ choices: [
3342
+ ...options.map((o) => ({ name: o, value: o })),
3343
+ { name: chalk12.dim("Other (describe below)"), value: OTHER }
3344
+ ]
3345
+ });
3346
+ answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
3347
+ } catch {
3348
+ answer = "User skipped this question \u2014 use your best judgement.";
3349
+ }
3350
+ console.log("");
3351
+ return answer;
3352
+ }
3353
+
3354
+ // src/tools/registry.ts
3355
+ var toolModules = [
3356
+ // Command tools
3357
+ pick_exports,
3358
+ new_task_exports,
3359
+ close_exports,
3360
+ submit_exports,
3361
+ my_status_exports,
3362
+ review_exports,
3363
+ refresh_exports,
3364
+ open_code_exports,
3365
+ reject_exports,
3366
+ accept_exports,
3367
+ edit_task_exports,
3368
+ move_task_exports,
3369
+ wiki_exports,
3370
+ // Low-level tools
3371
+ list_tasks_exports,
3372
+ get_task_exports,
3373
+ get_comments_exports,
3374
+ get_diff_exports,
3375
+ run_command_exports,
3376
+ list_files_exports,
3377
+ grep_code_exports,
3378
+ ask_user_exports
3379
+ ];
3380
+
3381
+ // src/lib/agent-ui.ts
3382
+ import chalk13 from "chalk";
3383
+ function formatInput(input3) {
3384
+ return Object.entries(input3).map(([k, v]) => {
3385
+ if (typeof v === "number") return `${k}=${v}`;
3386
+ if (typeof v === "string") {
3387
+ if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
3388
+ return `${k}="${v}"`;
3389
+ }
3390
+ return `${k}=${JSON.stringify(v)}`;
3391
+ }).join(" ");
3392
+ }
3393
+ function summarize(result) {
3394
+ const first = result.split("\n").find((l) => l.trim()) ?? result;
3395
+ return first.length > 100 ? first.slice(0, 97) + "..." : first;
3396
+ }
3397
+ function printToolCall(name, input3) {
3398
+ const params = formatInput(input3);
3399
+ console.log(` ${chalk13.cyan("\u2192")} ${chalk13.bold(name)}${params ? " " + chalk13.dim(params) : ""}`);
3400
+ }
3401
+ function printToolResult(result) {
3402
+ const ok = !result.startsWith("Error:");
3403
+ const icon = ok ? chalk13.green("\u2713") : chalk13.red("\u2717");
3404
+ console.log(` ${icon} ${chalk13.dim(summarize(result))}`);
3405
+ }
3406
+
3407
+ // src/lib/sub-agent.ts
3408
+ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
3409
+ const client = createClient(config);
3410
+ const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
3411
+ const tools2 = selected.map((m) => m.definition);
3412
+ const messages = [
3413
+ { role: "system", content: systemPrompt },
3414
+ { role: "user", content: userMessage }
3415
+ ];
3416
+ const MAX_ITERATIONS = 100;
3417
+ let iterations = 0;
3418
+ for (; ; ) {
3419
+ if (++iterations > MAX_ITERATIONS) {
3420
+ throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
3421
+ }
3422
+ const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
3423
+ const choice = res.choices[0];
3424
+ messages.push({
3425
+ role: "assistant",
3426
+ content: choice.message.content ?? null,
3427
+ ...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
3428
+ });
3429
+ if (choice.finish_reason === "stop") {
3430
+ return choice.message.content ?? "";
3431
+ }
3432
+ if (choice.finish_reason === "tool_calls") {
3433
+ for (const tc of choice.message.tool_calls ?? []) {
3434
+ let input3;
3435
+ try {
3436
+ input3 = JSON.parse(tc.function.arguments);
3437
+ } catch {
3438
+ input3 = {};
3439
+ }
3440
+ printToolCall(tc.function.name, input3);
3441
+ const mod = selected.find((m) => m.definition.function.name === tc.function.name);
3442
+ const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
3443
+ printToolResult(result);
3444
+ messages.push({ role: "tool", tool_call_id: tc.id, content: result });
3445
+ }
3446
+ }
3447
+ }
3448
+ }
3449
+
3450
+ // src/tools/wiki/prompts.ts
3451
+ var WIKI_FORMAT = `
3452
+ The document you produce must be valid Markdown with these exact sections:
3453
+
3454
+ # [Project Name]
3455
+
3456
+ > One-sentence description of what this project does.
3457
+
3458
+ ## What Is This?
3459
+
3460
+ 2-4 paragraphs covering:
3461
+ - The problem this project solves
3462
+ - Who uses it and in what context
3463
+ - Core capabilities / key features
3464
+
3465
+ ## Quick Start
3466
+
3467
+ Numbered steps for a brand-new developer to install, configure, and run the project for the first time.
3468
+
3469
+ ## Architecture
3470
+
3471
+ High-level explanation of how the system is structured:
3472
+ - Key components / layers and their responsibilities
3473
+ - Request or data flow (prose or ASCII diagram)
3474
+ - Noteworthy design decisions
3475
+
3476
+ ## Key Files
3477
+
3478
+ | File / Directory | Purpose |
3479
+ |---|---|
3480
+ | ... | ... |
3481
+
3482
+ (List the 8-15 most important files.)
3483
+
3484
+ ## Development Workflow
3485
+
3486
+ Common day-to-day tasks a contributor will need:
3487
+ - How to build / run locally
3488
+ - How to add a new feature (brief steps)
3489
+ - Any testing or linting commands
3490
+
3491
+ ---
3492
+ *Maintained by Techunter \u2014 run \`tch wiki\` to regenerate*
3493
+ `;
3494
+
3495
+ // src/tools/wiki/wiki-generator.ts
3496
+ async function generateWiki(config) {
3497
+ return runSubAgentLoop(
3498
+ config,
3499
+ "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,
3500
+ "Analyze this project thoroughly and produce a comprehensive TECHUNTER.md overview document for new team members.",
3501
+ ["list_files", "grep_code", "run_command"]
3502
+ );
3503
+ }
3504
+
3505
+ // src/commands/init.ts
3506
+ async function getGitHubTokenViaPAT() {
3507
+ console.log(chalk14.dim("\n Create a token at: https://github.com/settings/tokens/new"));
3508
+ console.log(chalk14.dim(" Required scopes: repo, read:user\n"));
3509
+ const token = await password({
3510
+ message: "GitHub Personal Access Token:",
3511
+ mask: "*"
3512
+ });
3513
+ return { token: token.trim() };
3514
+ }
3515
+ var OAUTH_CLIENT_ID = "Ov23liW4zJ4r2RdZOsCJ";
3516
+ async function getGitHubTokenViaDeviceFlow() {
3517
+ let verificationUri = "";
3518
+ let userCode = "";
3519
+ const auth = createOAuthDeviceAuth({
3520
+ clientType: "oauth-app",
3521
+ clientId: OAUTH_CLIENT_ID,
3522
+ scopes: ["repo"],
3523
+ onVerification(verification) {
3524
+ verificationUri = verification.verification_uri;
3525
+ userCode = verification.user_code;
3526
+ console.log("");
3527
+ console.log(chalk14.bold(" 1. Open this URL in your browser:"));
3528
+ console.log(" " + chalk14.cyan(verificationUri));
3529
+ console.log("");
3530
+ console.log(chalk14.bold(" 2. Enter this code:"));
3531
+ console.log(" " + chalk14.yellow.bold(userCode));
3532
+ console.log("");
3533
+ open2(verificationUri).catch(() => {
3534
+ });
3535
+ }
3536
+ });
3537
+ const spinner = ora15("Waiting for authorization in browser...").start();
3538
+ let token;
3539
+ try {
3540
+ const result = await auth({ type: "oauth" });
3541
+ token = result.token;
3542
+ spinner.succeed("Authorized!");
3543
+ } catch (err) {
3544
+ spinner.fail("Authorization failed");
3545
+ throw err;
3546
+ }
3547
+ return { token, clientId: OAUTH_CLIENT_ID };
3548
+ }
3549
+ async function initCommand() {
3550
+ console.log(chalk14.bold.cyan("\nTechunter \u2014 Initial Setup\n"));
3551
+ let detectedOwner = "";
3552
+ let detectedRepo = "";
3553
+ const remoteUrl = await getRemoteUrl();
3554
+ if (remoteUrl) {
3555
+ const parsed = parseOwnerRepo(remoteUrl);
3556
+ if (parsed) {
3557
+ detectedOwner = parsed.owner;
3558
+ detectedRepo = parsed.repo;
3559
+ console.log(chalk14.dim(`Detected GitHub repo: ${detectedOwner}/${detectedRepo}
3560
+ `));
3561
+ }
3562
+ }
3563
+ const authMethod = await select12({
3564
+ message: "How would you like to authenticate with GitHub?",
3565
+ choices: [
3566
+ {
3567
+ name: "Browser login (OAuth) \u2014 open a URL and click Authorize",
3568
+ value: "device"
3569
+ },
3570
+ {
3571
+ name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
3572
+ value: "pat"
3573
+ }
3574
+ ]
3575
+ });
3576
+ let githubToken;
3577
+ let githubClientId;
3578
+ if (authMethod === "device") {
3579
+ const result = await getGitHubTokenViaDeviceFlow();
3580
+ githubToken = result.token;
3581
+ githubClientId = result.clientId;
3582
+ } else {
3583
+ const result = await getGitHubTokenViaPAT();
3584
+ githubToken = result.token;
3585
+ }
3586
+ const providerChoice = await select12({
3587
+ message: "AI provider:",
3588
+ choices: [
3589
+ { name: `OpenRouter (recommended) ${chalk14.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
3590
+ { name: "Custom (specify base URL and model)", value: "custom" }
3591
+ ]
3592
+ });
3593
+ let aiBaseUrl;
3594
+ let aiModel;
3595
+ if (providerChoice === "custom") {
3596
+ aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
3597
+ aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
3598
+ }
3599
+ const apiKeyHint = providerChoice === "openrouter" ? chalk14.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk14.dim(" API key for your provider\n");
3600
+ console.log(apiKeyHint);
3601
+ const aiApiKey = await password({
3602
+ message: "API Key:",
3603
+ mask: "*"
3604
+ });
3605
+ let owner = detectedOwner;
3606
+ let repo = detectedRepo;
3607
+ if (!owner || !repo) {
3608
+ owner = await input({
3609
+ message: "GitHub repo owner (user or org):",
3610
+ required: true
3611
+ });
3612
+ repo = await input({
3613
+ message: "GitHub repo name:",
3614
+ required: true
3615
+ });
3616
+ }
3617
+ const config = {
3618
+ githubToken,
3619
+ githubClientId,
3620
+ aiApiKey: aiApiKey.trim(),
3621
+ ...aiBaseUrl ? { aiBaseUrl } : {},
3622
+ ...aiModel ? { aiModel } : {},
3623
+ github: {
3624
+ owner: owner.trim(),
3625
+ repo: repo.trim()
3626
+ }
3627
+ };
3628
+ setConfig(config);
3629
+ const spinner = ora15("Setting up GitHub labels...").start();
2762
3630
  try {
2763
- regex = new RegExp(pattern, "i");
3631
+ await ensureLabels(config);
3632
+ spinner.succeed("GitHub labels created");
3633
+ } catch (err) {
3634
+ spinner.fail("Failed to create labels (check token permissions)");
3635
+ console.error(chalk14.red(String(err)));
3636
+ }
3637
+ console.log(chalk14.green("\nSetup complete!"));
3638
+ console.log(chalk14.dim(`Config saved to: ${getConfigPath()}
3639
+ `));
3640
+ let genWiki = false;
3641
+ try {
3642
+ genWiki = await select12({
3643
+ message: "Generate TECHUNTER.md project overview for new team members?",
3644
+ choices: [
3645
+ { name: "Yes, generate now", value: true },
3646
+ { name: "No, skip (run /wiki later)", value: false }
3647
+ ]
3648
+ });
2764
3649
  } catch {
2765
- regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
2766
3650
  }
2767
- const allFiles = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
2768
- const filtered = allFiles.filter((f) => !ig.ignores(f) && isText(f));
2769
- const matches = [];
2770
- let totalMatches = 0;
2771
- for (const file of filtered) {
2772
- if (totalMatches >= maxResults) break;
2773
- let content;
3651
+ if (genWiki) {
3652
+ const wikiSpinner = ora15("Analyzing project and generating TECHUNTER.md\u2026").start();
2774
3653
  try {
2775
- content = await readFile3(path3.join(cwd, file), "utf-8");
2776
- } catch {
2777
- continue;
2778
- }
2779
- const lines = content.split("\n");
2780
- const hitLines = [];
2781
- for (let i = 0; i < lines.length; i++) {
2782
- if (regex.test(lines[i])) hitLines.push(i);
2783
- }
2784
- if (hitLines.length === 0) continue;
2785
- const ranges = [];
2786
- for (const hit of hitLines) {
2787
- const s = Math.max(0, hit - contextLines);
2788
- const e = Math.min(lines.length - 1, hit + contextLines);
2789
- if (ranges.length > 0 && s <= ranges[ranges.length - 1][1] + 1) {
2790
- ranges[ranges.length - 1][1] = e;
2791
- } else {
2792
- ranges.push([s, e]);
2793
- }
2794
- }
2795
- const snippets = [];
2796
- for (const [s, e] of ranges) {
2797
- if (totalMatches >= maxResults) break;
2798
- snippets.push(
2799
- lines.slice(s, e + 1).map((l, i) => {
2800
- const n = s + i + 1;
2801
- return `${regex.test(l) ? ">" : " "} ${String(n).padStart(4)}: ${l}`;
2802
- }).join("\n")
2803
- );
2804
- totalMatches += hitLines.filter((h) => h >= s && h <= e).length;
2805
- }
2806
- if (snippets.length > 0) {
2807
- matches.push(`## ${file}
2808
- \`\`\`
2809
- ${snippets.join("\n---\n")}
2810
- \`\`\``);
3654
+ const content = await generateWiki(config);
3655
+ await upsertRepoFile(config, "TECHUNTER.md", content, "docs: add TECHUNTER.md project overview");
3656
+ wikiSpinner.succeed("TECHUNTER.md created");
3657
+ } catch (err) {
3658
+ wikiSpinner.fail(`Could not generate wiki: ${err.message}`);
2811
3659
  }
3660
+ console.log("");
2812
3661
  }
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");
2816
3662
  }
2817
3663
 
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"]
2842
- }
2843
- }
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);
2854
- }
2855
- console.log(chalk12.dim(" \u2514" + "\u2500".repeat(67)));
2856
- let answer;
3664
+ // src/commands/config.ts
3665
+ import { input as input2, password as password2, select as select13 } from "@inquirer/prompts";
3666
+ import chalk15 from "chalk";
3667
+ async function configCommand() {
3668
+ let config;
2857
3669
  try {
2858
- const chosen = await select10({
2859
- message: " ",
2860
- choices: [
2861
- ...options.map((o) => ({ name: o, value: o })),
2862
- { name: chalk12.dim("Other (describe below)"), value: OTHER }
2863
- ]
2864
- });
2865
- answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
3670
+ config = getConfig();
2866
3671
  } catch {
2867
- answer = "User skipped this question \u2014 use your best judgement.";
3672
+ console.error(chalk15.red("No config found. Run `tch init` first."));
3673
+ process.exit(1);
3674
+ }
3675
+ console.log(chalk15.bold.cyan("\nTechunter \u2014 Settings\n"));
3676
+ console.log(chalk15.dim(`Config file: ${getConfigPath()}
3677
+ `));
3678
+ const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
3679
+ const currentModel = config.aiModel ?? DEFAULT_MODEL;
3680
+ const currentBaseBranch = config.baseBranch ?? "main";
3681
+ const field = await select13({
3682
+ message: "Which setting to change?",
3683
+ choices: [
3684
+ { name: `GitHub repo ${chalk15.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
3685
+ { name: `Base branch ${chalk15.dim(currentBaseBranch)}`, value: "baseBranch" },
3686
+ { name: `AI base URL ${chalk15.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
3687
+ { name: `AI model ${chalk15.dim(currentModel)}`, value: "aiModel" },
3688
+ { name: `AI API Key ${chalk15.dim("(hidden)")}`, value: "aiApiKey" },
3689
+ { name: `GitHub Token ${chalk15.dim("(hidden)")}`, value: "githubToken" },
3690
+ { name: "Cancel", value: "cancel" }
3691
+ ]
3692
+ });
3693
+ if (field === "cancel") return;
3694
+ if (field === "baseBranch") {
3695
+ const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
3696
+ if (val.trim()) {
3697
+ setConfig({ baseBranch: val.trim() });
3698
+ console.log(chalk15.green(`
3699
+ Base branch set to: ${val.trim()}
3700
+ `));
3701
+ }
3702
+ } else if (field === "repo") {
3703
+ const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
3704
+ const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
3705
+ setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
3706
+ console.log(chalk15.green(`
3707
+ Repo set to: ${owner.trim()}/${repo.trim()}
3708
+ `));
3709
+ } else if (field === "aiBaseUrl") {
3710
+ const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
3711
+ if (val.trim()) {
3712
+ setConfig({ aiBaseUrl: val.trim() });
3713
+ console.log(chalk15.green(`
3714
+ AI base URL set to: ${val.trim()}
3715
+ `));
3716
+ }
3717
+ } else if (field === "aiModel") {
3718
+ const val = await input2({ message: "AI model name:", default: currentModel });
3719
+ if (val.trim()) {
3720
+ setConfig({ aiModel: val.trim() });
3721
+ console.log(chalk15.green(`
3722
+ AI model set to: ${val.trim()}
3723
+ `));
3724
+ }
3725
+ } else if (field === "aiApiKey") {
3726
+ const val = await password2({ message: "New AI API Key:", mask: "*" });
3727
+ if (val.trim()) {
3728
+ setConfig({ aiApiKey: val.trim() });
3729
+ console.log(chalk15.green("\nAI API Key updated.\n"));
3730
+ }
3731
+ } else if (field === "githubToken") {
3732
+ const val = await password2({ message: "New GitHub Token:", mask: "*" });
3733
+ if (val.trim()) {
3734
+ setConfig({ githubToken: val.trim() });
3735
+ console.log(chalk15.green("\nGitHub Token updated.\n"));
3736
+ }
2868
3737
  }
2869
- console.log("");
2870
- return answer;
2871
3738
  }
2872
3739
 
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
- ];
3740
+ // src/index.ts
3741
+ init_github();
2897
3742
 
2898
3743
  // src/lib/agent.ts
3744
+ import ora16 from "ora";
3745
+ import chalk16 from "chalk";
2899
3746
  var tools = toolModules.map((m) => m.definition);
2900
3747
  var HISTORY_KEEP_TURNS = 8;
2901
3748
  function trimHistory(messages) {
@@ -2981,7 +3828,7 @@ async function runAgentLoop(config, messages) {
2981
3828
  throw new Error(`Agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
2982
3829
  }
2983
3830
  trimHistory(messages);
2984
- const spinner = ora14({ text: chalk13.dim("Thinking\u2026"), color: "cyan" }).start();
3831
+ const spinner = ora16({ text: chalk16.dim("Thinking\u2026"), color: "cyan" }).start();
2985
3832
  let response;
2986
3833
  try {
2987
3834
  response = await client.chat.completions.create({
@@ -3026,7 +3873,7 @@ async function runAgentLoop(config, messages) {
3026
3873
  return executeTool(tc.function.name, parsed, config);
3027
3874
  })
3028
3875
  );
3029
- let terminal12 = false;
3876
+ let terminal14 = false;
3030
3877
  for (let i = 0; i < toolCalls.length; i++) {
3031
3878
  printToolResult(results[i]);
3032
3879
  messages.push({
@@ -3035,16 +3882,93 @@ async function runAgentLoop(config, messages) {
3035
3882
  content: results[i]
3036
3883
  });
3037
3884
  if (toolModules.find((m) => m.definition.function.name === toolCalls[i].function.name)?.terminal) {
3038
- terminal12 = true;
3885
+ terminal14 = true;
3039
3886
  }
3040
3887
  }
3041
- if (terminal12) return results[results.length - 1];
3888
+ if (terminal14) return results[results.length - 1];
3042
3889
  } else {
3043
3890
  return choice.message.content ?? "";
3044
3891
  }
3045
3892
  }
3046
3893
  }
3047
3894
 
3895
+ // src/lib/update-check.ts
3896
+ import Conf2 from "conf";
3897
+ import chalk17 from "chalk";
3898
+ import { execFile } from "child_process";
3899
+ var PACKAGE_NAME = "techunter";
3900
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3901
+ var cache = new Conf2({
3902
+ projectName: "techunter-update-cache",
3903
+ defaults: { lastChecked: 0, latestVersion: "" }
3904
+ });
3905
+ async function fetchLatestVersion() {
3906
+ try {
3907
+ const { fetch } = await import("undici");
3908
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
3909
+ signal: AbortSignal.timeout(5e3)
3910
+ });
3911
+ if (!res.ok) return null;
3912
+ const data = await res.json();
3913
+ return data.version ?? null;
3914
+ } catch {
3915
+ return null;
3916
+ }
3917
+ }
3918
+ function isNewer(latest, current) {
3919
+ const parse = (v) => v.split(".").map(Number);
3920
+ const [la, lb, lc] = parse(latest);
3921
+ const [ca, cb, cc] = parse(current);
3922
+ if (la !== ca) return la > ca;
3923
+ if (lb !== cb) return lb > cb;
3924
+ return lc > cc;
3925
+ }
3926
+ async function getAvailableUpdate(currentVersion) {
3927
+ const now = Date.now();
3928
+ const lastChecked = cache.get("lastChecked");
3929
+ let latest = cache.get("latestVersion");
3930
+ if (!latest || now - lastChecked > CHECK_INTERVAL_MS) {
3931
+ const fetched = await fetchLatestVersion();
3932
+ if (fetched) {
3933
+ latest = fetched;
3934
+ cache.set("latestVersion", fetched);
3935
+ cache.set("lastChecked", now);
3936
+ }
3937
+ }
3938
+ return latest && isNewer(latest, currentVersion) ? latest : null;
3939
+ }
3940
+ function installUpdate() {
3941
+ return new Promise((resolve, reject) => {
3942
+ execFile("npm", ["install", "-g", PACKAGE_NAME], { shell: true }, (err, _stdout, stderr) => {
3943
+ if (err) {
3944
+ reject(new Error(stderr.trim() || err.message));
3945
+ } else {
3946
+ resolve(cache.get("latestVersion"));
3947
+ }
3948
+ });
3949
+ });
3950
+ }
3951
+ async function startAutoUpdate(currentVersion) {
3952
+ const latest = await Promise.race([
3953
+ getAvailableUpdate(currentVersion),
3954
+ new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
3955
+ ]);
3956
+ if (!latest) return;
3957
+ console.log(
3958
+ chalk17.cyan("\n \u2191 Auto-updating to v" + latest + "\u2026") + chalk17.dim(" (running in background)\n")
3959
+ );
3960
+ installUpdate().then((installedVersion) => {
3961
+ console.log(
3962
+ "\n" + chalk17.green(" \u2714 Updated to v" + (installedVersion || latest)) + chalk17.dim(" \u2014 restart tch to use the new version\n") + chalk17.cyan(" You \u203A ")
3963
+ // redraw the prompt hint
3964
+ );
3965
+ }).catch((err) => {
3966
+ console.log(
3967
+ "\n" + chalk17.red(" \u2718 Auto-update failed: ") + chalk17.dim(err.message) + "\n" + chalk17.dim(" Run manually: npm install -g techunter\n") + chalk17.cyan(" You \u203A ")
3968
+ );
3969
+ });
3970
+ }
3971
+
3048
3972
  // src/index.ts
3049
3973
  var _require = createRequire(import.meta.url);
3050
3974
  var { version } = _require("../package.json");
@@ -3063,6 +3987,8 @@ var SLASH_NAMES = [
3063
3987
  "/d",
3064
3988
  "/edit",
3065
3989
  "/e",
3990
+ "/move",
3991
+ "/mv",
3066
3992
  "/review",
3067
3993
  "/rv",
3068
3994
  "/accept",
@@ -3071,6 +3997,8 @@ var SLASH_NAMES = [
3071
3997
  "/me",
3072
3998
  "/code",
3073
3999
  "/c",
4000
+ "/wiki",
4001
+ "/w",
3074
4002
  "/config",
3075
4003
  "/cfg",
3076
4004
  "/init"
@@ -3086,8 +4014,14 @@ var _rl = null;
3086
4014
  function promptUser() {
3087
4015
  return new Promise((resolve) => {
3088
4016
  if (process.stdin.isPaused()) process.stdin.resume();
4017
+ if (process.stdin.isTTY) {
4018
+ try {
4019
+ process.stdin.setRawMode(true);
4020
+ } catch {
4021
+ }
4022
+ }
3089
4023
  _rl.resume();
3090
- _rl.question(chalk14.cyan("You") + chalk14.dim(" \u203A "), resolve);
4024
+ _rl.question(chalk18.cyan("You") + chalk18.dim(" \u203A "), resolve);
3091
4025
  });
3092
4026
  }
3093
4027
  var COMMANDS = [
@@ -3097,50 +4031,52 @@ var COMMANDS = [
3097
4031
  { cmd: "/new", alias: "/n", desc: "Create a new task interactively" },
3098
4032
  { cmd: "/close", alias: "/d", desc: "Close (delete) a task" },
3099
4033
  { cmd: "/edit", alias: "/e", desc: "Edit the title or description of a task" },
4034
+ { cmd: "/move", alias: "/mv", desc: "Move your task under another task as a sub-task" },
3100
4035
  { cmd: "/submit", alias: "/s", desc: "Commit, create PR, and mark in-review" },
3101
4036
  { cmd: "/review", alias: "/rv", desc: "List tasks waiting for your approval" },
3102
4037
  { cmd: "/accept", alias: "/ac", desc: "Accept a reviewed task: merge PR and close issue" },
3103
4038
  { cmd: "/config", alias: "/cfg", desc: "Change settings (branch, repo, API keys)" },
3104
4039
  { cmd: "/init", desc: "Re-run setup wizard for this repo" },
3105
4040
  { cmd: "/status", alias: "/me", desc: "Show tasks assigned to you" },
3106
- { cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" }
4041
+ { cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" },
4042
+ { cmd: "/wiki", alias: "/w", desc: "Generate or refresh TECHUNTER.md project overview" }
3107
4043
  ];
3108
4044
  function cmdHelp() {
3109
4045
  console.log("");
3110
- console.log(chalk14.bold(" Commands"));
3111
- console.log(chalk14.dim(" \u2500".repeat(35)));
4046
+ console.log(chalk18.bold(" Commands"));
4047
+ console.log(chalk18.dim(" \u2500".repeat(35)));
3112
4048
  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));
4049
+ const left = (cmd + (alias ? ` ${chalk18.dim(alias)}` : "")).padEnd(22);
4050
+ console.log(` ${chalk18.cyan(cmd)}${alias ? " " + chalk18.dim(alias) : ""}`.padEnd(30) + chalk18.dim(desc));
3115
4051
  }
3116
- console.log(chalk14.dim("\n Anything else is sent to the AI agent.\n"));
4052
+ console.log(chalk18.dim("\n Anything else is sent to the AI agent.\n"));
3117
4053
  }
3118
4054
  function printBanner(config) {
3119
4055
  const { owner, repo } = config.github;
3120
- const g = chalk14.cyan;
3121
- const b = chalk14.bold.white;
3122
- const p = chalk14.yellow.bold;
4056
+ const g = chalk18.cyan;
4057
+ const b = chalk18.bold.white;
4058
+ const p = chalk18.yellow.bold;
3123
4059
  console.log("");
3124
4060
  console.log(" " + g("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
3125
4061
  console.log(p("\u25C6") + b("\u2550\u2550\u2550") + g("\u256C") + b(" TECHUNTER ") + g("\u256C") + b("\u2550\u2550\u2550\u25B6"));
3126
4062
  console.log(" " + g("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
3127
4063
  console.log("");
3128
4064
  console.log(
3129
- " " + chalk14.bold("Techunter") + chalk14.dim(` v${version}`) + chalk14.dim(" \xB7 ") + chalk14.cyan(getModel(config)) + chalk14.dim(" \xB7 ") + chalk14.dim(`${owner}/${repo}`)
4065
+ " " + chalk18.bold("Techunter") + chalk18.dim(` v${version}`) + chalk18.dim(" \xB7 ") + chalk18.cyan(getModel(config)) + chalk18.dim(" \xB7 ") + chalk18.dim(`${owner}/${repo}`)
3130
4066
  );
3131
4067
  console.log("");
3132
4068
  }
3133
4069
  async function initNewRepo(config, owner, repo) {
3134
4070
  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"));
4071
+ console.log(chalk18.bold.cyan(` New repo detected: ${owner}/${repo}`));
4072
+ console.log(chalk18.dim(" Setting up Techunter for this repository...\n"));
3137
4073
  const newConfig = {
3138
4074
  ...config,
3139
4075
  github: { owner, repo }
3140
4076
  };
3141
4077
  setConfig({ github: newConfig.github });
3142
- const ora15 = (await import("ora")).default;
3143
- const spinner = ora15("Creating Techunter labels...").start();
4078
+ const ora17 = (await import("ora")).default;
4079
+ const spinner = ora17("Creating Techunter labels...").start();
3144
4080
  try {
3145
4081
  await ensureLabels(newConfig);
3146
4082
  spinner.succeed("Labels ready");
@@ -3156,7 +4092,7 @@ async function main() {
3156
4092
  try {
3157
4093
  await configCommand();
3158
4094
  } catch (err) {
3159
- console.error(chalk14.red(`
4095
+ console.error(chalk18.red(`
3160
4096
  Error: ${err.message}`));
3161
4097
  process.exit(1);
3162
4098
  }
@@ -3170,7 +4106,7 @@ Error: ${err.message}`));
3170
4106
  await initCommand();
3171
4107
  config = getConfig();
3172
4108
  } catch (err) {
3173
- console.error(chalk14.red(`
4109
+ console.error(chalk18.red(`
3174
4110
  Setup failed: ${err.message}`));
3175
4111
  process.exit(1);
3176
4112
  return;
@@ -3188,11 +4124,13 @@ Setup failed: ${err.message}`));
3188
4124
  }
3189
4125
  }
3190
4126
  } else if (!config.github.owner) {
3191
- console.error(chalk14.red("\nNo git remote found and no repo configured. Run tch init."));
4127
+ console.error(chalk18.red("\nNo git remote found and no repo configured. Run tch init."));
3192
4128
  process.exit(1);
3193
4129
  }
3194
4130
  printBanner(config);
3195
- console.log(chalk14.dim(" Type /help for commands, or describe what you want.\n"));
4131
+ console.log(chalk18.dim(" Type /help for commands, or describe what you want.\n"));
4132
+ startAutoUpdate(version).catch(() => {
4133
+ });
3196
4134
  await printTaskList(config);
3197
4135
  await printMyTasks(config);
3198
4136
  _rl = readline.createInterface({
@@ -3202,11 +4140,11 @@ Setup failed: ${err.message}`));
3202
4140
  terminal: true
3203
4141
  });
3204
4142
  _rl.on("close", () => {
3205
- console.log(chalk14.gray("\nGoodbye!"));
4143
+ console.log(chalk18.gray("\nGoodbye!"));
3206
4144
  process.exit(0);
3207
4145
  });
3208
4146
  _rl.on("SIGINT", () => {
3209
- console.log(chalk14.gray("\nGoodbye!"));
4147
+ console.log(chalk18.gray("\nGoodbye!"));
3210
4148
  process.exit(0);
3211
4149
  });
3212
4150
  const messages = [];
@@ -3223,7 +4161,7 @@ Setup failed: ${err.message}`));
3223
4161
  break;
3224
4162
  case "/refresh":
3225
4163
  case "/r":
3226
- await run7({}, config);
4164
+ await run9({}, config);
3227
4165
  break;
3228
4166
  case "/pick":
3229
4167
  case "/p": {
@@ -3231,7 +4169,7 @@ Setup failed: ${err.message}`));
3231
4169
  const preselected = arg ? parseInt(arg, 10) : void 0;
3232
4170
  const result = await run3({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
3233
4171
  if (result && result !== "Cancelled.") {
3234
- console.log(chalk14.green(`
4172
+ console.log(chalk18.green(`
3235
4173
  ${result}
3236
4174
  `));
3237
4175
  }
@@ -3241,7 +4179,7 @@ Setup failed: ${err.message}`));
3241
4179
  case "/new":
3242
4180
  case "/n": {
3243
4181
  const result = await run4({}, config);
3244
- console.log(chalk14.green(`
4182
+ console.log(chalk18.green(`
3245
4183
  ${result}
3246
4184
  `));
3247
4185
  await printTaskList(config);
@@ -3259,7 +4197,18 @@ Setup failed: ${err.message}`));
3259
4197
  const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
3260
4198
  const preselected = arg ? parseInt(arg, 10) : void 0;
3261
4199
  const result = await run11({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
3262
- console.log(chalk14.green(`
4200
+ console.log(chalk18.green(`
4201
+ ${result}
4202
+ `));
4203
+ await printTaskList(config);
4204
+ break;
4205
+ }
4206
+ case "/move":
4207
+ case "/mv": {
4208
+ const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
4209
+ const preselected = arg ? parseInt(arg, 10) : void 0;
4210
+ const result = await run12({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
4211
+ console.log(chalk18.green(`
3263
4212
  ${result}
3264
4213
  `));
3265
4214
  await printTaskList(config);
@@ -3268,7 +4217,7 @@ Setup failed: ${err.message}`));
3268
4217
  case "/close":
3269
4218
  case "/d": {
3270
4219
  const result = await run2({}, config);
3271
- console.log(chalk14.green(`
4220
+ console.log(chalk18.green(`
3272
4221
  ${result}
3273
4222
  `));
3274
4223
  await printTaskList(config);
@@ -3276,7 +4225,7 @@ Setup failed: ${err.message}`));
3276
4225
  }
3277
4226
  case "/review":
3278
4227
  case "/rv": {
3279
- const result = await run6({}, config);
4228
+ const result = await run8({}, config);
3280
4229
  console.log("\n" + renderMarkdown(result));
3281
4230
  break;
3282
4231
  }
@@ -3290,8 +4239,8 @@ Setup failed: ${err.message}`));
3290
4239
  case "/ac": {
3291
4240
  const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
3292
4241
  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(`
4242
+ const result = await run6({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
4243
+ console.log(chalk18.green(`
3295
4244
  ${result}
3296
4245
  `));
3297
4246
  await printTaskList(config);
@@ -3307,17 +4256,25 @@ Setup failed: ${err.message}`));
3307
4256
  config = getConfig();
3308
4257
  await printTaskList(config);
3309
4258
  } catch (err) {
3310
- console.error(chalk14.red(`
4259
+ console.error(chalk18.red(`
3311
4260
  Init failed: ${err.message}
3312
4261
  `));
3313
4262
  }
3314
4263
  break;
3315
4264
  case "/code":
3316
4265
  case "/c":
3317
- await run8({}, config);
4266
+ await run10({}, config);
3318
4267
  break;
4268
+ case "/wiki":
4269
+ case "/w": {
4270
+ const result = await run13({}, config);
4271
+ console.log(chalk18.green(`
4272
+ ${result}
4273
+ `));
4274
+ break;
4275
+ }
3319
4276
  default:
3320
- console.log(chalk14.yellow(` Unknown command: ${cmd} (try /help)`));
4277
+ console.log(chalk18.yellow(` Unknown command: ${cmd} (try /help)`));
3321
4278
  }
3322
4279
  continue;
3323
4280
  }
@@ -3325,10 +4282,10 @@ Init failed: ${err.message}
3325
4282
  messages.push({ role: "user", content: userInput });
3326
4283
  try {
3327
4284
  const response = await runAgentLoop(config, messages);
3328
- console.log("\n" + chalk14.green("Techunter:") + "\n" + renderMarkdown(response));
4285
+ console.log("\n" + chalk18.green("Techunter:") + "\n" + renderMarkdown(response));
3329
4286
  } catch (err) {
3330
4287
  messages.splice(prevLength);
3331
- console.error(chalk14.red(`
4288
+ console.error(chalk18.red(`
3332
4289
  Error: ${err.message}
3333
4290
  `));
3334
4291
  }
@@ -3336,6 +4293,6 @@ Error: ${err.message}
3336
4293
  }
3337
4294
  }
3338
4295
  main().catch((err) => {
3339
- console.error(chalk14.red(`Fatal: ${err.message}`));
4296
+ console.error(chalk18.red(`Fatal: ${err.message}`));
3340
4297
  process.exit(1);
3341
4298
  });