claude-task-worker 0.31.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 +120 -37
  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
@@ -132,6 +132,38 @@ var init_table = __esm({
132
132
  }
133
133
  });
134
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
+
135
167
  // src/herdr.ts
136
168
  var herdr_exports = {};
137
169
  __export(herdr_exports, {
@@ -490,8 +522,9 @@ function observeAgentStatus(tracker, status) {
490
522
  }
491
523
  return { tracker, decision: "running" };
492
524
  }
493
- function buildHerdrTaskResult(paneOutput) {
494
- if (paneOutput.trim() === "") {
525
+ function buildHerdrTaskResult(paneOutput, options) {
526
+ const meaningful = options?.headroom ? stripHeadroomBanner(paneOutput) : paneOutput;
527
+ if (meaningful.trim() === "") {
495
528
  return {
496
529
  status: "failed",
497
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)"
@@ -549,7 +582,7 @@ async function waitForHerdrTask(paneId, options) {
549
582
  tracker = observed.tracker;
550
583
  if (observed.decision === "completed") {
551
584
  const output = await readPaneOutput(paneId, mod);
552
- return buildHerdrTaskResult(output);
585
+ return buildHerdrTaskResult(output, { headroom: options?.headroom });
553
586
  }
554
587
  if (observed.decision === "blocked-first-seen") {
555
588
  options?.onBlocked?.();
@@ -609,6 +642,7 @@ var AGENT_POLL_INTERVAL_MS, PANE_OUTPUT_LINES, CLAUDE_EXIT_TIMEOUT_MS, CLAUDE_EX
609
642
  var init_herdr_runner = __esm({
610
643
  "src/herdr-runner.ts"() {
611
644
  "use strict";
645
+ init_task_result();
612
646
  AGENT_POLL_INTERVAL_MS = 3 * 1e3;
613
647
  PANE_OUTPUT_LINES = 300;
614
648
  CLAUDE_EXIT_TIMEOUT_MS = 15 * 1e3;
@@ -1378,6 +1412,8 @@ var SYSTEM_PROMPT = `\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F \`claude-t
1378
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
1379
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
1380
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";
1381
1417
  function buildClaudeArgs({ mode, prompt, model, effort }) {
1382
1418
  return [
1383
1419
  ...mode === "herdr" ? [] : ["-p"],
@@ -1394,6 +1430,13 @@ function buildClaudeArgs({ mode, prompt, model, effort }) {
1394
1430
  effort
1395
1431
  ];
1396
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
+ }
1397
1440
  function buildClaudeEnv(mode) {
1398
1441
  if (mode === "herdr") {
1399
1442
  return {
@@ -1648,25 +1691,7 @@ async function assertRemoteTrackingExists(epicBranch) {
1648
1691
  import { spawn } from "node:child_process";
1649
1692
  import { basename } from "node:path";
1650
1693
  init_table();
1651
-
1652
- // src/task-result.ts
1653
- var STDERR_TAIL_LIMIT = 8 * 1024;
1654
- function buildTaskResult(code, stdout, stderrTail) {
1655
- const emptyOutput = stdout.trim() === "";
1656
- const completed = code === 0 && !emptyOutput;
1657
- let output = stdout;
1658
- if (code === 0 && emptyOutput) {
1659
- output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
1660
- } else if (!completed) {
1661
- output += `
1662
- [worker] claude exited with code ${code}`;
1663
- }
1664
- if (!completed && stderrTail.trim() !== "") {
1665
- output += `
1666
- [stderr] ${stderrTail.trim()}`;
1667
- }
1668
- return { status: completed ? "completed" : "failed", output };
1669
- }
1694
+ init_task_result();
1670
1695
 
1671
1696
  // src/user-config.ts
1672
1697
  import { readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1674,6 +1699,7 @@ import { homedir } from "node:os";
1674
1699
  import { isAbsolute, join as join2, resolve } from "node:path";
1675
1700
  var RESERVED_ALL = "all";
1676
1701
  var DEFAULT_RUN_MODE = "default";
1702
+ var DEFAULT_HEADROOM = false;
1677
1703
  var UserConfigError = class extends Error {
1678
1704
  constructor(message) {
1679
1705
  super(message);
@@ -1722,6 +1748,13 @@ function parseMode(raw, path) {
1722
1748
  console.warn(`[config] invalid mode: ${JSON.stringify(value)} in ${path}, using "${DEFAULT_RUN_MODE}"`);
1723
1749
  return DEFAULT_RUN_MODE;
1724
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
+ }
1725
1758
  function loadUserConfig() {
1726
1759
  const path = getUserConfigPath();
1727
1760
  const raw = readRawConfig();
@@ -1738,6 +1771,7 @@ function loadUserConfig() {
1738
1771
  throw new UserConfigError(`config.json "projectGroups" must be an object: ${path}`);
1739
1772
  }
1740
1773
  const mode = parseMode(raw, path);
1774
+ const headroom = parseHeadroom(raw, path);
1741
1775
  const rawProjects = raw["projects"];
1742
1776
  const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
1743
1777
  const projectKeys = Object.keys(rawProjects);
@@ -1788,7 +1822,7 @@ function loadUserConfig() {
1788
1822
  }
1789
1823
  projectGroups[groupName] = members;
1790
1824
  }
1791
- return { mode, projects, projectGroups };
1825
+ return { mode, headroom, projects, projectGroups };
1792
1826
  }
1793
1827
  var cachedRunMode;
1794
1828
  function getRunMode() {
@@ -1798,18 +1832,35 @@ function getRunMode() {
1798
1832
  return cachedRunMode;
1799
1833
  }
1800
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) {
1801
1852
  let raw;
1802
1853
  try {
1803
1854
  raw = readRawConfig();
1804
1855
  } catch (err) {
1805
- console.warn(`[config] failed to read config file, using "${DEFAULT_RUN_MODE}" mode: ${err}`);
1806
- return DEFAULT_RUN_MODE;
1856
+ console.warn(`[config] failed to read config file, ${fallbackDescription}: ${err}`);
1857
+ return void 0;
1807
1858
  }
1808
- if (raw === void 0) return DEFAULT_RUN_MODE;
1859
+ if (raw === void 0) return void 0;
1809
1860
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1810
- return DEFAULT_RUN_MODE;
1861
+ return void 0;
1811
1862
  }
1812
- return parseMode(raw, getUserConfigPath());
1863
+ return raw;
1813
1864
  }
1814
1865
  function findProjectNameByPath(path) {
1815
1866
  let config;
@@ -1945,6 +1996,7 @@ async function runViaHerdr(command, args, id, onComplete, cwd, env) {
1945
1996
  herdrTasks.set(id, task);
1946
1997
  result = await waitForHerdrTask2(task.paneId, {
1947
1998
  signal: herdrAbortSignal,
1999
+ headroom: getHeadroomEnabled(),
1948
2000
  onBlocked: () => console.warn(`[worker] #${id} is blocked and waiting for input in herdr tab "${label}"`),
1949
2001
  onStatus: (status) => {
1950
2002
  const task2 = tasks.get(id);
@@ -2002,7 +2054,8 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
2002
2054
  const result = buildTaskResult(
2003
2055
  code,
2004
2056
  Buffer.concat(outputChunks).toString("utf-8"),
2005
- Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
2057
+ Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT),
2058
+ { headroom: getHeadroomEnabled() }
2006
2059
  );
2007
2060
  await finishTask(id, result, onComplete);
2008
2061
  childProcesses.delete(id);
@@ -2711,7 +2764,13 @@ function createIssuePollingWorker(config) {
2711
2764
  const command = skill || config.command;
2712
2765
  const parentNumber = issue.parent?.number;
2713
2766
  const mode = getRunMode();
2714
- 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
+ });
2715
2774
  let baseBranch = defaultBranch;
2716
2775
  if (parentNumber !== void 0) {
2717
2776
  baseBranch = `cc-epic-${parentNumber}`;
@@ -2721,8 +2780,8 @@ function createIssuePollingWorker(config) {
2721
2780
  const cwd = getWorktreePath(worktreeId);
2722
2781
  console.log(`[${config.name}] #${issue.number}: created worktree ${worktreeId} from ${baseBranch}`);
2723
2782
  run(
2724
- "claude",
2725
- claudeArgs,
2783
+ execution.command,
2784
+ execution.args,
2726
2785
  issue.number,
2727
2786
  issue.title,
2728
2787
  config.name,
@@ -2877,9 +2936,16 @@ function createPrPollingWorker(config) {
2877
2936
  const { model, effort, skill } = getWorkerConfig(config.name);
2878
2937
  const command = skill || config.command;
2879
2938
  const mode = getRunMode();
2939
+ const execution = buildClaudeExecution({
2940
+ mode,
2941
+ prompt: `${command} ${pr.number}`,
2942
+ model,
2943
+ effort,
2944
+ headroom: getHeadroomEnabled()
2945
+ });
2880
2946
  run(
2881
- "claude",
2882
- buildClaudeArgs({ mode, prompt: `${command} ${pr.number}`, model, effort }),
2947
+ execution.command,
2948
+ execution.args,
2883
2949
  pr.number,
2884
2950
  `PR #${pr.number} (${pr.headRefName})`,
2885
2951
  config.name,
@@ -3543,6 +3609,23 @@ async function assertRunModeAvailable() {
3543
3609
  }
3544
3610
  console.log("[worker] run mode: herdr (each task runs as a TUI session in its own herdr tab)");
3545
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
+ }
3546
3629
  if (!hasProjectFilter()) {
3547
3630
  process.on("SIGTERM", async () => {
3548
3631
  if (isShuttingDown()) return;
@@ -3631,7 +3714,7 @@ if (hasProjectFilter()) {
3631
3714
  const epicFilters = parseEpicFilters();
3632
3715
  const labelFilters = parseLabelFilters();
3633
3716
  (async () => {
3634
- await assertRunModeAvailable();
3717
+ await assertRunPrerequisites();
3635
3718
  await removeStaleWorktrees();
3636
3719
  await Promise.all([
3637
3720
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3647,7 +3730,7 @@ if (hasProjectFilter()) {
3647
3730
  const epicFilters = parseEpicFilters();
3648
3731
  const labelFilters = parseLabelFilters();
3649
3732
  (async () => {
3650
- await assertRunModeAvailable();
3733
+ await assertRunPrerequisites();
3651
3734
  await removeStaleWorktrees();
3652
3735
  await Promise.all([
3653
3736
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3666,7 +3749,7 @@ if (hasProjectFilter()) {
3666
3749
  const epicFilters = parseEpicFilters();
3667
3750
  const labelFilters = parseLabelFilters();
3668
3751
  (async () => {
3669
- await assertRunModeAvailable();
3752
+ await assertRunPrerequisites();
3670
3753
  await removeStaleWorktrees();
3671
3754
  await WORKERS[workerType]({ epicFilters, labelFilters });
3672
3755
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.31.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",