claude-task-worker 0.30.0 → 0.32.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 +26 -1
  2. package/dist/index.js +201 -114
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -274,7 +274,7 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
274
274
  }
275
275
  ```
276
276
 
277
- `mode` については [`mode`(タスクの実行形態)](#modeタスクの実行形態) を参照。
277
+ `mode` については [`mode`(タスクの実行形態)](#modeタスクの実行形態) を、`headroom` については [`headroom`(Headroom 経由でのタスク実行)](#headroomheadroom-経由でのタスク実行) を参照。
278
278
 
279
279
  `--project` は繰り返し指定可能で、複数指定した場合は解決後のプロジェクト集合の和集合が対象になる(重複は一意化される)。`--epic` / `--label` と併用でき、ディスパッチ先の各プロジェクトで実行されるコマンドにそのまま引き継がれる。
280
280
 
@@ -331,6 +331,31 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
331
331
 
332
332
  適用は `herdr server reload-config`。この設定は herdr サーバー全体に効くため、ワーカー以外の対話セッションの完了音も鳴らなくなる(`[ui.sound.agents] claude = "off"` でも実質同じ範囲)。ワーカーだけを無音にしたい場合は、`HERDR_DISABLE_SOUND=1 herdr --session <name>` で別セッションを起動し、その中でディスパッチャーを動かす
333
333
 
334
+ ### `headroom`(Headroom 経由でのタスク実行)
335
+
336
+ `config.json` のトップレベルに `"headroom": true` を書くと、ワーカーは各タスクの claude を [Headroom](https://github.com/headroom-ai/headroom) 経由(`headroom wrap claude`)で起動する。Headroom がローカルプロキシを立ててコンテキストを圧縮し、`ANTHROPIC_BASE_URL` を差し替えた状態で claude を起動する。既定は `false`(`claude` を直接起動する)。
337
+
338
+ `mode` と同じくトップレベル一括の設定で、プロジェクト単位・ワーカー単位の指定はできない。
339
+
340
+ ```json
341
+ {
342
+ "headroom": true,
343
+ "projects": { "app-a": "/Users/me/repos/app-a" }
344
+ }
345
+ ```
346
+
347
+ | `headroom` | 実行されるコマンド |
348
+ |-----------|------------------|
349
+ | `false`(既定) | `claude <引数>` |
350
+ | `true` | `headroom wrap claude -- <引数>` |
351
+
352
+ 補足:
353
+
354
+ - `mode` とは独立して組み合わせられる(`mode: "herdr"` の TUI 起動も `headroom wrap claude` になる)
355
+ - claude へ渡す引数はすべて `--` の後ろに置かれる。`headroom wrap claude` 自身も `--port` / `--memory` などのオプションを持ち、`-p` のように衝突しうるフラグは `--` の後ろでないと claude に届かないため
356
+ - `headroom: true` で headroom コマンドが PATH に無い場合、ワーカーは起動時にエラー終了する(`mode: "herdr"` と同じく、サイレントに直接起動へフォールバックはしない)
357
+ - `headroom wrap claude` は claude を起動する前に起動バナー(枠線・`ANTHROPIC_BASE_URL=...` など)を stdout へ出力し、これを抑止するオプションは無い。ワーカーは空振りセッション検知(exit 0 かつ無出力を失敗とみなす判定)の前にこのバナーを取り除くため、検知は `headroom: true` でも従来どおり機能する。ただし Slack 通知の本文にはバナーがそのまま含まれる
358
+
334
359
  ### exec-issue
335
360
 
336
361
  `cc-exec-issue` ラベルが付いた自分にアサインされたIssueを定期取得し、Claude Codeで処理を実行する。(デフォルト1分間隔)
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ var __export = (target, all) => {
12
12
  // src/table.ts
13
13
  var table_exports = {};
14
14
  __export(table_exports, {
15
+ buildTaskTableLines: () => buildTaskTableLines,
15
16
  getDisplayWidth: () => getDisplayWidth,
16
17
  padToWidth: () => padToWidth,
17
18
  truncateToWidth: () => truncateToWidth
@@ -47,12 +48,122 @@ function padToWidth(str, targetWidth) {
47
48
  const padding = targetWidth - currentWidth;
48
49
  return padding > 0 ? str + " ".repeat(padding) : str;
49
50
  }
51
+ function formatDuration(start, end = /* @__PURE__ */ new Date()) {
52
+ const diffMs = end.getTime() - start.getTime();
53
+ const totalSeconds = Math.floor(diffMs / 1e3);
54
+ const minutes = Math.floor(totalSeconds / 60);
55
+ const seconds = totalSeconds % 60;
56
+ return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
57
+ }
58
+ function formatTime(date) {
59
+ const h = String(date.getHours()).padStart(2, "0");
60
+ const m = String(date.getMinutes()).padStart(2, "0");
61
+ const s = String(date.getSeconds()).padStart(2, "0");
62
+ return `${h}:${m}:${s}`;
63
+ }
64
+ function buildTaskTableLines(entries, now = /* @__PURE__ */ new Date()) {
65
+ if (entries.length === 0) return [];
66
+ const runningTasks = entries.filter((t) => t.status === "running");
67
+ const finishedTasks = entries.filter((t) => t.status !== "running");
68
+ const maxTitleWidth = 40;
69
+ const maxPathWidth = 40;
70
+ const runningRows = runningTasks.map((t) => ({
71
+ id: `#${t.id}`,
72
+ title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
73
+ worker: t.workerName,
74
+ path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
75
+ // herdr モードでは agent ステータス(working / blocked 等)を併記し、
76
+ // 人の介入が必要な blocked にも気づけるようにする。
77
+ status: t.agentStatus ? `${t.status}:${t.agentStatus}` : t.status,
78
+ time: formatTime(t.startedAt),
79
+ duration: formatDuration(t.startedAt, now)
80
+ }));
81
+ const finishedRows = finishedTasks.map((t) => ({
82
+ id: `#${t.id}`,
83
+ title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
84
+ worker: t.workerName,
85
+ path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
86
+ status: t.status,
87
+ time: formatTime(t.finishedAt ?? t.startedAt),
88
+ duration: formatDuration(t.startedAt, t.finishedAt ?? now)
89
+ }));
90
+ const allRows = [...runningRows, ...finishedRows];
91
+ const hasPath = allRows.some((r) => r.path !== "");
92
+ const colWidths = {
93
+ id: Math.max(3, ...allRows.map((r) => r.id.length)),
94
+ title: Math.max(5, ...allRows.map((r) => getDisplayWidth(r.title))),
95
+ worker: Math.max(6, ...allRows.map((r) => r.worker.length)),
96
+ ...hasPath ? { path: Math.max(8, ...allRows.map((r) => r.path.length)) } : {},
97
+ status: Math.max(6, ...allRows.map((r) => r.status.length)),
98
+ time: Math.max(4, ...allRows.map((r) => r.time.length)),
99
+ duration: Math.max(8, ...allRows.map((r) => r.duration.length))
100
+ };
101
+ const pad = (s, w, useDisplayWidth = false) => useDisplayWidth ? padToWidth(s, w) : s + " ".repeat(w - s.length);
102
+ const cols = hasPath ? [
103
+ colWidths.id,
104
+ colWidths.title,
105
+ colWidths.worker,
106
+ colWidths.path,
107
+ colWidths.status,
108
+ colWidths.time,
109
+ colWidths.duration
110
+ ] : [colWidths.id, colWidths.title, colWidths.worker, colWidths.status, colWidths.time, colWidths.duration];
111
+ const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
112
+ const row = (id, title, worker, path, status, time, duration) => hasPath ? `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(path, colWidths.path)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502` : `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502`;
113
+ const lines = [];
114
+ lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
115
+ lines.push(row("#", "Title", "Worker", "Worktree", "Status", "Time", "Duration"));
116
+ lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
117
+ for (const r of runningRows) {
118
+ lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
119
+ }
120
+ if (runningRows.length > 0 && finishedRows.length > 0) {
121
+ lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
122
+ }
123
+ for (const r of finishedRows) {
124
+ lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
125
+ }
126
+ lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
127
+ return lines;
128
+ }
50
129
  var init_table = __esm({
51
130
  "src/table.ts"() {
52
131
  "use strict";
53
132
  }
54
133
  });
55
134
 
135
+ // src/task-result.ts
136
+ function stripHeadroomBanner(stdout) {
137
+ const lines = stdout.split("\n");
138
+ let i = 0;
139
+ while (i < lines.length && (lines[i].trim() === "" || lines[i].startsWith(" "))) i++;
140
+ return lines.slice(i).join("\n");
141
+ }
142
+ function buildTaskResult(code, stdout, stderrTail, options) {
143
+ const meaningful = options?.headroom ? stripHeadroomBanner(stdout) : stdout;
144
+ const emptyOutput = meaningful.trim() === "";
145
+ const completed = code === 0 && !emptyOutput;
146
+ let output = stdout;
147
+ if (code === 0 && emptyOutput) {
148
+ output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
149
+ } else if (!completed) {
150
+ output += `
151
+ [worker] claude exited with code ${code}`;
152
+ }
153
+ if (!completed && stderrTail.trim() !== "") {
154
+ output += `
155
+ [stderr] ${stderrTail.trim()}`;
156
+ }
157
+ return { status: completed ? "completed" : "failed", output };
158
+ }
159
+ var STDERR_TAIL_LIMIT;
160
+ var init_task_result = __esm({
161
+ "src/task-result.ts"() {
162
+ "use strict";
163
+ STDERR_TAIL_LIMIT = 8 * 1024;
164
+ }
165
+ });
166
+
56
167
  // src/herdr.ts
57
168
  var herdr_exports = {};
58
169
  __export(herdr_exports, {
@@ -411,8 +522,9 @@ function observeAgentStatus(tracker, status) {
411
522
  }
412
523
  return { tracker, decision: "running" };
413
524
  }
414
- function buildHerdrTaskResult(paneOutput) {
415
- if (paneOutput.trim() === "") {
525
+ function buildHerdrTaskResult(paneOutput, options) {
526
+ const meaningful = options?.headroom ? stripHeadroomBanner(paneOutput) : paneOutput;
527
+ if (meaningful.trim() === "") {
416
528
  return {
417
529
  status: "failed",
418
530
  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)"
@@ -470,7 +582,7 @@ async function waitForHerdrTask(paneId, options) {
470
582
  tracker = observed.tracker;
471
583
  if (observed.decision === "completed") {
472
584
  const output = await readPaneOutput(paneId, mod);
473
- return buildHerdrTaskResult(output);
585
+ return buildHerdrTaskResult(output, { headroom: options?.headroom });
474
586
  }
475
587
  if (observed.decision === "blocked-first-seen") {
476
588
  options?.onBlocked?.();
@@ -530,6 +642,7 @@ var AGENT_POLL_INTERVAL_MS, PANE_OUTPUT_LINES, CLAUDE_EXIT_TIMEOUT_MS, CLAUDE_EX
530
642
  var init_herdr_runner = __esm({
531
643
  "src/herdr-runner.ts"() {
532
644
  "use strict";
645
+ init_task_result();
533
646
  AGENT_POLL_INTERVAL_MS = 3 * 1e3;
534
647
  PANE_OUTPUT_LINES = 300;
535
648
  CLAUDE_EXIT_TIMEOUT_MS = 15 * 1e3;
@@ -1299,6 +1412,8 @@ var SYSTEM_PROMPT = `\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F \`claude-t
1299
1412
  - CodeGraph \u304C\u8FD4\u3057\u305F\u30BD\u30FC\u30B9\u306F\u300C\u8AAD\u307F\u7D42\u3048\u305F\u3082\u306E\u300D\u3068\u3057\u3066\u6271\u3044\u3001\u540C\u3058\u7B87\u6240\u3092 \`Grep\`/\`Read\` \u3067\u88CF\u53D6\u308A\u3057\u76F4\u3055\u306A\u3044\u3002\u305F\u3060\u3057\u51FA\u529B\u306B staleness\uFF08\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u304C\u53E4\u3044\u65E8\uFF09\u306E\u8B66\u544A\u304C\u51FA\u3066\u3044\u308B\u5834\u5408\u306F\u8A72\u5F53\u30D5\u30A1\u30A4\u30EB\u3092 \`Read\` \u3057\u3066\u73FE\u7269\u3092\u78BA\u8A8D\u3059\u308B
1300
1413
  - \u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u30FB\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u30FB\u30B3\u30E1\u30F3\u30C8/\u6587\u5B57\u5217\u30EA\u30C6\u30E9\u30EB\u30FB\u672A\u5BFE\u5FDC\u8A00\u8A9E\u306A\u3069 CodeGraph \u304C\u6271\u308F\u306A\u3044\u5BFE\u8C61\u306F\u3001\u5F93\u6765\u3069\u304A\u308A\u30C6\u30AD\u30B9\u30C8\u691C\u7D22\u3067\u88DC\u3046
1301
1414
  - \u63A2\u7D22\u3092\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u3078\u59D4\u8B72\u3059\u308B\u5834\u5408\u306F\u3001\u3053\u306E\u65B9\u91DD\u3082\u59D4\u8B72\u30D7\u30ED\u30F3\u30D7\u30C8\u306B\u660E\u8A18\u3057\u3066\u4F1D\u3048\u308B`;
1415
+ var CLAUDE_COMMAND = "claude";
1416
+ var HEADROOM_COMMAND = "headroom";
1302
1417
  function buildClaudeArgs({ mode, prompt, model, effort }) {
1303
1418
  return [
1304
1419
  ...mode === "herdr" ? [] : ["-p"],
@@ -1315,6 +1430,13 @@ function buildClaudeArgs({ mode, prompt, model, effort }) {
1315
1430
  effort
1316
1431
  ];
1317
1432
  }
1433
+ function buildClaudeExecution(invocation) {
1434
+ const args = buildClaudeArgs(invocation);
1435
+ if (!invocation.headroom) {
1436
+ return { command: CLAUDE_COMMAND, args };
1437
+ }
1438
+ return { command: HEADROOM_COMMAND, args: ["wrap", CLAUDE_COMMAND, "--", ...args] };
1439
+ }
1318
1440
  function buildClaudeEnv(mode) {
1319
1441
  if (mode === "herdr") {
1320
1442
  return {
@@ -1569,25 +1691,7 @@ async function assertRemoteTrackingExists(epicBranch) {
1569
1691
  import { spawn } from "node:child_process";
1570
1692
  import { basename } from "node:path";
1571
1693
  init_table();
1572
-
1573
- // src/task-result.ts
1574
- var STDERR_TAIL_LIMIT = 8 * 1024;
1575
- function buildTaskResult(code, stdout, stderrTail) {
1576
- const emptyOutput = stdout.trim() === "";
1577
- const completed = code === 0 && !emptyOutput;
1578
- let output = stdout;
1579
- if (code === 0 && emptyOutput) {
1580
- output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
1581
- } else if (!completed) {
1582
- output += `
1583
- [worker] claude exited with code ${code}`;
1584
- }
1585
- if (!completed && stderrTail.trim() !== "") {
1586
- output += `
1587
- [stderr] ${stderrTail.trim()}`;
1588
- }
1589
- return { status: completed ? "completed" : "failed", output };
1590
- }
1694
+ init_task_result();
1591
1695
 
1592
1696
  // src/user-config.ts
1593
1697
  import { readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1595,6 +1699,7 @@ import { homedir } from "node:os";
1595
1699
  import { isAbsolute, join as join2, resolve } from "node:path";
1596
1700
  var RESERVED_ALL = "all";
1597
1701
  var DEFAULT_RUN_MODE = "default";
1702
+ var DEFAULT_HEADROOM = false;
1598
1703
  var UserConfigError = class extends Error {
1599
1704
  constructor(message) {
1600
1705
  super(message);
@@ -1643,6 +1748,13 @@ function parseMode(raw, path) {
1643
1748
  console.warn(`[config] invalid mode: ${JSON.stringify(value)} in ${path}, using "${DEFAULT_RUN_MODE}"`);
1644
1749
  return DEFAULT_RUN_MODE;
1645
1750
  }
1751
+ function parseHeadroom(raw, path) {
1752
+ if (!("headroom" in raw)) return DEFAULT_HEADROOM;
1753
+ const value = raw["headroom"];
1754
+ if (typeof value === "boolean") return value;
1755
+ console.warn(`[config] invalid headroom: ${JSON.stringify(value)} in ${path}, using ${DEFAULT_HEADROOM}`);
1756
+ return DEFAULT_HEADROOM;
1757
+ }
1646
1758
  function loadUserConfig() {
1647
1759
  const path = getUserConfigPath();
1648
1760
  const raw = readRawConfig();
@@ -1659,6 +1771,7 @@ function loadUserConfig() {
1659
1771
  throw new UserConfigError(`config.json "projectGroups" must be an object: ${path}`);
1660
1772
  }
1661
1773
  const mode = parseMode(raw, path);
1774
+ const headroom = parseHeadroom(raw, path);
1662
1775
  const rawProjects = raw["projects"];
1663
1776
  const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
1664
1777
  const projectKeys = Object.keys(rawProjects);
@@ -1709,7 +1822,7 @@ function loadUserConfig() {
1709
1822
  }
1710
1823
  projectGroups[groupName] = members;
1711
1824
  }
1712
- return { mode, projects, projectGroups };
1825
+ return { mode, headroom, projects, projectGroups };
1713
1826
  }
1714
1827
  var cachedRunMode;
1715
1828
  function getRunMode() {
@@ -1719,18 +1832,35 @@ function getRunMode() {
1719
1832
  return cachedRunMode;
1720
1833
  }
1721
1834
  function readRunMode() {
1835
+ const raw = readTopLevelConfig(`using "${DEFAULT_RUN_MODE}" mode`);
1836
+ if (raw === void 0) return DEFAULT_RUN_MODE;
1837
+ return parseMode(raw, getUserConfigPath());
1838
+ }
1839
+ var cachedHeadroom;
1840
+ function getHeadroomEnabled() {
1841
+ if (cachedHeadroom === void 0) {
1842
+ cachedHeadroom = readHeadroom();
1843
+ }
1844
+ return cachedHeadroom;
1845
+ }
1846
+ function readHeadroom() {
1847
+ const raw = readTopLevelConfig(`using headroom=${DEFAULT_HEADROOM}`);
1848
+ if (raw === void 0) return DEFAULT_HEADROOM;
1849
+ return parseHeadroom(raw, getUserConfigPath());
1850
+ }
1851
+ function readTopLevelConfig(fallbackDescription) {
1722
1852
  let raw;
1723
1853
  try {
1724
1854
  raw = readRawConfig();
1725
1855
  } catch (err) {
1726
- console.warn(`[config] failed to read config file, using "${DEFAULT_RUN_MODE}" mode: ${err}`);
1727
- return DEFAULT_RUN_MODE;
1856
+ console.warn(`[config] failed to read config file, ${fallbackDescription}: ${err}`);
1857
+ return void 0;
1728
1858
  }
1729
- if (raw === void 0) return DEFAULT_RUN_MODE;
1859
+ if (raw === void 0) return void 0;
1730
1860
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1731
- return DEFAULT_RUN_MODE;
1861
+ return void 0;
1732
1862
  }
1733
- return parseMode(raw, getUserConfigPath());
1863
+ return raw;
1734
1864
  }
1735
1865
  function findProjectNameByPath(path) {
1736
1866
  let config;
@@ -1812,84 +1942,9 @@ function isWorkerAtCapacity(workerName) {
1812
1942
  }
1813
1943
  return count >= getWorkerConfig(workerName).maxConcurrentTasks;
1814
1944
  }
1815
- function formatDuration(start, end = /* @__PURE__ */ new Date()) {
1816
- const diffMs = end.getTime() - start.getTime();
1817
- const totalSeconds = Math.floor(diffMs / 1e3);
1818
- const minutes = Math.floor(totalSeconds / 60);
1819
- const seconds = totalSeconds % 60;
1820
- return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
1821
- }
1822
- function formatTime(date) {
1823
- const h = String(date.getHours()).padStart(2, "0");
1824
- const m = String(date.getMinutes()).padStart(2, "0");
1825
- const s = String(date.getSeconds()).padStart(2, "0");
1826
- return `${h}:${m}:${s}`;
1827
- }
1828
1945
  function renderTable() {
1829
- const entries = [...tasks.values()];
1830
- if (entries.length === 0) return;
1831
- const runningTasks = entries.filter((t) => t.status === "running");
1832
- const finishedTasks = entries.filter((t) => t.status !== "running");
1833
- const maxTitleWidth = 40;
1834
- const maxPathWidth = 40;
1835
- const allRows = [
1836
- ...runningTasks.map((t) => ({
1837
- id: `#${t.id}`,
1838
- title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
1839
- worker: t.workerName,
1840
- path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
1841
- // herdr モードでは agent ステータス(working / blocked 等)を併記し、
1842
- // 人の介入が必要な blocked にも気づけるようにする。
1843
- status: t.agentStatus ? `${t.status}:${t.agentStatus}` : t.status,
1844
- time: formatTime(t.startedAt),
1845
- duration: formatDuration(t.startedAt)
1846
- })),
1847
- ...finishedTasks.map((t) => ({
1848
- id: `#${t.id}`,
1849
- title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
1850
- worker: t.workerName,
1851
- path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
1852
- status: t.status,
1853
- time: formatTime(t.finishedAt ?? t.startedAt),
1854
- duration: formatDuration(t.startedAt, t.finishedAt)
1855
- }))
1856
- ];
1857
- const hasPath = allRows.some((r) => r.path !== "");
1858
- const colWidths = {
1859
- id: Math.max(3, ...allRows.map((r) => r.id.length)),
1860
- title: Math.max(5, ...allRows.map((r) => getDisplayWidth(r.title))),
1861
- worker: Math.max(6, ...allRows.map((r) => r.worker.length)),
1862
- ...hasPath ? { path: Math.max(8, ...allRows.map((r) => r.path.length)) } : {},
1863
- status: Math.max(6, ...allRows.map((r) => r.status.length)),
1864
- time: Math.max(4, ...allRows.map((r) => r.time.length)),
1865
- duration: Math.max(8, ...allRows.map((r) => r.duration.length))
1866
- };
1867
- const pad = (s, w, useDisplayWidth = false) => useDisplayWidth ? padToWidth(s, w) : s + " ".repeat(w - s.length);
1868
- const cols = hasPath ? [
1869
- colWidths.id,
1870
- colWidths.title,
1871
- colWidths.worker,
1872
- colWidths.path,
1873
- colWidths.status,
1874
- colWidths.time,
1875
- colWidths.duration
1876
- ] : [colWidths.id, colWidths.title, colWidths.worker, colWidths.status, colWidths.time, colWidths.duration];
1877
- const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
1878
- const row = (id, title, worker, path, status, time, duration) => hasPath ? `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(path, colWidths.path)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502` : `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502`;
1879
- const lines = [];
1880
- lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
1881
- lines.push(row("#", "Title", "Worker", "Worktree", "Status", "Time", "Duration"));
1882
- lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
1883
- for (const r of allRows.filter((r2) => r2.status === "running")) {
1884
- lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
1885
- }
1886
- if (runningTasks.length > 0 && finishedTasks.length > 0) {
1887
- lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
1888
- }
1889
- for (const r of allRows.filter((r2) => r2.status !== "running")) {
1890
- lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
1891
- }
1892
- lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
1946
+ const lines = buildTaskTableLines([...tasks.values()]);
1947
+ if (lines.length === 0) return;
1893
1948
  console.clear();
1894
1949
  console.log(lines.join("\n"));
1895
1950
  }
@@ -1941,6 +1996,7 @@ async function runViaHerdr(command, args, id, onComplete, cwd, env) {
1941
1996
  herdrTasks.set(id, task);
1942
1997
  result = await waitForHerdrTask2(task.paneId, {
1943
1998
  signal: herdrAbortSignal,
1999
+ headroom: getHeadroomEnabled(),
1944
2000
  onBlocked: () => console.warn(`[worker] #${id} is blocked and waiting for input in herdr tab "${label}"`),
1945
2001
  onStatus: (status) => {
1946
2002
  const task2 = tasks.get(id);
@@ -1998,7 +2054,8 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
1998
2054
  const result = buildTaskResult(
1999
2055
  code,
2000
2056
  Buffer.concat(outputChunks).toString("utf-8"),
2001
- Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
2057
+ Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT),
2058
+ { headroom: getHeadroomEnabled() }
2002
2059
  );
2003
2060
  await finishTask(id, result, onComplete);
2004
2061
  childProcesses.delete(id);
@@ -2707,7 +2764,13 @@ function createIssuePollingWorker(config) {
2707
2764
  const command = skill || config.command;
2708
2765
  const parentNumber = issue.parent?.number;
2709
2766
  const mode = getRunMode();
2710
- const claudeArgs = buildClaudeArgs({ mode, prompt: `${command} ${issue.number}`, model, effort });
2767
+ const execution = buildClaudeExecution({
2768
+ mode,
2769
+ prompt: `${command} ${issue.number}`,
2770
+ model,
2771
+ effort,
2772
+ headroom: getHeadroomEnabled()
2773
+ });
2711
2774
  let baseBranch = defaultBranch;
2712
2775
  if (parentNumber !== void 0) {
2713
2776
  baseBranch = `cc-epic-${parentNumber}`;
@@ -2717,8 +2780,8 @@ function createIssuePollingWorker(config) {
2717
2780
  const cwd = getWorktreePath(worktreeId);
2718
2781
  console.log(`[${config.name}] #${issue.number}: created worktree ${worktreeId} from ${baseBranch}`);
2719
2782
  run(
2720
- "claude",
2721
- claudeArgs,
2783
+ execution.command,
2784
+ execution.args,
2722
2785
  issue.number,
2723
2786
  issue.title,
2724
2787
  config.name,
@@ -2873,9 +2936,16 @@ function createPrPollingWorker(config) {
2873
2936
  const { model, effort, skill } = getWorkerConfig(config.name);
2874
2937
  const command = skill || config.command;
2875
2938
  const mode = getRunMode();
2939
+ const execution = buildClaudeExecution({
2940
+ mode,
2941
+ prompt: `${command} ${pr.number}`,
2942
+ model,
2943
+ effort,
2944
+ headroom: getHeadroomEnabled()
2945
+ });
2876
2946
  run(
2877
- "claude",
2878
- buildClaudeArgs({ mode, prompt: `${command} ${pr.number}`, model, effort }),
2947
+ execution.command,
2948
+ execution.args,
2879
2949
  pr.number,
2880
2950
  `PR #${pr.number} (${pr.headRefName})`,
2881
2951
  config.name,
@@ -3539,6 +3609,23 @@ async function assertRunModeAvailable() {
3539
3609
  }
3540
3610
  console.log("[worker] run mode: herdr (each task runs as a TUI session in its own herdr tab)");
3541
3611
  }
3612
+ async function assertHeadroomAvailable() {
3613
+ if (!getHeadroomEnabled()) return;
3614
+ const { execFile: execFile3 } = await import("node:child_process");
3615
+ const { promisify: promisify4 } = await import("node:util");
3616
+ try {
3617
+ await promisify4(execFile3)(HEADROOM_COMMAND, ["--version"]);
3618
+ } catch (err) {
3619
+ const message = err instanceof Error ? err.message : String(err);
3620
+ console.error(`[worker] config.json has headroom: true but the headroom CLI is unavailable: ${message}`);
3621
+ process.exit(1);
3622
+ }
3623
+ console.log("[worker] headroom: enabled (each task runs as `headroom wrap claude`)");
3624
+ }
3625
+ async function assertRunPrerequisites() {
3626
+ await assertRunModeAvailable();
3627
+ await assertHeadroomAvailable();
3628
+ }
3542
3629
  if (!hasProjectFilter()) {
3543
3630
  process.on("SIGTERM", async () => {
3544
3631
  if (isShuttingDown()) return;
@@ -3627,7 +3714,7 @@ if (hasProjectFilter()) {
3627
3714
  const epicFilters = parseEpicFilters();
3628
3715
  const labelFilters = parseLabelFilters();
3629
3716
  (async () => {
3630
- await assertRunModeAvailable();
3717
+ await assertRunPrerequisites();
3631
3718
  await removeStaleWorktrees();
3632
3719
  await Promise.all([
3633
3720
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3643,7 +3730,7 @@ if (hasProjectFilter()) {
3643
3730
  const epicFilters = parseEpicFilters();
3644
3731
  const labelFilters = parseLabelFilters();
3645
3732
  (async () => {
3646
- await assertRunModeAvailable();
3733
+ await assertRunPrerequisites();
3647
3734
  await removeStaleWorktrees();
3648
3735
  await Promise.all([
3649
3736
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3662,7 +3749,7 @@ if (hasProjectFilter()) {
3662
3749
  const epicFilters = parseEpicFilters();
3663
3750
  const labelFilters = parseLabelFilters();
3664
3751
  (async () => {
3665
- await assertRunModeAvailable();
3752
+ await assertRunPrerequisites();
3666
3753
  await removeStaleWorktrees();
3667
3754
  await WORKERS[workerType]({ epicFilters, labelFilters });
3668
3755
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "CLI tool that polls GitHub Issues/PRs and delegates work to Claude CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",