claude-task-worker 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +39 -8
  2. package/dist/index.js +715 -297
  3. 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((resolve2) => {
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
- resolve2({ execError: error, parsed, stdout, stderr });
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
- async function tabCreate({ label, cwd }) {
111
- const result = await execHerdr(["tab", "create", "--label", label, "--cwd", cwd, "--no-focus"]);
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 loadHerdr() {
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 tabLabelFor(projectName) {
270
- return `${TAB_LABEL_PREFIX}${projectName}`;
570
+ function workspaceLabelFor(projectName) {
571
+ return `${LABEL_PREFIX}${projectName}`;
271
572
  }
272
- function sleep(ms) {
273
- return new Promise((resolve2) => setTimeout(resolve2, ms));
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 sleep(pollIntervalMs);
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 sleep(pollIntervalMs);
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 loadHerdr();
337
- const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2 } = herdr;
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 existingTabs = await tabList2();
346
- const existingLabels = new Set(existingTabs.map((tab) => tab.label));
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
- if (existingLabels.has(tabLabelFor(project.name))) {
350
- console.warn(`[dispatcher] project "${project.name}" already has a running tab, skipping`);
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 createdTabId;
655
+ let createdWorkspaceId;
354
656
  try {
355
- const { paneId, tabId } = await tabCreate2({ label: tabLabelFor(project.name), cwd: project.path });
356
- createdTabId = tabId;
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 (createdTabId !== void 0) {
683
+ if (createdWorkspaceId !== void 0) {
374
684
  try {
375
- await tabClose2(createdTabId);
685
+ await workspaceClose2(createdWorkspaceId);
376
686
  } catch (closeError) {
377
- console.error(`[dispatcher] failed to close dangling tab for project "${project.name}": ${closeError}`);
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, { closeTab }) {
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 (closeTab) {
389
- const { tabClose: tabClose2 } = await loadHerdr();
390
- await tabClose2(session.tabId);
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, { closeTab: true });
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, { closeTab: false });
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
- tab: session.tabId,
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
- tab: Math.max(3, ...rows.map((r) => r.tab.length)),
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.tab, colWidths.pane, colWidths.status, colWidths.uptime];
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, 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`;
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", "Tab", "Pane", "Status", "Uptime"));
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.tab, r.pane, r.status, r.uptime));
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((resolve2) => {
453
- resolveDone = resolve2;
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((resolve2) => setTimeout(resolve2, pollIntervalMs));
805
+ await new Promise((resolve3) => setTimeout(resolve3, pollIntervalMs));
496
806
  }
497
807
  return true;
498
808
  }
499
- async function closeRemainingTabs(sessions, herdr, timeoutMs) {
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.tabClose(session.tabId),
814
+ herdr.workspaceClose(session.workspaceId),
505
815
  new Promise((_, reject) => {
506
- setTimeout(() => reject(new Error("tabClose timed out")), timeoutMs).unref();
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 tab "${session.tabId}" for session "${session.name}" during shutdown: ${error}`
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, tabCloseTimeoutMs) {
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 closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
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 loadHerdr();
530
- const tabCloseTimeoutMs = options.tabCloseTimeoutMs ?? 5e3;
531
- await forceKillAllSessions(sessions, herdr, tabCloseTimeoutMs);
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 loadHerdr();
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 tabCloseTimeoutMs = options?.tabCloseTimeoutMs ?? 5e3;
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 allTabsClosed = await closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
558
- process.exit(finishedInTime && allTabsClosed ? 0 : 1);
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, TAB_LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
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
- TAB_LABEL_PREFIX = "ctw:";
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((resolve2, reject) => {
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
- resolve2(stdout.trim());
932
+ resolve3(stdout.trim());
623
933
  });
624
934
  });
625
935
  }
626
936
  function execGhAllowExit(args, allowedCodes) {
627
- return new Promise((resolve2, reject) => {
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
- resolve2(stdout.trim());
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
- resolve2(stdout.trim());
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((resolve2) => setTimeout(resolve2, delayMs));
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 \`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
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\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
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
- - \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`;
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
- status: t.status,
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 { status: finalStatus, output } = buildTaskResult(
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
- 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
- }
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
- 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
- }
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((resolve2) => {
1931
+ return new Promise((resolve3) => {
1369
1932
  const check = () => {
1370
- if (childProcesses.size === 0) {
1371
- resolve2();
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 readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1643
- import { homedir as homedir2 } from "node:os";
1644
- import { join as join3 } from "node:path";
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 join2 } from "node:path";
1651
- var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join2(process.env.HOME ?? homedir(), ".claude", "runcat-usage.json");
2215
+ import { homedir as homedir2 } from "node:os";
2216
+ import { dirname, join as join3 } from "node:path";
2217
+ var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join3(process.env.HOME ?? homedir2(), ".claude", "runcat-usage.json");
1652
2218
  var JST = "Asia/Tokyo";
1653
2219
  function formatG(value) {
1654
2220
  return String(Number(value.toPrecision(6)));
@@ -1712,7 +2278,7 @@ function buildRuncatSnapshot(usage, now = /* @__PURE__ */ new Date()) {
1712
2278
  function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
1713
2279
  const snapshot = buildRuncatSnapshot(usage);
1714
2280
  const dir = dirname(outFile);
1715
- const tmp = join2(dir, `.runcat-${process.pid}-${Date.now()}.json`);
2281
+ const tmp = join3(dir, `.runcat-${process.pid}-${Date.now()}.json`);
1716
2282
  try {
1717
2283
  mkdirSync(dir, { recursive: true });
1718
2284
  writeFileSync(tmp, JSON.stringify(snapshot), "utf-8");
@@ -1749,13 +2315,13 @@ async function getOAuthToken() {
1749
2315
  const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
1750
2316
  return extractToken(JSON.parse(stdout.trim()));
1751
2317
  } catch {
1752
- const raw = readFileSync2(join3(homedir2(), ".claude", ".credentials.json"), "utf-8");
2318
+ const raw = readFileSync3(join4(homedir3(), ".claude", ".credentials.json"), "utf-8");
1753
2319
  return extractToken(JSON.parse(raw));
1754
2320
  }
1755
2321
  }
1756
2322
  function readUsageCache() {
1757
2323
  try {
1758
- const raw = readFileSync2(USAGE_CACHE_PATH, "utf-8");
2324
+ const raw = readFileSync3(USAGE_CACHE_PATH, "utf-8");
1759
2325
  const cached = JSON.parse(raw);
1760
2326
  if (Date.now() - cached.timestamp < USAGE_CACHE_TTL_SECONDS * 1e3) {
1761
2327
  return cached.data;
@@ -1857,11 +2423,11 @@ async function notifyError(workerName, repoName, error) {
1857
2423
  import { execFile as execFile2 } from "node:child_process";
1858
2424
  import { promisify as promisify3 } from "node:util";
1859
2425
  import { readdir, rm, stat } from "node:fs/promises";
1860
- import { basename, resolve, sep } from "node:path";
2426
+ import { basename as basename2, resolve as resolve2, sep } from "node:path";
1861
2427
  var execFileAsync2 = promisify3(execFile2);
1862
2428
  var WORKTREES_DIR = ".claude/worktrees";
1863
2429
  function isManagedWorktreePath(path) {
1864
- return resolve(path).startsWith(resolve(WORKTREES_DIR) + sep);
2430
+ return resolve2(path).startsWith(resolve2(WORKTREES_DIR) + sep);
1865
2431
  }
1866
2432
  async function pathExists(path) {
1867
2433
  try {
@@ -1948,7 +2514,7 @@ async function removeWorktree(worktreeId) {
1948
2514
  const worktreePath = getWorktreePath(worktreeId);
1949
2515
  let entry;
1950
2516
  try {
1951
- entry = (await listWorktreeEntries()).find((e) => resolve(e.path) === resolve(worktreePath));
2517
+ entry = (await listWorktreeEntries()).find((e) => resolve2(e.path) === resolve2(worktreePath));
1952
2518
  } catch (error) {
1953
2519
  console.error(`[worktree] Failed to list worktrees:`, error);
1954
2520
  }
@@ -1972,7 +2538,7 @@ async function removeWorktreeByBranch(branchName) {
1972
2538
  console.log(`[worktree] Skipping unmanaged worktree for branch ${branchName}: ${entry.path}`);
1973
2539
  continue;
1974
2540
  }
1975
- const worktreeId = basename(entry.path);
2541
+ const worktreeId = basename2(entry.path);
1976
2542
  if (isWorktreeInUse(worktreeId)) {
1977
2543
  console.log(`[worktree] Skipping worktree in use by a running task for branch ${branchName}: ${entry.path}`);
1978
2544
  continue;
@@ -1999,7 +2565,7 @@ async function removeStaleWorktrees() {
1999
2565
  try {
2000
2566
  for (const entry of await listWorktreeEntries()) {
2001
2567
  if (!isManagedWorktreePath(entry.path)) continue;
2002
- const worktreeId = basename(entry.path);
2568
+ const worktreeId = basename2(entry.path);
2003
2569
  if (isGeneratedWorktreeName(worktreeId)) worktreeIds.add(worktreeId);
2004
2570
  }
2005
2571
  } catch (error) {
@@ -2058,22 +2624,8 @@ function createIssuePollingWorker(config) {
2058
2624
  const { model, effort, skill } = getWorkerConfig(config.name);
2059
2625
  const command = skill || config.command;
2060
2626
  const parentNumber = issue.parent?.number;
2061
- const 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
- ];
2627
+ const mode = getRunMode();
2628
+ const claudeArgs = buildClaudeArgs({ mode, prompt: `${command} ${issue.number}`, model, effort });
2077
2629
  let baseBranch = defaultBranch;
2078
2630
  if (parentNumber !== void 0) {
2079
2631
  baseBranch = `cc-epic-${parentNumber}`;
@@ -2129,7 +2681,7 @@ function createIssuePollingWorker(config) {
2129
2681
  }
2130
2682
  },
2131
2683
  cwd,
2132
- { ...CLAUDE_SPAWN_ENV }
2684
+ buildClaudeEnv(mode)
2133
2685
  );
2134
2686
  } catch (err) {
2135
2687
  console.error(`[${config.name}] setup error for #${issue.number}: ${err}`);
@@ -2238,24 +2790,10 @@ function createPrPollingWorker(config) {
2238
2790
  const cwd = getWorktreePath(worktreeId);
2239
2791
  const { model, effort, skill } = getWorkerConfig(config.name);
2240
2792
  const command = skill || config.command;
2793
+ const mode = getRunMode();
2241
2794
  run(
2242
2795
  "claude",
2243
- [
2244
- "-p",
2245
- `${command} ${pr.number}`,
2246
- "--dangerously-skip-permissions",
2247
- "--chrome",
2248
- "--disallowedTools",
2249
- DISALLOWED_TOOLS_ARG,
2250
- "--append-system-prompt",
2251
- SYSTEM_PROMPT,
2252
- "--append-subagent-system-prompt",
2253
- SUBAGENT_SYSTEM_PROMPT,
2254
- "--model",
2255
- model,
2256
- "--effort",
2257
- effort
2258
- ],
2796
+ buildClaudeArgs({ mode, prompt: `${command} ${pr.number}`, model, effort }),
2259
2797
  pr.number,
2260
2798
  `PR #${pr.number} (${pr.headRefName})`,
2261
2799
  config.name,
@@ -2298,7 +2836,7 @@ function createPrPollingWorker(config) {
2298
2836
  }
2299
2837
  },
2300
2838
  cwd,
2301
- { ...CLAUDE_SPAWN_ENV }
2839
+ buildClaudeEnv(mode)
2302
2840
  );
2303
2841
  } catch (err) {
2304
2842
  console.error(`[${config.name}] setup error for PR #${pr.number}: ${err}`);
@@ -2559,7 +3097,7 @@ async function init(options = {}) {
2559
3097
  // src/commands/run-command.ts
2560
3098
  import { spawn as spawn2 } from "node:child_process";
2561
3099
  function runCommand(command, args) {
2562
- return new Promise((resolve2, reject) => {
3100
+ return new Promise((resolve3, reject) => {
2563
3101
  const child = spawn2(command, args, {
2564
3102
  stdio: "inherit",
2565
3103
  shell: process.platform === "win32"
@@ -2582,7 +3120,7 @@ function runCommand(command, args) {
2582
3120
  child.on("close", (code) => {
2583
3121
  cleanup();
2584
3122
  if (code === 0) {
2585
- resolve2();
3123
+ resolve3();
2586
3124
  } else {
2587
3125
  reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
2588
3126
  }
@@ -2686,14 +3224,14 @@ async function update() {
2686
3224
  }
2687
3225
 
2688
3226
  // src/commands/version.ts
2689
- import { readFileSync as readFileSync3 } from "node:fs";
2690
- import { dirname as dirname2, join as join4 } from "node:path";
3227
+ import { readFileSync as readFileSync4 } from "node:fs";
3228
+ import { dirname as dirname2, join as join5 } from "node:path";
2691
3229
  import { fileURLToPath } from "node:url";
2692
3230
  function version() {
2693
3231
  const here = dirname2(fileURLToPath(import.meta.url));
2694
- const pkgPath = join4(here, "..", "package.json");
3232
+ const pkgPath = join5(here, "..", "package.json");
2695
3233
  try {
2696
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3234
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2697
3235
  console.log(pkg.version ?? "unknown");
2698
3236
  } catch (err) {
2699
3237
  console.error(`[version] Failed to read version: ${err.message}`);
@@ -2744,141 +3282,6 @@ function buildForwardedCommand(argv) {
2744
3282
  return ["claude-task-worker", ...tokens.map(shellQuote)].join(" ");
2745
3283
  }
2746
3284
 
2747
- // src/projects-config.ts
2748
- import { readFileSync as readFileSync4, statSync } from "node:fs";
2749
- import { homedir as homedir3 } from "node:os";
2750
- import { isAbsolute, join as join5 } from "node:path";
2751
- var RESERVED_ALL = "all";
2752
- var ProjectsConfigError = class extends Error {
2753
- constructor(message) {
2754
- super(message);
2755
- this.name = "ProjectsConfigError";
2756
- }
2757
- };
2758
- function getProjectsConfigPath() {
2759
- const xdg = process.env.XDG_CONFIG_HOME;
2760
- const configHome = xdg && xdg.length > 0 ? xdg : join5(homedir3(), ".config");
2761
- return join5(configHome, "claude-task-worker", "projects.json");
2762
- }
2763
- var PROJECTS_CONFIG_PATH = getProjectsConfigPath();
2764
- function isDirectory(path) {
2765
- try {
2766
- return statSync(path).isDirectory();
2767
- } catch {
2768
- return false;
2769
- }
2770
- }
2771
- function loadProjectsConfig() {
2772
- const configPath = PROJECTS_CONFIG_PATH;
2773
- let raw;
2774
- try {
2775
- raw = JSON.parse(readFileSync4(configPath, "utf-8"));
2776
- } catch (err) {
2777
- if (err.code === "ENOENT") {
2778
- throw new ProjectsConfigError(`projects.json not found: ${configPath}`);
2779
- }
2780
- if (err instanceof SyntaxError) {
2781
- throw new ProjectsConfigError(`projects.json contains invalid JSON: ${configPath}: ${err.message}`);
2782
- }
2783
- throw err;
2784
- }
2785
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2786
- throw new ProjectsConfigError(`projects.json must contain a JSON object: ${configPath}`);
2787
- }
2788
- if (!("projects" in raw) || typeof raw["projects"] !== "object" || raw["projects"] === null || Array.isArray(raw["projects"])) {
2789
- throw new ProjectsConfigError(`projects.json must contain a "projects" section as an object: ${configPath}`);
2790
- }
2791
- if ("projectGroups" in raw && (typeof raw["projectGroups"] !== "object" || raw["projectGroups"] === null || Array.isArray(raw["projectGroups"]))) {
2792
- throw new ProjectsConfigError(`projects.json "projectGroups" must be an object: ${configPath}`);
2793
- }
2794
- const rawProjects = raw["projects"];
2795
- const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
2796
- const projectKeys = Object.keys(rawProjects);
2797
- const groupKeys = Object.keys(rawProjectGroups);
2798
- if (projectKeys.includes(RESERVED_ALL) || groupKeys.includes(RESERVED_ALL)) {
2799
- throw new ProjectsConfigError(
2800
- `"${RESERVED_ALL}" is a reserved word and cannot be used as a key in "projects" or "projectGroups"`
2801
- );
2802
- }
2803
- const groupKeySet = new Set(groupKeys);
2804
- const duplicateKeys = projectKeys.filter((key) => groupKeySet.has(key));
2805
- if (duplicateKeys.length > 0) {
2806
- throw new ProjectsConfigError(
2807
- `"projects" and "projectGroups" share the same key namespace; duplicate keys are not allowed: ${duplicateKeys.join(", ")}`
2808
- );
2809
- }
2810
- const projects = {};
2811
- for (const [name, value] of Object.entries(rawProjects)) {
2812
- if (name === "__proto__") {
2813
- throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projects": ${configPath}`);
2814
- }
2815
- if (typeof value !== "string" || !isAbsolute(value)) {
2816
- console.warn(`[projects-config] invalid projects.${name}: expected an absolute path, skipping`);
2817
- continue;
2818
- }
2819
- if (!isDirectory(value)) {
2820
- console.warn(`[projects-config] projects.${name} does not exist as a directory: ${value}, skipping`);
2821
- continue;
2822
- }
2823
- projects[name] = value;
2824
- }
2825
- const projectGroups = {};
2826
- for (const [groupName, value] of Object.entries(rawProjectGroups)) {
2827
- if (groupName === "__proto__") {
2828
- throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projectGroups": ${configPath}`);
2829
- }
2830
- if (!Array.isArray(value)) {
2831
- console.warn(`[projects-config] invalid projectGroups.${groupName}: expected an array, skipping`);
2832
- continue;
2833
- }
2834
- const members = [];
2835
- for (const member of value) {
2836
- if (typeof member !== "string" || !Object.prototype.hasOwnProperty.call(projects, member)) {
2837
- console.warn(
2838
- `[projects-config] projectGroups.${groupName} references unknown project "${String(member)}", skipping`
2839
- );
2840
- continue;
2841
- }
2842
- members.push(member);
2843
- }
2844
- projectGroups[groupName] = members;
2845
- }
2846
- return { projects, projectGroups };
2847
- }
2848
- function resolveTargetProjects(requested, config) {
2849
- const resolved = /* @__PURE__ */ new Map();
2850
- for (const name of requested) {
2851
- if (name === RESERVED_ALL) {
2852
- for (const [projectName, projectPath] of Object.entries(config.projects)) {
2853
- resolved.set(projectName, { name: projectName, path: projectPath });
2854
- }
2855
- continue;
2856
- }
2857
- if (Object.prototype.hasOwnProperty.call(config.projects, name)) {
2858
- resolved.set(name, { name, path: config.projects[name] });
2859
- continue;
2860
- }
2861
- if (Object.prototype.hasOwnProperty.call(config.projectGroups, name)) {
2862
- for (const projectName of config.projectGroups[name]) {
2863
- const projectPath = config.projects[projectName];
2864
- if (projectPath === void 0) continue;
2865
- resolved.set(projectName, { name: projectName, path: projectPath });
2866
- }
2867
- continue;
2868
- }
2869
- const availableProjects = Object.keys(config.projects).join(", ") || "(none)";
2870
- const availableGroups = Object.keys(config.projectGroups).join(", ") || "(none)";
2871
- throw new ProjectsConfigError(
2872
- `Unknown project or group: "${name}". Available projects: ${availableProjects}. Available groups: ${availableGroups}.`
2873
- );
2874
- }
2875
- const resolvedProjects = Array.from(resolved.values());
2876
- if (resolvedProjects.length === 0) {
2877
- throw new ProjectsConfigError(`No projects resolved from requested targets: ${requested.join(", ")}`);
2878
- }
2879
- return resolvedProjects;
2880
- }
2881
-
2882
3285
  // src/index.ts
2883
3286
  var WORKERS = {
2884
3287
  "exec-issue": execIssueWorker,
@@ -2981,6 +3384,18 @@ process.on("unhandledRejection", (err) => {
2981
3384
  console.error("[worker] unhandled rejection:", err);
2982
3385
  process.exit(1);
2983
3386
  });
3387
+ async function assertRunModeAvailable() {
3388
+ if (getRunMode() !== "herdr") return;
3389
+ const herdr = await Promise.resolve().then(() => (init_herdr(), herdr_exports));
3390
+ try {
3391
+ await herdr.checkHerdrAvailable();
3392
+ } catch (err) {
3393
+ const message = err instanceof Error ? err.message : String(err);
3394
+ console.error(`[worker] config.json has mode "herdr" but herdr is unavailable: ${message}`);
3395
+ process.exit(1);
3396
+ }
3397
+ console.log("[worker] run mode: herdr (each task runs as a TUI session in its own herdr tab)");
3398
+ }
2984
3399
  if (!hasProjectFilter()) {
2985
3400
  process.on("SIGTERM", async () => {
2986
3401
  if (isShuttingDown()) return;
@@ -2998,7 +3413,7 @@ if (!hasProjectFilter()) {
2998
3413
  forceKilling = true;
2999
3414
  console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
3000
3415
  shutdown("SIGKILL");
3001
- const cleanupTimeout = new Promise((resolve2) => setTimeout(resolve2, 6e4).unref());
3416
+ const cleanupTimeout = new Promise((resolve3) => setTimeout(resolve3, 6e4).unref());
3002
3417
  await Promise.race([waitForAllProcesses(), cleanupTimeout]);
3003
3418
  process.exit(1);
3004
3419
  }
@@ -3022,7 +3437,7 @@ if (hasProjectFilter()) {
3022
3437
  process.on("SIGTERM", shutdownController.handle);
3023
3438
  process.on("SIGINT", shutdownController.handle);
3024
3439
  try {
3025
- const config = loadProjectsConfig();
3440
+ const config = loadUserConfig();
3026
3441
  const projects = resolveTargetProjects(parseProjectFilters(), config);
3027
3442
  const forwardedCommand = buildForwardedCommand(process.argv.slice(2));
3028
3443
  sessions = await dispatcher.runDispatcher(projects, forwardedCommand);
@@ -3037,7 +3452,7 @@ if (hasProjectFilter()) {
3037
3452
  process.exit(0);
3038
3453
  }
3039
3454
  } catch (err) {
3040
- if (err instanceof ProjectsConfigError || err instanceof herdr.HerdrUnavailableError) {
3455
+ if (err instanceof UserConfigError || err instanceof herdr.HerdrUnavailableError) {
3041
3456
  console.error(`[dispatcher] ${err.message}`);
3042
3457
  process.exit(1);
3043
3458
  }
@@ -3069,6 +3484,7 @@ if (hasProjectFilter()) {
3069
3484
  const epicFilters = parseEpicFilters();
3070
3485
  const labelFilters = parseLabelFilters();
3071
3486
  (async () => {
3487
+ await assertRunModeAvailable();
3072
3488
  await removeStaleWorktrees();
3073
3489
  await Promise.all([
3074
3490
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3084,6 +3500,7 @@ if (hasProjectFilter()) {
3084
3500
  const epicFilters = parseEpicFilters();
3085
3501
  const labelFilters = parseLabelFilters();
3086
3502
  (async () => {
3503
+ await assertRunModeAvailable();
3087
3504
  await removeStaleWorktrees();
3088
3505
  await Promise.all([
3089
3506
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3102,6 +3519,7 @@ if (hasProjectFilter()) {
3102
3519
  const epicFilters = parseEpicFilters();
3103
3520
  const labelFilters = parseLabelFilters();
3104
3521
  (async () => {
3522
+ await assertRunModeAvailable();
3105
3523
  await removeStaleWorktrees();
3106
3524
  await WORKERS[workerType]({ epicFilters, labelFilters });
3107
3525
  })();