Back to skills
SKILL.md
Org Suspend
ASecurity組織を中断し、全状態をディスクに保存する。「中断」「保存して終了」 「閉じたい」「一旦やめる」「今日は終わり」と言われたときに使う。
- 5 stars
- 0 votes
- 0 copies
- 2 views
- Added October 5, 2026
Works with
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add suisya-systems/claude-org-ja --skill org-suspend --agent claude-codeAre you the author of Org Suspend?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/suisya-systems-org-suspend)---
name: org-suspend
description: >
組織を中断し、全状態をディスクに保存する。「中断」「保存して終了」
「閉じたい」「一旦やめる」「今日は終わり」と言われたときに使う。
effort: low
allowed-tools:
- Read
- Bash(bash tools/journal_append.sh:*)
- Bash(py -3 tools/journal_append.py:*)
- Bash(py -3 tools/capability_gate.py:*)
- Bash(python3 tools/capability_gate.py:*)
- Bash(python -c "from tools.state_db import connect;*)
- Bash(python3 -c "from tools.state_db import connect;*)
- Bash(python -m tools.state_db.importer:*)
- Bash(python3 tools/secretary_queue_watcher.py:*)
- Bash(py -3 tools/secretary_queue_watcher.py:*)
- Bash(rm -f .state/attention_pane.json)
- Bash(del .state\attention_pane.json)
- mcp__org-broker__check_messages
- mcp__org-broker__close_pane
- mcp__org-broker__inspect_pane
- mcp__org-broker__list_panes
- mcp__org-broker__list_peers
- mcp__org-broker__poll_events
- mcp__org-broker__send_keys
- mcp__org-broker__send_message
- mcp__org-broker__set_pane_identity
- mcp__org-broker__set_summary
- mcp__org-broker__spawn_claude_pane
- mcp__org-broker__spawn_pane
---
# org-suspend: 組織の中断
全ワーカーの状態を収集し、ディスクに保存し、全ペインを停止する。
> **curator 不在は正常系(オンデマンド化)**: キュレーターは常駐しない。state.db の
> `curator_pane_id` / `curator_peer_id` は null が正常で、`list_panes` / `list_peers` に
> curator が見えないことは異常ではない。curator ペインが存在するのは「worker クローズ起点の
> オンデマンド curate が実行中に suspend が重なった」一時的なケースのみで、その場合だけ
> Phase 4 の停止対象に含める。
> **責務境界(/org-suspend と [`/org-down`](../org-down/SKILL.md))**: /org-suspend は
> 「状態保存 + ja 管理下の補助プロセス(dashboard / secretary_queue_watcher / attention
> watcher)とペインの停止」までを担い、**`claude-org-runtime org down`(broker daemon の停止)は
> 呼ばない**。suspend 単体は「また `/org-start` で再開する」前提の中断であり、broker daemon は
> 走らせたままにする(端末を閉じても daemon はすぐ再開できるよう生存する)。daemon ごと完全に
> 落とすのは [`/org-down`](../org-down/SKILL.md) の責務で、/org-down が suspend の成功を確認した
> 後にのみ `org down` を実行する。
ペイン操作は `mcp__org-broker__*` MCP ツール経由で行う。pane_exited
相当の lifecycle イベントは `mcp__org-broker__poll_events` で long-poll、画面スクレイプ
は `mcp__org-broker__inspect_pane` で取得、raw キー入力は `mcp__org-broker__send_keys`。
> **輸送層(transport)両系 — 既定 `broker` / opt-in `renga`**: 本ファイル(および各スキル)の `mcp__org-broker__*` 呼び出しは **既定 `broker`**(`ORG_TRANSPORT` 無設定)で書いてあり、そのまま従えばよい(既定挙動)。`ORG_TRANSPORT=renga`(opt-in・切戻し可)では MCP サーバー名が `renga-peers` になり、ツールの **完全修飾名が `mcp__org-broker__*` → `mcp__renga-peers__*`** に機械置換される(引数形・セマンティクスは同一なので手順の論理は変わらない)。輸送依存で手順が変わる点だけ renga 併記する:
>
> - **受信モデル**: 既定 broker は **push 一次**(各ペイン同居の channel sidecar `server:org-broker-channel` が broker キューを ~1 秒間隔で claim→`notifications/claude/channel` で idle セッションへ本文注入。pull = ナッジ + `check_messages` は sidecar 不在 / unhealthy / channel 非対応ペイン(codex pull-peer)/ claude.ai login 不在時のフォールバック層)。`ORG_TRANSPORT=renga` 時は dispatcher / worker メッセージが `<channel source="renga-peers" …>` として in-band で push される。
> - **spawn 儀式**: 既定 broker は `--mcp-config <broker>` 注入による Claude Code **folder-trust プロンプト**の画面判定による機械承認に加え、push 一次のため channel sidecar を `--dangerously-load-development-channels server:org-broker-channel` で load し dev-channel 承認プロンプトを画面判定で機械承認する(2 段承認)。`ORG_TRANSPORT=renga` 時は `--dangerously-load-development-channels server:renga-peers` の「Load development channel?」を画面判定で承認する 1 段。 **画面判定** = `inspect_pane` → `claude-org-runtime dispatcher spawn-prompt-step` が返した 1 手だけを `send_keys` する反復(runtime 0.1.43+)。盲目 Enter は禁止(folder-trust の初期カーソルは「No, exit」で、Enter 一発はワーカーを終了させる)。手順は spawn-flow 3-3b。
> - **エラー分岐**: 既定 broker は shared codes(`pane_not_found` / `last_pane` / `invalid-params`)に加え broker 固有 `[token_invalid]` / `[session_invalid]` / `[tool_not_authorized]` / `[no_backend]`(= adapter_unavailable)/ `[nudge_failed]` / `[peer_not_found]` / `[name_taken]` を返しうる(未知コードは default-branch で escalate)。`ORG_TRANSPORT=renga` 時は broker 固有コードは発生しない。
>
> `new_tab` / `focus_pane` は broker surface に**無い**(意図的除外)。契約面の正本は [`docs/contracts/backend-interface-contract.md`](../../../docs/contracts/backend-interface-contract.md) Surface 8 + push-primary amendment(broker push 一次が **既定の契約**、pull は fallback として retain)。**opt-in `renga` は削除せず常時有効な切戻しの安全装置**として維持する。broker 実走(dogfood)は Epic #6 Issue G スコープで本ファイルの既定運用経路ではない(**二フレーム注記(Refs #604)**: ここの「既定 `broker`」は**コード既定**(`tools/transport.py: DEFAULT_TRANSPORT`、生成面はこれで render)。**運用既定**は broker dogfood が Epic #6 Issue G まで未活性のため `renga` で、両者は指す対象が異なり矛盾しない。総説は root [`CLAUDE.md`](../../../CLAUDE.md)。)
## Phase 1: ワーカー状態収集
1. **id 台帳を作る**。messaging とペイン制御は**別々の面**なので、id もそれぞれの面から取る
(契約 Surface 4 の批准済み amendment (2026-08-09) T-§4.2 は「到達性は面ごとに別々に確立する」ことを MUST に
している。`list_peers` にレコードがあることは、そのピアがペイン制御で addressable であることの
証拠にならない):
- `mcp__org-broker__list_peers` — **messaging 用の peer id**。各レコードの `id` / `name` / `role` / `cwd` と、
マルチタブ対応 backend が返す optional な `same_tab` / `tab` / `tab_name` を控える(この 3 つは
`cross_tab_peers` capability を広告する backend でのみ載る。非広告 backend では欠落する)。
**`list_peers` のタブ範囲を決めるのは `cross_tab_peers` であって `caller_scope` ではない**
(契約 T-§cap は capability を互いに独立と規定する。`caller_scope` が変えるのは `list_panes` と
**Group A の** pane 制御呼び出しだけで、`list_peers` には影響しない。`close_pane` /
`set_pane_identity` は `caller_scope` の管轄外=別 token — 下記 (G) を参照):
- **`cross_tab_peers` を広告する backend**: `list_peers` は**全タブを列挙**し、`same_tab` は
**呼び出し側から見た**値として返る(契約 T-§2.2 / T-§2.2-fields)。フォーカスに依存しないので
フォーカスの確認は不要
- **`cross_tab_peers` を広告しない backend**(旧版・renga 1.x・現行 `org-broker`、および
**`caller_scope` だけを広告する backend**): ratified の messaging 規則どおり
**フォーカス中のタブ**に追随する。marker 全欠落が保証するのは「その列挙が単一タブに閉じて
いる」ことだけで(契約 T-§cap の conformance MUST)、**それが窓口のタブであることまでは
保証しない**。`/org-suspend` を走らせる前にフォーカスが窓口ペインのタブにあることを確認する。
列挙されたタブを呼び出し側に紐付けられない場合は **broadcast しない**(別 org のタブへ
SUSPEND / SHUTDOWN を撃つ経路になる)。紐付けの確認は、`list_panes` に窓口ペイン自身が
居ること、あるいは列挙に自組織の既知 identity(state.db の `dispatcher_peer_id` 等)が
含まれることで足りる
- `mcp__org-broker__list_panes` — **ペイン制御用の pane id**。各レコードの `id` / `name` / `role` を控える。
**どのタブが返るかは capability による**(契約 T-§cap は `caller_scope` と `cross_tab_peers` を
独立と規定する): `caller_scope` を広告する backend では **caller のタブ**が返る(`list_panes` は
Group A なのでこの token だけで足りる)。広告しない
backend(旧版、および `cross_tab_peers` だけを広告する backend)では ratified §4.3 の
visibility consequence どおり**フォーカス中のタブ**が返るので、`/org-suspend` を走らせる窓口ペインの
タブがフォーカスされていることが前提になる。フォーカスが別タブにあると**別タブのペイン id を
台帳に取り込み、後段で無関係なペインを `inspect_pane` / `close_pane` しうる**。前提が成り立つか
確認できない場合は pane 制御を行わず、pending として人間に報告する
- > **pane 制御の 2 グループ(G)— 以降この記号で参照する**: 契約 T-§4.2 は pane 制御ツールを
> **管轄 capability の異なる 2 群**に分ける。分ける理由は**リリース時期ではなくサーバー世代の
> スキュー**である(4 トークンは 1 リリースで出ており renga `src/ipc/mod.rs`:123-128 の
> `SERVER_CAPABILITIES` は 4 定数を並べる。それでもトークンが別なのは、先行 3 トークンを
> 広告する `#290` 世代の server が未知の `from_pane` を落として `close` /
> `set_pane_identity` を**ユーザーが見ているタブ**に解決し不可逆にペインを閉じるからで、
> 上流は `caller_scope_close_identity` を "a token of its own" と書いている
> — renga `docs/api-surface-v1.0.md`:576-582)。契約 T-§cap
> "Independence" は `caller_scope` から `caller_scope_close_identity` を推論することを
> MUST NOT としている。**「`caller_scope` があるからあらゆる pane 制御がフォーカス非依存」
> という推論をしてはならない**:
> - **Group A — `caller_scope` 管轄**(suisya-systems/renga#288 の 7 ツール):
> `list_panes` / `spawn_pane` / `spawn_claude_pane` / `spawn_codex_pane` / `focus_pane` /
> `inspect_pane` / `send_keys`。この token を広告する backend では相対セレクタ
> (リテラル `"focused"` / 裸の `name`)が **caller のタブ**に解決され、フォーカス非依存になる
> - **Group B — `caller_scope_close_identity` 管轄**(suisya-systems/renga#296):
> `close_pane` / `set_pane_identity` の 2 本。**`caller_scope` はこの 2 本を覆わない**。
> `caller_scope`(さらに `cross_tab_peers` / `spawn_tab`)を広告する backend であっても、
> この token が無ければ `"focused"` は **user が見ているタブ**に解決され、裸の `name` は
> **アクティブタブを最初に探し、そこで見つからなかったときだけ他タブを index 順に走査して
> 最初の一致を採る**(無差別の全タブ検索ではない。危険なのはこのフォールスルーと、優先される
> タブが caller ではなく **user の**タブである点。契約 T-§4.2 "Pane-control scope, Group B")
> - **(G-MUST)** `caller_scope_close_identity` が確立していない間は、`close_pane` /
> `set_pane_identity` に**相対セレクタ(リテラル `"focused"` / 裸の `name`)を使ってはならない
> (MUST NOT)**(契約 T-§4.2 "Fail-safe consequence for Group B (MUST)")。**自タブのものだと
> 独立に確立済みの列挙から採った数値 pane id でのみ**アドレスする(MUST)。誤爆した場合、
> `close_pane` は不可逆で(人間が入力中のペインを落とす / 並走する別 org の同名 `worker-*` を
> 閉じる)、しかも**エラーを返さない**。契約はこの MUST に carve-out を 1 つだけ置いているが
> (stale-binding cleanup = 再 spawn が `[name_taken]` で弾かれるのに当の name が `list_panes` に
> 出ない状態)、**本スキルはその条件に当たらないので取らない** — /org-suspend が閉じるのは列挙に
> 居るペインだけで、`[name_taken]` 起点の再 spawn 経路を持たないため、数値 pane_id は常に列挙から
> 採れる
> - **(G-OWNTAB)** 上の MUST が言う「自タブのものだと独立に確立済みの列挙」は、**`mcp__org-broker__list_panes`
> の列挙**を指す。`name` / `role` の一致が見るのは「その id が期待どおりのペインか」だけで、
> **その列挙が自分のタブのものか**は見ていないので、確立は照合の前段に置く。**確立手段は次の 2 つ
> だけ**で、いずれか 1 つが成立したときにその列挙を**自タブと確立済み**とみなす(規範の出所は
> 契約 T-§4.2 の MUST。同じ規則を harness 側で書き下したものが
> [`.dispatcher/references/pane-close.md`](../../../.dispatcher/references/pane-close.md) の
> 「照合の前に「その列挙が自タブのものか」を確立する」節で、両者は同一規則であり片方だけを
> 緩めない):
> 1. **backend が Group B を自身の単一タブモデル内で解決する** — `org-broker` は Group B も
> 単一タブモデル内で解決するので、列挙は構成上つねに自タブのもの(契約 §8.1 / §8.10)
> 2. **`caller_scope` を確立できている** — `list_panes` が caller のタブにフォーカス非依存で
> 解決する(契約 T-§cap。`caller_scope_close_identity` とは別トークンで、一方から他方を
> 導出しない)。**確立できたときだけ数える**: 広告を確かめられない / 確定できないときは
> T-§cap の fail-safe default で**不成立**に倒れる(version 文字列やサーバー名から推定しない)
>
> **`same_tab` は確立手段に数えない**。`same_tab` / `tab` は契約 T-§2.2-fields が
> **`list_peers` のレコード**に足すフィールドで、pane 制御の起点である `list_panes` のレコードには
> 載らない。`list_peers` 側の `same_tab` を根拠に pane 制御を許すのは、契約が MUST で禁じる
> 「messaging 到達性から pane 制御到達性を推論する」ことそのものである(同契約
> 「The two scopes are not interchangeable (MUST)」)。**`same_tab` が使えるのは否定方向の
> 絞り込みだけ**で、`same_tab == False` のレコードに `pane_id` を割り当てないという**狭める**
> 使い方は正しく、「自タブと確立できた」と**広げる**使い方は license されない。
> **2 つのいずれも成立しなければ Group B を撃たず**、pending として人間に報告する
> (相対セレクタへフォールバックしない)
> - **かつて SHOULD だった経緯(2026-08-07 の人間判断、ja#823 — 記録として保持、discharge 済み)**:
> 当時この規則は SHOULD として書かれていた。危険は実在するが**本 amendment が持ち込んだもの
> ではなく以前から存在**しており、かつ org 自身の正準手順が Group B を相対セレクタで撃つ箇所を
> 抱えていた(当時の実測は契約 T-§4.2 の台帳のとおり **8 ファイル 11 呼び出し**)。不可逆操作の
> 手順を未検証のまま変える方がリスクが高いと人間が判断し、**コード既定の輸送層 `org-broker` には
> この失敗モードが無い**(capability を広告せず単一タブ規約で解決する)ことがその判断を可逆に
> していた。**その呼び出し箇所の移行は 2026-08-09 に完了し、契約 T-§4.2 は同日 MUST へ in-place で
> 引き上げられた**(契約 T-§4.2 "Why SHOULD and not MUST" は当時の決定の記録として過去形で保持され、
> 台帳の全 row が discharge 済み)。したがって本スキルが従うのは上の (G-MUST) であって、この段落は
> 当時の状態を説明する記録である
> - capability gate 実装済みクライアントでは、Group B の呼び出しが token 欠落時に
> `[server_too_old]` で **fail-closed** する。これは黙って握り潰さず、未実行として
> pending に残し人間へ報告する(契約 T-§6 clause (b)。renga では回復手順は「daemon を
> 2.0.0 以降へ更新して**再起動**し `server_info` で再 probe」であり、旧挙動での継続ではない)
- > **フォーカス前提(F)— 以降この記号で参照する**: `caller_scope` を広告しない backend では
> `list_panes` と pane 制御呼び出しは**フォーカス中のタブ**に向く。したがって
> **(F-1)** `list_panes` にピアが**居ないこと**は、それ単独では消滅の証拠にならない
> (フォーカスが別タブへ移っただけで、生存中の同タブピアが「不在」になる)。
> **(F-2)** pane 制御呼び出しが返す `[pane_not_found]` も、同じ理由で単独では閉鎖の証拠にならない。
> 消滅を確定してよいのは **(i) `pane_exited` を観測した**、または
> **(ii) 列挙が窓口のタブのものだと確認できている**ときだけで、どちらも取れなければ
> **indeterminate** として残す。**F の解除も (G) のグループ別**である: `caller_scope` 広告 backend
> では Group A(`list_panes` / `inspect_pane` / `send_keys` 等)がフォーカス非依存になるので
> **F-1 は効かない**。一方 **F-2 は `close_pane` について残る** — `caller_scope_close_identity`
> が未確立なら `close_pane` の `[pane_not_found]` は依然フォーカス(user 可視タブ)由来でありうる。
> 契約 T-§4.3 の visibility supersession が完全になるのは **2 token 揃ったとき**だけで、
> `caller_scope` 単独では §4.3 の文言が Group B に対して verbatim で立つ。
- 2 つを突合して 1 エントリ =
`{peer_id, pane_id(割り当てられない場合は未設定), name, role, cwd, same_tab, tab}` の台帳にする。
**`to_id` には必ず `peer_id`、`target` には必ず `pane_id`** を渡す。2 つの `id` が同じ値空間だとは
仮定しない(backend 定義。契約 §4.1 は id を "opaque" と規定しており、面をまたいだ流用は契約が
保証していない)
- **`pane_id` の出所は必ず `mcp__org-broker__list_panes`(pane 制御面)である**。`list_peers` の台帳は
messaging(`to_id`)用に保ち、そこから pane 制御用の id を導かない — 契約が MUST で禁じる
「messaging 到達性から pane 制御到達性を推論する」ことになるため(「The two scopes are not
interchangeable (MUST)」)。割り当ては**列挙面のゲート → レコード面の絞り込み**の 2 段で決める:
- **(i) 列挙面のゲート((G-OWNTAB))**: `mcp__org-broker__list_panes` の列挙を**自タブと確立できない**なら、
**どのエントリにも `pane_id` を割り当てない**。確立手段は単一タブ backend / `caller_scope` の
2 つだけで、`same_tab` はここに数えない。確立できないまま台帳に id を載せると、後段の
identity 照合が「期待どおりのペインか」しか見ないため、**別タブのペインを照合済みとして
`close_pane` しうる**
- **(ii) レコード面の絞り込み(`same_tab` は否定方向にだけ使う)**: (i) が成立した列挙の中で
`list_peers` のレコードと突合するとき、**`same_tab == False` / 欠落のレコードを除外する**
- **突合(join)キーは `name` だけで、`role` は使わない**。`role` は非一意で(worker が複数居れば
必ず衝突する)join キーとして成立しない。`name` も**全タブ列挙では一意ではない**ので
(契約 T-§2.2: §1.8 の名前一意性は "this tab" 限定。`worker-{task_id}` 等の予約名は 2 org 並走で
構造的に衝突する)、`name` join が効くのは **`list_panes` 側が単一タブに閉じている**からであって、
`name` が無条件に一意だからではない。2.0 系の `list_peers` は**全タブ**を返す一方 `list_panes` は
**単一タブ分しか返さない**(契約 §1.5 は本 amendment で非 supersede。どのタブかは上記のとおり
capability 依存)ため、(ii) を誤ると別タブのピアのレコードに自タブの同名
ペインの `pane_id` が付き、Phase 2 が**別ワーカーのコンソールをスクレイプ**し Phase 4 の lifecycle
追跡が壊れる:
- `same_tab == False` のレコード → **`pane_id` を絶対に割り当てない**(pane 制御面では到達できない。
契約 T-§4.2)
- 2.0 系だが `same_tab` 欠落(‡ のケース)→ 「判定できない場合は他タブ扱いに倒す」規則どおり
**`pane_id` を割り当てない**
- 上の 2 つで**除外されなかった**レコード(`same_tab == True` が確定した 2.0 系のレコード、および
旧版 fallback の列挙 = 全レコードで `same_tab` / `tab` が両方欠落)→ (i) が成立している列挙に
同名レコードが在れば、**その `list_panes` レコードの `id`** を `pane_id` として割り当てる。
**`same_tab == True` はこの割り当てを license する根拠ではない** — license するのは (i) であり、
`same_tab` はそこから他タブのピアを外す negative filter として働いているだけである
- `pane_id` が**未設定のエントリは「pane 制御では触れない」**ことを意味する((i) が不成立で
台帳全体に id を載せなかった場合を含む)。Phase 2 ではスクレイプを
諦めて git 情報に倒し、Phase 4 では `close_pane` を撃たずに pending に残す(各 Phase の該当箇所と
同じ結論)
- この台帳を **Phase 2 / Phase 4 まで引き回す**。宛先解決・pending 集合はすべて台帳エントリを
primary key にし、`name` は人間可読の補助ラベルに留める
2. 直後の「**宛先選定の真理値表(Phase 1 / Phase 4 共通)**」で broadcast 対象を決める
(Phase 1 の対象 role 集合は `R = {worker, dispatcher}`)。**版判定が 2.0 系だった場合は、同節の
operational gate に従って手順 3 の送信前に人間へ報告し、確認を得てから進む**(旧版 fallback では
gate を発動せず、そのまま手順 3 へ進む)
3. 対象ピアの **`peer_id`** を `to_id` に渡して `mcp__org-broker__send_message` で以下を送信:
```
SUSPEND: 現在の状態を報告してください。
1. これまでに完了したこと
2. 変更したファイル(コミット済み/未コミット)
3. 次にやろうとしていたこと
4. ブロッカーや未解決の問題
```
4. 30 秒間 `mcp__org-broker__check_messages` で応答を待つ(5 秒間隔でポーリング)
5. 応答があったワーカーの報告を、送信先の台帳エントリに紐付けて記録する(受信メッセージの
`from_id` を `peer_id` と突合し、`name` 一致は補助にする)
### 宛先選定の真理値表(Phase 1 / Phase 4 共通)
`list_peers` の各ピアレコードについて、`same_tab` / `tab` / `role` の 3 つで broadcast 可否と
宛先指定の形を決める。Phase 1(SUSPEND)と Phase 4(SHUTDOWN)はこの 1 つの表を共有し、
**対象 role 集合 `R` だけをフェーズごとに差し替える**:
- **Phase 1**(状態報告の依頼): `R = {worker, dispatcher}`。curator には状態報告を求めないので含めない
- **Phase 4**(停止指示): 停止順序どおりステップごとに `R = {worker}` → `R = {dispatcher}` →
`R = {curator}`(curator ペインが存在する場合のみ)
`role` が**欠落 / `null` / 空文字列 / 未知の文字列**のピア、および `role == "secretary"` のピアは
どの `R` にも入らない。窓口自身が列挙に含まれるかは backend 次第だが(契約 §2.2 は caller を除外すると
書く一方、除外しない実装もありうる)、**自己除外は role gate が担う**ので列挙の挙動に依存しない。
他タブに別 org の `secretary` が居ても同じく `R` 外で除外される。
> **role gate で外した同タブピアは「黙って落とす」のではなく `unresolved` として追跡する**:
> 契約 §2.2 は `role` を **optional** と規定しているので、**実体はワーカーなのに `role` が欠落 / `null` の
> レコード**を適合 backend が返しうる。これを broadcast 対象から外すのは安全側で正しいが、そのまま
> 忘れると Phase 4 が「組織を中断しました」と報告する裏で**そのワーカーが走り続ける**。
> したがって、**同タブ**でありながら `R` 外で除外したピア(role 欠落 / `null` / 空文字列 / 未知)は
> 台帳に `unresolved` として残し、Phase 4 の最終報告で「role が判定できず停止操作の対象外にしたピア」
> として `peer_id` / `name` / `cwd` 付きで人間に提示する。`role == "secretary"` で外したものは
> 窓口自身なので `unresolved` に入れない。他タブの除外ピアは † の他タブ報告に含める。
**版判定は列挙 1 回につき 1 度だけ決める**(レコード単位で切り替えない)。`list_peers` の結果全体を見て、
`same_tab` / `tab` の**どちらか一方でも載っているレコードが 1 件でもあれば、その列挙全体を 2.0 系**として
扱う。全レコードが両方欠落しているときだけ旧版 fallback に落とす(契約 T-§2.2-fields の検出規則は
「backend についての証拠」であり、混在列挙で一部のピアだけ旧版規則を当てるのは禁止)。下表の「版判定」
列は、こうして決まった版のもとで各レコードがどの行に当たるかを読む。
| # | `same_tab` | `tab` | `role` | 版判定 | broadcast | 理由 |
|---|---|---|---|---|---|---|
| 1 | `True` | 値あり | ∈ `R` | 2.0 系 | **する**(`peer_id` 宛・† 同タブ = 所属 signal) | 自タブの対象役割。tab 情報が揃っており同タブが確定している |
| 2 | `True` | 値あり | ∉ `R` | 2.0 系 | **しない** | 役割が対象外 / 不明。SUSPEND・SHUTDOWN は org の役割にだけ意味を持つ |
| 3 | `True` | `None` | ∈ `R` | 2.0 系(片方欠落) | **する**(`peer_id` 宛・† 同タブ = 所属 signal) | `same_tab=True` だけで同タブが確定する。`tab` 欠落は旧版の徴候ではない |
| 4 | `True` | `None` | ∉ `R` | 2.0 系(片方欠落) | **しない** | 役割で除外(版判定より役割判定が優先) |
| 5 | `False` | 値あり | ∈ `R` | 2.0 系 | **しない**(† 他タブは対象外・人間に報告) | 他タブのピアは、この org が spawn したものではない(T-§4.2 retained の同一タブ launch 規則の対偶)。所属を証明する signal も無いので送らない |
| 6 | `False` | 値あり | ∉ `R` | 2.0 系 | **しない** | 他タブの無関係ペインである可能性が最も高い |
| 7 | `False` | `None` | ∈ `R` | 2.0 系(片方欠落) | **しない**(† 他タブは対象外・人間に報告) | `same_tab=False` だけで他タブが確定する。行 5 と同じ理由 |
| 8 | `False` | `None` | ∉ `R` | 2.0 系(片方欠落) | **しない** | 役割で除外 |
| 9 | `None` | 値あり | ∈ `R` | 2.0 系(片方欠落) | **原則 しない**(行 5 と同じ)。`tab` が caller 自身のタブ値と一致すると確認できたときだけ行 1 と同じ扱い(‡) | `tab` が載る時点で 2.0 系。`same_tab` 欠落は unknown であり、`name` 一致で同タブと結論してはならない(‡) |
| 10 | `None` | 値あり | ∉ `R` | 2.0 系(片方欠落) | **しない** | 役割で除外 |
| 11 | `None` | `None` | ∈ `R` | **旧版 fallback**(`cross_tab_peers` 非広告。renga 1.4 系および現行 `org-broker` はここ) | **する**(`peer_id` 宛・† 単一タブ = 所属 signal。**フォーカス前提の確認が済んでいること**) | tab 情報が無い=列挙が単一タブに閉じている(契約 T-§cap の conformance MUST)ので、従来どおり role gate だけで決める(現行挙動と同一)。ただしその単一タブが窓口のタブであることは手順 1 のフォーカス確認で担保する |
| 12 | `None` | `None` | ∉ `R` | 旧版 fallback | **しない** | 旧版経路でも未知 role は対象に含めない。役割が判定できないペインへの broadcast は org 外プロセスへの誤爆になる |
**旧版 fallback の発動条件は 1 つだけ**: 列挙内の**全レコード**で `same_tab` と `tab` が **両方 `None`**
のときに限る(行 #11 / #12)。片方でも値が載っていれば 2.0 系として扱う。特に `same_tab=False` は
「他タブに居ることが分かっている」という 2.0 系の積極的な情報であり、旧版扱いして role 判定だけに
戻してはならない(他タブのピアを名前宛で取りこぼす)。
**2.0 系と判定したら、broadcast の前に人間へ報告して確認を取る(operational gate)**: ゲート本文は
`list_peers` を呼ぶ全経路の共有 reference に切り出してある。**`list_peers` の直前に
[`.claude/skills/org-delegate/references/capability-first-drive-operational-gate.md`](../org-delegate/references/capability-first-drive-operational-gate.md)
を Read し、`interactive-action` の分岐を適用する**(`/org-suspend` は人間が起動する flow なので
人間が画面の前に居り、送信前に停止して確認を取れる。同 reference §6 の表 #1)。
同 reference が定めることのうち、`/org-suspend` にとって要点だけ再掲する(正本は reference 側。
ここに手順を複製しない):
- **版判定が capability 形なら、それを旧版形として読み替えない**(reference §1-1)。「未承認だから
非広告経路として解釈する」は契約が名指しで禁じている誤り。承認前に控えるのは cross-tab addressing に
**依拠した行動**であって、解釈ではない
- **報告に必ず含める**: 見えているタブ数 / 列挙されたピア総数 / うち他タブ判定の件数、および broadcast
対象に選んだピアの `name` / `peer_id` / `cwd` の一覧(人間が誤爆リスク=別 org を巻き込まないかを
判断できる材料にする)
- **人間の確認を得てから先へ進む。確認前は 1 通も出さない**
- 他タブピアは † のとおり**そもそも broadcast 対象に入らない**。gate の役割は「他タブに何が見えているか」を
人間に見せて、停止対象外にした集合が妥当か判断してもらうことにある
- **gate は初回駆動に対して 1 度だけ**で、`/org-suspend` の実行ごとではない。照会と記録は reference §2 /
§3-A のとおり [`tools/capability_gate.py`](../../../tools/capability_gate.py) と
`tools/journal_append.sh` で行う。以後の `/org-suspend`([`.claude/skills/org-down/SKILL.md`](../org-down/SKILL.md)
経由の suspend を含む)はこの記録に従う
- 記録がある場合でも、**他タブ判定のピアが前回より増えていた**ときはその差分を報告する(gate の
再発動ではなく、停止対象外にした集合の変化を人間に見せるため)
- **production-activation gate(reference §4)は `/org-suspend` を停止させない**。未記録の間は他タブ
ピアを対象外にしたまま同タブ集合で続行する(下の真理値表の行 5〜10 がまさにその形)。止まるのは
first drive gate だけである
- **旧版 fallback(行 #11 / #12。現在配備されている全 backend — `org-broker` を含む — はここを通る)では
この gate を発動しない**。従来どおり無停止で broadcast に進む(既存運用は一切変わらない)
**宛先は必ず `peer_id`**: broadcast 対象に決まったピアへは `send_message(to_id="<peer_id>")` で送る。
`to_id` に `name` を渡すと 2.0 系では**名前解決が送信者タブ内に限定される**ため、他タブのピアは
`[pane_not_found]` になる(`list_peers` 由来の id 宛はタブ横断で解決される)。旧版 fallback の行でも
`peer_id` 宛は同じく有効なので、経路を 1 本に揃えるため版によらず常に `peer_id` を使う。
**†(broadcast 対象の所属判定 — 同タブのみが自組織)**: SUSPEND / SHUTDOWN は**同タブのピアにだけ送る**。
他タブのピアには**送らず、人間に報告する**。根拠は identity 照合ではなく**構造**である:
- **同タブのピア(`same_tab=True`、および旧版 fallback の列挙全体)は自組織のもの**。caller のタブの
ペイン群は、この org が組み上げたレイアウトである(旧版 fallback では、その単一タブが窓口のタブで
あることを手順 1 のフォーカス前提の確認で担保したうえで成り立つ)。契約 T-§4.2 は ratified §4.2 の
「harnesses MUST launch every orchestrator-spawned pane in the same tab」を **retained(非 supersede)**
として維持しており、この org が spawn したペインは**全て同一タブに居る**ことが契約上保証されている。
したがってここは **role gate だけで判定し、追加の identity 一致を要求しない**(要求すると、identity を
state.db に記録しない役割 — 後述の on-demand curator — を永久に取りこぼし、現在配備の backend の挙動も
変わってしまう)
- **他タブのピアは、同じ契約規則の対偶により、この org が spawn したものではない**。よって
**broadcast 対象から外す**。`role` が対象役割でも送らない
> **なぜ identity 照合で他タブピアを「自組織」と認定しないのか**: **他タブピアの所属を証明できる signal が
> 現状存在しない**ためである。`worker-{task_id}` の task_id は人間が付ける slug で、**別 checkout の org が
> 独立に同じ値を使いうる**(`runs.task_id` の UNIQUE 制約はその DB の中だけの話で、org を跨いで一意で
> あることは何も保証しない)。`cwd` も証明にならない — ja root 配下には人間が開いたペインや pr-watch
> ペインが生えるし、別 checkout の org が似た階層構造を持つこともある。`runs.worker_dir_id` は nullable
> (`tools/state_db/schema.sql`)なので cwd 照合を必須にもできない。**証明できない以上、送らないのが唯一の
> 安全な選択**であり、「たぶん自組織だろう」で SUSPEND / SHUTDOWN を撃つと別 org を止める。
>
> **将来この規則を再訪する条件**: worker を意図的に別タブへ配置する運用(マルチタブ配置ポリシー)を
> 導入するときは、本規則と併せて **session スコープの identity をピアに刻む仕組み**が必要になる。
> それが無いまま他タブへ broadcast を許すと、上の証明不能性がそのまま誤爆になる。
**他タブピアの扱い**(役割共通):
- **broadcast しない**。SUSPEND も SHUTDOWN も送らない
- **`close_pane` も撃たない**(そもそも pane 制御面では到達できない。契約 T-§4.2 と下記 §)
- **人間に報告する**: 「他タブに `role` が {worker,dispatcher,curator} のピアが N 件見えているが、
自組織のものと確認できないため停止操作の対象外にした」ことを、`name` / `peer_id` / `cwd` 付きで
提示する。人間がそれを自組織のものだと判断した場合の対応(手動停止など)は人間の判断に委ねる
- **curator の補足**: on-demand curator の identity は設計上 **state.db に書かれない**
(`org-start` は `curator_pane_id` / `curator_peer_id` を `StateWriter.CLEAR` で明示クリアし、
[`.dispatcher/CLAUDE.md`](../../../.dispatcher/CLAUDE.md) は「state.db に**書かない**(null が正常系。
生存確認は `list_panes` のみ)」と規定する)。**同タブの curator はタブ所属で自組織と判定できるので
従来どおり graceful に SHUTDOWN を受け取る**(オンデマンド curate と suspend が重なったケースは
この経路で停止する)
**§(他タブピアの停止確認に関する注記)**: `same_tab=False` と判定されたピアは、messaging では到達
できても**ペイン制御では到達できない**(契約 T-§4.2: pane 制御は caller のタブ内に留まる)。Phase 4 で
このピアの `close_pane` が `[pane_not_found]` を返しても**閉鎖の証拠にはならない**ので、Phase 4 の
「`[pane_not_found]` の扱い」節の他タブ例外に従う。
**‡(`same_tab` 欠落時の扱い — 名前一致で「同タブ」と結論してはならない)**: 2.0 系と判定した列挙で
`same_tab` が載らないレコードは **unknown** であり、**他タブ扱いに倒す**(§ の制約と † の identity 確認を
両方適用する安全側)。
`mcp__org-broker__list_panes` に同じ `name` / `role` のペインが在ることを「同タブである」の根拠に**使ってはならない**。
`name` は全タブ列挙では一意でなく(契約 T-§2.2。予約名は 2 org 並走で構造的に衝突する)、`role` はさらに
非一意なので、**他タブのピアが自タブのペインと `name` / `role` を共有していても矛盾しない**。この一致を
同タブの証拠に使うと、他タブのピアが同タブと誤分類され、† の identity 確認を**素通り**して別 org へ
SUSPEND / SHUTDOWN が飛ぶ(本節が防ごうとしている事象そのもの)。
同タブと積極的に結論してよいのは、`same_tab == True` が載っているか、`tab` の値が **caller 自身のタブの
値と一致**することを確認できた場合だけである。どちらも取れなければ unknown のまま他タブ扱いにする。
## Phase 2: 未応答ワーカーのスクレイプ
応答がなかったワーカーについて:
1. `.state/workers/` から該当ワーカーの状態ファイルを読み、Pane Name と Directory を取得
2. Pane Name を Phase 1 の**台帳**で **`pane_id`** に解決する。`inspect_pane` はペイン制御面なので、
使うのは `list_panes` 由来の `pane_id` であって `peer_id` ではない:
- 台帳に `pane_id` が無いピア(`list_panes` に居ない、または Phase 1 の**列挙面のゲート (i) が
不成立**で台帳全体に id を載せなかった)は 3 通りに分かれる。**いずれもスクレイプは
諦め、Step 4 の git 情報だけで状態を推定する**が、**記録する状態が違う**:
- `same_tab=False` / ‡ で他タブと導出されたピア → 「他タブに居るのでスクレイプ不能」。生存して
いる可能性があるので**消滅と読み替えない**
- それ以外で、**F-1 の (i) `pane_exited` 観測 or (ii) 列挙が窓口タブのものと確認済み** →「消滅済み」
- それ以外で **F-1 のどちらも取れない** → **indeterminate**。フォーカスが別タブへ移っただけで
生存中のピアが不在に見えている可能性があるので、消滅済みとして記録せず判定不能のまま残し、
Phase 4 の最終報告に載せる。**列挙面のゲート (i) が不成立のケースはここに落ちる** — 列挙を
自タブと確立できていない以上 F-1 の (ii) も同時に満たせないため
- Phase 1 から時間が経っているので、使う直前に `mcp__org-broker__list_panes` を取り直し、その
`pane_id` の `name` / `role` が台帳と一致することを確認してから叩く。**一致しなければ
pane_id が別ペインへ再割当てされている**ので、そのペインには触れずスクレイプを諦める
(Phase 3.7 と同じ identity 照合の規律。無関係なペインの画面を当該ワーカーの
「Current State at Suspend」に書き込むのを防ぐ)
3. 画面内容スクレイプで最新のコンソール出力を読む:
```
mcp__org-broker__inspect_pane(target="<pane_id>", format="text")
```
`target="worker-{task_id}"` の**名前指定はしない** — 2.0 系では名前解決が呼び出し側のタブ内に
限定され、同名ペインが別タブに存在しうるため。画面表示だけでは不十分な場合は、次の Step 4 の
git 情報で補完する
4. ワーカーの作業ディレクトリで以下を実行:
- `git status`
- `git diff --stat`
- `git log --oneline -5`
5. これらの情報からワーカーの状態を推定する
## Phase 3: 状態書き込み
> **state-db cutover (M4, Issue #267)**: `.state/state.db` が唯一の SoT。
> 構造化セクション (Status / Updated / Suspended / Dispatcher / Curator /
> Worker Directory Registry / Active Work Items / Resume Instructions) は
> **必ず StateWriter 経由で書く**。`transaction()` の post-commit hook が
> `.state/org-state.md` を DB から自動再生成する (markdown 直接編集禁止 —
> drift_check で検出される)。free-form な session notes / Pending Lead /
> 学び等は `notes/` 配下に保存する (`notes/README.md` 参照)。
> `.state/journal.jsonl` は M4 で廃止 (events テーブルが SoT)。
> DB が古い場合は
> `python -m tools.state_db.importer --db .state/state.db --rebuild --no-strict`
> で再構築する。
1. 既存の `org-state.md` を `org-state.prev.md` にコピー(バックアップ)
2. **DB に Status / Suspended を書く** (`StateWriter.transaction()` 経由。post-commit hook が `.state/org-state.md` を自動再生成、regen 失敗時も DB は確定済みで stderr 警告のみ):
```bash
python -c "
from datetime import datetime, timezone
from pathlib import Path
from tools.state_db import connect
from tools.state_db.writer import StateWriter
ts = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%fZ')
conn = connect('.state/state.db')
with StateWriter(conn, claude_org_root=Path('.')).transaction() as w:
w.update_session(status='SUSPENDED', suspended_at=ts, updated_at=ts)
"
```
- bash / zsh / PowerShell すべて `"..."` 内の改行をそのまま透過するので multi-line でも cross-shell。Windows CMD は heredoc 不可なので `py -3 -c "ts=...; conn=...; w=...; w.begin(); w.update_session(...); w.commit()"` の単行 fallback を使う(その場合 `transaction()` の rollback / regen 自動 swallow は失われるので追加で try/except を書く)
- 同コマンドが `.state/org-state.md` の Status 行を `SUSPENDED` に切り替える (DB 由来で再生成)
- free-form な「Resume Instructions の補足説明」「Pending Lead」「学び」等は **`notes/` に保存する** (`notes/README.md` 参照)。markdown 直接編集は drift_check で検出される。`update_session(resume_instructions=...)` は構造化セクションとして DB に書く
3. 各 Work Item の状態を更新する場合は `upsert_run(task_id=..., status=...)` を `transaction()` 内で呼ぶ
4. 各ワーカーの `.state/workers/worker-{id}.md` を更新:
- Current State at Suspend セクションを追加/更新
- Progress Log に中断時の状態を追記
5. suspend イベントを DB に追記 (`tools/journal_append.py` は M4 で DB-only ルーティング。`ts` は自動付与):
```bash
py -3 tools/journal_append.py suspend \
reason=user_requested \
--json '{"active_workers": ["worker-xxx"], "pending_items": ["blog-redesign"]}'
```
event 名と payload key の規約は [`docs/journal-events.md`](../../../docs/journal-events.md) を参照。
## Phase 3.5: ダッシュボードサーバー停止
```bash
kill $(cat .state/dashboard.pid 2>/dev/null) 2>/dev/null || true
```
> **注**: この blind kill は「/org-start で再開する前提の中断」なので簡素なまま残す。daemon ごと
> 落とす [`/org-down`](../org-down/SKILL.md) では、pid recycle による誤 kill を避ける stale-pid-safe な
> 停止(`/proc` / `Get-CimInstance` の CommandLine 照合)に差し替える。
## Phase 3.6: secretary_queue_watcher の停止(broker のみ)
broker 面(`ORG_TRANSPORT=broker`)で org-start Block C3 が `run_in_background` で
常駐させた滞留 watcher を停止する。**renga では watcher が存在しない**(queue.jsonl 非依存)ので、
transport が `renga` なら本 Phase は**まるごと skip**する。
watcher は起動時に `.state/secretary_queue_watcher.json` へ自分の pid / cwd / cmdline / started_at /
broker_state_dir を記録している。停止は **pid 単独で kill せず**、(a) 記録された broker_state_dir が
現在の `ORG_BROKER_STATE_DIR` と一致し(別 org / 別 broker の watcher 誤停止防止)、かつ (b) pid が
生存し live argv(Linux/WSL は `/proc/<pid>/cmdline`、macOS/BSD は `ps -p <pid> -o args=` フォールバック)が
本 watcher であることを照合できたときだけ SIGTERM する。照合が外れたら kill せず sidecar を stale として
削除する(誤 kill 防止)。この照合ロジックは helper に入っているので、POSIX では 1 行呼ぶだけでよい:
**Mac / Linux / WSL**:
```bash
python3 tools/secretary_queue_watcher.py --stop # Windows で console python を使う場合は py -3 ...
```
出力の 1 行(`STOP: ...`)で結果を確認する(`SIGTERM を送信し停止` / `stale sidecar を削除` /
`既に停止済み`)。exit 0 が正常系(停止・stale 掃除・既停止のいずれも 0)。macOS は `ps` フォールバックで
identity 照合できるので `--stop` がそのまま効く。exit 2(identity 未確認)は `/proc` も `ps` も無い環境
(Windows native)でのみ出るシグナルで、その場合は次の PowerShell 手順を使う。
**Windows native(PowerShell)** — `/proc` が無く helper の argv 照合が使えないので、
`Get-CimInstance Win32_Process` の CommandLine で identity を照合してから `Stop-Process` する
(`kill -0` / `kill -TERM` の直訳ではなく Windows 別手順):
```powershell
$pf = ".state\secretary_queue_watcher.json"
if (Test-Path $pf) {
$rec = Get-Content $pf -Raw | ConvertFrom-Json
$wpid = [int]$rec.pid
$ownOk = $false
try {
if ($env:ORG_BROKER_STATE_DIR) {
$ownOk = ((Resolve-Path $rec.broker_state_dir).Path -eq (Resolve-Path $env:ORG_BROKER_STATE_DIR).Path)
} else {
$ownOk = ((Resolve-Path $rec.cwd).Path -eq (Get-Location).Path)
}
} catch { $ownOk = $false }
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$wpid" -ErrorAction SilentlyContinue
$idOk = $proc -and ($proc.CommandLine -match 'secretary_queue_watcher\.py')
if ($ownOk -and $idOk) {
Stop-Process -Id $wpid -Force
Write-Output "secretary_queue_watcher (pid=$wpid) stopped"
} else {
Write-Output "watcher pid stale / different org / not running; not killing, removing stale sidecar"
}
Remove-Item $pf -ErrorAction SilentlyContinue
}
```
## Phase 3.7: attention watcher の停止(ペイン teardown の前)
attention watcher は dispatcher ペインの右 split に常駐する CLI ペインなので、**Phase 4 のペイン
一括 teardown より前に**停止する(dispatcher を先に閉じると attention ペインが孤児化 / pane_id
recycle され、後続の識別を壊すため)。attention watcher を起動していないセッションでは sidecar も
live pane も無く、本 Phase は no-op。
停止は [`/org-attention-stop`](../org-attention-stop/SKILL.md) と同じ **identity 照合**を使う
(sidecar の pane_id を無検証で `close_pane` しない。pane_id が別ペインへ再割当てされていると
無関係なペインを kill する — Issue #468):
1. `mcp__org-broker__list_panes` で `name="attention"` **または** `role="attention"` の live pane を
**全て**集める(= 確認済み attention ペイン集合。各レコードの **`id`(= pane 制御用の pane_id)** を控える)。
**この列挙も (G-OWNTAB) の 2 手段で自タブのものと確立してから使う** — 確立できなければ close へ
進まない(相対セレクタへフォールバックしない)。以降の手順 5 / 6 は gone / recycled と同じ
「close しなかった」経路として扱い、**attention watcher を停止できなかったことを人間への報告に
明記する**(Phase 4 手順 7 の部分中断報告に載せる)
2. `.state/attention_pane.json` を `Read` で開けたら `pane_id` を読む(= sidecar pane_id)。無ければ
「sidecar 無し」
3. sidecar pane_id を **list_panes が返す name/role** で分類する(sidecar 記録の name は信用しない):
- **verified**(確認済み集合に含まれる)→ その pane はいまも本物の watcher。close 対象
- **recycled**(list_panes にあるが name/role が attention でない)→ pane_id が別ペインへ再割当て済み。
**絶対に close しない**
- **gone**(list_panes に無い)→ 既に消滅。close しない
4. 確認済み集合の各ペインを **`list_panes` 由来の `pane_id`** で `mcp__org-broker__close_pane(target="<pane_id>")` する
(`target="attention"` の name 指定はしない — role だけ持つ孤児に当たらないため)。
ここは **pane 制御文脈**で、かつ対象は**自タブと確立済みの `mcp__org-broker__list_panes`**(手順 1)由来=
caller のタブに居ることが確定
しているので、`[pane_not_found]` / `[pane_vanished]` は既に閉じた扱いで skip してよい(close の
目的は消滅であり、既に消えていれば結果は同じ。他タブのペインに対する例外と messaging 文脈での
扱いは Phase 4 の「**`[pane_not_found]` の扱い — messaging と pane 制御で分岐する**」を参照)
5. sidecar があれば **分類によらず必ず削除**する:
```bash
rm -f .state/attention_pane.json # Windows native は del .state\attention_pane.json
```
6. journal に 1 行追記する。**実際に close したペインがある場合のみ** pane_id を載せ、recycled / gone で
close を 1 つも行わなかった場合は `reason=stale_sidecar` にする(無関係なペインを停めた誤記録を防ぐ):
```bash
bash tools/journal_append.sh attention_watch_stopped pane_id=<N> # close した場合
bash tools/journal_append.sh attention_watch_stopped reason=stale_sidecar # close しなかった場合
```
分類別の詳細な挙動と報告文は [`/org-attention-stop`](../org-attention-stop/SKILL.md) を参照(本 Phase は
その要点を suspend フローに埋め込んだもの)。
## Phase 4: 全ペイン停止
停止順序が重要。ワーカー → ディスパッチャー → キュレーターの順で停止する。
**このフェーズは全ステップを Phase 1 の台帳の id で回す**: `mcp__org-broker__send_message` の `to_id` は
`peer_id`、`mcp__org-broker__close_pane` の `target` は `pane_id`、pending 集合と `poll_events` の突合は台帳
エントリを主キーにする。`name` は人間可読のラベルとしてのみ扱い、宛先解決には使わない — 2.0 系では
名前解決が呼び出し側のタブ内に限定され、同名ペインが別タブに存在しうるため。
### `[pane_not_found]` の扱い — messaging と pane 制御で分岐する
本フェーズ(および Phase 1 / Phase 2)の送信・ペイン制御すべてに適用する分岐。分岐の**第一軸は
呼び出し文脈**、**第二軸は同タブ / 他タブ**である(世代では分けない)。正本は
[`.claude/skills/org-delegate/references/renga-error-codes.md`](../org-delegate/references/renga-error-codes.md)。
- **messaging 文脈**(`mcp__org-broker__send_message`。**broker では `[peer_not_found]`** が同じ意味で返る):
受けても**即「ワーカー閉鎖」と断定しない**。2.0 系では名前解決が送信者タブ内に限定され、また
宛先台帳が古く id が再割当て / 消滅していると、生存中のペインでもこのコードが返るため、次の順で
切り分ける:
1. `mcp__org-broker__list_peers` を取り直す(2.0 系は全タブ列挙なので他タブのワーカーもここで拾える)
2. **宛先の選び直しは名前で行わない**。全タブ列挙では `name` が一意でなく(契約 T-§2.2)、
同名レコードが複数返りうるので、名前一致で拾うと**別 org のピアへ SUSPEND / SHUTDOWN を
送りうる**。次の優先順で 1 件に絞る([`.claude/skills/org-delegate/references/renga-error-codes.md`](../org-delegate/references/renga-error-codes.md)
の messaging 分岐と同一規則):
- a. 台帳に控えてある **`peer_id` がまだ列挙に在れば、その id をそのまま使う**
- b. その `peer_id` が消えている場合(再接続で id が振り直された等)、名前一致レコードは
候補にすぎない。採用してよいのは**名前が一意に定まる範囲に閉じていると分かっている**
ときだけで、次のどちらかを満たす場合:
- **`same_tab: true` を確認できたレコード**(同一タブ内でのみ名前は一意 — 契約 §1.8)
- **旧版 fallback の列挙**(全レコードで marker 欠落)で、**その名前のレコードが 1 件だけ**の場合。
marker 無し列挙は caller のタブに閉じていることが契約 T-§cap の conformance MUST で保証され、
そのタブが窓口のタブであることは手順 1 のフォーカス前提で確認済みなので、名前は一意に働く。
**現在配備の backend(`org-broker` / renga 1.x)はここを通る** — この枝が無いと、再接続で
id が変わった生存ワーカーを retarget できず indeterminate に落として取り逃がす
- c. 上のどちらも満たさない(他タブの同名しか無い / 同名が複数残る)→ **再送しない**。
lifecycle 確認(次項)へ進む
3. 絞れた **`peer_id`** で **1 回だけ**再送する(ループにしない — 2 回目以降も結果は同じで、
本当に閉じていた場合の検知が遅れるだけ)
4. **`peer_id` 宛でも失敗した / 2-c で再送できなかったときに初めて** lifecycle **確認**へ進む。ここで確認を省いて
いきなり閉鎖確定にしない(契約 T-§2.1 の復旧順は確認段を含む):
- 同タブのピア: **`pane_exited` の観測**をもって消滅を確定する。`mcp__org-broker__list_panes` の不在を根拠に
するのは **F-1 の (ii) を満たすときだけ**で、満たせなければ indeterminate として残す
- **他タブのピア**(`same_tab=False` / ‡ で他タブと導出): `list_panes` には原理的に出ないので
不在は証拠にならない。`pane_exited` の観測か、後続 `mcp__org-broker__list_peers` からの消失で確定する
- どちらも取れなければ **indeterminate**。閉鎖に倒さず journal に記録して人間に報告する
4. 消滅が確定したら pending 集合から外す。**worker 状態ファイルの `Status: pane_closed` は窓口が
書かない** — この terminal transition の書き手はディスパッチャーである
([`docs/contracts/state-semantics-contract.md`](../../../docs/contracts/state-semantics-contract.md))。
窓口は Progress Log への追記と journal 記録に留める
- **pane 制御文脈**(`mcp__org-broker__close_pane` / `mcp__org-broker__inspect_pane` / `mcp__org-broker__list_panes` の target):
- **同タブのペイン**に `pane_id` 宛で `[pane_not_found]` / `[pane_vanished]` が返ったら、従来どおり
「既に閉じた」扱いで skip してよい(close は消滅させるのが目的なので、既に消えていれば結果は同じ)
- **他タブのペインは例外 — 閉鎖と読んではならない**。契約 T-§4.2 はタブ横断の pane-addressed
control が `pane_not_found` を返すと規定しているので、**生存中でも同じコードが返る**。
このコードを「閉じた」と読むと、稼働中のワーカーを残したまま「全ペイン停止」を完了報告する。
該当ピアは pending に残し、journal に記録して人間に報告する(停止できない残存)
- 名前宛で受けた場合はこの結論を出さず、`pane_id` に解決し直してから判断する
- **`close_pane` / `set_pane_identity` はそもそも名前宛・`"focused"` 宛で撃たない**((G-MUST))。
この 2 本は Group B なので、`caller_scope` が広告されていても
`caller_scope_close_identity` が確立していなければ相対セレクタが user 可視タブ / 全タブに
解決されうる。**(G-OWNTAB) の 2 手段で自タブと確立した `mcp__org-broker__list_panes` 由来の数値 `pane_id`**
でのみアドレスする
### 停止手順
1. `mcp__org-broker__list_peers` と `mcp__org-broker__list_panes` を取り直し、Phase 1 の**台帳を最新化**する(Phase 1 から
時間が経っているため、`peer_id` / `pane_id` の両方を取り直す)。再構築でも Phase 1 手順 1 の
**join 規則をそのまま適用する**(`pane_id` の出所は `mcp__org-broker__list_panes` だけ / 列挙を自タブと
確立できなければどのエントリにも `pane_id` を割り当てない((G-OWNTAB)) / `role` は join キーに
しない / 他タブ・`same_tab` 欠落のエントリには `pane_id` を割り当てない)。**他タブ判定のピアが
Phase 1 から増えていた場合は、真理値表の operational gate に従ってその差分を人間に報告し、
確認を得てから停止指示へ進む**
- **この取り直しが (G-OWNTAB) の確立段でもある**: 以降の `mcp__org-broker__close_pane` はすべてこの列挙から
採った数値 `pane_id` で撃つので、**列挙を自タブと確立できたかをここで判定して以降の全 close に
引き回す**(単一タブ backend = `org-broker` / `caller_scope` の 2 手段だけ。`list_peers` の
`same_tab` は確立手段に数えない)。**いずれも成立しなければ close を 1 つも撃たない** — 台帳の
全エントリが「`pane_id` を持たないエントリ」と同じ扱いになり、pending のまま手順 7 の**部分中断**
報告に載せる(相対セレクタへフォールバックしない)
2. **ワーカーを先に停止**: Phase 1 の**宛先選定の真理値表**を `R = {worker}` で適用し、選ばれた
ワーカーの **`peer_id`** を `to_id` に渡して `mcp__org-broker__send_message` で終了を指示:
「SHUTDOWN: 作業を終了してください。」
3. **ワーカーペインが閉じたことを確認** — 2-pass 構造で実施:
**Pass 1 (polite shutdown の観察、最大 10 秒)**:
`mcp__org-broker__poll_events` で `pane_exited` を long-poll する。`types=["pane_exited"]` フィルタで他 type を除外しつつ、deadline 内でループして待機対象が全て閉じたら break:
```
pending = {SHUTDOWN を送った全ワーカーの台帳エントリ set}
cursor = None # 初回は since 省略
deadline = now + 10 秒
while pending not empty and now < deadline:
remaining_ms = (deadline - now) ミリ秒
result = mcp__org-broker__poll_events(
since=cursor,
timeout_ms=min(remaining_ms, 10000),
types=["pane_exited"]
)
cursor = result.next_since
for ev in result.events:
# 突合は id で行う。契約 T-§3.1 が canonical 名を固定している
ev_pane = ev.id ?? ev.pane_id # canonical: ev.id (list_panes と同じ id 空間)
ev_peer = ev.peer_id ?? ev.agent_id # canonical: ev.peer_id (list_peers と同じ id 空間)
# id 突合は「両辺が非 null」を前提にする。name のみのイベントでは ev_pane / ev_peer が
# どちらも null になり、pane_id 未設定 (他タブ) のエントリと null == null で誤一致して
# 無関係な生存ワーカーを pending から external してしまう
hit = pending の中で
(ev_pane is not None かつ entry.pane_id == ev_pane) または
(ev_peer is not None かつ entry.peer_id == ev_peer)
if hit is None and ev.name is not None:
# name フォールバックは「同タブ確定エントリ」に限定する(下記)
hit = pending の中で entry.name == ev.name かつ entry が同タブ確定
if hit: pending.remove(hit)
# deadline 到達 or pending が空で抜ける
```
- **突合キーは id**。ratified §3.1 は per-event フィールド名を規定していないが、批准済み amendment (2026-08-09) の
**T-§3.1 が canonical 名を `id`(pane 側)/ `peer_id`(peer 側)に固定**している。上の疑似コードが
`ev.pane_id` / `ev.agent_id` も見るのは、canonical 名を出さない backend への**寛容(tolerance)**で
あって、これらが正の名前だという意味ではない。**pane 側 id と peer 側 id のどちらか一方でも
一致したら hit** とする(他タブピアは `list_panes` に出ないので pane 側だけでは join できない)
- **`name` 単独を突合キーにしてはならない**。2.0 系の名前はタブ内一意でしかないので、別タブの
同名ペインの `pane_exited` で自組織の**生存**ワーカーを誤って retire する。`name` フォールバックは
`same_tab=True` / 旧版 fallback で**同タブと確定しているエントリ**に限って使う(他タブ対象が
pending に居る間は name フォールバックを無効にしてもよい)
- 初回 `since` 省略で「今以降のイベントだけ」セマンティクス(過去の pane_exited を replay しない)
- `types=["pane_exited"]` filter は cursor を全 type で advance させるので重複 scan なし
- filter 不一致イベント到着で long-poll が early return (`events:[]` + advanced cursor) するため、空応答時は deadline までループ継続
- 10 秒以内に閉じなかった残留ワーカーは Pass 2 へ
**Pass 2 (残留ワーカーへのフォールバック + 再確認、最大 5 秒)**:
- **close の前に (G-OWNTAB) が成立していることを確かめる**(手順 1 で判定済み。成立していなければ
この Pass 2 では 1 つも close せず、`pending` をそのまま次の再確認へ送る)
- **close の直前に identity 照合する**(Phase 3.7 の attention watcher 停止と同じ idiom。台帳を
取り直してから時間が経つと `pane_id` が別ペインへ再割当てされうるため、控えた id が期待どおりの
ペインをなお指していることを毎回確かめる):
1. `mcp__org-broker__list_panes` を実行する
2. 控えた `pane_id` のレコードが `name == "worker-{task_id}"` かつ `role == "worker"` を**なお
指している** → その数値 id で close する
3. レコードはあるが `name` / `role` が一致しない(別ペインへ再割当て済み)→ **close しない**
(相対セレクタへフォールバックしない)。`pending` に残したまま次の再確認へ送る
4. レコードが列挙に無い → close 対象を確認できないので **close しない**。消滅の確定は F-1 の
(ii) を満たすときだけで、満たせなければ「判定不能」に倒す
- `pending` に残っているエントリのうち、**`pane_id` を持ち、上の照合を通ったもの**
(= caller のタブに居ることが確定したペイン)それぞれに対して:
```
mcp__org-broker__close_pane(target="<照合済みの数値 pane_id>")
```
でペインを明示破棄する(`worker-{task_id}` のような相対セレクタでは撃たない — (G-MUST))。成功時は `"Closed pane id=N."` テキストが返る。ここは **pane 制御文脈**なので `[pane_not_found]` / `[pane_vanished]` は既に閉じた扱いで skip してよい([`.claude/skills/org-delegate/references/renga-error-codes.md`](../org-delegate/references/renga-error-codes.md) 参照)。`[last_pane]` はワーカー停止段階では通常発生しない(窓口/ディスパッチャー/キュレーターが残っているため)
- **`pane_id` を持たないエントリ(他タブのワーカー、および (G-OWNTAB) 不成立で id を割り当てられ
なかったエントリ)には `close_pane` を撃たない**。タブ横断の
pane 制御は原理的に不可で、返る `[pane_not_found]` は閉鎖の証拠にならない(契約 T-§4.2)。
このエントリは pending に残したまま次の再確認へ送る
- その後、同じ `poll_events` ループを `timeout_ms=5000` / deadline 5 秒で再度回し、close_pane 由来の `pane_exited` を消化する
- Pass 2 後もまだ `pending` が空でなければ `mcp__org-broker__list_panes` / `mcp__org-broker__list_peers` で生存確認する(**id 突合**。`list_panes` は 1 タブ分しか返さないため、他タブのワーカーは `list_peers` からの消失で判定する。**`list_panes` の不在を停止の証拠にするのは F-1 の (ii) を満たすときだけ**で、満たせなければ「判定不能」に倒す)。**残存または判定不能なら**、閉鎖扱いにせず journal に記録して人間に報告する(強制終了は現状未サポート。「停止できなかったワーカーが居る」ことを報告に明記し、全ペイン停止を偽って完了報告しない)
4. **ディスパッチャーを停止**: 真理値表を `R = {dispatcher}` で適用し、選ばれたディスパッチャーの
**`peer_id`** を `to_id` に渡して `mcp__org-broker__send_message` で終了を指示:
「SHUTDOWN: 作業を終了してください。」
5. **キュレーターを停止(存在する場合のみ)**: curator は常駐しないため、通常このステップは
no-op。curator の存在確認は `mcp__org-broker__list_peers` の `role == "curator"`(`name == "curator"` は補助)
で行い、**`peer_id`** を得る(`list_panes` は呼び出し側のタブしか返さないので、別タブで起動した
curator を取りこぼす)。存在した場合(オンデマンド curate 実行中に suspend が重なったケース)
のみ真理値表を `R = {curator}` で適用し、その `peer_id` 宛に `send_message` で終了を指示:
「SHUTDOWN: 作業を終了してください。」(curate は move-then-mark 設計のため途中停止でも
破壊的な中間状態は残らない)
6. ディスパッチャー(および存在した場合のみキュレーター)も (3) と同じ 2-pass 構造で確認する。
pending 集合は名前ではなく**台帳エントリの集合**にする(`pending = {dispatcher のエントリ}`、
curator が存在した場合はそのエントリも追加)。`pane_exited` の突合も (3) と同じく
pane 側 id / peer 側 id のいずれか一致で行い、`ev.role == "dispatcher"` / `"curator"` と
`ev.name` は同タブ確定エントリに対する補助に留める:
- Pass 1: `poll_events(types=["pane_exited"], timeout_ms=10000)` 相当ループ
- Pass 2: **(3) と同じ前段**((G-OWNTAB) の成立確認 → close 直前の identity 照合。期待する
`name` / `role` は `dispatcher` / `curator` に読み替える)を通したうえで、`pending` に残った
エントリのうち **`pane_id` を持ち照合を通ったもの**に
`mcp__org-broker__close_pane(target="<照合済みの数値 pane_id>")` を送り(相対セレクタでは撃たない)、
`poll_events` ループ
(timeout_ms=5000) で再確認。`pane_id` を持たない(他タブの / (G-OWNTAB) 不成立の)エントリや
照合が外れたエントリには撃たず、(3) と同じく
残存として人間に報告する
**最後のペイン (窓口) の扱い**: ディスパッチャー(と存在した場合のキュレーター)を閉じた時点で、
窓口ペインが窓口タブに残る最後のペインになる。**窓口は自分自身を `mcp__org-broker__close_pane` しない**。
その根拠は次の 2 つで、どちらも「最後の 1 ペインだから backend が閉じさせてくれない」ことには
依存しない**構造的な理由**である:
- **窓口は人間との唯一の接点である**: 自分を閉じると、この後の「人間に報告」ステップで suspend 完了を
渡す相手の画面が消える。報告前に自分を殺すと、人間から見て「中断できたのか分からないまま画面が
消えた」になる
- **自己終了は人間の責務境界にある**: 窓口プロセスの終了は人間の `exit` / `/exit` / 端末クローズに
委ねる。org-suspend が担うのは「状態保存 + ja 管理下の補助プロセスとペインの停止」までで、窓口
ペインを閉じる責任は負わない(冒頭の「責務境界(/org-suspend と /org-down)」で引いた線と同じ)
`[last_pane]` は**エラーハンドリングとしてのみ残す**: 何らかの経路で窓口ペインに `close_pane` が飛び
`[last_pane]` が返った場合は、**強制再試行せず**そのまま人間の `exit` に委ねる。ただしこのコードを
自己 close の安全網として当てにしてはならない — このコードの意味は
[`.claude/skills/org-delegate/references/renga-error-codes.md`](../org-delegate/references/renga-error-codes.md)
のとおり「**唯一のタブの**唯一のペイン」であり、他のタブが存在する状況で窓口タブの最後のペインが
保護されるかは 2.0 系では未確認である(保護されない読みが素直だが、いずれにせよ当てにしない)。
自己 close をしない根拠は上の 2 つだけである。
7. 人間に報告。**`pending` と `unresolved` が両方空のときだけ「中断しました」と言い切る**。
どちらかに残りがあれば**部分中断**として報告し、完了を偽らない:
**全て停止できた場合**:
```
組織を中断しました。
- 保存済み: {N}件の作業アイテム
- 状態ファイル: .state/org-state.md
/org-start で再開できます。
```
**停止できなかった / 判定できなかったピアが残る場合**(`pending` が空でない、`unresolved` が
空でない、他タブの対象 role ピアを検出した、Phase 3.7 で attention watcher を停止できなかった、
のいずれか):
```
組織を部分的に中断しました(停止しきれていないペインがあります)。
- 保存済み: {N}件の作業アイテム
- 状態ファイル: .state/org-state.md
- 停止できなかったワーカー: {peer_id / name / cwd を列挙}
- role が判定できず対象外にしたピア: {peer_id / name / cwd を列挙}
- 他タブに見えたが自組織と確認できずに対象外にしたピア: {peer_id / name / cwd を列挙}
- 停止できなかった attention watcher: {pane_id を列挙}
これらは走り続けている可能性があります。手動での確認 / 停止をお願いします。
/org-start で再開できますが、上記のペインが残っている前提でご判断ください。
```
- 該当が無いセクションは行ごと省く。**1 件でも残っていれば「組織を中断しました」の文面は使わない**
(全ペイン停止を偽って報告しないこと自体が本フェーズの目的)
Files in this skill
- SKILL.md
- SKILL.md.in
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.