techunter 0.1.11 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +237 -209
  2. package/dist/index.js +1734 -956
  3. package/dist/mcp.js +1049 -378
  4. package/package.json +1 -1
package/dist/mcp.js CHANGED
@@ -39,12 +39,20 @@ __export(github_exports, {
39
39
  createTask: () => createTask,
40
40
  editTask: () => editTask,
41
41
  embedBaseCommit: () => embedBaseCommit,
42
+ embedTargetBranch: () => embedTargetBranch,
42
43
  ensureLabels: () => ensureLabels,
44
+ ensureRemoteBranch: () => ensureRemoteBranch,
43
45
  extractBaseCommit: () => extractBaseCommit,
46
+ extractTargetBranch: () => extractTargetBranch,
44
47
  formatGuideAsMarkdown: () => formatGuideAsMarkdown,
45
48
  getAuthenticatedUser: () => getAuthenticatedUser,
46
49
  getDefaultBranch: () => getDefaultBranch,
50
+ getIssueNumberFromBranch: () => getIssueNumberFromBranch,
51
+ getOpenSubtasks: () => getOpenSubtasks,
52
+ getRepoFile: () => getRepoFile,
47
53
  getTask: () => getTask,
54
+ getTaskPR: () => getTaskPR,
55
+ getTaskPRDiff: () => getTaskPRDiff,
48
56
  isCollaborator: () => isCollaborator,
49
57
  listComments: () => listComments,
50
58
  listMyTasks: () => listMyTasks,
@@ -54,7 +62,8 @@ __export(github_exports, {
54
62
  mergeWorkerIntoBase: () => mergeWorkerIntoBase,
55
63
  postComment: () => postComment,
56
64
  postGuideComment: () => postGuideComment,
57
- rejectTask: () => rejectTask
65
+ rejectTask: () => rejectTask,
66
+ upsertRepoFile: () => upsertRepoFile
58
67
  });
59
68
  import { Octokit } from "@octokit/rest";
60
69
  import { fetch as undiciFetch } from "undici";
@@ -117,11 +126,22 @@ function extractBaseCommit(body) {
117
126
  const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
118
127
  return match?.[1] ?? null;
119
128
  }
120
- async function createTask(config, title, body, baseCommit) {
129
+ function embedTargetBranch(body, branch) {
130
+ return `${body}
131
+ ${TARGET_BRANCH_MARKER}${branch} -->`;
132
+ }
133
+ function extractTargetBranch(body) {
134
+ if (!body) return null;
135
+ const match = body.match(/<!-- techunter-target:([^\s>]+) -->/);
136
+ return match?.[1] ?? null;
137
+ }
138
+ async function createTask(config, title, body, baseCommit, targetBranch) {
121
139
  const octokit = createOctokit(config.githubToken);
122
140
  const { owner, repo } = config.github;
123
141
  await ensureLabels(config);
124
- const finalBody = baseCommit ? embedBaseCommit(body ?? "", baseCommit) : body;
142
+ let finalBody = body ?? "";
143
+ if (baseCommit) finalBody = embedBaseCommit(finalBody, baseCommit);
144
+ if (targetBranch) finalBody = embedTargetBranch(finalBody, targetBranch);
125
145
  const { data } = await octokit.issues.create({
126
146
  owner,
127
147
  repo,
@@ -134,13 +154,22 @@ async function createTask(config, title, body, baseCommit) {
134
154
  async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
135
155
  const octokit = createOctokit(config.githubToken);
136
156
  const { owner, repo } = config.github;
137
- await octokit.repos.merge({
138
- owner,
139
- repo,
140
- base: baseBranch,
141
- head: workerBranch,
142
- commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
143
- });
157
+ try {
158
+ await octokit.repos.merge({
159
+ owner,
160
+ repo,
161
+ base: baseBranch,
162
+ head: workerBranch,
163
+ commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
164
+ });
165
+ } catch (err) {
166
+ if (err.status === 409) {
167
+ throw new Error(
168
+ `Merge conflict: ${workerBranch} cannot be merged into ${baseBranch} cleanly. Resolve conflicts manually.`
169
+ );
170
+ }
171
+ throw err;
172
+ }
144
173
  }
145
174
  async function claimTask(config, number, username) {
146
175
  const octokit = createOctokit(config.githubToken);
@@ -216,6 +245,23 @@ async function postGuideComment(config, number, guide) {
216
245
  body
217
246
  });
218
247
  }
248
+ async function ensureRemoteBranch(config, branchName, fallbackBase) {
249
+ const octokit = createOctokit(config.githubToken);
250
+ const { owner, repo } = config.github;
251
+ try {
252
+ await octokit.repos.getBranch({ owner, repo, branch: branchName });
253
+ return;
254
+ } catch (err) {
255
+ if (err.status !== 404) throw err;
256
+ }
257
+ const { data: baseRef } = await octokit.repos.getBranch({ owner, repo, branch: fallbackBase });
258
+ await octokit.git.createRef({
259
+ owner,
260
+ repo,
261
+ ref: `refs/heads/${branchName}`,
262
+ sha: baseRef.commit.sha
263
+ });
264
+ }
219
265
  async function createPR(config, title, body, branch, base) {
220
266
  const octokit = createOctokit(config.githubToken);
221
267
  const { owner, repo } = config.github;
@@ -232,14 +278,11 @@ async function createPR(config, title, body, branch, base) {
232
278
  async function markInReview(config, number) {
233
279
  const octokit = createOctokit(config.githubToken);
234
280
  const { owner, repo } = config.github;
235
- try {
236
- await octokit.issues.removeLabel({
237
- owner,
238
- repo,
239
- issue_number: number,
240
- name: LABEL_CLAIMED
241
- });
242
- } catch {
281
+ for (const label of [LABEL_CLAIMED, LABEL_CHANGES_NEEDED]) {
282
+ try {
283
+ await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
284
+ } catch {
285
+ }
243
286
  }
244
287
  await octokit.issues.addLabels({
245
288
  owner,
@@ -354,28 +397,115 @@ async function editTask(config, number, title, body) {
354
397
  const { owner, repo } = config.github;
355
398
  await octokit.issues.update({ owner, repo, issue_number: number, title, body });
356
399
  }
400
+ async function upsertRepoFile(config, filePath, content, message) {
401
+ const octokit = createOctokit(config.githubToken);
402
+ const { owner, repo } = config.github;
403
+ let sha;
404
+ try {
405
+ const { data: data2 } = await octokit.repos.getContent({ owner, repo, path: filePath });
406
+ if (!Array.isArray(data2) && data2.type === "file") {
407
+ sha = data2.sha;
408
+ }
409
+ } catch {
410
+ }
411
+ const { data } = await octokit.repos.createOrUpdateFileContents({
412
+ owner,
413
+ repo,
414
+ path: filePath,
415
+ message,
416
+ content: Buffer.from(content, "utf-8").toString("base64"),
417
+ ...sha ? { sha } : {}
418
+ });
419
+ return data.content?.html_url ?? `https://github.com/${owner}/${repo}/blob/main/${filePath}`;
420
+ }
421
+ async function getRepoFile(config, filePath) {
422
+ const octokit = createOctokit(config.githubToken);
423
+ const { owner, repo } = config.github;
424
+ try {
425
+ const { data } = await octokit.repos.getContent({ owner, repo, path: filePath });
426
+ if (!Array.isArray(data) && data.type === "file" && "content" in data) {
427
+ return Buffer.from(data.content, "base64").toString("utf-8");
428
+ }
429
+ return null;
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
357
434
  async function getDefaultBranch(config) {
358
435
  const octokit = createOctokit(config.githubToken);
359
436
  const { owner, repo } = config.github;
360
437
  const { data } = await octokit.repos.get({ owner, repo });
361
438
  return data.default_branch;
362
439
  }
363
- async function acceptTask(config, issueNumber, headBranch) {
440
+ async function getTaskPR(config, issueNumber) {
364
441
  const octokit = createOctokit(config.githubToken);
365
442
  const { owner, repo } = config.github;
366
443
  const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
367
- const pr = headBranch ? prs.find((p) => p.head.ref === headBranch) : prs.find((p) => p.head.ref.startsWith(`task-${issueNumber}-`) || p.head.ref.startsWith("worker-"));
368
- if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
369
- const { data: merge } = await octokit.pulls.merge({
444
+ const pr = prs.find(
445
+ (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
446
+ );
447
+ if (!pr) return null;
448
+ return { number: pr.number, url: pr.html_url, body: pr.body ?? "", baseBranch: pr.base.ref };
449
+ }
450
+ async function getOpenSubtasks(config, targetBranch) {
451
+ const octokit = createOctokit(config.githubToken);
452
+ const { owner, repo } = config.github;
453
+ const { data } = await octokit.issues.listForRepo({
454
+ owner,
455
+ repo,
456
+ state: "open",
457
+ per_page: 100
458
+ });
459
+ return data.filter((issue) => !issue.pull_request).filter((issue) => extractTargetBranch(issue.body ?? null) === targetBranch).map((issue) => issue.number);
460
+ }
461
+ async function getIssueNumberFromBranch(config, branch) {
462
+ const octokit = createOctokit(config.githubToken);
463
+ const { owner, repo } = config.github;
464
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
465
+ const pr = prs.find((p) => p.head.ref === branch);
466
+ if (!pr) return null;
467
+ const match = (pr.body ?? "").match(/Closes #(\d+)/i);
468
+ if (!match) return null;
469
+ return { issueNumber: parseInt(match[1], 10), prUrl: pr.html_url };
470
+ }
471
+ async function getTaskPRDiff(config, prNumber) {
472
+ const octokit = createOctokit(config.githubToken);
473
+ const { owner, repo } = config.github;
474
+ const response = await octokit.pulls.get({
370
475
  owner,
371
476
  repo,
372
- pull_number: pr.number,
373
- merge_method: "merge"
477
+ pull_number: prNumber,
478
+ mediaType: { format: "diff" }
374
479
  });
375
- await closeTask(config, issueNumber);
376
- return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "" };
480
+ return response.data;
481
+ }
482
+ async function acceptTask(config, issueNumber) {
483
+ const octokit = createOctokit(config.githubToken);
484
+ const { owner, repo } = config.github;
485
+ const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
486
+ const pr = prs.find(
487
+ (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
488
+ );
489
+ if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
490
+ try {
491
+ const { data: merge } = await octokit.pulls.merge({
492
+ owner,
493
+ repo,
494
+ pull_number: pr.number,
495
+ merge_method: "merge"
496
+ });
497
+ await closeTask(config, issueNumber);
498
+ return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "", baseBranch: pr.base.ref };
499
+ } catch (err) {
500
+ if (err.status === 405) {
501
+ throw new Error(
502
+ `PR #${pr.number} cannot be merged \u2014 may have conflicts or is not in a mergeable state.`
503
+ );
504
+ }
505
+ throw err;
506
+ }
377
507
  }
378
- var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER;
508
+ var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER, TARGET_BRANCH_MARKER;
379
509
  var init_github = __esm({
380
510
  "src/lib/github.ts"() {
381
511
  "use strict";
@@ -392,6 +522,7 @@ var init_github = __esm({
392
522
  ];
393
523
  TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
394
524
  BASE_COMMIT_MARKER = "<!-- techunter-base:";
525
+ TARGET_BRANCH_MARKER = "<!-- techunter-target:";
395
526
  }
396
527
  });
397
528
 
@@ -428,6 +559,17 @@ function makeWorkerBranchName(username) {
428
559
  const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
429
560
  return `worker-${slug}`;
430
561
  }
562
+ function makeTaskBranchName(issueNumber, username) {
563
+ const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
564
+ return `task-${issueNumber}-${slug}`;
565
+ }
566
+ function isTaskBranch(branch) {
567
+ return /^task-\d+-/.test(branch);
568
+ }
569
+ function parseIssueNumberFromBranch(branch) {
570
+ const match = branch.match(/^task-(\d+)-/);
571
+ return match ? parseInt(match[1], 10) : null;
572
+ }
431
573
  async function getCurrentCommit() {
432
574
  return (await git.revparse(["HEAD"])).trim();
433
575
  }
@@ -548,16 +690,27 @@ async function getRemoteHeadSha(baseBranch) {
548
690
  await git.fetch("origin", baseBranch);
549
691
  return (await git.revparse([`origin/${baseBranch}`])).trim();
550
692
  }
551
- async function resetOrCreateBranch(branchName, sha) {
552
- const branches = await git.branch();
553
- const localExists = Object.keys(branches.branches).some((b) => b === branchName);
554
- if (localExists) {
693
+ async function checkoutFromCommit(branchName, sha) {
694
+ const branches = await git.branch(["-a"]);
695
+ const exists = Object.keys(branches.branches).some(
696
+ (b) => b === branchName || b === `remotes/origin/${branchName}`
697
+ );
698
+ if (exists) {
555
699
  await git.checkout(branchName);
556
- await git.reset(["--hard", sha]);
557
700
  } else {
558
701
  await git.checkoutBranch(branchName, sha);
559
702
  }
560
703
  }
704
+ async function hasUncommittedChanges() {
705
+ const status = await git.status();
706
+ return !status.isClean();
707
+ }
708
+ async function stash(message) {
709
+ await git.stash(["push", "-u", "-m", message]);
710
+ }
711
+ async function stashPop() {
712
+ await git.stash(["pop"]);
713
+ }
561
714
 
562
715
  // src/tools/pick/index.ts
563
716
  init_github();
@@ -578,7 +731,8 @@ var configSchema = z.object({
578
731
  }),
579
732
  taskState: z.object({
580
733
  activeIssueNumber: z.number().optional(),
581
- baseCommit: z.string().optional()
734
+ baseCommit: z.string().optional(),
735
+ activeBranch: z.string().optional()
582
736
  }).optional()
583
737
  });
584
738
  var store = new Conf({
@@ -665,11 +819,22 @@ function colorStatus(status) {
665
819
  return padded;
666
820
  }
667
821
  }
822
+ function parentIssueFromBranch(branch) {
823
+ if (!isTaskBranch(branch)) return null;
824
+ const match = branch.match(/^task-(\d+)-/);
825
+ return match ? parseInt(match[1], 10) : null;
826
+ }
827
+ function getParentIssueNumber(issue) {
828
+ const target = extractTargetBranch(issue.body);
829
+ if (!target) return null;
830
+ return parentIssueFromBranch(target);
831
+ }
668
832
  function printTaskDetail(issue) {
669
833
  const divider = chalk2.dim("\u2500".repeat(70));
834
+ const parentNum = getParentIssueNumber(issue);
670
835
  console.log("\n" + divider);
671
836
  console.log(
672
- chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014")
837
+ chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014") + (parentNum ? chalk2.dim(` sub-task of #${parentNum}`) : "")
673
838
  );
674
839
  console.log(chalk2.bold("\n " + issue.title));
675
840
  if (issue.body) {
@@ -689,12 +854,34 @@ async function printTaskList(config) {
689
854
  if (tasks.length === 0) {
690
855
  console.log(chalk2.dim(" (no tasks)"));
691
856
  } else {
692
- for (const t of tasks) {
857
+ let printTask2 = function(t, indent, connector, isLast) {
693
858
  const num = `#${t.number}`.padEnd(5);
694
859
  const status = colorStatus(getStatus(t));
695
860
  const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
696
- const title = t.title.length > 36 ? t.title.slice(0, 33) + "..." : t.title;
697
- console.log(` ${num}${status}${assignee}${title}`);
861
+ const fullPrefix = indent + connector;
862
+ const maxTitle = 36 - fullPrefix.length;
863
+ const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
864
+ console.log(` ${num}${status}${assignee}${chalk2.dim(fullPrefix)}${title}`);
865
+ const children = childrenOf.get(t.number) ?? [];
866
+ const childIndent = indent + (isLast ? " " : "\u2502 ");
867
+ for (let i = 0; i < children.length; i++) {
868
+ const childIsLast = i === children.length - 1;
869
+ printTask2(children[i], childIndent, childIsLast ? "\u2514\u2500 " : "\u251C\u2500 ", childIsLast);
870
+ }
871
+ };
872
+ var printTask = printTask2;
873
+ const taskMap = new Map(tasks.map((t) => [t.number, t]));
874
+ const childrenOf = /* @__PURE__ */ new Map();
875
+ for (const t of tasks) {
876
+ const parentNum = getParentIssueNumber(t);
877
+ const key = parentNum !== null && taskMap.has(parentNum) ? parentNum : null;
878
+ if (!childrenOf.has(key)) childrenOf.set(key, []);
879
+ childrenOf.get(key).push(t);
880
+ }
881
+ const roots = childrenOf.get(null) ?? [];
882
+ for (let i = 0; i < roots.length; i++) {
883
+ const isLast = i === roots.length - 1;
884
+ printTask2(roots[i], "", isLast ? "\u2514\u2500 " : "\u251C\u2500 ", isLast);
698
885
  }
699
886
  }
700
887
  console.log(divider);
@@ -873,9 +1060,19 @@ var definition = {
873
1060
  };
874
1061
  async function run(_input, config) {
875
1062
  const taskState = getConfig().taskState;
876
- const issueNumber = taskState?.activeIssueNumber;
1063
+ const currentBranch = await getCurrentBranch();
1064
+ let issueNumber = taskState?.activeIssueNumber && taskState?.activeBranch && currentBranch === taskState.activeBranch ? taskState.activeIssueNumber : void 0;
877
1065
  if (!issueNumber) {
878
- return "No active task found. Claim a task first with /pick.";
1066
+ const fromBranch = parseIssueNumberFromBranch(currentBranch);
1067
+ if (fromBranch) {
1068
+ issueNumber = fromBranch;
1069
+ } else {
1070
+ const found = await getIssueNumberFromBranch(config, currentBranch);
1071
+ if (!found) {
1072
+ return "No active task found. Claim a task first with /pick.";
1073
+ }
1074
+ issueNumber = found.issueNumber;
1075
+ }
879
1076
  }
880
1077
  let spinner = ora("Loading task and diff\u2026").start();
881
1078
  const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
@@ -885,9 +1082,16 @@ async function run(_input, config) {
885
1082
  getAuthenticatedUser(config)
886
1083
  ]);
887
1084
  spinner.stop();
888
- const workerBranch = makeWorkerBranchName(issue.author ?? me);
1085
+ const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
889
1086
  const branch = await getCurrentBranch();
890
1087
  const isSelfSubmit = issue.author !== null && issue.author === me;
1088
+ spinner = ora("Checking for open sub-tasks\u2026").start();
1089
+ const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1090
+ spinner.stop();
1091
+ if (openSubtaskNumbers.length > 0) {
1092
+ return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open:
1093
+ ` + openSubtaskNumbers.map((n) => ` - #${n}`).join("\n") + "\nComplete all sub-tasks before submitting.";
1094
+ }
891
1095
  let review = "";
892
1096
  if (!isSelfSubmit) {
893
1097
  const reviewSpinner = ora("Reviewing changes\u2026").start();
@@ -948,26 +1152,35 @@ async function run(_input, config) {
948
1152
  spinner.stop();
949
1153
  console.error(chalk5.yellow(`Warning: failed to close issue: ${err.message}`));
950
1154
  }
951
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1155
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
952
1156
  return `Task #${issueNumber} committed and closed.
953
1157
  Commit: "${commitMessage.trim()}"`;
954
1158
  }
955
- spinner = ora("Creating pull request\u2026").start();
1159
+ spinner = ora("Checking for existing PR\u2026").start();
1160
+ const existingPR = await getTaskPR(config, issueNumber);
1161
+ spinner.stop();
956
1162
  let prUrl;
957
- try {
958
- const prBody = [
959
- `Closes #${issueNumber}`,
960
- issue.body ? `
1163
+ if (existingPR) {
1164
+ prUrl = existingPR.url;
1165
+ console.log(chalk5.dim(` Existing PR found: ${prUrl} \u2014 updating.`));
1166
+ } else {
1167
+ spinner = ora("Creating pull request\u2026").start();
1168
+ try {
1169
+ await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1170
+ const prBody = [
1171
+ `Closes #${issueNumber}`,
1172
+ issue.body ? `
961
1173
  ${issue.body}` : "",
962
- review ? `
1174
+ review ? `
963
1175
  ## AI Review
964
1176
  ${review}` : ""
965
- ].join("\n").trim();
966
- prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
967
- spinner.stop();
968
- } catch (err) {
969
- spinner.stop();
970
- return `Committed but PR creation failed: ${err.message}`;
1177
+ ].join("\n").trim();
1178
+ prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1179
+ spinner.stop();
1180
+ } catch (err) {
1181
+ spinner.stop();
1182
+ return `Committed but PR creation failed: ${err.message}`;
1183
+ }
971
1184
  }
972
1185
  spinner = ora("Marking as in-review\u2026").start();
973
1186
  try {
@@ -975,17 +1188,27 @@ ${review}` : ""
975
1188
  spinner.stop();
976
1189
  } catch (err) {
977
1190
  spinner.stop();
978
- return `PR created (${prUrl}) but failed to update label: ${err.message}`;
1191
+ return `PR ${existingPR ? "updated" : "created"} (${prUrl}) but failed to update label: ${err.message}`;
979
1192
  }
980
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
981
- return `Task #${issueNumber} submitted.
1193
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1194
+ return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
982
1195
  Commit: "${commitMessage.trim()}"
983
1196
  PR: ${prUrl}`;
984
1197
  }
985
1198
  async function execute(input, config) {
986
1199
  const taskState = getConfig().taskState;
987
- const issueNumber = taskState?.activeIssueNumber;
988
- if (!issueNumber) return "No active task found. Claim a task first.";
1200
+ let issueNumber = taskState?.activeIssueNumber;
1201
+ if (!issueNumber) {
1202
+ const currentBranch = await getCurrentBranch();
1203
+ const fromBranch = parseIssueNumberFromBranch(currentBranch);
1204
+ if (fromBranch) {
1205
+ issueNumber = fromBranch;
1206
+ } else {
1207
+ const found = await getIssueNumberFromBranch(config, currentBranch);
1208
+ if (!found) return "No active task found. Claim a task first.";
1209
+ issueNumber = found.issueNumber;
1210
+ }
1211
+ }
989
1212
  const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
990
1213
  const [issue, diff, branch, me] = await Promise.all([
991
1214
  getTask(config, issueNumber),
@@ -993,7 +1216,11 @@ async function execute(input, config) {
993
1216
  getCurrentBranch(),
994
1217
  getAuthenticatedUser(config)
995
1218
  ]);
996
- const workerBranch = makeWorkerBranchName(issue.author ?? me);
1219
+ const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
1220
+ const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1221
+ if (openSubtaskNumbers.length > 0) {
1222
+ return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open: ` + openSubtaskNumbers.map((n) => `#${n}`).join(", ");
1223
+ }
997
1224
  const isSelfSubmit = issue.author !== null && issue.author === me;
998
1225
  let review = "";
999
1226
  if (!isSelfSubmit) {
@@ -1014,29 +1241,36 @@ async function execute(input, config) {
1014
1241
  await closeTask(config, issueNumber);
1015
1242
  } catch {
1016
1243
  }
1017
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1244
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1018
1245
  return `Task #${issueNumber} committed and closed.
1019
1246
  Commit: "${commitMessage}"`;
1020
1247
  }
1248
+ const existingPR = await getTaskPR(config, issueNumber);
1021
1249
  let prUrl;
1022
- try {
1023
- const prBody = [
1024
- `Closes #${issueNumber}`,
1025
- issue.body ? `
1250
+ if (existingPR) {
1251
+ prUrl = existingPR.url;
1252
+ } else {
1253
+ try {
1254
+ await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1255
+ const prBody = [
1256
+ `Closes #${issueNumber}`,
1257
+ issue.body ? `
1026
1258
  ${issue.body}` : "",
1027
- review ? `
1259
+ review ? `
1028
1260
  ## AI Review
1029
1261
  ${review}` : ""
1030
- ].join("\n").trim();
1031
- prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
1032
- } catch (err) {
1033
- return `Committed but PR creation failed: ${err.message}`;
1262
+ ].join("\n").trim();
1263
+ prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1264
+ } catch (err) {
1265
+ return `Committed but PR creation failed: ${err.message}`;
1266
+ }
1034
1267
  }
1035
1268
  try {
1036
1269
  await markInReview(config, issueNumber);
1037
1270
  } catch {
1038
1271
  }
1039
- return `Task #${issueNumber} submitted.
1272
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1273
+ return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
1040
1274
  Review:
1041
1275
  ${review}
1042
1276
  Commit: "${commitMessage}"
@@ -1195,8 +1429,23 @@ async function run3(input, config) {
1195
1429
  }
1196
1430
  }
1197
1431
  const actions = [];
1198
- if (status === "available") actions.push({ name: "Claim this task", value: "claim" });
1199
- if (status === "claimed" || status === "changes-needed") actions.push({ name: "Submit this task", value: "submit" });
1432
+ if (status === "available") {
1433
+ actions.push({ name: "Claim this task", value: "claim" });
1434
+ }
1435
+ if (status === "claimed") {
1436
+ actions.push({ name: "Submit this task", value: "submit" });
1437
+ }
1438
+ if (status === "changes-needed") {
1439
+ const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1440
+ const me = await getAuthenticatedUser2(config);
1441
+ const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1442
+ const currentBranch = await getCurrentBranch();
1443
+ if (currentBranch === taskBranch) {
1444
+ actions.push({ name: "Submit this task (fixes done)", value: "submit" });
1445
+ } else {
1446
+ actions.push({ name: `Switch to ${taskBranch} to fix`, value: "switch-fix" });
1447
+ }
1448
+ }
1200
1449
  actions.push({ name: "Close this task", value: "close" });
1201
1450
  actions.push({ name: "Nothing, just viewing", value: "none" });
1202
1451
  let action;
@@ -1208,9 +1457,8 @@ async function run3(input, config) {
1208
1457
  if (action === "none") return `Viewed task #${issue.number}.`;
1209
1458
  if (action === "claim") {
1210
1459
  try {
1211
- const { getAuthenticatedUser: getAuthenticatedUser2, listMyTasks: listMyTasks2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1212
- const me = await getAuthenticatedUser2(config);
1213
- const myTasks = await listMyTasks2(config, me);
1460
+ const me = await getAuthenticatedUser(config);
1461
+ const myTasks = await listMyTasks(config, me);
1214
1462
  const activeTask = myTasks.find((t) => {
1215
1463
  const labels = t.labels;
1216
1464
  return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
@@ -1219,33 +1467,61 @@ async function run3(input, config) {
1219
1467
  return `You already have an active task: #${activeTask.number} "${activeTask.title}"
1220
1468
  Finish or submit it before claiming a new one.`;
1221
1469
  }
1470
+ let stashed = false;
1471
+ if (await hasUncommittedChanges()) {
1472
+ let choice;
1473
+ try {
1474
+ choice = await select3({
1475
+ message: "You have uncommitted changes. What would you like to do?",
1476
+ choices: [
1477
+ { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1478
+ { name: "Cancel", value: "cancel" }
1479
+ ]
1480
+ });
1481
+ } catch {
1482
+ choice = "cancel";
1483
+ }
1484
+ if (choice === "cancel") return "Cancelled.";
1485
+ await stash(`tch: before claiming #${issue.number}`);
1486
+ stashed = true;
1487
+ console.log(chalk6.dim(" Changes stashed. Run `git stash pop` after you finish this task to restore them."));
1488
+ }
1222
1489
  let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
1223
1490
  await claimTask(config, issue.number, me);
1224
1491
  spinner.stop();
1225
- const workerBranch = makeWorkerBranchName(me);
1492
+ const taskBranch = makeTaskBranchName(issue.number, me);
1226
1493
  const taskBase = extractBaseCommit(issue.body);
1227
- spinner = ora3(`Switching to ${workerBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
1494
+ spinner = ora3(`Creating branch ${taskBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
1228
1495
  try {
1229
1496
  if (taskBase) {
1230
- await resetOrCreateBranch(workerBranch, taskBase);
1497
+ await checkoutFromCommit(taskBranch, taskBase);
1231
1498
  } else {
1232
- await switchToBranchOrCreate(workerBranch);
1499
+ await switchToBranchOrCreate(taskBranch);
1233
1500
  }
1234
1501
  spinner.stop();
1235
- spinner = ora3("Pushing worker branch\u2026").start();
1502
+ spinner = ora3("Pushing task branch\u2026").start();
1236
1503
  try {
1237
- await pushBranch(workerBranch);
1504
+ await pushBranch(taskBranch);
1238
1505
  spinner.stop();
1239
1506
  } catch {
1240
- spinner.warn("Could not push worker branch");
1507
+ spinner.warn("Could not push task branch \u2014 will push on submit");
1241
1508
  }
1242
- } catch {
1243
- spinner.warn(`Could not switch to ${workerBranch}`);
1509
+ } catch (err) {
1510
+ spinner.warn(`Could not switch to ${taskBranch}`);
1511
+ if (stashed) {
1512
+ try {
1513
+ await stashPop();
1514
+ console.log(chalk6.dim(" Restored stashed changes."));
1515
+ } catch {
1516
+ console.log(chalk6.yellow(" Warning: could not restore stash automatically. Run `git stash pop` manually."));
1517
+ }
1518
+ }
1519
+ throw err;
1244
1520
  }
1245
1521
  const baseCommit = await getCurrentCommit();
1246
- setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
1522
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1247
1523
  console.log(chalk6.green(`
1248
- Claimed! Branch: ${workerBranch} (base: ${baseCommit.slice(0, 7)})
1524
+ Claimed! Branch: ${taskBranch} (base: ${baseCommit.slice(0, 7)})
1249
1525
  `));
1250
1526
  let openClaude;
1251
1527
  try {
@@ -1259,12 +1535,58 @@ Finish or submit it before claiming a new one.`;
1259
1535
  } catch {
1260
1536
  openClaude = false;
1261
1537
  }
1262
- if (openClaude) await launchClaudeCode(issue, workerBranch);
1263
- return `Task #${issue.number} claimed. Branch: ${workerBranch}`;
1538
+ if (openClaude) await launchClaudeCode(issue, taskBranch);
1539
+ return `Task #${issue.number} claimed. Branch: ${taskBranch}`;
1264
1540
  } catch (err) {
1265
1541
  return `Error claiming task: ${err.message}`;
1266
1542
  }
1267
1543
  }
1544
+ if (action === "switch-fix") {
1545
+ const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1546
+ const me = await getAuthenticatedUser2(config);
1547
+ const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1548
+ let stashed = false;
1549
+ if (await hasUncommittedChanges()) {
1550
+ let choice;
1551
+ try {
1552
+ choice = await select3({
1553
+ message: "You have uncommitted changes. What would you like to do?",
1554
+ choices: [
1555
+ { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1556
+ { name: "Cancel", value: "cancel" }
1557
+ ]
1558
+ });
1559
+ } catch {
1560
+ choice = "cancel";
1561
+ }
1562
+ if (choice === "cancel") return "Cancelled.";
1563
+ await stash(`tch: before switching to ${taskBranch}`);
1564
+ stashed = true;
1565
+ console.log(chalk6.dim(" Changes stashed. Run `git stash pop` to restore them later."));
1566
+ }
1567
+ const spinner = ora3(`Switching to ${taskBranch}\u2026`).start();
1568
+ try {
1569
+ await switchToBranchOrCreate(taskBranch);
1570
+ spinner.stop();
1571
+ } catch (err) {
1572
+ spinner.warn(`Could not switch to ${taskBranch}: ${err.message}`);
1573
+ if (stashed) {
1574
+ try {
1575
+ await stashPop();
1576
+ console.log(chalk6.dim(" Restored stashed changes."));
1577
+ } catch {
1578
+ console.log(chalk6.yellow(" Run `git stash pop` manually to restore your changes."));
1579
+ }
1580
+ }
1581
+ return `Error: ${err.message}`;
1582
+ }
1583
+ const baseCommit = extractBaseCommit(issue.body) ?? await getCurrentCommit();
1584
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1585
+ console.log(chalk6.green(`
1586
+ Switched to ${taskBranch}. Fix the issues then run /submit.
1587
+ `));
1588
+ return `Switched to ${taskBranch} for task #${issue.number}.`;
1589
+ }
1268
1590
  if (action === "submit") return run({}, config);
1269
1591
  if (action === "close") return run2({ issue_number: issue.number }, config);
1270
1592
  return "Cancelled.";
@@ -1293,28 +1615,31 @@ async function execute3(input, config) {
1293
1615
  if (activeTask) {
1294
1616
  return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
1295
1617
  }
1618
+ if (await hasUncommittedChanges()) {
1619
+ return "Cannot claim: you have uncommitted changes. Commit or stash them first (git stash).";
1620
+ }
1296
1621
  try {
1297
1622
  await claimTask(config, issueNumber, me);
1298
1623
  } catch (err) {
1299
1624
  return `Error claiming task: ${err.message}`;
1300
1625
  }
1301
- const workerBranch = makeWorkerBranchName(me);
1626
+ const taskBranch = makeTaskBranchName(issue.number, me);
1302
1627
  const taskBase = extractBaseCommit(issue.body);
1303
1628
  try {
1304
1629
  if (taskBase) {
1305
- await resetOrCreateBranch(workerBranch, taskBase);
1630
+ await checkoutFromCommit(taskBranch, taskBase);
1306
1631
  } else {
1307
- await switchToBranchOrCreate(workerBranch);
1632
+ await switchToBranchOrCreate(taskBranch);
1308
1633
  }
1309
1634
  } catch {
1310
1635
  }
1311
1636
  try {
1312
- await pushBranch(workerBranch);
1637
+ await pushBranch(taskBranch);
1313
1638
  } catch {
1314
1639
  }
1315
1640
  const baseCommit = await getCurrentCommit();
1316
- setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
1317
- return `Task #${issueNumber} claimed. Branch: ${workerBranch} (base commit: ${baseCommit.slice(0, 7)})`;
1641
+ setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit, activeBranch: taskBranch } });
1642
+ return `Task #${issueNumber} claimed. Branch: ${taskBranch} (base commit: ${baseCommit.slice(0, 7)})`;
1318
1643
  }
1319
1644
  return `Unknown action: ${action}`;
1320
1645
  }
@@ -1389,6 +1714,77 @@ async function openInEditor(content) {
1389
1714
  await rm(dir, { recursive: true, force: true });
1390
1715
  }
1391
1716
  }
1717
+ async function resolveBaseAndTarget(config, me, interactive) {
1718
+ const currentBranch = await getCurrentBranch();
1719
+ if (isTaskBranch(currentBranch)) {
1720
+ if (await hasUncommittedChanges()) {
1721
+ if (!interactive) {
1722
+ throw new Error("Cannot create sub-task: you have uncommitted changes. Commit them first so the executor starts from the correct base.");
1723
+ }
1724
+ const { select: inquirerSelect } = await import("@inquirer/prompts");
1725
+ let choice;
1726
+ try {
1727
+ choice = await inquirerSelect({
1728
+ message: "You have uncommitted changes. The sub-task executor will start from the last commit \u2014 they won't see your current unsaved work.",
1729
+ choices: [
1730
+ { name: "Commit first (cancel and commit manually)", value: "cancel" },
1731
+ { name: "Continue anyway (executor starts without my unsaved changes)", value: "continue" }
1732
+ ]
1733
+ });
1734
+ } catch {
1735
+ choice = "cancel";
1736
+ }
1737
+ if (choice === "cancel") throw new Error("Cancelled. Commit your changes first, then create the sub-task.");
1738
+ }
1739
+ const baseCommit2 = await getCurrentCommit();
1740
+ return { baseCommit: baseCommit2, targetBranch: currentBranch, isSubtask: true };
1741
+ }
1742
+ let stashedForSync = false;
1743
+ if (await hasUncommittedChanges()) {
1744
+ if (!interactive) {
1745
+ throw new Error("Cannot create task: you have uncommitted changes. Commit or stash them first (git stash).");
1746
+ }
1747
+ const { select: inquirerSelect } = await import("@inquirer/prompts");
1748
+ let choice;
1749
+ try {
1750
+ choice = await inquirerSelect({
1751
+ message: "You have uncommitted changes. Syncing with main requires a clean working tree.",
1752
+ choices: [
1753
+ { name: "Stash changes and continue (restore with: git stash pop)", value: "stash" },
1754
+ { name: "Cancel", value: "cancel" }
1755
+ ]
1756
+ });
1757
+ } catch {
1758
+ choice = "cancel";
1759
+ }
1760
+ if (choice === "cancel") throw new Error("Cancelled.");
1761
+ await stash("tch: before creating new task");
1762
+ stashedForSync = true;
1763
+ console.log(chalk7.dim(" Changes stashed. Run `git stash pop` after creating the task."));
1764
+ }
1765
+ const baseBranch = config.baseBranch ?? "main";
1766
+ let baseCommit;
1767
+ const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
1768
+ try {
1769
+ await syncWithBase(baseBranch);
1770
+ baseCommit = await getCurrentCommit();
1771
+ syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
1772
+ } catch {
1773
+ syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
1774
+ try {
1775
+ baseCommit = await getRemoteHeadSha(baseBranch);
1776
+ } catch {
1777
+ }
1778
+ if (stashedForSync) {
1779
+ try {
1780
+ await stashPop();
1781
+ } catch {
1782
+ }
1783
+ throw new Error(`Could not sync with ${baseBranch}. Your changes have been restored from stash.`);
1784
+ }
1785
+ }
1786
+ return { baseCommit, targetBranch: makeWorkerBranchName(me), isSubtask: false };
1787
+ }
1392
1788
  var definition4 = {
1393
1789
  type: "function",
1394
1790
  function: {
@@ -1484,26 +1880,23 @@ async function run4(input, config) {
1484
1880
  console.log(chalk7.yellow(` Revision error: ${err.message}`));
1485
1881
  }
1486
1882
  }
1487
- const baseBranch = config.baseBranch ?? "main";
1488
1883
  let baseCommit;
1489
- const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
1884
+ let targetBranch;
1885
+ let isSubtask;
1490
1886
  try {
1491
- await syncWithBase(baseBranch);
1492
- baseCommit = await getCurrentCommit();
1493
- syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
1494
- } catch {
1495
- syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
1496
- try {
1497
- baseCommit = await getRemoteHeadSha(baseBranch);
1498
- } catch {
1499
- }
1887
+ ({ baseCommit, targetBranch, isSubtask } = await resolveBaseAndTarget(config, me, true));
1888
+ } catch (err) {
1889
+ return err.message;
1890
+ }
1891
+ if (isSubtask) {
1892
+ console.log(chalk7.dim(` Sub-task: will target branch ${chalk7.cyan(targetBranch)} (base: ${baseCommit?.slice(0, 7) ?? "HEAD"})`));
1500
1893
  }
1501
1894
  const createSpinner = ora4(`Creating "${title}"\u2026`).start();
1502
1895
  let htmlUrl;
1503
1896
  let issueNumber;
1504
1897
  let issueTitle;
1505
1898
  try {
1506
- const issue = await createTask(config, title, guide, baseCommit);
1899
+ const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1507
1900
  createSpinner.stop();
1508
1901
  htmlUrl = issue.htmlUrl;
1509
1902
  issueNumber = issue.number;
@@ -1540,19 +1933,9 @@ async function execute4(input, config) {
1540
1933
  if (feedback) {
1541
1934
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
1542
1935
  }
1543
- const baseBranch = config.baseBranch ?? "main";
1544
- let baseCommit;
1545
- try {
1546
- await syncWithBase(baseBranch);
1547
- baseCommit = await getCurrentCommit();
1548
- } catch {
1549
- try {
1550
- baseCommit = await getRemoteHeadSha(baseBranch);
1551
- } catch {
1552
- }
1553
- }
1936
+ const { baseCommit, targetBranch } = await resolveBaseAndTarget(config, me, false);
1554
1937
  try {
1555
- const issue = await createTask(config, title, guide, baseCommit);
1938
+ const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1556
1939
  return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
1557
1940
 
1558
1941
  Guide:
@@ -1602,119 +1985,249 @@ var terminal5 = true;
1602
1985
  // src/tools/review/index.ts
1603
1986
  var review_exports = {};
1604
1987
  __export(review_exports, {
1988
+ definition: () => definition8,
1989
+ execute: () => execute8,
1990
+ run: () => run8,
1991
+ terminal: () => terminal8
1992
+ });
1993
+ init_github();
1994
+ import chalk10 from "chalk";
1995
+ import ora8 from "ora";
1996
+ import { select as select7 } from "@inquirer/prompts";
1997
+
1998
+ // src/tools/accept/index.ts
1999
+ var accept_exports = {};
2000
+ __export(accept_exports, {
1605
2001
  definition: () => definition6,
1606
2002
  execute: () => execute6,
1607
2003
  run: () => run6,
1608
2004
  terminal: () => terminal6
1609
2005
  });
1610
2006
  init_github();
2007
+ import chalk8 from "chalk";
2008
+ import { select as select5 } from "@inquirer/prompts";
1611
2009
  import ora6 from "ora";
1612
- var definition6 = {
1613
- type: "function",
1614
- function: {
1615
- name: "review",
1616
- description: "List tasks waiting for your review (submitted by others, created by you). Equivalent to /review.",
1617
- parameters: { type: "object", properties: {}, required: [] }
1618
- }
1619
- };
1620
- async function run6(_input, config) {
1621
- const spinner = ora6("Loading tasks for review\u2026").start();
1622
- try {
1623
- const me = await getAuthenticatedUser(config);
1624
- const tasks = await listTasksForReview(config, me);
1625
- spinner.stop();
1626
- if (tasks.length === 0) return `No tasks pending review for @${me}.`;
1627
- const lines = tasks.map((t) => ` #${t.number} [in-review] @${t.assignee ?? "\u2014"} ${t.title}`);
1628
- return `Tasks pending review (created by @${me}):
1629
- ${lines.join("\n")}`;
1630
- } catch (err) {
1631
- spinner.stop();
1632
- return `Error: ${err.message}`;
1633
- }
1634
- }
1635
- var execute6 = run6;
1636
- var terminal6 = true;
1637
2010
 
1638
- // src/tools/refresh/index.ts
1639
- var refresh_exports = {};
1640
- __export(refresh_exports, {
1641
- definition: () => definition7,
1642
- execute: () => execute7,
1643
- run: () => run7,
1644
- terminal: () => terminal7
1645
- });
1646
- var definition7 = {
1647
- type: "function",
1648
- function: {
1649
- name: "refresh",
1650
- description: "Reload and display the full task list. Equivalent to /refresh.",
1651
- parameters: { type: "object", properties: {}, required: [] }
1652
- }
1653
- };
1654
- async function run7(_input, config) {
1655
- const tasks = await printTaskList(config);
1656
- if (tasks.length === 0) return "No tasks found.";
1657
- const lines = tasks.map((t) => {
1658
- const status = getStatus(t);
1659
- const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
1660
- return `#${t.number} [${status}] ${assignee} ${t.title}`;
1661
- });
1662
- return `Tasks (${tasks.length}):
1663
- ${lines.join("\n")}`;
2011
+ // src/tools/wiki/prompts.ts
2012
+ var WIKI_FORMAT = `
2013
+ The document you produce must be valid Markdown with these exact sections:
2014
+
2015
+ # [Project Name]
2016
+
2017
+ > One-sentence description of what this project does.
2018
+
2019
+ ## What Is This?
2020
+
2021
+ 2-4 paragraphs covering:
2022
+ - The problem this project solves
2023
+ - Who uses it and in what context
2024
+ - Core capabilities / key features
2025
+
2026
+ ## Quick Start
2027
+
2028
+ Numbered steps for a brand-new developer to install, configure, and run the project for the first time.
2029
+
2030
+ ## Architecture
2031
+
2032
+ High-level explanation of how the system is structured:
2033
+ - Key components / layers and their responsibilities
2034
+ - Request or data flow (prose or ASCII diagram)
2035
+ - Noteworthy design decisions
2036
+
2037
+ ## Key Files
2038
+
2039
+ | File / Directory | Purpose |
2040
+ |---|---|
2041
+ | ... | ... |
2042
+
2043
+ (List the 8-15 most important files.)
2044
+
2045
+ ## Development Workflow
2046
+
2047
+ Common day-to-day tasks a contributor will need:
2048
+ - How to build / run locally
2049
+ - How to add a new feature (brief steps)
2050
+ - Any testing or linting commands
2051
+
2052
+ ---
2053
+ *Maintained by Techunter \u2014 run \`tch wiki\` to regenerate*
2054
+ `;
2055
+
2056
+ // src/tools/wiki/wiki-generator.ts
2057
+ async function generateWiki(config) {
2058
+ return runSubAgentLoop(
2059
+ config,
2060
+ "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,
2061
+ "Analyze this project thoroughly and produce a comprehensive TECHUNTER.md overview document for new team members.",
2062
+ ["list_files", "grep_code", "run_command"]
2063
+ );
1664
2064
  }
1665
- var execute7 = run7;
1666
- var terminal7 = true;
1667
2065
 
1668
- // src/tools/open-code/index.ts
1669
- var open_code_exports = {};
1670
- __export(open_code_exports, {
1671
- definition: () => definition8,
1672
- execute: () => execute8,
1673
- run: () => run8,
1674
- terminal: () => terminal8
1675
- });
1676
- init_github();
1677
- var definition8 = {
2066
+ // src/tools/accept/index.ts
2067
+ var definition6 = {
1678
2068
  type: "function",
1679
2069
  function: {
1680
- name: "open_code",
1681
- description: "Launch Claude Code for the current task branch. Equivalent to /code.",
1682
- parameters: { type: "object", properties: {}, required: [] }
2070
+ name: "accept",
2071
+ description: "Accept an in-review task: merges the PR into the target branch and closes the issue.",
2072
+ parameters: {
2073
+ type: "object",
2074
+ properties: {
2075
+ issue_number: { type: "number", description: "GitHub issue number to accept" }
2076
+ },
2077
+ required: ["issue_number"]
2078
+ }
1683
2079
  }
1684
2080
  };
1685
- async function run8(_input, config) {
1686
- let branch;
2081
+ async function run6(input, config) {
2082
+ let issueNumber = input["issue_number"];
2083
+ if (!issueNumber) {
2084
+ const spinner3 = ora6("Loading tasks for review\u2026").start();
2085
+ let tasks;
2086
+ let me;
2087
+ try {
2088
+ me = await getAuthenticatedUser(config);
2089
+ tasks = await listTasksForReview(config, me);
2090
+ spinner3.stop();
2091
+ } catch (err) {
2092
+ spinner3.stop();
2093
+ return `Error: ${err.message}`;
2094
+ }
2095
+ if (tasks.length === 0) return "No tasks pending review.";
2096
+ try {
2097
+ issueNumber = await select5({
2098
+ message: "Which task to accept?",
2099
+ choices: tasks.map((t) => ({
2100
+ name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
2101
+ value: t.number
2102
+ }))
2103
+ });
2104
+ } catch {
2105
+ return "Cancelled.";
2106
+ }
2107
+ }
2108
+ const spinner2 = ora6("Verifying permissions\u2026").start();
2109
+ let me2;
2110
+ let issue;
1687
2111
  try {
1688
- branch = await getCurrentBranch();
2112
+ [me2, issue] = await Promise.all([
2113
+ getAuthenticatedUser(config),
2114
+ getTask(config, issueNumber)
2115
+ ]);
2116
+ spinner2.stop();
1689
2117
  } catch (err) {
2118
+ spinner2.stop();
1690
2119
  return `Error: ${err.message}`;
1691
2120
  }
1692
- const match = branch.match(/^task-(\d+)-/);
1693
- if (!match) return `Not on a task branch (current: ${branch}).`;
1694
- const issueNum = parseInt(match[1], 10);
1695
- let issue;
2121
+ if (issue.author && issue.author !== me2) {
2122
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2123
+ }
2124
+ let confirmed;
1696
2125
  try {
1697
- issue = await getTask(config, issueNum);
2126
+ confirmed = await select5({
2127
+ message: `Merge PR for #${issueNumber} and close issue?`,
2128
+ choices: [
2129
+ { name: "Yes, accept", value: true },
2130
+ { name: "Cancel", value: false }
2131
+ ]
2132
+ });
2133
+ } catch {
2134
+ return "Cancelled.";
2135
+ }
2136
+ if (!confirmed) return "Cancelled.";
2137
+ const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2138
+ let result;
2139
+ try {
2140
+ result = await acceptTask(config, issueNumber);
2141
+ spinner.succeed(`PR #${result.prNumber} merged \u2192 ${chalk8.cyan(result.baseBranch)}`);
1698
2142
  } catch (err) {
2143
+ spinner.fail("Failed");
1699
2144
  return `Error: ${err.message}`;
1700
2145
  }
1701
- await launchClaudeCode(issue, branch);
1702
- return "Claude Code session ended.";
2146
+ const mergedIntoTaskBranch = isTaskBranch(result.baseBranch);
2147
+ if (!mergedIntoTaskBranch) {
2148
+ const baseBranch = config.baseBranch ?? "main";
2149
+ let pushToMain;
2150
+ try {
2151
+ pushToMain = await select5({
2152
+ message: `Push ${chalk8.cyan(result.baseBranch)} \u2192 ${chalk8.cyan(baseBranch)}?`,
2153
+ choices: [
2154
+ { name: `Yes, push to ${baseBranch}`, value: true },
2155
+ { name: "No, keep in worker branch", value: false }
2156
+ ]
2157
+ });
2158
+ } catch {
2159
+ pushToMain = false;
2160
+ }
2161
+ if (pushToMain) {
2162
+ const mergeSpinner = ora6(`Merging ${result.baseBranch} \u2192 ${baseBranch}\u2026`).start();
2163
+ try {
2164
+ await mergeWorkerIntoBase(config, result.baseBranch, baseBranch);
2165
+ mergeSpinner.succeed(`Merged ${result.baseBranch} \u2192 ${baseBranch}`);
2166
+ } catch (err) {
2167
+ mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
2168
+ }
2169
+ }
2170
+ }
2171
+ let updateWiki = false;
2172
+ try {
2173
+ updateWiki = await select5({
2174
+ message: "Update TECHUNTER.md project overview?",
2175
+ choices: [
2176
+ { name: "Yes, regenerate", value: true },
2177
+ { name: "No, skip", value: false }
2178
+ ]
2179
+ });
2180
+ } catch {
2181
+ }
2182
+ if (updateWiki) {
2183
+ const wikiSpinner = ora6("Regenerating TECHUNTER.md\u2026").start();
2184
+ try {
2185
+ const content = await generateWiki(config);
2186
+ await upsertRepoFile(config, "TECHUNTER.md", content, "docs: update TECHUNTER.md project overview");
2187
+ wikiSpinner.succeed("TECHUNTER.md updated");
2188
+ } catch (err) {
2189
+ wikiSpinner.fail(`Wiki update failed: ${err.message}`);
2190
+ }
2191
+ }
2192
+ const mergeTarget = mergedIntoTaskBranch ? `${result.baseBranch} (sub-task merged, no push to main)` : result.baseBranch;
2193
+ return `Task #${issueNumber} accepted.
2194
+ PR #${result.prNumber} merged \u2192 ${mergeTarget}
2195
+ Issue closed.`;
1703
2196
  }
1704
- var execute8 = run8;
1705
- var terminal8 = true;
2197
+ async function execute6(input, config) {
2198
+ const issueNumber = input["issue_number"];
2199
+ const [me, issue] = await Promise.all([
2200
+ getAuthenticatedUser(config),
2201
+ getTask(config, issueNumber)
2202
+ ]);
2203
+ if (issue.author && issue.author !== me) {
2204
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2205
+ }
2206
+ const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2207
+ try {
2208
+ const result = await acceptTask(config, issueNumber);
2209
+ spinner.stop();
2210
+ return `Task #${issueNumber} accepted.
2211
+ PR #${result.prNumber} merged \u2192 ${result.baseBranch}
2212
+ Issue closed.`;
2213
+ } catch (err) {
2214
+ spinner.stop();
2215
+ return `Error: ${err.message}`;
2216
+ }
2217
+ }
2218
+ var terminal6 = true;
1706
2219
 
1707
2220
  // src/tools/reject/index.ts
1708
2221
  var reject_exports = {};
1709
2222
  __export(reject_exports, {
1710
- definition: () => definition9,
1711
- execute: () => execute9,
1712
- run: () => run9,
1713
- terminal: () => terminal9
2223
+ definition: () => definition7,
2224
+ execute: () => execute7,
2225
+ run: () => run7,
2226
+ terminal: () => terminal7
1714
2227
  });
1715
2228
  init_github();
1716
- import chalk8 from "chalk";
1717
- import { select as select5, input as promptInput3 } from "@inquirer/prompts";
2229
+ import chalk9 from "chalk";
2230
+ import { select as select6, input as promptInput3 } from "@inquirer/prompts";
1718
2231
  import ora7 from "ora";
1719
2232
 
1720
2233
  // src/tools/reject/prompts.ts
@@ -1746,7 +2259,7 @@ Reviewer feedback: ${userFeedback}`,
1746
2259
  }
1747
2260
 
1748
2261
  // src/tools/reject/index.ts
1749
- var definition9 = {
2262
+ var definition7 = {
1750
2263
  type: "function",
1751
2264
  function: {
1752
2265
  name: "reject",
@@ -1761,7 +2274,7 @@ var definition9 = {
1761
2274
  }
1762
2275
  }
1763
2276
  };
1764
- async function run9(input, config) {
2277
+ async function run7(input, config) {
1765
2278
  const issueNumber = input["issue_number"];
1766
2279
  const [me, issue] = await Promise.all([
1767
2280
  getAuthenticatedUser(config),
@@ -1779,7 +2292,7 @@ async function run9(input, config) {
1779
2292
  return "Cancelled.";
1780
2293
  }
1781
2294
  if (!feedback.trim()) return "Cancelled.";
1782
- const divider = chalk8.dim("\u2500".repeat(70));
2295
+ const divider = chalk9.dim("\u2500".repeat(70));
1783
2296
  for (; ; ) {
1784
2297
  const spinner = ora7("Generating rejection comment\u2026").start();
1785
2298
  let comment;
@@ -1791,13 +2304,13 @@ async function run9(input, config) {
1791
2304
  return `Error generating comment: ${err.message}`;
1792
2305
  }
1793
2306
  console.log("\n" + divider);
1794
- console.log(chalk8.bold(` Rejection preview \u2014 issue #${issueNumber}`));
2307
+ console.log(chalk9.bold(` Rejection preview \u2014 issue #${issueNumber}`));
1795
2308
  console.log(divider);
1796
2309
  console.log(renderMarkdown(comment));
1797
2310
  console.log(divider + "\n");
1798
2311
  let decision;
1799
2312
  try {
1800
- decision = await select5({
2313
+ decision = await select6({
1801
2314
  message: `Post rejection and mark #${issueNumber} as changes-needed?`,
1802
2315
  choices: [
1803
2316
  { name: "Post & Reject", value: "yes" },
@@ -1836,7 +2349,7 @@ async function run9(input, config) {
1836
2349
  return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
1837
2350
  }
1838
2351
  }
1839
- async function execute9(input, config) {
2352
+ async function execute7(input, config) {
1840
2353
  const issueNumber = input["issue_number"];
1841
2354
  const feedback = input["feedback"];
1842
2355
  const [me, issue] = await Promise.all([
@@ -1867,150 +2380,181 @@ async function execute9(input, config) {
1867
2380
  Comment posted:
1868
2381
  ${comment}`;
1869
2382
  }
1870
- var terminal9 = true;
2383
+ var terminal7 = true;
1871
2384
 
1872
- // src/tools/accept/index.ts
1873
- var accept_exports = {};
1874
- __export(accept_exports, {
1875
- definition: () => definition10,
1876
- execute: () => execute10,
1877
- run: () => run10,
1878
- terminal: () => terminal10
1879
- });
1880
- init_github();
1881
- import chalk9 from "chalk";
1882
- import { select as select6 } from "@inquirer/prompts";
1883
- import ora8 from "ora";
1884
- var definition10 = {
2385
+ // src/tools/review/index.ts
2386
+ var definition8 = {
1885
2387
  type: "function",
1886
2388
  function: {
1887
- name: "accept",
1888
- description: "Accept an in-review task: merges the PR into your worker branch and closes the issue.",
1889
- parameters: {
1890
- type: "object",
1891
- properties: {
1892
- issue_number: { type: "number", description: "GitHub issue number to accept" }
1893
- },
1894
- required: ["issue_number"]
1895
- }
2389
+ name: "review",
2390
+ description: "List tasks waiting for your review (submitted by others, created by you), then let you accept or reject one. Equivalent to /review.",
2391
+ parameters: { type: "object", properties: {}, required: [] }
1896
2392
  }
1897
2393
  };
1898
- async function run10(input, config) {
1899
- let issueNumber = input["issue_number"];
1900
- if (!issueNumber) {
1901
- const spinner3 = ora8("Loading tasks for review\u2026").start();
1902
- let tasks;
1903
- let me;
1904
- try {
1905
- me = await getAuthenticatedUser(config);
1906
- tasks = await listTasksForReview(config, me);
1907
- spinner3.stop();
1908
- } catch (err) {
1909
- spinner3.stop();
1910
- return `Error: ${err.message}`;
1911
- }
1912
- if (tasks.length === 0) return "No tasks pending review.";
1913
- try {
1914
- issueNumber = await select6({
1915
- message: "Which task to accept?",
1916
- choices: tasks.map((t) => ({
1917
- name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
1918
- value: t.number
1919
- }))
1920
- });
1921
- } catch {
1922
- return "Cancelled.";
1923
- }
1924
- }
1925
- const spinner2 = ora8("Verifying permissions\u2026").start();
1926
- let me2;
1927
- let issue;
2394
+ async function run8(_input, config) {
2395
+ const spinner = ora8("Loading tasks for review\u2026").start();
2396
+ let me;
2397
+ let tasks;
1928
2398
  try {
1929
- [me2, issue] = await Promise.all([
1930
- getAuthenticatedUser(config),
1931
- getTask(config, issueNumber)
1932
- ]);
1933
- spinner2.stop();
2399
+ me = await getAuthenticatedUser(config);
2400
+ tasks = await listTasksForReview(config, me);
2401
+ spinner.stop();
1934
2402
  } catch (err) {
1935
- spinner2.stop();
2403
+ spinner.stop();
1936
2404
  return `Error: ${err.message}`;
1937
2405
  }
1938
- if (issue.author && issue.author !== me2) {
1939
- return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
1940
- }
1941
- const targetBranch = makeWorkerBranchName(me2);
1942
- let confirmed;
2406
+ if (tasks.length === 0) return `No tasks pending review for @${me}.`;
2407
+ let issueNumber;
1943
2408
  try {
1944
- confirmed = await select6({
1945
- message: `Merge PR for #${issueNumber} into ${chalk9.cyan(targetBranch)} and close issue?`,
1946
- choices: [
1947
- { name: "Yes, accept", value: true },
1948
- { name: "Cancel", value: false }
1949
- ]
2409
+ issueNumber = await select7({
2410
+ message: "Select a task to review:",
2411
+ choices: tasks.map((t) => ({
2412
+ name: `#${String(t.number).padEnd(4)} @${t.assignee ?? "\u2014"} ${t.title}`,
2413
+ value: t.number
2414
+ }))
1950
2415
  });
1951
2416
  } catch {
1952
2417
  return "Cancelled.";
1953
2418
  }
1954
- if (!confirmed) return "Cancelled.";
1955
- const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
1956
- let result;
2419
+ const spinner2 = ora8(`Loading #${issueNumber}\u2026`).start();
2420
+ let pr;
1957
2421
  try {
1958
- const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
1959
- result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
1960
- spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
2422
+ pr = await getTaskPR(config, issueNumber);
2423
+ spinner2.stop();
1961
2424
  } catch (err) {
1962
- spinner.fail("Failed");
1963
- return `Error: ${err.message}`;
2425
+ spinner2.stop();
2426
+ return `Error loading PR: ${err.message}`;
1964
2427
  }
1965
- const baseBranch = config.baseBranch ?? "main";
1966
- let pushToMain;
1967
- try {
1968
- pushToMain = await select6({
1969
- message: `Push ${chalk9.cyan(targetBranch)} \u2192 ${chalk9.cyan(baseBranch)}?`,
1970
- choices: [
1971
- { name: `Yes, push to ${baseBranch}`, value: true },
1972
- { name: "No, keep in worker branch", value: false }
1973
- ]
1974
- });
1975
- } catch {
1976
- pushToMain = false;
2428
+ const divider = chalk10.dim("\u2500".repeat(70));
2429
+ console.log("\n" + divider);
2430
+ if (pr) {
2431
+ console.log(chalk10.bold(` PR #${pr.number}`) + " " + chalk10.dim(pr.url));
2432
+ console.log(divider);
2433
+ console.log(renderMarkdown(pr.body));
2434
+ } else {
2435
+ console.log(chalk10.yellow(` No open PR found for task #${issueNumber}`));
1977
2436
  }
1978
- if (pushToMain) {
1979
- const mergeSpinner = ora8(`Merging ${targetBranch} \u2192 ${baseBranch}\u2026`).start();
2437
+ console.log(divider + "\n");
2438
+ for (; ; ) {
2439
+ let action;
1980
2440
  try {
1981
- await mergeWorkerIntoBase(config, targetBranch, baseBranch);
1982
- mergeSpinner.succeed(`Merged ${targetBranch} \u2192 ${baseBranch}`);
1983
- } catch (err) {
1984
- mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
2441
+ action = await select7({
2442
+ message: "Review action:",
2443
+ choices: [
2444
+ ...pr ? [{ name: "View diff", value: "diff" }] : [],
2445
+ { name: chalk10.green("Accept") + " \u2014 merge PR and close issue", value: "accept" },
2446
+ { name: chalk10.red("Reject") + " \u2014 request changes", value: "reject" },
2447
+ { name: "Nothing, just viewing", value: "none" }
2448
+ ]
2449
+ });
2450
+ } catch {
2451
+ return "Cancelled.";
2452
+ }
2453
+ if (action === "none") return `Viewed task #${issueNumber}.`;
2454
+ if (action === "accept") return run6({ issue_number: issueNumber }, config);
2455
+ if (action === "reject") return run7({ issue_number: issueNumber }, config);
2456
+ if (action === "diff") {
2457
+ const diffSpinner = ora8("Fetching diff\u2026").start();
2458
+ let diff;
2459
+ try {
2460
+ diff = await getTaskPRDiff(config, pr.number);
2461
+ diffSpinner.stop();
2462
+ } catch (err) {
2463
+ diffSpinner.stop();
2464
+ console.log(chalk10.red(`Error fetching diff: ${err.message}`));
2465
+ continue;
2466
+ }
2467
+ console.log("\n" + divider);
2468
+ for (const line of diff.split("\n")) {
2469
+ if (line.startsWith("+") && !line.startsWith("+++")) {
2470
+ process.stdout.write(chalk10.green(line) + "\n");
2471
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
2472
+ process.stdout.write(chalk10.red(line) + "\n");
2473
+ } else if (line.startsWith("@@")) {
2474
+ process.stdout.write(chalk10.cyan(line) + "\n");
2475
+ } else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
2476
+ process.stdout.write(chalk10.bold(line) + "\n");
2477
+ } else {
2478
+ process.stdout.write(line + "\n");
2479
+ }
2480
+ }
2481
+ console.log(divider + "\n");
1985
2482
  }
1986
2483
  }
1987
- return `Task #${issueNumber} accepted.
1988
- PR #${result.prNumber} merged \u2192 ${targetBranch}${pushToMain ? ` \u2192 ${baseBranch}` : ""}
1989
- Issue closed.`;
1990
2484
  }
1991
- async function execute10(input, config) {
1992
- const issueNumber = input["issue_number"];
1993
- const [me, issue] = await Promise.all([
1994
- getAuthenticatedUser(config),
1995
- getTask(config, issueNumber)
1996
- ]);
1997
- if (issue.author && issue.author !== me) {
1998
- return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2485
+ var execute8 = run8;
2486
+ var terminal8 = true;
2487
+
2488
+ // src/tools/refresh/index.ts
2489
+ var refresh_exports = {};
2490
+ __export(refresh_exports, {
2491
+ definition: () => definition9,
2492
+ execute: () => execute9,
2493
+ run: () => run9,
2494
+ terminal: () => terminal9
2495
+ });
2496
+ var definition9 = {
2497
+ type: "function",
2498
+ function: {
2499
+ name: "refresh",
2500
+ description: "Reload and display the full task list. Equivalent to /refresh.",
2501
+ parameters: { type: "object", properties: {}, required: [] }
1999
2502
  }
2000
- const targetBranch = makeWorkerBranchName(me);
2001
- const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
2503
+ };
2504
+ async function run9(_input, config) {
2505
+ const tasks = await printTaskList(config);
2506
+ if (tasks.length === 0) return "No tasks found.";
2507
+ const lines = tasks.map((t) => {
2508
+ const status = getStatus(t);
2509
+ const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
2510
+ return `#${t.number} [${status}] ${assignee} ${t.title}`;
2511
+ });
2512
+ return `Tasks (${tasks.length}):
2513
+ ${lines.join("\n")}`;
2514
+ }
2515
+ var execute9 = run9;
2516
+ var terminal9 = true;
2517
+
2518
+ // src/tools/open-code/index.ts
2519
+ var open_code_exports = {};
2520
+ __export(open_code_exports, {
2521
+ definition: () => definition10,
2522
+ execute: () => execute10,
2523
+ run: () => run10,
2524
+ terminal: () => terminal10
2525
+ });
2526
+ init_github();
2527
+ var definition10 = {
2528
+ type: "function",
2529
+ function: {
2530
+ name: "open_code",
2531
+ description: "Launch Claude Code for the current task branch. Equivalent to /code.",
2532
+ parameters: { type: "object", properties: {}, required: [] }
2533
+ }
2534
+ };
2535
+ async function run10(_input, config) {
2536
+ let branch;
2002
2537
  try {
2003
- const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
2004
- const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
2005
- spinner.stop();
2006
- return `Task #${issueNumber} accepted.
2007
- PR #${result.prNumber} merged \u2192 ${targetBranch}
2008
- Issue closed.`;
2538
+ branch = await getCurrentBranch();
2539
+ } catch (err) {
2540
+ return `Error: ${err.message}`;
2541
+ }
2542
+ let issueNum = getConfig().taskState?.activeIssueNumber;
2543
+ if (!issueNum) {
2544
+ const found = await getIssueNumberFromBranch(config, branch);
2545
+ if (!found) return `No active task found (current branch: ${branch}).`;
2546
+ issueNum = found.issueNumber;
2547
+ }
2548
+ let issue;
2549
+ try {
2550
+ issue = await getTask(config, issueNum);
2009
2551
  } catch (err) {
2010
- spinner.stop();
2011
2552
  return `Error: ${err.message}`;
2012
2553
  }
2554
+ await launchClaudeCode(issue, branch);
2555
+ return "Claude Code session ended.";
2013
2556
  }
2557
+ var execute10 = run10;
2014
2558
  var terminal10 = true;
2015
2559
 
2016
2560
  // src/tools/edit-task/index.ts
@@ -2022,7 +2566,7 @@ __export(edit_task_exports, {
2022
2566
  terminal: () => terminal11
2023
2567
  });
2024
2568
  init_github();
2025
- import { select as select7, input as promptInput4 } from "@inquirer/prompts";
2569
+ import { select as select8, input as promptInput4 } from "@inquirer/prompts";
2026
2570
  import ora9 from "ora";
2027
2571
  var definition11 = {
2028
2572
  type: "function",
@@ -2051,7 +2595,7 @@ async function run11(input, config) {
2051
2595
  }
2052
2596
  if (tasks.length === 0) return "No tasks found.";
2053
2597
  try {
2054
- issueNumber = await select7({
2598
+ issueNumber = await select8({
2055
2599
  message: "Select task to edit:",
2056
2600
  choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
2057
2601
  });
@@ -2108,14 +2652,140 @@ async function execute11(input, config) {
2108
2652
  }
2109
2653
  var terminal11 = true;
2110
2654
 
2655
+ // src/tools/wiki/index.ts
2656
+ var wiki_exports = {};
2657
+ __export(wiki_exports, {
2658
+ definition: () => definition12,
2659
+ execute: () => execute12,
2660
+ run: () => run12,
2661
+ terminal: () => terminal12
2662
+ });
2663
+ import ora10 from "ora";
2664
+ import chalk11 from "chalk";
2665
+ import { readFile as readFile2 } from "fs/promises";
2666
+ import { select as select9 } from "@inquirer/prompts";
2667
+ init_github();
2668
+ var WIKI_PATH = "TECHUNTER.md";
2669
+ var definition12 = {
2670
+ type: "function",
2671
+ function: {
2672
+ name: "update_wiki",
2673
+ 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.",
2674
+ parameters: {
2675
+ type: "object",
2676
+ properties: {},
2677
+ required: []
2678
+ }
2679
+ }
2680
+ };
2681
+ async function readWikiContent(config) {
2682
+ try {
2683
+ return await readFile2(WIKI_PATH, "utf-8");
2684
+ } catch {
2685
+ }
2686
+ return getRepoFile(config, WIKI_PATH);
2687
+ }
2688
+ function printWiki(content) {
2689
+ const divider = chalk11.dim("\u2500".repeat(70));
2690
+ console.log("\n" + divider);
2691
+ console.log(chalk11.bold(" TECHUNTER.md"));
2692
+ console.log(divider);
2693
+ console.log(renderMarkdown(content));
2694
+ console.log(divider + "\n");
2695
+ }
2696
+ async function run12(_input, config) {
2697
+ const fetchSpinner = ora10("Checking for existing wiki\u2026").start();
2698
+ const existing = await readWikiContent(config).catch(() => null);
2699
+ fetchSpinner.stop();
2700
+ let action;
2701
+ try {
2702
+ action = await select9({
2703
+ message: "TECHUNTER.md \u2014 what would you like to do?",
2704
+ choices: [
2705
+ ...existing ? [{ name: "View current wiki", value: "view" }] : [],
2706
+ { name: existing ? "Regenerate & commit to repo" : "Generate & commit to repo", value: "generate" },
2707
+ { name: "Cancel", value: "cancel" }
2708
+ ]
2709
+ });
2710
+ } catch {
2711
+ return "Cancelled.";
2712
+ }
2713
+ if (action === "cancel") return "Cancelled.";
2714
+ if (action === "view") {
2715
+ printWiki(existing);
2716
+ return "Displayed TECHUNTER.md.";
2717
+ }
2718
+ const authSpinner = ora10("Checking permissions\u2026").start();
2719
+ let me;
2720
+ let allowed;
2721
+ try {
2722
+ me = await getAuthenticatedUser(config);
2723
+ allowed = await isCollaborator(config, me);
2724
+ authSpinner.stop();
2725
+ } catch (err) {
2726
+ authSpinner.stop();
2727
+ return `Error checking permissions: ${err.message}`;
2728
+ }
2729
+ if (!allowed) {
2730
+ return `Permission denied: only repository collaborators can update the project wiki.`;
2731
+ }
2732
+ const genSpinner = ora10("Analyzing project and generating overview\u2026").start();
2733
+ let content;
2734
+ try {
2735
+ content = await generateWiki(config);
2736
+ genSpinner.stop();
2737
+ } catch (err) {
2738
+ genSpinner.stop();
2739
+ return `Error generating wiki: ${err.message}`;
2740
+ }
2741
+ printWiki(content);
2742
+ let confirm;
2743
+ try {
2744
+ confirm = await select9({
2745
+ message: `Publish to repository as ${WIKI_PATH}?`,
2746
+ choices: [
2747
+ { name: "Yes, commit to repo", value: "publish" },
2748
+ { name: "Cancel", value: "cancel" }
2749
+ ]
2750
+ });
2751
+ } catch {
2752
+ return "Cancelled.";
2753
+ }
2754
+ if (confirm === "cancel") return "Cancelled.";
2755
+ const writeSpinner = ora10(`Writing ${WIKI_PATH}\u2026`).start();
2756
+ try {
2757
+ const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2758
+ writeSpinner.succeed(`Written: ${url}`);
2759
+ console.log("");
2760
+ return `TECHUNTER.md updated \u2014 ${url}`;
2761
+ } catch (err) {
2762
+ writeSpinner.fail(`Failed: ${err.message}`);
2763
+ return `Error: ${err.message}`;
2764
+ }
2765
+ }
2766
+ async function execute12(_input, config) {
2767
+ const me = await getAuthenticatedUser(config);
2768
+ if (!await isCollaborator(config, me)) {
2769
+ return `Permission denied: only repository collaborators can update the project wiki.`;
2770
+ }
2771
+ const content = await generateWiki(config);
2772
+ try {
2773
+ const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2774
+ return `TECHUNTER.md updated \u2014 ${url}`;
2775
+ } catch (err) {
2776
+ return `Error: ${err.message}`;
2777
+ }
2778
+ }
2779
+ var terminal12 = true;
2780
+
2111
2781
  // src/tools/list-tasks/index.ts
2112
2782
  var list_tasks_exports = {};
2113
2783
  __export(list_tasks_exports, {
2114
- definition: () => definition12,
2115
- execute: () => execute12
2784
+ definition: () => definition13,
2785
+ execute: () => execute13
2116
2786
  });
2117
2787
  init_github();
2118
- var definition12 = {
2788
+ var definition13 = {
2119
2789
  type: "function",
2120
2790
  function: {
2121
2791
  name: "list_tasks",
@@ -2127,7 +2797,7 @@ var definition12 = {
2127
2797
  }
2128
2798
  }
2129
2799
  };
2130
- async function execute12(_input, config) {
2800
+ async function execute13(_input, config) {
2131
2801
  const tasks = await listTasks(config);
2132
2802
  if (tasks.length === 0) return "No open tasks.";
2133
2803
  return tasks.map((t) => {
@@ -2140,11 +2810,11 @@ async function execute12(_input, config) {
2140
2810
  // src/tools/get-task/index.ts
2141
2811
  var get_task_exports = {};
2142
2812
  __export(get_task_exports, {
2143
- definition: () => definition13,
2144
- execute: () => execute13
2813
+ definition: () => definition14,
2814
+ execute: () => execute14
2145
2815
  });
2146
2816
  init_github();
2147
- var definition13 = {
2817
+ var definition14 = {
2148
2818
  type: "function",
2149
2819
  function: {
2150
2820
  name: "get_task",
@@ -2158,7 +2828,7 @@ var definition13 = {
2158
2828
  }
2159
2829
  }
2160
2830
  };
2161
- async function execute13(input, config) {
2831
+ async function execute14(input, config) {
2162
2832
  const issue = await getTask(config, input["issue_number"]);
2163
2833
  const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2164
2834
  const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
@@ -2175,12 +2845,12 @@ ${issue.body}`);
2175
2845
  // src/tools/get-comments/index.ts
2176
2846
  var get_comments_exports = {};
2177
2847
  __export(get_comments_exports, {
2178
- definition: () => definition14,
2179
- execute: () => execute14
2848
+ definition: () => definition15,
2849
+ execute: () => execute15
2180
2850
  });
2181
2851
  init_github();
2182
- import ora10 from "ora";
2183
- var definition14 = {
2852
+ import ora11 from "ora";
2853
+ var definition15 = {
2184
2854
  type: "function",
2185
2855
  function: {
2186
2856
  name: "get_comments",
@@ -2195,10 +2865,10 @@ var definition14 = {
2195
2865
  }
2196
2866
  }
2197
2867
  };
2198
- async function execute14(input, config) {
2868
+ async function execute15(input, config) {
2199
2869
  const issueNumber = input["issue_number"];
2200
2870
  const limit = input["limit"] ?? 5;
2201
- const spinner = ora10(`Loading comments for #${issueNumber}...`).start();
2871
+ const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
2202
2872
  try {
2203
2873
  const comments = await listComments(config, issueNumber, limit);
2204
2874
  spinner.stop();
@@ -2217,11 +2887,11 @@ ${lines.join("\n\n")}`;
2217
2887
  // src/tools/get-diff/index.ts
2218
2888
  var get_diff_exports = {};
2219
2889
  __export(get_diff_exports, {
2220
- definition: () => definition15,
2221
- execute: () => execute15
2890
+ definition: () => definition16,
2891
+ execute: () => execute16
2222
2892
  });
2223
- import ora11 from "ora";
2224
- var definition15 = {
2893
+ import ora12 from "ora";
2894
+ var definition16 = {
2225
2895
  type: "function",
2226
2896
  function: {
2227
2897
  name: "get_diff",
@@ -2229,8 +2899,8 @@ var definition15 = {
2229
2899
  parameters: { type: "object", properties: {}, required: [] }
2230
2900
  }
2231
2901
  };
2232
- async function execute15(_input, _config) {
2233
- const spinner = ora11("Reading git diff...").start();
2902
+ async function execute16(_input, _config) {
2903
+ const spinner = ora12("Reading git diff...").start();
2234
2904
  try {
2235
2905
  const diff = await getDiff();
2236
2906
  spinner.stop();
@@ -2244,14 +2914,14 @@ async function execute15(_input, _config) {
2244
2914
  // src/tools/run-command/index.ts
2245
2915
  var run_command_exports = {};
2246
2916
  __export(run_command_exports, {
2247
- definition: () => definition16,
2248
- execute: () => execute16
2917
+ definition: () => definition17,
2918
+ execute: () => execute17
2249
2919
  });
2250
2920
  import { exec } from "child_process";
2251
2921
  import { promisify } from "util";
2252
- import ora12 from "ora";
2922
+ import ora13 from "ora";
2253
2923
  var execAsync = promisify(exec);
2254
- var definition16 = {
2924
+ var definition17 = {
2255
2925
  type: "function",
2256
2926
  function: {
2257
2927
  name: "run_command",
@@ -2265,10 +2935,10 @@ var definition16 = {
2265
2935
  }
2266
2936
  }
2267
2937
  };
2268
- async function execute16(input, _config) {
2938
+ async function execute17(input, _config) {
2269
2939
  const command = input["command"];
2270
2940
  const cwd = process.cwd();
2271
- const spinner = ora12(`$ ${command}`).start();
2941
+ const spinner = ora13(`$ ${command}`).start();
2272
2942
  try {
2273
2943
  const { stdout, stderr } = await execAsync(command, { cwd, timeout: 6e4, maxBuffer: 1024 * 1024 });
2274
2944
  spinner.stop();
@@ -2287,15 +2957,15 @@ ${out || e.message}`;
2287
2957
  // src/tools/list-files/index.ts
2288
2958
  var list_files_exports = {};
2289
2959
  __export(list_files_exports, {
2290
- definition: () => definition17,
2291
- execute: () => execute17
2960
+ definition: () => definition18,
2961
+ execute: () => execute18
2292
2962
  });
2293
- import { readFile as readFile2 } from "fs/promises";
2963
+ import { readFile as readFile3 } from "fs/promises";
2294
2964
  import { existsSync } from "fs";
2295
2965
  import path2 from "path";
2296
2966
  import { globby } from "globby";
2297
2967
  import ignore from "ignore";
2298
- var definition17 = {
2968
+ var definition18 = {
2299
2969
  type: "function",
2300
2970
  function: {
2301
2971
  name: "list_files",
@@ -2335,13 +3005,13 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2335
3005
  ".sqlite",
2336
3006
  ".lock"
2337
3007
  ]);
2338
- async function execute17(input, _config) {
3008
+ async function execute18(input, _config) {
2339
3009
  const glob = input["glob"] ?? "**/*";
2340
3010
  const cwd = process.cwd();
2341
3011
  const ig = ignore();
2342
3012
  const gitignorePath = path2.join(cwd, ".gitignore");
2343
3013
  if (existsSync(gitignorePath)) {
2344
- ig.add(await readFile2(gitignorePath, "utf-8"));
3014
+ ig.add(await readFile3(gitignorePath, "utf-8"));
2345
3015
  }
2346
3016
  ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
2347
3017
  const files = await globby(glob, { cwd, dot: false, onlyFiles: true, gitignore: false });
@@ -2354,15 +3024,15 @@ ${filtered.join("\n")}`;
2354
3024
  // src/tools/grep-code/index.ts
2355
3025
  var grep_code_exports = {};
2356
3026
  __export(grep_code_exports, {
2357
- definition: () => definition18,
2358
- execute: () => execute18
3027
+ definition: () => definition19,
3028
+ execute: () => execute19
2359
3029
  });
2360
- import { readFile as readFile3 } from "fs/promises";
3030
+ import { readFile as readFile4 } from "fs/promises";
2361
3031
  import { existsSync as existsSync2 } from "fs";
2362
3032
  import path3 from "path";
2363
3033
  import { globby as globby2 } from "globby";
2364
3034
  import ignore2 from "ignore";
2365
- var definition18 = {
3035
+ var definition19 = {
2366
3036
  type: "function",
2367
3037
  function: {
2368
3038
  name: "grep_code",
@@ -2403,7 +3073,7 @@ async function buildIgnore(cwd) {
2403
3073
  const ig = ignore2();
2404
3074
  const gitignorePath = path3.join(cwd, ".gitignore");
2405
3075
  if (existsSync2(gitignorePath)) {
2406
- ig.add(await readFile3(gitignorePath, "utf-8"));
3076
+ ig.add(await readFile4(gitignorePath, "utf-8"));
2407
3077
  }
2408
3078
  ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
2409
3079
  return ig;
@@ -2435,7 +3105,7 @@ function isText(f) {
2435
3105
  return !BINARY_EXTENSIONS2.has(path3.extname(f).toLowerCase());
2436
3106
  }
2437
3107
  var MAX_RANGE_LINES = 300;
2438
- async function execute18(input, _config) {
3108
+ async function execute19(input, _config) {
2439
3109
  const pattern = input["pattern"] ?? "";
2440
3110
  const fileGlob = input["file_glob"] ?? "**/*";
2441
3111
  const contextLines = Math.min(input["context_lines"] ?? 2, 5);
@@ -2447,7 +3117,7 @@ async function execute18(input, _config) {
2447
3117
  const files = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
2448
3118
  if (files.length === 0) return `No file matched: ${fileGlob}`;
2449
3119
  if (files.length > 1) return `file_glob matched ${files.length} files \u2014 narrow it to a single file for read-range mode.`;
2450
- const raw = await readFile3(path3.join(cwd, files[0]), "utf-8");
3120
+ const raw = await readFile4(path3.join(cwd, files[0]), "utf-8");
2451
3121
  const lines = raw.split("\n");
2452
3122
  const total = lines.length;
2453
3123
  const from = Math.max(1, startLine);
@@ -2476,7 +3146,7 @@ ${numbered}
2476
3146
  if (totalMatches >= maxResults) break;
2477
3147
  let content;
2478
3148
  try {
2479
- content = await readFile3(path3.join(cwd, file), "utf-8");
3149
+ content = await readFile4(path3.join(cwd, file), "utf-8");
2480
3150
  } catch {
2481
3151
  continue;
2482
3152
  }
@@ -2522,12 +3192,12 @@ ${snippets.join("\n---\n")}
2522
3192
  // src/tools/ask-user/index.ts
2523
3193
  var ask_user_exports = {};
2524
3194
  __export(ask_user_exports, {
2525
- definition: () => definition19,
2526
- execute: () => execute19
3195
+ definition: () => definition20,
3196
+ execute: () => execute20
2527
3197
  });
2528
- import chalk10 from "chalk";
2529
- import { select as select8, input as promptInput5 } from "@inquirer/prompts";
2530
- var definition19 = {
3198
+ import chalk12 from "chalk";
3199
+ import { select as select10, input as promptInput5 } from "@inquirer/prompts";
3200
+ var definition20 = {
2531
3201
  type: "function",
2532
3202
  function: {
2533
3203
  name: "ask_user",
@@ -2546,24 +3216,24 @@ var definition19 = {
2546
3216
  }
2547
3217
  }
2548
3218
  };
2549
- async function execute19(input, _config) {
3219
+ async function execute20(input, _config) {
2550
3220
  const question = input["question"];
2551
3221
  const options = input["options"];
2552
3222
  const OTHER = "__other__";
2553
3223
  console.log("");
2554
- console.log(chalk10.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
2555
- console.log(chalk10.dim(" \u2502"));
3224
+ console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
3225
+ console.log(chalk12.dim(" \u2502"));
2556
3226
  for (const line of question.split("\n")) {
2557
- console.log(chalk10.dim(" \u2502 ") + line);
3227
+ console.log(chalk12.dim(" \u2502 ") + line);
2558
3228
  }
2559
- console.log(chalk10.dim(" \u2514" + "\u2500".repeat(67)));
3229
+ console.log(chalk12.dim(" \u2514" + "\u2500".repeat(67)));
2560
3230
  let answer;
2561
3231
  try {
2562
- const chosen = await select8({
3232
+ const chosen = await select10({
2563
3233
  message: " ",
2564
3234
  choices: [
2565
3235
  ...options.map((o) => ({ name: o, value: o })),
2566
- { name: chalk10.dim("Other (describe below)"), value: OTHER }
3236
+ { name: chalk12.dim("Other (describe below)"), value: OTHER }
2567
3237
  ]
2568
3238
  });
2569
3239
  answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
@@ -2588,6 +3258,7 @@ var toolModules = [
2588
3258
  reject_exports,
2589
3259
  accept_exports,
2590
3260
  edit_task_exports,
3261
+ wiki_exports,
2591
3262
  // Low-level tools
2592
3263
  list_tasks_exports,
2593
3264
  get_task_exports,