SUZ-LAB

「移行した」と「定着した」は違う — herdr を日々の母艦に馴染ませ、乗り換えた意味を取り戻すまで

前回「自前の通知機構を捨てて herdr へ全面移行した」と書いた。だが「移行した」は宣言で、「定着した」は毎日使って初めて分かる——その翌週から母艦に SSH で入り続けて埋めた穴の記録。最大の発見は、乗り換えの核心だったはずの「ペイン単位のエージェント状態検出」が、Claude タブで Agent View を全画面常駐させていたせいで実は届いていなかったこと(=二重の多重化)。素の claude を 1 ペイン=1 エージェントに割り直して初めて、移行の意味が出た。あわせて、タブ起動をプラグインへ畳み、起動系をヒアドキュメントから「追跡ファイル+symlink」へ寄せ、その代償(ツールの自動書き戻しで dirty になる税金)を意図的な固定で払い、そして SSH × マルチプレクサ × 全画面 TUI の三重の間でコピペとスクロールが何度も壊れ、copy_mode を足して revert し、最後は「マルチプレクサではなく TUI 自身のスクロールに委ねる」に着地するまで——想定と違う決着まで正直に残します。

「移行した」は、まだ半分だった

つい先日、自前の通知機構を捨てて herdr へ全面移行した回を書いた。tmux で組んだ開発コックピットの本丸だったクロスセッション通知(フック4種+シェル2本)を丸ごと捨て、AI エージェント特化マルチプレクサ herdrネイティブなペイン状態検出idle / working / done / blocked)へ載せ替えた——という記録だ。あの回は「検証で通したところ」と「まだ確かめきれていない穴(blocked の実発火・iPad での操作感)」を分けて、正直に書いたつもりだった。

その続きがこれだ。移行を宣言してから、翌週いっぱい母艦に SSH で入り続けた。そうして分かったのは、「移行した」と「定着した」は別物だということだ。前回の PR で構造は載せ替わっていたが、日々使って初めて見える穴がいくつもあった。しかも最大の穴は、移行の核心そのものにあった。

この記録は、その定着の過程で埋めた穴の一覧だ。設計判断だけでなく、足して revert した徒労や、想定と違う形で決着した穴まで、そのまま残す。

乗り換えた意味が、実は届いていなかった — 1 ペイン=1 エージェントへ

前回いちばん胸を張ったのは「herdr はペイン単位でエージェントの状態を検出する」ところだった。ところが実運用で Claude タブを開いても、サイドバーに個々のセッションの状態が出ない。調べて、青ざめた。

Claude タブは、当時まだ claude agents(Agent View)を全画面で常駐させていた。すると複数の Claude セッションが単一ペインの中に入れ子になる。herdr から見えるのは外側の 1 ペインだけで、中の個々セッションの idle / working を出しようがない。前回わざわざ乗り換えた「ペイン単位の状態検出」が、この構成ではまったく効いていなかった——tmux の中で tmux を動かすような、二重の多重化をしていたわけだ。

なぜ状態が「取り違う」のか — 実機で見えた壊れ方

herdr の claude 連携フックは SessionStart で「このペイン(HERDR_PANE_ID)で動いている session-id はこれ」というバインドを張る。ところが 1 ペインに複数セッションが入れ子だと、後から起動したセッションが同じペインの前のバインドを上書きする。実機では、フォーカス中のペイン p1 の状態が、まったく別のバックグラウンド・ジョブ(e5a4fc12…)の状態を指しているのを確認した。加えて表示ステータスは Agent View のダッシュボード端末をスクレイプした二次情報で、これも不正確だった。要するに、状態検出の入力(pane→session バインド)が構造的に壊れていた。

対処はシンプルで、しかし破壊的だった(PR #125)。Claude タブを素の claude 1 セッションに戻し、1 ペイン=1 エージェントにした。セッションを増やすときは Agent View ではなく herdr ネイティブのファンアウトを使う:

herdr agent start Claude1 --split right -- claude   # 右に分割して新しい claude
herdr agent start Claude2 --tab <tab_id> -- claude  # 指定タブ内に起動

こうすると各 claude個別ペイン=個別エージェントとしてサイドバーに並び、agent_panel_sort = "priority" の「要対応が上」ソートが初めて機能する。移行の核心的な価値は、載せ替えた瞬間ではなく、この割り直しで初めて出た。「乗り換えた」と書いた翌週に、「まだ乗り換えられていなかった」と気づく——定着とは、そういう作業だった。

タブ起動を、herdr プラグインへ畳む

1 ペイン=1 claude に切り替えると、タブの作り方・ファンアウトの持ち方も見直しになった。ここで herdr 0.7.x のプラグイン機構へ寄せた(PR #127、その後 #128 / #129 で命名を整理)。

  • 同梱プラグイン suzlab.view.devcontainer/herdr-plugin/)に、3 タブの起動コマンドを [[panes]]claude / lf / bash -l)として、ファンアウトを [[actions]] fanoutfanout.sh。空いている最小の Claude<N> を採番して新タブで起動)として集約した。ランチャー(herdr.sh)は起動ごとにこれを冪等 herdr plugin link し、レイアウト構築を herdr plugin pane open に置き換えた。旧 ~/.local/bin/claude-fanout の生成は削除。
  • 初期タブ順を左→右で Shell | File | Claude0 にした。herdr にはタブ並べ替えが無く、表示順=作成順なので、root タブを Shell にして、その右へ FileClaude0 を作成順に足す。着地は最右の Claude。ただしこの順が保証されるのはワークスペース新規作成(初回ログイン)のときだけで、既存ワークスペースや自己修復での復活は末尾に付く——ここは仕様として AGENTS.md に明記した。
  • 主 Claude タブを Claude0 に改名(PR #128)。fanout が採番する Claude1 / Claude2… の先頭=0 番目として並びが揃う。あわせてランチャー名・ワークスペースラベル・プラグイン ID を suzlab 系に統一(PR #129。lab-view.shherdr.shplugin_id = suzlab.view、ワークスペースラベルは Codespaces のリポジトリ名へ縮退)。
  • File タブは lf を継続。herdr マーケットプレイスのプラグインを 198 件調べたが、lf 相当のファイラーは無く、候補(別ツール+ビルド依存+未審査)は採用を見送った——前回の gh-dash 判断と同じ「この環境で確実に動くか」を軸にした見送りだ。

「守る側/土台のコード」は機械の目が効く、というlefthook 回の学びはここでも当たり、Copilot レビューを 4 ラウンド回した(focus の誤着地、jq パースの明示ガード、herdr agent start 失敗時に空タブへ着地しない縮退、AGENTS.md のタブ順注記)。ファンアウトが「唯一のペインを失った空タブ」に着地しうる不整合は、実際に指摘で拾って直した。

起動系を「ヒアドキュメント」から「追跡ファイル+symlink」へ

前回は herdr の設定も連携フックも「起動時に冪等生成」していた。実体はシェルスクリプト中のヒアドキュメントで、毎起動で config.toml~/.bashrc のブロックを吐き直す方式だ。これを、リポジトリ同梱の追跡ファイルへ実体を出す方式に畳んだ(PR #130 / #131)。lf 設定でやっていた「repo ファイル+symlink」と同じ型だ。

  • SSH ログインフックを外部ファイル化(PR #130)。ログイン起動ロジックを ~/.bashrc のヒアドキュメントから追跡ファイル ssh-login-hook.sh へ切り出し、rc に貼るのは source の 2 行だけにした。rc のマーカーもタブ構成の世代から脱結合して固定文字列 login hook にしたので、フックの中身を変えても rc は不変になり、世代バンプや孤立ブロックによる握り潰しが起きなくなった。旧 (SSH → …) 系のマーカーブロックは、以前の「警告のみ」から自動除去へ切り替えた。
  • config.toml を追跡ファイル化(PR #131)。ヒアドキュメント生成をやめ、実体を .devcontainer/herdr/config.toml に置き、~/.config/herdr/config.toml をそれへの symlink に冪等収束させる(別リンク先・壊れリンク・実ファイルなら張り直し、目的の symlink 済みなら何もしない)。追跡ファイルが欠けていれば link せず素の herdr へ縮退する。

利点は、設定がTOML/シェル単体で検証・編集できることだ。ヒアドキュメントは「起動して初めて確定する設定」で、単体では読みにくく壊れやすい。だが repo の実体に寄せると、代わりに税金がつく——それが次の節だ。

rc に launcher パスを二重管理しない

~/.bashrc が持つのは「このフックへの source 行」だけで、launcher(herdr.sh)の絶対パスは rc に焼き込まない。フックは自分のファイル位置(BASH_SOURCE)から同じ .devcontainerherdr.sh を解決するので、フックと launcher は「同じディレクトリに並ぶ 1 組」として常に整合する。ただし Copilot 指摘で 1 点訂正した——リポジトリ自体を別パスへ移動した場合は、rc の source 行が移動前のパスを指したままなので、post-create の再実行(rebuild)まで追随しない。「置き場所が変わっても追随する」は過剰主張だった。

repo に設定を持つ税金 — ツールの自動書き戻し

config.toml を追跡ファイル本体にした(~/.config 側は symlink)ことで、思わぬ副作用が出た。herdr はオンボーディング完了/スキップ時に onboarding = false を config へ書き戻す。symlink 経由なので、この書き戻しが追跡ファイル本体を書き換え、毎回ワークツリーが dirty になる。実際、あるスクロール実験の PR(後述の #135)に、この書き戻しが無関係な差分として混入した。

対処は「消す」ではなく「固定する」だった(PR #136)。devcontainer は使い捨て・再作成が前提なので、onboarding = false を repo として明示的にコミットして「セットアップ済み」を固定し、再作成のたびにウェルカム画面が出ないようにしつつ、自動書き戻しによる dirty を封じた。設定を単一の出所(repo の追跡ファイル)に寄せると、ツールが勝手に書き戻す値まで自分の管理下に置くことになる——その代償を、意図的な固定で払った格好だ。

コピペとスクロール — 三重の間で何度も壊れた

いちばん手こずったのは、コピペとスクロールという端末の地味な当たり前だった。母艦は Mac の Terminal.app や iPad の Blink から SSH で入る。SSH × マルチプレクサ × 全画面 TUI(Claude Code)の三重の間で、これが何度も壊れた。

まずコピペ。 herdr 既定(mouse_capture = true)だと herdr がマウスドラッグを横取りし、mac ターミナルからのネイティブ範囲選択→Cmd+C ができない。[ui]mouse_capture = false を足して端末側の選択に任せた(PR #132)。ところがClaude タブだけまだ選択が奪われる。原因は、ペイン内の Claude Code が自前でマウストラッキングを ON にしていたことだった。CLAUDE_CODE_DISABLE_MOUSE=1 で Claude Code のマウス捕捉だけを切って解決した(PR #134)。トレードオフとして Claude 内のクリック操作・ホイールスクロールは無効になり、スクロールは別手段が要る——という次の問題に直結した。

そのスクロールで、足して revert する徒労をやった。 ホイールが無効になったので会話履歴を遡れない。そこで会話履歴をレンダリングしたまま辿れる copy_modeprefix+u に割り当てた(PR #135。生 ANSI を出す edit_scrollback は文字化けするため避けた)。ところがこれを翌 PR で revert した(PR #137)。

理由はこうだ(PR #138 で明文化)。claude全画面(alternate screen)TUI で、会話履歴を端末/herdr のスクロールバックに書き出さない。実際 herdr pane read <pane> --lines 200 はビューポート分(34 行)しか返さず max_offset_from_bottom = 0 だった。スクロールする対象がそもそも herdr 側に無いのだから、copy_modeedit_scrollback も Claude ペインでは効きようがない。copy_mode を足したこと自体が無意味だった。

正解は「マルチプレクサでスクロールしようとしない」だった。会話スクロールは Claude Code 自身のキーに委ねる——これは全画面 TUI アプリの責務で、外側のマルチプレクサの仕事ではない。機能がどのレイヤーの責務かを見誤ると、丸ごと徒労になる、という学びだ。

最後に、そのキー割り当てをMac と iPad で揃えた(PR #139)。当初は Mac 前提で Fn+↑↓←→(PgUp 等)を案内していたが、iPad Magic Keyboard には Fn が無いので押せない。Ctrl+英字だけで完結する Ctrl+O のトランスクリプトモード(k=上 / j=下 / gG/検索・q 退出)を Mac / iPad 共通の推奨に前面化し、Fn+矢印 は Fn 付き Mac 限定の補助へ降格した。前回「iPad での操作感が未確認」と残した穴は、Shift+矢印ではなくスクロールという別の入口から、iPad 前提が効く形で決着した。

検証 — 通ったところと、まだ残る穴

前回の作法を継いで、通したところと未確認を分けて残す。

  • 通った。 1 ペイン=1 エージェントでサイドバーに個々の Claude の状態が出ること(priority ソートで要対応が上に来ること)。fanout が Claude<N> を採番して新タブに開くこと。初回ログインで Shell | File | Claude0 の左→右順に並ぶこと。config.toml の symlink 収束(新規/誤リンク/実ファイル/冪等/source 欠落縮退の 5 ケース)と、rc の source 行移行。mac Terminal.app からのネイティブ選択→Cmd+C コピペ。Ctrl+O トランスクリプトでの会話スクロール(Mac で確認)。触れたのは devcontainer 側のスクリプトと設定だけなので、pnpm typecheck / pnpm lint / pnpm format:check が通ること。
  • まだ残る穴。 blocked の実発火は、本リポジトリが bypassPermissions 運用のため許可プロンプト由来では出にくく、前回から引き続き実運用の遷移として見届けられていないCtrl+O は設計上 iPad Magic Keyboard で完結するはずだが、実機での配信・操作感の体感確認は今後。新タブ順(Shell | File | Claude0)が保証されるのは新規ワークスペースの初回ログインのみで、既存ワークスペースは並べ替わらない。

学び

  • 「移行した」は宣言、「定着した」は毎日使って初めて分かる。 乗り換えの核心(ペイン単位の状態検出)が、Agent View の全画面常駐という二重の多重化で実は届いていなかった。それを炙り出したのは翌週の日常運用だった。構造を載せ替えただけで「終わった」と思わないこと。
  • 設定を単一の出所(repo の追跡ファイル)に寄せると、税金がつく。 ヒアドキュメント生成をやめて TOML/シェルの実体に寄せると単体で検証・編集できる一方、ツールが勝手に書き戻す値(onboarding)まで自分の管理下に入り、dirty を生む。代償は「消す」でなく「意図的に固定する」で払う。
  • 機能がどのレイヤーの責務かを見誤ると、丸ごと徒労になる。 スクロールを「マルチプレクサの機能」で解こうとして copy_mode を足し、全画面 TUI がスクロールバックを持たないと理解して revert した。責務の切り分けを間違えた分だけ、手戻りする。
  • iPad 前提は「補助キーが無い」制約として効く。 Fn なしで完結する操作(Ctrl+英字)に寄せると、Mac / iPad 共通で楽になる。母艦をどこから触るかが、キー設計の制約になる。
  • 足して revert した記録も、一次情報として残す価値がある。 定着は、うまくいった設計判断の裏で徒労と手戻りを払う過程でもある。前回が「検証」を丁寧に書いたぶん、続編で徒労を隠さないことに意味がある。
#herdr#Claude Code#SSH#DevContainer#マルチエージェント#プラグイン#lf#端末#AI

同じ柱のほかのログ

この柱の Roadmap を見る