Back to skills
SKILL.md
Dispatcher Resume
ASecurity/dispatcher-handover で書き出した handover ファイルを読み込み、 ディスパッチャーを新しい session で復帰させる。/clear 直後の最初のターンで使う。 state.db の dispatcher_pane_id / dispatcher_peer_id を atomic に更新し、 worker monitoring の /loop 3m を再開する。 「ディスパッチャー復帰」「resume」「引き継ぎから再開」と secretary から指示された ときに使う。/org-start ではない(ワーカー・窓口・キュレーターは生きている前提)。
- 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 dispatcher-resume --agent claude-codeAre you the author of Dispatcher Resume?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/suisya-systems-dispatcher-resume)---
name: dispatcher-resume
owner: dispatcher
description: >
/dispatcher-handover で書き出した handover ファイルを読み込み、
ディスパッチャーを新しい session で復帰させる。/clear 直後の最初のターンで使う。
state.db の dispatcher_pane_id / dispatcher_peer_id を atomic に更新し、
worker monitoring の /loop 3m を再開する。
「ディスパッチャー復帰」「resume」「引き継ぎから再開」と secretary から指示された
ときに使う。/org-start ではない(ワーカー・窓口・キュレーターは生きている前提)。
effort: low
allowed-tools:
- Read
- Bash(echo:*)
- Bash(py -3 ../tools/journal_append.py:*)
- Bash(bash ../tools/journal_append.sh:*)
- Bash(py -3 ../tools/capability_gate.py:*)
- Bash(python3 ../tools/capability_gate.py:*)
- Bash(python3 -c:*)
- Bash(py -3 -c:*)
- Bash(ls:*)
- Bash(mv:*)
- mcp__org-broker__set_summary
- mcp__org-broker__list_panes
- mcp__org-broker__set_pane_identity
- mcp__org-broker__list_peers
- mcp__org-broker__check_messages
- mcp__org-broker__send_message
---
# dispatcher-resume: ディスパッチャーの復帰
`/dispatcher-handover` で書き出した `.state/dispatcher-handover.md` を読み込み、
ディスパッチャーとして最低限の自覚(組織員としての立ち位置・進行中の派遣・監視
対象ワーカー)を復元し、`/loop 3m` の worker monitoring を再開する。
> **前提**:
> - ワーカー / 窓口 / キュレーターのペインは前 session から生きたまま残っている。
> 新たに spawn しない(/org-start ではない)。
> - 自ペイン(name=`dispatcher`)も生きている。secretary が `send_keys` で
> `/clear` → `/dispatcher-resume` を打鍵した直後の状態。pane_id / peer_id は
> 変わっていないはずだが、必ず観測して state.db を **atomic 更新する**。
> - state DB (`.state/state.db`) はそのまま使う。
> - 監視 gap を埋める内部状態ファイル(`.state/dispatcher-event-cursor.txt`(backend
> session に束縛された poll_events cursor) /
> `.state/dispatcher/worker-idle-state.json` / `.state/dispatcher/curate-inflight.json`(存在時) /
> `.state/pending_decisions.json`)は
> 前 session から残っている。新規作成・初期化しない(既存値からそのまま継続)。
> - handover ファイルが存在しないか古すぎる場合は、`/org-start` を案内して停止する。
> **輸送層(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)。)
## Step 0 の前: 残存 loop 予約 / stale 再発火ガード(重複起動の早期遮断)
`/dispatcher-resume` は **handover を 1 回だけ消費する one-shot** である(Step 7 で
`.state/dispatcher-handover.md` を `.consumed.md` に rename する)。だが Step 5 で起動する
`/loop 3m` の予約が、過去のバグや手動操作で `/dispatcher-resume` 自身を反復対象に握って
いると、監視サイクルのたびに本 skill が再発火する。この **stale 再発火 / 重複起動** を
Step 0 に入る前に遮断する(恒久対策の経緯は
[`knowledge/raw/2026-06-19-dispatcher-resume-loop-recursion.md`](../../../knowledge/raw/2026-06-19-dispatcher-resume-loop-recursion.md))。
1. live / consumed の handover 状態を **存在ベース**で判定する(`now` との時刻比較に
依存しない。cold-start vs resume の分岐が live `.md` のみを対象とする
`.dispatcher/CLAUDE.md` の規約と揃える):
```bash
python3 -c "
import os
md = '../.state/dispatcher-handover.md'
cm = '../.state/dispatcher-handover.consumed.md'
if os.path.exists(md):
print('resume') # 正規の resume: live handover あり
elif os.path.exists(cm):
print('already_consumed') # 既に消費済み: 残存 loop / 重複起動の疑い
else:
print('no_handover') # handover が一度も無い: 真の cold-start 候補
"
```
2. **`already_consumed`(live `.md` なし・`.consumed.md` あり)**: live handover が無い
resume 起動だが `.consumed.md` が残っている。これは「(i) 直前に成功した resume の残存
loop 予約による再発火 / 重複起動(組織は健全・監視ループ稼働中)」か「(ii) 組織が止まって
いて本来 cold-start が要る状況での誤起動」のいずれか。**存在判定だけで no-op に倒すと (ii)
の cold-start 案内を恒久的に隠す**ので、監視対象が実際に live かを観測して分岐する:
- `mcp__org-broker__list_panes` / `mcp__org-broker__list_peers` で **監視対象が live か**を確認する: active な
worker ペイン(`role == "worker"`)が 1 つ以上ある、または
`.state/dispatcher/curate-inflight.json` が存在する(= 監視ループが回っているべき状態)。
**`list_peers` の直前に
[`.claude/skills/org-delegate/references/capability-first-drive-operational-gate.md`](../org-delegate/references/capability-first-drive-operational-gate.md)
を Read し、`monitoring-read-only` の分岐を適用する**(同 reference §6 の表 #5)。capability 形
かつ未承認なら列挙を live 判定に使わず破棄し、`list_panes` の `role == "worker"` と
`curate-inflight.json` の有無だけで分岐する(停止しない・待ち時間 0 分)。
**承認済み(同 reference §2 の `first_drive` が `recorded`)のときは §1-2 の三値判定を通し、
「在」と出たワーカーだけを live として数える** — 他タブの別 org のワーカーを live と誤認すると
(ii) の cold-start 経路が抑止される。**「unknown」は live に数えないが、(ii) にも倒さない**:
判定不能を「監視対象なし」と読み替えると誤った `/org-start` 案内で健全な組織を壊しうる
(不可逆側)。**終端処理は (i) と同じ**(早期 exit + Step 5 の monitoring ディレクティブで
`/loop` を再 arm し、残存予約を空のまま放置しない)とし、secretary には
`DISPATCHER_RESUME_INDETERMINATE: 監視対象の同タブ性を確定できず(unknown N 件)。
live 判定を保留して監視ループのみ再開しました。` を送って判断を仰ぐ。
**unknown のワーカーは監視対象・active のまま残す**(`.state/workers/worker-*.md` を
`Status: pane_closed` にせず `WORKER_PANE_EXITED` も送らず
`.state/dispatcher/worker-idle-state.json` の record も消さない)。再評価は再 arm した
`/loop 3m` の通常サイクルが行う。
**どの分岐にも落ちないまま skill を終えない**(一度きりの skill が loop に再発火され続ける)。
- **(i) 監視対象が live = stale 再発火 / 重複起動**: 組織は健全で cold-start 不要。
`DISPATCHER_RESUME_FAILED` を **送らず**(誤った `/org-start` 案内を出さない)、
Step 1 以降に進まず早期 exit する。**残存予約を空のまま放置しない**: 監視 `/loop` が
`/dispatcher-resume` を反復対象に握っている兆候があれば、Step 5 の monitoring 専用
ディレクティブ(INVARIANT(loop-prompt) 準拠の `/loop 3m ...`)で loop を明示的に再 arm
して予約を上書きする(重複起動はしない=単一 loop に収束)。既に monitoring ディレクティブ
で回っていれば当該サイクルは **no-op heartbeat**(何も出力しない)。どちらの分岐でも予約を
skill 自身に握らせたまま放置しない。再発火の事実だけ journal に残す:
```bash
bash ../tools/journal_append.sh anomaly_observed \
source=dispatcher_resume worker=dispatcher kind=stale_loop_refire confidence=n/a \
note=handover_already_consumed
```
- **(ii) 監視対象が無い = 本来 cold-start が要る誤起動**: stale 再発火ではない。no-op に
倒さず **Step 1 の「handover ファイルなし」分岐へフォールスルーし `DISPATCHER_RESUME_FAILED`
→ cold-start 案内を出す**(cold-start 案内を隠さない)。
3. **`resume`(live `.md` あり)**: 通常どおり Step 0 へ進む。
4. **`no_handover`(live `.md` も `.consumed.md` も無し)**: 通常どおり Step 0 → Step 1 へ
進み、Step 1 の「handover ファイルなし」分岐で `DISPATCHER_RESUME_FAILED` → cold-start
案内に落ちる。
## Step 0: 自分の identity を確認する
1. `mcp__org-broker__set_summary` で「Dispatcher: 監視(resumed)」をセット
2. **caller pane id を確定する**(`focused: true` は「そのタブで今フォーカスされているペイン」で
あって caller 自身ではない。人間がフォーカスを移した瞬間、別ペインを `dispatcher` に改名し、
その id を `dispatcher_pane_id` として DB に焼き付けてしまう): 環境変数 `RENGA_PANE_ID`
(`echo "${RENGA_PANE_ID:-unset}"`)を起点に、`mcp__org-broker__list_panes` へ**同一 id のレコードが
在ることを照合**する。ここで確定した id が Step 2 で書く `dispatcher_pane_id` であり、以後の
routing はこれを掴む。規則の正本は
[`docs/contracts/backend-interface-contract.md`](../../../docs/contracts/backend-interface-contract.md)
T-§4.2 の caller pane id 取得規則
- **broker(`ORG_TRANSPORT=broker`)では `RENGA_PANE_ID` を読まず、最初から `caller-id 未確定` に
倒す**: broker には `RENGA_PANE_ID` に相当する caller pane id を out of band で供給する surface が
無いので、この取得規則は broker では常に未確定に落ちる。`list_panes` に数値 id のエントリが在って
手元で掴めていても例外にならない(数値 id は MUST の**片方**=相対セレクタでないことしか満たさず、
**もう片方**=自タブと確立済みの列挙から採った id でありその対象が caller であること、を満たさない)。
したがって broker では下の `caller-id 未確定` 分岐へそのまま進み、`dispatcher_pane_id` を渡さない
3. 照合できたレコードの name/role を確認:
- 期待値: `name == "dispatcher"` かつ `role == "dispatcher"`
- 不一致なら `mcp__org-broker__set_pane_identity(target="<RENGA_PANE_ID の値>", name="dispatcher", role="dispatcher")` で修復(相対セレクタでは撃たない)
- **`caller-id 未確定`**(`RENGA_PANE_ID` 未設定 / `list_panes` に同一 id 無し / broker 等で同等の
caller-id を入手できない): 自己修復を実行せず、**`dispatcher_pane_id` も渡さない**まま
(Step 2 の `update_session()` は未指定を「保持」と解釈するので既存値が温存され、これ自体が
fail-closed になる)Step 1 へ進み、identity 未確認であることを Step 6 のブリーフィングで
secretary に明示して続行判断を仰ぐ(`target="focused"` や裸 `name` へフォールバックしない)
4. `mcp__org-broker__list_peers` で `name == "dispatcher"` の `peer_id` を取得。
**呼び出しの直前に
[`.claude/skills/org-delegate/references/capability-first-drive-operational-gate.md`](../org-delegate/references/capability-first-drive-operational-gate.md)
を Read し、`monitoring-read-only` の分岐を適用する**(同 reference §6 の表 #4)。capability 形
かつ未承認なら列挙から `peer_id` を採らず破棄し、2 で確定した caller pane id で identity を
確定して進む(停止しない・待ち時間 0 分)。
**このとき Step 2 の DB 書き込みでは `dispatcher_peer_id` を渡さない**(`update_session()` は
None / 未指定を「保持」と解釈する契約なので、**既存値がそのまま残る**)。placeholder や
観測できなかった値を書くと、以後の routing が壊れた id を掴む。`dispatcher_pane_id` は
2 で確定した caller pane id をそのまま書く(`caller-id 未確定` のときは渡さない)。Step 6 のブリーフィングでは
`peer=<未取得: capability gate 縮退中>` と明示して secretary に伝える。
**capability 形かつ承認済み(同 reference §2 の `first_drive` が `recorded`)のときも、自分の
`peer_id` は名前引きで確定しない**(§1-2-d)。`mcp__org-broker__list_peers` は caller を除外するので、
`name == "dispatcher"` に一致したレコードは**同タブに 1 件だけ在っても自分ではありえず**、
他タブのみ / 同タブ性 unknown / `same_tab=True` が複数の各分岐と合わせて、**名前引きの結果が
何であれ `peer_id` は未確定のまま**先へ進む。**したがって Step 2 の DB 書き込みでは
`dispatcher_peer_id` を渡さない**(`update_session()` の「未指定 = 保持」で既存値を温存する。
生きた同一ペインを引き継ぐ resume では保持が正しく、渡さないこと自体が fail-closed になる)。
更新するのは 2 で確定した caller pane id(`dispatcher_pane_id`)だけである。Step 6 の
ブリーフィングでは `peer=<未取得: 名前引きでは自 peer を同定できない>` と明示して secretary に伝える。
## Step 1: handover ファイルを読み込む
ディスパッチャーの cwd は `.dispatcher/` なので、リポジトリ root のパスを 1 階層上に解決する。
1. `.state/dispatcher-handover.md` が存在するか確認:
```bash
ls -la ../.state/dispatcher-handover.md 2>&1
```
- 存在しない → secretary に通知して停止:
```
DISPATCHER_RESUME_FAILED: handover ファイルがありません。
/org-start でディスパッチャーを cold start してください。
```
2. フロントマター `created_at` を見て鮮度を判定する。`created_at` は決定的 UTC で書かれている
([`/dispatcher-handover`](../dispatcher-handover/SKILL.md)) ので、**比較用の `now` も決定的 UTC で取得する**
(`date -u +%Y-%m-%dT%H:%M:%SZ`、PowerShell 環境は
`(Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")`)。local(JST)時刻で `now` を
取ると `now - created_at` が約 9 時間ぶれ、24h / 7d 境界付近で鮮度を誤判定するため:
- 24 時間以内 → そのまま採用
- 24 時間超〜7 日以内 → secretary に警告(「handover が古い、続行する旨」)
- 7 日超 → 採用せず `/org-start` への切り替えを推奨し停止
3. ファイル本文を Read で取り込む。書かれている内容は次 session の自分にとっての
「事実」として扱う(後の Step 3 で state.db と照合する)。
## Step 2: state.db の dispatcher identity を atomic 更新する
Step 0 で観測した `pane_id` / `peer_id` を `StateWriter.transaction()` 経由で
**1 トランザクションで** 書く(post-commit hook が `.state/org-state.md` を再生成
してくれる)。これが acceptance の「state.db identity が atomic 更新」要件の本体。
```bash
python3 -c "
import sys, os
sys.path.insert(0, os.path.abspath('..'))
from pathlib import Path
from tools.state_db import connect
from tools.state_db.writer import StateWriter
conn = connect('../.state/state.db')
with StateWriter(conn, claude_org_root=Path('..')).transaction() as w:
w.update_session(
dispatcher_pane_id='<observed_pane_id>',
dispatcher_peer_id='<observed_peer_id>',
)
"
```
**Step 0 で `peer_id` が未確定だった場合(capability gate の縮退中、または承認済み路で名前引きから
自 peer を同定できなかった場合)は、`dispatcher_peer_id` を渡さない形で呼ぶ**。上の例をそのまま
使うと観測していない値を書き込むことになる:
```bash
python3 -c "
import sys, os
sys.path.insert(0, os.path.abspath('..'))
from pathlib import Path
from tools.state_db import connect
from tools.state_db.writer import StateWriter
conn = connect('../.state/state.db')
with StateWriter(conn, claude_org_root=Path('..')).transaction() as w:
w.update_session(
dispatcher_pane_id='<observed_pane_id>',
)
"
```
**Step 0 が `caller-id 未確定` に落ちていた場合は、同じ理由で `dispatcher_pane_id` も渡さない**
(両方とも未確定で `update_session()` に渡す引数が残らないなら、この Step 2 自体を skip する)。
`focused` から採った id で埋めてはならない — 人間のフォーカス位置に依存した別ペインの id を、
以後の routing の宛先として恒久的に焼き付けることになる。
`update_session()` は未指定を「保持」と解釈する契約なので、既存値がそのまま残る(生きた同一ペインを
引き継ぐ resume ではこれが正しい。詳細は Step 0 手順 3 / 手順 4 と共有 reference §1-2-d)。
- `dispatcher_pane_id` / `dispatcher_peer_id` は **文字列**で書く(schema は TEXT、
既存 `/org-start` も文字列を入れているので型を揃える)
- `transaction()` 内なので片方だけ書いて失敗しても DB は半端な状態にならない
- 観測値が handover frontmatter と異なっても、現観測(list_panes / list_peers)を
真値として優先する。差分があった旨を secretary 宛 message に含める
## Step 3: state.db で現状を再取得し handover と照合する
```bash
python3 -c "
import sys, os
sys.path.insert(0, os.path.abspath('..'))
from tools.state_db import connect
from tools.state_db.queries import get_org_state_summary
import json
conn = connect('../.state/state.db')
print(json.dumps(get_org_state_summary(conn), ensure_ascii=False, indent=2, default=str))
"
```
確認項目:
- `active_runs[]` が handover の「監視対象のワーカー」セクションと整合するか
- `active_worker_dirs[]` のワーカーディレクトリ存在
handover には書かれていないが state.db に `active` で残っている worker、または
handover には書かれているが state.db / list_panes で消えている worker があれば、
secretary に **報告して** 判断を仰ぐ(勝手に再 spawn / status 変更しない)。
## Step 4: ペイン生存確認
`mcp__org-broker__list_peers` を再度呼び、handover に記載のワーカー名が現存するか
確認する。消えていれば secretary に `WORKER_PANE_EXITED: worker-{task_id} (resume
時点で不存在)` を通知(reconcile は窓口の責務)。
**`list_peers` の直前に
[`.claude/skills/org-delegate/references/capability-first-drive-operational-gate.md`](../org-delegate/references/capability-first-drive-operational-gate.md)
を Read し、`monitoring-read-only` の分岐を適用する**(同 reference §6 の表 #6)。capability 形かつ
未承認なら列挙を不存在の根拠にせず破棄し、`mcp__org-broker__list_panes` と events テーブルの報告痕跡で判定する。
**確定できなければ `WORKER_PANE_EXITED` を送らない**(縮退中の「列挙に居ない」は死亡の証拠ではなく、
別 org の同名ピアと取り違えうる。同 reference §3-B-1)。停止しない・待ち時間 0 分。
**capability 形かつ承認済み(同 reference §2 の `first_drive` が `recorded`)のときは、§1-2 の三値判定を
handover 記載の各ワーカー名に適用する。「在」は生存として扱う。「不在」だけでは `WORKER_PANE_EXITED` を
送らず、送信可否は §1-2-c の pane 面裏取りが取れたかに従う。「unknown」でも送らず、indeterminate として
secretary に報告する。**(判定条件・三値の定義・評価順の正本は §1-2。ここに重ねて書かない)
**unknown / indeterminate のワーカーは監視対象・active のまま残し**、Step 5 で再開する `/loop 3m` の
通常サイクルが再評価する。
## Step 5: 監視ループの再開
以下を**この順序で**評価する:
1. **inflight 再生成(先に実行)**: Step 4 の `list_peers` / `list_panes` に
`name == "curator"` のペインが生きているのに `.state/dispatcher/curate-inflight.json`
が無い場合(前 session が spawn 直後の inflight 書き込み前に途切れた等)、untracked
curator を放置しないよう `started_at = <決定的 UTC>` / `pane_id = <下記の pane id(backend が返した型のまま)>` /
`reasons: []` / `extended: false` / `curate_result: null` / `last_inspect_hash: null` /
`last_inspect_ts: null` で
inflight を**再生成する**(フィールドの正本は
[`.dispatcher/references/pane-close.md` 5-3](../../../.dispatcher/references/pane-close.md) の
JSON。`trigger_task_id` は inflight を失った前 session の CLOSE_PANE 対象を指すフィールドで
resume 側に値の出所が無いため書かない — Step 5.3 の完了受領 / timeout 判定はこの
フィールドを読まない。`curate_result` は受領した `CURATE_*` を次サイクルへ持ち越す
フィールドで、resume 時点では何も受領していないので**初期値 `null` を明示して書く** —
Step 5.3 (a) が close できないまま inflight を保持するときだけ `"done"` / `"skipped"` /
`"error"` に更新される)。
`started_at` は **`date -u +%Y-%m-%dT%H:%M:%SZ`**(PowerShell 環境は
`(Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")`)の出力をそのまま埋め、
**手書きの local(JST)時刻を `Z` で書かない**(再生成 started_at が未来時刻になると Step 5.3 の
`now - started_at` が負値となり curator が永久に孤立する。決定的取得の正本は同 5-3)。
`pane_id` は**生存確認に使った `mcp__org-broker__list_panes` の列挙で `name == "curator"` かつ
`role == "curator"` を指しているレコードの `id`** を、**`mcp__org-broker__list_panes` が返した JSON の型を
変えずに**書く(列挙が手元にあるのでその場で採れる)。tmux backend は `"%3"` のような**文字列**、
WezTerm backend は**整数**を返すので、**引用符の有無を型に合わせる** — 文字列を引用符無しで
書くと JSON が構文非妥当になり、Step 5.3 も次の resume もこのファイルを読めず curator の
追跡が失われる(正本は同 5-3 の注記)。**`pane_id` を欠いた形で再生成しない** — Step 5.3 (a)/(b) の
curator close はこの `pane_id` を identity 照合の起点にするので、欠けると schema 追加前の
旧形式ファイルと同じ扱いになり、close が列挙引き直しの fallback 経路に落ちる
([`.dispatcher/references/worker-monitoring.md` Step 5.3](../../../.dispatcher/references/worker-monitoring.md))。
`list_peers` にのみ `curator` 名があり `mcp__org-broker__list_panes` の列挙に該当レコードが無い場合は
**再生成せず** secretary に informational として報告する(数値 pane id を採れないうえ、
messaging 面の名前一致は同タブの pane 生存の根拠にならない。Step 4 と同じ規律で、
契約 T-§4.2 が pane 制御可能性の推論を禁じている)。
以降の判定はこの再生成後の状態で行う(= このケースは必ず 2 の再開条件を満たす)
2. **`/loop 3m` 再開条件**: handover の `active_worker_count > 0`、state.db の
active worker dirs が非空、**または `curate-inflight.json` が存在する**
(1 の再生成分を含む。オンデマンド curate の完了監視が引き継ぎ対象。
`.dispatcher/references/worker-monitoring.md` Step 5.3)のいずれかを満たせば
下記の monitoring 専用ディレクティブを渡して `/loop 3m` で worker monitoring を
再開する(**prompt を省略しない**)。この再開は「/loop で監視を再開します」と
**宣言するだけでは武装されない** — 宣言はターン終了と同時に失効するため、必ず
本ターン内で `/loop` を実 invoke すること(窓口側も `inspect_pane` で実行痕跡を
裏取りし、無ければ `send_keys` で再点火する運用である):
<!--
INVARIANT(loop-prompt): `/loop` の prompt 引数に skill 自身(`/dispatcher-resume`)や
他のスラッシュコマンドを渡さない。skill 実行ターン内で prompt を省略して `/loop` を
起動すると、反復対象としてアクティブな slash command が捕捉され、本 skill が 3 分ごとに
自己再帰する(2026-06-19 incident。詳細は
knowledge/raw/2026-06-19-dispatcher-resume-loop-recursion.md)。必ず monitoring 専用の
自然文ディレクティブを明示的に渡すこと。tests/test_dispatcher_resume_loop_invariant.py が
この不変条件を pin する。
-->
```
/loop 3m references/worker-monitoring.md(ディスパッチャー cwd .dispatcher/ 基準)の「監視ループ 1 サイクル」を 1 回だけ実行する(poll_events → check_messages → list_panes → inspect_pane → stall / relay-gap / pane_output 評価)。anomaly / stall / relay-gap / pane_exited を検出したサイクルのみ secretary へ通知し、検出が無ければ何も出力しない(毎サイクルの状況サマリや自然言語の状況描写を書かない)。cadence は 3 分以上を保ち短縮しない。スラッシュコマンドや本 skill を反復対象にしない。
```
- 監視ループの 1 サイクル目で `mcp__org-broker__poll_events` は
`.state/dispatcher-event-cursor.txt` の前 cursor(前 session 終了時点)から resume
する。これで「pane が閉じている間に来た pane_exited は次回 poll で必ず拾える」
semantics が維持される(cursor 仕様)。**ただし resume するのは cursor に記録された
`session_key` が現 backend session と一致したときだけ**で、一致しなければ 1 サイクル目が
cursor を破棄して「今以降」から張り直し、窓口へ `EVENT_CURSOR_RESET` を 1 行報告する
(`/clear` を挟む間に renga / broker daemon が再起動していた場合がこれ。判定と手当ての
正本は [`.dispatcher/references/worker-monitoring.md`](../../../.dispatcher/references/worker-monitoring.md)
「監視ループ 1 サイクル」Step 1)。**本 skill は cursor ファイルを読まず・書かず・消さない**
— 束縛の照合は監視ループ側の責務で、resume 側で先回りして初期化してはならない
- `mcp__org-broker__check_messages` の 1 サイクル目で前 session 中にキューに溜まった
worker → dispatcher peer message を drain する(broker = `mcp__org-broker__check_messages`。
broker は push 一次に再設計済だが、`/clear` 中に channel sidecar 経由で注入できなかった
滞留分はキューに残るため、resume 1 サイクル目の `check_messages` フォールバック drain で
取りこぼさず回収する=ツール名のみ変わり drain 論理は同型。§9.6 / §9.3 lease-reap)
- `.state/dispatcher/worker-idle-state.json` は前 session の `idle_streak_cycles` を
保持しているので stall 検出の連続性も維持される
監視対象が 0 件(active worker dir も 0、active_runs も 0、`curate-inflight.json` も
無し、かつ 1 の評価後なので `list_panes` に curator ペインも不在)の場合のみ `/loop` を
始動せず、idle 状態を secretary に通知して待機する:
```
DISPATCHER_RESUMED_IDLE: 監視対象なしで resume 完了。DELEGATE 待機。
```
## Step 6: secretary にブリーフィングを返す
handover + state.db の現状を統合した上で、secretary に簡潔に報告:
```
DISPATCHER_RESUMED: ディスパッチャー復帰完了。
- pane=<observed_pane_id> / peer=<observed_peer_id> (state.db 更新済)
- 監視対象ワーカー: <task_id list>
- pending decisions: <count>
- handover との差分: <あれば 1 行、無ければ「なし」>
- 監視ループ: /loop 3m 再開 (or idle)
```
- Step 0 が `caller-id 未確定` に落ちていた場合は `pane=<未確定: caller pane id を同定できない>` と
書き、`(state.db 更新済)` は付けない(前値保持のままであることを secretary に伝える)
## Step 7: handover ファイルを consumed 状態に切り替える
resume が成功したら、`.state/dispatcher-handover.md` を `.state/dispatcher-handover.consumed.md` に **rename する**。これにより:
- `.dispatcher/CLAUDE.md` 起動時の自動分岐(handover ファイルが直近 7 日以内に存在すれば resume)が「次の `/org-start` cold-start 時にも誤って resume に分岐する」事故を防ぐ
- 直近 1 件のみ参照用に `.consumed.md` 形式で残る(次の `/dispatcher-handover` が新たに `.md` を書いた時点で `.prev.md` バックアップに置き換わる、または上書きされる)
```bash
mv ../.state/dispatcher-handover.md ../.state/dispatcher-handover.consumed.md
```
- 既に `.consumed.md` があれば上書きで構わない(直近 1 件のみ保持)
- `.state/dispatcher-handover.prev.md` は前回の `/dispatcher-handover` で書かれたバックアップ。読み込み済みでも消さない
## イベント記録
ディスパッチャーの cwd は `.dispatcher/` なので 1 階層上を呼ぶ:
```bash
bash ../tools/journal_append.sh dispatcher_resumed \
pane_id=<observed_pane_id> peer_id=<observed_peer_id> \
active_workers=<count> note=resumed_from_handover
```
## やってはいけないこと
- 新規にディスパッチャー / キュレーターを spawn する(既に生きている)
- ワーカーに勝手に SUSPEND / SHUTDOWN を送る
- `.state/dispatcher-event-cursor.txt` / `worker-idle-state.json` /
`curate-inflight.json` / `pending_decisions.json` を初期化 / 削除する(前 session からの監視連続性が壊れる)
- handover の内容と state.db の現状が食い違うときに、勝手にどちらかへ寄せる
(必ず secretary に報告して判断を仰ぐ)
- atomic 更新を分割して書く(必ず `StateWriter.transaction()` 1 ブロックで完結させる)
- Step 5 の `/loop 3m` を prompt 省略で起動する / `/dispatcher-resume`(本 skill 自身)や
他のスラッシュコマンドを `/loop` の反復対象に渡す(skill が 3 分ごとに自己再帰する。
必ず monitoring 専用ディレクティブを明示的に渡す。Step 5 の INVARIANT(loop-prompt) 参照)
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.