## 概要 - [[cmux]]から[[herdr]]に移行した - planファイルを[[herdr]]の新しいペインで開きたい - `plan` コマンドを作成することにした ## 前提 以下がインストールされている。 - [[herdr]] - [[leaf]] ### 環境 | 対象 | バージョン | | ----------- | ------ | | [[macOS]] | 26.5.1 | | [[Ghostty]] | 1.3.1 | | [[herdr]] | 0.7.1 | | [[leaf]] | 1.25.0 | ## プラン > [!code]- > ````markdown > # planコマンドの実装計画 > > ## Context > > herdr上で起動しているAIエージェントCLI (Claude Code / Codex CLI / GitHub Copilot CLI) から `!plan` で呼び出され、最新のplanファイルを右ペインにleafで表示するコマンドを `mnt/plan/` に新規作成する。 > > ## 変更ファイル > > 1. **`mnt/plan/plan.sh`** (新規作成) - メインスクリプト > 2. **`provision.sh`** (1行追加) - `~/bin/plan` へのシンボリックリンク > > ## `mnt/plan/plan.sh` の実装 > > ### 全体フロー > > ``` > plan [<mdファイル>] > ├─ 引数あり → そのファイルを使用 > └─ 引数なし → Agent自動判別 > ├─ $HERDR_PANE_ID + herdr pane list → agent名取得 > ├─ claude → ~/.claude/plans/ の最新.mdファイル (ls -t) > ├─ copilot → ~/.copilot/session-state/*/plan.md の最新 (find + stat) > └─ codex → ~/.codex/sessions/ のJSONLから<proposed_plan>抽出 > → 一時ファイルに書き出してleafで表示 > ``` > > ### Agent判別方法 > > ```bash > agent=$(herdr pane list | jq -r --arg pid "$HERDR_PANE_ID" \ > '.result.panes[] | select(.pane_id == $pid) | .agent // empty') > ``` > > ### 各Agentのplanファイル取得 > > - **Claude Code**: `ls -t ~/.claude/plans/*.md | head -1` > - **GitHub Copilot CLI**: `find ~/.copilot/session-state -name "plan.md" -exec stat -f '%m %N' {} + | sort -rn | head -1 | cut -d' ' -f2-` > - **Codex CLI**: `find ~/.codex/sessions -name "*.jsonl" | sort -r` で最新JONLから `<proposed_plan>...</proposed_plan>` タグを抽出。セッションの `session_meta.cwd == $PWD` でカレントディレクトリのセッションに限定。抽出結果を一時ファイル(`/tmp/codex-plan-XXXXX.md`)に書き出す。見つからなければエラー。 > > ### ペイン分割 + leaf起動 > > ```bash > NEW_PANE=$(herdr pane split --direction right --focus | jq -r '.result.pane.pane_id') > herdr pane run "$NEW_PANE" "leaf --watch --editor nvim $plan_file" > ``` > > ## `provision.sh` への追加 > > 428行目 `ln -snf "$MNT"/otm/otm.sh ~/bin/otm` の後に追加: > > ```bash > ln -snf "$MNT"/plan/plan.sh ~/bin/plan > ``` > > ## 検証方法 > > 1. herdr上でClaude Codeを起動し、planモードでplanファイルを作成 > 2. Claude Codeから `!plan` を実行 → 右ペインにplanファイルが表示されることを確認 > 3. `!plan path/to/file.md` で指定ファイルが表示されることを確認 > > ```` ## 成果物 `plan.sh` ```bash #!/bin/bash set -euo pipefail usage() { echo "Usage: plan [<md file>]" echo "" echo "herdr上で起動中のAIエージェントの最新planファイルをleafで表示します。" echo "" echo "引数なし: 現在のペインのエージェントを自動判別し、最新のplanを開く" echo "引数あり: 指定したMarkdownファイルを開く" exit 1 } case "${1:-}" in -h | --help) usage ;; esac find_claude_plan() { local plans_dir="$HOME/.claude/plans" if [[ ! -d "$plans_dir" ]]; then echo "Error: Claude Codeのplansディレクトリが見つかりません: ${plans_dir}" >&2 return 1 fi local plan_file plan_file=$(ls -t "$plans_dir"/*.md 2>/dev/null | head -1) if [[ -z "$plan_file" ]]; then echo "Error: Claude Codeのplanファイルが見つかりません" >&2 return 1 fi echo "$plan_file" } find_copilot_plan() { local session_dir="$HOME/.copilot/session-state" if [[ ! -d "$session_dir" ]]; then echo "Error: GitHub Copilot CLIのsession-stateディレクトリが見つかりません: ${session_dir}" >&2 return 1 fi local plan_file plan_file=$(find "$session_dir" -name "plan.md" -exec stat -f '%m %N' {} + 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-) if [[ -z "$plan_file" ]]; then echo "Error: GitHub Copilot CLIのplanファイルが見つかりません" >&2 return 1 fi echo "$plan_file" } find_codex_plan() { local target_cwd="${1:-$PWD}" local sessions_dir="$HOME/.codex/sessions" if [[ ! -d "$sessions_dir" ]]; then echo "Error: Codex CLIのsessionsディレクトリが見つかりません: ${sessions_dir}" >&2 return 1 fi local plan_text="" while IFS= read -r file; do jq -e --arg cwd "$target_cwd" ' select(.type == "session_meta") | .payload.cwd == $cwd ' "$file" >/dev/null 2>&1 || continue local extracted extracted=$(jq -rs -r ' [.[] | select(.type == "event_msg" and .payload.type == "item_completed") | .payload.item | select(.type == "Plan") | .text] | last // empty ' "$file" 2>/dev/null) if [[ -n "$extracted" ]]; then plan_text="$extracted" break fi done < <(find "$sessions_dir" -name "*.jsonl" 2>/dev/null | sort -r) if [[ -z "$plan_text" ]]; then echo "Error: Codex CLIのplanが見つかりません (cwd: $target_cwd)" >&2 return 1 fi local tmp_file tmp_file=$(mktemp /tmp/codex-plan-XXXXXX) printf '%s\n' "$plan_text" >"$tmp_file" echo "$tmp_file" } get_plan_file() { if [[ $# -ge 1 ]]; then local file="$1" if [[ ! -f "$file" ]]; then echo "Error: ファイルが見つかりません: ${file}" >&2 exit 1 fi echo "$file" return fi local pane_id="${HERDR_PANE_ID:-${HERDR_ACTIVE_PANE_ID:-}}" if [[ -z "$pane_id" ]]; then echo "Error: HERDR_PANE_ID/HERDR_ACTIVE_PANE_ID が設定されていません。herdr内で実行してください。" >&2 exit 1 fi local pane_info pane_info=$(herdr pane list | jq -r --arg pid "$pane_id" '.result.panes[] | select(.pane_id == $pid)') local agent agent=$(echo "$pane_info" | jq -r '.agent // empty') if [[ -z "$agent" ]]; then echo "Error: 現在のペインでエージェントが検出できません (pane_id: ${pane_id})" >&2 exit 1 fi local pane_cwd pane_cwd=$(echo "$pane_info" | jq -r '.cwd // empty') case "$agent" in claude) find_claude_plan ;; copilot) find_copilot_plan ;; codex) find_codex_plan "$pane_cwd" ;; *) echo "Error: 未対応のエージェント: ${agent}" >&2 exit 1 ;; esac } plan_file=$(get_plan_file "$@") if [[ -z "$plan_file" ]]; then echo "Error: planファイルのパスが空です" >&2 exit 1 fi NEW_PANE=$(herdr pane split --direction right --focus | jq -r '.result.pane.pane_id') herdr pane run "$NEW_PANE" "leaf --watch --editor 'nvim +{\$line} +\"setlocal ft=markdown\" +\"normal! zz\"' $(printf '%q' "$plan_file")" ``` ## 使い方 ### 1. `plan` コマンドを実行できるようにする 1. `plan.sh` に実行権限をつける 2. `plan -> plan.sh` の[[シンボリックリンク]]を貼る 3. `plan` に PATHを通す ### 2. `config.toml` に設定を追加する [[herdr]]の設定ファイルに追加。`key`は好きなものでOK。 `config.toml` ```toml [[keys.command]] key = "prefix+p" type = "shell" command = "plan" ``` ### 3. [[AIコーディングエージェント]]から実行 `config.toml` で設定したkeyを押すと、右側に分割されたペインに[[leaf]]のプランが表示される。 ## 機能/サポート対象 #### サポートしている[[AIコーディングエージェント]] | エージェント | ホットリロード | | ---------------------- | --------- | | [[Claude Code]] | ✅ **対応** | | [[Codex CLI]] | ❌ **未対応** | | [[GitHub Copilot CLI]] | ✅ **対応** | #### 機能 - [[herdr]]のペインを右側に分割して開き、[[leaf]]でプランの[[Markdown]]ファイルを表示する - `<C-e>` で [[Neovim]]による編集モードに切り替わる - スクロール位置も可能な限り保持して切り替わる - ホットリロード - 元の[[Markdown]]ファイルが変更されたら自動で内容が更新される - ただし、一部の[[AIコーディングエージェント]]は未対応 ## 関連コミット - [feat(plan): planコマンドを追加](https://github.com/tadashi-aikawa/toki/commit/edca743d13e4e8d6e312b70125c93433e0dd9bd7)