claude-task-worker 0.6.0 → 0.11.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 +126 -19
  2. package/dist/index.js +692 -65
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -48,7 +48,7 @@ GitHub Issues/PRを定期ポーリングし、Claude Codeに処理を委譲す
48
48
  | `fix-review-point` | `cc-fix-onetime` | `/claude-task-worker:fix-review-point` | 1分 |
49
49
  | `triage-created-issue` | `cc-issue-created` + `cc-triage-scope` (Issue) | `/claude-task-worker:triage-created-issue` | 1分 |
50
50
  | `triage-pr` | `cc-triage-scope` (PR) | `/claude-task-worker:triage-pr` | 1分 |
51
- | `resolve-conflict` | `cc-resolve-conflict` (PR) | `/claude-task-worker:resolve-conflict` | 1分 |
51
+ | `resolve-conflict` | `cc-resolve-conflict` (PR) | `/claude-task-worker:resolve-pr-conflict` | 1分 |
52
52
  | `check-dependabot` | `dependencies` (PR) | `/claude-task-worker:check-dependabot` | 1時間 |
53
53
  | `epic-issue` | `cc-epic-issue` (Issue, sub-issues が全て Close) | `/claude-task-worker:create-epic-pr` | 5分 |
54
54
 
@@ -60,13 +60,63 @@ GitHub Issues/PRを定期ポーリングし、Claude Codeに処理を委譲す
60
60
 
61
61
  `epic-issue` ワーカーは、`cc-epic-issue` ラベル付きの親Issueに紐づくサブIssueがすべて Close されたタイミングで `/claude-task-worker:create-epic-pr` を起動し、エピックブランチからまとめてPRを作成する。
62
62
 
63
+ ### `--project` 指定時(ディスパッチャーモード)
64
+
65
+ `--project` オプションを指定した場合、CLIは自身でワーカーを起動する代わりに [herdr](https://herdr.dev) 経由のディスパッチャーとして動作する。指定したプロジェクトごとにherdrのタブ(ペイン)を作成し、そのプロジェクトのディレクトリで(`--project` を除いた)同じコマンドを非同期実行させ、稼働状況をステータステーブルで監視する。詳細は「[`--project <name>` オプション](#--project-name-オプション)」を参照。
66
+
67
+ ## 必要なライブラリ
68
+
69
+ ### 実行時に必要なもの
70
+
71
+ CLI 本体に npm の実行時依存パッケージはない(ビルド時に esbuild で単一ファイル `dist/index.js` にバンドルされ、Node.js 標準モジュールのみで動作する)。代わりに以下の外部ツールが必要。
72
+
73
+ | 名前 | バージョン | 用途 |
74
+ |---|---|---|
75
+ | [Node.js](https://nodejs.org/) | >= 22.6.0 | CLI の実行ランタイム(`--experimental-strip-types` を使用するテスト実行に必要) |
76
+ | [GitHub CLI (`gh`)](https://cli.github.com/) | - | Issue/PR の取得・ラベル操作など全GitHub操作(認証済みであること) |
77
+ | [Claude Code (`claude`)](https://docs.anthropic.com/en/docs/claude-code) | - | タスク実行エンジン。各ワーカーが Claude CLI プロセスとして起動する |
78
+ | [Git (`git`)](https://git-scm.com/) | - | worktree の作成・ブランチ操作 |
79
+ | [jq](https://jqlang.org/) | - | プラグインスキル内でのJSON加工(`check-dependabot` / `edit-pencil-design` スキルなどで使用) |
80
+ | [Pencil CLI (`pencil`)](https://docs.pencil.dev/for-developers/pencil-cli) | - | `.pen` デザインファイルの編集・参照・コンフリクト解消(`edit-pencil-design` / `inspect-pencil-node` / `resolve-pencil-conflict` スキルで使用) |
81
+ | [herdr](https://herdr.dev) | - | `--project` 使用時にのみ必要(通常ワーカー実行には不要)。複数プロジェクトへのディスパッチ・セッション監視・一括停止に使用 |
82
+ | `claude-task-worker` プラグイン | - | 各ワーカーが呼び出すスキル群(本リポジトリの `plugin/`) |
83
+
84
+ Slack通知(任意)を使う場合は Slack Incoming Webhook URL が必要。Claude API使用状況の取得には、macOSでは `security` コマンド(Keychain)を使用し、それ以外の環境では `~/.claude/.credentials.json` にフォールバックする。
85
+
86
+ ### 開発時に必要なライブラリ(devDependencies)
87
+
88
+ | パッケージ | バージョン | 用途 |
89
+ |---|---|---|
90
+ | `typescript` | ^5.7.0 | 型チェック(`tsc --noEmit`) |
91
+ | `esbuild` | ^0.27.4 | `dist/index.js` へのバンドル |
92
+ | `@types/node` | ^22.0.0 | Node.js の型定義 |
93
+ | `eslint` | ^10.3.0 | Lint |
94
+ | `@eslint/js` | ^10.0.1 | ESLint 推奨設定 |
95
+ | `typescript-eslint` | ^8.59.1 | TypeScript 対応の ESLint ルール |
96
+ | `eslint-config-prettier` | ^10.1.8 | ESLint と Prettier の競合ルール無効化 |
97
+ | `prettier` | ^3.8.3 | コードフォーマッタ |
98
+
99
+ ### プラグインが利用する MCP サーバー
100
+
101
+ `claude-task-worker` プラグイン(`plugin/.mcp.json`)は以下の MCP サーバーを定義しており、プラグイン有効化時に Claude Code から自動的に利用される。
102
+
103
+ | サーバー | 接続方法 | 用途 |
104
+ |---|---|---|
105
+ | `context7` | HTTP(`https://mcp.context7.com/mcp`) | ライブラリの最新ドキュメント取得 |
106
+ | `next-devtools` | `npx -y next-devtools-mcp@latest` | Next.js 開発支援 |
107
+ | `shadcn` | `npx shadcn@latest mcp` | shadcn/ui コンポーネント情報の取得 |
108
+
63
109
  ## セットアップ
64
110
 
65
111
  ### 前提条件
66
112
 
113
+ - [Node.js](https://nodejs.org/) v22.6 以上がインストール済みであること
67
114
  - [GitHub CLI (`gh`)](https://cli.github.com/) がインストール・認証済みであること
68
115
  - [Claude Code (`claude`)](https://docs.anthropic.com/en/docs/claude-code) がインストール済みであること
69
116
  - `claude-task-worker` プラグインがインストール済みであること(下記インストール手順を参照)
117
+ - `--project` オプションを使う場合のみ、[herdr](https://herdr.dev) がインストール済みであること(通常のワーカー実行には不要)
118
+
119
+ 詳細は上記「[必要なライブラリ](#必要なライブラリ)」を参照。
70
120
 
71
121
  ### インストール
72
122
 
@@ -111,6 +161,16 @@ claude plugin install claude-task-worker@claude-task-worker
111
161
 
112
162
  インストール後、Claude Code のセッションを再起動するとプラグインが有効化される。
113
163
 
164
+ herdr(`--project` オプションを使う場合のみ必要。通常ワーカー実行には不要):
165
+
166
+ ```bash
167
+ curl -fsSL https://herdr.dev/install.sh | sh
168
+ # または
169
+ brew install herdr
170
+ ```
171
+
172
+ 詳細は [herdr インストールドキュメント](https://herdr.dev/docs/install/) を参照。
173
+
114
174
  #### 更新
115
175
 
116
176
  CLI とプラグイン(マーケットプレイス)をまとめて更新する。
@@ -153,7 +213,7 @@ claude-task-worker init --force # 既存ファイルを強制上書き
153
213
 
154
214
  - `.github/ISSUE_TEMPLATE/cc-triage-scope.yml` — `cc-triage-scope` ラベル付きIssue作成用テンプレート
155
215
  - `.github/workflows/assign-creator-on-cc-triage-scope.yml` — Issue作成者を自動アサインするワークフロー
156
- - `claude-task-worker.json` — 設定ファイル(コマンド実行ディレクトリ直下)
216
+ - `claude-task-worker.json` — 設定ファイル(コマンド実行ディレクトリ直下。全ワーカーのデフォルト設定が書き込まれた状態で作成される)
157
217
 
158
218
  ## コマンド
159
219
 
@@ -188,6 +248,49 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
188
248
 
189
249
  > ℹ️ `--label` で指定したラベルはユーザーのスコープ指定なので、タスク完了時にワーカー側で除去されることはない(トリガーラベルだけが除去される)。
190
250
 
251
+ ### `--project <name>` オプション
252
+
253
+ 指定したプロジェクト(またはプロジェクトグループ、`all`)に対して、[herdr](https://herdr.dev) 経由でコマンドをディスパッチする。指定すると、CLIはワーカーをその場で実行する代わりにディスパッチャーとして動作し、対象プロジェクトごとに独立したherdrタブでコマンドを実行する。
254
+
255
+ プロジェクト名・グループ名・`all` のいずれかを指定できる。
256
+
257
+ - プロジェクト名: `projects.json` の `projects` に登録された個別プロジェクト名
258
+ - グループ名: `projects.json` の `projectGroups` に登録された、複数プロジェクト名をまとめたグループ名
259
+ - `all`: `projects.json` の `projects` に登録された全プロジェクトが対象になる予約語(`projects` / `projectGroups` のキーとして使用不可)
260
+
261
+ `projects.json` は `$XDG_CONFIG_HOME/claude-task-worker/projects.json`(`XDG_CONFIG_HOME` 未設定の場合は `~/.config/claude-task-worker/projects.json`)に配置する。`projects` にプロジェクト名から絶対パスへのマッピングを、`projectGroups` にグループ名からプロジェクト名配列へのマッピングを記述する(`projectGroups` は省略可)。
262
+
263
+ ```json
264
+ {
265
+ "projects": {
266
+ "app-a": "/Users/me/repos/app-a",
267
+ "app-b": "/Users/me/repos/app-b",
268
+ "app-c": "/Users/me/repos/app-c"
269
+ },
270
+ "projectGroups": {
271
+ "frontend": ["app-a", "app-b"]
272
+ }
273
+ }
274
+ ```
275
+
276
+ `--project` は繰り返し指定可能で、複数指定した場合は解決後のプロジェクト集合の和集合が対象になる(重複は一意化される)。`--epic` / `--label` と併用でき、ディスパッチ先の各プロジェクトで実行されるコマンドにそのまま引き継がれる。
277
+
278
+ 以下のコマンドは `--project` と併用できない: `init` / `install` / `update` / `usage` / `version`
279
+
280
+ ディスパッチャーは以下の3つの機能を持つ。
281
+
282
+ - **一斉起動**: 対象プロジェクトごとにherdrタブを作成し、そのプロジェクトのディレクトリで(`--project` を除いた)同じコマンドを非同期実行する
283
+ - **稼働一覧**: 各セッションのプロジェクト名・タブID・ペインID・ステータス・稼働時間をステータステーブルとして定期的に画面へ描画し、対象プロセスが終了したセッションは自動的に一覧から除去する
284
+ - **一括停止**: SIGTERM/SIGINTを受けると、稼働中の全セッションへ ctrl-c を送信して各プロジェクトのコマンドを終了させ、終了を待ってからherdrタブを閉じる
285
+
286
+ ```bash
287
+ claude-task-worker all --project all
288
+ claude-task-worker all --project app-a
289
+ claude-task-worker all --project frontend
290
+ claude-task-worker all --project app-a --project app-c # app-a と app-c の和集合
291
+ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
292
+ ```
293
+
191
294
  ### exec-issue
192
295
 
193
296
  `cc-exec-issue` ラベルが付いた自分にアサインされたIssueを定期取得し、Claude Codeで処理を実行する。(デフォルト1分間隔)
@@ -202,6 +305,7 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
202
305
  `cc-fix-onetime` ラベルが付いたPRを定期取得し、Claude Codeで修正を実行する。(1分間隔)
203
306
 
204
307
  - CI完了済みで `cc-in-progress` がないPRが対象
308
+ - 完了後、`cc-fix-onetime` ラベルを除去
205
309
  - 完了後、設定ファイルに `fixReviewPointCallbackCommentMessage` が設定されていればPRにコメント投稿
206
310
 
207
311
  ### create-issue
@@ -210,7 +314,8 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
210
314
 
211
315
  `init` コマンドで作成されるIssueテンプレートを使えば、`cc-triage-scope` ラベル付与と作成者アサインが自動で行われる。ブロック中の依存 Issue が残っている間はワーカーが拾わず、依存がすべて Close された時点で処理が開始される。
212
316
 
213
- 除外ラベル: `cc-issue-created` / `cc-pr-created` / `cc-update-issue` / `cc-answer-issue-questions` / `cc-exec-issue` のいずれかが付いている Issue は対象外。
317
+ - 除外ラベル: `cc-issue-created` / `cc-pr-created` / `cc-update-issue` / `cc-answer-issue-questions` / `cc-exec-issue` のいずれかが付いている Issue は対象外
318
+ - 完了後、`cc-issue-created` ラベルを付与して triage-created-issue ワーカーに引き継ぎ
214
319
 
215
320
  ### update-issue
216
321
 
@@ -240,7 +345,7 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
240
345
 
241
346
  ### resolve-conflict
242
347
 
243
- `cc-resolve-conflict` ラベルが付いたPRを定期取得し、`/claude-task-worker:resolve-conflict` を実行してコンフリクト解消を行う。(1分間隔)
348
+ `cc-resolve-conflict` ラベルが付いたPRを定期取得し、`/claude-task-worker:resolve-pr-conflict` を実行してコンフリクト解消を行う。(1分間隔)
244
349
 
245
350
  - 完了後、`cc-resolve-conflict` ラベルを除去
246
351
 
@@ -270,7 +375,7 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
270
375
 
271
376
  ### usage
272
377
 
273
- 現在のClaude API使用状況をSlackに通知する。
378
+ 現在のClaude API使用状況(5時間/7日間の利用率とリセット時刻)を標準出力に表示し、Slack Webhookが設定されていればSlackにも通知する。
274
379
 
275
380
  ### install
276
381
 
@@ -310,7 +415,7 @@ claude-task-worker --version
310
415
  claude-task-worker -v
311
416
  ```
312
417
 
313
- `package.json` の `version` を出力する(例: `0.2.0`)。
418
+ `package.json` の `version` を出力する(例: `0.6.0`)。
314
419
 
315
420
  ## 設定ファイル
316
421
 
@@ -328,15 +433,15 @@ claude-task-worker -v
328
433
  | ワーカー名 | デフォルト `skill` | デフォルト `model` | デフォルト `effort` | デフォルト `pollingIntervalSeconds` | デフォルト `cooldownSeconds` | デフォルト `maxConcurrentTasks` |
329
434
  |---|---|---|---|---|---|---|
330
435
  | `answer-issue-questions` | `/claude-task-worker:answer-issue-questions` | `opus` | `xhigh` | 60 | 0 | 1 |
331
- | `create-issue` | `/claude-task-worker:create-issue-from-issue-number` | `opus` | `xhigh` | 60 | 0 | 1 |
332
- | `update-issue` | `/claude-task-worker:update-issue` | `sonnet` | `xhigh` | 60 | 0 | 1 |
333
- | `exec-issue` | `/claude-task-worker:exec-issue` | `sonnet` | `xhigh` | 60 | 0 | 1 |
334
- | `fix-review-point` | `/claude-task-worker:fix-review-point` | `sonnet` | `xhigh` | 60 | 0 | 1 |
335
- | `triage-created-issue` | `/claude-task-worker:triage-created-issue` | `sonnet` | `xhigh` | 60 | 0 | 1 |
336
- | `triage-pr` | `/claude-task-worker:triage-pr` | `sonnet` | `xhigh` | 60 | 0 | 1 |
337
- | `resolve-conflict` | `/claude-task-worker:resolve-pr-conflict` | `sonnet` | `xhigh` | 60 | 0 | 1 |
338
- | `check-dependabot` | `/claude-task-worker:check-dependabot` | `sonnet` | `xhigh` | 3600 | 0 | 1 |
339
- | `epic-issue` | `/claude-task-worker:create-epic-pr` | `sonnet` | `xhigh` | 300 | 0 | 1 |
436
+ | `create-issue` | `/claude-task-worker:create-issue-from-issue-number` | `sonnet` | `xhigh` | 60 | 0 | 1 |
437
+ | `update-issue` | `/claude-task-worker:update-issue` | `sonnet` | `high` | 60 | 0 | 1 |
438
+ | `exec-issue` | `/claude-task-worker:exec-issue` | `sonnet` | `high` | 60 | 0 | 1 |
439
+ | `fix-review-point` | `/claude-task-worker:fix-review-point` | `sonnet` | `high` | 60 | 0 | 1 |
440
+ | `triage-created-issue` | `/claude-task-worker:triage-created-issue` | `sonnet` | `high` | 60 | 0 | 1 |
441
+ | `triage-pr` | `/claude-task-worker:triage-pr` | `sonnet` | `high` | 60 | 0 | 1 |
442
+ | `resolve-conflict` | `/claude-task-worker:resolve-pr-conflict` | `sonnet` | `high` | 60 | 0 | 1 |
443
+ | `check-dependabot` | `/claude-task-worker:check-dependabot` | `sonnet` | `high` | 3600 | 0 | 1 |
444
+ | `epic-issue` | `/claude-task-worker:create-epic-pr` | `sonnet` | `high` | 300 | 0 | 1 |
340
445
 
341
446
  各フィールドの値:
342
447
 
@@ -362,8 +467,6 @@ claude-task-worker -v
362
467
  }
363
468
  ```
364
469
 
365
- > 💡 **推奨:** 重い実装タスク(`exec-issue`, `fix-review-point`)では `/advisor` を有効にすることを推奨する。事前/事後に強力なレビューモデルへ相談することで、アプローチの誤りや見落としを早期に検出でき、完了率と品質が向上する。各ワーカーが呼び出すスキルのプロンプト内で `advisor` ツールを活用する運用を推奨。
366
-
367
470
  ## Slack通知
368
471
 
369
472
  環境変数 `CLAUDE_TASK_WORKER_SLACK_WEBHOOK_URL` にSlack Incoming Webhook URLを設定すると、各ワーカーのタスク完了時・失敗時にSlackへ通知が送信される。
@@ -387,8 +490,12 @@ claude-task-worker all
387
490
 
388
491
  ```bash
389
492
  npm install
390
- npm run build # TypeScript dist/
391
- npm run dev # Watch mode (auto-rebuild)
493
+ npm run build # 型チェック(tsc --noEmit)+ esbuild で dist/index.js にバンドル
494
+ npm run dev # 型チェックの watch モード
495
+ npm run lint # ESLint
496
+ npm run lint:fix # ESLint(自動修正)
497
+ npm run format # Prettier で整形
498
+ npm run format:check # Prettier のチェックのみ
392
499
  ```
393
500
 
394
501
  コントリビューションを歓迎します。開発環境のセットアップ・PRの出し方は [CONTRIBUTING.md](./CONTRIBUTING.md) を参照してください。バグ報告・機能要望は [Issue テンプレート](https://github.com/getty104/claude-task-worker/issues/new/choose) から作成してください。
package/dist/index.js CHANGED
@@ -1,4 +1,448 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/table.ts
13
+ var table_exports = {};
14
+ __export(table_exports, {
15
+ getDisplayWidth: () => getDisplayWidth,
16
+ padToWidth: () => padToWidth,
17
+ truncateToWidth: () => truncateToWidth
18
+ });
19
+ function getDisplayWidth(str) {
20
+ let width = 0;
21
+ for (const char of str) {
22
+ const code = char.codePointAt(0);
23
+ if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 12350 || code >= 12352 && code <= 13247 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 42191 || code >= 44032 && code <= 55215 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072 && code <= 196605 || code >= 196608 && code <= 262141) {
24
+ width += 2;
25
+ } else {
26
+ width += 1;
27
+ }
28
+ }
29
+ return width;
30
+ }
31
+ function truncateToWidth(str, maxWidth) {
32
+ let width = 0;
33
+ let i = 0;
34
+ const chars = [...str];
35
+ while (i < chars.length) {
36
+ const charWidth = getDisplayWidth(chars[i]);
37
+ if (width + charWidth > maxWidth - 3) {
38
+ return chars.slice(0, i).join("") + "...";
39
+ }
40
+ width += charWidth;
41
+ i++;
42
+ }
43
+ return str;
44
+ }
45
+ function padToWidth(str, targetWidth) {
46
+ const currentWidth = getDisplayWidth(str);
47
+ const padding = targetWidth - currentWidth;
48
+ return padding > 0 ? str + " ".repeat(padding) : str;
49
+ }
50
+ var init_table = __esm({
51
+ "src/table.ts"() {
52
+ "use strict";
53
+ }
54
+ });
55
+
56
+ // src/herdr.ts
57
+ var herdr_exports = {};
58
+ __export(herdr_exports, {
59
+ HERDR_TIMEOUT_MS: () => HERDR_TIMEOUT_MS,
60
+ HerdrError: () => HerdrError,
61
+ HerdrUnavailableError: () => HerdrUnavailableError,
62
+ checkHerdrAvailable: () => checkHerdrAvailable,
63
+ paneProcessInfo: () => paneProcessInfo,
64
+ paneSendKeys: () => paneSendKeys,
65
+ paneSendText: () => paneSendText,
66
+ tabClose: () => tabClose,
67
+ tabCreate: () => tabCreate,
68
+ tabList: () => tabList
69
+ });
70
+ import { createRequire } from "node:module";
71
+ function runHerdr(args) {
72
+ return new Promise((resolve2) => {
73
+ childProcess.execFile(
74
+ "herdr",
75
+ args,
76
+ { timeout: HERDR_TIMEOUT_MS, killSignal: "SIGKILL" },
77
+ (error, stdout, stderr) => {
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(stdout);
81
+ } catch {
82
+ parsed = void 0;
83
+ }
84
+ resolve2({ execError: error, parsed, stderr });
85
+ }
86
+ );
87
+ });
88
+ }
89
+ async function execHerdr(args) {
90
+ const { execError, parsed, stderr } = await runHerdr(args);
91
+ const stderrSuffix = stderr.trim() ? `: ${stderr.trim()}` : "";
92
+ if (parsed?.error) {
93
+ throw new HerdrError(
94
+ `herdr ${args.join(" ")} failed: [${parsed.error.code}] ${parsed.error.message}`,
95
+ parsed.error.code
96
+ );
97
+ }
98
+ if (execError) {
99
+ throw new Error(`herdr ${args.join(" ")} failed: ${execError.message}${stderrSuffix}`);
100
+ }
101
+ if (!parsed) {
102
+ throw new Error(`herdr ${args.join(" ")} failed: invalid JSON output${stderrSuffix}`);
103
+ }
104
+ return parsed.result;
105
+ }
106
+ async function tabCreate({ label, cwd }) {
107
+ const result = await execHerdr(["tab", "create", "--label", label, "--cwd", cwd, "--no-focus"]);
108
+ const paneId = result?.root_pane?.pane_id;
109
+ const tabId = result?.tab?.tab_id;
110
+ if (!paneId || !tabId) {
111
+ throw new Error("Failed to create tab: invalid response structure from herdr");
112
+ }
113
+ return { paneId, tabId };
114
+ }
115
+ async function tabClose(tabId) {
116
+ await execHerdr(["tab", "close", tabId]);
117
+ }
118
+ async function tabList() {
119
+ const result = await execHerdr(["tab", "list"]);
120
+ if (!result || !Array.isArray(result.tabs)) {
121
+ return [];
122
+ }
123
+ return result.tabs.map((tab) => ({
124
+ tabId: tab.tab_id,
125
+ label: tab.label,
126
+ workspaceId: tab.workspace_id
127
+ }));
128
+ }
129
+ async function paneSendText(paneId, text) {
130
+ await execHerdr(["pane", "send-text", paneId, text]);
131
+ }
132
+ async function paneSendKeys(paneId, ...keys) {
133
+ await execHerdr(["pane", "send-keys", paneId, ...keys]);
134
+ }
135
+ async function paneProcessInfo(paneId) {
136
+ const result = await execHerdr(["pane", "process-info", "--pane", paneId]);
137
+ const foregroundProcesses = result?.process_info?.foreground_processes ?? [];
138
+ return {
139
+ foregroundProcesses: foregroundProcesses.map((process2) => ({
140
+ name: process2.name,
141
+ argv: process2.argv,
142
+ cmdline: process2.cmdline,
143
+ pid: process2.pid
144
+ }))
145
+ };
146
+ }
147
+ async function checkHerdrAvailable() {
148
+ const { execError, parsed } = await runHerdr(["tab", "list"]);
149
+ if (execError?.code === "ENOENT") {
150
+ throw new HerdrUnavailableError(
151
+ "herdr\u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002herdr CLI\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
152
+ "not-installed"
153
+ );
154
+ }
155
+ if (!parsed) {
156
+ throw new HerdrUnavailableError(
157
+ "herdr\u30B5\u30FC\u30D0\u30FC\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002`herdr status` \u7B49\u3067\u8D77\u52D5\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
158
+ "server-unreachable"
159
+ );
160
+ }
161
+ }
162
+ var childProcess, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS;
163
+ var init_herdr = __esm({
164
+ "src/herdr.ts"() {
165
+ "use strict";
166
+ childProcess = createRequire(import.meta.url)("node:child_process");
167
+ HerdrUnavailableError = class extends Error {
168
+ reason;
169
+ constructor(message, reason) {
170
+ super(message);
171
+ this.name = "HerdrUnavailableError";
172
+ this.reason = reason;
173
+ }
174
+ };
175
+ HerdrError = class extends Error {
176
+ code;
177
+ constructor(message, code) {
178
+ super(message);
179
+ this.name = "HerdrError";
180
+ this.code = code;
181
+ }
182
+ };
183
+ HERDR_TIMEOUT_MS = 15 * 1e3;
184
+ }
185
+ });
186
+
187
+ // src/dispatcher.ts
188
+ var dispatcher_exports = {};
189
+ __export(dispatcher_exports, {
190
+ POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
191
+ SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
192
+ SHUTDOWN_TIMEOUT_MS: () => SHUTDOWN_TIMEOUT_MS,
193
+ formatUptime: () => formatUptime,
194
+ monitorSessions: () => monitorSessions,
195
+ pollOnce: () => pollOnce,
196
+ removeSession: () => removeSession,
197
+ renderSessionTable: () => renderSessionTable,
198
+ runDispatcher: () => runDispatcher,
199
+ shutdownDispatcher: () => shutdownDispatcher
200
+ });
201
+ async function loadHerdr() {
202
+ return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
203
+ }
204
+ async function loadTable() {
205
+ return await Promise.resolve().then(() => (init_table(), table_exports));
206
+ }
207
+ async function runDispatcher(projects, forwardedCommand) {
208
+ const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2, paneSendText: paneSendText2, paneSendKeys: paneSendKeys2 } = await loadHerdr();
209
+ try {
210
+ await checkHerdrAvailable2();
211
+ } catch (error) {
212
+ const message = error instanceof Error ? error.message : String(error);
213
+ console.error(`[dispatcher] ${message}`);
214
+ throw error;
215
+ }
216
+ const existingTabs = await tabList2();
217
+ const existingLabels = new Set(existingTabs.map((tab) => tab.label));
218
+ const sessions = /* @__PURE__ */ new Map();
219
+ for (const project of projects) {
220
+ if (existingLabels.has(project.name)) {
221
+ console.warn(`[dispatcher] project "${project.name}" already has a running tab, skipping`);
222
+ continue;
223
+ }
224
+ let createdTabId;
225
+ try {
226
+ const { paneId, tabId } = await tabCreate2({ label: project.name, cwd: project.path });
227
+ createdTabId = tabId;
228
+ sessions.set(project.name, {
229
+ name: project.name,
230
+ tabId,
231
+ paneId,
232
+ startedAt: /* @__PURE__ */ new Date(),
233
+ status: "running"
234
+ });
235
+ await paneSendText2(paneId, forwardedCommand);
236
+ await paneSendKeys2(paneId, "enter");
237
+ } catch (error) {
238
+ console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
239
+ sessions.delete(project.name);
240
+ if (createdTabId !== void 0) {
241
+ try {
242
+ await tabClose2(createdTabId);
243
+ } catch (closeError) {
244
+ console.error(`[dispatcher] failed to close dangling tab for project "${project.name}": ${closeError}`);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ return sessions;
250
+ }
251
+ async function removeSession(sessions, name, { closeTab }) {
252
+ const session = sessions.get(name);
253
+ if (!session) return;
254
+ sessions.delete(name);
255
+ if (closeTab) {
256
+ const { tabClose: tabClose2 } = await loadHerdr();
257
+ await tabClose2(session.tabId);
258
+ }
259
+ }
260
+ async function pollOnce(sessions, herdr) {
261
+ for (const [name, session] of [...sessions.entries()]) {
262
+ try {
263
+ const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
264
+ const isAlive = foregroundProcesses.some((process2) => process2.cmdline?.includes("claude-task-worker"));
265
+ if (!isAlive) {
266
+ await removeSession(sessions, name, { closeTab: true });
267
+ }
268
+ } catch (error) {
269
+ if (error instanceof herdr.HerdrError && error.code === "pane_not_found") {
270
+ await removeSession(sessions, name, { closeTab: false });
271
+ continue;
272
+ }
273
+ console.error(`[dispatcher] failed to poll session "${name}": ${error}`);
274
+ }
275
+ }
276
+ }
277
+ function formatUptime(start, now) {
278
+ const totalSeconds = Math.max(0, Math.floor((now.getTime() - start.getTime()) / 1e3));
279
+ const hours = Math.floor(totalSeconds / 3600);
280
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
281
+ const seconds = totalSeconds % 60;
282
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
283
+ }
284
+ function renderSessionTable(sessions) {
285
+ const entries = [...sessions.values()];
286
+ if (entries.length === 0) return;
287
+ const maxProjectWidth = 20;
288
+ const rows = entries.map((session) => ({
289
+ project: getDisplayWidth2(session.name) > maxProjectWidth ? truncateToWidth2(session.name, maxProjectWidth) : session.name,
290
+ tab: session.tabId,
291
+ pane: session.paneId,
292
+ status: session.status,
293
+ uptime: formatUptime(session.startedAt, /* @__PURE__ */ new Date())
294
+ }));
295
+ const colWidths = {
296
+ project: Math.max(7, ...rows.map((r) => getDisplayWidth2(r.project))),
297
+ tab: Math.max(3, ...rows.map((r) => r.tab.length)),
298
+ pane: Math.max(4, ...rows.map((r) => r.pane.length)),
299
+ status: Math.max(6, ...rows.map((r) => r.status.length)),
300
+ uptime: Math.max(6, ...rows.map((r) => r.uptime.length))
301
+ };
302
+ const cols = [colWidths.project, colWidths.tab, colWidths.pane, colWidths.status, colWidths.uptime];
303
+ const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
304
+ 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`;
305
+ const lines = [];
306
+ lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
307
+ lines.push(row("Project", "Tab", "Pane", "Status", "Uptime"));
308
+ lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
309
+ for (const r of rows) {
310
+ lines.push(row(r.project, r.tab, r.pane, r.status, r.uptime));
311
+ }
312
+ lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
313
+ console.clear();
314
+ console.log(lines.join("\n"));
315
+ }
316
+ function monitorSessions(sessions, herdr, options) {
317
+ const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
318
+ const renderIntervalMs = options?.renderIntervalMs ?? 1e3;
319
+ let resolveDone;
320
+ const done = new Promise((resolve2) => {
321
+ resolveDone = resolve2;
322
+ });
323
+ let settled = false;
324
+ const finish = () => {
325
+ if (settled) return;
326
+ settled = true;
327
+ clearInterval(pollInterval);
328
+ clearInterval(renderInterval2);
329
+ resolveDone();
330
+ };
331
+ const pollInterval = setInterval(() => {
332
+ void pollOnce(sessions, herdr).then(() => {
333
+ if (sessions.size === 0) {
334
+ finish();
335
+ }
336
+ });
337
+ }, pollIntervalMs);
338
+ pollInterval.unref();
339
+ const renderInterval2 = setInterval(() => {
340
+ renderSessionTable(sessions);
341
+ }, renderIntervalMs);
342
+ renderInterval2.unref();
343
+ return {
344
+ stop: finish,
345
+ done
346
+ };
347
+ }
348
+ async function sendCtrlCToAllSessions(sessions, herdr) {
349
+ await Promise.all(
350
+ [...sessions.values()].map(async (session) => {
351
+ try {
352
+ await herdr.paneSendKeys(session.paneId, "ctrl-c");
353
+ } catch (error) {
354
+ console.error(`[dispatcher] failed to send ctrl-c to session "${session.name}": ${error}`);
355
+ }
356
+ })
357
+ );
358
+ }
359
+ async function waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, timeoutMs) {
360
+ const deadline = Date.now() + timeoutMs;
361
+ while (sessions.size > 0) {
362
+ await pollOnce(sessions, herdr);
363
+ if (sessions.size === 0) return true;
364
+ if (Date.now() >= deadline) return false;
365
+ await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
366
+ }
367
+ return true;
368
+ }
369
+ async function closeRemainingTabs(sessions, herdr, timeoutMs) {
370
+ const results = await Promise.all(
371
+ [...sessions.values()].map(async (session) => {
372
+ try {
373
+ await Promise.race([
374
+ herdr.tabClose(session.tabId),
375
+ new Promise((_, reject) => {
376
+ setTimeout(() => reject(new Error("tabClose timed out")), timeoutMs).unref();
377
+ })
378
+ ]);
379
+ return true;
380
+ } catch (error) {
381
+ console.error(
382
+ `[dispatcher] failed to close tab "${session.tabId}" for session "${session.name}" during shutdown: ${error}`
383
+ );
384
+ return false;
385
+ }
386
+ })
387
+ );
388
+ return results.every((closed) => closed);
389
+ }
390
+ async function forceKillAllSessions(sessions, herdr, tabCloseTimeoutMs) {
391
+ console.log(`[dispatcher] force-kill requested, sending ctrl-c and closing ${sessions.size} session(s) immediately`);
392
+ await sendCtrlCToAllSessions(sessions, herdr);
393
+ await closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
394
+ process.exit(1);
395
+ }
396
+ async function shutdownDispatcher(sessions, monitorHandle, options) {
397
+ if (shutdownPromise) {
398
+ if (options?.forceKill) {
399
+ const herdr = options.herdr ?? await loadHerdr();
400
+ const tabCloseTimeoutMs = options.tabCloseTimeoutMs ?? 5e3;
401
+ await forceKillAllSessions(sessions, herdr, tabCloseTimeoutMs);
402
+ return;
403
+ }
404
+ console.log("[dispatcher] shutdown already in progress, ignoring duplicate request");
405
+ return shutdownPromise;
406
+ }
407
+ shutdownPromise = (async () => {
408
+ const herdr = options?.herdr ?? await loadHerdr();
409
+ const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
410
+ const shutdownTimeoutMs = options?.shutdownTimeoutMs ?? SHUTDOWN_TIMEOUT_MS;
411
+ const retryTimeoutMs = options?.retryTimeoutMs ?? SHUTDOWN_RETRY_TIMEOUT_MS;
412
+ const tabCloseTimeoutMs = options?.tabCloseTimeoutMs ?? 5e3;
413
+ if (monitorHandle) {
414
+ monitorHandle.stop();
415
+ await monitorHandle.done;
416
+ }
417
+ console.log(`[dispatcher] shutting down, sending ctrl-c to ${sessions.size} session(s)`);
418
+ await sendCtrlCToAllSessions(sessions, herdr);
419
+ let finishedInTime = await waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, shutdownTimeoutMs);
420
+ if (!finishedInTime) {
421
+ console.log(
422
+ `[dispatcher] ${sessions.size} session(s) still alive after ${shutdownTimeoutMs}ms, resending ctrl-c once`
423
+ );
424
+ await sendCtrlCToAllSessions(sessions, herdr);
425
+ finishedInTime = await waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, retryTimeoutMs);
426
+ }
427
+ const allTabsClosed = await closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
428
+ process.exit(finishedInTime && allTabsClosed ? 0 : 1);
429
+ })();
430
+ try {
431
+ await shutdownPromise;
432
+ } finally {
433
+ shutdownPromise = void 0;
434
+ }
435
+ }
436
+ var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, SHUTDOWN_RETRY_TIMEOUT_MS, shutdownPromise;
437
+ var init_dispatcher = __esm({
438
+ async "src/dispatcher.ts"() {
439
+ "use strict";
440
+ ({ getDisplayWidth: getDisplayWidth2, truncateToWidth: truncateToWidth2, padToWidth: padToWidth2 } = await loadTable());
441
+ POLL_INTERVAL_MS = 7 * 1e3;
442
+ SHUTDOWN_TIMEOUT_MS = 10 * 60 * 1e3;
443
+ SHUTDOWN_RETRY_TIMEOUT_MS = 90 * 1e3;
444
+ }
445
+ });
2
446
 
3
447
  // src/gh.ts
4
448
  import { execFile } from "node:child_process";
@@ -460,39 +904,9 @@ async function assertRemoteTrackingExists(epicBranch) {
460
904
 
461
905
  // src/process-manager.ts
462
906
  import { spawn } from "node:child_process";
907
+ init_table();
463
908
  var childProcesses = /* @__PURE__ */ new Map();
464
909
  var TASK_TIMEOUT_MS = 90 * 60 * 1e3;
465
- function getDisplayWidth(str) {
466
- let width = 0;
467
- for (const char of str) {
468
- const code = char.codePointAt(0);
469
- if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 12350 || code >= 12352 && code <= 13247 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 42191 || code >= 44032 && code <= 55215 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072 && code <= 196605 || code >= 196608 && code <= 262141) {
470
- width += 2;
471
- } else {
472
- width += 1;
473
- }
474
- }
475
- return width;
476
- }
477
- function truncateToWidth(str, maxWidth) {
478
- let width = 0;
479
- let i = 0;
480
- const chars = [...str];
481
- while (i < chars.length) {
482
- const charWidth = getDisplayWidth(chars[i]);
483
- if (width + charWidth > maxWidth - 3) {
484
- return chars.slice(0, i).join("") + "...";
485
- }
486
- width += charWidth;
487
- i++;
488
- }
489
- return str;
490
- }
491
- function padToWidth(str, targetWidth) {
492
- const currentWidth = getDisplayWidth(str);
493
- const padding = targetWidth - currentWidth;
494
- return padding > 0 ? str + " ".repeat(padding) : str;
495
- }
496
910
  var tasks = /* @__PURE__ */ new Map();
497
911
  var shuttingDown = false;
498
912
  function setShuttingDown() {
@@ -1175,7 +1589,7 @@ function getWorktreePath(worktreeId) {
1175
1589
  }
1176
1590
  async function createWorktreeFromBranch(worktreeId, baseBranch) {
1177
1591
  const worktreePath = getWorktreePath(worktreeId);
1178
- await execFileAsync2("git", ["worktree", "add", "-B", worktreeId, worktreePath, `origin/${baseBranch}`]);
1592
+ await execFileAsync2("git", ["worktree", "add", "--track", "-B", worktreeId, worktreePath, `origin/${baseBranch}`]);
1179
1593
  }
1180
1594
  async function removeWorktree(worktreeId) {
1181
1595
  const worktreePath = getWorktreePath(worktreeId);
@@ -1842,6 +2256,184 @@ function version() {
1842
2256
  }
1843
2257
  }
1844
2258
 
2259
+ // src/dispatch-args.ts
2260
+ var PROJECT_INCOMPATIBLE_COMMANDS = ["init", "install", "update", "usage", "version"];
2261
+ function collectFlagValues(argv, flag) {
2262
+ const values = [];
2263
+ for (let i = 0; i < argv.length; i++) {
2264
+ if (argv[i] !== flag) continue;
2265
+ const raw = argv[i + 1];
2266
+ if (!raw || raw.startsWith("--")) {
2267
+ console.error(`[dispatcher] ${flag} requires a value`);
2268
+ process.exit(1);
2269
+ }
2270
+ values.push(raw);
2271
+ }
2272
+ return values;
2273
+ }
2274
+ function parseProjectFilters() {
2275
+ return collectFlagValues(process.argv, "--project");
2276
+ }
2277
+ function hasProjectFilter() {
2278
+ return process.argv.includes("--project");
2279
+ }
2280
+ function assertProjectCompatibleCommand(command) {
2281
+ if (PROJECT_INCOMPATIBLE_COMMANDS.includes(command)) {
2282
+ console.error(`[dispatcher] --project cannot be used with the "${command}" command`);
2283
+ process.exit(1);
2284
+ }
2285
+ }
2286
+ function shellQuote(value) {
2287
+ if (value === "") return "''";
2288
+ return `'${value.replace(/'/g, "'\\''")}'`;
2289
+ }
2290
+ function buildForwardedCommand(argv) {
2291
+ const tokens = [];
2292
+ for (let i = 0; i < argv.length; i++) {
2293
+ if (argv[i] === "--project") {
2294
+ i++;
2295
+ continue;
2296
+ }
2297
+ tokens.push(argv[i]);
2298
+ }
2299
+ return ["claude-task-worker", ...tokens.map(shellQuote)].join(" ");
2300
+ }
2301
+
2302
+ // src/projects-config.ts
2303
+ import { readFileSync as readFileSync4, statSync } from "node:fs";
2304
+ import { homedir as homedir2 } from "node:os";
2305
+ import { isAbsolute, join as join4 } from "node:path";
2306
+ var RESERVED_ALL = "all";
2307
+ var ProjectsConfigError = class extends Error {
2308
+ constructor(message) {
2309
+ super(message);
2310
+ this.name = "ProjectsConfigError";
2311
+ }
2312
+ };
2313
+ function getProjectsConfigPath() {
2314
+ const xdg = process.env.XDG_CONFIG_HOME;
2315
+ const configHome = xdg && xdg.length > 0 ? xdg : join4(homedir2(), ".config");
2316
+ return join4(configHome, "claude-task-worker", "projects.json");
2317
+ }
2318
+ var PROJECTS_CONFIG_PATH = getProjectsConfigPath();
2319
+ function isDirectory(path) {
2320
+ try {
2321
+ return statSync(path).isDirectory();
2322
+ } catch {
2323
+ return false;
2324
+ }
2325
+ }
2326
+ function loadProjectsConfig() {
2327
+ const configPath = PROJECTS_CONFIG_PATH;
2328
+ let raw;
2329
+ try {
2330
+ raw = JSON.parse(readFileSync4(configPath, "utf-8"));
2331
+ } catch (err) {
2332
+ if (err.code === "ENOENT") {
2333
+ throw new ProjectsConfigError(`projects.json not found: ${configPath}`);
2334
+ }
2335
+ if (err instanceof SyntaxError) {
2336
+ throw new ProjectsConfigError(`projects.json contains invalid JSON: ${configPath}: ${err.message}`);
2337
+ }
2338
+ throw err;
2339
+ }
2340
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2341
+ throw new ProjectsConfigError(`projects.json must contain a JSON object: ${configPath}`);
2342
+ }
2343
+ if (!("projects" in raw) || typeof raw["projects"] !== "object" || raw["projects"] === null || Array.isArray(raw["projects"])) {
2344
+ throw new ProjectsConfigError(`projects.json must contain a "projects" section as an object: ${configPath}`);
2345
+ }
2346
+ if ("projectGroups" in raw && (typeof raw["projectGroups"] !== "object" || raw["projectGroups"] === null || Array.isArray(raw["projectGroups"]))) {
2347
+ throw new ProjectsConfigError(`projects.json "projectGroups" must be an object: ${configPath}`);
2348
+ }
2349
+ const rawProjects = raw["projects"];
2350
+ const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
2351
+ const projectKeys = Object.keys(rawProjects);
2352
+ const groupKeys = Object.keys(rawProjectGroups);
2353
+ if (projectKeys.includes(RESERVED_ALL) || groupKeys.includes(RESERVED_ALL)) {
2354
+ throw new ProjectsConfigError(
2355
+ `"${RESERVED_ALL}" is a reserved word and cannot be used as a key in "projects" or "projectGroups"`
2356
+ );
2357
+ }
2358
+ const groupKeySet = new Set(groupKeys);
2359
+ const duplicateKeys = projectKeys.filter((key) => groupKeySet.has(key));
2360
+ if (duplicateKeys.length > 0) {
2361
+ throw new ProjectsConfigError(
2362
+ `"projects" and "projectGroups" share the same key namespace; duplicate keys are not allowed: ${duplicateKeys.join(", ")}`
2363
+ );
2364
+ }
2365
+ const projects = {};
2366
+ for (const [name, value] of Object.entries(rawProjects)) {
2367
+ if (name === "__proto__") {
2368
+ throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projects": ${configPath}`);
2369
+ }
2370
+ if (typeof value !== "string" || !isAbsolute(value)) {
2371
+ console.warn(`[projects-config] invalid projects.${name}: expected an absolute path, skipping`);
2372
+ continue;
2373
+ }
2374
+ if (!isDirectory(value)) {
2375
+ console.warn(`[projects-config] projects.${name} does not exist as a directory: ${value}, skipping`);
2376
+ continue;
2377
+ }
2378
+ projects[name] = value;
2379
+ }
2380
+ const projectGroups = {};
2381
+ for (const [groupName, value] of Object.entries(rawProjectGroups)) {
2382
+ if (groupName === "__proto__") {
2383
+ throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projectGroups": ${configPath}`);
2384
+ }
2385
+ if (!Array.isArray(value)) {
2386
+ console.warn(`[projects-config] invalid projectGroups.${groupName}: expected an array, skipping`);
2387
+ continue;
2388
+ }
2389
+ const members = [];
2390
+ for (const member of value) {
2391
+ if (typeof member !== "string" || !Object.prototype.hasOwnProperty.call(projects, member)) {
2392
+ console.warn(
2393
+ `[projects-config] projectGroups.${groupName} references unknown project "${String(member)}", skipping`
2394
+ );
2395
+ continue;
2396
+ }
2397
+ members.push(member);
2398
+ }
2399
+ projectGroups[groupName] = members;
2400
+ }
2401
+ return { projects, projectGroups };
2402
+ }
2403
+ function resolveTargetProjects(requested, config) {
2404
+ const resolved = /* @__PURE__ */ new Map();
2405
+ for (const name of requested) {
2406
+ if (name === RESERVED_ALL) {
2407
+ for (const [projectName, projectPath] of Object.entries(config.projects)) {
2408
+ resolved.set(projectName, { name: projectName, path: projectPath });
2409
+ }
2410
+ continue;
2411
+ }
2412
+ if (Object.prototype.hasOwnProperty.call(config.projects, name)) {
2413
+ resolved.set(name, { name, path: config.projects[name] });
2414
+ continue;
2415
+ }
2416
+ if (Object.prototype.hasOwnProperty.call(config.projectGroups, name)) {
2417
+ for (const projectName of config.projectGroups[name]) {
2418
+ const projectPath = config.projects[projectName];
2419
+ if (projectPath === void 0) continue;
2420
+ resolved.set(projectName, { name: projectName, path: projectPath });
2421
+ }
2422
+ continue;
2423
+ }
2424
+ const availableProjects = Object.keys(config.projects).join(", ") || "(none)";
2425
+ const availableGroups = Object.keys(config.projectGroups).join(", ") || "(none)";
2426
+ throw new ProjectsConfigError(
2427
+ `Unknown project or group: "${name}". Available projects: ${availableProjects}. Available groups: ${availableGroups}.`
2428
+ );
2429
+ }
2430
+ const resolvedProjects = Array.from(resolved.values());
2431
+ if (resolvedProjects.length === 0) {
2432
+ throw new ProjectsConfigError(`No projects resolved from requested targets: ${requested.join(", ")}`);
2433
+ }
2434
+ return resolvedProjects;
2435
+ }
2436
+
1845
2437
  // src/index.ts
1846
2438
  var WORKERS = {
1847
2439
  "exec-issue": execIssueWorker,
@@ -1856,7 +2448,7 @@ var WORKERS = {
1856
2448
  "epic-issue": epicIssueWorker
1857
2449
  };
1858
2450
  function printUsage() {
1859
- console.log(`Usage: claude-task-worker <command> [--epic <issue-number>] [--label <label-name>]
2451
+ console.log(`Usage: claude-task-worker <command> [--project <name>] [--epic <issue-number>] [--label <label-name>]
1860
2452
 
1861
2453
  Commands:
1862
2454
  init [--force] Create required GitHub labels and config file (use --force to overwrite existing files)
@@ -1880,6 +2472,7 @@ Workers:
1880
2472
  yolo Poll all workers including triage-created-issue, triage-pr, check-dependabot
1881
2473
 
1882
2474
  Options:
2475
+ --project <name> Dispatch to project(s) via herdr instead of running the worker locally. Accepts a project name, a project group name, or "all". Repeatable.
1883
2476
  --epic <number> Limit issue-based workers to sub-issues of the specified epic issue. Repeatable: any matching parent (OR).
1884
2477
  --label <name> Limit issue-based workers to issues that also carry the specified label. Repeatable: all must be present (AND).
1885
2478
 
@@ -1890,7 +2483,10 @@ Example:
1890
2483
  claude-task-worker all --epic 100 --epic 200
1891
2484
  claude-task-worker all --label priority-high
1892
2485
  claude-task-worker all --label priority-high --label needs-design
1893
- claude-task-worker yolo --epic 100 --epic 200 --label priority-high`);
2486
+ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
2487
+ claude-task-worker all --project all
2488
+ claude-task-worker all --project igsa
2489
+ claude-task-worker exec-issue --project my-app --epic 100`);
1894
2490
  }
1895
2491
  var workerType = process.argv[2];
1896
2492
  if (workerType === "version" || workerType === "--version" || workerType === "-v") {
@@ -1906,7 +2502,10 @@ if (workerType !== "all" && workerType !== "yolo" && workerType !== "init" && wo
1906
2502
  printUsage();
1907
2503
  process.exit(1);
1908
2504
  }
1909
- function collectFlagValues(flag) {
2505
+ if (hasProjectFilter()) {
2506
+ assertProjectCompatibleCommand(workerType);
2507
+ }
2508
+ function collectFlagValues2(flag) {
1910
2509
  const values = [];
1911
2510
  for (let i = 0; i < process.argv.length; i++) {
1912
2511
  if (process.argv[i] !== flag) continue;
@@ -1920,7 +2519,7 @@ function collectFlagValues(flag) {
1920
2519
  return values;
1921
2520
  }
1922
2521
  function parseEpicFilters() {
1923
- const raws = collectFlagValues("--epic");
2522
+ const raws = collectFlagValues2("--epic");
1924
2523
  return raws.map((raw) => {
1925
2524
  const num = Number(raw);
1926
2525
  if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
@@ -1931,40 +2530,68 @@ function parseEpicFilters() {
1931
2530
  });
1932
2531
  }
1933
2532
  function parseLabelFilters() {
1934
- return collectFlagValues("--label");
2533
+ return collectFlagValues2("--label");
1935
2534
  }
1936
2535
  process.on("unhandledRejection", (err) => {
1937
2536
  console.error("[worker] unhandled rejection:", err);
1938
2537
  process.exit(1);
1939
2538
  });
1940
- process.on("SIGTERM", async () => {
1941
- if (isShuttingDown()) return;
1942
- setShuttingDown();
1943
- console.log(
1944
- "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Send SIGTERM again to force kill)"
1945
- );
1946
- await waitForAllProcesses();
1947
- process.exit(0);
1948
- });
1949
- var forceKilling = false;
1950
- process.on("SIGINT", async () => {
1951
- if (isShuttingDown()) {
1952
- if (forceKilling) return;
1953
- forceKilling = true;
1954
- console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
1955
- shutdown("SIGKILL");
1956
- const cleanupTimeout = new Promise((resolve2) => setTimeout(resolve2, 6e4).unref());
1957
- await Promise.race([waitForAllProcesses(), cleanupTimeout]);
1958
- process.exit(1);
1959
- }
1960
- setShuttingDown();
1961
- console.log(
1962
- "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Press Ctrl-C again to force kill)"
1963
- );
1964
- await waitForAllProcesses();
1965
- process.exit(0);
1966
- });
1967
- if (workerType === "init") {
2539
+ if (!hasProjectFilter()) {
2540
+ process.on("SIGTERM", async () => {
2541
+ if (isShuttingDown()) return;
2542
+ setShuttingDown();
2543
+ console.log(
2544
+ "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Send SIGTERM again to force kill)"
2545
+ );
2546
+ await waitForAllProcesses();
2547
+ process.exit(0);
2548
+ });
2549
+ let forceKilling = false;
2550
+ process.on("SIGINT", async () => {
2551
+ if (isShuttingDown()) {
2552
+ if (forceKilling) return;
2553
+ forceKilling = true;
2554
+ console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
2555
+ shutdown("SIGKILL");
2556
+ const cleanupTimeout = new Promise((resolve2) => setTimeout(resolve2, 6e4).unref());
2557
+ await Promise.race([waitForAllProcesses(), cleanupTimeout]);
2558
+ process.exit(1);
2559
+ }
2560
+ setShuttingDown();
2561
+ console.log(
2562
+ "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Press Ctrl-C again to force kill)"
2563
+ );
2564
+ await waitForAllProcesses();
2565
+ process.exit(0);
2566
+ });
2567
+ }
2568
+ if (hasProjectFilter()) {
2569
+ (async () => {
2570
+ let sessions = /* @__PURE__ */ new Map();
2571
+ let monitorHandle;
2572
+ const handleShutdown = async () => {
2573
+ const { shutdownDispatcher: shutdownDispatcher2 } = await init_dispatcher().then(() => dispatcher_exports);
2574
+ await shutdownDispatcher2(sessions, monitorHandle);
2575
+ };
2576
+ process.on("SIGTERM", handleShutdown);
2577
+ process.on("SIGINT", handleShutdown);
2578
+ const herdr = await Promise.resolve().then(() => (init_herdr(), herdr_exports));
2579
+ try {
2580
+ const config = loadProjectsConfig();
2581
+ const projects = resolveTargetProjects(parseProjectFilters(), config);
2582
+ const forwardedCommand = buildForwardedCommand(process.argv.slice(2));
2583
+ const { runDispatcher: runDispatcher2, monitorSessions: monitorSessions2 } = await init_dispatcher().then(() => dispatcher_exports);
2584
+ sessions = await runDispatcher2(projects, forwardedCommand);
2585
+ monitorHandle = monitorSessions2(sessions, herdr);
2586
+ } catch (err) {
2587
+ if (err instanceof ProjectsConfigError || err instanceof herdr.HerdrUnavailableError) {
2588
+ console.error(`[dispatcher] ${err.message}`);
2589
+ process.exit(1);
2590
+ }
2591
+ throw err;
2592
+ }
2593
+ })();
2594
+ } else if (workerType === "init") {
1968
2595
  const force = process.argv.slice(3).includes("--force");
1969
2596
  init({ force });
1970
2597
  } else if (workerType === "install") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.6.0",
3
+ "version": "0.11.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",
@@ -12,6 +12,7 @@
12
12
  "dev": "tsc --noEmit --watch",
13
13
  "lint": "eslint .",
14
14
  "lint:fix": "eslint . --fix",
15
+ "test": "node --experimental-strip-types --test src/**/*.test.ts",
15
16
  "format": "prettier --write .",
16
17
  "format:check": "prettier --check .",
17
18
  "prepublishOnly": "npm run build"
@@ -39,7 +40,7 @@
39
40
  "author": "getty104",
40
41
  "license": "MIT",
41
42
  "engines": {
42
- "node": ">=20"
43
+ "node": ">=22.6.0"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@eslint/js": "^10.0.1",