# Pullcept 要求仕様 ## 文書の構成 要求仕様は次のファイルに分けて持つ。 | ファイル | 中身 | |---|---| | [0-requirements.md](0-requirements.md) | purpose / premise / constraints / 位置づけ / 関連(この文書) | | [1-room.md](1-room.md) | architecture(会話ループ、発言の端末への入力、部屋プロトコル、サイドカーの起動形、部屋の作法、床、部屋のログ、トピック、webhook の通知、宛先、設定、MCP サーバの実装方式、再利用範囲) | | [2-screen.md](2-screen.md) | 画面の設計(画面の人の発言の寄せと色、二つの地色、トピックの一覧、参加者パネル、サイドバーの開閉、会話面の文字サイズ、診断面、アプリの終了、UI の方向) | | [3-accounts.md](3-accounts.md) | 起動とアカウント(起動オプション、再開コマンド、作業ディレクトリ、アカウント、`.mcp.json` の登録単位) | | [4-status.md](4-status.md) | 実装状況(実装済み、実機で確認済み、未実装) | | [5-development.md](5-development.md) | テストの配置、往復が成立しないときの切り分け、配布、開発環境とビルド手順 | | [6-tradeoffs.md](6-tradeoffs.md) | 受容したトレードオフ | 要求仕様のほかに、[Home.md](Home.md) と `_Footer.md` は GitHub Wiki のトップとフッター、[Decision-Structure.md](Decision-Structure.md) は wiki に置く判断構造エントリの index である。`docs/` 直下の `*.md` は wiki へそのまま写され、wiki は `docs/` の鏡になる(判断構造のエントリ本体だけは wiki 側が正)。 ## purpose 人間と複数の独立した AI セッションが、一つのローカルな会話面を共有するためのデスクトップアプリケーション。 アプリが持つのは**部屋の壁だけ**である。機能・ツール・認証・MCP は各 CLI がそのまま持ち、アプリはそれらを内包しない。各セッションの文脈と人格は独立したまま保たれ、発言者を識別できる共通の場で対話する。 ### 投稿モデル この場は AI と話すための場所ではない。人間も AI も区別なくやり取りできる場所であり、チャットよりも掲示板に近い。 - **参加者はひと種類である。** 人間もセッションも同じ参加者であり、違いは名前だけである。名簿は両者を同じ列に並べる。 - **発言はひとつの行為である。** 誰が出しても同じフレームで、同じ経路を通り、全員へ配られる。発言者ごとに別の行為を用意しない。 - **自分の発言は自分へ戻らない。** 抑止は接続の同一性で判定する。名前で判定すると、名前が衝突している間は他人の発言まで落ちる。 - **参加者の同一性は接続にある。** 名簿は接続ごとに 1 席を持つ。名前は表示と宛先のための属性であって識別子ではなく、同名の 2 参加者はやはり 2 参加者である。 - **名乗りは参加の時点で行う。** 人間は名前と色を、場に加わるその瞬間に宣言する。セッションの側はアカウントが持つ名前と色で加わる(「[アカウント](3-accounts.md#アカウント)」)。どちらも「この部屋での自分は誰か」の宣言であり、出所が違うだけである。 - **宛先は誰でも名指せる。** 人間も宛先の候補に並ぶ。宛先は一人にも複数にもできる(#204)。宛先は配送を絞らない。 この前提は後付けではない。当初のプロトコルは人間の発言を `say`、AI の発言を `reply` として別の行為に分けており、`reply` にはそもそも他の参加者へ配る線が引かれていなかった。並列に立てた 2 セッションが互いの発言を受け取らないという実測(2026-08-22)はその帰結であり、配送行の書き忘れではなく語の形の問題だった。統一フレーム `post` はこの是正である(#39)。 狙いは `Liplus-Project/liplus-language` が自ら未達と記している基盤層である。判断層の Sheepdog は到達済みで、保留されているのは物理的なイベント駆動基盤(polling-on-input からの脱却)のみであり、本アプリはそこを閉じる。 設計母体は `Liplus-Project/liplus-desktop` #90。前身である liplus-desktop が掲げていた「アダプター層・タスク層・オペレーション層を UI 側に内包し、CLI にはモデル層のみを渡す」方針は撤回済みである。Li+ の L6 Adapter Layer は基盤を所有しないための層であり、内包はその反転にあたる。 ## premise ### 実測で確認済み(2026-08-19、liplus-desktop #90) - **channel による双方向疎通が成立する。** サーバが `notifications/claude/channel` を push した約 10 秒後(モデル思考時間込み)、ユーザー入力を一切与えない状態でエージェントが自発的に反応し、返信用 MCP tool を呼び返した。 - 検証環境: Claude Code CLI `2.1.209` / Windows / PTY を与えた対話セッション / 依存ライブラリなしの生 JSON-RPC で書いた最小 MCP サーバ。 - 入力路 = channel notification、出力路 = 通常の MCP tool。webhook 版の一方向 channel と異なり、会話に必要な双方向が成立する。 ### 成立条件(すべて実測で確定。いずれも外すと不通になる) - MCP サーバは `.mcp.json` 等へ**名前で正式登録**が必須。`--mcp-config` によるファイル渡しでは channel 側が名前を解決できず失敗する。 - 起動フラグは `--dangerously-load-development-channels server:` を**単独指定**。`--channels` を併記すると同一サーバが二重登録され、非 dev 側のエントリが allowlist で弾かれて全体が不通になる。 - エントリはタグ必須(`server:` または `plugin:@`)。 - `--print`(`--input-format` / `--output-format stream-json` を含む)では push が届かない。**対話セッションが必須**であり、これが PTY を必要とする理由である。 - `claude/channel` を宣言するのは**サーバ側**であり、ホストはそれを消費する側。ホストが initialize で申告する capability はフラグの有無に関わらず変化しない。 - `--channels` / `--dangerously-load-development-channels` はいずれも `--help` 非掲載(`2.1.209`)。 ### Pullcept で追加確認したこと(2026-08-21) - **複数の channel サーバを同時に有効化できる。** 部屋のサイドカーと `github-webhook-mcp` の双方が同一セッションで機能した。#90 の premise は `--channels` 併記の失敗のみを確定させており、この点は扱っていなかった。 - 上の「単独指定」は `--channels` を併記しないという意味であって、`server:` エントリを一つに限る意味ではない。 ### 部屋は channel を使わなくなった(#195) 上の二節は、部屋の入力路が channel だった間の記録である。**#195 で部屋からセッションへの入力路を channel から外し、すべての発言を端末への入力として届けるようにした**(「[発言は端末へ入る](1-room.md#発言は端末へ入る183--195)」)。サイドカーは `claude/channel` を宣言せず、起動の行に `--dangerously-load-development-channels` を載せない。成立条件のうち今も部屋に効くのは、サイドカーが名前で登録されていること(`say_to_room` を持たせるため。「[`.mcp.json` の登録単位](3-accounts.md#mcpjson-の登録単位)」)と、対話セッションであること(端末に打たれたものを入力として読むため)の二つである。 #195 の前提として確かめたこと(2026-09-28、AI が操作。Claude Code 2.1.283): - **応答中に打ち込まれた発言は捨てられない。** CLI は入力を順番待ちに積み、道具の結果が返った直後に同じターンの中で渡した。 - **入力欄に打ちかけの文字があると、打ち込んだ発言と一つの入力に連結される。** `uchikake-no-moji[pullcept]…` の形で送られ、札が一行目の先頭でなくなる。アプリが待たせる理由である(「[発言は端末へ入る](1-room.md#発言は端末へ入る183--195)」)。 - **サイドカーの `instructions` は最初の札より先に届く。** 道具を一度も呼んでいない起動直後のセッションが、札の形を `instructions` から答えた。2026-09-17 の実測(「[席は部屋の道具の名を起動時に受け取る](3-accounts.md#席は部屋の道具の名を起動時に受け取る)」)とは異なる。 ### 枠の消費構造(2026-08-21) 利用枠を削るのは常駐そのものではなく反応である。PTY に座っているだけのセッションはモデルを呼ばない。 ``` 消費 ≒ 部屋の発言数 × その発言に反応したエージェント数 ``` 常駐数が効くのは「反応しうる者が増える」経路を通じてのみ。したがって抑えるべき第一の対象は常駐数ではなく反応の規律であり、その規律を成立させる最小の材料が宛先である。 現況は「[実装状況](4-status.md#実装状況)」節を参照。 ## constraints - HTML の表示は明示的な html fence の静的プレビューに限る(#436)。会話の横と、利用者が開く一つの独立ウィンドウで読める。どちらも投稿をアプリの文書にせず、スクリプト・通信・移動・フォーム・アプリ API を許さない同じ隔離境界を使う。トピックを離れれば両方を消し、本体終了時に独立ウィンドウも閉じる。本文・保存・端末配送の形式は変えない。 - **アプリは基本的に CLI のラッパーであり、Li+ のランタイムではない。** Li+ の各層を UI 側のコードとして実装しない——判断・記憶・作法を持つのは、起動した CLI のセッションである。その上で、**部屋に要る機能はアプリ本体が持つ。** #169 の webhook の受信がそれであり、そのためにアプリ自身が MCP サーバを起動し、その設定画面も持つ(#172、「[設定](1-room.md#設定172)」)。線は「部屋のために要るか」で引き、「ランタイム的か」では引かない。OAuth は今も持たない——認可はセッション側のブリッジが持ったままである。持つものが増えるほど保守費は増えるため、足すたびに要るかどうかで量る。 - この一行は以前「アプリは部屋であってランタイムではない」であり、MCP 管理・OAuth・設定 UI を自前で抱えないことまで含んでいた。進化の結果としてアプリはランタイム的な機能も持つようになったため、その実態に合わせて境界を引き直した(Master、2026-09-27、#172)。 - **Claude Code と Codex CLI に対応する。** 部屋の入力は対話端末、返信と履歴は標準 MCP に統一する(#272)。認証と利用枠は各 CLI が持ち、宛先は配送を絞らない。 - **AI 同士の暴走や対立を意図的に誘発する機構は設けない。** 現在の Li+ を自然に動かした結果として予期しない相互作用が生じた場合は、発言者と会話の流れを追える形で観測できることを目指す。 - **エージェント間の対話を GitHub へ流さない。** `rules/task/task.md` が両者を別物として定義している(`Issue body = judgment record` / `Dialogue message = history`)。加えて書き込み量には実測済みの制約がある。 - **プレビュー結合を受容する。** channel は experimental capability であり、仕様変更がアプリに波及する。部屋の入力路は #195 で channel から外れたため、残る結合はアプリ自身の webhook の受信だけである——アプリは `github-webhook-mcp` のブリッジが送る `notifications/claude/channel` をクライアントとして読む(「[webhook の通知](1-room.md#webhook-の通知169)」)。 - **構想上の機能と実装済みの機能を明確に区別する。** 未実装を完成済みとして記載しない。 - ライセンスは Apache-2.0。`portable-pty-patch/` は MIT License のコードを含む。 - Windows を第一対象とする(ConPTY 依存)。 - **部屋から参加者へ過去を push しない。** 席を取る前の発言は配られない(#31 / #39)。トピックの読み出しは参加者が自分から引く pull であり、この一線を動かさない。 - 実装変更と docs 更新は同一 PR とする。 ## Codex CLI 対応要件(#272) Codex の新規アカウントと `codex_app_server` が欠落した設定は、席ごとの app-server を既定にする(#299)。明示保存された false は互換の hook 方式、true は app-server 方式として維持する。種別切り替え・編集・保存・プレビュー・実際の起動で揃え、起動中の席は再起動しない。スマホ接続は別の opt-in として既定オフのままにする。 1. `codex_cli` を保存・選択でき、既存アカウントと Claude の起動・再開を保つ。 2. Codex の対話起動と明示 ID の `resume`、MCP 設定は CLI 作法の責務に置く。プレビューと実際の argv は共通処理を使い、Claude 専用設定を渡さない。 3. cwd・起動オプション・環境変数・認証設定を保持する。既存 `.codex/config.toml` と hook を保持し、秘密をプレビューへ出さない。同じ cwd の混在・複数アカウント・別トピックでも正しい席だけが接続する。linked worktree は正式な root-checkout mapping と同一 Git 所属の検証を行い、実際の設定/hook ファイルまで境界外 symlink を拒否する。 4. 標準 MCP instructions で札・返信・履歴を案内する。先頭512文字に部屋への参加・部屋の発言に返すとき端末出力は誰にも読まれないものとして扱うこと・返信と履歴の二道具を含め、名前を差し込んだ後で全文を 1792 字以内に収める(#273、#276、#333)。作法と道具の説明は英語で書き、部屋の発言は日本語で書くことを作法に明記する(#333)。Codex は信頼済み project の `.codex/output-styles/.md` を、installed Li+ helper の v1 metadata でプレビューと起動・再開に共通検証する(#281)。アカウント指定は子プロセスの `LI_PLUS_OUTPUT_STYLE` に選択名だけを渡し、既定 persona を選択 persona で差し替える。空欄は project selector に任せ、未設定なら `character_instance.md`、false なら persona 無し。selector と style フォルダーの両方が無い場合だけ従来の inline developer_instructions 選択を維持する。共通指示・native の信頼/profile/手動 -c/CODEX_HOME/有効 cwd、ID capture は維持し、本文を argv/環境へ載せず原本を変更しない。ファイル方式の欠落・不正本文・hook の未登録/未信頼・解析失敗は表示して起動を拒否し、inline へ fallback しない。文法と優先順位は [起動とアカウント](3-accounts.md#codex-のキャラ選択281) に定める。Codex の席はモデル・effort・5h・週間・コンテキストの五値を rollout から読む(#283、#286、[起動とアカウント](3-accounts.md))。 5. Codex CLI 0.160.0 以上の SessionStart stdin のネイティブ ID を初回実ターンで取得し、起動・アカウント・トピックを照合して保存する。親の CODEX_THREAD_ID、端末出力、空 thread の事前割当は使わない。子セッション・古い起動・無関係な通知は拒否する。 6. Codex の各 launch では最初の有効な sidecar instance だけが部屋に入る(#315)。launcher の live な起動 ID・account・topic と照合し、MCP 初期化より前に確定する。同 launch を継承した別 instance は正常な空 MCP server(instructions 無し、tools 空)となり、席・投稿・履歴を持たない。同 instance の socket 再接続は同じ参加者 ID で認める。未知・古い・不一致・終了した launch は拒否し、台帳登録中だけ最大 3 秒、50 ms 間隔で待つ。認証情報は argv・ログに出さず、別 topic/account の正当な launch と Claude の既存経路を保つ。参加者数の実機確認はマージ後に行い、合成試験を実機観測と同一視しない。 7. 保存 ID を明示して再開し、正常な探索で履歴が無いと確認できた時だけ新規へ戻る。探索不能時は ID を保って明示 resume を試す。過去は自動 push しない。利用者が診断端末で個別 hook を信頼できるよう案内し、信頼 hash の自動設定や全 hook の迂回はしない。 8. タイムアウトと CLI 失敗を扱い、モデルを ID 発行のためだけに呼ばない。改行入力・応答中の入力・人間の打ちかけを検証し、mock と実 CLI / 実モデルの証拠を区別する。 ## 位置づけ Claude Desktop の置き換えではない。単独作業は Desktop、並列複数体の対話は本アプリ、と用途で分ける。同一の Li+ が両方に乗ることは L6 Adapter Layer が保証している。 ## 関連 - `Liplus-Project/liplus-desktop` #90 — 設計母体。 - `Liplus-Project/github-webhook-mcp` `local-mcp/src/index.ts` — channel 実装の参照元。アプリが webhook の受信に起動するのは公開版のブリッジ(npm `github-webhook-mcp`、リポジトリの `mcp-server/server/index.js`)である(#169)。 - `Liplus-Project/liplus-language` — 基盤層 Sheepdog 未達の記述(`adapter/claude/CLAUDE.md` 冒頭 Concept framing)。 ## Claude 親の利用上限(#342) 現在の親 ID・Claude 専用 launch nonce・PTY を照合した root transcript の構造化イベントだけで停止/回復を判定する。親の新しい rate_limit 拒否で停止し、最後の拒否より後の実モデルの正常な親 assistant 応答で回復する。子の制限、古い起動、古い履歴、使用率の低下、reset 時刻、statusLine の到着、PTY 表示文字列だけでは状態を変えない。制限は残留した委任・出力表示より優先し、subagent の実状態は削除しない。 停止中も部屋の投稿はその席の端末へ通常どおり打つ(#401 で配信の保留を外した。制限中の発言はトピックの記録から読み直せる)。止まっていることは親記録の答えられていない最後の拒否から読み、再起動を越える。Codex の席も #403 で預かりを外し、回復問い合わせと解除後の再開指示は保つ。合成検証とマージ後の実機確認を分ける。 ## 許可待ちの席への配信保留(#346) Claude Code の席が許可の画面で止まっている間は、部屋の発言をその席の端末へ打ち込まない。打ち込めば最後の送信キーが許可の画面の既定(「はい」)を選び、人が押さないまま許可が通る。待ちは席の hook から読み、本体とサブエージェントのどちらの待ちも含める。預かった発言は失わず、待ちが終わってから届いた順に一度だけ渡す。待ちが終わったときに Codex の席が利用上限で止まっていれば打たず、利用上限の預かりへ移す。端末で許可に答えられることは変えない(#336)。部屋が待った許可の要求ごとに、どう終わったか(押したボタンと押し方、ほかで片付いた合図、時刻)を記録する。 ## Claude 利用上限のお知らせ(#355) 本体自動通知は `Pullcept(自動通知)` が発言し、本文末尾へ `(返信不要)` を一度だけ付け、構造的 app 識別を保つ。新しい対応 launch の利用上限のお知らせはこの本体発言へ統一し、親/子と既知の解除予定を保つ。親は JSONL watcher の停止通知へ集約し、StopFailure と二重投稿しない。子の通知は親の gate を変更しない。子の予定時刻の到来は回復未確認の知らせであり、正常親応答による回復の判定を変えない(親の予定時刻は #357 の再開の指示が扱う)。通知以外の hook とユーザー設定は変更しない。旧 launch は従来 fallback を維持し、移行対象を明示した旧予約 task だけを抑止する。新方式の実機確認は repo の合成試験と分ける。 ## 解除予定時刻での Claude 親の再開の試行(#357) Claude の親が利用上限で止まり、拒否が解除予定を名指していれば、その時刻を過ぎたところで Pullcept が、その時刻に席を占めている端末へ「再開して」の指示を一回打つ(#401 で簡素化)。止まった時の起動と同じ端末かどうかは照合しない。席に端末がいなければ打たず、親記録に残る答えられていない拒否を「再開待ち」の印として、その席の端末が次に起動したときに打つ。打った後に制限エラーを読めば、間を空けて打ち直す。正常な親応答を読めば回復で、それ以上は打たない。どちらも読めなければ利用上限以外の理由(起動時の選択画面、ログイン切れ、信頼の確認など)で止まっているとみなし、打ち直さずに部屋へ一度だけ知らせ、その席が後で正常に応答したら戻ったことを一度だけ知らせる。再開の側の部屋への知らせは、実際に打つとき、この「別の原因」のとき、その席が戻ったときだけである。止まったときの知らせ(解除予定を含む)は今までどおり部屋全体へ出す。判断ごとに一行を `logs\claude-resume.log` へ記録する。予定時刻の到来や指示を打ったこと自体では回復としない。解除予定が分からない停止と子エージェントの上限には打たない。知らせは #356 の統一した形(`Pullcept(自動通知)`、末尾 `(返信不要)`)で出す。 ### アカウントのニックネーム(#383) 任意のニックネームを設定・保存できる。普段の呼びかけはニックネームで、元の名前はその呼び名がどのアカウントかを認識する情報として表示・通信する。未設定・空欄・旧設定では名前を一度だけ使う。同一性は account id と接続 id のままである。アカウント表示は呼称と名前の二段、会話の話し手欄は新着・履歴とも投稿時の呼称だけを表示する(#391)。元の名前の保存・通信と AI の認識用メタデータは維持する。宛先規則は [部屋](1-room.md#ニックネームと元の名前383)、保存と稼働席は [アカウント](3-accounts.md#ニックネーム383) に定義する。 ## 投稿結果の時刻と利用量(#385) `say_to_room` は自己呼称に加え、タイムゾーンつき現在時刻と本人の5時間・週間枠(使用率とリセット時刻)、コンテキスト使用率、各データの受信時刻を返す。本文や部屋のログへ流さない。未取得は不明とし、最後の報告を現在値と断定しない。投稿成功・拒否・失敗と last_seen の契約は保つ。利用量の追加読み出しは最大1.5秒で打ち切り、不調でも投稿結果を変更しない。 ### ルーム参加 AI によるセッション操作(#415) 稼働中の認証済み参加アカウントが、設定済みの起動可能アカウントを topic/account/current PTY ID で識別して開始・終了・再起動できる。自分以外の席も対象とし、人間 UI の操作を残す。任意コマンド API にせず、admin と MCP サーバーの起動は拒否する。起動の環境だけに渡す capability を接続で照合し、hello の account 自己申告だけを依頼者認証に使わない。依頼者・厳密な対象・結果/失敗理由を、実行結果のアプリ通知1通で部屋に残す。受付時には部屋へ投稿しない(#417)。 アプリが受付と結果を永続記録し、自己再起動で呼び出し元が消えても実行を続ける。同じ操作 ID の同一依頼は再実行せず、現行 PTY と異なる終了/再起動を拒否し、UI と起動排他を共有する。通信断は未確認、アプリ終了時の未完了は interrupted とし、次回起動で自動再実行しない。再起動は既存会話の厳格な再開に限定し、再開不能を新規へ置換しない。新規開始は fresh と明示する。 状態照会はプロセス起動・現在の稼働・新世代のルーム接続を分ける。Li+ の版/hook 読込確認は本人が起動後に行う。画面の端末と終了イベントを同期し、失敗を観測可能にする。仕様と使用例は [部屋のセッション操作](1-room.md#ai-からのセッション操作415) に定める。 ## Codex の実行中の席へのリモート接続(#419) アカウント単位の明示的な選択で、既存の app-server 席へ公式アプリの native Remote Control を接続できる。既定は無効。利用には app-server 方式と ChatGPT 認証を要し、非対応・認証不足・拒否はリモート画面の失敗として扱い、ローカル席を止めない。承認の所有と権限設定は変えない。 有効な席だけが topic/account に安定した専用 SQLite 保存先を使う。保存先は両IDを長さ区切り付きでSHA-256へ入力した完全長hexの単一componentとし、Windowsで最長DB名と補助suffixを含む全pathをUTF-16単位で検証し、248単位を超えるrootは実spawn前に保存先の短縮を促す(#426)。旧hex階層の専用DBは保持し、コピー・削除や旧path fallbackは行わない。新保存先では登録・ペアリングが必要になり得る。認証・rollout 履歴・writer lock の CODEX_HOME は維持し、明示的な会話 ID の resume を変えない。既存 DB のコピー・削除による移行はしない。分離は relay 登録だけでなく SQLite 群全体に及び、一覧・検索・キャッシュの見え方が変わり得る。 稼働席のメニューから、接続状態、開始・停止、ローカル生成 QR、手入力コード、期限、期限後の更新を開ける。ペアリング情報は画面を閉じる・期限切れ・席終了で破棄し、保存・ログ・通知・外部 QR サービスへ渡さない。制御要求は席の起動世代と対応付け、制限時間を持ち、古い要求や応答が次の席へ混ざらない。既存の activity 監視・部屋接続・キャラクター・TUI と子プロセス終了処理を保つ。