はじめに
pipe というツールも並行で開発しているのだが、こちらは当初gemini-cliをターゲットにしたものの、軸足が完全にAPIに移ってしまっていた。
一方で仕事ではAPIを使うことができないという事情があり、pipeでClaude Codeをラップして使っていたが、開発を始めてすぐにClaude CodeにはSkillsという仕組み実装されてしまった。。
.claude/skills/ にルールを書いておけば、エージェントが関連ファイルを触ったときに自動でコンテキストが注入される。これを再利用しない手はない。
ただ、実際に使ってみるといくつか不便な点があった:
-
ヘッドレスモードではSkillsが使えない。インタラクティブセッションでは自動注入されるのに、
-pだと完全に無視される - 権限チェックで詰まる。ヘッドレスモードでは権限確認のUIがないため、ツール呼び出しが静かにブロックされる
- セッションの管理がUUID頼り。再開するたびにUUIDをメモしておく必要があり、「あのimplementerセッションを再開したい」が一発でできない
- rewindが使いにくい。過去の会話分岐点まで戻る機能はあるが、どの時点に戻るか把握するのが難しい
「Claude Codeのセッション管理自体はClaude Codeに任せて、その上に薄いラッパーを乗せる形でpipeに近いことをしたい」という方針で作ったのが perclst だ。
perclstとは
persist + claude = perclst(「persist」の「si」を「cl」で置き換えた造語、発音は "persist" と同じ)
CLIからClaude Codeのサブエージェントを起動・管理するツールだ。APIキーは不要で、Claude Codeの認証をそのまま使う。
できること
Skillsをヘッドレスモードで使う
インタラクティブセッション限定だったSkillsを、PreToolUse フックで復元している。エージェントがファイルを触るたびに関連Skillの内容を additionalContext として注入するため、ヘッドレスでも同じルールが効く。
ただし、Claude CodeならSkillをロードしないケースでもファイルPathが合うと強制的に差し込んでしまうのが今後の課題となっている。
perclst start "feature Xを実装して"
perclst start "feature Xを実装して" --name feature-x
perclst start "task" --procedure conductor
セッションに名前をつけて管理
UUIDではなく人が読める名前でセッションを管理できる。
# セッション開始時に名前を付ける
perclst start "task" --name "implementer" --model opus
# セッション一覧(名前が表示される)
perclst list
# 後から名前をつける
perclst rename <session-id> "hotfix-session"
# 再開はセッションIDで(listで確認してから)
perclst resume <session-id> "続きをやって"
権限プロンプトをヘッドレスで受け取る
ヘッドレスモードでの権限確認を /dev/tty 経由でターミナルに転送する。全許可せずとも、ツール呼び出しのたびに [y/N] で個別に承認・拒否できる。
rewindを使いやすく
過去の会話分岐点まで戻る rewind を、UUIDではなくインデックス番号で操作できる。
# どこに戻れるか確認(0が最新)
perclst rewind --list <session-id>
# 2ターン前に戻る
perclst rewind <session-id> 2
# そこから再開
perclst resume <new-session-id> "別のアプローチを試して"
セッション分析
会話のターン数、ツール使用状況、トークン統計を確認できる。
perclst analyze <session-id>
perclst analyze <session-id> --print-detail # 全ターン内容を表示
perclst analyze <session-id> --format json
パイプライン実行
複数エージェントのワークフローをJSONで定義して実行できる。同じ名前のタスクは既存セッションを再開するため、2回目以降の実行は前回の続きから始まる。以下のサンプルは[こちらのPR]のテストを生成させた実際のサンプルだ。
src/domains/checker.test.ts が__tests__に入っていないという問題はあるので、今後解決していきたい。
{
"tasks": [
{
"type": "agent",
"name": "unit-test-domains-analyze",
"task": "target_file_path: src/domains/analyze.ts",
"procedure": "test-unit",
"allowed_tools": ["Read", "Write", "Bash", "mcp__perclst__ts_test_strategist", "mcp__perclst__ts_checker"]
},
{
"type": "agent",
"name": "unit-test-domains-checker",
"task": "target_file_path: src/domains/checker.ts",
"procedure": "test-unit",
"allowed_tools": ["Read", "Write", "Bash", "mcp__perclst__ts_test_strategist", "mcp__perclst__ts_checker"]
},
{
"type": "agent",
"name": "unit-test-domains-import",
"task": "target_file_path: src/domains/import.ts",
"procedure": "test-unit",
"allowed_tools": ["Read", "Write", "Bash", "mcp__perclst__ts_test_strategist", "mcp__perclst__ts_checker"]
}
]
}
npm test が失敗すると、テスト出力をフィードバックとして implementer セッションに戻し、最大2回まで自動リトライする。
perclst run pipeline.json
一括削除(sweep)
条件を指定してセッションをまとめて削除できる。
# 名前なしセッションを一覧表示(削除はしない)
perclst sweep --anon-only --dry-run
# 3月末までの完了済みセッションを削除
perclst sweep --from 2025-01-01 --to 2025-03-31 --status completed
Docker的な整理
perclst(&pipe)で実現したいことはDockerの実行モデルに対応させて考えると分かりやすい:
| Docker | perclst |
|---|---|
| ベースイメージ | モデル(能力レベル) |
| イメージ内のコマンド | Skills(コーディング規約、制約のHow) |
ENTRYPOINT |
Procedure(エージェントが従う手順のWhat) |
CMD |
プロンプト(各実行時の指示) |
| 実行中コンテナ | セッション(モデル+Skills+Procedureの実体) |