claude-task-worker 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -8
- package/dist/index.js +715 -297
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -59,18 +59,28 @@ __export(herdr_exports, {
|
|
|
59
59
|
HERDR_TIMEOUT_MS: () => HERDR_TIMEOUT_MS,
|
|
60
60
|
HerdrError: () => HerdrError,
|
|
61
61
|
HerdrUnavailableError: () => HerdrUnavailableError,
|
|
62
|
+
agentGet: () => agentGet,
|
|
63
|
+
agentStart: () => agentStart,
|
|
62
64
|
checkHerdrAvailable: () => checkHerdrAvailable,
|
|
65
|
+
getCurrentWorkspaceId: () => getCurrentWorkspaceId,
|
|
66
|
+
paneClose: () => paneClose,
|
|
67
|
+
paneGet: () => paneGet,
|
|
68
|
+
paneMoveToNewTab: () => paneMoveToNewTab,
|
|
63
69
|
paneProcessInfo: () => paneProcessInfo,
|
|
64
70
|
paneRead: () => paneRead,
|
|
65
71
|
paneSendKeys: () => paneSendKeys,
|
|
66
72
|
paneSendText: () => paneSendText,
|
|
67
73
|
tabClose: () => tabClose,
|
|
68
74
|
tabCreate: () => tabCreate,
|
|
69
|
-
tabList: () => tabList
|
|
75
|
+
tabList: () => tabList,
|
|
76
|
+
tabRename: () => tabRename,
|
|
77
|
+
workspaceClose: () => workspaceClose,
|
|
78
|
+
workspaceCreate: () => workspaceCreate,
|
|
79
|
+
workspaceList: () => workspaceList
|
|
70
80
|
});
|
|
71
81
|
import { createRequire as createRequire2 } from "node:module";
|
|
72
82
|
function runHerdr(args) {
|
|
73
|
-
return new Promise((
|
|
83
|
+
return new Promise((resolve3) => {
|
|
74
84
|
childProcess2.execFile(
|
|
75
85
|
"herdr",
|
|
76
86
|
args,
|
|
@@ -82,7 +92,7 @@ function runHerdr(args) {
|
|
|
82
92
|
} catch {
|
|
83
93
|
parsed = void 0;
|
|
84
94
|
}
|
|
85
|
-
|
|
95
|
+
resolve3({ execError: error, parsed, stdout, stderr });
|
|
86
96
|
}
|
|
87
97
|
);
|
|
88
98
|
});
|
|
@@ -107,8 +117,27 @@ async function execHerdr(args, options) {
|
|
|
107
117
|
}
|
|
108
118
|
return parsed.result;
|
|
109
119
|
}
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
function envArgs(env) {
|
|
121
|
+
if (!env) return [];
|
|
122
|
+
return Object.entries(env).flatMap(([key, value]) => ["--env", `${key}=${value}`]);
|
|
123
|
+
}
|
|
124
|
+
async function tabCreate({
|
|
125
|
+
label,
|
|
126
|
+
cwd,
|
|
127
|
+
workspaceId,
|
|
128
|
+
env
|
|
129
|
+
}) {
|
|
130
|
+
const result = await execHerdr([
|
|
131
|
+
"tab",
|
|
132
|
+
"create",
|
|
133
|
+
...workspaceId ? ["--workspace", workspaceId] : [],
|
|
134
|
+
"--label",
|
|
135
|
+
label,
|
|
136
|
+
"--cwd",
|
|
137
|
+
cwd,
|
|
138
|
+
...envArgs(env),
|
|
139
|
+
"--no-focus"
|
|
140
|
+
]);
|
|
112
141
|
const paneId = result?.root_pane?.pane_id;
|
|
113
142
|
const tabId = result?.tab?.tab_id;
|
|
114
143
|
if (!paneId || !tabId) {
|
|
@@ -116,6 +145,20 @@ async function tabCreate({ label, cwd }) {
|
|
|
116
145
|
}
|
|
117
146
|
return { paneId, tabId };
|
|
118
147
|
}
|
|
148
|
+
async function tabRename(tabId, label) {
|
|
149
|
+
await execHerdr(["tab", "rename", tabId, label]);
|
|
150
|
+
}
|
|
151
|
+
async function paneClose(paneId) {
|
|
152
|
+
await execHerdr(["pane", "close", paneId]);
|
|
153
|
+
}
|
|
154
|
+
async function paneGet(paneId) {
|
|
155
|
+
const result = await execHerdr(["pane", "get", paneId]);
|
|
156
|
+
const pane = result?.pane;
|
|
157
|
+
if (!pane?.pane_id) {
|
|
158
|
+
throw new Error(`Failed to get pane ${paneId}: invalid response structure from herdr`);
|
|
159
|
+
}
|
|
160
|
+
return { paneId: pane.pane_id, tabId: pane.tab_id ?? "" };
|
|
161
|
+
}
|
|
119
162
|
async function tabClose(tabId) {
|
|
120
163
|
await execHerdr(["tab", "close", tabId]);
|
|
121
164
|
}
|
|
@@ -130,6 +173,105 @@ async function tabList() {
|
|
|
130
173
|
workspaceId: tab.workspace_id
|
|
131
174
|
}));
|
|
132
175
|
}
|
|
176
|
+
async function workspaceCreate({
|
|
177
|
+
label,
|
|
178
|
+
cwd,
|
|
179
|
+
env
|
|
180
|
+
}) {
|
|
181
|
+
const result = await execHerdr([
|
|
182
|
+
"workspace",
|
|
183
|
+
"create",
|
|
184
|
+
"--label",
|
|
185
|
+
label,
|
|
186
|
+
"--cwd",
|
|
187
|
+
cwd,
|
|
188
|
+
...envArgs(env),
|
|
189
|
+
"--no-focus"
|
|
190
|
+
]);
|
|
191
|
+
const workspaceId = result?.workspace?.workspace_id;
|
|
192
|
+
const paneId = result?.root_pane?.pane_id;
|
|
193
|
+
const tabId = result?.root_pane?.tab_id;
|
|
194
|
+
if (!workspaceId || !paneId || !tabId) {
|
|
195
|
+
throw new Error("Failed to create workspace: invalid response structure from herdr");
|
|
196
|
+
}
|
|
197
|
+
return { workspaceId, paneId, tabId };
|
|
198
|
+
}
|
|
199
|
+
async function workspaceList() {
|
|
200
|
+
const result = await execHerdr(["workspace", "list"]);
|
|
201
|
+
if (!result || !Array.isArray(result.workspaces)) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
return result.workspaces.map((workspace) => ({
|
|
205
|
+
workspaceId: workspace.workspace_id,
|
|
206
|
+
label: workspace.label ?? ""
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
async function workspaceClose(workspaceId) {
|
|
210
|
+
await execHerdr(["workspace", "close", workspaceId]);
|
|
211
|
+
}
|
|
212
|
+
async function agentStart({
|
|
213
|
+
name,
|
|
214
|
+
cwd,
|
|
215
|
+
argv,
|
|
216
|
+
workspaceId,
|
|
217
|
+
env
|
|
218
|
+
}) {
|
|
219
|
+
const result = await execHerdr([
|
|
220
|
+
"agent",
|
|
221
|
+
"start",
|
|
222
|
+
name,
|
|
223
|
+
...workspaceId ? ["--workspace", workspaceId] : [],
|
|
224
|
+
"--cwd",
|
|
225
|
+
cwd,
|
|
226
|
+
...envArgs(env),
|
|
227
|
+
"--no-focus",
|
|
228
|
+
"--",
|
|
229
|
+
...argv
|
|
230
|
+
]);
|
|
231
|
+
const paneId = result?.agent?.pane_id;
|
|
232
|
+
const tabId = result?.agent?.tab_id;
|
|
233
|
+
if (!paneId || !tabId) {
|
|
234
|
+
throw new Error("Failed to start agent: invalid response structure from herdr");
|
|
235
|
+
}
|
|
236
|
+
return { paneId, tabId };
|
|
237
|
+
}
|
|
238
|
+
async function paneMoveToNewTab(paneId, { label, workspaceId }) {
|
|
239
|
+
const result = await execHerdr([
|
|
240
|
+
"pane",
|
|
241
|
+
"move",
|
|
242
|
+
paneId,
|
|
243
|
+
"--new-tab",
|
|
244
|
+
...workspaceId ? ["--workspace", workspaceId] : [],
|
|
245
|
+
"--label",
|
|
246
|
+
label,
|
|
247
|
+
"--no-focus"
|
|
248
|
+
]);
|
|
249
|
+
const tabId = result?.move_result?.created_tab?.tab_id;
|
|
250
|
+
if (!tabId) {
|
|
251
|
+
throw new Error("Failed to move pane to a new tab: invalid response structure from herdr");
|
|
252
|
+
}
|
|
253
|
+
return { tabId };
|
|
254
|
+
}
|
|
255
|
+
function toAgentStatus(value) {
|
|
256
|
+
return value === "working" || value === "idle" || value === "blocked" ? value : "unknown";
|
|
257
|
+
}
|
|
258
|
+
async function agentGet(target) {
|
|
259
|
+
const result = await execHerdr(["agent", "get", target]);
|
|
260
|
+
const agent = result?.agent;
|
|
261
|
+
if (!agent?.pane_id) {
|
|
262
|
+
throw new Error(`Failed to get agent info for ${target}: invalid response structure from herdr`);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
paneId: agent.pane_id,
|
|
266
|
+
tabId: agent.tab_id ?? "",
|
|
267
|
+
workspaceId: agent.workspace_id ?? "",
|
|
268
|
+
agentStatus: toAgentStatus(agent.agent_status)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function getCurrentWorkspaceId() {
|
|
272
|
+
const id = process.env.HERDR_WORKSPACE_ID;
|
|
273
|
+
return id && id.length > 0 ? id : void 0;
|
|
274
|
+
}
|
|
133
275
|
function isPaneReadErrorPayload(value) {
|
|
134
276
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
135
277
|
return false;
|
|
@@ -233,16 +375,175 @@ var init_herdr = __esm({
|
|
|
233
375
|
}
|
|
234
376
|
});
|
|
235
377
|
|
|
378
|
+
// src/herdr-runner.ts
|
|
379
|
+
var herdr_runner_exports = {};
|
|
380
|
+
__export(herdr_runner_exports, {
|
|
381
|
+
AGENT_POLL_INTERVAL_MS: () => AGENT_POLL_INTERVAL_MS,
|
|
382
|
+
CLAUDE_EXIT_POLL_INTERVAL_MS: () => CLAUDE_EXIT_POLL_INTERVAL_MS,
|
|
383
|
+
CLAUDE_EXIT_TIMEOUT_MS: () => CLAUDE_EXIT_TIMEOUT_MS,
|
|
384
|
+
PANE_OUTPUT_LINES: () => PANE_OUTPUT_LINES,
|
|
385
|
+
buildHerdrTaskResult: () => buildHerdrTaskResult,
|
|
386
|
+
createCompletionTracker: () => createCompletionTracker,
|
|
387
|
+
observeAgentStatus: () => observeAgentStatus,
|
|
388
|
+
startHerdrTask: () => startHerdrTask,
|
|
389
|
+
stopHerdrTask: () => stopHerdrTask,
|
|
390
|
+
taskTabLabel: () => taskTabLabel,
|
|
391
|
+
waitForHerdrTask: () => waitForHerdrTask
|
|
392
|
+
});
|
|
393
|
+
async function loadHerdr() {
|
|
394
|
+
return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
|
|
395
|
+
}
|
|
396
|
+
function taskTabLabel(projectName, number) {
|
|
397
|
+
return `ctw:${projectName}:#${number}`;
|
|
398
|
+
}
|
|
399
|
+
function createCompletionTracker() {
|
|
400
|
+
return { seenWorking: false, warnedBlocked: false };
|
|
401
|
+
}
|
|
402
|
+
function observeAgentStatus(tracker, status) {
|
|
403
|
+
if (status === "working") {
|
|
404
|
+
return { tracker: { ...tracker, seenWorking: true }, decision: "running" };
|
|
405
|
+
}
|
|
406
|
+
if (status === "blocked") {
|
|
407
|
+
if (tracker.warnedBlocked) return { tracker, decision: "running" };
|
|
408
|
+
return { tracker: { ...tracker, warnedBlocked: true }, decision: "blocked-first-seen" };
|
|
409
|
+
}
|
|
410
|
+
if (status === "idle" && tracker.seenWorking) {
|
|
411
|
+
return { tracker, decision: "completed" };
|
|
412
|
+
}
|
|
413
|
+
return { tracker, decision: "running" };
|
|
414
|
+
}
|
|
415
|
+
function buildHerdrTaskResult(paneOutput) {
|
|
416
|
+
if (paneOutput.trim() === "") {
|
|
417
|
+
return {
|
|
418
|
+
status: "failed",
|
|
419
|
+
output: "[worker] the claude session became idle but its pane produced no output (session aborted before the model ran; e.g. a skill preamble command failed)"
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return { status: "completed", output: paneOutput };
|
|
423
|
+
}
|
|
424
|
+
async function startHerdrTask({
|
|
425
|
+
label,
|
|
426
|
+
cwd,
|
|
427
|
+
argv,
|
|
428
|
+
env,
|
|
429
|
+
workspaceId,
|
|
430
|
+
herdr
|
|
431
|
+
}) {
|
|
432
|
+
const mod = herdr ?? await loadHerdr();
|
|
433
|
+
const { paneId } = await mod.agentStart({ name: label, cwd, argv, env, workspaceId });
|
|
434
|
+
try {
|
|
435
|
+
const { tabId } = await mod.paneMoveToNewTab(paneId, { label, workspaceId });
|
|
436
|
+
return { paneId, tabId };
|
|
437
|
+
} catch (err) {
|
|
438
|
+
await mod.paneClose(paneId).catch(() => {
|
|
439
|
+
});
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function waitForHerdrTask(paneId, options) {
|
|
444
|
+
const mod = options?.herdr ?? await loadHerdr();
|
|
445
|
+
const pollIntervalMs = options?.pollIntervalMs ?? AGENT_POLL_INTERVAL_MS;
|
|
446
|
+
let tracker = createCompletionTracker();
|
|
447
|
+
for (; ; ) {
|
|
448
|
+
if (options?.signal?.aborted) {
|
|
449
|
+
return { status: "failed", output: "[worker] the worker is shutting down; the task was interrupted" };
|
|
450
|
+
}
|
|
451
|
+
let status;
|
|
452
|
+
try {
|
|
453
|
+
status = (await mod.agentGet(paneId)).agentStatus;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
if (err instanceof mod.HerdrError && err.code === "pane_not_found") {
|
|
456
|
+
return {
|
|
457
|
+
status: "failed",
|
|
458
|
+
output: "[worker] the claude pane disappeared before the task completed (claude died or the tab was closed)"
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
console.error(`[herdr-runner] failed to read agent status for pane ${paneId}: ${err}`);
|
|
462
|
+
await sleep(pollIntervalMs);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
options?.onStatus?.(status);
|
|
466
|
+
const observed = observeAgentStatus(tracker, status);
|
|
467
|
+
tracker = observed.tracker;
|
|
468
|
+
if (observed.decision === "completed") {
|
|
469
|
+
const output = await readPaneOutput(paneId, mod);
|
|
470
|
+
return buildHerdrTaskResult(output);
|
|
471
|
+
}
|
|
472
|
+
if (observed.decision === "blocked-first-seen") {
|
|
473
|
+
options?.onBlocked?.();
|
|
474
|
+
}
|
|
475
|
+
await sleep(pollIntervalMs);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function readPaneOutput(paneId, mod) {
|
|
479
|
+
try {
|
|
480
|
+
return await mod.paneRead(paneId, { source: "recent", lines: PANE_OUTPUT_LINES });
|
|
481
|
+
} catch (err) {
|
|
482
|
+
console.error(`[herdr-runner] failed to read pane ${paneId}: ${err}`);
|
|
483
|
+
return "";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function waitForPaneGone(paneId, mod, options) {
|
|
487
|
+
const timeoutMs = options?.timeoutMs ?? CLAUDE_EXIT_TIMEOUT_MS;
|
|
488
|
+
const pollIntervalMs = options?.pollIntervalMs ?? CLAUDE_EXIT_POLL_INTERVAL_MS;
|
|
489
|
+
const deadline = Date.now() + timeoutMs;
|
|
490
|
+
for (; ; ) {
|
|
491
|
+
try {
|
|
492
|
+
await mod.paneGet(paneId);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
if (err instanceof mod.HerdrError && err.code === "pane_not_found") return true;
|
|
495
|
+
}
|
|
496
|
+
if (Date.now() >= deadline) return false;
|
|
497
|
+
await sleep(pollIntervalMs);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function stopHerdrTask(task, herdr, options) {
|
|
501
|
+
const mod = herdr ?? await loadHerdr();
|
|
502
|
+
try {
|
|
503
|
+
await mod.paneSendKeys(task.paneId, "ctrl+c", "ctrl+c");
|
|
504
|
+
const exited = await waitForPaneGone(task.paneId, mod, {
|
|
505
|
+
timeoutMs: options?.exitTimeoutMs,
|
|
506
|
+
pollIntervalMs: options?.exitPollIntervalMs
|
|
507
|
+
});
|
|
508
|
+
if (!exited) {
|
|
509
|
+
console.warn(
|
|
510
|
+
`[herdr-runner] claude did not exit in pane ${task.paneId} after ctrl-c, closing the tab forcefully`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
console.error(`[herdr-runner] failed to send ctrl-c to pane ${task.paneId}: ${err}`);
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
await mod.tabClose(task.tabId);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
if (err instanceof mod.HerdrError && (err.code === "tab_not_found" || err.code === "pane_not_found")) return;
|
|
520
|
+
console.error(`[herdr-runner] failed to close tab ${task.tabId}: ${err}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function sleep(ms) {
|
|
524
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
525
|
+
}
|
|
526
|
+
var AGENT_POLL_INTERVAL_MS, PANE_OUTPUT_LINES, CLAUDE_EXIT_TIMEOUT_MS, CLAUDE_EXIT_POLL_INTERVAL_MS;
|
|
527
|
+
var init_herdr_runner = __esm({
|
|
528
|
+
"src/herdr-runner.ts"() {
|
|
529
|
+
"use strict";
|
|
530
|
+
AGENT_POLL_INTERVAL_MS = 3 * 1e3;
|
|
531
|
+
PANE_OUTPUT_LINES = 300;
|
|
532
|
+
CLAUDE_EXIT_TIMEOUT_MS = 15 * 1e3;
|
|
533
|
+
CLAUDE_EXIT_POLL_INTERVAL_MS = 200;
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
236
537
|
// src/dispatcher.ts
|
|
237
538
|
var dispatcher_exports = {};
|
|
238
539
|
__export(dispatcher_exports, {
|
|
540
|
+
LABEL_PREFIX: () => LABEL_PREFIX,
|
|
239
541
|
PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS,
|
|
240
542
|
PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS,
|
|
241
543
|
POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
|
|
242
544
|
SEND_MAX_ATTEMPTS: () => SEND_MAX_ATTEMPTS,
|
|
243
545
|
SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
|
|
244
546
|
SHUTDOWN_TIMEOUT_MS: () => SHUTDOWN_TIMEOUT_MS,
|
|
245
|
-
TAB_LABEL_PREFIX: () => TAB_LABEL_PREFIX,
|
|
246
547
|
WORKER_STARTUP_POLL_INTERVAL_MS: () => WORKER_STARTUP_POLL_INTERVAL_MS,
|
|
247
548
|
WORKER_STARTUP_TIMEOUT_MS: () => WORKER_STARTUP_TIMEOUT_MS,
|
|
248
549
|
createDispatcherShutdownHandler: () => createDispatcherShutdownHandler,
|
|
@@ -256,21 +557,21 @@ __export(dispatcher_exports, {
|
|
|
256
557
|
runDispatcher: () => runDispatcher,
|
|
257
558
|
shutdownDispatcher: () => shutdownDispatcher,
|
|
258
559
|
startWorkerInPane: () => startWorkerInPane,
|
|
259
|
-
tabLabelFor: () => tabLabelFor,
|
|
260
560
|
waitForPaneReady: () => waitForPaneReady,
|
|
261
|
-
waitForWorkerStartup: () => waitForWorkerStartup
|
|
561
|
+
waitForWorkerStartup: () => waitForWorkerStartup,
|
|
562
|
+
workspaceLabelFor: () => workspaceLabelFor
|
|
262
563
|
});
|
|
263
|
-
async function
|
|
564
|
+
async function loadHerdr2() {
|
|
264
565
|
return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
|
|
265
566
|
}
|
|
266
567
|
async function loadTable() {
|
|
267
568
|
return await Promise.resolve().then(() => (init_table(), table_exports));
|
|
268
569
|
}
|
|
269
|
-
function
|
|
270
|
-
return `${
|
|
570
|
+
function workspaceLabelFor(projectName) {
|
|
571
|
+
return `${LABEL_PREFIX}${projectName}`;
|
|
271
572
|
}
|
|
272
|
-
function
|
|
273
|
-
return new Promise((
|
|
573
|
+
function sleep2(ms) {
|
|
574
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
274
575
|
}
|
|
275
576
|
function isWorkerProcess(process2) {
|
|
276
577
|
return process2.cmdline?.includes("claude-task-worker") ?? false;
|
|
@@ -286,7 +587,7 @@ async function waitForPaneReady(paneId, herdr, options) {
|
|
|
286
587
|
const content = await herdr.paneRead(paneId, { source: "visible" });
|
|
287
588
|
if (content.trim() !== "") return true;
|
|
288
589
|
if (Date.now() >= deadline) return false;
|
|
289
|
-
await
|
|
590
|
+
await sleep2(pollIntervalMs);
|
|
290
591
|
}
|
|
291
592
|
}
|
|
292
593
|
async function waitForWorkerStartup(paneId, herdr, options) {
|
|
@@ -298,7 +599,7 @@ async function waitForWorkerStartup(paneId, herdr, options) {
|
|
|
298
599
|
if (foregroundProcesses.some(isWorkerProcess)) return "started";
|
|
299
600
|
if (foregroundProcesses.length > 0 && !foregroundProcesses.every(isShellProcess)) return "other";
|
|
300
601
|
if (Date.now() >= deadline) return "shell";
|
|
301
|
-
await
|
|
602
|
+
await sleep2(pollIntervalMs);
|
|
302
603
|
}
|
|
303
604
|
}
|
|
304
605
|
async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
|
|
@@ -333,8 +634,8 @@ async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
|
|
|
333
634
|
return false;
|
|
334
635
|
}
|
|
335
636
|
async function runDispatcher(projects, forwardedCommand, timing) {
|
|
336
|
-
const herdr = await
|
|
337
|
-
const { checkHerdrAvailable: checkHerdrAvailable2,
|
|
637
|
+
const herdr = await loadHerdr2();
|
|
638
|
+
const { checkHerdrAvailable: checkHerdrAvailable2, workspaceCreate: workspaceCreate2, workspaceClose: workspaceClose2, workspaceList: workspaceList2, tabRename: tabRename2 } = herdr;
|
|
338
639
|
try {
|
|
339
640
|
await checkHerdrAvailable2();
|
|
340
641
|
} catch (error) {
|
|
@@ -342,20 +643,29 @@ async function runDispatcher(projects, forwardedCommand, timing) {
|
|
|
342
643
|
console.error(`[dispatcher] ${message}`);
|
|
343
644
|
throw error;
|
|
344
645
|
}
|
|
345
|
-
const
|
|
346
|
-
const existingLabels = new Set(
|
|
646
|
+
const existingWorkspaces = await workspaceList2();
|
|
647
|
+
const existingLabels = new Set(existingWorkspaces.map((workspace) => workspace.label));
|
|
347
648
|
const sessions = /* @__PURE__ */ new Map();
|
|
348
649
|
for (const project of projects) {
|
|
349
|
-
|
|
350
|
-
|
|
650
|
+
const label = workspaceLabelFor(project.name);
|
|
651
|
+
if (existingLabels.has(label)) {
|
|
652
|
+
console.warn(`[dispatcher] project "${project.name}" already has a running workspace, skipping`);
|
|
351
653
|
continue;
|
|
352
654
|
}
|
|
353
|
-
let
|
|
655
|
+
let createdWorkspaceId;
|
|
354
656
|
try {
|
|
355
|
-
const {
|
|
356
|
-
|
|
657
|
+
const { workspaceId, tabId, paneId } = await workspaceCreate2({
|
|
658
|
+
label,
|
|
659
|
+
cwd: project.path,
|
|
660
|
+
env: { CTW_PROJECT_NAME: project.name }
|
|
661
|
+
});
|
|
662
|
+
createdWorkspaceId = workspaceId;
|
|
663
|
+
await tabRename2(tabId, label).catch(
|
|
664
|
+
(error) => console.error(`[dispatcher] failed to rename the root tab for project "${project.name}": ${error}`)
|
|
665
|
+
);
|
|
357
666
|
sessions.set(project.name, {
|
|
358
667
|
name: project.name,
|
|
668
|
+
workspaceId,
|
|
359
669
|
tabId,
|
|
360
670
|
paneId,
|
|
361
671
|
startedAt: /* @__PURE__ */ new Date(),
|
|
@@ -370,24 +680,24 @@ async function runDispatcher(projects, forwardedCommand, timing) {
|
|
|
370
680
|
} catch (error) {
|
|
371
681
|
console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
|
|
372
682
|
sessions.delete(project.name);
|
|
373
|
-
if (
|
|
683
|
+
if (createdWorkspaceId !== void 0) {
|
|
374
684
|
try {
|
|
375
|
-
await
|
|
685
|
+
await workspaceClose2(createdWorkspaceId);
|
|
376
686
|
} catch (closeError) {
|
|
377
|
-
console.error(`[dispatcher] failed to close dangling
|
|
687
|
+
console.error(`[dispatcher] failed to close dangling workspace for project "${project.name}": ${closeError}`);
|
|
378
688
|
}
|
|
379
689
|
}
|
|
380
690
|
}
|
|
381
691
|
}
|
|
382
692
|
return sessions;
|
|
383
693
|
}
|
|
384
|
-
async function removeSession(sessions, name, {
|
|
694
|
+
async function removeSession(sessions, name, { closeWorkspace }) {
|
|
385
695
|
const session = sessions.get(name);
|
|
386
696
|
if (!session) return;
|
|
387
697
|
sessions.delete(name);
|
|
388
|
-
if (
|
|
389
|
-
const {
|
|
390
|
-
await
|
|
698
|
+
if (closeWorkspace) {
|
|
699
|
+
const { workspaceClose: workspaceClose2 } = await loadHerdr2();
|
|
700
|
+
await workspaceClose2(session.workspaceId);
|
|
391
701
|
}
|
|
392
702
|
}
|
|
393
703
|
async function pollOnce(sessions, herdr) {
|
|
@@ -395,11 +705,11 @@ async function pollOnce(sessions, herdr) {
|
|
|
395
705
|
try {
|
|
396
706
|
const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
|
|
397
707
|
if (!foregroundProcesses.some(isWorkerProcess)) {
|
|
398
|
-
await removeSession(sessions, name, {
|
|
708
|
+
await removeSession(sessions, name, { closeWorkspace: true });
|
|
399
709
|
}
|
|
400
710
|
} catch (error) {
|
|
401
711
|
if (error instanceof herdr.HerdrError && error.code === "pane_not_found") {
|
|
402
|
-
await removeSession(sessions, name, {
|
|
712
|
+
await removeSession(sessions, name, { closeWorkspace: false });
|
|
403
713
|
continue;
|
|
404
714
|
}
|
|
405
715
|
console.error(`[dispatcher] failed to poll session "${name}": ${error}`);
|
|
@@ -419,27 +729,27 @@ function renderSessionTable(sessions) {
|
|
|
419
729
|
const maxProjectWidth = 20;
|
|
420
730
|
const rows = entries.map((session) => ({
|
|
421
731
|
project: getDisplayWidth2(session.name) > maxProjectWidth ? truncateToWidth2(session.name, maxProjectWidth) : session.name,
|
|
422
|
-
|
|
732
|
+
workspace: session.workspaceId,
|
|
423
733
|
pane: session.paneId,
|
|
424
734
|
status: session.status,
|
|
425
735
|
uptime: formatUptime(session.startedAt, /* @__PURE__ */ new Date())
|
|
426
736
|
}));
|
|
427
737
|
const colWidths = {
|
|
428
738
|
project: Math.max(7, ...rows.map((r) => getDisplayWidth2(r.project))),
|
|
429
|
-
|
|
739
|
+
workspace: Math.max(9, ...rows.map((r) => r.workspace.length)),
|
|
430
740
|
pane: Math.max(4, ...rows.map((r) => r.pane.length)),
|
|
431
741
|
status: Math.max(6, ...rows.map((r) => r.status.length)),
|
|
432
742
|
uptime: Math.max(6, ...rows.map((r) => r.uptime.length))
|
|
433
743
|
};
|
|
434
|
-
const cols = [colWidths.project, colWidths.
|
|
744
|
+
const cols = [colWidths.project, colWidths.workspace, colWidths.pane, colWidths.status, colWidths.uptime];
|
|
435
745
|
const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
|
|
436
|
-
const row = (project,
|
|
746
|
+
const row = (project, workspace, pane, status, uptime) => `\u2502 ${padToWidth2(project, colWidths.project)} \u2502 ${workspace.padEnd(colWidths.workspace)} \u2502 ${pane.padEnd(colWidths.pane)} \u2502 ${status.padEnd(colWidths.status)} \u2502 ${uptime.padEnd(colWidths.uptime)} \u2502`;
|
|
437
747
|
const lines = [];
|
|
438
748
|
lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
|
|
439
|
-
lines.push(row("Project", "
|
|
749
|
+
lines.push(row("Project", "Workspace", "Pane", "Status", "Uptime"));
|
|
440
750
|
lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
|
|
441
751
|
for (const r of rows) {
|
|
442
|
-
lines.push(row(r.project, r.
|
|
752
|
+
lines.push(row(r.project, r.workspace, r.pane, r.status, r.uptime));
|
|
443
753
|
}
|
|
444
754
|
lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
|
|
445
755
|
console.clear();
|
|
@@ -449,8 +759,8 @@ function monitorSessions(sessions, herdr, options) {
|
|
|
449
759
|
const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
450
760
|
const renderIntervalMs = options?.renderIntervalMs ?? 1e3;
|
|
451
761
|
let resolveDone;
|
|
452
|
-
const done = new Promise((
|
|
453
|
-
resolveDone =
|
|
762
|
+
const done = new Promise((resolve3) => {
|
|
763
|
+
resolveDone = resolve3;
|
|
454
764
|
});
|
|
455
765
|
let settled = false;
|
|
456
766
|
const finish = () => {
|
|
@@ -492,24 +802,24 @@ async function waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, timeoutMs
|
|
|
492
802
|
await pollOnce(sessions, herdr);
|
|
493
803
|
if (sessions.size === 0) return true;
|
|
494
804
|
if (Date.now() >= deadline) return false;
|
|
495
|
-
await new Promise((
|
|
805
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollIntervalMs));
|
|
496
806
|
}
|
|
497
807
|
return true;
|
|
498
808
|
}
|
|
499
|
-
async function
|
|
809
|
+
async function closeRemainingWorkspaces(sessions, herdr, timeoutMs) {
|
|
500
810
|
const results = await Promise.all(
|
|
501
811
|
[...sessions.values()].map(async (session) => {
|
|
502
812
|
try {
|
|
503
813
|
await Promise.race([
|
|
504
|
-
herdr.
|
|
814
|
+
herdr.workspaceClose(session.workspaceId),
|
|
505
815
|
new Promise((_, reject) => {
|
|
506
|
-
setTimeout(() => reject(new Error("
|
|
816
|
+
setTimeout(() => reject(new Error("workspaceClose timed out")), timeoutMs).unref();
|
|
507
817
|
})
|
|
508
818
|
]);
|
|
509
819
|
return true;
|
|
510
820
|
} catch (error) {
|
|
511
821
|
console.error(
|
|
512
|
-
`[dispatcher] failed to close
|
|
822
|
+
`[dispatcher] failed to close workspace "${session.workspaceId}" for session "${session.name}" during shutdown: ${error}`
|
|
513
823
|
);
|
|
514
824
|
return false;
|
|
515
825
|
}
|
|
@@ -517,29 +827,29 @@ async function closeRemainingTabs(sessions, herdr, timeoutMs) {
|
|
|
517
827
|
);
|
|
518
828
|
return results.every((closed) => closed);
|
|
519
829
|
}
|
|
520
|
-
async function forceKillAllSessions(sessions, herdr,
|
|
830
|
+
async function forceKillAllSessions(sessions, herdr, workspaceCloseTimeoutMs) {
|
|
521
831
|
console.log(`[dispatcher] force-kill requested, sending ctrl-c and closing ${sessions.size} session(s) immediately`);
|
|
522
832
|
await sendCtrlCToAllSessions(sessions, herdr);
|
|
523
|
-
await
|
|
833
|
+
await closeRemainingWorkspaces(sessions, herdr, workspaceCloseTimeoutMs);
|
|
524
834
|
process.exit(1);
|
|
525
835
|
}
|
|
526
836
|
async function shutdownDispatcher(sessions, monitorHandle, options) {
|
|
527
837
|
if (shutdownPromise) {
|
|
528
838
|
if (options?.forceKill) {
|
|
529
|
-
const herdr = options.herdr ?? await
|
|
530
|
-
const
|
|
531
|
-
await forceKillAllSessions(sessions, herdr,
|
|
839
|
+
const herdr = options.herdr ?? await loadHerdr2();
|
|
840
|
+
const workspaceCloseTimeoutMs = options.workspaceCloseTimeoutMs ?? 5e3;
|
|
841
|
+
await forceKillAllSessions(sessions, herdr, workspaceCloseTimeoutMs);
|
|
532
842
|
return;
|
|
533
843
|
}
|
|
534
844
|
console.log("[dispatcher] shutdown already in progress, ignoring duplicate request");
|
|
535
845
|
return shutdownPromise;
|
|
536
846
|
}
|
|
537
847
|
shutdownPromise = (async () => {
|
|
538
|
-
const herdr = options?.herdr ?? await
|
|
848
|
+
const herdr = options?.herdr ?? await loadHerdr2();
|
|
539
849
|
const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
540
850
|
const shutdownTimeoutMs = options?.shutdownTimeoutMs ?? SHUTDOWN_TIMEOUT_MS;
|
|
541
851
|
const retryTimeoutMs = options?.retryTimeoutMs ?? SHUTDOWN_RETRY_TIMEOUT_MS;
|
|
542
|
-
const
|
|
852
|
+
const workspaceCloseTimeoutMs = options?.workspaceCloseTimeoutMs ?? 5e3;
|
|
543
853
|
if (monitorHandle) {
|
|
544
854
|
monitorHandle.stop();
|
|
545
855
|
await monitorHandle.done;
|
|
@@ -554,8 +864,8 @@ async function shutdownDispatcher(sessions, monitorHandle, options) {
|
|
|
554
864
|
await sendCtrlCToAllSessions(sessions, herdr);
|
|
555
865
|
finishedInTime = await waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, retryTimeoutMs);
|
|
556
866
|
}
|
|
557
|
-
const
|
|
558
|
-
process.exit(finishedInTime &&
|
|
867
|
+
const allWorkspacesClosed = await closeRemainingWorkspaces(sessions, herdr, workspaceCloseTimeoutMs);
|
|
868
|
+
process.exit(finishedInTime && allWorkspacesClosed ? 0 : 1);
|
|
559
869
|
})();
|
|
560
870
|
try {
|
|
561
871
|
await shutdownPromise;
|
|
@@ -590,7 +900,7 @@ function createDispatcherShutdownHandler(shutdown2) {
|
|
|
590
900
|
};
|
|
591
901
|
return { handle, isShuttingDown: () => shuttingDown2 };
|
|
592
902
|
}
|
|
593
|
-
var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, PANE_READY_TIMEOUT_MS, PANE_READY_POLL_INTERVAL_MS, WORKER_STARTUP_TIMEOUT_MS, WORKER_STARTUP_POLL_INTERVAL_MS, SEND_MAX_ATTEMPTS,
|
|
903
|
+
var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, PANE_READY_TIMEOUT_MS, PANE_READY_POLL_INTERVAL_MS, WORKER_STARTUP_TIMEOUT_MS, WORKER_STARTUP_POLL_INTERVAL_MS, SEND_MAX_ATTEMPTS, LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
|
|
594
904
|
var init_dispatcher = __esm({
|
|
595
905
|
async "src/dispatcher.ts"() {
|
|
596
906
|
"use strict";
|
|
@@ -602,7 +912,7 @@ var init_dispatcher = __esm({
|
|
|
602
912
|
WORKER_STARTUP_TIMEOUT_MS = 30 * 1e3;
|
|
603
913
|
WORKER_STARTUP_POLL_INTERVAL_MS = 500;
|
|
604
914
|
SEND_MAX_ATTEMPTS = 3;
|
|
605
|
-
|
|
915
|
+
LABEL_PREFIX = "ctw:";
|
|
606
916
|
SHELL_NAME_PATTERN = /^-?(zsh|bash|sh)$/;
|
|
607
917
|
SHUTDOWN_RETRY_TIMEOUT_MS = 90 * 1e3;
|
|
608
918
|
CTRL_C_KEY = "ctrl+c";
|
|
@@ -613,29 +923,29 @@ var init_dispatcher = __esm({
|
|
|
613
923
|
import { createRequire } from "node:module";
|
|
614
924
|
var childProcess = createRequire(import.meta.url)("node:child_process");
|
|
615
925
|
function execGh(args) {
|
|
616
|
-
return new Promise((
|
|
926
|
+
return new Promise((resolve3, reject) => {
|
|
617
927
|
childProcess.execFile("gh", args, (error, stdout, stderr) => {
|
|
618
928
|
if (error) {
|
|
619
929
|
reject(new Error(`gh ${args.join(" ")} failed: ${stderr || error.message}`));
|
|
620
930
|
return;
|
|
621
931
|
}
|
|
622
|
-
|
|
932
|
+
resolve3(stdout.trim());
|
|
623
933
|
});
|
|
624
934
|
});
|
|
625
935
|
}
|
|
626
936
|
function execGhAllowExit(args, allowedCodes) {
|
|
627
|
-
return new Promise((
|
|
937
|
+
return new Promise((resolve3, reject) => {
|
|
628
938
|
childProcess.execFile("gh", args, (error, stdout, stderr) => {
|
|
629
939
|
if (error) {
|
|
630
940
|
const code = error.code;
|
|
631
941
|
if (typeof code === "number" && allowedCodes.includes(code)) {
|
|
632
|
-
|
|
942
|
+
resolve3(stdout.trim());
|
|
633
943
|
return;
|
|
634
944
|
}
|
|
635
945
|
reject(new Error(`gh ${args.join(" ")} failed: ${stderr || error.message}`));
|
|
636
946
|
return;
|
|
637
947
|
}
|
|
638
|
-
|
|
948
|
+
resolve3(stdout.trim());
|
|
639
949
|
});
|
|
640
950
|
});
|
|
641
951
|
}
|
|
@@ -820,7 +1130,7 @@ async function withRetry(fn, maxRetries = 5, baseDelayMs = 1e3) {
|
|
|
820
1130
|
if (attempt === maxRetries) throw err;
|
|
821
1131
|
const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
|
|
822
1132
|
console.error(`[gh] Attempt ${attempt}/${maxRetries} failed, retrying in ${delayMs}ms...`);
|
|
823
|
-
await new Promise((
|
|
1133
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
824
1134
|
}
|
|
825
1135
|
}
|
|
826
1136
|
throw new Error("unreachable");
|
|
@@ -899,13 +1209,38 @@ var CLAUDE_SPAWN_ENV = {
|
|
|
899
1209
|
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1",
|
|
900
1210
|
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: "0"
|
|
901
1211
|
};
|
|
902
|
-
var SYSTEM_PROMPT = `\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F \`claude-task-worker\` \u306E\u30EF\u30FC\u30AB\u30FC\u304B\u3089
|
|
903
|
-
var SUBAGENT_SYSTEM_PROMPT = `\u3042\u306A\u305F\u306F claude-task-worker \u306E\u30EF\u30FC\u30AB\u30FC\u304C \`claude -p\`\uFF08\u975E\u5BFE\u8A71 print \u30E2\u30FC\u30C9\uFF09\u3067\u81EA\u52D5\u8D77\u52D5\u3057\u305F\u30BB\u30C3\u30B7\u30E7\u30F3\u5185\u306E\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u3067\u3059\u3002\u4EE5\u4E0B\u306E\u81EA\u5F8B\u5B9F\u884C\u539F\u5247\u3092\u5FC5\u305A\u9075\u5B88\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
1212
|
+
var SYSTEM_PROMPT = `\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F \`claude-task-worker\` \u306E\u30EF\u30FC\u30AB\u30FC\u304B\u3089\u81EA\u52D5\u8D77\u52D5\u3055\u308C\u3066\u3044\u308B\uFF08\u5FDC\u7B54\u3067\u304D\u308B\u30E6\u30FC\u30B6\u30FC\u306F\u5E38\u99D0\u3057\u3066\u3044\u306A\u3044\uFF09\u3002\u4EE5\u4E0B\u306E\u81EA\u5F8B\u5B9F\u884C\u539F\u5247\u3092\u5FC5\u305A\u9075\u5B88\u3059\u308B\u3053\u3068\u3002
|
|
904
1213
|
|
|
905
|
-
- \u30E6\u30FC\u30B6\u30FC\u3078\u306E\u78BA\u8A8D\u30FB\u8CEA\u554F\u306F\u884C\u308F\
|
|
1214
|
+
- \u30E6\u30FC\u30B6\u30FC\u3078\u306E\u78BA\u8A8D\u30FB\u8CEA\u554F\u306F\u884C\u308F\u305A\u3001\u8D77\u52D5\u3055\u308C\u305F\u30B9\u30AD\u30EB\u306E\u30EB\u30FC\u30EB\u306B\u5F93\u3063\u3066\u81EA\u5F8B\u7684\u306B\u5224\u65AD\u3059\u308B
|
|
906
1215
|
- \u66D6\u6627\u306A\u5834\u5408\u306F\u300C\u3088\u308A\u5B89\u5168\u306A\u5074\uFF08\u7834\u58CA\u7684\u3067\u306A\u3044\u5074\uFF09\u300D\u3092\u9078\u629E\u3057\u3001\u305D\u306E\u5224\u65AD\u3068\u6839\u62E0\u3092\u6700\u7D42\u5831\u544A\u306B\u660E\u8A18\u3059\u308B
|
|
907
|
-
- \
|
|
908
|
-
- \
|
|
1216
|
+
- \u5168\u30B9\u30C6\u30C3\u30D7\u3092\u5B8C\u9042\u3057\u3066\u304B\u3089\u7D42\u4E86\u3059\u308B\uFF08\u30B9\u30AD\u30EB\u306B\u5B9A\u7FA9\u3055\u308C\u305F\u4E2D\u65AD\u6761\u4EF6\u306B\u8A72\u5F53\u3057\u305F\u5834\u5408\u306E\u307F\u3001\u7406\u7531\u3092\u51FA\u529B\u3057\u3066\u7D42\u4E86\u3059\u308B\uFF09
|
|
1217
|
+
- \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u3078\u4F5C\u696D\u3092\u59D4\u8B72\u3059\u308B\u5834\u5408\u306F\u3001\u4E0A\u8A18\u306E\u539F\u5247\u3092\u59D4\u8B72\u30D7\u30ED\u30F3\u30D7\u30C8\u306B\u3082\u660E\u8A18\u3057\u3066\u4F1D\u3048\u308B
|
|
1218
|
+
- \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u306E\u5B8C\u4E86\u5831\u544A\u306F\u9D5C\u5451\u307F\u306B\u3057\u306A\u3044\u3002\`git diff\` \u7B49\u3067\u5B9F\u969B\u306E\u6210\u679C\u7269\u3092\u691C\u8A3C\u3057\u3066\u304B\u3089\u5B8C\u4E86\u6271\u3044\u306B\u3059\u308B`;
|
|
1219
|
+
function buildClaudeArgs({ mode, prompt, model, effort }) {
|
|
1220
|
+
return [
|
|
1221
|
+
...mode === "herdr" ? [] : ["-p"],
|
|
1222
|
+
prompt,
|
|
1223
|
+
"--dangerously-skip-permissions",
|
|
1224
|
+
"--chrome",
|
|
1225
|
+
"--disallowedTools",
|
|
1226
|
+
DISALLOWED_TOOLS_ARG,
|
|
1227
|
+
"--append-system-prompt",
|
|
1228
|
+
SYSTEM_PROMPT,
|
|
1229
|
+
"--model",
|
|
1230
|
+
model,
|
|
1231
|
+
"--effort",
|
|
1232
|
+
effort
|
|
1233
|
+
];
|
|
1234
|
+
}
|
|
1235
|
+
function buildClaudeEnv(mode) {
|
|
1236
|
+
if (mode === "herdr") {
|
|
1237
|
+
return {
|
|
1238
|
+
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: CLAUDE_SPAWN_ENV.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS,
|
|
1239
|
+
HERDR_DISABLE_SOUND: "1"
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
return { ...CLAUDE_SPAWN_ENV };
|
|
1243
|
+
}
|
|
909
1244
|
|
|
910
1245
|
// src/config.ts
|
|
911
1246
|
import { readFileSync } from "node:fs";
|
|
@@ -1150,6 +1485,7 @@ async function assertRemoteTrackingExists(epicBranch) {
|
|
|
1150
1485
|
|
|
1151
1486
|
// src/process-manager.ts
|
|
1152
1487
|
import { spawn } from "node:child_process";
|
|
1488
|
+
import { basename } from "node:path";
|
|
1153
1489
|
init_table();
|
|
1154
1490
|
|
|
1155
1491
|
// src/task-result.ts
|
|
@@ -1171,8 +1507,200 @@ function buildTaskResult(code, stdout, stderrTail) {
|
|
|
1171
1507
|
return { status: completed ? "completed" : "failed", output };
|
|
1172
1508
|
}
|
|
1173
1509
|
|
|
1510
|
+
// src/user-config.ts
|
|
1511
|
+
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
1512
|
+
import { homedir } from "node:os";
|
|
1513
|
+
import { isAbsolute, join as join2, resolve } from "node:path";
|
|
1514
|
+
var RESERVED_ALL = "all";
|
|
1515
|
+
var DEFAULT_RUN_MODE = "default";
|
|
1516
|
+
var UserConfigError = class extends Error {
|
|
1517
|
+
constructor(message) {
|
|
1518
|
+
super(message);
|
|
1519
|
+
this.name = "UserConfigError";
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
function getConfigDir() {
|
|
1523
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
1524
|
+
const configHome = xdg && xdg.length > 0 ? xdg : join2(homedir(), ".config");
|
|
1525
|
+
return join2(configHome, "claude-task-worker");
|
|
1526
|
+
}
|
|
1527
|
+
function getUserConfigPath() {
|
|
1528
|
+
return join2(getConfigDir(), "config.json");
|
|
1529
|
+
}
|
|
1530
|
+
function isDirectory(path) {
|
|
1531
|
+
try {
|
|
1532
|
+
return statSync(path).isDirectory();
|
|
1533
|
+
} catch {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function parseConfigFile(path) {
|
|
1538
|
+
let content;
|
|
1539
|
+
try {
|
|
1540
|
+
content = readFileSync2(path, "utf-8");
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
if (err.code === "ENOENT") return void 0;
|
|
1543
|
+
throw err;
|
|
1544
|
+
}
|
|
1545
|
+
try {
|
|
1546
|
+
return JSON.parse(content);
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
if (err instanceof SyntaxError) {
|
|
1549
|
+
throw new UserConfigError(`config file contains invalid JSON: ${path}: ${err.message}`);
|
|
1550
|
+
}
|
|
1551
|
+
throw err;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function readRawConfig() {
|
|
1555
|
+
return parseConfigFile(getUserConfigPath());
|
|
1556
|
+
}
|
|
1557
|
+
function parseMode(raw, path) {
|
|
1558
|
+
if (!("mode" in raw)) return DEFAULT_RUN_MODE;
|
|
1559
|
+
const value = raw["mode"];
|
|
1560
|
+
if (value === "default" || value === "herdr") return value;
|
|
1561
|
+
console.warn(`[config] invalid mode: ${JSON.stringify(value)} in ${path}, using "${DEFAULT_RUN_MODE}"`);
|
|
1562
|
+
return DEFAULT_RUN_MODE;
|
|
1563
|
+
}
|
|
1564
|
+
function loadUserConfig() {
|
|
1565
|
+
const path = getUserConfigPath();
|
|
1566
|
+
const raw = readRawConfig();
|
|
1567
|
+
if (raw === void 0) {
|
|
1568
|
+
throw new UserConfigError(`config.json not found: ${path}`);
|
|
1569
|
+
}
|
|
1570
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1571
|
+
throw new UserConfigError(`config.json must contain a JSON object: ${path}`);
|
|
1572
|
+
}
|
|
1573
|
+
if (!("projects" in raw) || typeof raw["projects"] !== "object" || raw["projects"] === null || Array.isArray(raw["projects"])) {
|
|
1574
|
+
throw new UserConfigError(`config.json must contain a "projects" section as an object: ${path}`);
|
|
1575
|
+
}
|
|
1576
|
+
if ("projectGroups" in raw && (typeof raw["projectGroups"] !== "object" || raw["projectGroups"] === null || Array.isArray(raw["projectGroups"]))) {
|
|
1577
|
+
throw new UserConfigError(`config.json "projectGroups" must be an object: ${path}`);
|
|
1578
|
+
}
|
|
1579
|
+
const mode = parseMode(raw, path);
|
|
1580
|
+
const rawProjects = raw["projects"];
|
|
1581
|
+
const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
|
|
1582
|
+
const projectKeys = Object.keys(rawProjects);
|
|
1583
|
+
const groupKeys = Object.keys(rawProjectGroups);
|
|
1584
|
+
if (projectKeys.includes(RESERVED_ALL) || groupKeys.includes(RESERVED_ALL)) {
|
|
1585
|
+
throw new UserConfigError(
|
|
1586
|
+
`"${RESERVED_ALL}" is a reserved word and cannot be used as a key in "projects" or "projectGroups"`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const groupKeySet = new Set(groupKeys);
|
|
1590
|
+
const duplicateKeys = projectKeys.filter((key) => groupKeySet.has(key));
|
|
1591
|
+
if (duplicateKeys.length > 0) {
|
|
1592
|
+
throw new UserConfigError(
|
|
1593
|
+
`"projects" and "projectGroups" share the same key namespace; duplicate keys are not allowed: ${duplicateKeys.join(", ")}`
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
const projects = {};
|
|
1597
|
+
for (const [name, value] of Object.entries(rawProjects)) {
|
|
1598
|
+
if (name === "__proto__") {
|
|
1599
|
+
throw new UserConfigError(`"__proto__" cannot be used as a key in "projects": ${path}`);
|
|
1600
|
+
}
|
|
1601
|
+
if (typeof value !== "string" || !isAbsolute(value)) {
|
|
1602
|
+
console.warn(`[config] invalid projects.${name}: expected an absolute path, skipping`);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
if (!isDirectory(value)) {
|
|
1606
|
+
console.warn(`[config] projects.${name} does not exist as a directory: ${value}, skipping`);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
projects[name] = value;
|
|
1610
|
+
}
|
|
1611
|
+
const projectGroups = {};
|
|
1612
|
+
for (const [groupName, value] of Object.entries(rawProjectGroups)) {
|
|
1613
|
+
if (groupName === "__proto__") {
|
|
1614
|
+
throw new UserConfigError(`"__proto__" cannot be used as a key in "projectGroups": ${path}`);
|
|
1615
|
+
}
|
|
1616
|
+
if (!Array.isArray(value)) {
|
|
1617
|
+
console.warn(`[config] invalid projectGroups.${groupName}: expected an array, skipping`);
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const members = [];
|
|
1621
|
+
for (const member of value) {
|
|
1622
|
+
if (typeof member !== "string" || !Object.prototype.hasOwnProperty.call(projects, member)) {
|
|
1623
|
+
console.warn(`[config] projectGroups.${groupName} references unknown project "${String(member)}", skipping`);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
members.push(member);
|
|
1627
|
+
}
|
|
1628
|
+
projectGroups[groupName] = members;
|
|
1629
|
+
}
|
|
1630
|
+
return { mode, projects, projectGroups };
|
|
1631
|
+
}
|
|
1632
|
+
var cachedRunMode;
|
|
1633
|
+
function getRunMode() {
|
|
1634
|
+
if (cachedRunMode === void 0) {
|
|
1635
|
+
cachedRunMode = readRunMode();
|
|
1636
|
+
}
|
|
1637
|
+
return cachedRunMode;
|
|
1638
|
+
}
|
|
1639
|
+
function readRunMode() {
|
|
1640
|
+
let raw;
|
|
1641
|
+
try {
|
|
1642
|
+
raw = readRawConfig();
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
console.warn(`[config] failed to read config file, using "${DEFAULT_RUN_MODE}" mode: ${err}`);
|
|
1645
|
+
return DEFAULT_RUN_MODE;
|
|
1646
|
+
}
|
|
1647
|
+
if (raw === void 0) return DEFAULT_RUN_MODE;
|
|
1648
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1649
|
+
return DEFAULT_RUN_MODE;
|
|
1650
|
+
}
|
|
1651
|
+
return parseMode(raw, getUserConfigPath());
|
|
1652
|
+
}
|
|
1653
|
+
function findProjectNameByPath(path) {
|
|
1654
|
+
let config;
|
|
1655
|
+
try {
|
|
1656
|
+
config = loadUserConfig();
|
|
1657
|
+
} catch {
|
|
1658
|
+
return void 0;
|
|
1659
|
+
}
|
|
1660
|
+
const target = resolve(path);
|
|
1661
|
+
for (const [name, projectPath] of Object.entries(config.projects)) {
|
|
1662
|
+
if (resolve(projectPath) === target) return name;
|
|
1663
|
+
}
|
|
1664
|
+
return void 0;
|
|
1665
|
+
}
|
|
1666
|
+
function resolveTargetProjects(requested, config) {
|
|
1667
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1668
|
+
for (const name of requested) {
|
|
1669
|
+
if (name === RESERVED_ALL) {
|
|
1670
|
+
for (const [projectName, projectPath] of Object.entries(config.projects)) {
|
|
1671
|
+
resolved.set(projectName, { name: projectName, path: projectPath });
|
|
1672
|
+
}
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (Object.prototype.hasOwnProperty.call(config.projects, name)) {
|
|
1676
|
+
resolved.set(name, { name, path: config.projects[name] });
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (Object.prototype.hasOwnProperty.call(config.projectGroups, name)) {
|
|
1680
|
+
for (const projectName of config.projectGroups[name]) {
|
|
1681
|
+
const projectPath = config.projects[projectName];
|
|
1682
|
+
if (projectPath === void 0) continue;
|
|
1683
|
+
resolved.set(projectName, { name: projectName, path: projectPath });
|
|
1684
|
+
}
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const availableProjects = Object.keys(config.projects).join(", ") || "(none)";
|
|
1688
|
+
const availableGroups = Object.keys(config.projectGroups).join(", ") || "(none)";
|
|
1689
|
+
throw new UserConfigError(
|
|
1690
|
+
`Unknown project or group: "${name}". Available projects: ${availableProjects}. Available groups: ${availableGroups}.`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
const resolvedProjects = Array.from(resolved.values());
|
|
1694
|
+
if (resolvedProjects.length === 0) {
|
|
1695
|
+
throw new UserConfigError(`No projects resolved from requested targets: ${requested.join(", ")}`);
|
|
1696
|
+
}
|
|
1697
|
+
return resolvedProjects;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1174
1700
|
// src/process-manager.ts
|
|
1175
1701
|
var childProcesses = /* @__PURE__ */ new Map();
|
|
1702
|
+
var herdrTasks = /* @__PURE__ */ new Map();
|
|
1703
|
+
var herdrAbortSignal = { aborted: false };
|
|
1176
1704
|
var tasks = /* @__PURE__ */ new Map();
|
|
1177
1705
|
var shuttingDown = false;
|
|
1178
1706
|
function setShuttingDown() {
|
|
@@ -1228,7 +1756,9 @@ function renderTable() {
|
|
|
1228
1756
|
title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
|
|
1229
1757
|
worker: t.workerName,
|
|
1230
1758
|
path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
|
|
1231
|
-
|
|
1759
|
+
// herdr モードでは agent ステータス(working / blocked 等)を併記し、
|
|
1760
|
+
// 人の介入が必要な blocked にも気づけるようにする。
|
|
1761
|
+
status: t.agentStatus ? `${t.status}:${t.agentStatus}` : t.status,
|
|
1232
1762
|
time: formatTime(t.startedAt),
|
|
1233
1763
|
duration: formatDuration(t.startedAt)
|
|
1234
1764
|
})),
|
|
@@ -1287,6 +1817,65 @@ function ensureRenderInterval() {
|
|
|
1287
1817
|
renderInterval = setInterval(renderTable, 1e3);
|
|
1288
1818
|
renderInterval.unref();
|
|
1289
1819
|
}
|
|
1820
|
+
async function finishTask(id, result, onComplete) {
|
|
1821
|
+
try {
|
|
1822
|
+
await Promise.race([
|
|
1823
|
+
onComplete?.(result.status, result.output) ?? Promise.resolve(),
|
|
1824
|
+
new Promise(
|
|
1825
|
+
(_, reject) => setTimeout(() => reject(new Error("onComplete timed out after 120s")), 12e4).unref()
|
|
1826
|
+
)
|
|
1827
|
+
]);
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
console.error(`[worker] onComplete error for #${id}: ${err}`);
|
|
1830
|
+
}
|
|
1831
|
+
const task = tasks.get(id);
|
|
1832
|
+
if (task) {
|
|
1833
|
+
task.status = result.status;
|
|
1834
|
+
task.finishedAt = /* @__PURE__ */ new Date();
|
|
1835
|
+
task.agentStatus = void 0;
|
|
1836
|
+
}
|
|
1837
|
+
renderTable();
|
|
1838
|
+
}
|
|
1839
|
+
function resolveProjectName(cwd = process.cwd()) {
|
|
1840
|
+
const injected = process.env.CTW_PROJECT_NAME;
|
|
1841
|
+
if (injected && injected.length > 0) return injected;
|
|
1842
|
+
return findProjectNameByPath(cwd) ?? basename(cwd);
|
|
1843
|
+
}
|
|
1844
|
+
async function runViaHerdr(command, args, id, onComplete, cwd, env) {
|
|
1845
|
+
const { startHerdrTask: startHerdrTask2, stopHerdrTask: stopHerdrTask2, taskTabLabel: taskTabLabel2, waitForHerdrTask: waitForHerdrTask2 } = await Promise.resolve().then(() => (init_herdr_runner(), herdr_runner_exports));
|
|
1846
|
+
const { getCurrentWorkspaceId: getCurrentWorkspaceId2 } = await Promise.resolve().then(() => (init_herdr(), herdr_exports));
|
|
1847
|
+
const label = taskTabLabel2(resolveProjectName(), id);
|
|
1848
|
+
let task;
|
|
1849
|
+
let result;
|
|
1850
|
+
herdrTasks.set(id, { paneId: "", tabId: "" });
|
|
1851
|
+
try {
|
|
1852
|
+
task = await startHerdrTask2({
|
|
1853
|
+
label,
|
|
1854
|
+
cwd: cwd ?? process.cwd(),
|
|
1855
|
+
argv: [command, ...args],
|
|
1856
|
+
env,
|
|
1857
|
+
workspaceId: getCurrentWorkspaceId2()
|
|
1858
|
+
});
|
|
1859
|
+
herdrTasks.set(id, task);
|
|
1860
|
+
result = await waitForHerdrTask2(task.paneId, {
|
|
1861
|
+
signal: herdrAbortSignal,
|
|
1862
|
+
onBlocked: () => console.warn(`[worker] #${id} is blocked and waiting for input in herdr tab "${label}"`),
|
|
1863
|
+
onStatus: (status) => {
|
|
1864
|
+
const task2 = tasks.get(id);
|
|
1865
|
+
if (task2 && task2.status === "running") task2.agentStatus = status;
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
console.error(`[worker] failed to run #${id} via herdr: ${err}`);
|
|
1870
|
+
result = { status: "failed", output: `[worker] failed to run the task via herdr: ${err}` };
|
|
1871
|
+
} finally {
|
|
1872
|
+
if (task) {
|
|
1873
|
+
await stopHerdrTask2(task);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
await finishTask(id, result, onComplete);
|
|
1877
|
+
herdrTasks.delete(id);
|
|
1878
|
+
}
|
|
1290
1879
|
function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
|
|
1291
1880
|
tasks.set(id, {
|
|
1292
1881
|
id,
|
|
@@ -1298,6 +1887,10 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
|
|
|
1298
1887
|
});
|
|
1299
1888
|
ensureRenderInterval();
|
|
1300
1889
|
renderTable();
|
|
1890
|
+
if (getRunMode() === "herdr") {
|
|
1891
|
+
void runViaHerdr(command, args, id, onComplete, cwd, env);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1301
1894
|
const child = spawn(command, args, {
|
|
1302
1895
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1303
1896
|
detached: true,
|
|
@@ -1320,55 +1913,25 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
|
|
|
1320
1913
|
}
|
|
1321
1914
|
});
|
|
1322
1915
|
child.on("close", async (code) => {
|
|
1323
|
-
const
|
|
1916
|
+
const result = buildTaskResult(
|
|
1324
1917
|
code,
|
|
1325
1918
|
Buffer.concat(outputChunks).toString("utf-8"),
|
|
1326
1919
|
Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
|
|
1327
1920
|
);
|
|
1328
|
-
|
|
1329
|
-
await Promise.race([
|
|
1330
|
-
onComplete?.(finalStatus, output) ?? Promise.resolve(),
|
|
1331
|
-
new Promise(
|
|
1332
|
-
(_, reject) => setTimeout(() => reject(new Error("onComplete timed out after 120s")), 12e4).unref()
|
|
1333
|
-
)
|
|
1334
|
-
]);
|
|
1335
|
-
} catch (err) {
|
|
1336
|
-
console.error(`[worker] onComplete error for #${id}: ${err}`);
|
|
1337
|
-
}
|
|
1338
|
-
const task = tasks.get(id);
|
|
1339
|
-
if (task) {
|
|
1340
|
-
task.status = finalStatus;
|
|
1341
|
-
task.finishedAt = /* @__PURE__ */ new Date();
|
|
1342
|
-
}
|
|
1921
|
+
await finishTask(id, result, onComplete);
|
|
1343
1922
|
childProcesses.delete(id);
|
|
1344
|
-
renderTable();
|
|
1345
1923
|
});
|
|
1346
1924
|
child.on("error", async (err) => {
|
|
1347
1925
|
console.error(`[worker] failed to spawn process for #${id}: ${err.message}`);
|
|
1348
|
-
|
|
1349
|
-
await Promise.race([
|
|
1350
|
-
onComplete?.("failed", err.message) ?? Promise.resolve(),
|
|
1351
|
-
new Promise(
|
|
1352
|
-
(_, reject) => setTimeout(() => reject(new Error("onComplete timed out after 120s")), 12e4).unref()
|
|
1353
|
-
)
|
|
1354
|
-
]);
|
|
1355
|
-
} catch (callbackErr) {
|
|
1356
|
-
console.error(`[worker] onComplete error for #${id}: ${callbackErr}`);
|
|
1357
|
-
}
|
|
1358
|
-
const task = tasks.get(id);
|
|
1359
|
-
if (task) {
|
|
1360
|
-
task.status = "failed";
|
|
1361
|
-
task.finishedAt = /* @__PURE__ */ new Date();
|
|
1362
|
-
}
|
|
1926
|
+
await finishTask(id, { status: "failed", output: err.message }, onComplete);
|
|
1363
1927
|
childProcesses.delete(id);
|
|
1364
|
-
renderTable();
|
|
1365
1928
|
});
|
|
1366
1929
|
}
|
|
1367
1930
|
function waitForAllProcesses() {
|
|
1368
|
-
return new Promise((
|
|
1931
|
+
return new Promise((resolve3) => {
|
|
1369
1932
|
const check = () => {
|
|
1370
|
-
if (childProcesses.size === 0) {
|
|
1371
|
-
|
|
1933
|
+
if (childProcesses.size === 0 && herdrTasks.size === 0) {
|
|
1934
|
+
resolve3();
|
|
1372
1935
|
} else {
|
|
1373
1936
|
setTimeout(check, 500);
|
|
1374
1937
|
}
|
|
@@ -1388,6 +1951,9 @@ function shutdown(signal = "SIGTERM") {
|
|
|
1388
1951
|
}
|
|
1389
1952
|
}
|
|
1390
1953
|
}
|
|
1954
|
+
if (herdrTasks.size > 0) {
|
|
1955
|
+
herdrAbortSignal.aborted = true;
|
|
1956
|
+
}
|
|
1391
1957
|
}
|
|
1392
1958
|
|
|
1393
1959
|
// src/random-name.ts
|
|
@@ -1639,16 +2205,16 @@ function isGeneratedWorktreeName(name) {
|
|
|
1639
2205
|
|
|
1640
2206
|
// src/slack.ts
|
|
1641
2207
|
import { exec } from "node:child_process";
|
|
1642
|
-
import { readFileSync as
|
|
1643
|
-
import { homedir as
|
|
1644
|
-
import { join as
|
|
2208
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2209
|
+
import { homedir as homedir3 } from "node:os";
|
|
2210
|
+
import { join as join4 } from "node:path";
|
|
1645
2211
|
import { promisify as promisify2 } from "node:util";
|
|
1646
2212
|
|
|
1647
2213
|
// src/runcat.ts
|
|
1648
2214
|
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1649
|
-
import { homedir } from "node:os";
|
|
1650
|
-
import { dirname, join as
|
|
1651
|
-
var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ??
|
|
2215
|
+
import { homedir as homedir2 } from "node:os";
|
|
2216
|
+
import { dirname, join as join3 } from "node:path";
|
|
2217
|
+
var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join3(process.env.HOME ?? homedir2(), ".claude", "runcat-usage.json");
|
|
1652
2218
|
var JST = "Asia/Tokyo";
|
|
1653
2219
|
function formatG(value) {
|
|
1654
2220
|
return String(Number(value.toPrecision(6)));
|
|
@@ -1712,7 +2278,7 @@ function buildRuncatSnapshot(usage, now = /* @__PURE__ */ new Date()) {
|
|
|
1712
2278
|
function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
|
|
1713
2279
|
const snapshot = buildRuncatSnapshot(usage);
|
|
1714
2280
|
const dir = dirname(outFile);
|
|
1715
|
-
const tmp =
|
|
2281
|
+
const tmp = join3(dir, `.runcat-${process.pid}-${Date.now()}.json`);
|
|
1716
2282
|
try {
|
|
1717
2283
|
mkdirSync(dir, { recursive: true });
|
|
1718
2284
|
writeFileSync(tmp, JSON.stringify(snapshot), "utf-8");
|
|
@@ -1749,13 +2315,13 @@ async function getOAuthToken() {
|
|
|
1749
2315
|
const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
|
|
1750
2316
|
return extractToken(JSON.parse(stdout.trim()));
|
|
1751
2317
|
} catch {
|
|
1752
|
-
const raw =
|
|
2318
|
+
const raw = readFileSync3(join4(homedir3(), ".claude", ".credentials.json"), "utf-8");
|
|
1753
2319
|
return extractToken(JSON.parse(raw));
|
|
1754
2320
|
}
|
|
1755
2321
|
}
|
|
1756
2322
|
function readUsageCache() {
|
|
1757
2323
|
try {
|
|
1758
|
-
const raw =
|
|
2324
|
+
const raw = readFileSync3(USAGE_CACHE_PATH, "utf-8");
|
|
1759
2325
|
const cached = JSON.parse(raw);
|
|
1760
2326
|
if (Date.now() - cached.timestamp < USAGE_CACHE_TTL_SECONDS * 1e3) {
|
|
1761
2327
|
return cached.data;
|
|
@@ -1857,11 +2423,11 @@ async function notifyError(workerName, repoName, error) {
|
|
|
1857
2423
|
import { execFile as execFile2 } from "node:child_process";
|
|
1858
2424
|
import { promisify as promisify3 } from "node:util";
|
|
1859
2425
|
import { readdir, rm, stat } from "node:fs/promises";
|
|
1860
|
-
import { basename, resolve, sep } from "node:path";
|
|
2426
|
+
import { basename as basename2, resolve as resolve2, sep } from "node:path";
|
|
1861
2427
|
var execFileAsync2 = promisify3(execFile2);
|
|
1862
2428
|
var WORKTREES_DIR = ".claude/worktrees";
|
|
1863
2429
|
function isManagedWorktreePath(path) {
|
|
1864
|
-
return
|
|
2430
|
+
return resolve2(path).startsWith(resolve2(WORKTREES_DIR) + sep);
|
|
1865
2431
|
}
|
|
1866
2432
|
async function pathExists(path) {
|
|
1867
2433
|
try {
|
|
@@ -1948,7 +2514,7 @@ async function removeWorktree(worktreeId) {
|
|
|
1948
2514
|
const worktreePath = getWorktreePath(worktreeId);
|
|
1949
2515
|
let entry;
|
|
1950
2516
|
try {
|
|
1951
|
-
entry = (await listWorktreeEntries()).find((e) =>
|
|
2517
|
+
entry = (await listWorktreeEntries()).find((e) => resolve2(e.path) === resolve2(worktreePath));
|
|
1952
2518
|
} catch (error) {
|
|
1953
2519
|
console.error(`[worktree] Failed to list worktrees:`, error);
|
|
1954
2520
|
}
|
|
@@ -1972,7 +2538,7 @@ async function removeWorktreeByBranch(branchName) {
|
|
|
1972
2538
|
console.log(`[worktree] Skipping unmanaged worktree for branch ${branchName}: ${entry.path}`);
|
|
1973
2539
|
continue;
|
|
1974
2540
|
}
|
|
1975
|
-
const worktreeId =
|
|
2541
|
+
const worktreeId = basename2(entry.path);
|
|
1976
2542
|
if (isWorktreeInUse(worktreeId)) {
|
|
1977
2543
|
console.log(`[worktree] Skipping worktree in use by a running task for branch ${branchName}: ${entry.path}`);
|
|
1978
2544
|
continue;
|
|
@@ -1999,7 +2565,7 @@ async function removeStaleWorktrees() {
|
|
|
1999
2565
|
try {
|
|
2000
2566
|
for (const entry of await listWorktreeEntries()) {
|
|
2001
2567
|
if (!isManagedWorktreePath(entry.path)) continue;
|
|
2002
|
-
const worktreeId =
|
|
2568
|
+
const worktreeId = basename2(entry.path);
|
|
2003
2569
|
if (isGeneratedWorktreeName(worktreeId)) worktreeIds.add(worktreeId);
|
|
2004
2570
|
}
|
|
2005
2571
|
} catch (error) {
|
|
@@ -2058,22 +2624,8 @@ function createIssuePollingWorker(config) {
|
|
|
2058
2624
|
const { model, effort, skill } = getWorkerConfig(config.name);
|
|
2059
2625
|
const command = skill || config.command;
|
|
2060
2626
|
const parentNumber = issue.parent?.number;
|
|
2061
|
-
const
|
|
2062
|
-
|
|
2063
|
-
`${command} ${issue.number}`,
|
|
2064
|
-
"--dangerously-skip-permissions",
|
|
2065
|
-
"--chrome",
|
|
2066
|
-
"--disallowedTools",
|
|
2067
|
-
DISALLOWED_TOOLS_ARG,
|
|
2068
|
-
"--append-system-prompt",
|
|
2069
|
-
SYSTEM_PROMPT,
|
|
2070
|
-
"--append-subagent-system-prompt",
|
|
2071
|
-
SUBAGENT_SYSTEM_PROMPT,
|
|
2072
|
-
"--model",
|
|
2073
|
-
model,
|
|
2074
|
-
"--effort",
|
|
2075
|
-
effort
|
|
2076
|
-
];
|
|
2627
|
+
const mode = getRunMode();
|
|
2628
|
+
const claudeArgs = buildClaudeArgs({ mode, prompt: `${command} ${issue.number}`, model, effort });
|
|
2077
2629
|
let baseBranch = defaultBranch;
|
|
2078
2630
|
if (parentNumber !== void 0) {
|
|
2079
2631
|
baseBranch = `cc-epic-${parentNumber}`;
|
|
@@ -2129,7 +2681,7 @@ function createIssuePollingWorker(config) {
|
|
|
2129
2681
|
}
|
|
2130
2682
|
},
|
|
2131
2683
|
cwd,
|
|
2132
|
-
|
|
2684
|
+
buildClaudeEnv(mode)
|
|
2133
2685
|
);
|
|
2134
2686
|
} catch (err) {
|
|
2135
2687
|
console.error(`[${config.name}] setup error for #${issue.number}: ${err}`);
|
|
@@ -2238,24 +2790,10 @@ function createPrPollingWorker(config) {
|
|
|
2238
2790
|
const cwd = getWorktreePath(worktreeId);
|
|
2239
2791
|
const { model, effort, skill } = getWorkerConfig(config.name);
|
|
2240
2792
|
const command = skill || config.command;
|
|
2793
|
+
const mode = getRunMode();
|
|
2241
2794
|
run(
|
|
2242
2795
|
"claude",
|
|
2243
|
-
|
|
2244
|
-
"-p",
|
|
2245
|
-
`${command} ${pr.number}`,
|
|
2246
|
-
"--dangerously-skip-permissions",
|
|
2247
|
-
"--chrome",
|
|
2248
|
-
"--disallowedTools",
|
|
2249
|
-
DISALLOWED_TOOLS_ARG,
|
|
2250
|
-
"--append-system-prompt",
|
|
2251
|
-
SYSTEM_PROMPT,
|
|
2252
|
-
"--append-subagent-system-prompt",
|
|
2253
|
-
SUBAGENT_SYSTEM_PROMPT,
|
|
2254
|
-
"--model",
|
|
2255
|
-
model,
|
|
2256
|
-
"--effort",
|
|
2257
|
-
effort
|
|
2258
|
-
],
|
|
2796
|
+
buildClaudeArgs({ mode, prompt: `${command} ${pr.number}`, model, effort }),
|
|
2259
2797
|
pr.number,
|
|
2260
2798
|
`PR #${pr.number} (${pr.headRefName})`,
|
|
2261
2799
|
config.name,
|
|
@@ -2298,7 +2836,7 @@ function createPrPollingWorker(config) {
|
|
|
2298
2836
|
}
|
|
2299
2837
|
},
|
|
2300
2838
|
cwd,
|
|
2301
|
-
|
|
2839
|
+
buildClaudeEnv(mode)
|
|
2302
2840
|
);
|
|
2303
2841
|
} catch (err) {
|
|
2304
2842
|
console.error(`[${config.name}] setup error for PR #${pr.number}: ${err}`);
|
|
@@ -2559,7 +3097,7 @@ async function init(options = {}) {
|
|
|
2559
3097
|
// src/commands/run-command.ts
|
|
2560
3098
|
import { spawn as spawn2 } from "node:child_process";
|
|
2561
3099
|
function runCommand(command, args) {
|
|
2562
|
-
return new Promise((
|
|
3100
|
+
return new Promise((resolve3, reject) => {
|
|
2563
3101
|
const child = spawn2(command, args, {
|
|
2564
3102
|
stdio: "inherit",
|
|
2565
3103
|
shell: process.platform === "win32"
|
|
@@ -2582,7 +3120,7 @@ function runCommand(command, args) {
|
|
|
2582
3120
|
child.on("close", (code) => {
|
|
2583
3121
|
cleanup();
|
|
2584
3122
|
if (code === 0) {
|
|
2585
|
-
|
|
3123
|
+
resolve3();
|
|
2586
3124
|
} else {
|
|
2587
3125
|
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
2588
3126
|
}
|
|
@@ -2686,14 +3224,14 @@ async function update() {
|
|
|
2686
3224
|
}
|
|
2687
3225
|
|
|
2688
3226
|
// src/commands/version.ts
|
|
2689
|
-
import { readFileSync as
|
|
2690
|
-
import { dirname as dirname2, join as
|
|
3227
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
3228
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
2691
3229
|
import { fileURLToPath } from "node:url";
|
|
2692
3230
|
function version() {
|
|
2693
3231
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
2694
|
-
const pkgPath =
|
|
3232
|
+
const pkgPath = join5(here, "..", "package.json");
|
|
2695
3233
|
try {
|
|
2696
|
-
const pkg = JSON.parse(
|
|
3234
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
2697
3235
|
console.log(pkg.version ?? "unknown");
|
|
2698
3236
|
} catch (err) {
|
|
2699
3237
|
console.error(`[version] Failed to read version: ${err.message}`);
|
|
@@ -2744,141 +3282,6 @@ function buildForwardedCommand(argv) {
|
|
|
2744
3282
|
return ["claude-task-worker", ...tokens.map(shellQuote)].join(" ");
|
|
2745
3283
|
}
|
|
2746
3284
|
|
|
2747
|
-
// src/projects-config.ts
|
|
2748
|
-
import { readFileSync as readFileSync4, statSync } from "node:fs";
|
|
2749
|
-
import { homedir as homedir3 } from "node:os";
|
|
2750
|
-
import { isAbsolute, join as join5 } from "node:path";
|
|
2751
|
-
var RESERVED_ALL = "all";
|
|
2752
|
-
var ProjectsConfigError = class extends Error {
|
|
2753
|
-
constructor(message) {
|
|
2754
|
-
super(message);
|
|
2755
|
-
this.name = "ProjectsConfigError";
|
|
2756
|
-
}
|
|
2757
|
-
};
|
|
2758
|
-
function getProjectsConfigPath() {
|
|
2759
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2760
|
-
const configHome = xdg && xdg.length > 0 ? xdg : join5(homedir3(), ".config");
|
|
2761
|
-
return join5(configHome, "claude-task-worker", "projects.json");
|
|
2762
|
-
}
|
|
2763
|
-
var PROJECTS_CONFIG_PATH = getProjectsConfigPath();
|
|
2764
|
-
function isDirectory(path) {
|
|
2765
|
-
try {
|
|
2766
|
-
return statSync(path).isDirectory();
|
|
2767
|
-
} catch {
|
|
2768
|
-
return false;
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
function loadProjectsConfig() {
|
|
2772
|
-
const configPath = PROJECTS_CONFIG_PATH;
|
|
2773
|
-
let raw;
|
|
2774
|
-
try {
|
|
2775
|
-
raw = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
2776
|
-
} catch (err) {
|
|
2777
|
-
if (err.code === "ENOENT") {
|
|
2778
|
-
throw new ProjectsConfigError(`projects.json not found: ${configPath}`);
|
|
2779
|
-
}
|
|
2780
|
-
if (err instanceof SyntaxError) {
|
|
2781
|
-
throw new ProjectsConfigError(`projects.json contains invalid JSON: ${configPath}: ${err.message}`);
|
|
2782
|
-
}
|
|
2783
|
-
throw err;
|
|
2784
|
-
}
|
|
2785
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
2786
|
-
throw new ProjectsConfigError(`projects.json must contain a JSON object: ${configPath}`);
|
|
2787
|
-
}
|
|
2788
|
-
if (!("projects" in raw) || typeof raw["projects"] !== "object" || raw["projects"] === null || Array.isArray(raw["projects"])) {
|
|
2789
|
-
throw new ProjectsConfigError(`projects.json must contain a "projects" section as an object: ${configPath}`);
|
|
2790
|
-
}
|
|
2791
|
-
if ("projectGroups" in raw && (typeof raw["projectGroups"] !== "object" || raw["projectGroups"] === null || Array.isArray(raw["projectGroups"]))) {
|
|
2792
|
-
throw new ProjectsConfigError(`projects.json "projectGroups" must be an object: ${configPath}`);
|
|
2793
|
-
}
|
|
2794
|
-
const rawProjects = raw["projects"];
|
|
2795
|
-
const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
|
|
2796
|
-
const projectKeys = Object.keys(rawProjects);
|
|
2797
|
-
const groupKeys = Object.keys(rawProjectGroups);
|
|
2798
|
-
if (projectKeys.includes(RESERVED_ALL) || groupKeys.includes(RESERVED_ALL)) {
|
|
2799
|
-
throw new ProjectsConfigError(
|
|
2800
|
-
`"${RESERVED_ALL}" is a reserved word and cannot be used as a key in "projects" or "projectGroups"`
|
|
2801
|
-
);
|
|
2802
|
-
}
|
|
2803
|
-
const groupKeySet = new Set(groupKeys);
|
|
2804
|
-
const duplicateKeys = projectKeys.filter((key) => groupKeySet.has(key));
|
|
2805
|
-
if (duplicateKeys.length > 0) {
|
|
2806
|
-
throw new ProjectsConfigError(
|
|
2807
|
-
`"projects" and "projectGroups" share the same key namespace; duplicate keys are not allowed: ${duplicateKeys.join(", ")}`
|
|
2808
|
-
);
|
|
2809
|
-
}
|
|
2810
|
-
const projects = {};
|
|
2811
|
-
for (const [name, value] of Object.entries(rawProjects)) {
|
|
2812
|
-
if (name === "__proto__") {
|
|
2813
|
-
throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projects": ${configPath}`);
|
|
2814
|
-
}
|
|
2815
|
-
if (typeof value !== "string" || !isAbsolute(value)) {
|
|
2816
|
-
console.warn(`[projects-config] invalid projects.${name}: expected an absolute path, skipping`);
|
|
2817
|
-
continue;
|
|
2818
|
-
}
|
|
2819
|
-
if (!isDirectory(value)) {
|
|
2820
|
-
console.warn(`[projects-config] projects.${name} does not exist as a directory: ${value}, skipping`);
|
|
2821
|
-
continue;
|
|
2822
|
-
}
|
|
2823
|
-
projects[name] = value;
|
|
2824
|
-
}
|
|
2825
|
-
const projectGroups = {};
|
|
2826
|
-
for (const [groupName, value] of Object.entries(rawProjectGroups)) {
|
|
2827
|
-
if (groupName === "__proto__") {
|
|
2828
|
-
throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projectGroups": ${configPath}`);
|
|
2829
|
-
}
|
|
2830
|
-
if (!Array.isArray(value)) {
|
|
2831
|
-
console.warn(`[projects-config] invalid projectGroups.${groupName}: expected an array, skipping`);
|
|
2832
|
-
continue;
|
|
2833
|
-
}
|
|
2834
|
-
const members = [];
|
|
2835
|
-
for (const member of value) {
|
|
2836
|
-
if (typeof member !== "string" || !Object.prototype.hasOwnProperty.call(projects, member)) {
|
|
2837
|
-
console.warn(
|
|
2838
|
-
`[projects-config] projectGroups.${groupName} references unknown project "${String(member)}", skipping`
|
|
2839
|
-
);
|
|
2840
|
-
continue;
|
|
2841
|
-
}
|
|
2842
|
-
members.push(member);
|
|
2843
|
-
}
|
|
2844
|
-
projectGroups[groupName] = members;
|
|
2845
|
-
}
|
|
2846
|
-
return { projects, projectGroups };
|
|
2847
|
-
}
|
|
2848
|
-
function resolveTargetProjects(requested, config) {
|
|
2849
|
-
const resolved = /* @__PURE__ */ new Map();
|
|
2850
|
-
for (const name of requested) {
|
|
2851
|
-
if (name === RESERVED_ALL) {
|
|
2852
|
-
for (const [projectName, projectPath] of Object.entries(config.projects)) {
|
|
2853
|
-
resolved.set(projectName, { name: projectName, path: projectPath });
|
|
2854
|
-
}
|
|
2855
|
-
continue;
|
|
2856
|
-
}
|
|
2857
|
-
if (Object.prototype.hasOwnProperty.call(config.projects, name)) {
|
|
2858
|
-
resolved.set(name, { name, path: config.projects[name] });
|
|
2859
|
-
continue;
|
|
2860
|
-
}
|
|
2861
|
-
if (Object.prototype.hasOwnProperty.call(config.projectGroups, name)) {
|
|
2862
|
-
for (const projectName of config.projectGroups[name]) {
|
|
2863
|
-
const projectPath = config.projects[projectName];
|
|
2864
|
-
if (projectPath === void 0) continue;
|
|
2865
|
-
resolved.set(projectName, { name: projectName, path: projectPath });
|
|
2866
|
-
}
|
|
2867
|
-
continue;
|
|
2868
|
-
}
|
|
2869
|
-
const availableProjects = Object.keys(config.projects).join(", ") || "(none)";
|
|
2870
|
-
const availableGroups = Object.keys(config.projectGroups).join(", ") || "(none)";
|
|
2871
|
-
throw new ProjectsConfigError(
|
|
2872
|
-
`Unknown project or group: "${name}". Available projects: ${availableProjects}. Available groups: ${availableGroups}.`
|
|
2873
|
-
);
|
|
2874
|
-
}
|
|
2875
|
-
const resolvedProjects = Array.from(resolved.values());
|
|
2876
|
-
if (resolvedProjects.length === 0) {
|
|
2877
|
-
throw new ProjectsConfigError(`No projects resolved from requested targets: ${requested.join(", ")}`);
|
|
2878
|
-
}
|
|
2879
|
-
return resolvedProjects;
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
3285
|
// src/index.ts
|
|
2883
3286
|
var WORKERS = {
|
|
2884
3287
|
"exec-issue": execIssueWorker,
|
|
@@ -2981,6 +3384,18 @@ process.on("unhandledRejection", (err) => {
|
|
|
2981
3384
|
console.error("[worker] unhandled rejection:", err);
|
|
2982
3385
|
process.exit(1);
|
|
2983
3386
|
});
|
|
3387
|
+
async function assertRunModeAvailable() {
|
|
3388
|
+
if (getRunMode() !== "herdr") return;
|
|
3389
|
+
const herdr = await Promise.resolve().then(() => (init_herdr(), herdr_exports));
|
|
3390
|
+
try {
|
|
3391
|
+
await herdr.checkHerdrAvailable();
|
|
3392
|
+
} catch (err) {
|
|
3393
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3394
|
+
console.error(`[worker] config.json has mode "herdr" but herdr is unavailable: ${message}`);
|
|
3395
|
+
process.exit(1);
|
|
3396
|
+
}
|
|
3397
|
+
console.log("[worker] run mode: herdr (each task runs as a TUI session in its own herdr tab)");
|
|
3398
|
+
}
|
|
2984
3399
|
if (!hasProjectFilter()) {
|
|
2985
3400
|
process.on("SIGTERM", async () => {
|
|
2986
3401
|
if (isShuttingDown()) return;
|
|
@@ -2998,7 +3413,7 @@ if (!hasProjectFilter()) {
|
|
|
2998
3413
|
forceKilling = true;
|
|
2999
3414
|
console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
|
|
3000
3415
|
shutdown("SIGKILL");
|
|
3001
|
-
const cleanupTimeout = new Promise((
|
|
3416
|
+
const cleanupTimeout = new Promise((resolve3) => setTimeout(resolve3, 6e4).unref());
|
|
3002
3417
|
await Promise.race([waitForAllProcesses(), cleanupTimeout]);
|
|
3003
3418
|
process.exit(1);
|
|
3004
3419
|
}
|
|
@@ -3022,7 +3437,7 @@ if (hasProjectFilter()) {
|
|
|
3022
3437
|
process.on("SIGTERM", shutdownController.handle);
|
|
3023
3438
|
process.on("SIGINT", shutdownController.handle);
|
|
3024
3439
|
try {
|
|
3025
|
-
const config =
|
|
3440
|
+
const config = loadUserConfig();
|
|
3026
3441
|
const projects = resolveTargetProjects(parseProjectFilters(), config);
|
|
3027
3442
|
const forwardedCommand = buildForwardedCommand(process.argv.slice(2));
|
|
3028
3443
|
sessions = await dispatcher.runDispatcher(projects, forwardedCommand);
|
|
@@ -3037,7 +3452,7 @@ if (hasProjectFilter()) {
|
|
|
3037
3452
|
process.exit(0);
|
|
3038
3453
|
}
|
|
3039
3454
|
} catch (err) {
|
|
3040
|
-
if (err instanceof
|
|
3455
|
+
if (err instanceof UserConfigError || err instanceof herdr.HerdrUnavailableError) {
|
|
3041
3456
|
console.error(`[dispatcher] ${err.message}`);
|
|
3042
3457
|
process.exit(1);
|
|
3043
3458
|
}
|
|
@@ -3069,6 +3484,7 @@ if (hasProjectFilter()) {
|
|
|
3069
3484
|
const epicFilters = parseEpicFilters();
|
|
3070
3485
|
const labelFilters = parseLabelFilters();
|
|
3071
3486
|
(async () => {
|
|
3487
|
+
await assertRunModeAvailable();
|
|
3072
3488
|
await removeStaleWorktrees();
|
|
3073
3489
|
await Promise.all([
|
|
3074
3490
|
execIssueWorker({ epicFilters, labelFilters }),
|
|
@@ -3084,6 +3500,7 @@ if (hasProjectFilter()) {
|
|
|
3084
3500
|
const epicFilters = parseEpicFilters();
|
|
3085
3501
|
const labelFilters = parseLabelFilters();
|
|
3086
3502
|
(async () => {
|
|
3503
|
+
await assertRunModeAvailable();
|
|
3087
3504
|
await removeStaleWorktrees();
|
|
3088
3505
|
await Promise.all([
|
|
3089
3506
|
execIssueWorker({ epicFilters, labelFilters }),
|
|
@@ -3102,6 +3519,7 @@ if (hasProjectFilter()) {
|
|
|
3102
3519
|
const epicFilters = parseEpicFilters();
|
|
3103
3520
|
const labelFilters = parseLabelFilters();
|
|
3104
3521
|
(async () => {
|
|
3522
|
+
await assertRunModeAvailable();
|
|
3105
3523
|
await removeStaleWorktrees();
|
|
3106
3524
|
await WORKERS[workerType]({ epicFilters, labelFilters });
|
|
3107
3525
|
})();
|