😺

Claude Codeを使うなら、タスクは SubAgent にをやらせよう

に公開

TimelabLynx というカレンダーサービスを開発している takahashi(@stak_22)です。

Lynx 開発チームでは Claude Code に一点集中してナレッジを深めています。
今回は、SubAgent機能を利用して、エージェントにタスクを実施させるということをしてみました。

何ができるようになるの?

一言で言うと、コンテキストを一貫して保ったまま、最終アウトプットを出してくれます!
SubAgentは独立したコンテキストウィンドウを持つエージェントなので、普通にプロンプトで指示する場合と比べてコンテキストを圧縮する必要がないので最後までやり切ってくれます。

SubAgentとは?

公式ドキュメントによると、サブエージェントは以下のように説明されています。

サブエージェントは、Claude Codeがタスクを委任できる事前設定されたAIパーソナリティです。各サブエージェントは:

  • 特定の目的と専門分野を持つ
  • メインの会話とは独立した独自のコンテキストウィンドウを使用する
  • 使用を許可された特定のツールで設定できる
  • その動作を導くカスタムシステムプロンプトを含む

Claude Codeがサブエージェントの専門分野に一致するタスクに遭遇すると、その専門的なサブエージェントにタスクを委任でき、サブエージェントは独立して作業し結果を返します。

つまり、専門家みたいな感じです。
どういうことができる専門家かを定義し、それを呼び出すことで機能してくれます。

メリット

この記述においては特に箇条書き二つ目の「メインの会話とは独立した独自のコンテキストウィンドウを使用する」が最もSubAgentを使うメリットなのかなと思います。
コンテキストが途中で圧縮されることがないので、最後までやり切ってくれます。

やり方次第ではSubAgentを使わずとも再現可能かと思います。例えば、まずはタスクに対してやることチェックリストを洗い出させてマークダウンファイルを出力させ、そうすれば別のコンテキストを失ってもそれに従って作業させることができる、みたいなやり方です。だったら提供されているSubAgentの方が勝手にやってくれるので良いかもしれないです。

(補足)

Claude Code が普通のプロンプトから判断してサブエージェントを起動してくれるかはわからないので、今回はそういった文脈ではなく、サブエージェントを自ら起動させる(プロンプト上で「このサブエージェントを起動して」と直接呼び出させる)という使い方を前提とした内容になっています。

どういうことかというと、「品質チェックエージェント」のようなサブエージェントを用意します。これはtestやlintが落ちているかを確認して直してくれる専門家です。しかし、普通にプロンプトからコードを修正させていても、これが起動されるかは分からない(うまく起動されるようなサブエージェントにするためのチューニングをする必要がある)のです。

どうやって設定するの?

設定自体のやり方は、公式ドキュメントのクイックスタートの通りです。

今回は特定のタスクを実行させるというのが目的なので、その文脈でのサブエージェントの.mdの書き方を紹介します。
とはいっても、もちろんタスクの内容次第でサブエージェントのマークダウンの書き方は異なります。
例として、実際に Lynx の開発にて不要になったカラムを削除するというタスクを実行させるためのサ
ブエージェントを紹介します。(Lynx開発における実際の修正タスクなのでこのマークダウン自体は転用しても無意味なものです…あくまで参考程度に…)

もちろん、このサブエージェントは Claude Code に書かせました。
何を最終的に達成させたいのか、何を考慮する必要があるのか、抜け漏れを許さない点はあるか、などを箇条書きでもいいので書き出してみて、それを「以下に考慮してサブエージェントとしてマークダウンファイルに書いて」とつけて指示すれば、生成してくれます。それを後で確認して必要ならテコ入れすれば大丈夫だと思います。

サブエージェント例 `.claude/agents/remove-is-connected.md`
---
name: remove-is-connected
description: CalendarResourceからis_connectedカラムを削除し、関連する全てのコードとテストを修正する
tools: Bash, Read, Edit, MultiEdit, Glob, Grep
---

# Remove is_connected Column Agent

CalendarResourceモデルから`is_connected`カラムを削除し、それに伴うバックエンド、フロントエンド、テストコードの修正を自動的に実行するエージェントです。

## 前提条件

- マイグレーションは既に実行済み(`is_connected`カラムは既にDBから削除されている)
- `db/migrate/20251002071209_remove_is_connected_from_calendar_resources.rb`が存在する

## 主な責務

1. **バックエンドコードの修正**
   - モデルのバリデーション削除
   - Serializerの属性削除
   - Command/Queryでの`is_connected`設定削除
   - `Authorization#connected?`メソッドの修正または削除

2. **フロントエンドコードの修正**
   - TypeScript型定義の修正
   - コンポーネントのフィルタリング条件修正

3. **テストコードの修正**
   - 関連するテストケースの削除または修正
   - 期待値の修正

## 実行タスク

### Phase 1: 影響範囲の最終確認

#### Authorization#connected?メソッドの使用箇所を調査
```bash
# connected?メソッドの使用箇所を検索
grep -r "\.connected?" --include="*.rb" app/
grep -r "\.connected?" --include="*.rb" spec/
```

使用箇所が見つかった場合は、それらも修正対象に含める。

---

### Phase 2: バックエンドコードの修正(7ファイル)

#### 2-1. app/models/calendar_resource.rb
**修正内容:** `is_connected`のバリデーションを削除

```ruby
# 削除する行
validates :is_connected, inclusion: { in: [true, false] }
```

#### 2-2. app/models/authorization.rb
**修正内容:** `connected?`メソッドを削除

```ruby
# 削除するメソッド(73-78行目あたり)
def connected?(resource_uid:)
  resource = calendar_resources.find_by(resource_uid:)

  !!resource&.is_connected
end
```

**注意:** Phase 1で他に使用箇所が見つかった場合は、代替実装を検討する。

#### 2-3. app/serializers/calendar_resource_serializer.rb
**修正内容:** attributes から `is_connected` を削除

```ruby
# 修正前
attributes :id, :calendar_name, :description, :display_color, :etag, :is_connected, :is_primary, ...

# 修正後
attributes :id, :calendar_name, :description, :display_color, :etag, :is_primary, ...
```

#### 2-4. app/domains/authorizations/commands/register_google_authorization.rb
**修正内容:** `is_connected: true` の行を削除

```ruby
# 削除する行(103行目あたり)
is_connected: true,
```

#### 2-5. app/domains/authorizations/commands/register_microsoft_authorization.rb
**修正内容:** `is_connected: true` の行を削除

```ruby
# 削除する行(117行目あたり)
is_connected: true,
```

#### 2-6. app/controllers/authorizations_controller.rb
**修正内容:** `is_connected: true` の行を削除

```ruby
# 削除する行(128行目あたり)
is_connected: true,
```

#### 2-7. app/domains/calendar/queries/list_resource_events.rb
**修正内容:** 一時Struct定義から `is_connected` を削除

```ruby
# 修正前(110行目あたり)
temporary_resource_struct = Struct.new(
  :id, :resource_uid, :display_name, :display_color, :text_color,
  :provider, :calendar_name, :summary, :description, :time_zone,
  :is_primary, :is_connected, :etag, :provider_data, :authorization_id,
  :authorization, :user
)

# 修正後
temporary_resource_struct = Struct.new(
  :id, :resource_uid, :display_name, :display_color, :text_color,
  :provider, :calendar_name, :summary, :description, :time_zone,
  :is_primary, :etag, :provider_data, :authorization_id,
  :authorization, :user
)
```

また、Structの初期化部分も修正(147行目あたり):
```ruby
# 修正前
temporary_resource_struct.new(
  temp_id, # id
  resource_uid, # resource_uid
  ERB::Util.html_escape(resource_uid.split('@').first), # display_name
  display_color, # display_color
  text_color, # text_color
  provider,
  ERB::Util.html_escape(resource_uid.split('@').first), # calendar_name
  '', # summary
  '', # description
  'Asia/Tokyo', # time_zone
  false, # is_primary
  false, # is_connected
  nil, # etag
  {}, # provider_data
  nil, # authorization_id
  nil, # authorization
  user # user
)

# 修正後
temporary_resource_struct.new(
  temp_id, # id
  resource_uid, # resource_uid
  ERB::Util.html_escape(resource_uid.split('@').first), # display_name
  display_color, # display_color
  text_color, # text_color
  provider,
  ERB::Util.html_escape(resource_uid.split('@').first), # calendar_name
  '', # summary
  '', # description
  'Asia/Tokyo', # time_zone
  false, # is_primary
  nil, # etag
  {}, # provider_data
  nil, # authorization_id
  nil, # authorization
  user # user
)
```

---

### Phase 3: フロントエンドコードの修正(2ファイル)

#### 3-1. front/src/domains/CalendarResource.ts
**修正内容:** `isConnected` プロパティを型定義から削除

```typescript
// 修正前(13行目)
isConnected: boolean;

// この行を削除する
```

#### 3-2. front/src/pages/calendar/ui/sidemenu/PrivateMenu.vue
**修正内容:** フィルタリング条件から `r.isConnected &&` を削除

```typescript
// 修正前(405行目あたり)
authorization.calendarResources.filter((r) => r.isConnected && !r.isPrimary)

// 修正後
authorization.calendarResources.filter((r) => !r.isPrimary)
```

---

### Phase 4: テストコードの修正(4ファイル)

#### 4-1. spec/models/calendar_resource_spec.rb
**修正内容:** `is_connected`のバリデーションテストを削除

```ruby
# 削除するテストケース(51-55行目あたり)
it 'is invalid without is_connected' do
  calendar_resource.is_connected = nil
  expect(calendar_resource).to be_invalid
end
```

#### 4-2. spec/models/authorization_spec.rb
**修正内容:** `connected?`メソッドのテストを削除

```ruby
# 削除するコンテキスト全体(143-161行目あたり)
context '#connected?' do
  let(:resource_uid) { 'test@example.com' }
  let!(:calendar_resource) { create(:calendar_resource, authorization:, resource_uid:, is_connected: true) }

  it 'returns true when calendar resource is connected' do
    expect(authorization.connected?(resource_uid:)).to be true
  end

  it 'returns false when calendar resource is not connected' do
    calendar_resource.is_connected = false
    calendar_resource.save

    expect(authorization.connected?(resource_uid:)).to be false
  end

  it 'returns false when calendar resource is not found' do
    expect(authorization.connected?(resource_uid: 'not_found')).to be false
  end
end
```

#### 4-3. spec/domains/authorizations/commands/register_google_authorization_spec.rb
**修正内容:** 期待値から `is_connected` を削除

```ruby
# 修正箇所を特定して is_connected: true の行を削除
# 例(28-32行目、47-51行目あたり)
```

ファイルを読んで該当箇所を特定し、期待値から削除する。

#### 4-4. spec/domains/sync/entities/google_calendar_resource_spec.rb
**修正内容:** テストデータ作成時の `is_connected` パラメータを削除

```ruby
# 修正前(191行目あたり)
create(:calendar_resource,
  authorization:,
  resource_uid: 'validation-test-calendar-id',
  is_connected: true)

# 修正後
create(:calendar_resource,
  authorization:,
  resource_uid: 'validation-test-calendar-id')
```

---

### Phase 5: 修正の検証

#### 5-1. Rubocopチェック
```bash
# 修正したRubyファイルに対してRubocopを実行
docker compose exec web bundle exec rubocop app/models/calendar_resource.rb
docker compose exec web bundle exec rubocop app/models/authorization.rb
docker compose exec web bundle exec rubocop app/serializers/calendar_resource_serializer.rb
docker compose exec web bundle exec rubocop app/domains/authorizations/commands/register_google_authorization.rb
docker compose exec web bundle exec rubocop app/domains/authorizations/commands/register_microsoft_authorization.rb
docker compose exec web bundle exec rubocop app/controllers/authorizations_controller.rb
docker compose exec web bundle exec rubocop app/domains/calendar/queries/list_resource_events.rb
```

#### 5-2. RSpecテスト実行
```bash
# 修正したテストファイルを実行
docker compose exec web bundle exec rspec spec/models/calendar_resource_spec.rb
docker compose exec web bundle exec rspec spec/models/authorization_spec.rb
docker compose exec web bundle exec rspec spec/domains/authorizations/commands/register_google_authorization_spec.rb
docker compose exec web bundle exec rspec spec/domains/sync/entities/google_calendar_resource_spec.rb
```

#### 5-3. フロントエンド型チェック
```bash
# TypeScriptの型チェック
( cd front && pnpm type-check )
```

#### 5-4. フロントエンドビルド確認
```bash
# ビルドが通ることを確認
( cd front && pnpm build )
```

---

## 成功条件

以下の条件が全て満たされた場合、タスク完了とする:

1. ✅ バックエンド7ファイルの修正が完了
2. ✅ フロントエンド2ファイルの修正が完了
3. ✅ テスト4ファイルの修正が完了
4. ✅ Rubocopチェックが全て通る
5. ✅ RSpecテストが全て通る
6. ✅ TypeScript型チェックが通る
7. ✅ フロントエンドのビルドが通る

## 注意事項

- 各Phaseは順序通りに実行すること
- エラーが発生した場合は、そのPhaseを完了させてから次に進む
- `Authorization#connected?`メソッドが他の箇所で使用されている場合は、それらも修正する
- テスト実行時にタイムアウトする場合は、ファイルを指定して実行する
- 修正後は必ずgit statusで変更内容を確認する

## トラブルシューティング

### Rubocopエラーが出る場合
```bash
# 自動修正を試す
docker compose exec web bundle exec rubocop -a <file_path>
```

### テストが失敗する場合
- エラーメッセージを確認し、`is_connected`関連の残存参照がないかチェック
- Factoryの定義も確認(spec/factories/calendar_resources.rb)

### TypeScriptエラーが出る場合
- フロントエンドの型定義が正しく更新されているか確認
- `pnpm api:build`を実行してAPI型を再生成

## 実行後の確認

```bash
# 変更されたファイルの確認
git status

# 変更内容の確認
git diff
```

全ての修正が完了したら、コミットメッセージの例:
```
is_connectedカラムを削除

- CalendarResourceモデルからis_connectedカラムとバリデーションを削除
- Authorization#connected?メソッドを削除
- 関連するCommand/Query/Controllerからis_connected設定を削除
- フロントエンドの型定義とフィルタリング条件を修正
- 関連するテストケースを削除・修正
```

これができたら、「サブエージェント remove-is-connected を起動して」みたいな感じで支持するだけで作業してくれます。

push するときにはもう SubAgent のマークダウンは不要なので、削除して差分だけ push すればいいと思います👍

感想

SubAgent機能は最近のものでもないですが、ベストプラクティス的なものはない(あってもまだいい感じに使いこなせていない)ですし、まだ手探りで色々やってみています。もちろんそれはチームによって良し悪しが分かれると思うので、とにかく試してみるのが良いです。
今回の使い方は一ついいかなと思った使い方なので、もし興味がある方はぜひこのような使い方でも試してみていただけますと幸いです。

Timelabテックブログ

Discussion