claude-task-worker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/dist/commands/init.d.ts +1 -0
  4. package/dist/commands/init.js +84 -0
  5. package/dist/commands/init.js.map +1 -0
  6. package/dist/gh.d.ts +47 -0
  7. package/dist/gh.js +113 -0
  8. package/dist/gh.js.map +1 -0
  9. package/dist/git.d.ts +1 -0
  10. package/dist/git.js +14 -0
  11. package/dist/git.js.map +1 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +1890 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/process-manager.d.ts +8 -0
  16. package/dist/process-manager.js +218 -0
  17. package/dist/process-manager.js.map +1 -0
  18. package/dist/random-name.d.ts +1 -0
  19. package/dist/random-name.js +59 -0
  20. package/dist/random-name.js.map +1 -0
  21. package/dist/slack.d.ts +5 -0
  22. package/dist/slack.js +125 -0
  23. package/dist/slack.js.map +1 -0
  24. package/dist/usage.d.ts +2 -0
  25. package/dist/usage.js +52 -0
  26. package/dist/usage.js.map +1 -0
  27. package/dist/workers/create-issue.d.ts +1 -0
  28. package/dist/workers/create-issue.js +55 -0
  29. package/dist/workers/create-issue.js.map +1 -0
  30. package/dist/workers/exec-issue.d.ts +1 -0
  31. package/dist/workers/exec-issue.js +50 -0
  32. package/dist/workers/exec-issue.js.map +1 -0
  33. package/dist/workers/fix-review-point.d.ts +1 -0
  34. package/dist/workers/fix-review-point.js +62 -0
  35. package/dist/workers/fix-review-point.js.map +1 -0
  36. package/dist/workers/triage-issues.d.ts +3 -0
  37. package/dist/workers/triage-issues.js +54 -0
  38. package/dist/workers/triage-issues.js.map +1 -0
  39. package/dist/workers/triage-prs.d.ts +3 -0
  40. package/dist/workers/triage-prs.js +56 -0
  41. package/dist/workers/triage-prs.js.map +1 -0
  42. package/dist/workers/update-issue.d.ts +1 -0
  43. package/dist/workers/update-issue.js +61 -0
  44. package/dist/workers/update-issue.js.map +1 -0
  45. package/dist/worktree.d.ts +2 -0
  46. package/dist/worktree.js +34 -0
  47. package/dist/worktree.js.map +1 -0
  48. package/package.json +54 -0
package/dist/index.js ADDED
@@ -0,0 +1,1890 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/gh.ts
4
+ import { execFile } from "node:child_process";
5
+ function execGh(args) {
6
+ return new Promise((resolve2, reject) => {
7
+ execFile("gh", args, (error, stdout, stderr) => {
8
+ if (error) {
9
+ reject(new Error(`gh ${args.join(" ")} failed: ${stderr || error.message}`));
10
+ return;
11
+ }
12
+ resolve2(stdout.trim());
13
+ });
14
+ });
15
+ }
16
+ function execGhAllowExit(args, allowedCodes) {
17
+ return new Promise((resolve2, reject) => {
18
+ execFile("gh", args, (error, stdout, stderr) => {
19
+ if (error) {
20
+ const code = error.code;
21
+ if (typeof code === "number" && allowedCodes.includes(code)) {
22
+ resolve2(stdout.trim());
23
+ return;
24
+ }
25
+ reject(new Error(`gh ${args.join(" ")} failed: ${stderr || error.message}`));
26
+ return;
27
+ }
28
+ resolve2(stdout.trim());
29
+ });
30
+ });
31
+ }
32
+ async function getCurrentUser() {
33
+ return execGh(["api", "user", "--jq", ".login"]);
34
+ }
35
+ async function getRepoInfo() {
36
+ const output = await execGh(["repo", "view", "--json", "owner,name,defaultBranchRef"]);
37
+ const parsed = JSON.parse(output);
38
+ return { owner: parsed.owner.login, name: parsed.name, defaultBranch: parsed.defaultBranchRef.name };
39
+ }
40
+ async function listIssuesByLabel(assignee, labels, excludeLabels = [], epicFilter) {
41
+ const labelArgs = labels.flatMap((label) => ["--label", label]);
42
+ const searchTerms = ["sort:created-asc", "-is:blocked", ...excludeLabels.map((label) => `-label:"${label}"`)];
43
+ if (epicFilter && epicFilter.numbers.length > 0) {
44
+ for (const number of epicFilter.numbers) {
45
+ searchTerms.push(`parent-issue:${epicFilter.owner}/${epicFilter.repo}#${number}`);
46
+ }
47
+ }
48
+ const search = searchTerms.join(" ");
49
+ const output = await execGh([
50
+ "issue",
51
+ "list",
52
+ "--assignee",
53
+ assignee,
54
+ ...labelArgs,
55
+ "--json",
56
+ "number,title,labels,parent",
57
+ "--search",
58
+ search,
59
+ "--limit",
60
+ "10"
61
+ ]);
62
+ return JSON.parse(output);
63
+ }
64
+ async function listIssuesByNumbers(assignee, labels, excludeLabels, numbers) {
65
+ const results = [];
66
+ for (const number of numbers) {
67
+ try {
68
+ const output = await execGh([
69
+ "issue",
70
+ "view",
71
+ String(number),
72
+ "--json",
73
+ "number,title,labels,parent,assignees,state"
74
+ ]);
75
+ const parsed = JSON.parse(output);
76
+ if (parsed.state !== "OPEN") continue;
77
+ if (!parsed.assignees.some((a) => a.login === assignee)) continue;
78
+ const labelNames = new Set(parsed.labels.map((l) => l.name));
79
+ if (!labels.every((label) => labelNames.has(label))) continue;
80
+ if (excludeLabels.some((label) => labelNames.has(label))) continue;
81
+ results.push({
82
+ number: parsed.number,
83
+ title: parsed.title,
84
+ labels: parsed.labels,
85
+ parent: parsed.parent
86
+ });
87
+ } catch (err) {
88
+ console.error(`[gh] listIssuesByNumbers failed for #${number}: ${err}`);
89
+ }
90
+ }
91
+ return results;
92
+ }
93
+ async function getIssueSubIssuesSummary(issueNumber) {
94
+ const output = await execGh(["issue", "view", String(issueNumber), "--json", "subIssuesSummary"]);
95
+ const parsed = JSON.parse(output);
96
+ const summary = parsed?.subIssuesSummary;
97
+ if (!summary || typeof summary.total !== "number" || typeof summary.completed !== "number" || typeof summary.percentCompleted !== "number") {
98
+ return { total: 0, completed: 0, percentCompleted: 0 };
99
+ }
100
+ return {
101
+ total: summary.total,
102
+ completed: summary.completed,
103
+ percentCompleted: summary.percentCompleted
104
+ };
105
+ }
106
+ var COMPLETED_CHECK_STATES = /* @__PURE__ */ new Set([
107
+ "SUCCESS",
108
+ "FAILURE",
109
+ "ERROR",
110
+ "NEUTRAL",
111
+ "CANCELLED",
112
+ "SKIPPED",
113
+ "TIMED_OUT",
114
+ "ACTION_REQUIRED",
115
+ "STARTUP_FAILURE",
116
+ "STALE"
117
+ ]);
118
+ function isCICompleted(checks) {
119
+ if (checks.length === 0) return true;
120
+ if (checks.some((check) => check.state === "FAILURE" || check.state === "ERROR")) return true;
121
+ return checks.every((check) => COMPLETED_CHECK_STATES.has(check.state));
122
+ }
123
+ async function fetchPRChecks(prNumber) {
124
+ const output = await execGhAllowExit(["pr", "checks", String(prNumber), "--json", "state"], [0, 1, 8]);
125
+ if (!output) return [];
126
+ return JSON.parse(output);
127
+ }
128
+ async function listPullRequestsWithChecks(assignee, label, excludeLabels = []) {
129
+ const search = ["sort:created-asc", ...excludeLabels.map((l) => `-label:"${l}"`)].join(" ");
130
+ const args = [
131
+ "pr",
132
+ "list",
133
+ "--state",
134
+ "open",
135
+ "--json",
136
+ "number,title,labels,headRefName",
137
+ "--search",
138
+ search,
139
+ "--limit",
140
+ "10"
141
+ ];
142
+ if (assignee) {
143
+ args.push("--assignee", assignee);
144
+ }
145
+ if (label) {
146
+ args.push("--label", label);
147
+ }
148
+ const output = await execGh(args);
149
+ const prs = JSON.parse(output);
150
+ const withChecks = await Promise.all(prs.map(async (pr) => ({ ...pr, checks: await fetchPRChecks(pr.number) })));
151
+ return withChecks.filter((pr) => isCICompleted(pr.checks));
152
+ }
153
+ async function withRetry(fn, maxRetries = 5, baseDelayMs = 1e3) {
154
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
155
+ try {
156
+ return await fn();
157
+ } catch (err) {
158
+ if (attempt === maxRetries) throw err;
159
+ const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
160
+ console.error(`[gh] Attempt ${attempt}/${maxRetries} failed, retrying in ${delayMs}ms...`);
161
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
162
+ }
163
+ }
164
+ throw new Error("unreachable");
165
+ }
166
+ async function addLabel(type, number, label) {
167
+ await withRetry(async () => {
168
+ try {
169
+ await execGh([type, "edit", String(number), "--add-label", label]);
170
+ } catch (err) {
171
+ const message = err instanceof Error ? err.message : String(err);
172
+ if (message.includes("already exists") || message.includes("already has")) return;
173
+ throw err;
174
+ }
175
+ });
176
+ }
177
+ async function removeLabel(type, number, label) {
178
+ await withRetry(async () => {
179
+ try {
180
+ await execGh([type, "edit", String(number), "--remove-label", label]);
181
+ } catch (err) {
182
+ const message = err instanceof Error ? err.message : String(err);
183
+ if (message.includes("not found") || message.includes("does not exist") || message.includes("could not remove"))
184
+ return;
185
+ throw err;
186
+ }
187
+ });
188
+ }
189
+ async function commentOnPR(prNumber, body) {
190
+ await execGh(["pr", "comment", String(prNumber), "--body", body]);
191
+ }
192
+ async function createLabel(name, color, force) {
193
+ try {
194
+ const args = ["label", "create", name];
195
+ if (color) args.push("--color", color);
196
+ if (force) args.push("--force");
197
+ await execGh(args);
198
+ return true;
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ // src/config.ts
205
+ import { readFileSync } from "node:fs";
206
+ import { join } from "node:path";
207
+ var DEFAULT_WORKER_CONFIG = {
208
+ skill: "",
209
+ model: "sonnet",
210
+ effort: "high",
211
+ pollingIntervalSeconds: 60,
212
+ cooldownSeconds: 0,
213
+ maxConcurrentTasks: 1
214
+ };
215
+ var WORKER_DEFAULTS = {
216
+ "answer-issue-questions": {
217
+ skill: "/claude-task-worker:answer-issue-questions",
218
+ model: "sonnet",
219
+ effort: "xhigh",
220
+ pollingIntervalSeconds: 60,
221
+ cooldownSeconds: 0,
222
+ maxConcurrentTasks: 1
223
+ },
224
+ "create-issue": {
225
+ skill: "/claude-task-worker:create-issue-from-issue-number",
226
+ model: "sonnet",
227
+ effort: "xhigh",
228
+ pollingIntervalSeconds: 60,
229
+ cooldownSeconds: 0,
230
+ maxConcurrentTasks: 1
231
+ },
232
+ "update-issue": {
233
+ skill: "/claude-task-worker:update-issue",
234
+ model: "sonnet",
235
+ effort: "high",
236
+ pollingIntervalSeconds: 60,
237
+ cooldownSeconds: 0,
238
+ maxConcurrentTasks: 1
239
+ },
240
+ "exec-issue": {
241
+ skill: "/claude-task-worker:exec-issue",
242
+ model: "sonnet",
243
+ effort: "high",
244
+ pollingIntervalSeconds: 60,
245
+ cooldownSeconds: 0,
246
+ maxConcurrentTasks: 1
247
+ },
248
+ "fix-review-point": {
249
+ skill: "/claude-task-worker:fix-review-point",
250
+ model: "sonnet",
251
+ effort: "high",
252
+ pollingIntervalSeconds: 60,
253
+ cooldownSeconds: 0,
254
+ maxConcurrentTasks: 1
255
+ },
256
+ "triage-created-issue": {
257
+ skill: "/claude-task-worker:triage-created-issue",
258
+ model: "sonnet",
259
+ effort: "high",
260
+ pollingIntervalSeconds: 60,
261
+ cooldownSeconds: 0,
262
+ maxConcurrentTasks: 1
263
+ },
264
+ "triage-pr": {
265
+ skill: "/claude-task-worker:triage-pr",
266
+ model: "sonnet",
267
+ effort: "high",
268
+ pollingIntervalSeconds: 60,
269
+ cooldownSeconds: 0,
270
+ maxConcurrentTasks: 1
271
+ },
272
+ "resolve-conflict": {
273
+ skill: "/claude-task-worker:resolve-pr-conflict",
274
+ model: "sonnet",
275
+ effort: "high",
276
+ pollingIntervalSeconds: 60,
277
+ cooldownSeconds: 0,
278
+ maxConcurrentTasks: 1
279
+ },
280
+ "check-dependabot": {
281
+ skill: "/claude-task-worker:check-dependabot",
282
+ model: "sonnet",
283
+ effort: "high",
284
+ pollingIntervalSeconds: 3600,
285
+ cooldownSeconds: 0,
286
+ maxConcurrentTasks: 1
287
+ },
288
+ "epic-issue": {
289
+ skill: "/claude-task-worker:create-epic-pr",
290
+ model: "sonnet",
291
+ effort: "high",
292
+ pollingIntervalSeconds: 300,
293
+ cooldownSeconds: 0,
294
+ maxConcurrentTasks: 1
295
+ }
296
+ };
297
+ var DEFAULT_CONFIG = {
298
+ fixReviewPointCallbackCommentMessage: "",
299
+ workers: {}
300
+ };
301
+ var CONFIG_PATH = join(process.cwd(), "claude-task-worker.json");
302
+ function defaultsFor(name) {
303
+ return WORKER_DEFAULTS[name] ?? DEFAULT_WORKER_CONFIG;
304
+ }
305
+ function parseWorkerEntry(name, val) {
306
+ const base = defaultsFor(name);
307
+ if (typeof val !== "object" || val === null || Array.isArray(val)) {
308
+ console.warn(`[config] invalid workers.${name}: expected object, using defaults`);
309
+ return null;
310
+ }
311
+ const entry = val;
312
+ const result = { ...base };
313
+ if ("skill" in entry) {
314
+ if (typeof entry.skill === "string" && entry.skill.length > 0) {
315
+ result.skill = entry.skill;
316
+ } else {
317
+ console.warn(`[config] invalid workers.${name}.skill: ${String(entry.skill)}, using default ${base.skill}`);
318
+ }
319
+ }
320
+ if ("model" in entry) {
321
+ if (typeof entry.model === "string" && entry.model.length > 0) {
322
+ result.model = entry.model;
323
+ } else {
324
+ console.warn(`[config] invalid workers.${name}.model: ${String(entry.model)}, using default ${base.model}`);
325
+ }
326
+ }
327
+ if ("effort" in entry) {
328
+ if (typeof entry.effort === "string" && entry.effort.length > 0) {
329
+ result.effort = entry.effort;
330
+ } else {
331
+ console.warn(`[config] invalid workers.${name}.effort: ${String(entry.effort)}, using default ${base.effort}`);
332
+ }
333
+ }
334
+ if ("pollingIntervalSeconds" in entry) {
335
+ const val2 = entry.pollingIntervalSeconds;
336
+ if (typeof val2 === "number" && Number.isFinite(val2) && val2 > 0) {
337
+ result.pollingIntervalSeconds = val2;
338
+ } else {
339
+ console.warn(
340
+ `[config] invalid workers.${name}.pollingIntervalSeconds: ${String(val2)}, using default ${base.pollingIntervalSeconds}`
341
+ );
342
+ }
343
+ }
344
+ if ("cooldownSeconds" in entry) {
345
+ const val2 = entry.cooldownSeconds;
346
+ if (typeof val2 === "number" && Number.isFinite(val2) && val2 >= 0) {
347
+ result.cooldownSeconds = val2;
348
+ } else {
349
+ console.warn(
350
+ `[config] invalid workers.${name}.cooldownSeconds: ${String(val2)}, using default ${base.cooldownSeconds}`
351
+ );
352
+ }
353
+ }
354
+ if ("maxConcurrentTasks" in entry) {
355
+ const val2 = entry.maxConcurrentTasks;
356
+ if (typeof val2 === "number" && Number.isInteger(val2) && val2 > 0) {
357
+ result.maxConcurrentTasks = val2;
358
+ } else {
359
+ console.warn(
360
+ `[config] invalid workers.${name}.maxConcurrentTasks: ${String(val2)}, using default ${base.maxConcurrentTasks}`
361
+ );
362
+ }
363
+ }
364
+ return result;
365
+ }
366
+ function loadConfig() {
367
+ const configPath = CONFIG_PATH;
368
+ let raw;
369
+ try {
370
+ raw = JSON.parse(readFileSync(configPath, "utf-8"));
371
+ } catch (err) {
372
+ if (err.code === "ENOENT") {
373
+ return { ...DEFAULT_CONFIG, workers: {} };
374
+ }
375
+ throw err;
376
+ }
377
+ const result = { ...DEFAULT_CONFIG, workers: {} };
378
+ if ("fixReviewPointCallbackCommentMessage" in raw) {
379
+ const val = raw["fixReviewPointCallbackCommentMessage"];
380
+ if (typeof val === "string") {
381
+ result.fixReviewPointCallbackCommentMessage = val;
382
+ }
383
+ }
384
+ if ("workers" in raw) {
385
+ const workers = raw["workers"];
386
+ if (typeof workers !== "object" || workers === null || Array.isArray(workers)) {
387
+ console.warn(`[config] invalid workers: expected object, ignoring`);
388
+ } else {
389
+ for (const [name, val] of Object.entries(workers)) {
390
+ const parsed = parseWorkerEntry(name, val);
391
+ if (parsed) result.workers[name] = parsed;
392
+ }
393
+ }
394
+ }
395
+ return result;
396
+ }
397
+ function getWorkerConfig(workerName) {
398
+ const config = loadConfig();
399
+ return config.workers[workerName] ?? { ...defaultsFor(workerName) };
400
+ }
401
+
402
+ // src/git.ts
403
+ import { execSync, execFile as execFile2 } from "node:child_process";
404
+ import { promisify } from "node:util";
405
+ var execFileAsync = promisify(execFile2);
406
+ var running = false;
407
+ function syncDefaultBranch(branch) {
408
+ if (running) return;
409
+ running = true;
410
+ try {
411
+ execSync(
412
+ `git worktree prune && git checkout ${branch} && git add -A && git reset --hard && git fetch origin ${branch} && git reset --hard origin/${branch}`,
413
+ { stdio: "pipe" }
414
+ );
415
+ } finally {
416
+ running = false;
417
+ }
418
+ }
419
+ async function ensureEpicBranch(epicBranch, defaultBranch) {
420
+ try {
421
+ await fetchRemoteTracking(epicBranch);
422
+ await assertRemoteTrackingExists(epicBranch);
423
+ return;
424
+ } catch (error) {
425
+ if (!isMissingRemoteRefError(error)) {
426
+ throw error;
427
+ }
428
+ }
429
+ await fetchRemoteTracking(defaultBranch);
430
+ await execFileAsync("git", ["push", "origin", `refs/remotes/origin/${defaultBranch}:refs/heads/${epicBranch}`]);
431
+ await fetchRemoteTracking(epicBranch);
432
+ await assertRemoteTrackingExists(epicBranch);
433
+ }
434
+ async function fetchRemoteTracking(branch) {
435
+ await execFileAsync("git", ["fetch", "--no-prune", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]);
436
+ }
437
+ function isMissingRemoteRefError(error) {
438
+ const stderr = String(error?.stderr ?? "");
439
+ return /couldn't find remote ref/i.test(stderr);
440
+ }
441
+ async function assertRemoteTrackingExists(epicBranch) {
442
+ await execFileAsync("git", ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${epicBranch}`]);
443
+ }
444
+
445
+ // src/process-manager.ts
446
+ import { spawn } from "node:child_process";
447
+ var childProcesses = /* @__PURE__ */ new Map();
448
+ var TASK_TIMEOUT_MS = 90 * 60 * 1e3;
449
+ function getDisplayWidth(str) {
450
+ let width = 0;
451
+ for (const char of str) {
452
+ const code = char.codePointAt(0);
453
+ if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 12350 || code >= 12352 && code <= 13247 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 42191 || code >= 44032 && code <= 55215 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072 && code <= 196605 || code >= 196608 && code <= 262141) {
454
+ width += 2;
455
+ } else {
456
+ width += 1;
457
+ }
458
+ }
459
+ return width;
460
+ }
461
+ function truncateToWidth(str, maxWidth) {
462
+ let width = 0;
463
+ let i = 0;
464
+ const chars = [...str];
465
+ while (i < chars.length) {
466
+ const charWidth = getDisplayWidth(chars[i]);
467
+ if (width + charWidth > maxWidth - 3) {
468
+ return chars.slice(0, i).join("") + "...";
469
+ }
470
+ width += charWidth;
471
+ i++;
472
+ }
473
+ return str;
474
+ }
475
+ function padToWidth(str, targetWidth) {
476
+ const currentWidth = getDisplayWidth(str);
477
+ const padding = targetWidth - currentWidth;
478
+ return padding > 0 ? str + " ".repeat(padding) : str;
479
+ }
480
+ var tasks = /* @__PURE__ */ new Map();
481
+ var shuttingDown = false;
482
+ function setShuttingDown() {
483
+ shuttingDown = true;
484
+ }
485
+ function isShuttingDown() {
486
+ return shuttingDown;
487
+ }
488
+ function isRunning(id) {
489
+ const task = tasks.get(id);
490
+ return task?.status === "running";
491
+ }
492
+ function isWorkerAtCapacity(workerName) {
493
+ let count = 0;
494
+ for (const task of tasks.values()) {
495
+ if (task.workerName === workerName && task.status === "running") {
496
+ count++;
497
+ }
498
+ }
499
+ return count >= getWorkerConfig(workerName).maxConcurrentTasks;
500
+ }
501
+ function formatDuration(start, end = /* @__PURE__ */ new Date()) {
502
+ const diffMs = end.getTime() - start.getTime();
503
+ const totalSeconds = Math.floor(diffMs / 1e3);
504
+ const minutes = Math.floor(totalSeconds / 60);
505
+ const seconds = totalSeconds % 60;
506
+ return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
507
+ }
508
+ function formatTime(date) {
509
+ const h = String(date.getHours()).padStart(2, "0");
510
+ const m = String(date.getMinutes()).padStart(2, "0");
511
+ const s = String(date.getSeconds()).padStart(2, "0");
512
+ return `${h}:${m}:${s}`;
513
+ }
514
+ function renderTable() {
515
+ const entries = [...tasks.values()];
516
+ if (entries.length === 0) return;
517
+ const runningTasks = entries.filter((t) => t.status === "running");
518
+ const finishedTasks = entries.filter((t) => t.status !== "running");
519
+ const maxTitleWidth = 40;
520
+ const maxPathWidth = 40;
521
+ const allRows = [
522
+ ...runningTasks.map((t) => ({
523
+ id: `#${t.id}`,
524
+ title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
525
+ worker: t.workerName,
526
+ path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
527
+ status: t.status,
528
+ time: formatTime(t.startedAt),
529
+ duration: formatDuration(t.startedAt)
530
+ })),
531
+ ...finishedTasks.map((t) => ({
532
+ id: `#${t.id}`,
533
+ title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
534
+ worker: t.workerName,
535
+ path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
536
+ status: t.status,
537
+ time: formatTime(t.finishedAt ?? t.startedAt),
538
+ duration: formatDuration(t.startedAt, t.finishedAt)
539
+ }))
540
+ ];
541
+ const hasPath = allRows.some((r) => r.path !== "");
542
+ const colWidths = {
543
+ id: Math.max(3, ...allRows.map((r) => r.id.length)),
544
+ title: Math.max(5, ...allRows.map((r) => getDisplayWidth(r.title))),
545
+ worker: Math.max(6, ...allRows.map((r) => r.worker.length)),
546
+ ...hasPath ? { path: Math.max(8, ...allRows.map((r) => r.path.length)) } : {},
547
+ status: Math.max(6, ...allRows.map((r) => r.status.length)),
548
+ time: Math.max(4, ...allRows.map((r) => r.time.length)),
549
+ duration: Math.max(8, ...allRows.map((r) => r.duration.length))
550
+ };
551
+ const pad = (s, w, useDisplayWidth = false) => useDisplayWidth ? padToWidth(s, w) : s + " ".repeat(w - s.length);
552
+ const cols = hasPath ? [
553
+ colWidths.id,
554
+ colWidths.title,
555
+ colWidths.worker,
556
+ colWidths.path,
557
+ colWidths.status,
558
+ colWidths.time,
559
+ colWidths.duration
560
+ ] : [colWidths.id, colWidths.title, colWidths.worker, colWidths.status, colWidths.time, colWidths.duration];
561
+ const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
562
+ const row = (id, title, worker, path, status, time, duration) => hasPath ? `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(path, colWidths.path)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502` : `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502`;
563
+ const lines = [];
564
+ lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
565
+ lines.push(row("#", "Title", "Worker", "Worktree", "Status", "Time", "Duration"));
566
+ lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
567
+ for (const r of allRows.filter((r2) => r2.status === "running")) {
568
+ lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
569
+ }
570
+ if (runningTasks.length > 0 && finishedTasks.length > 0) {
571
+ lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
572
+ }
573
+ for (const r of allRows.filter((r2) => r2.status !== "running")) {
574
+ lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
575
+ }
576
+ lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
577
+ console.clear();
578
+ console.log(lines.join("\n"));
579
+ }
580
+ var renderInterval;
581
+ function ensureRenderInterval() {
582
+ if (renderInterval) return;
583
+ renderInterval = setInterval(renderTable, 1e3);
584
+ renderInterval.unref();
585
+ }
586
+ function run(command, args, id, title, workerName, path, onComplete, cwd) {
587
+ tasks.set(id, {
588
+ id,
589
+ title,
590
+ status: "running",
591
+ workerName,
592
+ path,
593
+ startedAt: /* @__PURE__ */ new Date()
594
+ });
595
+ ensureRenderInterval();
596
+ renderTable();
597
+ const child = spawn(command, args, {
598
+ stdio: ["ignore", "pipe", "pipe"],
599
+ detached: true,
600
+ ...cwd ? { cwd } : {}
601
+ });
602
+ childProcesses.set(id, child);
603
+ child.stderr?.resume();
604
+ const outputChunks = [];
605
+ child.stdout?.on("data", (chunk) => {
606
+ outputChunks.push(chunk);
607
+ });
608
+ let timedOut = false;
609
+ const timeoutHandle = setTimeout(() => {
610
+ timedOut = true;
611
+ console.error(`[worker] task #${id} timed out after ${TASK_TIMEOUT_MS / 1e3}s, terminating`);
612
+ if (child.pid) {
613
+ try {
614
+ process.kill(-child.pid, "SIGTERM");
615
+ } catch {
616
+ try {
617
+ child.kill("SIGTERM");
618
+ } catch {
619
+ }
620
+ }
621
+ }
622
+ }, TASK_TIMEOUT_MS);
623
+ timeoutHandle.unref();
624
+ child.on("close", async (code) => {
625
+ clearTimeout(timeoutHandle);
626
+ const output = Buffer.concat(outputChunks).toString("utf-8") + (timedOut ? `
627
+ [worker] task timed out after ${TASK_TIMEOUT_MS / 1e3}s` : "");
628
+ const finalStatus = !timedOut && code === 0 ? "completed" : "failed";
629
+ try {
630
+ await Promise.race([
631
+ onComplete?.(finalStatus, output) ?? Promise.resolve(),
632
+ new Promise(
633
+ (_, reject) => setTimeout(() => reject(new Error("onComplete timed out after 120s")), 12e4).unref()
634
+ )
635
+ ]);
636
+ } catch (err) {
637
+ console.error(`[worker] onComplete error for #${id}: ${err}`);
638
+ }
639
+ const task = tasks.get(id);
640
+ if (task) {
641
+ task.status = finalStatus;
642
+ task.finishedAt = /* @__PURE__ */ new Date();
643
+ }
644
+ childProcesses.delete(id);
645
+ renderTable();
646
+ });
647
+ child.on("error", async (err) => {
648
+ clearTimeout(timeoutHandle);
649
+ console.error(`[worker] failed to spawn process for #${id}: ${err.message}`);
650
+ try {
651
+ await Promise.race([
652
+ onComplete?.("failed", err.message) ?? Promise.resolve(),
653
+ new Promise(
654
+ (_, reject) => setTimeout(() => reject(new Error("onComplete timed out after 120s")), 12e4).unref()
655
+ )
656
+ ]);
657
+ } catch (callbackErr) {
658
+ console.error(`[worker] onComplete error for #${id}: ${callbackErr}`);
659
+ }
660
+ const task = tasks.get(id);
661
+ if (task) {
662
+ task.status = "failed";
663
+ task.finishedAt = /* @__PURE__ */ new Date();
664
+ }
665
+ childProcesses.delete(id);
666
+ renderTable();
667
+ });
668
+ }
669
+ function waitForAllProcesses() {
670
+ return new Promise((resolve2) => {
671
+ const check = () => {
672
+ if (childProcesses.size === 0) {
673
+ resolve2();
674
+ } else {
675
+ setTimeout(check, 500);
676
+ }
677
+ };
678
+ check();
679
+ });
680
+ }
681
+ function shutdown(signal = "SIGTERM") {
682
+ for (const [, child] of childProcesses) {
683
+ if (!child.pid) continue;
684
+ try {
685
+ process.kill(-child.pid, signal);
686
+ } catch {
687
+ try {
688
+ child.kill(signal);
689
+ } catch {
690
+ }
691
+ }
692
+ }
693
+ }
694
+
695
+ // src/random-name.ts
696
+ import { randomInt } from "node:crypto";
697
+ var ADJECTIVES = [
698
+ "brave",
699
+ "calm",
700
+ "dark",
701
+ "eager",
702
+ "fair",
703
+ "gentle",
704
+ "happy",
705
+ "keen",
706
+ "lively",
707
+ "noble",
708
+ "proud",
709
+ "quiet",
710
+ "rapid",
711
+ "sharp",
712
+ "swift",
713
+ "tender",
714
+ "vivid",
715
+ "warm",
716
+ "bold",
717
+ "bright",
718
+ "clever",
719
+ "daring",
720
+ "fierce",
721
+ "grand",
722
+ "humble",
723
+ "jovial",
724
+ "kind",
725
+ "lucky",
726
+ "mighty",
727
+ "neat",
728
+ "agile",
729
+ "amber",
730
+ "breezy",
731
+ "crisp",
732
+ "deep",
733
+ "dusty",
734
+ "elfin",
735
+ "frosty",
736
+ "golden",
737
+ "hardy",
738
+ "icy",
739
+ "joyful",
740
+ "lean",
741
+ "misty",
742
+ "nimble",
743
+ "open",
744
+ "peppy",
745
+ "radiant",
746
+ "serene",
747
+ "tidy",
748
+ "upbeat",
749
+ "valiant",
750
+ "wiry",
751
+ "zesty",
752
+ "ancient",
753
+ "brisk",
754
+ "crispy",
755
+ "deft",
756
+ "earnest",
757
+ "fleet",
758
+ "airy",
759
+ "ardent",
760
+ "astute",
761
+ "azure",
762
+ "balmy",
763
+ "blithe",
764
+ "buoyant",
765
+ "candid",
766
+ "cheery",
767
+ "coastal",
768
+ "dashing",
769
+ "dazzling",
770
+ "devoted",
771
+ "dynamic",
772
+ "edgy",
773
+ "elegant",
774
+ "enduring",
775
+ "exotic",
776
+ "fabled",
777
+ "faithful",
778
+ "famous",
779
+ "fearless",
780
+ "flawless",
781
+ "fluent",
782
+ "focused",
783
+ "forceful",
784
+ "forged",
785
+ "frank",
786
+ "fresh",
787
+ "frugal",
788
+ "gallant",
789
+ "gifted",
790
+ "glowing",
791
+ "graceful",
792
+ "grounded",
793
+ "honest",
794
+ "hopeful",
795
+ "ideal",
796
+ "immense",
797
+ "intact",
798
+ "intrepid",
799
+ "ironclad",
800
+ "ivory",
801
+ "keen",
802
+ "knowing",
803
+ "lavish",
804
+ "level",
805
+ "lithe",
806
+ "lofty",
807
+ "loyal",
808
+ "luminous",
809
+ "majestic",
810
+ "mellow",
811
+ "mindful",
812
+ "modern"
813
+ ];
814
+ var NOUNS = [
815
+ "falcon",
816
+ "river",
817
+ "cedar",
818
+ "flame",
819
+ "stone",
820
+ "breeze",
821
+ "coral",
822
+ "delta",
823
+ "frost",
824
+ "grove",
825
+ "harbor",
826
+ "iris",
827
+ "jade",
828
+ "lark",
829
+ "maple",
830
+ "orbit",
831
+ "pearl",
832
+ "ridge",
833
+ "spark",
834
+ "tide",
835
+ "vale",
836
+ "wolf",
837
+ "apex",
838
+ "bloom",
839
+ "crest",
840
+ "dawn",
841
+ "ember",
842
+ "flint",
843
+ "hawk",
844
+ "sage",
845
+ "arrow",
846
+ "basin",
847
+ "cliff",
848
+ "dune",
849
+ "echo",
850
+ "fern",
851
+ "gale",
852
+ "helm",
853
+ "isle",
854
+ "jungle",
855
+ "kite",
856
+ "lance",
857
+ "marsh",
858
+ "nova",
859
+ "oak",
860
+ "pine",
861
+ "quill",
862
+ "reef",
863
+ "shore",
864
+ "thorn",
865
+ "umber",
866
+ "veil",
867
+ "wave",
868
+ "xenon",
869
+ "yew",
870
+ "zenith",
871
+ "acorn",
872
+ "birch",
873
+ "comet",
874
+ "dew",
875
+ "abyss",
876
+ "anchor",
877
+ "anvil",
878
+ "arc",
879
+ "ash",
880
+ "atlas",
881
+ "aurora",
882
+ "axe",
883
+ "bay",
884
+ "beacon",
885
+ "blade",
886
+ "bolt",
887
+ "brook",
888
+ "bud",
889
+ "burl",
890
+ "cairn",
891
+ "canopy",
892
+ "cape",
893
+ "cavern",
894
+ "chain",
895
+ "chalk",
896
+ "chasm",
897
+ "chord",
898
+ "chrome",
899
+ "citrus",
900
+ "claw",
901
+ "cloud",
902
+ "cobalt",
903
+ "creek",
904
+ "crown",
905
+ "crystal",
906
+ "current",
907
+ "cypress",
908
+ "depot",
909
+ "depth",
910
+ "drift",
911
+ "dusk",
912
+ "dust",
913
+ "eddy",
914
+ "elm",
915
+ "epoch",
916
+ "ether",
917
+ "field",
918
+ "fin",
919
+ "fjord",
920
+ "foam",
921
+ "forge",
922
+ "fossil",
923
+ "geyser",
924
+ "glacier",
925
+ "glow",
926
+ "granite",
927
+ "heath",
928
+ "heron",
929
+ "hollow"
930
+ ];
931
+ function pick(list) {
932
+ return list[randomInt(list.length)];
933
+ }
934
+ function generateWorktreeName() {
935
+ const suffix = String(randomInt(1e4)).padStart(4, "0");
936
+ return `${pick(ADJECTIVES)}-${pick(NOUNS)}-${suffix}`;
937
+ }
938
+
939
+ // src/slack.ts
940
+ import { exec } from "node:child_process";
941
+ import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
942
+ import { homedir } from "node:os";
943
+ import { join as join2 } from "node:path";
944
+ import { promisify as promisify2 } from "node:util";
945
+ var execAsync = promisify2(exec);
946
+ var WEBHOOK_URL = process.env.CLAUDE_TASK_WORKER_SLACK_WEBHOOK_URL;
947
+ var USAGE_CACHE_PATH = "/tmp/claude-usage-cache.json";
948
+ var USAGE_CACHE_TTL_SECONDS = 360;
949
+ async function send(payload) {
950
+ if (!WEBHOOK_URL) return;
951
+ try {
952
+ await fetch(WEBHOOK_URL, {
953
+ method: "POST",
954
+ headers: { "Content-Type": "application/json" },
955
+ body: JSON.stringify(payload)
956
+ });
957
+ } catch (err) {
958
+ console.error(`[slack] Failed to send notification: ${err}`);
959
+ }
960
+ }
961
+ function extractToken(credentials) {
962
+ if (typeof credentials === "string") return credentials;
963
+ return credentials.claudeAiOauth?.accessToken ?? credentials.oauth_token ?? credentials.access_token ?? "";
964
+ }
965
+ async function getOAuthToken() {
966
+ try {
967
+ const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
968
+ return extractToken(JSON.parse(stdout.trim()));
969
+ } catch {
970
+ const raw = readFileSync2(join2(homedir(), ".claude", ".credentials.json"), "utf-8");
971
+ return extractToken(JSON.parse(raw));
972
+ }
973
+ }
974
+ function readUsageCache() {
975
+ try {
976
+ const raw = readFileSync2(USAGE_CACHE_PATH, "utf-8");
977
+ const cached = JSON.parse(raw);
978
+ if (Date.now() - cached.timestamp < USAGE_CACHE_TTL_SECONDS * 1e3) {
979
+ return cached.data;
980
+ }
981
+ } catch {
982
+ }
983
+ return null;
984
+ }
985
+ function writeUsageCache(data) {
986
+ try {
987
+ writeFileSync(USAGE_CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }));
988
+ } catch {
989
+ }
990
+ }
991
+ async function fetchUsageInfo() {
992
+ const cached = readUsageCache();
993
+ if (cached) return cached;
994
+ try {
995
+ const token = await getOAuthToken();
996
+ const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
997
+ headers: {
998
+ Authorization: `Bearer ${token}`,
999
+ "anthropic-beta": "oauth-2025-04-20"
1000
+ }
1001
+ });
1002
+ if (!res.ok) {
1003
+ console.error(`[slack] Usage API returned ${res.status}`);
1004
+ return null;
1005
+ }
1006
+ const body = await res.json();
1007
+ const data = {
1008
+ fiveHourUtilization: body.five_hour.utilization,
1009
+ fiveHourResetsAt: body.five_hour.resets_at,
1010
+ sevenDayUtilization: body.seven_day.utilization,
1011
+ sevenDayResetsAt: body.seven_day.resets_at
1012
+ };
1013
+ writeUsageCache(data);
1014
+ return data;
1015
+ } catch (err) {
1016
+ console.error(`[slack] Failed to fetch usage info: ${err}`);
1017
+ return null;
1018
+ }
1019
+ }
1020
+ function utilizationEmoji(value) {
1021
+ if (value < 50) return "\u{1F7E2}";
1022
+ if (value < 80) return "\u{1F7E1}";
1023
+ return "\u{1F534}";
1024
+ }
1025
+ function formatResetTimeJST(isoString) {
1026
+ const date = new Date(isoString);
1027
+ return date.toLocaleString("ja-JP", {
1028
+ timeZone: "Asia/Tokyo",
1029
+ month: "numeric",
1030
+ day: "numeric",
1031
+ hour: "numeric",
1032
+ minute: "2-digit"
1033
+ });
1034
+ }
1035
+ async function buildTokenLimitText() {
1036
+ const usage = await fetchUsageInfo();
1037
+ if (!usage) return "";
1038
+ const fiveH = usage.fiveHourUtilization.toFixed(1);
1039
+ const sevenD = usage.sevenDayUtilization.toFixed(1);
1040
+ const emoji = utilizationEmoji(Math.max(usage.fiveHourUtilization, usage.sevenDayUtilization));
1041
+ const fiveHReset = formatResetTimeJST(usage.fiveHourResetsAt);
1042
+ const sevenDReset = formatResetTimeJST(usage.sevenDayResetsAt);
1043
+ return ` | ${emoji} 5h: ${fiveH}% (reset: ${fiveHReset}) / 7d: ${sevenD}% (reset: ${sevenDReset})`;
1044
+ }
1045
+ async function notifyTaskCompleted(workerName, repoName, id, title, url, output) {
1046
+ const tokenText = await buildTokenLimitText();
1047
+ const truncatedOutput = output && output.length > 1e3 ? `\u2026${output.slice(-1e3)}` : output;
1048
+ const outputBlock = truncatedOutput ? `
1049
+ \`\`\`${truncatedOutput}\`\`\`` : "";
1050
+ await send({
1051
+ text: `\u2705 [${workerName}] ${repoName} | Task completed: <${url}|#${id} ${title}>${tokenText}${outputBlock}`
1052
+ });
1053
+ }
1054
+ async function notifyTaskFailed(workerName, repoName, id, title, url, output) {
1055
+ const tokenText = await buildTokenLimitText();
1056
+ const truncatedOutput = output && output.length > 1e3 ? `\u2026${output.slice(-1e3)}` : output;
1057
+ const outputBlock = truncatedOutput ? `
1058
+ \`\`\`${truncatedOutput}\`\`\`` : "";
1059
+ await send({
1060
+ text: `\u274C [${workerName}] ${repoName} | Task failed: <${url}|#${id} ${title}>${tokenText}${outputBlock}`
1061
+ });
1062
+ }
1063
+ async function notifyError(workerName, repoName, error) {
1064
+ const message = error instanceof Error ? error.message : String(error);
1065
+ await send({
1066
+ text: `\u{1F6A8} [${workerName}] ${repoName} | Error: ${message}`
1067
+ });
1068
+ }
1069
+
1070
+ // src/worktree.ts
1071
+ import { execFile as execFile3 } from "node:child_process";
1072
+ import { promisify as promisify3 } from "node:util";
1073
+ import { readdir, rm, stat } from "node:fs/promises";
1074
+ import { resolve, sep } from "node:path";
1075
+ var execFileAsync2 = promisify3(execFile3);
1076
+ var WORKTREES_DIR = ".claude/worktrees";
1077
+ function isManagedWorktreePath(path) {
1078
+ return resolve(path).startsWith(resolve(WORKTREES_DIR) + sep);
1079
+ }
1080
+ async function forceRemoveIfExists(path) {
1081
+ if (!isManagedWorktreePath(path)) {
1082
+ console.error(`[worktree] Refusing to remove path outside ${WORKTREES_DIR}: ${path}`);
1083
+ return;
1084
+ }
1085
+ try {
1086
+ await stat(path);
1087
+ } catch {
1088
+ return;
1089
+ }
1090
+ await rm(path, { recursive: true, force: true });
1091
+ console.log(`[worktree] Force removed remaining directory: ${path}`);
1092
+ }
1093
+ function getWorktreePath(worktreeId) {
1094
+ return `${WORKTREES_DIR}/${worktreeId}`;
1095
+ }
1096
+ async function createWorktreeFromBranch(worktreeId, baseBranch) {
1097
+ const worktreePath = `${WORKTREES_DIR}/${worktreeId}`;
1098
+ await execFileAsync2("git", ["worktree", "add", "-B", worktreeId, worktreePath, `origin/${baseBranch}`]);
1099
+ }
1100
+ async function removeWorktree(worktreeId) {
1101
+ const worktreePath = `${WORKTREES_DIR}/${worktreeId}`;
1102
+ try {
1103
+ await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath]);
1104
+ } catch (error) {
1105
+ console.error(`[worktree] Failed to remove worktree ${worktreeId}:`, error);
1106
+ }
1107
+ await forceRemoveIfExists(worktreePath);
1108
+ }
1109
+ async function removeWorktreeByBranch(branchName) {
1110
+ try {
1111
+ const { stdout } = await execFileAsync2("git", ["worktree", "list", "--porcelain"]);
1112
+ const entries = stdout.trim().split("\n\n").filter(Boolean);
1113
+ for (const entry of entries) {
1114
+ const branchLine = entry.split("\n").find((l) => l.startsWith("branch "));
1115
+ if (!branchLine) continue;
1116
+ const branch = branchLine.replace("branch refs/heads/", "");
1117
+ if (branch !== branchName) continue;
1118
+ const worktreeLine = entry.split("\n").find((l) => l.startsWith("worktree "));
1119
+ if (!worktreeLine) continue;
1120
+ const worktreePath = worktreeLine.replace("worktree ", "");
1121
+ if (!isManagedWorktreePath(worktreePath)) {
1122
+ console.log(`[worktree] Skipping unmanaged worktree for branch ${branchName}: ${worktreePath}`);
1123
+ continue;
1124
+ }
1125
+ try {
1126
+ await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath]);
1127
+ console.log(`[worktree] Removed worktree for branch ${branchName}: ${worktreePath}`);
1128
+ } catch (error) {
1129
+ console.error(`[worktree] Failed to remove worktree for branch ${branchName}:`, error);
1130
+ }
1131
+ await forceRemoveIfExists(worktreePath);
1132
+ }
1133
+ } catch (error) {
1134
+ console.error(`[worktree] Failed to remove worktree for branch ${branchName}:`, error);
1135
+ }
1136
+ }
1137
+
1138
+ // src/workers/issue-worker.ts
1139
+ var LABEL_TRIAGE_SCOPE = "cc-triage-scope";
1140
+ function createIssuePollingWorker(config) {
1141
+ return async () => {
1142
+ const { owner, name, defaultBranch } = await getRepoInfo();
1143
+ const user = await getCurrentUser();
1144
+ const { pollingIntervalSeconds, cooldownSeconds } = getWorkerConfig(config.name);
1145
+ const pollingIntervalMs = pollingIntervalSeconds * 1e3;
1146
+ const cooldownMs = cooldownSeconds * 1e3;
1147
+ console.log(
1148
+ `[${config.name}] Polling issues every ${pollingIntervalSeconds} seconds for ${owner}/${name} (assignee: ${user})`
1149
+ );
1150
+ let lastCompletionAt = 0;
1151
+ const tick = async () => {
1152
+ if (isShuttingDown()) return;
1153
+ if (cooldownMs > 0 && lastCompletionAt > 0 && Date.now() - lastCompletionAt < cooldownMs) return;
1154
+ try {
1155
+ const excludeLabels = ["cc-in-progress", "cc-need-human-check", ...config.excludeLabels ?? []];
1156
+ const epicFilter = config.epicFilters && config.epicFilters.length > 0 ? { owner, repo: name, numbers: config.epicFilters } : void 0;
1157
+ const labels = config.labelFilters && config.labelFilters.length > 0 ? [...config.triggerLabels, ...config.labelFilters] : config.triggerLabels;
1158
+ const candidates = config.ownNumberFilters && config.ownNumberFilters.length > 0 ? await listIssuesByNumbers(user, labels, excludeLabels, config.ownNumberFilters) : await listIssuesByLabel(user, labels, excludeLabels, epicFilter);
1159
+ for (const issue of candidates) {
1160
+ if (isRunning(issue.number)) continue;
1161
+ if (isWorkerAtCapacity(config.name)) break;
1162
+ if (config.preflight) {
1163
+ const action = await config.preflight(issue);
1164
+ if (action === "skip") continue;
1165
+ if (action === "mark-pr-created") {
1166
+ await addLabel("issue", issue.number, "cc-pr-created").catch(
1167
+ (err) => console.error(`[${config.name}] addLabel cc-pr-created failed for #${issue.number}: ${err}`)
1168
+ );
1169
+ continue;
1170
+ }
1171
+ }
1172
+ const hadTriageScope = issue.labels.some((l) => l.name === LABEL_TRIAGE_SCOPE);
1173
+ await addLabel("issue", issue.number, "cc-in-progress");
1174
+ const worktreeId = generateWorktreeName();
1175
+ try {
1176
+ const issueUrl = `https://github.com/${owner}/${name}/issues/${issue.number}`;
1177
+ syncDefaultBranch(defaultBranch);
1178
+ const { model, effort, skill } = getWorkerConfig(config.name);
1179
+ const command = skill || config.command;
1180
+ const parentNumber = issue.parent?.number;
1181
+ let cwd;
1182
+ const claudeArgs = [
1183
+ "-p",
1184
+ `${command} ${issue.number}`,
1185
+ "--dangerously-skip-permissions",
1186
+ "--model",
1187
+ model,
1188
+ "--effort",
1189
+ effort
1190
+ ];
1191
+ if (parentNumber !== void 0) {
1192
+ const epicBranch = `cc-epic-${parentNumber}`;
1193
+ await ensureEpicBranch(epicBranch, defaultBranch);
1194
+ await createWorktreeFromBranch(worktreeId, epicBranch);
1195
+ cwd = getWorktreePath(worktreeId);
1196
+ console.log(
1197
+ `[${config.name}] #${issue.number}: created worktree ${worktreeId} from ${epicBranch} (parent #${parentNumber})`
1198
+ );
1199
+ } else {
1200
+ claudeArgs.push("--worktree", worktreeId);
1201
+ }
1202
+ run(
1203
+ "claude",
1204
+ claudeArgs,
1205
+ issue.number,
1206
+ issue.title,
1207
+ config.name,
1208
+ worktreeId,
1209
+ async (status, output) => {
1210
+ lastCompletionAt = Date.now();
1211
+ for (const label of config.triggerLabels) {
1212
+ await removeLabel("issue", issue.number, label).catch(
1213
+ (err) => console.error(`[${config.name}] removeLabel ${label} failed for #${issue.number}: ${err}`)
1214
+ );
1215
+ }
1216
+ if (hadTriageScope) {
1217
+ await addLabel("issue", issue.number, LABEL_TRIAGE_SCOPE).catch(
1218
+ (err) => console.error(
1219
+ `[${config.name}] addLabel ${LABEL_TRIAGE_SCOPE} failed for #${issue.number}: ${err}`
1220
+ )
1221
+ );
1222
+ }
1223
+ try {
1224
+ if (status === "completed") {
1225
+ await config.onCompleted?.(issue.number);
1226
+ await notifyTaskCompleted(config.name, name, issue.number, issue.title, issueUrl, output);
1227
+ } else {
1228
+ await notifyTaskFailed(config.name, name, issue.number, issue.title, issueUrl, output);
1229
+ }
1230
+ } catch (err) {
1231
+ console.error(`[${config.name}] post-task error for #${issue.number}: ${err}`);
1232
+ } finally {
1233
+ await removeLabel("issue", issue.number, "cc-in-progress").catch(
1234
+ (err) => console.error(`[${config.name}] removeLabel cc-in-progress failed for #${issue.number}: ${err}`)
1235
+ );
1236
+ await removeWorktree(worktreeId).catch(
1237
+ (err) => console.error(`[${config.name}] removeWorktree failed for #${issue.number}: ${err}`)
1238
+ );
1239
+ }
1240
+ },
1241
+ cwd
1242
+ );
1243
+ } catch (err) {
1244
+ console.error(`[${config.name}] setup error for #${issue.number}: ${err}`);
1245
+ await removeLabel("issue", issue.number, "cc-in-progress").catch(() => {
1246
+ });
1247
+ await removeWorktree(worktreeId).catch(() => {
1248
+ });
1249
+ await notifyError(config.name, name, err);
1250
+ }
1251
+ }
1252
+ } catch (err) {
1253
+ console.error(`[${config.name}] tick error: ${err}`);
1254
+ await notifyError(config.name, name, err);
1255
+ }
1256
+ };
1257
+ await tick();
1258
+ setInterval(tick, pollingIntervalMs);
1259
+ };
1260
+ }
1261
+
1262
+ // src/workers/exec-issue.ts
1263
+ var execIssueWorker = (opts = {}) => createIssuePollingWorker({
1264
+ name: "exec-issue",
1265
+ command: "/claude-task-worker:exec-issue",
1266
+ triggerLabels: ["cc-exec-issue"],
1267
+ epicFilters: opts.epicFilters,
1268
+ labelFilters: opts.labelFilters,
1269
+ onCompleted: async (issueNumber) => {
1270
+ await addLabel("issue", issueNumber, "cc-pr-created");
1271
+ }
1272
+ })();
1273
+
1274
+ // src/workers/pr-worker.ts
1275
+ var LABEL_IN_PROGRESS = "cc-in-progress";
1276
+ var LABEL_TRIAGE_SCOPE2 = "cc-triage-scope";
1277
+ function createPrPollingWorker(config) {
1278
+ return async () => {
1279
+ const { owner, name, defaultBranch } = await getRepoInfo();
1280
+ const user = await getCurrentUser();
1281
+ const { pollingIntervalSeconds, cooldownSeconds } = getWorkerConfig(config.name);
1282
+ const pollingIntervalMs = pollingIntervalSeconds * 1e3;
1283
+ const cooldownMs = cooldownSeconds * 1e3;
1284
+ console.log(
1285
+ `[${config.name}] Polling PRs every ${pollingIntervalSeconds} seconds for ${owner}/${name} (assignee: ${user})`
1286
+ );
1287
+ let lastCompletionAt = 0;
1288
+ const tick = async () => {
1289
+ if (isShuttingDown()) return;
1290
+ if (cooldownMs > 0 && lastCompletionAt > 0 && Date.now() - lastCompletionAt < cooldownMs) return;
1291
+ try {
1292
+ const excludeLabels = [LABEL_IN_PROGRESS, ...config.excludeLabels ?? []];
1293
+ const candidates = await listPullRequestsWithChecks(user, config.triggerLabel, excludeLabels);
1294
+ for (const pr of candidates) {
1295
+ if (isRunning(pr.number)) continue;
1296
+ if (isWorkerAtCapacity(config.name)) break;
1297
+ const prUrl = `https://github.com/${owner}/${name}/pull/${pr.number}`;
1298
+ const hadTriageScope = pr.labels.some((l) => l.name === LABEL_TRIAGE_SCOPE2);
1299
+ await addLabel("pr", pr.number, LABEL_IN_PROGRESS);
1300
+ try {
1301
+ await removeWorktreeByBranch(pr.headRefName);
1302
+ const worktreeId = generateWorktreeName();
1303
+ syncDefaultBranch(defaultBranch);
1304
+ const { model, effort, skill } = getWorkerConfig(config.name);
1305
+ const command = skill || config.command;
1306
+ run(
1307
+ "claude",
1308
+ [
1309
+ "-p",
1310
+ `${command} ${pr.number}`,
1311
+ "--dangerously-skip-permissions",
1312
+ "--model",
1313
+ model,
1314
+ "--effort",
1315
+ effort,
1316
+ "--worktree",
1317
+ worktreeId
1318
+ ],
1319
+ pr.number,
1320
+ `PR #${pr.number} (${pr.headRefName})`,
1321
+ config.name,
1322
+ worktreeId,
1323
+ async (status, output) => {
1324
+ lastCompletionAt = Date.now();
1325
+ try {
1326
+ if (status === "completed") {
1327
+ await config.onCompleted?.(pr);
1328
+ await notifyTaskCompleted(config.name, name, pr.number, pr.title, prUrl, output);
1329
+ } else {
1330
+ await notifyTaskFailed(config.name, name, pr.number, pr.title, prUrl, output);
1331
+ }
1332
+ } catch (err) {
1333
+ console.error(`[${config.name}] post-task error for PR #${pr.number}: ${err}`);
1334
+ } finally {
1335
+ await removeLabel("pr", pr.number, config.triggerLabel).catch(
1336
+ (err) => console.error(
1337
+ `[${config.name}] removeLabel ${config.triggerLabel} failed for PR #${pr.number}: ${err}`
1338
+ )
1339
+ );
1340
+ if (hadTriageScope) {
1341
+ await addLabel("pr", pr.number, LABEL_TRIAGE_SCOPE2).catch(
1342
+ (err) => console.error(
1343
+ `[${config.name}] addLabel ${LABEL_TRIAGE_SCOPE2} failed for PR #${pr.number}: ${err}`
1344
+ )
1345
+ );
1346
+ }
1347
+ if (config.onFinally) {
1348
+ await config.onFinally(pr).catch((err) => console.error(`[${config.name}] onFinally failed for PR #${pr.number}: ${err}`));
1349
+ }
1350
+ await removeLabel("pr", pr.number, LABEL_IN_PROGRESS).catch(
1351
+ (err) => console.error(
1352
+ `[${config.name}] removeLabel ${LABEL_IN_PROGRESS} failed for PR #${pr.number}: ${err}`
1353
+ )
1354
+ );
1355
+ await removeWorktree(worktreeId).catch(
1356
+ (err) => console.error(`[${config.name}] removeWorktree failed for PR #${pr.number}: ${err}`)
1357
+ );
1358
+ }
1359
+ }
1360
+ );
1361
+ } catch (err) {
1362
+ console.error(`[${config.name}] setup error for PR #${pr.number}: ${err}`);
1363
+ await removeLabel("pr", pr.number, LABEL_IN_PROGRESS).catch(() => {
1364
+ });
1365
+ await notifyError(config.name, name, err);
1366
+ }
1367
+ }
1368
+ } catch (err) {
1369
+ console.error(`[${config.name}] tick error: ${err}`);
1370
+ await notifyError(config.name, name, err);
1371
+ }
1372
+ };
1373
+ await tick();
1374
+ setInterval(tick, pollingIntervalMs);
1375
+ };
1376
+ }
1377
+
1378
+ // src/workers/fix-review-point.ts
1379
+ var fixReviewPointWorker = createPrPollingWorker({
1380
+ name: "fix-review-point",
1381
+ command: "/claude-task-worker:fix-review-point",
1382
+ triggerLabel: "cc-fix-onetime",
1383
+ onCompleted: async (pr) => {
1384
+ const config = loadConfig();
1385
+ if (config.fixReviewPointCallbackCommentMessage) {
1386
+ try {
1387
+ await commentOnPR(pr.number, config.fixReviewPointCallbackCommentMessage);
1388
+ } catch (err) {
1389
+ console.error(`[fix-review-point] failed to post comment on PR #${pr.number}: ${err}`);
1390
+ }
1391
+ }
1392
+ }
1393
+ });
1394
+
1395
+ // src/workers/create-issue.ts
1396
+ var createIssueWorker = (opts = {}) => createIssuePollingWorker({
1397
+ name: "create-issue",
1398
+ command: "/claude-task-worker:create-issue-from-issue-number",
1399
+ triggerLabels: ["cc-triage-scope"],
1400
+ excludeLabels: [
1401
+ "cc-issue-created",
1402
+ "cc-pr-created",
1403
+ "cc-update-issue",
1404
+ "cc-answer-issue-questions",
1405
+ "cc-exec-issue"
1406
+ ],
1407
+ epicFilters: opts.epicFilters,
1408
+ labelFilters: opts.labelFilters,
1409
+ onCompleted: async (issueNumber) => {
1410
+ await addLabel("issue", issueNumber, "cc-issue-created");
1411
+ }
1412
+ })();
1413
+
1414
+ // src/workers/update-issue.ts
1415
+ var updateIssueWorker = (opts = {}) => createIssuePollingWorker({
1416
+ name: "update-issue",
1417
+ command: "/claude-task-worker:update-issue",
1418
+ triggerLabels: ["cc-update-issue"],
1419
+ epicFilters: opts.epicFilters,
1420
+ labelFilters: opts.labelFilters
1421
+ })();
1422
+
1423
+ // src/workers/answer-issue-questions.ts
1424
+ var answerIssueQuestionsWorker = (opts = {}) => createIssuePollingWorker({
1425
+ name: "answer-issue-questions",
1426
+ command: "/claude-task-worker:answer-issue-questions",
1427
+ triggerLabels: ["cc-answer-issue-questions"],
1428
+ epicFilters: opts.epicFilters,
1429
+ labelFilters: opts.labelFilters,
1430
+ onCompleted: async (issueNumber) => {
1431
+ await addLabel("issue", issueNumber, "cc-update-issue");
1432
+ }
1433
+ })();
1434
+
1435
+ // src/workers/triage-created-issue.ts
1436
+ var triageCreatedIssueWorker = (opts = {}) => createIssuePollingWorker({
1437
+ name: "triage-created-issue",
1438
+ command: "/claude-task-worker:triage-created-issue",
1439
+ triggerLabels: ["cc-issue-created", "cc-triage-scope"],
1440
+ excludeLabels: ["cc-pr-created", "cc-update-issue", "cc-answer-issue-questions", "cc-exec-issue"],
1441
+ epicFilters: opts.epicFilters,
1442
+ labelFilters: opts.labelFilters,
1443
+ onCompleted: async (issueNumber) => {
1444
+ await addLabel("issue", issueNumber, "cc-issue-created");
1445
+ }
1446
+ })();
1447
+
1448
+ // src/workers/triage-pr.ts
1449
+ var triagePrWorker = createPrPollingWorker({
1450
+ name: "triage-pr",
1451
+ command: "/claude-task-worker:triage-pr",
1452
+ triggerLabel: "cc-triage-scope",
1453
+ excludeLabels: ["cc-fix-onetime", "cc-resolve-conflict"]
1454
+ });
1455
+
1456
+ // src/workers/resolve-conflict.ts
1457
+ var resolveConflictWorker = createPrPollingWorker({
1458
+ name: "resolve-conflict",
1459
+ command: "/claude-task-worker:resolve-pr-conflict",
1460
+ triggerLabel: "cc-resolve-conflict"
1461
+ });
1462
+
1463
+ // src/workers/check-dependabot.ts
1464
+ var checkDependabotWorker = createPrPollingWorker({
1465
+ name: "check-dependabot",
1466
+ command: "/claude-task-worker:check-dependabot",
1467
+ triggerLabel: "dependencies",
1468
+ excludeLabels: ["cc-triage-scope"],
1469
+ onCompleted: async (pr) => {
1470
+ await addLabel("pr", pr.number, "cc-triage-scope");
1471
+ }
1472
+ });
1473
+
1474
+ // src/workers/epic-issue.ts
1475
+ var epicIssueWorker = (opts = {}) => createIssuePollingWorker({
1476
+ name: "epic-issue",
1477
+ command: "/claude-task-worker:create-epic-pr",
1478
+ triggerLabels: ["cc-epic-issue"],
1479
+ excludeLabels: ["cc-pr-created"],
1480
+ ownNumberFilters: opts.epicFilters,
1481
+ labelFilters: opts.labelFilters,
1482
+ preflight: async (epic) => {
1483
+ const summary = await getIssueSubIssuesSummary(epic.number);
1484
+ if (summary.total === 0) return "skip";
1485
+ if (summary.completed !== summary.total) return "skip";
1486
+ return "proceed";
1487
+ },
1488
+ onCompleted: async (issueNumber) => {
1489
+ await addLabel("issue", issueNumber, "cc-pr-created");
1490
+ }
1491
+ })();
1492
+
1493
+ // src/commands/init.ts
1494
+ import { mkdir, writeFile, access } from "node:fs/promises";
1495
+ var LABELS = [
1496
+ { name: "cc-update-issue", color: "e4e669" },
1497
+ { name: "cc-answer-issue-questions", color: "5319e7" },
1498
+ { name: "cc-exec-issue", color: "7057ff" },
1499
+ { name: "cc-fix-onetime", color: "d93f0b" },
1500
+ { name: "cc-in-progress", color: "0e8a16" },
1501
+ { name: "cc-need-human-check", color: "b60205" },
1502
+ { name: "cc-issue-created", color: "f9a825" },
1503
+ { name: "cc-pr-created", color: "006b75" },
1504
+ { name: "cc-triage-scope", color: "c5def5" },
1505
+ { name: "cc-resolve-conflict", color: "fbca04" },
1506
+ { name: "cc-epic-issue", color: "8b5cf6" }
1507
+ ];
1508
+ var ISSUE_TEMPLATE = `name: "[claude-task-worker] Issue\u4F5C\u6210\u4F9D\u983C"
1509
+ description: claude-task-worker\u3067GitHub Issue\u3092\u4F5C\u6210\u3059\u308B
1510
+ title: "[claude-task-worker] Issue\u4F5C\u6210\u4F9D\u983C"
1511
+ labels:
1512
+ - cc-triage-scope
1513
+ body:
1514
+ - type: textarea
1515
+ id: request
1516
+ attributes:
1517
+ label: \u4F9D\u983C\u5185\u5BB9
1518
+ description: \u4F5C\u6210\u3057\u3066\u307B\u3057\u3044Issue\u306E\u5185\u5BB9\u3092\u8A18\u8FF0\u3057\u3066\u304F\u3060\u3055\u3044
1519
+ validations:
1520
+ required: true
1521
+ `;
1522
+ var ASSIGN_CREATOR_WORKFLOW = `name: Assign creator on cc-triage-scope
1523
+
1524
+ on:
1525
+ issues:
1526
+ types: [opened]
1527
+
1528
+ jobs:
1529
+ assign:
1530
+ if: contains(github.event.issue.labels.*.name, 'cc-triage-scope')
1531
+ runs-on: ubuntu-latest
1532
+ permissions:
1533
+ issues: write
1534
+ steps:
1535
+ - uses: actions/github-script@v9
1536
+ with:
1537
+ script: |
1538
+ await github.rest.issues.addAssignees({
1539
+ owner: context.repo.owner,
1540
+ repo: context.repo.repo,
1541
+ issue_number: context.issue.number,
1542
+ assignees: [context.payload.issue.user.login]
1543
+ });
1544
+ `;
1545
+ async function writeFileWithMode(path, content, force) {
1546
+ try {
1547
+ await access(path);
1548
+ if (!force) return "skipped";
1549
+ await writeFile(path, content, "utf-8");
1550
+ return "overwritten";
1551
+ } catch {
1552
+ await writeFile(path, content, "utf-8");
1553
+ return "created";
1554
+ }
1555
+ }
1556
+ function logWriteResult(result, path) {
1557
+ if (result === "created") console.log(`[init] Created: ${path}`);
1558
+ else if (result === "overwritten") console.log(`[init] Overwritten: ${path}`);
1559
+ else console.log(`[init] Already exists: ${path}`);
1560
+ }
1561
+ async function createConfig(force) {
1562
+ const initialConfig = { ...DEFAULT_CONFIG, workers: { ...WORKER_DEFAULTS } };
1563
+ const result = await writeFileWithMode(CONFIG_PATH, JSON.stringify(initialConfig, null, 2), force);
1564
+ logWriteResult(result, CONFIG_PATH);
1565
+ }
1566
+ async function init(options = {}) {
1567
+ const force = options.force ?? false;
1568
+ console.log(`[init] Creating labels...${force ? " (force mode)" : ""}`);
1569
+ for (const label of LABELS) {
1570
+ const ok = await createLabel(label.name, label.color, true);
1571
+ if (ok) {
1572
+ console.log(`[init] Ensured label: ${label.name}`);
1573
+ } else {
1574
+ console.log(`[init] Failed to create label: ${label.name}`);
1575
+ }
1576
+ }
1577
+ console.log("[init] Creating issue template...");
1578
+ await mkdir(".github/ISSUE_TEMPLATE", { recursive: true });
1579
+ const templatePath = ".github/ISSUE_TEMPLATE/cc-triage-scope.yml";
1580
+ logWriteResult(await writeFileWithMode(templatePath, ISSUE_TEMPLATE, force), templatePath);
1581
+ console.log("[init] Creating GitHub Actions workflow...");
1582
+ await mkdir(".github/workflows", { recursive: true });
1583
+ const workflowPath = ".github/workflows/assign-creator-on-cc-triage-scope.yml";
1584
+ logWriteResult(await writeFileWithMode(workflowPath, ASSIGN_CREATOR_WORKFLOW, force), workflowPath);
1585
+ console.log("[init] Creating config file...");
1586
+ await createConfig(force);
1587
+ console.log("[init] Done.");
1588
+ }
1589
+
1590
+ // src/commands/run-command.ts
1591
+ import { spawn as spawn2 } from "node:child_process";
1592
+ function runCommand(command, args) {
1593
+ return new Promise((resolve2, reject) => {
1594
+ const child = spawn2(command, args, {
1595
+ stdio: "inherit",
1596
+ shell: process.platform === "win32"
1597
+ });
1598
+ const forwardSignal = (signal) => {
1599
+ child.kill(signal);
1600
+ };
1601
+ const onSigint = () => forwardSignal("SIGINT");
1602
+ const onSigterm = () => forwardSignal("SIGTERM");
1603
+ const cleanup = () => {
1604
+ process.removeListener("SIGINT", onSigint);
1605
+ process.removeListener("SIGTERM", onSigterm);
1606
+ };
1607
+ process.once("SIGINT", onSigint);
1608
+ process.once("SIGTERM", onSigterm);
1609
+ child.on("error", (err) => {
1610
+ cleanup();
1611
+ reject(err);
1612
+ });
1613
+ child.on("close", (code) => {
1614
+ cleanup();
1615
+ if (code === 0) {
1616
+ resolve2();
1617
+ } else {
1618
+ reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
1619
+ }
1620
+ });
1621
+ });
1622
+ }
1623
+
1624
+ // src/commands/install.ts
1625
+ var PLUGIN_NAME = "claude-task-worker";
1626
+ var MARKETPLACE_NAME = "claude-task-worker";
1627
+ var MARKETPLACE_SOURCE = "getty104/claude-task-worker";
1628
+ async function addMarketplace() {
1629
+ console.log(`[install] Adding marketplace: ${MARKETPLACE_SOURCE}...`);
1630
+ try {
1631
+ await runCommand("claude", ["plugin", "marketplace", "add", MARKETPLACE_SOURCE]);
1632
+ console.log("[install] Marketplace added.");
1633
+ } catch (err) {
1634
+ console.error(
1635
+ `[install] Failed to add marketplace (already added is expected and safe to ignore): ${err.message}`
1636
+ );
1637
+ }
1638
+ }
1639
+ async function installPlugin() {
1640
+ console.log(`[install] Installing plugin: ${PLUGIN_NAME}@${MARKETPLACE_NAME}...`);
1641
+ try {
1642
+ await runCommand("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`]);
1643
+ console.log("[install] Plugin installed. Restart your Claude Code session to apply.");
1644
+ return true;
1645
+ } catch (err) {
1646
+ console.error(`[install] Failed to install plugin: ${err.message}`);
1647
+ return false;
1648
+ }
1649
+ }
1650
+ async function installCli() {
1651
+ console.log("[install] Installing claude-task-worker CLI (npm install -g claude-task-worker@latest)...");
1652
+ try {
1653
+ await runCommand("npm", ["install", "-g", "claude-task-worker@latest"]);
1654
+ console.log("[install] claude-task-worker CLI installed.");
1655
+ return true;
1656
+ } catch (err) {
1657
+ console.error(`[install] Failed to install claude-task-worker CLI: ${err.message}`);
1658
+ return false;
1659
+ }
1660
+ }
1661
+ async function install() {
1662
+ console.log("[install] Starting install...");
1663
+ await addMarketplace();
1664
+ const pluginOk = await installPlugin();
1665
+ const cliOk = await installCli();
1666
+ if (!pluginOk || !cliOk) {
1667
+ process.exitCode = 1;
1668
+ }
1669
+ console.log("[install] Done.");
1670
+ }
1671
+
1672
+ // src/commands/update.ts
1673
+ var PLUGIN_NAME2 = "claude-task-worker";
1674
+ var MARKETPLACE_NAME2 = "claude-task-worker";
1675
+ async function updateMarketplace() {
1676
+ console.log(`[update] Updating marketplace: ${MARKETPLACE_NAME2}...`);
1677
+ try {
1678
+ await runCommand("claude", ["plugin", "marketplace", "update", MARKETPLACE_NAME2]);
1679
+ console.log("[update] Marketplace updated.");
1680
+ return true;
1681
+ } catch (err) {
1682
+ console.error(`[update] Failed to update marketplace: ${err.message}`);
1683
+ return false;
1684
+ }
1685
+ }
1686
+ async function updatePlugin() {
1687
+ console.log(`[update] Updating plugin: ${PLUGIN_NAME2}@${MARKETPLACE_NAME2}...`);
1688
+ try {
1689
+ await runCommand("claude", ["plugin", "update", `${PLUGIN_NAME2}@${MARKETPLACE_NAME2}`]);
1690
+ console.log("[update] Plugin updated. Restart your Claude Code session to apply the update.");
1691
+ return true;
1692
+ } catch (err) {
1693
+ console.error(`[update] Failed to update plugin: ${err.message}`);
1694
+ return false;
1695
+ }
1696
+ }
1697
+ async function updateCli() {
1698
+ console.log("[update] Updating claude-task-worker CLI (npm install -g claude-task-worker@latest)...");
1699
+ try {
1700
+ await runCommand("npm", ["install", "-g", "claude-task-worker@latest"]);
1701
+ console.log("[update] claude-task-worker CLI updated.");
1702
+ return true;
1703
+ } catch (err) {
1704
+ console.error(`[update] Failed to update claude-task-worker CLI: ${err.message}`);
1705
+ return false;
1706
+ }
1707
+ }
1708
+ async function update() {
1709
+ console.log("[update] Starting update...");
1710
+ const marketplaceOk = await updateMarketplace();
1711
+ const pluginOk = await updatePlugin();
1712
+ const cliOk = await updateCli();
1713
+ if (!marketplaceOk || !pluginOk || !cliOk) {
1714
+ process.exitCode = 1;
1715
+ }
1716
+ console.log("[update] Done.");
1717
+ }
1718
+
1719
+ // src/index.ts
1720
+ var WORKERS = {
1721
+ "exec-issue": execIssueWorker,
1722
+ "fix-review-point": fixReviewPointWorker,
1723
+ "create-issue": createIssueWorker,
1724
+ "update-issue": updateIssueWorker,
1725
+ "answer-issue-questions": answerIssueQuestionsWorker,
1726
+ "triage-created-issue": triageCreatedIssueWorker,
1727
+ "triage-pr": triagePrWorker,
1728
+ "resolve-conflict": resolveConflictWorker,
1729
+ "check-dependabot": checkDependabotWorker,
1730
+ "epic-issue": epicIssueWorker
1731
+ };
1732
+ function printUsage() {
1733
+ console.log(`Usage: claude-task-worker <command> [--epic <issue-number>] [--label <label-name>]
1734
+
1735
+ Commands:
1736
+ init [--force] Create required GitHub labels and config file (use --force to overwrite existing files)
1737
+ install Add the claude-task-worker marketplace, install the plugin, and install/update the CLI
1738
+ update Update the claude-task-worker plugin/marketplace and the CLI itself
1739
+ usage Notify current usage to Slack
1740
+
1741
+ Workers:
1742
+ exec-issue Poll issues and run /exec-issue
1743
+ fix-review-point Poll PRs and run /fix-review-point
1744
+ create-issue Poll issues and run /create-issue
1745
+ update-issue Poll issues and run update command
1746
+ answer-issue-questions Poll issues and run /answer-issue-questions
1747
+ triage-created-issue Poll cc-issue-created + cc-triage-scope issues and run /triage-created-issue
1748
+ triage-pr Poll and triage PRs every 5 minutes
1749
+ resolve-conflict Poll cc-resolve-conflict PRs and run /resolve-conflict
1750
+ check-dependabot Poll dependabot PRs every 1 hour
1751
+ epic-issue Poll cc-epic-issue issues and create epic PR when all sub-issues are closed
1752
+ all Poll all workers except triage-created-issue, triage-pr, check-dependabot
1753
+ yolo Poll all workers including triage-created-issue, triage-pr, check-dependabot
1754
+
1755
+ Options:
1756
+ --epic <number> Limit issue-based workers to sub-issues of the specified epic issue. Repeatable: any matching parent (OR).
1757
+ --label <name> Limit issue-based workers to issues that also carry the specified label. Repeatable: all must be present (AND).
1758
+
1759
+ Example:
1760
+ claude-task-worker init
1761
+ claude-task-worker exec-issue
1762
+ claude-task-worker all --epic 100
1763
+ claude-task-worker all --epic 100 --epic 200
1764
+ claude-task-worker all --label priority-high
1765
+ claude-task-worker all --label priority-high --label needs-design
1766
+ claude-task-worker yolo --epic 100 --epic 200 --label priority-high`);
1767
+ }
1768
+ var workerType = process.argv[2];
1769
+ if (!workerType) {
1770
+ printUsage();
1771
+ process.exit(1);
1772
+ }
1773
+ if (workerType !== "all" && workerType !== "yolo" && workerType !== "init" && workerType !== "install" && workerType !== "update" && workerType !== "usage" && !WORKERS[workerType]) {
1774
+ console.error(`Unknown command: ${workerType}`);
1775
+ printUsage();
1776
+ process.exit(1);
1777
+ }
1778
+ function collectFlagValues(flag) {
1779
+ const values = [];
1780
+ for (let i = 0; i < process.argv.length; i++) {
1781
+ if (process.argv[i] !== flag) continue;
1782
+ const raw = process.argv[i + 1];
1783
+ if (!raw || raw.startsWith("--")) {
1784
+ console.error(`${flag} requires a value`);
1785
+ process.exit(1);
1786
+ }
1787
+ values.push(raw);
1788
+ }
1789
+ return values;
1790
+ }
1791
+ function parseEpicFilters() {
1792
+ const raws = collectFlagValues("--epic");
1793
+ return raws.map((raw) => {
1794
+ const num = Number(raw);
1795
+ if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
1796
+ console.error(`--epic requires a positive integer issue number, got: ${raw}`);
1797
+ process.exit(1);
1798
+ }
1799
+ return num;
1800
+ });
1801
+ }
1802
+ function parseLabelFilters() {
1803
+ return collectFlagValues("--label");
1804
+ }
1805
+ process.on("unhandledRejection", (err) => {
1806
+ console.error("[worker] unhandled rejection:", err);
1807
+ process.exit(1);
1808
+ });
1809
+ process.on("SIGTERM", async () => {
1810
+ if (isShuttingDown()) return;
1811
+ setShuttingDown();
1812
+ console.log(
1813
+ "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Send SIGTERM again to force kill)"
1814
+ );
1815
+ await waitForAllProcesses();
1816
+ process.exit(0);
1817
+ });
1818
+ var forceKilling = false;
1819
+ process.on("SIGINT", async () => {
1820
+ if (isShuttingDown()) {
1821
+ if (forceKilling) return;
1822
+ forceKilling = true;
1823
+ console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
1824
+ shutdown("SIGKILL");
1825
+ const cleanupTimeout = new Promise((resolve2) => setTimeout(resolve2, 6e4).unref());
1826
+ await Promise.race([waitForAllProcesses(), cleanupTimeout]);
1827
+ process.exit(1);
1828
+ }
1829
+ setShuttingDown();
1830
+ console.log(
1831
+ "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Press Ctrl-C again to force kill)"
1832
+ );
1833
+ await waitForAllProcesses();
1834
+ process.exit(0);
1835
+ });
1836
+ if (workerType === "init") {
1837
+ const force = process.argv.slice(3).includes("--force");
1838
+ init({ force });
1839
+ } else if (workerType === "install") {
1840
+ (async () => {
1841
+ await install();
1842
+ })();
1843
+ } else if (workerType === "update") {
1844
+ (async () => {
1845
+ await update();
1846
+ })();
1847
+ } else if (workerType === "usage") {
1848
+ (async () => {
1849
+ const text = await buildTokenLimitText();
1850
+ if (!text) {
1851
+ console.error("Failed to fetch usage info");
1852
+ process.exit(1);
1853
+ }
1854
+ console.log(text.trim());
1855
+ await send({ text: `\u{1F4CA} Usage${text}` });
1856
+ })();
1857
+ } else if (workerType === "all") {
1858
+ const epicFilters = parseEpicFilters();
1859
+ const labelFilters = parseLabelFilters();
1860
+ Promise.all([
1861
+ execIssueWorker({ epicFilters, labelFilters }),
1862
+ fixReviewPointWorker(),
1863
+ createIssueWorker({ epicFilters, labelFilters }),
1864
+ updateIssueWorker({ epicFilters, labelFilters }),
1865
+ answerIssueQuestionsWorker({ epicFilters, labelFilters }),
1866
+ resolveConflictWorker(),
1867
+ epicIssueWorker({ epicFilters, labelFilters })
1868
+ ]);
1869
+ } else if (workerType === "yolo") {
1870
+ const epicFilters = parseEpicFilters();
1871
+ const labelFilters = parseLabelFilters();
1872
+ (async () => {
1873
+ await Promise.all([
1874
+ execIssueWorker({ epicFilters, labelFilters }),
1875
+ fixReviewPointWorker(),
1876
+ createIssueWorker({ epicFilters, labelFilters }),
1877
+ updateIssueWorker({ epicFilters, labelFilters }),
1878
+ answerIssueQuestionsWorker({ epicFilters, labelFilters }),
1879
+ triageCreatedIssueWorker({ epicFilters, labelFilters }),
1880
+ checkDependabotWorker(),
1881
+ triagePrWorker(),
1882
+ resolveConflictWorker(),
1883
+ epicIssueWorker({ epicFilters, labelFilters })
1884
+ ]);
1885
+ })();
1886
+ } else {
1887
+ const epicFilters = parseEpicFilters();
1888
+ const labelFilters = parseLabelFilters();
1889
+ WORKERS[workerType]({ epicFilters, labelFilters });
1890
+ }