techunter 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/dist/index.cjs +151307 -0
  2. package/dist/mcp.cjs +155828 -0
  3. package/package.json +67 -68
  4. package/dist/index.js +0 -4298
  5. package/dist/mcp.js +0 -3459
package/dist/mcp.js DELETED
@@ -1,3459 +0,0 @@
1
- #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
- };
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
-
12
- // src/lib/proxy.ts
13
- import { HttpsProxyAgent } from "https-proxy-agent";
14
- import { ProxyAgent } from "undici";
15
- function getProxyUrl() {
16
- return process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? process.env.ALL_PROXY ?? process.env.all_proxy;
17
- }
18
- function getHttpsProxyAgent() {
19
- const proxy = getProxyUrl();
20
- return proxy ? new HttpsProxyAgent(proxy) : void 0;
21
- }
22
- function getUndiciProxyAgent() {
23
- const proxy = getProxyUrl();
24
- return proxy ? new ProxyAgent(proxy) : void 0;
25
- }
26
- var init_proxy = __esm({
27
- "src/lib/proxy.ts"() {
28
- "use strict";
29
- }
30
- });
31
-
32
- // src/lib/github.ts
33
- var github_exports = {};
34
- __export(github_exports, {
35
- acceptTask: () => acceptTask,
36
- claimTask: () => claimTask,
37
- closeTask: () => closeTask,
38
- createPR: () => createPR,
39
- createTask: () => createTask,
40
- editTask: () => editTask,
41
- embedBaseCommit: () => embedBaseCommit,
42
- embedTargetBranch: () => embedTargetBranch,
43
- ensureLabels: () => ensureLabels,
44
- ensureRemoteBranch: () => ensureRemoteBranch,
45
- extractBaseCommit: () => extractBaseCommit,
46
- extractTargetBranch: () => extractTargetBranch,
47
- formatGuideAsMarkdown: () => formatGuideAsMarkdown,
48
- getAuthenticatedUser: () => getAuthenticatedUser,
49
- getBranchHeadSha: () => getBranchHeadSha,
50
- getDefaultBranch: () => getDefaultBranch,
51
- getIssueNumberFromBranch: () => getIssueNumberFromBranch,
52
- getOpenSubtasks: () => getOpenSubtasks,
53
- getRepoFile: () => getRepoFile,
54
- getTask: () => getTask,
55
- getTaskBranch: () => getTaskBranch,
56
- getTaskPR: () => getTaskPR,
57
- getTaskPRDiff: () => getTaskPRDiff,
58
- isCollaborator: () => isCollaborator,
59
- listComments: () => listComments,
60
- listMyTasks: () => listMyTasks,
61
- listTasks: () => listTasks,
62
- listTasksForReview: () => listTasksForReview,
63
- markInReview: () => markInReview,
64
- mergeWorkerIntoBase: () => mergeWorkerIntoBase,
65
- moveTask: () => moveTask,
66
- postComment: () => postComment,
67
- postGuideComment: () => postGuideComment,
68
- rejectTask: () => rejectTask,
69
- upsertRepoFile: () => upsertRepoFile
70
- });
71
- import { Octokit } from "@octokit/rest";
72
- import { fetch as undiciFetch } from "undici";
73
- function createOctokit(token) {
74
- const agent = getUndiciProxyAgent();
75
- return new Octokit({
76
- auth: token,
77
- log: { debug: () => {
78
- }, info: () => {
79
- }, warn: () => {
80
- }, error: () => {
81
- } },
82
- ...agent && {
83
- request: {
84
- fetch: (url, opts) => undiciFetch(url, { ...opts, dispatcher: agent })
85
- }
86
- }
87
- });
88
- }
89
- function parseIssue(issue) {
90
- return {
91
- number: issue.number,
92
- title: issue.title,
93
- body: issue.body ?? null,
94
- state: issue.state,
95
- author: issue.user?.login ?? null,
96
- assignee: issue.assignee?.login ?? null,
97
- labels: (issue.labels ?? []).map(
98
- (l) => typeof l === "string" ? l : l.name ?? ""
99
- ),
100
- htmlUrl: issue.html_url
101
- };
102
- }
103
- async function listTasks(config) {
104
- const octokit = createOctokit(config.githubToken);
105
- const { owner, repo } = config.github;
106
- const { data } = await octokit.issues.listForRepo({
107
- owner,
108
- repo,
109
- state: "open",
110
- per_page: 100
111
- });
112
- return data.filter(
113
- (issue) => !issue.pull_request && issue.labels.some((l) => TECHUNTER_LABELS.has(l.name ?? ""))
114
- ).map(parseIssue).sort((a, b) => a.number - b.number);
115
- }
116
- async function getTask(config, number) {
117
- const octokit = createOctokit(config.githubToken);
118
- const { owner, repo } = config.github;
119
- const { data } = await octokit.issues.get({ owner, repo, issue_number: number });
120
- return parseIssue(data);
121
- }
122
- function embedBaseCommit(body, sha) {
123
- return `${body}
124
-
125
- ${BASE_COMMIT_MARKER}${sha} -->`;
126
- }
127
- function extractBaseCommit(body) {
128
- if (!body) return null;
129
- const match = body.match(/<!-- techunter-base:([a-f0-9]{7,40}) -->/);
130
- return match?.[1] ?? null;
131
- }
132
- function embedTargetBranch(body, branch) {
133
- return `${body}
134
- ${TARGET_BRANCH_MARKER}${branch} -->`;
135
- }
136
- function extractTargetBranch(body) {
137
- if (!body) return null;
138
- const match = body.match(/<!-- techunter-target:([^\s>]+) -->/);
139
- return match?.[1] ?? null;
140
- }
141
- async function createTask(config, title, body, baseCommit, targetBranch) {
142
- const octokit = createOctokit(config.githubToken);
143
- const { owner, repo } = config.github;
144
- await ensureLabels(config);
145
- let finalBody = body ?? "";
146
- if (baseCommit) finalBody = embedBaseCommit(finalBody, baseCommit);
147
- if (targetBranch) finalBody = embedTargetBranch(finalBody, targetBranch);
148
- const { data } = await octokit.issues.create({
149
- owner,
150
- repo,
151
- title,
152
- body: finalBody,
153
- labels: [LABEL_AVAILABLE]
154
- });
155
- return parseIssue(data);
156
- }
157
- async function mergeWorkerIntoBase(config, workerBranch, baseBranch) {
158
- const octokit = createOctokit(config.githubToken);
159
- const { owner, repo } = config.github;
160
- try {
161
- await octokit.repos.merge({
162
- owner,
163
- repo,
164
- base: baseBranch,
165
- head: workerBranch,
166
- commit_message: `chore: merge ${workerBranch} into ${baseBranch}`
167
- });
168
- } catch (err) {
169
- if (err.status === 409) {
170
- throw new Error(
171
- `Merge conflict: ${workerBranch} cannot be merged into ${baseBranch} cleanly. Resolve conflicts manually.`
172
- );
173
- }
174
- throw err;
175
- }
176
- }
177
- async function claimTask(config, number, username) {
178
- const octokit = createOctokit(config.githubToken);
179
- const { owner, repo } = config.github;
180
- await octokit.issues.update({
181
- owner,
182
- repo,
183
- issue_number: number,
184
- assignees: [username]
185
- });
186
- try {
187
- await octokit.issues.removeLabel({
188
- owner,
189
- repo,
190
- issue_number: number,
191
- name: LABEL_AVAILABLE
192
- });
193
- } catch {
194
- }
195
- await octokit.issues.addLabels({
196
- owner,
197
- repo,
198
- issue_number: number,
199
- labels: [LABEL_CLAIMED]
200
- });
201
- }
202
- function formatGuideAsMarkdown(guide, issueNumber) {
203
- const lines = [
204
- `## Task Guide \u2014 #${issueNumber}`,
205
- "",
206
- `> ${guide.summary}`,
207
- ""
208
- ];
209
- if (guide.acceptanceCriteria.length > 0) {
210
- lines.push("### Must Deliver");
211
- for (const item of guide.acceptanceCriteria) lines.push(`- [ ] ${item}`);
212
- lines.push("");
213
- }
214
- if (guide.filesToModify.length > 0) {
215
- lines.push("### Files");
216
- for (const file of guide.filesToModify) lines.push(`- \`${file}\``);
217
- lines.push("");
218
- }
219
- if (guide.suggestedSteps.length > 0) {
220
- lines.push("<details><summary>Implementation steps</summary>");
221
- lines.push("");
222
- guide.suggestedSteps.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
223
- lines.push("</details>");
224
- lines.push("");
225
- }
226
- if (guide.optionalImprovements.length > 0) {
227
- lines.push("### Optional Improvements");
228
- for (const item of guide.optionalImprovements) lines.push(`- ${item}`);
229
- lines.push("");
230
- }
231
- lines.push("---");
232
- lines.push("*Generated by Techunter*");
233
- return lines.join("\n");
234
- }
235
- async function postComment(config, number, body) {
236
- const octokit = createOctokit(config.githubToken);
237
- const { owner, repo } = config.github;
238
- await octokit.issues.createComment({ owner, repo, issue_number: number, body });
239
- }
240
- async function postGuideComment(config, number, guide) {
241
- const octokit = createOctokit(config.githubToken);
242
- const { owner, repo } = config.github;
243
- const body = formatGuideAsMarkdown(guide, number);
244
- await octokit.issues.createComment({
245
- owner,
246
- repo,
247
- issue_number: number,
248
- body
249
- });
250
- }
251
- async function ensureRemoteBranch(config, branchName, fallbackBase) {
252
- const octokit = createOctokit(config.githubToken);
253
- const { owner, repo } = config.github;
254
- try {
255
- await octokit.repos.getBranch({ owner, repo, branch: branchName });
256
- return;
257
- } catch (err) {
258
- if (err.status !== 404) throw err;
259
- }
260
- const { data: baseRef } = await octokit.repos.getBranch({ owner, repo, branch: fallbackBase });
261
- await octokit.git.createRef({
262
- owner,
263
- repo,
264
- ref: `refs/heads/${branchName}`,
265
- sha: baseRef.commit.sha
266
- });
267
- }
268
- async function createPR(config, title, body, branch, base) {
269
- const octokit = createOctokit(config.githubToken);
270
- const { owner, repo } = config.github;
271
- const { data } = await octokit.pulls.create({
272
- owner,
273
- repo,
274
- title,
275
- body,
276
- head: branch,
277
- base
278
- });
279
- return data.html_url;
280
- }
281
- async function markInReview(config, number) {
282
- const octokit = createOctokit(config.githubToken);
283
- const { owner, repo } = config.github;
284
- for (const label of [LABEL_CLAIMED, LABEL_CHANGES_NEEDED]) {
285
- try {
286
- await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
287
- } catch {
288
- }
289
- }
290
- await octokit.issues.addLabels({
291
- owner,
292
- repo,
293
- issue_number: number,
294
- labels: [LABEL_IN_REVIEW]
295
- });
296
- }
297
- async function closeTask(config, number) {
298
- const octokit = createOctokit(config.githubToken);
299
- const { owner, repo } = config.github;
300
- const { data: issue } = await octokit.issues.get({ owner, repo, issue_number: number });
301
- const techunterLabels = issue.labels.map((l) => l.name ?? "").filter((l) => [LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED].includes(l));
302
- await octokit.issues.update({ owner, repo, issue_number: number, state: "closed" });
303
- for (const label of techunterLabels) {
304
- await octokit.issues.removeLabel({ owner, repo, issue_number: number, name: label });
305
- }
306
- }
307
- async function listComments(config, number, limit = 5) {
308
- const octokit = createOctokit(config.githubToken);
309
- const { owner, repo } = config.github;
310
- const { data } = await octokit.issues.listComments({
311
- owner,
312
- repo,
313
- issue_number: number,
314
- per_page: 100
315
- });
316
- return data.slice(-limit).map((c) => ({
317
- id: c.id,
318
- author: c.user?.login ?? "unknown",
319
- body: c.body ?? "",
320
- createdAt: c.created_at
321
- }));
322
- }
323
- async function getAuthenticatedUser(config) {
324
- const octokit = createOctokit(config.githubToken);
325
- const { data } = await octokit.users.getAuthenticated();
326
- return data.login;
327
- }
328
- async function isCollaborator(config, username) {
329
- const octokit = createOctokit(config.githubToken);
330
- const { owner, repo } = config.github;
331
- try {
332
- const { data } = await octokit.repos.getCollaboratorPermissionLevel({ owner, repo, username });
333
- return data.permission === "admin" || data.permission === "write" || data.permission === "maintain";
334
- } catch {
335
- return false;
336
- }
337
- }
338
- async function listMyTasks(config, username) {
339
- const octokit = createOctokit(config.githubToken);
340
- const { owner, repo } = config.github;
341
- const { data } = await octokit.issues.listForRepo({
342
- owner,
343
- repo,
344
- assignee: username,
345
- state: "open",
346
- per_page: 50
347
- });
348
- return data.filter(
349
- (issue) => issue.labels.some(
350
- (l) => l.name === LABEL_CLAIMED || l.name === LABEL_IN_REVIEW || l.name === LABEL_CHANGES_NEEDED
351
- )
352
- ).map(parseIssue);
353
- }
354
- async function listTasksForReview(config, username) {
355
- const octokit = createOctokit(config.githubToken);
356
- const { owner, repo } = config.github;
357
- const { data } = await octokit.issues.listForRepo({
358
- owner,
359
- repo,
360
- creator: username,
361
- labels: LABEL_IN_REVIEW,
362
- state: "open",
363
- per_page: 50
364
- });
365
- return data.map(parseIssue).sort((a, b) => a.number - b.number);
366
- }
367
- async function rejectTask(config, number) {
368
- const octokit = createOctokit(config.githubToken);
369
- const { owner, repo } = config.github;
370
- try {
371
- await octokit.issues.removeLabel({
372
- owner,
373
- repo,
374
- issue_number: number,
375
- name: LABEL_IN_REVIEW
376
- });
377
- } catch {
378
- }
379
- await octokit.issues.addLabels({
380
- owner,
381
- repo,
382
- issue_number: number,
383
- labels: [LABEL_CHANGES_NEEDED]
384
- });
385
- }
386
- async function ensureLabels(config) {
387
- const octokit = createOctokit(config.githubToken);
388
- const { owner, repo } = config.github;
389
- const { data: existing } = await octokit.issues.listLabelsForRepo({ owner, repo, per_page: 100 });
390
- const existingNames = new Set(existing.map((l) => l.name));
391
- await Promise.all(
392
- LABELS.filter((label) => !existingNames.has(label.name)).map(
393
- (label) => octokit.issues.createLabel({ owner, repo, name: label.name, color: label.color, description: label.description }).catch(() => {
394
- })
395
- )
396
- );
397
- }
398
- async function editTask(config, number, title, body) {
399
- const octokit = createOctokit(config.githubToken);
400
- const { owner, repo } = config.github;
401
- await octokit.issues.update({ owner, repo, issue_number: number, title, body });
402
- }
403
- async function upsertRepoFile(config, filePath, content, message) {
404
- const octokit = createOctokit(config.githubToken);
405
- const { owner, repo } = config.github;
406
- let sha;
407
- try {
408
- const { data: data2 } = await octokit.repos.getContent({ owner, repo, path: filePath });
409
- if (!Array.isArray(data2) && data2.type === "file") {
410
- sha = data2.sha;
411
- }
412
- } catch {
413
- }
414
- const { data } = await octokit.repos.createOrUpdateFileContents({
415
- owner,
416
- repo,
417
- path: filePath,
418
- message,
419
- content: Buffer.from(content, "utf-8").toString("base64"),
420
- ...sha ? { sha } : {}
421
- });
422
- return data.content?.html_url ?? `https://github.com/${owner}/${repo}/blob/main/${filePath}`;
423
- }
424
- async function getRepoFile(config, filePath) {
425
- const octokit = createOctokit(config.githubToken);
426
- const { owner, repo } = config.github;
427
- try {
428
- const { data } = await octokit.repos.getContent({ owner, repo, path: filePath });
429
- if (!Array.isArray(data) && data.type === "file" && "content" in data) {
430
- return Buffer.from(data.content, "base64").toString("utf-8");
431
- }
432
- return null;
433
- } catch {
434
- return null;
435
- }
436
- }
437
- async function getDefaultBranch(config) {
438
- const octokit = createOctokit(config.githubToken);
439
- const { owner, repo } = config.github;
440
- const { data } = await octokit.repos.get({ owner, repo });
441
- return data.default_branch;
442
- }
443
- async function getTaskBranch(config, issueNumber) {
444
- const octokit = createOctokit(config.githubToken);
445
- const { owner, repo } = config.github;
446
- const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
447
- const pr = prs.find((p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? ""));
448
- if (pr) return pr.head.ref;
449
- const { data: branches } = await octokit.repos.listBranches({ owner, repo, per_page: 100 });
450
- const taskBranch = branches.find((b) => new RegExp(`^task-${issueNumber}-`).test(b.name));
451
- return taskBranch?.name ?? null;
452
- }
453
- async function getBranchHeadSha(config, branchName) {
454
- const octokit = createOctokit(config.githubToken);
455
- const { owner, repo } = config.github;
456
- try {
457
- const { data } = await octokit.repos.getBranch({ owner, repo, branch: branchName });
458
- return data.commit.sha;
459
- } catch {
460
- return null;
461
- }
462
- }
463
- async function moveTask(config, issueNumber, newTargetBranch, newBaseCommit) {
464
- const octokit = createOctokit(config.githubToken);
465
- const { owner, repo } = config.github;
466
- const { data } = await octokit.issues.get({ owner, repo, issue_number: issueNumber });
467
- let body = data.body ?? "";
468
- body = body.replace(/\n*<!-- techunter-base:[a-f0-9]{7,40} -->/g, "");
469
- body = body.replace(/\n*<!-- techunter-target:[^\s>]+ -->/g, "");
470
- body = embedBaseCommit(body, newBaseCommit);
471
- body = embedTargetBranch(body, newTargetBranch);
472
- await octokit.issues.update({ owner, repo, issue_number: issueNumber, body });
473
- }
474
- async function getTaskPR(config, issueNumber) {
475
- const octokit = createOctokit(config.githubToken);
476
- const { owner, repo } = config.github;
477
- const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
478
- const pr = prs.find(
479
- (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
480
- );
481
- if (!pr) return null;
482
- return { number: pr.number, url: pr.html_url, body: pr.body ?? "", baseBranch: pr.base.ref };
483
- }
484
- async function getOpenSubtasks(config, targetBranch) {
485
- const octokit = createOctokit(config.githubToken);
486
- const { owner, repo } = config.github;
487
- const { data } = await octokit.issues.listForRepo({
488
- owner,
489
- repo,
490
- state: "open",
491
- per_page: 100
492
- });
493
- return data.filter((issue) => !issue.pull_request).filter((issue) => extractTargetBranch(issue.body ?? null) === targetBranch).map((issue) => issue.number);
494
- }
495
- async function getIssueNumberFromBranch(config, branch) {
496
- const octokit = createOctokit(config.githubToken);
497
- const { owner, repo } = config.github;
498
- const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
499
- const pr = prs.find((p) => p.head.ref === branch);
500
- if (!pr) return null;
501
- const match = (pr.body ?? "").match(/Closes #(\d+)/i);
502
- if (!match) return null;
503
- return { issueNumber: parseInt(match[1], 10), prUrl: pr.html_url };
504
- }
505
- async function getTaskPRDiff(config, prNumber) {
506
- const octokit = createOctokit(config.githubToken);
507
- const { owner, repo } = config.github;
508
- const response = await octokit.pulls.get({
509
- owner,
510
- repo,
511
- pull_number: prNumber,
512
- mediaType: { format: "diff" }
513
- });
514
- return response.data;
515
- }
516
- async function acceptTask(config, issueNumber) {
517
- const octokit = createOctokit(config.githubToken);
518
- const { owner, repo } = config.github;
519
- const { data: prs } = await octokit.pulls.list({ owner, repo, state: "open", per_page: 100 });
520
- const pr = prs.find(
521
- (p) => new RegExp(`Closes #${issueNumber}\\b`, "i").test(p.body ?? "")
522
- );
523
- if (!pr) throw new Error(`No open PR found for task #${issueNumber}`);
524
- try {
525
- const { data: merge } = await octokit.pulls.merge({
526
- owner,
527
- repo,
528
- pull_number: pr.number,
529
- merge_method: "merge"
530
- });
531
- await closeTask(config, issueNumber);
532
- return { prNumber: pr.number, prUrl: pr.html_url, sha: merge.sha ?? "", baseBranch: pr.base.ref };
533
- } catch (err) {
534
- if (err.status === 405) {
535
- throw new Error(
536
- `PR #${pr.number} cannot be merged \u2014 may have conflicts or is not in a mergeable state.`
537
- );
538
- }
539
- throw err;
540
- }
541
- }
542
- var LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED, LABELS, TECHUNTER_LABELS, BASE_COMMIT_MARKER, TARGET_BRANCH_MARKER;
543
- var init_github = __esm({
544
- "src/lib/github.ts"() {
545
- "use strict";
546
- init_proxy();
547
- LABEL_AVAILABLE = "techunter:available";
548
- LABEL_CLAIMED = "techunter:claimed";
549
- LABEL_IN_REVIEW = "techunter:in-review";
550
- LABEL_CHANGES_NEEDED = "techunter:changes-needed";
551
- LABELS = [
552
- { name: LABEL_AVAILABLE, color: "0e8a16", description: "Task available to claim" },
553
- { name: LABEL_CLAIMED, color: "e4a000", description: "Task claimed by a developer" },
554
- { name: LABEL_IN_REVIEW, color: "0075ca", description: "Task submitted for review" },
555
- { name: LABEL_CHANGES_NEEDED, color: "e11d48", description: "Task needs changes" }
556
- ];
557
- TECHUNTER_LABELS = /* @__PURE__ */ new Set([LABEL_AVAILABLE, LABEL_CLAIMED, LABEL_IN_REVIEW, LABEL_CHANGES_NEEDED]);
558
- BASE_COMMIT_MARKER = "<!-- techunter-base:";
559
- TARGET_BRANCH_MARKER = "<!-- techunter-target:";
560
- }
561
- });
562
-
563
- // src/mcp.ts
564
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
565
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
566
- import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
567
-
568
- // src/tools/pick/index.ts
569
- var pick_exports = {};
570
- __export(pick_exports, {
571
- definition: () => definition3,
572
- execute: () => execute3,
573
- run: () => run3,
574
- terminal: () => terminal3
575
- });
576
- init_github();
577
- import chalk6 from "chalk";
578
- import ora3 from "ora";
579
- import { select as select3 } from "@inquirer/prompts";
580
-
581
- // src/lib/git.ts
582
- import chalk from "chalk";
583
- import { simpleGit } from "simple-git";
584
- var git = simpleGit();
585
- async function getCurrentBranch() {
586
- const summary = await git.branch();
587
- return summary.current;
588
- }
589
- async function pushBranch(name) {
590
- await git.push("origin", name, ["--set-upstream"]);
591
- }
592
- function makeWorkerBranchName(username) {
593
- const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
594
- return `worker-${slug}`;
595
- }
596
- function makeTaskBranchName(issueNumber, username) {
597
- const slug = username.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "user";
598
- return `task-${issueNumber}-${slug}`;
599
- }
600
- function isTaskBranch(branch) {
601
- return /^task-\d+-/.test(branch);
602
- }
603
- function parseIssueNumberFromBranch(branch) {
604
- const match = branch.match(/^task-(\d+)-/);
605
- return match ? parseInt(match[1], 10) : null;
606
- }
607
- async function getCurrentCommit() {
608
- return (await git.revparse(["HEAD"])).trim();
609
- }
610
- async function switchToBranchOrCreate(name) {
611
- try {
612
- const branches = await git.branch(["-a"]);
613
- const exists = Object.keys(branches.branches).some(
614
- (b) => b === name || b === `remotes/origin/${name}`
615
- );
616
- if (exists) {
617
- await git.checkout(name);
618
- return false;
619
- }
620
- await git.checkoutLocalBranch(name);
621
- return true;
622
- } catch {
623
- await git.checkoutLocalBranch(name);
624
- return true;
625
- }
626
- }
627
- async function getDiffFromCommit(baseCommit) {
628
- const status = await git.status();
629
- const parts = [];
630
- const fileLines = [
631
- ...status.modified.map((f) => ` M ${f}`),
632
- ...status.created.map((f) => ` A ${f}`),
633
- ...status.deleted.map((f) => ` D ${f}`),
634
- ...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
635
- ...status.not_added.map((f) => ` ? ${f}`)
636
- ];
637
- if (fileLines.length > 0) {
638
- parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
639
- const uncommitted = await git.diff(["HEAD"]);
640
- if (uncommitted) {
641
- const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
642
- parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
643
- }
644
- }
645
- const log = await git.log({ from: baseCommit, to: "HEAD" });
646
- if (log.total > 0) {
647
- const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
648
- parts.push(`## Commits since task claimed (${log.total} total)
649
- ` + logLines.join("\n"));
650
- const branchDiff = await git.diff([baseCommit, "HEAD"]);
651
- if (branchDiff) {
652
- const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
653
- parts.push("## Full diff since task claimed\n```diff\n" + capped + "\n```");
654
- }
655
- }
656
- return parts.length > 0 ? parts.join("\n\n") : "No changes since task was claimed.";
657
- }
658
- async function findMergeBase(configuredBase) {
659
- const candidates = configuredBase ? [`origin/${configuredBase}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
660
- const unique = [...new Set(candidates)];
661
- for (const base of unique) {
662
- try {
663
- const result = await git.raw(["merge-base", "HEAD", base]);
664
- return result.trim();
665
- } catch {
666
- }
667
- }
668
- return null;
669
- }
670
- async function getDiff(baseBranch) {
671
- const status = await git.status();
672
- const parts = [];
673
- const fileLines = [
674
- ...status.modified.map((f) => ` M ${f}`),
675
- ...status.created.map((f) => ` A ${f}`),
676
- ...status.deleted.map((f) => ` D ${f}`),
677
- ...status.renamed.map((f) => ` R ${f.from} \u2192 ${f.to}`),
678
- ...status.not_added.map((f) => ` ? ${f}`)
679
- ];
680
- if (fileLines.length > 0) {
681
- parts.push("## Uncommitted changes\n" + fileLines.join("\n"));
682
- const uncommitted = await git.diff(["HEAD"]);
683
- if (uncommitted) {
684
- const capped = uncommitted.length > 8e3 ? uncommitted.slice(0, 8e3) + "\n... (truncated)" : uncommitted;
685
- parts.push("## Uncommitted diff\n```diff\n" + capped + "\n```");
686
- }
687
- }
688
- const mergeBase = await findMergeBase(baseBranch);
689
- if (mergeBase) {
690
- const log = await git.log({ from: mergeBase, to: "HEAD" });
691
- if (log.total > 0) {
692
- const logLines = log.all.map((c) => ` ${c.hash.slice(0, 7)} ${c.message}`);
693
- parts.push(`## Branch commits (${log.total} total)
694
- ` + logLines.join("\n"));
695
- const branchDiff = await git.diff([mergeBase, "HEAD"]);
696
- if (branchDiff) {
697
- const capped = branchDiff.length > 12e3 ? branchDiff.slice(0, 12e3) + "\n... (truncated)" : branchDiff;
698
- parts.push("## Full branch diff vs main\n```diff\n" + capped + "\n```");
699
- }
700
- }
701
- }
702
- return parts.length > 0 ? parts.join("\n\n") : "No changes detected.";
703
- }
704
- async function stageAllAndCommit(message) {
705
- const status = await git.status();
706
- if (!status.isClean()) {
707
- await git.add(".");
708
- await git.commit(message);
709
- } else {
710
- console.log(chalk.dim(" Working tree clean \u2014 no new commit created, pushing existing commits."));
711
- }
712
- const branch = (await git.branch()).current;
713
- await git.push("origin", branch, ["--set-upstream"]);
714
- }
715
- async function syncWithBase(baseBranch) {
716
- await git.fetch("origin", baseBranch);
717
- try {
718
- await git.merge([`origin/${baseBranch}`, "--ff-only"]);
719
- } catch {
720
- await git.merge([`origin/${baseBranch}`, "-m", `chore: sync with ${baseBranch}`]);
721
- }
722
- }
723
- async function getRemoteHeadSha(baseBranch) {
724
- await git.fetch("origin", baseBranch);
725
- return (await git.revparse([`origin/${baseBranch}`])).trim();
726
- }
727
- async function checkoutFromCommit(branchName, sha) {
728
- const branches = await git.branch(["-a"]);
729
- const exists = Object.keys(branches.branches).some(
730
- (b) => b === branchName || b === `remotes/origin/${branchName}`
731
- );
732
- if (exists) {
733
- await git.checkout(branchName);
734
- } else {
735
- await git.checkoutBranch(branchName, sha);
736
- }
737
- }
738
- async function hasUncommittedChanges() {
739
- const status = await git.status();
740
- return !status.isClean();
741
- }
742
- async function stash(message) {
743
- await git.stash(["push", "-u", "-m", message]);
744
- }
745
- async function stashPop() {
746
- await git.stash(["pop"]);
747
- }
748
-
749
- // src/tools/pick/index.ts
750
- init_github();
751
-
752
- // src/lib/config.ts
753
- import Conf from "conf";
754
- import { z } from "zod";
755
- var configSchema = z.object({
756
- aiApiKey: z.string().min(1),
757
- aiBaseUrl: z.string().optional(),
758
- aiModel: z.string().optional(),
759
- githubToken: z.string().min(1),
760
- githubClientId: z.string().optional(),
761
- baseBranch: z.string().optional(),
762
- github: z.object({
763
- owner: z.string().min(1),
764
- repo: z.string().min(1)
765
- }),
766
- taskState: z.object({
767
- activeIssueNumber: z.number().optional(),
768
- baseCommit: z.string().optional(),
769
- activeBranch: z.string().optional()
770
- }).optional()
771
- });
772
- var store = new Conf({
773
- projectName: "techunter",
774
- defaults: {}
775
- });
776
- function getConfig() {
777
- const raw = store.store;
778
- const result = configSchema.safeParse(raw);
779
- if (!result.success) {
780
- throw new Error("Configuration is missing or invalid.");
781
- }
782
- return result.data;
783
- }
784
- function setConfig(partial) {
785
- const current = store.store;
786
- if (partial.github) {
787
- current["github"] = {
788
- ...current["github"] ?? {},
789
- ...partial.github
790
- };
791
- }
792
- if (partial.aiApiKey !== void 0) {
793
- current["aiApiKey"] = partial.aiApiKey;
794
- }
795
- if (partial.aiBaseUrl !== void 0) {
796
- current["aiBaseUrl"] = partial.aiBaseUrl;
797
- }
798
- if (partial.aiModel !== void 0) {
799
- current["aiModel"] = partial.aiModel;
800
- }
801
- if (partial.githubToken !== void 0) {
802
- current["githubToken"] = partial.githubToken;
803
- }
804
- if (partial.githubClientId !== void 0) {
805
- current["githubClientId"] = partial.githubClientId;
806
- }
807
- if (partial.baseBranch !== void 0) {
808
- current["baseBranch"] = partial.baseBranch;
809
- }
810
- if (partial.taskState !== void 0) {
811
- current["taskState"] = {
812
- ...current["taskState"] ?? {},
813
- ...partial.taskState
814
- };
815
- }
816
- store.store = current;
817
- }
818
-
819
- // src/lib/markdown.ts
820
- import { marked } from "marked";
821
- import { markedTerminal } from "marked-terminal";
822
- marked.use(markedTerminal({ showSectionPrefix: false }));
823
- function renderMarkdown(text) {
824
- return marked(text);
825
- }
826
-
827
- // src/lib/display.ts
828
- init_github();
829
- import chalk2 from "chalk";
830
- var LABEL_AVAILABLE2 = "techunter:available";
831
- var LABEL_CLAIMED2 = "techunter:claimed";
832
- var LABEL_IN_REVIEW2 = "techunter:in-review";
833
- var LABEL_CHANGES_NEEDED2 = "techunter:changes-needed";
834
- function getStatus(issue) {
835
- if (issue.labels.includes(LABEL_CHANGES_NEEDED2)) return "changes-needed";
836
- if (issue.labels.includes(LABEL_IN_REVIEW2)) return "in-review";
837
- if (issue.labels.includes(LABEL_CLAIMED2)) return "claimed";
838
- if (issue.labels.includes(LABEL_AVAILABLE2)) return "available";
839
- return "unknown";
840
- }
841
- function colorStatus(status) {
842
- const padded = status.padEnd(14);
843
- switch (status) {
844
- case "available":
845
- return chalk2.green(padded);
846
- case "claimed":
847
- return chalk2.yellow(padded);
848
- case "in-review":
849
- return chalk2.blue(padded);
850
- case "changes-needed":
851
- return chalk2.red(padded);
852
- default:
853
- return padded;
854
- }
855
- }
856
- function parentIssueFromBranch(branch) {
857
- if (!isTaskBranch(branch)) return null;
858
- const match = branch.match(/^task-(\d+)-/);
859
- return match ? parseInt(match[1], 10) : null;
860
- }
861
- function getParentIssueNumber(issue) {
862
- const target = extractTargetBranch(issue.body);
863
- if (!target) return null;
864
- return parentIssueFromBranch(target);
865
- }
866
- function printTaskDetail(issue) {
867
- const divider = chalk2.dim("\u2500".repeat(70));
868
- const parentNum = getParentIssueNumber(issue);
869
- console.log("\n" + divider);
870
- console.log(
871
- chalk2.bold(` #${issue.number}`) + " " + colorStatus(getStatus(issue)) + " " + chalk2.dim(issue.assignee ? `@${issue.assignee}` : "\u2014") + (parentNum ? chalk2.dim(` sub-task of #${parentNum}`) : "")
872
- );
873
- console.log(chalk2.bold("\n " + issue.title));
874
- if (issue.body) {
875
- console.log("");
876
- console.log(renderMarkdown(issue.body));
877
- }
878
- console.log("\n " + chalk2.dim(issue.htmlUrl));
879
- console.log(divider + "\n");
880
- }
881
- async function printTaskList(config) {
882
- try {
883
- const tasks = await listTasks(config);
884
- const divider = chalk2.dim("\u2500".repeat(70));
885
- console.log("");
886
- console.log(chalk2.dim(" " + "#".padEnd(5) + "Status".padEnd(14) + "Assignee".padEnd(16) + "Title"));
887
- console.log(divider);
888
- if (tasks.length === 0) {
889
- console.log(chalk2.dim(" (no tasks)"));
890
- } else {
891
- let printTask2 = function(t, indent, connector, isLast) {
892
- const num = `#${t.number}`.padEnd(5);
893
- const status = colorStatus(getStatus(t));
894
- const assignee = (t.assignee ? `@${t.assignee}` : "\u2014").padEnd(16);
895
- const fullPrefix = indent + connector;
896
- const maxTitle = 36 - fullPrefix.length;
897
- const title = t.title.length > maxTitle ? t.title.slice(0, maxTitle - 3) + "..." : t.title;
898
- console.log(` ${num}${status}${assignee}${chalk2.dim(fullPrefix)}${title}`);
899
- const children = childrenOf.get(t.number) ?? [];
900
- const childIndent = indent + (isLast ? " " : "\u2502 ");
901
- for (let i = 0; i < children.length; i++) {
902
- const childIsLast = i === children.length - 1;
903
- printTask2(children[i], childIndent, childIsLast ? "\u2514\u2500 " : "\u251C\u2500 ", childIsLast);
904
- }
905
- };
906
- var printTask = printTask2;
907
- const taskMap = new Map(tasks.map((t) => [t.number, t]));
908
- const childrenOf = /* @__PURE__ */ new Map();
909
- for (const t of tasks) {
910
- const parentNum = getParentIssueNumber(t);
911
- const key = parentNum !== null && taskMap.has(parentNum) ? parentNum : null;
912
- if (!childrenOf.has(key)) childrenOf.set(key, []);
913
- childrenOf.get(key).push(t);
914
- }
915
- const roots = childrenOf.get(null) ?? [];
916
- for (let i = 0; i < roots.length; i++) {
917
- const isLast = i === roots.length - 1;
918
- printTask2(roots[i], "", isLast ? "\u2514\u2500 " : "\u251C\u2500 ", isLast);
919
- }
920
- }
921
- console.log(divider);
922
- return tasks;
923
- } catch (err) {
924
- console.log(chalk2.yellow(`(Could not load tasks: ${err.message})`));
925
- return [];
926
- }
927
- }
928
-
929
- // src/lib/launch.ts
930
- import { spawn } from "child_process";
931
- import chalk3 from "chalk";
932
- function buildClaudePrompt(issue, branch) {
933
- const lines = [
934
- `You are working on task #${issue.number}: ${issue.title}`,
935
- `Branch: ${branch}`,
936
- ""
937
- ];
938
- if (issue.body) lines.push(issue.body.trim(), "");
939
- lines.push(
940
- "Implement the task. A detailed guide has been posted as a comment on the GitHub issue.",
941
- "When done, return to tch and run /submit to review and deliver."
942
- );
943
- return lines.join("\n");
944
- }
945
- async function launchClaudeCode(issue, branch) {
946
- const prompt = buildClaudePrompt(issue, branch);
947
- console.log(chalk3.dim("\n Launching Claude Code\u2026\n"));
948
- await new Promise((resolve) => {
949
- const safePrompt = prompt.replace(/\r?\n/g, " ").replace(/"/g, "'");
950
- const child = spawn(`claude "${safePrompt}"`, [], { stdio: "inherit", shell: true });
951
- child.on("close", () => resolve());
952
- child.on("error", () => {
953
- console.log(
954
- chalk3.yellow(
955
- " Could not launch claude. Make sure Claude Code is installed:\n npm install -g @anthropic-ai/claude-code"
956
- )
957
- );
958
- resolve();
959
- });
960
- });
961
- }
962
-
963
- // src/tools/submit/index.ts
964
- var submit_exports = {};
965
- __export(submit_exports, {
966
- definition: () => definition,
967
- execute: () => execute,
968
- run: () => run,
969
- terminal: () => terminal
970
- });
971
- init_github();
972
- import chalk5 from "chalk";
973
- import ora from "ora";
974
- import { select, input as promptInput } from "@inquirer/prompts";
975
-
976
- // src/lib/client.ts
977
- init_proxy();
978
- import OpenAI from "openai";
979
- var DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
980
- var DEFAULT_MODEL = "z-ai/glm-5";
981
- function createClient(config) {
982
- return new OpenAI({
983
- baseURL: config.aiBaseUrl ?? DEFAULT_BASE_URL,
984
- apiKey: config.aiApiKey,
985
- httpAgent: getHttpsProxyAgent()
986
- });
987
- }
988
- function getModel(config) {
989
- return config.aiModel ?? DEFAULT_MODEL;
990
- }
991
-
992
- // src/lib/agent-ui.ts
993
- import chalk4 from "chalk";
994
- function formatInput(input) {
995
- return Object.entries(input).map(([k, v]) => {
996
- if (typeof v === "number") return `${k}=${v}`;
997
- if (typeof v === "string") {
998
- if (k === "body" || v.length > 50) return `${k}=[${v.length} chars]`;
999
- return `${k}="${v}"`;
1000
- }
1001
- return `${k}=${JSON.stringify(v)}`;
1002
- }).join(" ");
1003
- }
1004
- function summarize(result) {
1005
- const first = result.split("\n").find((l) => l.trim()) ?? result;
1006
- return first.length > 100 ? first.slice(0, 97) + "..." : first;
1007
- }
1008
- function printToolCall(name, input) {
1009
- const params = formatInput(input);
1010
- console.log(` ${chalk4.cyan("\u2192")} ${chalk4.bold(name)}${params ? " " + chalk4.dim(params) : ""}`);
1011
- }
1012
- function printToolResult(result) {
1013
- const ok = !result.startsWith("Error:");
1014
- const icon = ok ? chalk4.green("\u2713") : chalk4.red("\u2717");
1015
- console.log(` ${icon} ${chalk4.dim(summarize(result))}`);
1016
- }
1017
-
1018
- // src/lib/sub-agent.ts
1019
- async function runSubAgentLoop(config, systemPrompt, userMessage, toolNames) {
1020
- const client = createClient(config);
1021
- const selected = toolModules.filter((m) => toolNames.includes(m.definition.function.name));
1022
- const tools2 = selected.map((m) => m.definition);
1023
- const messages = [
1024
- { role: "system", content: systemPrompt },
1025
- { role: "user", content: userMessage }
1026
- ];
1027
- const MAX_ITERATIONS = 100;
1028
- let iterations = 0;
1029
- for (; ; ) {
1030
- if (++iterations > MAX_ITERATIONS) {
1031
- throw new Error(`Sub-agent exceeded ${MAX_ITERATIONS} iterations without finishing.`);
1032
- }
1033
- const res = await client.chat.completions.create({ model: getModel(config), tools: tools2, messages });
1034
- const choice = res.choices[0];
1035
- messages.push({
1036
- role: "assistant",
1037
- content: choice.message.content ?? null,
1038
- ...choice.message.tool_calls ? { tool_calls: choice.message.tool_calls } : {}
1039
- });
1040
- if (choice.finish_reason === "stop") {
1041
- return choice.message.content ?? "";
1042
- }
1043
- if (choice.finish_reason === "tool_calls") {
1044
- for (const tc of choice.message.tool_calls ?? []) {
1045
- let input;
1046
- try {
1047
- input = JSON.parse(tc.function.arguments);
1048
- } catch {
1049
- input = {};
1050
- }
1051
- printToolCall(tc.function.name, input);
1052
- const mod = selected.find((m) => m.definition.function.name === tc.function.name);
1053
- const result = mod ? await mod.execute(input, config) : `Unknown tool: ${tc.function.name}`;
1054
- printToolResult(result);
1055
- messages.push({ role: "tool", tool_call_id: tc.id, content: result });
1056
- }
1057
- }
1058
- }
1059
- }
1060
-
1061
- // src/tools/submit/prompts.ts
1062
- 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.";
1063
-
1064
- // src/tools/submit/reviewer.ts
1065
- async function reviewChanges(config, issueNumber, issue, diff) {
1066
- return runSubAgentLoop(
1067
- config,
1068
- REVIEWER_SYSTEM_PROMPT,
1069
- `Task #${issueNumber}: ${issue.title}
1070
-
1071
- Acceptance Criteria:
1072
- ${issue.body ?? "(none)"}
1073
-
1074
- Diff:
1075
- ${diff || "(no changes)"}`,
1076
- ["run_command", "grep_code", "get_diff"]
1077
- );
1078
- }
1079
-
1080
- // src/tools/submit/index.ts
1081
- var definition = {
1082
- type: "function",
1083
- function: {
1084
- name: "submit",
1085
- description: "Submit the current task: reviews changes against acceptance criteria, then commits, creates a PR, and marks the issue as in-review. Equivalent to /submit.",
1086
- parameters: {
1087
- type: "object",
1088
- properties: {
1089
- commit_message: { type: "string", description: 'Commit message (optional \u2014 defaults to "complete: {task title}").' }
1090
- },
1091
- required: []
1092
- }
1093
- }
1094
- };
1095
- async function run(_input, config) {
1096
- const taskState = getConfig().taskState;
1097
- const currentBranch = await getCurrentBranch();
1098
- let issueNumber = taskState?.activeIssueNumber && taskState?.activeBranch && currentBranch === taskState.activeBranch ? taskState.activeIssueNumber : void 0;
1099
- if (!issueNumber) {
1100
- const fromBranch = parseIssueNumberFromBranch(currentBranch);
1101
- if (fromBranch) {
1102
- issueNumber = fromBranch;
1103
- } else {
1104
- const found = await getIssueNumberFromBranch(config, currentBranch);
1105
- if (!found) {
1106
- return "No active task found. Claim a task first with /pick.";
1107
- }
1108
- issueNumber = found.issueNumber;
1109
- }
1110
- }
1111
- let spinner = ora("Loading task and diff\u2026").start();
1112
- const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1113
- const [issue, diff, me] = await Promise.all([
1114
- getTask(config, issueNumber),
1115
- diffPromise,
1116
- getAuthenticatedUser(config)
1117
- ]);
1118
- spinner.stop();
1119
- const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
1120
- const branch = await getCurrentBranch();
1121
- const isSelfSubmit = issue.author !== null && issue.author === me;
1122
- spinner = ora("Checking for open sub-tasks\u2026").start();
1123
- const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1124
- spinner.stop();
1125
- if (openSubtaskNumbers.length > 0) {
1126
- return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open:
1127
- ` + openSubtaskNumbers.map((n) => ` - #${n}`).join("\n") + "\nComplete all sub-tasks before submitting.";
1128
- }
1129
- let review = "";
1130
- if (!isSelfSubmit) {
1131
- const reviewSpinner = ora("Reviewing changes\u2026").start();
1132
- try {
1133
- review = await reviewChanges(config, issueNumber, issue, diff);
1134
- } catch (err) {
1135
- review = `(Review failed: ${err.message})`;
1136
- }
1137
- reviewSpinner.stop();
1138
- }
1139
- const divider = chalk5.dim("\u2500".repeat(70));
1140
- console.log("\n" + divider);
1141
- if (isSelfSubmit) {
1142
- console.log(chalk5.yellow(` Self-submit detected \u2014 AI review skipped.`));
1143
- } else {
1144
- console.log(chalk5.bold(` Review \u2014 task #${issueNumber} "${issue.title}"`));
1145
- console.log(divider);
1146
- console.log(renderMarkdown(review));
1147
- }
1148
- console.log(divider + "\n");
1149
- let shouldProceed;
1150
- try {
1151
- shouldProceed = await select({
1152
- message: `Submit task #${issueNumber}?`,
1153
- choices: [
1154
- { name: "Yes, submit", value: true },
1155
- { name: "No, not ready yet", value: false }
1156
- ]
1157
- });
1158
- } catch {
1159
- return "Submit cancelled.";
1160
- }
1161
- if (!shouldProceed) return "Submit cancelled by user.";
1162
- let commitMessage;
1163
- try {
1164
- commitMessage = await promptInput({
1165
- message: "Commit message:",
1166
- default: `complete: ${issue.title}`
1167
- });
1168
- } catch {
1169
- return "Submit cancelled.";
1170
- }
1171
- if (!commitMessage.trim()) return "Submit cancelled.";
1172
- spinner = ora("Committing and pushing\u2026").start();
1173
- try {
1174
- await stageAllAndCommit(commitMessage.trim());
1175
- spinner.stop();
1176
- } catch (err) {
1177
- spinner.stop();
1178
- return `Commit failed: ${err.message}`;
1179
- }
1180
- if (isSelfSubmit) {
1181
- spinner = ora("Closing issue\u2026").start();
1182
- try {
1183
- await closeTask(config, issueNumber);
1184
- spinner.stop();
1185
- } catch (err) {
1186
- spinner.stop();
1187
- console.error(chalk5.yellow(`Warning: failed to close issue: ${err.message}`));
1188
- }
1189
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1190
- return `Task #${issueNumber} committed and closed.
1191
- Commit: "${commitMessage.trim()}"`;
1192
- }
1193
- spinner = ora("Checking for existing PR\u2026").start();
1194
- const existingPR = await getTaskPR(config, issueNumber);
1195
- spinner.stop();
1196
- let prUrl;
1197
- if (existingPR) {
1198
- prUrl = existingPR.url;
1199
- console.log(chalk5.dim(` Existing PR found: ${prUrl} \u2014 updating.`));
1200
- } else {
1201
- spinner = ora("Creating pull request\u2026").start();
1202
- try {
1203
- await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1204
- const prBody = [
1205
- `Closes #${issueNumber}`,
1206
- issue.body ? `
1207
- ${issue.body}` : "",
1208
- review ? `
1209
- ## AI Review
1210
- ${review}` : ""
1211
- ].join("\n").trim();
1212
- prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1213
- spinner.stop();
1214
- } catch (err) {
1215
- spinner.stop();
1216
- return `Committed but PR creation failed: ${err.message}`;
1217
- }
1218
- }
1219
- spinner = ora("Marking as in-review\u2026").start();
1220
- try {
1221
- await markInReview(config, issueNumber);
1222
- spinner.stop();
1223
- } catch (err) {
1224
- spinner.stop();
1225
- return `PR ${existingPR ? "updated" : "created"} (${prUrl}) but failed to update label: ${err.message}`;
1226
- }
1227
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1228
- return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
1229
- Commit: "${commitMessage.trim()}"
1230
- PR: ${prUrl}`;
1231
- }
1232
- async function execute(input, config) {
1233
- const taskState = getConfig().taskState;
1234
- let issueNumber = taskState?.activeIssueNumber;
1235
- if (!issueNumber) {
1236
- const currentBranch = await getCurrentBranch();
1237
- const fromBranch = parseIssueNumberFromBranch(currentBranch);
1238
- if (fromBranch) {
1239
- issueNumber = fromBranch;
1240
- } else {
1241
- const found = await getIssueNumberFromBranch(config, currentBranch);
1242
- if (!found) return "No active task found. Claim a task first.";
1243
- issueNumber = found.issueNumber;
1244
- }
1245
- }
1246
- const diffPromise = taskState?.baseCommit ? getDiffFromCommit(taskState.baseCommit) : getDiff();
1247
- const [issue, diff, branch, me] = await Promise.all([
1248
- getTask(config, issueNumber),
1249
- diffPromise,
1250
- getCurrentBranch(),
1251
- getAuthenticatedUser(config)
1252
- ]);
1253
- const targetBranch = extractTargetBranch(issue.body) ?? makeWorkerBranchName(issue.author ?? me);
1254
- const openSubtaskNumbers = await getOpenSubtasks(config, branch);
1255
- if (openSubtaskNumbers.length > 0) {
1256
- return `Cannot submit: ${openSubtaskNumbers.length} sub-task(s) still open: ` + openSubtaskNumbers.map((n) => `#${n}`).join(", ");
1257
- }
1258
- const isSelfSubmit = issue.author !== null && issue.author === me;
1259
- let review = "";
1260
- if (!isSelfSubmit) {
1261
- try {
1262
- review = await reviewChanges(config, issueNumber, issue, diff);
1263
- } catch (err) {
1264
- review = `(Review failed: ${err.message})`;
1265
- }
1266
- }
1267
- const commitMessage = input["commit_message"]?.trim() || `complete: ${issue.title}`;
1268
- try {
1269
- await stageAllAndCommit(commitMessage);
1270
- } catch (err) {
1271
- return `Commit failed: ${err.message}`;
1272
- }
1273
- if (isSelfSubmit) {
1274
- try {
1275
- await closeTask(config, issueNumber);
1276
- } catch {
1277
- }
1278
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1279
- return `Task #${issueNumber} committed and closed.
1280
- Commit: "${commitMessage}"`;
1281
- }
1282
- const existingPR = await getTaskPR(config, issueNumber);
1283
- let prUrl;
1284
- if (existingPR) {
1285
- prUrl = existingPR.url;
1286
- } else {
1287
- try {
1288
- await ensureRemoteBranch(config, targetBranch, config.baseBranch ?? "main");
1289
- const prBody = [
1290
- `Closes #${issueNumber}`,
1291
- issue.body ? `
1292
- ${issue.body}` : "",
1293
- review ? `
1294
- ## AI Review
1295
- ${review}` : ""
1296
- ].join("\n").trim();
1297
- prUrl = await createPR(config, issue.title, prBody, branch, targetBranch);
1298
- } catch (err) {
1299
- return `Committed but PR creation failed: ${err.message}`;
1300
- }
1301
- }
1302
- try {
1303
- await markInReview(config, issueNumber);
1304
- } catch {
1305
- }
1306
- setConfig({ taskState: { activeIssueNumber: void 0, baseCommit: void 0, activeBranch: void 0 } });
1307
- return `Task #${issueNumber} ${existingPR ? "re-submitted" : "submitted"}.
1308
- Review:
1309
- ${review}
1310
- Commit: "${commitMessage}"
1311
- PR: ${prUrl}`;
1312
- }
1313
- var terminal = true;
1314
-
1315
- // src/tools/close/index.ts
1316
- var close_exports = {};
1317
- __export(close_exports, {
1318
- definition: () => definition2,
1319
- execute: () => execute2,
1320
- run: () => run2,
1321
- terminal: () => terminal2
1322
- });
1323
- init_github();
1324
- import { select as select2 } from "@inquirer/prompts";
1325
- import ora2 from "ora";
1326
- var definition2 = {
1327
- type: "function",
1328
- function: {
1329
- name: "close",
1330
- description: "Close a task (GitHub Issue). Equivalent to /close.",
1331
- parameters: {
1332
- type: "object",
1333
- properties: {
1334
- issue_number: { type: "number", description: "Issue number to close." }
1335
- },
1336
- required: ["issue_number"]
1337
- }
1338
- }
1339
- };
1340
- async function run2(input, config) {
1341
- let issueNumber = input["issue_number"];
1342
- if (!issueNumber) {
1343
- let tasks;
1344
- try {
1345
- tasks = await listTasks(config);
1346
- } catch (err) {
1347
- return `Error loading tasks: ${err.message}`;
1348
- }
1349
- if (tasks.length === 0) return "No tasks found.";
1350
- try {
1351
- issueNumber = await select2({
1352
- message: "Select task to close:",
1353
- choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
1354
- });
1355
- } catch {
1356
- return "Cancelled.";
1357
- }
1358
- }
1359
- let confirmed;
1360
- try {
1361
- confirmed = await select2({
1362
- message: `Close task #${issueNumber}?`,
1363
- choices: [
1364
- { name: "Yes, close it", value: true },
1365
- { name: "No, cancel", value: false }
1366
- ]
1367
- });
1368
- } catch {
1369
- return "Cancelled.";
1370
- }
1371
- if (!confirmed) return "Cancelled.";
1372
- const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
1373
- try {
1374
- await closeTask(config, issueNumber);
1375
- spinner.stop();
1376
- return `Task #${issueNumber} closed.`;
1377
- } catch (err) {
1378
- spinner.stop();
1379
- return `Error: ${err.message}`;
1380
- }
1381
- }
1382
- async function execute2(input, config) {
1383
- const issueNumber = input["issue_number"];
1384
- const spinner = ora2(`Closing #${issueNumber}\u2026`).start();
1385
- try {
1386
- await closeTask(config, issueNumber);
1387
- spinner.stop();
1388
- return `Task #${issueNumber} closed.`;
1389
- } catch (err) {
1390
- spinner.stop();
1391
- return `Error: ${err.message}`;
1392
- }
1393
- }
1394
- var terminal2 = true;
1395
-
1396
- // src/tools/pick/index.ts
1397
- var definition3 = {
1398
- type: "function",
1399
- function: {
1400
- name: "pick",
1401
- description: "Browse the task list and act on a specific task (claim, submit, close, or view). Equivalent to /pick. Use when the user wants to explore or take action on a task.",
1402
- parameters: {
1403
- type: "object",
1404
- properties: {
1405
- issue_number: { type: "number", description: "Issue number to act on." },
1406
- action: {
1407
- type: "string",
1408
- enum: ["claim", "view"],
1409
- description: '"claim" to assign yourself and create a branch; "view" to return task details.'
1410
- }
1411
- },
1412
- required: ["issue_number", "action"]
1413
- }
1414
- }
1415
- };
1416
- async function run3(input, config) {
1417
- const preselected = input["issue_number"];
1418
- let chosenNumber;
1419
- if (preselected !== void 0) {
1420
- chosenNumber = preselected;
1421
- } else {
1422
- let tasks;
1423
- try {
1424
- tasks = await listTasks(config);
1425
- } catch (err) {
1426
- return `Error: ${err.message}`;
1427
- }
1428
- if (tasks.length === 0) return "No tasks found.";
1429
- try {
1430
- chosenNumber = await select3({
1431
- message: "Select a task:",
1432
- choices: tasks.map((t) => ({
1433
- name: `#${String(t.number).padEnd(4)} ${colorStatus(getStatus(t))} ${t.title}`,
1434
- value: t.number
1435
- }))
1436
- });
1437
- } catch {
1438
- return "Cancelled.";
1439
- }
1440
- }
1441
- let issue;
1442
- try {
1443
- issue = await getTask(config, chosenNumber);
1444
- } catch (err) {
1445
- return `Error loading task: ${err.message}`;
1446
- }
1447
- printTaskDetail(issue);
1448
- const status = getStatus(issue);
1449
- if (status === "changes-needed") {
1450
- try {
1451
- const comments = await listComments(config, issue.number, 1);
1452
- if (comments.length > 0) {
1453
- const c = comments[0];
1454
- const divider = chalk6.dim("\u2500".repeat(70));
1455
- console.log(
1456
- chalk6.red.bold(" Latest rejection feedback") + chalk6.dim(` \u2014 @${c.author} \xB7 ${c.createdAt.slice(0, 10)}`)
1457
- );
1458
- console.log(divider);
1459
- console.log(renderMarkdown(c.body));
1460
- console.log(divider + "\n");
1461
- }
1462
- } catch {
1463
- }
1464
- }
1465
- const actions = [];
1466
- if (status === "available") {
1467
- actions.push({ name: "Claim this task", value: "claim" });
1468
- }
1469
- if (status === "claimed") {
1470
- actions.push({ name: "Submit this task", value: "submit" });
1471
- }
1472
- if (status === "changes-needed") {
1473
- const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1474
- const me = await getAuthenticatedUser2(config);
1475
- const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1476
- const currentBranch = await getCurrentBranch();
1477
- if (currentBranch === taskBranch) {
1478
- actions.push({ name: "Submit this task (fixes done)", value: "submit" });
1479
- } else {
1480
- actions.push({ name: `Switch to ${taskBranch} to fix`, value: "switch-fix" });
1481
- }
1482
- }
1483
- actions.push({ name: "Close this task", value: "close" });
1484
- actions.push({ name: "Nothing, just viewing", value: "none" });
1485
- let action;
1486
- try {
1487
- action = await select3({ message: "Action:", choices: actions });
1488
- } catch {
1489
- return "Cancelled.";
1490
- }
1491
- if (action === "none") return `Viewed task #${issue.number}.`;
1492
- if (action === "claim") {
1493
- try {
1494
- const me = await getAuthenticatedUser(config);
1495
- const myTasks = await listMyTasks(config, me);
1496
- const activeTask = myTasks.find((t) => {
1497
- const labels = t.labels;
1498
- return labels.includes("techunter:claimed") || labels.includes("techunter:changes-needed");
1499
- });
1500
- if (activeTask) {
1501
- return `You already have an active task: #${activeTask.number} "${activeTask.title}"
1502
- Finish or submit it before claiming a new one.`;
1503
- }
1504
- let stashed = false;
1505
- if (await hasUncommittedChanges()) {
1506
- let choice;
1507
- try {
1508
- choice = await select3({
1509
- message: "You have uncommitted changes. What would you like to do?",
1510
- choices: [
1511
- { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1512
- { name: "Cancel", value: "cancel" }
1513
- ]
1514
- });
1515
- } catch {
1516
- choice = "cancel";
1517
- }
1518
- if (choice === "cancel") return "Cancelled.";
1519
- await stash(`tch: before claiming #${issue.number}`);
1520
- stashed = true;
1521
- console.log(chalk6.dim(" Changes stashed. Run `git stash pop` after you finish this task to restore them."));
1522
- }
1523
- let spinner = ora3(`Claiming #${issue.number}\u2026`).start();
1524
- await claimTask(config, issue.number, me);
1525
- spinner.stop();
1526
- const taskBranch = makeTaskBranchName(issue.number, me);
1527
- const taskBase = extractBaseCommit(issue.body);
1528
- spinner = ora3(`Creating branch ${taskBranch}${taskBase ? ` from ${taskBase.slice(0, 7)}` : ""}\u2026`).start();
1529
- try {
1530
- if (taskBase) {
1531
- await checkoutFromCommit(taskBranch, taskBase);
1532
- } else {
1533
- await switchToBranchOrCreate(taskBranch);
1534
- }
1535
- spinner.stop();
1536
- spinner = ora3("Pushing task branch\u2026").start();
1537
- try {
1538
- await pushBranch(taskBranch);
1539
- spinner.stop();
1540
- } catch {
1541
- spinner.warn("Could not push task branch \u2014 will push on submit");
1542
- }
1543
- } catch (err) {
1544
- spinner.warn(`Could not switch to ${taskBranch}`);
1545
- if (stashed) {
1546
- try {
1547
- await stashPop();
1548
- console.log(chalk6.dim(" Restored stashed changes."));
1549
- } catch {
1550
- console.log(chalk6.yellow(" Warning: could not restore stash automatically. Run `git stash pop` manually."));
1551
- }
1552
- }
1553
- throw err;
1554
- }
1555
- const baseCommit = await getCurrentCommit();
1556
- setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1557
- console.log(chalk6.green(`
1558
- Claimed! Branch: ${taskBranch} (base: ${baseCommit.slice(0, 7)})
1559
- `));
1560
- let openClaude;
1561
- try {
1562
- openClaude = await select3({
1563
- message: "Open Claude Code for this task?",
1564
- choices: [
1565
- { name: "Yes, start coding now", value: true },
1566
- { name: "No, return to tch", value: false }
1567
- ]
1568
- });
1569
- } catch {
1570
- openClaude = false;
1571
- }
1572
- if (openClaude) await launchClaudeCode(issue, taskBranch);
1573
- return `Task #${issue.number} claimed. Branch: ${taskBranch}`;
1574
- } catch (err) {
1575
- return `Error claiming task: ${err.message}`;
1576
- }
1577
- }
1578
- if (action === "switch-fix") {
1579
- const { getAuthenticatedUser: getAuthenticatedUser2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1580
- const me = await getAuthenticatedUser2(config);
1581
- const taskBranch = issue.assignee ? makeTaskBranchName(issue.number, issue.assignee) : makeTaskBranchName(issue.number, me);
1582
- let stashed = false;
1583
- if (await hasUncommittedChanges()) {
1584
- let choice;
1585
- try {
1586
- choice = await select3({
1587
- message: "You have uncommitted changes. What would you like to do?",
1588
- choices: [
1589
- { name: "Stash changes and switch branch (restore with: git stash pop)", value: "stash" },
1590
- { name: "Cancel", value: "cancel" }
1591
- ]
1592
- });
1593
- } catch {
1594
- choice = "cancel";
1595
- }
1596
- if (choice === "cancel") return "Cancelled.";
1597
- await stash(`tch: before switching to ${taskBranch}`);
1598
- stashed = true;
1599
- console.log(chalk6.dim(" Changes stashed. Run `git stash pop` to restore them later."));
1600
- }
1601
- const spinner = ora3(`Switching to ${taskBranch}\u2026`).start();
1602
- try {
1603
- await switchToBranchOrCreate(taskBranch);
1604
- spinner.stop();
1605
- } catch (err) {
1606
- spinner.warn(`Could not switch to ${taskBranch}: ${err.message}`);
1607
- if (stashed) {
1608
- try {
1609
- await stashPop();
1610
- console.log(chalk6.dim(" Restored stashed changes."));
1611
- } catch {
1612
- console.log(chalk6.yellow(" Run `git stash pop` manually to restore your changes."));
1613
- }
1614
- }
1615
- return `Error: ${err.message}`;
1616
- }
1617
- const baseCommit = extractBaseCommit(issue.body) ?? await getCurrentCommit();
1618
- setConfig({ taskState: { activeIssueNumber: issue.number, baseCommit, activeBranch: taskBranch } });
1619
- console.log(chalk6.green(`
1620
- Switched to ${taskBranch}. Fix the issues then run /submit.
1621
- `));
1622
- return `Switched to ${taskBranch} for task #${issue.number}.`;
1623
- }
1624
- if (action === "submit") return run({}, config);
1625
- if (action === "close") return run2({ issue_number: issue.number }, config);
1626
- return "Cancelled.";
1627
- }
1628
- async function execute3(input, config) {
1629
- const issueNumber = input["issue_number"];
1630
- const action = input["action"];
1631
- let issue;
1632
- try {
1633
- issue = await getTask(config, issueNumber);
1634
- } catch (err) {
1635
- return `Error loading task: ${err.message}`;
1636
- }
1637
- if (action === "view") {
1638
- const status = getStatus(issue);
1639
- const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
1640
- return [`#${issue.number} [${status}] ${assignee} ${issue.title}`, issue.body ?? ""].join("\n\n");
1641
- }
1642
- if (action === "claim") {
1643
- const { getAuthenticatedUser: getAuthenticatedUser2, listMyTasks: listMyTasks2 } = await Promise.resolve().then(() => (init_github(), github_exports));
1644
- const me = await getAuthenticatedUser2(config);
1645
- const myTasks = await listMyTasks2(config, me);
1646
- const activeTask = myTasks.find((t) => {
1647
- return t.labels.includes("techunter:claimed") || t.labels.includes("techunter:changes-needed");
1648
- });
1649
- if (activeTask) {
1650
- return `You already have an active task: #${activeTask.number} "${activeTask.title}". Finish it before claiming a new one.`;
1651
- }
1652
- if (await hasUncommittedChanges()) {
1653
- return "Cannot claim: you have uncommitted changes. Commit or stash them first (git stash).";
1654
- }
1655
- try {
1656
- await claimTask(config, issueNumber, me);
1657
- } catch (err) {
1658
- return `Error claiming task: ${err.message}`;
1659
- }
1660
- const taskBranch = makeTaskBranchName(issue.number, me);
1661
- const taskBase = extractBaseCommit(issue.body);
1662
- try {
1663
- if (taskBase) {
1664
- await checkoutFromCommit(taskBranch, taskBase);
1665
- } else {
1666
- await switchToBranchOrCreate(taskBranch);
1667
- }
1668
- } catch {
1669
- }
1670
- try {
1671
- await pushBranch(taskBranch);
1672
- } catch {
1673
- }
1674
- const baseCommit = await getCurrentCommit();
1675
- setConfig({ taskState: { activeIssueNumber: issueNumber, baseCommit, activeBranch: taskBranch } });
1676
- return `Task #${issueNumber} claimed. Branch: ${taskBranch} (base commit: ${baseCommit.slice(0, 7)})`;
1677
- }
1678
- return `Unknown action: ${action}`;
1679
- }
1680
- var terminal3 = true;
1681
-
1682
- // src/tools/new-task/index.ts
1683
- var new_task_exports = {};
1684
- __export(new_task_exports, {
1685
- definition: () => definition4,
1686
- execute: () => execute4,
1687
- run: () => run4,
1688
- terminal: () => terminal4
1689
- });
1690
- init_github();
1691
- import { select as select4, input as promptInput2 } from "@inquirer/prompts";
1692
- import { writeFile, readFile, mkdtemp, rm } from "fs/promises";
1693
- import { spawn as spawn2 } from "child_process";
1694
- import { tmpdir } from "os";
1695
- import path from "path";
1696
- import ora4 from "ora";
1697
- import chalk7 from "chalk";
1698
- import open from "open";
1699
-
1700
- // src/tools/new-task/prompts.ts
1701
- var GUIDE_FORMAT = `
1702
- ## Guide format
1703
- Detect the language from the task title and write everything \u2014 including section headings \u2014 in that same language. Use plain markdown, no code blocks. Be concise. Include exactly these four sections (translate the heading names accordingly):
1704
-
1705
- ### Task Description
1706
- Describe what needs to be done and why. Cover the background, the problem being solved, and the expected outcome.
1707
-
1708
- ### Files Involved
1709
- List each file path with CREATE/MODIFY, and one sentence describing what changes.
1710
-
1711
- ### Input / Output
1712
- What the feature/fix receives as input and what it produces or affects.
1713
-
1714
- ### Acceptance Criteria
1715
- Checkbox list of only the most essential testable conditions. Maximum 3 items.
1716
- `.trim();
1717
-
1718
- // src/tools/new-task/guide-generator.ts
1719
- async function generateGuide(config, title, revise) {
1720
- const userMessage = revise ? `Revise the following implementation guide for task: "${title}"
1721
-
1722
- User feedback: ${revise.feedback}
1723
-
1724
- Previous guide:
1725
- ${revise.previousGuide}` : `Write an implementation guide for this task: "${title}"`;
1726
- return runSubAgentLoop(
1727
- config,
1728
- "You are a senior engineer writing a brief task guide for a developer. Use list_files then grep_code to identify which files are relevant. Do NOT include code snippets or implementation details. When you have enough context, write the guide.\n\n" + GUIDE_FORMAT,
1729
- userMessage,
1730
- ["list_files", "grep_code", "run_command", "ask_user"]
1731
- );
1732
- }
1733
-
1734
- // src/tools/new-task/index.ts
1735
- async function openInEditor(content) {
1736
- const dir = await mkdtemp(path.join(tmpdir(), "tch-guide-"));
1737
- const file = path.join(dir, "guide.md");
1738
- try {
1739
- await writeFile(file, content, "utf-8");
1740
- const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? (process.platform === "win32" ? "notepad" : "vi");
1741
- await new Promise((resolve, reject) => {
1742
- const child = spawn2(editor, [file], { stdio: "inherit", shell: true });
1743
- child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Editor exited with code ${code}`)));
1744
- child.on("error", reject);
1745
- });
1746
- return await readFile(file, "utf-8");
1747
- } finally {
1748
- await rm(dir, { recursive: true, force: true });
1749
- }
1750
- }
1751
- async function resolveBaseAndTarget(config, me, interactive) {
1752
- const currentBranch = await getCurrentBranch();
1753
- if (isTaskBranch(currentBranch)) {
1754
- if (await hasUncommittedChanges()) {
1755
- if (!interactive) {
1756
- throw new Error("Cannot create sub-task: you have uncommitted changes. Commit them first so the executor starts from the correct base.");
1757
- }
1758
- const { select: inquirerSelect } = await import("@inquirer/prompts");
1759
- let choice;
1760
- try {
1761
- choice = await inquirerSelect({
1762
- message: "You have uncommitted changes. The sub-task executor will start from the last commit \u2014 they won't see your current unsaved work.",
1763
- choices: [
1764
- { name: "Commit first (cancel and commit manually)", value: "cancel" },
1765
- { name: "Continue anyway (executor starts without my unsaved changes)", value: "continue" }
1766
- ]
1767
- });
1768
- } catch {
1769
- choice = "cancel";
1770
- }
1771
- if (choice === "cancel") throw new Error("Cancelled. Commit your changes first, then create the sub-task.");
1772
- }
1773
- const baseCommit2 = await getCurrentCommit();
1774
- return { baseCommit: baseCommit2, targetBranch: currentBranch, isSubtask: true };
1775
- }
1776
- let stashedForSync = false;
1777
- if (await hasUncommittedChanges()) {
1778
- if (!interactive) {
1779
- throw new Error("Cannot create task: you have uncommitted changes. Commit or stash them first (git stash).");
1780
- }
1781
- const { select: inquirerSelect } = await import("@inquirer/prompts");
1782
- let choice;
1783
- try {
1784
- choice = await inquirerSelect({
1785
- message: "You have uncommitted changes. Syncing with main requires a clean working tree.",
1786
- choices: [
1787
- { name: "Stash changes and continue (restore with: git stash pop)", value: "stash" },
1788
- { name: "Cancel", value: "cancel" }
1789
- ]
1790
- });
1791
- } catch {
1792
- choice = "cancel";
1793
- }
1794
- if (choice === "cancel") throw new Error("Cancelled.");
1795
- await stash("tch: before creating new task");
1796
- stashedForSync = true;
1797
- console.log(chalk7.dim(" Changes stashed. Run `git stash pop` after creating the task."));
1798
- }
1799
- const baseBranch = config.baseBranch ?? "main";
1800
- let baseCommit;
1801
- const syncSpinner = ora4(`Syncing with ${baseBranch}\u2026`).start();
1802
- try {
1803
- await syncWithBase(baseBranch);
1804
- baseCommit = await getCurrentCommit();
1805
- syncSpinner.succeed(`Synced with ${baseBranch} (base: ${baseCommit.slice(0, 7)})`);
1806
- } catch {
1807
- syncSpinner.warn(`Could not sync with ${baseBranch} \u2014 recording remote HEAD as base`);
1808
- try {
1809
- baseCommit = await getRemoteHeadSha(baseBranch);
1810
- } catch {
1811
- }
1812
- if (stashedForSync) {
1813
- try {
1814
- await stashPop();
1815
- } catch {
1816
- }
1817
- throw new Error(`Could not sync with ${baseBranch}. Your changes have been restored from stash.`);
1818
- }
1819
- }
1820
- return { baseCommit, targetBranch: makeWorkerBranchName(me), isSubtask: false };
1821
- }
1822
- var definition4 = {
1823
- type: "function",
1824
- function: {
1825
- name: "new_task",
1826
- description: "Create a new task (GitHub Issue): scans the project, generates a full implementation guide, then creates the issue. Equivalent to /new.",
1827
- parameters: {
1828
- type: "object",
1829
- properties: {
1830
- title: { type: "string", description: "Task title." },
1831
- feedback: { type: "string", description: "Optional feedback to revise the generated guide before creating the issue." }
1832
- },
1833
- required: ["title"]
1834
- }
1835
- }
1836
- };
1837
- async function run4(input, config) {
1838
- const authSpinner = ora4("Checking permissions\u2026").start();
1839
- let me;
1840
- let allowed;
1841
- try {
1842
- me = await getAuthenticatedUser(config);
1843
- allowed = await isCollaborator(config, me);
1844
- authSpinner.stop();
1845
- } catch (err) {
1846
- authSpinner.stop();
1847
- return `Error checking permissions: ${err.message}`;
1848
- }
1849
- if (!allowed) {
1850
- return `Permission denied: only repository collaborators can create tasks.`;
1851
- }
1852
- let title = input["title"]?.trim();
1853
- if (!title) {
1854
- try {
1855
- title = (await promptInput2({ message: "Task title:" })).trim();
1856
- } catch {
1857
- return "Cancelled.";
1858
- }
1859
- if (!title) return "Cancelled.";
1860
- }
1861
- const spinner = ora4("Scanning project and generating guide\u2026").start();
1862
- let guide;
1863
- try {
1864
- guide = await generateGuide(config, title);
1865
- spinner.stop();
1866
- } catch (err) {
1867
- spinner.stop();
1868
- return `Error generating guide: ${err.message}`;
1869
- }
1870
- const divider = chalk7.dim("\u2500".repeat(70));
1871
- for (; ; ) {
1872
- console.log("\n" + divider);
1873
- console.log(chalk7.bold(" Generated guide preview"));
1874
- console.log(divider);
1875
- console.log(renderMarkdown(guide));
1876
- console.log(divider + "\n");
1877
- let action;
1878
- try {
1879
- action = await select4({
1880
- message: "Create this task?",
1881
- choices: [
1882
- { name: "Yes, create task", value: "create" },
1883
- { name: "Edit in editor", value: "edit" },
1884
- { name: "Let AI revise", value: "ai" },
1885
- { name: "Cancel", value: "cancel" }
1886
- ]
1887
- });
1888
- } catch {
1889
- return "Cancelled.";
1890
- }
1891
- if (action === "cancel") return "Cancelled.";
1892
- if (action === "create") break;
1893
- if (action === "edit") {
1894
- try {
1895
- guide = await openInEditor(guide);
1896
- } catch (err) {
1897
- console.log(chalk7.yellow(` Editor error: ${err.message}`));
1898
- }
1899
- continue;
1900
- }
1901
- let feedback;
1902
- try {
1903
- feedback = (await promptInput2({ message: "What should be changed?" })).trim();
1904
- } catch {
1905
- return "Cancelled.";
1906
- }
1907
- if (!feedback) continue;
1908
- const reviseSpinner = ora4("Revising guide\u2026").start();
1909
- try {
1910
- guide = await generateGuide(config, title, { feedback, previousGuide: guide });
1911
- reviseSpinner.stop();
1912
- } catch (err) {
1913
- reviseSpinner.stop();
1914
- console.log(chalk7.yellow(` Revision error: ${err.message}`));
1915
- }
1916
- }
1917
- let baseCommit;
1918
- let targetBranch;
1919
- let isSubtask;
1920
- try {
1921
- ({ baseCommit, targetBranch, isSubtask } = await resolveBaseAndTarget(config, me, true));
1922
- } catch (err) {
1923
- return err.message;
1924
- }
1925
- if (isSubtask) {
1926
- console.log(chalk7.dim(` Sub-task: will target branch ${chalk7.cyan(targetBranch)} (base: ${baseCommit?.slice(0, 7) ?? "HEAD"})`));
1927
- }
1928
- const createSpinner = ora4(`Creating "${title}"\u2026`).start();
1929
- let htmlUrl;
1930
- let issueNumber;
1931
- let issueTitle;
1932
- try {
1933
- const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1934
- createSpinner.stop();
1935
- htmlUrl = issue.htmlUrl;
1936
- issueNumber = issue.number;
1937
- issueTitle = issue.title;
1938
- } catch (err) {
1939
- createSpinner.stop();
1940
- return `Error: ${err.message}`;
1941
- }
1942
- console.log(chalk7.green(`
1943
- Created #${issueNumber} "${issueTitle}"
1944
- ${chalk7.dim(htmlUrl)}
1945
- `));
1946
- try {
1947
- const openBrowser = await select4({
1948
- message: "Open issue in browser?",
1949
- choices: [
1950
- { name: "Yes", value: true },
1951
- { name: "No", value: false }
1952
- ]
1953
- });
1954
- if (openBrowser) await open(htmlUrl);
1955
- } catch {
1956
- }
1957
- return `Created #${issueNumber} "${issueTitle}" \u2014 ${htmlUrl}`;
1958
- }
1959
- async function execute4(input, config) {
1960
- const me = await getAuthenticatedUser(config);
1961
- if (!await isCollaborator(config, me)) {
1962
- return `Permission denied: only repository collaborators can create tasks.`;
1963
- }
1964
- const title = input["title"].trim();
1965
- const feedback = input["feedback"];
1966
- let guide = await generateGuide(config, title);
1967
- if (feedback) {
1968
- guide = await generateGuide(config, title, { feedback, previousGuide: guide });
1969
- }
1970
- const { baseCommit, targetBranch } = await resolveBaseAndTarget(config, me, false);
1971
- try {
1972
- const issue = await createTask(config, title, guide, baseCommit, targetBranch);
1973
- return `Created #${issue.number} "${issue.title}" \u2014 ${issue.htmlUrl}
1974
-
1975
- Guide:
1976
- ${guide}`;
1977
- } catch (err) {
1978
- return `Error: ${err.message}`;
1979
- }
1980
- }
1981
- var terminal4 = true;
1982
-
1983
- // src/tools/my-status/index.ts
1984
- var my_status_exports = {};
1985
- __export(my_status_exports, {
1986
- definition: () => definition5,
1987
- execute: () => execute5,
1988
- run: () => run5,
1989
- terminal: () => terminal5
1990
- });
1991
- init_github();
1992
- import ora5 from "ora";
1993
- var definition5 = {
1994
- type: "function",
1995
- function: {
1996
- name: "my_status",
1997
- description: "Show all tasks currently assigned to the authenticated GitHub user. Equivalent to /status.",
1998
- parameters: { type: "object", properties: {}, required: [] }
1999
- }
2000
- };
2001
- async function run5(_input, config) {
2002
- const spinner = ora5("Fetching your tasks\u2026").start();
2003
- try {
2004
- const me = await getAuthenticatedUser(config);
2005
- const tasks = await listMyTasks(config, me);
2006
- spinner.stop();
2007
- if (tasks.length === 0) return `No tasks assigned to @${me}.`;
2008
- const lines = tasks.map((t) => ` #${t.number} [${getStatus(t)}] ${t.title}`);
2009
- return `Tasks assigned to @${me}:
2010
- ${lines.join("\n")}`;
2011
- } catch (err) {
2012
- spinner.stop();
2013
- return `Error: ${err.message}`;
2014
- }
2015
- }
2016
- var execute5 = run5;
2017
- var terminal5 = true;
2018
-
2019
- // src/tools/review/index.ts
2020
- var review_exports = {};
2021
- __export(review_exports, {
2022
- definition: () => definition8,
2023
- execute: () => execute8,
2024
- run: () => run8,
2025
- terminal: () => terminal8
2026
- });
2027
- init_github();
2028
- import chalk10 from "chalk";
2029
- import ora8 from "ora";
2030
- import { select as select7 } from "@inquirer/prompts";
2031
-
2032
- // src/tools/accept/index.ts
2033
- var accept_exports = {};
2034
- __export(accept_exports, {
2035
- definition: () => definition6,
2036
- execute: () => execute6,
2037
- run: () => run6,
2038
- terminal: () => terminal6
2039
- });
2040
- init_github();
2041
- import chalk8 from "chalk";
2042
- import { select as select5 } from "@inquirer/prompts";
2043
- import ora6 from "ora";
2044
-
2045
- // src/tools/wiki/prompts.ts
2046
- var WIKI_FORMAT = `
2047
- The document you produce must be valid Markdown with these exact sections:
2048
-
2049
- # [Project Name]
2050
-
2051
- > One-sentence description of what this project does.
2052
-
2053
- ## What Is This?
2054
-
2055
- 2-4 paragraphs covering:
2056
- - The problem this project solves
2057
- - Who uses it and in what context
2058
- - Core capabilities / key features
2059
-
2060
- ## Quick Start
2061
-
2062
- Numbered steps for a brand-new developer to install, configure, and run the project for the first time.
2063
-
2064
- ## Architecture
2065
-
2066
- High-level explanation of how the system is structured:
2067
- - Key components / layers and their responsibilities
2068
- - Request or data flow (prose or ASCII diagram)
2069
- - Noteworthy design decisions
2070
-
2071
- ## Key Files
2072
-
2073
- | File / Directory | Purpose |
2074
- |---|---|
2075
- | ... | ... |
2076
-
2077
- (List the 8-15 most important files.)
2078
-
2079
- ## Development Workflow
2080
-
2081
- Common day-to-day tasks a contributor will need:
2082
- - How to build / run locally
2083
- - How to add a new feature (brief steps)
2084
- - Any testing or linting commands
2085
-
2086
- ---
2087
- *Maintained by Techunter \u2014 run \`tch wiki\` to regenerate*
2088
- `;
2089
-
2090
- // src/tools/wiki/wiki-generator.ts
2091
- async function generateWiki(config) {
2092
- return runSubAgentLoop(
2093
- config,
2094
- "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,
2095
- "Analyze this project thoroughly and produce a comprehensive TECHUNTER.md overview document for new team members.",
2096
- ["list_files", "grep_code", "run_command"]
2097
- );
2098
- }
2099
-
2100
- // src/tools/accept/index.ts
2101
- var definition6 = {
2102
- type: "function",
2103
- function: {
2104
- name: "accept",
2105
- description: "Accept an in-review task: merges the PR into the target branch and closes the issue.",
2106
- parameters: {
2107
- type: "object",
2108
- properties: {
2109
- issue_number: { type: "number", description: "GitHub issue number to accept" }
2110
- },
2111
- required: ["issue_number"]
2112
- }
2113
- }
2114
- };
2115
- async function run6(input, config) {
2116
- let issueNumber = input["issue_number"];
2117
- if (!issueNumber) {
2118
- const spinner3 = ora6("Loading tasks for review\u2026").start();
2119
- let tasks;
2120
- let me;
2121
- try {
2122
- me = await getAuthenticatedUser(config);
2123
- tasks = await listTasksForReview(config, me);
2124
- spinner3.stop();
2125
- } catch (err) {
2126
- spinner3.stop();
2127
- return `Error: ${err.message}`;
2128
- }
2129
- if (tasks.length === 0) return "No tasks pending review.";
2130
- try {
2131
- issueNumber = await select5({
2132
- message: "Which task to accept?",
2133
- choices: tasks.map((t) => ({
2134
- name: `#${t.number} @${t.assignee ?? "\u2014"} ${t.title}`,
2135
- value: t.number
2136
- }))
2137
- });
2138
- } catch {
2139
- return "Cancelled.";
2140
- }
2141
- }
2142
- const spinner2 = ora6("Verifying permissions\u2026").start();
2143
- let me2;
2144
- let issue;
2145
- try {
2146
- [me2, issue] = await Promise.all([
2147
- getAuthenticatedUser(config),
2148
- getTask(config, issueNumber)
2149
- ]);
2150
- spinner2.stop();
2151
- } catch (err) {
2152
- spinner2.stop();
2153
- return `Error: ${err.message}`;
2154
- }
2155
- if (issue.author && issue.author !== me2) {
2156
- return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2157
- }
2158
- let confirmed;
2159
- try {
2160
- confirmed = await select5({
2161
- message: `Merge PR for #${issueNumber} and close issue?`,
2162
- choices: [
2163
- { name: "Yes, accept", value: true },
2164
- { name: "Cancel", value: false }
2165
- ]
2166
- });
2167
- } catch {
2168
- return "Cancelled.";
2169
- }
2170
- if (!confirmed) return "Cancelled.";
2171
- const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2172
- let result;
2173
- try {
2174
- result = await acceptTask(config, issueNumber);
2175
- spinner.succeed(`PR #${result.prNumber} merged \u2192 ${chalk8.cyan(result.baseBranch)}`);
2176
- } catch (err) {
2177
- spinner.fail("Failed");
2178
- return `Error: ${err.message}`;
2179
- }
2180
- const mergedIntoTaskBranch = isTaskBranch(result.baseBranch);
2181
- if (!mergedIntoTaskBranch) {
2182
- const baseBranch = config.baseBranch ?? "main";
2183
- let pushToMain;
2184
- try {
2185
- pushToMain = await select5({
2186
- message: `Push ${chalk8.cyan(result.baseBranch)} \u2192 ${chalk8.cyan(baseBranch)}?`,
2187
- choices: [
2188
- { name: `Yes, push to ${baseBranch}`, value: true },
2189
- { name: "No, keep in worker branch", value: false }
2190
- ]
2191
- });
2192
- } catch {
2193
- pushToMain = false;
2194
- }
2195
- if (pushToMain) {
2196
- const mergeSpinner = ora6(`Merging ${result.baseBranch} \u2192 ${baseBranch}\u2026`).start();
2197
- try {
2198
- await mergeWorkerIntoBase(config, result.baseBranch, baseBranch);
2199
- mergeSpinner.succeed(`Merged ${result.baseBranch} \u2192 ${baseBranch}`);
2200
- } catch (err) {
2201
- mergeSpinner.fail(`Could not merge to ${baseBranch}: ${err.message}`);
2202
- }
2203
- }
2204
- }
2205
- let updateWiki = false;
2206
- try {
2207
- updateWiki = await select5({
2208
- message: "Update TECHUNTER.md project overview?",
2209
- choices: [
2210
- { name: "Yes, regenerate", value: true },
2211
- { name: "No, skip", value: false }
2212
- ]
2213
- });
2214
- } catch {
2215
- }
2216
- if (updateWiki) {
2217
- const wikiSpinner = ora6("Regenerating TECHUNTER.md\u2026").start();
2218
- try {
2219
- const content = await generateWiki(config);
2220
- await upsertRepoFile(config, "TECHUNTER.md", content, "docs: update TECHUNTER.md project overview");
2221
- wikiSpinner.succeed("TECHUNTER.md updated");
2222
- } catch (err) {
2223
- wikiSpinner.fail(`Wiki update failed: ${err.message}`);
2224
- }
2225
- }
2226
- const mergeTarget = mergedIntoTaskBranch ? `${result.baseBranch} (sub-task merged, no push to main)` : result.baseBranch;
2227
- return `Task #${issueNumber} accepted.
2228
- PR #${result.prNumber} merged \u2192 ${mergeTarget}
2229
- Issue closed.`;
2230
- }
2231
- async function execute6(input, config) {
2232
- const issueNumber = input["issue_number"];
2233
- const [me, issue] = await Promise.all([
2234
- getAuthenticatedUser(config),
2235
- getTask(config, issueNumber)
2236
- ]);
2237
- if (issue.author && issue.author !== me) {
2238
- return `Permission denied: only the task author (@${issue.author}) can accept task #${issueNumber}.`;
2239
- }
2240
- const spinner = ora6(`Merging PR for #${issueNumber}\u2026`).start();
2241
- try {
2242
- const result = await acceptTask(config, issueNumber);
2243
- spinner.stop();
2244
- return `Task #${issueNumber} accepted.
2245
- PR #${result.prNumber} merged \u2192 ${result.baseBranch}
2246
- Issue closed.`;
2247
- } catch (err) {
2248
- spinner.stop();
2249
- return `Error: ${err.message}`;
2250
- }
2251
- }
2252
- var terminal6 = true;
2253
-
2254
- // src/tools/reject/index.ts
2255
- var reject_exports = {};
2256
- __export(reject_exports, {
2257
- definition: () => definition7,
2258
- execute: () => execute7,
2259
- run: () => run7,
2260
- terminal: () => terminal7
2261
- });
2262
- init_github();
2263
- import chalk9 from "chalk";
2264
- import { select as select6, input as promptInput3 } from "@inquirer/prompts";
2265
- import ora7 from "ora";
2266
-
2267
- // src/tools/reject/prompts.ts
2268
- var REJECTION_FORMAT = `
2269
- Detect the language from the reviewer feedback and write everything \u2014 including section headings \u2014 in that same language. Use markdown. Include exactly these four sections (translate the heading names accordingly):
2270
-
2271
- ### \u274C Rejection Reason
2272
- One paragraph: what was reviewed and what the main problem is.
2273
-
2274
- ### \u{1F527} Required Changes
2275
- Numbered, specific, actionable items. Reference file names and function names.
2276
-
2277
- ### \u2705 Failed Acceptance Criteria
2278
- Re-list each criterion that was NOT met, prefixed with \u274C.
2279
-
2280
- ### \u{1F4CB} Next Steps
2281
- Clear instruction on what to fix and how to re-submit (via /submit).
2282
- `.trim();
2283
-
2284
- // src/tools/reject/comment-generator.ts
2285
- async function generateRejectionComment(config, issueNumber, userFeedback) {
2286
- return runSubAgentLoop(
2287
- config,
2288
- "You are a senior engineer writing a structured code review rejection comment. Use get_task to read the acceptance criteria, get_diff or grep_code to inspect the implementation, and get_comments to see prior discussion. Then write the rejection comment.\n\n" + REJECTION_FORMAT,
2289
- `Write a rejection comment for issue #${issueNumber}.
2290
- Reviewer feedback: ${userFeedback}`,
2291
- ["get_task", "get_comments", "get_diff", "grep_code"]
2292
- );
2293
- }
2294
-
2295
- // src/tools/reject/index.ts
2296
- var definition7 = {
2297
- type: "function",
2298
- function: {
2299
- name: "reject",
2300
- description: "Reject an in-review task: collects reviewer feedback, generates a structured rejection comment, shows a preview for confirmation, then posts the comment and marks the issue as changes-needed.",
2301
- parameters: {
2302
- type: "object",
2303
- properties: {
2304
- issue_number: { type: "number", description: "GitHub issue number to reject." },
2305
- feedback: { type: "string", description: "Description of what is wrong with the submission." }
2306
- },
2307
- required: ["issue_number", "feedback"]
2308
- }
2309
- }
2310
- };
2311
- async function run7(input, config) {
2312
- const issueNumber = input["issue_number"];
2313
- const [me, issue] = await Promise.all([
2314
- getAuthenticatedUser(config),
2315
- getTask(config, issueNumber)
2316
- ]);
2317
- if (issue.author && issue.author !== me) {
2318
- return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
2319
- }
2320
- let feedback;
2321
- try {
2322
- feedback = await promptInput3({
2323
- message: `What's wrong with #${issueNumber}? (brief description for the reviewer agent)`
2324
- });
2325
- } catch {
2326
- return "Cancelled.";
2327
- }
2328
- if (!feedback.trim()) return "Cancelled.";
2329
- const divider = chalk9.dim("\u2500".repeat(70));
2330
- for (; ; ) {
2331
- const spinner = ora7("Generating rejection comment\u2026").start();
2332
- let comment;
2333
- try {
2334
- comment = await generateRejectionComment(config, issueNumber, feedback);
2335
- spinner.stop();
2336
- } catch (err) {
2337
- spinner.stop();
2338
- return `Error generating comment: ${err.message}`;
2339
- }
2340
- console.log("\n" + divider);
2341
- console.log(chalk9.bold(` Rejection preview \u2014 issue #${issueNumber}`));
2342
- console.log(divider);
2343
- console.log(renderMarkdown(comment));
2344
- console.log(divider + "\n");
2345
- let decision;
2346
- try {
2347
- decision = await select6({
2348
- message: `Post rejection and mark #${issueNumber} as changes-needed?`,
2349
- choices: [
2350
- { name: "Post & Reject", value: "yes" },
2351
- { name: "Revise \u2014 describe what to change", value: "revise" },
2352
- { name: "Cancel", value: "cancel" }
2353
- ]
2354
- });
2355
- } catch {
2356
- return "Cancelled.";
2357
- }
2358
- if (decision === "cancel") return "User cancelled rejection.";
2359
- if (decision === "revise") {
2360
- try {
2361
- feedback = await promptInput3({ message: "What should be changed?" });
2362
- } catch {
2363
- return "Cancelled.";
2364
- }
2365
- continue;
2366
- }
2367
- let spinner2 = ora7(`Posting rejection comment on #${issueNumber}\u2026`).start();
2368
- try {
2369
- await postComment(config, issueNumber, comment);
2370
- spinner2.stop();
2371
- } catch (err) {
2372
- spinner2.stop();
2373
- return `Error posting comment: ${err.message}`;
2374
- }
2375
- spinner2 = ora7(`Marking #${issueNumber} as changes-needed\u2026`).start();
2376
- try {
2377
- await rejectTask(config, issueNumber);
2378
- spinner2.stop();
2379
- } catch (err) {
2380
- spinner2.stop();
2381
- return `Comment posted but failed to update label: ${err.message}`;
2382
- }
2383
- return `Task #${issueNumber} rejected. Label changed to changes-needed.`;
2384
- }
2385
- }
2386
- async function execute7(input, config) {
2387
- const issueNumber = input["issue_number"];
2388
- const feedback = input["feedback"];
2389
- const [me, issue] = await Promise.all([
2390
- getAuthenticatedUser(config),
2391
- getTask(config, issueNumber)
2392
- ]);
2393
- if (issue.author && issue.author !== me) {
2394
- return `Permission denied: only the task author (@${issue.author}) can reject task #${issueNumber}.`;
2395
- }
2396
- let comment;
2397
- try {
2398
- comment = await generateRejectionComment(config, issueNumber, feedback);
2399
- } catch (err) {
2400
- return `Error generating comment: ${err.message}`;
2401
- }
2402
- try {
2403
- await postComment(config, issueNumber, comment);
2404
- } catch (err) {
2405
- return `Error posting comment: ${err.message}`;
2406
- }
2407
- try {
2408
- await rejectTask(config, issueNumber);
2409
- } catch (err) {
2410
- return `Comment posted but failed to update label: ${err.message}`;
2411
- }
2412
- return `Task #${issueNumber} rejected.
2413
-
2414
- Comment posted:
2415
- ${comment}`;
2416
- }
2417
- var terminal7 = true;
2418
-
2419
- // src/tools/review/index.ts
2420
- var definition8 = {
2421
- type: "function",
2422
- function: {
2423
- name: "review",
2424
- description: "List tasks waiting for your review (submitted by others, created by you), then let you accept or reject one. Equivalent to /review.",
2425
- parameters: { type: "object", properties: {}, required: [] }
2426
- }
2427
- };
2428
- async function run8(_input, config) {
2429
- const spinner = ora8("Loading tasks for review\u2026").start();
2430
- let me;
2431
- let tasks;
2432
- try {
2433
- me = await getAuthenticatedUser(config);
2434
- tasks = await listTasksForReview(config, me);
2435
- spinner.stop();
2436
- } catch (err) {
2437
- spinner.stop();
2438
- return `Error: ${err.message}`;
2439
- }
2440
- if (tasks.length === 0) return `No tasks pending review for @${me}.`;
2441
- let issueNumber;
2442
- try {
2443
- issueNumber = await select7({
2444
- message: "Select a task to review:",
2445
- choices: tasks.map((t) => ({
2446
- name: `#${String(t.number).padEnd(4)} @${t.assignee ?? "\u2014"} ${t.title}`,
2447
- value: t.number
2448
- }))
2449
- });
2450
- } catch {
2451
- return "Cancelled.";
2452
- }
2453
- const spinner2 = ora8(`Loading #${issueNumber}\u2026`).start();
2454
- let pr;
2455
- try {
2456
- pr = await getTaskPR(config, issueNumber);
2457
- spinner2.stop();
2458
- } catch (err) {
2459
- spinner2.stop();
2460
- return `Error loading PR: ${err.message}`;
2461
- }
2462
- const divider = chalk10.dim("\u2500".repeat(70));
2463
- console.log("\n" + divider);
2464
- if (pr) {
2465
- console.log(chalk10.bold(` PR #${pr.number}`) + " " + chalk10.dim(pr.url));
2466
- console.log(divider);
2467
- console.log(renderMarkdown(pr.body));
2468
- } else {
2469
- console.log(chalk10.yellow(` No open PR found for task #${issueNumber}`));
2470
- }
2471
- console.log(divider + "\n");
2472
- for (; ; ) {
2473
- let action;
2474
- try {
2475
- action = await select7({
2476
- message: "Review action:",
2477
- choices: [
2478
- ...pr ? [{ name: "View diff", value: "diff" }] : [],
2479
- { name: chalk10.green("Accept") + " \u2014 merge PR and close issue", value: "accept" },
2480
- { name: chalk10.red("Reject") + " \u2014 request changes", value: "reject" },
2481
- { name: "Nothing, just viewing", value: "none" }
2482
- ]
2483
- });
2484
- } catch {
2485
- return "Cancelled.";
2486
- }
2487
- if (action === "none") return `Viewed task #${issueNumber}.`;
2488
- if (action === "accept") return run6({ issue_number: issueNumber }, config);
2489
- if (action === "reject") return run7({ issue_number: issueNumber }, config);
2490
- if (action === "diff") {
2491
- const diffSpinner = ora8("Fetching diff\u2026").start();
2492
- let diff;
2493
- try {
2494
- diff = await getTaskPRDiff(config, pr.number);
2495
- diffSpinner.stop();
2496
- } catch (err) {
2497
- diffSpinner.stop();
2498
- console.log(chalk10.red(`Error fetching diff: ${err.message}`));
2499
- continue;
2500
- }
2501
- console.log("\n" + divider);
2502
- for (const line of diff.split("\n")) {
2503
- if (line.startsWith("+") && !line.startsWith("+++")) {
2504
- process.stdout.write(chalk10.green(line) + "\n");
2505
- } else if (line.startsWith("-") && !line.startsWith("---")) {
2506
- process.stdout.write(chalk10.red(line) + "\n");
2507
- } else if (line.startsWith("@@")) {
2508
- process.stdout.write(chalk10.cyan(line) + "\n");
2509
- } else if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("+++") || line.startsWith("---")) {
2510
- process.stdout.write(chalk10.bold(line) + "\n");
2511
- } else {
2512
- process.stdout.write(line + "\n");
2513
- }
2514
- }
2515
- console.log(divider + "\n");
2516
- }
2517
- }
2518
- }
2519
- var execute8 = run8;
2520
- var terminal8 = true;
2521
-
2522
- // src/tools/refresh/index.ts
2523
- var refresh_exports = {};
2524
- __export(refresh_exports, {
2525
- definition: () => definition9,
2526
- execute: () => execute9,
2527
- run: () => run9,
2528
- terminal: () => terminal9
2529
- });
2530
- var definition9 = {
2531
- type: "function",
2532
- function: {
2533
- name: "refresh",
2534
- description: "Reload and display the full task list. Equivalent to /refresh.",
2535
- parameters: { type: "object", properties: {}, required: [] }
2536
- }
2537
- };
2538
- async function run9(_input, config) {
2539
- const tasks = await printTaskList(config);
2540
- if (tasks.length === 0) return "No tasks found.";
2541
- const lines = tasks.map((t) => {
2542
- const status = getStatus(t);
2543
- const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
2544
- return `#${t.number} [${status}] ${assignee} ${t.title}`;
2545
- });
2546
- return `Tasks (${tasks.length}):
2547
- ${lines.join("\n")}`;
2548
- }
2549
- var execute9 = run9;
2550
- var terminal9 = true;
2551
-
2552
- // src/tools/open-code/index.ts
2553
- var open_code_exports = {};
2554
- __export(open_code_exports, {
2555
- definition: () => definition10,
2556
- execute: () => execute10,
2557
- run: () => run10,
2558
- terminal: () => terminal10
2559
- });
2560
- init_github();
2561
- var definition10 = {
2562
- type: "function",
2563
- function: {
2564
- name: "open_code",
2565
- description: "Launch Claude Code for the current task branch. Equivalent to /code.",
2566
- parameters: { type: "object", properties: {}, required: [] }
2567
- }
2568
- };
2569
- async function run10(_input, config) {
2570
- let branch;
2571
- try {
2572
- branch = await getCurrentBranch();
2573
- } catch (err) {
2574
- return `Error: ${err.message}`;
2575
- }
2576
- let issueNum = getConfig().taskState?.activeIssueNumber;
2577
- if (!issueNum) {
2578
- const found = await getIssueNumberFromBranch(config, branch);
2579
- if (!found) return `No active task found (current branch: ${branch}).`;
2580
- issueNum = found.issueNumber;
2581
- }
2582
- let issue;
2583
- try {
2584
- issue = await getTask(config, issueNum);
2585
- } catch (err) {
2586
- return `Error: ${err.message}`;
2587
- }
2588
- await launchClaudeCode(issue, branch);
2589
- return "Claude Code session ended.";
2590
- }
2591
- var execute10 = run10;
2592
- var terminal10 = true;
2593
-
2594
- // src/tools/edit-task/index.ts
2595
- var edit_task_exports = {};
2596
- __export(edit_task_exports, {
2597
- definition: () => definition11,
2598
- execute: () => execute11,
2599
- run: () => run11,
2600
- terminal: () => terminal11
2601
- });
2602
- init_github();
2603
- import { select as select8, input as promptInput4 } from "@inquirer/prompts";
2604
- import ora9 from "ora";
2605
- var definition11 = {
2606
- type: "function",
2607
- function: {
2608
- name: "edit_task",
2609
- description: "Edit the title and/or body of an existing task (GitHub Issue). Equivalent to /edit.",
2610
- parameters: {
2611
- type: "object",
2612
- properties: {
2613
- issue_number: { type: "number", description: "Issue number to edit." },
2614
- title: { type: "string", description: "New title." },
2615
- body: { type: "string", description: "New body/description." }
2616
- },
2617
- required: ["issue_number", "title", "body"]
2618
- }
2619
- }
2620
- };
2621
- async function run11(input, config) {
2622
- let issueNumber = input["issue_number"];
2623
- if (!issueNumber) {
2624
- let tasks;
2625
- try {
2626
- tasks = await listTasks(config);
2627
- } catch (err) {
2628
- return `Error loading tasks: ${err.message}`;
2629
- }
2630
- if (tasks.length === 0) return "No tasks found.";
2631
- try {
2632
- issueNumber = await select8({
2633
- message: "Select task to edit:",
2634
- choices: tasks.map((t) => ({ name: `#${t.number} [${getStatus(t)}] ${t.title}`, value: t.number }))
2635
- });
2636
- } catch {
2637
- return "Cancelled.";
2638
- }
2639
- }
2640
- let issue;
2641
- try {
2642
- issue = await getTask(config, issueNumber);
2643
- } catch (err) {
2644
- return `Error loading task: ${err.message}`;
2645
- }
2646
- let title;
2647
- let body;
2648
- try {
2649
- title = await promptInput4({
2650
- message: "Title:",
2651
- default: issue.title
2652
- });
2653
- body = await promptInput4({
2654
- message: "Description:",
2655
- default: issue.body ?? ""
2656
- });
2657
- } catch {
2658
- return "Cancelled.";
2659
- }
2660
- if (title.trim() === issue.title && body.trim() === (issue.body ?? "")) {
2661
- return "No changes made.";
2662
- }
2663
- const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
2664
- try {
2665
- await editTask(config, issueNumber, title.trim() || issue.title, body.trim());
2666
- spinner.stop();
2667
- return `Task #${issueNumber} updated.`;
2668
- } catch (err) {
2669
- spinner.stop();
2670
- return `Error: ${err.message}`;
2671
- }
2672
- }
2673
- async function execute11(input, config) {
2674
- const issueNumber = input["issue_number"];
2675
- const title = input["title"];
2676
- const body = input["body"];
2677
- const spinner = ora9(`Updating #${issueNumber}\u2026`).start();
2678
- try {
2679
- await editTask(config, issueNumber, title, body);
2680
- spinner.stop();
2681
- return `Task #${issueNumber} updated.`;
2682
- } catch (err) {
2683
- spinner.stop();
2684
- return `Error: ${err.message}`;
2685
- }
2686
- }
2687
- var terminal11 = true;
2688
-
2689
- // src/tools/move-task/index.ts
2690
- var move_task_exports = {};
2691
- __export(move_task_exports, {
2692
- definition: () => definition12,
2693
- execute: () => execute12,
2694
- run: () => run12,
2695
- terminal: () => terminal12
2696
- });
2697
- init_github();
2698
- import { select as select9 } from "@inquirer/prompts";
2699
- import ora10 from "ora";
2700
- import chalk11 from "chalk";
2701
- var definition12 = {
2702
- type: "function",
2703
- function: {
2704
- name: "move_task",
2705
- description: "Move one of your own published tasks to be a sub-task of another task. Updates the target branch and base commit so executors sync from the new parent HEAD. Equivalent to /move.",
2706
- parameters: {
2707
- type: "object",
2708
- properties: {
2709
- issue_number: { type: "number", description: "Issue number of the task to move." },
2710
- parent_issue_number: { type: "number", description: "Issue number of the new parent task." }
2711
- },
2712
- required: ["issue_number", "parent_issue_number"]
2713
- }
2714
- }
2715
- };
2716
- async function run12(input, config) {
2717
- const me = await getAuthenticatedUser(config);
2718
- const allTasks = await listTasks(config);
2719
- let issueNumber = input["issue_number"];
2720
- let taskToMove;
2721
- if (issueNumber) {
2722
- try {
2723
- taskToMove = await getTask(config, issueNumber);
2724
- } catch (err) {
2725
- return `Error loading task #${issueNumber}: ${err.message}`;
2726
- }
2727
- if (taskToMove.author !== me) {
2728
- return `Task #${issueNumber} was not authored by you \u2014 you can only move your own tasks.`;
2729
- }
2730
- } else {
2731
- const myTasks = allTasks.filter((t) => t.author === me);
2732
- if (myTasks.length === 0) return "No tasks you authored are available to move.";
2733
- try {
2734
- issueNumber = await select9({
2735
- message: "Select task to move:",
2736
- choices: myTasks.map((t) => ({
2737
- name: `#${t.number} [${getStatus(t)}] ${t.title}`,
2738
- value: t.number
2739
- }))
2740
- });
2741
- } catch {
2742
- return "Cancelled.";
2743
- }
2744
- taskToMove = myTasks.find((t) => t.number === issueNumber);
2745
- }
2746
- const candidates = allTasks.filter((t) => t.number !== taskToMove.number);
2747
- const resolveSpinner = ora10("Finding parent task branches\u2026").start();
2748
- const parents = [];
2749
- for (const t of candidates) {
2750
- const branch = await getTaskBranch(config, t.number);
2751
- if (branch) parents.push({ task: t, branch });
2752
- }
2753
- resolveSpinner.stop();
2754
- if (parents.length === 0) {
2755
- return "No other tasks with known branches are available as a parent.";
2756
- }
2757
- let parentIssueNumber = input["parent_issue_number"];
2758
- let chosen;
2759
- if (parentIssueNumber) {
2760
- const found = parents.find((p) => p.task.number === parentIssueNumber);
2761
- if (!found) {
2762
- return `Task #${parentIssueNumber} is not available as a parent (no branch found or not open).`;
2763
- }
2764
- chosen = found;
2765
- } else {
2766
- try {
2767
- const selectedBranch = await select9({
2768
- message: `Move #${taskToMove.number} under which task?`,
2769
- choices: parents.map((p) => ({
2770
- name: `#${p.task.number} [${getStatus(p.task)}] ${p.task.title} ${chalk11.dim("\u2192 " + p.branch)}`,
2771
- value: p.branch
2772
- }))
2773
- });
2774
- chosen = parents.find((p) => p.branch === selectedBranch);
2775
- } catch {
2776
- return "Cancelled.";
2777
- }
2778
- }
2779
- const sha = await getBranchHeadSha(config, chosen.branch);
2780
- if (!sha) {
2781
- return `Could not resolve HEAD of branch ${chosen.branch} \u2014 does it exist on the remote?`;
2782
- }
2783
- const spinner = ora10(`Moving #${taskToMove.number} under #${chosen.task.number}\u2026`).start();
2784
- try {
2785
- await moveTask(config, taskToMove.number, chosen.branch, sha);
2786
- spinner.succeed(
2787
- `Task #${taskToMove.number} moved under #${chosen.task.number} "${chosen.task.title}"
2788
- target: ${chalk11.cyan(chosen.branch)} base: ${chalk11.dim(sha.slice(0, 7))}`
2789
- );
2790
- return `Task #${taskToMove.number} moved under #${chosen.task.number} (branch: ${chosen.branch}, base: ${sha.slice(0, 7)})`;
2791
- } catch (err) {
2792
- spinner.fail(`Failed: ${err.message}`);
2793
- return `Error: ${err.message}`;
2794
- }
2795
- }
2796
- async function execute12(input, config) {
2797
- const me = await getAuthenticatedUser(config);
2798
- const issueNumber = input["issue_number"];
2799
- const parentIssueNumber = input["parent_issue_number"];
2800
- const task = await getTask(config, issueNumber);
2801
- if (task.author !== me) {
2802
- return `Task #${issueNumber} was not authored by you \u2014 you can only move your own tasks.`;
2803
- }
2804
- const branch = await getTaskBranch(config, parentIssueNumber);
2805
- if (!branch) return `No branch found for parent task #${parentIssueNumber}.`;
2806
- const sha = await getBranchHeadSha(config, branch);
2807
- if (!sha) return `Could not resolve HEAD of branch ${branch}.`;
2808
- await moveTask(config, issueNumber, branch, sha);
2809
- return `Task #${issueNumber} moved under #${parentIssueNumber} (branch: ${branch}, base: ${sha.slice(0, 7)})`;
2810
- }
2811
- var terminal12 = true;
2812
-
2813
- // src/tools/wiki/index.ts
2814
- var wiki_exports = {};
2815
- __export(wiki_exports, {
2816
- definition: () => definition13,
2817
- execute: () => execute13,
2818
- run: () => run13,
2819
- terminal: () => terminal13
2820
- });
2821
- import ora11 from "ora";
2822
- import chalk12 from "chalk";
2823
- import { readFile as readFile2 } from "fs/promises";
2824
- import { select as select10 } from "@inquirer/prompts";
2825
- init_github();
2826
- var WIKI_PATH = "TECHUNTER.md";
2827
- var definition13 = {
2828
- type: "function",
2829
- function: {
2830
- name: "update_wiki",
2831
- 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.",
2832
- parameters: {
2833
- type: "object",
2834
- properties: {},
2835
- required: []
2836
- }
2837
- }
2838
- };
2839
- async function readWikiContent(config) {
2840
- try {
2841
- return await readFile2(WIKI_PATH, "utf-8");
2842
- } catch {
2843
- }
2844
- return getRepoFile(config, WIKI_PATH);
2845
- }
2846
- function printWiki(content) {
2847
- const divider = chalk12.dim("\u2500".repeat(70));
2848
- console.log("\n" + divider);
2849
- console.log(chalk12.bold(" TECHUNTER.md"));
2850
- console.log(divider);
2851
- console.log(renderMarkdown(content));
2852
- console.log(divider + "\n");
2853
- }
2854
- async function run13(_input, config) {
2855
- const fetchSpinner = ora11("Checking for existing wiki\u2026").start();
2856
- const existing = await readWikiContent(config).catch(() => null);
2857
- fetchSpinner.stop();
2858
- let action;
2859
- try {
2860
- action = await select10({
2861
- message: "TECHUNTER.md \u2014 what would you like to do?",
2862
- choices: [
2863
- ...existing ? [{ name: "View current wiki", value: "view" }] : [],
2864
- { name: existing ? "Regenerate & commit to repo" : "Generate & commit to repo", value: "generate" },
2865
- { name: "Cancel", value: "cancel" }
2866
- ]
2867
- });
2868
- } catch {
2869
- return "Cancelled.";
2870
- }
2871
- if (action === "cancel") return "Cancelled.";
2872
- if (action === "view") {
2873
- printWiki(existing);
2874
- return "Displayed TECHUNTER.md.";
2875
- }
2876
- const authSpinner = ora11("Checking permissions\u2026").start();
2877
- let me;
2878
- let allowed;
2879
- try {
2880
- me = await getAuthenticatedUser(config);
2881
- allowed = await isCollaborator(config, me);
2882
- authSpinner.stop();
2883
- } catch (err) {
2884
- authSpinner.stop();
2885
- return `Error checking permissions: ${err.message}`;
2886
- }
2887
- if (!allowed) {
2888
- return `Permission denied: only repository collaborators can update the project wiki.`;
2889
- }
2890
- const genSpinner = ora11("Analyzing project and generating overview\u2026").start();
2891
- let content;
2892
- try {
2893
- content = await generateWiki(config);
2894
- genSpinner.stop();
2895
- } catch (err) {
2896
- genSpinner.stop();
2897
- return `Error generating wiki: ${err.message}`;
2898
- }
2899
- printWiki(content);
2900
- let confirm;
2901
- try {
2902
- confirm = await select10({
2903
- message: `Publish to repository as ${WIKI_PATH}?`,
2904
- choices: [
2905
- { name: "Yes, commit to repo", value: "publish" },
2906
- { name: "Cancel", value: "cancel" }
2907
- ]
2908
- });
2909
- } catch {
2910
- return "Cancelled.";
2911
- }
2912
- if (confirm === "cancel") return "Cancelled.";
2913
- const writeSpinner = ora11(`Writing ${WIKI_PATH}\u2026`).start();
2914
- try {
2915
- const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2916
- writeSpinner.succeed(`Written: ${url}`);
2917
- console.log("");
2918
- return `TECHUNTER.md updated \u2014 ${url}`;
2919
- } catch (err) {
2920
- writeSpinner.fail(`Failed: ${err.message}`);
2921
- return `Error: ${err.message}`;
2922
- }
2923
- }
2924
- async function execute13(_input, config) {
2925
- const me = await getAuthenticatedUser(config);
2926
- if (!await isCollaborator(config, me)) {
2927
- return `Permission denied: only repository collaborators can update the project wiki.`;
2928
- }
2929
- const content = await generateWiki(config);
2930
- try {
2931
- const url = await upsertRepoFile(config, WIKI_PATH, content, "docs: update TECHUNTER.md project overview");
2932
- return `TECHUNTER.md updated \u2014 ${url}`;
2933
- } catch (err) {
2934
- return `Error: ${err.message}`;
2935
- }
2936
- }
2937
- var terminal13 = true;
2938
-
2939
- // src/tools/list-tasks/index.ts
2940
- var list_tasks_exports = {};
2941
- __export(list_tasks_exports, {
2942
- definition: () => definition14,
2943
- execute: () => execute14
2944
- });
2945
- init_github();
2946
- var definition14 = {
2947
- type: "function",
2948
- function: {
2949
- name: "list_tasks",
2950
- description: "List all open tasks (GitHub Issues) with their status and assignee. Use this to answer questions about available work, task progress, or who is working on what.",
2951
- parameters: {
2952
- type: "object",
2953
- properties: {},
2954
- required: []
2955
- }
2956
- }
2957
- };
2958
- async function execute14(_input, config) {
2959
- const tasks = await listTasks(config);
2960
- if (tasks.length === 0) return "No open tasks.";
2961
- return tasks.map((t) => {
2962
- const status = t.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2963
- const assignee = t.assignee ? `@${t.assignee}` : "\u2014";
2964
- return `#${t.number} [${status}] ${assignee} ${t.title}`;
2965
- }).join("\n");
2966
- }
2967
-
2968
- // src/tools/get-task/index.ts
2969
- var get_task_exports = {};
2970
- __export(get_task_exports, {
2971
- definition: () => definition15,
2972
- execute: () => execute15
2973
- });
2974
- init_github();
2975
- var definition15 = {
2976
- type: "function",
2977
- function: {
2978
- name: "get_task",
2979
- description: "Get full details of a specific GitHub issue: title, body, status, assignee.",
2980
- parameters: {
2981
- type: "object",
2982
- properties: {
2983
- issue_number: { type: "number", description: "GitHub issue number" }
2984
- },
2985
- required: ["issue_number"]
2986
- }
2987
- }
2988
- };
2989
- async function execute15(input, config) {
2990
- const issue = await getTask(config, input["issue_number"]);
2991
- const status = issue.labels.find((l) => l.startsWith("techunter:"))?.replace("techunter:", "") ?? "unknown";
2992
- const assignee = issue.assignee ? `@${issue.assignee}` : "\u2014";
2993
- const lines = [
2994
- `#${issue.number} [${status}] ${assignee}`,
2995
- `Title: ${issue.title}`,
2996
- `URL: ${issue.htmlUrl}`
2997
- ];
2998
- if (issue.body) lines.push(`
2999
- ${issue.body}`);
3000
- return lines.join("\n");
3001
- }
3002
-
3003
- // src/tools/get-comments/index.ts
3004
- var get_comments_exports = {};
3005
- __export(get_comments_exports, {
3006
- definition: () => definition16,
3007
- execute: () => execute16
3008
- });
3009
- init_github();
3010
- import ora12 from "ora";
3011
- var definition16 = {
3012
- type: "function",
3013
- function: {
3014
- name: "get_comments",
3015
- description: "Get the latest comments on a GitHub issue. Useful for reading rejection feedback.",
3016
- parameters: {
3017
- type: "object",
3018
- properties: {
3019
- issue_number: { type: "number", description: "GitHub issue number" },
3020
- limit: { type: "number", description: "Max number of latest comments to return (default 5)" }
3021
- },
3022
- required: ["issue_number"]
3023
- }
3024
- }
3025
- };
3026
- async function execute16(input, config) {
3027
- const issueNumber = input["issue_number"];
3028
- const limit = input["limit"] ?? 5;
3029
- const spinner = ora12(`Loading comments for #${issueNumber}...`).start();
3030
- try {
3031
- const comments = await listComments(config, issueNumber, limit);
3032
- spinner.stop();
3033
- if (comments.length === 0) return `No comments on issue #${issueNumber}.`;
3034
- const lines = comments.map((c) => `--- @${c.author} (${c.createdAt.slice(0, 10)}) ---
3035
- ${c.body}`);
3036
- return `Latest ${comments.length} comment(s) on #${issueNumber}:
3037
-
3038
- ${lines.join("\n\n")}`;
3039
- } catch (err) {
3040
- spinner.stop();
3041
- throw err;
3042
- }
3043
- }
3044
-
3045
- // src/tools/get-diff/index.ts
3046
- var get_diff_exports = {};
3047
- __export(get_diff_exports, {
3048
- definition: () => definition17,
3049
- execute: () => execute17
3050
- });
3051
- import ora13 from "ora";
3052
- var definition17 = {
3053
- type: "function",
3054
- function: {
3055
- name: "get_diff",
3056
- description: "Get the current git diff: changed files, diff vs HEAD, and any unpushed commits.",
3057
- parameters: { type: "object", properties: {}, required: [] }
3058
- }
3059
- };
3060
- async function execute17(_input, _config) {
3061
- const spinner = ora13("Reading git diff...").start();
3062
- try {
3063
- const diff = await getDiff();
3064
- spinner.stop();
3065
- return diff;
3066
- } catch (err) {
3067
- spinner.stop();
3068
- throw err;
3069
- }
3070
- }
3071
-
3072
- // src/tools/run-command/index.ts
3073
- var run_command_exports = {};
3074
- __export(run_command_exports, {
3075
- definition: () => definition18,
3076
- execute: () => execute18
3077
- });
3078
- import { exec } from "child_process";
3079
- import { promisify } from "util";
3080
- import ora14 from "ora";
3081
- var execAsync = promisify(exec);
3082
- var definition18 = {
3083
- type: "function",
3084
- function: {
3085
- name: "run_command",
3086
- description: "Run a shell command in the project root directory. Use for building, testing, linting, or git status checks. stdout and stderr are both returned. Commands time out after 60 seconds.",
3087
- parameters: {
3088
- type: "object",
3089
- properties: {
3090
- command: { type: "string", description: "The shell command to run" }
3091
- },
3092
- required: ["command"]
3093
- }
3094
- }
3095
- };
3096
- async function execute18(input, _config) {
3097
- const command = input["command"];
3098
- const cwd = process.cwd();
3099
- const spinner = ora14(`$ ${command}`).start();
3100
- try {
3101
- const { stdout, stderr } = await execAsync(command, { cwd, timeout: 6e4, maxBuffer: 1024 * 1024 });
3102
- spinner.stop();
3103
- const out = [stdout, stderr].filter(Boolean).join("\n").trim();
3104
- const result = out.length > 5e3 ? out.slice(0, 5e3) + "\n... (truncated)" : out;
3105
- return result || "(no output)";
3106
- } catch (err) {
3107
- spinner.stop();
3108
- const e = err;
3109
- const out = [e.stdout, e.stderr].filter(Boolean).join("\n").trim();
3110
- return `Exit ${e.code ?? 1}:
3111
- ${out || e.message}`;
3112
- }
3113
- }
3114
-
3115
- // src/tools/list-files/index.ts
3116
- var list_files_exports = {};
3117
- __export(list_files_exports, {
3118
- definition: () => definition19,
3119
- execute: () => execute19
3120
- });
3121
- import { readFile as readFile3 } from "fs/promises";
3122
- import { existsSync } from "fs";
3123
- import path2 from "path";
3124
- import { globby } from "globby";
3125
- import ignore from "ignore";
3126
- var definition19 = {
3127
- type: "function",
3128
- function: {
3129
- name: "list_files",
3130
- description: "List file paths in the project. Use this first to orient yourself before searching or reading.",
3131
- parameters: {
3132
- type: "object",
3133
- properties: {
3134
- glob: {
3135
- type: "string",
3136
- description: 'Glob pattern to filter results, e.g. "src/**/*.ts". Defaults to all text files.'
3137
- }
3138
- },
3139
- required: []
3140
- }
3141
- }
3142
- };
3143
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3144
- ".png",
3145
- ".jpg",
3146
- ".jpeg",
3147
- ".gif",
3148
- ".svg",
3149
- ".ico",
3150
- ".webp",
3151
- ".pdf",
3152
- ".zip",
3153
- ".tar",
3154
- ".gz",
3155
- ".exe",
3156
- ".dll",
3157
- ".woff",
3158
- ".woff2",
3159
- ".ttf",
3160
- ".mp3",
3161
- ".mp4",
3162
- ".db",
3163
- ".sqlite",
3164
- ".lock"
3165
- ]);
3166
- async function execute19(input, _config) {
3167
- const glob = input["glob"] ?? "**/*";
3168
- const cwd = process.cwd();
3169
- const ig = ignore();
3170
- const gitignorePath = path2.join(cwd, ".gitignore");
3171
- if (existsSync(gitignorePath)) {
3172
- ig.add(await readFile3(gitignorePath, "utf-8"));
3173
- }
3174
- ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
3175
- const files = await globby(glob, { cwd, dot: false, onlyFiles: true, gitignore: false });
3176
- const filtered = files.filter((f) => !ig.ignores(f) && !BINARY_EXTENSIONS.has(path2.extname(f).toLowerCase()));
3177
- if (filtered.length === 0) return `No files matched: ${glob}`;
3178
- return `${filtered.length} file(s):
3179
- ${filtered.join("\n")}`;
3180
- }
3181
-
3182
- // src/tools/grep-code/index.ts
3183
- var grep_code_exports = {};
3184
- __export(grep_code_exports, {
3185
- definition: () => definition20,
3186
- execute: () => execute20
3187
- });
3188
- import { readFile as readFile4 } from "fs/promises";
3189
- import { existsSync as existsSync2 } from "fs";
3190
- import path3 from "path";
3191
- import { globby as globby2 } from "globby";
3192
- import ignore2 from "ignore";
3193
- var definition20 = {
3194
- type: "function",
3195
- function: {
3196
- name: "grep_code",
3197
- description: "Search for a pattern across files, or read a specific line range from a file.\n- Search mode: provide `pattern` \u2014 returns matching lines with context.\n- Read-range mode: provide `file_glob` (single file) + `start_line` + `end_line`, no `pattern` \u2014 read an exact section. Use after grep has given you line numbers.",
3198
- parameters: {
3199
- type: "object",
3200
- properties: {
3201
- pattern: {
3202
- type: "string",
3203
- description: "Regex or plain text to search for (case-insensitive). Omit for read-range mode."
3204
- },
3205
- file_glob: {
3206
- type: "string",
3207
- description: 'Glob to restrict which files to search or read, e.g. "src/**/*.ts" or "src/lib/agent.ts". Defaults to all text files.'
3208
- },
3209
- context_lines: {
3210
- type: "number",
3211
- description: "Lines of context before/after each match (search mode only). Default: 2."
3212
- },
3213
- max_results: {
3214
- type: "number",
3215
- description: "Max matches to return (search mode only). Default: 50."
3216
- },
3217
- start_line: {
3218
- type: "number",
3219
- description: "First line to read, 1-based (read-range mode). Requires file_glob pointing to a single file."
3220
- },
3221
- end_line: {
3222
- type: "number",
3223
- description: "Last line to read, 1-based (read-range mode)."
3224
- }
3225
- },
3226
- required: []
3227
- }
3228
- }
3229
- };
3230
- async function buildIgnore(cwd) {
3231
- const ig = ignore2();
3232
- const gitignorePath = path3.join(cwd, ".gitignore");
3233
- if (existsSync2(gitignorePath)) {
3234
- ig.add(await readFile4(gitignorePath, "utf-8"));
3235
- }
3236
- ig.add(["node_modules", "dist", ".git", ".next", "__pycache__", "build", "coverage"]);
3237
- return ig;
3238
- }
3239
- var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
3240
- ".png",
3241
- ".jpg",
3242
- ".jpeg",
3243
- ".gif",
3244
- ".svg",
3245
- ".ico",
3246
- ".webp",
3247
- ".pdf",
3248
- ".zip",
3249
- ".tar",
3250
- ".gz",
3251
- ".exe",
3252
- ".dll",
3253
- ".woff",
3254
- ".woff2",
3255
- ".ttf",
3256
- ".mp3",
3257
- ".mp4",
3258
- ".db",
3259
- ".sqlite",
3260
- ".lock"
3261
- ]);
3262
- function isText(f) {
3263
- return !BINARY_EXTENSIONS2.has(path3.extname(f).toLowerCase());
3264
- }
3265
- var MAX_RANGE_LINES = 300;
3266
- async function execute20(input, _config) {
3267
- const pattern = input["pattern"] ?? "";
3268
- const fileGlob = input["file_glob"] ?? "**/*";
3269
- const contextLines = Math.min(input["context_lines"] ?? 2, 5);
3270
- const maxResults = Math.min(input["max_results"] ?? 50, 200);
3271
- const startLine = input["start_line"];
3272
- const endLine = input["end_line"];
3273
- const cwd = process.cwd();
3274
- if (!pattern && startLine != null && endLine != null) {
3275
- const files = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
3276
- if (files.length === 0) return `No file matched: ${fileGlob}`;
3277
- if (files.length > 1) return `file_glob matched ${files.length} files \u2014 narrow it to a single file for read-range mode.`;
3278
- const raw = await readFile4(path3.join(cwd, files[0]), "utf-8");
3279
- const lines = raw.split("\n");
3280
- const total = lines.length;
3281
- const from = Math.max(1, startLine);
3282
- const to = Math.min(total, endLine, from + MAX_RANGE_LINES - 1);
3283
- const numbered = lines.slice(from - 1, to).map((l, i) => `${String(from + i).padStart(5)}: ${l}`).join("\n");
3284
- const truncNote = to < Math.min(total, endLine) ? `
3285
- \u2026 (use start_line=${to + 1} to continue)` : "";
3286
- return `${files[0]} \u2014 lines ${from}\u2013${to} of ${total}:
3287
- \`\`\`
3288
- ${numbered}
3289
- \`\`\`${truncNote}`;
3290
- }
3291
- if (!pattern) return "Provide a `pattern` to search, or `start_line` + `end_line` for read-range mode. Use list_files to browse file paths.";
3292
- const ig = await buildIgnore(cwd);
3293
- let regex;
3294
- try {
3295
- regex = new RegExp(pattern, "i");
3296
- } catch {
3297
- regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
3298
- }
3299
- const allFiles = await globby2(fileGlob, { cwd, dot: false, onlyFiles: true, gitignore: false });
3300
- const filtered = allFiles.filter((f) => !ig.ignores(f) && isText(f));
3301
- const matches = [];
3302
- let totalMatches = 0;
3303
- for (const file of filtered) {
3304
- if (totalMatches >= maxResults) break;
3305
- let content;
3306
- try {
3307
- content = await readFile4(path3.join(cwd, file), "utf-8");
3308
- } catch {
3309
- continue;
3310
- }
3311
- const lines = content.split("\n");
3312
- const hitLines = [];
3313
- for (let i = 0; i < lines.length; i++) {
3314
- if (regex.test(lines[i])) hitLines.push(i);
3315
- }
3316
- if (hitLines.length === 0) continue;
3317
- const ranges = [];
3318
- for (const hit of hitLines) {
3319
- const s = Math.max(0, hit - contextLines);
3320
- const e = Math.min(lines.length - 1, hit + contextLines);
3321
- if (ranges.length > 0 && s <= ranges[ranges.length - 1][1] + 1) {
3322
- ranges[ranges.length - 1][1] = e;
3323
- } else {
3324
- ranges.push([s, e]);
3325
- }
3326
- }
3327
- const snippets = [];
3328
- for (const [s, e] of ranges) {
3329
- if (totalMatches >= maxResults) break;
3330
- snippets.push(
3331
- lines.slice(s, e + 1).map((l, i) => {
3332
- const n = s + i + 1;
3333
- return `${regex.test(l) ? ">" : " "} ${String(n).padStart(4)}: ${l}`;
3334
- }).join("\n")
3335
- );
3336
- totalMatches += hitLines.filter((h) => h >= s && h <= e).length;
3337
- }
3338
- if (snippets.length > 0) {
3339
- matches.push(`## ${file}
3340
- \`\`\`
3341
- ${snippets.join("\n---\n")}
3342
- \`\`\``);
3343
- }
3344
- }
3345
- if (matches.length === 0) return `No matches found for: ${pattern}`;
3346
- const header = `Found matches in ${matches.length} file(s) (${totalMatches} match${totalMatches === 1 ? "" : "es"})${totalMatches >= maxResults ? " \u2014 limit reached" : ""}:`;
3347
- return [header, ...matches].join("\n\n");
3348
- }
3349
-
3350
- // src/tools/ask-user/index.ts
3351
- var ask_user_exports = {};
3352
- __export(ask_user_exports, {
3353
- definition: () => definition21,
3354
- execute: () => execute21
3355
- });
3356
- import chalk13 from "chalk";
3357
- import { select as select11, input as promptInput5 } from "@inquirer/prompts";
3358
- var definition21 = {
3359
- type: "function",
3360
- function: {
3361
- name: "ask_user",
3362
- 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.",
3363
- parameters: {
3364
- type: "object",
3365
- properties: {
3366
- question: { type: "string", description: "The question to ask the user" },
3367
- options: {
3368
- type: "array",
3369
- items: { type: "string" },
3370
- description: "2\u20134 concrete answer choices"
3371
- }
3372
- },
3373
- required: ["question", "options"]
3374
- }
3375
- }
3376
- };
3377
- async function execute21(input, _config) {
3378
- const question = input["question"];
3379
- const options = input["options"];
3380
- const OTHER = "__other__";
3381
- console.log("");
3382
- console.log(chalk13.dim(" \u250C\u2500 Agent question " + "\u2500".repeat(51)));
3383
- console.log(chalk13.dim(" \u2502"));
3384
- for (const line of question.split("\n")) {
3385
- console.log(chalk13.dim(" \u2502 ") + line);
3386
- }
3387
- console.log(chalk13.dim(" \u2514" + "\u2500".repeat(67)));
3388
- let answer;
3389
- try {
3390
- const chosen = await select11({
3391
- message: " ",
3392
- choices: [
3393
- ...options.map((o) => ({ name: o, value: o })),
3394
- { name: chalk13.dim("Other (describe below)"), value: OTHER }
3395
- ]
3396
- });
3397
- answer = chosen === OTHER ? await promptInput5({ message: "Your answer:" }) : chosen;
3398
- } catch {
3399
- answer = "User skipped this question \u2014 use your best judgement.";
3400
- }
3401
- console.log("");
3402
- return answer;
3403
- }
3404
-
3405
- // src/tools/registry.ts
3406
- var toolModules = [
3407
- // Command tools
3408
- pick_exports,
3409
- new_task_exports,
3410
- close_exports,
3411
- submit_exports,
3412
- my_status_exports,
3413
- review_exports,
3414
- refresh_exports,
3415
- open_code_exports,
3416
- reject_exports,
3417
- accept_exports,
3418
- edit_task_exports,
3419
- move_task_exports,
3420
- wiki_exports,
3421
- // Low-level tools
3422
- list_tasks_exports,
3423
- get_task_exports,
3424
- get_comments_exports,
3425
- get_diff_exports,
3426
- run_command_exports,
3427
- list_files_exports,
3428
- grep_code_exports,
3429
- ask_user_exports
3430
- ];
3431
-
3432
- // src/mcp.ts
3433
- var tools = toolModules.filter((m) => m.definition.function.name !== "ask_user");
3434
- var server = new Server(
3435
- { name: "techunter", version: "1" },
3436
- { capabilities: { tools: {} } }
3437
- );
3438
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
3439
- tools: tools.map((m) => ({
3440
- name: m.definition.function.name,
3441
- description: m.definition.function.description,
3442
- inputSchema: m.definition.function.parameters
3443
- }))
3444
- }));
3445
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
3446
- const mod = tools.find((m) => m.definition.function.name === request.params.name);
3447
- if (!mod) {
3448
- return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }], isError: true };
3449
- }
3450
- try {
3451
- const config = getConfig();
3452
- const result = await mod.execute(request.params.arguments ?? {}, config);
3453
- return { content: [{ type: "text", text: result }] };
3454
- } catch (err) {
3455
- return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
3456
- }
3457
- });
3458
- var transport = new StdioServerTransport();
3459
- await server.connect(transport);