techunter 0.1.11 → 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 +1734 -956
- package/dist/mcp.js +1049 -378
- package/package.json +1 -1
package/dist/index.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,19 +522,20 @@ 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
|
|
|
398
529
|
// src/index.ts
|
|
399
|
-
import
|
|
530
|
+
import chalk17 from "chalk";
|
|
400
531
|
import readline from "readline";
|
|
401
532
|
import { createRequire } from "module";
|
|
402
533
|
|
|
403
534
|
// src/commands/init.ts
|
|
404
|
-
import { input, password, select } from "@inquirer/prompts";
|
|
405
|
-
import
|
|
406
|
-
import
|
|
407
|
-
import
|
|
535
|
+
import { input, password, select as select11 } from "@inquirer/prompts";
|
|
536
|
+
import chalk13 from "chalk";
|
|
537
|
+
import ora14 from "ora";
|
|
538
|
+
import open2 from "open";
|
|
408
539
|
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device";
|
|
409
540
|
|
|
410
541
|
// src/lib/config.ts
|
|
@@ -423,7 +554,8 @@ var configSchema = z.object({
|
|
|
423
554
|
}),
|
|
424
555
|
taskState: z.object({
|
|
425
556
|
activeIssueNumber: z.number().optional(),
|
|
426
|
-
baseCommit: z.string().optional()
|
|
557
|
+
baseCommit: z.string().optional(),
|
|
558
|
+
activeBranch: z.string().optional()
|
|
427
559
|
}).optional()
|
|
428
560
|
});
|
|
429
561
|
var store = new Conf({
|
|
@@ -510,14 +642,21 @@ function parseOwnerRepo(remoteUrl) {
|
|
|
510
642
|
}
|
|
511
643
|
return null;
|
|
512
644
|
}
|
|
513
|
-
function makeBranchName(issueNumber, username) {
|
|
514
|
-
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
515
|
-
return `task-${issueNumber}-${slug}`;
|
|
516
|
-
}
|
|
517
645
|
function makeWorkerBranchName(username) {
|
|
518
646
|
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
519
647
|
return `worker-${slug}`;
|
|
520
648
|
}
|
|
649
|
+
function makeTaskBranchName(issueNumber, username) {
|
|
650
|
+
const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
|
|
651
|
+
return `task-${issueNumber}-${slug}`;
|
|
652
|
+
}
|
|
653
|
+
function isTaskBranch(branch) {
|
|
654
|
+
return /^task-\d+-/.test(branch);
|
|
655
|
+
}
|
|
656
|
+
function parseIssueNumberFromBranch(branch) {
|
|
657
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
658
|
+
return match ? parseInt(match[1], 10) : null;
|
|
659
|
+
}
|
|
521
660
|
async function getCurrentCommit() {
|
|
522
661
|
return (await git.revparse(["HEAD"])).trim();
|
|
523
662
|
}
|
|
@@ -638,16 +777,27 @@ async function getRemoteHeadSha(baseBranch) {
|
|
|
638
777
|
await git.fetch("origin", baseBranch);
|
|
639
778
|
return (await git.revparse([`origin/${baseBranch}`])).trim();
|
|
640
779
|
}
|
|
641
|
-
async function
|
|
642
|
-
const branches = await git.branch();
|
|
643
|
-
const
|
|
644
|
-
|
|
780
|
+
async function checkoutFromCommit(branchName, sha) {
|
|
781
|
+
const branches = await git.branch(["-a"]);
|
|
782
|
+
const exists = Object.keys(branches.branches).some(
|
|
783
|
+
(b) => b === branchName || b === `remotes/origin/${branchName}`
|
|
784
|
+
);
|
|
785
|
+
if (exists) {
|
|
645
786
|
await git.checkout(branchName);
|
|
646
|
-
await git.reset(["--hard", sha]);
|
|
647
787
|
} else {
|
|
648
788
|
await git.checkoutBranch(branchName, sha);
|
|
649
789
|
}
|
|
650
790
|
}
|
|
791
|
+
async function hasUncommittedChanges() {
|
|
792
|
+
const status = await git.status();
|
|
793
|
+
return !status.isClean();
|
|
794
|
+
}
|
|
795
|
+
async function stash(message) {
|
|
796
|
+
await git.stash(["push", "-u", "-m", message]);
|
|
797
|
+
}
|
|
798
|
+
async function stashPop() {
|
|
799
|
+
await git.stash(["pop"]);
|
|
800
|
+
}
|
|
651
801
|
|
|
652
802
|
// src/lib/client.ts
|
|
653
803
|
init_proxy();
|
|
@@ -665,349 +815,165 @@ function getModel(config) {
|
|
|
665
815
|
return config.aiModel ?? DEFAULT_MODEL;
|
|
666
816
|
}
|
|
667
817
|
|
|
668
|
-
// src/
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
818
|
+
// src/tools/pick/index.ts
|
|
819
|
+
var pick_exports = {};
|
|
820
|
+
__export(pick_exports, {
|
|
821
|
+
definition: () => definition3,
|
|
822
|
+
execute: () => execute3,
|
|
823
|
+
run: () => run3,
|
|
824
|
+
terminal: () => terminal3
|
|
825
|
+
});
|
|
826
|
+
init_github();
|
|
827
|
+
import chalk5 from "chalk";
|
|
828
|
+
import ora3 from "ora";
|
|
829
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
830
|
+
init_github();
|
|
831
|
+
|
|
832
|
+
// src/lib/markdown.ts
|
|
833
|
+
import { marked } from "marked";
|
|
834
|
+
import { markedTerminal } from "marked-terminal";
|
|
835
|
+
marked.use(markedTerminal({ showSectionPrefix: false }));
|
|
836
|
+
function renderMarkdown(text) {
|
|
837
|
+
return marked(text);
|
|
677
838
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
839
|
+
|
|
840
|
+
// src/lib/display.ts
|
|
841
|
+
init_github();
|
|
842
|
+
import chalk2 from "chalk";
|
|
843
|
+
var LABEL_AVAILABLE2 = "techunter:available";
|
|
844
|
+
var LABEL_CLAIMED2 = "techunter:claimed";
|
|
845
|
+
var LABEL_IN_REVIEW2 = "techunter:in-review";
|
|
846
|
+
var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
|
|
847
|
+
function getStatus(issue) {
|
|
848
|
+
if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
|
|
849
|
+
if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
|
|
850
|
+
if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
|
|
851
|
+
if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
|
|
852
|
+
return "unknown";
|
|
853
|
+
}
|
|
854
|
+
function colorStatus(status) {
|
|
855
|
+
const padded = status.padEnd(14);
|
|
856
|
+
switch (status) {
|
|
857
|
+
case "available":
|
|
858
|
+
return chalk2.green(padded);
|
|
859
|
+
case "claimed":
|
|
860
|
+
return chalk2.yellow(padded);
|
|
861
|
+
case "in-review":
|
|
862
|
+
return chalk2.blue(padded);
|
|
863
|
+
case "changes-needed":
|
|
864
|
+
return chalk2.red(padded);
|
|
865
|
+
default:
|
|
866
|
+
return padded;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function parentIssueFromBranch(branch) {
|
|
870
|
+
if (!isTaskBranch(branch)) return null;
|
|
871
|
+
const match = branch.match(/^task-(\d+)-/);
|
|
872
|
+
return match ? parseInt(match[1], 10) : null;
|
|
873
|
+
}
|
|
874
|
+
function getParentIssueNumber(issue) {
|
|
875
|
+
const target = extractTargetBranch(issue.body);
|
|
876
|
+
if (!target) return null;
|
|
877
|
+
return parentIssueFromBranch(target);
|
|
878
|
+
}
|
|
879
|
+
function printTaskDetail(issue) {
|
|
880
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
881
|
+
const parentNum = getParentIssueNumber(issue);
|
|
882
|
+
console.log("\n" + divider);
|
|
883
|
+
console.log(
|
|
884
|
+
chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014") + (parentNum ? chalk2.dim(` sub-task of #${parentNum}`) : "")
|
|
885
|
+
);
|
|
886
|
+
console.log(chalk2.bold("\n " + issue.title));
|
|
887
|
+
if (issue.body) {
|
|
888
|
+
console.log("");
|
|
889
|
+
console.log(renderMarkdown(issue.body));
|
|
890
|
+
}
|
|
891
|
+
console.log("\n " + chalk2.dim(issue.htmlUrl));
|
|
892
|
+
console.log(divider + "\n");
|
|
893
|
+
}
|
|
894
|
+
async function printTaskList(config) {
|
|
702
895
|
try {
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
896
|
+
const tasks = await listTasks(config);
|
|
897
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
898
|
+
console.log("");
|
|
899
|
+
console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
|
|
900
|
+
console.log(divider);
|
|
901
|
+
if (tasks.length === 0) {
|
|
902
|
+
console.log(chalk2.dim(" (no tasks)"));
|
|
903
|
+
} else {
|
|
904
|
+
let printTask2 = function(t, indent, connector, isLast) {
|
|
905
|
+
const num = `#${t.number}`.padEnd(5);
|
|
906
|
+
const status = colorStatus(getStatus(t));
|
|
907
|
+
const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
|
|
908
|
+
const fullPrefix = indent + connector;
|
|
909
|
+
const maxTitle = 36 - fullPrefix.length;
|
|
910
|
+
const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
|
|
911
|
+
console.log(` ${num}${status}${assignee}${chalk2.dim(fullPrefix)}${title}`);
|
|
912
|
+
const children = childrenOf.get(t.number) ?? [];
|
|
913
|
+
const childIndent = indent + (isLast ? " " : "\u2502 ");
|
|
914
|
+
for (let i = 0; i < children.length; i++) {
|
|
915
|
+
const childIsLast = i === children.length - 1;
|
|
916
|
+
printTask2(children[i], childIndent, childIsLast ? "\u2514\u2500 " : "\u251C\u2500 ", childIsLast);
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
var printTask = printTask2;
|
|
920
|
+
const taskMap = new Map(tasks.map((t) => [t.number, t]));
|
|
921
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const t of tasks) {
|
|
923
|
+
const parentNum = getParentIssueNumber(t);
|
|
924
|
+
const key = parentNum !== null && taskMap.has(parentNum) ? parentNum : null;
|
|
925
|
+
if (!childrenOf.has(key)) childrenOf.set(key, []);
|
|
926
|
+
childrenOf.get(key).push(t);
|
|
927
|
+
}
|
|
928
|
+
const roots = childrenOf.get(null) ?? [];
|
|
929
|
+
for (let i = 0; i < roots.length; i++) {
|
|
930
|
+
const isLast = i === roots.length - 1;
|
|
931
|
+
printTask2(roots[i], "", isLast ? "\u2514\u2500 " : "\u251C\u2500 ", isLast);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
console.log(divider);
|
|
935
|
+
return tasks;
|
|
706
936
|
} catch (err) {
|
|
707
|
-
|
|
708
|
-
|
|
937
|
+
console.log(chalk2.yellow(`(Could not load tasks: ${err.message})`));
|
|
938
|
+
return [];
|
|
709
939
|
}
|
|
710
|
-
return { token, clientId: OAUTH_CLIENT_ID };
|
|
711
940
|
}
|
|
712
|
-
async function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
941
|
+
async function printMyTasks(config) {
|
|
942
|
+
try {
|
|
943
|
+
const me = await getAuthenticatedUser(config);
|
|
944
|
+
const tasks = await listMyTasks(config, me);
|
|
945
|
+
if (tasks.length === 0) return;
|
|
946
|
+
const divider = chalk2.dim("\u2500".repeat(70));
|
|
947
|
+
console.log("");
|
|
948
|
+
console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
|
|
949
|
+
console.log(divider);
|
|
950
|
+
for (const t of tasks) {
|
|
951
|
+
const num = `#${t.number}`.padEnd(5);
|
|
952
|
+
const status = colorStatus(getStatus(t));
|
|
953
|
+
const parentNum = getParentIssueNumber(t);
|
|
954
|
+
const parentTag = parentNum ? chalk2.dim(` (sub of #${parentNum})`) : "";
|
|
955
|
+
const maxTitle = parentNum ? 34 : 46;
|
|
956
|
+
const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
|
|
957
|
+
console.log(` ${num}${status}${title}${parentTag}`);
|
|
724
958
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
{
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
},
|
|
733
|
-
{
|
|
734
|
-
name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
|
|
735
|
-
value: "pat"
|
|
736
|
-
}
|
|
737
|
-
]
|
|
738
|
-
});
|
|
739
|
-
let githubToken;
|
|
740
|
-
let githubClientId;
|
|
741
|
-
if (authMethod === "device") {
|
|
742
|
-
const result = await getGitHubTokenViaDeviceFlow();
|
|
743
|
-
githubToken = result.token;
|
|
744
|
-
githubClientId = result.clientId;
|
|
745
|
-
} else {
|
|
746
|
-
const result = await getGitHubTokenViaPAT();
|
|
747
|
-
githubToken = result.token;
|
|
748
|
-
}
|
|
749
|
-
const providerChoice = await select({
|
|
750
|
-
message: "AI provider:",
|
|
751
|
-
choices: [
|
|
752
|
-
{ name: `OpenRouter (recommended) ${chalk2.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
|
|
753
|
-
{ name: "Custom (specify base URL and model)", value: "custom" }
|
|
754
|
-
]
|
|
755
|
-
});
|
|
756
|
-
let aiBaseUrl;
|
|
757
|
-
let aiModel;
|
|
758
|
-
if (providerChoice === "custom") {
|
|
759
|
-
aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
|
|
760
|
-
aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
|
|
761
|
-
}
|
|
762
|
-
const apiKeyHint = providerChoice === "openrouter" ? chalk2.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk2.dim(" API key for your provider\n");
|
|
763
|
-
console.log(apiKeyHint);
|
|
764
|
-
const aiApiKey = await password({
|
|
765
|
-
message: "API Key:",
|
|
766
|
-
mask: "*"
|
|
767
|
-
});
|
|
768
|
-
let owner = detectedOwner;
|
|
769
|
-
let repo = detectedRepo;
|
|
770
|
-
if (!owner || !repo) {
|
|
771
|
-
owner = await input({
|
|
772
|
-
message: "GitHub repo owner (user or org):",
|
|
773
|
-
required: true
|
|
774
|
-
});
|
|
775
|
-
repo = await input({
|
|
776
|
-
message: "GitHub repo name:",
|
|
777
|
-
required: true
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
const config = {
|
|
781
|
-
githubToken,
|
|
782
|
-
githubClientId,
|
|
783
|
-
aiApiKey: aiApiKey.trim(),
|
|
784
|
-
...aiBaseUrl ? { aiBaseUrl } : {},
|
|
785
|
-
...aiModel ? { aiModel } : {},
|
|
786
|
-
github: {
|
|
787
|
-
owner: owner.trim(),
|
|
788
|
-
repo: repo.trim()
|
|
789
|
-
}
|
|
790
|
-
};
|
|
791
|
-
setConfig(config);
|
|
792
|
-
const spinner = ora("Setting up GitHub labels...").start();
|
|
793
|
-
try {
|
|
794
|
-
await ensureLabels(config);
|
|
795
|
-
spinner.succeed("GitHub labels created");
|
|
796
|
-
} catch (err) {
|
|
797
|
-
spinner.fail("Failed to create labels (check token permissions)");
|
|
798
|
-
console.error(chalk2.red(String(err)));
|
|
799
|
-
}
|
|
800
|
-
console.log(chalk2.green("\nSetup complete!"));
|
|
801
|
-
console.log(chalk2.dim(`Config saved to: ${getConfigPath()}
|
|
802
|
-
`));
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// src/commands/config.ts
|
|
806
|
-
import { input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
807
|
-
import chalk3 from "chalk";
|
|
808
|
-
async function configCommand() {
|
|
809
|
-
let config;
|
|
810
|
-
try {
|
|
811
|
-
config = getConfig();
|
|
812
|
-
} catch {
|
|
813
|
-
console.error(chalk3.red("No config found. Run `tch init` first."));
|
|
814
|
-
process.exit(1);
|
|
815
|
-
}
|
|
816
|
-
console.log(chalk3.bold.cyan("\nTechunter \u2014 Settings\n"));
|
|
817
|
-
console.log(chalk3.dim(`Config file: ${getConfigPath()}
|
|
818
|
-
`));
|
|
819
|
-
const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
|
|
820
|
-
const currentModel = config.aiModel ?? DEFAULT_MODEL;
|
|
821
|
-
const currentBaseBranch = config.baseBranch ?? "main";
|
|
822
|
-
const field = await select2({
|
|
823
|
-
message: "Which setting to change?",
|
|
824
|
-
choices: [
|
|
825
|
-
{ name: `GitHub repo ${chalk3.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
|
|
826
|
-
{ name: `Base branch ${chalk3.dim(currentBaseBranch)}`, value: "baseBranch" },
|
|
827
|
-
{ name: `AI base URL ${chalk3.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
|
|
828
|
-
{ name: `AI model ${chalk3.dim(currentModel)}`, value: "aiModel" },
|
|
829
|
-
{ name: `AI API Key ${chalk3.dim("(hidden)")}`, value: "aiApiKey" },
|
|
830
|
-
{ name: `GitHub Token ${chalk3.dim("(hidden)")}`, value: "githubToken" },
|
|
831
|
-
{ name: "Cancel", value: "cancel" }
|
|
832
|
-
]
|
|
833
|
-
});
|
|
834
|
-
if (field === "cancel") return;
|
|
835
|
-
if (field === "baseBranch") {
|
|
836
|
-
const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
|
|
837
|
-
if (val.trim()) {
|
|
838
|
-
setConfig({ baseBranch: val.trim() });
|
|
839
|
-
console.log(chalk3.green(`
|
|
840
|
-
Base branch set to: ${val.trim()}
|
|
841
|
-
`));
|
|
842
|
-
}
|
|
843
|
-
} else if (field === "repo") {
|
|
844
|
-
const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
|
|
845
|
-
const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
|
|
846
|
-
setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
|
|
847
|
-
console.log(chalk3.green(`
|
|
848
|
-
Repo set to: ${owner.trim()}/${repo.trim()}
|
|
849
|
-
`));
|
|
850
|
-
} else if (field === "aiBaseUrl") {
|
|
851
|
-
const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
|
|
852
|
-
if (val.trim()) {
|
|
853
|
-
setConfig({ aiBaseUrl: val.trim() });
|
|
854
|
-
console.log(chalk3.green(`
|
|
855
|
-
AI base URL set to: ${val.trim()}
|
|
856
|
-
`));
|
|
857
|
-
}
|
|
858
|
-
} else if (field === "aiModel") {
|
|
859
|
-
const val = await input2({ message: "AI model name:", default: currentModel });
|
|
860
|
-
if (val.trim()) {
|
|
861
|
-
setConfig({ aiModel: val.trim() });
|
|
862
|
-
console.log(chalk3.green(`
|
|
863
|
-
AI model set to: ${val.trim()}
|
|
864
|
-
`));
|
|
865
|
-
}
|
|
866
|
-
} else if (field === "aiApiKey") {
|
|
867
|
-
const val = await password2({ message: "New AI API Key:", mask: "*" });
|
|
868
|
-
if (val.trim()) {
|
|
869
|
-
setConfig({ aiApiKey: val.trim() });
|
|
870
|
-
console.log(chalk3.green("\nAI API Key updated.\n"));
|
|
871
|
-
}
|
|
872
|
-
} else if (field === "githubToken") {
|
|
873
|
-
const val = await password2({ message: "New GitHub Token:", mask: "*" });
|
|
874
|
-
if (val.trim()) {
|
|
875
|
-
setConfig({ githubToken: val.trim() });
|
|
876
|
-
console.log(chalk3.green("\nGitHub Token updated.\n"));
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// src/index.ts
|
|
882
|
-
init_github();
|
|
883
|
-
|
|
884
|
-
// src/lib/agent.ts
|
|
885
|
-
import ora14 from "ora";
|
|
886
|
-
import chalk13 from "chalk";
|
|
887
|
-
|
|
888
|
-
// src/tools/pick/index.ts
|
|
889
|
-
var pick_exports = {};
|
|
890
|
-
__export(pick_exports, {
|
|
891
|
-
definition: () => definition3,
|
|
892
|
-
execute: () => execute3,
|
|
893
|
-
run: () => run3,
|
|
894
|
-
terminal: () => terminal3
|
|
895
|
-
});
|
|
896
|
-
init_github();
|
|
897
|
-
import chalk8 from "chalk";
|
|
898
|
-
import ora4 from "ora";
|
|
899
|
-
import { select as select5 } from "@inquirer/prompts";
|
|
900
|
-
init_github();
|
|
901
|
-
|
|
902
|
-
// src/lib/markdown.ts
|
|
903
|
-
import { marked } from "marked";
|
|
904
|
-
import { markedTerminal } from "marked-terminal";
|
|
905
|
-
marked.use(markedTerminal({ showSectionPrefix: false }));
|
|
906
|
-
function renderMarkdown(text) {
|
|
907
|
-
return marked(text);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// src/lib/display.ts
|
|
911
|
-
init_github();
|
|
912
|
-
import chalk4 from "chalk";
|
|
913
|
-
var LABEL_AVAILABLE2 = "techunter:available";
|
|
914
|
-
var LABEL_CLAIMED2 = "techunter:claimed";
|
|
915
|
-
var LABEL_IN_REVIEW2 = "techunter:in-review";
|
|
916
|
-
var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
|
|
917
|
-
function getStatus(issue) {
|
|
918
|
-
if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
|
|
919
|
-
if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
|
|
920
|
-
if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
|
|
921
|
-
if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
|
|
922
|
-
return "unknown";
|
|
923
|
-
}
|
|
924
|
-
function colorStatus(status) {
|
|
925
|
-
const padded = status.padEnd(14);
|
|
926
|
-
switch (status) {
|
|
927
|
-
case "available":
|
|
928
|
-
return chalk4.green(padded);
|
|
929
|
-
case "claimed":
|
|
930
|
-
return chalk4.yellow(padded);
|
|
931
|
-
case "in-review":
|
|
932
|
-
return chalk4.blue(padded);
|
|
933
|
-
case "changes-needed":
|
|
934
|
-
return chalk4.red(padded);
|
|
935
|
-
default:
|
|
936
|
-
return padded;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
function printTaskDetail(issue) {
|
|
940
|
-
const divider = chalk4.dim("\u2500".repeat(70));
|
|
941
|
-
console.log("\n" + divider);
|
|
942
|
-
console.log(
|
|
943
|
-
chalk4.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk4.dim(issue.assignee ? `@${issue.assignee}` : "\u2014")
|
|
944
|
-
);
|
|
945
|
-
console.log(chalk4.bold("\n " + issue.title));
|
|
946
|
-
if (issue.body) {
|
|
947
|
-
console.log("");
|
|
948
|
-
console.log(renderMarkdown(issue.body));
|
|
949
|
-
}
|
|
950
|
-
console.log("\n " + chalk4.dim(issue.htmlUrl));
|
|
951
|
-
console.log(divider + "\n");
|
|
952
|
-
}
|
|
953
|
-
async function printTaskList(config) {
|
|
954
|
-
try {
|
|
955
|
-
const tasks = await listTasks(config);
|
|
956
|
-
const divider = chalk4.dim("\u2500".repeat(70));
|
|
957
|
-
console.log("");
|
|
958
|
-
console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
|
|
959
|
-
console.log(divider);
|
|
960
|
-
if (tasks.length === 0) {
|
|
961
|
-
console.log(chalk4.dim(" (no tasks)"));
|
|
962
|
-
} else {
|
|
963
|
-
for (const t of tasks) {
|
|
964
|
-
const num = `#${t.number}`.padEnd(5);
|
|
965
|
-
const status = colorStatus(getStatus(t));
|
|
966
|
-
const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
|
|
967
|
-
const title = t.title.length > 36 ? t.title.slice(0, 33) + "..." : t.title;
|
|
968
|
-
console.log(` ${num}${status}${assignee}${title}`);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
console.log(divider);
|
|
972
|
-
return tasks;
|
|
973
|
-
} catch (err) {
|
|
974
|
-
console.log(chalk4.yellow(`(Could not load tasks: ${err.message})`));
|
|
975
|
-
return [];
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
async function printMyTasks(config) {
|
|
979
|
-
try {
|
|
980
|
-
const me = await getAuthenticatedUser(config);
|
|
981
|
-
const tasks = await listMyTasks(config, me);
|
|
982
|
-
if (tasks.length === 0) return;
|
|
983
|
-
const divider = chalk4.dim("\u2500".repeat(70));
|
|
984
|
-
console.log("");
|
|
985
|
-
console.log(chalk4.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + `My Tasks @${me}`));
|
|
986
|
-
console.log(divider);
|
|
987
|
-
for (const t of tasks) {
|
|
988
|
-
const num = `#${t.number}`.padEnd(5);
|
|
989
|
-
const status = colorStatus(getStatus(t));
|
|
990
|
-
const title = t.title.length > 46 ? t.title.slice(0, 43) + "..." : t.title;
|
|
991
|
-
console.log(` ${num}${status}${title}`);
|
|
992
|
-
}
|
|
993
|
-
console.log(divider);
|
|
994
|
-
const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
|
|
995
|
-
if (rejectedTasks.length > 0) {
|
|
996
|
-
let currentBranch = "";
|
|
997
|
-
try {
|
|
998
|
-
currentBranch = await getCurrentBranch();
|
|
999
|
-
} catch {
|
|
959
|
+
console.log(divider);
|
|
960
|
+
const rejectedTasks = tasks.filter((t) => t.labels.includes(LABEL_CHANGES_NEEDED2));
|
|
961
|
+
if (rejectedTasks.length > 0) {
|
|
962
|
+
let currentBranch = "";
|
|
963
|
+
try {
|
|
964
|
+
currentBranch = await getCurrentBranch();
|
|
965
|
+
} catch {
|
|
1000
966
|
}
|
|
1001
967
|
console.log("");
|
|
1002
968
|
for (const t of rejectedTasks) {
|
|
1003
|
-
const
|
|
1004
|
-
const onCorrectBranch = currentBranch ===
|
|
969
|
+
const taskBranch = t.assignee ? makeTaskBranchName(t.number, t.assignee) : `task-${t.number}`;
|
|
970
|
+
const onCorrectBranch = currentBranch === taskBranch;
|
|
1005
971
|
console.log(
|
|
1006
|
-
|
|
972
|
+
chalk2.red.bold(" \u26A0 Changes requested") + chalk2.red(` on #${t.number} "${t.title}"`)
|
|
1007
973
|
);
|
|
1008
974
|
if (!onCorrectBranch) {
|
|
1009
975
|
console.log(
|
|
1010
|
-
|
|
976
|
+
chalk2.dim(" Switch branch: ") + chalk2.cyan(`git checkout ${taskBranch}`)
|
|
1011
977
|
);
|
|
1012
978
|
}
|
|
1013
979
|
}
|
|
@@ -1019,7 +985,7 @@ async function printMyTasks(config) {
|
|
|
1019
985
|
|
|
1020
986
|
// src/lib/launch.ts
|
|
1021
987
|
import { spawn } from "child_process";
|
|
1022
|
-
import
|
|
988
|
+
import chalk3 from "chalk";
|
|
1023
989
|
function buildClaudePrompt(issue, branch) {
|
|
1024
990
|
const lines = [
|
|
1025
991
|
`You are working on task #${issue.number}: ${issue.title}`,
|
|
@@ -1035,14 +1001,14 @@ function buildClaudePrompt(issue, branch) {
|
|
|
1035
1001
|
}
|
|
1036
1002
|
async function launchClaudeCode(issue, branch) {
|
|
1037
1003
|
const prompt = buildClaudePrompt(issue, branch);
|
|
1038
|
-
console.log(
|
|
1004
|
+
console.log(chalk3.dim("\n Launching Claude Code\u2026\n"));
|
|
1039
1005
|
await new Promise((resolve) => {
|
|
1040
1006
|
const safePrompt = prompt.replace(/\r?\n/g, " ").replace(/"/g, "'");
|
|
1041
1007
|
const child = spawn(`claude "${safePrompt}"`, [], { stdio: "inherit", shell: true });
|
|
1042
1008
|
child.on("close", () => resolve());
|
|
1043
1009
|
child.on("error", () => {
|
|
1044
1010
|
console.log(
|
|
1045
|
-
|
|
1011
|
+
chalk3.yellow(
|
|
1046
1012
|
" Could not launch claude. Make sure Claude Code is installed:\n npm install -g @anthropic-ai/claude-code"
|
|
1047
1013
|
)
|
|
1048
1014
|
);
|
|
@@ -1060,78 +1026,9 @@ __export(submit_exports, {
|
|
|
1060
1026
|
terminal: () => terminal
|
|
1061
1027
|
});
|
|
1062
1028
|
init_github();
|
|
1063
|
-
import
|
|
1064
|
-
import
|
|
1065
|
-
import { select
|
|
1066
|
-
|
|
1067
|
-
// src/lib/agent-ui.ts
|
|
1068
|
-
import chalk6 from "chalk";
|
|
1069
|
-
function formatInput(input3) {
|
|
1070
|
-
return Object.entries(input3).map(([k, v]) => {
|
|
1071
|
-
if (typeof v === "number") return `${k}=${v}`;
|
|
1072
|
-
if (typeof v === "string") {
|
|
1073
|
-
if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
|
|
1074
|
-
return `${k}="${v}"`;
|
|
1075
|
-
}
|
|
1076
|
-
return `${k}=${JSON.stringify(v)}`;
|
|
1077
|
-
}).join(" ");
|
|
1078
|
-
}
|
|
1079
|
-
function summarize(result) {
|
|
1080
|
-
const first = result.split("\n").find((l) => l.trim()) ?? result;
|
|
1081
|
-
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
1082
|
-
}
|
|
1083
|
-
function printToolCall(name, input3) {
|
|
1084
|
-
const params = formatInput(input3);
|
|
1085
|
-
console.log(` ${chalk6.cyan("\u2192")} ${chalk6.bold(name)}${params ? " " + chalk6.dim(params) : ""}`);
|
|
1086
|
-
}
|
|
1087
|
-
function printToolResult(result) {
|
|
1088
|
-
const ok = !result.startsWith("Error:");
|
|
1089
|
-
const icon = ok ? chalk6.green("\u2713") : chalk6.red("\u2717");
|
|
1090
|
-
console.log(` ${icon} ${chalk6.dim(summarize(result))}`);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// src/lib/sub-agent.ts
|
|
1094
|
-
async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
1095
|
-
const client = createClient(config);
|
|
1096
|
-
const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
|
|
1097
|
-
const tools2 = selected.map((m) => m.definition);
|
|
1098
|
-
const messages = [
|
|
1099
|
-
{ role: "system", content: systemPrompt },
|
|
1100
|
-
{ role: "user", content: userMessage }
|
|
1101
|
-
];
|
|
1102
|
-
const MAX_ITERATIONS = 100;
|
|
1103
|
-
let iterations = 0;
|
|
1104
|
-
for (; ; ) {
|
|
1105
|
-
if (++iterations > MAX_ITERATIONS) {
|
|
1106
|
-
throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
1107
|
-
}
|
|
1108
|
-
const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
|
|
1109
|
-
const choice = res.choices[0];
|
|
1110
|
-
messages.push({
|
|
1111
|
-
role: "assistant",
|
|
1112
|
-
content: choice.message.content ?? null,
|
|
1113
|
-
...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
|
|
1114
|
-
});
|
|
1115
|
-
if (choice.finish_reason === "stop") {
|
|
1116
|
-
return choice.message.content ?? "";
|
|
1117
|
-
}
|
|
1118
|
-
if (choice.finish_reason === "tool_calls") {
|
|
1119
|
-
for (const tc of choice.message.tool_calls ?? []) {
|
|
1120
|
-
let input3;
|
|
1121
|
-
try {
|
|
1122
|
-
input3 = JSON.parse(tc.function.arguments);
|
|
1123
|
-
} catch {
|
|
1124
|
-
input3 = {};
|
|
1125
|
-
}
|
|
1126
|
-
printToolCall(tc.function.name, input3);
|
|
1127
|
-
const mod = selected.find((m) => m.definition.function.name === tc.function.name);
|
|
1128
|
-
const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
|
|
1129
|
-
printToolResult(result);
|
|
1130
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1029
|
+
import chalk4 from "chalk";
|
|
1030
|
+
import ora from "ora";
|
|
1031
|
+
import { select, input as promptInput } from "@inquirer/prompts";
|
|
1135
1032
|
|
|
1136
1033
|
// src/tools/submit/prompts.ts
|
|
1137
1034
|
var REVIEWER_SYSTEM_PROMPT = "You are a concise code reviewer. The diff provided shows all changes on this worker branch since the task was claimed. The branch is shared across tasks, so the diff may contain changes unrelated to this specific task \u2014 use the task title and acceptance criteria to identify which changes are relevant, and ignore the rest. Use run_command to run tests/lint if needed, and read_file to inspect specific files. Then output your review: for each acceptance criterion mark \u2705 met or \u274C not met with a one-line reason. End with an overall verdict line: Ready to submit / Not ready. Reply in the same language as the task.";
|
|
@@ -1169,11 +1066,21 @@ var definition = {
|
|
|
1169
1066
|
};
|
|
1170
1067
|
async function run(_input, config) {
|
|
1171
1068
|
const taskState = getConfig().taskState;
|
|
1172
|
-
const
|
|
1069
|
+
const currentBranch = await getCurrentBranch();
|
|
1070
|
+
let issueNumber = taskState?.activeIssueNumber && taskState?.activeBranch && currentBranch === taskState.activeBranch ? taskState.activeIssueNumber : void 0;
|
|
1173
1071
|
if (!issueNumber) {
|
|
1174
|
-
|
|
1072
|
+
const fromBranch = parseIssueNumberFromBranch(currentBranch);
|
|
1073
|
+
if (fromBranch) {
|
|
1074
|
+
issueNumber = fromBranch;
|
|
1075
|
+
} else {
|
|
1076
|
+
const found = await getIssueNumberFromBranch(config, currentBranch);
|
|
1077
|
+
if (!found) {
|
|
1078
|
+
return "No active task found. Claim a task first with /pick.";
|
|
1079
|
+
}
|
|
1080
|
+
issueNumber = found.issueNumber;
|
|
1081
|
+
}
|
|
1175
1082
|
}
|
|
1176
|
-
let spinner =
|
|
1083
|
+
let spinner = ora("Loading task and diff\u2026").start();
|
|
1177
1084
|
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
|
|
1178
1085
|
const [issue, diff, me] = await Promise.all([
|
|
1179
1086
|
getTask(config, issueNumber),
|
|
@@ -1181,12 +1088,19 @@ async function run(_input, config) {
|
|
|
1181
1088
|
getAuthenticatedUser(config)
|
|
1182
1089
|
]);
|
|
1183
1090
|
spinner.stop();
|
|
1184
|
-
const
|
|
1091
|
+
const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
|
|
1185
1092
|
const branch = await getCurrentBranch();
|
|
1186
1093
|
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
1094
|
+
spinner = ora("Checking for open sub-tasks\u2026").start();
|
|
1095
|
+
const openSubtaskNumbers = await getOpenSubtasks(config, branch);
|
|
1096
|
+
spinner.stop();
|
|
1097
|
+
if (openSubtaskNumbers.length > 0) {
|
|
1098
|
+
return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open:
|
|
1099
|
+
` + openSubtaskNumbers.map((n) => ` - #${n}`).join("\n") + "\nComplete all sub-tasks before submitting.";
|
|
1100
|
+
}
|
|
1187
1101
|
let review = "";
|
|
1188
1102
|
if (!isSelfSubmit) {
|
|
1189
|
-
const reviewSpinner =
|
|
1103
|
+
const reviewSpinner = ora("Reviewing changes\u2026").start();
|
|
1190
1104
|
try {
|
|
1191
1105
|
review = await reviewChanges(config, issueNumber, issue, diff);
|
|
1192
1106
|
} catch (err) {
|
|
@@ -1194,19 +1108,19 @@ async function run(_input, config) {
|
|
|
1194
1108
|
}
|
|
1195
1109
|
reviewSpinner.stop();
|
|
1196
1110
|
}
|
|
1197
|
-
const divider =
|
|
1111
|
+
const divider = chalk4.dim("\u2500".repeat(70));
|
|
1198
1112
|
console.log("\n" + divider);
|
|
1199
1113
|
if (isSelfSubmit) {
|
|
1200
|
-
console.log(
|
|
1114
|
+
console.log(chalk4.yellow(` Self-submit detected \u2014 AI review skipped.`));
|
|
1201
1115
|
} else {
|
|
1202
|
-
console.log(
|
|
1116
|
+
console.log(chalk4.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
|
|
1203
1117
|
console.log(divider);
|
|
1204
1118
|
console.log(renderMarkdown(review));
|
|
1205
1119
|
}
|
|
1206
1120
|
console.log(divider + "\n");
|
|
1207
1121
|
let shouldProceed;
|
|
1208
1122
|
try {
|
|
1209
|
-
shouldProceed = await
|
|
1123
|
+
shouldProceed = await select({
|
|
1210
1124
|
message: `Submit task #${issueNumber}?`,
|
|
1211
1125
|
choices: [
|
|
1212
1126
|
{ name: "Yes, submit", value: true },
|
|
@@ -1227,7 +1141,7 @@ async function run(_input, config) {
|
|
|
1227
1141
|
return "Submit cancelled.";
|
|
1228
1142
|
}
|
|
1229
1143
|
if (!commitMessage.trim()) return "Submit cancelled.";
|
|
1230
|
-
spinner =
|
|
1144
|
+
spinner = ora("Committing and pushing\u2026").start();
|
|
1231
1145
|
try {
|
|
1232
1146
|
await stageAllAndCommit(commitMessage.trim());
|
|
1233
1147
|
spinner.stop();
|
|
@@ -1236,52 +1150,71 @@ async function run(_input, config) {
|
|
|
1236
1150
|
return `Commit failed: ${err.message}`;
|
|
1237
1151
|
}
|
|
1238
1152
|
if (isSelfSubmit) {
|
|
1239
|
-
spinner =
|
|
1153
|
+
spinner = ora("Closing issue\u2026").start();
|
|
1240
1154
|
try {
|
|
1241
1155
|
await closeTask(config, issueNumber);
|
|
1242
1156
|
spinner.stop();
|
|
1243
1157
|
} catch (err) {
|
|
1244
1158
|
spinner.stop();
|
|
1245
|
-
console.error(
|
|
1159
|
+
console.error(chalk4.yellow(`Warning: failed to close issue: ${err.message}`));
|
|
1246
1160
|
}
|
|
1247
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1161
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1248
1162
|
return `Task #${issueNumber} committed and closed.
|
|
1249
1163
|
Commit: "${commitMessage.trim()}"`;
|
|
1250
1164
|
}
|
|
1251
|
-
spinner =
|
|
1165
|
+
spinner = ora("Checking for existing PR\u2026").start();
|
|
1166
|
+
const existingPR = await getTaskPR(config, issueNumber);
|
|
1167
|
+
spinner.stop();
|
|
1252
1168
|
let prUrl;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1169
|
+
if (existingPR) {
|
|
1170
|
+
prUrl = existingPR.url;
|
|
1171
|
+
console.log(chalk4.dim(` Existing PR found: ${prUrl} \u2014 updating.`));
|
|
1172
|
+
} else {
|
|
1173
|
+
spinner = ora("Creating pull request\u2026").start();
|
|
1174
|
+
try {
|
|
1175
|
+
await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
|
|
1176
|
+
const prBody = [
|
|
1177
|
+
`Closes #${issueNumber}`,
|
|
1178
|
+
issue.body ? `
|
|
1257
1179
|
${issue.body}` : "",
|
|
1258
|
-
|
|
1180
|
+
review ? `
|
|
1259
1181
|
## AI Review
|
|
1260
1182
|
${review}` : ""
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1183
|
+
].join("\n").trim();
|
|
1184
|
+
prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
|
|
1185
|
+
spinner.stop();
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
spinner.stop();
|
|
1188
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1189
|
+
}
|
|
1267
1190
|
}
|
|
1268
|
-
spinner =
|
|
1191
|
+
spinner = ora("Marking as in-review\u2026").start();
|
|
1269
1192
|
try {
|
|
1270
1193
|
await markInReview(config, issueNumber);
|
|
1271
1194
|
spinner.stop();
|
|
1272
1195
|
} catch (err) {
|
|
1273
1196
|
spinner.stop();
|
|
1274
|
-
return `PR created (${prUrl}) but failed to update label: ${err.message}`;
|
|
1197
|
+
return `PR ${existingPR ? "updated" : "created"} (${prUrl}) but failed to update label: ${err.message}`;
|
|
1275
1198
|
}
|
|
1276
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1277
|
-
return `Task #${issueNumber} submitted.
|
|
1199
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1200
|
+
return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
|
|
1278
1201
|
Commit: "${commitMessage.trim()}"
|
|
1279
1202
|
PR: ${prUrl}`;
|
|
1280
1203
|
}
|
|
1281
1204
|
async function execute(input3, config) {
|
|
1282
1205
|
const taskState = getConfig().taskState;
|
|
1283
|
-
|
|
1284
|
-
if (!issueNumber)
|
|
1206
|
+
let issueNumber = taskState?.activeIssueNumber;
|
|
1207
|
+
if (!issueNumber) {
|
|
1208
|
+
const currentBranch = await getCurrentBranch();
|
|
1209
|
+
const fromBranch = parseIssueNumberFromBranch(currentBranch);
|
|
1210
|
+
if (fromBranch) {
|
|
1211
|
+
issueNumber = fromBranch;
|
|
1212
|
+
} else {
|
|
1213
|
+
const found = await getIssueNumberFromBranch(config, currentBranch);
|
|
1214
|
+
if (!found) return "No active task found. Claim a task first.";
|
|
1215
|
+
issueNumber = found.issueNumber;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1285
1218
|
const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
|
|
1286
1219
|
const [issue, diff, branch, me] = await Promise.all([
|
|
1287
1220
|
getTask(config, issueNumber),
|
|
@@ -1289,7 +1222,11 @@ async function execute(input3, config) {
|
|
|
1289
1222
|
getCurrentBranch(),
|
|
1290
1223
|
getAuthenticatedUser(config)
|
|
1291
1224
|
]);
|
|
1292
|
-
const
|
|
1225
|
+
const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
|
|
1226
|
+
const openSubtaskNumbers = await getOpenSubtasks(config, branch);
|
|
1227
|
+
if (openSubtaskNumbers.length > 0) {
|
|
1228
|
+
return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open: ` + openSubtaskNumbers.map((n) => `#${n}`).join(", ");
|
|
1229
|
+
}
|
|
1293
1230
|
const isSelfSubmit = issue.author !== null && issue.author === me;
|
|
1294
1231
|
let review = "";
|
|
1295
1232
|
if (!isSelfSubmit) {
|
|
@@ -1310,29 +1247,36 @@ async function execute(input3, config) {
|
|
|
1310
1247
|
await closeTask(config, issueNumber);
|
|
1311
1248
|
} catch {
|
|
1312
1249
|
}
|
|
1313
|
-
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0 } });
|
|
1250
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1314
1251
|
return `Task #${issueNumber} committed and closed.
|
|
1315
1252
|
Commit: "${commitMessage}"`;
|
|
1316
1253
|
}
|
|
1254
|
+
const existingPR = await getTaskPR(config, issueNumber);
|
|
1317
1255
|
let prUrl;
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1256
|
+
if (existingPR) {
|
|
1257
|
+
prUrl = existingPR.url;
|
|
1258
|
+
} else {
|
|
1259
|
+
try {
|
|
1260
|
+
await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
|
|
1261
|
+
const prBody = [
|
|
1262
|
+
`Closes #${issueNumber}`,
|
|
1263
|
+
issue.body ? `
|
|
1322
1264
|
${issue.body}` : "",
|
|
1323
|
-
|
|
1265
|
+
review ? `
|
|
1324
1266
|
## AI Review
|
|
1325
1267
|
${review}` : ""
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1268
|
+
].join("\n").trim();
|
|
1269
|
+
prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
return `Committed but PR creation failed: ${err.message}`;
|
|
1272
|
+
}
|
|
1330
1273
|
}
|
|
1331
1274
|
try {
|
|
1332
1275
|
await markInReview(config, issueNumber);
|
|
1333
1276
|
} catch {
|
|
1334
1277
|
}
|
|
1335
|
-
|
|
1278
|
+
setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
|
|
1279
|
+
return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
|
|
1336
1280
|
Review:
|
|
1337
1281
|
${review}
|
|
1338
1282
|
Commit: "${commitMessage}"
|
|
@@ -1349,8 +1293,8 @@ __export(close_exports, {
|
|
|
1349
1293
|
terminal: () => terminal2
|
|
1350
1294
|
});
|
|
1351
1295
|
init_github();
|
|
1352
|
-
import { select as
|
|
1353
|
-
import
|
|
1296
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
1297
|
+
import ora2 from "ora";
|
|
1354
1298
|
var definition2 = {
|
|
1355
1299
|
type: "function",
|
|
1356
1300
|
function: {
|
|
@@ -1376,7 +1320,7 @@ async function run2(input3, config) {
|
|
|
1376
1320
|
}
|
|
1377
1321
|
if (tasks.length === 0) return "No tasks found.";
|
|
1378
1322
|
try {
|
|
1379
|
-
issueNumber = await
|
|
1323
|
+
issueNumber = await select2({
|
|
1380
1324
|
message: "Select task to close:",
|
|
1381
1325
|
choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
|
|
1382
1326
|
});
|
|
@@ -1386,7 +1330,7 @@ async function run2(input3, config) {
|
|
|
1386
1330
|
}
|
|
1387
1331
|
let confirmed;
|
|
1388
1332
|
try {
|
|
1389
|
-
confirmed = await
|
|
1333
|
+
confirmed = await select2({
|
|
1390
1334
|
message: `Close task #${issueNumber}?`,
|
|
1391
1335
|
choices: [
|
|
1392
1336
|
{ name: "Yes, close it", value: true },
|
|
@@ -1397,7 +1341,7 @@ async function run2(input3, config) {
|
|
|
1397
1341
|
return "Cancelled.";
|
|
1398
1342
|
}
|
|
1399
1343
|
if (!confirmed) return "Cancelled.";
|
|
1400
|
-
const spinner =
|
|
1344
|
+
const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
|
|
1401
1345
|
try {
|
|
1402
1346
|
await closeTask(config, issueNumber);
|
|
1403
1347
|
spinner.stop();
|
|
@@ -1409,7 +1353,7 @@ async function run2(input3, config) {
|
|
|
1409
1353
|
}
|
|
1410
1354
|
async function execute2(input3, config) {
|
|
1411
1355
|
const issueNumber = input3["issue_number"];
|
|
1412
|
-
const spinner =
|
|
1356
|
+
const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
|
|
1413
1357
|
try {
|
|
1414
1358
|
await closeTask(config, issueNumber);
|
|
1415
1359
|
spinner.stop();
|
|
@@ -1455,7 +1399,7 @@ async function run3(input3, config) {
|
|
|
1455
1399
|
}
|
|
1456
1400
|
if (tasks.length === 0) return "No tasks found.";
|
|
1457
1401
|
try {
|
|
1458
|
-
chosenNumber = await
|
|
1402
|
+
chosenNumber = await select3({
|
|
1459
1403
|
message: "Select a task:",
|
|
1460
1404
|
choices: tasks.map((t) => ({
|
|
1461
1405
|
name: `#${String(t.number).padEnd(4)} ${colorStatus(getStatus(t))} ${t.title}`,
|
|
@@ -1479,9 +1423,9 @@ async function run3(input3, config) {
|
|
|
1479
1423
|
const comments = await listComments(config, issue.number, 1);
|
|
1480
1424
|
if (comments.length > 0) {
|
|
1481
1425
|
const c = comments[0];
|
|
1482
|
-
const divider =
|
|
1426
|
+
const divider = chalk5.dim("\u2500".repeat(70));
|
|
1483
1427
|
console.log(
|
|
1484
|
-
|
|
1428
|
+
chalk5.red.bold(" Latest rejection feedback") + chalk5.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
|
|
1485
1429
|
);
|
|
1486
1430
|
console.log(divider);
|
|
1487
1431
|
console.log(renderMarkdown(c.body));
|
|
@@ -1491,22 +1435,36 @@ async function run3(input3, config) {
|
|
|
1491
1435
|
}
|
|
1492
1436
|
}
|
|
1493
1437
|
const actions = [];
|
|
1494
|
-
if (status === "available")
|
|
1495
|
-
|
|
1438
|
+
if (status === "available") {
|
|
1439
|
+
actions.push({ name: "Claim this task", value: "claim" });
|
|
1440
|
+
}
|
|
1441
|
+
if (status === "claimed") {
|
|
1442
|
+
actions.push({ name: "Submit this task", value: "submit" });
|
|
1443
|
+
}
|
|
1444
|
+
if (status === "changes-needed") {
|
|
1445
|
+
const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1446
|
+
const me = await getAuthenticatedUser2(config);
|
|
1447
|
+
const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
|
|
1448
|
+
const currentBranch = await getCurrentBranch();
|
|
1449
|
+
if (currentBranch === taskBranch) {
|
|
1450
|
+
actions.push({ name: "Submit this task (fixes done)", value: "submit" });
|
|
1451
|
+
} else {
|
|
1452
|
+
actions.push({ name: `Switch to ${taskBranch} to fix`, value: "switch-fix" });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1496
1455
|
actions.push({ name: "Close this task", value: "close" });
|
|
1497
1456
|
actions.push({ name: "Nothing, just viewing", value: "none" });
|
|
1498
1457
|
let action;
|
|
1499
1458
|
try {
|
|
1500
|
-
action = await
|
|
1459
|
+
action = await select3({ message: "Action:", choices: actions });
|
|
1501
1460
|
} catch {
|
|
1502
1461
|
return "Cancelled.";
|
|
1503
1462
|
}
|
|
1504
1463
|
if (action === "none") return `Viewed task #${issue.number}.`;
|
|
1505
1464
|
if (action === "claim") {
|
|
1506
1465
|
try {
|
|
1507
|
-
const
|
|
1508
|
-
const
|
|
1509
|
-
const myTasks = await listMyTasks2(config, me);
|
|
1466
|
+
const me = await getAuthenticatedUser(config);
|
|
1467
|
+
const myTasks = await listMyTasks(config, me);
|
|
1510
1468
|
const activeTask = myTasks.find((t) => {
|
|
1511
1469
|
const labels = t.labels;
|
|
1512
1470
|
return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
|
|
@@ -1515,37 +1473,65 @@ async function run3(input3, config) {
|
|
|
1515
1473
|
return `You already have an active task: #${activeTask.number} "${activeTask.title}"
|
|
1516
1474
|
Finish or submit it before claiming a new one.`;
|
|
1517
1475
|
}
|
|
1518
|
-
let
|
|
1476
|
+
let stashed = false;
|
|
1477
|
+
if (await hasUncommittedChanges()) {
|
|
1478
|
+
let choice;
|
|
1479
|
+
try {
|
|
1480
|
+
choice = await select3({
|
|
1481
|
+
message: "You have uncommitted changes. What would you like to do?",
|
|
1482
|
+
choices: [
|
|
1483
|
+
{ name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
|
|
1484
|
+
{ name: "Cancel", value: "cancel" }
|
|
1485
|
+
]
|
|
1486
|
+
});
|
|
1487
|
+
} catch {
|
|
1488
|
+
choice = "cancel";
|
|
1489
|
+
}
|
|
1490
|
+
if (choice === "cancel") return "Cancelled.";
|
|
1491
|
+
await stash(`tch: before claiming #${issue.number}`);
|
|
1492
|
+
stashed = true;
|
|
1493
|
+
console.log(chalk5.dim(" Changes stashed. Run `git stash pop` after you finish this task to restore them."));
|
|
1494
|
+
}
|
|
1495
|
+
let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
|
|
1519
1496
|
await claimTask(config, issue.number, me);
|
|
1520
1497
|
spinner.stop();
|
|
1521
|
-
const
|
|
1498
|
+
const taskBranch = makeTaskBranchName(issue.number, me);
|
|
1522
1499
|
const taskBase = extractBaseCommit(issue.body);
|
|
1523
|
-
spinner =
|
|
1500
|
+
spinner = ora3(`Creating branch ${taskBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
|
|
1524
1501
|
try {
|
|
1525
1502
|
if (taskBase) {
|
|
1526
|
-
await
|
|
1503
|
+
await checkoutFromCommit(taskBranch, taskBase);
|
|
1527
1504
|
} else {
|
|
1528
|
-
await switchToBranchOrCreate(
|
|
1505
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1529
1506
|
}
|
|
1530
1507
|
spinner.stop();
|
|
1531
|
-
spinner =
|
|
1508
|
+
spinner = ora3("Pushing task branch\u2026").start();
|
|
1532
1509
|
try {
|
|
1533
|
-
await pushBranch(
|
|
1510
|
+
await pushBranch(taskBranch);
|
|
1534
1511
|
spinner.stop();
|
|
1535
1512
|
} catch {
|
|
1536
|
-
spinner.warn("Could not push
|
|
1513
|
+
spinner.warn("Could not push task branch \u2014 will push on submit");
|
|
1537
1514
|
}
|
|
1538
|
-
} catch {
|
|
1539
|
-
spinner.warn(`Could not switch to ${
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
spinner.warn(`Could not switch to ${taskBranch}`);
|
|
1517
|
+
if (stashed) {
|
|
1518
|
+
try {
|
|
1519
|
+
await stashPop();
|
|
1520
|
+
console.log(chalk5.dim(" Restored stashed changes."));
|
|
1521
|
+
} catch {
|
|
1522
|
+
console.log(chalk5.yellow(" Warning: could not restore stash automatically. Run `git stash pop` manually."));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
throw err;
|
|
1540
1526
|
}
|
|
1541
1527
|
const baseCommit = await getCurrentCommit();
|
|
1542
|
-
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit } });
|
|
1543
|
-
console.log(
|
|
1544
|
-
Claimed! Branch: ${
|
|
1528
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
|
|
1529
|
+
console.log(chalk5.green(`
|
|
1530
|
+
Claimed! Branch: ${taskBranch} (base: ${baseCommit.slice(0, 7)})
|
|
1545
1531
|
`));
|
|
1546
1532
|
let openClaude;
|
|
1547
1533
|
try {
|
|
1548
|
-
openClaude = await
|
|
1534
|
+
openClaude = await select3({
|
|
1549
1535
|
message: "Open Claude Code for this task?",
|
|
1550
1536
|
choices: [
|
|
1551
1537
|
{ name: "Yes, start coding now", value: true },
|
|
@@ -1555,12 +1541,58 @@ Finish or submit it before claiming a new one.`;
|
|
|
1555
1541
|
} catch {
|
|
1556
1542
|
openClaude = false;
|
|
1557
1543
|
}
|
|
1558
|
-
if (openClaude) await launchClaudeCode(issue,
|
|
1559
|
-
return `Task #${issue.number} claimed. Branch: ${
|
|
1544
|
+
if (openClaude) await launchClaudeCode(issue, taskBranch);
|
|
1545
|
+
return `Task #${issue.number} claimed. Branch: ${taskBranch}`;
|
|
1560
1546
|
} catch (err) {
|
|
1561
1547
|
return `Error claiming task: ${err.message}`;
|
|
1562
1548
|
}
|
|
1563
1549
|
}
|
|
1550
|
+
if (action === "switch-fix") {
|
|
1551
|
+
const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
1552
|
+
const me = await getAuthenticatedUser2(config);
|
|
1553
|
+
const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
|
|
1554
|
+
let stashed = false;
|
|
1555
|
+
if (await hasUncommittedChanges()) {
|
|
1556
|
+
let choice;
|
|
1557
|
+
try {
|
|
1558
|
+
choice = await select3({
|
|
1559
|
+
message: "You have uncommitted changes. What would you like to do?",
|
|
1560
|
+
choices: [
|
|
1561
|
+
{ name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
|
|
1562
|
+
{ name: "Cancel", value: "cancel" }
|
|
1563
|
+
]
|
|
1564
|
+
});
|
|
1565
|
+
} catch {
|
|
1566
|
+
choice = "cancel";
|
|
1567
|
+
}
|
|
1568
|
+
if (choice === "cancel") return "Cancelled.";
|
|
1569
|
+
await stash(`tch: before switching to ${taskBranch}`);
|
|
1570
|
+
stashed = true;
|
|
1571
|
+
console.log(chalk5.dim(" Changes stashed. Run `git stash pop` to restore them later."));
|
|
1572
|
+
}
|
|
1573
|
+
const spinner = ora3(`Switching to ${taskBranch}\u2026`).start();
|
|
1574
|
+
try {
|
|
1575
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1576
|
+
spinner.stop();
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
spinner.warn(`Could not switch to ${taskBranch}: ${err.message}`);
|
|
1579
|
+
if (stashed) {
|
|
1580
|
+
try {
|
|
1581
|
+
await stashPop();
|
|
1582
|
+
console.log(chalk5.dim(" Restored stashed changes."));
|
|
1583
|
+
} catch {
|
|
1584
|
+
console.log(chalk5.yellow(" Run `git stash pop` manually to restore your changes."));
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return `Error: ${err.message}`;
|
|
1588
|
+
}
|
|
1589
|
+
const baseCommit = extractBaseCommit(issue.body) ?? await getCurrentCommit();
|
|
1590
|
+
setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
|
|
1591
|
+
console.log(chalk5.green(`
|
|
1592
|
+
Switched to ${taskBranch}. Fix the issues then run /submit.
|
|
1593
|
+
`));
|
|
1594
|
+
return `Switched to ${taskBranch} for task #${issue.number}.`;
|
|
1595
|
+
}
|
|
1564
1596
|
if (action === "submit") return run({}, config);
|
|
1565
1597
|
if (action === "close") return run2({ issue_number: issue.number }, config);
|
|
1566
1598
|
return "Cancelled.";
|
|
@@ -1589,28 +1621,31 @@ async function execute3(input3, config) {
|
|
|
1589
1621
|
if (activeTask) {
|
|
1590
1622
|
return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
|
|
1591
1623
|
}
|
|
1624
|
+
if (await hasUncommittedChanges()) {
|
|
1625
|
+
return "Cannot claim: you have uncommitted changes. Commit or stash them first (git stash).";
|
|
1626
|
+
}
|
|
1592
1627
|
try {
|
|
1593
1628
|
await claimTask(config, issueNumber, me);
|
|
1594
1629
|
} catch (err) {
|
|
1595
1630
|
return `Error claiming task: ${err.message}`;
|
|
1596
1631
|
}
|
|
1597
|
-
const
|
|
1632
|
+
const taskBranch = makeTaskBranchName(issue.number, me);
|
|
1598
1633
|
const taskBase = extractBaseCommit(issue.body);
|
|
1599
1634
|
try {
|
|
1600
1635
|
if (taskBase) {
|
|
1601
|
-
await
|
|
1636
|
+
await checkoutFromCommit(taskBranch, taskBase);
|
|
1602
1637
|
} else {
|
|
1603
|
-
await switchToBranchOrCreate(
|
|
1638
|
+
await switchToBranchOrCreate(taskBranch);
|
|
1604
1639
|
}
|
|
1605
1640
|
} catch {
|
|
1606
1641
|
}
|
|
1607
1642
|
try {
|
|
1608
|
-
await pushBranch(
|
|
1643
|
+
await pushBranch(taskBranch);
|
|
1609
1644
|
} catch {
|
|
1610
1645
|
}
|
|
1611
1646
|
const baseCommit = await getCurrentCommit();
|
|
1612
|
-
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit } });
|
|
1613
|
-
return `Task #${issueNumber} claimed. Branch: ${
|
|
1647
|
+
setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit, activeBranch: taskBranch } });
|
|
1648
|
+
return `Task #${issueNumber} claimed. Branch: ${taskBranch} (base commit: ${baseCommit.slice(0, 7)})`;
|
|
1614
1649
|
}
|
|
1615
1650
|
return `Unknown action: ${action}`;
|
|
1616
1651
|
}
|
|
@@ -1625,14 +1660,14 @@ __export(new_task_exports, {
|
|
|
1625
1660
|
terminal: () => terminal4
|
|
1626
1661
|
});
|
|
1627
1662
|
init_github();
|
|
1628
|
-
import { select as
|
|
1663
|
+
import { select as select4, input as promptInput2 } from "@inquirer/prompts";
|
|
1629
1664
|
import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
|
|
1630
1665
|
import { spawn as spawn2 } from "child_process";
|
|
1631
1666
|
import { tmpdir } from "os";
|
|
1632
1667
|
import path from "path";
|
|
1633
|
-
import
|
|
1634
|
-
import
|
|
1635
|
-
import
|
|
1668
|
+
import ora4 from "ora";
|
|
1669
|
+
import chalk6 from "chalk";
|
|
1670
|
+
import open from "open";
|
|
1636
1671
|
|
|
1637
1672
|
// src/tools/new-task/prompts.ts
|
|
1638
1673
|
var GUIDE_FORMAT = `
|
|
@@ -1685,6 +1720,77 @@ async function openInEditor(content) {
|
|
|
1685
1720
|
await rm(dir, { recursive: true, force: true });
|
|
1686
1721
|
}
|
|
1687
1722
|
}
|
|
1723
|
+
async function resolveBaseAndTarget(config, me, interactive) {
|
|
1724
|
+
const currentBranch = await getCurrentBranch();
|
|
1725
|
+
if (isTaskBranch(currentBranch)) {
|
|
1726
|
+
if (await hasUncommittedChanges()) {
|
|
1727
|
+
if (!interactive) {
|
|
1728
|
+
throw new Error("Cannot create sub-task: you have uncommitted changes. Commit them first so the executor starts from the correct base.");
|
|
1729
|
+
}
|
|
1730
|
+
const { select: inquirerSelect } = await import("@inquirer/prompts");
|
|
1731
|
+
let choice;
|
|
1732
|
+
try {
|
|
1733
|
+
choice = await inquirerSelect({
|
|
1734
|
+
message: "You have uncommitted changes. The sub-task executor will start from the last commit \u2014 they won't see your current unsaved work.",
|
|
1735
|
+
choices: [
|
|
1736
|
+
{ name: "Commit first (cancel and commit manually)", value: "cancel" },
|
|
1737
|
+
{ name: "Continue anyway (executor starts without my unsaved changes)", value: "continue" }
|
|
1738
|
+
]
|
|
1739
|
+
});
|
|
1740
|
+
} catch {
|
|
1741
|
+
choice = "cancel";
|
|
1742
|
+
}
|
|
1743
|
+
if (choice === "cancel") throw new Error("Cancelled. Commit your changes first, then create the sub-task.");
|
|
1744
|
+
}
|
|
1745
|
+
const baseCommit2 = await getCurrentCommit();
|
|
1746
|
+
return { baseCommit: baseCommit2, targetBranch: currentBranch, isSubtask: true };
|
|
1747
|
+
}
|
|
1748
|
+
let stashedForSync = false;
|
|
1749
|
+
if (await hasUncommittedChanges()) {
|
|
1750
|
+
if (!interactive) {
|
|
1751
|
+
throw new Error("Cannot create task: you have uncommitted changes. Commit or stash them first (git stash).");
|
|
1752
|
+
}
|
|
1753
|
+
const { select: inquirerSelect } = await import("@inquirer/prompts");
|
|
1754
|
+
let choice;
|
|
1755
|
+
try {
|
|
1756
|
+
choice = await inquirerSelect({
|
|
1757
|
+
message: "You have uncommitted changes. Syncing with main requires a clean working tree.",
|
|
1758
|
+
choices: [
|
|
1759
|
+
{ name: "Stash changes and continue (restore with: git stash pop)", value: "stash" },
|
|
1760
|
+
{ name: "Cancel", value: "cancel" }
|
|
1761
|
+
]
|
|
1762
|
+
});
|
|
1763
|
+
} catch {
|
|
1764
|
+
choice = "cancel";
|
|
1765
|
+
}
|
|
1766
|
+
if (choice === "cancel") throw new Error("Cancelled.");
|
|
1767
|
+
await stash("tch: before creating new task");
|
|
1768
|
+
stashedForSync = true;
|
|
1769
|
+
console.log(chalk6.dim(" Changes stashed. Run `git stash pop` after creating the task."));
|
|
1770
|
+
}
|
|
1771
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
1772
|
+
let baseCommit;
|
|
1773
|
+
const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
|
|
1774
|
+
try {
|
|
1775
|
+
await syncWithBase(baseBranch);
|
|
1776
|
+
baseCommit = await getCurrentCommit();
|
|
1777
|
+
syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
|
|
1778
|
+
} catch {
|
|
1779
|
+
syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
|
|
1780
|
+
try {
|
|
1781
|
+
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1782
|
+
} catch {
|
|
1783
|
+
}
|
|
1784
|
+
if (stashedForSync) {
|
|
1785
|
+
try {
|
|
1786
|
+
await stashPop();
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
throw new Error(`Could not sync with ${baseBranch}. Your changes have been restored from stash.`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return { baseCommit, targetBranch: makeWorkerBranchName(me), isSubtask: false };
|
|
1793
|
+
}
|
|
1688
1794
|
var definition4 = {
|
|
1689
1795
|
type: "function",
|
|
1690
1796
|
function: {
|
|
@@ -1701,7 +1807,7 @@ var definition4 = {
|
|
|
1701
1807
|
}
|
|
1702
1808
|
};
|
|
1703
1809
|
async function run4(input3, config) {
|
|
1704
|
-
const authSpinner =
|
|
1810
|
+
const authSpinner = ora4("Checking permissions\u2026").start();
|
|
1705
1811
|
let me;
|
|
1706
1812
|
let allowed;
|
|
1707
1813
|
try {
|
|
@@ -1724,7 +1830,7 @@ async function run4(input3, config) {
|
|
|
1724
1830
|
}
|
|
1725
1831
|
if (!title) return "Cancelled.";
|
|
1726
1832
|
}
|
|
1727
|
-
const spinner =
|
|
1833
|
+
const spinner = ora4("Scanning project and generating guide\u2026").start();
|
|
1728
1834
|
let guide;
|
|
1729
1835
|
try {
|
|
1730
1836
|
guide = await generateGuide(config, title);
|
|
@@ -1733,16 +1839,16 @@ async function run4(input3, config) {
|
|
|
1733
1839
|
spinner.stop();
|
|
1734
1840
|
return `Error generating guide: ${err.message}`;
|
|
1735
1841
|
}
|
|
1736
|
-
const divider =
|
|
1842
|
+
const divider = chalk6.dim("\u2500".repeat(70));
|
|
1737
1843
|
for (; ; ) {
|
|
1738
1844
|
console.log("\n" + divider);
|
|
1739
|
-
console.log(
|
|
1845
|
+
console.log(chalk6.bold(" Generated guide preview"));
|
|
1740
1846
|
console.log(divider);
|
|
1741
1847
|
console.log(renderMarkdown(guide));
|
|
1742
1848
|
console.log(divider + "\n");
|
|
1743
1849
|
let action;
|
|
1744
1850
|
try {
|
|
1745
|
-
action = await
|
|
1851
|
+
action = await select4({
|
|
1746
1852
|
message: "Create this task?",
|
|
1747
1853
|
choices: [
|
|
1748
1854
|
{ name: "Yes, create task", value: "create" },
|
|
@@ -1760,7 +1866,7 @@ async function run4(input3, config) {
|
|
|
1760
1866
|
try {
|
|
1761
1867
|
guide = await openInEditor(guide);
|
|
1762
1868
|
} catch (err) {
|
|
1763
|
-
console.log(
|
|
1869
|
+
console.log(chalk6.yellow(` Editor error: ${err.message}`));
|
|
1764
1870
|
}
|
|
1765
1871
|
continue;
|
|
1766
1872
|
}
|
|
@@ -1771,35 +1877,32 @@ async function run4(input3, config) {
|
|
|
1771
1877
|
return "Cancelled.";
|
|
1772
1878
|
}
|
|
1773
1879
|
if (!feedback) continue;
|
|
1774
|
-
const reviseSpinner =
|
|
1880
|
+
const reviseSpinner = ora4("Revising guide\u2026").start();
|
|
1775
1881
|
try {
|
|
1776
1882
|
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1777
1883
|
reviseSpinner.stop();
|
|
1778
1884
|
} catch (err) {
|
|
1779
1885
|
reviseSpinner.stop();
|
|
1780
|
-
console.log(
|
|
1886
|
+
console.log(chalk6.yellow(` Revision error: ${err.message}`));
|
|
1781
1887
|
}
|
|
1782
1888
|
}
|
|
1783
|
-
const baseBranch = config.baseBranch ?? "main";
|
|
1784
1889
|
let baseCommit;
|
|
1785
|
-
|
|
1890
|
+
let targetBranch;
|
|
1891
|
+
let isSubtask;
|
|
1786
1892
|
try {
|
|
1787
|
-
await
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
} catch {
|
|
1791
|
-
syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
|
|
1792
|
-
try {
|
|
1793
|
-
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1794
|
-
} catch {
|
|
1795
|
-
}
|
|
1893
|
+
({ baseCommit, targetBranch, isSubtask } = await resolveBaseAndTarget(config, me, true));
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
return err.message;
|
|
1796
1896
|
}
|
|
1797
|
-
|
|
1897
|
+
if (isSubtask) {
|
|
1898
|
+
console.log(chalk6.dim(` Sub-task: will target branch ${chalk6.cyan(targetBranch)} (base: ${baseCommit?.slice(0, 7) ?? "HEAD"})`));
|
|
1899
|
+
}
|
|
1900
|
+
const createSpinner = ora4(`Creating "${title}"\u2026`).start();
|
|
1798
1901
|
let htmlUrl;
|
|
1799
1902
|
let issueNumber;
|
|
1800
1903
|
let issueTitle;
|
|
1801
1904
|
try {
|
|
1802
|
-
const issue = await createTask(config, title, guide, baseCommit);
|
|
1905
|
+
const issue = await createTask(config, title, guide, baseCommit, targetBranch);
|
|
1803
1906
|
createSpinner.stop();
|
|
1804
1907
|
htmlUrl = issue.htmlUrl;
|
|
1805
1908
|
issueNumber = issue.number;
|
|
@@ -1808,19 +1911,19 @@ async function run4(input3, config) {
|
|
|
1808
1911
|
createSpinner.stop();
|
|
1809
1912
|
return `Error: ${err.message}`;
|
|
1810
1913
|
}
|
|
1811
|
-
console.log(
|
|
1914
|
+
console.log(chalk6.green(`
|
|
1812
1915
|
Created #${issueNumber} "${issueTitle}"
|
|
1813
|
-
${
|
|
1916
|
+
${chalk6.dim(htmlUrl)}
|
|
1814
1917
|
`));
|
|
1815
1918
|
try {
|
|
1816
|
-
const openBrowser = await
|
|
1919
|
+
const openBrowser = await select4({
|
|
1817
1920
|
message: "Open issue in browser?",
|
|
1818
1921
|
choices: [
|
|
1819
1922
|
{ name: "Yes", value: true },
|
|
1820
1923
|
{ name: "No", value: false }
|
|
1821
1924
|
]
|
|
1822
1925
|
});
|
|
1823
|
-
if (openBrowser) await
|
|
1926
|
+
if (openBrowser) await open(htmlUrl);
|
|
1824
1927
|
} catch {
|
|
1825
1928
|
}
|
|
1826
1929
|
return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
|
|
@@ -1836,19 +1939,9 @@ async function execute4(input3, config) {
|
|
|
1836
1939
|
if (feedback) {
|
|
1837
1940
|
guide = await generateGuide(config, title, { feedback, previousGuide: guide });
|
|
1838
1941
|
}
|
|
1839
|
-
const
|
|
1840
|
-
let baseCommit;
|
|
1841
|
-
try {
|
|
1842
|
-
await syncWithBase(baseBranch);
|
|
1843
|
-
baseCommit = await getCurrentCommit();
|
|
1844
|
-
} catch {
|
|
1845
|
-
try {
|
|
1846
|
-
baseCommit = await getRemoteHeadSha(baseBranch);
|
|
1847
|
-
} catch {
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1942
|
+
const { baseCommit, targetBranch } = await resolveBaseAndTarget(config, me, false);
|
|
1850
1943
|
try {
|
|
1851
|
-
const issue = await createTask(config, title, guide, baseCommit);
|
|
1944
|
+
const issue = await createTask(config, title, guide, baseCommit, targetBranch);
|
|
1852
1945
|
return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
|
|
1853
1946
|
|
|
1854
1947
|
Guide:
|
|
@@ -1868,7 +1961,7 @@ __export(my_status_exports, {
|
|
|
1868
1961
|
terminal: () => terminal5
|
|
1869
1962
|
});
|
|
1870
1963
|
init_github();
|
|
1871
|
-
import
|
|
1964
|
+
import ora5 from "ora";
|
|
1872
1965
|
var definition5 = {
|
|
1873
1966
|
type: "function",
|
|
1874
1967
|
function: {
|
|
@@ -1878,7 +1971,7 @@ var definition5 = {
|
|
|
1878
1971
|
}
|
|
1879
1972
|
};
|
|
1880
1973
|
async function run5(_input, config) {
|
|
1881
|
-
const spinner =
|
|
1974
|
+
const spinner = ora5("Fetching your tasks\u2026").start();
|
|
1882
1975
|
try {
|
|
1883
1976
|
const me = await getAuthenticatedUser(config);
|
|
1884
1977
|
const tasks = await listMyTasks(config, me);
|
|
@@ -1898,120 +1991,193 @@ var terminal5 = true;
|
|
|
1898
1991
|
// src/tools/review/index.ts
|
|
1899
1992
|
var review_exports = {};
|
|
1900
1993
|
__export(review_exports, {
|
|
1994
|
+
definition: () => definition8,
|
|
1995
|
+
execute: () => execute8,
|
|
1996
|
+
run: () => run8,
|
|
1997
|
+
terminal: () => terminal8
|
|
1998
|
+
});
|
|
1999
|
+
init_github();
|
|
2000
|
+
import chalk9 from "chalk";
|
|
2001
|
+
import ora8 from "ora";
|
|
2002
|
+
import { select as select7 } from "@inquirer/prompts";
|
|
2003
|
+
|
|
2004
|
+
// src/tools/accept/index.ts
|
|
2005
|
+
var accept_exports = {};
|
|
2006
|
+
__export(accept_exports, {
|
|
1901
2007
|
definition: () => definition6,
|
|
1902
2008
|
execute: () => execute6,
|
|
1903
2009
|
run: () => run6,
|
|
1904
2010
|
terminal: () => terminal6
|
|
1905
2011
|
});
|
|
1906
2012
|
init_github();
|
|
1907
|
-
import
|
|
2013
|
+
import chalk7 from "chalk";
|
|
2014
|
+
import { select as select5 } from "@inquirer/prompts";
|
|
2015
|
+
import ora6 from "ora";
|
|
1908
2016
|
var definition6 = {
|
|
1909
2017
|
type: "function",
|
|
1910
2018
|
function: {
|
|
1911
|
-
name: "
|
|
1912
|
-
description: "
|
|
1913
|
-
parameters: {
|
|
2019
|
+
name: "accept",
|
|
2020
|
+
description: "Accept an in-review task: merges the PR into the target branch and closes the issue.",
|
|
2021
|
+
parameters: {
|
|
2022
|
+
type: "object",
|
|
2023
|
+
properties: {
|
|
2024
|
+
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
2025
|
+
},
|
|
2026
|
+
required: ["issue_number"]
|
|
2027
|
+
}
|
|
1914
2028
|
}
|
|
1915
2029
|
};
|
|
1916
|
-
async function run6(
|
|
1917
|
-
|
|
2030
|
+
async function run6(input3, config) {
|
|
2031
|
+
let issueNumber = input3["issue_number"];
|
|
2032
|
+
if (!issueNumber) {
|
|
2033
|
+
const spinner3 = ora6("Loading tasks for review\u2026").start();
|
|
2034
|
+
let tasks;
|
|
2035
|
+
let me;
|
|
2036
|
+
try {
|
|
2037
|
+
me = await getAuthenticatedUser(config);
|
|
2038
|
+
tasks = await listTasksForReview(config, me);
|
|
2039
|
+
spinner3.stop();
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
spinner3.stop();
|
|
2042
|
+
return `Error: ${err.message}`;
|
|
2043
|
+
}
|
|
2044
|
+
if (tasks.length === 0) return "No tasks pending review.";
|
|
2045
|
+
try {
|
|
2046
|
+
issueNumber = await select5({
|
|
2047
|
+
message: "Which task to accept?",
|
|
2048
|
+
choices: tasks.map((t) => ({
|
|
2049
|
+
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2050
|
+
value: t.number
|
|
2051
|
+
}))
|
|
2052
|
+
});
|
|
2053
|
+
} catch {
|
|
2054
|
+
return "Cancelled.";
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
const spinner2 = ora6("Verifying permissions\u2026").start();
|
|
2058
|
+
let me2;
|
|
2059
|
+
let issue;
|
|
1918
2060
|
try {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
return `Tasks pending review (created by @${me}):
|
|
1925
|
-
${lines.join("\n")}`;
|
|
2061
|
+
[me2, issue] = await Promise.all([
|
|
2062
|
+
getAuthenticatedUser(config),
|
|
2063
|
+
getTask(config, issueNumber)
|
|
2064
|
+
]);
|
|
2065
|
+
spinner2.stop();
|
|
1926
2066
|
} catch (err) {
|
|
1927
|
-
|
|
2067
|
+
spinner2.stop();
|
|
1928
2068
|
return `Error: ${err.message}`;
|
|
1929
2069
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
var terminal6 = true;
|
|
1933
|
-
|
|
1934
|
-
// src/tools/refresh/index.ts
|
|
1935
|
-
var refresh_exports = {};
|
|
1936
|
-
__export(refresh_exports, {
|
|
1937
|
-
definition: () => definition7,
|
|
1938
|
-
execute: () => execute7,
|
|
1939
|
-
run: () => run7,
|
|
1940
|
-
terminal: () => terminal7
|
|
1941
|
-
});
|
|
1942
|
-
var definition7 = {
|
|
1943
|
-
type: "function",
|
|
1944
|
-
function: {
|
|
1945
|
-
name: "refresh",
|
|
1946
|
-
description: "Reload and display the full task list. Equivalent to /refresh.",
|
|
1947
|
-
parameters: { type: "object", properties: {}, required: [] }
|
|
2070
|
+
if (issue.author && issue.author !== me2) {
|
|
2071
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
1948
2072
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
}
|
|
1961
|
-
var execute7 = run7;
|
|
1962
|
-
var terminal7 = true;
|
|
1963
|
-
|
|
1964
|
-
// src/tools/open-code/index.ts
|
|
1965
|
-
var open_code_exports = {};
|
|
1966
|
-
__export(open_code_exports, {
|
|
1967
|
-
definition: () => definition8,
|
|
1968
|
-
execute: () => execute8,
|
|
1969
|
-
run: () => run8,
|
|
1970
|
-
terminal: () => terminal8
|
|
1971
|
-
});
|
|
1972
|
-
init_github();
|
|
1973
|
-
var definition8 = {
|
|
1974
|
-
type: "function",
|
|
1975
|
-
function: {
|
|
1976
|
-
name: "open_code",
|
|
1977
|
-
description: "Launch Claude Code for the current task branch. Equivalent to /code.",
|
|
1978
|
-
parameters: { type: "object", properties: {}, required: [] }
|
|
2073
|
+
let confirmed;
|
|
2074
|
+
try {
|
|
2075
|
+
confirmed = await select5({
|
|
2076
|
+
message: `Merge PR for #${issueNumber} and close issue?`,
|
|
2077
|
+
choices: [
|
|
2078
|
+
{ name: "Yes, accept", value: true },
|
|
2079
|
+
{ name: "Cancel", value: false }
|
|
2080
|
+
]
|
|
2081
|
+
});
|
|
2082
|
+
} catch {
|
|
2083
|
+
return "Cancelled.";
|
|
1979
2084
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
let
|
|
2085
|
+
if (!confirmed) return "Cancelled.";
|
|
2086
|
+
const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2087
|
+
let result;
|
|
1983
2088
|
try {
|
|
1984
|
-
|
|
2089
|
+
result = await acceptTask(config, issueNumber);
|
|
2090
|
+
spinner.succeed(`PR #${result.prNumber} merged \u2192 ${chalk7.cyan(result.baseBranch)}`);
|
|
1985
2091
|
} catch (err) {
|
|
2092
|
+
spinner.fail("Failed");
|
|
1986
2093
|
return `Error: ${err.message}`;
|
|
1987
2094
|
}
|
|
1988
|
-
const
|
|
1989
|
-
if (!
|
|
1990
|
-
|
|
1991
|
-
|
|
2095
|
+
const mergedIntoTaskBranch = isTaskBranch(result.baseBranch);
|
|
2096
|
+
if (!mergedIntoTaskBranch) {
|
|
2097
|
+
const baseBranch = config.baseBranch ?? "main";
|
|
2098
|
+
let pushToMain;
|
|
2099
|
+
try {
|
|
2100
|
+
pushToMain = await select5({
|
|
2101
|
+
message: `Push ${chalk7.cyan(result.baseBranch)} \u2192 ${chalk7.cyan(baseBranch)}?`,
|
|
2102
|
+
choices: [
|
|
2103
|
+
{ name: `Yes, push to ${baseBranch}`, value: true },
|
|
2104
|
+
{ name: "No, keep in worker branch", value: false }
|
|
2105
|
+
]
|
|
2106
|
+
});
|
|
2107
|
+
} catch {
|
|
2108
|
+
pushToMain = false;
|
|
2109
|
+
}
|
|
2110
|
+
if (pushToMain) {
|
|
2111
|
+
const mergeSpinner = ora6(`Merging ${result.baseBranch} \u2192 ${baseBranch}\u2026`).start();
|
|
2112
|
+
try {
|
|
2113
|
+
await mergeWorkerIntoBase(config, result.baseBranch, baseBranch);
|
|
2114
|
+
mergeSpinner.succeed(`Merged ${result.baseBranch} \u2192 ${baseBranch}`);
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
let updateWiki = false;
|
|
1992
2121
|
try {
|
|
1993
|
-
|
|
2122
|
+
updateWiki = await select5({
|
|
2123
|
+
message: "Update TECHUNTER.md project overview?",
|
|
2124
|
+
choices: [
|
|
2125
|
+
{ name: "Yes, regenerate", value: true },
|
|
2126
|
+
{ name: "No, skip", value: false }
|
|
2127
|
+
]
|
|
2128
|
+
});
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
if (updateWiki) {
|
|
2132
|
+
const wikiSpinner = ora6("Regenerating TECHUNTER.md\u2026").start();
|
|
2133
|
+
try {
|
|
2134
|
+
const content = await generateWiki(config);
|
|
2135
|
+
await upsertRepoFile(config, "TECHUNTER.md", content, "docs: update TECHUNTER.md project overview");
|
|
2136
|
+
wikiSpinner.succeed("TECHUNTER.md updated");
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
wikiSpinner.fail(`Wiki update failed: ${err.message}`);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const mergeTarget = mergedIntoTaskBranch ? `${result.baseBranch} (sub-task merged, no push to main)` : result.baseBranch;
|
|
2142
|
+
return `Task #${issueNumber} accepted.
|
|
2143
|
+
PR #${result.prNumber} merged \u2192 ${mergeTarget}
|
|
2144
|
+
Issue closed.`;
|
|
2145
|
+
}
|
|
2146
|
+
async function execute6(input3, config) {
|
|
2147
|
+
const issueNumber = input3["issue_number"];
|
|
2148
|
+
const [me, issue] = await Promise.all([
|
|
2149
|
+
getAuthenticatedUser(config),
|
|
2150
|
+
getTask(config, issueNumber)
|
|
2151
|
+
]);
|
|
2152
|
+
if (issue.author && issue.author !== me) {
|
|
2153
|
+
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2154
|
+
}
|
|
2155
|
+
const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2156
|
+
try {
|
|
2157
|
+
const result = await acceptTask(config, issueNumber);
|
|
2158
|
+
spinner.stop();
|
|
2159
|
+
return `Task #${issueNumber} accepted.
|
|
2160
|
+
PR #${result.prNumber} merged \u2192 ${result.baseBranch}
|
|
2161
|
+
Issue closed.`;
|
|
1994
2162
|
} catch (err) {
|
|
2163
|
+
spinner.stop();
|
|
1995
2164
|
return `Error: ${err.message}`;
|
|
1996
2165
|
}
|
|
1997
|
-
await launchClaudeCode(issue, branch);
|
|
1998
|
-
return "Claude Code session ended.";
|
|
1999
2166
|
}
|
|
2000
|
-
var
|
|
2001
|
-
var terminal8 = true;
|
|
2167
|
+
var terminal6 = true;
|
|
2002
2168
|
|
|
2003
2169
|
// src/tools/reject/index.ts
|
|
2004
2170
|
var reject_exports = {};
|
|
2005
2171
|
__export(reject_exports, {
|
|
2006
|
-
definition: () =>
|
|
2007
|
-
execute: () =>
|
|
2008
|
-
run: () =>
|
|
2009
|
-
terminal: () =>
|
|
2172
|
+
definition: () => definition7,
|
|
2173
|
+
execute: () => execute7,
|
|
2174
|
+
run: () => run7,
|
|
2175
|
+
terminal: () => terminal7
|
|
2010
2176
|
});
|
|
2011
2177
|
init_github();
|
|
2012
|
-
import
|
|
2013
|
-
import { select as
|
|
2014
|
-
import
|
|
2178
|
+
import chalk8 from "chalk";
|
|
2179
|
+
import { select as select6, input as promptInput3 } from "@inquirer/prompts";
|
|
2180
|
+
import ora7 from "ora";
|
|
2015
2181
|
|
|
2016
2182
|
// src/tools/reject/prompts.ts
|
|
2017
2183
|
var REJECTION_FORMAT = `
|
|
@@ -2042,7 +2208,7 @@ Reviewer feedback: ${userFeedback}`,
|
|
|
2042
2208
|
}
|
|
2043
2209
|
|
|
2044
2210
|
// src/tools/reject/index.ts
|
|
2045
|
-
var
|
|
2211
|
+
var definition7 = {
|
|
2046
2212
|
type: "function",
|
|
2047
2213
|
function: {
|
|
2048
2214
|
name: "reject",
|
|
@@ -2057,7 +2223,7 @@ var definition9 = {
|
|
|
2057
2223
|
}
|
|
2058
2224
|
}
|
|
2059
2225
|
};
|
|
2060
|
-
async function
|
|
2226
|
+
async function run7(input3, config) {
|
|
2061
2227
|
const issueNumber = input3["issue_number"];
|
|
2062
2228
|
const [me, issue] = await Promise.all([
|
|
2063
2229
|
getAuthenticatedUser(config),
|
|
@@ -2075,9 +2241,9 @@ async function run9(input3, config) {
|
|
|
2075
2241
|
return "Cancelled.";
|
|
2076
2242
|
}
|
|
2077
2243
|
if (!feedback.trim()) return "Cancelled.";
|
|
2078
|
-
const divider =
|
|
2244
|
+
const divider = chalk8.dim("\u2500".repeat(70));
|
|
2079
2245
|
for (; ; ) {
|
|
2080
|
-
const spinner =
|
|
2246
|
+
const spinner = ora7("Generating rejection comment\u2026").start();
|
|
2081
2247
|
let comment;
|
|
2082
2248
|
try {
|
|
2083
2249
|
comment = await generateRejectionComment(config, issueNumber, feedback);
|
|
@@ -2087,13 +2253,13 @@ async function run9(input3, config) {
|
|
|
2087
2253
|
return `Error generating comment: ${err.message}`;
|
|
2088
2254
|
}
|
|
2089
2255
|
console.log("\n" + divider);
|
|
2090
|
-
console.log(
|
|
2256
|
+
console.log(chalk8.bold(` Rejection preview \u2014 issue #${issueNumber}`));
|
|
2091
2257
|
console.log(divider);
|
|
2092
2258
|
console.log(renderMarkdown(comment));
|
|
2093
2259
|
console.log(divider + "\n");
|
|
2094
2260
|
let decision;
|
|
2095
2261
|
try {
|
|
2096
|
-
decision = await
|
|
2262
|
+
decision = await select6({
|
|
2097
2263
|
message: `Post rejection and mark #${issueNumber} as changes-needed?`,
|
|
2098
2264
|
choices: [
|
|
2099
2265
|
{ name: "Post & Reject", value: "yes" },
|
|
@@ -2113,7 +2279,7 @@ async function run9(input3, config) {
|
|
|
2113
2279
|
}
|
|
2114
2280
|
continue;
|
|
2115
2281
|
}
|
|
2116
|
-
let spinner2 =
|
|
2282
|
+
let spinner2 = ora7(`Posting rejection comment on #${issueNumber}\u2026`).start();
|
|
2117
2283
|
try {
|
|
2118
2284
|
await postComment(config, issueNumber, comment);
|
|
2119
2285
|
spinner2.stop();
|
|
@@ -2121,7 +2287,7 @@ async function run9(input3, config) {
|
|
|
2121
2287
|
spinner2.stop();
|
|
2122
2288
|
return `Error posting comment: ${err.message}`;
|
|
2123
2289
|
}
|
|
2124
|
-
spinner2 =
|
|
2290
|
+
spinner2 = ora7(`Marking #${issueNumber} as changes-needed\u2026`).start();
|
|
2125
2291
|
try {
|
|
2126
2292
|
await rejectTask(config, issueNumber);
|
|
2127
2293
|
spinner2.stop();
|
|
@@ -2132,7 +2298,7 @@ async function run9(input3, config) {
|
|
|
2132
2298
|
return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
|
|
2133
2299
|
}
|
|
2134
2300
|
}
|
|
2135
|
-
async function
|
|
2301
|
+
async function execute7(input3, config) {
|
|
2136
2302
|
const issueNumber = input3["issue_number"];
|
|
2137
2303
|
const feedback = input3["feedback"];
|
|
2138
2304
|
const [me, issue] = await Promise.all([
|
|
@@ -2149,164 +2315,195 @@ async function execute9(input3, config) {
|
|
|
2149
2315
|
return `Error generating comment: ${err.message}`;
|
|
2150
2316
|
}
|
|
2151
2317
|
try {
|
|
2152
|
-
await postComment(config, issueNumber, comment);
|
|
2153
|
-
} catch (err) {
|
|
2154
|
-
return `Error posting comment: ${err.message}`;
|
|
2318
|
+
await postComment(config, issueNumber, comment);
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
return `Error posting comment: ${err.message}`;
|
|
2321
|
+
}
|
|
2322
|
+
try {
|
|
2323
|
+
await rejectTask(config, issueNumber);
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
return `Comment posted but failed to update label: ${err.message}`;
|
|
2326
|
+
}
|
|
2327
|
+
return `Task #${issueNumber} rejected.
|
|
2328
|
+
|
|
2329
|
+
Comment posted:
|
|
2330
|
+
${comment}`;
|
|
2331
|
+
}
|
|
2332
|
+
var terminal7 = true;
|
|
2333
|
+
|
|
2334
|
+
// src/tools/review/index.ts
|
|
2335
|
+
var definition8 = {
|
|
2336
|
+
type: "function",
|
|
2337
|
+
function: {
|
|
2338
|
+
name: "review",
|
|
2339
|
+
description: "List tasks waiting for your review (submitted by others, created by you), then let you accept or reject one. Equivalent to /review.",
|
|
2340
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
async function run8(_input, config) {
|
|
2344
|
+
const spinner = ora8("Loading tasks for review\u2026").start();
|
|
2345
|
+
let me;
|
|
2346
|
+
let tasks;
|
|
2347
|
+
try {
|
|
2348
|
+
me = await getAuthenticatedUser(config);
|
|
2349
|
+
tasks = await listTasksForReview(config, me);
|
|
2350
|
+
spinner.stop();
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
spinner.stop();
|
|
2353
|
+
return `Error: ${err.message}`;
|
|
2354
|
+
}
|
|
2355
|
+
if (tasks.length === 0) return `No tasks pending review for @${me}.`;
|
|
2356
|
+
let issueNumber;
|
|
2357
|
+
try {
|
|
2358
|
+
issueNumber = await select7({
|
|
2359
|
+
message: "Select a task to review:",
|
|
2360
|
+
choices: tasks.map((t) => ({
|
|
2361
|
+
name: `#${String(t.number).padEnd(4)} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2362
|
+
value: t.number
|
|
2363
|
+
}))
|
|
2364
|
+
});
|
|
2365
|
+
} catch {
|
|
2366
|
+
return "Cancelled.";
|
|
2155
2367
|
}
|
|
2368
|
+
const spinner2 = ora8(`Loading #${issueNumber}\u2026`).start();
|
|
2369
|
+
let pr;
|
|
2156
2370
|
try {
|
|
2157
|
-
await
|
|
2371
|
+
pr = await getTaskPR(config, issueNumber);
|
|
2372
|
+
spinner2.stop();
|
|
2158
2373
|
} catch (err) {
|
|
2159
|
-
|
|
2374
|
+
spinner2.stop();
|
|
2375
|
+
return `Error loading PR: ${err.message}`;
|
|
2160
2376
|
}
|
|
2161
|
-
|
|
2377
|
+
const divider = chalk9.dim("\u2500".repeat(70));
|
|
2378
|
+
console.log("\n" + divider);
|
|
2379
|
+
if (pr) {
|
|
2380
|
+
console.log(chalk9.bold(` PR #${pr.number}`) + " " + chalk9.dim(pr.url));
|
|
2381
|
+
console.log(divider);
|
|
2382
|
+
console.log(renderMarkdown(pr.body));
|
|
2383
|
+
} else {
|
|
2384
|
+
console.log(chalk9.yellow(` No open PR found for task #${issueNumber}`));
|
|
2385
|
+
}
|
|
2386
|
+
console.log(divider + "\n");
|
|
2387
|
+
for (; ; ) {
|
|
2388
|
+
let action;
|
|
2389
|
+
try {
|
|
2390
|
+
action = await select7({
|
|
2391
|
+
message: "Review action:",
|
|
2392
|
+
choices: [
|
|
2393
|
+
...pr ? [{ name: "View diff", value: "diff" }] : [],
|
|
2394
|
+
{ name: chalk9.green("Accept") + " \u2014 merge PR and close issue", value: "accept" },
|
|
2395
|
+
{ name: chalk9.red("Reject") + " \u2014 request changes", value: "reject" },
|
|
2396
|
+
{ name: "Nothing, just viewing", value: "none" }
|
|
2397
|
+
]
|
|
2398
|
+
});
|
|
2399
|
+
} catch {
|
|
2400
|
+
return "Cancelled.";
|
|
2401
|
+
}
|
|
2402
|
+
if (action === "none") return `Viewed task #${issueNumber}.`;
|
|
2403
|
+
if (action === "accept") return run6({ issue_number: issueNumber }, config);
|
|
2404
|
+
if (action === "reject") return run7({ issue_number: issueNumber }, config);
|
|
2405
|
+
if (action === "diff") {
|
|
2406
|
+
const diffSpinner = ora8("Fetching diff\u2026").start();
|
|
2407
|
+
let diff;
|
|
2408
|
+
try {
|
|
2409
|
+
diff = await getTaskPRDiff(config, pr.number);
|
|
2410
|
+
diffSpinner.stop();
|
|
2411
|
+
} catch (err) {
|
|
2412
|
+
diffSpinner.stop();
|
|
2413
|
+
console.log(chalk9.red(`Error fetching diff: ${err.message}`));
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
console.log("\n" + divider);
|
|
2417
|
+
for (const line of diff.split("\n")) {
|
|
2418
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2419
|
+
process.stdout.write(chalk9.green(line) + "\n");
|
|
2420
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2421
|
+
process.stdout.write(chalk9.red(line) + "\n");
|
|
2422
|
+
} else if (line.startsWith("@@")) {
|
|
2423
|
+
process.stdout.write(chalk9.cyan(line) + "\n");
|
|
2424
|
+
} else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
|
|
2425
|
+
process.stdout.write(chalk9.bold(line) + "\n");
|
|
2426
|
+
} else {
|
|
2427
|
+
process.stdout.write(line + "\n");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
console.log(divider + "\n");
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
var execute8 = run8;
|
|
2435
|
+
var terminal8 = true;
|
|
2162
2436
|
|
|
2163
|
-
|
|
2164
|
-
|
|
2437
|
+
// src/tools/refresh/index.ts
|
|
2438
|
+
var refresh_exports = {};
|
|
2439
|
+
__export(refresh_exports, {
|
|
2440
|
+
definition: () => definition9,
|
|
2441
|
+
execute: () => execute9,
|
|
2442
|
+
run: () => run9,
|
|
2443
|
+
terminal: () => terminal9
|
|
2444
|
+
});
|
|
2445
|
+
var definition9 = {
|
|
2446
|
+
type: "function",
|
|
2447
|
+
function: {
|
|
2448
|
+
name: "refresh",
|
|
2449
|
+
description: "Reload and display the full task list. Equivalent to /refresh.",
|
|
2450
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
async function run9(_input, config) {
|
|
2454
|
+
const tasks = await printTaskList(config);
|
|
2455
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
2456
|
+
const lines = tasks.map((t) => {
|
|
2457
|
+
const status = getStatus(t);
|
|
2458
|
+
const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
|
|
2459
|
+
return `#${t.number} [${status}] ${assignee} ${t.title}`;
|
|
2460
|
+
});
|
|
2461
|
+
return `Tasks (${tasks.length}):
|
|
2462
|
+
${lines.join("\n")}`;
|
|
2165
2463
|
}
|
|
2464
|
+
var execute9 = run9;
|
|
2166
2465
|
var terminal9 = true;
|
|
2167
2466
|
|
|
2168
|
-
// src/tools/
|
|
2169
|
-
var
|
|
2170
|
-
__export(
|
|
2467
|
+
// src/tools/open-code/index.ts
|
|
2468
|
+
var open_code_exports = {};
|
|
2469
|
+
__export(open_code_exports, {
|
|
2171
2470
|
definition: () => definition10,
|
|
2172
2471
|
execute: () => execute10,
|
|
2173
2472
|
run: () => run10,
|
|
2174
2473
|
terminal: () => terminal10
|
|
2175
2474
|
});
|
|
2176
2475
|
init_github();
|
|
2177
|
-
import chalk11 from "chalk";
|
|
2178
|
-
import { select as select8 } from "@inquirer/prompts";
|
|
2179
|
-
import ora9 from "ora";
|
|
2180
2476
|
var definition10 = {
|
|
2181
2477
|
type: "function",
|
|
2182
2478
|
function: {
|
|
2183
|
-
name: "
|
|
2184
|
-
description: "
|
|
2185
|
-
parameters: {
|
|
2186
|
-
type: "object",
|
|
2187
|
-
properties: {
|
|
2188
|
-
issue_number: { type: "number", description: "GitHub issue number to accept" }
|
|
2189
|
-
},
|
|
2190
|
-
required: ["issue_number"]
|
|
2191
|
-
}
|
|
2479
|
+
name: "open_code",
|
|
2480
|
+
description: "Launch Claude Code for the current task branch. Equivalent to /code.",
|
|
2481
|
+
parameters: { type: "object", properties: {}, required: [] }
|
|
2192
2482
|
}
|
|
2193
2483
|
};
|
|
2194
|
-
async function run10(
|
|
2195
|
-
let
|
|
2196
|
-
if (!issueNumber) {
|
|
2197
|
-
const spinner3 = ora9("Loading tasks for review\u2026").start();
|
|
2198
|
-
let tasks;
|
|
2199
|
-
let me;
|
|
2200
|
-
try {
|
|
2201
|
-
me = await getAuthenticatedUser(config);
|
|
2202
|
-
tasks = await listTasksForReview(config, me);
|
|
2203
|
-
spinner3.stop();
|
|
2204
|
-
} catch (err) {
|
|
2205
|
-
spinner3.stop();
|
|
2206
|
-
return `Error: ${err.message}`;
|
|
2207
|
-
}
|
|
2208
|
-
if (tasks.length === 0) return "No tasks pending review.";
|
|
2209
|
-
try {
|
|
2210
|
-
issueNumber = await select8({
|
|
2211
|
-
message: "Which task to accept?",
|
|
2212
|
-
choices: tasks.map((t) => ({
|
|
2213
|
-
name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
|
|
2214
|
-
value: t.number
|
|
2215
|
-
}))
|
|
2216
|
-
});
|
|
2217
|
-
} catch {
|
|
2218
|
-
return "Cancelled.";
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
const spinner2 = ora9("Verifying permissions\u2026").start();
|
|
2222
|
-
let me2;
|
|
2223
|
-
let issue;
|
|
2224
|
-
try {
|
|
2225
|
-
[me2, issue] = await Promise.all([
|
|
2226
|
-
getAuthenticatedUser(config),
|
|
2227
|
-
getTask(config, issueNumber)
|
|
2228
|
-
]);
|
|
2229
|
-
spinner2.stop();
|
|
2230
|
-
} catch (err) {
|
|
2231
|
-
spinner2.stop();
|
|
2232
|
-
return `Error: ${err.message}`;
|
|
2233
|
-
}
|
|
2234
|
-
if (issue.author && issue.author !== me2) {
|
|
2235
|
-
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2236
|
-
}
|
|
2237
|
-
const targetBranch = makeWorkerBranchName(me2);
|
|
2238
|
-
let confirmed;
|
|
2239
|
-
try {
|
|
2240
|
-
confirmed = await select8({
|
|
2241
|
-
message: `Merge PR for #${issueNumber} into ${chalk11.cyan(targetBranch)} and close issue?`,
|
|
2242
|
-
choices: [
|
|
2243
|
-
{ name: "Yes, accept", value: true },
|
|
2244
|
-
{ name: "Cancel", value: false }
|
|
2245
|
-
]
|
|
2246
|
-
});
|
|
2247
|
-
} catch {
|
|
2248
|
-
return "Cancelled.";
|
|
2249
|
-
}
|
|
2250
|
-
if (!confirmed) return "Cancelled.";
|
|
2251
|
-
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2252
|
-
let result;
|
|
2484
|
+
async function run10(_input, config) {
|
|
2485
|
+
let branch;
|
|
2253
2486
|
try {
|
|
2254
|
-
|
|
2255
|
-
result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
2256
|
-
spinner.succeed(`PR #${result.prNumber} merged into ${targetBranch}`);
|
|
2487
|
+
branch = await getCurrentBranch();
|
|
2257
2488
|
} catch (err) {
|
|
2258
|
-
spinner.fail("Failed");
|
|
2259
2489
|
return `Error: ${err.message}`;
|
|
2260
2490
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
choices: [
|
|
2267
|
-
{ name: `Yes, push to ${baseBranch}`, value: true },
|
|
2268
|
-
{ name: "No, keep in worker branch", value: false }
|
|
2269
|
-
]
|
|
2270
|
-
});
|
|
2271
|
-
} catch {
|
|
2272
|
-
pushToMain = false;
|
|
2273
|
-
}
|
|
2274
|
-
if (pushToMain) {
|
|
2275
|
-
const mergeSpinner = ora9(`Merging ${targetBranch} \u2192 ${baseBranch}\u2026`).start();
|
|
2276
|
-
try {
|
|
2277
|
-
await mergeWorkerIntoBase(config, targetBranch, baseBranch);
|
|
2278
|
-
mergeSpinner.succeed(`Merged ${targetBranch} \u2192 ${baseBranch}`);
|
|
2279
|
-
} catch (err) {
|
|
2280
|
-
mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
return `Task #${issueNumber} accepted.
|
|
2284
|
-
PR #${result.prNumber} merged \u2192 ${targetBranch}${pushToMain ? ` \u2192 ${baseBranch}` : ""}
|
|
2285
|
-
Issue closed.`;
|
|
2286
|
-
}
|
|
2287
|
-
async function execute10(input3, config) {
|
|
2288
|
-
const issueNumber = input3["issue_number"];
|
|
2289
|
-
const [me, issue] = await Promise.all([
|
|
2290
|
-
getAuthenticatedUser(config),
|
|
2291
|
-
getTask(config, issueNumber)
|
|
2292
|
-
]);
|
|
2293
|
-
if (issue.author && issue.author !== me) {
|
|
2294
|
-
return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
|
|
2491
|
+
let issueNum = getConfig().taskState?.activeIssueNumber;
|
|
2492
|
+
if (!issueNum) {
|
|
2493
|
+
const found = await getIssueNumberFromBranch(config, branch);
|
|
2494
|
+
if (!found) return `No active task found (current branch: ${branch}).`;
|
|
2495
|
+
issueNum = found.issueNumber;
|
|
2295
2496
|
}
|
|
2296
|
-
|
|
2297
|
-
const spinner = ora9(`Merging PR for #${issueNumber}\u2026`).start();
|
|
2497
|
+
let issue;
|
|
2298
2498
|
try {
|
|
2299
|
-
|
|
2300
|
-
const result = await acceptTask(config, issueNumber, assigneeWorkerBranch);
|
|
2301
|
-
spinner.stop();
|
|
2302
|
-
return `Task #${issueNumber} accepted.
|
|
2303
|
-
PR #${result.prNumber} merged \u2192 ${targetBranch}
|
|
2304
|
-
Issue closed.`;
|
|
2499
|
+
issue = await getTask(config, issueNum);
|
|
2305
2500
|
} catch (err) {
|
|
2306
|
-
spinner.stop();
|
|
2307
2501
|
return `Error: ${err.message}`;
|
|
2308
2502
|
}
|
|
2503
|
+
await launchClaudeCode(issue, branch);
|
|
2504
|
+
return "Claude Code session ended.";
|
|
2309
2505
|
}
|
|
2506
|
+
var execute10 = run10;
|
|
2310
2507
|
var terminal10 = true;
|
|
2311
2508
|
|
|
2312
2509
|
// src/tools/edit-task/index.ts
|
|
@@ -2318,8 +2515,8 @@ __export(edit_task_exports, {
|
|
|
2318
2515
|
terminal: () => terminal11
|
|
2319
2516
|
});
|
|
2320
2517
|
init_github();
|
|
2321
|
-
import { select as
|
|
2322
|
-
import
|
|
2518
|
+
import { select as select8, input as promptInput4 } from "@inquirer/prompts";
|
|
2519
|
+
import ora9 from "ora";
|
|
2323
2520
|
var definition11 = {
|
|
2324
2521
|
type: "function",
|
|
2325
2522
|
function: {
|
|
@@ -2347,7 +2544,7 @@ async function run11(input3, config) {
|
|
|
2347
2544
|
}
|
|
2348
2545
|
if (tasks.length === 0) return "No tasks found.";
|
|
2349
2546
|
try {
|
|
2350
|
-
issueNumber = await
|
|
2547
|
+
issueNumber = await select8({
|
|
2351
2548
|
message: "Select task to edit:",
|
|
2352
2549
|
choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
|
|
2353
2550
|
});
|
|
@@ -2375,43 +2572,169 @@ async function run11(input3, config) {
|
|
|
2375
2572
|
} catch {
|
|
2376
2573
|
return "Cancelled.";
|
|
2377
2574
|
}
|
|
2378
|
-
if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
|
|
2379
|
-
return "No changes made.";
|
|
2380
|
-
}
|
|
2381
|
-
const spinner =
|
|
2575
|
+
if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
|
|
2576
|
+
return "No changes made.";
|
|
2577
|
+
}
|
|
2578
|
+
const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
|
|
2579
|
+
try {
|
|
2580
|
+
await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
|
|
2581
|
+
spinner.stop();
|
|
2582
|
+
return `Task #${issueNumber} updated.`;
|
|
2583
|
+
} catch (err) {
|
|
2584
|
+
spinner.stop();
|
|
2585
|
+
return `Error: ${err.message}`;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
async function execute11(input3, config) {
|
|
2589
|
+
const issueNumber = input3["issue_number"];
|
|
2590
|
+
const title = input3["title"];
|
|
2591
|
+
const body = input3["body"];
|
|
2592
|
+
const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
|
|
2593
|
+
try {
|
|
2594
|
+
await editTask(config, issueNumber, title, body);
|
|
2595
|
+
spinner.stop();
|
|
2596
|
+
return `Task #${issueNumber} updated.`;
|
|
2597
|
+
} catch (err) {
|
|
2598
|
+
spinner.stop();
|
|
2599
|
+
return `Error: ${err.message}`;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
var terminal11 = true;
|
|
2603
|
+
|
|
2604
|
+
// src/tools/wiki/index.ts
|
|
2605
|
+
var wiki_exports = {};
|
|
2606
|
+
__export(wiki_exports, {
|
|
2607
|
+
definition: () => definition12,
|
|
2608
|
+
execute: () => execute12,
|
|
2609
|
+
run: () => run12,
|
|
2610
|
+
terminal: () => terminal12
|
|
2611
|
+
});
|
|
2612
|
+
import ora10 from "ora";
|
|
2613
|
+
import chalk10 from "chalk";
|
|
2614
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2615
|
+
import { select as select9 } from "@inquirer/prompts";
|
|
2616
|
+
init_github();
|
|
2617
|
+
var WIKI_PATH = "TECHUNTER.md";
|
|
2618
|
+
var definition12 = {
|
|
2619
|
+
type: "function",
|
|
2620
|
+
function: {
|
|
2621
|
+
name: "update_wiki",
|
|
2622
|
+
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.",
|
|
2623
|
+
parameters: {
|
|
2624
|
+
type: "object",
|
|
2625
|
+
properties: {},
|
|
2626
|
+
required: []
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
async function readWikiContent(config) {
|
|
2631
|
+
try {
|
|
2632
|
+
return await readFile2(WIKI_PATH, "utf-8");
|
|
2633
|
+
} catch {
|
|
2634
|
+
}
|
|
2635
|
+
return getRepoFile(config, WIKI_PATH);
|
|
2636
|
+
}
|
|
2637
|
+
function printWiki(content) {
|
|
2638
|
+
const divider = chalk10.dim("\u2500".repeat(70));
|
|
2639
|
+
console.log("\n" + divider);
|
|
2640
|
+
console.log(chalk10.bold(" TECHUNTER.md"));
|
|
2641
|
+
console.log(divider);
|
|
2642
|
+
console.log(renderMarkdown(content));
|
|
2643
|
+
console.log(divider + "\n");
|
|
2644
|
+
}
|
|
2645
|
+
async function run12(_input, config) {
|
|
2646
|
+
const fetchSpinner = ora10("Checking for existing wiki\u2026").start();
|
|
2647
|
+
const existing = await readWikiContent(config).catch(() => null);
|
|
2648
|
+
fetchSpinner.stop();
|
|
2649
|
+
let action;
|
|
2650
|
+
try {
|
|
2651
|
+
action = await select9({
|
|
2652
|
+
message: "TECHUNTER.md \u2014 what would you like to do?",
|
|
2653
|
+
choices: [
|
|
2654
|
+
...existing ? [{ name: "View current wiki", value: "view" }] : [],
|
|
2655
|
+
{ name: existing ? "Regenerate & commit to repo" : "Generate & commit to repo", value: "generate" },
|
|
2656
|
+
{ name: "Cancel", value: "cancel" }
|
|
2657
|
+
]
|
|
2658
|
+
});
|
|
2659
|
+
} catch {
|
|
2660
|
+
return "Cancelled.";
|
|
2661
|
+
}
|
|
2662
|
+
if (action === "cancel") return "Cancelled.";
|
|
2663
|
+
if (action === "view") {
|
|
2664
|
+
printWiki(existing);
|
|
2665
|
+
return "Displayed TECHUNTER.md.";
|
|
2666
|
+
}
|
|
2667
|
+
const authSpinner = ora10("Checking permissions\u2026").start();
|
|
2668
|
+
let me;
|
|
2669
|
+
let allowed;
|
|
2670
|
+
try {
|
|
2671
|
+
me = await getAuthenticatedUser(config);
|
|
2672
|
+
allowed = await isCollaborator(config, me);
|
|
2673
|
+
authSpinner.stop();
|
|
2674
|
+
} catch (err) {
|
|
2675
|
+
authSpinner.stop();
|
|
2676
|
+
return `Error checking permissions: ${err.message}`;
|
|
2677
|
+
}
|
|
2678
|
+
if (!allowed) {
|
|
2679
|
+
return `Permission denied: only repository collaborators can update the project wiki.`;
|
|
2680
|
+
}
|
|
2681
|
+
const genSpinner = ora10("Analyzing project and generating overview\u2026").start();
|
|
2682
|
+
let content;
|
|
2683
|
+
try {
|
|
2684
|
+
content = await generateWiki(config);
|
|
2685
|
+
genSpinner.stop();
|
|
2686
|
+
} catch (err) {
|
|
2687
|
+
genSpinner.stop();
|
|
2688
|
+
return `Error generating wiki: ${err.message}`;
|
|
2689
|
+
}
|
|
2690
|
+
printWiki(content);
|
|
2691
|
+
let confirm;
|
|
2692
|
+
try {
|
|
2693
|
+
confirm = await select9({
|
|
2694
|
+
message: `Publish to repository as ${WIKI_PATH}?`,
|
|
2695
|
+
choices: [
|
|
2696
|
+
{ name: "Yes, commit to repo", value: "publish" },
|
|
2697
|
+
{ name: "Cancel", value: "cancel" }
|
|
2698
|
+
]
|
|
2699
|
+
});
|
|
2700
|
+
} catch {
|
|
2701
|
+
return "Cancelled.";
|
|
2702
|
+
}
|
|
2703
|
+
if (confirm === "cancel") return "Cancelled.";
|
|
2704
|
+
const writeSpinner = ora10(`Writing ${WIKI_PATH}\u2026`).start();
|
|
2382
2705
|
try {
|
|
2383
|
-
await
|
|
2384
|
-
|
|
2385
|
-
|
|
2706
|
+
const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
|
|
2707
|
+
writeSpinner.succeed(`Written: ${url}`);
|
|
2708
|
+
console.log("");
|
|
2709
|
+
return `TECHUNTER.md updated \u2014 ${url}`;
|
|
2386
2710
|
} catch (err) {
|
|
2387
|
-
|
|
2711
|
+
writeSpinner.fail(`Failed: ${err.message}`);
|
|
2388
2712
|
return `Error: ${err.message}`;
|
|
2389
2713
|
}
|
|
2390
2714
|
}
|
|
2391
|
-
async function
|
|
2392
|
-
const
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2715
|
+
async function execute12(_input, config) {
|
|
2716
|
+
const me = await getAuthenticatedUser(config);
|
|
2717
|
+
if (!await isCollaborator(config, me)) {
|
|
2718
|
+
return `Permission denied: only repository collaborators can update the project wiki.`;
|
|
2719
|
+
}
|
|
2720
|
+
const content = await generateWiki(config);
|
|
2396
2721
|
try {
|
|
2397
|
-
await
|
|
2398
|
-
|
|
2399
|
-
return `Task #${issueNumber} updated.`;
|
|
2722
|
+
const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
|
|
2723
|
+
return `TECHUNTER.md updated \u2014 ${url}`;
|
|
2400
2724
|
} catch (err) {
|
|
2401
|
-
spinner.stop();
|
|
2402
2725
|
return `Error: ${err.message}`;
|
|
2403
2726
|
}
|
|
2404
2727
|
}
|
|
2405
|
-
var
|
|
2728
|
+
var terminal12 = true;
|
|
2406
2729
|
|
|
2407
2730
|
// src/tools/list-tasks/index.ts
|
|
2408
2731
|
var list_tasks_exports = {};
|
|
2409
2732
|
__export(list_tasks_exports, {
|
|
2410
|
-
definition: () =>
|
|
2411
|
-
execute: () =>
|
|
2733
|
+
definition: () => definition13,
|
|
2734
|
+
execute: () => execute13
|
|
2412
2735
|
});
|
|
2413
2736
|
init_github();
|
|
2414
|
-
var
|
|
2737
|
+
var definition13 = {
|
|
2415
2738
|
type: "function",
|
|
2416
2739
|
function: {
|
|
2417
2740
|
name: "list_tasks",
|
|
@@ -2423,7 +2746,7 @@ var definition12 = {
|
|
|
2423
2746
|
}
|
|
2424
2747
|
}
|
|
2425
2748
|
};
|
|
2426
|
-
async function
|
|
2749
|
+
async function execute13(_input, config) {
|
|
2427
2750
|
const tasks = await listTasks(config);
|
|
2428
2751
|
if (tasks.length === 0) return "No open tasks.";
|
|
2429
2752
|
return tasks.map((t) => {
|
|
@@ -2436,11 +2759,11 @@ async function execute12(_input, config) {
|
|
|
2436
2759
|
// src/tools/get-task/index.ts
|
|
2437
2760
|
var get_task_exports = {};
|
|
2438
2761
|
__export(get_task_exports, {
|
|
2439
|
-
definition: () =>
|
|
2440
|
-
execute: () =>
|
|
2762
|
+
definition: () => definition14,
|
|
2763
|
+
execute: () => execute14
|
|
2441
2764
|
});
|
|
2442
2765
|
init_github();
|
|
2443
|
-
var
|
|
2766
|
+
var definition14 = {
|
|
2444
2767
|
type: "function",
|
|
2445
2768
|
function: {
|
|
2446
2769
|
name: "get_task",
|
|
@@ -2454,7 +2777,7 @@ var definition13 = {
|
|
|
2454
2777
|
}
|
|
2455
2778
|
}
|
|
2456
2779
|
};
|
|
2457
|
-
async function
|
|
2780
|
+
async function execute14(input3, config) {
|
|
2458
2781
|
const issue = await getTask(config, input3["issue_number"]);
|
|
2459
2782
|
const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
|
|
2460
2783
|
const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
|
|
@@ -2471,12 +2794,12 @@ ${issue.body}`);
|
|
|
2471
2794
|
// src/tools/get-comments/index.ts
|
|
2472
2795
|
var get_comments_exports = {};
|
|
2473
2796
|
__export(get_comments_exports, {
|
|
2474
|
-
definition: () =>
|
|
2475
|
-
execute: () =>
|
|
2797
|
+
definition: () => definition15,
|
|
2798
|
+
execute: () => execute15
|
|
2476
2799
|
});
|
|
2477
2800
|
init_github();
|
|
2478
2801
|
import ora11 from "ora";
|
|
2479
|
-
var
|
|
2802
|
+
var definition15 = {
|
|
2480
2803
|
type: "function",
|
|
2481
2804
|
function: {
|
|
2482
2805
|
name: "get_comments",
|
|
@@ -2491,7 +2814,7 @@ var definition14 = {
|
|
|
2491
2814
|
}
|
|
2492
2815
|
}
|
|
2493
2816
|
};
|
|
2494
|
-
async function
|
|
2817
|
+
async function execute15(input3, config) {
|
|
2495
2818
|
const issueNumber = input3["issue_number"];
|
|
2496
2819
|
const limit = input3["limit"] ?? 5;
|
|
2497
2820
|
const spinner = ora11(`Loading comments for #${issueNumber}...`).start();
|
|
@@ -2513,11 +2836,11 @@ ${lines.join("\n\n")}`;
|
|
|
2513
2836
|
// src/tools/get-diff/index.ts
|
|
2514
2837
|
var get_diff_exports = {};
|
|
2515
2838
|
__export(get_diff_exports, {
|
|
2516
|
-
definition: () =>
|
|
2517
|
-
execute: () =>
|
|
2839
|
+
definition: () => definition16,
|
|
2840
|
+
execute: () => execute16
|
|
2518
2841
|
});
|
|
2519
2842
|
import ora12 from "ora";
|
|
2520
|
-
var
|
|
2843
|
+
var definition16 = {
|
|
2521
2844
|
type: "function",
|
|
2522
2845
|
function: {
|
|
2523
2846
|
name: "get_diff",
|
|
@@ -2525,7 +2848,7 @@ var definition15 = {
|
|
|
2525
2848
|
parameters: { type: "object", properties: {}, required: [] }
|
|
2526
2849
|
}
|
|
2527
2850
|
};
|
|
2528
|
-
async function
|
|
2851
|
+
async function execute16(_input, _config) {
|
|
2529
2852
|
const spinner = ora12("Reading git diff...").start();
|
|
2530
2853
|
try {
|
|
2531
2854
|
const diff = await getDiff();
|
|
@@ -2540,14 +2863,14 @@ async function execute15(_input, _config) {
|
|
|
2540
2863
|
// src/tools/run-command/index.ts
|
|
2541
2864
|
var run_command_exports = {};
|
|
2542
2865
|
__export(run_command_exports, {
|
|
2543
|
-
definition: () =>
|
|
2544
|
-
execute: () =>
|
|
2866
|
+
definition: () => definition17,
|
|
2867
|
+
execute: () => execute17
|
|
2545
2868
|
});
|
|
2546
2869
|
import { exec } from "child_process";
|
|
2547
2870
|
import { promisify } from "util";
|
|
2548
2871
|
import ora13 from "ora";
|
|
2549
2872
|
var execAsync = promisify(exec);
|
|
2550
|
-
var
|
|
2873
|
+
var definition17 = {
|
|
2551
2874
|
type: "function",
|
|
2552
2875
|
function: {
|
|
2553
2876
|
name: "run_command",
|
|
@@ -2561,7 +2884,7 @@ var definition16 = {
|
|
|
2561
2884
|
}
|
|
2562
2885
|
}
|
|
2563
2886
|
};
|
|
2564
|
-
async function
|
|
2887
|
+
async function execute17(input3, _config) {
|
|
2565
2888
|
const command = input3["command"];
|
|
2566
2889
|
const cwd = process.cwd();
|
|
2567
2890
|
const spinner = ora13(`$ ${command}`).start();
|
|
@@ -2583,15 +2906,15 @@ ${out || e.message}`;
|
|
|
2583
2906
|
// src/tools/list-files/index.ts
|
|
2584
2907
|
var list_files_exports = {};
|
|
2585
2908
|
__export(list_files_exports, {
|
|
2586
|
-
definition: () =>
|
|
2587
|
-
execute: () =>
|
|
2909
|
+
definition: () => definition18,
|
|
2910
|
+
execute: () => execute18
|
|
2588
2911
|
});
|
|
2589
|
-
import { readFile as
|
|
2912
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2590
2913
|
import { existsSync } from "fs";
|
|
2591
2914
|
import path2 from "path";
|
|
2592
2915
|
import { globby } from "globby";
|
|
2593
2916
|
import ignore from "ignore";
|
|
2594
|
-
var
|
|
2917
|
+
var definition18 = {
|
|
2595
2918
|
type: "function",
|
|
2596
2919
|
function: {
|
|
2597
2920
|
name: "list_files",
|
|
@@ -2631,13 +2954,13 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2631
2954
|
".sqlite",
|
|
2632
2955
|
".lock"
|
|
2633
2956
|
]);
|
|
2634
|
-
async function
|
|
2957
|
+
async function execute18(input3, _config) {
|
|
2635
2958
|
const glob = input3["glob"] ?? "**/*";
|
|
2636
2959
|
const cwd = process.cwd();
|
|
2637
2960
|
const ig = ignore();
|
|
2638
2961
|
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
2639
2962
|
if (existsSync(gitignorePath)) {
|
|
2640
|
-
ig.add(await
|
|
2963
|
+
ig.add(await readFile3(gitignorePath, "utf-8"));
|
|
2641
2964
|
}
|
|
2642
2965
|
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
|
|
2643
2966
|
const files = await globby(glob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
@@ -2650,15 +2973,15 @@ ${filtered.join("\n")}`;
|
|
|
2650
2973
|
// src/tools/grep-code/index.ts
|
|
2651
2974
|
var grep_code_exports = {};
|
|
2652
2975
|
__export(grep_code_exports, {
|
|
2653
|
-
definition: () =>
|
|
2654
|
-
execute: () =>
|
|
2976
|
+
definition: () => definition19,
|
|
2977
|
+
execute: () => execute19
|
|
2655
2978
|
});
|
|
2656
|
-
import { readFile as
|
|
2979
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2657
2980
|
import { existsSync as existsSync2 } from "fs";
|
|
2658
2981
|
import path3 from "path";
|
|
2659
2982
|
import { globby as globby2 } from "globby";
|
|
2660
2983
|
import ignore2 from "ignore";
|
|
2661
|
-
var
|
|
2984
|
+
var definition19 = {
|
|
2662
2985
|
type: "function",
|
|
2663
2986
|
function: {
|
|
2664
2987
|
name: "grep_code",
|
|
@@ -2699,7 +3022,7 @@ async function buildIgnore(cwd) {
|
|
|
2699
3022
|
const ig = ignore2();
|
|
2700
3023
|
const gitignorePath = path3.join(cwd, ".gitignore");
|
|
2701
3024
|
if (existsSync2(gitignorePath)) {
|
|
2702
|
-
ig.add(await
|
|
3025
|
+
ig.add(await readFile4(gitignorePath, "utf-8"));
|
|
2703
3026
|
}
|
|
2704
3027
|
ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
|
|
2705
3028
|
return ig;
|
|
@@ -2731,7 +3054,7 @@ function isText(f) {
|
|
|
2731
3054
|
return !BINARY_EXTENSIONS2.has(path3.extname(f).toLowerCase());
|
|
2732
3055
|
}
|
|
2733
3056
|
var MAX_RANGE_LINES = 300;
|
|
2734
|
-
async function
|
|
3057
|
+
async function execute19(input3, _config) {
|
|
2735
3058
|
const pattern = input3["pattern"] ?? "";
|
|
2736
3059
|
const fileGlob = input3["file_glob"] ?? "**/*";
|
|
2737
3060
|
const contextLines = Math.min(input3["context_lines"] ?? 2, 5);
|
|
@@ -2743,7 +3066,7 @@ async function execute18(input3, _config) {
|
|
|
2743
3066
|
const files = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
|
|
2744
3067
|
if (files.length === 0) return `No file matched: ${fileGlob}`;
|
|
2745
3068
|
if (files.length > 1) return `file_glob matched ${files.length} files \u2014 narrow it to a single file for read-range mode.`;
|
|
2746
|
-
const raw = await
|
|
3069
|
+
const raw = await readFile4(path3.join(cwd, files[0]), "utf-8");
|
|
2747
3070
|
const lines = raw.split("\n");
|
|
2748
3071
|
const total = lines.length;
|
|
2749
3072
|
const from = Math.max(1, startLine);
|
|
@@ -2772,7 +3095,7 @@ ${numbered}
|
|
|
2772
3095
|
if (totalMatches >= maxResults) break;
|
|
2773
3096
|
let content;
|
|
2774
3097
|
try {
|
|
2775
|
-
content = await
|
|
3098
|
+
content = await readFile4(path3.join(cwd, file), "utf-8");
|
|
2776
3099
|
} catch {
|
|
2777
3100
|
continue;
|
|
2778
3101
|
}
|
|
@@ -2810,92 +3133,457 @@ ${snippets.join("\n---\n")}
|
|
|
2810
3133
|
\`\`\``);
|
|
2811
3134
|
}
|
|
2812
3135
|
}
|
|
2813
|
-
if (matches.length === 0) return `No matches found for: ${pattern}`;
|
|
2814
|
-
const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
|
|
2815
|
-
return [header, ...matches].join("\n\n");
|
|
3136
|
+
if (matches.length === 0) return `No matches found for: ${pattern}`;
|
|
3137
|
+
const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
|
|
3138
|
+
return [header, ...matches].join("\n\n");
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/tools/ask-user/index.ts
|
|
3142
|
+
var ask_user_exports = {};
|
|
3143
|
+
__export(ask_user_exports, {
|
|
3144
|
+
definition: () => definition20,
|
|
3145
|
+
execute: () => execute20
|
|
3146
|
+
});
|
|
3147
|
+
import chalk11 from "chalk";
|
|
3148
|
+
import { select as select10, input as promptInput5 } from "@inquirer/prompts";
|
|
3149
|
+
var definition20 = {
|
|
3150
|
+
type: "function",
|
|
3151
|
+
function: {
|
|
3152
|
+
name: "ask_user",
|
|
3153
|
+
description: "Ask the user to clarify something ambiguous \u2014 scope, expected behaviour, edge cases, or business decisions. Do NOT ask about technical implementation choices. Use at most 3 times per task.",
|
|
3154
|
+
parameters: {
|
|
3155
|
+
type: "object",
|
|
3156
|
+
properties: {
|
|
3157
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
3158
|
+
options: {
|
|
3159
|
+
type: "array",
|
|
3160
|
+
items: { type: "string" },
|
|
3161
|
+
description: "2\u20134 concrete answer choices"
|
|
3162
|
+
}
|
|
3163
|
+
},
|
|
3164
|
+
required: ["question", "options"]
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
};
|
|
3168
|
+
async function execute20(input3, _config) {
|
|
3169
|
+
const question = input3["question"];
|
|
3170
|
+
const options = input3["options"];
|
|
3171
|
+
const OTHER = "__other__";
|
|
3172
|
+
console.log("");
|
|
3173
|
+
console.log(chalk11.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
|
|
3174
|
+
console.log(chalk11.dim(" \u2502"));
|
|
3175
|
+
for (const line of question.split("\n")) {
|
|
3176
|
+
console.log(chalk11.dim(" \u2502 ") + line);
|
|
3177
|
+
}
|
|
3178
|
+
console.log(chalk11.dim(" \u2514" + "\u2500".repeat(67)));
|
|
3179
|
+
let answer;
|
|
3180
|
+
try {
|
|
3181
|
+
const chosen = await select10({
|
|
3182
|
+
message: " ",
|
|
3183
|
+
choices: [
|
|
3184
|
+
...options.map((o) => ({ name: o, value: o })),
|
|
3185
|
+
{ name: chalk11.dim("Other (describe below)"), value: OTHER }
|
|
3186
|
+
]
|
|
3187
|
+
});
|
|
3188
|
+
answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
|
|
3189
|
+
} catch {
|
|
3190
|
+
answer = "User skipped this question \u2014 use your best judgement.";
|
|
3191
|
+
}
|
|
3192
|
+
console.log("");
|
|
3193
|
+
return answer;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/tools/registry.ts
|
|
3197
|
+
var toolModules = [
|
|
3198
|
+
// Command tools
|
|
3199
|
+
pick_exports,
|
|
3200
|
+
new_task_exports,
|
|
3201
|
+
close_exports,
|
|
3202
|
+
submit_exports,
|
|
3203
|
+
my_status_exports,
|
|
3204
|
+
review_exports,
|
|
3205
|
+
refresh_exports,
|
|
3206
|
+
open_code_exports,
|
|
3207
|
+
reject_exports,
|
|
3208
|
+
accept_exports,
|
|
3209
|
+
edit_task_exports,
|
|
3210
|
+
wiki_exports,
|
|
3211
|
+
// Low-level tools
|
|
3212
|
+
list_tasks_exports,
|
|
3213
|
+
get_task_exports,
|
|
3214
|
+
get_comments_exports,
|
|
3215
|
+
get_diff_exports,
|
|
3216
|
+
run_command_exports,
|
|
3217
|
+
list_files_exports,
|
|
3218
|
+
grep_code_exports,
|
|
3219
|
+
ask_user_exports
|
|
3220
|
+
];
|
|
3221
|
+
|
|
3222
|
+
// src/lib/agent-ui.ts
|
|
3223
|
+
import chalk12 from "chalk";
|
|
3224
|
+
function formatInput(input3) {
|
|
3225
|
+
return Object.entries(input3).map(([k, v]) => {
|
|
3226
|
+
if (typeof v === "number") return `${k}=${v}`;
|
|
3227
|
+
if (typeof v === "string") {
|
|
3228
|
+
if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
|
|
3229
|
+
return `${k}="${v}"`;
|
|
3230
|
+
}
|
|
3231
|
+
return `${k}=${JSON.stringify(v)}`;
|
|
3232
|
+
}).join(" ");
|
|
3233
|
+
}
|
|
3234
|
+
function summarize(result) {
|
|
3235
|
+
const first = result.split("\n").find((l) => l.trim()) ?? result;
|
|
3236
|
+
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
3237
|
+
}
|
|
3238
|
+
function printToolCall(name, input3) {
|
|
3239
|
+
const params = formatInput(input3);
|
|
3240
|
+
console.log(` ${chalk12.cyan("\u2192")} ${chalk12.bold(name)}${params ? " " + chalk12.dim(params) : ""}`);
|
|
3241
|
+
}
|
|
3242
|
+
function printToolResult(result) {
|
|
3243
|
+
const ok = !result.startsWith("Error:");
|
|
3244
|
+
const icon = ok ? chalk12.green("\u2713") : chalk12.red("\u2717");
|
|
3245
|
+
console.log(` ${icon} ${chalk12.dim(summarize(result))}`);
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
// src/lib/sub-agent.ts
|
|
3249
|
+
async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
|
|
3250
|
+
const client = createClient(config);
|
|
3251
|
+
const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
|
|
3252
|
+
const tools2 = selected.map((m) => m.definition);
|
|
3253
|
+
const messages = [
|
|
3254
|
+
{ role: "system", content: systemPrompt },
|
|
3255
|
+
{ role: "user", content: userMessage }
|
|
3256
|
+
];
|
|
3257
|
+
const MAX_ITERATIONS = 100;
|
|
3258
|
+
let iterations = 0;
|
|
3259
|
+
for (; ; ) {
|
|
3260
|
+
if (++iterations > MAX_ITERATIONS) {
|
|
3261
|
+
throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
3262
|
+
}
|
|
3263
|
+
const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
|
|
3264
|
+
const choice = res.choices[0];
|
|
3265
|
+
messages.push({
|
|
3266
|
+
role: "assistant",
|
|
3267
|
+
content: choice.message.content ?? null,
|
|
3268
|
+
...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
|
|
3269
|
+
});
|
|
3270
|
+
if (choice.finish_reason === "stop") {
|
|
3271
|
+
return choice.message.content ?? "";
|
|
3272
|
+
}
|
|
3273
|
+
if (choice.finish_reason === "tool_calls") {
|
|
3274
|
+
for (const tc of choice.message.tool_calls ?? []) {
|
|
3275
|
+
let input3;
|
|
3276
|
+
try {
|
|
3277
|
+
input3 = JSON.parse(tc.function.arguments);
|
|
3278
|
+
} catch {
|
|
3279
|
+
input3 = {};
|
|
3280
|
+
}
|
|
3281
|
+
printToolCall(tc.function.name, input3);
|
|
3282
|
+
const mod = selected.find((m) => m.definition.function.name === tc.function.name);
|
|
3283
|
+
const result = mod ? await mod.execute(input3, config) : `Unknown tool: ${tc.function.name}`;
|
|
3284
|
+
printToolResult(result);
|
|
3285
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
// src/tools/wiki/prompts.ts
|
|
3292
|
+
var WIKI_FORMAT = `
|
|
3293
|
+
The document you produce must be valid Markdown with these exact sections:
|
|
3294
|
+
|
|
3295
|
+
# [Project Name]
|
|
3296
|
+
|
|
3297
|
+
> One-sentence description of what this project does.
|
|
3298
|
+
|
|
3299
|
+
## What Is This?
|
|
3300
|
+
|
|
3301
|
+
2-4 paragraphs covering:
|
|
3302
|
+
- The problem this project solves
|
|
3303
|
+
- Who uses it and in what context
|
|
3304
|
+
- Core capabilities / key features
|
|
3305
|
+
|
|
3306
|
+
## Quick Start
|
|
3307
|
+
|
|
3308
|
+
Numbered steps for a brand-new developer to install, configure, and run the project for the first time.
|
|
3309
|
+
|
|
3310
|
+
## Architecture
|
|
3311
|
+
|
|
3312
|
+
High-level explanation of how the system is structured:
|
|
3313
|
+
- Key components / layers and their responsibilities
|
|
3314
|
+
- Request or data flow (prose or ASCII diagram)
|
|
3315
|
+
- Noteworthy design decisions
|
|
3316
|
+
|
|
3317
|
+
## Key Files
|
|
3318
|
+
|
|
3319
|
+
| File / Directory | Purpose |
|
|
3320
|
+
|---|---|
|
|
3321
|
+
| ... | ... |
|
|
3322
|
+
|
|
3323
|
+
(List the 8-15 most important files.)
|
|
3324
|
+
|
|
3325
|
+
## Development Workflow
|
|
3326
|
+
|
|
3327
|
+
Common day-to-day tasks a contributor will need:
|
|
3328
|
+
- How to build / run locally
|
|
3329
|
+
- How to add a new feature (brief steps)
|
|
3330
|
+
- Any testing or linting commands
|
|
3331
|
+
|
|
3332
|
+
---
|
|
3333
|
+
*Maintained by Techunter \u2014 run \`tch wiki\` to regenerate*
|
|
3334
|
+
`;
|
|
3335
|
+
|
|
3336
|
+
// src/tools/wiki/wiki-generator.ts
|
|
3337
|
+
async function generateWiki(config) {
|
|
3338
|
+
return runSubAgentLoop(
|
|
3339
|
+
config,
|
|
3340
|
+
"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,
|
|
3341
|
+
"Analyze this project thoroughly and produce a comprehensive TECHUNTER.md overview document for new team members.",
|
|
3342
|
+
["list_files", "grep_code", "run_command"]
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/commands/init.ts
|
|
3347
|
+
async function getGitHubTokenViaPAT() {
|
|
3348
|
+
console.log(chalk13.dim("\n Create a token at: https://github.com/settings/tokens/new"));
|
|
3349
|
+
console.log(chalk13.dim(" Required scopes: repo, read:user\n"));
|
|
3350
|
+
const token = await password({
|
|
3351
|
+
message: "GitHub Personal Access Token:",
|
|
3352
|
+
mask: "*"
|
|
3353
|
+
});
|
|
3354
|
+
return { token: token.trim() };
|
|
3355
|
+
}
|
|
3356
|
+
var OAUTH_CLIENT_ID = "Ov23liW4zJ4r2RdZOsCJ";
|
|
3357
|
+
async function getGitHubTokenViaDeviceFlow() {
|
|
3358
|
+
let verificationUri = "";
|
|
3359
|
+
let userCode = "";
|
|
3360
|
+
const auth = createOAuthDeviceAuth({
|
|
3361
|
+
clientType: "oauth-app",
|
|
3362
|
+
clientId: OAUTH_CLIENT_ID,
|
|
3363
|
+
scopes: ["repo"],
|
|
3364
|
+
onVerification(verification) {
|
|
3365
|
+
verificationUri = verification.verification_uri;
|
|
3366
|
+
userCode = verification.user_code;
|
|
3367
|
+
console.log("");
|
|
3368
|
+
console.log(chalk13.bold(" 1. Open this URL in your browser:"));
|
|
3369
|
+
console.log(" " + chalk13.cyan(verificationUri));
|
|
3370
|
+
console.log("");
|
|
3371
|
+
console.log(chalk13.bold(" 2. Enter this code:"));
|
|
3372
|
+
console.log(" " + chalk13.yellow.bold(userCode));
|
|
3373
|
+
console.log("");
|
|
3374
|
+
open2(verificationUri).catch(() => {
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
});
|
|
3378
|
+
const spinner = ora14("Waiting for authorization in browser...").start();
|
|
3379
|
+
let token;
|
|
3380
|
+
try {
|
|
3381
|
+
const result = await auth({ type: "oauth" });
|
|
3382
|
+
token = result.token;
|
|
3383
|
+
spinner.succeed("Authorized!");
|
|
3384
|
+
} catch (err) {
|
|
3385
|
+
spinner.fail("Authorization failed");
|
|
3386
|
+
throw err;
|
|
3387
|
+
}
|
|
3388
|
+
return { token, clientId: OAUTH_CLIENT_ID };
|
|
2816
3389
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
name: "ask_user",
|
|
2830
|
-
description: "Ask the user to clarify something ambiguous \u2014 scope, expected behaviour, edge cases, or business decisions. Do NOT ask about technical implementation choices. Use at most 3 times per task.",
|
|
2831
|
-
parameters: {
|
|
2832
|
-
type: "object",
|
|
2833
|
-
properties: {
|
|
2834
|
-
question: { type: "string", description: "The question to ask the user" },
|
|
2835
|
-
options: {
|
|
2836
|
-
type: "array",
|
|
2837
|
-
items: { type: "string" },
|
|
2838
|
-
description: "2\u20134 concrete answer choices"
|
|
2839
|
-
}
|
|
2840
|
-
},
|
|
2841
|
-
required: ["question", "options"]
|
|
3390
|
+
async function initCommand() {
|
|
3391
|
+
console.log(chalk13.bold.cyan("\nTechunter \u2014 Initial Setup\n"));
|
|
3392
|
+
let detectedOwner = "";
|
|
3393
|
+
let detectedRepo = "";
|
|
3394
|
+
const remoteUrl = await getRemoteUrl();
|
|
3395
|
+
if (remoteUrl) {
|
|
3396
|
+
const parsed = parseOwnerRepo(remoteUrl);
|
|
3397
|
+
if (parsed) {
|
|
3398
|
+
detectedOwner = parsed.owner;
|
|
3399
|
+
detectedRepo = parsed.repo;
|
|
3400
|
+
console.log(chalk13.dim(`Detected GitHub repo: ${detectedOwner}/${detectedRepo}
|
|
3401
|
+
`));
|
|
2842
3402
|
}
|
|
2843
3403
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3404
|
+
const authMethod = await select11({
|
|
3405
|
+
message: "How would you like to authenticate with GitHub?",
|
|
3406
|
+
choices: [
|
|
3407
|
+
{
|
|
3408
|
+
name: "Browser login (OAuth) \u2014 open a URL and click Authorize",
|
|
3409
|
+
value: "device"
|
|
3410
|
+
},
|
|
3411
|
+
{
|
|
3412
|
+
name: "Personal Access Token (PAT) \u2014 paste a token from github.com/settings/tokens",
|
|
3413
|
+
value: "pat"
|
|
3414
|
+
}
|
|
3415
|
+
]
|
|
3416
|
+
});
|
|
3417
|
+
let githubToken;
|
|
3418
|
+
let githubClientId;
|
|
3419
|
+
if (authMethod === "device") {
|
|
3420
|
+
const result = await getGitHubTokenViaDeviceFlow();
|
|
3421
|
+
githubToken = result.token;
|
|
3422
|
+
githubClientId = result.clientId;
|
|
3423
|
+
} else {
|
|
3424
|
+
const result = await getGitHubTokenViaPAT();
|
|
3425
|
+
githubToken = result.token;
|
|
2854
3426
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
3427
|
+
const providerChoice = await select11({
|
|
3428
|
+
message: "AI provider:",
|
|
3429
|
+
choices: [
|
|
3430
|
+
{ name: `OpenRouter (recommended) ${chalk13.dim(`${DEFAULT_BASE_URL} \xB7 ${DEFAULT_MODEL}`)}`, value: "openrouter" },
|
|
3431
|
+
{ name: "Custom (specify base URL and model)", value: "custom" }
|
|
3432
|
+
]
|
|
3433
|
+
});
|
|
3434
|
+
let aiBaseUrl;
|
|
3435
|
+
let aiModel;
|
|
3436
|
+
if (providerChoice === "custom") {
|
|
3437
|
+
aiBaseUrl = (await input({ message: "API base URL:", default: DEFAULT_BASE_URL })).trim();
|
|
3438
|
+
aiModel = (await input({ message: "Model name:", default: DEFAULT_MODEL })).trim();
|
|
3439
|
+
}
|
|
3440
|
+
const apiKeyHint = providerChoice === "openrouter" ? chalk13.dim(" Get a key at: https://openrouter.ai/settings/keys\n") : chalk13.dim(" API key for your provider\n");
|
|
3441
|
+
console.log(apiKeyHint);
|
|
3442
|
+
const aiApiKey = await password({
|
|
3443
|
+
message: "API Key:",
|
|
3444
|
+
mask: "*"
|
|
3445
|
+
});
|
|
3446
|
+
let owner = detectedOwner;
|
|
3447
|
+
let repo = detectedRepo;
|
|
3448
|
+
if (!owner || !repo) {
|
|
3449
|
+
owner = await input({
|
|
3450
|
+
message: "GitHub repo owner (user or org):",
|
|
3451
|
+
required: true
|
|
3452
|
+
});
|
|
3453
|
+
repo = await input({
|
|
3454
|
+
message: "GitHub repo name:",
|
|
3455
|
+
required: true
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
const config = {
|
|
3459
|
+
githubToken,
|
|
3460
|
+
githubClientId,
|
|
3461
|
+
aiApiKey: aiApiKey.trim(),
|
|
3462
|
+
...aiBaseUrl ? { aiBaseUrl } : {},
|
|
3463
|
+
...aiModel ? { aiModel } : {},
|
|
3464
|
+
github: {
|
|
3465
|
+
owner: owner.trim(),
|
|
3466
|
+
repo: repo.trim()
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
setConfig(config);
|
|
3470
|
+
const spinner = ora14("Setting up GitHub labels...").start();
|
|
2857
3471
|
try {
|
|
2858
|
-
|
|
2859
|
-
|
|
3472
|
+
await ensureLabels(config);
|
|
3473
|
+
spinner.succeed("GitHub labels created");
|
|
3474
|
+
} catch (err) {
|
|
3475
|
+
spinner.fail("Failed to create labels (check token permissions)");
|
|
3476
|
+
console.error(chalk13.red(String(err)));
|
|
3477
|
+
}
|
|
3478
|
+
console.log(chalk13.green("\nSetup complete!"));
|
|
3479
|
+
console.log(chalk13.dim(`Config saved to: ${getConfigPath()}
|
|
3480
|
+
`));
|
|
3481
|
+
let genWiki = false;
|
|
3482
|
+
try {
|
|
3483
|
+
genWiki = await select11({
|
|
3484
|
+
message: "Generate TECHUNTER.md project overview for new team members?",
|
|
2860
3485
|
choices: [
|
|
2861
|
-
|
|
2862
|
-
{ name:
|
|
3486
|
+
{ name: "Yes, generate now", value: true },
|
|
3487
|
+
{ name: "No, skip (run /wiki later)", value: false }
|
|
2863
3488
|
]
|
|
2864
3489
|
});
|
|
2865
|
-
answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
|
|
2866
3490
|
} catch {
|
|
2867
|
-
answer = "User skipped this question \u2014 use your best judgement.";
|
|
2868
3491
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
3492
|
+
if (genWiki) {
|
|
3493
|
+
const wikiSpinner = ora14("Analyzing project and generating TECHUNTER.md\u2026").start();
|
|
3494
|
+
try {
|
|
3495
|
+
const content = await generateWiki(config);
|
|
3496
|
+
await upsertRepoFile(config, "TECHUNTER.md", content, "docs: add TECHUNTER.md project overview");
|
|
3497
|
+
wikiSpinner.succeed("TECHUNTER.md created");
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
wikiSpinner.fail(`Could not generate wiki: ${err.message}`);
|
|
3500
|
+
}
|
|
3501
|
+
console.log("");
|
|
3502
|
+
}
|
|
2871
3503
|
}
|
|
2872
3504
|
|
|
2873
|
-
// src/
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3505
|
+
// src/commands/config.ts
|
|
3506
|
+
import { input as input2, password as password2, select as select12 } from "@inquirer/prompts";
|
|
3507
|
+
import chalk14 from "chalk";
|
|
3508
|
+
async function configCommand() {
|
|
3509
|
+
let config;
|
|
3510
|
+
try {
|
|
3511
|
+
config = getConfig();
|
|
3512
|
+
} catch {
|
|
3513
|
+
console.error(chalk14.red("No config found. Run `tch init` first."));
|
|
3514
|
+
process.exit(1);
|
|
3515
|
+
}
|
|
3516
|
+
console.log(chalk14.bold.cyan("\nTechunter \u2014 Settings\n"));
|
|
3517
|
+
console.log(chalk14.dim(`Config file: ${getConfigPath()}
|
|
3518
|
+
`));
|
|
3519
|
+
const currentBaseUrl = config.aiBaseUrl ?? DEFAULT_BASE_URL;
|
|
3520
|
+
const currentModel = config.aiModel ?? DEFAULT_MODEL;
|
|
3521
|
+
const currentBaseBranch = config.baseBranch ?? "main";
|
|
3522
|
+
const field = await select12({
|
|
3523
|
+
message: "Which setting to change?",
|
|
3524
|
+
choices: [
|
|
3525
|
+
{ name: `GitHub repo ${chalk14.dim(`${config.github.owner}/${config.github.repo}`)}`, value: "repo" },
|
|
3526
|
+
{ name: `Base branch ${chalk14.dim(currentBaseBranch)}`, value: "baseBranch" },
|
|
3527
|
+
{ name: `AI base URL ${chalk14.dim(currentBaseUrl)}`, value: "aiBaseUrl" },
|
|
3528
|
+
{ name: `AI model ${chalk14.dim(currentModel)}`, value: "aiModel" },
|
|
3529
|
+
{ name: `AI API Key ${chalk14.dim("(hidden)")}`, value: "aiApiKey" },
|
|
3530
|
+
{ name: `GitHub Token ${chalk14.dim("(hidden)")}`, value: "githubToken" },
|
|
3531
|
+
{ name: "Cancel", value: "cancel" }
|
|
3532
|
+
]
|
|
3533
|
+
});
|
|
3534
|
+
if (field === "cancel") return;
|
|
3535
|
+
if (field === "baseBranch") {
|
|
3536
|
+
const val = await input2({ message: "Base branch name:", default: currentBaseBranch });
|
|
3537
|
+
if (val.trim()) {
|
|
3538
|
+
setConfig({ baseBranch: val.trim() });
|
|
3539
|
+
console.log(chalk14.green(`
|
|
3540
|
+
Base branch set to: ${val.trim()}
|
|
3541
|
+
`));
|
|
3542
|
+
}
|
|
3543
|
+
} else if (field === "repo") {
|
|
3544
|
+
const owner = await input2({ message: "GitHub repo owner:", default: config.github.owner });
|
|
3545
|
+
const repo = await input2({ message: "GitHub repo name:", default: config.github.repo });
|
|
3546
|
+
setConfig({ github: { ...config.github, owner: owner.trim(), repo: repo.trim() } });
|
|
3547
|
+
console.log(chalk14.green(`
|
|
3548
|
+
Repo set to: ${owner.trim()}/${repo.trim()}
|
|
3549
|
+
`));
|
|
3550
|
+
} else if (field === "aiBaseUrl") {
|
|
3551
|
+
const val = await input2({ message: "AI base URL:", default: currentBaseUrl });
|
|
3552
|
+
if (val.trim()) {
|
|
3553
|
+
setConfig({ aiBaseUrl: val.trim() });
|
|
3554
|
+
console.log(chalk14.green(`
|
|
3555
|
+
AI base URL set to: ${val.trim()}
|
|
3556
|
+
`));
|
|
3557
|
+
}
|
|
3558
|
+
} else if (field === "aiModel") {
|
|
3559
|
+
const val = await input2({ message: "AI model name:", default: currentModel });
|
|
3560
|
+
if (val.trim()) {
|
|
3561
|
+
setConfig({ aiModel: val.trim() });
|
|
3562
|
+
console.log(chalk14.green(`
|
|
3563
|
+
AI model set to: ${val.trim()}
|
|
3564
|
+
`));
|
|
3565
|
+
}
|
|
3566
|
+
} else if (field === "aiApiKey") {
|
|
3567
|
+
const val = await password2({ message: "New AI API Key:", mask: "*" });
|
|
3568
|
+
if (val.trim()) {
|
|
3569
|
+
setConfig({ aiApiKey: val.trim() });
|
|
3570
|
+
console.log(chalk14.green("\nAI API Key updated.\n"));
|
|
3571
|
+
}
|
|
3572
|
+
} else if (field === "githubToken") {
|
|
3573
|
+
const val = await password2({ message: "New GitHub Token:", mask: "*" });
|
|
3574
|
+
if (val.trim()) {
|
|
3575
|
+
setConfig({ githubToken: val.trim() });
|
|
3576
|
+
console.log(chalk14.green("\nGitHub Token updated.\n"));
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
// src/index.ts
|
|
3582
|
+
init_github();
|
|
2897
3583
|
|
|
2898
3584
|
// src/lib/agent.ts
|
|
3585
|
+
import ora15 from "ora";
|
|
3586
|
+
import chalk15 from "chalk";
|
|
2899
3587
|
var tools = toolModules.map((m) => m.definition);
|
|
2900
3588
|
var HISTORY_KEEP_TURNS = 8;
|
|
2901
3589
|
function trimHistory(messages) {
|
|
@@ -2981,7 +3669,7 @@ async function runAgentLoop(config, messages) {
|
|
|
2981
3669
|
throw new Error(`Agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
|
|
2982
3670
|
}
|
|
2983
3671
|
trimHistory(messages);
|
|
2984
|
-
const spinner =
|
|
3672
|
+
const spinner = ora15({ text: chalk15.dim("Thinking\u2026"), color: "cyan" }).start();
|
|
2985
3673
|
let response;
|
|
2986
3674
|
try {
|
|
2987
3675
|
response = await client.chat.completions.create({
|
|
@@ -3026,7 +3714,7 @@ async function runAgentLoop(config, messages) {
|
|
|
3026
3714
|
return executeTool(tc.function.name, parsed, config);
|
|
3027
3715
|
})
|
|
3028
3716
|
);
|
|
3029
|
-
let
|
|
3717
|
+
let terminal13 = false;
|
|
3030
3718
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
3031
3719
|
printToolResult(results[i]);
|
|
3032
3720
|
messages.push({
|
|
@@ -3035,16 +3723,93 @@ async function runAgentLoop(config, messages) {
|
|
|
3035
3723
|
content: results[i]
|
|
3036
3724
|
});
|
|
3037
3725
|
if (toolModules.find((m) => m.definition.function.name === toolCalls[i].function.name)?.terminal) {
|
|
3038
|
-
|
|
3726
|
+
terminal13 = true;
|
|
3039
3727
|
}
|
|
3040
3728
|
}
|
|
3041
|
-
if (
|
|
3729
|
+
if (terminal13) return results[results.length - 1];
|
|
3042
3730
|
} else {
|
|
3043
3731
|
return choice.message.content ?? "";
|
|
3044
3732
|
}
|
|
3045
3733
|
}
|
|
3046
3734
|
}
|
|
3047
3735
|
|
|
3736
|
+
// src/lib/update-check.ts
|
|
3737
|
+
import Conf2 from "conf";
|
|
3738
|
+
import chalk16 from "chalk";
|
|
3739
|
+
import { execFile } from "child_process";
|
|
3740
|
+
var PACKAGE_NAME = "techunter";
|
|
3741
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3742
|
+
var cache = new Conf2({
|
|
3743
|
+
projectName: "techunter-update-cache",
|
|
3744
|
+
defaults: { lastChecked: 0, latestVersion: "" }
|
|
3745
|
+
});
|
|
3746
|
+
async function fetchLatestVersion() {
|
|
3747
|
+
try {
|
|
3748
|
+
const { fetch } = await import("undici");
|
|
3749
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
3750
|
+
signal: AbortSignal.timeout(5e3)
|
|
3751
|
+
});
|
|
3752
|
+
if (!res.ok) return null;
|
|
3753
|
+
const data = await res.json();
|
|
3754
|
+
return data.version ?? null;
|
|
3755
|
+
} catch {
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
function isNewer(latest, current) {
|
|
3760
|
+
const parse = (v) => v.split(".").map(Number);
|
|
3761
|
+
const [la, lb, lc] = parse(latest);
|
|
3762
|
+
const [ca, cb, cc] = parse(current);
|
|
3763
|
+
if (la !== ca) return la > ca;
|
|
3764
|
+
if (lb !== cb) return lb > cb;
|
|
3765
|
+
return lc > cc;
|
|
3766
|
+
}
|
|
3767
|
+
async function getAvailableUpdate(currentVersion) {
|
|
3768
|
+
const now = Date.now();
|
|
3769
|
+
const lastChecked = cache.get("lastChecked");
|
|
3770
|
+
let latest = cache.get("latestVersion");
|
|
3771
|
+
if (!latest || now - lastChecked > CHECK_INTERVAL_MS) {
|
|
3772
|
+
const fetched = await fetchLatestVersion();
|
|
3773
|
+
if (fetched) {
|
|
3774
|
+
latest = fetched;
|
|
3775
|
+
cache.set("latestVersion", fetched);
|
|
3776
|
+
cache.set("lastChecked", now);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
return latest && isNewer(latest, currentVersion) ? latest : null;
|
|
3780
|
+
}
|
|
3781
|
+
function installUpdate() {
|
|
3782
|
+
return new Promise((resolve, reject) => {
|
|
3783
|
+
execFile("npm", ["install", "-g", PACKAGE_NAME], { shell: true }, (err, _stdout, stderr) => {
|
|
3784
|
+
if (err) {
|
|
3785
|
+
reject(new Error(stderr.trim() || err.message));
|
|
3786
|
+
} else {
|
|
3787
|
+
resolve(cache.get("latestVersion"));
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
async function startAutoUpdate(currentVersion) {
|
|
3793
|
+
const latest = await Promise.race([
|
|
3794
|
+
getAvailableUpdate(currentVersion),
|
|
3795
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
|
|
3796
|
+
]);
|
|
3797
|
+
if (!latest) return;
|
|
3798
|
+
console.log(
|
|
3799
|
+
chalk16.cyan("\n \u2191 Auto-updating to v" + latest + "\u2026") + chalk16.dim(" (running in background)\n")
|
|
3800
|
+
);
|
|
3801
|
+
installUpdate().then((installedVersion) => {
|
|
3802
|
+
console.log(
|
|
3803
|
+
"\n" + chalk16.green(" \u2714 Updated to v" + (installedVersion || latest)) + chalk16.dim(" \u2014 restart tch to use the new version\n") + chalk16.cyan(" You \u203A ")
|
|
3804
|
+
// redraw the prompt hint
|
|
3805
|
+
);
|
|
3806
|
+
}).catch((err) => {
|
|
3807
|
+
console.log(
|
|
3808
|
+
"\n" + chalk16.red(" \u2718 Auto-update failed: ") + chalk16.dim(err.message) + "\n" + chalk16.dim(" Run manually: npm install -g techunter\n") + chalk16.cyan(" You \u203A ")
|
|
3809
|
+
);
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3048
3813
|
// src/index.ts
|
|
3049
3814
|
var _require = createRequire(import.meta.url);
|
|
3050
3815
|
var { version } = _require("../package.json");
|
|
@@ -3071,6 +3836,8 @@ var SLASH_NAMES = [
|
|
|
3071
3836
|
"/me",
|
|
3072
3837
|
"/code",
|
|
3073
3838
|
"/c",
|
|
3839
|
+
"/wiki",
|
|
3840
|
+
"/w",
|
|
3074
3841
|
"/config",
|
|
3075
3842
|
"/cfg",
|
|
3076
3843
|
"/init"
|
|
@@ -3087,7 +3854,7 @@ function promptUser() {
|
|
|
3087
3854
|
return new Promise((resolve) => {
|
|
3088
3855
|
if (process.stdin.isPaused()) process.stdin.resume();
|
|
3089
3856
|
_rl.resume();
|
|
3090
|
-
_rl.question(
|
|
3857
|
+
_rl.question(chalk17.cyan("You") + chalk17.dim(" \u203A "), resolve);
|
|
3091
3858
|
});
|
|
3092
3859
|
}
|
|
3093
3860
|
var COMMANDS = [
|
|
@@ -3103,44 +3870,45 @@ var COMMANDS = [
|
|
|
3103
3870
|
{ cmd: "/config", alias: "/cfg", desc: "Change settings (branch, repo, API keys)" },
|
|
3104
3871
|
{ cmd: "/init", desc: "Re-run setup wizard for this repo" },
|
|
3105
3872
|
{ cmd: "/status", alias: "/me", desc: "Show tasks assigned to you" },
|
|
3106
|
-
{ cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" }
|
|
3873
|
+
{ cmd: "/code", alias: "/c", desc: "Open Claude Code for the current task branch" },
|
|
3874
|
+
{ cmd: "/wiki", alias: "/w", desc: "Generate or refresh TECHUNTER.md project overview" }
|
|
3107
3875
|
];
|
|
3108
3876
|
function cmdHelp() {
|
|
3109
3877
|
console.log("");
|
|
3110
|
-
console.log(
|
|
3111
|
-
console.log(
|
|
3878
|
+
console.log(chalk17.bold(" Commands"));
|
|
3879
|
+
console.log(chalk17.dim(" \u2500".repeat(35)));
|
|
3112
3880
|
for (const { cmd, alias, desc } of COMMANDS) {
|
|
3113
|
-
const left = (cmd + (alias ? ` ${
|
|
3114
|
-
console.log(` ${
|
|
3881
|
+
const left = (cmd + (alias ? ` ${chalk17.dim(alias)}` : "")).padEnd(22);
|
|
3882
|
+
console.log(` ${chalk17.cyan(cmd)}${alias ? " " + chalk17.dim(alias) : ""}`.padEnd(30) + chalk17.dim(desc));
|
|
3115
3883
|
}
|
|
3116
|
-
console.log(
|
|
3884
|
+
console.log(chalk17.dim("\n Anything else is sent to the AI agent.\n"));
|
|
3117
3885
|
}
|
|
3118
3886
|
function printBanner(config) {
|
|
3119
3887
|
const { owner, repo } = config.github;
|
|
3120
|
-
const g =
|
|
3121
|
-
const b =
|
|
3122
|
-
const p =
|
|
3888
|
+
const g = chalk17.cyan;
|
|
3889
|
+
const b = chalk17.bold.white;
|
|
3890
|
+
const p = chalk17.yellow.bold;
|
|
3123
3891
|
console.log("");
|
|
3124
3892
|
console.log(" " + g("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
3125
3893
|
console.log(p("\u25C6") + b("\u2550\u2550\u2550") + g("\u256C") + b(" TECHUNTER ") + g("\u256C") + b("\u2550\u2550\u2550\u25B6"));
|
|
3126
3894
|
console.log(" " + g("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
3127
3895
|
console.log("");
|
|
3128
3896
|
console.log(
|
|
3129
|
-
" " +
|
|
3897
|
+
" " + chalk17.bold("Techunter") + chalk17.dim(` v${version}`) + chalk17.dim(" \xB7 ") + chalk17.cyan(getModel(config)) + chalk17.dim(" \xB7 ") + chalk17.dim(`${owner}/${repo}`)
|
|
3130
3898
|
);
|
|
3131
3899
|
console.log("");
|
|
3132
3900
|
}
|
|
3133
3901
|
async function initNewRepo(config, owner, repo) {
|
|
3134
3902
|
console.log("");
|
|
3135
|
-
console.log(
|
|
3136
|
-
console.log(
|
|
3903
|
+
console.log(chalk17.bold.cyan(` New repo detected: ${owner}/${repo}`));
|
|
3904
|
+
console.log(chalk17.dim(" Setting up Techunter for this repository...\n"));
|
|
3137
3905
|
const newConfig = {
|
|
3138
3906
|
...config,
|
|
3139
3907
|
github: { owner, repo }
|
|
3140
3908
|
};
|
|
3141
3909
|
setConfig({ github: newConfig.github });
|
|
3142
|
-
const
|
|
3143
|
-
const spinner =
|
|
3910
|
+
const ora16 = (await import("ora")).default;
|
|
3911
|
+
const spinner = ora16("Creating Techunter labels...").start();
|
|
3144
3912
|
try {
|
|
3145
3913
|
await ensureLabels(newConfig);
|
|
3146
3914
|
spinner.succeed("Labels ready");
|
|
@@ -3156,7 +3924,7 @@ async function main() {
|
|
|
3156
3924
|
try {
|
|
3157
3925
|
await configCommand();
|
|
3158
3926
|
} catch (err) {
|
|
3159
|
-
console.error(
|
|
3927
|
+
console.error(chalk17.red(`
|
|
3160
3928
|
Error: ${err.message}`));
|
|
3161
3929
|
process.exit(1);
|
|
3162
3930
|
}
|
|
@@ -3170,7 +3938,7 @@ Error: ${err.message}`));
|
|
|
3170
3938
|
await initCommand();
|
|
3171
3939
|
config = getConfig();
|
|
3172
3940
|
} catch (err) {
|
|
3173
|
-
console.error(
|
|
3941
|
+
console.error(chalk17.red(`
|
|
3174
3942
|
Setup failed: ${err.message}`));
|
|
3175
3943
|
process.exit(1);
|
|
3176
3944
|
return;
|
|
@@ -3188,11 +3956,13 @@ Setup failed: ${err.message}`));
|
|
|
3188
3956
|
}
|
|
3189
3957
|
}
|
|
3190
3958
|
} else if (!config.github.owner) {
|
|
3191
|
-
console.error(
|
|
3959
|
+
console.error(chalk17.red("\nNo git remote found and no repo configured. Run tch init."));
|
|
3192
3960
|
process.exit(1);
|
|
3193
3961
|
}
|
|
3194
3962
|
printBanner(config);
|
|
3195
|
-
console.log(
|
|
3963
|
+
console.log(chalk17.dim(" Type /help for commands, or describe what you want.\n"));
|
|
3964
|
+
startAutoUpdate(version).catch(() => {
|
|
3965
|
+
});
|
|
3196
3966
|
await printTaskList(config);
|
|
3197
3967
|
await printMyTasks(config);
|
|
3198
3968
|
_rl = readline.createInterface({
|
|
@@ -3202,11 +3972,11 @@ Setup failed: ${err.message}`));
|
|
|
3202
3972
|
terminal: true
|
|
3203
3973
|
});
|
|
3204
3974
|
_rl.on("close", () => {
|
|
3205
|
-
console.log(
|
|
3975
|
+
console.log(chalk17.gray("\nGoodbye!"));
|
|
3206
3976
|
process.exit(0);
|
|
3207
3977
|
});
|
|
3208
3978
|
_rl.on("SIGINT", () => {
|
|
3209
|
-
console.log(
|
|
3979
|
+
console.log(chalk17.gray("\nGoodbye!"));
|
|
3210
3980
|
process.exit(0);
|
|
3211
3981
|
});
|
|
3212
3982
|
const messages = [];
|
|
@@ -3223,7 +3993,7 @@ Setup failed: ${err.message}`));
|
|
|
3223
3993
|
break;
|
|
3224
3994
|
case "/refresh":
|
|
3225
3995
|
case "/r":
|
|
3226
|
-
await
|
|
3996
|
+
await run9({}, config);
|
|
3227
3997
|
break;
|
|
3228
3998
|
case "/pick":
|
|
3229
3999
|
case "/p": {
|
|
@@ -3231,7 +4001,7 @@ Setup failed: ${err.message}`));
|
|
|
3231
4001
|
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
3232
4002
|
const result = await run3({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
|
|
3233
4003
|
if (result && result !== "Cancelled.") {
|
|
3234
|
-
console.log(
|
|
4004
|
+
console.log(chalk17.green(`
|
|
3235
4005
|
${result}
|
|
3236
4006
|
`));
|
|
3237
4007
|
}
|
|
@@ -3241,7 +4011,7 @@ Setup failed: ${err.message}`));
|
|
|
3241
4011
|
case "/new":
|
|
3242
4012
|
case "/n": {
|
|
3243
4013
|
const result = await run4({}, config);
|
|
3244
|
-
console.log(
|
|
4014
|
+
console.log(chalk17.green(`
|
|
3245
4015
|
${result}
|
|
3246
4016
|
`));
|
|
3247
4017
|
await printTaskList(config);
|
|
@@ -3259,7 +4029,7 @@ Setup failed: ${err.message}`));
|
|
|
3259
4029
|
const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
|
|
3260
4030
|
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
3261
4031
|
const result = await run11({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
|
|
3262
|
-
console.log(
|
|
4032
|
+
console.log(chalk17.green(`
|
|
3263
4033
|
${result}
|
|
3264
4034
|
`));
|
|
3265
4035
|
await printTaskList(config);
|
|
@@ -3268,7 +4038,7 @@ Setup failed: ${err.message}`));
|
|
|
3268
4038
|
case "/close":
|
|
3269
4039
|
case "/d": {
|
|
3270
4040
|
const result = await run2({}, config);
|
|
3271
|
-
console.log(
|
|
4041
|
+
console.log(chalk17.green(`
|
|
3272
4042
|
${result}
|
|
3273
4043
|
`));
|
|
3274
4044
|
await printTaskList(config);
|
|
@@ -3276,7 +4046,7 @@ Setup failed: ${err.message}`));
|
|
|
3276
4046
|
}
|
|
3277
4047
|
case "/review":
|
|
3278
4048
|
case "/rv": {
|
|
3279
|
-
const result = await
|
|
4049
|
+
const result = await run8({}, config);
|
|
3280
4050
|
console.log("\n" + renderMarkdown(result));
|
|
3281
4051
|
break;
|
|
3282
4052
|
}
|
|
@@ -3290,8 +4060,8 @@ Setup failed: ${err.message}`));
|
|
|
3290
4060
|
case "/ac": {
|
|
3291
4061
|
const arg = userInput.slice(cmd.length).trim().replace(/^#/, "");
|
|
3292
4062
|
const preselected = arg ? parseInt(arg, 10) : void 0;
|
|
3293
|
-
const result = await
|
|
3294
|
-
console.log(
|
|
4063
|
+
const result = await run6({ issue_number: Number.isNaN(preselected) ? void 0 : preselected }, config);
|
|
4064
|
+
console.log(chalk17.green(`
|
|
3295
4065
|
${result}
|
|
3296
4066
|
`));
|
|
3297
4067
|
await printTaskList(config);
|
|
@@ -3307,17 +4077,25 @@ Setup failed: ${err.message}`));
|
|
|
3307
4077
|
config = getConfig();
|
|
3308
4078
|
await printTaskList(config);
|
|
3309
4079
|
} catch (err) {
|
|
3310
|
-
console.error(
|
|
4080
|
+
console.error(chalk17.red(`
|
|
3311
4081
|
Init failed: ${err.message}
|
|
3312
4082
|
`));
|
|
3313
4083
|
}
|
|
3314
4084
|
break;
|
|
3315
4085
|
case "/code":
|
|
3316
4086
|
case "/c":
|
|
3317
|
-
await
|
|
4087
|
+
await run10({}, config);
|
|
3318
4088
|
break;
|
|
4089
|
+
case "/wiki":
|
|
4090
|
+
case "/w": {
|
|
4091
|
+
const result = await run12({}, config);
|
|
4092
|
+
console.log(chalk17.green(`
|
|
4093
|
+
${result}
|
|
4094
|
+
`));
|
|
4095
|
+
break;
|
|
4096
|
+
}
|
|
3319
4097
|
default:
|
|
3320
|
-
console.log(
|
|
4098
|
+
console.log(chalk17.yellow(` Unknown command: ${cmd} (try /help)`));
|
|
3321
4099
|
}
|
|
3322
4100
|
continue;
|
|
3323
4101
|
}
|
|
@@ -3325,10 +4103,10 @@ Init failed: ${err.message}
|
|
|
3325
4103
|
messages.push({ role: "user", content: userInput });
|
|
3326
4104
|
try {
|
|
3327
4105
|
const response = await runAgentLoop(config, messages);
|
|
3328
|
-
console.log("\n" +
|
|
4106
|
+
console.log("\n" + chalk17.green("Techunter:") + "\n" + renderMarkdown(response));
|
|
3329
4107
|
} catch (err) {
|
|
3330
4108
|
messages.splice(prevLength);
|
|
3331
|
-
console.error(
|
|
4109
|
+
console.error(chalk17.red(`
|
|
3332
4110
|
Error: ${err.message}
|
|
3333
4111
|
`));
|
|
3334
4112
|
}
|
|
@@ -3336,6 +4114,6 @@ Error: ${err.message}
|
|
|
3336
4114
|
}
|
|
3337
4115
|
}
|
|
3338
4116
|
main().catch((err) => {
|
|
3339
|
-
console.error(
|
|
4117
|
+
console.error(chalk17.red(`Fatal: ${err.message}`));
|
|
3340
4118
|
process.exit(1);
|
|
3341
4119
|
});
|