claude-task-worker 0.26.0 → 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 +721 -298
- 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)));
|
|
@@ -1665,9 +2231,14 @@ function jstFields(date) {
|
|
|
1665
2231
|
const pick2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
1666
2232
|
return { month: pick2("month"), day: pick2("day"), hour: pick2("hour"), minute: pick2("minute") };
|
|
1667
2233
|
}
|
|
2234
|
+
var MINUTE_MS = 6e4;
|
|
2235
|
+
function ceilToMinute(date) {
|
|
2236
|
+
const remainder = date.getTime() % MINUTE_MS;
|
|
2237
|
+
return remainder === 0 ? date : new Date(date.getTime() + (MINUTE_MS - remainder));
|
|
2238
|
+
}
|
|
1668
2239
|
function parseResetsAt(isoString) {
|
|
1669
2240
|
const date = new Date(isoString);
|
|
1670
|
-
return Number.isNaN(date.getTime()) ? null : date;
|
|
2241
|
+
return Number.isNaN(date.getTime()) ? null : ceilToMinute(date);
|
|
1671
2242
|
}
|
|
1672
2243
|
function resetStamp(isoString, now = /* @__PURE__ */ new Date()) {
|
|
1673
2244
|
const date = parseResetsAt(isoString);
|
|
@@ -1707,7 +2278,7 @@ function buildRuncatSnapshot(usage, now = /* @__PURE__ */ new Date()) {
|
|
|
1707
2278
|
function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
|
|
1708
2279
|
const snapshot = buildRuncatSnapshot(usage);
|
|
1709
2280
|
const dir = dirname(outFile);
|
|
1710
|
-
const tmp =
|
|
2281
|
+
const tmp = join3(dir, `.runcat-${process.pid}-${Date.now()}.json`);
|
|
1711
2282
|
try {
|
|
1712
2283
|
mkdirSync(dir, { recursive: true });
|
|
1713
2284
|
writeFileSync(tmp, JSON.stringify(snapshot), "utf-8");
|
|
@@ -1744,13 +2315,13 @@ async function getOAuthToken() {
|
|
|
1744
2315
|
const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
|
|
1745
2316
|
return extractToken(JSON.parse(stdout.trim()));
|
|
1746
2317
|
} catch {
|
|
1747
|
-
const raw =
|
|
2318
|
+
const raw = readFileSync3(join4(homedir3(), ".claude", ".credentials.json"), "utf-8");
|
|
1748
2319
|
return extractToken(JSON.parse(raw));
|
|
1749
2320
|
}
|
|
1750
2321
|
}
|
|
1751
2322
|
function readUsageCache() {
|
|
1752
2323
|
try {
|
|
1753
|
-
const raw =
|
|
2324
|
+
const raw = readFileSync3(USAGE_CACHE_PATH, "utf-8");
|
|
1754
2325
|
const cached = JSON.parse(raw);
|
|
1755
2326
|
if (Date.now() - cached.timestamp < USAGE_CACHE_TTL_SECONDS * 1e3) {
|
|
1756
2327
|
return cached.data;
|
|
@@ -1852,11 +2423,11 @@ async function notifyError(workerName, repoName, error) {
|
|
|
1852
2423
|
import { execFile as execFile2 } from "node:child_process";
|
|
1853
2424
|
import { promisify as promisify3 } from "node:util";
|
|
1854
2425
|
import { readdir, rm, stat } from "node:fs/promises";
|
|
1855
|
-
import { basename, resolve, sep } from "node:path";
|
|
2426
|
+
import { basename as basename2, resolve as resolve2, sep } from "node:path";
|
|
1856
2427
|
var execFileAsync2 = promisify3(execFile2);
|
|
1857
2428
|
var WORKTREES_DIR = ".claude/worktrees";
|
|
1858
2429
|
function isManagedWorktreePath(path) {
|
|
1859
|
-
return
|
|
2430
|
+
return resolve2(path).startsWith(resolve2(WORKTREES_DIR) + sep);
|
|
1860
2431
|
}
|
|
1861
2432
|
async function pathExists(path) {
|
|
1862
2433
|
try {
|
|
@@ -1943,7 +2514,7 @@ async function removeWorktree(worktreeId) {
|
|
|
1943
2514
|
const worktreePath = getWorktreePath(worktreeId);
|
|
1944
2515
|
let entry;
|
|
1945
2516
|
try {
|
|
1946
|
-
entry = (await listWorktreeEntries()).find((e) =>
|
|
2517
|
+
entry = (await listWorktreeEntries()).find((e) => resolve2(e.path) === resolve2(worktreePath));
|
|
1947
2518
|
} catch (error) {
|
|
1948
2519
|
console.error(`[worktree] Failed to list worktrees:`, error);
|
|
1949
2520
|
}
|
|
@@ -1967,7 +2538,7 @@ async function removeWorktreeByBranch(branchName) {
|
|
|
1967
2538
|
console.log(`[worktree] Skipping unmanaged worktree for branch ${branchName}: ${entry.path}`);
|
|
1968
2539
|
continue;
|
|
1969
2540
|
}
|
|
1970
|
-
const worktreeId =
|
|
2541
|
+
const worktreeId = basename2(entry.path);
|
|
1971
2542
|
if (isWorktreeInUse(worktreeId)) {
|
|
1972
2543
|
console.log(`[worktree] Skipping worktree in use by a running task for branch ${branchName}: ${entry.path}`);
|
|
1973
2544
|
continue;
|
|
@@ -1994,7 +2565,7 @@ async function removeStaleWorktrees() {
|
|
|
1994
2565
|
try {
|
|
1995
2566
|
for (const entry of await listWorktreeEntries()) {
|
|
1996
2567
|
if (!isManagedWorktreePath(entry.path)) continue;
|
|
1997
|
-
const worktreeId =
|
|
2568
|
+
const worktreeId = basename2(entry.path);
|
|
1998
2569
|
if (isGeneratedWorktreeName(worktreeId)) worktreeIds.add(worktreeId);
|
|
1999
2570
|
}
|
|
2000
2571
|
} catch (error) {
|
|
@@ -2053,22 +2624,8 @@ function createIssuePollingWorker(config) {
|
|
|
2053
2624
|
const { model, effort, skill } = getWorkerConfig(config.name);
|
|
2054
2625
|
const command = skill || config.command;
|
|
2055
2626
|
const parentNumber = issue.parent?.number;
|
|
2056
|
-
const
|
|
2057
|
-
|
|
2058
|
-
`${command} ${issue.number}`,
|
|
2059
|
-
"--dangerously-skip-permissions",
|
|
2060
|
-
"--chrome",
|
|
2061
|
-
"--disallowedTools",
|
|
2062
|
-
DISALLOWED_TOOLS_ARG,
|
|
2063
|
-
"--append-system-prompt",
|
|
2064
|
-
SYSTEM_PROMPT,
|
|
2065
|
-
"--append-subagent-system-prompt",
|
|
2066
|
-
SUBAGENT_SYSTEM_PROMPT,
|
|
2067
|
-
"--model",
|
|
2068
|
-
model,
|
|
2069
|
-
"--effort",
|
|
2070
|
-
effort
|
|
2071
|
-
];
|
|
2627
|
+
const mode = getRunMode();
|
|
2628
|
+
const claudeArgs = buildClaudeArgs({ mode, prompt: `${command} ${issue.number}`, model, effort });
|
|
2072
2629
|
let baseBranch = defaultBranch;
|
|
2073
2630
|
if (parentNumber !== void 0) {
|
|
2074
2631
|
baseBranch = `cc-epic-${parentNumber}`;
|
|
@@ -2124,7 +2681,7 @@ function createIssuePollingWorker(config) {
|
|
|
2124
2681
|
}
|
|
2125
2682
|
},
|
|
2126
2683
|
cwd,
|
|
2127
|
-
|
|
2684
|
+
buildClaudeEnv(mode)
|
|
2128
2685
|
);
|
|
2129
2686
|
} catch (err) {
|
|
2130
2687
|
console.error(`[${config.name}] setup error for #${issue.number}: ${err}`);
|
|
@@ -2233,24 +2790,10 @@ function createPrPollingWorker(config) {
|
|
|
2233
2790
|
const cwd = getWorktreePath(worktreeId);
|
|
2234
2791
|
const { model, effort, skill } = getWorkerConfig(config.name);
|
|
2235
2792
|
const command = skill || config.command;
|
|
2793
|
+
const mode = getRunMode();
|
|
2236
2794
|
run(
|
|
2237
2795
|
"claude",
|
|
2238
|
-
|
|
2239
|
-
"-p",
|
|
2240
|
-
`${command} ${pr.number}`,
|
|
2241
|
-
"--dangerously-skip-permissions",
|
|
2242
|
-
"--chrome",
|
|
2243
|
-
"--disallowedTools",
|
|
2244
|
-
DISALLOWED_TOOLS_ARG,
|
|
2245
|
-
"--append-system-prompt",
|
|
2246
|
-
SYSTEM_PROMPT,
|
|
2247
|
-
"--append-subagent-system-prompt",
|
|
2248
|
-
SUBAGENT_SYSTEM_PROMPT,
|
|
2249
|
-
"--model",
|
|
2250
|
-
model,
|
|
2251
|
-
"--effort",
|
|
2252
|
-
effort
|
|
2253
|
-
],
|
|
2796
|
+
buildClaudeArgs({ mode, prompt: `${command} ${pr.number}`, model, effort }),
|
|
2254
2797
|
pr.number,
|
|
2255
2798
|
`PR #${pr.number} (${pr.headRefName})`,
|
|
2256
2799
|
config.name,
|
|
@@ -2293,7 +2836,7 @@ function createPrPollingWorker(config) {
|
|
|
2293
2836
|
}
|
|
2294
2837
|
},
|
|
2295
2838
|
cwd,
|
|
2296
|
-
|
|
2839
|
+
buildClaudeEnv(mode)
|
|
2297
2840
|
);
|
|
2298
2841
|
} catch (err) {
|
|
2299
2842
|
console.error(`[${config.name}] setup error for PR #${pr.number}: ${err}`);
|
|
@@ -2554,7 +3097,7 @@ async function init(options = {}) {
|
|
|
2554
3097
|
// src/commands/run-command.ts
|
|
2555
3098
|
import { spawn as spawn2 } from "node:child_process";
|
|
2556
3099
|
function runCommand(command, args) {
|
|
2557
|
-
return new Promise((
|
|
3100
|
+
return new Promise((resolve3, reject) => {
|
|
2558
3101
|
const child = spawn2(command, args, {
|
|
2559
3102
|
stdio: "inherit",
|
|
2560
3103
|
shell: process.platform === "win32"
|
|
@@ -2577,7 +3120,7 @@ function runCommand(command, args) {
|
|
|
2577
3120
|
child.on("close", (code) => {
|
|
2578
3121
|
cleanup();
|
|
2579
3122
|
if (code === 0) {
|
|
2580
|
-
|
|
3123
|
+
resolve3();
|
|
2581
3124
|
} else {
|
|
2582
3125
|
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
2583
3126
|
}
|
|
@@ -2681,14 +3224,14 @@ async function update() {
|
|
|
2681
3224
|
}
|
|
2682
3225
|
|
|
2683
3226
|
// src/commands/version.ts
|
|
2684
|
-
import { readFileSync as
|
|
2685
|
-
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";
|
|
2686
3229
|
import { fileURLToPath } from "node:url";
|
|
2687
3230
|
function version() {
|
|
2688
3231
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
2689
|
-
const pkgPath =
|
|
3232
|
+
const pkgPath = join5(here, "..", "package.json");
|
|
2690
3233
|
try {
|
|
2691
|
-
const pkg = JSON.parse(
|
|
3234
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
2692
3235
|
console.log(pkg.version ?? "unknown");
|
|
2693
3236
|
} catch (err) {
|
|
2694
3237
|
console.error(`[version] Failed to read version: ${err.message}`);
|
|
@@ -2739,141 +3282,6 @@ function buildForwardedCommand(argv) {
|
|
|
2739
3282
|
return ["claude-task-worker", ...tokens.map(shellQuote)].join(" ");
|
|
2740
3283
|
}
|
|
2741
3284
|
|
|
2742
|
-
// src/projects-config.ts
|
|
2743
|
-
import { readFileSync as readFileSync4, statSync } from "node:fs";
|
|
2744
|
-
import { homedir as homedir3 } from "node:os";
|
|
2745
|
-
import { isAbsolute, join as join5 } from "node:path";
|
|
2746
|
-
var RESERVED_ALL = "all";
|
|
2747
|
-
var ProjectsConfigError = class extends Error {
|
|
2748
|
-
constructor(message) {
|
|
2749
|
-
super(message);
|
|
2750
|
-
this.name = "ProjectsConfigError";
|
|
2751
|
-
}
|
|
2752
|
-
};
|
|
2753
|
-
function getProjectsConfigPath() {
|
|
2754
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2755
|
-
const configHome = xdg && xdg.length > 0 ? xdg : join5(homedir3(), ".config");
|
|
2756
|
-
return join5(configHome, "claude-task-worker", "projects.json");
|
|
2757
|
-
}
|
|
2758
|
-
var PROJECTS_CONFIG_PATH = getProjectsConfigPath();
|
|
2759
|
-
function isDirectory(path) {
|
|
2760
|
-
try {
|
|
2761
|
-
return statSync(path).isDirectory();
|
|
2762
|
-
} catch {
|
|
2763
|
-
return false;
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
function loadProjectsConfig() {
|
|
2767
|
-
const configPath = PROJECTS_CONFIG_PATH;
|
|
2768
|
-
let raw;
|
|
2769
|
-
try {
|
|
2770
|
-
raw = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
2771
|
-
} catch (err) {
|
|
2772
|
-
if (err.code === "ENOENT") {
|
|
2773
|
-
throw new ProjectsConfigError(`projects.json not found: ${configPath}`);
|
|
2774
|
-
}
|
|
2775
|
-
if (err instanceof SyntaxError) {
|
|
2776
|
-
throw new ProjectsConfigError(`projects.json contains invalid JSON: ${configPath}: ${err.message}`);
|
|
2777
|
-
}
|
|
2778
|
-
throw err;
|
|
2779
|
-
}
|
|
2780
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
2781
|
-
throw new ProjectsConfigError(`projects.json must contain a JSON object: ${configPath}`);
|
|
2782
|
-
}
|
|
2783
|
-
if (!("projects" in raw) || typeof raw["projects"] !== "object" || raw["projects"] === null || Array.isArray(raw["projects"])) {
|
|
2784
|
-
throw new ProjectsConfigError(`projects.json must contain a "projects" section as an object: ${configPath}`);
|
|
2785
|
-
}
|
|
2786
|
-
if ("projectGroups" in raw && (typeof raw["projectGroups"] !== "object" || raw["projectGroups"] === null || Array.isArray(raw["projectGroups"]))) {
|
|
2787
|
-
throw new ProjectsConfigError(`projects.json "projectGroups" must be an object: ${configPath}`);
|
|
2788
|
-
}
|
|
2789
|
-
const rawProjects = raw["projects"];
|
|
2790
|
-
const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
|
|
2791
|
-
const projectKeys = Object.keys(rawProjects);
|
|
2792
|
-
const groupKeys = Object.keys(rawProjectGroups);
|
|
2793
|
-
if (projectKeys.includes(RESERVED_ALL) || groupKeys.includes(RESERVED_ALL)) {
|
|
2794
|
-
throw new ProjectsConfigError(
|
|
2795
|
-
`"${RESERVED_ALL}" is a reserved word and cannot be used as a key in "projects" or "projectGroups"`
|
|
2796
|
-
);
|
|
2797
|
-
}
|
|
2798
|
-
const groupKeySet = new Set(groupKeys);
|
|
2799
|
-
const duplicateKeys = projectKeys.filter((key) => groupKeySet.has(key));
|
|
2800
|
-
if (duplicateKeys.length > 0) {
|
|
2801
|
-
throw new ProjectsConfigError(
|
|
2802
|
-
`"projects" and "projectGroups" share the same key namespace; duplicate keys are not allowed: ${duplicateKeys.join(", ")}`
|
|
2803
|
-
);
|
|
2804
|
-
}
|
|
2805
|
-
const projects = {};
|
|
2806
|
-
for (const [name, value] of Object.entries(rawProjects)) {
|
|
2807
|
-
if (name === "__proto__") {
|
|
2808
|
-
throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projects": ${configPath}`);
|
|
2809
|
-
}
|
|
2810
|
-
if (typeof value !== "string" || !isAbsolute(value)) {
|
|
2811
|
-
console.warn(`[projects-config] invalid projects.${name}: expected an absolute path, skipping`);
|
|
2812
|
-
continue;
|
|
2813
|
-
}
|
|
2814
|
-
if (!isDirectory(value)) {
|
|
2815
|
-
console.warn(`[projects-config] projects.${name} does not exist as a directory: ${value}, skipping`);
|
|
2816
|
-
continue;
|
|
2817
|
-
}
|
|
2818
|
-
projects[name] = value;
|
|
2819
|
-
}
|
|
2820
|
-
const projectGroups = {};
|
|
2821
|
-
for (const [groupName, value] of Object.entries(rawProjectGroups)) {
|
|
2822
|
-
if (groupName === "__proto__") {
|
|
2823
|
-
throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projectGroups": ${configPath}`);
|
|
2824
|
-
}
|
|
2825
|
-
if (!Array.isArray(value)) {
|
|
2826
|
-
console.warn(`[projects-config] invalid projectGroups.${groupName}: expected an array, skipping`);
|
|
2827
|
-
continue;
|
|
2828
|
-
}
|
|
2829
|
-
const members = [];
|
|
2830
|
-
for (const member of value) {
|
|
2831
|
-
if (typeof member !== "string" || !Object.prototype.hasOwnProperty.call(projects, member)) {
|
|
2832
|
-
console.warn(
|
|
2833
|
-
`[projects-config] projectGroups.${groupName} references unknown project "${String(member)}", skipping`
|
|
2834
|
-
);
|
|
2835
|
-
continue;
|
|
2836
|
-
}
|
|
2837
|
-
members.push(member);
|
|
2838
|
-
}
|
|
2839
|
-
projectGroups[groupName] = members;
|
|
2840
|
-
}
|
|
2841
|
-
return { projects, projectGroups };
|
|
2842
|
-
}
|
|
2843
|
-
function resolveTargetProjects(requested, config) {
|
|
2844
|
-
const resolved = /* @__PURE__ */ new Map();
|
|
2845
|
-
for (const name of requested) {
|
|
2846
|
-
if (name === RESERVED_ALL) {
|
|
2847
|
-
for (const [projectName, projectPath] of Object.entries(config.projects)) {
|
|
2848
|
-
resolved.set(projectName, { name: projectName, path: projectPath });
|
|
2849
|
-
}
|
|
2850
|
-
continue;
|
|
2851
|
-
}
|
|
2852
|
-
if (Object.prototype.hasOwnProperty.call(config.projects, name)) {
|
|
2853
|
-
resolved.set(name, { name, path: config.projects[name] });
|
|
2854
|
-
continue;
|
|
2855
|
-
}
|
|
2856
|
-
if (Object.prototype.hasOwnProperty.call(config.projectGroups, name)) {
|
|
2857
|
-
for (const projectName of config.projectGroups[name]) {
|
|
2858
|
-
const projectPath = config.projects[projectName];
|
|
2859
|
-
if (projectPath === void 0) continue;
|
|
2860
|
-
resolved.set(projectName, { name: projectName, path: projectPath });
|
|
2861
|
-
}
|
|
2862
|
-
continue;
|
|
2863
|
-
}
|
|
2864
|
-
const availableProjects = Object.keys(config.projects).join(", ") || "(none)";
|
|
2865
|
-
const availableGroups = Object.keys(config.projectGroups).join(", ") || "(none)";
|
|
2866
|
-
throw new ProjectsConfigError(
|
|
2867
|
-
`Unknown project or group: "${name}". Available projects: ${availableProjects}. Available groups: ${availableGroups}.`
|
|
2868
|
-
);
|
|
2869
|
-
}
|
|
2870
|
-
const resolvedProjects = Array.from(resolved.values());
|
|
2871
|
-
if (resolvedProjects.length === 0) {
|
|
2872
|
-
throw new ProjectsConfigError(`No projects resolved from requested targets: ${requested.join(", ")}`);
|
|
2873
|
-
}
|
|
2874
|
-
return resolvedProjects;
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
3285
|
// src/index.ts
|
|
2878
3286
|
var WORKERS = {
|
|
2879
3287
|
"exec-issue": execIssueWorker,
|
|
@@ -2976,6 +3384,18 @@ process.on("unhandledRejection", (err) => {
|
|
|
2976
3384
|
console.error("[worker] unhandled rejection:", err);
|
|
2977
3385
|
process.exit(1);
|
|
2978
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
|
+
}
|
|
2979
3399
|
if (!hasProjectFilter()) {
|
|
2980
3400
|
process.on("SIGTERM", async () => {
|
|
2981
3401
|
if (isShuttingDown()) return;
|
|
@@ -2993,7 +3413,7 @@ if (!hasProjectFilter()) {
|
|
|
2993
3413
|
forceKilling = true;
|
|
2994
3414
|
console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
|
|
2995
3415
|
shutdown("SIGKILL");
|
|
2996
|
-
const cleanupTimeout = new Promise((
|
|
3416
|
+
const cleanupTimeout = new Promise((resolve3) => setTimeout(resolve3, 6e4).unref());
|
|
2997
3417
|
await Promise.race([waitForAllProcesses(), cleanupTimeout]);
|
|
2998
3418
|
process.exit(1);
|
|
2999
3419
|
}
|
|
@@ -3017,7 +3437,7 @@ if (hasProjectFilter()) {
|
|
|
3017
3437
|
process.on("SIGTERM", shutdownController.handle);
|
|
3018
3438
|
process.on("SIGINT", shutdownController.handle);
|
|
3019
3439
|
try {
|
|
3020
|
-
const config =
|
|
3440
|
+
const config = loadUserConfig();
|
|
3021
3441
|
const projects = resolveTargetProjects(parseProjectFilters(), config);
|
|
3022
3442
|
const forwardedCommand = buildForwardedCommand(process.argv.slice(2));
|
|
3023
3443
|
sessions = await dispatcher.runDispatcher(projects, forwardedCommand);
|
|
@@ -3032,7 +3452,7 @@ if (hasProjectFilter()) {
|
|
|
3032
3452
|
process.exit(0);
|
|
3033
3453
|
}
|
|
3034
3454
|
} catch (err) {
|
|
3035
|
-
if (err instanceof
|
|
3455
|
+
if (err instanceof UserConfigError || err instanceof herdr.HerdrUnavailableError) {
|
|
3036
3456
|
console.error(`[dispatcher] ${err.message}`);
|
|
3037
3457
|
process.exit(1);
|
|
3038
3458
|
}
|
|
@@ -3064,6 +3484,7 @@ if (hasProjectFilter()) {
|
|
|
3064
3484
|
const epicFilters = parseEpicFilters();
|
|
3065
3485
|
const labelFilters = parseLabelFilters();
|
|
3066
3486
|
(async () => {
|
|
3487
|
+
await assertRunModeAvailable();
|
|
3067
3488
|
await removeStaleWorktrees();
|
|
3068
3489
|
await Promise.all([
|
|
3069
3490
|
execIssueWorker({ epicFilters, labelFilters }),
|
|
@@ -3079,6 +3500,7 @@ if (hasProjectFilter()) {
|
|
|
3079
3500
|
const epicFilters = parseEpicFilters();
|
|
3080
3501
|
const labelFilters = parseLabelFilters();
|
|
3081
3502
|
(async () => {
|
|
3503
|
+
await assertRunModeAvailable();
|
|
3082
3504
|
await removeStaleWorktrees();
|
|
3083
3505
|
await Promise.all([
|
|
3084
3506
|
execIssueWorker({ epicFilters, labelFilters }),
|
|
@@ -3097,6 +3519,7 @@ if (hasProjectFilter()) {
|
|
|
3097
3519
|
const epicFilters = parseEpicFilters();
|
|
3098
3520
|
const labelFilters = parseLabelFilters();
|
|
3099
3521
|
(async () => {
|
|
3522
|
+
await assertRunModeAvailable();
|
|
3100
3523
|
await removeStaleWorktrees();
|
|
3101
3524
|
await WORKERS[workerType]({ epicFilters, labelFilters });
|
|
3102
3525
|
})();
|