techunter 0.1.10 → 1.0.0
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/README.md +237 -209
- package/dist/index.js +1913 -1109
- package/dist/mcp.js +1228 -557
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -39,12 +39,20 @@ __export(github_exports, {
|
|
|
39
39
|
createTask: () => createTask,
|
|
40
40
|
editTask: () => editTask,
|
|
41
41
|
embedBaseCommit: () => embedBaseCommit,
|
|
42
|
+
embedTargetBranch: () => embedTargetBranch,
|
|
42
43
|
ensureLabels: () => ensureLabels,
|
|
44
|
+
ensureRemoteBranch: () => ensureRemoteBranch,
|
|
43
45
|
extractBaseCommit: () => extractBaseCommit,
|
|
46
|
+
extractTargetBranch: () => extractTargetBranch,
|
|
44
47
|
formatGuideAsMarkdown: () => formatGuideAsMarkdown,
|
|
45
48
|
getAuthenticatedUser: () => getAuthenticatedUser,
|
|
46
49
|
getDefaultBranch: () => getDefaultBranch,
|
|
50
|
+
getIssueNumberFromBranch: () => getIssueNumberFromBranch,
|
|
51
|
+
getOpenSubtasks: () => getOpenSubtasks,
|
|
52
|
+
getRepoFile: () => getRepoFile,
|
|
47
53
|
getTask: () => getTask,
|
|
54
|
+
getTaskPR: () => getTaskPR,
|
|
55
|
+
getTaskPRDiff: () => getTaskPRDiff,
|
|
48
56
|
isCollaborator: () => isCollaborator,
|
|
49
57
|
listComments: () => listComments,
|
|
50
58
|
listMyTasks: () => listMyTasks,
|
|
@@ -54,7 +62,8 @@ __export(github_exports, {
|
|
|
54
62
|
mergeWorkerIntoBase: () => mergeWorkerIntoBase,
|
|
55
63
|
postComment: () => postComment,
|
|
56
64
|
postGuideComment: () => postGuideComment,
|
|
57
|
-
rejectTask: () => rejectTask
|
|
65
|
+
rejectTask: () => rejectTask,
|
|
66
|
+
upsertRepoFile: () => upsertRepoFile
|
|
58
67
|
});
|
|
59
68
|
import { Octokit } from "@octokit/rest";
|
|
60
69
|
import { fetch as undiciFetch } from "undici";
|
|
@@ -117,11 +126,22 @@ function extractBaseCommit(body) {
|
|
|
117
126
|
const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
|
|
118
127
|
return match?.[1] ?? null;
|
|
119
128
|
}
|
|
120
|
-
|
|
129
|
+
function embedTargetBranch(body, branch) {
|
|
130
|
+
return `${body}
|
|
131
|
+
${TARGET_BRANCH_MARKER}${branch} -->`;
|
|
132
|
+
}
|
|
133
|
+
function extractTargetBranch(body) {
|
|
134
|
+
if (!body) return null;
|
|
135
|
+
const match = body.match(/<!-- techunter-target:([^\s>]+) -->/);
|
|
136
|
+
return match?.[1] ?? null;
|
|
137
|
+
}
|
|
138
|
+
async function createTask(config, title, body, baseCommit, targetBranch) {
|
|
121
139
|
const octokit = createOctokit(config.githubToken);
|
|
122
140
|
const { owner, repo } = config.github;
|
|
123
141
|
await ensureLabels(config);
|
|
124
|
-
|
|
142
|
+
let finalBody = body ?? "";
|
|
143
|
+
if (baseCommit) finalBody = embedBaseCommit(finalBody, baseCommit);
|
|
144
|
+
if (targetBranch) finalBody = embedTargetBranch(finalBody, targetBranch);
|
|
125
145
|
const { data } = await octokit.issues.create({
|
|
126
146
|
owner,
|
|
127
147
|
repo,
|
|
@@ -134,13 +154,22 @@ async function createTask(config, title, body, baseCommit) {
|
|
|
134
154
|
async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
|
|
135
155
|
const octokit = createOctokit(config.githubToken);
|
|
136
156
|
const { owner, repo } = config.github;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
try {
|
|
158
|
+
await octokit.repos.merge({
|
|
159
|
+
owner,
|
|
160
|
+
repo,
|
|
161
|
+
base: baseBranch,
|
|
162
|
+
head: workerBranch,
|
|
163
|
+
commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (err.status === 409) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Merge conflict: ${workerBranch} cannot be merged into ${baseBranch} cleanly. Resolve conflicts manually.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
144
173
|
}
|
|
145
174
|
async function claimTask(config, number, username) {
|
|
146
175
|
const octokit = createOctokit(config.githubToken);
|
|
@@ -216,6 +245,23 @@ async function postGuideComment(config, number, guide) {
|
|
|
216
245
|
body
|
|
217
246
|
});
|
|
218
247
|
}
|
|
248
|
+
async function ensureRemoteBranch(config, branchName, fallbackBase) {
|
|
249
|
+
const octokit = createOctokit(config.githubToken);
|
|
250
|
+
const { owner, repo } = config.github;
|
|
251
|
+
try {
|
|
252
|
+
await octokit.repos.getBranch({ owner, repo, branch: branchName });
|
|
253
|
+
return;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err.status !== 404) throw err;
|
|
256
|
+
}
|
|
257
|
+
const { data: baseRef } = await octokit.repos.getBranch({ owner, repo, branch: fallbackBase });
|
|
258
|
+
await octokit.git.createRef({
|
|
259
|
+
owner,
|
|
260
|
+
repo,
|
|
261
|
+
ref: `refs/heads/${branchName}`,
|
|
262
|
+
sha: baseRef.commit.sha
|
|
263
|
+
});
|
|
264
|
+
}
|
|
219
265
|
async function createPR(config, title, body, branch, base) {
|
|
220
266
|
const octokit = createOctokit(config.githubToken);
|
|
221
267
|
const { owner, repo } = config.github;
|
|
@@ -232,14 +278,11 @@ async function createPR(config, title, body, branch, base) {
|
|
|
232
278
|
async function markInReview(config, number) {
|
|
233
279
|
const octokit = createOctokit(config.githubToken);
|
|
234
280
|
const { owner, repo } = config.github;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
owner,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
name: LABEL_CLAIMED
|
|
241
|
-
});
|
|
242
|
-
} catch {
|
|
281
|
+
for (const label of [LABEL_CLAIMED, LABEL_CHANGES_NEEDED]) {
|
|
282
|
+
try {
|
|
283
|
+
await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
243
286
|
}
|
|
244
287
|
await octokit.issues.addLabels({
|
|
245
288
|
owner,
|
|
@@ -354,28 +397,115 @@ async function editTask(config, number, title, body) {
|
|
|
354
397
|
const { owner, repo } = config.github;
|
|
355
398
|
await octokit.issues.update({ owner, repo, issue_number: number, title, body });
|
|
356
399
|
}
|
|
400
|
+
async function upsertRepoFile(config, filePath, content, message) {
|
|
401
|
+
const octokit = createOctokit(config.githubToken);
|
|
402
|
+
const { owner, repo } = config.github;
|
|
403
|
+
let sha;
|
|
404
|
+
try {
|
|
405
|
+
const { data: data2 } = await octokit.repos.getContent({ owner, repo, path: filePath });
|
|
406
|
+
if (!Array.isArray(data2) && data2.type === "file") {
|
|
407
|
+
sha = data2.sha;
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
const { data } = await octokit.repos.createOrUpdateFileContents({
|
|
412
|
+
owner,
|
|
413
|
+
repo,
|
|
414
|
+
path: filePath,
|
|
415
|
+
message,
|
|
416
|
+
content: Buffer.from(content, "utf-8").toString("base64"),
|
|
417
|
+
...sha ? { sha } : {}
|
|
418
|
+
});
|
|
419
|
+
return data.content?.html_url ?? `https://github.com/${owner}/${repo}/blob/main/${filePath}`;
|
|
420
|
+
}
|
|
421
|
+
async function getRepoFile(config, filePath) {
|
|
422
|
+
const octokit = createOctokit(config.githubToken);
|
|
423
|
+
const { owner, repo } = config.github;
|
|
424
|
+
try {
|
|
425
|
+
const { data } = await octokit.repos.getContent({ owner, repo, path: filePath });
|
|
426
|
+
if (!Array.isArray(data) && data.type === "file" && "content" in data) {
|
|
427
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
357
434
|
async function getDefaultBranch(config) {
|
|
358
435
|
const octokit = createOctokit(config.githubToken);
|
|
359
436
|
const { owner, repo } = config.github;
|
|
360
437
|
const { data } = await octokit.repos.get({ owner, repo });
|
|
361
438
|
return data.default_branch;
|
|
362
439
|
}
|
|
363
|
-
async function
|
|
440
|
+
async function getTaskPR(config, issueNumber) {
|
|
364
441
|
const octokit = createOctokit(config.githubToken);
|
|
365
442
|
const { owner, repo } = config.github;
|
|
366
443
|
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
367
|
-
const pr =
|
|
368
|
-
|
|
369
|
-
|
|
444
|
+
const pr = prs.find(
|
|
445
|
+
(p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
|
|
446
|
+
);
|
|
447
|
+
if (!pr) return null;
|
|
448
|
+
return { number: pr.number, url: pr.html_url, body: pr.body ?? "", baseBranch: pr.base.ref };
|
|
449
|
+
}
|
|
450
|
+
async function getOpenSubtasks(config, targetBranch) {
|
|
451
|
+
const octokit = createOctokit(config.githubToken);
|
|
452
|
+
const { owner, repo } = config.github;
|
|
453
|
+
const { data } = await octokit.issues.listForRepo({
|
|
454
|
+
owner,
|
|
455
|
+
repo,
|
|
456
|
+
state: "open",
|
|
457
|
+
per_page: 100
|
|
458
|
+
});
|
|
459
|
+
return data.filter((issue) => !issue.pull_request).filter((issue) => extractTargetBranch(issue.body ?? null) === targetBranch).map((issue) => issue.number);
|
|
460
|
+
}
|
|
461
|
+
async function getIssueNumberFromBranch(config, branch) {
|
|
462
|
+
const octokit = createOctokit(config.githubToken);
|
|
463
|
+
const { owner, repo } = config.github;
|
|
464
|
+
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
465
|
+
const pr = prs.find((p) => p.head.ref === branch);
|
|
466
|
+
if (!pr) return null;
|
|
467
|
+
const match = (pr.body ?? "").match(/Closes #(\d+)/i);
|
|
468
|
+
if (!match) return null;
|
|
469
|
+
return { issueNumber: parseInt(match[1], 10), prUrl: pr.html_url };
|
|
470
|
+
}
|
|
471
|
+
async function getTaskPRDiff(config, prNumber) {
|
|
472
|
+
const octokit = createOctokit(config.githubToken);
|
|
473
|
+
const { owner, repo } = config.github;
|
|
474
|
+
const response = await octokit.pulls.get({
|
|
370
475
|
owner,
|
|
371
476
|
repo,
|
|
372
|
-
pull_number:
|
|
373
|
-
|
|
477
|
+
pull_number: prNumber,
|
|
478
|
+
mediaType: { format: "diff" }
|
|
374
479
|
});
|
|
375
|
-
|
|
376
|
-
|
|
480
|
+
return response.data;
|
|
481
|
+
}
|
|
482
|
+
async function acceptTask(config, issueNumber) {
|
|
483
|
+
const octokit = createOctokit(config.githubToken);
|
|
484
|
+
const { owner, repo } = config.github;
|
|
485
|
+
const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
|
|
486
|
+
const pr = prs.find(
|
|
487
|
+
(p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
|
|
488
|
+
);
|
|
489
|
+
if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
|
|
490
|
+
try {
|
|
491
|
+
const { data: merge } = await octokit.pulls.merge({
|
|
492
|
+
owner,
|
|
493
|
+
repo,
|
|
494
|
+
pull_number: pr.number,
|
|
495
|
+
merge_method: "merge"
|
|
496
|
+
});
|
|
497
|
+
await closeTask(config, issueNumber);
|
|
498
|
+
return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "", baseBranch: pr.base.ref };
|
|
499
|
+
} catch (err) {
|
|
500
|
+
if (err.status === 405) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`PR #${pr.number} cannot be merged \u2014 may have conflicts or is not in a mergeable state.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
377
507
|
}
|
|
378
|
-
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER;
|
|
508
|
+
var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER, TARGET_BRANCH_MARKER;
|
|
379
509
|
var init_github = __esm({
|
|
380
510
|
"src/lib/github.ts"() {
|
|
381
511
|
"use strict";
|
|
@@ -392,6 +522,7 @@ var init_github = __esm({
|
|
|
392
522
|
];
|
|
393
523
|
TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
|
|
394
524
|
BASE_COMMIT_MARKER = "<!-- techunter-base:";
|
|
525
|
+
TARGET_BRANCH_MARKER = "<!-- techunter-target:";
|
|
395
526
|
}
|
|
396
527
|
});
|
|
397
528
|
|
|
@@ -428,6 +559,17 @@ function makeWorkerBranchName(username) {
|
|
|
428
559
|
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
429
560
|
return `worker-${slug}`;
|
|
430
561
|
}
|
|
562
|
+
function makeTaskBranchName(issueNumber, username) {
|
|
563
|
+
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
564
|
+
return `task-${issueNumber}-${slug}`;
|
|
565
|
+
}
|
|
566
|
+
function isTaskBranch(branch) {
|
|
567
|
+
return /^task-\d+-/.test(branch);
|
|
568
|
+
}
|
|
569
|
+
function parseIssueNumberFromBranch(branch) {
|
|
570
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
571
|
+
return match ? parseInt(match[1], 10) : null;
|
|
572
|
+
}
|
|
431
573
|
async function getCurrentCommit() {
|
|
432
574
|
return (await git.revparse(["HEAD"])).trim();
|
|
433
575
|
}
|
|
@@ -548,16 +690,27 @@ async function getRemoteHeadSha(baseBranch) {
|
|
|
548
690
|
await git.fetch("origin", baseBranch);
|
|
549
691
|
return (await git.revparse([`origin/${baseBranch}`])).trim();
|
|
550
692
|
}
|
|
551
|
-
async function
|
|
552
|
-
const branches = await git.branch();
|
|
553
|
-
const
|
|
554
|
-
|
|
693
|
+
async function checkoutFromCommit(branchName, sha) {
|
|
694
|
+
const branches = await git.branch(["-a"]);
|
|
695
|
+
const exists = Object.keys(branches.branches).some(
|
|
696
|
+
(b) => b === branchName || b === `remotes/origin/${branchName}`
|
|
697
|
+
);
|
|
698
|
+
if (exists) {
|
|
555
699
|
await git.checkout(branchName);
|
|
556
|
-
await git.reset(["--hard", sha]);
|
|
557
700
|
} else {
|
|
558
701
|
await git.checkoutBranch(branchName, sha);
|
|
559
702
|
}
|
|
560
703
|
}
|
|
704
|
+
async function hasUncommittedChanges() {
|
|
705
|
+
const status = await git.status();
|
|
706
|
+
return !status.isClean();
|
|
707
|
+
}
|
|
708
|
+
async function stash(message) {
|
|
709
|
+
await git.stash(["push", "-u", "-m", message]);
|
|
710
|
+
}
|
|
711
|
+
async function stashPop() {
|
|
712
|
+
await git.stash(["pop"]);
|
|
713
|
+
}
|
|
561
714
|
|
|
562
715
|
// src/tools/pick/index.ts
|
|
563
716
|
init_github();
|
|
@@ -578,7 +731,8 @@ var configSchema = z.object({
|
|
|
578
731
|
}),
|
|
579
732
|
taskState: z.object({
|
|
580
733
|
activeIssueNumber: z.number().optional(),
|
|
581
|
-
baseCommit: z.string().optional()
|
|
734
|
+
baseCommit: z.string().optional(),
|
|
735
|
+
activeBranch: z.string().optional()
|
|
582
736
|
}).optional()
|
|
583
737
|
});
|
|
584
738
|
var store = new Conf({
|
|
@@ -665,11 +819,22 @@ function colorStatus(status) {
|
|
|
665
819
|
return padded;
|
|
666
820
|
}
|
|
667
821
|
}
|
|
822
|
+
function parentIssueFromBranch(branch) {
|
|
823
|
+
if (!isTaskBranch(branch)) return null;
|
|
824
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
825
|
+
return match ? parseInt(match[1], 10) : null;
|
|
826
|
+
}
|
|
827
|
+
function getParentIssueNumber(issue) {
|
|
828
|
+
const target = extractTargetBranch(issue.body);
|
|
829
|
+
if (!target) return null;
|
|
830
|
+
return parentIssueFromBranch(target);
|
|
831
|
+
}
|
|
668
832
|
function printTaskDetail(issue) {
|
|
669
833
|
const divider = chalk2.dim("\u2500".repeat(70));
|
|
834
|
+
const parentNum = getParentIssueNumber(issue);
|
|
670
835
|
console.log("\n" + divider);
|
|
671
836
|
console.log(
|
|
672
|
-
chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014")
|
|
837
|
+
chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014") + (parentNum ? chalk2.dim(` sub-task of #${parentNum}`) : "")
|
|
673
838
|
);
|
|
674
839
|
console.log(chalk2.bold("\n " + issue.title));
|
|
675
840
|
if (issue.body) {
|
|
@@ -689,12 +854,34 @@ async function printTaskList(config) {
|
|
|
689
854
|
if (tasks.length === 0) {
|
|
690
855
|
console.log(chalk2.dim(" (no tasks)"));
|
|
691
856
|
} else {
|
|
692
|
-
|
|
857
|
+
let printTask2 = function(t, indent, connector, isLast) {
|
|
693
858
|
const num = `#${t.number}`.padEnd(5);
|
|
694
859
|
const status = colorStatus(getStatus(t));
|
|
695
860
|
const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
|
|
696
|
-
const
|
|
697
|
-
|
|
861
|
+
const fullPrefix = indent + connector;
|
|
862
|
+
const maxTitle = 36 - fullPrefix.length;
|
|
863
|
+
const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
|
|
864
|
+
console.log(` ${num}${status}${assignee}${chalk2.dim(fullPrefix)}${title}`);
|
|
865
|
+
const children = childrenOf.get(t.number) ?? [];
|
|
866
|
+
const childIndent = indent + (isLast ? " " : "\u2502 ");
|
|
867
|
+
for (let i = 0; i < children.length; i++) {
|
|
868
|
+
const childIsLast = i === children.length - 1;
|
|
869
|
+
printTask2(children[i], childIndent, childIsLast ? "\u2514\u2500 " : "\u251C\u2500 ", childIsLast);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
var printTask = printTask2;
|
|
873
|
+
const taskMap = new Map(tasks.map((t) => [t.number, t]));
|
|
874
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
875
|
+
for (const t of tasks) {
|
|
876
|
+
const parentNum = getParentIssueNumber(t);
|
|
877
|
+
const key = parentNum !== null && taskMap.has(parentNum) ? parentNum : null;
|
|
878
|
+
if (!childrenOf.has(key)) childrenOf.set(key, []);
|
|
879
|
+
childrenOf.get(key).push(t);
|
|
880
|
+
}
|
|
881
|
+
const roots = childrenOf.get(null) ?? [];
|
|
882
|
+
for (let i = 0; i < roots.length; i++) {
|
|
883
|
+
const isLast = i === roots.length - 1;
|
|
884
|
+
printTask2(roots[i], "", isLast ? "\u2514\u2500 " : "\u251C\u2500 ", isLast);
|
|
698
885
|
}
|
|
699
886
|
}
|
|
700
887
|
console.log(divider);
|
|
@@ -852,7 +1039,7 @@ ${issue.body ?? "(none)"}
|
|
|
852
1039
|
|
|
853
1040
|
Diff:
|
|
854
1041
|
${diff || "(no changes)"}`,
|
|
855
|
-
["run_command", "
|
|
1042
|
+
["run_command", "grep_code", "get_diff"]
|
|
856
1043
|
);
|
|
857
1044
|
}
|
|
858
1045
|
|
|
@@ -873,9 +1060,19 @@ var definition = {
|
|
|
873
1060
|
};
|
|
874
1061
|
async function run(_input, config) {
|
|
875
1062
|
const taskState = getConfig().taskState;
|
|
876
|
-
const
|
|
1063
|
+
const currentBranch = await getCurrentBranch();
|
|
1064
|
+
let issueNumber = taskState?.activeIssueNumber && taskState?.activeBranch && currentBranch === taskState.activeBranch ? taskState.activeIssueNumber : void 0;
|
|
877
1065
|
if (!issueNumber) {
|
|
878
|
-
|
|
1066
|
+
const fromBranch = parseIssueNumberFromBranch(currentBranch);
|
|
1067
|
+
if (fromBranch) {
|
|
1068
|
+
issueNumber = fromBranch;
|
|
1069
|
+
} else {
|
|
1070
|
+
const found = await getIssueNumberFromBranch(config, currentBranch);
|
|
1071
|
+
if (!found) {
|
|
1072
|
+
return "No active task found. Claim a task first with /pick.";
|
|
1073
|
+
}
|
|
1074
|
+
issueNumber = found.issueNumber;
|
|
1075
|
+
}
|
|
879
1076
|
}
|
|
880
1077
|
let spinner = ora("Loading task and diff\u2026").start();
|
|
881
1078
|
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
|
|
@@ -885,9 +1082,16 @@ async function run(_input, config) {
|
|
|
885
1082
|
getAuthenticatedUser(config)
|
|
886
1083
|
]);
|
|
887
1084
|
spinner.stop();
|
|
888
|
-
const
|
|
1085
|
+
const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
|
|
889
1086
|
const branch = await getCurrentBranch();
|
|
890
1087
|
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
1088
|
+
spinner = ora("Checking for open sub-tasks\u2026").start();
|
|
1089
|
+
const openSubtaskNumbers = await getOpenSubtasks(config, branch);
|
|
1090
|
+
spinner.stop();
|
|
1091
|
+
if (openSubtaskNumbers.length > 0) {
|
|
1092
|
+
return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open:
|
|
1093
|
+
` + openSubtaskNumbers.map((n) => ` - #${n}`).join("\n") + "\nComplete all sub-tasks before submitting.";
|
|
1094
|
+
}
|
|
891
1095
|
let review = "";
|
|
892
1096
|
if (!isSelfSubmit) {
|
|
893
1097
|
const reviewSpinner = ora("Reviewing changes\u2026").start();
|
|
@@ -948,26 +1152,35 @@ async function run(_input, config) {
|
|
|
948
1152
|
spinner.stop();
|
|
949
1153
|
console.error(chalk5.yellow(`Warning: failed to close issue: ${err.message}`));
|
|
950
1154
|
}
|
|
951
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1155
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
952
1156
|
return `Task #${issueNumber} committed and closed.
|
|
953
1157
|
Commit: "${commitMessage.trim()}"`;
|
|
954
1158
|
}
|
|
955
|
-
spinner = ora("
|
|
1159
|
+
spinner = ora("Checking for existing PR\u2026").start();
|
|
1160
|
+
const existingPR = await getTaskPR(config, issueNumber);
|
|
1161
|
+
spinner.stop();
|
|
956
1162
|
let prUrl;
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1163
|
+
if (existingPR) {
|
|
1164
|
+
prUrl = existingPR.url;
|
|
1165
|
+
console.log(chalk5.dim(` Existing PR found: ${prUrl} \u2014 updating.`));
|
|
1166
|
+
} else {
|
|
1167
|
+
spinner = ora("Creating pull request\u2026").start();
|
|
1168
|
+
try {
|
|
1169
|
+
await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
|
|
1170
|
+
const prBody = [
|
|
1171
|
+
`Closes #${issueNumber}`,
|
|
1172
|
+
issue.body ? `
|
|
961
1173
|
${issue.body}` : "",
|
|
962
|
-
|
|
1174
|
+
review ? `
|
|
963
1175
|
## AI Review
|
|
964
1176
|
${review}` : ""
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1177
|
+
].join("\n").trim();
|
|
1178
|
+
prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
|
|
1179
|
+
spinner.stop();
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
spinner.stop();
|
|
1182
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1183
|
+
}
|
|
971
1184
|
}
|
|
972
1185
|
spinner = ora("Marking as in-review\u2026").start();
|
|
973
1186
|
try {
|
|
@@ -975,17 +1188,27 @@ ${review}` : ""
|
|
|
975
1188
|
spinner.stop();
|
|
976
1189
|
} catch (err) {
|
|
977
1190
|
spinner.stop();
|
|
978
|
-
return `PR created (${prUrl}) but failed to update label: ${err.message}`;
|
|
1191
|
+
return `PR ${existingPR ? "updated" : "created"} (${prUrl}) but failed to update label: ${err.message}`;
|
|
979
1192
|
}
|
|
980
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
981
|
-
return `Task #${issueNumber} submitted.
|
|
1193
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1194
|
+
return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
|
|
982
1195
|
Commit: "${commitMessage.trim()}"
|
|
983
1196
|
PR: ${prUrl}`;
|
|
984
1197
|
}
|
|
985
1198
|
async function execute(input, config) {
|
|
986
1199
|
const taskState = getConfig().taskState;
|
|
987
|
-
|
|
988
|
-
if (!issueNumber)
|
|
1200
|
+
let issueNumber = taskState?.activeIssueNumber;
|
|
1201
|
+
if (!issueNumber) {
|
|
1202
|
+
const currentBranch = await getCurrentBranch();
|
|
1203
|
+
const fromBranch = parseIssueNumberFromBranch(currentBranch);
|
|
1204
|
+
if (fromBranch) {
|
|
1205
|
+
issueNumber = fromBranch;
|
|
1206
|
+
} else {
|
|
1207
|
+
const found = await getIssueNumberFromBranch(config, currentBranch);
|
|
1208
|
+
if (!found) return "No active task found. Claim a task first.";
|
|
1209
|
+
issueNumber = found.issueNumber;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
989
1212
|
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
|
|
990
1213
|
const [issue, diff, branch, me] = await Promise.all([
|
|
991
1214
|
getTask(config, issueNumber),
|
|
@@ -993,7 +1216,11 @@ async function execute(input, config) {
|
|
|
993
1216
|
getCurrentBranch(),
|
|
994
1217
|
getAuthenticatedUser(config)
|
|
995
1218
|
]);
|
|
996
|
-
const
|
|
1219
|
+
const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
|
|
1220
|
+
const openSubtaskNumbers = await getOpenSubtasks(config, branch);
|
|
1221
|
+
if (openSubtaskNumbers.length > 0) {
|
|
1222
|
+
return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open: ` + openSubtaskNumbers.map((n) => `#${n}`).join(", ");
|
|
1223
|
+
}
|
|
997
1224
|
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
998
1225
|
let review = "";
|
|
999
1226
|
if (!isSelfSubmit) {
|
|
@@ -1014,29 +1241,36 @@ async function execute(input, config) {
|
|
|
1014
1241
|
await closeTask(config, issueNumber);
|
|
1015
1242
|
} catch {
|
|
1016
1243
|
}
|
|
1017
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1244
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1018
1245
|
return `Task #${issueNumber} committed and closed.
|
|
1019
1246
|
Commit: "${commitMessage}"`;
|
|
1020
1247
|
}
|
|
1248
|
+
const existingPR = await getTaskPR(config, issueNumber);
|
|
1021
1249
|
let prUrl;
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1250
|
+
if (existingPR) {
|
|
1251
|
+
prUrl = existingPR.url;
|
|
1252
|
+
} else {
|
|
1253
|
+
try {
|
|
1254
|
+
await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
|
|
1255
|
+
const prBody = [
|
|
1256
|
+
`Closes #${issueNumber}`,
|
|
1257
|
+
issue.body ? `
|
|
1026
1258
|
${issue.body}` : "",
|
|
1027
|
-
|
|
1259
|
+
review ? `
|
|
1028
1260
|
## AI Review
|
|
1029
1261
|
${review}` : ""
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1262
|
+
].join("\n").trim();
|
|
1263
|
+
prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1266
|
+
}
|
|
1034
1267
|
}
|
|
1035
1268
|
try {
|
|
1036
1269
|
await markInReview(config, issueNumber);
|
|
1037
1270
|
} catch {
|
|
1038
1271
|
}
|
|
1039
|
-
|
|
1272
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1273
|
+
return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
|
|
1040
1274
|
Review:
|
|
1041
1275
|
${review}
|
|
1042
1276
|
Commit: "${commitMessage}"
|
|
@@ -1195,8 +1429,23 @@ async function run3(input, config) {
|
|
|
1195
1429
|
}
|
|
1196
1430
|
}
|
|
1197
1431
|
const actions = [];
|
|
1198
|
-
if (status === "available")
|
|
1199
|
-
|
|
1432
|
+
if (status === "available") {
|
|
1433
|
+
actions.push({ name: "Claim this task", value: "claim" });
|
|
1434
|
+
}
|
|
1435
|
+
if (status === "claimed") {
|
|
1436
|
+
actions.push({ name: "Submit this task", value: "submit" });
|
|
1437
|
+
}
|
|
1438
|
+
if (status === "changes-needed") {
|
|
1439
|
+
const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1440
|
+
const me = await getAuthenticatedUser2(config);
|
|
1441
|
+
const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
|
|
1442
|
+
const currentBranch = await getCurrentBranch();
|
|
1443
|
+
if (currentBranch === taskBranch) {
|
|
1444
|
+
actions.push({ name: "Submit this task (fixes done)", value: "submit" });
|
|
1445
|
+
} else {
|
|
1446
|
+
actions.push({ name: `Switch to ${taskBranch} to fix`, value: "switch-fix" });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1200
1449
|
actions.push({ name: "Close this task", value: "close" });
|
|
1201
1450
|
actions.push({ name: "Nothing, just viewing", value: "none" });
|
|
1202
1451
|
let action;
|
|
@@ -1208,9 +1457,8 @@ async function run3(input, config) {
|
|
|
1208
1457
|
if (action === "none") return `Viewed task #${issue.number}.`;
|
|
1209
1458
|
if (action === "claim") {
|
|
1210
1459
|
try {
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1213
|
-
const myTasks = await listMyTasks2(config, me);
|
|
1460
|
+
const me = await getAuthenticatedUser(config);
|
|
1461
|
+
const myTasks = await listMyTasks(config, me);
|
|
1214
1462
|
const activeTask = myTasks.find((t) => {
|
|
1215
1463
|
const labels = t.labels;
|
|
1216
1464
|
return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
|
|
@@ -1219,33 +1467,61 @@ async function run3(input, config) {
|
|
|
1219
1467
|
return `You already have an active task: #${activeTask.number} "${activeTask.title}"
|
|
1220
1468
|
Finish or submit it before claiming a new one.`;
|
|
1221
1469
|
}
|
|
1470
|
+
let stashed = false;
|
|
1471
|
+
if (await hasUncommittedChanges()) {
|
|
1472
|
+
let choice;
|
|
1473
|
+
try {
|
|
1474
|
+
choice = await select3({
|
|
1475
|
+
message: "You have uncommitted changes. What would you like to do?",
|
|
1476
|
+
choices: [
|
|
1477
|
+
{ name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
|
|
1478
|
+
{ name: "Cancel", value: "cancel" }
|
|
1479
|
+
]
|
|
1480
|
+
});
|
|
1481
|
+
} catch {
|
|
1482
|
+
choice = "cancel";
|
|
1483
|
+
}
|
|
1484
|
+
if (choice === "cancel") return "Cancelled.";
|
|
1485
|
+
await stash(`tch: before claiming #${issue.number}`);
|
|
1486
|
+
stashed = true;
|
|
1487
|
+
console.log(chalk6.dim(" Changes stashed. Run `git stash pop` after you finish this task to restore them."));
|
|
1488
|
+
}
|
|
1222
1489
|
let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
|
|
1223
1490
|
await claimTask(config, issue.number, me);
|
|
1224
1491
|
spinner.stop();
|
|
1225
|
-
const
|
|
1492
|
+
const taskBranch = makeTaskBranchName(issue.number, me);
|
|
1226
1493
|
const taskBase = extractBaseCommit(issue.body);
|
|
1227
|
-
spinner = ora3(`
|
|
1494
|
+
spinner = ora3(`Creating branch ${taskBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
|
|
1228
1495
|
try {
|
|
1229
1496
|
if (taskBase) {
|
|
1230
|
-
await
|
|
1497
|
+
await checkoutFromCommit(taskBranch, taskBase);
|
|
1231
1498
|
} else {
|
|
1232
|
-
await switchToBranchOrCreate(
|
|
1499
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1233
1500
|
}
|
|
1234
1501
|
spinner.stop();
|
|
1235
|
-
spinner = ora3("Pushing
|
|
1502
|
+
spinner = ora3("Pushing task branch\u2026").start();
|
|
1236
1503
|
try {
|
|
1237
|
-
await pushBranch(
|
|
1504
|
+
await pushBranch(taskBranch);
|
|
1238
1505
|
spinner.stop();
|
|
1239
1506
|
} catch {
|
|
1240
|
-
spinner.warn("Could not push
|
|
1507
|
+
spinner.warn("Could not push task branch \u2014 will push on submit");
|
|
1241
1508
|
}
|
|
1242
|
-
} catch {
|
|
1243
|
-
spinner.warn(`Could not switch to ${
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
spinner.warn(`Could not switch to ${taskBranch}`);
|
|
1511
|
+
if (stashed) {
|
|
1512
|
+
try {
|
|
1513
|
+
await stashPop();
|
|
1514
|
+
console.log(chalk6.dim(" Restored stashed changes."));
|
|
1515
|
+
} catch {
|
|
1516
|
+
console.log(chalk6.yellow(" Warning: could not restore stash automatically. Run `git stash pop` manually."));
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
throw err;
|
|
1244
1520
|
}
|
|
1245
1521
|
const baseCommit = await getCurrentCommit();
|
|
1246
|
-
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
|
|
1522
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
|
|
1247
1523
|
console.log(chalk6.green(`
|
|
1248
|
-
Claimed! Branch: ${
|
|
1524
|
+
Claimed! Branch: ${taskBranch} (base: ${baseCommit.slice(0, 7)})
|
|
1249
1525
|
`));
|
|
1250
1526
|
let openClaude;
|
|
1251
1527
|
try {
|
|
@@ -1259,12 +1535,58 @@ Finish or submit it before claiming a new one.`;
|
|
|
1259
1535
|
} catch {
|
|
1260
1536
|
openClaude = false;
|
|
1261
1537
|
}
|
|
1262
|
-
if (openClaude) await launchClaudeCode(issue,
|
|
1263
|
-
return `Task #${issue.number} claimed. Branch: ${
|
|
1538
|
+
if (openClaude) await launchClaudeCode(issue, taskBranch);
|
|
1539
|
+
return `Task #${issue.number} claimed. Branch: ${taskBranch}`;
|
|
1264
1540
|
} catch (err) {
|
|
1265
1541
|
return `Error claiming task: ${err.message}`;
|
|
1266
1542
|
}
|
|
1267
1543
|
}
|
|
1544
|
+
if (action === "switch-fix") {
|
|
1545
|
+
const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1546
|
+
const me = await getAuthenticatedUser2(config);
|
|
1547
|
+
const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
|
|
1548
|
+
let stashed = false;
|
|
1549
|
+
if (await hasUncommittedChanges()) {
|
|
1550
|
+
let choice;
|
|
1551
|
+
try {
|
|
1552
|
+
choice = await select3({
|
|
1553
|
+
message: "You have uncommitted changes. What would you like to do?",
|
|
1554
|
+
choices: [
|
|
1555
|
+
{ name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
|
|
1556
|
+
{ name: "Cancel", value: "cancel" }
|
|
1557
|
+
]
|
|
1558
|
+
});
|
|
1559
|
+
} catch {
|
|
1560
|
+
choice = "cancel";
|
|
1561
|
+
}
|
|
1562
|
+
if (choice === "cancel") return "Cancelled.";
|
|
1563
|
+
await stash(`tch: before switching to ${taskBranch}`);
|
|
1564
|
+
stashed = true;
|
|
1565
|
+
console.log(chalk6.dim(" Changes stashed. Run `git stash pop` to restore them later."));
|
|
1566
|
+
}
|
|
1567
|
+
const spinner = ora3(`Switching to ${taskBranch}\u2026`).start();
|
|
1568
|
+
try {
|
|
1569
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1570
|
+
spinner.stop();
|
|
1571
|
+
} catch (err) {
|
|
1572
|
+
spinner.warn(`Could not switch to ${taskBranch}: ${err.message}`);
|
|
1573
|
+
if (stashed) {
|
|
1574
|
+
try {
|
|
1575
|
+
await stashPop();
|
|
1576
|
+
console.log(chalk6.dim(" Restored stashed changes."));
|
|
1577
|
+
} catch {
|
|
1578
|
+
console.log(chalk6.yellow(" Run `git stash pop` manually to restore your changes."));
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return `Error: ${err.message}`;
|
|
1582
|
+
}
|
|
1583
|
+
const baseCommit = extractBaseCommit(issue.body) ?? await getCurrentCommit();
|
|
1584
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
|
|
1585
|
+
console.log(chalk6.green(`
|
|
1586
|
+
Switched to ${taskBranch}. Fix the issues then run /submit.
|
|
1587
|
+
`));
|
|
1588
|
+
return `Switched to ${taskBranch} for task #${issue.number}.`;
|
|
1589
|
+
}
|
|
1268
1590
|
if (action === "submit") return run({}, config);
|
|
1269
1591
|
if (action === "close") return run2({ issue_number: issue.number }, config);
|
|
1270
1592
|
return "Cancelled.";
|
|
@@ -1293,28 +1615,31 @@ async function execute3(input, config) {
|
|
|
1293
1615
|
if (activeTask) {
|
|
1294
1616
|
return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
|
|
1295
1617
|
}
|
|
1618
|
+
if (await hasUncommittedChanges()) {
|
|
1619
|
+
return "Cannot claim: you have uncommitted changes. Commit or stash them first (git stash).";
|
|
1620
|
+
}
|
|
1296
1621
|
try {
|
|
1297
1622
|
await claimTask(config, issueNumber, me);
|
|
1298
1623
|
} catch (err) {
|
|
1299
1624
|
return `Error claiming task: ${err.message}`;
|
|
1300
1625
|
}
|
|
1301
|
-
const
|
|
1626
|
+
const taskBranch = makeTaskBranchName(issue.number, me);
|
|
1302
1627
|
const taskBase = extractBaseCommit(issue.body);
|
|
1303
1628
|
try {
|
|
1304
1629
|
if (taskBase) {
|
|
1305
|
-
await
|
|
1630
|
+
await checkoutFromCommit(taskBranch, taskBase);
|
|
1306
1631
|
} else {
|
|
1307
|
-
await switchToBranchOrCreate(
|
|
1632
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1308
1633
|
}
|
|
1309
1634
|
} catch {
|
|
1310
1635
|
}
|
|
1311
1636
|
try {
|
|
1312
|
-
await pushBranch(
|
|
1637
|
+
await pushBranch(taskBranch);
|
|
1313
1638
|
} catch {
|
|
1314
1639
|
}
|
|
1315
1640
|
const baseCommit = await getCurrentCommit();
|
|
1316
|
-
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
|
|
1317
|
-
return `Task #${issueNumber} claimed. Branch: ${
|
|
1641
|
+
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit, activeBranch: taskBranch } });
|
|
1642
|
+
return `Task #${issueNumber} claimed. Branch: ${taskBranch} (base commit: ${baseCommit.slice(0, 7)})`;
|
|
1318
1643
|
}
|
|
1319
1644
|
return `Unknown action: ${action}`;
|
|
1320
1645
|
}
|
|
@@ -1366,9 +1691,9 @@ Previous guide:
|
|
|
1366
1691
|
${revise.previousGuide}` : `Write an implementation guide for this task: "${title}"`;
|
|
1367
1692
|
return runSubAgentLoop(
|
|
1368
1693
|
config,
|
|
1369
|
-
"You are a senior engineer writing a brief task guide for a developer. Use
|
|
1694
|
+
"You are a senior engineer writing a brief task guide for a developer. Use list_files then grep_code to identify which files are relevant. Do NOT include code snippets or implementation details. When you have enough context, write the guide.\n\n" + GUIDE_FORMAT,
|
|
1370
1695
|
userMessage,
|
|
1371
|
-
["
|
|
1696
|
+
["list_files", "grep_code", "run_command", "ask_user"]
|
|
1372
1697
|
);
|
|
1373
1698
|
}
|
|
1374
1699
|
|
|
@@ -1389,6 +1714,77 @@ async function openInEditor(content) {
|
|
|
1389
1714
|
await rm(dir, { recursive: true, force: true });
|
|
1390
1715
|
}
|
|
1391
1716
|
}
|
|
1717
|
+
async function resolveBaseAndTarget(config, me, interactive) {
|
|
1718
|
+
const currentBranch = await getCurrentBranch();
|
|
1719
|
+
if (isTaskBranch(currentBranch)) {
|
|
1720
|
+
if (await hasUncommittedChanges()) {
|
|
1721
|
+
if (!interactive) {
|
|
1722
|
+
throw new Error("Cannot create sub-task: you have uncommitted changes. Commit them first so the executor starts from the correct base.");
|
|
1723
|
+
}
|
|
1724
|
+
const { select: inquirerSelect } = await import("@inquirer/prompts");
|
|
1725
|
+
let choice;
|
|
1726
|
+
try {
|
|
1727
|
+
choice = await inquirerSelect({
|
|
1728
|
+
message: "You have uncommitted changes. The sub-task executor will start from the last commit \u2014 they won't see your current unsaved work.",
|
|
1729
|
+
choices: [
|
|
1730
|
+
{ name: "Commit first (cancel and commit manually)", value: "cancel" },
|
|
1731
|
+
{ name: "Continue anyway (executor starts without my unsaved changes)", value: "continue" }
|
|
1732
|
+
]
|
|
1733
|
+
});
|
|
1734
|
+
} catch {
|
|
1735
|
+
choice = "cancel";
|
|
1736
|
+
}
|
|
1737
|
+
if (choice === "cancel") throw new Error("Cancelled. Commit your changes first, then create the sub-task.");
|
|
1738
|
+
}
|
|
1739
|
+
const baseCommit2 = await getCurrentCommit();
|
|
1740
|
+
return { baseCommit: baseCommit2, targetBranch: currentBranch, isSubtask: true };
|
|
1741
|
+
}
|
|
1742
|
+
let stashedForSync = false;
|
|
1743
|
+
if (await hasUncommittedChanges()) {
|
|
1744
|
+
if (!interactive) {
|
|
1745
|
+
throw new Error("Cannot create task: you have uncommitted changes. Commit or stash them first (git stash).");
|
|
1746
|
+
}
|
|
1747
|
+
const { select: inquirerSelect } = await import("@inquirer/prompts");
|
|
1748
|
+
let choice;
|
|
1749
|
+
try {
|
|
1750
|
+
choice = await inquirerSelect({
|
|
1751
|
+
message: "You have uncommitted changes. Syncing with main requires a clean working tree.",
|
|
1752
|
+
choices: [
|
|
1753
|
+
{ name: "Stash changes and continue (restore with: git stash pop)", value: "stash" },
|
|
1754
|
+
{ name: "Cancel", value: "cancel" }
|
|
1755
|
+
]
|
|
1756
|
+
});
|
|
1757
|
+
} catch {
|
|
1758
|
+
choice = "cancel";
|
|
1759
|
+
}
|
|
1760
|
+
if (choice === "cancel") throw new Error("Cancelled.");
|
|
1761
|
+
await stash("tch: before creating new task");
|
|
1762
|
+
stashedForSync = true;
|
|
1763
|
+
console.log(chalk7.dim(" Changes stashed. Run `git stash pop` after creating the task."));
|
|
1764
|
+
}
|
|
1765
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1766
|
+
let baseCommit;
|
|
1767
|
+
const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
|
|
1768
|
+
try {
|
|
1769
|
+
await syncWithBase(baseBranch);
|
|
1770
|
+
baseCommit = await getCurrentCommit();
|
|
1771
|
+
syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
|
|
1772
|
+
} catch {
|
|
1773
|
+
syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
|
|
1774
|
+
try {
|
|
1775
|
+
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1776
|
+
} catch {
|
|
1777
|
+
}
|
|
1778
|
+
if (stashedForSync) {
|
|
1779
|
+
try {
|
|
1780
|
+
await stashPop();
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
throw new Error(`Could not sync with ${baseBranch}. Your changes have been restored from stash.`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return { baseCommit, targetBranch: makeWorkerBranchName(me), isSubtask: false };
|
|
1787
|
+
}
|
|
1392
1788
|
var definition4 = {
|
|
1393
1789
|
type: "function",
|
|
1394
1790
|
function: {
|
|
@@ -1484,26 +1880,23 @@ async function run4(input, config) {
|
|
|
1484
1880
|
console.log(chalk7.yellow(` Revision error: ${err.message}`));
|
|
1485
1881
|
}
|
|
1486
1882
|
}
|
|
1487
|
-
const baseBranch = config.baseBranch ?? "main";
|
|
1488
1883
|
let baseCommit;
|
|
1489
|
-
|
|
1884
|
+
let targetBranch;
|
|
1885
|
+
let isSubtask;
|
|
1490
1886
|
try {
|
|
1491
|
-
await
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1498
|
-
} catch {
|
|
1499
|
-
}
|
|
1887
|
+
({ baseCommit, targetBranch, isSubtask } = await resolveBaseAndTarget(config, me, true));
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
return err.message;
|
|
1890
|
+
}
|
|
1891
|
+
if (isSubtask) {
|
|
1892
|
+
console.log(chalk7.dim(` Sub-task: will target branch ${chalk7.cyan(targetBranch)} (base: ${baseCommit?.slice(0, 7) ?? "HEAD"})`));
|
|
1500
1893
|
}
|
|
1501
1894
|
const createSpinner = ora4(`Creating "${title}"\u2026`).start();
|
|
1502
1895
|
let htmlUrl;
|
|
1503
1896
|
let issueNumber;
|
|
1504
1897
|
let issueTitle;
|
|
1505
1898
|
try {
|
|
1506
|
-
const issue = await createTask(config, title, guide, baseCommit);
|
|
1899
|
+
const issue = await createTask(config, title, guide, baseCommit, targetBranch);
|
|
1507
1900
|
createSpinner.stop();
|
|
1508
1901
|
htmlUrl = issue.htmlUrl;
|
|
1509
1902
|
issueNumber = issue.number;
|
|
@@ -1540,19 +1933,9 @@ async function execute4(input, config) {
|
|
|
1540
1933
|
if (feedback) {
|
|
1541
1934
|
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1542
1935
|
}
|
|
1543
|
-
const
|
|
1544
|
-
let baseCommit;
|
|
1545
|
-
try {
|
|
1546
|
-
await syncWithBase(baseBranch);
|
|
1547
|
-
baseCommit = await getCurrentCommit();
|
|
1548
|
-
} catch {
|
|
1549
|
-
try {
|
|
1550
|
-
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1551
|
-
} catch {
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1936
|
+
const { baseCommit, targetBranch } = await resolveBaseAndTarget(config, me, false);
|
|
1554
1937
|
try {
|
|
1555
|
-
const issue = await createTask(config, title, guide, baseCommit);
|
|
1938
|
+
const issue = await createTask(config, title, guide, baseCommit, targetBranch);
|
|
1556
1939
|
return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
|
|
1557
1940
|
|
|
1558
1941
|
Guide:
|
|
@@ -1602,119 +1985,249 @@ var terminal5 = true;
|
|
|
1602
1985
|
// src/tools/review/index.ts
|
|
1603
1986
|
var review_exports = {};
|
|
1604
1987
|
__export(review_exports, {
|
|
1988
|
+
definition: () => definition8,
|
|
1989
|
+
execute: () => execute8,
|
|
1990
|
+
run: () => run8,
|
|
1991
|
+
terminal: () => terminal8
|
|
1992
|
+
});
|
|
1993
|
+
init_github();
|
|
1994
|
+
import chalk10 from "chalk";
|
|
1995
|
+
import ora8 from "ora";
|
|
1996
|
+
import { select as select7 } from "@inquirer/prompts";
|
|
1997
|
+
|
|
1998
|
+
// src/tools/accept/index.ts
|
|
1999
|
+
var accept_exports = {};
|
|
2000
|
+
__export(accept_exports, {
|
|
1605
2001
|
definition: () => definition6,
|
|
1606
2002
|
execute: () => execute6,
|
|
1607
2003
|
run: () => run6,
|
|
1608
2004
|
terminal: () => terminal6
|
|
1609
2005
|
});
|
|
1610
2006
|
init_github();
|
|
2007
|
+
import chalk8 from "chalk";
|
|
2008
|
+
import { select as select5 } from "@inquirer/prompts";
|
|
1611
2009
|
import ora6 from "ora";
|
|
1612
|
-
var definition6 = {
|
|
1613
|
-
type: "function",
|
|
1614
|
-
function: {
|
|
1615
|
-
name: "review",
|
|
1616
|
-
description: "List tasks waiting for your review (submitted by others, created by you). Equivalent to /review.",
|
|
1617
|
-
parameters: { type: "object", properties: {}, required: [] }
|
|
1618
|
-
}
|
|
1619
|
-
};
|
|
1620
|
-
async function run6(_input, config) {
|
|
1621
|
-
const spinner = ora6("Loading tasks for review\u2026").start();
|
|
1622
|
-
try {
|
|
1623
|
-
const me = await getAuthenticatedUser(config);
|
|
1624
|
-
const tasks = await listTasksForReview(config, me);
|
|
1625
|
-
spinner.stop();
|
|
1626
|
-
if (tasks.length === 0) return `No tasks pending review for @${me}.`;
|
|
1627
|
-
const lines = tasks.map((t) => ` #${t.number} [in-review] @${t.assignee ?? "\u2014"} ${t.title}`);
|
|
1628
|
-
return `Tasks pending review (created by @${me}):
|
|
1629
|
-
${lines.join("\n")}`;
|
|
1630
|
-
} catch (err) {
|
|
1631
|
-
spinner.stop();
|
|
1632
|
-
return `Error: ${err.message}`;
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
var execute6 = run6;
|
|
1636
|
-
var terminal6 = true;
|
|
1637
2010
|
|
|
1638
|
-
// src/tools/
|
|
1639
|
-
var
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
2011
|
+
// src/tools/wiki/prompts.ts
|
|
2012
|
+
var WIKI_FORMAT = `
|
|
2013
|
+
The document you produce must be valid Markdown with these exact sections:
|
|
2014
|
+
|
|
2015
|
+
# [Project Name]
|
|
2016
|
+
|
|
2017
|
+
> One-sentence description of what this project does.
|
|
2018
|
+
|
|
2019
|
+
## What Is This?
|
|
2020
|
+
|
|
2021
|
+
2-4 paragraphs covering:
|
|
2022
|
+
- The problem this project solves
|
|
2023
|
+
- Who uses it and in what context
|
|
2024
|
+
- Core capabilities / key features
|
|
2025
|
+
|
|
2026
|
+
## Quick Start
|
|
2027
|
+
|
|
2028
|
+
Numbered steps for a brand-new developer to install, configure, and run the project for the first time.
|
|
2029
|
+
|
|
2030
|
+
## Architecture
|
|
2031
|
+
|
|
2032
|
+
High-level explanation of how the system is structured:
|
|
2033
|
+
- Key components / layers and their responsibilities
|
|
2034
|
+
- Request or data flow (prose or ASCII diagram)
|
|
2035
|
+
- Noteworthy design decisions
|
|
2036
|
+
|
|
2037
|
+
## Key Files
|
|
2038
|
+
|
|
2039
|
+
| File / Directory | Purpose |
|
|
2040
|
+
|---|---|
|
|
2041
|
+
| ... | ... |
|
|
2042
|
+
|
|
2043
|
+
(List the 8-15 most important files.)
|
|
2044
|
+
|
|
2045
|
+
## Development Workflow
|
|
2046
|
+
|
|
2047
|
+
Common day-to-day tasks a contributor will need:
|
|
2048
|
+
- How to build / run locally
|
|
2049
|
+
- How to add a new feature (brief steps)
|
|
2050
|
+
- Any testing or linting commands
|
|
2051
|
+
|
|
2052
|
+
---
|
|
2053
|
+
*Maintained by Techunter \u2014 run \`tch wiki\` to regenerate*
|
|
2054
|
+
`;
|
|
2055
|
+
|
|
2056
|
+
// src/tools/wiki/wiki-generator.ts
|
|
2057
|
+
async function generateWiki(config) {
|
|
2058
|
+
return runSubAgentLoop(
|
|
2059
|
+
config,
|
|
2060
|
+
"You are a senior engineer writing a project overview document for new team members. Use list_files to understand the project structure, then grep_code and run_command to read key files (e.g. package.json, README, entry points, config files). Be concrete and specific \u2014 reference real file names, commands, and concepts from this codebase. Avoid vague filler. When you have enough context, write the document.\n\n" + WIKI_FORMAT,
|
|
2061
|
+
"Analyze this project thoroughly and produce a comprehensive TECHUNTER.md overview document for new team members.",
|
|
2062
|
+
["list_files", "grep_code", "run_command"]
|
|
2063
|
+
);
|
|
1664
2064
|
}
|
|
1665
|
-
var execute7 = run7;
|
|
1666
|
-
var terminal7 = true;
|
|
1667
2065
|
|
|
1668
|
-
// src/tools/
|
|
1669
|
-
var
|
|
1670
|
-
__export(open_code_exports, {
|
|
1671
|
-
definition: () => definition8,
|
|
1672
|
-
execute: () => execute8,
|
|
1673
|
-
run: () => run8,
|
|
1674
|
-
terminal: () => terminal8
|
|
1675
|
-
});
|
|
1676
|
-
init_github();
|
|
1677
|
-
var definition8 = {
|
|
2066
|
+
// src/tools/accept/index.ts
|
|
2067
|
+
var definition6 = {
|
|
1678
2068
|
type: "function",
|
|
1679
2069
|
function: {
|
|
1680
|
-
name: "
|
|
1681
|
-
description: "
|
|
1682
|
-
parameters: {
|
|
2070
|
+
name: "accept",
|
|
2071
|
+
description: "Accept an in-review task: merges the PR into the target branch and closes the issue.",
|
|
2072
|
+
parameters: {
|
|
2073
|
+
type: "object",
|
|
2074
|
+
properties: {
|
|
2075
|
+
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
2076
|
+
},
|
|
2077
|
+
required: ["issue_number"]
|
|
2078
|
+
}
|
|
1683
2079
|
}
|
|
1684
2080
|
};
|
|
1685
|
-
async function
|
|
1686
|
-
let
|
|
2081
|
+
async function run6(input, config) {
|
|
2082
|
+
let issueNumber = input["issue_number"];
|
|
2083
|
+
if (!issueNumber) {
|
|
2084
|
+
const spinner3 = ora6("Loading tasks for review\u2026").start();
|
|
2085
|
+
let tasks;
|
|
2086
|
+
let me;
|
|
2087
|
+
try {
|
|
2088
|
+
me = await getAuthenticatedUser(config);
|
|
2089
|
+
tasks = await listTasksForReview(config, me);
|
|
2090
|
+
spinner3.stop();
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
spinner3.stop();
|
|
2093
|
+
return `Error: ${err.message}`;
|
|
2094
|
+
}
|
|
2095
|
+
if (tasks.length === 0) return "No tasks pending review.";
|
|
2096
|
+
try {
|
|
2097
|
+
issueNumber = await select5({
|
|
2098
|
+
message: "Which task to accept?",
|
|
2099
|
+
choices: tasks.map((t) => ({
|
|
2100
|
+
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2101
|
+
value: t.number
|
|
2102
|
+
}))
|
|
2103
|
+
});
|
|
2104
|
+
} catch {
|
|
2105
|
+
return "Cancelled.";
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const spinner2 = ora6("Verifying permissions\u2026").start();
|
|
2109
|
+
let me2;
|
|
2110
|
+
let issue;
|
|
1687
2111
|
try {
|
|
1688
|
-
|
|
2112
|
+
[me2, issue] = await Promise.all([
|
|
2113
|
+
getAuthenticatedUser(config),
|
|
2114
|
+
getTask(config, issueNumber)
|
|
2115
|
+
]);
|
|
2116
|
+
spinner2.stop();
|
|
1689
2117
|
} catch (err) {
|
|
2118
|
+
spinner2.stop();
|
|
1690
2119
|
return `Error: ${err.message}`;
|
|
1691
2120
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
let
|
|
2121
|
+
if (issue.author && issue.author !== me2) {
|
|
2122
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2123
|
+
}
|
|
2124
|
+
let confirmed;
|
|
1696
2125
|
try {
|
|
1697
|
-
|
|
2126
|
+
confirmed = await select5({
|
|
2127
|
+
message: `Merge PR for #${issueNumber} and close issue?`,
|
|
2128
|
+
choices: [
|
|
2129
|
+
{ name: "Yes, accept", value: true },
|
|
2130
|
+
{ name: "Cancel", value: false }
|
|
2131
|
+
]
|
|
2132
|
+
});
|
|
2133
|
+
} catch {
|
|
2134
|
+
return "Cancelled.";
|
|
2135
|
+
}
|
|
2136
|
+
if (!confirmed) return "Cancelled.";
|
|
2137
|
+
const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2138
|
+
let result;
|
|
2139
|
+
try {
|
|
2140
|
+
result = await acceptTask(config, issueNumber);
|
|
2141
|
+
spinner.succeed(`PR #${result.prNumber} merged \u2192 ${chalk8.cyan(result.baseBranch)}`);
|
|
1698
2142
|
} catch (err) {
|
|
2143
|
+
spinner.fail("Failed");
|
|
1699
2144
|
return `Error: ${err.message}`;
|
|
1700
2145
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
2146
|
+
const mergedIntoTaskBranch = isTaskBranch(result.baseBranch);
|
|
2147
|
+
if (!mergedIntoTaskBranch) {
|
|
2148
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
2149
|
+
let pushToMain;
|
|
2150
|
+
try {
|
|
2151
|
+
pushToMain = await select5({
|
|
2152
|
+
message: `Push ${chalk8.cyan(result.baseBranch)} \u2192 ${chalk8.cyan(baseBranch)}?`,
|
|
2153
|
+
choices: [
|
|
2154
|
+
{ name: `Yes, push to ${baseBranch}`, value: true },
|
|
2155
|
+
{ name: "No, keep in worker branch", value: false }
|
|
2156
|
+
]
|
|
2157
|
+
});
|
|
2158
|
+
} catch {
|
|
2159
|
+
pushToMain = false;
|
|
2160
|
+
}
|
|
2161
|
+
if (pushToMain) {
|
|
2162
|
+
const mergeSpinner = ora6(`Merging ${result.baseBranch} \u2192 ${baseBranch}\u2026`).start();
|
|
2163
|
+
try {
|
|
2164
|
+
await mergeWorkerIntoBase(config, result.baseBranch, baseBranch);
|
|
2165
|
+
mergeSpinner.succeed(`Merged ${result.baseBranch} \u2192 ${baseBranch}`);
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
let updateWiki = false;
|
|
2172
|
+
try {
|
|
2173
|
+
updateWiki = await select5({
|
|
2174
|
+
message: "Update TECHUNTER.md project overview?",
|
|
2175
|
+
choices: [
|
|
2176
|
+
{ name: "Yes, regenerate", value: true },
|
|
2177
|
+
{ name: "No, skip", value: false }
|
|
2178
|
+
]
|
|
2179
|
+
});
|
|
2180
|
+
} catch {
|
|
2181
|
+
}
|
|
2182
|
+
if (updateWiki) {
|
|
2183
|
+
const wikiSpinner = ora6("Regenerating TECHUNTER.md\u2026").start();
|
|
2184
|
+
try {
|
|
2185
|
+
const content = await generateWiki(config);
|
|
2186
|
+
await upsertRepoFile(config, "TECHUNTER.md", content, "docs: update TECHUNTER.md project overview");
|
|
2187
|
+
wikiSpinner.succeed("TECHUNTER.md updated");
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
wikiSpinner.fail(`Wiki update failed: ${err.message}`);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
const mergeTarget = mergedIntoTaskBranch ? `${result.baseBranch} (sub-task merged, no push to main)` : result.baseBranch;
|
|
2193
|
+
return `Task #${issueNumber} accepted.
|
|
2194
|
+
PR #${result.prNumber} merged \u2192 ${mergeTarget}
|
|
2195
|
+
Issue closed.`;
|
|
1703
2196
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
2197
|
+
async function execute6(input, config) {
|
|
2198
|
+
const issueNumber = input["issue_number"];
|
|
2199
|
+
const [me, issue] = await Promise.all([
|
|
2200
|
+
getAuthenticatedUser(config),
|
|
2201
|
+
getTask(config, issueNumber)
|
|
2202
|
+
]);
|
|
2203
|
+
if (issue.author && issue.author !== me) {
|
|
2204
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2205
|
+
}
|
|
2206
|
+
const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2207
|
+
try {
|
|
2208
|
+
const result = await acceptTask(config, issueNumber);
|
|
2209
|
+
spinner.stop();
|
|
2210
|
+
return `Task #${issueNumber} accepted.
|
|
2211
|
+
PR #${result.prNumber} merged \u2192 ${result.baseBranch}
|
|
2212
|
+
Issue closed.`;
|
|
2213
|
+
} catch (err) {
|
|
2214
|
+
spinner.stop();
|
|
2215
|
+
return `Error: ${err.message}`;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
var terminal6 = true;
|
|
1706
2219
|
|
|
1707
2220
|
// src/tools/reject/index.ts
|
|
1708
2221
|
var reject_exports = {};
|
|
1709
2222
|
__export(reject_exports, {
|
|
1710
|
-
definition: () =>
|
|
1711
|
-
execute: () =>
|
|
1712
|
-
run: () =>
|
|
1713
|
-
terminal: () =>
|
|
2223
|
+
definition: () => definition7,
|
|
2224
|
+
execute: () => execute7,
|
|
2225
|
+
run: () => run7,
|
|
2226
|
+
terminal: () => terminal7
|
|
1714
2227
|
});
|
|
1715
2228
|
init_github();
|
|
1716
|
-
import
|
|
1717
|
-
import { select as
|
|
2229
|
+
import chalk9 from "chalk";
|
|
2230
|
+
import { select as select6, input as promptInput3 } from "@inquirer/prompts";
|
|
1718
2231
|
import ora7 from "ora";
|
|
1719
2232
|
|
|
1720
2233
|
// src/tools/reject/prompts.ts
|
|
@@ -1738,15 +2251,15 @@ Clear instruction on what to fix and how to re-submit (via /submit).
|
|
|
1738
2251
|
async function generateRejectionComment(config, issueNumber, userFeedback) {
|
|
1739
2252
|
return runSubAgentLoop(
|
|
1740
2253
|
config,
|
|
1741
|
-
"You are a senior engineer writing a structured code review rejection comment. Use get_task to read the acceptance criteria, get_diff or
|
|
2254
|
+
"You are a senior engineer writing a structured code review rejection comment. Use get_task to read the acceptance criteria, get_diff or grep_code to inspect the implementation, and get_comments to see prior discussion. Then write the rejection comment.\n\n" + REJECTION_FORMAT,
|
|
1742
2255
|
`Write a rejection comment for issue #${issueNumber}.
|
|
1743
2256
|
Reviewer feedback: ${userFeedback}`,
|
|
1744
|
-
["get_task", "get_comments", "get_diff", "
|
|
2257
|
+
["get_task", "get_comments", "get_diff", "grep_code"]
|
|
1745
2258
|
);
|
|
1746
2259
|
}
|
|
1747
2260
|
|
|
1748
2261
|
// src/tools/reject/index.ts
|
|
1749
|
-
var
|
|
2262
|
+
var definition7 = {
|
|
1750
2263
|
type: "function",
|
|
1751
2264
|
function: {
|
|
1752
2265
|
name: "reject",
|
|
@@ -1761,7 +2274,7 @@ var definition9 = {
|
|
|
1761
2274
|
}
|
|
1762
2275
|
}
|
|
1763
2276
|
};
|
|
1764
|
-
async function
|
|
2277
|
+
async function run7(input, config) {
|
|
1765
2278
|
const issueNumber = input["issue_number"];
|
|
1766
2279
|
const [me, issue] = await Promise.all([
|
|
1767
2280
|
getAuthenticatedUser(config),
|
|
@@ -1779,7 +2292,7 @@ async function run9(input, config) {
|
|
|
1779
2292
|
return "Cancelled.";
|
|
1780
2293
|
}
|
|
1781
2294
|
if (!feedback.trim()) return "Cancelled.";
|
|
1782
|
-
const divider =
|
|
2295
|
+
const divider = chalk9.dim("\u2500".repeat(70));
|
|
1783
2296
|
for (; ; ) {
|
|
1784
2297
|
const spinner = ora7("Generating rejection comment\u2026").start();
|
|
1785
2298
|
let comment;
|
|
@@ -1791,13 +2304,13 @@ async function run9(input, config) {
|
|
|
1791
2304
|
return `Error generating comment: ${err.message}`;
|
|
1792
2305
|
}
|
|
1793
2306
|
console.log("\n" + divider);
|
|
1794
|
-
console.log(
|
|
2307
|
+
console.log(chalk9.bold(` Rejection preview \u2014 issue #${issueNumber}`));
|
|
1795
2308
|
console.log(divider);
|
|
1796
2309
|
console.log(renderMarkdown(comment));
|
|
1797
2310
|
console.log(divider + "\n");
|
|
1798
2311
|
let decision;
|
|
1799
2312
|
try {
|
|
1800
|
-
decision = await
|
|
2313
|
+
decision = await select6({
|
|
1801
2314
|
message: `Post rejection and mark #${issueNumber} as changes-needed?`,
|
|
1802
2315
|
choices: [
|
|
1803
2316
|
{ name: "Post & Reject", value: "yes" },
|
|
@@ -1836,7 +2349,7 @@ async function run9(input, config) {
|
|
|
1836
2349
|
return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
|
|
1837
2350
|
}
|
|
1838
2351
|
}
|
|
1839
|
-
async function
|
|
2352
|
+
async function execute7(input, config) {
|
|
1840
2353
|
const issueNumber = input["issue_number"];
|
|
1841
2354
|
const feedback = input["feedback"];
|
|
1842
2355
|
const [me, issue] = await Promise.all([
|
|
@@ -1867,150 +2380,181 @@ async function execute9(input, config) {
|
|
|
1867
2380
|
Comment posted:
|
|
1868
2381
|
${comment}`;
|
|
1869
2382
|
}
|
|
1870
|
-
var
|
|
2383
|
+
var terminal7 = true;
|
|
1871
2384
|
|
|
1872
|
-
// src/tools/
|
|
1873
|
-
var
|
|
1874
|
-
__export(accept_exports, {
|
|
1875
|
-
definition: () => definition10,
|
|
1876
|
-
execute: () => execute10,
|
|
1877
|
-
run: () => run10,
|
|
1878
|
-
terminal: () => terminal10
|
|
1879
|
-
});
|
|
1880
|
-
init_github();
|
|
1881
|
-
import chalk9 from "chalk";
|
|
1882
|
-
import { select as select6 } from "@inquirer/prompts";
|
|
1883
|
-
import ora8 from "ora";
|
|
1884
|
-
var definition10 = {
|
|
2385
|
+
// src/tools/review/index.ts
|
|
2386
|
+
var definition8 = {
|
|
1885
2387
|
type: "function",
|
|
1886
2388
|
function: {
|
|
1887
|
-
name: "
|
|
1888
|
-
description: "
|
|
1889
|
-
parameters: {
|
|
1890
|
-
type: "object",
|
|
1891
|
-
properties: {
|
|
1892
|
-
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
1893
|
-
},
|
|
1894
|
-
required: ["issue_number"]
|
|
1895
|
-
}
|
|
2389
|
+
name: "review",
|
|
2390
|
+
description: "List tasks waiting for your review (submitted by others, created by you), then let you accept or reject one. Equivalent to /review.",
|
|
2391
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1896
2392
|
}
|
|
1897
2393
|
};
|
|
1898
|
-
async function
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
let tasks;
|
|
1903
|
-
let me;
|
|
1904
|
-
try {
|
|
1905
|
-
me = await getAuthenticatedUser(config);
|
|
1906
|
-
tasks = await listTasksForReview(config, me);
|
|
1907
|
-
spinner3.stop();
|
|
1908
|
-
} catch (err) {
|
|
1909
|
-
spinner3.stop();
|
|
1910
|
-
return `Error: ${err.message}`;
|
|
1911
|
-
}
|
|
1912
|
-
if (tasks.length === 0) return "No tasks pending review.";
|
|
1913
|
-
try {
|
|
1914
|
-
issueNumber = await select6({
|
|
1915
|
-
message: "Which task to accept?",
|
|
1916
|
-
choices: tasks.map((t) => ({
|
|
1917
|
-
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
1918
|
-
value: t.number
|
|
1919
|
-
}))
|
|
1920
|
-
});
|
|
1921
|
-
} catch {
|
|
1922
|
-
return "Cancelled.";
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
const spinner2 = ora8("Verifying permissions\u2026").start();
|
|
1926
|
-
let me2;
|
|
1927
|
-
let issue;
|
|
2394
|
+
async function run8(_input, config) {
|
|
2395
|
+
const spinner = ora8("Loading tasks for review\u2026").start();
|
|
2396
|
+
let me;
|
|
2397
|
+
let tasks;
|
|
1928
2398
|
try {
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
]);
|
|
1933
|
-
spinner2.stop();
|
|
2399
|
+
me = await getAuthenticatedUser(config);
|
|
2400
|
+
tasks = await listTasksForReview(config, me);
|
|
2401
|
+
spinner.stop();
|
|
1934
2402
|
} catch (err) {
|
|
1935
|
-
|
|
2403
|
+
spinner.stop();
|
|
1936
2404
|
return `Error: ${err.message}`;
|
|
1937
2405
|
}
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1940
|
-
}
|
|
1941
|
-
const targetBranch = makeWorkerBranchName(me2);
|
|
1942
|
-
let confirmed;
|
|
2406
|
+
if (tasks.length === 0) return `No tasks pending review for @${me}.`;
|
|
2407
|
+
let issueNumber;
|
|
1943
2408
|
try {
|
|
1944
|
-
|
|
1945
|
-
message:
|
|
1946
|
-
choices:
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2409
|
+
issueNumber = await select7({
|
|
2410
|
+
message: "Select a task to review:",
|
|
2411
|
+
choices: tasks.map((t) => ({
|
|
2412
|
+
name: `#${String(t.number).padEnd(4)} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2413
|
+
value: t.number
|
|
2414
|
+
}))
|
|
1950
2415
|
});
|
|
1951
2416
|
} catch {
|
|
1952
2417
|
return "Cancelled.";
|
|
1953
2418
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
let result;
|
|
2419
|
+
const spinner2 = ora8(`Loading #${issueNumber}\u2026`).start();
|
|
2420
|
+
let pr;
|
|
1957
2421
|
try {
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
|
|
2422
|
+
pr = await getTaskPR(config, issueNumber);
|
|
2423
|
+
spinner2.stop();
|
|
1961
2424
|
} catch (err) {
|
|
1962
|
-
|
|
1963
|
-
return `Error: ${err.message}`;
|
|
2425
|
+
spinner2.stop();
|
|
2426
|
+
return `Error loading PR: ${err.message}`;
|
|
1964
2427
|
}
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2428
|
+
const divider = chalk10.dim("\u2500".repeat(70));
|
|
2429
|
+
console.log("\n" + divider);
|
|
2430
|
+
if (pr) {
|
|
2431
|
+
console.log(chalk10.bold(` PR #${pr.number}`) + " " + chalk10.dim(pr.url));
|
|
2432
|
+
console.log(divider);
|
|
2433
|
+
console.log(renderMarkdown(pr.body));
|
|
2434
|
+
} else {
|
|
2435
|
+
console.log(chalk10.yellow(` No open PR found for task #${issueNumber}`));
|
|
2436
|
+
}
|
|
2437
|
+
console.log(divider + "\n");
|
|
2438
|
+
for (; ; ) {
|
|
2439
|
+
let action;
|
|
2440
|
+
try {
|
|
2441
|
+
action = await select7({
|
|
2442
|
+
message: "Review action:",
|
|
2443
|
+
choices: [
|
|
2444
|
+
...pr ? [{ name: "View diff", value: "diff" }] : [],
|
|
2445
|
+
{ name: chalk10.green("Accept") + " \u2014 merge PR and close issue", value: "accept" },
|
|
2446
|
+
{ name: chalk10.red("Reject") + " \u2014 request changes", value: "reject" },
|
|
2447
|
+
{ name: "Nothing, just viewing", value: "none" }
|
|
2448
|
+
]
|
|
2449
|
+
});
|
|
2450
|
+
} catch {
|
|
2451
|
+
return "Cancelled.";
|
|
2452
|
+
}
|
|
2453
|
+
if (action === "none") return `Viewed task #${issueNumber}.`;
|
|
2454
|
+
if (action === "accept") return run6({ issue_number: issueNumber }, config);
|
|
2455
|
+
if (action === "reject") return run7({ issue_number: issueNumber }, config);
|
|
2456
|
+
if (action === "diff") {
|
|
2457
|
+
const diffSpinner = ora8("Fetching diff\u2026").start();
|
|
2458
|
+
let diff;
|
|
2459
|
+
try {
|
|
2460
|
+
diff = await getTaskPRDiff(config, pr.number);
|
|
2461
|
+
diffSpinner.stop();
|
|
2462
|
+
} catch (err) {
|
|
2463
|
+
diffSpinner.stop();
|
|
2464
|
+
console.log(chalk10.red(`Error fetching diff: ${err.message}`));
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
console.log("\n" + divider);
|
|
2468
|
+
for (const line of diff.split("\n")) {
|
|
2469
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2470
|
+
process.stdout.write(chalk10.green(line) + "\n");
|
|
2471
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2472
|
+
process.stdout.write(chalk10.red(line) + "\n");
|
|
2473
|
+
} else if (line.startsWith("@@")) {
|
|
2474
|
+
process.stdout.write(chalk10.cyan(line) + "\n");
|
|
2475
|
+
} else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
|
|
2476
|
+
process.stdout.write(chalk10.bold(line) + "\n");
|
|
2477
|
+
} else {
|
|
2478
|
+
process.stdout.write(line + "\n");
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
console.log(divider + "\n");
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
var execute8 = run8;
|
|
2486
|
+
var terminal8 = true;
|
|
2487
|
+
|
|
2488
|
+
// src/tools/refresh/index.ts
|
|
2489
|
+
var refresh_exports = {};
|
|
2490
|
+
__export(refresh_exports, {
|
|
2491
|
+
definition: () => definition9,
|
|
2492
|
+
execute: () => execute9,
|
|
2493
|
+
run: () => run9,
|
|
2494
|
+
terminal: () => terminal9
|
|
2495
|
+
});
|
|
2496
|
+
var definition9 = {
|
|
2497
|
+
type: "function",
|
|
2498
|
+
function: {
|
|
2499
|
+
name: "refresh",
|
|
2500
|
+
description: "Reload and display the full task list. Equivalent to /refresh.",
|
|
2501
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
async function run9(_input, config) {
|
|
2505
|
+
const tasks = await printTaskList(config);
|
|
2506
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
2507
|
+
const lines = tasks.map((t) => {
|
|
2508
|
+
const status = getStatus(t);
|
|
2509
|
+
const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
|
|
2510
|
+
return `#${t.number} [${status}] ${assignee} ${t.title}`;
|
|
2511
|
+
});
|
|
2512
|
+
return `Tasks (${tasks.length}):
|
|
2513
|
+
${lines.join("\n")}`;
|
|
2514
|
+
}
|
|
2515
|
+
var execute9 = run9;
|
|
2516
|
+
var terminal9 = true;
|
|
2517
|
+
|
|
2518
|
+
// src/tools/open-code/index.ts
|
|
2519
|
+
var open_code_exports = {};
|
|
2520
|
+
__export(open_code_exports, {
|
|
2521
|
+
definition: () => definition10,
|
|
2522
|
+
execute: () => execute10,
|
|
2523
|
+
run: () => run10,
|
|
2524
|
+
terminal: () => terminal10
|
|
2525
|
+
});
|
|
2526
|
+
init_github();
|
|
2527
|
+
var definition10 = {
|
|
2528
|
+
type: "function",
|
|
2529
|
+
function: {
|
|
2530
|
+
name: "open_code",
|
|
2531
|
+
description: "Launch Claude Code for the current task branch. Equivalent to /code.",
|
|
2532
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
1977
2533
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
}
|
|
2534
|
+
};
|
|
2535
|
+
async function run10(_input, config) {
|
|
2536
|
+
let branch;
|
|
2537
|
+
try {
|
|
2538
|
+
branch = await getCurrentBranch();
|
|
2539
|
+
} catch (err) {
|
|
2540
|
+
return `Error: ${err.message}`;
|
|
1986
2541
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
const issueNumber = input["issue_number"];
|
|
1993
|
-
const [me, issue] = await Promise.all([
|
|
1994
|
-
getAuthenticatedUser(config),
|
|
1995
|
-
getTask(config, issueNumber)
|
|
1996
|
-
]);
|
|
1997
|
-
if (issue.author && issue.author !== me) {
|
|
1998
|
-
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2542
|
+
let issueNum = getConfig().taskState?.activeIssueNumber;
|
|
2543
|
+
if (!issueNum) {
|
|
2544
|
+
const found = await getIssueNumberFromBranch(config, branch);
|
|
2545
|
+
if (!found) return `No active task found (current branch: ${branch}).`;
|
|
2546
|
+
issueNum = found.issueNumber;
|
|
1999
2547
|
}
|
|
2000
|
-
|
|
2001
|
-
const spinner = ora8(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2548
|
+
let issue;
|
|
2002
2549
|
try {
|
|
2003
|
-
|
|
2004
|
-
const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
2005
|
-
spinner.stop();
|
|
2006
|
-
return `Task #${issueNumber} accepted.
|
|
2007
|
-
PR #${result.prNumber} merged \u2192 ${targetBranch}
|
|
2008
|
-
Issue closed.`;
|
|
2550
|
+
issue = await getTask(config, issueNum);
|
|
2009
2551
|
} catch (err) {
|
|
2010
|
-
spinner.stop();
|
|
2011
2552
|
return `Error: ${err.message}`;
|
|
2012
2553
|
}
|
|
2554
|
+
await launchClaudeCode(issue, branch);
|
|
2555
|
+
return "Claude Code session ended.";
|
|
2013
2556
|
}
|
|
2557
|
+
var execute10 = run10;
|
|
2014
2558
|
var terminal10 = true;
|
|
2015
2559
|
|
|
2016
2560
|
// src/tools/edit-task/index.ts
|
|
@@ -2022,7 +2566,7 @@ __export(edit_task_exports, {
|
|
|
2022
2566
|
terminal: () => terminal11
|
|
2023
2567
|
});
|
|
2024
2568
|
init_github();
|
|
2025
|
-
import { select as
|
|
2569
|
+
import { select as select8, input as promptInput4 } from "@inquirer/prompts";
|
|
2026
2570
|
import ora9 from "ora";
|
|
2027
2571
|
var definition11 = {
|
|
2028
2572
|
type: "function",
|
|
@@ -2051,7 +2595,7 @@ async function run11(input, config) {
|
|
|
2051
2595
|
}
|
|
2052
2596
|
if (tasks.length === 0) return "No tasks found.";
|
|
2053
2597
|
try {
|
|
2054
|
-
issueNumber = await
|
|
2598
|
+
issueNumber = await select8({
|
|
2055
2599
|
message: "Select task to edit:",
|
|
2056
2600
|
choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
|
|
2057
2601
|
});
|
|
@@ -2108,14 +2652,140 @@ async function execute11(input, config) {
|
|
|
2108
2652
|
}
|
|
2109
2653
|
var terminal11 = true;
|
|
2110
2654
|
|
|
2655
|
+
// src/tools/wiki/index.ts
|
|
2656
|
+
var wiki_exports = {};
|
|
2657
|
+
__export(wiki_exports, {
|
|
2658
|
+
definition: () => definition12,
|
|
2659
|
+
execute: () => execute12,
|
|
2660
|
+
run: () => run12,
|
|
2661
|
+
terminal: () => terminal12
|
|
2662
|
+
});
|
|
2663
|
+
import ora10 from "ora";
|
|
2664
|
+
import chalk11 from "chalk";
|
|
2665
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2666
|
+
import { select as select9 } from "@inquirer/prompts";
|
|
2667
|
+
init_github();
|
|
2668
|
+
var WIKI_PATH = "TECHUNTER.md";
|
|
2669
|
+
var definition12 = {
|
|
2670
|
+
type: "function",
|
|
2671
|
+
function: {
|
|
2672
|
+
name: "update_wiki",
|
|
2673
|
+
description: "Generate or refresh the project overview document (TECHUNTER.md) by scanning the codebase. The document helps new team members understand what the project does, how it is architected, and how to start contributing. Equivalent to /wiki.",
|
|
2674
|
+
parameters: {
|
|
2675
|
+
type: "object",
|
|
2676
|
+
properties: {},
|
|
2677
|
+
required: []
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
async function readWikiContent(config) {
|
|
2682
|
+
try {
|
|
2683
|
+
return await readFile2(WIKI_PATH, "utf-8");
|
|
2684
|
+
} catch {
|
|
2685
|
+
}
|
|
2686
|
+
return getRepoFile(config, WIKI_PATH);
|
|
2687
|
+
}
|
|
2688
|
+
function printWiki(content) {
|
|
2689
|
+
const divider = chalk11.dim("\u2500".repeat(70));
|
|
2690
|
+
console.log("\n" + divider);
|
|
2691
|
+
console.log(chalk11.bold(" TECHUNTER.md"));
|
|
2692
|
+
console.log(divider);
|
|
2693
|
+
console.log(renderMarkdown(content));
|
|
2694
|
+
console.log(divider + "\n");
|
|
2695
|
+
}
|
|
2696
|
+
async function run12(_input, config) {
|
|
2697
|
+
const fetchSpinner = ora10("Checking for existing wiki\u2026").start();
|
|
2698
|
+
const existing = await readWikiContent(config).catch(() => null);
|
|
2699
|
+
fetchSpinner.stop();
|
|
2700
|
+
let action;
|
|
2701
|
+
try {
|
|
2702
|
+
action = await select9({
|
|
2703
|
+
message: "TECHUNTER.md \u2014 what would you like to do?",
|
|
2704
|
+
choices: [
|
|
2705
|
+
...existing ? [{ name: "View current wiki", value: "view" }] : [],
|
|
2706
|
+
{ name: existing ? "Regenerate & commit to repo" : "Generate & commit to repo", value: "generate" },
|
|
2707
|
+
{ name: "Cancel", value: "cancel" }
|
|
2708
|
+
]
|
|
2709
|
+
});
|
|
2710
|
+
} catch {
|
|
2711
|
+
return "Cancelled.";
|
|
2712
|
+
}
|
|
2713
|
+
if (action === "cancel") return "Cancelled.";
|
|
2714
|
+
if (action === "view") {
|
|
2715
|
+
printWiki(existing);
|
|
2716
|
+
return "Displayed TECHUNTER.md.";
|
|
2717
|
+
}
|
|
2718
|
+
const authSpinner = ora10("Checking permissions\u2026").start();
|
|
2719
|
+
let me;
|
|
2720
|
+
let allowed;
|
|
2721
|
+
try {
|
|
2722
|
+
me = await getAuthenticatedUser(config);
|
|
2723
|
+
allowed = await isCollaborator(config, me);
|
|
2724
|
+
authSpinner.stop();
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
authSpinner.stop();
|
|
2727
|
+
return `Error checking permissions: ${err.message}`;
|
|
2728
|
+
}
|
|
2729
|
+
if (!allowed) {
|
|
2730
|
+
return `Permission denied: only repository collaborators can update the project wiki.`;
|
|
2731
|
+
}
|
|
2732
|
+
const genSpinner = ora10("Analyzing project and generating overview\u2026").start();
|
|
2733
|
+
let content;
|
|
2734
|
+
try {
|
|
2735
|
+
content = await generateWiki(config);
|
|
2736
|
+
genSpinner.stop();
|
|
2737
|
+
} catch (err) {
|
|
2738
|
+
genSpinner.stop();
|
|
2739
|
+
return `Error generating wiki: ${err.message}`;
|
|
2740
|
+
}
|
|
2741
|
+
printWiki(content);
|
|
2742
|
+
let confirm;
|
|
2743
|
+
try {
|
|
2744
|
+
confirm = await select9({
|
|
2745
|
+
message: `Publish to repository as ${WIKI_PATH}?`,
|
|
2746
|
+
choices: [
|
|
2747
|
+
{ name: "Yes, commit to repo", value: "publish" },
|
|
2748
|
+
{ name: "Cancel", value: "cancel" }
|
|
2749
|
+
]
|
|
2750
|
+
});
|
|
2751
|
+
} catch {
|
|
2752
|
+
return "Cancelled.";
|
|
2753
|
+
}
|
|
2754
|
+
if (confirm === "cancel") return "Cancelled.";
|
|
2755
|
+
const writeSpinner = ora10(`Writing ${WIKI_PATH}\u2026`).start();
|
|
2756
|
+
try {
|
|
2757
|
+
const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
|
|
2758
|
+
writeSpinner.succeed(`Written: ${url}`);
|
|
2759
|
+
console.log("");
|
|
2760
|
+
return `TECHUNTER.md updated \u2014 ${url}`;
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
writeSpinner.fail(`Failed: ${err.message}`);
|
|
2763
|
+
return `Error: ${err.message}`;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
async function execute12(_input, config) {
|
|
2767
|
+
const me = await getAuthenticatedUser(config);
|
|
2768
|
+
if (!await isCollaborator(config, me)) {
|
|
2769
|
+
return `Permission denied: only repository collaborators can update the project wiki.`;
|
|
2770
|
+
}
|
|
2771
|
+
const content = await generateWiki(config);
|
|
2772
|
+
try {
|
|
2773
|
+
const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
|
|
2774
|
+
return `TECHUNTER.md updated \u2014 ${url}`;
|
|
2775
|
+
} catch (err) {
|
|
2776
|
+
return `Error: ${err.message}`;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
var terminal12 = true;
|
|
2780
|
+
|
|
2111
2781
|
// src/tools/list-tasks/index.ts
|
|
2112
2782
|
var list_tasks_exports = {};
|
|
2113
2783
|
__export(list_tasks_exports, {
|
|
2114
|
-
definition: () =>
|
|
2115
|
-
execute: () =>
|
|
2784
|
+
definition: () => definition13,
|
|
2785
|
+
execute: () => execute13
|
|
2116
2786
|
});
|
|
2117
2787
|
init_github();
|
|
2118
|
-
var
|
|
2788
|
+
var definition13 = {
|
|
2119
2789
|
type: "function",
|
|
2120
2790
|
function: {
|
|
2121
2791
|
name: "list_tasks",
|
|
@@ -2127,7 +2797,7 @@ var definition12 = {
|
|
|
2127
2797
|
}
|
|
2128
2798
|
}
|
|
2129
2799
|
};
|
|
2130
|
-
async function
|
|
2800
|
+
async function execute13(_input, config) {
|
|
2131
2801
|
const tasks = await listTasks(config);
|
|
2132
2802
|
if (tasks.length === 0) return "No open tasks.";
|
|
2133
2803
|
return tasks.map((t) => {
|
|
@@ -2140,11 +2810,11 @@ async function execute12(_input, config) {
|
|
|
2140
2810
|
// src/tools/get-task/index.ts
|
|
2141
2811
|
var get_task_exports = {};
|
|
2142
2812
|
__export(get_task_exports, {
|
|
2143
|
-
definition: () =>
|
|
2144
|
-
execute: () =>
|
|
2813
|
+
definition: () => definition14,
|
|
2814
|
+
execute: () => execute14
|
|
2145
2815
|
});
|
|
2146
2816
|
init_github();
|
|
2147
|
-
var
|
|
2817
|
+
var definition14 = {
|
|
2148
2818
|
type: "function",
|
|
2149
2819
|
function: {
|
|
2150
2820
|
name: "get_task",
|
|
@@ -2158,7 +2828,7 @@ var definition13 = {
|
|
|
2158
2828
|
}
|
|
2159
2829
|
}
|
|
2160
2830
|
};
|
|
2161
|
-
async function
|
|
2831
|
+
async function execute14(input, config) {
|
|
2162
2832
|
const issue = await getTask(config, input["issue_number"]);
|
|
2163
2833
|
const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
|
|
2164
2834
|
const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
|
|
@@ -2175,12 +2845,12 @@ ${issue.body}`);
|
|
|
2175
2845
|
// src/tools/get-comments/index.ts
|
|
2176
2846
|
var get_comments_exports = {};
|
|
2177
2847
|
__export(get_comments_exports, {
|
|
2178
|
-
definition: () =>
|
|
2179
|
-
execute: () =>
|
|
2848
|
+
definition: () => definition15,
|
|
2849
|
+
execute: () => execute15
|
|
2180
2850
|
});
|
|
2181
2851
|
init_github();
|
|
2182
|
-
import
|
|
2183
|
-
var
|
|
2852
|
+
import ora11 from "ora";
|
|
2853
|
+
var definition15 = {
|
|
2184
2854
|
type: "function",
|
|
2185
2855
|
function: {
|
|
2186
2856
|
name: "get_comments",
|
|
@@ -2195,10 +2865,10 @@ var definition14 = {
|
|
|
2195
2865
|
}
|
|
2196
2866
|
}
|
|
2197
2867
|
};
|
|
2198
|
-
async function
|
|
2868
|
+
async function execute15(input, config) {
|
|
2199
2869
|
const issueNumber = input["issue_number"];
|
|
2200
2870
|
const limit = input["limit"] ?? 5;
|
|
2201
|
-
const spinner =
|
|
2871
|
+
const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
|
|
2202
2872
|
try {
|
|
2203
2873
|
const comments = await listComments(config, issueNumber, limit);
|
|
2204
2874
|
spinner.stop();
|
|
@@ -2217,11 +2887,11 @@ ${lines.join("\n\n")}`;
|
|
|
2217
2887
|
// src/tools/get-diff/index.ts
|
|
2218
2888
|
var get_diff_exports = {};
|
|
2219
2889
|
__export(get_diff_exports, {
|
|
2220
|
-
definition: () =>
|
|
2221
|
-
execute: () =>
|
|
2890
|
+
definition: () => definition16,
|
|
2891
|
+
execute: () => execute16
|
|
2222
2892
|
});
|
|
2223
|
-
import
|
|
2224
|
-
var
|
|
2893
|
+
import ora12 from "ora";
|
|
2894
|
+
var definition16 = {
|
|
2225
2895
|
type: "function",
|
|
2226
2896
|
function: {
|
|
2227
2897
|
name: "get_diff",
|
|
@@ -2229,8 +2899,8 @@ var definition15 = {
|
|
|
2229
2899
|
parameters: { type: "object", properties: {}, required: [] }
|
|
2230
2900
|
}
|
|
2231
2901
|
};
|
|
2232
|
-
async function
|
|
2233
|
-
const spinner =
|
|
2902
|
+
async function execute16(_input, _config) {
|
|
2903
|
+
const spinner = ora12("Reading git diff...").start();
|
|
2234
2904
|
try {
|
|
2235
2905
|
const diff = await getDiff();
|
|
2236
2906
|
spinner.stop();
|
|
@@ -2244,14 +2914,14 @@ async function execute15(_input, _config) {
|
|
|
2244
2914
|
// src/tools/run-command/index.ts
|
|
2245
2915
|
var run_command_exports = {};
|
|
2246
2916
|
__export(run_command_exports, {
|
|
2247
|
-
definition: () =>
|
|
2248
|
-
execute: () =>
|
|
2917
|
+
definition: () => definition17,
|
|
2918
|
+
execute: () => execute17
|
|
2249
2919
|
});
|
|
2250
2920
|
import { exec } from "child_process";
|
|
2251
2921
|
import { promisify } from "util";
|
|
2252
|
-
import
|
|
2922
|
+
import ora13 from "ora";
|
|
2253
2923
|
var execAsync = promisify(exec);
|
|
2254
|
-
var
|
|
2924
|
+
var definition17 = {
|
|
2255
2925
|
type: "function",
|
|
2256
2926
|
function: {
|
|
2257
2927
|
name: "run_command",
|
|
@@ -2265,10 +2935,10 @@ var definition16 = {
|
|
|
2265
2935
|
}
|
|
2266
2936
|
}
|
|
2267
2937
|
};
|
|
2268
|
-
async function
|
|
2938
|
+
async function execute17(input, _config) {
|
|
2269
2939
|
const command = input["command"];
|
|
2270
2940
|
const cwd = process.cwd();
|
|
2271
|
-
const spinner =
|
|
2941
|
+
const spinner = ora13(`$ ${command}`).start();
|
|
2272
2942
|
try {
|
|
2273
2943
|
const { stdout, stderr } = await execAsync(command, { cwd, timeout: 6e4, maxBuffer: 1024 * 1024 });
|
|
2274
2944
|
spinner.stop();
|
|
@@ -2284,20 +2954,34 @@ ${out || e.message}`;
|
|
|
2284
2954
|
}
|
|
2285
2955
|
}
|
|
2286
2956
|
|
|
2287
|
-
// src/tools/
|
|
2288
|
-
var
|
|
2289
|
-
__export(
|
|
2290
|
-
definition: () =>
|
|
2291
|
-
execute: () =>
|
|
2957
|
+
// src/tools/list-files/index.ts
|
|
2958
|
+
var list_files_exports = {};
|
|
2959
|
+
__export(list_files_exports, {
|
|
2960
|
+
definition: () => definition18,
|
|
2961
|
+
execute: () => execute18
|
|
2292
2962
|
});
|
|
2293
|
-
import
|
|
2294
|
-
|
|
2295
|
-
// src/lib/project.ts
|
|
2296
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
2963
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2297
2964
|
import { existsSync } from "fs";
|
|
2298
2965
|
import path2 from "path";
|
|
2299
2966
|
import { globby } from "globby";
|
|
2300
2967
|
import ignore from "ignore";
|
|
2968
|
+
var definition18 = {
|
|
2969
|
+
type: "function",
|
|
2970
|
+
function: {
|
|
2971
|
+
name: "list_files",
|
|
2972
|
+
description: "List file paths in the project. Use this first to orient yourself before searching or reading.",
|
|
2973
|
+
parameters: {
|
|
2974
|
+
type: "object",
|
|
2975
|
+
properties: {
|
|
2976
|
+
glob: {
|
|
2977
|
+
type: "string",
|
|
2978
|
+
description: 'Glob pattern to filter results, e.g. "src/**/*.ts". Defaults to all text files.'
|
|
2979
|
+
}
|
|
2980
|
+
},
|
|
2981
|
+
required: []
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
};
|
|
2301
2985
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2302
2986
|
".png",
|
|
2303
2987
|
".jpg",
|
|
@@ -2310,224 +2994,210 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2310
2994
|
".zip",
|
|
2311
2995
|
".tar",
|
|
2312
2996
|
".gz",
|
|
2313
|
-
".bz2",
|
|
2314
|
-
".rar",
|
|
2315
2997
|
".exe",
|
|
2316
2998
|
".dll",
|
|
2317
|
-
".so",
|
|
2318
|
-
".dylib",
|
|
2319
2999
|
".woff",
|
|
2320
3000
|
".woff2",
|
|
2321
3001
|
".ttf",
|
|
2322
|
-
".otf",
|
|
2323
|
-
".eot",
|
|
2324
3002
|
".mp3",
|
|
2325
3003
|
".mp4",
|
|
2326
|
-
".wav",
|
|
2327
|
-
".avi",
|
|
2328
|
-
".mov",
|
|
2329
3004
|
".db",
|
|
2330
3005
|
".sqlite",
|
|
2331
3006
|
".lock"
|
|
2332
3007
|
]);
|
|
2333
|
-
|
|
2334
|
-
"
|
|
2335
|
-
|
|
2336
|
-
"README",
|
|
2337
|
-
"package.json",
|
|
2338
|
-
"pyproject.toml",
|
|
2339
|
-
"go.mod",
|
|
2340
|
-
"Cargo.toml",
|
|
2341
|
-
"tsconfig.json",
|
|
2342
|
-
"vite.config.ts",
|
|
2343
|
-
"vite.config.js",
|
|
2344
|
-
"webpack.config.js",
|
|
2345
|
-
"rollup.config.js",
|
|
2346
|
-
".env.example",
|
|
2347
|
-
"docker-compose.yml",
|
|
2348
|
-
"Dockerfile"
|
|
2349
|
-
];
|
|
2350
|
-
var MAX_TOTAL_BYTES = 8e4;
|
|
2351
|
-
var MAX_FILE_BYTES = 15e3;
|
|
2352
|
-
async function buildIgnoreFilter(cwd) {
|
|
3008
|
+
async function execute18(input, _config) {
|
|
3009
|
+
const glob = input["glob"] ?? "**/*";
|
|
3010
|
+
const cwd = process.cwd();
|
|
2353
3011
|
const ig = ignore();
|
|
2354
3012
|
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
2355
3013
|
if (existsSync(gitignorePath)) {
|
|
2356
|
-
|
|
2357
|
-
ig.add(content);
|
|
2358
|
-
}
|
|
2359
|
-
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "*.pyc", "build", "coverage"]);
|
|
2360
|
-
return ig;
|
|
2361
|
-
}
|
|
2362
|
-
async function safeReadFile(filePath, maxBytes = MAX_FILE_BYTES) {
|
|
2363
|
-
try {
|
|
2364
|
-
const content = await readFile2(filePath, "utf-8");
|
|
2365
|
-
if (content.length > maxBytes) {
|
|
2366
|
-
return content.slice(0, maxBytes) + `
|
|
2367
|
-
... (truncated at ${maxBytes} chars)`;
|
|
2368
|
-
}
|
|
2369
|
-
return content;
|
|
2370
|
-
} catch {
|
|
2371
|
-
return null;
|
|
3014
|
+
ig.add(await readFile3(gitignorePath, "utf-8"));
|
|
2372
3015
|
}
|
|
3016
|
+
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
|
|
3017
|
+
const files = await globby(glob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
3018
|
+
const filtered = files.filter((f) => !ig.ignores(f) && !BINARY_EXTENSIONS.has(path2.extname(f).toLowerCase()));
|
|
3019
|
+
if (filtered.length === 0) return `No files matched: ${glob}`;
|
|
3020
|
+
return `${filtered.length} file(s):
|
|
3021
|
+
${filtered.join("\n")}`;
|
|
2373
3022
|
}
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
for (const f of rootFiles) lines.push(f);
|
|
2388
|
-
const dirs = Object.keys(tree).filter((d) => d !== ".").sort();
|
|
2389
|
-
for (const dir of dirs) {
|
|
2390
|
-
lines.push(`${dir}/`);
|
|
2391
|
-
for (const f of tree[dir]) {
|
|
2392
|
-
lines.push(` ${f}`);
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
return lines.join("\n");
|
|
2396
|
-
}
|
|
2397
|
-
function scoreRelevance(filePath, keywords) {
|
|
2398
|
-
const lower = filePath.toLowerCase();
|
|
2399
|
-
let score = 0;
|
|
2400
|
-
for (const kw of keywords) {
|
|
2401
|
-
if (lower.includes(kw)) score += 1;
|
|
2402
|
-
}
|
|
2403
|
-
return score;
|
|
2404
|
-
}
|
|
2405
|
-
async function buildProjectContext(cwd, issueTitle, issueBody) {
|
|
2406
|
-
const ig = await buildIgnoreFilter(cwd);
|
|
2407
|
-
const allFiles = await globby("**/*", {
|
|
2408
|
-
cwd,
|
|
2409
|
-
gitignore: false,
|
|
2410
|
-
// We handle ignore ourselves
|
|
2411
|
-
dot: false,
|
|
2412
|
-
onlyFiles: true
|
|
2413
|
-
});
|
|
2414
|
-
const filtered = allFiles.filter((f) => !ig.ignores(f) && !isBinaryFile(f));
|
|
2415
|
-
const fileTree = buildFileTree(filtered);
|
|
2416
|
-
const issueText = `${issueTitle} ${issueBody}`.toLowerCase();
|
|
2417
|
-
const keywords = issueText.replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
2418
|
-
const keyFiles = {};
|
|
2419
|
-
let totalBytes = 0;
|
|
2420
|
-
for (const always of ALWAYS_READ) {
|
|
2421
|
-
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
2422
|
-
const fullPath = path2.join(cwd, always);
|
|
2423
|
-
if (!existsSync(fullPath)) continue;
|
|
2424
|
-
const content = await safeReadFile(fullPath);
|
|
2425
|
-
if (content !== null) {
|
|
2426
|
-
keyFiles[always] = content;
|
|
2427
|
-
totalBytes += content.length;
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
const scored = filtered.filter((f) => !ALWAYS_READ.includes(f) && !ALWAYS_READ.includes(path2.basename(f))).map((f) => ({ file: f, score: scoreRelevance(f, keywords) })).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, 10);
|
|
2431
|
-
for (const { file } of scored) {
|
|
2432
|
-
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
2433
|
-
const fullPath = path2.join(cwd, file);
|
|
2434
|
-
const content = await safeReadFile(fullPath);
|
|
2435
|
-
if (content !== null) {
|
|
2436
|
-
keyFiles[file] = content;
|
|
2437
|
-
totalBytes += content.length;
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
return { fileTree, keyFiles };
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
// src/tools/scan-project/index.ts
|
|
2444
|
-
var definition17 = {
|
|
3023
|
+
|
|
3024
|
+
// src/tools/grep-code/index.ts
|
|
3025
|
+
var grep_code_exports = {};
|
|
3026
|
+
__export(grep_code_exports, {
|
|
3027
|
+
definition: () => definition19,
|
|
3028
|
+
execute: () => execute19
|
|
3029
|
+
});
|
|
3030
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3031
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3032
|
+
import path3 from "path";
|
|
3033
|
+
import { globby as globby2 } from "globby";
|
|
3034
|
+
import ignore2 from "ignore";
|
|
3035
|
+
var definition19 = {
|
|
2445
3036
|
type: "function",
|
|
2446
3037
|
function: {
|
|
2447
|
-
name: "
|
|
2448
|
-
description: "
|
|
3038
|
+
name: "grep_code",
|
|
3039
|
+
description: "Search for a pattern across files, or read a specific line range from a file.\n- Search mode: provide `pattern` \u2014 returns matching lines with context.\n- Read-range mode: provide `file_glob` (single file) + `start_line` + `end_line`, no `pattern` \u2014 read an exact section. Use after grep has given you line numbers.",
|
|
2449
3040
|
parameters: {
|
|
2450
3041
|
type: "object",
|
|
2451
3042
|
properties: {
|
|
2452
|
-
|
|
3043
|
+
pattern: {
|
|
2453
3044
|
type: "string",
|
|
2454
|
-
description: "
|
|
3045
|
+
description: "Regex or plain text to search for (case-insensitive). Omit for read-range mode."
|
|
3046
|
+
},
|
|
3047
|
+
file_glob: {
|
|
3048
|
+
type: "string",
|
|
3049
|
+
description: 'Glob to restrict which files to search or read, e.g. "src/**/*.ts" or "src/lib/agent.ts". Defaults to all text files.'
|
|
3050
|
+
},
|
|
3051
|
+
context_lines: {
|
|
3052
|
+
type: "number",
|
|
3053
|
+
description: "Lines of context before/after each match (search mode only). Default: 2."
|
|
3054
|
+
},
|
|
3055
|
+
max_results: {
|
|
3056
|
+
type: "number",
|
|
3057
|
+
description: "Max matches to return (search mode only). Default: 50."
|
|
3058
|
+
},
|
|
3059
|
+
start_line: {
|
|
3060
|
+
type: "number",
|
|
3061
|
+
description: "First line to read, 1-based (read-range mode). Requires file_glob pointing to a single file."
|
|
3062
|
+
},
|
|
3063
|
+
end_line: {
|
|
3064
|
+
type: "number",
|
|
3065
|
+
description: "Last line to read, 1-based (read-range mode)."
|
|
2455
3066
|
}
|
|
2456
3067
|
},
|
|
2457
3068
|
required: []
|
|
2458
3069
|
}
|
|
2459
3070
|
}
|
|
2460
3071
|
};
|
|
2461
|
-
async function
|
|
2462
|
-
const
|
|
2463
|
-
const
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
const context = await buildProjectContext(cwd, focus, "");
|
|
2467
|
-
spinner.stop();
|
|
2468
|
-
const fileCount = context.fileTree.split("\n").filter(Boolean).length;
|
|
2469
|
-
const readCount = Object.keys(context.keyFiles).length;
|
|
2470
|
-
const totalBytes = Object.values(context.keyFiles).reduce((s, c) => s + c.length, 0);
|
|
2471
|
-
const summary = `Scanned ${fileCount} files \xB7 ${readCount} read \xB7 ${(totalBytes / 1024).toFixed(1)} KB`;
|
|
2472
|
-
const parts = [summary, `## File tree
|
|
2473
|
-
\`\`\`
|
|
2474
|
-
${context.fileTree}
|
|
2475
|
-
\`\`\``];
|
|
2476
|
-
for (const [filePath, content] of Object.entries(context.keyFiles)) {
|
|
2477
|
-
parts.push(`## ${filePath}
|
|
2478
|
-
\`\`\`
|
|
2479
|
-
${content}
|
|
2480
|
-
\`\`\``);
|
|
2481
|
-
}
|
|
2482
|
-
return parts.join("\n\n");
|
|
2483
|
-
} catch (err) {
|
|
2484
|
-
spinner.stop();
|
|
2485
|
-
throw err;
|
|
3072
|
+
async function buildIgnore(cwd) {
|
|
3073
|
+
const ig = ignore2();
|
|
3074
|
+
const gitignorePath = path3.join(cwd, ".gitignore");
|
|
3075
|
+
if (existsSync2(gitignorePath)) {
|
|
3076
|
+
ig.add(await readFile4(gitignorePath, "utf-8"));
|
|
2486
3077
|
}
|
|
3078
|
+
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
|
|
3079
|
+
return ig;
|
|
2487
3080
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
3081
|
+
var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
3082
|
+
".png",
|
|
3083
|
+
".jpg",
|
|
3084
|
+
".jpeg",
|
|
3085
|
+
".gif",
|
|
3086
|
+
".svg",
|
|
3087
|
+
".ico",
|
|
3088
|
+
".webp",
|
|
3089
|
+
".pdf",
|
|
3090
|
+
".zip",
|
|
3091
|
+
".tar",
|
|
3092
|
+
".gz",
|
|
3093
|
+
".exe",
|
|
3094
|
+
".dll",
|
|
3095
|
+
".woff",
|
|
3096
|
+
".woff2",
|
|
3097
|
+
".ttf",
|
|
3098
|
+
".mp3",
|
|
3099
|
+
".mp4",
|
|
3100
|
+
".db",
|
|
3101
|
+
".sqlite",
|
|
3102
|
+
".lock"
|
|
3103
|
+
]);
|
|
3104
|
+
function isText(f) {
|
|
3105
|
+
return !BINARY_EXTENSIONS2.has(path3.extname(f).toLowerCase());
|
|
3106
|
+
}
|
|
3107
|
+
var MAX_RANGE_LINES = 300;
|
|
3108
|
+
async function execute19(input, _config) {
|
|
3109
|
+
const pattern = input["pattern"] ?? "";
|
|
3110
|
+
const fileGlob = input["file_glob"] ?? "**/*";
|
|
3111
|
+
const contextLines = Math.min(input["context_lines"] ?? 2, 5);
|
|
3112
|
+
const maxResults = Math.min(input["max_results"] ?? 50, 200);
|
|
3113
|
+
const startLine = input["start_line"];
|
|
3114
|
+
const endLine = input["end_line"];
|
|
3115
|
+
const cwd = process.cwd();
|
|
3116
|
+
if (!pattern && startLine != null && endLine != null) {
|
|
3117
|
+
const files = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
3118
|
+
if (files.length === 0) return `No file matched: ${fileGlob}`;
|
|
3119
|
+
if (files.length > 1) return `file_glob matched ${files.length} files \u2014 narrow it to a single file for read-range mode.`;
|
|
3120
|
+
const raw = await readFile4(path3.join(cwd, files[0]), "utf-8");
|
|
3121
|
+
const lines = raw.split("\n");
|
|
3122
|
+
const total = lines.length;
|
|
3123
|
+
const from = Math.max(1, startLine);
|
|
3124
|
+
const to = Math.min(total, endLine, from + MAX_RANGE_LINES - 1);
|
|
3125
|
+
const numbered = lines.slice(from - 1, to).map((l, i) => `${String(from + i).padStart(5)}: ${l}`).join("\n");
|
|
3126
|
+
const truncNote = to < Math.min(total, endLine) ? `
|
|
3127
|
+
\u2026 (use start_line=${to + 1} to continue)` : "";
|
|
3128
|
+
return `${files[0]} \u2014 lines ${from}\u2013${to} of ${total}:
|
|
3129
|
+
\`\`\`
|
|
3130
|
+
${numbered}
|
|
3131
|
+
\`\`\`${truncNote}`;
|
|
2509
3132
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
3133
|
+
if (!pattern) return "Provide a `pattern` to search, or `start_line` + `end_line` for read-range mode. Use list_files to browse file paths.";
|
|
3134
|
+
const ig = await buildIgnore(cwd);
|
|
3135
|
+
let regex;
|
|
2513
3136
|
try {
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
}
|
|
2518
|
-
|
|
3137
|
+
regex = new RegExp(pattern, "i");
|
|
3138
|
+
} catch {
|
|
3139
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
3140
|
+
}
|
|
3141
|
+
const allFiles = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
3142
|
+
const filtered = allFiles.filter((f) => !ig.ignores(f) && isText(f));
|
|
3143
|
+
const matches = [];
|
|
3144
|
+
let totalMatches = 0;
|
|
3145
|
+
for (const file of filtered) {
|
|
3146
|
+
if (totalMatches >= maxResults) break;
|
|
3147
|
+
let content;
|
|
3148
|
+
try {
|
|
3149
|
+
content = await readFile4(path3.join(cwd, file), "utf-8");
|
|
3150
|
+
} catch {
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
const lines = content.split("\n");
|
|
3154
|
+
const hitLines = [];
|
|
3155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3156
|
+
if (regex.test(lines[i])) hitLines.push(i);
|
|
3157
|
+
}
|
|
3158
|
+
if (hitLines.length === 0) continue;
|
|
3159
|
+
const ranges = [];
|
|
3160
|
+
for (const hit of hitLines) {
|
|
3161
|
+
const s = Math.max(0, hit - contextLines);
|
|
3162
|
+
const e = Math.min(lines.length - 1, hit + contextLines);
|
|
3163
|
+
if (ranges.length > 0 && s <= ranges[ranges.length - 1][1] + 1) {
|
|
3164
|
+
ranges[ranges.length - 1][1] = e;
|
|
3165
|
+
} else {
|
|
3166
|
+
ranges.push([s, e]);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
const snippets = [];
|
|
3170
|
+
for (const [s, e] of ranges) {
|
|
3171
|
+
if (totalMatches >= maxResults) break;
|
|
3172
|
+
snippets.push(
|
|
3173
|
+
lines.slice(s, e + 1).map((l, i) => {
|
|
3174
|
+
const n = s + i + 1;
|
|
3175
|
+
return `${regex.test(l) ? ">" : " "} ${String(n).padStart(4)}: ${l}`;
|
|
3176
|
+
}).join("\n")
|
|
3177
|
+
);
|
|
3178
|
+
totalMatches += hitLines.filter((h) => h >= s && h <= e).length;
|
|
3179
|
+
}
|
|
3180
|
+
if (snippets.length > 0) {
|
|
3181
|
+
matches.push(`## ${file}
|
|
3182
|
+
\`\`\`
|
|
3183
|
+
${snippets.join("\n---\n")}
|
|
3184
|
+
\`\`\``);
|
|
3185
|
+
}
|
|
2519
3186
|
}
|
|
3187
|
+
if (matches.length === 0) return `No matches found for: ${pattern}`;
|
|
3188
|
+
const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
|
|
3189
|
+
return [header, ...matches].join("\n\n");
|
|
2520
3190
|
}
|
|
2521
3191
|
|
|
2522
3192
|
// src/tools/ask-user/index.ts
|
|
2523
3193
|
var ask_user_exports = {};
|
|
2524
3194
|
__export(ask_user_exports, {
|
|
2525
|
-
definition: () =>
|
|
2526
|
-
execute: () =>
|
|
3195
|
+
definition: () => definition20,
|
|
3196
|
+
execute: () => execute20
|
|
2527
3197
|
});
|
|
2528
|
-
import
|
|
2529
|
-
import { select as
|
|
2530
|
-
var
|
|
3198
|
+
import chalk12 from "chalk";
|
|
3199
|
+
import { select as select10, input as promptInput5 } from "@inquirer/prompts";
|
|
3200
|
+
var definition20 = {
|
|
2531
3201
|
type: "function",
|
|
2532
3202
|
function: {
|
|
2533
3203
|
name: "ask_user",
|
|
@@ -2546,24 +3216,24 @@ var definition19 = {
|
|
|
2546
3216
|
}
|
|
2547
3217
|
}
|
|
2548
3218
|
};
|
|
2549
|
-
async function
|
|
3219
|
+
async function execute20(input, _config) {
|
|
2550
3220
|
const question = input["question"];
|
|
2551
3221
|
const options = input["options"];
|
|
2552
3222
|
const OTHER = "__other__";
|
|
2553
3223
|
console.log("");
|
|
2554
|
-
console.log(
|
|
2555
|
-
console.log(
|
|
3224
|
+
console.log(chalk12.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
|
|
3225
|
+
console.log(chalk12.dim(" \u2502"));
|
|
2556
3226
|
for (const line of question.split("\n")) {
|
|
2557
|
-
console.log(
|
|
3227
|
+
console.log(chalk12.dim(" \u2502 ") + line);
|
|
2558
3228
|
}
|
|
2559
|
-
console.log(
|
|
3229
|
+
console.log(chalk12.dim(" \u2514" + "\u2500".repeat(67)));
|
|
2560
3230
|
let answer;
|
|
2561
3231
|
try {
|
|
2562
|
-
const chosen = await
|
|
3232
|
+
const chosen = await select10({
|
|
2563
3233
|
message: " ",
|
|
2564
3234
|
choices: [
|
|
2565
3235
|
...options.map((o) => ({ name: o, value: o })),
|
|
2566
|
-
{ name:
|
|
3236
|
+
{ name: chalk12.dim("Other (describe below)"), value: OTHER }
|
|
2567
3237
|
]
|
|
2568
3238
|
});
|
|
2569
3239
|
answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
|
|
@@ -2588,14 +3258,15 @@ var toolModules = [
|
|
|
2588
3258
|
reject_exports,
|
|
2589
3259
|
accept_exports,
|
|
2590
3260
|
edit_task_exports,
|
|
3261
|
+
wiki_exports,
|
|
2591
3262
|
// Low-level tools
|
|
2592
3263
|
list_tasks_exports,
|
|
2593
3264
|
get_task_exports,
|
|
2594
3265
|
get_comments_exports,
|
|
2595
3266
|
get_diff_exports,
|
|
2596
3267
|
run_command_exports,
|
|
2597
|
-
|
|
2598
|
-
|
|
3268
|
+
list_files_exports,
|
|
3269
|
+
grep_code_exports,
|
|
2599
3270
|
ask_user_exports
|
|
2600
3271
|
];
|
|
2601
3272
|
|