Skip to content

relay.py が同じコンテナの複数の claude セッションを統括する(2 本目以降はホストが起動し tmux で端末を結び付ける) #2346

Closed

Description

@takemi-ohama

現状(2026-11-05、origin/develop e6af6e5)

  • 統括役(2 本目以降の claude を tmux の window で起動し、relay.py ls で一覧し、名前で止める)は ndf v10.27.14 で配布した。仕様は確定仕様 https://github.com/devbasex/ai-plugins/blob/develop/docs/specifications/ndf-relay-multi-session-supervisor.md にある
  • 受け入れ条件 9 の状態の語 running / awaiting_approval / stopped は、実装(plugins/ndf/scripts/relay_lib/hub_client.py)と確定仕様に入っている。relay.py ls --json の各行は name・state・awaiting_approval・sections・cost_usd・at・account を持つ
  • 受け入れ条件 25 の欠け(環境変数 NDF_RELAY_HUB_WAIT・NDF_RELAY_STOP_WAIT と hub/ の受け渡しのファイル、副命令の契約)は relay.py の統括役の環境変数 3 つと hub/ の受け渡しのファイルが契約の宣言に無く、契約の検査も検出しない #2434(relay.py の統括役の環境変数と hub/ の受け渡しのファイルが契約の宣言に無い)が持つ
  • 2026-11-04 に、受け入れ条件 3・8 を確かめる結合テスト plugins/ndf/scripts/tests/test_relay_hub.py::test_spawn_ls_stop_and_hub_handover が開発コンテナ(tmux 3.6)で「待ちが切れた: lroot の区間 2」で落ちたと記録された。2026-11-05 の実測では再現しない(下の表)。落ちた原因は未確認である
  • tmux を使う結合テスト 4 件は needs_tmux(pytest.mark.skipif(shutil.which("tmux") is None))で、tmux が無い実行環境では黙って飛ばされる。リポジトリの根の conftest.py には、テストの置き場所ごとに要る外部コマンドの一覧(REQUIRED_COMMANDS)があり、欠けると既定で失敗にする(読み飛ばすのは NDF_TESTS_ALLOW_MISSING_COMMANDS を付けたときだけ)。tmux はこの一覧に無い
  • CI のテストのジョブ(.github/workflows/pytest.yml の分割の 2 本、ubuntu-latest)に tmux を入れる手順は無い(grep -n tmux .github/workflows/*.yml が 0 件)。runner に tmux が入っているか、4 件が CI で走ったかは未確認(実行 41945018391 の分割のジョブのログを gh run view --job <番号> --log で取ろうとしたが、件数の行を含むログが得られなかった)
実行(2026-11-05) 対象 結果
pytest plugins/ndf/scripts/tests/test_relay_hub.py -k test_spawn_ls_stop_and_hub_handover を 5 回続けて develop の先頭 e6af6e5 5 回とも通過(1 回 3.6 秒前後)
同じテストを、環境変数 TMUX を立てた(tmux の中から打った)状態で 同上 通過
pytest plugins/ndf/scripts/tests/test_relay_hub.py -n 4 同上 48 件通過
pytest plugins/ndf/scripts/tests -n 4 同上 5,780 件通過・4 件飛ばし(8 分 39 秒)
同じテストを 1 回 タグ ndf--v10.27.14 通過

具体例

2026-11-01、利用者は 3 本の開発ループを並列で回すと決めた(Factory の l20261031・根本解決の lroot・開発フローの見直しの lflow。引継ぎ文書 .ndf/handoff/loop-*.md)。今のラッパー(relay.py)では、利用者が端末を 3 つ開き、それぞれで claude を打ち、文書の ndf-next のコマンドを手で貼る必要がある。

  • 3 本のどれが動いていて、どれが止まり、どれが承認待ちかを一目で見る手段が無い
  • relay.py stop は 3 本すべてを止める。1 本だけを止められない
  • 端末を閉じるとそのラッパーが終わる
  • conductor(m26 を回しているセッション)が「3 本のループを始める」と決めても、自分では起動できない
  • 3 本が同じアカウントの 5 時間の枠を 3 倍の速さで使うが、枠を見るのは区間ごとのラッパーで、3 本をまとめて見ていない

この課題が終わると、2 本目の claude を打てばその端末が tmux の window の中の新しいラッパーへ結び付き、relay.py ls で 3 本の状態と承認待ちが 1 つの表で見え、relay.py stop lroot で 1 本だけを止められる。

依頼(原文)

現状はclaude コマンドを実行するとrelay.pyが立ち上がり、それを経由してclaudeが立ち上がります。これを、同じコンテナ内の1本目のclaudeはこのまま、2本目の場合、1本目のrelay.pyが2本目のclaudeを立ち上げ、screen(tmux)には2本目のclaudeに結び付ける、3本目以降も同様、とかいう形にできないでしょうか。

つまり、relay.pyが複数セッションのclaudeを統括してコントロールできるようにするイメージです

(書き手: 利用者、日付: 2026-11-01。起票者が課題の「由来」の節に原文として写したもの)

決定(2026-11-01、利用者): 案 B を採る。tmux を親にし、セッションごとに今の relay.py run を 1 つの window で動かす。ホストは登録・起動・方針・一覧だけを持つ。設計の工程はこの前提で進め、案 A は比べ直さない。

(書き手: 起票者、日付: 2026-11-01。利用者の決定を起票者が記録した文で、利用者の原文ではない。起票者の推定 S1 として承認ゲート 1 で確かめる)

起票者の推定

題・本文のうち、上の利用者の原文に無く起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。

# 条件 推した出所 この要求での扱い
S1 案 B(tmux を親にし、ラッパーごとに今の relay.py run を 1 つの window で動かす。統括役は登録・起動・方針・一覧だけを持つ)を採り、案 A(統括役が全 claude の擬似端末の親になる)は比べ直さない 課題の本文「設計の候補」(起票者が利用者の決定として記録) 前提 1
S2 conductor の応答(ndf-spawn のブロックなど)から名前つきのラッパーを起動できる 課題の本文「あるべき姿」の「起動」の行 受け入れ条件 5
S3 副命令 relay.py spawn --name <名前> -- <最初の入力> で端末の無いラッパーを起動でき、後から relay.py attach <名前> で結び付けられる 同上 受け入れ条件 4・6
S4 端末を閉じてもラッパーは動き続け、カットポイントの再起動(ndf-next)も続く 課題の本文「あるべき姿」の「端末の結び付け」の行 受け入れ条件 7・8
S5 relay.py ls がラッパーごとの名前・状態・承認待ちの有無・区間の数・使った費用・最後に動いた時刻を出す 課題の本文「あるべき姿」の「一覧」の行 受け入れ条件 9・10(費用は取れないとき空の値。前提 5)
S6 relay.py stop <名前>・restart <名前>・attach <名前> を持ち、名前を付けない stop は今どおり全部を止める 課題の本文「あるべき姿」の「個別の操作」の行 受け入れ条件 11〜14
S7 同時に動かすラッパーの数の上限(既定 3)を宣言で持ち、超える起動を理由つきで断る 課題の本文「あるべき姿」の「共有の方針」の行 受け入れ条件 15・16
S8 同時に流すプランの合計の上限(例 6)を、ラッパーをまたいで持つ 同上 未決 1
S9 アカウントの枠と起動の数の上限を、ラッパーをまたいで数える 同上 受け入れ条件 17・18
S10 どのラッパーが承認ゲートで待っているかを一覧と通知に出す(端末の結び付いていないラッパーの承認を取りこぼさない) 課題の本文「あるべき姿」の「承認待ちの知らせ」の行 受け入れ条件 10・19
S11 1 本のラッパーが落ちてもほかと統括役は動き続け、統括役が落ちても動いているラッパーは止まらず、次の claude が新しい統括役になる 課題の本文の旧受け入れ条件 8 受け入れ条件 20〜22
S12 tmux の無い環境では今の 1 本ずつの起動に戻り、そのことを 1 行で知らせる 課題の本文の旧受け入れ条件 9 受け入れ条件 23
S13 新しいコードは共通部品(lib/step_result.py・lib/locks.py・lib/procs.py・lib/run_state.py)を通し、#2335(異常系・ログ・回復を 1 つの実行の核へ集める大方針)の契約(#2336 入口・受け渡しのファイル・設定・環境変数の契約の宣言)の対象に入る 課題の本文の旧受け入れ条件 10 受け入れ条件 24・25
S14 起動・一覧・停止の入出力を、Factory の契約(factory/contracts/ の実行ジョブの指示と進捗の事象)と同じ語と形で持つ。コードは参照しない 課題の本文「Neural Development Factory との関係」 受け入れ条件 26・27
S15 relay.py ls の出力を 1 行の JSON(統一 JSON レポート)と人の読む表の 2 つにする 課題の本文の旧受け入れ条件 3 と、2026-10-31 の利用者の方針(統一 JSON レポート) 受け入れ条件 9
S16 残りの作業は「統括役の結合テスト」である 引継ぎ文書 .ndf/handoff/notice-version-lfactory2.md の 2026-11-05 09:58 JST の行(AI の起動指示。ループ lflow2 の周回 6 の版の予約)と、本文の「現状」の節(起票者、2026-11-04) 受け入れ条件 28〜32
S17 tmux を使う結合テストが、tmux の無い環境で黙って飛ばされないようにする 本文の「現状」の節の「CI は tmux を入れていないため、この結合テストは needs_tmux で飛ばされうる」(起票者、2026-11-04) 受け入れ条件 28・29
S18 2026-11-04 の失敗の原因を突き止めて直す 本文の「現状」の節の失敗の記録(起票者、2026-11-04) 採らない。2026-11-05 に再現しないため(前提 10)。代わりに、再び落ちたときに原因を読める出力を受け入れ条件 31 に置く

目的

  • 同じコンテナで複数の開発ループを並列に回すとき、利用者が端末を開いてコマンドを貼る手間をなくし、どのループが動き・止まり・承認待ちかを 1 か所で見て、1 本ずつ止め・やり直し・結び付け直せる状態にする
  • conductor が自分の判断で別の開発ループを起動できるようにする(人の手を止めない。Value 2)
  • 受け入れ条件 3・8(端末の結び付いていない名前つきのラッパーでも ndf-next で次の区間が起動する)を確かめる結合テストが、手元と CI のどちらでも毎回走り、飛ばされたまま通過と数えられない状態にする(確かめていないものを済んだと記録しない。Value 1)

前提

  • 前提 1: 構成は案 B に従う。tmux がラッパーごとの端末の親になり、各 window で今の relay.py run を動かす。統括役は登録・起動・方針・一覧だけを持ち、各ラッパーの状態機械(区間の切り替え・ndf-next の拾い方・質問シグナルファイル)を変えない(S1。承認ゲート 1 で確かめる)
  • 前提 2: 1 本目の claude は今どおり利用者の端末の前景で動き、tmux の中へ移さない。1 本目の端末を閉じれば 1 本目のラッパーは今どおり終わる。統括役の役目は、そのとき受け入れ条件 21 に従って次の claude へ移る
  • 前提 3: 端末から打った 2 本目以降の claude に名前の指定が無いときは、統括役が重ならない名前を自動で付ける。名前の形は設計で決める
  • 前提 4: 利用者のシェルの設定(claude の alias・関数)を書き換える手段は、今の relay.py install が置いたものの範囲に留める。新しく利用者のシェルの設定へ書き込む必要が出たら、設計で止めて人へ戻す(共通原則 C6)
  • 前提 5: 区間の数・最後に動いた時刻は各ラッパーの作業ディレクトリの記録から読める。使った費用はラッパーが記録を持っていない区間があり得るため、読めないときは空の値(null)で出し、0 と書かない
  • 前提 6: アカウントの 5 時間の枠の使用率はアカウントごとの値で、どのラッパーが読んでも同じ値になる(今の relay_lib/switch.py の読み方)。ラッパーをまたいだ枠の数え方は、この値を共有することで満たす
  • 前提 7: 承認待ち通知は、今ある待ち通知(lib/notify.py・lib/wait_notice.py)の経路を使い、通知先を新しく足さない
  • 前提 8: 対象は 1 つのコンテナ(1 つの OS の利用者)の中だけで、ほかのコンテナや機械のラッパーは統括しない
  • 前提 9: 対象のランタイムは Claude Code だけである(ラッパーが Claude Code だけのため)。Codex は対象外
  • 前提 10: 2026-11-04 に記録された結合テストの失敗は、2026-11-05 に develop の先頭とタグ ndf--v10.27.14 のどちらでも再現しない(「現状」の節の表)。原因は未確認のまま残し、この要求では原因を追わない。受け入れ条件 30 の実行で再び落ちたら、受け入れ条件 31 の出力を材料に別の課題を起こす
  • 前提 11: tmux の不足を失敗にする仕組みは、根の conftest.py の外部コマンドの一覧(REQUIRED_COMMANDS)と読み飛ばしの指定(NDF_TESTS_ALLOW_MISSING_COMMANDS)をそのまま使い、同じ役割の判定をテストのファイルに別に作らない(Value 7)

対象範囲

含む:

  • 2 本目以降の claude が統括役に起動を頼み、tmux の window へ結び付く入口
  • 副命令 relay.py spawn・ls・attach・restart、名前つきの stop
  • conductor の応答から名前つきのラッパーを起動する口
  • 同時に動かすラッパーの数の上限の宣言と判定
  • 統括役が落ちたときの引き継ぎ、tmux の無い環境での今の起動への戻り
  • 一覧と通知での承認待ちの表示
  • tmux を使う結合テストを、tmux の無い環境で飛ばさず失敗にすること(読み飛ばしの指定があるときを除く)
  • CI のテストのジョブで tmux を使えるようにし、4 件を走らせること
  • 結合テストの待ちが切れたときに、原因を読める記録を失敗の出力へ出すこと

含まない:

ドメインイベント

# イベント 引き金 失敗したとき 順序の前提
E1 1 本目の claude でラッパーが起動し、統括役を兼ねた 統括役の居ないコンテナで利用者が claude を打った 統括役になれなければ、統括役の無い今の 1 本の起動として続ける(受け入れ条件 1) —
E2 2 本目の claude が統括役へ起動を頼んだ 統括役の居るコンテナで利用者が claude を打った 統括役が応答しなければ、頼んだ側が統括役になる(受け入れ条件 21) E1
E3 conductor が名前つきのラッパーの起動を頼んだ conductor の応答の起動のブロック、または relay.py spawn 名前の重複・上限超えは理由つきで断った(受け入れ条件 6・16) E1
E4 統括役が同時に動かす数の上限を判定した E2・E3 上限を超えれば起動せず理由を返した(受け入れ条件 16) E2 か E3
E5 統括役が tmux の window で名前つきのラッパーを起動した E4 を通った tmux が無ければ今の起動に戻った(受け入れ条件 23)。window を作れなければ理由を返した E4
E6 端末が名前つきのラッパーの window へ結び付いた E2 の頼み、または relay.py attach 名前が無ければ理由つきで終わった(受け入れ条件 13) E5
E7 端末が閉じられた 利用者が端末を閉じた ラッパーは動き続けた(受け入れ条件 7) E6
E8 名前つきのラッパーがカットポイントで区間を切り替えた ndf-next 今のラッパーの規則のまま(変えない) E5
E9 名前つきのラッパーが承認待ちに入った claude が承認ゲートで問いを出した 一覧と通知に出した(受け入れ条件 10・19) E5
E10 利用者が 1 本を止めた・やり直した relay.py stop <名前> / restart <名前> 名前が無ければ理由つきで終わった E5
E11 名前つきのラッパーが落ちた ラッパーのプロセスの異常終了 一覧で「止まった」と出し、ほかは動き続けた(受け入れ条件 20) E5
E12 統括役が落ちた 1 本目のラッパーの終了・異常終了 動いているラッパーは止まらず、次に打った claude が統括役になった(受け入れ条件 21・22) E1
E13 テストの実行が、tmux を使う結合テストを集めた pytest の収集 — —
E14 tmux が PATH に無く、テストの実行が失敗として tmux を名指しした E13 で tmux が見つからない 読み飛ばしの指定があれば、tmux を使う 4 件だけを飛ばした(受け入れ条件 28) E13
E15 CI のテストのジョブが tmux を使える状態でテストを始めた Pull Request・push での CI の起動 tmux を用意できなければ、E14 でジョブが落ちた(受け入れ条件 29) —
E16 結合テストの待ちが切れ、ラッパーの記録を失敗の出力に出した 待ちの上限(区間 2 は 40 秒)に達した — E13

用語

用語 意味
統括役 同じコンテナで最初に起動したラッパーが兼ねる役。ほかのラッパーの登録・起動・同時に動かす数の判定・一覧だけを持ち、各ラッパーの状態機械には触れない。課題の本文の「ホスト」に当たる(用語集の「ホスト」は cross-review の収束ループを起動する CLI を指すため使わない)
名前つきのラッパー 統括役に名前で登録され、tmux の 1 つの window で動く relay.py run の 1 本。カットポイントで区間(セッション)を切り替えても同じ名前で続く。課題の本文の「セッション」に当たる(用語集の「セッション」は claude の 1 回の起動を指すため使わない)
端末の結び付け 利用者の端末を、名前つきのラッパーの tmux の window へ attach すること。端末を閉じても window の中のラッパーは動き続ける

受け入れ条件

起動と結び付け:

  • 1. 統括役の居ないコンテナで claude を打つと、今と同じく端末の前景でラッパーが起動し、tmux を使わない(1 本目の振る舞いが変わらない。退行しない)
  • 2. 前提: 統括役が動いている。操作: 別の端末で claude を打つ。結果: tmux の新しい window で名前つきのラッパーが起動し、その端末がその window へ結び付く
  • 3. 2 本目の claude が起動した名前つきのラッパーは、端末で直接 claude を打ったときと同じ区間の切り替え(ndf-next で次の区間を起動する)を行う
  • 4. relay.py spawn --name <名前> -- <最初の入力> を打つと、端末の結び付いていない名前つきのラッパーが起動し、最初の区間の claude が <最初の入力> を受け取る。終了コードは 0
  • 5. conductor の応答に起動のブロック(名前と最初の入力を持つ。形は設計で決める)を置くと、統括役がそのブロックの名前で名前つきのラッパーを起動し、置いた側の区間は止まらない
  • 6. 既に動いている名前で spawn すると起動せず、終了コードが 0 でなく、理由(名前が重なる)を出す
  • 7. 名前つきのラッパーに結び付いた端末を閉じても、そのラッパーのプロセスは動き続け、relay.py ls で「動いている」と出る
  • 8. 端末の結び付いていない名前つきのラッパーでも、カットポイントの ndf-next で次の区間が起動する

一覧と個別の操作:

  • 9. relay.py ls --json は 1 行の JSON(統一 JSON レポート。lib/step_result.py の形)を出し、items に名前つきのラッパーごとに名前・状態(running / awaiting_approval / stopped のどれか。Factory と同じ名前、2026-11-02 の承認ゲート 2 で利用者が決めた)・承認待ちの有無・区間の数・使った費用・最後に動いた時刻を持つ。--json の無い relay.py ls は同じ中身を人の読む表で出す
  • 10. 名前つきのラッパーの claude が承認ゲートの問い(AskUserQuestion)を出している間、relay.py ls のその行の状態は承認待ちになり、答えた後は動いているへ戻る
  • 11. 名前つきのラッパーが 3 本動いているとき relay.py stop <名前> を打つと、その 1 本だけが止まり、残りの 2 本は relay.py ls で動いているのまま
  • 12. 名前を付けない relay.py stop は、今と同じく動いているラッパーすべてに停止シグナルファイルを置く(退行しない。前提の確認: relay_lib/mark.py の cmd_stop が state_root() の下の動いているラッパーすべてに stop を置く、2026-11-02 に読んで確かめた)
  • 13. 動いていない名前で stop / restart / attach を打つと、何も変えずに終了コードが 0 でなく、理由(その名前は無い)を出す
  • 14. relay.py restart <名前> は、その 1 本を止めてから同じ名前・同じ作業ディレクトリで起動し直し、ほかのラッパーに触れない。relay.py attach <名前> は打った端末をその window へ結び付ける

共有の方針:

  • 15. 同時に動かす名前つきのラッパーの数の上限を宣言(置き場は設計で決める)で持ち、宣言が無いときの既定は 3 である
  • 16. 上限の数だけ動いているとき、端末の claude・spawn・conductor の起動のブロックのどれから頼んでも起動せず、理由(上限の数と動いている名前)を出す
  • 17. 1 日の起動回数の上限(NDF_RELAY_MAX_STARTS)は、今と同じくコンテナの中の全ラッパーの起動を合わせて数える(退行しない。前提の確認: relay_lib/record.py の StartLimit.count_today が state_root() の下の全ディレクトリの記録を数える、2026-11-02 に読んで確かめた)
  • 18. relay.py ls の各行に、そのラッパーが今使っているアカウントの名前が出る。同じアカウントを使うラッパーが何本あるかを一覧から数えられる

承認待ちの知らせ:

  • 19. 端末の結び付いていない名前つきのラッパーが承認待ちに入ると、今ある待ち通知が、そのラッパーの名前と relay.py attach <名前> の案内を付けて 1 回出る

落ちたとき・環境の差:

  • 20. 名前つきのラッパーのプロセスを 1 本 kill すると、relay.py ls でその行が止まったになり、ほかのラッパーと統括役は動き続ける
  • 21. 統括役のプロセスを kill しても、動いている名前つきのラッパーは止まらない。その後に打った claude が新しい統括役になり、relay.py ls に既存のラッパーが出る
  • 22. 2 つの端末でほぼ同時に claude を打っても、統括役になるのは 1 つだけである(もう一方は名前つきのラッパーとして起動する)
  • 23. tmux の実行ファイルが PATH に無い環境で 2 本目の claude を打つと、今の 1 本ずつの起動(端末の前景で自分のラッパーを動かす)になり、tmux が無いため統括しないことを 1 行で出す

共通部品と契約:

  • 24. 新しい副命令の結果は lib/step_result.py の形で出し、ロック・子プロセス・実行の状態は lib/locks.py・lib/procs.py・lib/run_state.py を通す(同じ役割の関数を relay_lib に新しく作らない。Value 6)
  • 25. 新しい副命令・受け渡しのファイル・設定・環境変数が 入口・受け渡しのファイル・設定・環境変数の契約を 1 か所に宣言し、宣言の外を検査で落とす #2336 の契約の宣言に載り、契約の検査(plugins/ndf/scripts/tests/ の契約テスト)が通る
  • 26. 設計文書に、起動・一覧・停止の入出力の項目と factory/contracts/schemas/(実行ジョブの指示・進捗の事象)の項目の対応表があり、対応する項目は同じ名前を使う
  • 27. plugins/ から factory/ を参照しない(python3 scripts/check-service-boundary.py が 0 で終わる)

結合テストを毎回走らせる(2026-11-05 に足した。起票者の推定 S16・S17):

  • 28. 前提: tmux が PATH に無い。操作: uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests/test_relay_hub.py -q を打つ。結果: 終了コードが 0 でなく、出力が不足した外部コマンドとして tmux を名指しする。同じ操作に NDF_TESTS_ALLOW_MISSING_COMMANDS=1 を付けると、tmux を使う 4 件が skipped になり、終了コード 0 で終わる(tmux を使わない 44 件を一緒に飛ばすかは未決 6)
  • 29. develop 宛の Pull Request の CI のテストのジョブ(pytest.yml の分割の 2 本)で、tmux を使う 4 件(test_spawn_ls_stop_and_hub_handover・test_second_claude_attaches_and_survives_terminal_close・test_limit_refuses_from_terminal_and_spawn・test_killed_named_wrapper_shows_stopped)が実行されて通り、どれも skipped に数えられない。CI のログか成果物から、4 件の結果を名前で読める
  • 30. 開発コンテナ(tmux 3.6)で test_spawn_ls_stop_and_hub_handover を 10 回続けて実行し、10 回とも通る。pytest plugins/ndf/scripts/tests -n 4 の全体の実行でも通る
  • 31. 結合テストが名前つきのラッパーの区間の待ちで切れて落ちるとき、失敗の出力に、そのラッパーの区間ごとの起動の引数と、作業ディレクトリのシグナルファイル(next.json・stop など)の有無が載る
  • 32. relay.py と relay_lib/ の振る舞いを変えない(変更は plugins/ndf/scripts/tests/・根の conftest.py・.github/workflows/ に限る。git diff --stat origin/develop で確かめる) → 変更の範囲に、根の conftest.py のテストの置き場 scripts/tests/test_root_conftest.py を加える(2026-11-05、承認ゲート 1 で利用者が設計を承認)
  • 32. relay.py と relay_lib/ の振る舞いを変えない(変更は plugins/ndf/scripts/tests/・根の conftest.py・scripts/tests/test_root_conftest.py・.github/workflows/ に限る。git diff --stat origin/develop で確かめる)

非機能の条件

大項目 条件
可用性 名前つきのラッパー 1 本・統括役の異常終了が、ほかのラッパーを止めない(受け入れ条件 20・21)
運用・保守性 統括役の起動・断り・引き継ぎ、名前つきのラッパーの起動・停止は、統括役の記録に時刻・名前・理由つきで 1 行ずつ残る
移行性 今の 1 本だけで使う利用者は、設定を変えずに今と同じ振る舞いになる(受け入れ条件 1・12)。tmux の無い環境も同じ(受け入れ条件 23)
セキュリティ 統括役への頼みは同じ OS の利用者だけが送れる(受け口のファイル・ソケットの権限を所有者だけにする)。最初の入力・一覧に秘密を出さない
システム環境 tmux 3.x(2026-11-02 の開発環境は 3.6)。Linux のコンテナ。Claude Code だけ。CI は GitHub Actions の ubuntu-latest
性能・拡張性 CI で tmux を用意する手順が足す時間は、テストのジョブ 1 本あたり 60 秒以内(今の分割の 2 本は -n auto で並列に回る)

影響

対象 影響
公開インタフェース 変わる(足すだけ)。relay.py に副命令 spawn・ls・attach・restart と、stop の名前の引数を足す。名前を付けない stop と今の run の振る舞いは変えない(互換あり)
データ ラッパーの作業ディレクトリの置き場(~/.local/state/ndf/relay/)に、統括役の登録と名前の記録を足す。既存のシグナルファイルの形は変えない
既存の振る舞い 統括役が居るときの 2 本目以降の claude が、自分の端末の前景で動く代わりに tmux の window へ結び付く
テストの実行 tmux の無い手元の環境で pytest plugins/ndf/scripts/tests を打つと、今は tmux を使う 4 件が飛ばされて通るが、失敗に変わる。続けて走らせるには NDF_TESTS_ALLOW_MISSING_COMMANDS=1 を付ける(node・npm の不足と同じ扱い)

検証手段

項目 手段
起動 python3 plugins/ndf/scripts/relay.py ls --json
テスト uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests -q -n 4(擬似端末と使い捨ての tmux サーバ tmux -L <使い捨ての名前> で受け入れ条件 1〜23 を確かめる)
静的解析・型検査 リポジトリの CI と同じ検査(python3 scripts/check-service-boundary.py・契約の検査)
tmux の不足(受け入れ条件 28) tmux を除いた PATH で pytest plugins/ndf/scripts/tests/test_relay_hub.py を打つ(pytester か env PATH=... の子プロセスで確かめるテスト)
CI(受け入れ条件 29) 設計 PR の後の実装 PR の CI の結果で、4 件の名前と結果を読む
手動確認 利用者が実際の端末エミュレータで 2 本目の claude を打ち、tmux の window の画面が崩れずに描かれ、キー入力が claude に届くことを見る(リリース後テスト)

前提とする取り決め

項目 参照先 / 決めたこと
プロジェクト構造 ラッパーのコードは plugins/ndf/scripts/relay_lib/、共通部品は plugins/ndf/scripts/lib/。factory/ を参照しない(AGENTS.md の「マーケットプレイスの構造」)
コーディング規約 AGENTS.md の「最小限のコード実装」。外部コマンド(tmux)の挙動は書く前に実行して確かめる(AGENTS.md のベストプラクティス)
テスト戦略 統括役の判定(上限・名前の重なり・引き継ぎ)は単体テスト、tmux と擬似端末を通す振る舞いは使い捨ての tmux サーバでの結合テスト。.md の文言を照合するテストは書かない

境界

区分 内容
常に行う 既存の relay のテストを通す、tmux は使い捨てのサーバ名で試し利用者の tmux サーバに触れない
確認してから行う relay.py install が置く claude の alias・関数の変更(承認ゲートで人が見る。利用者のシェルの設定へ新しく書き込むなら設計で止めて人へ戻す。C6)
行わない 各ラッパーの区間の切り替え・アカウントの切り替えの規則の変更、tmux の自動導入、factory/ の参照

未決

項目 誰が決めるか 期限
1. 同時に流すプランの合計の上限(起票者の推定 S8、例 6)をこの課題に含めるか。含めないなら別の課題に起こす 利用者(承認ゲート 1) 設計の承認まで
2. 同時に動かす数の上限の宣言の置き場(.ndf/ の既存の設定か新しいファイルか) 設計 設計の承認まで
3. conductor の起動のブロックの形(ndf-next と同じくコードブロックの情報文字列で拾うか) 設計 設計の承認まで
4. 統括役が落ちた後、次の claude が来るまでの間に出た承認待ち通知を誰が出すか(各ラッパーが自分で出すか) 設計 設計の承認まで
5. CI の runner(ubuntu-latest)に tmux が入っているか。入っていなければ CI の手順で入れる(apt-get install tmux など)か。設計で runner の上で tmux -V を実測して決める 設計 設計の承認まで
6. tmux を外部コマンドの一覧へ載せる単位(test_relay_hub.py のファイル全体か、tmux を使う 4 件だけか)。ファイル全体にすると、tmux を使わない 44 件も tmux の無い環境で失敗になる 設計 設計の承認まで
7. CI のログか成果物で 4 件の結果を名前で読む形(-rs の要約・JUnit XML など) 設計 設計の承認まで

背景

今の仕組み(2026-11-01、plugins/ndf/scripts/relay_lib/)

観点 今
起動 claude の alias が relay.py run を前景で起動し、ラッパーが claude を擬似端末の子として 1 つ持つ(run.Relay・terminal.Terminal、ptyprocess)
作業ディレクトリ ラッパーごとに ~/.local/state/ndf/relay/<時刻-pid-乱数>/(NDF_RELAY_DIR)。シグナルファイル next.json・stop・relay.lock などを持つ
共有 count.lock(起動の数)とアカウント(relay_lib/switch.py)
停止 relay.py stop は動いているラッパーすべてに停止シグナルファイルを置く(mark.cmd_stop)
端末 ラッパーは起動した端末の前景に常駐する。端末とラッパーは 1 対 1

設計の候補(2026-11-01 に利用者が案 B に決めた。起票者の記録)

案 形 得 失
A 統括役が全 claude の擬似端末の親になり、端末の切り替えも自分で行う プロセスが 1 つで状態を持ちやすい 統括役が落ちると全ラッパーが落ちる。端末の切り替え(画面の再描画・入力の振り分け)を自作する。統括役の入れ替え(#1587 の handover)が全ラッパーに効く
B(採る) tmux を親にし、ラッパーごとに今の relay.py run を 1 つの window で動かす。統括役は制御(登録・起動・方針・一覧)だけを持つ 今のラッパーの状態機械を変えずに使える。1 本が落ちても他は動く。端末の切り替えと結び付け直しは tmux に任せる(既存ライブラリを使う方針) tmux に依存する(無い環境では今の 1 本ずつの起動に戻る)

Neural Development Factory との関係(2026-11-01 に確かめた)

Factory(確定仕様 docs/specifications/ndf-factory-runs-and-runner.md)は、並行をサービスの側で持つ。スプリントの実行 1 本を実行ジョブ(ECS Fargate のタスク 1 つ)で動かし、conductor は端末の無い claude -p で起動する。同時に動かせる数は「実行の枠」(アカウントごと・リポジトリごと。プランの表で上限 3 など)が開始のトランザクションで数える。承認ゲートで止まったら状態を S3 へ保存してジョブを終え、答えで別のコンテナが続ける。人は端末でなく画面(#2172 一覧・詳細、#2173 承認と通知)で見る。

この課題と重なるのは 手元のマシンで Factory の実行ジョブを動かす仕組み(#2189) である。どちらも「利用者の機械の上で、複数の claude を起動し、見て、止める常駐のプロセス」になる。

この課題(ラッパーの統括役) #2189(手元のマシンで実行ジョブを動かす仕組み)
起動の出所 端末の claude・conductor の応答・relay.py spawn Factory の実行ジョブの指示(サービスから受け取る)
claude の形 対話の端末(tmux の window) 端末の無い claude -p
見る場所 relay.py ls と tmux Factory の画面

起動・一覧・停止の口を Factory の契約と同じ語と形で持てば、#2189 は統括役に「Factory から受け取った指示で起動する」入口を足すだけで作れる。plugins/ は factory/ を参照しないため、共有するのは契約の形だけである(受け入れ条件 26・27)。

関連: #2315(開発フローの見直しと大整理。同じマイルストーン 35)・#2335(根本解決)・#1587(ラッパーの入れ替え)・#1389(アカウントの切り替え)

進行

モード: — / 進め方: auto

  • 要求と受け入れ条件 — 2026-11-02 00:57
  • 作業場所の用意 — 2026-11-02 03:20
  • 設計 — 2026-11-02 01:09
  • 素材の収集と出典の確定
  • ドキュメント再構成 — 2026-11-05 01:22
  • ドキュメントレビュー
  • 計画 — 2026-11-02 03:20
  • 実装 — 2026-11-02 03:20
  • 構造改善 — 2026-11-02 08:24
  • 実装レビュー — 2026-11-02 09:21
  • 完了判定 — 2026-11-02 03:56
  • Pull Request — 2026-11-02 04:18
  • 確定仕様化 — 2026-11-02 12:26
  • 後片付け — 2026-11-02 12:26
  • 配布 — 2026-11-02 11:58
  • 体裁レビュー
  • リリース後テスト — 2026-11-02 13:00
  • 振り返り — 2026-11-04 06:11
  • 棚卸し — 2026-11-04 06:16

振り返り: #2426 (comment)

人の判断待ち(開発ループ lflow2)

  • 判定: unknown(承認ゲート 1)
  • 理由: 設計の中身は MVV に沿っている。4 件をクラスにまとめて一覧の 1 行で指す(決定 1)、tmux の有無を一覧だけで決める(決定 2)、失敗の文を Term の共通の処理に足す(決定 5)は、どれも判定を 1 か所に寄せる形で、Value 7(根本原因の場所で直す・同じ役割を分けない)に合う。無いときだけ apt で入れる形は、上限に当たる前にジョブを止めない作りで Value 1・3 に合い、JUnit XML で 4 件を名前で確かめる形は Value 4 に合う/設計の文書自身が、受け入れ条件 32 の変更範囲の外にある scripts/tests/test_root_conftest.py を変更範囲へ加えてよいかを、承認ゲート 1 で利用者が決めるべき事項として残している。承認済みの受け入れ条件の範囲を変える判断なので、MVV 判定では代われない。共通原則の優先順位で、利用者の承認はプロジェクト MVV より上にある/要求の検査(requirements-check)が exit 2 で止まり、原因は未確認のままである。PR の本文も、このブランチで再実行して通ることを確かめる必要があると書いている。確かめていないものを済んだと扱えないため(Value 1・Value 4)、検査を通ったとは判定できない/設計は実装で .github/workflows/pytest.yml を変える。ここは .ndf/pace.json の boundary_paths(.github/workflows/**)の対象で、設計の文書も、承認ゲートの扱いは development-workflow の判定に従うとしている。この PR に含まれるのは文書だけで、C1〜C8 と P1 に当たる操作は無い
  • 根拠の項目: Mission, Vision, Value 1, Value 3, Value 4, Value 7, C7, P1
  • 設計 Pull Request: relay の統括役の結合テスト: tmux を使う 4 件が手元と CI で毎回走り、落ちたときに原因を読める(#2346) #2524
  • 答え: approved
  • 答え方: python3 /persistent/ai/.claude/plugins/cache/ai-plugins/ndf/10.27.18/scripts/sprint-state.py loop answer lflow2 2346 --approved|--rejected|--revise --note <指摘ファイル>

Activity

  1. added 12 commits that reference this issue on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions