Plan Canvas
計画とビジュアルアーティファクトのためのレビューループ:あなたがアーティファクトを作成し、人間がブラウザでレビューします — 意図した要素を正確に注釈し、チャットし、計画承認/変更要求の判定を下します — その間、あなたは単一のCLI呼び出しでブロックし、フィードバックをJSONとして受け取ります。
lavish-axiに触発され、/plan確認ゲートを中心にECCネイティブとして再構築され、依存関係ゼロです。
使用するタイミング
- 計画アーティファクト(
/planからの.claude/plans/*.plan.md)を作成した直後で、CONFIRM/承認の判断が必要な場合 — キャンバスの判定がタイプ入力の「はい/続行」に取って代わります。 - ユーザーが変更すべき場所を指し示す必要がある場合:デザイン、比較、レポート、またはローカルの
.md/.htmlアーティファクトのレビュー。 - ユーザーが
/plan-canvas、ビジュアルレビュー、または「ブラウザで開いて」と要求した場合。
次の場合には使用しないでください:diffのコードレビュー(/code-review)、Webアプリの実行、リモートURL。キャンバスはローカルアーティファクトファイルのみを提供します。
仕組み
CLIをecc-plan-canvasとして起動します — ecc-universalパッケージによって提供されるバイナリです(グローバルまたはプラグインインストール後、PATHが通ります。プラグインインストールの場合はnode "$CLAUDE_PLUGIN_ROOT/scripts/plan-canvas.js"も使用できます)。レビュー対象のプロジェクトから実行します。任意のワーキングディレクトリから動作します。全セッションで共有されるループバックサーバー(127.0.0.1:4517)を管理し、アーティファクトパスでキー設定されます — セッションIDを追跡する必要はありません。
ワークフローはプレーンなCLI+JSONループであるため、モデルやハーネスに依存しません:シェルコマンドを実行してstdoutを読み取れる任意のエージェントが同じようにドライブできます(Claude Code、Codex、Cursor、Gemini、OpenCode、Copilot)。ハーネスがスキルを表面化する方法に応じてトリガーします — 例えばClaude Codeでは/plan-canvas、Codexでは$plan-canvas — または直接ecc-plan-canvasコマンドを実行します。
# 1. アーティファクトをユーザーのブラウザで開く(すぐに戻る)
ecc-plan-canvas open .claude/plans/feature.plan.md
# 2. 人間が応答するまでブロックする。実行したままにしておく。中断された場合は再実行:キューに入れられたフィードバックは決して失われない。
ecc-plan-canvas await .claude/plans/feature.plan.md
待機を続ける、さもないと人間は誰もいない場所に話しかけることになる
フィードバックがあなたに届くのは、awaitが実際にセッションに待機している間だけです。あなたのターンが何も待機せずに終了すると、メッセージはキューに残り、人間側から見ると送信しても何も起こっていないように見えます。
そのため、ハーネスがバックグラウンドタスクをサポートしている場合は、awaitをバックグラウンドタスクとして実行してください(Claude Codeでは、run_in_background: trueを指定したBash呼び出し)。フィードバックが到着するとすぐに終了し、ハーネスがJSONをあなたに渡すため、フォアグラウンド呼び出しのようにタイムアウトで死ぬことなく、ターンをまたいでループを維持できます。フォアグラウンドのawaitも機能しますが、ハーネスが時間制限するまでしか機能しません。
2つのバックストップが存在しますが、どちらも上記をスキップする言い訳にはなりません:
ecc-plan-canvas pendingは、リスナーなしでキューに入れられたフィードバックをリストします。何か見逃したか不明な場合はいつでも確認してください。stop:plan-canvas-pendingフックは、キャンバスのフィードバックが未配信の間、あなたのターンの終了をブロックし、メッセージを渡します。そのフックからフィードバックを読んでいる場合、あなたはリスニングを早く停止しすぎています。
awaitは、人間がアクションを起こしたときにJSONを出力します:
{
"status": "feedback",
"items": [
{ "kind": "annotation", "text": "これを2つのフェーズに分割してください",
"anchor": { "selector": "h2:nth-of-type(3)", "tag": "h2", "snippet": "フェーズ2: 移行" } },
{ "kind": "verdict", "verdict": "request-changes" }
]
}
kind: "chat"— 自由形式のメッセージ。ターミナルではなくキャンバス内で回答してください。kind: "annotation"— 要素に固定されたフィードバック(anchor.selector、anchor.snippetはユーザーが指し示したものを示します。ユーザーが一節をハイライトした場合はanchor.textRange.text)。kind: "verdict"—approveは計画がCONFIRMEDされたことを意味します:ポーリングを停止し、セッションを終了し、実装を開始します。request-changesはアーティファクトを修正することを意味します(キャンバスはライブリロードします)そしてループを続けます。
3. 常にキャンバス内で応答し、その後もリスニングを続けてください。1つのコマンドで両方を実行できます:
ecc-plan-canvas await <file> --reply "要求どおりフェーズ2を分割しました。ご確認ください。"
すべての人間のメッセージにはキャンバス内で返信してください。「リスクテーブルを書き直しています」のような一言でも構いません。チャットパネルでの沈黙は、キャンバスが壊れていることと区別がつきません。これはまさにこのループが防止しようとしている問題です。ターミナルだけでなく、そちらで回答してください。
作業中は、アクティビティインジケーターでチャットの誠実さを保ってください:
# アニメーション付きの「エージェントが考えています...」バブル。長時間の作業中は更新してください
ecc-plan-canvas typing <file> --state thinking
# 返信が届く直前に「エージェントが入力中...」に切り替える
ecc-plan-canvas typing <file> --state typing
awaitは、フィードバックのバッチを渡す瞬間にthinkingを設定し、--replyはそれをクリアします。両方の状態は自動的に期限切れになるため、クラッシュしたエージェントは、人間を永遠にドットを見つめさせる代わりに、誠実な「キューに入れられました」状態になります。修正に1分以上かかる場合は、thinkingを更新してください。
4. レビューが終了したら 終了してください:ecc-plan-canvas end <file>。
図(Mermaid)
計画の一部がフロー、アーキテクチャ、シーケンス、ステートマシン、ERモデル、または依存関係グラフである場合、ASCIIアートや文章の壁の代わりに、フェンスで囲まれたmermaidブロックとして作成してください — キャンバスはそれをテーマ付きの図としてレンダリングし、人間が指し示すことができます。1段落よりも図の方が速く読める場合に使用してください。シンプルなリストやテーブルにはスキップしてください。
```mermaid
flowchart LR
A[市場が確定] --> B{ウォッチャーはいるか?}
B -->|はい| C[ジョブをエンキュー] --> D[ファンアウトワーカー]
```
図はECCダークテーマでアクセントパレットとともにレンダリングされます。Mermaidは固定CDNからブラウザに読み込まれます。それが利用できない場合(オフライン)、ブロックはソースを表示するように劣化するため、レビューがブロックされることはありません。エアギャップ環境での使用のために、ECC_PLAN_CANVAS_MERMAID_URLでローカルミラーを指定してください。
ルール
- MarkdownアーティファクトはECCの計画テンプレート(Mermaidブロックを含む)でレンダリングされます。
.htmlアーティファクトはそのままレンダリングされ、注釈レイヤーが注入されます。HTML作成のガイダンスについては、frontend-design-directionおよびartifact-designスキルを使用してください。 - 修正するにはアーティファクトファイルを編集してください — キャンバスは保存時にライブリロードします。更新のために
openを再実行しないでください。 {"status": "ended", "endedBy": "user"}(またはフィードバックバッチのsessionEnded: true)は、ユーザーがレビューを閉じたことを意味します:ポーリングを停止し、残りの更新をチャットで配信し、再開しないでください。そのセッションに対する通常のopenは拒否されます。ユーザーが再開を要求した場合のみ--reopenを渡してください。- 関連するアセット(画像、CSS)はアーティファクトと同じ場所に配置し、相対パスで参照する必要があります。
- サーバーはループバック専用で、30分間アイドル状態が続くと終了します(
ECC_PLAN_CANVAS_IDLE_MS)。stopで明示的にシャットダウンします。状態は~/.claude/plan-canvas/(ECC_PLAN_CANVAS_STATE_DIR)に保存されます。
例
計画承認フロー — /planが.claude/plans/notifications.plan.mdを作成し、確認のために待機する必要があります:
ecc-plan-canvas open .claude/plans/notifications.plan.md
ecc-plan-canvas await .claude/plans/notifications.plan.md
# → {"status":"feedback","items":[{"kind":"verdict","verdict":"approve"}]}
ecc-plan-canvas end .claude/plans/notifications.plan.md
# 計画が確認されました — 実装を開始
修正ループ — フィードバックが届き、ファイルを編集し、返信し、リスニングを続けます:
# awaitが注釈を返す → .plan.mdを編集する(キャンバスはライブリロード)
ecc-plan-canvas await <file> --reply "リスクテーブルを作り直しました。"
# → 次の応答まで再びブロックする
アンチパターン
- ループ内で
--timeout-msを使用したポーリング。これはテスト用に存在します。代わりにプレーンなawaitを実行したままにしてください。 - レビューがまだ開いている間に、リスニングしている
awaitなしでターンを終了すること。それが人間が「メッセージを送ったのに何も起こらなかった」と感じる唯一の失敗です。 - フィードバックを読んだが、ターミナルでのみ回答すること。人間はキャンバスを見ています。
- ユーザーが終了した後に「ちょっと見せるため」に再開すること。
- 計画全体をチャットに貼り付け、かつキャンバスを開くこと — キャンバスを選び、ターミナルの要約は1行に抑えてください。
- 状態ファイルからキャンバスチャットを解析すること — 必要なものはすべて
awaitを介して届きます。
デザインノートと起源: docs/design/plan-canvas.md