claude-task-worker 0.26.1 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +39 -8
  2. package/dist/index.js +720 -307
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -59,41 +59,57 @@ __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,
63
68
  paneProcessInfo: () => paneProcessInfo,
64
69
  paneRead: () => paneRead,
65
70
  paneSendKeys: () => paneSendKeys,
66
71
  paneSendText: () => paneSendText,
67
72
  tabClose: () => tabClose,
68
73
  tabCreate: () => tabCreate,
69
- tabList: () => tabList
74
+ tabList: () => tabList,
75
+ tabRename: () => tabRename,
76
+ workspaceClose: () => workspaceClose,
77
+ workspaceCreate: () => workspaceCreate,
78
+ workspaceList: () => workspaceList
70
79
  });
71
80
  import { createRequire as createRequire2 } from "node:module";
81
+ import { resolve as resolvePath } from "node:path";
72
82
  function runHerdr(args) {
73
- return new Promise((resolve2) => {
83
+ return new Promise((resolve3) => {
74
84
  childProcess2.execFile(
75
85
  "herdr",
76
86
  args,
77
87
  { timeout: HERDR_TIMEOUT_MS, killSignal: "SIGKILL" },
78
88
  (error, stdout, stderr) => {
79
- let parsed;
80
- try {
81
- parsed = JSON.parse(stdout);
82
- } catch {
83
- parsed = void 0;
84
- }
85
- resolve2({ execError: error, parsed, stdout, stderr });
89
+ const parsed = parseHerdrResponse(stdout);
90
+ const stderrError = parsed?.error ? void 0 : parseHerdrResponse(stderr)?.error;
91
+ resolve3({ execError: error, parsed, stderrError, stdout, stderr });
86
92
  }
87
93
  );
88
94
  });
89
95
  }
96
+ function parseHerdrResponse(output) {
97
+ try {
98
+ const parsed = JSON.parse(output);
99
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
100
+ return parsed;
101
+ } catch {
102
+ return void 0;
103
+ }
104
+ }
90
105
  async function execHerdr(args, options) {
91
- const { execError, parsed, stdout, stderr } = await runHerdr(args);
106
+ const { execError, parsed, stderrError, stdout, stderr } = await runHerdr(args);
92
107
  const stderrSuffix = stderr.trim() ? `: ${stderr.trim()}` : "";
93
- if (parsed?.error) {
108
+ const errorPayload = parsed?.error ?? stderrError;
109
+ if (errorPayload) {
94
110
  throw new HerdrError(
95
- `herdr ${args.join(" ")} failed: [${parsed.error.code}] ${parsed.error.message}`,
96
- parsed.error.code
111
+ `herdr ${args.join(" ")} failed: [${errorPayload.code}] ${errorPayload.message}`,
112
+ errorPayload.code
97
113
  );
98
114
  }
99
115
  if (execError) {
@@ -107,8 +123,29 @@ async function execHerdr(args, options) {
107
123
  }
108
124
  return parsed.result;
109
125
  }
110
- async function tabCreate({ label, cwd }) {
111
- const result = await execHerdr(["tab", "create", "--label", label, "--cwd", cwd, "--no-focus"]);
126
+ function envArgs(env) {
127
+ if (!env) return [];
128
+ return Object.entries(env).flatMap(([key, value]) => ["--env", `${key}=${value}`]);
129
+ }
130
+ function cwdArgs(cwd) {
131
+ return ["--cwd", resolvePath(cwd)];
132
+ }
133
+ async function tabCreate({
134
+ label,
135
+ cwd,
136
+ workspaceId,
137
+ env
138
+ }) {
139
+ const result = await execHerdr([
140
+ "tab",
141
+ "create",
142
+ ...workspaceId ? ["--workspace", workspaceId] : [],
143
+ "--label",
144
+ label,
145
+ ...cwdArgs(cwd),
146
+ ...envArgs(env),
147
+ "--no-focus"
148
+ ]);
112
149
  const paneId = result?.root_pane?.pane_id;
113
150
  const tabId = result?.tab?.tab_id;
114
151
  if (!paneId || !tabId) {
@@ -116,6 +153,20 @@ async function tabCreate({ label, cwd }) {
116
153
  }
117
154
  return { paneId, tabId };
118
155
  }
156
+ async function tabRename(tabId, label) {
157
+ await execHerdr(["tab", "rename", tabId, label]);
158
+ }
159
+ async function paneClose(paneId) {
160
+ await execHerdr(["pane", "close", paneId]);
161
+ }
162
+ async function paneGet(paneId) {
163
+ const result = await execHerdr(["pane", "get", paneId]);
164
+ const pane = result?.pane;
165
+ if (!pane?.pane_id) {
166
+ throw new Error(`Failed to get pane ${paneId}: invalid response structure from herdr`);
167
+ }
168
+ return { paneId: pane.pane_id, tabId: pane.tab_id ?? "" };
169
+ }
119
170
  async function tabClose(tabId) {
120
171
  await execHerdr(["tab", "close", tabId]);
121
172
  }
@@ -130,6 +181,88 @@ async function tabList() {
130
181
  workspaceId: tab.workspace_id
131
182
  }));
132
183
  }
184
+ async function workspaceCreate({
185
+ label,
186
+ cwd,
187
+ env
188
+ }) {
189
+ const result = await execHerdr([
190
+ "workspace",
191
+ "create",
192
+ "--label",
193
+ label,
194
+ ...cwdArgs(cwd),
195
+ ...envArgs(env),
196
+ "--no-focus"
197
+ ]);
198
+ const workspaceId = result?.workspace?.workspace_id;
199
+ const paneId = result?.root_pane?.pane_id;
200
+ const tabId = result?.root_pane?.tab_id;
201
+ if (!workspaceId || !paneId || !tabId) {
202
+ throw new Error("Failed to create workspace: invalid response structure from herdr");
203
+ }
204
+ return { workspaceId, paneId, tabId };
205
+ }
206
+ async function workspaceList() {
207
+ const result = await execHerdr(["workspace", "list"]);
208
+ if (!result || !Array.isArray(result.workspaces)) {
209
+ return [];
210
+ }
211
+ return result.workspaces.map((workspace) => ({
212
+ workspaceId: workspace.workspace_id,
213
+ label: workspace.label ?? ""
214
+ }));
215
+ }
216
+ async function workspaceClose(workspaceId) {
217
+ await execHerdr(["workspace", "close", workspaceId]);
218
+ }
219
+ async function agentStart({
220
+ name,
221
+ cwd,
222
+ argv,
223
+ workspaceId,
224
+ tabId: targetTabId,
225
+ env
226
+ }) {
227
+ const result = await execHerdr([
228
+ "agent",
229
+ "start",
230
+ name,
231
+ ...workspaceId ? ["--workspace", workspaceId] : [],
232
+ ...targetTabId ? ["--tab", targetTabId] : [],
233
+ ...cwdArgs(cwd),
234
+ ...envArgs(env),
235
+ "--no-focus",
236
+ "--",
237
+ ...argv
238
+ ]);
239
+ const paneId = result?.agent?.pane_id;
240
+ const tabId = result?.agent?.tab_id;
241
+ if (!paneId || !tabId) {
242
+ throw new Error("Failed to start agent: invalid response structure from herdr");
243
+ }
244
+ return { paneId, tabId };
245
+ }
246
+ function toAgentStatus(value) {
247
+ return value === "working" || value === "idle" || value === "blocked" ? value : "unknown";
248
+ }
249
+ async function agentGet(target) {
250
+ const result = await execHerdr(["agent", "get", target]);
251
+ const agent = result?.agent;
252
+ if (!agent?.pane_id) {
253
+ throw new Error(`Failed to get agent info for ${target}: invalid response structure from herdr`);
254
+ }
255
+ return {
256
+ paneId: agent.pane_id,
257
+ tabId: agent.tab_id ?? "",
258
+ workspaceId: agent.workspace_id ?? "",
259
+ agentStatus: toAgentStatus(agent.agent_status)
260
+ };
261
+ }
262
+ function getCurrentWorkspaceId() {
263
+ const id = process.env.HERDR_WORKSPACE_ID;
264
+ return id && id.length > 0 ? id : void 0;
265
+ }
133
266
  function isPaneReadErrorPayload(value) {
134
267
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
135
268
  return false;
@@ -233,16 +366,179 @@ var init_herdr = __esm({
233
366
  }
234
367
  });
235
368
 
369
+ // src/herdr-runner.ts
370
+ var herdr_runner_exports = {};
371
+ __export(herdr_runner_exports, {
372
+ AGENT_POLL_INTERVAL_MS: () => AGENT_POLL_INTERVAL_MS,
373
+ CLAUDE_EXIT_POLL_INTERVAL_MS: () => CLAUDE_EXIT_POLL_INTERVAL_MS,
374
+ CLAUDE_EXIT_TIMEOUT_MS: () => CLAUDE_EXIT_TIMEOUT_MS,
375
+ PANE_OUTPUT_LINES: () => PANE_OUTPUT_LINES,
376
+ buildHerdrTaskResult: () => buildHerdrTaskResult,
377
+ createCompletionTracker: () => createCompletionTracker,
378
+ observeAgentStatus: () => observeAgentStatus,
379
+ startHerdrTask: () => startHerdrTask,
380
+ stopHerdrTask: () => stopHerdrTask,
381
+ taskTabLabel: () => taskTabLabel,
382
+ waitForHerdrTask: () => waitForHerdrTask
383
+ });
384
+ async function loadHerdr() {
385
+ return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
386
+ }
387
+ function taskTabLabel(projectName, number) {
388
+ return `ctw:${projectName}:#${number}`;
389
+ }
390
+ function createCompletionTracker() {
391
+ return { seenWorking: false, warnedBlocked: false };
392
+ }
393
+ function observeAgentStatus(tracker, status) {
394
+ if (status === "working") {
395
+ return { tracker: { ...tracker, seenWorking: true }, decision: "running" };
396
+ }
397
+ if (status === "blocked") {
398
+ if (tracker.warnedBlocked) return { tracker, decision: "running" };
399
+ return { tracker: { ...tracker, warnedBlocked: true }, decision: "blocked-first-seen" };
400
+ }
401
+ if (status === "idle" && tracker.seenWorking) {
402
+ return { tracker, decision: "completed" };
403
+ }
404
+ return { tracker, decision: "running" };
405
+ }
406
+ function buildHerdrTaskResult(paneOutput) {
407
+ if (paneOutput.trim() === "") {
408
+ return {
409
+ status: "failed",
410
+ 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)"
411
+ };
412
+ }
413
+ return { status: "completed", output: paneOutput };
414
+ }
415
+ async function startHerdrTask({
416
+ label,
417
+ cwd,
418
+ argv,
419
+ env,
420
+ workspaceId,
421
+ herdr
422
+ }) {
423
+ const mod = herdr ?? await loadHerdr();
424
+ const { tabId, paneId: shellPaneId } = await mod.tabCreate({ label, cwd, workspaceId, env });
425
+ let paneId;
426
+ try {
427
+ ({ paneId } = await mod.agentStart({ name: label, cwd, argv, env, workspaceId, tabId }));
428
+ } catch (err) {
429
+ await mod.tabClose(tabId).catch(() => {
430
+ });
431
+ throw err;
432
+ }
433
+ await mod.paneClose(shellPaneId).catch((err) => {
434
+ console.error(`[herdr-runner] failed to close the placeholder shell pane ${shellPaneId}: ${err}`);
435
+ });
436
+ return { paneId, tabId };
437
+ }
438
+ async function waitForHerdrTask(paneId, options) {
439
+ const mod = options?.herdr ?? await loadHerdr();
440
+ const pollIntervalMs = options?.pollIntervalMs ?? AGENT_POLL_INTERVAL_MS;
441
+ let tracker = createCompletionTracker();
442
+ for (; ; ) {
443
+ if (options?.signal?.aborted) {
444
+ return { status: "failed", output: "[worker] the worker is shutting down; the task was interrupted" };
445
+ }
446
+ let status;
447
+ try {
448
+ status = (await mod.agentGet(paneId)).agentStatus;
449
+ } catch (err) {
450
+ if (err instanceof mod.HerdrError && err.code === "pane_not_found") {
451
+ return {
452
+ status: "failed",
453
+ output: "[worker] the claude pane disappeared before the task completed (claude died or the tab was closed)"
454
+ };
455
+ }
456
+ console.error(`[herdr-runner] failed to read agent status for pane ${paneId}: ${err}`);
457
+ await sleep(pollIntervalMs);
458
+ continue;
459
+ }
460
+ options?.onStatus?.(status);
461
+ const observed = observeAgentStatus(tracker, status);
462
+ tracker = observed.tracker;
463
+ if (observed.decision === "completed") {
464
+ const output = await readPaneOutput(paneId, mod);
465
+ return buildHerdrTaskResult(output);
466
+ }
467
+ if (observed.decision === "blocked-first-seen") {
468
+ options?.onBlocked?.();
469
+ }
470
+ await sleep(pollIntervalMs);
471
+ }
472
+ }
473
+ async function readPaneOutput(paneId, mod) {
474
+ try {
475
+ return await mod.paneRead(paneId, { source: "recent", lines: PANE_OUTPUT_LINES });
476
+ } catch (err) {
477
+ console.error(`[herdr-runner] failed to read pane ${paneId}: ${err}`);
478
+ return "";
479
+ }
480
+ }
481
+ async function waitForPaneGone(paneId, mod, options) {
482
+ const timeoutMs = options?.timeoutMs ?? CLAUDE_EXIT_TIMEOUT_MS;
483
+ const pollIntervalMs = options?.pollIntervalMs ?? CLAUDE_EXIT_POLL_INTERVAL_MS;
484
+ const deadline = Date.now() + timeoutMs;
485
+ for (; ; ) {
486
+ try {
487
+ await mod.paneGet(paneId);
488
+ } catch (err) {
489
+ if (err instanceof mod.HerdrError && err.code === "pane_not_found") return true;
490
+ }
491
+ if (Date.now() >= deadline) return false;
492
+ await sleep(pollIntervalMs);
493
+ }
494
+ }
495
+ async function stopHerdrTask(task, herdr, options) {
496
+ const mod = herdr ?? await loadHerdr();
497
+ try {
498
+ await mod.paneSendKeys(task.paneId, "ctrl+c", "ctrl+c");
499
+ const exited = await waitForPaneGone(task.paneId, mod, {
500
+ timeoutMs: options?.exitTimeoutMs,
501
+ pollIntervalMs: options?.exitPollIntervalMs
502
+ });
503
+ if (!exited) {
504
+ console.warn(
505
+ `[herdr-runner] claude did not exit in pane ${task.paneId} after ctrl-c, closing the tab forcefully`
506
+ );
507
+ }
508
+ } catch (err) {
509
+ console.error(`[herdr-runner] failed to send ctrl-c to pane ${task.paneId}: ${err}`);
510
+ }
511
+ try {
512
+ await mod.tabClose(task.tabId);
513
+ } catch (err) {
514
+ if (err instanceof mod.HerdrError && (err.code === "tab_not_found" || err.code === "pane_not_found")) return;
515
+ console.error(`[herdr-runner] failed to close tab ${task.tabId}: ${err}`);
516
+ }
517
+ }
518
+ function sleep(ms) {
519
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
520
+ }
521
+ var AGENT_POLL_INTERVAL_MS, PANE_OUTPUT_LINES, CLAUDE_EXIT_TIMEOUT_MS, CLAUDE_EXIT_POLL_INTERVAL_MS;
522
+ var init_herdr_runner = __esm({
523
+ "src/herdr-runner.ts"() {
524
+ "use strict";
525
+ AGENT_POLL_INTERVAL_MS = 3 * 1e3;
526
+ PANE_OUTPUT_LINES = 300;
527
+ CLAUDE_EXIT_TIMEOUT_MS = 15 * 1e3;
528
+ CLAUDE_EXIT_POLL_INTERVAL_MS = 200;
529
+ }
530
+ });
531
+
236
532
  // src/dispatcher.ts
237
533
  var dispatcher_exports = {};
238
534
  __export(dispatcher_exports, {
535
+ LABEL_PREFIX: () => LABEL_PREFIX,
239
536
  PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS,
240
537
  PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS,
241
538
  POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
242
539
  SEND_MAX_ATTEMPTS: () => SEND_MAX_ATTEMPTS,
243
540
  SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
244
541
  SHUTDOWN_TIMEOUT_MS: () => SHUTDOWN_TIMEOUT_MS,
245
- TAB_LABEL_PREFIX: () => TAB_LABEL_PREFIX,
246
542
  WORKER_STARTUP_POLL_INTERVAL_MS: () => WORKER_STARTUP_POLL_INTERVAL_MS,
247
543
  WORKER_STARTUP_TIMEOUT_MS: () => WORKER_STARTUP_TIMEOUT_MS,
248
544
  createDispatcherShutdownHandler: () => createDispatcherShutdownHandler,
@@ -256,21 +552,21 @@ __export(dispatcher_exports, {
256
552
  runDispatcher: () => runDispatcher,
257
553
  shutdownDispatcher: () => shutdownDispatcher,
258
554
  startWorkerInPane: () => startWorkerInPane,
259
- tabLabelFor: () => tabLabelFor,
260
555
  waitForPaneReady: () => waitForPaneReady,
261
- waitForWorkerStartup: () => waitForWorkerStartup
556
+ waitForWorkerStartup: () => waitForWorkerStartup,
557
+ workspaceLabelFor: () => workspaceLabelFor
262
558
  });
263
- async function loadHerdr() {
559
+ async function loadHerdr2() {
264
560
  return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
265
561
  }
266
562
  async function loadTable() {
267
563
  return await Promise.resolve().then(() => (init_table(), table_exports));
268
564
  }
269
- function tabLabelFor(projectName) {
270
- return `${TAB_LABEL_PREFIX}${projectName}`;
565
+ function workspaceLabelFor(projectName) {
566
+ return `${LABEL_PREFIX}${projectName}`;
271
567
  }
272
- function sleep(ms) {
273
- return new Promise((resolve2) => setTimeout(resolve2, ms));
568
+ function sleep2(ms) {
569
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
274
570
  }
275
571
  function isWorkerProcess(process2) {
276
572
  return process2.cmdline?.includes("claude-task-worker") ?? false;
@@ -286,7 +582,7 @@ async function waitForPaneReady(paneId, herdr, options) {
286
582
  const content = await herdr.paneRead(paneId, { source: "visible" });
287
583
  if (content.trim() !== "") return true;
288
584
  if (Date.now() >= deadline) return false;
289
- await sleep(pollIntervalMs);
585
+ await sleep2(pollIntervalMs);
290
586
  }
291
587
  }
292
588
  async function waitForWorkerStartup(paneId, herdr, options) {
@@ -298,7 +594,7 @@ async function waitForWorkerStartup(paneId, herdr, options) {
298
594
  if (foregroundProcesses.some(isWorkerProcess)) return "started";
299
595
  if (foregroundProcesses.length > 0 && !foregroundProcesses.every(isShellProcess)) return "other";
300
596
  if (Date.now() >= deadline) return "shell";
301
- await sleep(pollIntervalMs);
597
+ await sleep2(pollIntervalMs);
302
598
  }
303
599
  }
304
600
  async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
@@ -333,8 +629,8 @@ async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
333
629
  return false;
334
630
  }
335
631
  async function runDispatcher(projects, forwardedCommand, timing) {
336
- const herdr = await loadHerdr();
337
- const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2 } = herdr;
632
+ const herdr = await loadHerdr2();
633
+ const { checkHerdrAvailable: checkHerdrAvailable2, workspaceCreate: workspaceCreate2, workspaceClose: workspaceClose2, workspaceList: workspaceList2, tabRename: tabRename2 } = herdr;
338
634
  try {
339
635
  await checkHerdrAvailable2();
340
636
  } catch (error) {
@@ -342,20 +638,29 @@ async function runDispatcher(projects, forwardedCommand, timing) {
342
638
  console.error(`[dispatcher] ${message}`);
343
639
  throw error;
344
640
  }
345
- const existingTabs = await tabList2();
346
- const existingLabels = new Set(existingTabs.map((tab) => tab.label));
641
+ const existingWorkspaces = await workspaceList2();
642
+ const existingLabels = new Set(existingWorkspaces.map((workspace) => workspace.label));
347
643
  const sessions = /* @__PURE__ */ new Map();
348
644
  for (const project of projects) {
349
- if (existingLabels.has(tabLabelFor(project.name))) {
350
- console.warn(`[dispatcher] project "${project.name}" already has a running tab, skipping`);
645
+ const label = workspaceLabelFor(project.name);
646
+ if (existingLabels.has(label)) {
647
+ console.warn(`[dispatcher] project "${project.name}" already has a running workspace, skipping`);
351
648
  continue;
352
649
  }
353
- let createdTabId;
650
+ let createdWorkspaceId;
354
651
  try {
355
- const { paneId, tabId } = await tabCreate2({ label: tabLabelFor(project.name), cwd: project.path });
356
- createdTabId = tabId;
652
+ const { workspaceId, tabId, paneId } = await workspaceCreate2({
653
+ label,
654
+ cwd: project.path,
655
+ env: { CTW_PROJECT_NAME: project.name }
656
+ });
657
+ createdWorkspaceId = workspaceId;
658
+ await tabRename2(tabId, label).catch(
659
+ (error) => console.error(`[dispatcher] failed to rename the root tab for project "${project.name}": ${error}`)
660
+ );
357
661
  sessions.set(project.name, {
358
662
  name: project.name,
663
+ workspaceId,
359
664
  tabId,
360
665
  paneId,
361
666
  startedAt: /* @__PURE__ */ new Date(),
@@ -370,24 +675,24 @@ async function runDispatcher(projects, forwardedCommand, timing) {
370
675
  } catch (error) {
371
676
  console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
372
677
  sessions.delete(project.name);
373
- if (createdTabId !== void 0) {
678
+ if (createdWorkspaceId !== void 0) {
374
679
  try {
375
- await tabClose2(createdTabId);
680
+ await workspaceClose2(createdWorkspaceId);
376
681
  } catch (closeError) {
377
- console.error(`[dispatcher] failed to close dangling tab for project "${project.name}": ${closeError}`);
682
+ console.error(`[dispatcher] failed to close dangling workspace for project "${project.name}": ${closeError}`);
378
683
  }
379
684
  }
380
685
  }
381
686
  }
382
687
  return sessions;
383
688
  }
384
- async function removeSession(sessions, name, { closeTab }) {
689
+ async function removeSession(sessions, name, { closeWorkspace }) {
385
690
  const session = sessions.get(name);
386
691
  if (!session) return;
387
692
  sessions.delete(name);
388
- if (closeTab) {
389
- const { tabClose: tabClose2 } = await loadHerdr();
390
- await tabClose2(session.tabId);
693
+ if (closeWorkspace) {
694
+ const { workspaceClose: workspaceClose2 } = await loadHerdr2();
695
+ await workspaceClose2(session.workspaceId);
391
696
  }
392
697
  }
393
698
  async function pollOnce(sessions, herdr) {
@@ -395,11 +700,11 @@ async function pollOnce(sessions, herdr) {
395
700
  try {
396
701
  const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
397
702
  if (!foregroundProcesses.some(isWorkerProcess)) {
398
- await removeSession(sessions, name, { closeTab: true });
703
+ await removeSession(sessions, name, { closeWorkspace: true });
399
704
  }
400
705
  } catch (error) {
401
706
  if (error instanceof herdr.HerdrError && error.code === "pane_not_found") {
402
- await removeSession(sessions, name, { closeTab: false });
707
+ await removeSession(sessions, name, { closeWorkspace: false });
403
708
  continue;
404
709
  }
405
710
  console.error(`[dispatcher] failed to poll session "${name}": ${error}`);
@@ -419,27 +724,27 @@ function renderSessionTable(sessions) {
419
724
  const maxProjectWidth = 20;
420
725
  const rows = entries.map((session) => ({
421
726
  project: getDisplayWidth2(session.name) > maxProjectWidth ? truncateToWidth2(session.name, maxProjectWidth) : session.name,
422
- tab: session.tabId,
727
+ workspace: session.workspaceId,
423
728
  pane: session.paneId,
424
729
  status: session.status,
425
730
  uptime: formatUptime(session.startedAt, /* @__PURE__ */ new Date())
426
731
  }));
427
732
  const colWidths = {
428
733
  project: Math.max(7, ...rows.map((r) => getDisplayWidth2(r.project))),
429
- tab: Math.max(3, ...rows.map((r) => r.tab.length)),
734
+ workspace: Math.max(9, ...rows.map((r) => r.workspace.length)),
430
735
  pane: Math.max(4, ...rows.map((r) => r.pane.length)),
431
736
  status: Math.max(6, ...rows.map((r) => r.status.length)),
432
737
  uptime: Math.max(6, ...rows.map((r) => r.uptime.length))
433
738
  };
434
- const cols = [colWidths.project, colWidths.tab, colWidths.pane, colWidths.status, colWidths.uptime];
739
+ const cols = [colWidths.project, colWidths.workspace, colWidths.pane, colWidths.status, colWidths.uptime];
435
740
  const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
436
- const row = (project, tab, pane, status, uptime) => `\u2502 ${padToWidth2(project, colWidths.project)} \u2502 ${tab.padEnd(colWidths.tab)} \u2502 ${pane.padEnd(colWidths.pane)} \u2502 ${status.padEnd(colWidths.status)} \u2502 ${uptime.padEnd(colWidths.uptime)} \u2502`;
741
+ 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
742
  const lines = [];
438
743
  lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
439
- lines.push(row("Project", "Tab", "Pane", "Status", "Uptime"));
744
+ lines.push(row("Project", "Workspace", "Pane", "Status", "Uptime"));
440
745
  lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
441
746
  for (const r of rows) {
442
- lines.push(row(r.project, r.tab, r.pane, r.status, r.uptime));
747
+ lines.push(row(r.project, r.workspace, r.pane, r.status, r.uptime));
443
748
  }
444
749
  lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
445
750
  console.clear();
@@ -449,8 +754,8 @@ function monitorSessions(sessions, herdr, options) {
449
754
  const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
450
755
  const renderIntervalMs = options?.renderIntervalMs ?? 1e3;
451
756
  let resolveDone;
452
- const done = new Promise((resolve2) => {
453
- resolveDone = resolve2;
757
+ const done = new Promise((resolve3) => {
758
+ resolveDone = resolve3;
454
759
  });
455
760
  let settled = false;
456
761
  const finish = () => {
@@ -492,24 +797,24 @@ async function waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, timeoutMs
492
797
  await pollOnce(sessions, herdr);
493
798
  if (sessions.size === 0) return true;
494
799
  if (Date.now() >= deadline) return false;
495
- await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
800
+ await new Promise((resolve3) => setTimeout(resolve3, pollIntervalMs));
496
801
  }
497
802
  return true;
498
803
  }
499
- async function closeRemainingTabs(sessions, herdr, timeoutMs) {
804
+ async function closeRemainingWorkspaces(sessions, herdr, timeoutMs) {
500
805
  const results = await Promise.all(
501
806
  [...sessions.values()].map(async (session) => {
502
807
  try {
503
808
  await Promise.race([
504
- herdr.tabClose(session.tabId),
809
+ herdr.workspaceClose(session.workspaceId),
505
810
  new Promise((_, reject) => {
506
- setTimeout(() => reject(new Error("tabClose timed out")), timeoutMs).unref();
811
+ setTimeout(() => reject(new Error("workspaceClose timed out")), timeoutMs).unref();
507
812
  })
508
813
  ]);
509
814
  return true;
510
815
  } catch (error) {
511
816
  console.error(
512
- `[dispatcher] failed to close tab "${session.tabId}" for session "${session.name}" during shutdown: ${error}`
817
+ `[dispatcher] failed to close workspace "${session.workspaceId}" for session "${session.name}" during shutdown: ${error}`
513
818
  );
514
819
  return false;
515
820
  }
@@ -517,29 +822,29 @@ async function closeRemainingTabs(sessions, herdr, timeoutMs) {
517
822
  );
518
823
  return results.every((closed) => closed);
519
824
  }
520
- async function forceKillAllSessions(sessions, herdr, tabCloseTimeoutMs) {
825
+ async function forceKillAllSessions(sessions, herdr, workspaceCloseTimeoutMs) {
521
826
  console.log(`[dispatcher] force-kill requested, sending ctrl-c and closing ${sessions.size} session(s) immediately`);
522
827
  await sendCtrlCToAllSessions(sessions, herdr);
523
- await closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
828
+ await closeRemainingWorkspaces(sessions, herdr, workspaceCloseTimeoutMs);
524
829
  process.exit(1);
525
830
  }
526
831
  async function shutdownDispatcher(sessions, monitorHandle, options) {
527
832
  if (shutdownPromise) {
528
833
  if (options?.forceKill) {
529
- const herdr = options.herdr ?? await loadHerdr();
530
- const tabCloseTimeoutMs = options.tabCloseTimeoutMs ?? 5e3;
531
- await forceKillAllSessions(sessions, herdr, tabCloseTimeoutMs);
834
+ const herdr = options.herdr ?? await loadHerdr2();
835
+ const workspaceCloseTimeoutMs = options.workspaceCloseTimeoutMs ?? 5e3;
836
+ await forceKillAllSessions(sessions, herdr, workspaceCloseTimeoutMs);
532
837
  return;
533
838
  }
534
839
  console.log("[dispatcher] shutdown already in progress, ignoring duplicate request");
535
840
  return shutdownPromise;
536
841
  }
537
842
  shutdownPromise = (async () => {
538
- const herdr = options?.herdr ?? await loadHerdr();
843
+ const herdr = options?.herdr ?? await loadHerdr2();
539
844
  const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
540
845
  const shutdownTimeoutMs = options?.shutdownTimeoutMs ?? SHUTDOWN_TIMEOUT_MS;
541
846
  const retryTimeoutMs = options?.retryTimeoutMs ?? SHUTDOWN_RETRY_TIMEOUT_MS;
542
- const tabCloseTimeoutMs = options?.tabCloseTimeoutMs ?? 5e3;
847
+ const workspaceCloseTimeoutMs = options?.workspaceCloseTimeoutMs ?? 5e3;
543
848
  if (monitorHandle) {
544
849
  monitorHandle.stop();
545
850
  await monitorHandle.done;
@@ -554,8 +859,8 @@ async function shutdownDispatcher(sessions, monitorHandle, options) {
554
859
  await sendCtrlCToAllSessions(sessions, herdr);
555
860
  finishedInTime = await waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, retryTimeoutMs);
556
861
  }
557
- const allTabsClosed = await closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
558
- process.exit(finishedInTime && allTabsClosed ? 0 : 1);
862
+ const allWorkspacesClosed = await closeRemainingWorkspaces(sessions, herdr, workspaceCloseTimeoutMs);
863
+ process.exit(finishedInTime && allWorkspacesClosed ? 0 : 1);
559
864
  })();
560
865
  try {
561
866
  await shutdownPromise;
@@ -590,7 +895,7 @@ function createDispatcherShutdownHandler(shutdown2) {
590
895
  };
591
896
  return { handle, isShuttingDown: () => shuttingDown2 };
592
897
  }
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, TAB_LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
898
+ 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
899
  var init_dispatcher = __esm({
595
900
  async "src/dispatcher.ts"() {
596
901
  "use strict";
@@ -602,7 +907,7 @@ var init_dispatcher = __esm({
602
907
  WORKER_STARTUP_TIMEOUT_MS = 30 * 1e3;
603
908
  WORKER_STARTUP_POLL_INTERVAL_MS = 500;
604
909
  SEND_MAX_ATTEMPTS = 3;
605
- TAB_LABEL_PREFIX = "ctw:";
910
+ LABEL_PREFIX = "ctw:";
606
911
  SHELL_NAME_PATTERN = /^-?(zsh|bash|sh)$/;
607
912
  SHUTDOWN_RETRY_TIMEOUT_MS = 90 * 1e3;
608
913
  CTRL_C_KEY = "ctrl+c";
@@ -613,29 +918,29 @@ var init_dispatcher = __esm({
613
918
  import { createRequire } from "node:module";
614
919
  var childProcess = createRequire(import.meta.url)("node:child_process");
615
920
  function execGh(args) {
616
- return new Promise((resolve2, reject) => {
921
+ return new Promise((resolve3, reject) => {
617
922
  childProcess.execFile("gh", args, (error, stdout, stderr) => {
618
923
  if (error) {
619
924
  reject(new Error(`gh ${args.join(" ")} failed: ${stderr || error.message}`));
620
925
  return;
621
926
  }
622
- resolve2(stdout.trim());
927
+ resolve3(stdout.trim());
623
928
  });
624
929
  });
625
930
  }
626
931
  function execGhAllowExit(args, allowedCodes) {
627
- return new Promise((resolve2, reject) => {
932
+ return new Promise((resolve3, reject) => {
628
933
  childProcess.execFile("gh", args, (error, stdout, stderr) => {
629
934
  if (error) {
630
935
  const code = error.code;
631
936
  if (typeof code === "number" && allowedCodes.includes(code)) {
632
- resolve2(stdout.trim());
937
+ resolve3(stdout.trim());
633
938
  return;
634
939
  }
635
940
  reject(new Error(`gh ${args.join(" ")} failed: ${stderr || error.message}`));
636
941
  return;
637
942
  }
638
- resolve2(stdout.trim());
943
+ resolve3(stdout.trim());
639
944
  });
640
945
  });
641
946
  }
@@ -820,7 +1125,7 @@ async function withRetry(fn, maxRetries = 5, baseDelayMs = 1e3) {
820
1125
  if (attempt === maxRetries) throw err;
821
1126
  const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
822
1127
  console.error(`[gh] Attempt ${attempt}/${maxRetries} failed, retrying in ${delayMs}ms...`);
823
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1128
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
824
1129
  }
825
1130
  }
826
1131
  throw new Error("unreachable");
@@ -899,13 +1204,38 @@ var CLAUDE_SPAWN_ENV = {
899
1204
  CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1",
900
1205
  CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: "0"
901
1206
  };
902
- var SYSTEM_PROMPT = `\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F \`claude-task-worker\` \u306E\u30EF\u30FC\u30AB\u30FC\u304B\u3089 \`claude -p\`\uFF08\u975E\u5BFE\u8A71 print \u30E2\u30FC\u30C9\uFF09\u3067\u81EA\u52D5\u8D77\u52D5\u3055\u308C\u3066\u3044\u308B\u3002\u30E6\u30FC\u30B6\u30FC\u3078\u306E\u78BA\u8A8D\u30FB\u8CEA\u554F\u306F\u884C\u308F\u305A\uFF08\u56DE\u7B54\u3059\u308B\u30E6\u30FC\u30B6\u30FC\u306F\u5B58\u5728\u3057\u306A\u3044\uFF09\u3001\u8D77\u52D5\u3055\u308C\u305F\u30B9\u30AD\u30EB\u306E\u30EB\u30FC\u30EB\u306B\u5F93\u3063\u3066\u81EA\u5F8B\u7684\u306B\u5224\u65AD\u3057\u3001\u5168\u30B9\u30C6\u30C3\u30D7\u3092\u5B8C\u9042\u3057\u3066\u304B\u3089\uFF08\u307E\u305F\u306F\u4E2D\u65AD\u6761\u4EF6\u306B\u8A72\u5F53\u3057\u305F\u5834\u5408\u306F\u7406\u7531\u3092\u51FA\u529B\u3057\u3066\uFF09\u7D42\u4E86\u3059\u308B\u3053\u3068\u3002\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\u3002`;
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
1207
+ 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
1208
 
905
- - \u30E6\u30FC\u30B6\u30FC\u3078\u306E\u78BA\u8A8D\u30FB\u8CEA\u554F\u306F\u884C\u308F\u306A\u3044\uFF08\u56DE\u7B54\u3059\u308B\u30E6\u30FC\u30B6\u30FC\u306F\u5B58\u5728\u3057\u306A\u3044\uFF09\u3002\u5224\u65AD\u306F\u3059\u3079\u3066\u59D4\u8B72\u3055\u308C\u305F\u6307\u793A\uFF08\u30D7\u30ED\u30F3\u30D7\u30C8\u3067\u660E\u793A\u7684\u306B\u6E21\u3055\u308C\u305F\u5185\u5BB9\uFF09\u306B\u5F93\u3063\u3066\u81EA\u52D5\u3067\u6C7A\u5B9A\u3059\u308B
1209
+ - \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
1210
  - \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
- - \u59D4\u8B72\u3055\u308C\u305F\u30BF\u30B9\u30AF\u306F\u6700\u5F8C\u307E\u3067\u5B8C\u9042\u3057\u3066\u304B\u3089\u6700\u7D42\u5831\u544A\u3059\u308B\u3002\u6307\u793A\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
908
- - \u5B50\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u306B\u4F5C\u696D\u3092\u59D4\u8B72\u3057\u305F\u5834\u5408\u3001\u305D\u306E\u5B8C\u4E86\u5831\u544A\u3092\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`;
1211
+ - \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
1212
+ - \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
1213
+ - \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`;
1214
+ function buildClaudeArgs({ mode, prompt, model, effort }) {
1215
+ return [
1216
+ ...mode === "herdr" ? [] : ["-p"],
1217
+ prompt,
1218
+ "--dangerously-skip-permissions",
1219
+ "--chrome",
1220
+ "--disallowedTools",
1221
+ DISALLOWED_TOOLS_ARG,
1222
+ "--append-system-prompt",
1223
+ SYSTEM_PROMPT,
1224
+ "--model",
1225
+ model,
1226
+ "--effort",
1227
+ effort
1228
+ ];
1229
+ }
1230
+ function buildClaudeEnv(mode) {
1231
+ if (mode === "herdr") {
1232
+ return {
1233
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: CLAUDE_SPAWN_ENV.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS,
1234
+ HERDR_DISABLE_SOUND: "1"
1235
+ };
1236
+ }
1237
+ return { ...CLAUDE_SPAWN_ENV };
1238
+ }
909
1239
 
910
1240
  // src/config.ts
911
1241
  import { readFileSync } from "node:fs";
@@ -1150,6 +1480,7 @@ async function assertRemoteTrackingExists(epicBranch) {
1150
1480
 
1151
1481
  // src/process-manager.ts
1152
1482
  import { spawn } from "node:child_process";
1483
+ import { basename } from "node:path";
1153
1484
  init_table();
1154
1485
 
1155
1486
  // src/task-result.ts
@@ -1171,8 +1502,200 @@ function buildTaskResult(code, stdout, stderrTail) {
1171
1502
  return { status: completed ? "completed" : "failed", output };
1172
1503
  }
1173
1504
 
1505
+ // src/user-config.ts
1506
+ import { readFileSync as readFileSync2, statSync } from "node:fs";
1507
+ import { homedir } from "node:os";
1508
+ import { isAbsolute, join as join2, resolve } from "node:path";
1509
+ var RESERVED_ALL = "all";
1510
+ var DEFAULT_RUN_MODE = "default";
1511
+ var UserConfigError = class extends Error {
1512
+ constructor(message) {
1513
+ super(message);
1514
+ this.name = "UserConfigError";
1515
+ }
1516
+ };
1517
+ function getConfigDir() {
1518
+ const xdg = process.env.XDG_CONFIG_HOME;
1519
+ const configHome = xdg && xdg.length > 0 ? xdg : join2(homedir(), ".config");
1520
+ return join2(configHome, "claude-task-worker");
1521
+ }
1522
+ function getUserConfigPath() {
1523
+ return join2(getConfigDir(), "config.json");
1524
+ }
1525
+ function isDirectory(path) {
1526
+ try {
1527
+ return statSync(path).isDirectory();
1528
+ } catch {
1529
+ return false;
1530
+ }
1531
+ }
1532
+ function parseConfigFile(path) {
1533
+ let content;
1534
+ try {
1535
+ content = readFileSync2(path, "utf-8");
1536
+ } catch (err) {
1537
+ if (err.code === "ENOENT") return void 0;
1538
+ throw err;
1539
+ }
1540
+ try {
1541
+ return JSON.parse(content);
1542
+ } catch (err) {
1543
+ if (err instanceof SyntaxError) {
1544
+ throw new UserConfigError(`config file contains invalid JSON: ${path}: ${err.message}`);
1545
+ }
1546
+ throw err;
1547
+ }
1548
+ }
1549
+ function readRawConfig() {
1550
+ return parseConfigFile(getUserConfigPath());
1551
+ }
1552
+ function parseMode(raw, path) {
1553
+ if (!("mode" in raw)) return DEFAULT_RUN_MODE;
1554
+ const value = raw["mode"];
1555
+ if (value === "default" || value === "herdr") return value;
1556
+ console.warn(`[config] invalid mode: ${JSON.stringify(value)} in ${path}, using "${DEFAULT_RUN_MODE}"`);
1557
+ return DEFAULT_RUN_MODE;
1558
+ }
1559
+ function loadUserConfig() {
1560
+ const path = getUserConfigPath();
1561
+ const raw = readRawConfig();
1562
+ if (raw === void 0) {
1563
+ throw new UserConfigError(`config.json not found: ${path}`);
1564
+ }
1565
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1566
+ throw new UserConfigError(`config.json must contain a JSON object: ${path}`);
1567
+ }
1568
+ if (!("projects" in raw) || typeof raw["projects"] !== "object" || raw["projects"] === null || Array.isArray(raw["projects"])) {
1569
+ throw new UserConfigError(`config.json must contain a "projects" section as an object: ${path}`);
1570
+ }
1571
+ if ("projectGroups" in raw && (typeof raw["projectGroups"] !== "object" || raw["projectGroups"] === null || Array.isArray(raw["projectGroups"]))) {
1572
+ throw new UserConfigError(`config.json "projectGroups" must be an object: ${path}`);
1573
+ }
1574
+ const mode = parseMode(raw, path);
1575
+ const rawProjects = raw["projects"];
1576
+ const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
1577
+ const projectKeys = Object.keys(rawProjects);
1578
+ const groupKeys = Object.keys(rawProjectGroups);
1579
+ if (projectKeys.includes(RESERVED_ALL) || groupKeys.includes(RESERVED_ALL)) {
1580
+ throw new UserConfigError(
1581
+ `"${RESERVED_ALL}" is a reserved word and cannot be used as a key in "projects" or "projectGroups"`
1582
+ );
1583
+ }
1584
+ const groupKeySet = new Set(groupKeys);
1585
+ const duplicateKeys = projectKeys.filter((key) => groupKeySet.has(key));
1586
+ if (duplicateKeys.length > 0) {
1587
+ throw new UserConfigError(
1588
+ `"projects" and "projectGroups" share the same key namespace; duplicate keys are not allowed: ${duplicateKeys.join(", ")}`
1589
+ );
1590
+ }
1591
+ const projects = {};
1592
+ for (const [name, value] of Object.entries(rawProjects)) {
1593
+ if (name === "__proto__") {
1594
+ throw new UserConfigError(`"__proto__" cannot be used as a key in "projects": ${path}`);
1595
+ }
1596
+ if (typeof value !== "string" || !isAbsolute(value)) {
1597
+ console.warn(`[config] invalid projects.${name}: expected an absolute path, skipping`);
1598
+ continue;
1599
+ }
1600
+ if (!isDirectory(value)) {
1601
+ console.warn(`[config] projects.${name} does not exist as a directory: ${value}, skipping`);
1602
+ continue;
1603
+ }
1604
+ projects[name] = value;
1605
+ }
1606
+ const projectGroups = {};
1607
+ for (const [groupName, value] of Object.entries(rawProjectGroups)) {
1608
+ if (groupName === "__proto__") {
1609
+ throw new UserConfigError(`"__proto__" cannot be used as a key in "projectGroups": ${path}`);
1610
+ }
1611
+ if (!Array.isArray(value)) {
1612
+ console.warn(`[config] invalid projectGroups.${groupName}: expected an array, skipping`);
1613
+ continue;
1614
+ }
1615
+ const members = [];
1616
+ for (const member of value) {
1617
+ if (typeof member !== "string" || !Object.prototype.hasOwnProperty.call(projects, member)) {
1618
+ console.warn(`[config] projectGroups.${groupName} references unknown project "${String(member)}", skipping`);
1619
+ continue;
1620
+ }
1621
+ members.push(member);
1622
+ }
1623
+ projectGroups[groupName] = members;
1624
+ }
1625
+ return { mode, projects, projectGroups };
1626
+ }
1627
+ var cachedRunMode;
1628
+ function getRunMode() {
1629
+ if (cachedRunMode === void 0) {
1630
+ cachedRunMode = readRunMode();
1631
+ }
1632
+ return cachedRunMode;
1633
+ }
1634
+ function readRunMode() {
1635
+ let raw;
1636
+ try {
1637
+ raw = readRawConfig();
1638
+ } catch (err) {
1639
+ console.warn(`[config] failed to read config file, using "${DEFAULT_RUN_MODE}" mode: ${err}`);
1640
+ return DEFAULT_RUN_MODE;
1641
+ }
1642
+ if (raw === void 0) return DEFAULT_RUN_MODE;
1643
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1644
+ return DEFAULT_RUN_MODE;
1645
+ }
1646
+ return parseMode(raw, getUserConfigPath());
1647
+ }
1648
+ function findProjectNameByPath(path) {
1649
+ let config;
1650
+ try {
1651
+ config = loadUserConfig();
1652
+ } catch {
1653
+ return void 0;
1654
+ }
1655
+ const target = resolve(path);
1656
+ for (const [name, projectPath] of Object.entries(config.projects)) {
1657
+ if (resolve(projectPath) === target) return name;
1658
+ }
1659
+ return void 0;
1660
+ }
1661
+ function resolveTargetProjects(requested, config) {
1662
+ const resolved = /* @__PURE__ */ new Map();
1663
+ for (const name of requested) {
1664
+ if (name === RESERVED_ALL) {
1665
+ for (const [projectName, projectPath] of Object.entries(config.projects)) {
1666
+ resolved.set(projectName, { name: projectName, path: projectPath });
1667
+ }
1668
+ continue;
1669
+ }
1670
+ if (Object.prototype.hasOwnProperty.call(config.projects, name)) {
1671
+ resolved.set(name, { name, path: config.projects[name] });
1672
+ continue;
1673
+ }
1674
+ if (Object.prototype.hasOwnProperty.call(config.projectGroups, name)) {
1675
+ for (const projectName of config.projectGroups[name]) {
1676
+ const projectPath = config.projects[projectName];
1677
+ if (projectPath === void 0) continue;
1678
+ resolved.set(projectName, { name: projectName, path: projectPath });
1679
+ }
1680
+ continue;
1681
+ }
1682
+ const availableProjects = Object.keys(config.projects).join(", ") || "(none)";
1683
+ const availableGroups = Object.keys(config.projectGroups).join(", ") || "(none)";
1684
+ throw new UserConfigError(
1685
+ `Unknown project or group: "${name}". Available projects: ${availableProjects}. Available groups: ${availableGroups}.`
1686
+ );
1687
+ }
1688
+ const resolvedProjects = Array.from(resolved.values());
1689
+ if (resolvedProjects.length === 0) {
1690
+ throw new UserConfigError(`No projects resolved from requested targets: ${requested.join(", ")}`);
1691
+ }
1692
+ return resolvedProjects;
1693
+ }
1694
+
1174
1695
  // src/process-manager.ts
1175
1696
  var childProcesses = /* @__PURE__ */ new Map();
1697
+ var herdrTasks = /* @__PURE__ */ new Map();
1698
+ var herdrAbortSignal = { aborted: false };
1176
1699
  var tasks = /* @__PURE__ */ new Map();
1177
1700
  var shuttingDown = false;
1178
1701
  function setShuttingDown() {
@@ -1228,7 +1751,9 @@ function renderTable() {
1228
1751
  title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
1229
1752
  worker: t.workerName,
1230
1753
  path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
1231
- status: t.status,
1754
+ // herdr モードでは agent ステータス(working / blocked 等)を併記し、
1755
+ // 人の介入が必要な blocked にも気づけるようにする。
1756
+ status: t.agentStatus ? `${t.status}:${t.agentStatus}` : t.status,
1232
1757
  time: formatTime(t.startedAt),
1233
1758
  duration: formatDuration(t.startedAt)
1234
1759
  })),
@@ -1287,6 +1812,65 @@ function ensureRenderInterval() {
1287
1812
  renderInterval = setInterval(renderTable, 1e3);
1288
1813
  renderInterval.unref();
1289
1814
  }
1815
+ async function finishTask(id, result, onComplete) {
1816
+ try {
1817
+ await Promise.race([
1818
+ onComplete?.(result.status, result.output) ?? Promise.resolve(),
1819
+ new Promise(
1820
+ (_, reject) => setTimeout(() => reject(new Error("onComplete timed out after 120s")), 12e4).unref()
1821
+ )
1822
+ ]);
1823
+ } catch (err) {
1824
+ console.error(`[worker] onComplete error for #${id}: ${err}`);
1825
+ }
1826
+ const task = tasks.get(id);
1827
+ if (task) {
1828
+ task.status = result.status;
1829
+ task.finishedAt = /* @__PURE__ */ new Date();
1830
+ task.agentStatus = void 0;
1831
+ }
1832
+ renderTable();
1833
+ }
1834
+ function resolveProjectName(cwd = process.cwd()) {
1835
+ const injected = process.env.CTW_PROJECT_NAME;
1836
+ if (injected && injected.length > 0) return injected;
1837
+ return findProjectNameByPath(cwd) ?? basename(cwd);
1838
+ }
1839
+ async function runViaHerdr(command, args, id, onComplete, cwd, env) {
1840
+ const { startHerdrTask: startHerdrTask2, stopHerdrTask: stopHerdrTask2, taskTabLabel: taskTabLabel2, waitForHerdrTask: waitForHerdrTask2 } = await Promise.resolve().then(() => (init_herdr_runner(), herdr_runner_exports));
1841
+ const { getCurrentWorkspaceId: getCurrentWorkspaceId2 } = await Promise.resolve().then(() => (init_herdr(), herdr_exports));
1842
+ const label = taskTabLabel2(resolveProjectName(), id);
1843
+ let task;
1844
+ let result;
1845
+ herdrTasks.set(id, { paneId: "", tabId: "" });
1846
+ try {
1847
+ task = await startHerdrTask2({
1848
+ label,
1849
+ cwd: cwd ?? process.cwd(),
1850
+ argv: [command, ...args],
1851
+ env,
1852
+ workspaceId: getCurrentWorkspaceId2()
1853
+ });
1854
+ herdrTasks.set(id, task);
1855
+ result = await waitForHerdrTask2(task.paneId, {
1856
+ signal: herdrAbortSignal,
1857
+ onBlocked: () => console.warn(`[worker] #${id} is blocked and waiting for input in herdr tab "${label}"`),
1858
+ onStatus: (status) => {
1859
+ const task2 = tasks.get(id);
1860
+ if (task2 && task2.status === "running") task2.agentStatus = status;
1861
+ }
1862
+ });
1863
+ } catch (err) {
1864
+ console.error(`[worker] failed to run #${id} via herdr: ${err}`);
1865
+ result = { status: "failed", output: `[worker] failed to run the task via herdr: ${err}` };
1866
+ } finally {
1867
+ if (task) {
1868
+ await stopHerdrTask2(task);
1869
+ }
1870
+ }
1871
+ await finishTask(id, result, onComplete);
1872
+ herdrTasks.delete(id);
1873
+ }
1290
1874
  function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
1291
1875
  tasks.set(id, {
1292
1876
  id,
@@ -1298,6 +1882,10 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
1298
1882
  });
1299
1883
  ensureRenderInterval();
1300
1884
  renderTable();
1885
+ if (getRunMode() === "herdr") {
1886
+ void runViaHerdr(command, args, id, onComplete, cwd, env);
1887
+ return;
1888
+ }
1301
1889
  const child = spawn(command, args, {
1302
1890
  stdio: ["ignore", "pipe", "pipe"],
1303
1891
  detached: true,
@@ -1320,55 +1908,25 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
1320
1908
  }
1321
1909
  });
1322
1910
  child.on("close", async (code) => {
1323
- const { status: finalStatus, output } = buildTaskResult(
1911
+ const result = buildTaskResult(
1324
1912
  code,
1325
1913
  Buffer.concat(outputChunks).toString("utf-8"),
1326
1914
  Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
1327
1915
  );
1328
- try {
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
- }
1916
+ await finishTask(id, result, onComplete);
1343
1917
  childProcesses.delete(id);
1344
- renderTable();
1345
1918
  });
1346
1919
  child.on("error", async (err) => {
1347
1920
  console.error(`[worker] failed to spawn process for #${id}: ${err.message}`);
1348
- try {
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
- }
1921
+ await finishTask(id, { status: "failed", output: err.message }, onComplete);
1363
1922
  childProcesses.delete(id);
1364
- renderTable();
1365
1923
  });
1366
1924
  }
1367
1925
  function waitForAllProcesses() {
1368
- return new Promise((resolve2) => {
1926
+ return new Promise((resolve3) => {
1369
1927
  const check = () => {
1370
- if (childProcesses.size === 0) {
1371
- resolve2();
1928
+ if (childProcesses.size === 0 && herdrTasks.size === 0) {
1929
+ resolve3();
1372
1930
  } else {
1373
1931
  setTimeout(check, 500);
1374
1932
  }
@@ -1388,6 +1946,9 @@ function shutdown(signal = "SIGTERM") {
1388
1946
  }
1389
1947
  }
1390
1948
  }
1949
+ if (herdrTasks.size > 0) {
1950
+ herdrAbortSignal.aborted = true;
1951
+ }
1391
1952
  }
1392
1953
 
1393
1954
  // src/random-name.ts
@@ -1639,16 +2200,16 @@ function isGeneratedWorktreeName(name) {
1639
2200
 
1640
2201
  // src/slack.ts
1641
2202
  import { exec } from "node:child_process";
1642
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1643
- import { homedir as homedir2 } from "node:os";
1644
- import { join as join3 } from "node:path";
2203
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2204
+ import { homedir as homedir3 } from "node:os";
2205
+ import { join as join4 } from "node:path";
1645
2206
  import { promisify as promisify2 } from "node:util";
1646
2207
 
1647
2208
  // src/runcat.ts
1648
2209
  import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
1649
- import { homedir } from "node:os";
1650
- import { dirname, join as join2 } from "node:path";
1651
- var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join2(process.env.HOME ?? homedir(), ".claude", "runcat-usage.json");
2210
+ import { homedir as homedir2 } from "node:os";
2211
+ import { dirname, join as join3 } from "node:path";
2212
+ var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join3(process.env.HOME ?? homedir2(), ".claude", "runcat-usage.json");
1652
2213
  var JST = "Asia/Tokyo";
1653
2214
  function formatG(value) {
1654
2215
  return String(Number(value.toPrecision(6)));
@@ -1712,7 +2273,7 @@ function buildRuncatSnapshot(usage, now = /* @__PURE__ */ new Date()) {
1712
2273
  function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
1713
2274
  const snapshot = buildRuncatSnapshot(usage);
1714
2275
  const dir = dirname(outFile);
1715
- const tmp = join2(dir, `.runcat-${process.pid}-${Date.now()}.json`);
2276
+ const tmp = join3(dir, `.runcat-${process.pid}-${Date.now()}.json`);
1716
2277
  try {
1717
2278
  mkdirSync(dir, { recursive: true });
1718
2279
  writeFileSync(tmp, JSON.stringify(snapshot), "utf-8");
@@ -1749,13 +2310,13 @@ async function getOAuthToken() {
1749
2310
  const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
1750
2311
  return extractToken(JSON.parse(stdout.trim()));
1751
2312
  } catch {
1752
- const raw = readFileSync2(join3(homedir2(), ".claude", ".credentials.json"), "utf-8");
2313
+ const raw = readFileSync3(join4(homedir3(), ".claude", ".credentials.json"), "utf-8");
1753
2314
  return extractToken(JSON.parse(raw));
1754
2315
  }
1755
2316
  }
1756
2317
  function readUsageCache() {
1757
2318
  try {
1758
- const raw = readFileSync2(USAGE_CACHE_PATH, "utf-8");
2319
+ const raw = readFileSync3(USAGE_CACHE_PATH, "utf-8");
1759
2320
  const cached = JSON.parse(raw);
1760
2321
  if (Date.now() - cached.timestamp < USAGE_CACHE_TTL_SECONDS * 1e3) {
1761
2322
  return cached.data;
@@ -1857,11 +2418,11 @@ async function notifyError(workerName, repoName, error) {
1857
2418
  import { execFile as execFile2 } from "node:child_process";
1858
2419
  import { promisify as promisify3 } from "node:util";
1859
2420
  import { readdir, rm, stat } from "node:fs/promises";
1860
- import { basename, resolve, sep } from "node:path";
2421
+ import { basename as basename2, resolve as resolve2, sep } from "node:path";
1861
2422
  var execFileAsync2 = promisify3(execFile2);
1862
2423
  var WORKTREES_DIR = ".claude/worktrees";
1863
2424
  function isManagedWorktreePath(path) {
1864
- return resolve(path).startsWith(resolve(WORKTREES_DIR) + sep);
2425
+ return resolve2(path).startsWith(resolve2(WORKTREES_DIR) + sep);
1865
2426
  }
1866
2427
  async function pathExists(path) {
1867
2428
  try {
@@ -1948,7 +2509,7 @@ async function removeWorktree(worktreeId) {
1948
2509
  const worktreePath = getWorktreePath(worktreeId);
1949
2510
  let entry;
1950
2511
  try {
1951
- entry = (await listWorktreeEntries()).find((e) => resolve(e.path) === resolve(worktreePath));
2512
+ entry = (await listWorktreeEntries()).find((e) => resolve2(e.path) === resolve2(worktreePath));
1952
2513
  } catch (error) {
1953
2514
  console.error(`[worktree] Failed to list worktrees:`, error);
1954
2515
  }
@@ -1972,7 +2533,7 @@ async function removeWorktreeByBranch(branchName) {
1972
2533
  console.log(`[worktree] Skipping unmanaged worktree for branch ${branchName}: ${entry.path}`);
1973
2534
  continue;
1974
2535
  }
1975
- const worktreeId = basename(entry.path);
2536
+ const worktreeId = basename2(entry.path);
1976
2537
  if (isWorktreeInUse(worktreeId)) {
1977
2538
  console.log(`[worktree] Skipping worktree in use by a running task for branch ${branchName}: ${entry.path}`);
1978
2539
  continue;
@@ -1999,7 +2560,7 @@ async function removeStaleWorktrees() {
1999
2560
  try {
2000
2561
  for (const entry of await listWorktreeEntries()) {
2001
2562
  if (!isManagedWorktreePath(entry.path)) continue;
2002
- const worktreeId = basename(entry.path);
2563
+ const worktreeId = basename2(entry.path);
2003
2564
  if (isGeneratedWorktreeName(worktreeId)) worktreeIds.add(worktreeId);
2004
2565
  }
2005
2566
  } catch (error) {
@@ -2058,22 +2619,8 @@ function createIssuePollingWorker(config) {
2058
2619
  const { model, effort, skill } = getWorkerConfig(config.name);
2059
2620
  const command = skill || config.command;
2060
2621
  const parentNumber = issue.parent?.number;
2061
- const claudeArgs = [
2062
- "-p",
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
- ];
2622
+ const mode = getRunMode();
2623
+ const claudeArgs = buildClaudeArgs({ mode, prompt: `${command} ${issue.number}`, model, effort });
2077
2624
  let baseBranch = defaultBranch;
2078
2625
  if (parentNumber !== void 0) {
2079
2626
  baseBranch = `cc-epic-${parentNumber}`;
@@ -2129,7 +2676,7 @@ function createIssuePollingWorker(config) {
2129
2676
  }
2130
2677
  },
2131
2678
  cwd,
2132
- { ...CLAUDE_SPAWN_ENV }
2679
+ buildClaudeEnv(mode)
2133
2680
  );
2134
2681
  } catch (err) {
2135
2682
  console.error(`[${config.name}] setup error for #${issue.number}: ${err}`);
@@ -2238,24 +2785,10 @@ function createPrPollingWorker(config) {
2238
2785
  const cwd = getWorktreePath(worktreeId);
2239
2786
  const { model, effort, skill } = getWorkerConfig(config.name);
2240
2787
  const command = skill || config.command;
2788
+ const mode = getRunMode();
2241
2789
  run(
2242
2790
  "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
- ],
2791
+ buildClaudeArgs({ mode, prompt: `${command} ${pr.number}`, model, effort }),
2259
2792
  pr.number,
2260
2793
  `PR #${pr.number} (${pr.headRefName})`,
2261
2794
  config.name,
@@ -2298,7 +2831,7 @@ function createPrPollingWorker(config) {
2298
2831
  }
2299
2832
  },
2300
2833
  cwd,
2301
- { ...CLAUDE_SPAWN_ENV }
2834
+ buildClaudeEnv(mode)
2302
2835
  );
2303
2836
  } catch (err) {
2304
2837
  console.error(`[${config.name}] setup error for PR #${pr.number}: ${err}`);
@@ -2559,7 +3092,7 @@ async function init(options = {}) {
2559
3092
  // src/commands/run-command.ts
2560
3093
  import { spawn as spawn2 } from "node:child_process";
2561
3094
  function runCommand(command, args) {
2562
- return new Promise((resolve2, reject) => {
3095
+ return new Promise((resolve3, reject) => {
2563
3096
  const child = spawn2(command, args, {
2564
3097
  stdio: "inherit",
2565
3098
  shell: process.platform === "win32"
@@ -2582,7 +3115,7 @@ function runCommand(command, args) {
2582
3115
  child.on("close", (code) => {
2583
3116
  cleanup();
2584
3117
  if (code === 0) {
2585
- resolve2();
3118
+ resolve3();
2586
3119
  } else {
2587
3120
  reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
2588
3121
  }
@@ -2686,14 +3219,14 @@ async function update() {
2686
3219
  }
2687
3220
 
2688
3221
  // src/commands/version.ts
2689
- import { readFileSync as readFileSync3 } from "node:fs";
2690
- import { dirname as dirname2, join as join4 } from "node:path";
3222
+ import { readFileSync as readFileSync4 } from "node:fs";
3223
+ import { dirname as dirname2, join as join5 } from "node:path";
2691
3224
  import { fileURLToPath } from "node:url";
2692
3225
  function version() {
2693
3226
  const here = dirname2(fileURLToPath(import.meta.url));
2694
- const pkgPath = join4(here, "..", "package.json");
3227
+ const pkgPath = join5(here, "..", "package.json");
2695
3228
  try {
2696
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3229
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2697
3230
  console.log(pkg.version ?? "unknown");
2698
3231
  } catch (err) {
2699
3232
  console.error(`[version] Failed to read version: ${err.message}`);
@@ -2744,141 +3277,6 @@ function buildForwardedCommand(argv) {
2744
3277
  return ["claude-task-worker", ...tokens.map(shellQuote)].join(" ");
2745
3278
  }
2746
3279
 
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
3280
  // src/index.ts
2883
3281
  var WORKERS = {
2884
3282
  "exec-issue": execIssueWorker,
@@ -2981,6 +3379,18 @@ process.on("unhandledRejection", (err) => {
2981
3379
  console.error("[worker] unhandled rejection:", err);
2982
3380
  process.exit(1);
2983
3381
  });
3382
+ async function assertRunModeAvailable() {
3383
+ if (getRunMode() !== "herdr") return;
3384
+ const herdr = await Promise.resolve().then(() => (init_herdr(), herdr_exports));
3385
+ try {
3386
+ await herdr.checkHerdrAvailable();
3387
+ } catch (err) {
3388
+ const message = err instanceof Error ? err.message : String(err);
3389
+ console.error(`[worker] config.json has mode "herdr" but herdr is unavailable: ${message}`);
3390
+ process.exit(1);
3391
+ }
3392
+ console.log("[worker] run mode: herdr (each task runs as a TUI session in its own herdr tab)");
3393
+ }
2984
3394
  if (!hasProjectFilter()) {
2985
3395
  process.on("SIGTERM", async () => {
2986
3396
  if (isShuttingDown()) return;
@@ -2998,7 +3408,7 @@ if (!hasProjectFilter()) {
2998
3408
  forceKilling = true;
2999
3409
  console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
3000
3410
  shutdown("SIGKILL");
3001
- const cleanupTimeout = new Promise((resolve2) => setTimeout(resolve2, 6e4).unref());
3411
+ const cleanupTimeout = new Promise((resolve3) => setTimeout(resolve3, 6e4).unref());
3002
3412
  await Promise.race([waitForAllProcesses(), cleanupTimeout]);
3003
3413
  process.exit(1);
3004
3414
  }
@@ -3022,7 +3432,7 @@ if (hasProjectFilter()) {
3022
3432
  process.on("SIGTERM", shutdownController.handle);
3023
3433
  process.on("SIGINT", shutdownController.handle);
3024
3434
  try {
3025
- const config = loadProjectsConfig();
3435
+ const config = loadUserConfig();
3026
3436
  const projects = resolveTargetProjects(parseProjectFilters(), config);
3027
3437
  const forwardedCommand = buildForwardedCommand(process.argv.slice(2));
3028
3438
  sessions = await dispatcher.runDispatcher(projects, forwardedCommand);
@@ -3037,7 +3447,7 @@ if (hasProjectFilter()) {
3037
3447
  process.exit(0);
3038
3448
  }
3039
3449
  } catch (err) {
3040
- if (err instanceof ProjectsConfigError || err instanceof herdr.HerdrUnavailableError) {
3450
+ if (err instanceof UserConfigError || err instanceof herdr.HerdrUnavailableError) {
3041
3451
  console.error(`[dispatcher] ${err.message}`);
3042
3452
  process.exit(1);
3043
3453
  }
@@ -3069,6 +3479,7 @@ if (hasProjectFilter()) {
3069
3479
  const epicFilters = parseEpicFilters();
3070
3480
  const labelFilters = parseLabelFilters();
3071
3481
  (async () => {
3482
+ await assertRunModeAvailable();
3072
3483
  await removeStaleWorktrees();
3073
3484
  await Promise.all([
3074
3485
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3084,6 +3495,7 @@ if (hasProjectFilter()) {
3084
3495
  const epicFilters = parseEpicFilters();
3085
3496
  const labelFilters = parseLabelFilters();
3086
3497
  (async () => {
3498
+ await assertRunModeAvailable();
3087
3499
  await removeStaleWorktrees();
3088
3500
  await Promise.all([
3089
3501
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3102,6 +3514,7 @@ if (hasProjectFilter()) {
3102
3514
  const epicFilters = parseEpicFilters();
3103
3515
  const labelFilters = parseLabelFilters();
3104
3516
  (async () => {
3517
+ await assertRunModeAvailable();
3105
3518
  await removeStaleWorktrees();
3106
3519
  await WORKERS[workerType]({ epicFilters, labelFilters });
3107
3520
  })();