claude-task-worker 0.31.0 → 0.33.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 +124 -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,9 @@ 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";
1417
+ var HEADROOM_WRAP_OPTIONS = ["--1m", "--memory", "--code-graph"];
1381
1418
  function buildClaudeArgs({ mode, prompt, model, effort }) {
1382
1419
  return [
1383
1420
  ...mode === "herdr" ? [] : ["-p"],
@@ -1394,6 +1431,16 @@ function buildClaudeArgs({ mode, prompt, model, effort }) {
1394
1431
  effort
1395
1432
  ];
1396
1433
  }
1434
+ function buildClaudeExecution(invocation) {
1435
+ const args = buildClaudeArgs(invocation);
1436
+ if (!invocation.headroom) {
1437
+ return { command: CLAUDE_COMMAND, args };
1438
+ }
1439
+ return {
1440
+ command: HEADROOM_COMMAND,
1441
+ args: ["wrap", CLAUDE_COMMAND, ...HEADROOM_WRAP_OPTIONS, "--", ...args]
1442
+ };
1443
+ }
1397
1444
  function buildClaudeEnv(mode) {
1398
1445
  if (mode === "herdr") {
1399
1446
  return {
@@ -1648,25 +1695,7 @@ async function assertRemoteTrackingExists(epicBranch) {
1648
1695
  import { spawn } from "node:child_process";
1649
1696
  import { basename } from "node:path";
1650
1697
  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
- }
1698
+ init_task_result();
1670
1699
 
1671
1700
  // src/user-config.ts
1672
1701
  import { readFileSync as readFileSync2, statSync } from "node:fs";
@@ -1674,6 +1703,7 @@ import { homedir } from "node:os";
1674
1703
  import { isAbsolute, join as join2, resolve } from "node:path";
1675
1704
  var RESERVED_ALL = "all";
1676
1705
  var DEFAULT_RUN_MODE = "default";
1706
+ var DEFAULT_HEADROOM = false;
1677
1707
  var UserConfigError = class extends Error {
1678
1708
  constructor(message) {
1679
1709
  super(message);
@@ -1722,6 +1752,13 @@ function parseMode(raw, path) {
1722
1752
  console.warn(`[config] invalid mode: ${JSON.stringify(value)} in ${path}, using "${DEFAULT_RUN_MODE}"`);
1723
1753
  return DEFAULT_RUN_MODE;
1724
1754
  }
1755
+ function parseHeadroom(raw, path) {
1756
+ if (!("headroom" in raw)) return DEFAULT_HEADROOM;
1757
+ const value = raw["headroom"];
1758
+ if (typeof value === "boolean") return value;
1759
+ console.warn(`[config] invalid headroom: ${JSON.stringify(value)} in ${path}, using ${DEFAULT_HEADROOM}`);
1760
+ return DEFAULT_HEADROOM;
1761
+ }
1725
1762
  function loadUserConfig() {
1726
1763
  const path = getUserConfigPath();
1727
1764
  const raw = readRawConfig();
@@ -1738,6 +1775,7 @@ function loadUserConfig() {
1738
1775
  throw new UserConfigError(`config.json "projectGroups" must be an object: ${path}`);
1739
1776
  }
1740
1777
  const mode = parseMode(raw, path);
1778
+ const headroom = parseHeadroom(raw, path);
1741
1779
  const rawProjects = raw["projects"];
1742
1780
  const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
1743
1781
  const projectKeys = Object.keys(rawProjects);
@@ -1788,7 +1826,7 @@ function loadUserConfig() {
1788
1826
  }
1789
1827
  projectGroups[groupName] = members;
1790
1828
  }
1791
- return { mode, projects, projectGroups };
1829
+ return { mode, headroom, projects, projectGroups };
1792
1830
  }
1793
1831
  var cachedRunMode;
1794
1832
  function getRunMode() {
@@ -1798,18 +1836,35 @@ function getRunMode() {
1798
1836
  return cachedRunMode;
1799
1837
  }
1800
1838
  function readRunMode() {
1839
+ const raw = readTopLevelConfig(`using "${DEFAULT_RUN_MODE}" mode`);
1840
+ if (raw === void 0) return DEFAULT_RUN_MODE;
1841
+ return parseMode(raw, getUserConfigPath());
1842
+ }
1843
+ var cachedHeadroom;
1844
+ function getHeadroomEnabled() {
1845
+ if (cachedHeadroom === void 0) {
1846
+ cachedHeadroom = readHeadroom();
1847
+ }
1848
+ return cachedHeadroom;
1849
+ }
1850
+ function readHeadroom() {
1851
+ const raw = readTopLevelConfig(`using headroom=${DEFAULT_HEADROOM}`);
1852
+ if (raw === void 0) return DEFAULT_HEADROOM;
1853
+ return parseHeadroom(raw, getUserConfigPath());
1854
+ }
1855
+ function readTopLevelConfig(fallbackDescription) {
1801
1856
  let raw;
1802
1857
  try {
1803
1858
  raw = readRawConfig();
1804
1859
  } catch (err) {
1805
- console.warn(`[config] failed to read config file, using "${DEFAULT_RUN_MODE}" mode: ${err}`);
1806
- return DEFAULT_RUN_MODE;
1860
+ console.warn(`[config] failed to read config file, ${fallbackDescription}: ${err}`);
1861
+ return void 0;
1807
1862
  }
1808
- if (raw === void 0) return DEFAULT_RUN_MODE;
1863
+ if (raw === void 0) return void 0;
1809
1864
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1810
- return DEFAULT_RUN_MODE;
1865
+ return void 0;
1811
1866
  }
1812
- return parseMode(raw, getUserConfigPath());
1867
+ return raw;
1813
1868
  }
1814
1869
  function findProjectNameByPath(path) {
1815
1870
  let config;
@@ -1945,6 +2000,7 @@ async function runViaHerdr(command, args, id, onComplete, cwd, env) {
1945
2000
  herdrTasks.set(id, task);
1946
2001
  result = await waitForHerdrTask2(task.paneId, {
1947
2002
  signal: herdrAbortSignal,
2003
+ headroom: getHeadroomEnabled(),
1948
2004
  onBlocked: () => console.warn(`[worker] #${id} is blocked and waiting for input in herdr tab "${label}"`),
1949
2005
  onStatus: (status) => {
1950
2006
  const task2 = tasks.get(id);
@@ -2002,7 +2058,8 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
2002
2058
  const result = buildTaskResult(
2003
2059
  code,
2004
2060
  Buffer.concat(outputChunks).toString("utf-8"),
2005
- Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
2061
+ Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT),
2062
+ { headroom: getHeadroomEnabled() }
2006
2063
  );
2007
2064
  await finishTask(id, result, onComplete);
2008
2065
  childProcesses.delete(id);
@@ -2711,7 +2768,13 @@ function createIssuePollingWorker(config) {
2711
2768
  const command = skill || config.command;
2712
2769
  const parentNumber = issue.parent?.number;
2713
2770
  const mode = getRunMode();
2714
- const claudeArgs = buildClaudeArgs({ mode, prompt: `${command} ${issue.number}`, model, effort });
2771
+ const execution = buildClaudeExecution({
2772
+ mode,
2773
+ prompt: `${command} ${issue.number}`,
2774
+ model,
2775
+ effort,
2776
+ headroom: getHeadroomEnabled()
2777
+ });
2715
2778
  let baseBranch = defaultBranch;
2716
2779
  if (parentNumber !== void 0) {
2717
2780
  baseBranch = `cc-epic-${parentNumber}`;
@@ -2721,8 +2784,8 @@ function createIssuePollingWorker(config) {
2721
2784
  const cwd = getWorktreePath(worktreeId);
2722
2785
  console.log(`[${config.name}] #${issue.number}: created worktree ${worktreeId} from ${baseBranch}`);
2723
2786
  run(
2724
- "claude",
2725
- claudeArgs,
2787
+ execution.command,
2788
+ execution.args,
2726
2789
  issue.number,
2727
2790
  issue.title,
2728
2791
  config.name,
@@ -2877,9 +2940,16 @@ function createPrPollingWorker(config) {
2877
2940
  const { model, effort, skill } = getWorkerConfig(config.name);
2878
2941
  const command = skill || config.command;
2879
2942
  const mode = getRunMode();
2943
+ const execution = buildClaudeExecution({
2944
+ mode,
2945
+ prompt: `${command} ${pr.number}`,
2946
+ model,
2947
+ effort,
2948
+ headroom: getHeadroomEnabled()
2949
+ });
2880
2950
  run(
2881
- "claude",
2882
- buildClaudeArgs({ mode, prompt: `${command} ${pr.number}`, model, effort }),
2951
+ execution.command,
2952
+ execution.args,
2883
2953
  pr.number,
2884
2954
  `PR #${pr.number} (${pr.headRefName})`,
2885
2955
  config.name,
@@ -3543,6 +3613,23 @@ async function assertRunModeAvailable() {
3543
3613
  }
3544
3614
  console.log("[worker] run mode: herdr (each task runs as a TUI session in its own herdr tab)");
3545
3615
  }
3616
+ async function assertHeadroomAvailable() {
3617
+ if (!getHeadroomEnabled()) return;
3618
+ const { execFile: execFile3 } = await import("node:child_process");
3619
+ const { promisify: promisify4 } = await import("node:util");
3620
+ try {
3621
+ await promisify4(execFile3)(HEADROOM_COMMAND, ["--version"]);
3622
+ } catch (err) {
3623
+ const message = err instanceof Error ? err.message : String(err);
3624
+ console.error(`[worker] config.json has headroom: true but the headroom CLI is unavailable: ${message}`);
3625
+ process.exit(1);
3626
+ }
3627
+ console.log("[worker] headroom: enabled (each task runs as `headroom wrap claude`)");
3628
+ }
3629
+ async function assertRunPrerequisites() {
3630
+ await assertRunModeAvailable();
3631
+ await assertHeadroomAvailable();
3632
+ }
3546
3633
  if (!hasProjectFilter()) {
3547
3634
  process.on("SIGTERM", async () => {
3548
3635
  if (isShuttingDown()) return;
@@ -3631,7 +3718,7 @@ if (hasProjectFilter()) {
3631
3718
  const epicFilters = parseEpicFilters();
3632
3719
  const labelFilters = parseLabelFilters();
3633
3720
  (async () => {
3634
- await assertRunModeAvailable();
3721
+ await assertRunPrerequisites();
3635
3722
  await removeStaleWorktrees();
3636
3723
  await Promise.all([
3637
3724
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3647,7 +3734,7 @@ if (hasProjectFilter()) {
3647
3734
  const epicFilters = parseEpicFilters();
3648
3735
  const labelFilters = parseLabelFilters();
3649
3736
  (async () => {
3650
- await assertRunModeAvailable();
3737
+ await assertRunPrerequisites();
3651
3738
  await removeStaleWorktrees();
3652
3739
  await Promise.all([
3653
3740
  execIssueWorker({ epicFilters, labelFilters }),
@@ -3666,7 +3753,7 @@ if (hasProjectFilter()) {
3666
3753
  const epicFilters = parseEpicFilters();
3667
3754
  const labelFilters = parseLabelFilters();
3668
3755
  (async () => {
3669
- await assertRunModeAvailable();
3756
+ await assertRunPrerequisites();
3670
3757
  await removeStaleWorktrees();
3671
3758
  await WORKERS[workerType]({ epicFilters, labelFilters });
3672
3759
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.31.0",
3
+ "version": "0.33.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",