claude-task-worker 0.26.0 → 0.27.0

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