techunter 0.1.0 → 0.1.2

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 +1 -1
  2. package/dist/index.js +533 -228
  3. package/dist/mcp.js +477 -199
  4. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -17,12 +17,14 @@ __export(github_exports, {
17
17
  closeTask: () => closeTask,
18
18
  createPR: () => createPR,
19
19
  createTask: () => createTask,
20
+ editTask: () => editTask,
20
21
  ensureLabels: () => ensureLabels,
21
22
  formatGuideAsMarkdown: () => formatGuideAsMarkdown,
22
23
  getAuthenticatedUser: () => getAuthenticatedUser,
23
24
  getBaseBranch: () => getBaseBranch,
24
25
  getDefaultBranch: () => getDefaultBranch,
25
26
  getTask: () => getTask,
27
+ isCollaborator: () => isCollaborator,
26
28
  listComments: () => listComments,
27
29
  listMyTasks: () => listMyTasks,
28
30
  listTasks: () => listTasks,
@@ -49,6 +51,7 @@ function parseIssue(issue) {
49
51
  title: issue.title,
50
52
  body: issue.body ?? null,
51
53
  state: issue.state,
54
+ author: issue.user?.login ?? null,
52
55
  assignee: issue.assignee?.login ?? null,
53
56
  labels: (issue.labels ?? []).map(
54
57
  (l) => typeof l === "string" ? l : l.name ?? ""
@@ -59,46 +62,15 @@ function parseIssue(issue) {
59
62
  async function listTasks(config) {
60
63
  const octokit = createOctokit(config.githubToken);
61
64
  const { owner, repo } = config.github;
62
- const [available, claimed, inReview, changesNeeded] = await Promise.all([
63
- octokit.issues.listForRepo({
64
- owner,
65
- repo,
66
- labels: LABEL_AVAILABLE,
67
- state: "open",
68
- per_page: 50
69
- }),
70
- octokit.issues.listForRepo({
71
- owner,
72
- repo,
73
- labels: LABEL_CLAIMED,
74
- state: "open",
75
- per_page: 50
76
- }),
77
- octokit.issues.listForRepo({
78
- owner,
79
- repo,
80
- labels: LABEL_IN_REVIEW,
81
- state: "open",
82
- per_page: 50
83
- }),
84
- octokit.issues.listForRepo({
85
- owner,
86
- repo,
87
- labels: LABEL_CHANGES_NEEDED,
88
- state: "open",
89
- per_page: 50
90
- })
91
- ]);
92
- const allIssues = [...available.data, ...claimed.data, ...inReview.data, ...changesNeeded.data];
93
- const seen = /* @__PURE__ */ new Set();
94
- const unique = [];
95
- for (const issue of allIssues) {
96
- if (!seen.has(issue.number)) {
97
- seen.add(issue.number);
98
- unique.push(parseIssue(issue));
99
- }
100
- }
101
- return unique.sort((a, b) => a.number - b.number);
65
+ const { data } = await octokit.issues.listForRepo({
66
+ owner,
67
+ repo,
68
+ state: "open",
69
+ per_page: 100
70
+ });
71
+ return data.filter(
72
+ (issue) => !issue.pull_request && issue.labels.some((l) => TECHUNTER_LABELS.has(l.name ?? ""))
73
+ ).map(parseIssue).sort((a, b) => a.number - b.number);
102
74
  }
103
75
  async function getTask(config, number) {
104
76
  const octokit = createOctokit(config.githubToken);
@@ -256,6 +228,16 @@ async function getAuthenticatedUser(config) {
256
228
  const { data } = await octokit.users.getAuthenticated();
257
229
  return data.login;
258
230
  }
231
+ async function isCollaborator(config, username) {
232
+ const octokit = createOctokit(config.githubToken);
233
+ const { owner, repo } = config.github;
234
+ try {
235
+ const { data } = await octokit.repos.getCollaboratorPermissionLevel({ owner, repo, username });
236
+ return data.permission === "admin" || data.permission === "write" || data.permission === "maintain";
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
259
241
  async function listMyTasks(config, username) {
260
242
  const octokit = createOctokit(config.githubToken);
261
243
  const { owner, repo } = config.github;
@@ -316,6 +298,11 @@ async function ensureLabels(config) {
316
298
  )
317
299
  );
318
300
  }
301
+ async function editTask(config, number, title, body) {
302
+ const octokit = createOctokit(config.githubToken);
303
+ const { owner, repo } = config.github;
304
+ await octokit.issues.update({ owner, repo, issue_number: number, title, body });
305
+ }
319
306
  async function getDefaultBranch(config) {
320
307
  const octokit = createOctokit(config.githubToken);
321
308
  const { owner, repo } = config.github;
@@ -341,7 +328,7 @@ async function acceptTask(config, issueNumber) {
341
328
  await closeTask(config, issueNumber);
342
329
  return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "" };
343
330
  }
344
- var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS;
331
+ var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS;
345
332
  var init_github = __esm({
346
333
  "src/lib/github.ts"() {
347
334
  "use strict";
@@ -355,6 +342,7 @@ var init_github = __esm({
355
342
  { name: LABEL_IN_REVIEW, color: "0075ca", description: "Task submitted for review" },
356
343
  { name: LABEL_CHANGES_NEEDED, color: "e11d48", description: "Task needs changes" }
357
344
  ];
345
+ TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
358
346
  }
359
347
  });
360
348
 
@@ -362,6 +350,7 @@ var init_github = __esm({
362
350
  import chalk14 from "chalk";
363
351
  import readline from "readline";
364
352
  import { createRequire } from "module";
353
+ import { input as input3 } from "@inquirer/prompts";
365
354
 
366
355
  // src/commands/init.ts
367
356
  import { input, password, select } from "@inquirer/prompts";
@@ -383,7 +372,11 @@ var configSchema = z.object({
383
372
  owner: z.string().min(1),
384
373
  repo: z.string().min(1),
385
374
  baseBranch: z.string().optional()
386
- })
375
+ }),
376
+ taskState: z.object({
377
+ activeIssueNumber: z.number().optional(),
378
+ baseCommit: z.string().optional()
379
+ }).optional()
387
380
  });
388
381
  var store = new Conf({
389
382
  projectName: "techunter",
@@ -420,6 +413,12 @@ function setConfig(partial) {
420
413
  if (partial.githubClientId !== void 0) {
421
414
  current["githubClientId"] = partial.githubClientId;
422
415
  }
416
+ if (partial.taskState !== void 0) {
417
+ current["taskState"] = {
418
+ ...current["taskState"] ?? {},
419
+ ...partial.taskState
420
+ };
421
+ }
423
422
  store.store = current;
424
423
  }
425
424
  function getConfigPath() {
@@ -437,9 +436,6 @@ async function getCurrentBranch() {
437
436
  const summary = await git.branch();
438
437
  return summary.current;
439
438
  }
440
- async function createAndSwitchBranch(name) {
441
- await git.checkoutLocalBranch(name);
442
- }
443
439
  async function pushBranch(name) {
444
440
  await git.push("origin", name, ["--set-upstream"]);
445
441
  }
@@ -467,6 +463,61 @@ function makeBranchName(issueNumber, username) {
467
463
  const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
468
464
  return `task-${issueNumber}-${slug}`;
469
465
  }
466
+ function makeWorkerBranchName(username) {
467
+ const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
468
+ return `worker-${slug}`;
469
+ }
470
+ async function getCurrentCommit() {
471
+ return (await git.revparse(["HEAD"])).trim();
472
+ }
473
+ async function switchToBranchOrCreate(name) {
474
+ try {
475
+ const branches = await git.branch(["-a"]);
476
+ const exists = Object.keys(branches.branches).some(
477
+ (b) => b === name || b === `remotes/origin/${name}`
478
+ );
479
+ if (exists) {
480
+ await git.checkout(name);
481
+ return false;
482
+ }
483
+ await git.checkoutLocalBranch(name);
484
+ return true;
485
+ } catch {
486
+ await git.checkoutLocalBranch(name);
487
+ return true;
488
+ }
489
+ }
490
+ async function getDiffFromCommit(baseCommit) {
491
+ const status = await git.status();
492
+ const parts = [];
493
+ const fileLines = [
494
+ ...status.modified.map((f) => ` M ${f}`),
495
+ ...status.created.map((f) => ` A ${f}`),
496
+ ...status.deleted.map((f) => ` D ${f}`),
497
+ ...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
498
+ ...status.not_added.map((f) => ` ? ${f}`)
499
+ ];
500
+ if (fileLines.length > 0) {
501
+ parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
502
+ const uncommitted = await git.diff(["HEAD"]);
503
+ if (uncommitted) {
504
+ const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
505
+ parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
506
+ }
507
+ }
508
+ const log = await git.log({ from: baseCommit, to: "HEAD" });
509
+ if (log.total > 0) {
510
+ const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
511
+ parts.push(`## Commits since task claimed (${log.total} total)
512
+ ` + logLines.join("\n"));
513
+ const branchDiff = await git.diff([baseCommit, "HEAD"]);
514
+ if (branchDiff) {
515
+ const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
516
+ parts.push("## Full diff since task claimed\n```diff\n" + capped + "\n```");
517
+ }
518
+ }
519
+ return parts.length > 0 ? parts.join("\n\n") : "No changes since task was claimed.";
520
+ }
470
521
  async function findMergeBase(configuredBase) {
471
522
  const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
472
523
  const unique = [...new Set(candidates)];
@@ -762,7 +813,7 @@ AI model set to: ${val.trim()}
762
813
  init_github();
763
814
 
764
815
  // src/lib/agent.ts
765
- import ora14 from "ora";
816
+ import ora15 from "ora";
766
817
  import chalk13 from "chalk";
767
818
 
768
819
  // src/tools/pick/index.ts
@@ -944,8 +995,8 @@ import { select as select3, input as promptInput } from "@inquirer/prompts";
944
995
 
945
996
  // src/lib/agent-ui.ts
946
997
  import chalk6 from "chalk";
947
- function formatInput(input3) {
948
- return Object.entries(input3).map(([k, v]) => {
998
+ function formatInput(input4) {
999
+ return Object.entries(input4).map(([k, v]) => {
949
1000
  if (typeof v === "number") return `${k}=${v}`;
950
1001
  if (typeof v === "string") {
951
1002
  if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
@@ -958,8 +1009,8 @@ function summarize(result) {
958
1009
  const first = result.split("\n").find((l) => l.trim()) ?? result;
959
1010
  return first.length > 100 ? first.slice(0, 97) + "..." : first;
960
1011
  }
961
- function printToolCall(name, input3) {
962
- const params = formatInput(input3);
1012
+ function printToolCall(name, input4) {
1013
+ const params = formatInput(input4);
963
1014
  console.log(` ${chalk6.cyan("\u2192")} ${chalk6.bold(name)}${params ? " " + chalk6.dim(params) : ""}`);
964
1015
  }
965
1016
  function printToolResult(result) {
@@ -995,15 +1046,15 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
995
1046
  }
996
1047
  if (choice.finish_reason === "tool_calls") {
997
1048
  for (const tc of choice.message.tool_calls ?? []) {
998
- let input3;
1049
+ let input4;
999
1050
  try {
1000
- input3 = JSON.parse(tc.function.arguments);
1051
+ input4 = JSON.parse(tc.function.arguments);
1001
1052
  } catch {
1002
- input3 = {};
1053
+ input4 = {};
1003
1054
  }
1004
- printToolCall(tc.function.name, input3);
1055
+ printToolCall(tc.function.name, input4);
1005
1056
  const mod = selected.find((m) => m.definition.function.name === tc.function.name);
1006
- const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
1057
+ const result = mod ? await mod.execute(input4, config) : `Unknown tool: ${tc.function.name}`;
1007
1058
  printToolResult(result);
1008
1059
  messages.push({ role: "tool", tool_call_id: tc.id, content: result });
1009
1060
  }
@@ -1012,7 +1063,7 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
1012
1063
  }
1013
1064
 
1014
1065
  // src/tools/submit/prompts.ts
1015
- 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.";
1066
+ 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.";
1016
1067
 
1017
1068
  // src/tools/submit/reviewer.ts
1018
1069
  async function reviewChanges(config, issueNumber, issue, diff) {
@@ -1045,33 +1096,42 @@ var definition = {
1045
1096
  }
1046
1097
  }
1047
1098
  };
1048
- async function run(config) {
1049
- const branch = await getCurrentBranch();
1050
- const match = branch.match(/^task-(\d+)-/);
1051
- if (!match) {
1052
- return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
1099
+ async function run(_input, config) {
1100
+ const taskState = getConfig().taskState;
1101
+ const issueNumber = taskState?.activeIssueNumber;
1102
+ if (!issueNumber) {
1103
+ return "No active task found. Claim a task first with /pick.";
1053
1104
  }
1054
- const issueNumber = parseInt(match[1], 10);
1055
1105
  let spinner = ora2("Loading task and diff\u2026").start();
1056
- const [issue, defaultBranch, diff] = await Promise.all([
1106
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff(config.github.baseBranch);
1107
+ const [issue, defaultBranch, diff, me] = await Promise.all([
1057
1108
  getTask(config, issueNumber),
1058
1109
  getBaseBranch(config),
1059
- getDiff(config.github.baseBranch)
1110
+ diffPromise,
1111
+ getAuthenticatedUser(config)
1060
1112
  ]);
1061
1113
  spinner.stop();
1062
- const reviewSpinner = ora2("Reviewing changes\u2026").start();
1114
+ const branch = await getCurrentBranch();
1115
+ const isSelfSubmit = issue.author !== null && issue.author === me;
1063
1116
  let review = "";
1064
- try {
1065
- review = await reviewChanges(config, issueNumber, issue, diff);
1066
- } catch (err) {
1067
- review = `(Review failed: ${err.message})`;
1117
+ if (!isSelfSubmit) {
1118
+ const reviewSpinner = ora2("Reviewing changes\u2026").start();
1119
+ try {
1120
+ review = await reviewChanges(config, issueNumber, issue, diff);
1121
+ } catch (err) {
1122
+ review = `(Review failed: ${err.message})`;
1123
+ }
1124
+ reviewSpinner.stop();
1068
1125
  }
1069
- reviewSpinner.stop();
1070
1126
  const divider = chalk7.dim("\u2500".repeat(70));
1071
1127
  console.log("\n" + divider);
1072
- console.log(chalk7.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1073
- console.log(divider);
1074
- console.log(renderMarkdown(review));
1128
+ if (isSelfSubmit) {
1129
+ console.log(chalk7.yellow(` Self-submit detected \u2014 AI review skipped.`));
1130
+ } else {
1131
+ console.log(chalk7.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1132
+ console.log(divider);
1133
+ console.log(renderMarkdown(review));
1134
+ }
1075
1135
  console.log(divider + "\n");
1076
1136
  let shouldProceed;
1077
1137
  try {
@@ -1107,15 +1167,15 @@ async function run(config) {
1107
1167
  spinner = ora2("Creating pull request\u2026").start();
1108
1168
  let prUrl;
1109
1169
  try {
1110
- prUrl = await createPR(
1111
- config,
1112
- issue.title,
1113
- `Closes #${issueNumber}
1114
-
1115
- ${issue.body ?? ""}`.trim(),
1116
- branch,
1117
- defaultBranch
1118
- );
1170
+ const prBody = [
1171
+ `Closes #${issueNumber}`,
1172
+ issue.body ? `
1173
+ ${issue.body}` : "",
1174
+ review ? `
1175
+ ## AI Review
1176
+ ${review}` : ""
1177
+ ].join("\n").trim();
1178
+ prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
1119
1179
  spinner.stop();
1120
1180
  } catch (err) {
1121
1181
  spinner.stop();
@@ -1129,27 +1189,33 @@ ${issue.body ?? ""}`.trim(),
1129
1189
  spinner.stop();
1130
1190
  return `PR created (${prUrl}) but failed to update label: ${err.message}`;
1131
1191
  }
1192
+ setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
1132
1193
  return `Task #${issueNumber} submitted.
1133
1194
  Commit: "${commitMessage.trim()}"
1134
1195
  PR: ${prUrl}`;
1135
1196
  }
1136
- async function execute(input3, config) {
1137
- const branch = await getCurrentBranch();
1138
- const match = branch.match(/^task-(\d+)-/);
1139
- if (!match) return `Not on a task branch (current: ${branch}). Expected format: task-N-title.`;
1140
- const issueNumber = parseInt(match[1], 10);
1141
- const [issue, defaultBranch, diff] = await Promise.all([
1197
+ async function execute(input4, config) {
1198
+ const taskState = getConfig().taskState;
1199
+ const issueNumber = taskState?.activeIssueNumber;
1200
+ if (!issueNumber) return "No active task found. Claim a task first.";
1201
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff(config.github.baseBranch);
1202
+ const [issue, defaultBranch, diff, branch, me] = await Promise.all([
1142
1203
  getTask(config, issueNumber),
1143
1204
  getBaseBranch(config),
1144
- getDiff(config.github.baseBranch)
1205
+ diffPromise,
1206
+ getCurrentBranch(),
1207
+ getAuthenticatedUser(config)
1145
1208
  ]);
1209
+ const isSelfSubmit = issue.author !== null && issue.author === me;
1146
1210
  let review = "";
1147
- try {
1148
- review = await reviewChanges(config, issueNumber, issue, diff);
1149
- } catch (err) {
1150
- review = `(Review failed: ${err.message})`;
1211
+ if (!isSelfSubmit) {
1212
+ try {
1213
+ review = await reviewChanges(config, issueNumber, issue, diff);
1214
+ } catch (err) {
1215
+ review = `(Review failed: ${err.message})`;
1216
+ }
1151
1217
  }
1152
- const commitMessage = input3["commit_message"]?.trim() || `complete: ${issue.title}`;
1218
+ const commitMessage = input4["commit_message"]?.trim() || `complete: ${issue.title}`;
1153
1219
  try {
1154
1220
  await stageAllAndCommit(commitMessage);
1155
1221
  } catch (err) {
@@ -1157,15 +1223,15 @@ async function execute(input3, config) {
1157
1223
  }
1158
1224
  let prUrl;
1159
1225
  try {
1160
- prUrl = await createPR(
1161
- config,
1162
- issue.title,
1163
- `Closes #${issueNumber}
1164
-
1165
- ${issue.body ?? ""}`.trim(),
1166
- branch,
1167
- defaultBranch
1168
- );
1226
+ const prBody = [
1227
+ `Closes #${issueNumber}`,
1228
+ issue.body ? `
1229
+ ${issue.body}` : "",
1230
+ review ? `
1231
+ ## AI Review
1232
+ ${review}` : ""
1233
+ ].join("\n").trim();
1234
+ prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
1169
1235
  } catch (err) {
1170
1236
  return `Committed but PR creation failed: ${err.message}`;
1171
1237
  }
@@ -1206,8 +1272,8 @@ var definition2 = {
1206
1272
  }
1207
1273
  }
1208
1274
  };
1209
- async function run2(config, opts = {}) {
1210
- let issueNumber = opts.issue_number;
1275
+ async function run2(input4, config) {
1276
+ let issueNumber = input4["issue_number"];
1211
1277
  if (!issueNumber) {
1212
1278
  let tasks;
1213
1279
  try {
@@ -1248,8 +1314,8 @@ async function run2(config, opts = {}) {
1248
1314
  return `Error: ${err.message}`;
1249
1315
  }
1250
1316
  }
1251
- async function execute2(input3, config) {
1252
- const issueNumber = input3["issue_number"];
1317
+ async function execute2(input4, config) {
1318
+ const issueNumber = input4["issue_number"];
1253
1319
  const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
1254
1320
  try {
1255
1321
  await closeTask(config, issueNumber);
@@ -1282,7 +1348,8 @@ var definition3 = {
1282
1348
  }
1283
1349
  }
1284
1350
  };
1285
- async function run3(config, preselected) {
1351
+ async function run3(input4, config) {
1352
+ const preselected = input4["issue_number"];
1286
1353
  let chosenNumber;
1287
1354
  if (preselected !== void 0) {
1288
1355
  chosenNumber = preselected;
@@ -1358,23 +1425,28 @@ Finish or submit it before claiming a new one.`;
1358
1425
  let spinner = ora4(`Claiming #${issue.number}\u2026`).start();
1359
1426
  await claimTask(config, issue.number, me);
1360
1427
  spinner.stop();
1361
- const branch = makeBranchName(issue.number, me);
1362
- spinner = ora4(`Creating branch ${branch}\u2026`).start();
1428
+ const branch = makeWorkerBranchName(me);
1429
+ spinner = ora4(`Switching to branch ${branch}\u2026`).start();
1430
+ let isNew = false;
1363
1431
  try {
1364
- await createAndSwitchBranch(branch);
1432
+ isNew = await switchToBranchOrCreate(branch);
1365
1433
  spinner.stop();
1366
1434
  } catch {
1367
- spinner.warn(`Could not create branch ${branch}`);
1435
+ spinner.warn(`Could not switch to branch ${branch}`);
1368
1436
  }
1369
- spinner = ora4("Pushing branch\u2026").start();
1370
- try {
1371
- await pushBranch(branch);
1372
- spinner.stop();
1373
- } catch {
1374
- spinner.warn("Could not push branch");
1437
+ if (isNew) {
1438
+ spinner = ora4("Pushing branch\u2026").start();
1439
+ try {
1440
+ await pushBranch(branch);
1441
+ spinner.stop();
1442
+ } catch {
1443
+ spinner.warn("Could not push branch");
1444
+ }
1375
1445
  }
1446
+ const baseCommit = await getCurrentCommit();
1447
+ setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
1376
1448
  console.log(chalk8.green(`
1377
- Claimed! Branch: ${branch}
1449
+ Claimed! Branch: ${branch} (base: ${baseCommit.slice(0, 7)})
1378
1450
  `));
1379
1451
  let openClaude;
1380
1452
  try {
@@ -1394,13 +1466,13 @@ Finish or submit it before claiming a new one.`;
1394
1466
  return `Error claiming task: ${err.message}`;
1395
1467
  }
1396
1468
  }
1397
- if (action === "submit") return run(config);
1398
- if (action === "close") return run2(config, { issue_number: issue.number });
1469
+ if (action === "submit") return run({}, config);
1470
+ if (action === "close") return run2({ issue_number: issue.number }, config);
1399
1471
  return "Cancelled.";
1400
1472
  }
1401
- async function execute3(input3, config) {
1402
- const issueNumber = input3["issue_number"];
1403
- const action = input3["action"];
1473
+ async function execute3(input4, config) {
1474
+ const issueNumber = input4["issue_number"];
1475
+ const action = input4["action"];
1404
1476
  let issue;
1405
1477
  try {
1406
1478
  issue = await getTask(config, issueNumber);
@@ -1427,16 +1499,21 @@ async function execute3(input3, config) {
1427
1499
  } catch (err) {
1428
1500
  return `Error claiming task: ${err.message}`;
1429
1501
  }
1430
- const branch = makeBranchName(issueNumber, me);
1502
+ const branch = makeWorkerBranchName(me);
1503
+ let isNew = false;
1431
1504
  try {
1432
- await createAndSwitchBranch(branch);
1505
+ isNew = await switchToBranchOrCreate(branch);
1433
1506
  } catch {
1434
1507
  }
1435
- try {
1436
- await pushBranch(branch);
1437
- } catch {
1508
+ if (isNew) {
1509
+ try {
1510
+ await pushBranch(branch);
1511
+ } catch {
1512
+ }
1438
1513
  }
1439
- return `Task #${issueNumber} claimed. Branch: ${branch}`;
1514
+ const baseCommit = await getCurrentCommit();
1515
+ setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
1516
+ return `Task #${issueNumber} claimed. Branch: ${branch} (base commit: ${baseCommit.slice(0, 7)})`;
1440
1517
  }
1441
1518
  return `Unknown action: ${action}`;
1442
1519
  }
@@ -1526,8 +1603,22 @@ var definition4 = {
1526
1603
  }
1527
1604
  }
1528
1605
  };
1529
- async function run4(config, opts = {}) {
1530
- let title = opts.title?.trim();
1606
+ async function run4(input4, config) {
1607
+ const authSpinner = ora5("Checking permissions\u2026").start();
1608
+ let me;
1609
+ let allowed;
1610
+ try {
1611
+ me = await getAuthenticatedUser(config);
1612
+ allowed = await isCollaborator(config, me);
1613
+ authSpinner.stop();
1614
+ } catch (err) {
1615
+ authSpinner.stop();
1616
+ return `Error checking permissions: ${err.message}`;
1617
+ }
1618
+ if (!allowed) {
1619
+ return `Permission denied: only repository collaborators can create tasks.`;
1620
+ }
1621
+ let title = input4["title"]?.trim();
1531
1622
  if (!title) {
1532
1623
  try {
1533
1624
  title = (await promptInput2({ message: "Task title:" })).trim();
@@ -1623,9 +1714,13 @@ async function run4(config, opts = {}) {
1623
1714
  }
1624
1715
  return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
1625
1716
  }
1626
- async function execute4(input3, config) {
1627
- const title = input3["title"].trim();
1628
- const feedback = input3["feedback"];
1717
+ async function execute4(input4, config) {
1718
+ const me = await getAuthenticatedUser(config);
1719
+ if (!await isCollaborator(config, me)) {
1720
+ return `Permission denied: only repository collaborators can create tasks.`;
1721
+ }
1722
+ const title = input4["title"].trim();
1723
+ const feedback = input4["feedback"];
1629
1724
  let guide = await generateGuide(config, title);
1630
1725
  if (feedback) {
1631
1726
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
@@ -1660,7 +1755,7 @@ var definition5 = {
1660
1755
  parameters: { type: "object", properties: {}, required: [] }
1661
1756
  }
1662
1757
  };
1663
- async function run5(config) {
1758
+ async function run5(_input, config) {
1664
1759
  const spinner = ora6("Fetching your tasks\u2026").start();
1665
1760
  try {
1666
1761
  const me = await getAuthenticatedUser(config);
@@ -1675,7 +1770,7 @@ ${lines.join("\n")}`;
1675
1770
  return `Error: ${err.message}`;
1676
1771
  }
1677
1772
  }
1678
- var execute5 = (_input, config) => run5(config);
1773
+ var execute5 = run5;
1679
1774
  var terminal5 = true;
1680
1775
 
1681
1776
  // src/tools/review/index.ts
@@ -1696,7 +1791,7 @@ var definition6 = {
1696
1791
  parameters: { type: "object", properties: {}, required: [] }
1697
1792
  }
1698
1793
  };
1699
- async function run6(config) {
1794
+ async function run6(_input, config) {
1700
1795
  const spinner = ora7("Loading tasks for review\u2026").start();
1701
1796
  try {
1702
1797
  const me = await getAuthenticatedUser(config);
@@ -1711,7 +1806,7 @@ ${lines.join("\n")}`;
1711
1806
  return `Error: ${err.message}`;
1712
1807
  }
1713
1808
  }
1714
- var execute6 = (_input, config) => run6(config);
1809
+ var execute6 = run6;
1715
1810
  var terminal6 = true;
1716
1811
 
1717
1812
  // src/tools/refresh/index.ts
@@ -1730,7 +1825,7 @@ var definition7 = {
1730
1825
  parameters: { type: "object", properties: {}, required: [] }
1731
1826
  }
1732
1827
  };
1733
- async function run7(config) {
1828
+ async function run7(_input, config) {
1734
1829
  const tasks = await printTaskList(config);
1735
1830
  if (tasks.length === 0) return "No tasks found.";
1736
1831
  const lines = tasks.map((t) => {
@@ -1741,7 +1836,7 @@ async function run7(config) {
1741
1836
  return `Tasks (${tasks.length}):
1742
1837
  ${lines.join("\n")}`;
1743
1838
  }
1744
- var execute7 = (_input, config) => run7(config);
1839
+ var execute7 = run7;
1745
1840
  var terminal7 = true;
1746
1841
 
1747
1842
  // src/tools/open-code/index.ts
@@ -1761,7 +1856,7 @@ var definition8 = {
1761
1856
  parameters: { type: "object", properties: {}, required: [] }
1762
1857
  }
1763
1858
  };
1764
- async function run8(config) {
1859
+ async function run8(_input, config) {
1765
1860
  let branch;
1766
1861
  try {
1767
1862
  branch = await getCurrentBranch();
@@ -1780,7 +1875,7 @@ async function run8(config) {
1780
1875
  await launchClaudeCode(issue, branch);
1781
1876
  return "Claude Code session ended.";
1782
1877
  }
1783
- var execute8 = (_input, config) => run8(config);
1878
+ var execute8 = run8;
1784
1879
  var terminal8 = true;
1785
1880
 
1786
1881
  // src/tools/reject/index.ts
@@ -1840,8 +1935,15 @@ var definition9 = {
1840
1935
  }
1841
1936
  }
1842
1937
  };
1843
- async function run9(config, opts) {
1844
- const { issue_number: issueNumber } = opts;
1938
+ async function run9(input4, config) {
1939
+ const issueNumber = input4["issue_number"];
1940
+ const [me, issue] = await Promise.all([
1941
+ getAuthenticatedUser(config),
1942
+ getTask(config, issueNumber)
1943
+ ]);
1944
+ if (issue.author && issue.author !== me) {
1945
+ return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
1946
+ }
1845
1947
  let feedback;
1846
1948
  try {
1847
1949
  feedback = await promptInput3({
@@ -1908,9 +2010,16 @@ async function run9(config, opts) {
1908
2010
  return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
1909
2011
  }
1910
2012
  }
1911
- async function execute9(input3, config) {
1912
- const issueNumber = input3["issue_number"];
1913
- const feedback = input3["feedback"];
2013
+ async function execute9(input4, config) {
2014
+ const issueNumber = input4["issue_number"];
2015
+ const feedback = input4["feedback"];
2016
+ const [me, issue] = await Promise.all([
2017
+ getAuthenticatedUser(config),
2018
+ getTask(config, issueNumber)
2019
+ ]);
2020
+ if (issue.author && issue.author !== me) {
2021
+ return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
2022
+ }
1914
2023
  let comment;
1915
2024
  try {
1916
2025
  comment = await generateRejectionComment(config, issueNumber, feedback);
@@ -1960,18 +2069,18 @@ var definition10 = {
1960
2069
  }
1961
2070
  }
1962
2071
  };
1963
- async function run10(config, opts) {
1964
- let issueNumber = opts?.issue_number;
2072
+ async function run10(input4, config) {
2073
+ let issueNumber = input4["issue_number"];
1965
2074
  if (!issueNumber) {
1966
- const spinner2 = ora9("Loading tasks for review\u2026").start();
2075
+ const spinner3 = ora9("Loading tasks for review\u2026").start();
1967
2076
  let tasks;
1968
2077
  let me;
1969
2078
  try {
1970
2079
  me = await getAuthenticatedUser(config);
1971
2080
  tasks = await listTasksForReview(config, me);
1972
- spinner2.stop();
2081
+ spinner3.stop();
1973
2082
  } catch (err) {
1974
- spinner2.stop();
2083
+ spinner3.stop();
1975
2084
  return `Error: ${err.message}`;
1976
2085
  }
1977
2086
  if (tasks.length === 0) return "No tasks pending review.";
@@ -1987,6 +2096,22 @@ async function run10(config, opts) {
1987
2096
  return "Cancelled.";
1988
2097
  }
1989
2098
  }
2099
+ const spinner2 = ora9("Verifying permissions\u2026").start();
2100
+ let me2;
2101
+ let issue;
2102
+ try {
2103
+ [me2, issue] = await Promise.all([
2104
+ getAuthenticatedUser(config),
2105
+ getTask(config, issueNumber)
2106
+ ]);
2107
+ spinner2.stop();
2108
+ } catch (err) {
2109
+ spinner2.stop();
2110
+ return `Error: ${err.message}`;
2111
+ }
2112
+ if (issue.author && issue.author !== me2) {
2113
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2114
+ }
1990
2115
  const baseBranch = config.github.baseBranch ?? "main";
1991
2116
  let confirmed;
1992
2117
  try {
@@ -2013,8 +2138,15 @@ Issue closed.`;
2013
2138
  return `Error: ${err.message}`;
2014
2139
  }
2015
2140
  }
2016
- async function execute10(input3, config) {
2017
- const issueNumber = input3["issue_number"];
2141
+ async function execute10(input4, config) {
2142
+ const issueNumber = input4["issue_number"];
2143
+ const [me, issue] = await Promise.all([
2144
+ getAuthenticatedUser(config),
2145
+ getTask(config, issueNumber)
2146
+ ]);
2147
+ if (issue.author && issue.author !== me) {
2148
+ return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2149
+ }
2018
2150
  const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
2019
2151
  try {
2020
2152
  const result = await acceptTask(config, issueNumber);
@@ -2030,14 +2162,138 @@ Issue closed.`;
2030
2162
  }
2031
2163
  var terminal10 = true;
2032
2164
 
2165
+ // src/tools/edit-task/index.ts
2166
+ var edit_task_exports = {};
2167
+ __export(edit_task_exports, {
2168
+ definition: () => definition11,
2169
+ execute: () => execute11,
2170
+ run: () => run11,
2171
+ terminal: () => terminal11
2172
+ });
2173
+ init_github();
2174
+ import { select as select9, input as promptInput4 } from "@inquirer/prompts";
2175
+ import ora10 from "ora";
2176
+ var definition11 = {
2177
+ type: "function",
2178
+ function: {
2179
+ name: "edit_task",
2180
+ description: "Edit the title and/or body of an existing task (GitHub Issue). Equivalent to /edit.",
2181
+ parameters: {
2182
+ type: "object",
2183
+ properties: {
2184
+ issue_number: { type: "number", description: "Issue number to edit." },
2185
+ title: { type: "string", description: "New title." },
2186
+ body: { type: "string", description: "New body/description." }
2187
+ },
2188
+ required: ["issue_number", "title", "body"]
2189
+ }
2190
+ }
2191
+ };
2192
+ async function run11(input4, config) {
2193
+ let issueNumber = input4["issue_number"];
2194
+ if (!issueNumber) {
2195
+ let tasks;
2196
+ try {
2197
+ tasks = await listTasks(config);
2198
+ } catch (err) {
2199
+ return `Error loading tasks: ${err.message}`;
2200
+ }
2201
+ if (tasks.length === 0) return "No tasks found.";
2202
+ try {
2203
+ issueNumber = await select9({
2204
+ message: "Select task to edit:",
2205
+ choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
2206
+ });
2207
+ } catch {
2208
+ return "Cancelled.";
2209
+ }
2210
+ }
2211
+ let issue;
2212
+ try {
2213
+ issue = await getTask(config, issueNumber);
2214
+ } catch (err) {
2215
+ return `Error loading task: ${err.message}`;
2216
+ }
2217
+ let title;
2218
+ let body;
2219
+ try {
2220
+ title = await promptInput4({
2221
+ message: "Title:",
2222
+ default: issue.title
2223
+ });
2224
+ body = await promptInput4({
2225
+ message: "Description:",
2226
+ default: issue.body ?? ""
2227
+ });
2228
+ } catch {
2229
+ return "Cancelled.";
2230
+ }
2231
+ if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
2232
+ return "No changes made.";
2233
+ }
2234
+ const spinner = ora10(`Updating #${issueNumber}\u2026`).start();
2235
+ try {
2236
+ await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
2237
+ spinner.stop();
2238
+ return `Task #${issueNumber} updated.`;
2239
+ } catch (err) {
2240
+ spinner.stop();
2241
+ return `Error: ${err.message}`;
2242
+ }
2243
+ }
2244
+ async function execute11(input4, config) {
2245
+ const issueNumber = input4["issue_number"];
2246
+ const title = input4["title"];
2247
+ const body = input4["body"];
2248
+ const spinner = ora10(`Updating #${issueNumber}\u2026`).start();
2249
+ try {
2250
+ await editTask(config, issueNumber, title, body);
2251
+ spinner.stop();
2252
+ return `Task #${issueNumber} updated.`;
2253
+ } catch (err) {
2254
+ spinner.stop();
2255
+ return `Error: ${err.message}`;
2256
+ }
2257
+ }
2258
+ var terminal11 = true;
2259
+
2260
+ // src/tools/list-tasks/index.ts
2261
+ var list_tasks_exports = {};
2262
+ __export(list_tasks_exports, {
2263
+ definition: () => definition12,
2264
+ execute: () => execute12
2265
+ });
2266
+ init_github();
2267
+ var definition12 = {
2268
+ type: "function",
2269
+ function: {
2270
+ name: "list_tasks",
2271
+ description: "List all open tasks (GitHub Issues) with their status and assignee. Use this to answer questions about available work, task progress, or who is working on what.",
2272
+ parameters: {
2273
+ type: "object",
2274
+ properties: {},
2275
+ required: []
2276
+ }
2277
+ }
2278
+ };
2279
+ async function execute12(_input, config) {
2280
+ const tasks = await listTasks(config);
2281
+ if (tasks.length === 0) return "No open tasks.";
2282
+ return tasks.map((t) => {
2283
+ const status = t.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2284
+ const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
2285
+ return `#${t.number} [${status}] ${assignee} ${t.title}`;
2286
+ }).join("\n");
2287
+ }
2288
+
2033
2289
  // src/tools/get-task/index.ts
2034
2290
  var get_task_exports = {};
2035
2291
  __export(get_task_exports, {
2036
- definition: () => definition11,
2037
- execute: () => execute11
2292
+ definition: () => definition13,
2293
+ execute: () => execute13
2038
2294
  });
2039
2295
  init_github();
2040
- var definition11 = {
2296
+ var definition13 = {
2041
2297
  type: "function",
2042
2298
  function: {
2043
2299
  name: "get_task",
@@ -2051,8 +2307,8 @@ var definition11 = {
2051
2307
  }
2052
2308
  }
2053
2309
  };
2054
- async function execute11(input3, config) {
2055
- const issue = await getTask(config, input3["issue_number"]);
2310
+ async function execute13(input4, config) {
2311
+ const issue = await getTask(config, input4["issue_number"]);
2056
2312
  const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2057
2313
  const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
2058
2314
  const lines = [
@@ -2068,12 +2324,12 @@ ${issue.body}`);
2068
2324
  // src/tools/get-comments/index.ts
2069
2325
  var get_comments_exports = {};
2070
2326
  __export(get_comments_exports, {
2071
- definition: () => definition12,
2072
- execute: () => execute12
2327
+ definition: () => definition14,
2328
+ execute: () => execute14
2073
2329
  });
2074
2330
  init_github();
2075
- import ora10 from "ora";
2076
- var definition12 = {
2331
+ import ora11 from "ora";
2332
+ var definition14 = {
2077
2333
  type: "function",
2078
2334
  function: {
2079
2335
  name: "get_comments",
@@ -2088,10 +2344,10 @@ var definition12 = {
2088
2344
  }
2089
2345
  }
2090
2346
  };
2091
- async function execute12(input3, config) {
2092
- const issueNumber = input3["issue_number"];
2093
- const limit = input3["limit"] ?? 5;
2094
- const spinner = ora10(`Loading comments for #${issueNumber}...`).start();
2347
+ async function execute14(input4, config) {
2348
+ const issueNumber = input4["issue_number"];
2349
+ const limit = input4["limit"] ?? 5;
2350
+ const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
2095
2351
  try {
2096
2352
  const comments = await listComments(config, issueNumber, limit);
2097
2353
  spinner.stop();
@@ -2110,11 +2366,11 @@ ${lines.join("\n\n")}`;
2110
2366
  // src/tools/get-diff/index.ts
2111
2367
  var get_diff_exports = {};
2112
2368
  __export(get_diff_exports, {
2113
- definition: () => definition13,
2114
- execute: () => execute13
2369
+ definition: () => definition15,
2370
+ execute: () => execute15
2115
2371
  });
2116
- import ora11 from "ora";
2117
- var definition13 = {
2372
+ import ora12 from "ora";
2373
+ var definition15 = {
2118
2374
  type: "function",
2119
2375
  function: {
2120
2376
  name: "get_diff",
@@ -2122,8 +2378,8 @@ var definition13 = {
2122
2378
  parameters: { type: "object", properties: {}, required: [] }
2123
2379
  }
2124
2380
  };
2125
- async function execute13(_input, _config) {
2126
- const spinner = ora11("Reading git diff...").start();
2381
+ async function execute15(_input, _config) {
2382
+ const spinner = ora12("Reading git diff...").start();
2127
2383
  try {
2128
2384
  const diff = await getDiff(_config.github.baseBranch);
2129
2385
  spinner.stop();
@@ -2137,14 +2393,14 @@ async function execute13(_input, _config) {
2137
2393
  // src/tools/run-command/index.ts
2138
2394
  var run_command_exports = {};
2139
2395
  __export(run_command_exports, {
2140
- definition: () => definition14,
2141
- execute: () => execute14
2396
+ definition: () => definition16,
2397
+ execute: () => execute16
2142
2398
  });
2143
2399
  import { exec } from "child_process";
2144
2400
  import { promisify } from "util";
2145
- import ora12 from "ora";
2401
+ import ora13 from "ora";
2146
2402
  var execAsync = promisify(exec);
2147
- var definition14 = {
2403
+ var definition16 = {
2148
2404
  type: "function",
2149
2405
  function: {
2150
2406
  name: "run_command",
@@ -2158,10 +2414,10 @@ var definition14 = {
2158
2414
  }
2159
2415
  }
2160
2416
  };
2161
- async function execute14(input3, _config) {
2162
- const command = input3["command"];
2417
+ async function execute16(input4, _config) {
2418
+ const command = input4["command"];
2163
2419
  const cwd = process.cwd();
2164
- const spinner = ora12(`$ ${command}`).start();
2420
+ const spinner = ora13(`$ ${command}`).start();
2165
2421
  try {
2166
2422
  const { stdout, stderr } = await execAsync(command, { cwd, timeout: 6e4, maxBuffer: 1024 * 1024 });
2167
2423
  spinner.stop();
@@ -2180,10 +2436,10 @@ ${out || e.message}`;
2180
2436
  // src/tools/scan-project/index.ts
2181
2437
  var scan_project_exports = {};
2182
2438
  __export(scan_project_exports, {
2183
- definition: () => definition15,
2184
- execute: () => execute15
2439
+ definition: () => definition17,
2440
+ execute: () => execute17
2185
2441
  });
2186
- import ora13 from "ora";
2442
+ import ora14 from "ora";
2187
2443
 
2188
2444
  // src/lib/project.ts
2189
2445
  import { readFile as readFile2 } from "fs/promises";
@@ -2334,7 +2590,7 @@ async function buildProjectContext(cwd, issueTitle, issueBody) {
2334
2590
  }
2335
2591
 
2336
2592
  // src/tools/scan-project/index.ts
2337
- var definition15 = {
2593
+ var definition17 = {
2338
2594
  type: "function",
2339
2595
  function: {
2340
2596
  name: "scan_project",
@@ -2351,9 +2607,9 @@ var definition15 = {
2351
2607
  }
2352
2608
  }
2353
2609
  };
2354
- async function execute15(input3, _config) {
2355
- const focus = input3["focus"] ?? "";
2356
- const spinner = ora13("Scanning project...").start();
2610
+ async function execute17(input4, _config) {
2611
+ const focus = input4["focus"] ?? "";
2612
+ const spinner = ora14("Scanning project...").start();
2357
2613
  try {
2358
2614
  const cwd = process.cwd();
2359
2615
  const context = await buildProjectContext(cwd, focus, "");
@@ -2382,12 +2638,12 @@ ${content}
2382
2638
  // src/tools/read-file/index.ts
2383
2639
  var read_file_exports = {};
2384
2640
  __export(read_file_exports, {
2385
- definition: () => definition16,
2386
- execute: () => execute16
2641
+ definition: () => definition18,
2642
+ execute: () => execute18
2387
2643
  });
2388
2644
  import { readFile as readFile3 } from "fs/promises";
2389
2645
  import path3 from "path";
2390
- var definition16 = {
2646
+ var definition18 = {
2391
2647
  type: "function",
2392
2648
  function: {
2393
2649
  name: "read_file",
@@ -2401,8 +2657,8 @@ var definition16 = {
2401
2657
  }
2402
2658
  }
2403
2659
  };
2404
- async function execute16(input3, _config) {
2405
- const filePath = input3["path"];
2660
+ async function execute18(input4, _config) {
2661
+ const filePath = input4["path"];
2406
2662
  try {
2407
2663
  const fullPath = path3.join(process.cwd(), filePath);
2408
2664
  const content = await readFile3(fullPath, "utf-8");
@@ -2415,12 +2671,12 @@ async function execute16(input3, _config) {
2415
2671
  // src/tools/ask-user/index.ts
2416
2672
  var ask_user_exports = {};
2417
2673
  __export(ask_user_exports, {
2418
- definition: () => definition17,
2419
- execute: () => execute17
2674
+ definition: () => definition19,
2675
+ execute: () => execute19
2420
2676
  });
2421
2677
  import chalk12 from "chalk";
2422
- import { select as select9, input as promptInput4 } from "@inquirer/prompts";
2423
- var definition17 = {
2678
+ import { select as select10, input as promptInput5 } from "@inquirer/prompts";
2679
+ var definition19 = {
2424
2680
  type: "function",
2425
2681
  function: {
2426
2682
  name: "ask_user",
@@ -2439,9 +2695,9 @@ var definition17 = {
2439
2695
  }
2440
2696
  }
2441
2697
  };
2442
- async function execute17(input3, _config) {
2443
- const question = input3["question"];
2444
- const options = input3["options"];
2698
+ async function execute19(input4, _config) {
2699
+ const question = input4["question"];
2700
+ const options = input4["options"];
2445
2701
  const OTHER = "__other__";
2446
2702
  console.log("");
2447
2703
  console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
@@ -2452,14 +2708,14 @@ async function execute17(input3, _config) {
2452
2708
  console.log(chalk12.dim(" \u2514" + "\u2500".repeat(67)));
2453
2709
  let answer;
2454
2710
  try {
2455
- const chosen = await select9({
2711
+ const chosen = await select10({
2456
2712
  message: " ",
2457
2713
  choices: [
2458
2714
  ...options.map((o) => ({ name: o, value: o })),
2459
2715
  { name: chalk12.dim("Other (describe below)"), value: OTHER }
2460
2716
  ]
2461
2717
  });
2462
- answer = chosen === OTHER ? await promptInput4({ message: "Your answer:" }) : chosen;
2718
+ answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
2463
2719
  } catch {
2464
2720
  answer = "User skipped this question \u2014 use your best judgement.";
2465
2721
  }
@@ -2480,7 +2736,9 @@ var toolModules = [
2480
2736
  open_code_exports,
2481
2737
  reject_exports,
2482
2738
  accept_exports,
2739
+ edit_task_exports,
2483
2740
  // Low-level tools
2741
+ list_tasks_exports,
2484
2742
  get_task_exports,
2485
2743
  get_comments_exports,
2486
2744
  get_diff_exports,
@@ -2492,11 +2750,12 @@ var toolModules = [
2492
2750
 
2493
2751
  // src/lib/agent.ts
2494
2752
  var tools = toolModules.map((m) => m.definition);
2495
- async function executeTool(name, input3, config) {
2753
+ async function executeTool(name, input4, config) {
2496
2754
  const mod = toolModules.find((m) => m.definition.function.name === name);
2497
2755
  if (!mod) return `Unknown tool: ${name}`;
2498
2756
  try {
2499
- return await mod.execute(input3, config);
2757
+ const fn = mod.run ?? mod.execute;
2758
+ return await fn(input4, config);
2500
2759
  } catch (err) {
2501
2760
  return `Error: ${err.message}`;
2502
2761
  }
@@ -2549,9 +2808,7 @@ async function runAgentLoop(config, messages) {
2549
2808
  if (++iterations > MAX_ITERATIONS) {
2550
2809
  throw new Error(`Agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
2551
2810
  }
2552
- const isWindows = process.platform === "win32";
2553
- const spinner = isWindows ? null : ora14({ text: chalk13.dim("Thinking\u2026"), color: "cyan" }).start();
2554
- if (isWindows) process.stdout.write(chalk13.dim(" Thinking\u2026"));
2811
+ const spinner = ora15({ text: chalk13.dim("Thinking\u2026"), color: "cyan" }).start();
2555
2812
  let response;
2556
2813
  try {
2557
2814
  response = await client.chat.completions.create({
@@ -2560,12 +2817,10 @@ async function runAgentLoop(config, messages) {
2560
2817
  messages: [systemMessage, ...messages]
2561
2818
  });
2562
2819
  } catch (err) {
2563
- if (spinner) spinner.stop();
2564
- else process.stdout.write("\r" + " ".repeat(14) + "\r");
2820
+ spinner.stop();
2565
2821
  throw err;
2566
2822
  }
2567
- if (spinner) spinner.stop();
2568
- else process.stdout.write("\r" + " ".repeat(14) + "\r");
2823
+ spinner.stop();
2569
2824
  const choice = response.choices[0];
2570
2825
  const assistantMessage = {
2571
2826
  role: "assistant",
@@ -2598,7 +2853,7 @@ async function runAgentLoop(config, messages) {
2598
2853
  return executeTool(tc.function.name, parsed, config);
2599
2854
  })
2600
2855
  );
2601
- let terminal11 = false;
2856
+ let terminal12 = false;
2602
2857
  for (let i = 0; i < toolCalls.length; i++) {
2603
2858
  printToolResult(results[i]);
2604
2859
  messages.push({
@@ -2607,10 +2862,10 @@ async function runAgentLoop(config, messages) {
2607
2862
  content: results[i]
2608
2863
  });
2609
2864
  if (toolModules.find((m) => m.definition.function.name === toolCalls[i].function.name)?.terminal) {
2610
- terminal11 = true;
2865
+ terminal12 = true;
2611
2866
  }
2612
2867
  }
2613
- if (terminal11) return results[results.length - 1];
2868
+ if (terminal12) return results[results.length - 1];
2614
2869
  } else {
2615
2870
  return choice.message.content ?? "";
2616
2871
  }
@@ -2633,6 +2888,8 @@ var SLASH_NAMES = [
2633
2888
  "/s",
2634
2889
  "/close",
2635
2890
  "/d",
2891
+ "/edit",
2892
+ "/e",
2636
2893
  "/review",
2637
2894
  "/rv",
2638
2895
  "/accept",
@@ -2642,7 +2899,8 @@ var SLASH_NAMES = [
2642
2899
  "/code",
2643
2900
  "/c",
2644
2901
  "/config",
2645
- "/cfg"
2902
+ "/cfg",
2903
+ "/init"
2646
2904
  ];
2647
2905
  function completer(line) {
2648
2906
  if (line.startsWith("/")) {
@@ -2663,10 +2921,12 @@ var COMMANDS = [
2663
2921
  { cmd: "/pick", alias: "/p", desc: "Browse tasks and view details" },
2664
2922
  { cmd: "/new", alias: "/n", desc: "Create a new task interactively" },
2665
2923
  { cmd: "/close", alias: "/d", desc: "Close (delete) a task" },
2924
+ { cmd: "/edit", alias: "/e", desc: "Edit the title or description of a task" },
2666
2925
  { cmd: "/submit", alias: "/s", desc: "Commit, create PR, and mark in-review" },
2667
2926
  { cmd: "/review", alias: "/rv", desc: "List tasks waiting for your approval" },
2668
2927
  { cmd: "/accept", alias: "/ac", desc: "Accept a reviewed task: merge PR and close issue" },
2669
2928
  { cmd: "/config", alias: "/cfg", desc: "Change settings (branch, repo, API keys)" },
2929
+ { cmd: "/init", desc: "Re-run setup wizard for this repo" },
2670
2930
  { cmd: "/status", alias: "/me", desc: "Show tasks assigned to you" },
2671
2931
  { cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" }
2672
2932
  ];
@@ -2695,6 +2955,30 @@ function printBanner(config) {
2695
2955
  );
2696
2956
  console.log("");
2697
2957
  }
2958
+ async function initNewRepo(config, owner, repo) {
2959
+ console.log("");
2960
+ console.log(chalk14.bold.cyan(` New repo detected: ${owner}/${repo}`));
2961
+ console.log(chalk14.dim(" Setting up Techunter for this repository...\n"));
2962
+ const baseBranch = await input3({
2963
+ message: "Main branch to merge PRs into:",
2964
+ default: "main"
2965
+ });
2966
+ const newConfig = {
2967
+ ...config,
2968
+ github: { owner, repo, baseBranch: baseBranch.trim() || "main" }
2969
+ };
2970
+ setConfig({ github: newConfig.github });
2971
+ const ora16 = (await import("ora")).default;
2972
+ const spinner = ora16("Creating Techunter labels...").start();
2973
+ try {
2974
+ await ensureLabels(newConfig);
2975
+ spinner.succeed("Labels ready");
2976
+ } catch (err) {
2977
+ spinner.fail(`Could not create labels: ${err.message}`);
2978
+ }
2979
+ console.log("");
2980
+ return newConfig;
2981
+ }
2698
2982
  async function main() {
2699
2983
  const args = process.argv.slice(2);
2700
2984
  if (args[0] === "config") {
@@ -2726,11 +3010,10 @@ Setup failed: ${err.message}`));
2726
3010
  const detected = parseOwnerRepo(remoteUrl);
2727
3011
  if (detected) {
2728
3012
  const isNewRepo = detected.owner !== config.github.owner || detected.repo !== config.github.repo;
2729
- config = { ...config, github: { ...config.github, owner: detected.owner, repo: detected.repo, baseBranch: void 0 } };
2730
3013
  if (isNewRepo) {
2731
- console.log(chalk14.dim(` Repo: ${detected.owner}/${detected.repo}`));
2732
- ensureLabels(config).catch(() => {
2733
- });
3014
+ config = await initNewRepo(config, detected.owner, detected.repo);
3015
+ } else {
3016
+ config = { ...config, github: { ...config.github, owner: detected.owner, repo: detected.repo } };
2734
3017
  }
2735
3018
  }
2736
3019
  } else if (!config.github.owner) {
@@ -2769,13 +3052,13 @@ Setup failed: ${err.message}`));
2769
3052
  break;
2770
3053
  case "/refresh":
2771
3054
  case "/r":
2772
- await run7(config);
3055
+ await run7({}, config);
2773
3056
  break;
2774
3057
  case "/pick":
2775
3058
  case "/p": {
2776
3059
  const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
2777
3060
  const preselected = arg ? parseInt(arg, 10) : void 0;
2778
- const result = await run3(config, Number.isNaN(preselected) ? void 0 : preselected);
3061
+ const result = await run3({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
2779
3062
  if (result && result !== "Cancelled.") {
2780
3063
  console.log(chalk14.green(`
2781
3064
  ${result}
@@ -2786,7 +3069,7 @@ Setup failed: ${err.message}`));
2786
3069
  }
2787
3070
  case "/new":
2788
3071
  case "/n": {
2789
- const result = await run4(config);
3072
+ const result = await run4({}, config);
2790
3073
  console.log(chalk14.green(`
2791
3074
  ${result}
2792
3075
  `));
@@ -2795,14 +3078,25 @@ Setup failed: ${err.message}`));
2795
3078
  }
2796
3079
  case "/submit":
2797
3080
  case "/s": {
2798
- const result = await run(config);
3081
+ const result = await run({}, config);
2799
3082
  console.log("\n" + renderMarkdown(result));
2800
3083
  await printTaskList(config);
2801
3084
  break;
2802
3085
  }
3086
+ case "/edit":
3087
+ case "/e": {
3088
+ const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
3089
+ const preselected = arg ? parseInt(arg, 10) : void 0;
3090
+ const result = await run11({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
3091
+ console.log(chalk14.green(`
3092
+ ${result}
3093
+ `));
3094
+ await printTaskList(config);
3095
+ break;
3096
+ }
2803
3097
  case "/close":
2804
3098
  case "/d": {
2805
- const result = await run2(config);
3099
+ const result = await run2({}, config);
2806
3100
  console.log(chalk14.green(`
2807
3101
  ${result}
2808
3102
  `));
@@ -2811,13 +3105,13 @@ Setup failed: ${err.message}`));
2811
3105
  }
2812
3106
  case "/review":
2813
3107
  case "/rv": {
2814
- const result = await run6(config);
3108
+ const result = await run6({}, config);
2815
3109
  console.log("\n" + renderMarkdown(result));
2816
3110
  break;
2817
3111
  }
2818
3112
  case "/status":
2819
3113
  case "/me": {
2820
- const result = await run5(config);
3114
+ const result = await run5({}, config);
2821
3115
  console.log("\n" + renderMarkdown(result));
2822
3116
  break;
2823
3117
  }
@@ -2825,7 +3119,7 @@ Setup failed: ${err.message}`));
2825
3119
  case "/ac": {
2826
3120
  const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
2827
3121
  const preselected = arg ? parseInt(arg, 10) : void 0;
2828
- const result = await run10(config, Number.isNaN(preselected) ? void 0 : { issue_number: preselected });
3122
+ const result = await run10({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
2829
3123
  console.log(chalk14.green(`
2830
3124
  ${result}
2831
3125
  `));
@@ -2836,9 +3130,20 @@ Setup failed: ${err.message}`));
2836
3130
  case "/cfg":
2837
3131
  await configCommand();
2838
3132
  break;
3133
+ case "/init":
3134
+ try {
3135
+ await initCommand();
3136
+ config = getConfig();
3137
+ await printTaskList(config);
3138
+ } catch (err) {
3139
+ console.error(chalk14.red(`
3140
+ Init failed: ${err.message}
3141
+ `));
3142
+ }
3143
+ break;
2839
3144
  case "/code":
2840
3145
  case "/c":
2841
- await run8(config);
3146
+ await run8({}, config);
2842
3147
  break;
2843
3148
  default:
2844
3149
  console.log(chalk14.yellow(` Unknown command: ${cmd} (try /help)`));