claude-task-worker 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +152 -59
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -505,6 +505,14 @@ function isRunning(id) {
|
|
|
505
505
|
const task = tasks.get(id);
|
|
506
506
|
return task?.status === "running";
|
|
507
507
|
}
|
|
508
|
+
function isWorktreeInUse(worktreeId) {
|
|
509
|
+
for (const task of tasks.values()) {
|
|
510
|
+
if (task.status === "running" && task.path === worktreeId) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
508
516
|
function isWorkerAtCapacity(workerName) {
|
|
509
517
|
let count = 0;
|
|
510
518
|
for (const task of tasks.values()) {
|
|
@@ -951,6 +959,9 @@ function generateWorktreeName() {
|
|
|
951
959
|
const suffix = String(randomInt(1e4)).padStart(4, "0");
|
|
952
960
|
return `${pick(ADJECTIVES)}-${pick(NOUNS)}-${suffix}`;
|
|
953
961
|
}
|
|
962
|
+
function isGeneratedWorktreeName(name) {
|
|
963
|
+
return /^[a-z]+-[a-z]+-\d{4}$/.test(name);
|
|
964
|
+
}
|
|
954
965
|
|
|
955
966
|
// src/slack.ts
|
|
956
967
|
import { exec } from "node:child_process";
|
|
@@ -1087,69 +1098,152 @@ async function notifyError(workerName, repoName, error) {
|
|
|
1087
1098
|
import { execFile as execFile3 } from "node:child_process";
|
|
1088
1099
|
import { promisify as promisify3 } from "node:util";
|
|
1089
1100
|
import { readdir, rm, stat } from "node:fs/promises";
|
|
1090
|
-
import { resolve, sep } from "node:path";
|
|
1101
|
+
import { basename, resolve, sep } from "node:path";
|
|
1091
1102
|
var execFileAsync2 = promisify3(execFile3);
|
|
1092
1103
|
var WORKTREES_DIR = ".claude/worktrees";
|
|
1093
1104
|
function isManagedWorktreePath(path) {
|
|
1094
1105
|
return resolve(path).startsWith(resolve(WORKTREES_DIR) + sep);
|
|
1095
1106
|
}
|
|
1107
|
+
async function pathExists(path) {
|
|
1108
|
+
try {
|
|
1109
|
+
await stat(path);
|
|
1110
|
+
return true;
|
|
1111
|
+
} catch {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1096
1115
|
async function forceRemoveIfExists(path) {
|
|
1097
1116
|
if (!isManagedWorktreePath(path)) {
|
|
1098
1117
|
console.error(`[worktree] Refusing to remove path outside ${WORKTREES_DIR}: ${path}`);
|
|
1099
1118
|
return;
|
|
1100
1119
|
}
|
|
1120
|
+
if (!await pathExists(path)) return;
|
|
1101
1121
|
try {
|
|
1102
|
-
await
|
|
1122
|
+
await rm(path, { recursive: true, force: true });
|
|
1123
|
+
console.log(`[worktree] Force removed remaining directory: ${path}`);
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
console.error(`[worktree] Failed to remove directory ${path}:`, error);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async function listWorktreeEntries() {
|
|
1129
|
+
const { stdout } = await execFileAsync2("git", ["worktree", "list", "--porcelain"]);
|
|
1130
|
+
return stdout.replace(/\r\n/g, "\n").trim().split("\n\n").filter(Boolean).map((entry) => {
|
|
1131
|
+
const lines = entry.split("\n");
|
|
1132
|
+
const worktreeLine = lines.find((l) => l.startsWith("worktree "));
|
|
1133
|
+
const branchLine = lines.find((l) => l.startsWith("branch refs/heads/"));
|
|
1134
|
+
return {
|
|
1135
|
+
path: worktreeLine ? worktreeLine.slice("worktree ".length) : "",
|
|
1136
|
+
branch: branchLine?.slice("branch refs/heads/".length),
|
|
1137
|
+
locked: lines.some((l) => l === "locked" || l.startsWith("locked "))
|
|
1138
|
+
};
|
|
1139
|
+
}).filter((e) => e.path !== "");
|
|
1140
|
+
}
|
|
1141
|
+
async function removeRegisteredWorktree(worktreePath) {
|
|
1142
|
+
try {
|
|
1143
|
+
await execFileAsync2("git", ["worktree", "remove", "--force", "--force", worktreePath]);
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
console.error(`[worktree] Failed to remove worktree ${worktreePath}:`, error);
|
|
1146
|
+
}
|
|
1147
|
+
await forceRemoveIfExists(worktreePath);
|
|
1148
|
+
}
|
|
1149
|
+
var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["main", "master", "develop"]);
|
|
1150
|
+
async function getDefaultBranchName() {
|
|
1151
|
+
try {
|
|
1152
|
+
const { stdout } = await execFileAsync2("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
1153
|
+
return stdout.trim().replace(/^origin\//, "");
|
|
1103
1154
|
} catch {
|
|
1155
|
+
return void 0;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async function deleteLocalBranch(branchName) {
|
|
1159
|
+
const defaultBranch = await getDefaultBranchName();
|
|
1160
|
+
if (PROTECTED_BRANCHES.has(branchName) || branchName === defaultBranch) {
|
|
1161
|
+
console.log(`[worktree] Skipping deletion of protected branch: ${branchName}`);
|
|
1104
1162
|
return;
|
|
1105
1163
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1164
|
+
try {
|
|
1165
|
+
await execFileAsync2("git", ["branch", "-D", branchName]);
|
|
1166
|
+
console.log(`[worktree] Deleted local branch: ${branchName}`);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
const stderr = String(error?.stderr ?? "");
|
|
1169
|
+
if (/not found/i.test(stderr)) return;
|
|
1170
|
+
console.error(`[worktree] Failed to delete local branch ${branchName}: ${stderr.trim()}`);
|
|
1171
|
+
}
|
|
1108
1172
|
}
|
|
1109
1173
|
function getWorktreePath(worktreeId) {
|
|
1110
1174
|
return `${WORKTREES_DIR}/${worktreeId}`;
|
|
1111
1175
|
}
|
|
1112
1176
|
async function createWorktreeFromBranch(worktreeId, baseBranch) {
|
|
1113
|
-
const worktreePath =
|
|
1177
|
+
const worktreePath = getWorktreePath(worktreeId);
|
|
1114
1178
|
await execFileAsync2("git", ["worktree", "add", "-B", worktreeId, worktreePath, `origin/${baseBranch}`]);
|
|
1115
1179
|
}
|
|
1116
1180
|
async function removeWorktree(worktreeId) {
|
|
1117
|
-
const worktreePath =
|
|
1181
|
+
const worktreePath = getWorktreePath(worktreeId);
|
|
1182
|
+
let entry;
|
|
1118
1183
|
try {
|
|
1119
|
-
await
|
|
1184
|
+
entry = (await listWorktreeEntries()).find((e) => resolve(e.path) === resolve(worktreePath));
|
|
1120
1185
|
} catch (error) {
|
|
1121
|
-
console.error(`[worktree] Failed to
|
|
1186
|
+
console.error(`[worktree] Failed to list worktrees:`, error);
|
|
1187
|
+
}
|
|
1188
|
+
if (entry) {
|
|
1189
|
+
await removeRegisteredWorktree(worktreePath);
|
|
1190
|
+
} else {
|
|
1191
|
+
await forceRemoveIfExists(worktreePath);
|
|
1192
|
+
}
|
|
1193
|
+
const branches = /* @__PURE__ */ new Set([entry?.branch, worktreeId]);
|
|
1194
|
+
for (const branch of branches) {
|
|
1195
|
+
if (!branch) continue;
|
|
1196
|
+
await deleteLocalBranch(branch);
|
|
1122
1197
|
}
|
|
1123
|
-
await forceRemoveIfExists(worktreePath);
|
|
1124
1198
|
}
|
|
1125
1199
|
async function removeWorktreeByBranch(branchName) {
|
|
1126
1200
|
try {
|
|
1127
|
-
const
|
|
1128
|
-
const entries = stdout.trim().split("\n\n").filter(Boolean);
|
|
1201
|
+
const entries = await listWorktreeEntries();
|
|
1129
1202
|
for (const entry of entries) {
|
|
1130
|
-
|
|
1131
|
-
if (!
|
|
1132
|
-
|
|
1133
|
-
if (branch !== branchName) continue;
|
|
1134
|
-
const worktreeLine = entry.split("\n").find((l) => l.startsWith("worktree "));
|
|
1135
|
-
if (!worktreeLine) continue;
|
|
1136
|
-
const worktreePath = worktreeLine.replace("worktree ", "");
|
|
1137
|
-
if (!isManagedWorktreePath(worktreePath)) {
|
|
1138
|
-
console.log(`[worktree] Skipping unmanaged worktree for branch ${branchName}: ${worktreePath}`);
|
|
1203
|
+
if (entry.branch !== branchName) continue;
|
|
1204
|
+
if (!isManagedWorktreePath(entry.path)) {
|
|
1205
|
+
console.log(`[worktree] Skipping unmanaged worktree for branch ${branchName}: ${entry.path}`);
|
|
1139
1206
|
continue;
|
|
1140
1207
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
console.log(`[worktree]
|
|
1144
|
-
|
|
1145
|
-
|
|
1208
|
+
const worktreeId = basename(entry.path);
|
|
1209
|
+
if (isWorktreeInUse(worktreeId)) {
|
|
1210
|
+
console.log(`[worktree] Skipping worktree in use by a running task for branch ${branchName}: ${entry.path}`);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (entry.locked && await pathExists(entry.path)) {
|
|
1214
|
+
console.log(`[worktree] Skipping locked worktree for branch ${branchName}: ${entry.path}`);
|
|
1215
|
+
continue;
|
|
1146
1216
|
}
|
|
1147
|
-
await
|
|
1217
|
+
await removeRegisteredWorktree(entry.path);
|
|
1218
|
+
console.log(`[worktree] Removed worktree for branch ${branchName}: ${entry.path}`);
|
|
1148
1219
|
}
|
|
1149
1220
|
} catch (error) {
|
|
1150
1221
|
console.error(`[worktree] Failed to remove worktree for branch ${branchName}:`, error);
|
|
1151
1222
|
}
|
|
1152
1223
|
}
|
|
1224
|
+
async function removeStaleWorktrees() {
|
|
1225
|
+
const worktreeIds = /* @__PURE__ */ new Set();
|
|
1226
|
+
try {
|
|
1227
|
+
for (const name of await readdir(WORKTREES_DIR)) {
|
|
1228
|
+
if (isGeneratedWorktreeName(name)) worktreeIds.add(name);
|
|
1229
|
+
}
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
for (const entry of await listWorktreeEntries()) {
|
|
1234
|
+
if (!isManagedWorktreePath(entry.path)) continue;
|
|
1235
|
+
const worktreeId = basename(entry.path);
|
|
1236
|
+
if (isGeneratedWorktreeName(worktreeId)) worktreeIds.add(worktreeId);
|
|
1237
|
+
}
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
console.error(`[worktree] Failed to list worktrees:`, error);
|
|
1240
|
+
}
|
|
1241
|
+
for (const worktreeId of worktreeIds) {
|
|
1242
|
+
if (isWorktreeInUse(worktreeId)) continue;
|
|
1243
|
+
console.log(`[worktree] Removing stale worktree: ${worktreeId}`);
|
|
1244
|
+
await removeWorktree(worktreeId);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1153
1247
|
|
|
1154
1248
|
// src/workers/issue-worker.ts
|
|
1155
1249
|
var LABEL_TRIAGE_SCOPE = "cc-triage-scope";
|
|
@@ -1194,7 +1288,6 @@ function createIssuePollingWorker(config) {
|
|
|
1194
1288
|
const { model, effort, skill } = getWorkerConfig(config.name);
|
|
1195
1289
|
const command = skill || config.command;
|
|
1196
1290
|
const parentNumber = issue.parent?.number;
|
|
1197
|
-
let cwd;
|
|
1198
1291
|
const claudeArgs = [
|
|
1199
1292
|
"-p",
|
|
1200
1293
|
`${command} ${issue.number}`,
|
|
@@ -1204,17 +1297,14 @@ function createIssuePollingWorker(config) {
|
|
|
1204
1297
|
"--effort",
|
|
1205
1298
|
effort
|
|
1206
1299
|
];
|
|
1300
|
+
let baseBranch = defaultBranch;
|
|
1207
1301
|
if (parentNumber !== void 0) {
|
|
1208
|
-
|
|
1209
|
-
await ensureEpicBranch(
|
|
1210
|
-
await createWorktreeFromBranch(worktreeId, epicBranch);
|
|
1211
|
-
cwd = getWorktreePath(worktreeId);
|
|
1212
|
-
console.log(
|
|
1213
|
-
`[${config.name}] #${issue.number}: created worktree ${worktreeId} from ${epicBranch} (parent #${parentNumber})`
|
|
1214
|
-
);
|
|
1215
|
-
} else {
|
|
1216
|
-
claudeArgs.push("--worktree", worktreeId);
|
|
1302
|
+
baseBranch = `cc-epic-${parentNumber}`;
|
|
1303
|
+
await ensureEpicBranch(baseBranch, defaultBranch);
|
|
1217
1304
|
}
|
|
1305
|
+
await createWorktreeFromBranch(worktreeId, baseBranch);
|
|
1306
|
+
const cwd = getWorktreePath(worktreeId);
|
|
1307
|
+
console.log(`[${config.name}] #${issue.number}: created worktree ${worktreeId} from ${baseBranch}`);
|
|
1218
1308
|
run(
|
|
1219
1309
|
"claude",
|
|
1220
1310
|
claudeArgs,
|
|
@@ -1313,25 +1403,18 @@ function createPrPollingWorker(config) {
|
|
|
1313
1403
|
const prUrl = `https://github.com/${owner}/${name}/pull/${pr.number}`;
|
|
1314
1404
|
const hadTriageScope = pr.labels.some((l) => l.name === LABEL_TRIAGE_SCOPE2);
|
|
1315
1405
|
await addLabel("pr", pr.number, LABEL_IN_PROGRESS);
|
|
1406
|
+
const worktreeId = generateWorktreeName();
|
|
1316
1407
|
try {
|
|
1317
1408
|
await removeWorktreeByBranch(pr.headRefName);
|
|
1318
|
-
|
|
1409
|
+
await deleteLocalBranch(pr.headRefName);
|
|
1319
1410
|
syncDefaultBranch(defaultBranch);
|
|
1411
|
+
await createWorktreeFromBranch(worktreeId, defaultBranch);
|
|
1412
|
+
const cwd = getWorktreePath(worktreeId);
|
|
1320
1413
|
const { model, effort, skill } = getWorkerConfig(config.name);
|
|
1321
1414
|
const command = skill || config.command;
|
|
1322
1415
|
run(
|
|
1323
1416
|
"claude",
|
|
1324
|
-
[
|
|
1325
|
-
"-p",
|
|
1326
|
-
`${command} ${pr.number}`,
|
|
1327
|
-
"--dangerously-skip-permissions",
|
|
1328
|
-
"--model",
|
|
1329
|
-
model,
|
|
1330
|
-
"--effort",
|
|
1331
|
-
effort,
|
|
1332
|
-
"--worktree",
|
|
1333
|
-
worktreeId
|
|
1334
|
-
],
|
|
1417
|
+
["-p", `${command} ${pr.number}`, "--dangerously-skip-permissions", "--model", model, "--effort", effort],
|
|
1335
1418
|
pr.number,
|
|
1336
1419
|
`PR #${pr.number} (${pr.headRefName})`,
|
|
1337
1420
|
config.name,
|
|
@@ -1372,12 +1455,15 @@ function createPrPollingWorker(config) {
|
|
|
1372
1455
|
(err) => console.error(`[${config.name}] removeWorktree failed for PR #${pr.number}: ${err}`)
|
|
1373
1456
|
);
|
|
1374
1457
|
}
|
|
1375
|
-
}
|
|
1458
|
+
},
|
|
1459
|
+
cwd
|
|
1376
1460
|
);
|
|
1377
1461
|
} catch (err) {
|
|
1378
1462
|
console.error(`[${config.name}] setup error for PR #${pr.number}: ${err}`);
|
|
1379
1463
|
await removeLabel("pr", pr.number, LABEL_IN_PROGRESS).catch(() => {
|
|
1380
1464
|
});
|
|
1465
|
+
await removeWorktree(worktreeId).catch(() => {
|
|
1466
|
+
});
|
|
1381
1467
|
await notifyError(config.name, name, err);
|
|
1382
1468
|
}
|
|
1383
1469
|
}
|
|
@@ -1902,19 +1988,23 @@ if (workerType === "init") {
|
|
|
1902
1988
|
} else if (workerType === "all") {
|
|
1903
1989
|
const epicFilters = parseEpicFilters();
|
|
1904
1990
|
const labelFilters = parseLabelFilters();
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1991
|
+
(async () => {
|
|
1992
|
+
await removeStaleWorktrees();
|
|
1993
|
+
await Promise.all([
|
|
1994
|
+
execIssueWorker({ epicFilters, labelFilters }),
|
|
1995
|
+
fixReviewPointWorker(),
|
|
1996
|
+
createIssueWorker({ epicFilters, labelFilters }),
|
|
1997
|
+
updateIssueWorker({ epicFilters, labelFilters }),
|
|
1998
|
+
answerIssueQuestionsWorker({ epicFilters, labelFilters }),
|
|
1999
|
+
resolveConflictWorker(),
|
|
2000
|
+
epicIssueWorker({ epicFilters, labelFilters })
|
|
2001
|
+
]);
|
|
2002
|
+
})();
|
|
1914
2003
|
} else if (workerType === "yolo") {
|
|
1915
2004
|
const epicFilters = parseEpicFilters();
|
|
1916
2005
|
const labelFilters = parseLabelFilters();
|
|
1917
2006
|
(async () => {
|
|
2007
|
+
await removeStaleWorktrees();
|
|
1918
2008
|
await Promise.all([
|
|
1919
2009
|
execIssueWorker({ epicFilters, labelFilters }),
|
|
1920
2010
|
fixReviewPointWorker(),
|
|
@@ -1931,5 +2021,8 @@ if (workerType === "init") {
|
|
|
1931
2021
|
} else {
|
|
1932
2022
|
const epicFilters = parseEpicFilters();
|
|
1933
2023
|
const labelFilters = parseLabelFilters();
|
|
1934
|
-
|
|
2024
|
+
(async () => {
|
|
2025
|
+
await removeStaleWorktrees();
|
|
2026
|
+
await WORKERS[workerType]({ epicFilters, labelFilters });
|
|
2027
|
+
})();
|
|
1935
2028
|
}
|