techunter 0.1.2 → 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 +114 -119
  3. package/dist/mcp.js +59 -39
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  > An AI-powered task distribution CLI for development teams. Manage GitHub Issues through a conversational terminal interface.
4
4
 
5
5
  ```
6
- ▗▄▄▄▄▖ Techunter v0.1.1
7
- ◆──▐████▌══▶ GLM-5 · zai-org
8
- ▝▀▀▀▀▘ owner/repo
6
+ ╔═══════════════╗
7
+ ◆═══╬ TECHUNTER ╬═══▶ Techunter v0.1.3
8
+ ╚═══════════════╝ GLM-5 · z-ai · owner/repo
9
9
  ```
10
10
 
11
11
  ---
@@ -29,10 +29,11 @@
29
29
  ## Features
30
30
 
31
31
  - **Conversational REPL** — Describe what you need in plain English; the Agent calls the right tools automatically
32
- - **GitHub Issues integration** — Create, claim, submit, and close tasks; labels and assignees stay in sync
32
+ - **GitHub Issues integration** — Create, claim, submit, review, and close tasks; labels and assignees stay in sync
33
33
  - **Automatic branch management** — Claiming a task creates and pushes the corresponding Git branch
34
34
  - **Smart task guides** — Before you start, the Agent scans your codebase and posts a detailed implementation guide as an Issue comment
35
- - **One-command delivery** — `/submit` lets the Agent review your changes against acceptance criteria, then commits and pushes; `/deliver` opens a PR and marks it as in-review
35
+ - **One-command delivery** — `/submit` lets the Agent review your changes against acceptance criteria, then commits and pushes
36
+ - **Review & accept flow** — `/review` lists in-review PRs; `/accept` merges and closes
36
37
  - **Slash commands** — Common actions don't need a description: just `/pick`, `/new`, `/submit`
37
38
  - **Persistent conversation history** — Full context is retained across turns in the same REPL session
38
39
 
@@ -43,7 +44,7 @@
43
44
  - Node.js ≥ 18
44
45
  - A GitHub repository with Issues enabled
45
46
  - GitHub Personal Access Token or OAuth Device Flow authorization
46
- - [ppio.com](https://ppio.com) API Key (uses the GLM-5 model)
47
+ - An OpenAI-compatible API key (OpenRouter by default, or any custom provider)
47
48
 
48
49
  ---
49
50
 
@@ -75,9 +76,10 @@ tch init
75
76
 
76
77
  The wizard will ask for:
77
78
 
78
- 1. **GitHub authentication** — A Personal Access Token is recommended
79
- - Create one at https://github.com/settings/tokens/new with `repo` and `read:user` scopes
80
- 2. **PPIO API Key** — Get yours from the [ppio.com](https://ppio.com) console API Keys
79
+ 1. **GitHub authentication** — Browser OAuth (recommended) or a Personal Access Token
80
+ - PAT: create one at https://github.com/settings/tokens/new with `repo` and `read:user` scopes
81
+ 2. **AI provider** — OpenRouter (default) or a custom OpenAI-compatible endpoint
82
+ - OpenRouter key: https://openrouter.ai/settings/keys
81
83
  3. **GitHub repository** — Auto-detected from your git remote, or enter manually
82
84
 
83
85
  Config is stored at `~/.config/techunter/`.
@@ -99,10 +101,14 @@ Starts the conversational REPL. Type natural language or slash commands:
99
101
  | `/pick` | `/p` | Browse and act on tasks interactively |
100
102
  | `/new` | `/n` | Create a new task |
101
103
  | `/close` | `/d` | Close (delete) a task |
102
- | `/submit` | `/s` | Review changes for a task and commit + push |
103
- | `/review` | `/rv` | Review team submissions approve or request changes |
104
+ | `/edit` | `/e` | Edit the title or description of a task |
105
+ | `/submit` | `/s` | Review changes, commit, and push |
106
+ | `/review` | `/rv` | List tasks waiting for your approval |
107
+ | `/accept` | `/ac` | Accept a reviewed task: merge PR and close issue |
104
108
  | `/status` | `/me` | Show tasks assigned to you |
105
109
  | `/code` | `/c` | Launch Claude Code for the current task branch |
110
+ | `/config` | `/cfg` | Change settings (repo, API keys, etc.) |
111
+ | `/init` | | Re-run the setup wizard for this repo |
106
112
 
107
113
  Any other input is sent to the AI Agent, for example:
108
114
 
@@ -141,6 +147,12 @@ task-{issue_number}-{your-github-username}
141
147
 
142
148
  Example: Issue #7 claimed by `johndoe` → `task-7-johndoe`
143
149
 
150
+ Worker branches (for persistent personal workspaces) follow:
151
+
152
+ ```
153
+ worker-{your-github-username}
154
+ ```
155
+
144
156
  ---
145
157
 
146
158
  ## AI Agent Tools
package/dist/index.js CHANGED
@@ -21,7 +21,6 @@ __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,
27
26
  isCollaborator: () => isCollaborator,
@@ -309,10 +308,6 @@ async function getDefaultBranch(config) {
309
308
  const { data } = await octokit.repos.get({ owner, repo });
310
309
  return data.default_branch;
311
310
  }
312
- function getBaseBranch(config) {
313
- if (config.github.baseBranch) return Promise.resolve(config.github.baseBranch);
314
- return getDefaultBranch(config);
315
- }
316
311
  async function acceptTask(config, issueNumber) {
317
312
  const octokit = createOctokit(config.githubToken);
318
313
  const { owner, repo } = config.github;
@@ -350,7 +345,6 @@ var init_github = __esm({
350
345
  import chalk14 from "chalk";
351
346
  import readline from "readline";
352
347
  import { createRequire } from "module";
353
- import { input as input3 } from "@inquirer/prompts";
354
348
 
355
349
  // src/commands/init.ts
356
350
  import { input, password, select } from "@inquirer/prompts";
@@ -370,8 +364,7 @@ var configSchema = z.object({
370
364
  githubClientId: z.string().optional(),
371
365
  github: z.object({
372
366
  owner: z.string().min(1),
373
- repo: z.string().min(1),
374
- baseBranch: z.string().optional()
367
+ repo: z.string().min(1)
375
368
  }),
376
369
  taskState: z.object({
377
370
  activeIssueNumber: z.number().optional(),
@@ -702,11 +695,6 @@ async function initCommand() {
702
695
  required: true
703
696
  });
704
697
  }
705
- const detectedDefault = "main";
706
- const baseBranch = await input({
707
- message: "Main branch to merge PRs into:",
708
- default: detectedDefault
709
- });
710
698
  const config = {
711
699
  githubToken,
712
700
  githubClientId,
@@ -715,8 +703,7 @@ async function initCommand() {
715
703
  ...aiModel ? { aiModel } : {},
716
704
  github: {
717
705
  owner: owner.trim(),
718
- repo: repo.trim(),
719
- baseBranch: baseBranch.trim() || detectedDefault
706
+ repo: repo.trim()
720
707
  }
721
708
  };
722
709
  setConfig(config);
@@ -752,7 +739,6 @@ async function configCommand() {
752
739
  const field = await select2({
753
740
  message: "Which setting to change?",
754
741
  choices: [
755
- { name: `Base branch ${chalk3.dim(config.github.baseBranch ?? "(not set, uses repo default)")}`, value: "baseBranch" },
756
742
  { name: `GitHub repo ${chalk3.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
757
743
  { name: `AI base URL ${chalk3.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
758
744
  { name: `AI model ${chalk3.dim(currentModel)}`, value: "aiModel" },
@@ -762,16 +748,7 @@ async function configCommand() {
762
748
  ]
763
749
  });
764
750
  if (field === "cancel") return;
765
- if (field === "baseBranch") {
766
- const val = await input2({
767
- message: "Main branch to merge PRs into:",
768
- default: config.github.baseBranch ?? "main"
769
- });
770
- setConfig({ github: { ...config.github, baseBranch: val.trim() || "main" } });
771
- console.log(chalk3.green(`
772
- Base branch set to: ${val.trim() || "main"}
773
- `));
774
- } else if (field === "repo") {
751
+ if (field === "repo") {
775
752
  const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
776
753
  const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
777
754
  setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
@@ -995,8 +972,8 @@ import { select as select3, input as promptInput } from "@inquirer/prompts";
995
972
 
996
973
  // src/lib/agent-ui.ts
997
974
  import chalk6 from "chalk";
998
- function formatInput(input4) {
999
- return Object.entries(input4).map(([k, v]) => {
975
+ function formatInput(input3) {
976
+ return Object.entries(input3).map(([k, v]) => {
1000
977
  if (typeof v === "number") return `${k}=${v}`;
1001
978
  if (typeof v === "string") {
1002
979
  if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
@@ -1009,8 +986,8 @@ function summarize(result) {
1009
986
  const first = result.split("\n").find((l) => l.trim()) ?? result;
1010
987
  return first.length > 100 ? first.slice(0, 97) + "..." : first;
1011
988
  }
1012
- function printToolCall(name, input4) {
1013
- const params = formatInput(input4);
989
+ function printToolCall(name, input3) {
990
+ const params = formatInput(input3);
1014
991
  console.log(` ${chalk6.cyan("\u2192")} ${chalk6.bold(name)}${params ? " " + chalk6.dim(params) : ""}`);
1015
992
  }
1016
993
  function printToolResult(result) {
@@ -1046,15 +1023,15 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
1046
1023
  }
1047
1024
  if (choice.finish_reason === "tool_calls") {
1048
1025
  for (const tc of choice.message.tool_calls ?? []) {
1049
- let input4;
1026
+ let input3;
1050
1027
  try {
1051
- input4 = JSON.parse(tc.function.arguments);
1028
+ input3 = JSON.parse(tc.function.arguments);
1052
1029
  } catch {
1053
- input4 = {};
1030
+ input3 = {};
1054
1031
  }
1055
- printToolCall(tc.function.name, input4);
1032
+ printToolCall(tc.function.name, input3);
1056
1033
  const mod = selected.find((m) => m.definition.function.name === tc.function.name);
1057
- 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}`;
1058
1035
  printToolResult(result);
1059
1036
  messages.push({ role: "tool", tool_call_id: tc.id, content: result });
1060
1037
  }
@@ -1103,14 +1080,14 @@ async function run(_input, config) {
1103
1080
  return "No active task found. Claim a task first with /pick.";
1104
1081
  }
1105
1082
  let spinner = ora2("Loading task and diff\u2026").start();
1106
- const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff(config.github.baseBranch);
1107
- const [issue, defaultBranch, diff, me] = await Promise.all([
1083
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1084
+ const [issue, diff, me] = await Promise.all([
1108
1085
  getTask(config, issueNumber),
1109
- getBaseBranch(config),
1110
1086
  diffPromise,
1111
1087
  getAuthenticatedUser(config)
1112
1088
  ]);
1113
1089
  spinner.stop();
1090
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
1114
1091
  const branch = await getCurrentBranch();
1115
1092
  const isSelfSubmit = issue.author !== null && issue.author === me;
1116
1093
  let review = "";
@@ -1175,7 +1152,7 @@ ${issue.body}` : "",
1175
1152
  ## AI Review
1176
1153
  ${review}` : ""
1177
1154
  ].join("\n").trim();
1178
- prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
1155
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
1179
1156
  spinner.stop();
1180
1157
  } catch (err) {
1181
1158
  spinner.stop();
@@ -1194,18 +1171,18 @@ ${review}` : ""
1194
1171
  Commit: "${commitMessage.trim()}"
1195
1172
  PR: ${prUrl}`;
1196
1173
  }
1197
- async function execute(input4, config) {
1174
+ async function execute(input3, config) {
1198
1175
  const taskState = getConfig().taskState;
1199
1176
  const issueNumber = taskState?.activeIssueNumber;
1200
1177
  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([
1178
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1179
+ const [issue, diff, branch, me] = await Promise.all([
1203
1180
  getTask(config, issueNumber),
1204
- getBaseBranch(config),
1205
1181
  diffPromise,
1206
1182
  getCurrentBranch(),
1207
1183
  getAuthenticatedUser(config)
1208
1184
  ]);
1185
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
1209
1186
  const isSelfSubmit = issue.author !== null && issue.author === me;
1210
1187
  let review = "";
1211
1188
  if (!isSelfSubmit) {
@@ -1215,7 +1192,7 @@ async function execute(input4, config) {
1215
1192
  review = `(Review failed: ${err.message})`;
1216
1193
  }
1217
1194
  }
1218
- const commitMessage = input4["commit_message"]?.trim() || `complete: ${issue.title}`;
1195
+ const commitMessage = input3["commit_message"]?.trim() || `complete: ${issue.title}`;
1219
1196
  try {
1220
1197
  await stageAllAndCommit(commitMessage);
1221
1198
  } catch (err) {
@@ -1231,7 +1208,7 @@ ${issue.body}` : "",
1231
1208
  ## AI Review
1232
1209
  ${review}` : ""
1233
1210
  ].join("\n").trim();
1234
- prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
1211
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
1235
1212
  } catch (err) {
1236
1213
  return `Committed but PR creation failed: ${err.message}`;
1237
1214
  }
@@ -1272,8 +1249,8 @@ var definition2 = {
1272
1249
  }
1273
1250
  }
1274
1251
  };
1275
- async function run2(input4, config) {
1276
- let issueNumber = input4["issue_number"];
1252
+ async function run2(input3, config) {
1253
+ let issueNumber = input3["issue_number"];
1277
1254
  if (!issueNumber) {
1278
1255
  let tasks;
1279
1256
  try {
@@ -1314,8 +1291,8 @@ async function run2(input4, config) {
1314
1291
  return `Error: ${err.message}`;
1315
1292
  }
1316
1293
  }
1317
- async function execute2(input4, config) {
1318
- const issueNumber = input4["issue_number"];
1294
+ async function execute2(input3, config) {
1295
+ const issueNumber = input3["issue_number"];
1319
1296
  const spinner = ora3(`Closing #${issueNumber}\u2026`).start();
1320
1297
  try {
1321
1298
  await closeTask(config, issueNumber);
@@ -1348,8 +1325,8 @@ var definition3 = {
1348
1325
  }
1349
1326
  }
1350
1327
  };
1351
- async function run3(input4, config) {
1352
- const preselected = input4["issue_number"];
1328
+ async function run3(input3, config) {
1329
+ const preselected = input3["issue_number"];
1353
1330
  let chosenNumber;
1354
1331
  if (preselected !== void 0) {
1355
1332
  chosenNumber = preselected;
@@ -1425,23 +1402,37 @@ Finish or submit it before claiming a new one.`;
1425
1402
  let spinner = ora4(`Claiming #${issue.number}\u2026`).start();
1426
1403
  await claimTask(config, issue.number, me);
1427
1404
  spinner.stop();
1428
- const branch = makeWorkerBranchName(me);
1429
- spinner = ora4(`Switching to branch ${branch}\u2026`).start();
1430
- let isNew = false;
1405
+ const workerBranch = makeWorkerBranchName(me);
1406
+ spinner = ora4(`Switching to ${workerBranch}\u2026`).start();
1431
1407
  try {
1432
- isNew = await switchToBranchOrCreate(branch);
1408
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
1433
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
+ }
1434
1419
  } catch {
1435
- spinner.warn(`Could not switch to branch ${branch}`);
1420
+ spinner.warn(`Could not switch to ${workerBranch}`);
1436
1421
  }
1437
- if (isNew) {
1438
- spinner = ora4("Pushing branch\u2026").start();
1422
+ const branch = makeBranchName(issue.number, me);
1423
+ spinner = ora4(`Creating task branch ${branch}\u2026`).start();
1424
+ try {
1425
+ await switchToBranchOrCreate(branch);
1426
+ spinner.stop();
1427
+ spinner = ora4("Pushing task branch\u2026").start();
1439
1428
  try {
1440
1429
  await pushBranch(branch);
1441
1430
  spinner.stop();
1442
1431
  } catch {
1443
- spinner.warn("Could not push branch");
1432
+ spinner.warn("Could not push task branch");
1444
1433
  }
1434
+ } catch {
1435
+ spinner.warn(`Could not create branch ${branch}`);
1445
1436
  }
1446
1437
  const baseCommit = await getCurrentCommit();
1447
1438
  setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
@@ -1470,9 +1461,9 @@ Finish or submit it before claiming a new one.`;
1470
1461
  if (action === "close") return run2({ issue_number: issue.number }, config);
1471
1462
  return "Cancelled.";
1472
1463
  }
1473
- async function execute3(input4, config) {
1474
- const issueNumber = input4["issue_number"];
1475
- const action = input4["action"];
1464
+ async function execute3(input3, config) {
1465
+ const issueNumber = input3["issue_number"];
1466
+ const action = input3["action"];
1476
1467
  let issue;
1477
1468
  try {
1478
1469
  issue = await getTask(config, issueNumber);
@@ -1499,17 +1490,25 @@ async function execute3(input4, config) {
1499
1490
  } catch (err) {
1500
1491
  return `Error claiming task: ${err.message}`;
1501
1492
  }
1502
- const branch = makeWorkerBranchName(me);
1503
- let isNew = false;
1493
+ const workerBranch = makeWorkerBranchName(me);
1504
1494
  try {
1505
- isNew = await switchToBranchOrCreate(branch);
1495
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
1496
+ if (isNewWorker) {
1497
+ try {
1498
+ await pushBranch(workerBranch);
1499
+ } catch {
1500
+ }
1501
+ }
1506
1502
  } catch {
1507
1503
  }
1508
- if (isNew) {
1509
- try {
1510
- await pushBranch(branch);
1511
- } catch {
1512
- }
1504
+ const branch = makeBranchName(issueNumber, me);
1505
+ try {
1506
+ await switchToBranchOrCreate(branch);
1507
+ } catch {
1508
+ }
1509
+ try {
1510
+ await pushBranch(branch);
1511
+ } catch {
1513
1512
  }
1514
1513
  const baseCommit = await getCurrentCommit();
1515
1514
  setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
@@ -1603,7 +1602,7 @@ var definition4 = {
1603
1602
  }
1604
1603
  }
1605
1604
  };
1606
- async function run4(input4, config) {
1605
+ async function run4(input3, config) {
1607
1606
  const authSpinner = ora5("Checking permissions\u2026").start();
1608
1607
  let me;
1609
1608
  let allowed;
@@ -1618,7 +1617,7 @@ async function run4(input4, config) {
1618
1617
  if (!allowed) {
1619
1618
  return `Permission denied: only repository collaborators can create tasks.`;
1620
1619
  }
1621
- let title = input4["title"]?.trim();
1620
+ let title = input3["title"]?.trim();
1622
1621
  if (!title) {
1623
1622
  try {
1624
1623
  title = (await promptInput2({ message: "Task title:" })).trim();
@@ -1714,13 +1713,13 @@ async function run4(input4, config) {
1714
1713
  }
1715
1714
  return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
1716
1715
  }
1717
- async function execute4(input4, config) {
1716
+ async function execute4(input3, config) {
1718
1717
  const me = await getAuthenticatedUser(config);
1719
1718
  if (!await isCollaborator(config, me)) {
1720
1719
  return `Permission denied: only repository collaborators can create tasks.`;
1721
1720
  }
1722
- const title = input4["title"].trim();
1723
- const feedback = input4["feedback"];
1721
+ const title = input3["title"].trim();
1722
+ const feedback = input3["feedback"];
1724
1723
  let guide = await generateGuide(config, title);
1725
1724
  if (feedback) {
1726
1725
  guide = await generateGuide(config, title, { feedback, previousGuide: guide });
@@ -1935,8 +1934,8 @@ var definition9 = {
1935
1934
  }
1936
1935
  }
1937
1936
  };
1938
- async function run9(input4, config) {
1939
- const issueNumber = input4["issue_number"];
1937
+ async function run9(input3, config) {
1938
+ const issueNumber = input3["issue_number"];
1940
1939
  const [me, issue] = await Promise.all([
1941
1940
  getAuthenticatedUser(config),
1942
1941
  getTask(config, issueNumber)
@@ -2010,9 +2009,9 @@ async function run9(input4, config) {
2010
2009
  return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
2011
2010
  }
2012
2011
  }
2013
- async function execute9(input4, config) {
2014
- const issueNumber = input4["issue_number"];
2015
- const feedback = input4["feedback"];
2012
+ async function execute9(input3, config) {
2013
+ const issueNumber = input3["issue_number"];
2014
+ const feedback = input3["feedback"];
2016
2015
  const [me, issue] = await Promise.all([
2017
2016
  getAuthenticatedUser(config),
2018
2017
  getTask(config, issueNumber)
@@ -2059,7 +2058,7 @@ var definition10 = {
2059
2058
  type: "function",
2060
2059
  function: {
2061
2060
  name: "accept",
2062
- 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.",
2063
2062
  parameters: {
2064
2063
  type: "object",
2065
2064
  properties: {
@@ -2069,8 +2068,8 @@ var definition10 = {
2069
2068
  }
2070
2069
  }
2071
2070
  };
2072
- async function run10(input4, config) {
2073
- let issueNumber = input4["issue_number"];
2071
+ async function run10(input3, config) {
2072
+ let issueNumber = input3["issue_number"];
2074
2073
  if (!issueNumber) {
2075
2074
  const spinner3 = ora9("Loading tasks for review\u2026").start();
2076
2075
  let tasks;
@@ -2112,11 +2111,11 @@ async function run10(input4, config) {
2112
2111
  if (issue.author && issue.author !== me2) {
2113
2112
  return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2114
2113
  }
2115
- const baseBranch = config.github.baseBranch ?? "main";
2114
+ const targetBranch = makeWorkerBranchName(me2);
2116
2115
  let confirmed;
2117
2116
  try {
2118
2117
  confirmed = await select8({
2119
- 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?`,
2120
2119
  choices: [
2121
2120
  { name: "Yes, accept", value: true },
2122
2121
  { name: "Cancel", value: false }
@@ -2129,17 +2128,17 @@ async function run10(input4, config) {
2129
2128
  const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
2130
2129
  try {
2131
2130
  const result = await acceptTask(config, issueNumber);
2132
- spinner.succeed(`PR #${result.prNumber} merged into ${baseBranch}`);
2131
+ spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
2133
2132
  return `Task #${issueNumber} accepted.
2134
- PR #${result.prNumber} merged \u2192 ${baseBranch}
2133
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
2135
2134
  Issue closed.`;
2136
2135
  } catch (err) {
2137
2136
  spinner.fail("Failed");
2138
2137
  return `Error: ${err.message}`;
2139
2138
  }
2140
2139
  }
2141
- async function execute10(input4, config) {
2142
- const issueNumber = input4["issue_number"];
2140
+ async function execute10(input3, config) {
2141
+ const issueNumber = input3["issue_number"];
2143
2142
  const [me, issue] = await Promise.all([
2144
2143
  getAuthenticatedUser(config),
2145
2144
  getTask(config, issueNumber)
@@ -2147,13 +2146,13 @@ async function execute10(input4, config) {
2147
2146
  if (issue.author && issue.author !== me) {
2148
2147
  return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2149
2148
  }
2149
+ const targetBranch = makeWorkerBranchName(me);
2150
2150
  const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
2151
2151
  try {
2152
2152
  const result = await acceptTask(config, issueNumber);
2153
2153
  spinner.stop();
2154
- const baseBranch = config.github.baseBranch ?? "main";
2155
2154
  return `Task #${issueNumber} accepted.
2156
- PR #${result.prNumber} merged \u2192 ${baseBranch}
2155
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
2157
2156
  Issue closed.`;
2158
2157
  } catch (err) {
2159
2158
  spinner.stop();
@@ -2189,8 +2188,8 @@ var definition11 = {
2189
2188
  }
2190
2189
  }
2191
2190
  };
2192
- async function run11(input4, config) {
2193
- let issueNumber = input4["issue_number"];
2191
+ async function run11(input3, config) {
2192
+ let issueNumber = input3["issue_number"];
2194
2193
  if (!issueNumber) {
2195
2194
  let tasks;
2196
2195
  try {
@@ -2241,10 +2240,10 @@ async function run11(input4, config) {
2241
2240
  return `Error: ${err.message}`;
2242
2241
  }
2243
2242
  }
2244
- async function execute11(input4, config) {
2245
- const issueNumber = input4["issue_number"];
2246
- const title = input4["title"];
2247
- 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"];
2248
2247
  const spinner = ora10(`Updating #${issueNumber}\u2026`).start();
2249
2248
  try {
2250
2249
  await editTask(config, issueNumber, title, body);
@@ -2307,8 +2306,8 @@ var definition13 = {
2307
2306
  }
2308
2307
  }
2309
2308
  };
2310
- async function execute13(input4, config) {
2311
- const issue = await getTask(config, input4["issue_number"]);
2309
+ async function execute13(input3, config) {
2310
+ const issue = await getTask(config, input3["issue_number"]);
2312
2311
  const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2313
2312
  const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
2314
2313
  const lines = [
@@ -2344,9 +2343,9 @@ var definition14 = {
2344
2343
  }
2345
2344
  }
2346
2345
  };
2347
- async function execute14(input4, config) {
2348
- const issueNumber = input4["issue_number"];
2349
- const limit = input4["limit"] ?? 5;
2346
+ async function execute14(input3, config) {
2347
+ const issueNumber = input3["issue_number"];
2348
+ const limit = input3["limit"] ?? 5;
2350
2349
  const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
2351
2350
  try {
2352
2351
  const comments = await listComments(config, issueNumber, limit);
@@ -2381,7 +2380,7 @@ var definition15 = {
2381
2380
  async function execute15(_input, _config) {
2382
2381
  const spinner = ora12("Reading git diff...").start();
2383
2382
  try {
2384
- const diff = await getDiff(_config.github.baseBranch);
2383
+ const diff = await getDiff();
2385
2384
  spinner.stop();
2386
2385
  return diff;
2387
2386
  } catch (err) {
@@ -2414,8 +2413,8 @@ var definition16 = {
2414
2413
  }
2415
2414
  }
2416
2415
  };
2417
- async function execute16(input4, _config) {
2418
- const command = input4["command"];
2416
+ async function execute16(input3, _config) {
2417
+ const command = input3["command"];
2419
2418
  const cwd = process.cwd();
2420
2419
  const spinner = ora13(`$ ${command}`).start();
2421
2420
  try {
@@ -2607,8 +2606,8 @@ var definition17 = {
2607
2606
  }
2608
2607
  }
2609
2608
  };
2610
- async function execute17(input4, _config) {
2611
- const focus = input4["focus"] ?? "";
2609
+ async function execute17(input3, _config) {
2610
+ const focus = input3["focus"] ?? "";
2612
2611
  const spinner = ora14("Scanning project...").start();
2613
2612
  try {
2614
2613
  const cwd = process.cwd();
@@ -2657,8 +2656,8 @@ var definition18 = {
2657
2656
  }
2658
2657
  }
2659
2658
  };
2660
- async function execute18(input4, _config) {
2661
- const filePath = input4["path"];
2659
+ async function execute18(input3, _config) {
2660
+ const filePath = input3["path"];
2662
2661
  try {
2663
2662
  const fullPath = path3.join(process.cwd(), filePath);
2664
2663
  const content = await readFile3(fullPath, "utf-8");
@@ -2695,9 +2694,9 @@ var definition19 = {
2695
2694
  }
2696
2695
  }
2697
2696
  };
2698
- async function execute19(input4, _config) {
2699
- const question = input4["question"];
2700
- const options = input4["options"];
2697
+ async function execute19(input3, _config) {
2698
+ const question = input3["question"];
2699
+ const options = input3["options"];
2701
2700
  const OTHER = "__other__";
2702
2701
  console.log("");
2703
2702
  console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
@@ -2750,12 +2749,12 @@ var toolModules = [
2750
2749
 
2751
2750
  // src/lib/agent.ts
2752
2751
  var tools = toolModules.map((m) => m.definition);
2753
- async function executeTool(name, input4, config) {
2752
+ async function executeTool(name, input3, config) {
2754
2753
  const mod = toolModules.find((m) => m.definition.function.name === name);
2755
2754
  if (!mod) return `Unknown tool: ${name}`;
2756
2755
  try {
2757
2756
  const fn = mod.run ?? mod.execute;
2758
- return await fn(input4, config);
2757
+ return await fn(input3, config);
2759
2758
  } catch (err) {
2760
2759
  return `Error: ${err.message}`;
2761
2760
  }
@@ -2959,13 +2958,9 @@ async function initNewRepo(config, owner, repo) {
2959
2958
  console.log("");
2960
2959
  console.log(chalk14.bold.cyan(` New repo detected: ${owner}/${repo}`));
2961
2960
  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
2961
  const newConfig = {
2967
2962
  ...config,
2968
- github: { owner, repo, baseBranch: baseBranch.trim() || "main" }
2963
+ github: { owner, repo }
2969
2964
  };
2970
2965
  setConfig({ github: newConfig.github });
2971
2966
  const ora16 = (await import("ora")).default;
package/dist/mcp.js CHANGED
@@ -21,7 +21,6 @@ __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,
27
26
  isCollaborator: () => isCollaborator,
@@ -309,10 +308,6 @@ async function getDefaultBranch(config) {
309
308
  const { data } = await octokit.repos.get({ owner, repo });
310
309
  return data.default_branch;
311
310
  }
312
- function getBaseBranch(config) {
313
- if (config.github.baseBranch) return Promise.resolve(config.github.baseBranch);
314
- return getDefaultBranch(config);
315
- }
316
311
  async function acceptTask(config, issueNumber) {
317
312
  const octokit = createOctokit(config.githubToken);
318
313
  const { owner, repo } = config.github;
@@ -375,6 +370,10 @@ async function getCurrentBranch() {
375
370
  async function pushBranch(name) {
376
371
  await git.push("origin", name, ["--set-upstream"]);
377
372
  }
373
+ function makeBranchName(issueNumber, username) {
374
+ const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
375
+ return `task-${issueNumber}-${slug}`;
376
+ }
378
377
  function makeWorkerBranchName(username) {
379
378
  const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
380
379
  return `worker-${slug}`;
@@ -499,8 +498,7 @@ var configSchema = z.object({
499
498
  githubClientId: z.string().optional(),
500
499
  github: z.object({
501
500
  owner: z.string().min(1),
502
- repo: z.string().min(1),
503
- baseBranch: z.string().optional()
501
+ repo: z.string().min(1)
504
502
  }),
505
503
  taskState: z.object({
506
504
  activeIssueNumber: z.number().optional(),
@@ -798,14 +796,14 @@ async function run(_input, config) {
798
796
  return "No active task found. Claim a task first with /pick.";
799
797
  }
800
798
  let spinner = ora("Loading task and diff\u2026").start();
801
- const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff(config.github.baseBranch);
802
- const [issue, defaultBranch, diff, me] = await Promise.all([
799
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
800
+ const [issue, diff, me] = await Promise.all([
803
801
  getTask(config, issueNumber),
804
- getBaseBranch(config),
805
802
  diffPromise,
806
803
  getAuthenticatedUser(config)
807
804
  ]);
808
805
  spinner.stop();
806
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
809
807
  const branch = await getCurrentBranch();
810
808
  const isSelfSubmit = issue.author !== null && issue.author === me;
811
809
  let review = "";
@@ -870,7 +868,7 @@ ${issue.body}` : "",
870
868
  ## AI Review
871
869
  ${review}` : ""
872
870
  ].join("\n").trim();
873
- prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
871
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
874
872
  spinner.stop();
875
873
  } catch (err) {
876
874
  spinner.stop();
@@ -893,14 +891,14 @@ async function execute(input, config) {
893
891
  const taskState = getConfig().taskState;
894
892
  const issueNumber = taskState?.activeIssueNumber;
895
893
  if (!issueNumber) return "No active task found. Claim a task first.";
896
- const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff(config.github.baseBranch);
897
- const [issue, defaultBranch, diff, branch, me] = await Promise.all([
894
+ const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
895
+ const [issue, diff, branch, me] = await Promise.all([
898
896
  getTask(config, issueNumber),
899
- getBaseBranch(config),
900
897
  diffPromise,
901
898
  getCurrentBranch(),
902
899
  getAuthenticatedUser(config)
903
900
  ]);
901
+ const workerBranch = makeWorkerBranchName(issue.author ?? me);
904
902
  const isSelfSubmit = issue.author !== null && issue.author === me;
905
903
  let review = "";
906
904
  if (!isSelfSubmit) {
@@ -926,7 +924,7 @@ ${issue.body}` : "",
926
924
  ## AI Review
927
925
  ${review}` : ""
928
926
  ].join("\n").trim();
929
- prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
927
+ prUrl = await createPR(config, issue.title, prBody, branch, workerBranch);
930
928
  } catch (err) {
931
929
  return `Committed but PR creation failed: ${err.message}`;
932
930
  }
@@ -1120,23 +1118,37 @@ Finish or submit it before claiming a new one.`;
1120
1118
  let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
1121
1119
  await claimTask(config, issue.number, me);
1122
1120
  spinner.stop();
1123
- const branch = makeWorkerBranchName(me);
1124
- spinner = ora3(`Switching to branch ${branch}\u2026`).start();
1125
- let isNew = false;
1121
+ const workerBranch = makeWorkerBranchName(me);
1122
+ spinner = ora3(`Switching to ${workerBranch}\u2026`).start();
1126
1123
  try {
1127
- isNew = await switchToBranchOrCreate(branch);
1124
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
1128
1125
  spinner.stop();
1126
+ if (isNewWorker) {
1127
+ spinner = ora3("Pushing worker branch\u2026").start();
1128
+ try {
1129
+ await pushBranch(workerBranch);
1130
+ spinner.stop();
1131
+ } catch {
1132
+ spinner.warn("Could not push worker branch");
1133
+ }
1134
+ }
1129
1135
  } catch {
1130
- spinner.warn(`Could not switch to branch ${branch}`);
1136
+ spinner.warn(`Could not switch to ${workerBranch}`);
1131
1137
  }
1132
- if (isNew) {
1133
- spinner = ora3("Pushing branch\u2026").start();
1138
+ const branch = makeBranchName(issue.number, me);
1139
+ spinner = ora3(`Creating task branch ${branch}\u2026`).start();
1140
+ try {
1141
+ await switchToBranchOrCreate(branch);
1142
+ spinner.stop();
1143
+ spinner = ora3("Pushing task branch\u2026").start();
1134
1144
  try {
1135
1145
  await pushBranch(branch);
1136
1146
  spinner.stop();
1137
1147
  } catch {
1138
- spinner.warn("Could not push branch");
1148
+ spinner.warn("Could not push task branch");
1139
1149
  }
1150
+ } catch {
1151
+ spinner.warn(`Could not create branch ${branch}`);
1140
1152
  }
1141
1153
  const baseCommit = await getCurrentCommit();
1142
1154
  setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
@@ -1194,17 +1206,25 @@ async function execute3(input, config) {
1194
1206
  } catch (err) {
1195
1207
  return `Error claiming task: ${err.message}`;
1196
1208
  }
1197
- const branch = makeWorkerBranchName(me);
1198
- let isNew = false;
1209
+ const workerBranch = makeWorkerBranchName(me);
1199
1210
  try {
1200
- isNew = await switchToBranchOrCreate(branch);
1211
+ const isNewWorker = await switchToBranchOrCreate(workerBranch);
1212
+ if (isNewWorker) {
1213
+ try {
1214
+ await pushBranch(workerBranch);
1215
+ } catch {
1216
+ }
1217
+ }
1201
1218
  } catch {
1202
1219
  }
1203
- if (isNew) {
1204
- try {
1205
- await pushBranch(branch);
1206
- } catch {
1207
- }
1220
+ const branch = makeBranchName(issueNumber, me);
1221
+ try {
1222
+ await switchToBranchOrCreate(branch);
1223
+ } catch {
1224
+ }
1225
+ try {
1226
+ await pushBranch(branch);
1227
+ } catch {
1208
1228
  }
1209
1229
  const baseCommit = await getCurrentCommit();
1210
1230
  setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
@@ -1754,7 +1774,7 @@ var definition10 = {
1754
1774
  type: "function",
1755
1775
  function: {
1756
1776
  name: "accept",
1757
- description: "Accept an in-review task: merges the PR into the configured base branch and closes the issue.",
1777
+ description: "Accept an in-review task: merges the PR into your worker branch and closes the issue.",
1758
1778
  parameters: {
1759
1779
  type: "object",
1760
1780
  properties: {
@@ -1807,11 +1827,11 @@ async function run10(input, config) {
1807
1827
  if (issue.author && issue.author !== me2) {
1808
1828
  return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
1809
1829
  }
1810
- const baseBranch = config.github.baseBranch ?? "main";
1830
+ const targetBranch = makeWorkerBranchName(me2);
1811
1831
  let confirmed;
1812
1832
  try {
1813
1833
  confirmed = await select6({
1814
- message: `Merge PR for #${issueNumber} into ${chalk9.cyan(baseBranch)} and close issue?`,
1834
+ message: `Merge PR for #${issueNumber} into ${chalk9.cyan(targetBranch)} and close issue?`,
1815
1835
  choices: [
1816
1836
  { name: "Yes, accept", value: true },
1817
1837
  { name: "Cancel", value: false }
@@ -1824,9 +1844,9 @@ async function run10(input, config) {
1824
1844
  const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
1825
1845
  try {
1826
1846
  const result = await acceptTask(config, issueNumber);
1827
- spinner.succeed(`PR #${result.prNumber} merged into ${baseBranch}`);
1847
+ spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
1828
1848
  return `Task #${issueNumber} accepted.
1829
- PR #${result.prNumber} merged \u2192 ${baseBranch}
1849
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
1830
1850
  Issue closed.`;
1831
1851
  } catch (err) {
1832
1852
  spinner.fail("Failed");
@@ -1842,13 +1862,13 @@ async function execute10(input, config) {
1842
1862
  if (issue.author && issue.author !== me) {
1843
1863
  return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
1844
1864
  }
1865
+ const targetBranch = makeWorkerBranchName(me);
1845
1866
  const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
1846
1867
  try {
1847
1868
  const result = await acceptTask(config, issueNumber);
1848
1869
  spinner.stop();
1849
- const baseBranch = config.github.baseBranch ?? "main";
1850
1870
  return `Task #${issueNumber} accepted.
1851
- PR #${result.prNumber} merged \u2192 ${baseBranch}
1871
+ PR #${result.prNumber} merged \u2192 ${targetBranch}
1852
1872
  Issue closed.`;
1853
1873
  } catch (err) {
1854
1874
  spinner.stop();
@@ -2076,7 +2096,7 @@ var definition15 = {
2076
2096
  async function execute15(_input, _config) {
2077
2097
  const spinner = ora11("Reading git diff...").start();
2078
2098
  try {
2079
- const diff = await getDiff(_config.github.baseBranch);
2099
+ const diff = await getDiff();
2080
2100
  spinner.stop();
2081
2101
  return diff;
2082
2102
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "techunter",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "AI-powered task distribution CLI for development teams",
5
5
  "author": "Techunter Contributors",
6
6
  "license": "MIT",