claude-task-worker 0.33.1 → 0.35.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 +140 -21
  2. package/dist/index.js +514 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,8 +51,12 @@ GitHub Issues/PRを定期ポーリングし、Claude Codeに処理を委譲す
51
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
+ | `create-ui-design` | `cc-create-ui-design` (Issue) | `/claude-task-worker:create-ui-design` | 1分 |
55
+ | `apply-ui-design` | `cc-ui-design-pr-created` (Issue) | `/claude-task-worker:apply-ui-design` | 1分 |
54
56
 
55
57
  > ℹ️ Issue 系ワーカーはすべて GitHub Issue Dependencies の `-is:blocked` 検索 qualifier でサーバ側絞り込みを行うため、未解決の blockedBy Issue を持つ Issue は対象外となる。
58
+ >
59
+ > ℹ️ `create-ui-design` / `apply-ui-design` は `claude-task-worker.json` の `uiDesign.enabled` が `true` のときだけ起動する(既定は `false`)。詳細は「[UIデザイン先行ワークフロー](#uiデザイン先行ワークフロー)」を参照。
56
60
 
57
61
  ### Epic(親Issue)連携
58
62
 
@@ -60,9 +64,63 @@ GitHub Issues/PRを定期ポーリングし、Claude Codeに処理を委譲す
60
64
 
61
65
  `epic-issue` ワーカーは、`cc-epic-issue` ラベル付きの親Issueに紐づくサブIssueがすべて Close されたタイミングで `/claude-task-worker:create-epic-pr` を起動し、エピックブランチからまとめてPRを作成する。
62
66
 
67
+ ### UIデザイン先行ワークフロー
68
+
69
+ UI実装Issueについて、実装の前に Pencil(`.pen`)でデザインを作り、それを独立したPRとしてマージしてから実装へ進むフロー。デザインの見た目・構成を実装PRとは別に単体でレビュー・合意でき、合意済みデザインがリポジトリに永続化される。
70
+
71
+ `claude-task-worker.json` の `uiDesign.enabled` によるオプトインで、既定(`false`)では2つのワーカーが起動しないため、Pencil を使っていないリポジトリの挙動は本機能の追加前と完全に一致する。
72
+
73
+ ```json
74
+ {
75
+ "uiDesign": {
76
+ "enabled": true,
77
+ "designDir": "designs"
78
+ }
79
+ }
80
+ ```
81
+
82
+ | キー | 既定値 | 意味 |
83
+ |---|---|---|
84
+ | `uiDesign.enabled` | `false` | 本ワークフローの有効化。`false` の間は `triage-created-issue` がUI判定を行わず、2つのワーカーも起動しない |
85
+ | `uiDesign.designDir` | `"designs"` | `.pen` とスナップショットの配置先(リポジトリルートからの相対パス) |
86
+
87
+ フローは以下のとおり。
88
+
89
+ ```text
90
+ cc-issue-created + triage-created-issue(ルーティング)
91
+ ├─ UI実装タスクでない → cc-exec-issue(従来どおり)
92
+ └─ UI実装タスク → cc-create-ui-design
93
+ → create-ui-design ワーカー
94
+ ・.pen を作成/更新 + snapshots/ に PNG 出力
95
+ ・ブランチ cc-ui-design-<N> を push しデザインPRを作成(Refs #N。closing keyword は使わない)
96
+ ・PR に cc-triage-scope + cc-ui-design、Issue に cc-ui-design-pr-created を付与
97
+ → triage-pr / fix-review-point / resolve-conflict(既存フローでレビュー・マージ)
98
+ → apply-ui-design ワーカー
99
+ ・デザインPRが MERGED になるまで skip
100
+ ・Issue description に「## UIデザイン」セクションを追記
101
+ ・cc-ui-design-ready + cc-exec-issue を付与
102
+ → exec-issue(デザインを参照元として実装)
103
+ ```
104
+
105
+ デザインPRは Epic PR ではないため `cc-release-ready` によるマージ保留の対象外で、`triage-pr` が通常どおりレビュー・マージする。`triage-pr` は `cc-ui-design` ラベル付きPRに対してコードレビューではなくデザイン向けの観点(差分が `.pen` とPNGに限定されているか、スナップショットとデザイン意図が読み取れるか、Issue要件を満たしているか)で評価する。
106
+
107
+ デザインが不要と判明した場合は `create-ui-design` が理由をコメントして `cc-ui-design-ready` + `cc-exec-issue` を付与し、人手を介さず実装へ復帰する。Pencil が使えない環境やデザインPRが却下された場合は `cc-need-human-check` に落ちて停止する(共通除外ラベルのため無限リトライしない)。
108
+
63
109
  ### `--project` 指定時(ディスパッチャーモード)
64
110
 
65
- `--project` オプションを指定した場合、CLIは自身でワーカーを起動する代わりに [herdr](https://herdr.dev) 経由のディスパッチャーとして動作する。指定したプロジェクトごとにherdrのタブ(ペイン)を作成し、そのプロジェクトのディレクトリで(`--project` を除いた)同じコマンドを非同期実行させ、稼働状況をステータステーブルで監視する。詳細は「[`--project <name>` オプション](#--project-name-オプション)」を参照。
111
+ `--project` オプションを指定した場合、CLIは自身でワーカーを起動する代わりに [herdr](https://herdr.dev) 経由のディスパッチャーとして動作する。指定したプロジェクトごとにherdrのワークスペースを作成し、そのルートペインで(`--project` を除いた)同じコマンドを実行させ、稼働状況をステータステーブルで監視する。詳細は「[`--project <name>` オプション](#--project-name-オプション)」を参照。
112
+
113
+ ### claude-task-worker プラグインの構成
114
+
115
+ | ディレクトリ | 内容 |
116
+ |---|---|
117
+ | `plugin/skills/` | ワーカーが呼び出すスキル群と、対話セッションから使う補助スキル(`commit-push` / `create-pr` / `breakdown-issues` / `edit-pencil-design` など) |
118
+ | `plugin/agents/` | サブエージェント定義(`explore-agent` / `frontend-implementer` / `general-purpose-assistant` / `lightweight-assistant` / `pencil-design-updater` / `requirement-todo-organizer`) |
119
+ | `plugin/hooks/` | `SessionStart`(worktree セットアップ・`git fetch --prune`)と `UserPromptSubmit`(`codegraph prompt-hook`)のフック定義 |
120
+ | `plugin/scripts/` | フックから呼ばれるスクリプト(`setup-worktree.sh` / `stop-servers.mjs`) |
121
+ | `plugin/.mcp.json` | プラグインが提供する MCP サーバー定義(下記「[プラグインが利用する MCP サーバー](#プラグインが利用する-mcp-サーバー)」) |
122
+
123
+ ワーカー起動スキルには `Stop` フック(`plugin/scripts/stop-servers.mjs`)が設定されており、スキル終了時に `docker compose down` と、worktree を作業ディレクトリに持つ残留プロセスの `SIGTERM` をベストエフォートで実行する。worktree はスキル完了直後に削除されるため、切り離されたサーバープロセスが残ると削除の妨げになるのを防ぐ。
66
124
 
67
125
  ## 必要なライブラリ
68
126
 
@@ -77,8 +135,10 @@ CLI 本体に npm の実行時依存パッケージはない(ビルド時に e
77
135
  | [Claude Code (`claude`)](https://docs.anthropic.com/en/docs/claude-code) | - | タスク実行エンジン。各ワーカーが Claude CLI プロセスとして起動する |
78
136
  | [Git (`git`)](https://git-scm.com/) | - | worktree の作成・ブランチ操作 |
79
137
  | [jq](https://jqlang.org/) | - | プラグインスキル内でのJSON加工(`check-dependabot` / `edit-pencil-design` スキルなどで使用) |
138
+ | [CodeGraph (`codegraph`)](https://www.npmjs.com/package/@colbymchenry/codegraph) | - | コード探索用のインデックス。MCP サーバー(`codegraph serve --mcp`)としてプラグインから起動される。`install` / `update` / `init` が面倒を見る。未インストールでもワーカーは動作する(探索がテキスト検索に落ちるだけ) |
80
139
  | [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` 使用時にのみ必要(通常ワーカー実行には不要)。複数プロジェクトへのディスパッチ・セッション監視・一括停止に使用 |
140
+ | [herdr](https://herdr.dev) | - | `--project` 使用時と `mode: "herdr"` 使用時にのみ必要。複数プロジェクトへのディスパッチ・セッション監視・一括停止、タスクのTUI実行に使用 |
141
+ | [Headroom (`headroom`)](https://github.com/headroom-ai/headroom) | - | `headroom: true` 使用時にのみ必要。タスクの claude をコンテキスト圧縮プロキシ経由で起動する |
82
142
  | `claude-task-worker` プラグイン | - | 各ワーカーが呼び出すスキル群(本リポジトリの `plugin/`) |
83
143
 
84
144
  Slack通知(任意)を使う場合は Slack Incoming Webhook URL が必要。Claude API使用状況の取得には、macOSでは `security` コマンド(Keychain)を使用し、それ以外の環境では `~/.claude/.credentials.json` にフォールバックする。
@@ -102,6 +162,7 @@ Slack通知(任意)を使う場合は Slack Incoming Webhook URL が必要
102
162
 
103
163
  | サーバー | 接続方法 | 用途 |
104
164
  |---|---|---|
165
+ | `codegraph` | `codegraph serve --mcp`(stdio) | シンボルの定義元・参照元・呼び出し関係をたどるコード探索。ワーカー起動セッションは `--append-system-prompt` の指示により、`Grep`/`Glob` より優先してこの MCP を使う |
105
166
  | `context7` | HTTP(`https://mcp.context7.com/mcp`) | ライブラリの最新ドキュメント取得 |
106
167
  | `next-devtools` | `npx -y next-devtools-mcp@latest` | Next.js 開発支援 |
107
168
  | `shadcn` | `npx shadcn@latest mcp` | shadcn/ui コンポーネント情報の取得 |
@@ -114,7 +175,9 @@ Slack通知(任意)を使う場合は Slack Incoming Webhook URL が必要
114
175
  - [GitHub CLI (`gh`)](https://cli.github.com/) がインストール・認証済みであること
115
176
  - [Claude Code (`claude`)](https://docs.anthropic.com/en/docs/claude-code) がインストール済みであること
116
177
  - `claude-task-worker` プラグインがインストール済みであること(下記インストール手順を参照)
117
- - `--project` オプションを使う場合のみ、[herdr](https://herdr.dev) がインストール済みであること(通常のワーカー実行には不要)
178
+ - `--project` オプション・`mode: "herdr"` を使う場合のみ、[herdr](https://herdr.dev) がインストール済みであること
179
+ - `headroom: true` を使う場合のみ、[Headroom](https://github.com/headroom-ai/headroom) がインストール済みであること
180
+ - CodeGraph はコード探索の精度向上のために推奨(`install` / `init` が導入・インデックス構築を行う)。未導入でもワーカーは動作する
118
181
 
119
182
  詳細は上記「[必要なライブラリ](#必要なライブラリ)」を参照。
120
183
 
@@ -122,7 +185,7 @@ Slack通知(任意)を使う場合は Slack Incoming Webhook URL が必要
122
185
 
123
186
  #### 推奨手順(一発セットアップ)
124
187
 
125
- 以下のコマンド一発で、Claude Code マーケットプレイスの追加・プラグインのインストール・CLI本体のグローバルインストールをまとめて行える。
188
+ 以下のコマンド一発で、Claude Code マーケットプレイスの追加・プラグインのインストール・CLI本体のグローバルインストール・CodeGraph CLI のインストールをまとめて行える。
126
189
 
127
190
  ```bash
128
191
  npx claude-task-worker install
@@ -131,6 +194,7 @@ npx claude-task-worker install
131
194
  - `claude plugin marketplace add getty104/claude-task-worker` — マーケットプレイスの追加(追加済みの場合のエラーはログのみで無視して続行)
132
195
  - `claude plugin install claude-task-worker@claude-task-worker` — プラグインのインストール
133
196
  - `npm install -g claude-task-worker@latest` — CLI 本体のグローバルインストール(`npx` 実行時でも `claude-task-worker` コマンドを常設化する)
197
+ - `npm install -g @colbymchenry/codegraph@latest` — CodeGraph CLI のインストール(MCP サーバーの登録はプラグインの `.mcp.json` が担うため、`codegraph install` は実行しない)
134
198
 
135
199
  インストール後、Claude Code のセッションを再起動するとプラグインが有効化される。
136
200
 
@@ -173,7 +237,7 @@ brew install herdr
173
237
 
174
238
  #### 更新
175
239
 
176
- CLI とプラグイン(マーケットプレイス)をまとめて更新する。
240
+ CLI・プラグイン(マーケットプレイス)・CodeGraph CLI をまとめて更新する。
177
241
 
178
242
  ```bash
179
243
  claude-task-worker update
@@ -182,10 +246,11 @@ claude-task-worker update
182
246
  - `claude plugin marketplace update claude-task-worker` — マーケットプレイスの更新
183
247
  - `claude plugin update claude-task-worker@claude-task-worker` — プラグインの更新(反映にはセッション再起動が必要)
184
248
  - `npm install -g claude-task-worker@latest` — CLI 本体の更新
249
+ - `codegraph upgrade` — CodeGraph CLI の更新(未インストール環境ではコマンドが無く失敗するため、`npm install -g @colbymchenry/codegraph@latest` へフォールバックする)
185
250
 
186
251
  ### 初期化
187
252
 
188
- 対象リポジトリで実行すると、必要なGitHubラベル・Issueテンプレート・GitHub Actionsワークフロー・設定ファイルが作成される。既存ファイルは保護され、`--force` を指定すると上書きされる。
253
+ 対象リポジトリで実行すると、必要なGitHubラベル・Issueテンプレート・GitHub Actionsワークフロー・設定ファイルが作成され、CodeGraph のセットアップが行われる。既存ファイルは保護され、`--force` を指定すると上書きされる。
189
254
 
190
255
  ```bash
191
256
  claude-task-worker init # 既存ファイルは保護
@@ -208,12 +273,21 @@ claude-task-worker init --force # 既存ファイルを強制上書き
208
273
  | `cc-pr-created` | PR作成完了マーク |
209
274
  | `cc-epic-issue` | エピックマーク(Issueではサブ全Closeで `epic-issue` ワーカー起動、PRではリリースゲート対象を示すマーク) |
210
275
  | `cc-release-ready` | エピックPRがリリース可能(マージ問題なし)と判定されたマーク。実際のマージ(リリース)は人間が実施 |
276
+ | `cc-create-ui-design` | UIデザイン作成トリガー(`create-ui-design` ワーカー。`uiDesign.enabled` が `true` の場合のみ機能する) |
277
+ | `cc-ui-design-pr-created` | デザインPR作成済み・マージ待ちマーク(`apply-ui-design` ワーカーのトリガー) |
278
+ | `cc-ui-design-ready` | デザイン反映済みマーク(再デザイン抑止。デザイン不要と判定された場合も付与される) |
279
+ | `cc-ui-design` | デザインPRのマーカー(PR。`triage-pr` のレビュー観点切り替えに使う) |
211
280
 
212
281
  作成されるファイル:
213
282
 
214
283
  - `.github/ISSUE_TEMPLATE/cc-triage-scope.yml` — `cc-triage-scope` ラベル付きIssue作成用テンプレート
215
284
  - `.github/workflows/assign-creator-on-cc-triage-scope.yml` — Issue作成者を自動アサインするワークフロー
216
- - `claude-task-worker.json` — 設定ファイル(コマンド実行ディレクトリ直下。全ワーカーのデフォルト設定が書き込まれた状態で作成される)
285
+ - `claude-task-worker.json` — 設定ファイル(コマンド実行ディレクトリ直下。全ワーカーのデフォルト設定と `uiDesign`(既定は無効)が書き込まれた状態で作成される)
286
+
287
+ CodeGraph のセットアップ:
288
+
289
+ - グローバル gitignore(`$XDG_CONFIG_HOME/git/ignore`、未設定なら `~/.config/git/ignore`)へ `.codegraph/` を追記する(冪等)。`.codegraph/` はプロジェクトごとのローカルインデックスでコミット対象ではないが、対象リポジトリの `.gitignore` を汚さないためグローバル側に入れる
290
+ - `codegraph init` でインデックスを構築する。CodeGraph が未インストールでも `init` 全体は失敗せず、ログにエラーを出して続行する
217
291
 
218
292
  ## コマンド
219
293
 
@@ -282,9 +356,11 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
282
356
 
283
357
  ディスパッチャーは以下の3つの機能を持つ。
284
358
 
285
- - **一斉起動**: 対象プロジェクトごとに `ctw:<プロジェクト名>` ラベルのherdrワークスペースを作成し、そのプロジェクトのディレクトリで(`--project` を除いた)同じコマンドを非同期実行する
359
+ - **一斉起動**: 対象プロジェクトごとに `ctw:<プロジェクト名>` ラベルのherdrワークスペースを作成し、そのプロジェクトのディレクトリで(`--project` を除いた)同じコマンドを実行する。ペインのシェル初期化が終わるのを待ってからコマンドを送り、送信後はワーカープロセスが実際に起動したかを確認する(シェルのままなら再送し、それでも起動しなければそのプロジェクトは失敗としてタブを閉じる)
286
360
  - **稼働一覧**: 各セッションのプロジェクト名・ワークスペースID・ペインID・ステータス・稼働時間をステータステーブルとして定期的に画面へ描画し、対象プロセスが終了したセッションは自動的に一覧から除去する
287
- - **一括停止**: SIGTERM/SIGINTを受けると、稼働中の全セッションへ ctrl-c を送信して各プロジェクトのコマンドを終了させ、終了を待ってからherdrワークスペースを閉じる(`mode: "herdr"` でワーカーが作ったタスクタブもワークスペースごと片付く)
361
+ - **一括停止**: SIGTERM/SIGINTを受けると、稼働中の全セッションへ ctrl-c を送信して各プロジェクトのコマンドを終了させ、終了を待ってからherdrワークスペースを閉じる(`mode: "herdr"` でワーカーが作ったタスクタブもワークスペースごと片付く)。もう一度シグナルを送ると強制終了する
362
+
363
+ > ℹ️ herdr はワークスペースを閉じる際、閉じた対象がフォーカスされていなくても別のワークスペースへフォーカスを移す。ディスパッチャーはクローズ直前のフォーカス状態を控えて閉じた後に復元するため、無関係なワークスペースを見ていても表示は勝手に切り替わらない。
288
364
 
289
365
  ```bash
290
366
  claude-task-worker all --project all
@@ -312,9 +388,9 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
312
388
 
313
389
  `mode: "herdr"` のときの1タスクの流れ:
314
390
 
315
- 1. worktree を作成し、`ctw:<プロジェクト名>:#<Issue/PR番号>` ラベルのタブで claude を TUI 起動する
316
- 2. herdr が持つ agent ステータスを監視し、`working` → `idle` の遷移をタスク完了とみなす
317
- 3. 完了したらペインの内容を回収して通知に使い、タブを閉じてラベル・worktree を後片付けする
391
+ 1. worktree を作成し、`ctw:<プロジェクト名>:#<Issue/PR番号>` ラベルのタブで claude を TUI 起動する(タブを先に作ってからその中で起動するため、ユーザーが見ているタブにペインが割り込むことはない)
392
+ 2. herdr が持つ agent ステータスを監視し、`done`(未確認完了)または `working` → `idle` の遷移をタスク完了とみなす。ワーカーのタスクタブは誰も開かないため、完了したタスクは通常 `done` として観測される
393
+ 3. 完了したら claude のセッション transcript(`~/.claude/projects/*/<sessionId>.jsonl`)から最終レポートを回収して通知に使い、タブを閉じてラベル・worktree を後片付けする。transcript を引けない場合のみペインの内容にフォールバックする
318
394
 
319
395
  補足:
320
396
 
@@ -347,12 +423,14 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
347
423
  | `headroom` | 実行されるコマンド |
348
424
  |-----------|------------------|
349
425
  | `false`(既定) | `claude <引数>` |
350
- | `true` | `headroom wrap claude -- <引数>` |
426
+ | `true` | `headroom wrap claude --1m --memory --no-tokensave --no-serena -- <引数>` |
351
427
 
352
428
  補足:
353
429
 
354
430
  - `mode` とは独立して組み合わせられる(`mode: "herdr"` の TUI 起動も `headroom wrap claude` になる)
355
- - claude へ渡す引数はすべて `--` の後ろに置かれる。`headroom wrap claude` 自身も `--port` / `--memory` などのオプションを持ち、`-p` のように衝突しうるフラグは `--` の後ろでないと claude に届かないため
431
+ - claude へ渡す引数はすべて `--` の後ろに置かれる。`headroom wrap claude` 自身も `--port` / `--memory` などのオプションを持ち、`-p` のように衝突しうるフラグは `--` の後ろでないと claude に届かないため。逆に headroom 自身へのオプションは `--` の前に置く
432
+ - 1M コンテキストウィンドウは `--model` の値へ `[1m]` サフィックスを付けることで解放する(`--model 'sonnet[1m]'` のようになる)。headroom の `--1m` は `ANTHROPIC_MODEL` をセットするだけの実装で、CLI の `--model` が環境変数に勝つため、`--model` を明示するワーカー起動では単体では効かない
433
+ - `--no-tokensave` / `--no-serena` で headroom が既定で登録するコードグラフ系 MCP を止める。コード探索はプラグインの codegraph MCP が担うため機能面の損失はなく、タスク起動のたびに走る再登録・再インデックスと、ユーザーのグローバル設定(`~/.claude.json`)の書き換えを避けられる
356
434
  - `headroom: true` で headroom コマンドが PATH に無い場合、ワーカーは起動時にエラー終了する(`mode: "herdr"` と同じく、サイレントに直接起動へフォールバックはしない)
357
435
  - `headroom wrap claude` は claude を起動する前に起動バナー(枠線・`ANTHROPIC_BASE_URL=...` など)を stdout へ出力し、これを抑止するオプションは無い。ワーカーは空振りセッション検知(exit 0 かつ無出力を失敗とみなす判定)の前にこのバナーを取り除くため、検知は `headroom: true` でも従来どおり機能する。ただし Slack 通知の本文にはバナーがそのまま含まれる
358
436
 
@@ -430,9 +508,30 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
430
508
  - 完了後、親Issueに `cc-pr-created` ラベルを付与
431
509
  - 完了後、作成されたエピックPR(`cc-epic-<親Issue番号>` ブランチ)に `cc-epic-issue` と `cc-triage-scope` ラベルを付与し、triage-pr のリリースゲート判定に引き継ぐ
432
510
 
511
+ ### create-ui-design
512
+
513
+ `cc-create-ui-design` ラベルが付いたIssueを定期取得し、`/claude-task-worker:create-ui-design` を起動して Pencil デザイン(`.pen` + スナップショットPNG)を作成し、デザインのみのPRを作成する。(デフォルト1分間隔)
514
+
515
+ - `claude-task-worker.json` の `uiDesign.enabled` が `true` の場合のみ起動する(`false` なら1行ログを出して即終了)
516
+ - `cc-ui-design-pr-created` / `cc-ui-design-ready` が付いているIssueは除外
517
+ - デザインPRは `cc-ui-design-<Issue番号>` の固定名ブランチを head とする(後段の `apply-ui-design` が一意に特定するため)
518
+ - 完了後、デザインPRの実在を確認できた場合のみ、PRに `cc-ui-design` と `cc-triage-scope`、Issueに `cc-ui-design-pr-created` を付与する
519
+ - スキルが「デザイン不要」と判定した場合(`cc-ui-design-ready` 付与済み)は完了扱いとし、そのまま実装フェーズへ流す
520
+ - PRの実在を確認できない場合は `cc-need-human-check` を付与してIssueにコメントを残す
521
+
522
+ ### apply-ui-design
523
+
524
+ `cc-ui-design-pr-created` ラベルが付いたIssueを定期取得し、デザインPRのマージ後に `/claude-task-worker:apply-ui-design` を起動して Issue description へデザイン参照セクション(`## UIデザイン`)を書き戻す。(デフォルト1分間隔)
525
+
526
+ - `claude-task-worker.json` の `uiDesign.enabled` が `true` の場合のみ起動する
527
+ - `cc-ui-design-ready` / `cc-exec-issue` が付いているIssueは除外
528
+ - preflight でデザインPR(`cc-ui-design-<Issue番号>` を head とするPR)の状態を確認し、`OPEN` の間はスキップしてマージを待つ
529
+ - 未マージのままクローズされた場合・PRが見つからない場合は `cc-need-human-check` を付与してスキップする
530
+ - 完了後、description に `## UIデザイン` セクションと `.pen` パスが載っていることを検証できた場合のみ `cc-ui-design-ready` と `cc-exec-issue` を付与する
531
+
433
532
  ### all
434
533
 
435
- 通常ワーカー7つ(exec-issue, fix-review-point, create-issue, update-issue, answer-issue-questions, resolve-conflict, epic-issue)を同時にポーリングする。`--epic` / `--label` オプションでIssue系ワーカーの処理対象を絞り込み可能(どちらも複数指定可)。
534
+ 通常ワーカー9つ(exec-issue, fix-review-point, create-issue, update-issue, answer-issue-questions, resolve-conflict, epic-issue, create-ui-design, apply-ui-design)を同時にポーリングする。`--epic` / `--label` オプションでIssue系ワーカーの処理対象を絞り込み可能(どちらも複数指定可)。UIデザイン系2ワーカーは `uiDesign.enabled` が `false` なら起動しない。
436
535
 
437
536
  ### yolo
438
537
 
@@ -444,7 +543,7 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
444
543
 
445
544
  ### install
446
545
 
447
- `claude-task-worker` マーケットプレイスの追加・プラグインのインストール・CLI本体のグローバルインストールを一括で行う。
546
+ `claude-task-worker` マーケットプレイスの追加・プラグインのインストール・CLI本体のグローバルインストール・CodeGraph CLI のインストールを一括で行う。
448
547
 
449
548
  ```bash
450
549
  npx claude-task-worker install
@@ -453,12 +552,13 @@ npx claude-task-worker install
453
552
  - `claude plugin marketplace add getty104/claude-task-worker` — マーケットプレイスの追加(追加済みの場合のエラーはログのみで無視して続行)
454
553
  - `claude plugin install claude-task-worker@claude-task-worker` — プラグインのインストール
455
554
  - `npm install -g claude-task-worker@latest` — CLI 本体のグローバルインストール
555
+ - `npm install -g @colbymchenry/codegraph@latest` — CodeGraph CLI のインストール
456
556
 
457
- いずれかのステップが失敗しても処理は継続し、`[install]` プレフィックス付きでエラー内容がログ出力される。
557
+ いずれかのステップが失敗しても処理は継続し、`[install]` プレフィックス付きでエラー内容がログ出力される(失敗があった場合の終了コードは 1)。
458
558
 
459
559
  ### update
460
560
 
461
- `claude-task-worker` プラグイン/マーケットプレイスとCLI本体を更新する。
561
+ `claude-task-worker` プラグイン/マーケットプレイス・CLI本体・CodeGraph CLI を更新する。
462
562
 
463
563
  ```bash
464
564
  claude-task-worker update
@@ -467,8 +567,9 @@ claude-task-worker update
467
567
  - `claude plugin marketplace update claude-task-worker` — マーケットプレイスの更新
468
568
  - `claude plugin update claude-task-worker@claude-task-worker` — プラグインの更新(反映にはセッション再起動が必要)
469
569
  - `npm install -g claude-task-worker@latest` — CLI 本体の更新
570
+ - `codegraph upgrade` — CodeGraph CLI の更新(失敗時は `npm install -g @colbymchenry/codegraph@latest` へフォールバック)
470
571
 
471
- いずれかのステップが失敗しても処理は継続し、`[update]` プレフィックス付きでエラー内容がログ出力される。
572
+ いずれかのステップが失敗しても処理は継続し、`[update]` プレフィックス付きでエラー内容がログ出力される(失敗があった場合の終了コードは 1)。
472
573
 
473
574
  ### version
474
575
 
@@ -480,7 +581,7 @@ claude-task-worker --version
480
581
  claude-task-worker -v
481
582
  ```
482
583
 
483
- `package.json` の `version` を出力する(例: `0.6.0`)。
584
+ `package.json` の `version` を出力する(例: `0.34.0`)。
484
585
 
485
586
  ## 設定ファイル
486
587
 
@@ -489,6 +590,7 @@ claude-task-worker -v
489
590
  | キー | 型 | デフォルト | 説明 |
490
591
  |---|---|---|---|
491
592
  | `fixReviewPointCallbackCommentMessage` | string | - | fix-review-point 完了時にPRへ投稿するコメント(未設定の場合は投稿しない) |
593
+ | `uiDesign` | object | `{ "enabled": false, "designDir": "designs" }` | UIデザイン先行ワークフローの設定(詳細は「[UIデザイン先行ワークフロー](#uiデザイン先行ワークフロー)」) |
492
594
  | `workers` | object | `{}` | ワーカーごとに Claude CLI に渡すスキル、`--model` / `--effort`、ポーリング間隔、クールダウン時間、最大同時実行数を上書きする設定(詳細は下記) |
493
595
 
494
596
  ### ワーカーごとの設定
@@ -507,6 +609,8 @@ claude-task-worker -v
507
609
  | `resolve-conflict` | `/claude-task-worker:resolve-pr-conflict` | `sonnet` | `high` | 60 | 0 | 1 |
508
610
  | `check-dependabot` | `/claude-task-worker:check-dependabot` | `sonnet` | `high` | 3600 | 0 | 1 |
509
611
  | `epic-issue` | `/claude-task-worker:create-epic-pr` | `sonnet` | `high` | 300 | 0 | 1 |
612
+ | `create-ui-design` | `/claude-task-worker:create-ui-design` | `sonnet` | `high` | 60 | 0 | 1 |
613
+ | `apply-ui-design` | `/claude-task-worker:apply-ui-design` | `sonnet` | `high` | 60 | 0 | 1 |
510
614
 
511
615
  各フィールドの値:
512
616
 
@@ -543,13 +647,27 @@ claude-task-worker all
543
647
 
544
648
  通知にはClaude APIの使用状況(5時間/7日間の利用率とリセット時刻)も含まれる。未設定の場合、通知は送信されない。
545
649
 
650
+ 使用状況を組み立てる際、[RunCat Neo](https://kyome.io/runcat/) 用のスナップショットを `~/.claude/runcat-usage.json`(`RUNCAT_OUT_FILE` で変更可)へ一時ファイル + rename で原子的に書き出す。Webhook 未設定でも通知が no-op になるだけでスナップショットは更新される。使用状況の取得自体は `/tmp/claude-usage-cache.json` の360秒キャッシュを挟むため、値は最大6分古くなりうる。
651
+
546
652
  ## プロセス管理
547
653
 
548
654
  実行中のタスクはリアルタイムのステータステーブルで表示される。
549
655
 
550
656
  - タスクID・タイトル・ステータス(running/completed/failed)・開始時刻・経過時間を表示
657
+ - `mode: "herdr"` では実行中の行に agent ステータスが併記される(`running:working` / `running:blocked` など)
551
658
  - 同一Issue/PRの重複実行を自動防止
552
- - SIGTERM/SIGINTで全子プロセスをgraceful shutdown
659
+ - SIGTERM/SIGINTで全子プロセスをgraceful shutdown(もう一度送ると強制終了し、ラベル・worktree の後片付けを試みる)
660
+ - 前回の異常終了で残った worktree はワーカー起動時に自動回収される(実行中タスク・対話セッションが掴んでいる worktree は保護される)
661
+
662
+ ### タスク実行のガード
663
+
664
+ ワーカーは応答するユーザーがいない状態でスキルを起動するため、処理が未完のままセッションが終了してラベルだけ進む事故を防ぐガードを持つ。
665
+
666
+ - **バックグラウンド実行の無効化**: `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` を全タスクの環境変数として注入し、Bash の `run_in_background` やサブエージェントの自動バックグラウンド化を止める
667
+ - **ツールの無効化**: `--disallowedTools` で `Monitor` / `ScheduleWakeup` / `AskUserQuestion` / `EnterPlanMode` / `Cron*` / `RemoteTrigger` / `EnterWorktree` を無効化する(後続ウェイクアップ前提のもの、応答者が必要なもの、ワーカーの worktree 管理と競合するもの)
668
+ - **自律実行原則の注入**: `--append-system-prompt` で「ユーザーに質問しない・全ステップを完遂してから終了する・曖昧なら安全側を選ぶ・サブエージェントの完了報告を検証する」および CodeGraph 優先のコード探索方針を注入する
669
+ - **完了検証**: exec-issue / epic-issue は PR の実在(または Issue のクローズ)を確認できるまで `cc-pr-created` を付けず、確認できない場合は `cc-need-human-check` を付けて Issue にコメントを残す
670
+ - **空振り検知**: 正常終了しても出力が空のセッションは失敗として分類し、失敗通知(stderr の末尾を含む)を送る
553
671
 
554
672
  ## 開発
555
673
 
@@ -557,6 +675,7 @@ claude-task-worker all
557
675
  npm install
558
676
  npm run build # 型チェック(tsc --noEmit)+ esbuild で dist/index.js にバンドル
559
677
  npm run dev # 型チェックの watch モード
678
+ npm test # ユニットテスト(node --experimental-strip-types --test)
560
679
  npm run lint # ESLint
561
680
  npm run lint:fix # ESLint(自動修正)
562
681
  npm run format # Prettier で整形
package/dist/index.js CHANGED
@@ -164,6 +164,69 @@ var init_task_result = __esm({
164
164
  }
165
165
  });
166
166
 
167
+ // src/transcript.ts
168
+ import { existsSync, readFileSync as readFileSync3, readdirSync } from "node:fs";
169
+ import { homedir as homedir2 } from "node:os";
170
+ import { join as join3 } from "node:path";
171
+ function transcriptRoot() {
172
+ return join3(homedir2(), ".claude", "projects");
173
+ }
174
+ function findTranscriptPath(sessionId, root = transcriptRoot()) {
175
+ if (sessionId === "") return null;
176
+ let entries;
177
+ try {
178
+ entries = readdirSync(root);
179
+ } catch {
180
+ return null;
181
+ }
182
+ for (const entry of entries) {
183
+ const candidate = join3(root, entry, `${sessionId}.jsonl`);
184
+ if (existsSync(candidate)) return candidate;
185
+ }
186
+ return null;
187
+ }
188
+ function textOf(entry) {
189
+ const content = entry.message?.content;
190
+ if (typeof content === "string") return content;
191
+ if (!Array.isArray(content)) return "";
192
+ return content.filter(
193
+ (block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string"
194
+ ).map((block) => block.text).join("\n").trim();
195
+ }
196
+ function extractFinalAssistantText(jsonl) {
197
+ const lines = jsonl.split("\n");
198
+ for (let i = lines.length - 1; i >= 0; i--) {
199
+ const line = lines[i].trim();
200
+ if (line === "") continue;
201
+ let entry;
202
+ try {
203
+ entry = JSON.parse(line);
204
+ } catch {
205
+ continue;
206
+ }
207
+ if (entry.type !== "assistant" || entry.isSidechain === true) continue;
208
+ const text = textOf(entry);
209
+ if (text !== "") return text;
210
+ }
211
+ return "";
212
+ }
213
+ function readFinalReport(sessionId, root = transcriptRoot()) {
214
+ if (!sessionId) return "";
215
+ const path = findTranscriptPath(sessionId, root);
216
+ if (!path) return "";
217
+ try {
218
+ return extractFinalAssistantText(readFileSync3(path, "utf-8"));
219
+ } catch (err) {
220
+ console.error(`[transcript] failed to read ${path}: ${err}`);
221
+ return "";
222
+ }
223
+ }
224
+ var init_transcript = __esm({
225
+ "src/transcript.ts"() {
226
+ "use strict";
227
+ }
228
+ });
229
+
167
230
  // src/herdr.ts
168
231
  var herdr_exports = {};
169
232
  __export(herdr_exports, {
@@ -171,15 +234,16 @@ __export(herdr_exports, {
171
234
  HerdrError: () => HerdrError,
172
235
  HerdrUnavailableError: () => HerdrUnavailableError,
173
236
  agentGet: () => agentGet,
174
- agentStart: () => agentStart,
175
237
  checkHerdrAvailable: () => checkHerdrAvailable,
176
238
  getCurrentWorkspaceId: () => getCurrentWorkspaceId,
239
+ launchAgentInPane: () => launchAgentInPane,
177
240
  paneClose: () => paneClose,
178
241
  paneGet: () => paneGet,
179
242
  paneProcessInfo: () => paneProcessInfo,
180
243
  paneRead: () => paneRead,
181
244
  paneSendKeys: () => paneSendKeys,
182
245
  paneSendText: () => paneSendText,
246
+ shellQuoteArgv: () => shellQuoteArgv,
183
247
  tabClose: () => tabClose,
184
248
  tabCreate: () => tabCreate,
185
249
  tabList: () => tabList,
@@ -332,32 +396,12 @@ async function workspaceClose(workspaceId) {
332
396
  async function workspaceFocus(workspaceId) {
333
397
  await execHerdr(["workspace", "focus", workspaceId]);
334
398
  }
335
- async function agentStart({
336
- name,
337
- cwd,
338
- argv,
339
- workspaceId,
340
- tabId: targetTabId,
341
- env
342
- }) {
343
- const result = await execHerdr([
344
- "agent",
345
- "start",
346
- name,
347
- ...workspaceId ? ["--workspace", workspaceId] : [],
348
- ...targetTabId ? ["--tab", targetTabId] : [],
349
- ...cwdArgs(cwd),
350
- ...envArgs(env),
351
- "--no-focus",
352
- "--",
353
- ...argv
354
- ]);
355
- const paneId = result?.agent?.pane_id;
356
- const tabId = result?.agent?.tab_id;
357
- if (!paneId || !tabId) {
358
- throw new Error("Failed to start agent: invalid response structure from herdr");
359
- }
360
- return { paneId, tabId };
399
+ function shellQuoteArgv(argv) {
400
+ return argv.map((arg) => `'${arg.replace(/'/g, `'\\''`)}'`).join(" ");
401
+ }
402
+ async function launchAgentInPane(paneId, argv) {
403
+ await paneSendText(paneId, shellQuoteArgv(argv));
404
+ await paneSendKeys(paneId, "enter");
361
405
  }
362
406
  function toAgentStatus(value) {
363
407
  return value === "working" || value === "idle" || value === "blocked" || value === "done" ? value : "unknown";
@@ -368,11 +412,14 @@ async function agentGet(target) {
368
412
  if (!agent?.pane_id) {
369
413
  throw new Error(`Failed to get agent info for ${target}: invalid response structure from herdr`);
370
414
  }
415
+ const session = agent.agent_session;
416
+ const sessionId = session?.kind === "id" && typeof session.value === "string" ? session.value : void 0;
371
417
  return {
372
418
  paneId: agent.pane_id,
373
419
  tabId: agent.tab_id ?? "",
374
420
  workspaceId: agent.workspace_id ?? "",
375
- agentStatus: toAgentStatus(agent.agent_status)
421
+ agentStatus: toAgentStatus(agent.agent_status),
422
+ sessionId
376
423
  };
377
424
  }
378
425
  function getCurrentWorkspaceId() {
@@ -485,10 +532,14 @@ var init_herdr = __esm({
485
532
  // src/herdr-runner.ts
486
533
  var herdr_runner_exports = {};
487
534
  __export(herdr_runner_exports, {
535
+ AGENT_DETECT_POLL_INTERVAL_MS: () => AGENT_DETECT_POLL_INTERVAL_MS,
536
+ AGENT_DETECT_TIMEOUT_MS: () => AGENT_DETECT_TIMEOUT_MS,
488
537
  AGENT_POLL_INTERVAL_MS: () => AGENT_POLL_INTERVAL_MS,
489
538
  CLAUDE_EXIT_POLL_INTERVAL_MS: () => CLAUDE_EXIT_POLL_INTERVAL_MS,
490
539
  CLAUDE_EXIT_TIMEOUT_MS: () => CLAUDE_EXIT_TIMEOUT_MS,
491
540
  PANE_OUTPUT_LINES: () => PANE_OUTPUT_LINES,
541
+ PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS,
542
+ PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS,
492
543
  buildHerdrTaskResult: () => buildHerdrTaskResult,
493
544
  createCompletionTracker: () => createCompletionTracker,
494
545
  observeAgentStatus: () => observeAgentStatus,
@@ -523,6 +574,10 @@ function observeAgentStatus(tracker, status) {
523
574
  return { tracker, decision: "running" };
524
575
  }
525
576
  function buildHerdrTaskResult(paneOutput, options) {
577
+ const report = options?.report?.trim() ?? "";
578
+ if (report !== "") {
579
+ return { status: "completed", output: report };
580
+ }
526
581
  const meaningful = options?.headroom ? stripHeadroomBanner(paneOutput) : paneOutput;
527
582
  if (meaningful.trim() === "") {
528
583
  return {
@@ -538,39 +593,89 @@ async function startHerdrTask({
538
593
  argv,
539
594
  env,
540
595
  workspaceId,
541
- herdr
596
+ herdr,
597
+ timing
542
598
  }) {
543
599
  const mod = herdr ?? await loadHerdr();
544
- const { tabId, paneId: shellPaneId } = await mod.tabCreate({ label, cwd, workspaceId, env });
545
- let paneId;
600
+ const { tabId, paneId } = await mod.tabCreate({ label, cwd, workspaceId, env });
546
601
  try {
547
- ({ paneId } = await mod.agentStart({ name: label, cwd, argv, env, workspaceId, tabId }));
602
+ const ready = await waitForPaneReady(paneId, mod, {
603
+ timeoutMs: timing?.paneReadyTimeoutMs,
604
+ pollIntervalMs: timing?.paneReadyPollIntervalMs
605
+ });
606
+ if (!ready) {
607
+ console.warn(`[herdr-runner] pane ${paneId} produced no prompt before the timeout, launching anyway`);
608
+ }
609
+ await mod.launchAgentInPane(paneId, argv);
610
+ const detected = await waitForAgentDetected(paneId, mod, {
611
+ timeoutMs: timing?.agentDetectTimeoutMs,
612
+ pollIntervalMs: timing?.agentDetectPollIntervalMs
613
+ });
614
+ if (!detected) {
615
+ throw new Error(
616
+ `claude was not detected in pane ${paneId} after launch (it may have failed to start; e.g. a skill preamble command failed)`
617
+ );
618
+ }
548
619
  } catch (err) {
549
620
  await mod.tabClose(tabId).catch(() => {
550
621
  });
551
622
  throw err;
552
623
  }
553
- await mod.paneClose(shellPaneId).catch((err) => {
554
- console.error(`[herdr-runner] failed to close the placeholder shell pane ${shellPaneId}: ${err}`);
555
- });
556
624
  return { paneId, tabId };
557
625
  }
626
+ async function waitForPaneReady(paneId, mod, options) {
627
+ const timeoutMs = options?.timeoutMs ?? PANE_READY_TIMEOUT_MS;
628
+ const pollIntervalMs = options?.pollIntervalMs ?? PANE_READY_POLL_INTERVAL_MS;
629
+ const deadline = Date.now() + timeoutMs;
630
+ for (; ; ) {
631
+ let content = "";
632
+ try {
633
+ content = await mod.paneRead(paneId, { source: "visible" });
634
+ } catch (err) {
635
+ console.error(`[herdr-runner] failed to read pane ${paneId} while waiting for its prompt: ${err}`);
636
+ }
637
+ if (content.trim() !== "") return true;
638
+ if (Date.now() >= deadline) return false;
639
+ await sleep(pollIntervalMs);
640
+ }
641
+ }
642
+ async function waitForAgentDetected(paneId, mod, options) {
643
+ const timeoutMs = options?.timeoutMs ?? AGENT_DETECT_TIMEOUT_MS;
644
+ const pollIntervalMs = options?.pollIntervalMs ?? AGENT_DETECT_POLL_INTERVAL_MS;
645
+ const deadline = Date.now() + timeoutMs;
646
+ for (; ; ) {
647
+ try {
648
+ await mod.agentGet(paneId);
649
+ return true;
650
+ } catch (err) {
651
+ if (err instanceof mod.HerdrError && err.code === "pane_not_found") return false;
652
+ if (!(err instanceof mod.HerdrError && err.code === "agent_not_found")) {
653
+ console.error(`[herdr-runner] unexpected error while waiting for agent detection on pane ${paneId}: ${err}`);
654
+ }
655
+ }
656
+ if (Date.now() >= deadline) return false;
657
+ await sleep(pollIntervalMs);
658
+ }
659
+ }
558
660
  async function waitForHerdrTask(paneId, options) {
559
661
  const mod = options?.herdr ?? await loadHerdr();
560
662
  const pollIntervalMs = options?.pollIntervalMs ?? AGENT_POLL_INTERVAL_MS;
561
663
  let tracker = createCompletionTracker();
664
+ let sessionId;
562
665
  for (; ; ) {
563
666
  if (options?.signal?.aborted) {
564
667
  return { status: "failed", output: "[worker] the worker is shutting down; the task was interrupted" };
565
668
  }
566
669
  let status;
567
670
  try {
568
- status = (await mod.agentGet(paneId)).agentStatus;
671
+ const agent = await mod.agentGet(paneId);
672
+ status = agent.agentStatus;
673
+ if (agent.sessionId) sessionId = agent.sessionId;
569
674
  } catch (err) {
570
- if (err instanceof mod.HerdrError && err.code === "pane_not_found") {
675
+ if (err instanceof mod.HerdrError && (err.code === "pane_not_found" || err.code === "agent_not_found")) {
571
676
  return {
572
677
  status: "failed",
573
- output: "[worker] the claude pane disappeared before the task completed (claude died or the tab was closed)"
678
+ output: "[worker] the claude agent disappeared before the task completed (claude died or the tab was closed)"
574
679
  };
575
680
  }
576
681
  console.error(`[herdr-runner] failed to read agent status for pane ${paneId}: ${err}`);
@@ -582,7 +687,8 @@ async function waitForHerdrTask(paneId, options) {
582
687
  tracker = observed.tracker;
583
688
  if (observed.decision === "completed") {
584
689
  const output = await readPaneOutput(paneId, mod);
585
- return buildHerdrTaskResult(output, { headroom: options?.headroom });
690
+ const report = (options?.readReport ?? readFinalReport)(sessionId);
691
+ return buildHerdrTaskResult(output, { headroom: options?.headroom, report });
586
692
  }
587
693
  if (observed.decision === "blocked-first-seen") {
588
694
  options?.onBlocked?.();
@@ -598,15 +704,17 @@ async function readPaneOutput(paneId, mod) {
598
704
  return "";
599
705
  }
600
706
  }
601
- async function waitForPaneGone(paneId, mod, options) {
707
+ async function waitForAgentGone(paneId, mod, options) {
602
708
  const timeoutMs = options?.timeoutMs ?? CLAUDE_EXIT_TIMEOUT_MS;
603
709
  const pollIntervalMs = options?.pollIntervalMs ?? CLAUDE_EXIT_POLL_INTERVAL_MS;
604
710
  const deadline = Date.now() + timeoutMs;
605
711
  for (; ; ) {
606
712
  try {
607
- await mod.paneGet(paneId);
713
+ await mod.agentGet(paneId);
608
714
  } catch (err) {
609
- if (err instanceof mod.HerdrError && err.code === "pane_not_found") return true;
715
+ if (err instanceof mod.HerdrError && (err.code === "agent_not_found" || err.code === "pane_not_found")) {
716
+ return true;
717
+ }
610
718
  }
611
719
  if (Date.now() >= deadline) return false;
612
720
  await sleep(pollIntervalMs);
@@ -616,7 +724,7 @@ async function stopHerdrTask(task, herdr, options) {
616
724
  const mod = herdr ?? await loadHerdr();
617
725
  try {
618
726
  await mod.paneSendKeys(task.paneId, "ctrl+c", "ctrl+c");
619
- const exited = await waitForPaneGone(task.paneId, mod, {
727
+ const exited = await waitForAgentGone(task.paneId, mod, {
620
728
  timeoutMs: options?.exitTimeoutMs,
621
729
  pollIntervalMs: options?.exitPollIntervalMs
622
730
  });
@@ -638,13 +746,18 @@ async function stopHerdrTask(task, herdr, options) {
638
746
  function sleep(ms) {
639
747
  return new Promise((resolve3) => setTimeout(resolve3, ms));
640
748
  }
641
- var AGENT_POLL_INTERVAL_MS, PANE_OUTPUT_LINES, CLAUDE_EXIT_TIMEOUT_MS, CLAUDE_EXIT_POLL_INTERVAL_MS;
749
+ var AGENT_POLL_INTERVAL_MS, PANE_OUTPUT_LINES, PANE_READY_TIMEOUT_MS, PANE_READY_POLL_INTERVAL_MS, AGENT_DETECT_TIMEOUT_MS, AGENT_DETECT_POLL_INTERVAL_MS, CLAUDE_EXIT_TIMEOUT_MS, CLAUDE_EXIT_POLL_INTERVAL_MS;
642
750
  var init_herdr_runner = __esm({
643
751
  "src/herdr-runner.ts"() {
644
752
  "use strict";
645
753
  init_task_result();
754
+ init_transcript();
646
755
  AGENT_POLL_INTERVAL_MS = 3 * 1e3;
647
756
  PANE_OUTPUT_LINES = 300;
757
+ PANE_READY_TIMEOUT_MS = 30 * 1e3;
758
+ PANE_READY_POLL_INTERVAL_MS = 200;
759
+ AGENT_DETECT_TIMEOUT_MS = 120 * 1e3;
760
+ AGENT_DETECT_POLL_INTERVAL_MS = 500;
648
761
  CLAUDE_EXIT_TIMEOUT_MS = 15 * 1e3;
649
762
  CLAUDE_EXIT_POLL_INTERVAL_MS = 200;
650
763
  }
@@ -697,8 +810,8 @@ var init_run_command = __esm({
697
810
  var dispatcher_exports = {};
698
811
  __export(dispatcher_exports, {
699
812
  LABEL_PREFIX: () => LABEL_PREFIX,
700
- PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS,
701
- PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS,
813
+ PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS2,
814
+ PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS2,
702
815
  POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
703
816
  SEND_MAX_ATTEMPTS: () => SEND_MAX_ATTEMPTS,
704
817
  SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
@@ -718,7 +831,7 @@ __export(dispatcher_exports, {
718
831
  runDispatcher: () => runDispatcher,
719
832
  shutdownDispatcher: () => shutdownDispatcher,
720
833
  startWorkerInPane: () => startWorkerInPane,
721
- waitForPaneReady: () => waitForPaneReady,
834
+ waitForPaneReady: () => waitForPaneReady2,
722
835
  waitForWorkerStartup: () => waitForWorkerStartup,
723
836
  workspaceLabelFor: () => workspaceLabelFor
724
837
  });
@@ -759,9 +872,9 @@ function isWorkerProcess(process2) {
759
872
  function isShellProcess(process2) {
760
873
  return SHELL_NAME_PATTERN.test(process2.name ?? "") || SHELL_NAME_PATTERN.test(process2.cmdline ?? "");
761
874
  }
762
- async function waitForPaneReady(paneId, herdr, options) {
763
- const timeoutMs = options?.timeoutMs ?? PANE_READY_TIMEOUT_MS;
764
- const pollIntervalMs = options?.pollIntervalMs ?? PANE_READY_POLL_INTERVAL_MS;
875
+ async function waitForPaneReady2(paneId, herdr, options) {
876
+ const timeoutMs = options?.timeoutMs ?? PANE_READY_TIMEOUT_MS2;
877
+ const pollIntervalMs = options?.pollIntervalMs ?? PANE_READY_POLL_INTERVAL_MS2;
765
878
  const deadline = Date.now() + timeoutMs;
766
879
  for (; ; ) {
767
880
  const content = await herdr.paneRead(paneId, { source: "visible" });
@@ -785,7 +898,7 @@ async function waitForWorkerStartup(paneId, herdr, options) {
785
898
  async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
786
899
  const maxAttempts = options?.sendMaxAttempts ?? SEND_MAX_ATTEMPTS;
787
900
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
788
- const ready = await waitForPaneReady(paneId, herdr, {
901
+ const ready = await waitForPaneReady2(paneId, herdr, {
789
902
  timeoutMs: options?.paneReadyTimeoutMs,
790
903
  pollIntervalMs: options?.paneReadyPollIntervalMs
791
904
  });
@@ -1087,15 +1200,15 @@ function createDispatcherShutdownHandler(shutdown2) {
1087
1200
  };
1088
1201
  return { handle, isShuttingDown: () => shuttingDown2 };
1089
1202
  }
1090
- var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, PANE_READY_TIMEOUT_MS, PANE_READY_POLL_INTERVAL_MS, WORKER_STARTUP_TIMEOUT_MS, WORKER_STARTUP_POLL_INTERVAL_MS, SEND_MAX_ATTEMPTS, LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
1203
+ var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, PANE_READY_TIMEOUT_MS2, PANE_READY_POLL_INTERVAL_MS2, WORKER_STARTUP_TIMEOUT_MS, WORKER_STARTUP_POLL_INTERVAL_MS, SEND_MAX_ATTEMPTS, LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
1091
1204
  var init_dispatcher = __esm({
1092
1205
  async "src/dispatcher.ts"() {
1093
1206
  "use strict";
1094
1207
  ({ getDisplayWidth: getDisplayWidth2, truncateToWidth: truncateToWidth2, padToWidth: padToWidth2 } = await loadTable());
1095
1208
  POLL_INTERVAL_MS = 7 * 1e3;
1096
1209
  SHUTDOWN_TIMEOUT_MS = 10 * 60 * 1e3;
1097
- PANE_READY_TIMEOUT_MS = 30 * 1e3;
1098
- PANE_READY_POLL_INTERVAL_MS = 200;
1210
+ PANE_READY_TIMEOUT_MS2 = 30 * 1e3;
1211
+ PANE_READY_POLL_INTERVAL_MS2 = 200;
1099
1212
  WORKER_STARTUP_TIMEOUT_MS = 30 * 1e3;
1100
1213
  WORKER_STARTUP_POLL_INTERVAL_MS = 500;
1101
1214
  SEND_MAX_ATTEMPTS = 3;
@@ -1216,6 +1329,28 @@ async function findPrNumberByHeadRef(headRefName, state = "open") {
1216
1329
  const prs = JSON.parse(output);
1217
1330
  return prs.length > 0 ? prs[0].number : null;
1218
1331
  }
1332
+ async function findPrStateByHeadRef(headRefName) {
1333
+ const output = await execGh([
1334
+ "pr",
1335
+ "list",
1336
+ "--state",
1337
+ "all",
1338
+ "--head",
1339
+ headRefName,
1340
+ "--json",
1341
+ "number,state,mergedAt",
1342
+ "--limit",
1343
+ "1"
1344
+ ]);
1345
+ const prs = JSON.parse(output);
1346
+ if (prs.length === 0) return null;
1347
+ return { number: prs[0].number, state: prs[0].state, mergedAt: prs[0].mergedAt ?? null };
1348
+ }
1349
+ async function getIssueBody(issueNumber) {
1350
+ const output = await execGh(["issue", "view", String(issueNumber), "--json", "body"]);
1351
+ const parsed = JSON.parse(output);
1352
+ return typeof parsed?.body === "string" ? parsed.body : "";
1353
+ }
1219
1354
  async function getIssueState(issueNumber) {
1220
1355
  const output = await execGh(["issue", "view", String(issueNumber), "--json", "state"]);
1221
1356
  const parsed = JSON.parse(output);
@@ -1456,7 +1591,11 @@ function buildClaudeEnv(mode) {
1456
1591
 
1457
1592
  // src/config.ts
1458
1593
  import { readFileSync } from "node:fs";
1459
- import { join } from "node:path";
1594
+ import { isAbsolute, join, normalize, sep as SEP } from "node:path";
1595
+ var DEFAULT_UI_DESIGN_CONFIG = {
1596
+ enabled: false,
1597
+ designDir: "designs"
1598
+ };
1460
1599
  var DEFAULT_WORKER_CONFIG = {
1461
1600
  skill: "",
1462
1601
  model: "sonnet",
@@ -1545,10 +1684,27 @@ var WORKER_DEFAULTS = {
1545
1684
  pollingIntervalSeconds: 300,
1546
1685
  cooldownSeconds: 0,
1547
1686
  maxConcurrentTasks: 1
1687
+ },
1688
+ "create-ui-design": {
1689
+ skill: "/claude-task-worker:create-ui-design",
1690
+ model: "sonnet",
1691
+ effort: "high",
1692
+ pollingIntervalSeconds: 60,
1693
+ cooldownSeconds: 0,
1694
+ maxConcurrentTasks: 1
1695
+ },
1696
+ "apply-ui-design": {
1697
+ skill: "/claude-task-worker:apply-ui-design",
1698
+ model: "sonnet",
1699
+ effort: "high",
1700
+ pollingIntervalSeconds: 300,
1701
+ cooldownSeconds: 0,
1702
+ maxConcurrentTasks: 1
1548
1703
  }
1549
1704
  };
1550
1705
  var DEFAULT_CONFIG = {
1551
1706
  fixReviewPointCallbackCommentMessage: "",
1707
+ uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG },
1552
1708
  workers: {}
1553
1709
  };
1554
1710
  var CONFIG_PATH = join(process.cwd(), "claude-task-worker.json");
@@ -1616,6 +1772,35 @@ function parseWorkerEntry(name, val) {
1616
1772
  }
1617
1773
  return result;
1618
1774
  }
1775
+ function parseUiDesignEntry(val) {
1776
+ const result = { ...DEFAULT_UI_DESIGN_CONFIG };
1777
+ if (typeof val !== "object" || val === null || Array.isArray(val)) {
1778
+ console.warn(`[config] invalid uiDesign: expected object, using defaults`);
1779
+ return result;
1780
+ }
1781
+ const entry = val;
1782
+ if ("enabled" in entry) {
1783
+ if (typeof entry.enabled === "boolean") {
1784
+ result.enabled = entry.enabled;
1785
+ } else {
1786
+ console.warn(
1787
+ `[config] invalid uiDesign.enabled: ${String(entry.enabled)}, using default ${DEFAULT_UI_DESIGN_CONFIG.enabled}`
1788
+ );
1789
+ }
1790
+ }
1791
+ if ("designDir" in entry) {
1792
+ const normalized = typeof entry.designDir === "string" && entry.designDir.length > 0 ? normalize(entry.designDir) : null;
1793
+ const isContained = normalized !== null && !isAbsolute(normalized) && normalized !== ".." && !normalized.startsWith(`..${SEP}`);
1794
+ if (isContained) {
1795
+ result.designDir = normalized;
1796
+ } else {
1797
+ console.warn(
1798
+ `[config] invalid uiDesign.designDir: ${String(entry.designDir)}, using default ${DEFAULT_UI_DESIGN_CONFIG.designDir}`
1799
+ );
1800
+ }
1801
+ }
1802
+ return result;
1803
+ }
1619
1804
  function loadConfig() {
1620
1805
  const configPath = CONFIG_PATH;
1621
1806
  let raw;
@@ -1623,17 +1808,20 @@ function loadConfig() {
1623
1808
  raw = JSON.parse(readFileSync(configPath, "utf-8"));
1624
1809
  } catch (err) {
1625
1810
  if (err.code === "ENOENT") {
1626
- return { ...DEFAULT_CONFIG, workers: {} };
1811
+ return { ...DEFAULT_CONFIG, uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG }, workers: {} };
1627
1812
  }
1628
1813
  throw err;
1629
1814
  }
1630
- const result = { ...DEFAULT_CONFIG, workers: {} };
1815
+ const result = { ...DEFAULT_CONFIG, uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG }, workers: {} };
1631
1816
  if ("fixReviewPointCallbackCommentMessage" in raw) {
1632
1817
  const val = raw["fixReviewPointCallbackCommentMessage"];
1633
1818
  if (typeof val === "string") {
1634
1819
  result.fixReviewPointCallbackCommentMessage = val;
1635
1820
  }
1636
1821
  }
1822
+ if ("uiDesign" in raw) {
1823
+ result.uiDesign = parseUiDesignEntry(raw["uiDesign"]);
1824
+ }
1637
1825
  if ("workers" in raw) {
1638
1826
  const workers = raw["workers"];
1639
1827
  if (typeof workers !== "object" || workers === null || Array.isArray(workers)) {
@@ -1651,6 +1839,14 @@ function getWorkerConfig(workerName) {
1651
1839
  const config = loadConfig();
1652
1840
  return config.workers[workerName] ?? { ...defaultsFor(workerName) };
1653
1841
  }
1842
+ function getUiDesignConfig() {
1843
+ try {
1844
+ return loadConfig().uiDesign;
1845
+ } catch (err) {
1846
+ console.warn(`[config] failed to load uiDesign config, using defaults: ${err}`);
1847
+ return { ...DEFAULT_UI_DESIGN_CONFIG };
1848
+ }
1849
+ }
1654
1850
 
1655
1851
  // src/git.ts
1656
1852
  import { execSync, execFile } from "node:child_process";
@@ -1704,7 +1900,7 @@ init_task_result();
1704
1900
  // src/user-config.ts
1705
1901
  import { readFileSync as readFileSync2, statSync } from "node:fs";
1706
1902
  import { homedir } from "node:os";
1707
- import { isAbsolute, join as join2, resolve } from "node:path";
1903
+ import { isAbsolute as isAbsolute2, join as join2, resolve } from "node:path";
1708
1904
  var RESERVED_ALL = "all";
1709
1905
  var DEFAULT_RUN_MODE = "default";
1710
1906
  var DEFAULT_HEADROOM = false;
@@ -1801,7 +1997,7 @@ function loadUserConfig() {
1801
1997
  if (name === "__proto__") {
1802
1998
  throw new UserConfigError(`"__proto__" cannot be used as a key in "projects": ${path}`);
1803
1999
  }
1804
- if (typeof value !== "string" || !isAbsolute(value)) {
2000
+ if (typeof value !== "string" || !isAbsolute2(value)) {
1805
2001
  console.warn(`[config] invalid projects.${name}: expected an absolute path, skipping`);
1806
2002
  continue;
1807
2003
  }
@@ -2352,16 +2548,16 @@ function isGeneratedWorktreeName(name) {
2352
2548
 
2353
2549
  // src/slack.ts
2354
2550
  import { exec } from "node:child_process";
2355
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2356
- import { homedir as homedir3 } from "node:os";
2357
- import { join as join4 } from "node:path";
2551
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2552
+ import { homedir as homedir4 } from "node:os";
2553
+ import { join as join5 } from "node:path";
2358
2554
  import { promisify as promisify2 } from "node:util";
2359
2555
 
2360
2556
  // src/runcat.ts
2361
2557
  import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2362
- import { homedir as homedir2 } from "node:os";
2363
- import { dirname, join as join3 } from "node:path";
2364
- var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join3(process.env.HOME ?? homedir2(), ".claude", "runcat-usage.json");
2558
+ import { homedir as homedir3 } from "node:os";
2559
+ import { dirname, join as join4 } from "node:path";
2560
+ var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join4(process.env.HOME ?? homedir3(), ".claude", "runcat-usage.json");
2365
2561
  var JST = "Asia/Tokyo";
2366
2562
  function formatG(value) {
2367
2563
  return String(Number(value.toPrecision(6)));
@@ -2425,7 +2621,7 @@ function buildRuncatSnapshot(usage, now = /* @__PURE__ */ new Date()) {
2425
2621
  function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
2426
2622
  const snapshot = buildRuncatSnapshot(usage);
2427
2623
  const dir = dirname(outFile);
2428
- const tmp = join3(dir, `.runcat-${process.pid}-${Date.now()}.json`);
2624
+ const tmp = join4(dir, `.runcat-${process.pid}-${Date.now()}.json`);
2429
2625
  try {
2430
2626
  mkdirSync(dir, { recursive: true });
2431
2627
  writeFileSync(tmp, JSON.stringify(snapshot), "utf-8");
@@ -2462,13 +2658,13 @@ async function getOAuthToken() {
2462
2658
  const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
2463
2659
  return extractToken(JSON.parse(stdout.trim()));
2464
2660
  } catch {
2465
- const raw = readFileSync3(join4(homedir3(), ".claude", ".credentials.json"), "utf-8");
2661
+ const raw = readFileSync4(join5(homedir4(), ".claude", ".credentials.json"), "utf-8");
2466
2662
  return extractToken(JSON.parse(raw));
2467
2663
  }
2468
2664
  }
2469
2665
  function readUsageCache() {
2470
2666
  try {
2471
- const raw = readFileSync3(USAGE_CACHE_PATH, "utf-8");
2667
+ const raw = readFileSync4(USAGE_CACHE_PATH, "utf-8");
2472
2668
  const cached = JSON.parse(raw);
2473
2669
  if (Date.now() - cached.timestamp < USAGE_CACHE_TTL_SECONDS * 1e3) {
2474
2670
  return cached.data;
@@ -3156,22 +3352,247 @@ var epicIssueWorker = (opts = {}) => createIssuePollingWorker({
3156
3352
  }
3157
3353
  })();
3158
3354
 
3355
+ // src/workers/ui-design.ts
3356
+ var DESIGN_REFERENCE_HEADING = "## UI\u30C7\u30B6\u30A4\u30F3";
3357
+ function designBranchName(issueNumber) {
3358
+ return `cc-ui-design-${issueNumber}`;
3359
+ }
3360
+ var DESIGN_REFERENCE_HEADING_LINE = /^## UIデザイン[ \t]*$/m;
3361
+ var DESIGN_FILE_LINE = /^-\s*デザインファイル:\s*`([^`]+)`/m;
3362
+ function extractDesignSection(body) {
3363
+ const headingMatch = DESIGN_REFERENCE_HEADING_LINE.exec(body);
3364
+ if (headingMatch === null) return null;
3365
+ const rest = body.slice(headingMatch.index + headingMatch[0].length);
3366
+ const nextHeadingMatch = /^## /m.exec(rest);
3367
+ return nextHeadingMatch === null ? rest : rest.slice(0, nextHeadingMatch.index);
3368
+ }
3369
+ function extractDesignFilePath(body) {
3370
+ const section = extractDesignSection(body);
3371
+ if (section === null) return null;
3372
+ const match = DESIGN_FILE_LINE.exec(section);
3373
+ if (match === null) return null;
3374
+ const path = match[1].trim();
3375
+ if (path.length === 0 || path.includes("<") || path.includes(">") || !path.endsWith(".pen")) return null;
3376
+ return path;
3377
+ }
3378
+ function classifyDesignPr(pr) {
3379
+ if (pr === null) return "needs-human";
3380
+ if (pr.state === "MERGED" || pr.mergedAt !== null) return "proceed";
3381
+ if (pr.state === "OPEN") return "wait";
3382
+ return "needs-human";
3383
+ }
3384
+ function designPrMissingComment(issueNumber) {
3385
+ return [
3386
+ "## \u30C7\u30B6\u30A4\u30F3PR\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\uFF08\u8981\u4EBA\u624B\u78BA\u8A8D\uFF09",
3387
+ `apply-ui-design \u306F\u30C7\u30B6\u30A4\u30F3\u30D6\u30E9\u30F3\u30C1 \`${designBranchName(issueNumber)}\` \u3092 head \u3068\u3059\u308BPR\u3092\u5F85\u3063\u3066\u3044\u307E\u3059\u304C\u3001\u305D\u306EPR\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u304B\u3001\u30DE\u30FC\u30B8\u3055\u308C\u305A\u30AF\u30ED\u30FC\u30BA\u3055\u308C\u3066\u3044\u307E\u3059\u3002`,
3388
+ "",
3389
+ "## \u72B6\u614B\u306E\u78BA\u8A8D",
3390
+ `- \u30C7\u30B6\u30A4\u30F3PR\u304C\u5374\u4E0B\u3055\u308C\u305F\u5834\u5408: \u3053\u306EIssue\u3092\u30C7\u30B6\u30A4\u30F3\u306A\u3057\u3067\u9032\u3081\u308B\u304B\u3001\u30C7\u30B6\u30A4\u30F3\u3092\u3084\u308A\u76F4\u3059\u304B\u3092\u6C7A\u3081\u3066\u304F\u3060\u3055\u3044`,
3391
+ `- \u30C7\u30B6\u30A4\u30F3PR\u304C\u305D\u3082\u305D\u3082\u4F5C\u3089\u308C\u3066\u3044\u306A\u3044\u5834\u5408: \u30EA\u30E2\u30FC\u30C8\u30D6\u30E9\u30F3\u30C1 \`${designBranchName(issueNumber)}\` \u306E\u6709\u7121\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`,
3392
+ "",
3393
+ "## \u5BFE\u5FDC\u5F8C\u306E\u9032\u3081\u65B9",
3394
+ "- \u30C7\u30B6\u30A4\u30F3\u3092\u3084\u308A\u76F4\u3059\u5834\u5408: `cc-need-human-check` / `cc-ui-design-pr-created` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-create-ui-design` \u30E9\u30D9\u30EB\u3092\u4ED8\u3051\u76F4\u3057\u3066\u304F\u3060\u3055\u3044",
3395
+ "- \u30C7\u30B6\u30A4\u30F3\u306A\u3057\u3067\u5B9F\u88C5\u3078\u9032\u3081\u308B\u5834\u5408: `cc-need-human-check` / `cc-ui-design-pr-created` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-exec-issue` \u30E9\u30D9\u30EB\u306E\u307F\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044"
3396
+ ].join("\n");
3397
+ }
3398
+ function designPrNotCreatedComment(issueNumber) {
3399
+ return [
3400
+ "## \u30C7\u30B6\u30A4\u30F3PR\u672A\u4F5C\u6210\u306E\u307E\u307E\u81EA\u52D5\u5B9F\u884C\u304C\u7D42\u4E86\u3057\u307E\u3057\u305F\uFF08\u8981\u4EBA\u624B\u78BA\u8A8D\uFF09",
3401
+ `create-ui-design \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u6B63\u5E38\u7D42\u4E86\uFF08exit 0\uFF09\u3057\u307E\u3057\u305F\u304C\u3001\u30C7\u30B6\u30A4\u30F3\u30D6\u30E9\u30F3\u30C1 \`${designBranchName(issueNumber)}\` \u3092 head \u3068\u3059\u308BPR\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002PR\u4F5C\u6210\u524D\u306B\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u7D42\u4E86\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`,
3402
+ "",
3403
+ "## \u72B6\u614B\u306E\u78BA\u8A8D",
3404
+ `- \u5909\u66F4\u304C push \u6E08\u307F\u306E\u5834\u5408\u306F\u30EA\u30E2\u30FC\u30C8\u30D6\u30E9\u30F3\u30C1 \`${designBranchName(issueNumber)}\` \u304C\u6B8B\u3063\u3066\u3044\u307E\u3059\u3002\u5185\u5BB9\u3092\u78BA\u8A8D\u3057\u3001\u5FC5\u8981\u306A\u3089\u624B\u52D5\u3067\u30C7\u30B6\u30A4\u30F3PR\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044`,
3405
+ "",
3406
+ "## \u5BFE\u5FDC\u5F8C\u306E\u9032\u3081\u65B9",
3407
+ "- \u81EA\u52D5\u5B9F\u884C\u3092\u3084\u308A\u76F4\u3059\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-create-ui-design` \u30E9\u30D9\u30EB\u3092\u4ED8\u3051\u76F4\u3057\u3066\u304F\u3060\u3055\u3044",
3408
+ "- \u624B\u52D5\u3067\u30C7\u30B6\u30A4\u30F3PR\u3092\u4F5C\u6210\u3057\u305F\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-ui-design-pr-created` \u30E9\u30D9\u30EB\u3092\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044",
3409
+ "- \u30C7\u30B6\u30A4\u30F3\u3092\u8AE6\u3081\u3066\u305D\u306E\u307E\u307E\u5B9F\u88C5\u3078\u9032\u3081\u308B\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-exec-issue` \u30E9\u30D9\u30EB\u306E\u307F\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044"
3410
+ ].join("\n");
3411
+ }
3412
+ function designReferenceMissingComment(issueNumber) {
3413
+ return [
3414
+ "## \u30C7\u30B6\u30A4\u30F3\u53C2\u7167\u306E\u66F8\u304D\u623B\u3057\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\uFF08\u8981\u4EBA\u624B\u78BA\u8A8D\uFF09",
3415
+ `apply-ui-design \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u6B63\u5E38\u7D42\u4E86\uFF08exit 0\uFF09\u3057\u307E\u3057\u305F\u304C\u3001Issue #${issueNumber} \u306E description \u306B \`${DESIGN_REFERENCE_HEADING}\` \u30BB\u30AF\u30B7\u30E7\u30F3\uFF08\`.pen\` \u306E\u30D1\u30B9\u3092\u542B\u3080\uFF09\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002`,
3416
+ "",
3417
+ "## \u306A\u305C\u6B62\u3081\u3066\u3044\u308B\u304B",
3418
+ "- \u30C7\u30B6\u30A4\u30F3\u53C2\u7167\u304C description \u306B\u7121\u3044\u3068\u3001`exec-issue` \u306F\u30C7\u30B6\u30A4\u30F3\u3092\u5165\u529B\u306B\u3067\u304D\u305A\u3001\u5408\u610F\u6E08\u307F\u306E\u30C7\u30B6\u30A4\u30F3\u3068\u7121\u95A2\u4FC2\u306A\u5B9F\u88C5\u306B\u306A\u308A\u307E\u3059",
3419
+ "",
3420
+ "## \u5BFE\u5FDC\u5F8C\u306E\u9032\u3081\u65B9",
3421
+ "- \u81EA\u52D5\u5B9F\u884C\u3092\u3084\u308A\u76F4\u3059\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-ui-design-pr-created` \u30E9\u30D9\u30EB\u3092\u4ED8\u3051\u76F4\u3057\u3066\u304F\u3060\u3055\u3044",
3422
+ "- \u624B\u52D5\u3067 description \u306B\u53C2\u7167\u3092\u8FFD\u8A18\u3057\u305F\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-ui-design-ready` \u3068 `cc-exec-issue` \u30E9\u30D9\u30EB\u3092\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044"
3423
+ ].join("\n");
3424
+ }
3425
+
3426
+ // src/workers/create-ui-design.ts
3427
+ function designPrLabelingFailedComment(issueNumber, prNumber, error) {
3428
+ const message = error instanceof Error ? error.message : String(error);
3429
+ return [
3430
+ "## \u30C7\u30B6\u30A4\u30F3PR\u3078\u306E\u30E9\u30D9\u30EB\u4ED8\u4E0E\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08\u8981\u4EBA\u624B\u78BA\u8A8D\uFF09",
3431
+ `\u30C7\u30B6\u30A4\u30F3PR #${prNumber}\uFF08\u30D6\u30E9\u30F3\u30C1 \`${designBranchName(issueNumber)}\`\uFF09\u306F\u4F5C\u6210\u3055\u308C\u307E\u3057\u305F\u304C\u3001\u5F8C\u7D9A\u30E9\u30D9\u30EB\uFF08\`cc-ui-design\` / \`cc-triage-scope\` / \`cc-ui-design-pr-created\`\uFF09\u306E\u4ED8\u4E0E\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002`,
3432
+ "",
3433
+ "## \u8D77\u3053\u308A\u3046\u308B\u539F\u56E0",
3434
+ "- \u672C\u30EF\u30FC\u30AF\u30D5\u30ED\u30FC\u8FFD\u52A0\u6642\u306E\u30E9\u30D9\u30EB\u304C\u672A\u4F5C\u6210\u306E\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`claude-task-worker init` \u3092\u518D\u5B9F\u884C\u3057\u3066\u30E9\u30D9\u30EB\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044",
3435
+ "",
3436
+ "## \u30A8\u30E9\u30FC\u5185\u5BB9",
3437
+ "```",
3438
+ message,
3439
+ "```",
3440
+ "",
3441
+ "## \u5BFE\u5FDC\u5F8C\u306E\u9032\u3081\u65B9",
3442
+ "- \u30E9\u30D9\u30EB\u4F5C\u6210\u5F8C\u306B\u3084\u308A\u76F4\u3059\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-ui-design`\u30FB`cc-triage-scope`\uFF08PR\u5074\uFF09\u3068 `cc-ui-design-pr-created`\uFF08Issue\u5074\uFF09\u3092\u624B\u52D5\u3067\u4ED8\u3051\u3066\u304F\u3060\u3055\u3044"
3443
+ ].join("\n");
3444
+ }
3445
+ var createUiDesignWorker = async (opts = {}) => {
3446
+ if (!getUiDesignConfig().enabled) {
3447
+ console.log("[create-ui-design] uiDesign.enabled is false, skipping");
3448
+ return;
3449
+ }
3450
+ await createIssuePollingWorker({
3451
+ name: "create-ui-design",
3452
+ command: "/claude-task-worker:create-ui-design",
3453
+ triggerLabels: ["cc-create-ui-design"],
3454
+ excludeLabels: ["cc-ui-design-pr-created", "cc-ui-design-ready", "cc-exec-issue", "cc-pr-created"],
3455
+ epicFilters: opts.epicFilters,
3456
+ labelFilters: opts.labelFilters,
3457
+ onCompleted: async (issueNumber) => {
3458
+ if (await hasLabel("issue", issueNumber, "cc-need-human-check")) {
3459
+ console.log(`[create-ui-design] #${issueNumber}: cc-need-human-check present, skip cc-ui-design-pr-created`);
3460
+ return false;
3461
+ }
3462
+ if (await hasLabel("issue", issueNumber, "cc-ui-design-ready")) {
3463
+ console.log(`[create-ui-design] #${issueNumber}: design not needed (cc-ui-design-ready), completing`);
3464
+ return;
3465
+ }
3466
+ const branch = designBranchName(issueNumber);
3467
+ const prNumber = await findPrNumberByHeadRef(branch, "open");
3468
+ if (prNumber === null) {
3469
+ console.error(
3470
+ `[create-ui-design] #${issueNumber}: session exited without an open design PR (branch: ${branch}); marking cc-need-human-check`
3471
+ );
3472
+ await addLabel("issue", issueNumber, "cc-need-human-check");
3473
+ await commentOnIssue(issueNumber, designPrNotCreatedComment(issueNumber)).catch(
3474
+ (err) => console.error(`[create-ui-design] commentOnIssue failed for #${issueNumber}: ${err}`)
3475
+ );
3476
+ return false;
3477
+ }
3478
+ try {
3479
+ await addLabel("pr", prNumber, "cc-ui-design");
3480
+ await addLabel("pr", prNumber, "cc-triage-scope");
3481
+ await addLabel("issue", issueNumber, "cc-ui-design-pr-created");
3482
+ } catch (err) {
3483
+ console.error(`[create-ui-design] #${issueNumber}: labeling design PR #${prNumber} failed: ${err}`);
3484
+ await addLabel("issue", issueNumber, "cc-need-human-check");
3485
+ await commentOnIssue(issueNumber, designPrLabelingFailedComment(issueNumber, prNumber, err)).catch(
3486
+ (commentErr) => console.error(`[create-ui-design] commentOnIssue failed for #${issueNumber}: ${commentErr}`)
3487
+ );
3488
+ return false;
3489
+ }
3490
+ }
3491
+ })();
3492
+ };
3493
+
3494
+ // src/workers/apply-ui-design.ts
3495
+ import { existsSync as existsSync2 } from "node:fs";
3496
+ import { join as join6 } from "node:path";
3497
+ function designFileExistsInWorktree(worktreeId, designFilePath) {
3498
+ return existsSync2(join6(getWorktreePath(worktreeId), designFilePath));
3499
+ }
3500
+ async function markDesignReferenceMissing(issueNumber, logMessage) {
3501
+ console.error(logMessage);
3502
+ await addLabel("issue", issueNumber, "cc-need-human-check").catch(
3503
+ (err) => console.error(`[apply-ui-design] addLabel cc-need-human-check failed for #${issueNumber}: ${err}`)
3504
+ );
3505
+ await commentOnIssue(issueNumber, designReferenceMissingComment(issueNumber)).catch(
3506
+ (err) => console.error(`[apply-ui-design] commentOnIssue failed for #${issueNumber}: ${err}`)
3507
+ );
3508
+ }
3509
+ var applyUiDesignWorker = async (opts = {}) => {
3510
+ if (!getUiDesignConfig().enabled) {
3511
+ console.log("[apply-ui-design] uiDesign.enabled is false, skipping");
3512
+ return;
3513
+ }
3514
+ await createIssuePollingWorker({
3515
+ name: "apply-ui-design",
3516
+ command: "/claude-task-worker:apply-ui-design",
3517
+ triggerLabels: ["cc-ui-design-pr-created"],
3518
+ excludeLabels: ["cc-ui-design-ready", "cc-exec-issue"],
3519
+ epicFilters: opts.epicFilters,
3520
+ labelFilters: opts.labelFilters,
3521
+ preflight: async (issue) => {
3522
+ const branch = designBranchName(issue.number);
3523
+ let pr;
3524
+ try {
3525
+ pr = await findPrStateByHeadRef(branch);
3526
+ } catch (err) {
3527
+ console.error(`[apply-ui-design] #${issue.number}: findPrStateByHeadRef failed for ${branch}: ${err}`);
3528
+ return "skip";
3529
+ }
3530
+ const disposition = classifyDesignPr(pr);
3531
+ if (disposition === "proceed") return "proceed";
3532
+ if (disposition === "wait") {
3533
+ console.log(`[apply-ui-design] #${issue.number}: design PR #${pr?.number} is still open, waiting for merge`);
3534
+ return "skip";
3535
+ }
3536
+ console.error(`[apply-ui-design] #${issue.number}: design PR for ${branch} is missing or closed unmerged`);
3537
+ try {
3538
+ await addLabel("issue", issue.number, "cc-need-human-check");
3539
+ await commentOnIssue(issue.number, designPrMissingComment(issue.number)).catch(
3540
+ (err) => console.error(`[apply-ui-design] commentOnIssue failed for #${issue.number}: ${err}`)
3541
+ );
3542
+ } catch (err) {
3543
+ console.error(`[apply-ui-design] addLabel cc-need-human-check failed for #${issue.number}: ${err}`);
3544
+ }
3545
+ return "skip";
3546
+ },
3547
+ onCompleted: async (issueNumber, worktreeId) => {
3548
+ let body;
3549
+ try {
3550
+ body = await getIssueBody(issueNumber);
3551
+ } catch (err) {
3552
+ console.error(`[apply-ui-design] #${issueNumber}: getIssueBody failed: ${err}`);
3553
+ body = "";
3554
+ }
3555
+ const designFilePath = extractDesignFilePath(body);
3556
+ if (designFilePath === null) {
3557
+ await markDesignReferenceMissing(
3558
+ issueNumber,
3559
+ `[apply-ui-design] #${issueNumber}: session exited without a design reference section; marking cc-need-human-check`
3560
+ );
3561
+ return false;
3562
+ }
3563
+ if (!designFileExistsInWorktree(worktreeId, designFilePath)) {
3564
+ await markDesignReferenceMissing(
3565
+ issueNumber,
3566
+ `[apply-ui-design] #${issueNumber}: design reference points to ${designFilePath}, which does not exist in worktree ${worktreeId}; marking cc-need-human-check`
3567
+ );
3568
+ return false;
3569
+ }
3570
+ await addLabel("issue", issueNumber, "cc-exec-issue").catch(
3571
+ (err) => console.error(`[apply-ui-design] addLabel cc-exec-issue failed for #${issueNumber}: ${err}`)
3572
+ );
3573
+ await addLabel("issue", issueNumber, "cc-ui-design-ready").catch(
3574
+ (err) => console.error(`[apply-ui-design] addLabel cc-ui-design-ready failed for #${issueNumber}: ${err}`)
3575
+ );
3576
+ }
3577
+ })();
3578
+ };
3579
+
3159
3580
  // src/commands/init.ts
3160
3581
  import { mkdir as mkdir2, writeFile as writeFile2, access } from "node:fs/promises";
3161
3582
 
3162
3583
  // src/commands/codegraph.ts
3163
3584
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3164
- import { homedir as homedir4 } from "node:os";
3165
- import { dirname as dirname2, join as join5 } from "node:path";
3585
+ import { homedir as homedir5 } from "node:os";
3586
+ import { dirname as dirname2, join as join7 } from "node:path";
3166
3587
  async function loadRunCommand() {
3167
3588
  return await Promise.resolve().then(() => (init_run_command(), run_command_exports));
3168
3589
  }
3169
3590
  var CODEGRAPH_PACKAGE = "@colbymchenry/codegraph";
3170
3591
  var CODEGRAPH_IGNORE_ENTRY = ".codegraph/";
3171
- function globalGitIgnorePath(env = process.env, home = homedir4()) {
3592
+ function globalGitIgnorePath(env = process.env, home = homedir5()) {
3172
3593
  const xdg = env.XDG_CONFIG_HOME?.trim();
3173
- const base = xdg ? xdg : join5(home, ".config");
3174
- return join5(base, "git", "ignore");
3594
+ const base = xdg ? xdg : join7(home, ".config");
3595
+ return join7(base, "git", "ignore");
3175
3596
  }
3176
3597
  function appendIgnoreEntry(current, entry) {
3177
3598
  const bare = entry.replace(/\/$/, "");
@@ -3258,7 +3679,11 @@ var LABELS = [
3258
3679
  { name: "cc-triage-scope", color: "c5def5" },
3259
3680
  { name: "cc-resolve-conflict", color: "fbca04" },
3260
3681
  { name: "cc-epic-issue", color: "8b5cf6" },
3261
- { name: "cc-release-ready", color: "2da44e" }
3682
+ { name: "cc-release-ready", color: "2da44e" },
3683
+ { name: "cc-create-ui-design", color: "ff8ad8" },
3684
+ { name: "cc-ui-design-pr-created", color: "d4a5e8" },
3685
+ { name: "cc-ui-design-ready", color: "bfd4f2" },
3686
+ { name: "cc-ui-design", color: "ec4899" }
3262
3687
  ];
3263
3688
  var ISSUE_TEMPLATE = `name: "[claude-task-worker] Issue\u4F5C\u6210\u4F9D\u983C"
3264
3689
  description: claude-task-worker\u3067GitHub Issue\u3092\u4F5C\u6210\u3059\u308B
@@ -3445,14 +3870,14 @@ async function update() {
3445
3870
  }
3446
3871
 
3447
3872
  // src/commands/version.ts
3448
- import { readFileSync as readFileSync4 } from "node:fs";
3449
- import { dirname as dirname3, join as join6 } from "node:path";
3873
+ import { readFileSync as readFileSync5 } from "node:fs";
3874
+ import { dirname as dirname3, join as join8 } from "node:path";
3450
3875
  import { fileURLToPath } from "node:url";
3451
3876
  function version() {
3452
3877
  const here = dirname3(fileURLToPath(import.meta.url));
3453
- const pkgPath = join6(here, "..", "package.json");
3878
+ const pkgPath = join8(here, "..", "package.json");
3454
3879
  try {
3455
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
3880
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
3456
3881
  console.log(pkg.version ?? "unknown");
3457
3882
  } catch (err) {
3458
3883
  console.error(`[version] Failed to read version: ${err.message}`);
@@ -3514,7 +3939,9 @@ var WORKERS = {
3514
3939
  "triage-pr": triagePrWorker,
3515
3940
  "resolve-conflict": resolveConflictWorker,
3516
3941
  "check-dependabot": checkDependabotWorker,
3517
- "epic-issue": epicIssueWorker
3942
+ "epic-issue": epicIssueWorker,
3943
+ "create-ui-design": createUiDesignWorker,
3944
+ "apply-ui-design": applyUiDesignWorker
3518
3945
  };
3519
3946
  function printUsage() {
3520
3947
  console.log(`Usage: claude-task-worker <command> [--project <name>] [--epic <issue-number>] [--label <label-name>]
@@ -3537,6 +3964,8 @@ Workers:
3537
3964
  resolve-conflict Poll cc-resolve-conflict PRs and run /resolve-conflict
3538
3965
  check-dependabot Poll dependabot PRs every 1 hour
3539
3966
  epic-issue Poll cc-epic-issue issues and create epic PR when all sub-issues are closed
3967
+ create-ui-design Poll cc-create-ui-design issues and create a Pencil design PR (requires uiDesign.enabled)
3968
+ apply-ui-design Poll cc-ui-design-pr-created issues and write the design reference back once the design PR is merged (requires uiDesign.enabled)
3540
3969
  all Poll all workers except triage-created-issue, triage-pr, check-dependabot
3541
3970
  yolo Poll all workers including triage-created-issue, triage-pr, check-dependabot
3542
3971
 
@@ -3731,7 +4160,9 @@ if (hasProjectFilter()) {
3731
4160
  updateIssueWorker({ epicFilters, labelFilters }),
3732
4161
  answerIssueQuestionsWorker({ epicFilters, labelFilters }),
3733
4162
  resolveConflictWorker(),
3734
- epicIssueWorker({ epicFilters, labelFilters })
4163
+ epicIssueWorker({ epicFilters, labelFilters }),
4164
+ createUiDesignWorker({ epicFilters, labelFilters }),
4165
+ applyUiDesignWorker({ epicFilters, labelFilters })
3735
4166
  ]);
3736
4167
  })();
3737
4168
  } else if (workerType === "yolo") {
@@ -3750,7 +4181,9 @@ if (hasProjectFilter()) {
3750
4181
  checkDependabotWorker(),
3751
4182
  triagePrWorker(),
3752
4183
  resolveConflictWorker(),
3753
- epicIssueWorker({ epicFilters, labelFilters })
4184
+ epicIssueWorker({ epicFilters, labelFilters }),
4185
+ createUiDesignWorker({ epicFilters, labelFilters }),
4186
+ applyUiDesignWorker({ epicFilters, labelFilters })
3754
4187
  ]);
3755
4188
  })();
3756
4189
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.33.1",
3
+ "version": "0.35.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",