dispatcher-resume: ディスパッチャーの復帰
/dispatcher-handover で書き出した .state/dispatcher-handover.md を読み込み、
ディスパッチャーとして最低限の自覚(組織員としての立ち位置・進行中の派遣・監視
対象ワーカー)を復元し、/loop 3m の worker monitoring を再開する。
前提:
- ワーカー / 窓口のペインは前 session から生きたまま残っている。
新たに spawn しない(/org-start ではない)。キュレーターは常駐しない(起動中の
curator が居れば Step 5 で追跡を引き継ぐ)。
- 自ペイン(name=
dispatcher)も生きている。secretary が send_keys で
/clear → /dispatcher-resume を打鍵した直後の状態。pane_id / peer_id は
変わっていないはずだが、必ず観測し、観測で確定できた値だけを state.db に
atomic 更新する(確定できなかった値は書かず既存値を保持する。Step 0 / Step 2)。
- 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 から残っている。新規作成・初期化・削除しない(既存値からそのまま継続。
消すと前 session からの監視連続性が壊れる)。
- handover ファイルが存在しないか古すぎる場合は、
/org-start を案内して停止する。
/clear は /loop の予約を消さない。前 session の監視ループの予約は /clear の後も
残っており、同じ指示文で /loop をもう一度打つと予約が 2 本になる。監視ループを張る前に
必ず CronList で確かめる(Step 5。経緯は
.claude/skills/dispatcher-resume/references/rationale.md §4)。
自動引き継ぎ(auto op=<OP> 付きで呼ばれたとき)
context-handover watcher(tools/context_handover_watch.py)が
/dispatcher-handover auto op=<OP> → /clear → /dispatcher-resume auto op=<OP> を打鍵する。
手順は通常の resume と同じで、違いは次の 3 つだけ:
- 完了の event(末尾「イベント記録」)に
op=<OP> を付ける。watcher はこの行で復帰の完了を判定する。
Step 0 の前の already_consumed に落ちた場合(watcher の再送など)も、(i) と unknown の
分岐の終わりに同じ形の行を note=already_consumed で 1 行書く。初期化はやり直さない。
- Step 6 の後に、
/clear の前後に届いて会話から消えた入力を処理する(Step 6.5)。
- 窓口への
DISPATCHER_RESUMED / DISPATCHER_RESUMED_IDLE は通常どおり送る。
輸送層(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 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。)
Step 0 の前: 残存 loop 予約 / stale 再発火ガード(重複起動の早期遮断)
/dispatcher-resume は handover を 1 回だけ消費する one-shot である(Step 7 で
.state/dispatcher-handover.md を .consumed.md に rename する)。/loop 3m の予約が
/dispatcher-resume 自身を反復対象に握っていると監視サイクルのたびに本 skill が再発火するので、
この stale 再発火 / 重複起動 を Step 0 に入る前に遮断する(経緯と各分岐の理由は
.claude/skills/dispatcher-resume/references/rationale.md)。
- live / consumed の handover 状態を 存在ベースで判定する(
now との時刻比較に
依存しない):
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 候補
"
already_consumed(live .md なし・.consumed.md あり): live handover が無い
resume 起動だが .consumed.md が残っている。これは「(i) 直前に成功した resume の残存
loop 予約による再発火 / 重複起動(組織は健全・監視ループ稼働中)」か「(ii) 組織が止まって
いて本来 cold-start が要る状況での誤起動」のいずれかなので、存在判定だけで no-op に倒さず、
監視対象が実際に live かを観測して分岐する:
resume(live .md あり): 通常どおり Step 0 へ進む。
no_handover(live .md も .consumed.md も無し): 通常どおり Step 0 → Step 1 へ
進み、Step 1 の「handover ファイルなし」分岐で DISPATCHER_RESUME_FAILED → cold-start
案内に落ちる。
Step 0: 自分の identity を確認する
-
mcp__org-broker__set_summary で「Dispatcher: 監視(resumed)」をセット
-
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
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 を渡さない
-
照合できたレコードの 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 へフォールバックしない)
-
mcp__org-broker__list_peers で name == "dispatcher" の peer_id を取得。
呼び出しの直前に
.claude/skills/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 階層上に解決する。
.state/dispatcher-handover.md が存在するか確認:
ls -la ../.state/dispatcher-handover.md 2>&1
- フロントマター
created_at を見て鮮度を判定する。created_at は決定的 UTC で書かれている
(/dispatcher-handover) ので、比較用の 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 への切り替えを推奨し停止
- ファイル本文を 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 を再生成
してくれる)。
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 を渡さない形で呼ぶ。上の例をそのまま
使うと観測していない値を書き込むことになる:
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 と照合する
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: ペイン生存確認
handover の ## 監視外の run に「監視外: <task_id>(ペインなし)」とある task_id も、下の判定
(list_peers / list_panes)で引き直す。ペインが在れば監視対象に戻す(書き手の判定が
誤りでも監視から落とさない)。確定して不在なら記録のずれとして Step 6 の briefing に task_id を
一覧で載せ、WORKER_PANE_EXITED は送らず、監視対象にも入れない。確定できなければ監視対象・active
のまま残す。
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
を 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: 監視ループの再開
以下をこの順序で評価する:
-
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 の
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 照合の起点にするので、欠けると pane_id の
無いファイルとして扱われ、close が列挙引き直しの fallback 経路に落ちる
(.dispatcher/references/worker-monitoring.md Step 5.3)。
list_peers にのみ curator 名があり mcp__org-broker__list_panes の列挙に該当レコードが無い場合は
再生成せず secretary に informational として報告する(数値 pane id を採れないうえ、
messaging 面の名前一致は同タブの pane 生存の根拠にならない。Step 4 と同じ規律で、
契約 T-§4.2 が pane 制御可能性の推論を禁じている)。
以降の判定はこの再生成後の状態で行う(= このケースは必ず 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)のいずれかを満たせば、
まず CronList で監視ループの予約(prompt に worker-monitoring.md と
「監視ループ 1 サイクル」を含むもの)を数える:
- 1 本: 前 session の予約が
/clear を越えて残っている。それが監視を続けるので
/loop を張らない
- 2 本以上: 1 本を残して
CronDelete で消す(重複した監視ループは同じサイクルを
二重に回す)
- 0 本: 下記の monitoring 専用ディレクティブを渡して
/loop 3m で worker monitoring を
再開する(prompt を省略しない)
0 本のときの再開は「/loop で監視を再開します」と
宣言するだけでは武装されない — 宣言はターン終了と同時に失効するため、必ず
本ターン内で /loop を実 invoke すること(窓口側も inspect_pane で実行痕跡を
裏取りし、無ければ send_keys で再点火する運用である):
<!--
INVARIANT(loop-prompt): `/loop` の prompt 引数に skill 自身(`/dispatcher-resume`)や
他のスラッシュコマンドを渡さない。skill 実行ターン内で prompt を省略して `/loop` を
起動すると、反復対象としてアクティブな slash command が捕捉され、本 skill が 3 分ごとに
自己再帰する(経緯は references/rationale.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
「監視ループ 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 に通知して待機する(前 session の予約が残っていても消さない。
次のサイクルが監視対象なしを見て、worker-monitoring Step 7 の手順で自分で止まる):
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 6.5(自動引き継ぎのときだけ): /clear の前後に届いた入力を処理する
auto op=<OP> で呼ばれ、../.state/context/dispatcher-late-input-<OP>.md があるときだけ行う。
このファイルは watcher が旧会話記録から抜き出したもので、引き継ぎを始める直前から /clear の後
までに届いた入力(チャネルのメッセージ・/loop の反復・人の入力)が届いた順に並んでいる。
- Read で読み、各入力をいま届いたものとして処理する(ワーカーやキュレーターからの報告なら
通常どおり ack・転送する)。handover ファイルに処理済みと書かれている入力は飛ばしてよい
(拾い直しは欠落より重複を許す側に倒してあるので、同じ入力が両方に載ることがある)。
/loop の反復は処理しない(監視ループの次のサイクルが同じことをする)。
- 読み終えたら rename する(同じ op のファイルを 2 回処理しない):
mv ../.state/context/dispatcher-late-input-<OP>.md ../.state/context/dispatcher-late-input-<OP>.consumed.md
ファイルが無ければ何もしない(拾い直す入力が無かった)。別の op のファイルは読まない。
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 は live の .md だけを .prev.md に退避し .consumed.md には触らないので、次の resume の mv で上書きされるまで残る)
mv ../.state/dispatcher-handover.md ../.state/dispatcher-handover.consumed.md
- 既に
.consumed.md があれば上書きで構わない(直近 1 件のみ保持)
.state/dispatcher-handover.prev.md は前回の /dispatcher-handover で書かれたバックアップ。読み込み済みでも消さない
イベント記録
ディスパッチャーの cwd は .dispatcher/ なので 1 階層上を呼ぶ:
bash ../tools/journal_append.sh dispatcher_resumed \
pane_id=<observed_pane_id> peer_id=<observed_peer_id> \
active_workers=<count> note=resumed_from_handover
自動引き継ぎ(auto op=<OP>)のときは同じ行に op=<OP> と loop=<armed|kept|idle>
(Step 5 で /loop を張った / 残っていた予約を使った / 監視対象なし)を足す。watcher はこの行で
復帰の完了を判定し、loop の値で監視ループの確認をするかを決める。already_consumed に
落ちた再送では note=already_consumed で同じ形の行を書く。
やってはいけないこと
- 新規にディスパッチャー / キュレーターを spawn する(既に生きている)
- ワーカーに勝手に SUSPEND / SHUTDOWN を送る
- 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) 参照)