techunter 0.1.6 → 0.1.7
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.
- package/dist/index.js +151 -53
- package/dist/mcp.js +140 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,7 +18,9 @@ __export(github_exports, {
|
|
|
18
18
|
createPR: () => createPR,
|
|
19
19
|
createTask: () => createTask,
|
|
20
20
|
editTask: () => editTask,
|
|
21
|
+
embedBaseCommit: () => embedBaseCommit,
|
|
21
22
|
ensureLabels: () => ensureLabels,
|
|
23
|
+
extractBaseCommit: () => extractBaseCommit,
|
|
22
24
|
formatGuideAsMarkdown: () => formatGuideAsMarkdown,
|
|
23
25
|
getAuthenticatedUser: () => getAuthenticatedUser,
|
|
24
26
|
getDefaultBranch: () => getDefaultBranch,
|
|
@@ -29,6 +31,7 @@ __export(github_exports, {
|
|
|
29
31
|
listTasks: () => listTasks,
|
|
30
32
|
listTasksForReview: () => listTasksForReview,
|
|
31
33
|
markInReview: () => markInReview,
|
|
34
|
+
mergeWorkerIntoBase: () => mergeWorkerIntoBase,
|
|
32
35
|
postComment: () => postComment,
|
|
33
36
|
postGuideComment: () => postGuideComment,
|
|
34
37
|
rejectTask: () => rejectTask
|
|
@@ -77,19 +80,41 @@ async function getTask(config, number) {
|
|
|
77
80
|
const { data } = await octokit.issues.get({ owner, repo, issue_number: number });
|
|
78
81
|
return parseIssue(data);
|
|
79
82
|
}
|
|
80
|
-
|
|
83
|
+
function embedBaseCommit(body, sha) {
|
|
84
|
+
return `${body}
|
|
85
|
+
|
|
86
|
+
${BASE_COMMIT_MARKER}${sha} -->`;
|
|
87
|
+
}
|
|
88
|
+
function extractBaseCommit(body) {
|
|
89
|
+
if (!body) return null;
|
|
90
|
+
const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
|
|
91
|
+
return match?.[1] ?? null;
|
|
92
|
+
}
|
|
93
|
+
async function createTask(config, title, body, baseCommit) {
|
|
81
94
|
const octokit = createOctokit(config.githubToken);
|
|
82
95
|
const { owner, repo } = config.github;
|
|
83
96
|
await ensureLabels(config);
|
|
97
|
+
const finalBody = baseCommit ? embedBaseCommit(body ?? "", baseCommit) : body;
|
|
84
98
|
const { data } = await octokit.issues.create({
|
|
85
99
|
owner,
|
|
86
100
|
repo,
|
|
87
101
|
title,
|
|
88
|
-
body,
|
|
102
|
+
body: finalBody,
|
|
89
103
|
labels: [LABEL_AVAILABLE]
|
|
90
104
|
});
|
|
91
105
|
return parseIssue(data);
|
|
92
106
|
}
|
|
107
|
+
async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
|
|
108
|
+
const octokit = createOctokit(config.githubToken);
|
|
109
|
+
const { owner, repo } = config.github;
|
|
110
|
+
await octokit.repos.merge({
|
|
111
|
+
owner,
|
|
112
|
+
repo,
|
|
113
|
+
base: baseBranch,
|
|
114
|
+
head: workerBranch,
|
|
115
|
+
commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
93
118
|
async function claimTask(config, number, username) {
|
|
94
119
|
const octokit = createOctokit(config.githubToken);
|
|
95
120
|
const { owner, repo } = config.github;
|
|
@@ -308,12 +333,12 @@ async function getDefaultBranch(config) {
|
|
|
308
333
|
const { data } = await octokit.repos.get({ owner, repo });
|
|
309
334
|
return data.default_branch;
|
|
310
335
|
}
|
|
311
|
-
async function acceptTask(config, issueNumber) {
|
|
336
|
+
async function acceptTask(config, issueNumber, headBranch) {
|
|
312
337
|
const octokit = createOctokit(config.githubToken);
|
|
313
338
|
const { owner, repo } = config.github;
|
|
314
339
|
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
315
|
-
const pr = prs.find((p) => p.head.ref.startsWith(`task-${issueNumber}-`));
|
|
316
|
-
if (!pr) throw new Error(`No open PR found for task #${issueNumber}
|
|
340
|
+
const pr = headBranch ? prs.find((p) => p.head.ref === headBranch) : prs.find((p) => p.head.ref.startsWith(`task-${issueNumber}-`) || p.head.ref.startsWith("worker-"));
|
|
341
|
+
if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
|
|
317
342
|
const { data: merge } = await octokit.pulls.merge({
|
|
318
343
|
owner,
|
|
319
344
|
repo,
|
|
@@ -323,7 +348,7 @@ async function acceptTask(config, issueNumber) {
|
|
|
323
348
|
await closeTask(config, issueNumber);
|
|
324
349
|
return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "" };
|
|
325
350
|
}
|
|
326
|
-
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS;
|
|
351
|
+
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER;
|
|
327
352
|
var init_github = __esm({
|
|
328
353
|
"src/lib/github.ts"() {
|
|
329
354
|
"use strict";
|
|
@@ -338,6 +363,7 @@ var init_github = __esm({
|
|
|
338
363
|
{ name: LABEL_CHANGES_NEEDED, color: "e11d48", description: "Task needs changes" }
|
|
339
364
|
];
|
|
340
365
|
TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
|
|
366
|
+
BASE_COMMIT_MARKER = "<!-- techunter-base:";
|
|
341
367
|
}
|
|
342
368
|
});
|
|
343
369
|
|
|
@@ -362,6 +388,7 @@ var configSchema = z.object({
|
|
|
362
388
|
aiModel: z.string().optional(),
|
|
363
389
|
githubToken: z.string().min(1),
|
|
364
390
|
githubClientId: z.string().optional(),
|
|
391
|
+
baseBranch: z.string().optional(),
|
|
365
392
|
github: z.object({
|
|
366
393
|
owner: z.string().min(1),
|
|
367
394
|
repo: z.string().min(1)
|
|
@@ -406,6 +433,9 @@ function setConfig(partial) {
|
|
|
406
433
|
if (partial.githubClientId !== void 0) {
|
|
407
434
|
current["githubClientId"] = partial.githubClientId;
|
|
408
435
|
}
|
|
436
|
+
if (partial.baseBranch !== void 0) {
|
|
437
|
+
current["baseBranch"] = partial.baseBranch;
|
|
438
|
+
}
|
|
409
439
|
if (partial.taskState !== void 0) {
|
|
410
440
|
current["taskState"] = {
|
|
411
441
|
...current["taskState"] ?? {},
|
|
@@ -568,6 +598,28 @@ async function stageAllAndCommit(message) {
|
|
|
568
598
|
const branch = (await git.branch()).current;
|
|
569
599
|
await git.push("origin", branch, ["--set-upstream"]);
|
|
570
600
|
}
|
|
601
|
+
async function syncWithBase(baseBranch) {
|
|
602
|
+
await git.fetch("origin", baseBranch);
|
|
603
|
+
try {
|
|
604
|
+
await git.merge([`origin/${baseBranch}`, "--ff-only"]);
|
|
605
|
+
} catch {
|
|
606
|
+
await git.merge([`origin/${baseBranch}`, "-m", `chore: sync with ${baseBranch}`]);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function getRemoteHeadSha(baseBranch) {
|
|
610
|
+
await git.fetch("origin", baseBranch);
|
|
611
|
+
return (await git.revparse([`origin/${baseBranch}`])).trim();
|
|
612
|
+
}
|
|
613
|
+
async function resetOrCreateBranch(branchName, sha) {
|
|
614
|
+
const branches = await git.branch();
|
|
615
|
+
const localExists = Object.keys(branches.branches).some((b) => b === branchName);
|
|
616
|
+
if (localExists) {
|
|
617
|
+
await git.checkout(branchName);
|
|
618
|
+
await git.reset(["--hard", sha]);
|
|
619
|
+
} else {
|
|
620
|
+
await git.checkoutBranch(branchName, sha);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
571
623
|
|
|
572
624
|
// src/lib/client.ts
|
|
573
625
|
import OpenAI from "openai";
|
|
@@ -736,10 +788,12 @@ async function configCommand() {
|
|
|
736
788
|
`));
|
|
737
789
|
const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
|
|
738
790
|
const currentModel = config.aiModel ?? DEFAULT_MODEL;
|
|
791
|
+
const currentBaseBranch = config.baseBranch ?? "main";
|
|
739
792
|
const field = await select2({
|
|
740
793
|
message: "Which setting to change?",
|
|
741
794
|
choices: [
|
|
742
795
|
{ name: `GitHub repo ${chalk3.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
|
|
796
|
+
{ name: `Base branch ${chalk3.dim(currentBaseBranch)}`, value: "baseBranch" },
|
|
743
797
|
{ name: `AI base URL ${chalk3.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
|
|
744
798
|
{ name: `AI model ${chalk3.dim(currentModel)}`, value: "aiModel" },
|
|
745
799
|
{ name: `AI API Key ${chalk3.dim("(hidden)")}`, value: "aiApiKey" },
|
|
@@ -748,7 +802,15 @@ async function configCommand() {
|
|
|
748
802
|
]
|
|
749
803
|
});
|
|
750
804
|
if (field === "cancel") return;
|
|
751
|
-
if (field === "
|
|
805
|
+
if (field === "baseBranch") {
|
|
806
|
+
const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
|
|
807
|
+
if (val.trim()) {
|
|
808
|
+
setConfig({ baseBranch: val.trim() });
|
|
809
|
+
console.log(chalk3.green(`
|
|
810
|
+
Base branch set to: ${val.trim()}
|
|
811
|
+
`));
|
|
812
|
+
}
|
|
813
|
+
} else if (field === "repo") {
|
|
752
814
|
const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
|
|
753
815
|
const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
|
|
754
816
|
setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
|
|
@@ -805,6 +867,7 @@ init_github();
|
|
|
805
867
|
import chalk8 from "chalk";
|
|
806
868
|
import ora4 from "ora";
|
|
807
869
|
import { select as select5 } from "@inquirer/prompts";
|
|
870
|
+
init_github();
|
|
808
871
|
|
|
809
872
|
// src/lib/markdown.ts
|
|
810
873
|
import { marked } from "marked";
|
|
@@ -944,7 +1007,8 @@ async function launchClaudeCode(issue, branch) {
|
|
|
944
1007
|
const prompt = buildClaudePrompt(issue, branch);
|
|
945
1008
|
console.log(chalk5.dim("\n Launching Claude Code\u2026\n"));
|
|
946
1009
|
await new Promise((resolve) => {
|
|
947
|
-
const
|
|
1010
|
+
const safePrompt = prompt.replace(/\r?\n/g, " ").replace(/"/g, "'");
|
|
1011
|
+
const child = spawn(`claude "${safePrompt}"`, [], { stdio: "inherit", shell: true });
|
|
948
1012
|
child.on("close", () => resolve());
|
|
949
1013
|
child.on("error", () => {
|
|
950
1014
|
console.log(
|
|
@@ -1425,41 +1489,29 @@ Finish or submit it before claiming a new one.`;
|
|
|
1425
1489
|
await claimTask(config, issue.number, me);
|
|
1426
1490
|
spinner.stop();
|
|
1427
1491
|
const workerBranch = makeWorkerBranchName(me);
|
|
1428
|
-
|
|
1492
|
+
const taskBase = extractBaseCommit(issue.body);
|
|
1493
|
+
spinner = ora4(`Switching to ${workerBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
|
|
1429
1494
|
try {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
try {
|
|
1435
|
-
await pushBranch(workerBranch);
|
|
1436
|
-
spinner.stop();
|
|
1437
|
-
} catch {
|
|
1438
|
-
spinner.warn("Could not push worker branch");
|
|
1439
|
-
}
|
|
1495
|
+
if (taskBase) {
|
|
1496
|
+
await resetOrCreateBranch(workerBranch, taskBase);
|
|
1497
|
+
} else {
|
|
1498
|
+
await switchToBranchOrCreate(workerBranch);
|
|
1440
1499
|
}
|
|
1441
|
-
} catch {
|
|
1442
|
-
spinner.warn(`Could not switch to ${workerBranch}`);
|
|
1443
|
-
}
|
|
1444
|
-
const branch = makeBranchName(issue.number, me);
|
|
1445
|
-
spinner = ora4(`Creating task branch ${branch}\u2026`).start();
|
|
1446
|
-
try {
|
|
1447
|
-
await switchToBranchOrCreate(branch);
|
|
1448
1500
|
spinner.stop();
|
|
1449
|
-
spinner = ora4("Pushing
|
|
1501
|
+
spinner = ora4("Pushing worker branch\u2026").start();
|
|
1450
1502
|
try {
|
|
1451
|
-
await pushBranch(
|
|
1503
|
+
await pushBranch(workerBranch);
|
|
1452
1504
|
spinner.stop();
|
|
1453
1505
|
} catch {
|
|
1454
|
-
spinner.warn("Could not push
|
|
1506
|
+
spinner.warn("Could not push worker branch");
|
|
1455
1507
|
}
|
|
1456
1508
|
} catch {
|
|
1457
|
-
spinner.warn(`Could not
|
|
1509
|
+
spinner.warn(`Could not switch to ${workerBranch}`);
|
|
1458
1510
|
}
|
|
1459
1511
|
const baseCommit = await getCurrentCommit();
|
|
1460
1512
|
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
|
|
1461
1513
|
console.log(chalk8.green(`
|
|
1462
|
-
Claimed! Branch: ${
|
|
1514
|
+
Claimed! Branch: ${workerBranch} (base: ${baseCommit.slice(0, 7)})
|
|
1463
1515
|
`));
|
|
1464
1516
|
let openClaude;
|
|
1465
1517
|
try {
|
|
@@ -1473,8 +1525,8 @@ Finish or submit it before claiming a new one.`;
|
|
|
1473
1525
|
} catch {
|
|
1474
1526
|
openClaude = false;
|
|
1475
1527
|
}
|
|
1476
|
-
if (openClaude) await launchClaudeCode(issue,
|
|
1477
|
-
return `Task #${issue.number} claimed. Branch: ${
|
|
1528
|
+
if (openClaude) await launchClaudeCode(issue, workerBranch);
|
|
1529
|
+
return `Task #${issue.number} claimed. Branch: ${workerBranch}`;
|
|
1478
1530
|
} catch (err) {
|
|
1479
1531
|
return `Error claiming task: ${err.message}`;
|
|
1480
1532
|
}
|
|
@@ -1513,28 +1565,22 @@ async function execute3(input3, config) {
|
|
|
1513
1565
|
return `Error claiming task: ${err.message}`;
|
|
1514
1566
|
}
|
|
1515
1567
|
const workerBranch = makeWorkerBranchName(me);
|
|
1568
|
+
const taskBase = extractBaseCommit(issue.body);
|
|
1516
1569
|
try {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
} catch {
|
|
1522
|
-
}
|
|
1570
|
+
if (taskBase) {
|
|
1571
|
+
await resetOrCreateBranch(workerBranch, taskBase);
|
|
1572
|
+
} else {
|
|
1573
|
+
await switchToBranchOrCreate(workerBranch);
|
|
1523
1574
|
}
|
|
1524
1575
|
} catch {
|
|
1525
1576
|
}
|
|
1526
|
-
const branch = makeBranchName(issueNumber, me);
|
|
1527
1577
|
try {
|
|
1528
|
-
await
|
|
1529
|
-
} catch {
|
|
1530
|
-
}
|
|
1531
|
-
try {
|
|
1532
|
-
await pushBranch(branch);
|
|
1578
|
+
await pushBranch(workerBranch);
|
|
1533
1579
|
} catch {
|
|
1534
1580
|
}
|
|
1535
1581
|
const baseCommit = await getCurrentCommit();
|
|
1536
1582
|
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
|
|
1537
|
-
return `Task #${issueNumber} claimed. Branch: ${
|
|
1583
|
+
return `Task #${issueNumber} claimed. Branch: ${workerBranch} (base commit: ${baseCommit.slice(0, 7)})`;
|
|
1538
1584
|
}
|
|
1539
1585
|
return `Unknown action: ${action}`;
|
|
1540
1586
|
}
|
|
@@ -1704,12 +1750,26 @@ async function run4(input3, config) {
|
|
|
1704
1750
|
console.log(chalk9.yellow(` Revision error: ${err.message}`));
|
|
1705
1751
|
}
|
|
1706
1752
|
}
|
|
1753
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1754
|
+
let baseCommit;
|
|
1755
|
+
const syncSpinner = ora5(`Syncing with ${baseBranch}\u2026`).start();
|
|
1756
|
+
try {
|
|
1757
|
+
await syncWithBase(baseBranch);
|
|
1758
|
+
baseCommit = await getCurrentCommit();
|
|
1759
|
+
syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
|
|
1760
|
+
} catch {
|
|
1761
|
+
syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
|
|
1762
|
+
try {
|
|
1763
|
+
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1764
|
+
} catch {
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1707
1767
|
const createSpinner = ora5(`Creating "${title}"\u2026`).start();
|
|
1708
1768
|
let htmlUrl;
|
|
1709
1769
|
let issueNumber;
|
|
1710
1770
|
let issueTitle;
|
|
1711
1771
|
try {
|
|
1712
|
-
const issue = await createTask(config, title, guide);
|
|
1772
|
+
const issue = await createTask(config, title, guide, baseCommit);
|
|
1713
1773
|
createSpinner.stop();
|
|
1714
1774
|
htmlUrl = issue.htmlUrl;
|
|
1715
1775
|
issueNumber = issue.number;
|
|
@@ -1746,8 +1806,19 @@ async function execute4(input3, config) {
|
|
|
1746
1806
|
if (feedback) {
|
|
1747
1807
|
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1748
1808
|
}
|
|
1809
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1810
|
+
let baseCommit;
|
|
1811
|
+
try {
|
|
1812
|
+
await syncWithBase(baseBranch);
|
|
1813
|
+
baseCommit = await getCurrentCommit();
|
|
1814
|
+
} catch {
|
|
1815
|
+
try {
|
|
1816
|
+
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1749
1820
|
try {
|
|
1750
|
-
const issue = await createTask(config, title, guide);
|
|
1821
|
+
const issue = await createTask(config, title, guide, baseCommit);
|
|
1751
1822
|
return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
|
|
1752
1823
|
|
|
1753
1824
|
Guide:
|
|
@@ -2148,16 +2219,40 @@ async function run10(input3, config) {
|
|
|
2148
2219
|
}
|
|
2149
2220
|
if (!confirmed) return "Cancelled.";
|
|
2150
2221
|
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2222
|
+
let result;
|
|
2151
2223
|
try {
|
|
2152
|
-
const
|
|
2224
|
+
const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
|
|
2225
|
+
result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
2153
2226
|
spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
|
|
2154
|
-
return `Task #${issueNumber} accepted.
|
|
2155
|
-
PR #${result.prNumber} merged \u2192 ${targetBranch}
|
|
2156
|
-
Issue closed.`;
|
|
2157
2227
|
} catch (err) {
|
|
2158
2228
|
spinner.fail("Failed");
|
|
2159
2229
|
return `Error: ${err.message}`;
|
|
2160
2230
|
}
|
|
2231
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
2232
|
+
let pushToMain;
|
|
2233
|
+
try {
|
|
2234
|
+
pushToMain = await select8({
|
|
2235
|
+
message: `Push ${chalk11.cyan(targetBranch)} \u2192 ${chalk11.cyan(baseBranch)}?`,
|
|
2236
|
+
choices: [
|
|
2237
|
+
{ name: `Yes, push to ${baseBranch}`, value: true },
|
|
2238
|
+
{ name: "No, keep in worker branch", value: false }
|
|
2239
|
+
]
|
|
2240
|
+
});
|
|
2241
|
+
} catch {
|
|
2242
|
+
pushToMain = false;
|
|
2243
|
+
}
|
|
2244
|
+
if (pushToMain) {
|
|
2245
|
+
const mergeSpinner = ora9(`Merging ${targetBranch} \u2192 ${baseBranch}\u2026`).start();
|
|
2246
|
+
try {
|
|
2247
|
+
await mergeWorkerIntoBase(config, targetBranch, baseBranch);
|
|
2248
|
+
mergeSpinner.succeed(`Merged ${targetBranch} \u2192 ${baseBranch}`);
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return `Task #${issueNumber} accepted.
|
|
2254
|
+
PR #${result.prNumber} merged \u2192 ${targetBranch}${pushToMain ? ` \u2192 ${baseBranch}` : ""}
|
|
2255
|
+
Issue closed.`;
|
|
2161
2256
|
}
|
|
2162
2257
|
async function execute10(input3, config) {
|
|
2163
2258
|
const issueNumber = input3["issue_number"];
|
|
@@ -2171,7 +2266,8 @@ async function execute10(input3, config) {
|
|
|
2171
2266
|
const targetBranch = makeWorkerBranchName(me);
|
|
2172
2267
|
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2173
2268
|
try {
|
|
2174
|
-
const
|
|
2269
|
+
const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
|
|
2270
|
+
const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
2175
2271
|
spinner.stop();
|
|
2176
2272
|
return `Task #${issueNumber} accepted.
|
|
2177
2273
|
PR #${result.prNumber} merged \u2192 ${targetBranch}
|
|
@@ -2933,6 +3029,8 @@ function completer(line) {
|
|
|
2933
3029
|
var _rl = null;
|
|
2934
3030
|
function promptUser() {
|
|
2935
3031
|
return new Promise((resolve) => {
|
|
3032
|
+
if (process.stdin.isPaused()) process.stdin.resume();
|
|
3033
|
+
_rl.resume();
|
|
2936
3034
|
_rl.question(chalk14.cyan("You") + chalk14.dim(" \u203A "), resolve);
|
|
2937
3035
|
});
|
|
2938
3036
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -18,7 +18,9 @@ __export(github_exports, {
|
|
|
18
18
|
createPR: () => createPR,
|
|
19
19
|
createTask: () => createTask,
|
|
20
20
|
editTask: () => editTask,
|
|
21
|
+
embedBaseCommit: () => embedBaseCommit,
|
|
21
22
|
ensureLabels: () => ensureLabels,
|
|
23
|
+
extractBaseCommit: () => extractBaseCommit,
|
|
22
24
|
formatGuideAsMarkdown: () => formatGuideAsMarkdown,
|
|
23
25
|
getAuthenticatedUser: () => getAuthenticatedUser,
|
|
24
26
|
getDefaultBranch: () => getDefaultBranch,
|
|
@@ -29,6 +31,7 @@ __export(github_exports, {
|
|
|
29
31
|
listTasks: () => listTasks,
|
|
30
32
|
listTasksForReview: () => listTasksForReview,
|
|
31
33
|
markInReview: () => markInReview,
|
|
34
|
+
mergeWorkerIntoBase: () => mergeWorkerIntoBase,
|
|
32
35
|
postComment: () => postComment,
|
|
33
36
|
postGuideComment: () => postGuideComment,
|
|
34
37
|
rejectTask: () => rejectTask
|
|
@@ -77,19 +80,41 @@ async function getTask(config, number) {
|
|
|
77
80
|
const { data } = await octokit.issues.get({ owner, repo, issue_number: number });
|
|
78
81
|
return parseIssue(data);
|
|
79
82
|
}
|
|
80
|
-
|
|
83
|
+
function embedBaseCommit(body, sha) {
|
|
84
|
+
return `${body}
|
|
85
|
+
|
|
86
|
+
${BASE_COMMIT_MARKER}${sha} -->`;
|
|
87
|
+
}
|
|
88
|
+
function extractBaseCommit(body) {
|
|
89
|
+
if (!body) return null;
|
|
90
|
+
const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
|
|
91
|
+
return match?.[1] ?? null;
|
|
92
|
+
}
|
|
93
|
+
async function createTask(config, title, body, baseCommit) {
|
|
81
94
|
const octokit = createOctokit(config.githubToken);
|
|
82
95
|
const { owner, repo } = config.github;
|
|
83
96
|
await ensureLabels(config);
|
|
97
|
+
const finalBody = baseCommit ? embedBaseCommit(body ?? "", baseCommit) : body;
|
|
84
98
|
const { data } = await octokit.issues.create({
|
|
85
99
|
owner,
|
|
86
100
|
repo,
|
|
87
101
|
title,
|
|
88
|
-
body,
|
|
102
|
+
body: finalBody,
|
|
89
103
|
labels: [LABEL_AVAILABLE]
|
|
90
104
|
});
|
|
91
105
|
return parseIssue(data);
|
|
92
106
|
}
|
|
107
|
+
async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
|
|
108
|
+
const octokit = createOctokit(config.githubToken);
|
|
109
|
+
const { owner, repo } = config.github;
|
|
110
|
+
await octokit.repos.merge({
|
|
111
|
+
owner,
|
|
112
|
+
repo,
|
|
113
|
+
base: baseBranch,
|
|
114
|
+
head: workerBranch,
|
|
115
|
+
commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
93
118
|
async function claimTask(config, number, username) {
|
|
94
119
|
const octokit = createOctokit(config.githubToken);
|
|
95
120
|
const { owner, repo } = config.github;
|
|
@@ -308,12 +333,12 @@ async function getDefaultBranch(config) {
|
|
|
308
333
|
const { data } = await octokit.repos.get({ owner, repo });
|
|
309
334
|
return data.default_branch;
|
|
310
335
|
}
|
|
311
|
-
async function acceptTask(config, issueNumber) {
|
|
336
|
+
async function acceptTask(config, issueNumber, headBranch) {
|
|
312
337
|
const octokit = createOctokit(config.githubToken);
|
|
313
338
|
const { owner, repo } = config.github;
|
|
314
339
|
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
315
|
-
const pr = prs.find((p) => p.head.ref.startsWith(`task-${issueNumber}-`));
|
|
316
|
-
if (!pr) throw new Error(`No open PR found for task #${issueNumber}
|
|
340
|
+
const pr = headBranch ? prs.find((p) => p.head.ref === headBranch) : prs.find((p) => p.head.ref.startsWith(`task-${issueNumber}-`) || p.head.ref.startsWith("worker-"));
|
|
341
|
+
if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
|
|
317
342
|
const { data: merge } = await octokit.pulls.merge({
|
|
318
343
|
owner,
|
|
319
344
|
repo,
|
|
@@ -323,7 +348,7 @@ async function acceptTask(config, issueNumber) {
|
|
|
323
348
|
await closeTask(config, issueNumber);
|
|
324
349
|
return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "" };
|
|
325
350
|
}
|
|
326
|
-
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS;
|
|
351
|
+
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER;
|
|
327
352
|
var init_github = __esm({
|
|
328
353
|
"src/lib/github.ts"() {
|
|
329
354
|
"use strict";
|
|
@@ -338,6 +363,7 @@ var init_github = __esm({
|
|
|
338
363
|
{ name: LABEL_CHANGES_NEEDED, color: "e11d48", description: "Task needs changes" }
|
|
339
364
|
];
|
|
340
365
|
TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
|
|
366
|
+
BASE_COMMIT_MARKER = "<!-- techunter-base:";
|
|
341
367
|
}
|
|
342
368
|
});
|
|
343
369
|
|
|
@@ -370,10 +396,6 @@ async function getCurrentBranch() {
|
|
|
370
396
|
async function pushBranch(name) {
|
|
371
397
|
await git.push("origin", name, ["--set-upstream"]);
|
|
372
398
|
}
|
|
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
|
-
}
|
|
377
399
|
function makeWorkerBranchName(username) {
|
|
378
400
|
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
379
401
|
return `worker-${slug}`;
|
|
@@ -486,6 +508,31 @@ async function stageAllAndCommit(message) {
|
|
|
486
508
|
const branch = (await git.branch()).current;
|
|
487
509
|
await git.push("origin", branch, ["--set-upstream"]);
|
|
488
510
|
}
|
|
511
|
+
async function syncWithBase(baseBranch) {
|
|
512
|
+
await git.fetch("origin", baseBranch);
|
|
513
|
+
try {
|
|
514
|
+
await git.merge([`origin/${baseBranch}`, "--ff-only"]);
|
|
515
|
+
} catch {
|
|
516
|
+
await git.merge([`origin/${baseBranch}`, "-m", `chore: sync with ${baseBranch}`]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async function getRemoteHeadSha(baseBranch) {
|
|
520
|
+
await git.fetch("origin", baseBranch);
|
|
521
|
+
return (await git.revparse([`origin/${baseBranch}`])).trim();
|
|
522
|
+
}
|
|
523
|
+
async function resetOrCreateBranch(branchName, sha) {
|
|
524
|
+
const branches = await git.branch();
|
|
525
|
+
const localExists = Object.keys(branches.branches).some((b) => b === branchName);
|
|
526
|
+
if (localExists) {
|
|
527
|
+
await git.checkout(branchName);
|
|
528
|
+
await git.reset(["--hard", sha]);
|
|
529
|
+
} else {
|
|
530
|
+
await git.checkoutBranch(branchName, sha);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/tools/pick/index.ts
|
|
535
|
+
init_github();
|
|
489
536
|
|
|
490
537
|
// src/lib/config.ts
|
|
491
538
|
import Conf from "conf";
|
|
@@ -496,6 +543,7 @@ var configSchema = z.object({
|
|
|
496
543
|
aiModel: z.string().optional(),
|
|
497
544
|
githubToken: z.string().min(1),
|
|
498
545
|
githubClientId: z.string().optional(),
|
|
546
|
+
baseBranch: z.string().optional(),
|
|
499
547
|
github: z.object({
|
|
500
548
|
owner: z.string().min(1),
|
|
501
549
|
repo: z.string().min(1)
|
|
@@ -540,6 +588,9 @@ function setConfig(partial) {
|
|
|
540
588
|
if (partial.githubClientId !== void 0) {
|
|
541
589
|
current["githubClientId"] = partial.githubClientId;
|
|
542
590
|
}
|
|
591
|
+
if (partial.baseBranch !== void 0) {
|
|
592
|
+
current["baseBranch"] = partial.baseBranch;
|
|
593
|
+
}
|
|
543
594
|
if (partial.taskState !== void 0) {
|
|
544
595
|
current["taskState"] = {
|
|
545
596
|
...current["taskState"] ?? {},
|
|
@@ -646,7 +697,8 @@ async function launchClaudeCode(issue, branch) {
|
|
|
646
697
|
const prompt = buildClaudePrompt(issue, branch);
|
|
647
698
|
console.log(chalk3.dim("\n Launching Claude Code\u2026\n"));
|
|
648
699
|
await new Promise((resolve) => {
|
|
649
|
-
const
|
|
700
|
+
const safePrompt = prompt.replace(/\r?\n/g, " ").replace(/"/g, "'");
|
|
701
|
+
const child = spawn(`claude "${safePrompt}"`, [], { stdio: "inherit", shell: true });
|
|
650
702
|
child.on("close", () => resolve());
|
|
651
703
|
child.on("error", () => {
|
|
652
704
|
console.log(
|
|
@@ -1141,41 +1193,29 @@ Finish or submit it before claiming a new one.`;
|
|
|
1141
1193
|
await claimTask(config, issue.number, me);
|
|
1142
1194
|
spinner.stop();
|
|
1143
1195
|
const workerBranch = makeWorkerBranchName(me);
|
|
1144
|
-
|
|
1196
|
+
const taskBase = extractBaseCommit(issue.body);
|
|
1197
|
+
spinner = ora3(`Switching to ${workerBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
|
|
1145
1198
|
try {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
try {
|
|
1151
|
-
await pushBranch(workerBranch);
|
|
1152
|
-
spinner.stop();
|
|
1153
|
-
} catch {
|
|
1154
|
-
spinner.warn("Could not push worker branch");
|
|
1155
|
-
}
|
|
1199
|
+
if (taskBase) {
|
|
1200
|
+
await resetOrCreateBranch(workerBranch, taskBase);
|
|
1201
|
+
} else {
|
|
1202
|
+
await switchToBranchOrCreate(workerBranch);
|
|
1156
1203
|
}
|
|
1157
|
-
} catch {
|
|
1158
|
-
spinner.warn(`Could not switch to ${workerBranch}`);
|
|
1159
|
-
}
|
|
1160
|
-
const branch = makeBranchName(issue.number, me);
|
|
1161
|
-
spinner = ora3(`Creating task branch ${branch}\u2026`).start();
|
|
1162
|
-
try {
|
|
1163
|
-
await switchToBranchOrCreate(branch);
|
|
1164
1204
|
spinner.stop();
|
|
1165
|
-
spinner = ora3("Pushing
|
|
1205
|
+
spinner = ora3("Pushing worker branch\u2026").start();
|
|
1166
1206
|
try {
|
|
1167
|
-
await pushBranch(
|
|
1207
|
+
await pushBranch(workerBranch);
|
|
1168
1208
|
spinner.stop();
|
|
1169
1209
|
} catch {
|
|
1170
|
-
spinner.warn("Could not push
|
|
1210
|
+
spinner.warn("Could not push worker branch");
|
|
1171
1211
|
}
|
|
1172
1212
|
} catch {
|
|
1173
|
-
spinner.warn(`Could not
|
|
1213
|
+
spinner.warn(`Could not switch to ${workerBranch}`);
|
|
1174
1214
|
}
|
|
1175
1215
|
const baseCommit = await getCurrentCommit();
|
|
1176
1216
|
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
|
|
1177
1217
|
console.log(chalk6.green(`
|
|
1178
|
-
Claimed! Branch: ${
|
|
1218
|
+
Claimed! Branch: ${workerBranch} (base: ${baseCommit.slice(0, 7)})
|
|
1179
1219
|
`));
|
|
1180
1220
|
let openClaude;
|
|
1181
1221
|
try {
|
|
@@ -1189,8 +1229,8 @@ Finish or submit it before claiming a new one.`;
|
|
|
1189
1229
|
} catch {
|
|
1190
1230
|
openClaude = false;
|
|
1191
1231
|
}
|
|
1192
|
-
if (openClaude) await launchClaudeCode(issue,
|
|
1193
|
-
return `Task #${issue.number} claimed. Branch: ${
|
|
1232
|
+
if (openClaude) await launchClaudeCode(issue, workerBranch);
|
|
1233
|
+
return `Task #${issue.number} claimed. Branch: ${workerBranch}`;
|
|
1194
1234
|
} catch (err) {
|
|
1195
1235
|
return `Error claiming task: ${err.message}`;
|
|
1196
1236
|
}
|
|
@@ -1229,28 +1269,22 @@ async function execute3(input, config) {
|
|
|
1229
1269
|
return `Error claiming task: ${err.message}`;
|
|
1230
1270
|
}
|
|
1231
1271
|
const workerBranch = makeWorkerBranchName(me);
|
|
1272
|
+
const taskBase = extractBaseCommit(issue.body);
|
|
1232
1273
|
try {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
} catch {
|
|
1238
|
-
}
|
|
1274
|
+
if (taskBase) {
|
|
1275
|
+
await resetOrCreateBranch(workerBranch, taskBase);
|
|
1276
|
+
} else {
|
|
1277
|
+
await switchToBranchOrCreate(workerBranch);
|
|
1239
1278
|
}
|
|
1240
1279
|
} catch {
|
|
1241
1280
|
}
|
|
1242
|
-
const branch = makeBranchName(issueNumber, me);
|
|
1243
|
-
try {
|
|
1244
|
-
await switchToBranchOrCreate(branch);
|
|
1245
|
-
} catch {
|
|
1246
|
-
}
|
|
1247
1281
|
try {
|
|
1248
|
-
await pushBranch(
|
|
1282
|
+
await pushBranch(workerBranch);
|
|
1249
1283
|
} catch {
|
|
1250
1284
|
}
|
|
1251
1285
|
const baseCommit = await getCurrentCommit();
|
|
1252
1286
|
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
|
|
1253
|
-
return `Task #${issueNumber} claimed. Branch: ${
|
|
1287
|
+
return `Task #${issueNumber} claimed. Branch: ${workerBranch} (base commit: ${baseCommit.slice(0, 7)})`;
|
|
1254
1288
|
}
|
|
1255
1289
|
return `Unknown action: ${action}`;
|
|
1256
1290
|
}
|
|
@@ -1420,12 +1454,26 @@ async function run4(input, config) {
|
|
|
1420
1454
|
console.log(chalk7.yellow(` Revision error: ${err.message}`));
|
|
1421
1455
|
}
|
|
1422
1456
|
}
|
|
1457
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1458
|
+
let baseCommit;
|
|
1459
|
+
const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
|
|
1460
|
+
try {
|
|
1461
|
+
await syncWithBase(baseBranch);
|
|
1462
|
+
baseCommit = await getCurrentCommit();
|
|
1463
|
+
syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
|
|
1464
|
+
} catch {
|
|
1465
|
+
syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
|
|
1466
|
+
try {
|
|
1467
|
+
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1423
1471
|
const createSpinner = ora4(`Creating "${title}"\u2026`).start();
|
|
1424
1472
|
let htmlUrl;
|
|
1425
1473
|
let issueNumber;
|
|
1426
1474
|
let issueTitle;
|
|
1427
1475
|
try {
|
|
1428
|
-
const issue = await createTask(config, title, guide);
|
|
1476
|
+
const issue = await createTask(config, title, guide, baseCommit);
|
|
1429
1477
|
createSpinner.stop();
|
|
1430
1478
|
htmlUrl = issue.htmlUrl;
|
|
1431
1479
|
issueNumber = issue.number;
|
|
@@ -1462,8 +1510,19 @@ async function execute4(input, config) {
|
|
|
1462
1510
|
if (feedback) {
|
|
1463
1511
|
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1464
1512
|
}
|
|
1513
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1514
|
+
let baseCommit;
|
|
1515
|
+
try {
|
|
1516
|
+
await syncWithBase(baseBranch);
|
|
1517
|
+
baseCommit = await getCurrentCommit();
|
|
1518
|
+
} catch {
|
|
1519
|
+
try {
|
|
1520
|
+
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1465
1524
|
try {
|
|
1466
|
-
const issue = await createTask(config, title, guide);
|
|
1525
|
+
const issue = await createTask(config, title, guide, baseCommit);
|
|
1467
1526
|
return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
|
|
1468
1527
|
|
|
1469
1528
|
Guide:
|
|
@@ -1864,16 +1923,40 @@ async function run10(input, config) {
|
|
|
1864
1923
|
}
|
|
1865
1924
|
if (!confirmed) return "Cancelled.";
|
|
1866
1925
|
const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
|
|
1926
|
+
let result;
|
|
1867
1927
|
try {
|
|
1868
|
-
const
|
|
1928
|
+
const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
|
|
1929
|
+
result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
1869
1930
|
spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
|
|
1870
|
-
return `Task #${issueNumber} accepted.
|
|
1871
|
-
PR #${result.prNumber} merged \u2192 ${targetBranch}
|
|
1872
|
-
Issue closed.`;
|
|
1873
1931
|
} catch (err) {
|
|
1874
1932
|
spinner.fail("Failed");
|
|
1875
1933
|
return `Error: ${err.message}`;
|
|
1876
1934
|
}
|
|
1935
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1936
|
+
let pushToMain;
|
|
1937
|
+
try {
|
|
1938
|
+
pushToMain = await select6({
|
|
1939
|
+
message: `Push ${chalk9.cyan(targetBranch)} \u2192 ${chalk9.cyan(baseBranch)}?`,
|
|
1940
|
+
choices: [
|
|
1941
|
+
{ name: `Yes, push to ${baseBranch}`, value: true },
|
|
1942
|
+
{ name: "No, keep in worker branch", value: false }
|
|
1943
|
+
]
|
|
1944
|
+
});
|
|
1945
|
+
} catch {
|
|
1946
|
+
pushToMain = false;
|
|
1947
|
+
}
|
|
1948
|
+
if (pushToMain) {
|
|
1949
|
+
const mergeSpinner = ora8(`Merging ${targetBranch} \u2192 ${baseBranch}\u2026`).start();
|
|
1950
|
+
try {
|
|
1951
|
+
await mergeWorkerIntoBase(config, targetBranch, baseBranch);
|
|
1952
|
+
mergeSpinner.succeed(`Merged ${targetBranch} \u2192 ${baseBranch}`);
|
|
1953
|
+
} catch (err) {
|
|
1954
|
+
mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return `Task #${issueNumber} accepted.
|
|
1958
|
+
PR #${result.prNumber} merged \u2192 ${targetBranch}${pushToMain ? ` \u2192 ${baseBranch}` : ""}
|
|
1959
|
+
Issue closed.`;
|
|
1877
1960
|
}
|
|
1878
1961
|
async function execute10(input, config) {
|
|
1879
1962
|
const issueNumber = input["issue_number"];
|
|
@@ -1887,7 +1970,8 @@ async function execute10(input, config) {
|
|
|
1887
1970
|
const targetBranch = makeWorkerBranchName(me);
|
|
1888
1971
|
const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
|
|
1889
1972
|
try {
|
|
1890
|
-
const
|
|
1973
|
+
const assigneeWorkerBranch = issue.assignee ? makeWorkerBranchName(issue.assignee) : void 0;
|
|
1974
|
+
const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
1891
1975
|
spinner.stop();
|
|
1892
1976
|
return `Task #${issueNumber} accepted.
|
|
1893
1977
|
PR #${result.prNumber} merged \u2192 ${targetBranch}
|