techunter 0.1.1 → 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.
- package/dist/index.js +223 -69
- package/dist/mcp.js +273 -98
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(github_exports, {
|
|
|
24
24
|
getBaseBranch: () => getBaseBranch,
|
|
25
25
|
getDefaultBranch: () => getDefaultBranch,
|
|
26
26
|
getTask: () => getTask,
|
|
27
|
+
isCollaborator: () => isCollaborator,
|
|
27
28
|
listComments: () => listComments,
|
|
28
29
|
listMyTasks: () => listMyTasks,
|
|
29
30
|
listTasks: () => listTasks,
|
|
@@ -50,6 +51,7 @@ function parseIssue(issue) {
|
|
|
50
51
|
title: issue.title,
|
|
51
52
|
body: issue.body ?? null,
|
|
52
53
|
state: issue.state,
|
|
54
|
+
author: issue.user?.login ?? null,
|
|
53
55
|
assignee: issue.assignee?.login ?? null,
|
|
54
56
|
labels: (issue.labels ?? []).map(
|
|
55
57
|
(l) => typeof l === "string" ? l : l.name ?? ""
|
|
@@ -226,6 +228,16 @@ async function getAuthenticatedUser(config) {
|
|
|
226
228
|
const { data } = await octokit.users.getAuthenticated();
|
|
227
229
|
return data.login;
|
|
228
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
|
+
}
|
|
229
241
|
async function listMyTasks(config, username) {
|
|
230
242
|
const octokit = createOctokit(config.githubToken);
|
|
231
243
|
const { owner, repo } = config.github;
|
|
@@ -360,7 +372,11 @@ var configSchema = z.object({
|
|
|
360
372
|
owner: z.string().min(1),
|
|
361
373
|
repo: z.string().min(1),
|
|
362
374
|
baseBranch: z.string().optional()
|
|
363
|
-
})
|
|
375
|
+
}),
|
|
376
|
+
taskState: z.object({
|
|
377
|
+
activeIssueNumber: z.number().optional(),
|
|
378
|
+
baseCommit: z.string().optional()
|
|
379
|
+
}).optional()
|
|
364
380
|
});
|
|
365
381
|
var store = new Conf({
|
|
366
382
|
projectName: "techunter",
|
|
@@ -397,6 +413,12 @@ function setConfig(partial) {
|
|
|
397
413
|
if (partial.githubClientId !== void 0) {
|
|
398
414
|
current["githubClientId"] = partial.githubClientId;
|
|
399
415
|
}
|
|
416
|
+
if (partial.taskState !== void 0) {
|
|
417
|
+
current["taskState"] = {
|
|
418
|
+
...current["taskState"] ?? {},
|
|
419
|
+
...partial.taskState
|
|
420
|
+
};
|
|
421
|
+
}
|
|
400
422
|
store.store = current;
|
|
401
423
|
}
|
|
402
424
|
function getConfigPath() {
|
|
@@ -414,9 +436,6 @@ async function getCurrentBranch() {
|
|
|
414
436
|
const summary = await git.branch();
|
|
415
437
|
return summary.current;
|
|
416
438
|
}
|
|
417
|
-
async function createAndSwitchBranch(name) {
|
|
418
|
-
await git.checkoutLocalBranch(name);
|
|
419
|
-
}
|
|
420
439
|
async function pushBranch(name) {
|
|
421
440
|
await git.push("origin", name, ["--set-upstream"]);
|
|
422
441
|
}
|
|
@@ -444,6 +463,61 @@ function makeBranchName(issueNumber, username) {
|
|
|
444
463
|
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
445
464
|
return `task-${issueNumber}-${slug}`;
|
|
446
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
|
+
}
|
|
447
521
|
async function findMergeBase(configuredBase) {
|
|
448
522
|
const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
|
|
449
523
|
const unique = [...new Set(candidates)];
|
|
@@ -989,7 +1063,7 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
|
989
1063
|
}
|
|
990
1064
|
|
|
991
1065
|
// src/tools/submit/prompts.ts
|
|
992
|
-
var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. Use run_command to run tests/lint if needed, and read_file to inspect specific files. Then output your review: for each acceptance criterion mark \u2705 met or \u274C not met with a one-line reason. End with an overall verdict line: Ready to submit / Not ready. Reply in the same language as the task.";
|
|
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.";
|
|
993
1067
|
|
|
994
1068
|
// src/tools/submit/reviewer.ts
|
|
995
1069
|
async function reviewChanges(config, issueNumber, issue, diff) {
|
|
@@ -1023,32 +1097,41 @@ var definition = {
|
|
|
1023
1097
|
}
|
|
1024
1098
|
};
|
|
1025
1099
|
async function run(_input, config) {
|
|
1026
|
-
const
|
|
1027
|
-
const
|
|
1028
|
-
if (!
|
|
1029
|
-
return
|
|
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.";
|
|
1030
1104
|
}
|
|
1031
|
-
const issueNumber = parseInt(match[1], 10);
|
|
1032
1105
|
let spinner = ora2("Loading task and diff\u2026").start();
|
|
1033
|
-
const
|
|
1106
|
+
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff(config.github.baseBranch);
|
|
1107
|
+
const [issue, defaultBranch, diff, me] = await Promise.all([
|
|
1034
1108
|
getTask(config, issueNumber),
|
|
1035
1109
|
getBaseBranch(config),
|
|
1036
|
-
|
|
1110
|
+
diffPromise,
|
|
1111
|
+
getAuthenticatedUser(config)
|
|
1037
1112
|
]);
|
|
1038
1113
|
spinner.stop();
|
|
1039
|
-
const
|
|
1114
|
+
const branch = await getCurrentBranch();
|
|
1115
|
+
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
1040
1116
|
let review = "";
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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();
|
|
1045
1125
|
}
|
|
1046
|
-
reviewSpinner.stop();
|
|
1047
1126
|
const divider = chalk7.dim("\u2500".repeat(70));
|
|
1048
1127
|
console.log("\n" + divider);
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
+
}
|
|
1052
1135
|
console.log(divider + "\n");
|
|
1053
1136
|
let shouldProceed;
|
|
1054
1137
|
try {
|
|
@@ -1084,15 +1167,15 @@ async function run(_input, config) {
|
|
|
1084
1167
|
spinner = ora2("Creating pull request\u2026").start();
|
|
1085
1168
|
let prUrl;
|
|
1086
1169
|
try {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
issue.
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
);
|
|
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);
|
|
1096
1179
|
spinner.stop();
|
|
1097
1180
|
} catch (err) {
|
|
1098
1181
|
spinner.stop();
|
|
@@ -1106,25 +1189,31 @@ ${issue.body ?? ""}`.trim(),
|
|
|
1106
1189
|
spinner.stop();
|
|
1107
1190
|
return `PR created (${prUrl}) but failed to update label: ${err.message}`;
|
|
1108
1191
|
}
|
|
1192
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1109
1193
|
return `Task #${issueNumber} submitted.
|
|
1110
1194
|
Commit: "${commitMessage.trim()}"
|
|
1111
1195
|
PR: ${prUrl}`;
|
|
1112
1196
|
}
|
|
1113
1197
|
async function execute(input4, config) {
|
|
1114
|
-
const
|
|
1115
|
-
const
|
|
1116
|
-
if (!
|
|
1117
|
-
const
|
|
1118
|
-
const [issue, defaultBranch, diff] = await Promise.all([
|
|
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([
|
|
1119
1203
|
getTask(config, issueNumber),
|
|
1120
1204
|
getBaseBranch(config),
|
|
1121
|
-
|
|
1205
|
+
diffPromise,
|
|
1206
|
+
getCurrentBranch(),
|
|
1207
|
+
getAuthenticatedUser(config)
|
|
1122
1208
|
]);
|
|
1209
|
+
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
1123
1210
|
let review = "";
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1211
|
+
if (!isSelfSubmit) {
|
|
1212
|
+
try {
|
|
1213
|
+
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
review = `(Review failed: ${err.message})`;
|
|
1216
|
+
}
|
|
1128
1217
|
}
|
|
1129
1218
|
const commitMessage = input4["commit_message"]?.trim() || `complete: ${issue.title}`;
|
|
1130
1219
|
try {
|
|
@@ -1134,15 +1223,15 @@ async function execute(input4, config) {
|
|
|
1134
1223
|
}
|
|
1135
1224
|
let prUrl;
|
|
1136
1225
|
try {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
issue.
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
);
|
|
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);
|
|
1146
1235
|
} catch (err) {
|
|
1147
1236
|
return `Committed but PR creation failed: ${err.message}`;
|
|
1148
1237
|
}
|
|
@@ -1336,23 +1425,28 @@ Finish or submit it before claiming a new one.`;
|
|
|
1336
1425
|
let spinner = ora4(`Claiming #${issue.number}\u2026`).start();
|
|
1337
1426
|
await claimTask(config, issue.number, me);
|
|
1338
1427
|
spinner.stop();
|
|
1339
|
-
const branch =
|
|
1340
|
-
spinner = ora4(`
|
|
1428
|
+
const branch = makeWorkerBranchName(me);
|
|
1429
|
+
spinner = ora4(`Switching to branch ${branch}\u2026`).start();
|
|
1430
|
+
let isNew = false;
|
|
1341
1431
|
try {
|
|
1342
|
-
await
|
|
1432
|
+
isNew = await switchToBranchOrCreate(branch);
|
|
1343
1433
|
spinner.stop();
|
|
1344
1434
|
} catch {
|
|
1345
|
-
spinner.warn(`Could not
|
|
1435
|
+
spinner.warn(`Could not switch to branch ${branch}`);
|
|
1346
1436
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
+
}
|
|
1353
1445
|
}
|
|
1446
|
+
const baseCommit = await getCurrentCommit();
|
|
1447
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
|
|
1354
1448
|
console.log(chalk8.green(`
|
|
1355
|
-
Claimed! Branch: ${branch}
|
|
1449
|
+
Claimed! Branch: ${branch} (base: ${baseCommit.slice(0, 7)})
|
|
1356
1450
|
`));
|
|
1357
1451
|
let openClaude;
|
|
1358
1452
|
try {
|
|
@@ -1405,16 +1499,21 @@ async function execute3(input4, config) {
|
|
|
1405
1499
|
} catch (err) {
|
|
1406
1500
|
return `Error claiming task: ${err.message}`;
|
|
1407
1501
|
}
|
|
1408
|
-
const branch =
|
|
1502
|
+
const branch = makeWorkerBranchName(me);
|
|
1503
|
+
let isNew = false;
|
|
1409
1504
|
try {
|
|
1410
|
-
await
|
|
1505
|
+
isNew = await switchToBranchOrCreate(branch);
|
|
1411
1506
|
} catch {
|
|
1412
1507
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1508
|
+
if (isNew) {
|
|
1509
|
+
try {
|
|
1510
|
+
await pushBranch(branch);
|
|
1511
|
+
} catch {
|
|
1512
|
+
}
|
|
1416
1513
|
}
|
|
1417
|
-
|
|
1514
|
+
const baseCommit = await getCurrentCommit();
|
|
1515
|
+
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
|
|
1516
|
+
return `Task #${issueNumber} claimed. Branch: ${branch} (base commit: ${baseCommit.slice(0, 7)})`;
|
|
1418
1517
|
}
|
|
1419
1518
|
return `Unknown action: ${action}`;
|
|
1420
1519
|
}
|
|
@@ -1505,6 +1604,20 @@ var definition4 = {
|
|
|
1505
1604
|
}
|
|
1506
1605
|
};
|
|
1507
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
|
+
}
|
|
1508
1621
|
let title = input4["title"]?.trim();
|
|
1509
1622
|
if (!title) {
|
|
1510
1623
|
try {
|
|
@@ -1602,6 +1715,10 @@ async function run4(input4, config) {
|
|
|
1602
1715
|
return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
|
|
1603
1716
|
}
|
|
1604
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
|
+
}
|
|
1605
1722
|
const title = input4["title"].trim();
|
|
1606
1723
|
const feedback = input4["feedback"];
|
|
1607
1724
|
let guide = await generateGuide(config, title);
|
|
@@ -1820,6 +1937,13 @@ var definition9 = {
|
|
|
1820
1937
|
};
|
|
1821
1938
|
async function run9(input4, config) {
|
|
1822
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
|
+
}
|
|
1823
1947
|
let feedback;
|
|
1824
1948
|
try {
|
|
1825
1949
|
feedback = await promptInput3({
|
|
@@ -1889,6 +2013,13 @@ async function run9(input4, config) {
|
|
|
1889
2013
|
async function execute9(input4, config) {
|
|
1890
2014
|
const issueNumber = input4["issue_number"];
|
|
1891
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
|
+
}
|
|
1892
2023
|
let comment;
|
|
1893
2024
|
try {
|
|
1894
2025
|
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
@@ -1941,15 +2072,15 @@ var definition10 = {
|
|
|
1941
2072
|
async function run10(input4, config) {
|
|
1942
2073
|
let issueNumber = input4["issue_number"];
|
|
1943
2074
|
if (!issueNumber) {
|
|
1944
|
-
const
|
|
2075
|
+
const spinner3 = ora9("Loading tasks for review\u2026").start();
|
|
1945
2076
|
let tasks;
|
|
1946
2077
|
let me;
|
|
1947
2078
|
try {
|
|
1948
2079
|
me = await getAuthenticatedUser(config);
|
|
1949
2080
|
tasks = await listTasksForReview(config, me);
|
|
1950
|
-
|
|
2081
|
+
spinner3.stop();
|
|
1951
2082
|
} catch (err) {
|
|
1952
|
-
|
|
2083
|
+
spinner3.stop();
|
|
1953
2084
|
return `Error: ${err.message}`;
|
|
1954
2085
|
}
|
|
1955
2086
|
if (tasks.length === 0) return "No tasks pending review.";
|
|
@@ -1965,6 +2096,22 @@ async function run10(input4, config) {
|
|
|
1965
2096
|
return "Cancelled.";
|
|
1966
2097
|
}
|
|
1967
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
|
+
}
|
|
1968
2115
|
const baseBranch = config.github.baseBranch ?? "main";
|
|
1969
2116
|
let confirmed;
|
|
1970
2117
|
try {
|
|
@@ -1993,6 +2140,13 @@ Issue closed.`;
|
|
|
1993
2140
|
}
|
|
1994
2141
|
async function execute10(input4, config) {
|
|
1995
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
|
+
}
|
|
1996
2150
|
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
1997
2151
|
try {
|
|
1998
2152
|
const result = await acceptTask(config, issueNumber);
|
package/dist/mcp.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(github_exports, {
|
|
|
24
24
|
getBaseBranch: () => getBaseBranch,
|
|
25
25
|
getDefaultBranch: () => getDefaultBranch,
|
|
26
26
|
getTask: () => getTask,
|
|
27
|
+
isCollaborator: () => isCollaborator,
|
|
27
28
|
listComments: () => listComments,
|
|
28
29
|
listMyTasks: () => listMyTasks,
|
|
29
30
|
listTasks: () => listTasks,
|
|
@@ -50,6 +51,7 @@ function parseIssue(issue) {
|
|
|
50
51
|
title: issue.title,
|
|
51
52
|
body: issue.body ?? null,
|
|
52
53
|
state: issue.state,
|
|
54
|
+
author: issue.user?.login ?? null,
|
|
53
55
|
assignee: issue.assignee?.login ?? null,
|
|
54
56
|
labels: (issue.labels ?? []).map(
|
|
55
57
|
(l) => typeof l === "string" ? l : l.name ?? ""
|
|
@@ -226,6 +228,16 @@ async function getAuthenticatedUser(config) {
|
|
|
226
228
|
const { data } = await octokit.users.getAuthenticated();
|
|
227
229
|
return data.login;
|
|
228
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
|
+
}
|
|
229
241
|
async function listMyTasks(config, username) {
|
|
230
242
|
const octokit = createOctokit(config.githubToken);
|
|
231
243
|
const { owner, repo } = config.github;
|
|
@@ -360,15 +372,63 @@ async function getCurrentBranch() {
|
|
|
360
372
|
const summary = await git.branch();
|
|
361
373
|
return summary.current;
|
|
362
374
|
}
|
|
363
|
-
async function createAndSwitchBranch(name) {
|
|
364
|
-
await git.checkoutLocalBranch(name);
|
|
365
|
-
}
|
|
366
375
|
async function pushBranch(name) {
|
|
367
376
|
await git.push("origin", name, ["--set-upstream"]);
|
|
368
377
|
}
|
|
369
|
-
function
|
|
378
|
+
function makeWorkerBranchName(username) {
|
|
370
379
|
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
371
|
-
return `
|
|
380
|
+
return `worker-${slug}`;
|
|
381
|
+
}
|
|
382
|
+
async function getCurrentCommit() {
|
|
383
|
+
return (await git.revparse(["HEAD"])).trim();
|
|
384
|
+
}
|
|
385
|
+
async function switchToBranchOrCreate(name) {
|
|
386
|
+
try {
|
|
387
|
+
const branches = await git.branch(["-a"]);
|
|
388
|
+
const exists = Object.keys(branches.branches).some(
|
|
389
|
+
(b) => b === name || b === `remotes/origin/${name}`
|
|
390
|
+
);
|
|
391
|
+
if (exists) {
|
|
392
|
+
await git.checkout(name);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
await git.checkoutLocalBranch(name);
|
|
396
|
+
return true;
|
|
397
|
+
} catch {
|
|
398
|
+
await git.checkoutLocalBranch(name);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function getDiffFromCommit(baseCommit) {
|
|
403
|
+
const status = await git.status();
|
|
404
|
+
const parts = [];
|
|
405
|
+
const fileLines = [
|
|
406
|
+
...status.modified.map((f) => ` M ${f}`),
|
|
407
|
+
...status.created.map((f) => ` A ${f}`),
|
|
408
|
+
...status.deleted.map((f) => ` D ${f}`),
|
|
409
|
+
...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
|
|
410
|
+
...status.not_added.map((f) => ` ? ${f}`)
|
|
411
|
+
];
|
|
412
|
+
if (fileLines.length > 0) {
|
|
413
|
+
parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
|
|
414
|
+
const uncommitted = await git.diff(["HEAD"]);
|
|
415
|
+
if (uncommitted) {
|
|
416
|
+
const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
|
|
417
|
+
parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const log = await git.log({ from: baseCommit, to: "HEAD" });
|
|
421
|
+
if (log.total > 0) {
|
|
422
|
+
const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
|
|
423
|
+
parts.push(`## Commits since task claimed (${log.total} total)
|
|
424
|
+
` + logLines.join("\n"));
|
|
425
|
+
const branchDiff = await git.diff([baseCommit, "HEAD"]);
|
|
426
|
+
if (branchDiff) {
|
|
427
|
+
const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
|
|
428
|
+
parts.push("## Full diff since task claimed\n```diff\n" + capped + "\n```");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return parts.length > 0 ? parts.join("\n\n") : "No changes since task was claimed.";
|
|
372
432
|
}
|
|
373
433
|
async function findMergeBase(configuredBase) {
|
|
374
434
|
const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
|
|
@@ -428,6 +488,69 @@ async function stageAllAndCommit(message) {
|
|
|
428
488
|
await git.push("origin", branch, ["--set-upstream"]);
|
|
429
489
|
}
|
|
430
490
|
|
|
491
|
+
// src/lib/config.ts
|
|
492
|
+
import Conf from "conf";
|
|
493
|
+
import { z } from "zod";
|
|
494
|
+
var configSchema = z.object({
|
|
495
|
+
aiApiKey: z.string().min(1),
|
|
496
|
+
aiBaseUrl: z.string().optional(),
|
|
497
|
+
aiModel: z.string().optional(),
|
|
498
|
+
githubToken: z.string().min(1),
|
|
499
|
+
githubClientId: z.string().optional(),
|
|
500
|
+
github: z.object({
|
|
501
|
+
owner: z.string().min(1),
|
|
502
|
+
repo: z.string().min(1),
|
|
503
|
+
baseBranch: z.string().optional()
|
|
504
|
+
}),
|
|
505
|
+
taskState: z.object({
|
|
506
|
+
activeIssueNumber: z.number().optional(),
|
|
507
|
+
baseCommit: z.string().optional()
|
|
508
|
+
}).optional()
|
|
509
|
+
});
|
|
510
|
+
var store = new Conf({
|
|
511
|
+
projectName: "techunter",
|
|
512
|
+
defaults: {}
|
|
513
|
+
});
|
|
514
|
+
function getConfig() {
|
|
515
|
+
const raw = store.store;
|
|
516
|
+
const result = configSchema.safeParse(raw);
|
|
517
|
+
if (!result.success) {
|
|
518
|
+
throw new Error("Configuration is missing or invalid.");
|
|
519
|
+
}
|
|
520
|
+
return result.data;
|
|
521
|
+
}
|
|
522
|
+
function setConfig(partial) {
|
|
523
|
+
const current = store.store;
|
|
524
|
+
if (partial.github) {
|
|
525
|
+
current["github"] = {
|
|
526
|
+
...current["github"] ?? {},
|
|
527
|
+
...partial.github
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (partial.aiApiKey !== void 0) {
|
|
531
|
+
current["aiApiKey"] = partial.aiApiKey;
|
|
532
|
+
}
|
|
533
|
+
if (partial.aiBaseUrl !== void 0) {
|
|
534
|
+
current["aiBaseUrl"] = partial.aiBaseUrl;
|
|
535
|
+
}
|
|
536
|
+
if (partial.aiModel !== void 0) {
|
|
537
|
+
current["aiModel"] = partial.aiModel;
|
|
538
|
+
}
|
|
539
|
+
if (partial.githubToken !== void 0) {
|
|
540
|
+
current["githubToken"] = partial.githubToken;
|
|
541
|
+
}
|
|
542
|
+
if (partial.githubClientId !== void 0) {
|
|
543
|
+
current["githubClientId"] = partial.githubClientId;
|
|
544
|
+
}
|
|
545
|
+
if (partial.taskState !== void 0) {
|
|
546
|
+
current["taskState"] = {
|
|
547
|
+
...current["taskState"] ?? {},
|
|
548
|
+
...partial.taskState
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
store.store = current;
|
|
552
|
+
}
|
|
553
|
+
|
|
431
554
|
// src/lib/markdown.ts
|
|
432
555
|
import { marked } from "marked";
|
|
433
556
|
import { markedTerminal } from "marked-terminal";
|
|
@@ -635,7 +758,7 @@ async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
|
635
758
|
}
|
|
636
759
|
|
|
637
760
|
// src/tools/submit/prompts.ts
|
|
638
|
-
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.";
|
|
761
|
+
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.";
|
|
639
762
|
|
|
640
763
|
// src/tools/submit/reviewer.ts
|
|
641
764
|
async function reviewChanges(config, issueNumber, issue, diff) {
|
|
@@ -669,32 +792,41 @@ var definition = {
|
|
|
669
792
|
}
|
|
670
793
|
};
|
|
671
794
|
async function run(_input, config) {
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
if (!
|
|
675
|
-
return
|
|
795
|
+
const taskState = getConfig().taskState;
|
|
796
|
+
const issueNumber = taskState?.activeIssueNumber;
|
|
797
|
+
if (!issueNumber) {
|
|
798
|
+
return "No active task found. Claim a task first with /pick.";
|
|
676
799
|
}
|
|
677
|
-
const issueNumber = parseInt(match[1], 10);
|
|
678
800
|
let spinner = ora("Loading task and diff\u2026").start();
|
|
679
|
-
const
|
|
801
|
+
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff(config.github.baseBranch);
|
|
802
|
+
const [issue, defaultBranch, diff, me] = await Promise.all([
|
|
680
803
|
getTask(config, issueNumber),
|
|
681
804
|
getBaseBranch(config),
|
|
682
|
-
|
|
805
|
+
diffPromise,
|
|
806
|
+
getAuthenticatedUser(config)
|
|
683
807
|
]);
|
|
684
808
|
spinner.stop();
|
|
685
|
-
const
|
|
809
|
+
const branch = await getCurrentBranch();
|
|
810
|
+
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
686
811
|
let review = "";
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
812
|
+
if (!isSelfSubmit) {
|
|
813
|
+
const reviewSpinner = ora("Reviewing changes\u2026").start();
|
|
814
|
+
try {
|
|
815
|
+
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
review = `(Review failed: ${err.message})`;
|
|
818
|
+
}
|
|
819
|
+
reviewSpinner.stop();
|
|
691
820
|
}
|
|
692
|
-
reviewSpinner.stop();
|
|
693
821
|
const divider = chalk5.dim("\u2500".repeat(70));
|
|
694
822
|
console.log("\n" + divider);
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
823
|
+
if (isSelfSubmit) {
|
|
824
|
+
console.log(chalk5.yellow(` Self-submit detected \u2014 AI review skipped.`));
|
|
825
|
+
} else {
|
|
826
|
+
console.log(chalk5.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
|
|
827
|
+
console.log(divider);
|
|
828
|
+
console.log(renderMarkdown(review));
|
|
829
|
+
}
|
|
698
830
|
console.log(divider + "\n");
|
|
699
831
|
let shouldProceed;
|
|
700
832
|
try {
|
|
@@ -730,15 +862,15 @@ async function run(_input, config) {
|
|
|
730
862
|
spinner = ora("Creating pull request\u2026").start();
|
|
731
863
|
let prUrl;
|
|
732
864
|
try {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
issue.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
);
|
|
865
|
+
const prBody = [
|
|
866
|
+
`Closes #${issueNumber}`,
|
|
867
|
+
issue.body ? `
|
|
868
|
+
${issue.body}` : "",
|
|
869
|
+
review ? `
|
|
870
|
+
## AI Review
|
|
871
|
+
${review}` : ""
|
|
872
|
+
].join("\n").trim();
|
|
873
|
+
prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
|
|
742
874
|
spinner.stop();
|
|
743
875
|
} catch (err) {
|
|
744
876
|
spinner.stop();
|
|
@@ -752,25 +884,31 @@ ${issue.body ?? ""}`.trim(),
|
|
|
752
884
|
spinner.stop();
|
|
753
885
|
return `PR created (${prUrl}) but failed to update label: ${err.message}`;
|
|
754
886
|
}
|
|
887
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
755
888
|
return `Task #${issueNumber} submitted.
|
|
756
889
|
Commit: "${commitMessage.trim()}"
|
|
757
890
|
PR: ${prUrl}`;
|
|
758
891
|
}
|
|
759
892
|
async function execute(input, config) {
|
|
760
|
-
const
|
|
761
|
-
const
|
|
762
|
-
if (!
|
|
763
|
-
const
|
|
764
|
-
const [issue, defaultBranch, diff] = await Promise.all([
|
|
893
|
+
const taskState = getConfig().taskState;
|
|
894
|
+
const issueNumber = taskState?.activeIssueNumber;
|
|
895
|
+
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([
|
|
765
898
|
getTask(config, issueNumber),
|
|
766
899
|
getBaseBranch(config),
|
|
767
|
-
|
|
900
|
+
diffPromise,
|
|
901
|
+
getCurrentBranch(),
|
|
902
|
+
getAuthenticatedUser(config)
|
|
768
903
|
]);
|
|
904
|
+
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
769
905
|
let review = "";
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
906
|
+
if (!isSelfSubmit) {
|
|
907
|
+
try {
|
|
908
|
+
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
909
|
+
} catch (err) {
|
|
910
|
+
review = `(Review failed: ${err.message})`;
|
|
911
|
+
}
|
|
774
912
|
}
|
|
775
913
|
const commitMessage = input["commit_message"]?.trim() || `complete: ${issue.title}`;
|
|
776
914
|
try {
|
|
@@ -780,15 +918,15 @@ async function execute(input, config) {
|
|
|
780
918
|
}
|
|
781
919
|
let prUrl;
|
|
782
920
|
try {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
issue.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
);
|
|
921
|
+
const prBody = [
|
|
922
|
+
`Closes #${issueNumber}`,
|
|
923
|
+
issue.body ? `
|
|
924
|
+
${issue.body}` : "",
|
|
925
|
+
review ? `
|
|
926
|
+
## AI Review
|
|
927
|
+
${review}` : ""
|
|
928
|
+
].join("\n").trim();
|
|
929
|
+
prUrl = await createPR(config, issue.title, prBody, branch, defaultBranch);
|
|
792
930
|
} catch (err) {
|
|
793
931
|
return `Committed but PR creation failed: ${err.message}`;
|
|
794
932
|
}
|
|
@@ -982,23 +1120,28 @@ Finish or submit it before claiming a new one.`;
|
|
|
982
1120
|
let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
|
|
983
1121
|
await claimTask(config, issue.number, me);
|
|
984
1122
|
spinner.stop();
|
|
985
|
-
const branch =
|
|
986
|
-
spinner = ora3(`
|
|
1123
|
+
const branch = makeWorkerBranchName(me);
|
|
1124
|
+
spinner = ora3(`Switching to branch ${branch}\u2026`).start();
|
|
1125
|
+
let isNew = false;
|
|
987
1126
|
try {
|
|
988
|
-
await
|
|
1127
|
+
isNew = await switchToBranchOrCreate(branch);
|
|
989
1128
|
spinner.stop();
|
|
990
1129
|
} catch {
|
|
991
|
-
spinner.warn(`Could not
|
|
1130
|
+
spinner.warn(`Could not switch to branch ${branch}`);
|
|
992
1131
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1132
|
+
if (isNew) {
|
|
1133
|
+
spinner = ora3("Pushing branch\u2026").start();
|
|
1134
|
+
try {
|
|
1135
|
+
await pushBranch(branch);
|
|
1136
|
+
spinner.stop();
|
|
1137
|
+
} catch {
|
|
1138
|
+
spinner.warn("Could not push branch");
|
|
1139
|
+
}
|
|
999
1140
|
}
|
|
1141
|
+
const baseCommit = await getCurrentCommit();
|
|
1142
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
|
|
1000
1143
|
console.log(chalk6.green(`
|
|
1001
|
-
Claimed! Branch: ${branch}
|
|
1144
|
+
Claimed! Branch: ${branch} (base: ${baseCommit.slice(0, 7)})
|
|
1002
1145
|
`));
|
|
1003
1146
|
let openClaude;
|
|
1004
1147
|
try {
|
|
@@ -1051,16 +1194,21 @@ async function execute3(input, config) {
|
|
|
1051
1194
|
} catch (err) {
|
|
1052
1195
|
return `Error claiming task: ${err.message}`;
|
|
1053
1196
|
}
|
|
1054
|
-
const branch =
|
|
1197
|
+
const branch = makeWorkerBranchName(me);
|
|
1198
|
+
let isNew = false;
|
|
1055
1199
|
try {
|
|
1056
|
-
await
|
|
1200
|
+
isNew = await switchToBranchOrCreate(branch);
|
|
1057
1201
|
} catch {
|
|
1058
1202
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1203
|
+
if (isNew) {
|
|
1204
|
+
try {
|
|
1205
|
+
await pushBranch(branch);
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1062
1208
|
}
|
|
1063
|
-
|
|
1209
|
+
const baseCommit = await getCurrentCommit();
|
|
1210
|
+
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
|
|
1211
|
+
return `Task #${issueNumber} claimed. Branch: ${branch} (base commit: ${baseCommit.slice(0, 7)})`;
|
|
1064
1212
|
}
|
|
1065
1213
|
return `Unknown action: ${action}`;
|
|
1066
1214
|
}
|
|
@@ -1151,6 +1299,20 @@ var definition4 = {
|
|
|
1151
1299
|
}
|
|
1152
1300
|
};
|
|
1153
1301
|
async function run4(input, config) {
|
|
1302
|
+
const authSpinner = ora4("Checking permissions\u2026").start();
|
|
1303
|
+
let me;
|
|
1304
|
+
let allowed;
|
|
1305
|
+
try {
|
|
1306
|
+
me = await getAuthenticatedUser(config);
|
|
1307
|
+
allowed = await isCollaborator(config, me);
|
|
1308
|
+
authSpinner.stop();
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
authSpinner.stop();
|
|
1311
|
+
return `Error checking permissions: ${err.message}`;
|
|
1312
|
+
}
|
|
1313
|
+
if (!allowed) {
|
|
1314
|
+
return `Permission denied: only repository collaborators can create tasks.`;
|
|
1315
|
+
}
|
|
1154
1316
|
let title = input["title"]?.trim();
|
|
1155
1317
|
if (!title) {
|
|
1156
1318
|
try {
|
|
@@ -1248,6 +1410,10 @@ async function run4(input, config) {
|
|
|
1248
1410
|
return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
|
|
1249
1411
|
}
|
|
1250
1412
|
async function execute4(input, config) {
|
|
1413
|
+
const me = await getAuthenticatedUser(config);
|
|
1414
|
+
if (!await isCollaborator(config, me)) {
|
|
1415
|
+
return `Permission denied: only repository collaborators can create tasks.`;
|
|
1416
|
+
}
|
|
1251
1417
|
const title = input["title"].trim();
|
|
1252
1418
|
const feedback = input["feedback"];
|
|
1253
1419
|
let guide = await generateGuide(config, title);
|
|
@@ -1466,6 +1632,13 @@ var definition9 = {
|
|
|
1466
1632
|
};
|
|
1467
1633
|
async function run9(input, config) {
|
|
1468
1634
|
const issueNumber = input["issue_number"];
|
|
1635
|
+
const [me, issue] = await Promise.all([
|
|
1636
|
+
getAuthenticatedUser(config),
|
|
1637
|
+
getTask(config, issueNumber)
|
|
1638
|
+
]);
|
|
1639
|
+
if (issue.author && issue.author !== me) {
|
|
1640
|
+
return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
|
|
1641
|
+
}
|
|
1469
1642
|
let feedback;
|
|
1470
1643
|
try {
|
|
1471
1644
|
feedback = await promptInput3({
|
|
@@ -1535,6 +1708,13 @@ async function run9(input, config) {
|
|
|
1535
1708
|
async function execute9(input, config) {
|
|
1536
1709
|
const issueNumber = input["issue_number"];
|
|
1537
1710
|
const feedback = input["feedback"];
|
|
1711
|
+
const [me, issue] = await Promise.all([
|
|
1712
|
+
getAuthenticatedUser(config),
|
|
1713
|
+
getTask(config, issueNumber)
|
|
1714
|
+
]);
|
|
1715
|
+
if (issue.author && issue.author !== me) {
|
|
1716
|
+
return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
|
|
1717
|
+
}
|
|
1538
1718
|
let comment;
|
|
1539
1719
|
try {
|
|
1540
1720
|
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
@@ -1587,15 +1767,15 @@ var definition10 = {
|
|
|
1587
1767
|
async function run10(input, config) {
|
|
1588
1768
|
let issueNumber = input["issue_number"];
|
|
1589
1769
|
if (!issueNumber) {
|
|
1590
|
-
const
|
|
1770
|
+
const spinner3 = ora8("Loading tasks for review\u2026").start();
|
|
1591
1771
|
let tasks;
|
|
1592
1772
|
let me;
|
|
1593
1773
|
try {
|
|
1594
1774
|
me = await getAuthenticatedUser(config);
|
|
1595
1775
|
tasks = await listTasksForReview(config, me);
|
|
1596
|
-
|
|
1776
|
+
spinner3.stop();
|
|
1597
1777
|
} catch (err) {
|
|
1598
|
-
|
|
1778
|
+
spinner3.stop();
|
|
1599
1779
|
return `Error: ${err.message}`;
|
|
1600
1780
|
}
|
|
1601
1781
|
if (tasks.length === 0) return "No tasks pending review.";
|
|
@@ -1611,6 +1791,22 @@ async function run10(input, config) {
|
|
|
1611
1791
|
return "Cancelled.";
|
|
1612
1792
|
}
|
|
1613
1793
|
}
|
|
1794
|
+
const spinner2 = ora8("Verifying permissions\u2026").start();
|
|
1795
|
+
let me2;
|
|
1796
|
+
let issue;
|
|
1797
|
+
try {
|
|
1798
|
+
[me2, issue] = await Promise.all([
|
|
1799
|
+
getAuthenticatedUser(config),
|
|
1800
|
+
getTask(config, issueNumber)
|
|
1801
|
+
]);
|
|
1802
|
+
spinner2.stop();
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
spinner2.stop();
|
|
1805
|
+
return `Error: ${err.message}`;
|
|
1806
|
+
}
|
|
1807
|
+
if (issue.author && issue.author !== me2) {
|
|
1808
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
1809
|
+
}
|
|
1614
1810
|
const baseBranch = config.github.baseBranch ?? "main";
|
|
1615
1811
|
let confirmed;
|
|
1616
1812
|
try {
|
|
@@ -1639,6 +1835,13 @@ Issue closed.`;
|
|
|
1639
1835
|
}
|
|
1640
1836
|
async function execute10(input, config) {
|
|
1641
1837
|
const issueNumber = input["issue_number"];
|
|
1838
|
+
const [me, issue] = await Promise.all([
|
|
1839
|
+
getAuthenticatedUser(config),
|
|
1840
|
+
getTask(config, issueNumber)
|
|
1841
|
+
]);
|
|
1842
|
+
if (issue.author && issue.author !== me) {
|
|
1843
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
1844
|
+
}
|
|
1642
1845
|
const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
|
|
1643
1846
|
try {
|
|
1644
1847
|
const result = await acceptTask(config, issueNumber);
|
|
@@ -2240,34 +2443,6 @@ var toolModules = [
|
|
|
2240
2443
|
ask_user_exports
|
|
2241
2444
|
];
|
|
2242
2445
|
|
|
2243
|
-
// src/lib/config.ts
|
|
2244
|
-
import Conf from "conf";
|
|
2245
|
-
import { z } from "zod";
|
|
2246
|
-
var configSchema = z.object({
|
|
2247
|
-
aiApiKey: z.string().min(1),
|
|
2248
|
-
aiBaseUrl: z.string().optional(),
|
|
2249
|
-
aiModel: z.string().optional(),
|
|
2250
|
-
githubToken: z.string().min(1),
|
|
2251
|
-
githubClientId: z.string().optional(),
|
|
2252
|
-
github: z.object({
|
|
2253
|
-
owner: z.string().min(1),
|
|
2254
|
-
repo: z.string().min(1),
|
|
2255
|
-
baseBranch: z.string().optional()
|
|
2256
|
-
})
|
|
2257
|
-
});
|
|
2258
|
-
var store = new Conf({
|
|
2259
|
-
projectName: "techunter",
|
|
2260
|
-
defaults: {}
|
|
2261
|
-
});
|
|
2262
|
-
function getConfig() {
|
|
2263
|
-
const raw = store.store;
|
|
2264
|
-
const result = configSchema.safeParse(raw);
|
|
2265
|
-
if (!result.success) {
|
|
2266
|
-
throw new Error("Configuration is missing or invalid.");
|
|
2267
|
-
}
|
|
2268
|
-
return result.data;
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
2446
|
// src/mcp.ts
|
|
2272
2447
|
var tools = toolModules.filter((m) => m.definition.function.name !== "ask_user");
|
|
2273
2448
|
var server = new Server(
|