techunter 0.1.1 → 0.1.3

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 +23 -11
  2. package/dist/index.js +308 -159
  3. package/dist/mcp.js +299 -104
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21,9 +21,9 @@ __export(github_exports, {
21
21
  ensureLabels: () => ensureLabels,
22
22
  formatGuideAsMarkdown: () => formatGuideAsMarkdown,
23
23
  getAuthenticatedUser: () => getAuthenticatedUser,
24
- getBaseBranch: () => getBaseBranch,
25
24
  getDefaultBranch: () => getDefaultBranch,
26
25
  getTask: () => getTask,
26
+ isCollaborator: () => isCollaborator,
27
27
  listComments: () => listComments,
28
28
  listMyTasks: () => listMyTasks,
29
29
  listTasks: () => listTasks,
@@ -50,6 +50,7 @@ function parseIssue(issue) {
50
50
  title: issue.title,
51
51
  body: issue.body ?? null,
52
52
  state: issue.state,
53
+ author: issue.user?.login ?? null,
53
54
  assignee: issue.assignee?.login ?? null,
54
55
  labels: (issue.labels ?? []).map(
55
56
  (l) => typeof l === "string" ? l : l.name ?? ""
@@ -226,6 +227,16 @@ async function getAuthenticatedUser(config) {
226
227
  const { data } = await octokit.users.getAuthenticated();
227
228
  return data.login;
228
229
  }
230
+ async function isCollaborator(config, username) {
231
+ const octokit = createOctokit(config.githubToken);
232
+ const { owner, repo } = config.github;
233
+ try {
234
+ const { data } = await octokit.repos.getCollaboratorPermissionLevel({ owner, repo, username });
235
+ return data.permission === "admin" || data.permission === "write" || data.permission === "maintain";
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
229
240
  async function listMyTasks(config, username) {
230
241
  const octokit = createOctokit(config.githubToken);
231
242
  const { owner, repo } = config.github;
@@ -297,10 +308,6 @@ async function getDefaultBranch(config) {
297
308
  const { data } = await octokit.repos.get({ owner, repo });
298
309
  return data.default_branch;
299
310
  }
300
- function getBaseBranch(config) {
301
- if (config.github.baseBranch) return Promise.resolve(config.github.baseBranch);
302
- return getDefaultBranch(config);
303
- }
304
311
  async function acceptTask(config, issueNumber) {
305
312
  const octokit = createOctokit(config.githubToken);
306
313
  const { owner, repo } = config.github;
@@ -338,7 +345,6 @@ var init_github = __esm({
338
345
  import chalk14 from "chalk";
339
346
  import readline from "readline";
340
347
  import { createRequire } from "module";
341
- import { input as input3 } from "@inquirer/prompts";
342
348
 
343
349
  // src/commands/init.ts
344
350
  import { input, password, select } from "@inquirer/prompts";
@@ -358,9 +364,12 @@ var configSchema = z.object({
358
364
  githubClientId: z.string().optional(),
359
365
  github: z.object({
360
366
  owner: z.string().min(1),
361
- repo: z.string().min(1),
362
- baseBranch: z.string().optional()
363
- })
367
+ repo: z.string().min(1)
368
+ }),
369
+ taskState: z.object({
370
+ activeIssueNumber: z.number().optional(),
371
+ baseCommit: z.string().optional()
372
+ }).optional()
364
373
  });
365
374
  var store = new Conf({
366
375
  projectName: "techunter",
@@ -397,6 +406,12 @@ function setConfig(partial) {
397
406
  if (partial.githubClientId !== void 0) {
398
407
  current["githubClientId"] = partial.githubClientId;
399
408
  }
409
+ if (partial.taskState !== void 0) {
410
+ current["taskState"] = {
411
+ ...current["taskState"] ?? {},
412
+ ...partial.taskState
413
+ };
414
+ }
400
415
  store.store = current;
401
416
  }
402
417
  function getConfigPath() {
@@ -414,9 +429,6 @@ async function getCurrentBranch() {
414
429
  const summary = await git.branch();
415
430
  return summary.current;
416
431
  }
417
- async function createAndSwitchBranch(name) {
418
- await git.checkoutLocalBranch(name);
419
- }
420
432
  async function pushBranch(name) {
421
433
  await git.push("origin", name, ["--set-upstream"]);
422
434
  }
@@ -444,6 +456,61 @@ function makeBranchName(issueNumber, username) {
444
456
  const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
445
457
  return `task-${issueNumber}-${slug}`;
446
458
  }
459
+ function makeWorkerBranchName(username) {
460
+ const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
461
+ return `worker-${slug}`;
462
+ }
463
+ async function getCurrentCommit() {
464
+ return (await git.revparse(["HEAD"])).trim();
465
+ }
466
+ async function switchToBranchOrCreate(name) {
467
+ try {
468
+ const branches = await git.branch(["-a"]);
469
+ const exists = Object.keys(branches.branches).some(
470
+ (b) => b === name || b === `remotes/origin/${name}`
471
+ );
472
+ if (exists) {
473
+ await git.checkout(name);
474
+ return false;
475
+ }
476
+ await git.checkoutLocalBranch(name);
477
+ return true;
478
+ } catch {
479
+ await git.checkoutLocalBranch(name);
480
+ return true;
481
+ }
482
+ }
483
+ async function getDiffFromCommit(baseCommit) {
484
+ const status = await git.status();
485
+ const parts = [];
486
+ const fileLines = [
487
+ ...status.modified.map((f) => ` M ${f}`),
488
+ ...status.created.map((f) => ` A ${f}`),
489
+ ...status.deleted.map((f) => ` D ${f}`),
490
+ ...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
491
+ ...status.not_added.map((f) => ` ? ${f}`)
492
+ ];
493
+ if (fileLines.length > 0) {
494
+ parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
495
+ const uncommitted = await git.diff(["HEAD"]);
496
+ if (uncommitted) {
497
+ const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
498
+ parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
499
+ }
500
+ }
501
+ const log = await git.log({ from: baseCommit, to: "HEAD" });
502
+ if (log.total > 0) {
503
+ const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
504
+ parts.push(`## Commits since task claimed (${log.total} total)
505
+ ` + logLines.join("\n"));
506
+ const branchDiff = await git.diff([baseCommit, "HEAD"]);
507
+ if (branchDiff) {
508
+ const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
509
+ parts.push("## Full diff since task claimed\n```diff\n" + capped + "\n```");
510
+ }
511
+ }
512
+ return parts.length > 0 ? parts.join("\n\n") : "No changes since task was claimed.";
513
+ }
447
514
  async function findMergeBase(configuredBase) {
448
515
  const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
449
516
  const unique = [...new Set(candidates)];
@@ -628,11 +695,6 @@ async function initCommand() {
628
695
  required: true
629
696
  });
630
697
  }
631
- const detectedDefault = "main";
632
- const baseBranch = await input({
633
- message: "Main branch to merge PRs into:",
634
- default: detectedDefault
635
- });
636
698
  const config = {
637
699
  githubToken,
638
700
  githubClientId,
@@ -641,8 +703,7 @@ async function initCommand() {
641
703
  ...aiModel ? { aiModel } : {},
642
704
  github: {
643
705
  owner: owner.trim(),
644
- repo: repo.trim(),
645
- baseBranch: baseBranch.trim() || detectedDefault
706
+ repo: repo.trim()
646
707
  }
647
708
  };
648
709
  setConfig(config);
@@ -678,7 +739,6 @@ async function configCommand() {
678
739
  const field = await select2({
679
740
  message: "Which setting to change?",
680
741
  choices: [
681
- { name: `Base branch ${chalk3.dim(config.github.baseBranch ?? "(not set, uses repo default)")}`, value: "baseBranch" },
682
742
  { name: `GitHub repo ${chalk3.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
683
743
  { name: `AI base URL ${chalk3.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
684
744
  { name: `AI model ${chalk3.dim(currentModel)}`, value: "aiModel" },
@@ -688,16 +748,7 @@ async function configCommand() {
688
748
  ]
689
749
  });
690
750
  if (field === "cancel") return;
691
- if (field === "baseBranch") {
692
- const val = await input2({
693
- message: "Main branch to merge PRs into:",
694
- default: config.github.baseBranch ?? "main"
695
- });
696
- setConfig({ github: { ...config.github, baseBranch: val.trim() || "main" } });
697
- console.log(chalk3.green(`
698
- Base branch set to: ${val.trim() || "main"}
699
- `));
700
- } else if (field === "repo") {
751
+ if (field === "repo") {
701
752
  const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
702
753
  const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
703
754
  setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
@@ -921,8 +972,8 @@ import { select as select3, input as promptInput } from "@inquirer/prompts";
921
972
 
922
973
  // src/lib/agent-ui.ts
923
974
  import chalk6 from "chalk";
924
- function formatInput(input4) {
925
- return Object.entries(input4).map(([k, v]) => {
975
+ function formatInput(input3) {
976
+ return Object.entries(input3).map(([k, v]) => {
926
977
  if (typeof v === "number") return `${k}=${v}`;
927
978
  if (typeof v === "string") {
928
979
  if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
@@ -935,8 +986,8 @@ function summarize(result) {
935
986
  const first = result.split("\n").find((l) => l.trim()) ?? result;
936
987
  return first.length > 100 ? first.slice(0, 97) + "..." : first;
937
988
  }
938
- function printToolCall(name, input4) {
939
- const params = formatInput(input4);
989
+ function printToolCall(name, input3) {
990
+ const params = formatInput(input3);
940
991
  console.log(` ${chalk6.cyan("\u2192")} ${chalk6.bold(name)}${params ? " " + chalk6.dim(params) : ""}`);
941
992
  }
942
993
  function printToolResult(result) {
@@ -972,15 +1023,15 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
972
1023
  }
973
1024
  if (choice.finish_reason === "tool_calls") {
974
1025
  for (const tc of choice.message.tool_calls ?? []) {
975
- let input4;
1026
+ let input3;
976
1027
  try {
977
- input4 = JSON.parse(tc.function.arguments);
1028
+ input3 = JSON.parse(tc.function.arguments);
978
1029
  } catch {
979
- input4 = {};
1030
+ input3 = {};
980
1031
  }
981
- printToolCall(tc.function.name, input4);
1032
+ printToolCall(tc.function.name, input3);
982
1033
  const mod = selected.find((m) => m.definition.function.name === tc.function.name);
983
- const result = mod ? await mod.execute(input4, config) : `Unknown tool: ${tc.function.name}`;
1034
+ const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
984
1035
  printToolResult(result);
985
1036
  messages.push({ role: "tool", tool_call_id: tc.id, content: result });
986
1037
  }
@@ -989,7 +1040,7 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
989
1040
  }
990
1041
 
991
1042
  // src/tools/submit/prompts.ts
992
- var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. 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.";
1043
+ 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.";
993
1044
 
994
1045
  // src/tools/submit/reviewer.ts
995
1046
  async function reviewChanges(config, issueNumber, issue, diff) {
@@ -1023,32 +1074,41 @@ var definition = {
1023
1074
  }
1024
1075
  };
1025
1076
  async function run(_input, config) {
1026
- const branch = await getCurrentBranch();
1027
- const match = branch.match(/^task-(\d+)-/);
1028
- if (!match) {
1029
- return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
1077
+ const taskState = getConfig().taskState;
1078
+ const issueNumber = taskState?.activeIssueNumber;
1079
+ if (!issueNumber) {
1080
+ return "No active task found. Claim a task first with /pick.";
1030
1081
  }
1031
- const issueNumber = parseInt(match[1], 10);
1032
1082
  let spinner = ora2("Loading task and diff\u2026").start();
1033
- const [issue, defaultBranch, diff] = await Promise.all([
1083
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1084
+ const [issue, diff, me] = await Promise.all([
1034
1085
  getTask(config, issueNumber),
1035
- getBaseBranch(config),
1036
- getDiff(config.github.baseBranch)
1086
+ diffPromise,
1087
+ getAuthenticatedUser(config)
1037
1088
  ]);
1038
1089
  spinner.stop();
1039
- const reviewSpinner = ora2("Reviewing changes\u2026").start();
1090
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
1091
+ const branch = await getCurrentBranch();
1092
+ const isSelfSubmit = issue.author !== null && issue.author === me;
1040
1093
  let review = "";
1041
- try {
1042
- review = await reviewChanges(config, issueNumber, issue, diff);
1043
- } catch (err) {
1044
- review = `(Review failed: ${err.message})`;
1094
+ if (!isSelfSubmit) {
1095
+ const reviewSpinner = ora2("Reviewing changes\u2026").start();
1096
+ try {
1097
+ review = await reviewChanges(config, issueNumber, issue, diff);
1098
+ } catch (err) {
1099
+ review = `(Review failed: ${err.message})`;
1100
+ }
1101
+ reviewSpinner.stop();
1045
1102
  }
1046
- reviewSpinner.stop();
1047
1103
  const divider = chalk7.dim("\u2500".repeat(70));
1048
1104
  console.log("\n" + divider);
1049
- console.log(chalk7.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1050
- console.log(divider);
1051
- console.log(renderMarkdown(review));
1105
+ if (isSelfSubmit) {
1106
+ console.log(chalk7.yellow(` Self-submit detected \u2014 AI review skipped.`));
1107
+ } else {
1108
+ console.log(chalk7.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1109
+ console.log(divider);
1110
+ console.log(renderMarkdown(review));
1111
+ }
1052
1112
  console.log(divider + "\n");
1053
1113
  let shouldProceed;
1054
1114
  try {
@@ -1084,15 +1144,15 @@ async function run(_input, config) {
1084
1144
  spinner = ora2("Creating pull request\u2026").start();
1085
1145
  let prUrl;
1086
1146
  try {
1087
- prUrl = await createPR(
1088
- config,
1089
- issue.title,
1090
- `Closes #${issueNumber}
1091
-
1092
- ${issue.body ?? ""}`.trim(),
1093
- branch,
1094
- defaultBranch
1095
- );
1147
+ const prBody = [
1148
+ `Closes #${issueNumber}`,
1149
+ issue.body ? `
1150
+ ${issue.body}` : "",
1151
+ review ? `
1152
+ ## AI Review
1153
+ ${review}` : ""
1154
+ ].join("\n").trim();
1155
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
1096
1156
  spinner.stop();
1097
1157
  } catch (err) {
1098
1158
  spinner.stop();
@@ -1106,27 +1166,33 @@ ${issue.body ?? ""}`.trim(),
1106
1166
  spinner.stop();
1107
1167
  return `PR created (${prUrl}) but failed to update label: ${err.message}`;
1108
1168
  }
1169
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1109
1170
  return `Task #${issueNumber} submitted.
1110
1171
  Commit: "${commitMessage.trim()}"
1111
1172
  PR: ${prUrl}`;
1112
1173
  }
1113
- async function execute(input4, config) {
1114
- const branch = await getCurrentBranch();
1115
- const match = branch.match(/^task-(\d+)-/);
1116
- if (!match) return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
1117
- const issueNumber = parseInt(match[1], 10);
1118
- const [issue, defaultBranch, diff] = await Promise.all([
1174
+ async function execute(input3, config) {
1175
+ const taskState = getConfig().taskState;
1176
+ const issueNumber = taskState?.activeIssueNumber;
1177
+ if (!issueNumber) return "No active task found. Claim a task first.";
1178
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1179
+ const [issue, diff, branch, me] = await Promise.all([
1119
1180
  getTask(config, issueNumber),
1120
- getBaseBranch(config),
1121
- getDiff(config.github.baseBranch)
1181
+ diffPromise,
1182
+ getCurrentBranch(),
1183
+ getAuthenticatedUser(config)
1122
1184
  ]);
1185
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
1186
+ const isSelfSubmit = issue.author !== null && issue.author === me;
1123
1187
  let review = "";
1124
- try {
1125
- review = await reviewChanges(config, issueNumber, issue, diff);
1126
- } catch (err) {
1127
- review = `(Review failed: ${err.message})`;
1188
+ if (!isSelfSubmit) {
1189
+ try {
1190
+ review = await reviewChanges(config, issueNumber, issue, diff);
1191
+ } catch (err) {
1192
+ review = `(Review failed: ${err.message})`;
1193
+ }
1128
1194
  }
1129
- const commitMessage = input4["commit_message"]?.trim() || `complete: ${issue.title}`;
1195
+ const commitMessage = input3["commit_message"]?.trim() || `complete: ${issue.title}`;
1130
1196
  try {
1131
1197
  await stageAllAndCommit(commitMessage);
1132
1198
  } catch (err) {
@@ -1134,15 +1200,15 @@ async function execute(input4, config) {
1134
1200
  }
1135
1201
  let prUrl;
1136
1202
  try {
1137
- prUrl = await createPR(
1138
- config,
1139
- issue.title,
1140
- `Closes #${issueNumber}
1141
-
1142
- ${issue.body ?? ""}`.trim(),
1143
- branch,
1144
- defaultBranch
1145
- );
1203
+ const prBody = [
1204
+ `Closes #${issueNumber}`,
1205
+ issue.body ? `
1206
+ ${issue.body}` : "",
1207
+ review ? `
1208
+ ## AI Review
1209
+ ${review}` : ""
1210
+ ].join("\n").trim();
1211
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
1146
1212
  } catch (err) {
1147
1213
  return `Committed but PR creation failed: ${err.message}`;
1148
1214
  }
@@ -1183,8 +1249,8 @@ var definition2 = {
1183
1249
  }
1184
1250
  }
1185
1251
  };
1186
- async function run2(input4, config) {
1187
- let issueNumber = input4["issue_number"];
1252
+ async function run2(input3, config) {
1253
+ let issueNumber = input3["issue_number"];
1188
1254
  if (!issueNumber) {
1189
1255
  let tasks;
1190
1256
  try {
@@ -1225,8 +1291,8 @@ async function run2(input4, config) {
1225
1291
  return `Error: ${err.message}`;
1226
1292
  }
1227
1293
  }
1228
- async function execute2(input4, config) {
1229
- const issueNumber = input4["issue_number"];
1294
+ async function execute2(input3, config) {
1295
+ const issueNumber = input3["issue_number"];
1230
1296
  const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
1231
1297
  try {
1232
1298
  await closeTask(config, issueNumber);
@@ -1259,8 +1325,8 @@ var definition3 = {
1259
1325
  }
1260
1326
  }
1261
1327
  };
1262
- async function run3(input4, config) {
1263
- const preselected = input4["issue_number"];
1328
+ async function run3(input3, config) {
1329
+ const preselected = input3["issue_number"];
1264
1330
  let chosenNumber;
1265
1331
  if (preselected !== void 0) {
1266
1332
  chosenNumber = preselected;
@@ -1336,23 +1402,42 @@ Finish or submit it before claiming a new one.`;
1336
1402
  let spinner = ora4(`Claiming #${issue.number}\u2026`).start();
1337
1403
  await claimTask(config, issue.number, me);
1338
1404
  spinner.stop();
1339
- const branch = makeBranchName(issue.number, me);
1340
- spinner = ora4(`Creating branch ${branch}\u2026`).start();
1405
+ const workerBranch = makeWorkerBranchName(me);
1406
+ spinner = ora4(`Switching to ${workerBranch}\u2026`).start();
1341
1407
  try {
1342
- await createAndSwitchBranch(branch);
1408
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
1343
1409
  spinner.stop();
1410
+ if (isNewWorker) {
1411
+ spinner = ora4("Pushing worker branch\u2026").start();
1412
+ try {
1413
+ await pushBranch(workerBranch);
1414
+ spinner.stop();
1415
+ } catch {
1416
+ spinner.warn("Could not push worker branch");
1417
+ }
1418
+ }
1344
1419
  } catch {
1345
- spinner.warn(`Could not create branch ${branch}`);
1420
+ spinner.warn(`Could not switch to ${workerBranch}`);
1346
1421
  }
1347
- spinner = ora4("Pushing branch\u2026").start();
1422
+ const branch = makeBranchName(issue.number, me);
1423
+ spinner = ora4(`Creating task branch ${branch}\u2026`).start();
1348
1424
  try {
1349
- await pushBranch(branch);
1425
+ await switchToBranchOrCreate(branch);
1350
1426
  spinner.stop();
1427
+ spinner = ora4("Pushing task branch\u2026").start();
1428
+ try {
1429
+ await pushBranch(branch);
1430
+ spinner.stop();
1431
+ } catch {
1432
+ spinner.warn("Could not push task branch");
1433
+ }
1351
1434
  } catch {
1352
- spinner.warn("Could not push branch");
1435
+ spinner.warn(`Could not create branch ${branch}`);
1353
1436
  }
1437
+ const baseCommit = await getCurrentCommit();
1438
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
1354
1439
  console.log(chalk8.green(`
1355
- Claimed! Branch: ${branch}
1440
+ Claimed! Branch: ${branch} (base: ${baseCommit.slice(0, 7)})
1356
1441
  `));
1357
1442
  let openClaude;
1358
1443
  try {
@@ -1376,9 +1461,9 @@ Finish or submit it before claiming a new one.`;
1376
1461
  if (action === "close") return run2({ issue_number: issue.number }, config);
1377
1462
  return "Cancelled.";
1378
1463
  }
1379
- async function execute3(input4, config) {
1380
- const issueNumber = input4["issue_number"];
1381
- const action = input4["action"];
1464
+ async function execute3(input3, config) {
1465
+ const issueNumber = input3["issue_number"];
1466
+ const action = input3["action"];
1382
1467
  let issue;
1383
1468
  try {
1384
1469
  issue = await getTask(config, issueNumber);
@@ -1405,16 +1490,29 @@ async function execute3(input4, config) {
1405
1490
  } catch (err) {
1406
1491
  return `Error claiming task: ${err.message}`;
1407
1492
  }
1493
+ const workerBranch = makeWorkerBranchName(me);
1494
+ try {
1495
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
1496
+ if (isNewWorker) {
1497
+ try {
1498
+ await pushBranch(workerBranch);
1499
+ } catch {
1500
+ }
1501
+ }
1502
+ } catch {
1503
+ }
1408
1504
  const branch = makeBranchName(issueNumber, me);
1409
1505
  try {
1410
- await createAndSwitchBranch(branch);
1506
+ await switchToBranchOrCreate(branch);
1411
1507
  } catch {
1412
1508
  }
1413
1509
  try {
1414
1510
  await pushBranch(branch);
1415
1511
  } catch {
1416
1512
  }
1417
- return `Task #${issueNumber} claimed. Branch: ${branch}`;
1513
+ const baseCommit = await getCurrentCommit();
1514
+ setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
1515
+ return `Task #${issueNumber} claimed. Branch: ${branch} (base commit: ${baseCommit.slice(0, 7)})`;
1418
1516
  }
1419
1517
  return `Unknown action: ${action}`;
1420
1518
  }
@@ -1504,8 +1602,22 @@ var definition4 = {
1504
1602
  }
1505
1603
  }
1506
1604
  };
1507
- async function run4(input4, config) {
1508
- let title = input4["title"]?.trim();
1605
+ async function run4(input3, config) {
1606
+ const authSpinner = ora5("Checking permissions\u2026").start();
1607
+ let me;
1608
+ let allowed;
1609
+ try {
1610
+ me = await getAuthenticatedUser(config);
1611
+ allowed = await isCollaborator(config, me);
1612
+ authSpinner.stop();
1613
+ } catch (err) {
1614
+ authSpinner.stop();
1615
+ return `Error checking permissions: ${err.message}`;
1616
+ }
1617
+ if (!allowed) {
1618
+ return `Permission denied: only repository collaborators can create tasks.`;
1619
+ }
1620
+ let title = input3["title"]?.trim();
1509
1621
  if (!title) {
1510
1622
  try {
1511
1623
  title = (await promptInput2({ message: "Task title:" })).trim();
@@ -1601,9 +1713,13 @@ async function run4(input4, config) {
1601
1713
  }
1602
1714
  return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
1603
1715
  }
1604
- async function execute4(input4, config) {
1605
- const title = input4["title"].trim();
1606
- const feedback = input4["feedback"];
1716
+ async function execute4(input3, config) {
1717
+ const me = await getAuthenticatedUser(config);
1718
+ if (!await isCollaborator(config, me)) {
1719
+ return `Permission denied: only repository collaborators can create tasks.`;
1720
+ }
1721
+ const title = input3["title"].trim();
1722
+ const feedback = input3["feedback"];
1607
1723
  let guide = await generateGuide(config, title);
1608
1724
  if (feedback) {
1609
1725
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
@@ -1818,8 +1934,15 @@ var definition9 = {
1818
1934
  }
1819
1935
  }
1820
1936
  };
1821
- async function run9(input4, config) {
1822
- const issueNumber = input4["issue_number"];
1937
+ async function run9(input3, config) {
1938
+ const issueNumber = input3["issue_number"];
1939
+ const [me, issue] = await Promise.all([
1940
+ getAuthenticatedUser(config),
1941
+ getTask(config, issueNumber)
1942
+ ]);
1943
+ if (issue.author && issue.author !== me) {
1944
+ return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
1945
+ }
1823
1946
  let feedback;
1824
1947
  try {
1825
1948
  feedback = await promptInput3({
@@ -1886,9 +2009,16 @@ async function run9(input4, config) {
1886
2009
  return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
1887
2010
  }
1888
2011
  }
1889
- async function execute9(input4, config) {
1890
- const issueNumber = input4["issue_number"];
1891
- const feedback = input4["feedback"];
2012
+ async function execute9(input3, config) {
2013
+ const issueNumber = input3["issue_number"];
2014
+ const feedback = input3["feedback"];
2015
+ const [me, issue] = await Promise.all([
2016
+ getAuthenticatedUser(config),
2017
+ getTask(config, issueNumber)
2018
+ ]);
2019
+ if (issue.author && issue.author !== me) {
2020
+ return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
2021
+ }
1892
2022
  let comment;
1893
2023
  try {
1894
2024
  comment = await generateRejectionComment(config, issueNumber, feedback);
@@ -1928,7 +2058,7 @@ var definition10 = {
1928
2058
  type: "function",
1929
2059
  function: {
1930
2060
  name: "accept",
1931
- description: "Accept an in-review task: merges the PR into the configured base branch and closes the issue.",
2061
+ description: "Accept an in-review task: merges the PR into your worker branch and closes the issue.",
1932
2062
  parameters: {
1933
2063
  type: "object",
1934
2064
  properties: {
@@ -1938,18 +2068,18 @@ var definition10 = {
1938
2068
  }
1939
2069
  }
1940
2070
  };
1941
- async function run10(input4, config) {
1942
- let issueNumber = input4["issue_number"];
2071
+ async function run10(input3, config) {
2072
+ let issueNumber = input3["issue_number"];
1943
2073
  if (!issueNumber) {
1944
- const spinner2 = ora9("Loading tasks for review\u2026").start();
2074
+ const spinner3 = ora9("Loading tasks for review\u2026").start();
1945
2075
  let tasks;
1946
2076
  let me;
1947
2077
  try {
1948
2078
  me = await getAuthenticatedUser(config);
1949
2079
  tasks = await listTasksForReview(config, me);
1950
- spinner2.stop();
2080
+ spinner3.stop();
1951
2081
  } catch (err) {
1952
- spinner2.stop();
2082
+ spinner3.stop();
1953
2083
  return `Error: ${err.message}`;
1954
2084
  }
1955
2085
  if (tasks.length === 0) return "No tasks pending review.";
@@ -1965,11 +2095,27 @@ async function run10(input4, config) {
1965
2095
  return "Cancelled.";
1966
2096
  }
1967
2097
  }
1968
- const baseBranch = config.github.baseBranch ?? "main";
2098
+ const spinner2 = ora9("Verifying permissions\u2026").start();
2099
+ let me2;
2100
+ let issue;
2101
+ try {
2102
+ [me2, issue] = await Promise.all([
2103
+ getAuthenticatedUser(config),
2104
+ getTask(config, issueNumber)
2105
+ ]);
2106
+ spinner2.stop();
2107
+ } catch (err) {
2108
+ spinner2.stop();
2109
+ return `Error: ${err.message}`;
2110
+ }
2111
+ if (issue.author && issue.author !== me2) {
2112
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2113
+ }
2114
+ const targetBranch = makeWorkerBranchName(me2);
1969
2115
  let confirmed;
1970
2116
  try {
1971
2117
  confirmed = await select8({
1972
- message: `Merge PR for #${issueNumber} into ${chalk11.cyan(baseBranch)} and close issue?`,
2118
+ message: `Merge PR for #${issueNumber} into ${chalk11.cyan(targetBranch)} and close issue?`,
1973
2119
  choices: [
1974
2120
  { name: "Yes, accept", value: true },
1975
2121
  { name: "Cancel", value: false }
@@ -1982,24 +2128,31 @@ async function run10(input4, config) {
1982
2128
  const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
1983
2129
  try {
1984
2130
  const result = await acceptTask(config, issueNumber);
1985
- spinner.succeed(`PR #${result.prNumber} merged into ${baseBranch}`);
2131
+ spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
1986
2132
  return `Task #${issueNumber} accepted.
1987
- PR #${result.prNumber} merged \u2192 ${baseBranch}
2133
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
1988
2134
  Issue closed.`;
1989
2135
  } catch (err) {
1990
2136
  spinner.fail("Failed");
1991
2137
  return `Error: ${err.message}`;
1992
2138
  }
1993
2139
  }
1994
- async function execute10(input4, config) {
1995
- const issueNumber = input4["issue_number"];
2140
+ async function execute10(input3, config) {
2141
+ const issueNumber = input3["issue_number"];
2142
+ const [me, issue] = await Promise.all([
2143
+ getAuthenticatedUser(config),
2144
+ getTask(config, issueNumber)
2145
+ ]);
2146
+ if (issue.author && issue.author !== me) {
2147
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2148
+ }
2149
+ const targetBranch = makeWorkerBranchName(me);
1996
2150
  const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
1997
2151
  try {
1998
2152
  const result = await acceptTask(config, issueNumber);
1999
2153
  spinner.stop();
2000
- const baseBranch = config.github.baseBranch ?? "main";
2001
2154
  return `Task #${issueNumber} accepted.
2002
- PR #${result.prNumber} merged \u2192 ${baseBranch}
2155
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
2003
2156
  Issue closed.`;
2004
2157
  } catch (err) {
2005
2158
  spinner.stop();
@@ -2035,8 +2188,8 @@ var definition11 = {
2035
2188
  }
2036
2189
  }
2037
2190
  };
2038
- async function run11(input4, config) {
2039
- let issueNumber = input4["issue_number"];
2191
+ async function run11(input3, config) {
2192
+ let issueNumber = input3["issue_number"];
2040
2193
  if (!issueNumber) {
2041
2194
  let tasks;
2042
2195
  try {
@@ -2087,10 +2240,10 @@ async function run11(input4, config) {
2087
2240
  return `Error: ${err.message}`;
2088
2241
  }
2089
2242
  }
2090
- async function execute11(input4, config) {
2091
- const issueNumber = input4["issue_number"];
2092
- const title = input4["title"];
2093
- const body = input4["body"];
2243
+ async function execute11(input3, config) {
2244
+ const issueNumber = input3["issue_number"];
2245
+ const title = input3["title"];
2246
+ const body = input3["body"];
2094
2247
  const spinner = ora10(`Updating #${issueNumber}\u2026`).start();
2095
2248
  try {
2096
2249
  await editTask(config, issueNumber, title, body);
@@ -2153,8 +2306,8 @@ var definition13 = {
2153
2306
  }
2154
2307
  }
2155
2308
  };
2156
- async function execute13(input4, config) {
2157
- const issue = await getTask(config, input4["issue_number"]);
2309
+ async function execute13(input3, config) {
2310
+ const issue = await getTask(config, input3["issue_number"]);
2158
2311
  const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2159
2312
  const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
2160
2313
  const lines = [
@@ -2190,9 +2343,9 @@ var definition14 = {
2190
2343
  }
2191
2344
  }
2192
2345
  };
2193
- async function execute14(input4, config) {
2194
- const issueNumber = input4["issue_number"];
2195
- const limit = input4["limit"] ?? 5;
2346
+ async function execute14(input3, config) {
2347
+ const issueNumber = input3["issue_number"];
2348
+ const limit = input3["limit"] ?? 5;
2196
2349
  const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
2197
2350
  try {
2198
2351
  const comments = await listComments(config, issueNumber, limit);
@@ -2227,7 +2380,7 @@ var definition15 = {
2227
2380
  async function execute15(_input, _config) {
2228
2381
  const spinner = ora12("Reading git diff...").start();
2229
2382
  try {
2230
- const diff = await getDiff(_config.github.baseBranch);
2383
+ const diff = await getDiff();
2231
2384
  spinner.stop();
2232
2385
  return diff;
2233
2386
  } catch (err) {
@@ -2260,8 +2413,8 @@ var definition16 = {
2260
2413
  }
2261
2414
  }
2262
2415
  };
2263
- async function execute16(input4, _config) {
2264
- const command = input4["command"];
2416
+ async function execute16(input3, _config) {
2417
+ const command = input3["command"];
2265
2418
  const cwd = process.cwd();
2266
2419
  const spinner = ora13(`$ ${command}`).start();
2267
2420
  try {
@@ -2453,8 +2606,8 @@ var definition17 = {
2453
2606
  }
2454
2607
  }
2455
2608
  };
2456
- async function execute17(input4, _config) {
2457
- const focus = input4["focus"] ?? "";
2609
+ async function execute17(input3, _config) {
2610
+ const focus = input3["focus"] ?? "";
2458
2611
  const spinner = ora14("Scanning project...").start();
2459
2612
  try {
2460
2613
  const cwd = process.cwd();
@@ -2503,8 +2656,8 @@ var definition18 = {
2503
2656
  }
2504
2657
  }
2505
2658
  };
2506
- async function execute18(input4, _config) {
2507
- const filePath = input4["path"];
2659
+ async function execute18(input3, _config) {
2660
+ const filePath = input3["path"];
2508
2661
  try {
2509
2662
  const fullPath = path3.join(process.cwd(), filePath);
2510
2663
  const content = await readFile3(fullPath, "utf-8");
@@ -2541,9 +2694,9 @@ var definition19 = {
2541
2694
  }
2542
2695
  }
2543
2696
  };
2544
- async function execute19(input4, _config) {
2545
- const question = input4["question"];
2546
- const options = input4["options"];
2697
+ async function execute19(input3, _config) {
2698
+ const question = input3["question"];
2699
+ const options = input3["options"];
2547
2700
  const OTHER = "__other__";
2548
2701
  console.log("");
2549
2702
  console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
@@ -2596,12 +2749,12 @@ var toolModules = [
2596
2749
 
2597
2750
  // src/lib/agent.ts
2598
2751
  var tools = toolModules.map((m) => m.definition);
2599
- async function executeTool(name, input4, config) {
2752
+ async function executeTool(name, input3, config) {
2600
2753
  const mod = toolModules.find((m) => m.definition.function.name === name);
2601
2754
  if (!mod) return `Unknown tool: ${name}`;
2602
2755
  try {
2603
2756
  const fn = mod.run ?? mod.execute;
2604
- return await fn(input4, config);
2757
+ return await fn(input3, config);
2605
2758
  } catch (err) {
2606
2759
  return `Error: ${err.message}`;
2607
2760
  }
@@ -2805,13 +2958,9 @@ async function initNewRepo(config, owner, repo) {
2805
2958
  console.log("");
2806
2959
  console.log(chalk14.bold.cyan(` New repo detected: ${owner}/${repo}`));
2807
2960
  console.log(chalk14.dim(" Setting up Techunter for this repository...\n"));
2808
- const baseBranch = await input3({
2809
- message: "Main branch to merge PRs into:",
2810
- default: "main"
2811
- });
2812
2961
  const newConfig = {
2813
2962
  ...config,
2814
- github: { owner, repo, baseBranch: baseBranch.trim() || "main" }
2963
+ github: { owner, repo }
2815
2964
  };
2816
2965
  setConfig({ github: newConfig.github });
2817
2966
  const ora16 = (await import("ora")).default;