AIエージェントに調査やコーディングをさせていて、複数エージェントを並列で動かしたときに質問で止まっていることがある。複数起動していると気づくことが遅れてしまい、かといって巡回的に見て回るのも面倒くさい。自分は設計、指示、コードを読むこと、確認に時間を使いたい。
そこで、WindowsとWSL2の両方にHerdr・Neovim・AI CLIを用意し、WezTermから行き来する構成にしました。設定はGitで持ち、使うOSとAIはその時の作業に合わせて選びます。
Neovimを閉じても、AIの作業場所は残す
実装の依頼はAI CLIへ送り、Herdrではその作業画面を管理します。コードと差分を読むのがNeovim。役割はこう分けました。
| 道具 | 今回任せること |
|---|---|
| WezTerm | WindowsとWSLへの入口、フォント、タブ |
| Herdr | プロジェクトの画面、端末の分割、AIの状態表示 |
| Neovim | コード・設計資料を読む、変更差分を確認する |
| agy / Codex / Copilot CLI | 指示を受けて調査・実装する |
| Git | 設定とコードの履歴、タスクごとの作業領域 |
AIをNeovimの中へ全部詰め込むより、隣のHerdrペインで動かす形を選びました。Neovimは読みたいファイルを開き直せばいい。AIの端末は、そのまま残せます。
Herdrはサーバーが端末を保持する仕組みなので、画面から切断してもプロセスは続きます。ただし、ローカルPCをスリープ・終了した間も計算し続ける、という意味ではありません。
これまでのAIが変えたコードをNeovimとGitで読む流れに、作業場所を保持するHerdrと、プロジェクト設定の入口を足した形です。
WindowsとWSLは、作業に合わせて選ぶ
共通にするのはLua設定と操作、AIへの指示。WebはWindowsでもWSLでも、その時に都合のよい方で進めます。選んだOS側のNeovim・AI CLI・SDKを使います。
WezTerm(Windows)
├─ Windows Native
│ └─ Herdr → Neovim / 好きなAI / 必要なSDK
│ WPFの実行・画面確認、Web開発など
└─ WSL2 / Ubuntu
└─ Herdr → Neovim / 好きなAI / 必要なSDK
Web開発、Linuxでの確認、コードを読む作業など
WPFはWindows専用です。今回はWindows側でWPFサンプルをビルドし、実際にウィンドウを起動するところまで進めました。

Neovimのプラグイン本体、Masonのツール、SDK、CLIの認証情報は、それぞれのOSに置きます。設定の共通化は、以前のWindows移植で見直した方針を引き継ぎました。
同じWebプロジェクトを両OSで触るなら、それぞれへcloneしてGitで変更を渡す形にできます。node_modulesなどOSに依存する生成物は共有せず、各OSで作ります。フォルダを分けても未コミットの変更は自動では移らないので、切り替える前に差分を確認します。
まずHerdrを両方へ入れる
前提は、両OSのGit・Neovim・Node.jsと、WindowsのWezTerm・PowerShell 7です。今回も既存のLazyVim設定を引き継ぎました。新しいPCでは、Neovimの導入手順も先に確認します。今回のサンプルはNode.js 22以上、WPF側は.NET 10 SDKを使います。
WSL側では、コマンドがどのOSのものかを先に見ます。command -vは実行ファイルの場所を調べるコマンドです。
command -v nvim node npm agy codex
ここでWindowsのnpmフォルダを指していたら、Ubuntu側に入れ直します。WindowsのNeovimからLinuxのCLIを呼ぶ、といった混在は今回の構成から外しました。
Herdrは公式の導入手順に従います。WSLではスクリプトを保存して内容を見てから実行できます。
curl -fsSL https://herdr.dev/install.sh -o /tmp/herdr-install.sh
less /tmp/herdr-install.sh
sh /tmp/herdr-install.sh
herdr --version
Windowsは公式のPowerShellインストーラーか、リリースZIPを使います。私のPCではPowerShellの実行ポリシーに止められたので、ZIPを展開する方法へ切り替えました。Windows版はherdr.exeだけ抜き出さず、同梱のConPTYも含めて配置します。展開先をユーザーのPATHに追加し、新しいPowerShellでherdr --versionが出れば導入できています。
Get-Command nvim, node, git, agy, codex, herdr
dotnet --list-sdks
Get-CommandはWindows側で実行するコマンドの場所を見るためのもの。dotnet --list-sdksが空なら、.NETのランタイムがあってもWPFのビルド環境はそろっていません。今回もここでSDK不足が分かり、Microsoft公式の.NET SDKを追加しました。
使いたいAI CLIも、使うOS側へインストールしておきます。Codexなら公式の導入手順、Copilot CLIなら公式の導入ページから。初回起動時は自分のアカウントでログインします。Windows版Codexでサンドボックスの設定を求められたら、公式の案内に沿って設定してから使います。
環境の導入はローカルのCodexに任せました。導入時の版はHerdr 0.9.5、NeovimはWSL側0.12.1/Windows側0.12.2、Codex CLIは0.160.0です。
WezTermを開いて、HerdrからAIを起動する
まずは追加の起動スクリプトを使わずに開きます。WezTermでシェルを開く → プロジェクトへ移動 → Herdrを起動 → Herdrの中でNeovimとAIを起動、という順番です。
1. WezTermを開き、使うOSのシェルに入る
WindowsのスタートメニューからWezTermを起動します。私の設定では、最初のタブにPowerShell 7が開きます。コマンドプロンプトで開いた場合は、pwsh.exe -NoLogoと入力してEnterを押すとPowerShell 7へ入れます。
Windowsで作業するなら、そのPowerShellを使います。 WSLで作業するなら、同じ入力欄で次を実行します。
wsl.exe -d Ubuntu-24.04
これでタブの中身がUbuntuのシェルに切り替わります。Ubuntu-24.04は自分のディストリビューション名へ置き換えます。名前が分からなければ、PowerShellでwsl -l -qを実行すると一覧が出ます。WSLの起動コマンド
すでにUbuntuのシェルが開いていれば、WSLへ入り直す必要はありません。後半のWezTerm設定を追加すると、Ctrl+Shift+WでWindows、Ctrl+Shift+UでWSLのタブを直接開けます。
2. プロジェクトへ移動し、Herdrを起動する
次のうち、今使っているOSの方を入力します。パスは自分のプロジェクトがあるフォルダへ置き換えます。
WindowsのPowerShellなら、たとえば次です。
cd C:\src\my-project
herdr --session dev
WSLのシェルなら、次です。
cd ~/src/my-project
herdr --session dev
初回は、サイドバーと端末ペインのあるHerdrの画面が開きます。ここで開くのはシェルです。AIは次の手順で自分で起動します。 Herdrのクイックスタート
devはHerdrのセッション名です。同じOSで同じ名前を指定すると、前回の画面へ戻ります。プロジェクトを増やす時は、このセッション内にSpaceを追加します。別名のセッションを作ると、SpacesやAgentsの一覧も別々になるからです。名前を省いてherdrだけで起動すると、defaultという名前になります。セッションの公式説明
3. Herdrの左にNeovim、右にAIを開く
Herdr内の入力欄で、次を入力してEnterを押します。
nvim .
Neovimが開いたら、Ctrlを押しながらb → 両方離す → v。画面が左右に分かれ、右側に新しいシェルが開きます。
その右側の入力欄で、使いたいAIのコマンドを一つ入力してEnterを押します。
| 起動するAI | 入力するコマンド |
|---|---|
| agy | agy |
| Codex | codex --no-daemon |
| Copilot CLI | copilot |
AIの入力画面が出たら、そこへ依頼を書いて送信します。初回にログインやプロジェクトの信頼を尋ねられたら、先にその案内へ進みます。Copilot CLIも、プロジェクトのフォルダからcopilotで起動できます。
Codexには--no-daemonを付けました。このPCでは既存の共有サーバーと設定が衝突し、普通に起動すると止まったからです。独立起動の公式オプションを使い、ほかの作業を再起動せずにCLIを開ける形にしました。
AIだけ開きたい時は、Neovimの起動と分割を飛ばし、Herdrの最初のシェルでAIのコマンドを実行すれば大丈夫です。

Ctrl+bは「次のキーをHerdrへ渡す」合図
Herdrの中では、NeovimもAI CLIもシェルもキーを使っています。そこで、最初にCtrl+bを押した時だけ、次のキーをHerdrの操作として扱います。この合図をプレフィックスと呼びます。Ctrl+b自体が終了操作、というわけではありません。公式にもこの役割が説明されています。
たとえば切断は、Ctrlを押しながらb → 両方離す → q。3つを同時に押しません。普段のqまでHerdrへ取られないので、Neovimの操作と分けて使えます。
bが必須なのではなく、Herdrの既定値です。今回の設定もそれに合わせました。よく使う操作はこのあたりです。
| Ctrl+bの後 | 操作 |
|---|---|
| g | AI・端末の一覧へ。対象を選びEnterで移動 |
| w | Spaceを選ぶ。上下で選びEnterで移動 |
| Shift+n | 新しいSpaceを作る |
| Shift+w | 今のSpaceの名前を変える |
| Shift+d | 今のSpaceを閉じる。中の全タブ・ペインを終了 |
| v / – | 右/下へ分割 |
| h / j / k / l | ペイン移動 |
| z | 今のペインを拡大・元に戻す |
| c | タブを追加 |
| n / p | 次/前のHerdrタブへ移動 |
| 1〜9 | 今のSpaceの指定番号のタブへ移動 |
| Shift+t / Shift+x | タブの名前変更/タブ全体を閉じる |
| r | ペインのサイズ変更モード。矢印で調整、Escで戻る |
| [ | ログをさかのぼる・コピーするモード |
| b | 左のサイドバーを表示・非表示 |
| q | 切断。中の処理は残す |
| x | 今のペインを閉じる。中のプログラムも終了 |
狭い画面なら、AIを常時横に置くよりzでコードを広く読みます。操作を忘れた時はCtrl+bを離して?でヘルプを開けます。
別のキーにしたければ、後半に載せるHerdr用config.tomlの既存の[keys]にあるprefixを変更します。たとえば公式の変更例は次の形です。[keys]をもう一つ追加せず、元の行を置き換えます。
[keys]
prefix = "ctrl+a"
この設定を読み込んだ環境では、この記事のCtrl+bをCtrl+aに読み替えます。手元の設定はCtrl+bのままです。
Spacesで作業場所を分け、AgentsからAIへ戻る
左のSpacesは、プロジェクトや作業ごとの置き場です。公式資料ではWorkspaceと呼ばれています。Spaceの中にHerdrのタブがあり、タブの中をペインで分割します。左のAgentsは、同じセッション内で動くAIの一覧。別のSpaceで動いているAIへも、ここから戻れます。Herdrの画面構成
たとえば、こんな分け方です。名前は自分が見分けやすいもので構いません。
セッション dev
├─ Space: api
│ ├─ タブ: review → Neovim | Codex
│ └─ タブ: test → テスト・ログ
└─ Space: ui
└─ タブ: work → Neovim | agy
ここでいうタブはHerdrの内側のタブです。Windows/WSLへ入るWezTermのタブとは操作が別です。
Spaceを追加して、別プロジェクトへ移る
最初のSpaceは、Herdrを起動した時に作られます。名前を変更する時は、次の順です。
- 左のSpacesで、名前を変えたい行をクリックする。
Ctrl+bを押して両方離し、Shift+w(大文字のW)を押す。Shift+wを押す時にCtrlは押さない。- 名前の入力欄が開いたら、
End→Ctrl+uで今の名前を消す。 apiなど新しい名前を入力し、Enterで確定する。途中でやめるならEsc。
Endはカーソルを末尾へ移動し、Ctrl+uはそこから先頭までを消す操作です。新しい名前を入れる前に、元の名前を消します。名前変更と入力欄の操作
別プロジェクトを追加する時は、次の順に進めます。
Ctrl+b→ 離す →Shift+nで新しいSpaceを作る。名前の入力欄が出たら、uiなどと入力してEnter。- 新しいSpaceのシェルで、そのプロジェクトへ移動する。Windowsなら
cd C:\src\another-project、WSLならcd ~/src/another-projectのように入力する。 pwdで今のフォルダを表示し、対象プロジェクトにいることを見る。nvim .を開き、Ctrl+b→vで右に分割。右側でagyやcodex --no-daemon、copilotを起動する。
Spaceの名前を変えても、シェルのフォルダは変わりません。 新しいSpaceでは、AIを起動する前にcdで作業場所を合わせます。
Spaceを切り替える時は、Ctrl+b → 離す → w → ↑ / ↓で選ぶ → Enter。左のSpacesの名前をクリックしても移動できます。切り替え元のAIは、そのまま動き続けます。選ぶのをやめる時はEscです。Spaceとキー操作の説明
質問待ちのAIへ戻る
左のAgentsでAIをクリックすると、そのAIがいるSpace・タブ・ペインへ移ります。キーボードなら次です。
Ctrl+b→ 離す →gでGo Toの一覧を開く。↑/↓、またはj/kで移動先を選ぶ。- Enterで、そのAIの画面へ移る。Escでキャンセル。
AIが増えたら、一覧で/を押して名前やcodexなどの種類、プロジェクト名を検索します。検索入力中ではない時は、次のキーで絞れます。
| 一覧で押すキー | 表示する対象 |
|---|---|
| b | 質問・承認待ち(blocked) |
| w | 作業中(working) |
| d | 完了の表示が出ているAI(done) |
| i | 待機中(idle) |
| a | 絞り込みを外し、通常の端末も含めて表示 |
質問待ちを見に行くなら、Ctrl+b → g → b → 対象を選んでEnter。画面を順番に巡回する代わりに、判断が必要なAIへ戻るための操作です。Go Toと絞り込み
Herdrの状態表示は、どのAIの画面を見に行くかの目印にします。idleと表示されても、すべての処理が終わったとは限りません。たとえばagyは、バックグラウンド作業が残っていてもidleになる場合があります。移動先でAIの質問や結果を読んで判断します。状態表示の公式説明
テストやログは、同じSpaceの別タブへ
AIの画面を残したままテストしたい時は、Ctrl+b → 離す → cでHerdrのタブを追加します。新しいタブのシェルでpwdを見て、必要ならプロジェクトへ移動し、npm testやdotnet testなど、そのプロジェクトのコマンドを実行します。
Ctrl+b → Shift+tでタブをtestなどへ改名すると、AIを開いたタブと区別できます。次のタブはCtrl+b → n、前のタブはCtrl+b → p。Ctrl+b → 1〜9で番号を指定して切り替えます。タブの見出しをクリックしても構いません。
左右が狭ければ、Ctrl+b → zで今のペインを拡大し、もう一度同じ操作で戻します。幅を調整するならCtrl+b → r、矢印キーで動かしてEsc。マウスで境界線をドラッグしても調整できます。
流れたログを読む時は、Ctrl+b → [でコピーモードへ入ります。PageUp/PageDownでさかのぼり、コピーしたい位置でv、矢印キーで範囲を選んでy。選択せず戻るならEscです。選択中のEscは選択解除なので、もう一度Escで端末へ戻ります。マウスならドラッグ選択を離すとコピーされます。ログ閲覧・コピー操作
作業を残して離れる、戻る、完全に止める
少し離れるなら、Ctrl+bを押して離し、q。Herdrの画面から切断して元のシェルへ戻り、中のNeovimやAIは動いたまま残ります。切断できたら、WezTermのそのタブを閉じても構いません。
戻る時は同じOS・同じセッション名で起動します。先ほどの例ならherdr --session devです。別OS側のHerdrは別のセッションなので、WindowsとWSLのどちらで始めたかも合わせます。
個別に終了したい時は、次の操作です。
| 終了するもの | 操作 |
|---|---|
| Neovim | Esc → :wqa → Enter。全ファイルを保存して終了 |
| Codex CLI | 入力欄で/exit → Enter |
| 今のHerdrペイン | Ctrl+bを離してx。中のプログラムも終了 |
| 今のHerdrタブ全体 | Ctrl+bを離してShift+x。そのタブの全ペインを終了 |
| 今のSpace全体 | Ctrl+bを離してShift+d。そのSpaceの全タブ・ペインを終了 |
Codexの/exitは公式の終了コマンドです。AIを閉じた後にシェルが残っていれば、exitでそのシェルも終了できます。ペイン・タブ・Spaceを閉じる時は、その範囲のNeovimを保存し、AIの作業結果を読んでから閉じます。確認画面が出たら、対象を見てEnterで閉じるか、Escでやめます。
セッション全体を止めるなら、各Spaceの保存とAIの終了を済ませ、切断後のシェルでセッションを停止します。次のコマンドで名前を調べます。
herdr session list
一覧から止めたい名前を選びます。起動例のdevなら次です。実際には自分の一覧に出た名前へ置き換えます。
herdr session stop dev
名前なしのherdrで始めた作業場所ならherdr session stop defaultです。停止すると、そのセッションの全Spacesにあるプログラムが終了します。一つのプロジェクトだけ閉じるなら、上のSpace終了の操作を使います。停止後に再び開く時は、NeovimやAIを起動し直します。一覧・停止の公式コマンド
設定はGitで持ち、OSごとに配置する
共通設定は、自分のdotfilesリポジトリで管理します。WindowsとWSLの配置先は別ですが、直す原本は一つです。APIキー、認証ファイル、プラグイン本体やSDKはGitへ入れません。
| 設定 | Windows | WSL |
|---|---|---|
| Neovim | %LOCALAPPDATA%\nvim |
~/.config/nvim |
| Herdr | %APPDATA%\herdr\config.toml |
~/.config/herdr/config.toml |
| WezTerm | 今使っているWindows側の設定 | Windows側のWezTermから入る |
既存のLazyVim設定をバックアップし、Neovimのlua/plugins/ai-review.luaへ次を追加しました。新しいPCでLazyVimがなければ、公式スターターを先に導入します。
return {
{
"sindrets/diffview.nvim",
cmd = { "DiffviewOpen", "DiffviewClose", "DiffviewFileHistory" },
keys = {
{ "<leader>ad", "<cmd>DiffviewOpen<cr>", desc = "AI: Review working tree" },
{ "<leader>ah", "<cmd>DiffviewFileHistory %<cr>", desc = "AI: File history" },
{ "<leader>aq", "<cmd>DiffviewClose<cr>", desc = "AI: Close review" },
},
opts = {},
},
}
Space a dでDiffview、Space a qで閉じる、Space a hで今のファイルの履歴です。プラグインの版をそろえる時は、Neovimのlazy-lock.jsonもGitへ残します。
Herdrの共通設定はこちらです。WindowsはPowerShell 7、WSLはその環境のシェルを使うので、シェルの固定パスを共通設定へ書いていません。設定の保存先と既定シェルは、Herdrの設定資料にまとまっています。
# Shared between Windows (%APPDATA%/herdr) and WSL (~/.config/herdr).
# Leave the shell unset: PowerShell 7 on Windows; $SHELL on WSL.
onboarding = false
[theme]
name = "solarized"
[ui]
# Keep the real cursor for Japanese IME candidate positioning.
# On native Windows this can cause cursor flicker during heavy output.
host_cursor = "native"
[keys]
prefix = "ctrl+b"
Windowsの日本語入力では、IMEの変換候補が入力位置からずれる場合があります。この設定のhost_cursor = "native"は、端末のカーソルを使って候補位置を合わせるためのものです。その代わり、大量の出力中にカーソルがちらつく場合があります。候補位置より表示の安定を優先したければautoへ戻して比べます。HerdrのWindows対応資料
WezTermは、既存設定でconfig.keysを設定した後、return configの前へ次を加える形にできます。すでに同じキーがあれば、重複させずその定義を置き換えます。Ubuntu-24.04はwsl -l -qに出る自分の名前へ合わせます。
config.launch_menu = {
{ label = "Windows Native", args = { "pwsh.exe", "-NoLogo" } },
{ label = "WSL2 Ubuntu", args = { "wsl.exe", "-d", "Ubuntu-24.04" } },
}
config.keys = config.keys or {}
table.insert(config.keys, {
key = "W", mods = "CTRL|SHIFT",
action = wezterm.action.SpawnCommandInNewTab({ args = { "pwsh.exe", "-NoLogo" } }),
})
table.insert(config.keys, {
key = "U", mods = "CTRL|SHIFT",
action = wezterm.action.SpawnCommandInNewTab({ args = { "wsl.exe", "-d", "Ubuntu-24.04" } }),
})
これは、WezTermのタブ起動設定を使った追加例です。WindowsとWSLのタブをCtrl+Shift+W/Uで開きます。私のWezTerm自身のプレフィックスは従来のCtrl+Space、HerdrはCtrl+b、NeovimはSpace。どの層へ操作しているかを分けています。
最初のタブをどちらで開くかも変更できます。既存のconfig.default_progを、Windowsなら{ "pwsh.exe", "-NoLogo" }、WSLなら{ "wsl.exe", "-d", "Ubuntu-24.04" }へ。通常起動を変えても、もう一方へ入るキーは使えます。
設定を変える時もGit側の原本を直して、それぞれへ配置します。戻す時のために、最初の設定を退避しておきます。Herdrはconfig.tomlだけを入れ替え、同じフォルダにある稼働中のセッションやソケットは動かしません。
プロジェクトごとの設定もGitへ置く
プロジェクト固有の設定は、そのリポジトリへ置いてGitで管理します。AIごとの置き場所は次の通りです。Herdrの起動に、独自の.devフォルダを作る必要はありません。
| 何を置くか | 保存先の例 |
|---|---|
| 共通の開発ルール・確認方法 | プロジェクト直下のAGENTS.md |
| Codexのモデル・推論などの設定 | .codex/config.toml |
| Gemini CLIの設定と指示 | .gemini/settings.json、GEMINI.md |
| Copilotの指示 | .github/copilot-instructions.md、.github/instructions/*.instructions.md |
| ビルド・テストの定義 | package.json、.csprojなど、元から使っているファイル |
CodexはAGENTS.md、Copilot CLIはAGENTS.mdと.githubの指示ファイルを読めます。Gemini CLIはGEMINI.mdが既定です。既存のルールを使い、各AIが読むファイル名と形式へ合わせます。Gemini CLIとagyは別のCLIです。
既定のAIや起動コマンドまでプロジェクト別にまとめたい場合の設定例は、後半の補足へ載せました。
AIの説明を読み、差分とテストへ戻る
AIへ渡す依頼は、たとえばこんな形です。これは説明用の依頼例です。
AGENTS.mdとREVIEW.mdを読み、.devがあればその設定も読んでから進めて。
注文数量が負のときに処理を止めたい。
変更前に対象箇所と確認方法を説明して。
実装後はbuild/testを実行し、変更点と未確認を報告して。
NeovimにはDiffviewを追加しました。Space a dで作業ツリーの差分、Space a qで閉じる。普段のGit操作は、既存のSpace g gでLazyGitを開きます。ファイル一覧を隠してコードを広く読む時は:DiffviewToggleFiles。ブランチ全体を比べる時は:DiffviewOpen main...HEADです。比較先の名前は案件に合わせます。

この画面では、説明用サンプルの追加差分を開いています。自分のプロジェクトでは、そのリポジトリの差分を読みます。
気になった点はREVIEW.mdへ、次の形で書きます。
## 指摘
- 対象ファイル・行:
- 気になる動作:
- 期待する動作:
- 修正後に確かめること:
右のAIへ「REVIEW.mdの指摘を読んで直して」と渡す。ファイルに残るので、チャットをまたいでも指摘を追えます。
今回作ったWebサンプルでは、WindowsとWSLそれぞれで構文チェックと2件のテストが通りました。WPFではWindowsでのビルドと共有計算処理の2項目のスモークチェックが通り、その後にウィンドウも起動しています。サンプルの計算確認と、実際の案件の画面遷移・保存処理の確認は、それぞれ必要です。
補足:起動コマンドもプロジェクトごとにまとめるなら
ここまでの操作は、Herdrと各AIのコマンドで使えます。既定AIやビルドコマンドをGitでそろえたい時のために、任意の起動補助も残しました。
任意の .dev 設定・起動スクリプト・AI設定の詳細
毎回長いコマンドを打ちたくない時のために、任意の起動補助も作りました。採用する場合だけ、各プロジェクトに次を足します。
my-project/
├─ .dev/
│ ├─ project.json 既定AI・ビルド・テスト
│ └─ dev.mjs 共通の起動コマンド
├─ .codex/config.toml Codex固有の共有設定
├─ AGENTS.md AIへ伝える開発ルール
└─ REVIEW.md 確認点と修正依頼
.dev/project.jsonは今回作った起動スクリプト用のファイルです。HerdrやAIの標準設定ではなく、.devという名前にも特別な意味はありません。この補助の保存先を変える場合は、スクリプトの読み込み先も変更します。既存のタスクランナーがあれば、そちらを使っても構いません。WPFプロジェクトなら、たとえばこうします。
{
"name": "wpf-app",
"entryFile": "README.md",
"requiredTools": ["git", "nvim", "herdr", "node", "dotnet", "agy"],
"defaultAgent": "agy",
"agents": {
"agy": ["agy"],
"codex": ["codex", "--no-daemon"],
"copilot": ["copilot"]
},
"tasks": {
"restore": ["dotnet", "restore"],
"build": ["dotnet", "build", "--no-restore"],
"test": ["dotnet", "test", "--no-build"]
}
}
Web側は、次の設定を使えます。WindowsでもWSLでも同じJSONです。npm ciにはpackage-lock.json、buildにはpackage.jsonのbuildスクリプトが必要なので、実際の案件へ合わせます。
Windows・WSLで使えるWebプロジェクトの .dev/project.json
{
"name": "web-app",
"entryFile": "README.md",
"requiredTools": ["git", "nvim", "herdr", "node", "npm", "codex"],
"defaultAgent": "codex",
"agents": {
"codex": ["codex", "--no-daemon"],
"agy": ["agy"],
"copilot": ["copilot"]
},
"tasks": {
"restore": ["npm", "ci"],
"build": ["npm", "run", "build"],
"test": ["npm", "test"],
"dev": ["npm", "run", "dev"]
}
}
通常起動するAIも変更できます。 defaultAgentを"codex"や"agy"へ変えれば、そのプロジェクトの既定になります。今回の例でWindows側がagy、Web側がCodexなのは初期値です。OSとAIを結び付けて固定していません。既定AIを変えた時はrequiredToolsも使うCLIへ合わせます。
一回だけ変えるなら、設定を保存し直す必要もありません。
node .dev/dev.mjs ai codex
node .dev/dev.mjs ai agy
詳細な起動オプションはagentsの配列へ。テストやプロジェクトファイルの場所が違う時も、そのリポジトリの設定を直せば済みます。
さらにCodex固有の設定が必要なら、プロジェクトの.codex/config.tomlを使えます。公式の設定階層では、信頼したプロジェクトの設定だけが読み込まれます。プロジェクト設定には、推論の強さを指定するmodel_reasoning_effort = "high"の例を入れました。モデルはローカル側の選択を引き継ぎ、認証や個人の絶対パスを含む信頼登録はGitへ置きません。
# プロジェクトの .codex/config.toml
model_reasoning_effort = "high"
起動スクリプトは次の内容です。プロジェクトの.dev/dev.mjsへ保存すると、WindowsとWSLで同じコマンドを使えます。Node.jsの標準機能だけで動きます。
.dev/dev.mjs の全文
#!/usr/bin/env node
// Dependency-free project commands. Copy this file to PROJECT/.dev/dev.mjs.
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
const root = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
const config = JSON.parse(fs.readFileSync(path.join(root, ".dev", "project.json"), "utf8"));
const windows = process.platform === "win32";
const platform = windows ? "windows" : (process.env.WSL_DISTRO_NAME || /microsoft/i.test(os.release()) ? "wsl2" : "linux");
const argv = process.argv.slice(2);
const dryRun = argv.includes("--dry-run");
const args = argv.filter((arg) => arg !== "--dry-run");
const action = args[0] || "help";
const session = `${config.name}-${createHash("sha256").update(root).digest("hex").slice(0, 8)}`;
function command(value) {
if (!Array.isArray(value) || value.length === 0 || value.some((v) => typeof v !== "string" || /[\0\r\n]/.test(v))) {
throw new Error("Commands must be nonempty arrays of single-line strings.");
}
return value;
}
function run(value, capture = false) {
const cmd = command(value);
if (dryRun) {
console.log(JSON.stringify({ cwd: root, command: cmd }));
return { status: 0, stdout: "" };
}
// PowerShell resolves Windows .cmd/.ps1 shims. Pass arguments as data,
// never interpolate the project's command into a shell program.
const executable = windows ? "pwsh.exe" : cmd[0];
const childEnvironment = { ...process.env, GOROGORO_DEV_ARGV: JSON.stringify(cmd) };
// A user-local SDK can coexist with a machine-wide runtime-only install.
// Prefer the explicitly configured SDK root for all child processes.
if (windows && process.env.DOTNET_ROOT && fs.existsSync(path.join(process.env.DOTNET_ROOT, "dotnet.exe"))) {
childEnvironment.PATH = `${process.env.DOTNET_ROOT};${process.env.PATH || ""}`;
// Windows environment names are case-insensitive; Node must send one key.
for (const key of Object.keys(childEnvironment)) {
if (key !== "PATH" && key.toLowerCase() === "path") delete childEnvironment[key];
}
}
const childArgs = windows
? ["-NoLogo", "-NoProfile", "-Command",
"$a = @(ConvertFrom-Json $env:GOROGORO_DEV_ARGV); $exe = $a[0]; if ($exe -match '^[a-zA-Z0-9_-]+$' -and (Get-Command ($exe + '.cmd') -ErrorAction SilentlyContinue)) { $exe += '.cmd' }; $rest = @($a | Select-Object -Skip 1); & $exe @rest; if ($LASTEXITCODE) { exit $LASTEXITCODE }"]
: cmd.slice(1);
const result = spawnSync(executable, childArgs, {
cwd: root,
env: childEnvironment,
encoding: "utf8",
stdio: capture ? "pipe" : "inherit",
timeout: capture ? 15000 : undefined,
windowsHide: true,
});
if (result.error) throw result.error;
if (result.signal) throw new Error(`Command stopped by ${result.signal}`);
return result;
}
try {
if (!/^[a-z0-9][a-z0-9-]{0,39}$/.test(config.name)) throw new Error("Invalid project name.");
if (action === "help") {
console.log("node .dev/dev.mjs doctor | start | editor | ai [name] | task [name]\nAppend --dry-run to inspect without execution.");
} else if (action === "doctor") {
console.log(`Project: ${config.name}\nOS: ${platform}\nSession: ${session}`);
let failed = false;
for (const tool of config.requiredTools || ["git", "nvim", "herdr", "node"]) {
const result = run([tool, "--version"], true);
console.log(`${tool}: ${result.status === 0 ? "OK" : "MISSING/ERROR"}${result.stdout?.trim() ? ` (${result.stdout.trim().split(/\r?\n/)[0]})` : ""}`);
failed ||= result.status !== 0;
}
process.exitCode = failed ? 1 : 0;
} else if (action === "start") {
if (process.env.HERDR_ENV === "1") throw new Error("Already inside Herdr. Split a pane or open a workspace instead.");
process.exitCode = run(["herdr", "--session", session]).status ?? 1;
} else if (action === "editor") {
process.exitCode = run(["nvim", config.entryFile || "."]).status ?? 1;
} else if (action === "ai") {
const name = args[1] || config.defaultAgent;
if (!Object.hasOwn(config.agents || {}, name)) throw new Error(`Unknown agent: ${name}`);
process.exitCode = run(config.agents[name]).status ?? 1;
} else if (action === "task") {
const name = args[1];
if (!Object.hasOwn(config.tasks || {}, name)) throw new Error(`Unknown task: ${name}`);
process.exitCode = run(config.tasks[name]).status ?? 1;
} else {
throw new Error(`Unknown action: ${action}`);
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
起動補助を使う場合の操作
起動補助を使う場合は、プロジェクト直下へ.devフォルダを作り、上のJSONと起動スクリプトを保存します。最初は次の順に確認します。
node .dev/dev.mjs doctor
node .dev/dev.mjs task restore
node .dev/dev.mjs task build
node .dev/dev.mjs task test
node .dev/dev.mjs start
doctorは今いるOSと必要なツールの起動を調べ、OSの種類だけで起動を拒否しません。OKが出た後に、AIのログインと実際のビルドも確認します。日々の起動は、プロジェクトへ移動してnode .dev/dev.mjs startです。毎回restoreやテストから始める必要はありません。
最後のstartでHerdrへ入り、左ペインでnode .dev/dev.mjs editor、Ctrl+bを離してvで右へ分割し、右ペインでnode .dev/dev.mjs aiを実行します。Codexを指定するなら末尾にcodexを付けます。
切断後に戻る時も、同じプロジェクトでnode .dev/dev.mjs startを実行します。この補助が付けたセッション名は、doctorのSessionで調べられます。
この補助は、プロジェクトごとに別のセッションを作ります。複数プロジェクトのAIを一つのAgents一覧で見たい時は、前半のherdr --session devで入り、Spacesを追加する使い方にします。
Gemini CLIで共通の指示ファイルを読む場合
ルールの本文を何組も書き直したくなければ、AGENTS.mdへ集めます。Gemini CLIでは、プロジェクトの.gemini/settings.jsonへ次を既存設定とマージする方法があります。
{
"context": {
"fileName": ["AGENTS.md", "GEMINI.md"]
}
}
AIの状況を見て、必要な画面へ戻る
使うOSもAIも、その時の作業に合わせて選ぶ。HerdrでAIごとの状況を見て、質問や確認が必要な画面へ戻る。実装結果はNeovimで読み、Git差分とテストで確かめ、修正してほしい点はREVIEW.mdへ残します。WindowsとWSLで、この進め方を共通にします。
今回Neovimへ追加したのは、差分を読むためのDiffviewです。エディタ内でAIとやり取りする操作も必要になったら、sidekick.nvimやCodeCompanionの導入を考えます。まずはHerdrでAIを動かし、Neovimでその成果を確認する形で使い始めます。

コメント