# cy サブコマンド — エージェント一覧・閲覧・送信 `cy`(および `ay` / `agent-yes`)に、稼働中のエージェントを別端末から 操作するためのサブコマンドを追加した。koho の `terminal-ws` の設計思想 (セッション一覧、`@xterm/headless` による描画、キーで識別する入力)を ファイルベースで再現したもので、デーモンは持たない。 ## コマンド一覧 ``` cy ls [keyword] [--all] [--cwd ] [--json] cy read [--last N|--head N|--range A:B|--before-line L [--limit N]] [--latest] cy cat # read の全文エイリアス(窓指定なし=全行) cy tail [-f] [-n N] [--latest] # 末尾 N 行(既定 96)、-f で追従 cy head [-n N] [--latest] # 既定 N=96 cy send [--code=enter|esc|ctrl-c|ctrl-y|ctrl-d|ctrl-\|tab|none|raw:0xNN] cy attach [--escape ctrl-\] [--latest] [--cwd ] cy stop [--method=auto|graceful|double-ctrl-c] ``` ## キーワード解決順 `` は次の順で照合する。最初に一致した条件で確定する。 1. **PID 完全一致**(数字のみ) 2. **CWD の部分一致**(大小無視) 3. **CLI 名の完全一致**(`claude` / `codex` 等、大小無視) 4. **プロンプト本文の部分一致**(大小無視) `ls` は一致したものを **すべて** 表示する。`read` / `tail` / `head` / `send` は **複数一致したらエラー** にする(候補を最大 10 件表示)。 `--latest` を渡すと曖昧でも最新のものを採用する。 ## レジストリ `~/.agent-yes/pids.jsonl` を **TS / Rust 共通のグローバルインデックス** と して使う。Rust 実装はもともとこのファイルに `pid_store` で書いており、 TS 側 `PidStore` も `register` / `updateStatus` 時に同じファイルへ ミラーするようにした(snake_case スキーマで Rust とラウンドトリップ可 能)。 `cy ls` は次の二系統を読み、`pid` でマージ(後勝ち)する: - `~/.agent-yes/pids.jsonl`(クロスランタイム) - `/.agent-yes/pid-records.jsonl`(TS 旧来形式、過去の TS エージェントを後方互換で拾うため) 死んでいる PID と `status: exited` のレコードは既定で除外する。`--all` で履歴も含めて表示する。 レジストリは追記専用 JSONL のため、長期間運用するとイベント行が 肥大化する。新しいエージェント起動時に行数が 500 を超えていたら **自動でコンパクション** が走り、`pid` ごとに 1 行へ畳む(atomic rename・ロック付き、ベストエフォート)。コンパクション時、`status: exited` かつ既に死亡している PID はまるごと破棄される。手動で起動 したい場合は `maybeCompactGlobalPids()` を直接呼び出してもよい。 ## `cy read|tail|head` の描画 各エージェントは生 PTY 出力を `.raw.log` に追記している(TS: `/.agent-yes/.raw.log`、Rust: `~/.agent-yes/.raw.log`)。 読み出し時には `@xterm/headless` の `Terminal` に丸ごと書き込み、`buffer.active` を 1 行ずつ `translateToString` で取り出すことで、カーソル移動・行内 更新(Claude のスピナー、進捗表示など)を **最終的な画面状態** として 解決した上で出力する(`renderRawLogLines`)。スクロールバックは 5 万行を 確保する。 ### ページネーション(`cy read`) ログ途中からの再描画は PTY のカーソル移動・画面クリア・行折り返しを 壊すため不可能。よって **一度全文を描画し、描画後の行配列を窓で切り出す** 方式を取る(`resolveReadWindow`)。窓は描画済みスクロールバックに対する range/limit 操作であり、「過去のその瞬間の画面」ではなく **最終状態の行** を返す点に注意。優先順位(先勝ち): 1. `--range A:B` — 1 始まり包含の明示窓 2. `--before-line L [--limit N]` — 行 L の **直上** で終わる N 行のページ 3. `--head N` / `--last N` — 先頭 / 末尾 N 行 4. mode 既定 + `-n` — `tail`/`head` は末尾/先頭 N(既定 96)、`cat`/`read` は全行 静的読み出しのフッターは、現在表示の先頭行を使った **ページ送りカーソル** `cy read --before-line <先頭行> --limit <表示行数>` をそのまま出力する (上にさらに行がある場合のみ)。これを打てば 1 つ上のページに遡れる。 `-f`(追従)時はページネーションを無視し、初期コンテキストを出してから ライブ差分を流す。リモート読み出し(`token@host:port:keyword`)も同様に 窓指定は無視する。 `@xterm/headless` のロードに失敗した場合は ANSI 制御文字を正規表現で 除去するフォールバックに切り替わる(プラットフォーム互換のため)。 ## `cy send` と FIFO IPC 各エージェントは起動時に `~/.agent-yes/fifo/.stdin`(Rust)あるいは `/.agent-yes/fifo/.stdin`(TS)に名前付きパイプを作成し、 `PidStore` の `fifo_file` 欄に登録する。`cy send ` は このパスを引いて、メッセージ + 末尾コード(既定 `\r` = Enter)を書き込む。 Rust 側はパイプを **自プロセスで O_RDWR で開いたまま** にしているため、 外部の書き手が close しても EOF が立たず、`cy send` を何度でも繰り返し 呼べる(koho の `terminal-ws-lib.ts` と同じ手法)。受信側ではバイトを ユーザの stdin と同じ `stdin_tx` チャンネルへ流すため、`/auto` 検出・ `Ctrl+C` 処理・`stdin_ready` ゲートはそのまま適用される。 `--code=` の値: | 値 | 送信される末尾バイト | | --------------------------------- | -------------------------- | | `enter`(既定) / `cr` / `return` | `\r` | | `esc` / `escape` | `\x1b` | | `ctrl-c` | `\x03` | | `ctrl-y` | `\x19` | | `ctrl-d` | `\x04` | | `ctrl-\` / `ctrl-backslash` | `\x1c` | | `tab` | `\t` | | `none` / 空 | (何も付けない) | | `raw:0xNN` | 16 進指定の任意の 1 バイト | エージェントが `fifo_file` を持たない(`--stdpush` を使わずに起動した 古い TS エージェント、あるいは FIFO 未対応プラットフォームで起動した Rust エージェント)場合は明示的にエラーになる。 ## `cy attach` — 対話的アタッチ `cy attach ` は、稼働中のエージェントに対して **双方向の TTY 接続** を張る。`cy tail -f` がログを単方向で流すだけなのに対し、 `attach` はローカルのキー入力を FIFO 経由でエージェントに転送し、 ターミナルリサイズも伝搬する。オーケストレータ Claude が fan-out で 複数の subagent を回している最中に、人間が 1 つだけ介入したいとき 向けのコマンド。 接続シーケンス: 1. `log_file` の末尾を `@xterm/headless` でリプレイし、現在の画面を プレーンテキストで描き直す(フルスクリーン TUI を途中で覗いても 壊れたフレームを見ずに済む)。 2. 自分の端末サイズを `~/.agent-yes/winsize/` に書き出し、 エージェント本体に `SIGWINCH` を送る。Rust 側の SIGWINCH ハンドラは このファイルが新しければ(30 秒以内)ローカル ioctl より優先する。 3. ローカルの stdin を raw mode にして FIFO を keep-open でつかむ。 4. `log_file` を `fs.watch` + 100ms ポーリングで追従して stdout へ流す。 `--escape ` で離脱キーを指定する(既定 `ctrl-\` → `\x1c`)。 `--code` と同じ名前辞書を共有しているので `esc` / `ctrl-d` / `raw:0x1d` なども渡せる。離脱してもエージェント本体は **kill されず継続** する。 複数の `attach` クライアントが同時にぶら下がっても問題ない: log file は multi-reader、FIFO も multi-writer。ただし複数の人間が同時にタイプすると バイトが **後勝ち / インターリーブ** になる点には注意。 既知の制限: - フルスクリーン TUI に対する初期画面はリプレイ時点で **色情報が落ちる** (`@xterm/headless` の `translateToString(false)` を経由するため)。 エージェントが次に再描画した瞬間に色は戻る。SIGWINCH 起因の再描画が ほとんどの CLI で発火するため実用上は気にならないことが多い。 - リサイズ伝搬は **Unix のみ**。Windows は SIGWINCH が存在しないため attach そのものが現状非対応。 ## `cy stop` — graceful shutdown `ay send "" --code=ctrl-c` で停止しようとするユーザが多いが、 `claude` / `codex` は **単発 Ctrl+C をキャンセル扱い** にして終了しない。 正しい止め方は次のいずれか: | CLI | graceful 終了 | 強制終了 | | -------- | ------------------------ | ------------- | | `claude` | `/exit` + Enter | double Ctrl+C | | `codex` | `/exit` + Enter | double Ctrl+C | | `gemini` | `/quit` + Enter | double Ctrl+C | | その他 | (既知の graceful 無し) | double Ctrl+C | `cy stop ` はこれを 1 コマンドにまとめる。既定の `--method=auto` は `record.cli` を見て上の表に従って分岐し、未登録の CLI に対しては double Ctrl+C にフォールバックする。明示したい場合: - `--method=graceful` — `/exit` 系を強制(未登録 CLI ではエラー) - `--method=double-ctrl-c` — 強制 Ctrl+C を 2 回(200ms 間隔) `cy send "" --code=ctrl-c` を打った時にも、対象が `claude` / `codex` / `gemini` ならヒント行が表示されて `ay stop` に誘導される。 ## 実装ファイル - `ts/subcommands.ts` — ルータ、`ls` / `read|tail|head|cat` / `send` の本体 - `ts/globalPidIndex.ts` — `~/.agent-yes/pids.jsonl` 共通リーダー / ライター - `ts/pidStore.ts` — `register` / `updateStatus` でグローバルへミラー - `ts/cli.ts` — `--rust` ディスパッチより前にサブコマンドを早期処理 - `rs/src/fifo.rs` — `mkfifo` + RDWR オープン + 後始末 - `rs/src/pid_store.rs` — `fifo_file` フィールド追加 + `register_with_fifo` - `rs/src/context.rs` — FIFO リーダースレッドを `stdin_tx` へ流す - `rs/src/main.rs` — エージェント起動前に FIFO 作成、終了時に unlink ## 関連設計メモ - 計画と代替案の比較: `tmp/cy-multiplex-research.md` (Plan A: エージェントごとに HTTP / Plan B: 中央デーモン / Plan C: ファイル ベース。Plan C を採用した理由を記載) - ROADMAP の項目 10「FIFO IPC」がこの変更で Rust 側もカバー済みになる。 ## 既知の制限 - Windows ネームドパイプ版は TS のみ実装済み(Rust は未対応) - TS / Rust の per-cwd / global インデックス併存は当面そのまま、`cy ls` 読み出し側でマージして対応している。将来的にどちらかへ寄せる可能性あり - `cy send` の同時呼び出しは POSIX の `PIPE_BUF`(4096 byte)以下なら 原子的に書ける。それを超える長文を複数端末から同時に送ると混線する 可能性がある(実用上は十分)