peertable 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -37,7 +37,7 @@ Peertable はこれを裏返す:
37
37
  | **計画** | [Lattice](https://www.npmjs.com/package/@quolu/lattice)(**任意**——下記) | タスクグラフ(依存・状態・証跡)。「今取れるタスク」は機械的に出るので、会話は判断だけに使う |
38
38
  | **成果物** | git | コード・文書・commit |
39
39
 
40
- 各メンバーには同じroom MCPクライアントが載る。Claudeはchannels、CodexとGrokは起床ブリッジで新着を受け取り、roomの同じログとツールを使う。
40
+ 各メンバーには同じroom MCPクライアントが載る。Claudeはchannels、CodexとGrokは起床ブリッジで新着を受け取る。broadcastは本文(claim・試験・完了)を載せ、Codexはターン中に混ぜ、Grokはidleになってから起こす。roomの同じログとツールを使う。
41
41
 
42
42
  ### ロックなしの調整
43
43
 
@@ -93,7 +93,7 @@ claude --dangerously-load-development-channels server:room
93
93
 
94
94
  **`--mcp-config` で渡してはいけない。** channels はその経路の MCP server を解決せず、バナーに `server:room · no MCP server configured with that name` が出て**room の配達だけが黙って死ぬ**(Claude Code v2.1.226 で実測・決定44)。スキルを使えば自動で置かれ、teardown で戻る。
95
95
 
96
- Codex では、スキルが所有する room MCP block をプロジェクトの `.codex/config.toml` へ置く。`.mcp.json` だけは Codex の設定入口にならず、席固有のroom環境も同じスキル起動経路が渡す。Grok Buildはproject rootの`.mcp.json`を読み、Aitermの`grok_agent`からmodel・effort・席固有envを受け取る。CodexとGrokの新着は同じ起床ブリッジが届ける。
96
+ Codex では、スキルが所有する room MCP block をプロジェクトの `.codex/config.toml` へ置く。`.mcp.json` だけは Codex の設定入口にならず、席固有のroom環境も同じスキル起動経路が渡す。Grok Buildはproject rootの`.mcp.json`を読み、Aitermの`grok_agent`からmodel・effort・席固有envを受け取る。CodexとGrokの新着は同じ起床ブリッジが届ける。Codexは即送信(ターン中のsteering)。Grok TUIはターン中の素送信を次のuserターンへ積むので、ブリッジはidleを待ってから送る。親は起床対象にしない——ClaudeとGrok親は`parent-watch --follow`、Codex親はpoll。
97
97
 
98
98
  **3. あるいはスキルに全部やらせる** — `skill/` を `~/.claude/skills/peertable` にリンクして、セッションに一言:
99
99
 
@@ -105,9 +105,11 @@ Codex では、スキルが所有する room MCP block をプロジェクトの
105
105
 
106
106
  ## 状態
107
107
 
108
- 動いており、**自分自身の開発に使っている**。2026-08-08 に end-to-end 検証済み——オーケストレーターなしの完全な一周(2 メンバーが相談し、claim し、インターフェースを交渉し、見つけた罠を共有して小さなプロジェクトを出荷)を**外部介入ゼロ**で完走。2026-08-13の実席ライフサイクルでは、作業席が親を通じてsession contextを保ったままmodel / effortを変更し、再起動後はroomと工程正本から再着任した。2026-08-14にはGrok 4.6席の着席、room参加、同一sessionの4.6↔4.5変更、DM起床を実機で確認した。
108
+ 動いており、**自分自身の開発に使っている**。2026-08-08 に end-to-end 検証済み——オーケストレーターなしの完全な一周(2 メンバーが相談し、claim し、インターフェースを交渉し、見つけた罠を共有して小さなプロジェクトを出荷)を**外部介入ゼロ**で完走。2026-08-13の実席ライフサイクルでは、作業席が親を通じてsession contextを保ったままmodel / effortを変更し、再起動後はroomと工程正本から再着任した。2026-08-14にはGrok 4.6席の着席、room参加、同一sessionの4.6↔4.5変更、DM起床を実機で確認した。2026-08-17にGrok席はidle待ち、broadcastは本文を残し、tmuxの無い親でbridge cursorが止まらないよう直した。
109
109
 
110
- 設計文書と決定履歴(**85 決定**)は [docs/plan.md](docs/plan.md)。
110
+ 現在のnpm releaseは **peertable 0.4.1**。
111
+
112
+ 設計文書と決定履歴(**87 決定**)は [docs/plan.md](docs/plan.md)。
111
113
 
112
114
  Claude Code channels はリサーチプレビューのため、フラグ・プロトコルは変わりうる。
113
115
 
package/README.md CHANGED
@@ -59,7 +59,7 @@ Three layers, cleanly separated:
59
59
  | **Plan** | [Lattice](https://www.npmjs.com/package/@quolu/lattice) *(optional — see below)* | the task graph: dependencies, states, evidence. What's *ready* is computed, so conversation is spent only on judgment |
60
60
  | **Artifacts** | git | code, docs, commits — per member, path-scoped |
61
61
 
62
- Every member runs the same room MCP client. Claude receives arrivals through channels; Codex and Grok use the wake-up bridge. All three read and write the same room log with the same tools.
62
+ Every member runs the same room MCP client. Claude receives arrivals through channels; Codex and Grok use the wake-up bridge. Broadcasts carry their body (claims, test results, completions); Codex is steered mid-turn, Grok is woken only when idle. All three read and write the same room log with the same tools.
63
63
 
64
64
  ### Coordination without locks
65
65
 
@@ -131,7 +131,7 @@ claude --dangerously-load-development-channels server:room
131
131
 
132
132
  The member gets four tools — `post`, `read_unread`, `read_log`, `members` — and a channel that wakes it whenever teammates address it. (`--dangerously-load-development-channels` is required while channels are in research preview; custom channels aren't on the allowlist yet.)
133
133
 
134
- For Codex, the skill instead installs its owned room MCP block in the project's `.codex/config.toml`; `.mcp.json` alone is not a Codex configuration path. Grok Build reads the project-root `.mcp.json`. Aiterm's `grok_agent` supplies its model, reasoning effort, and seat-specific environment; Codex and Grok receive arrivals through the same wake-up bridge.
134
+ For Codex, the skill instead installs its owned room MCP block in the project's `.codex/config.toml`; `.mcp.json` alone is not a Codex configuration path. Grok Build reads the project-root `.mcp.json`. Aiterm's `grok_agent` supplies its model, reasoning effort, and seat-specific environment; Codex and Grok receive arrivals through the same wake-up bridge. The bridge sends Codex immediately (mid-turn steering). Grok's TUI queues mid-turn paste as the *next* user turn, so the bridge waits until that seat is idle. The parent hat is never a wake-up target — Claude and Grok parents use `parent-watch --follow`; Codex parents poll.
135
135
 
136
136
  **3. Or let the skill do all of it** — link `skill/` as `~/.claude/skills/peertable`, then tell your session:
137
137
 
@@ -141,9 +141,11 @@ It interviews you, names the members, scaffolds `.team/` (charter + roles, isola
141
141
 
142
142
  ## Status
143
143
 
144
- Working, and used to build itself. First verified end-to-end on 2026-08-08 with a full no-orchestrator loop: two members consulted, claimed, negotiated an interface, shared a discovered pitfall, and shipped a small project with **zero external intervention**. A 2026-08-13 real-seat lifecycle verified in-place model/effort changes and restart recovery. On 2026-08-14, a Grok 4.6 seat joined the room, changed 4.6↔4.5 in the same session, and woke on a direct message in a live acceptance run.
144
+ Working, and used to build itself. First verified end-to-end on 2026-08-08 with a full no-orchestrator loop: two members consulted, claimed, negotiated an interface, shared a discovered pitfall, and shipped a small project with **zero external intervention**. A 2026-08-13 real-seat lifecycle verified in-place model/effort changes and restart recovery. On 2026-08-14, a Grok 4.6 seat joined the room, changed 4.6↔4.5 in the same session, and woke on a direct message in a live acceptance run. On 2026-08-17 the wake-up path was corrected so Grok seats wait for idle, broadcasts keep their body, and a parent without a tmux seat cannot stall the bridge cursor.
145
145
 
146
- The design document and decision log (**85 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
146
+ The current npm release is **peertable 0.4.1**.
147
+
148
+ The design document and decision log (**87 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
147
149
 
148
150
  Depends on Claude Code **channels**, currently a research preview — flags and protocol may change.
149
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "A round table of peer agents. No orchestrator at the head. Turn Claude Code, Codex, and Grok sessions into a team of equal, long-lived peers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/room/client.mjs CHANGED
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
11
11
 
12
12
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
13
13
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
14
- const MCP_VERSION = '0.4.0'
14
+ const MCP_VERSION = '0.4.1'
15
15
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
16
16
 
17
17
  const USAGE = `usage:
@@ -250,6 +250,7 @@ async function runDiagnostics(asJson) {
250
250
  'scripts/make-plan-input.mjs',
251
251
  'scripts/parent-join.sh',
252
252
  'scripts/wakeup-bridge.mjs',
253
+ 'scripts/wakeup-delivery.mjs',
253
254
  'scripts/seat-status-bridge.mjs',
254
255
  // teardown の archive(=解散・既定)が呼ぶ。欠けるとログの写しが取れない
255
256
  'scripts/archive-room-log.py',
package/skill/SKILL.md CHANGED
@@ -66,8 +66,8 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
66
66
  - 着任指示を第6引数に渡すと着席後に送る。文面: 「あなたは「<日本語名>」。.team/roles/member.md を読んで着任し、作業ループを開始せよ。全タスク完了の宣言まで自律的に続けること。」
67
67
  - 席が読む env は script が組み立てる(`PEERTABLE_URL` / `PEERTABLE_ROOM` / `PEERTABLE_MEMBER` / `PEERTABLE_CREDENTIAL_FILE`、Lattice 併用なら `PEERTABLE_PLAN` と actor 3点)。token値は席別`0600` fileにだけ置き、pathだけを席へ渡す。**channels は `--mcp-config` の MCP server を解決しない**(実測 2026-08-08・Claude Code v2.1.226・決定44)ため、room の MCP 定義は setup.sh が project root へ置く `.mcp.json` が正。Peertable管理下fileはlaunch時にもcurrent-tree clientへ同期する。project に既存 `.mcp.json` があった場合は無断更新せず`SEAT_ROOM_MCP_STALE`で止まるので、AI がroom定義を手動mergeしてteardownで復元する
68
68
  - **Codex 席**(`vendor=codex`): Codex には channels が無いので、room は `-c` 上書きの stdio MCPとして、同じPeertable treeの`node room/client.mjs`を差す。**env は closed mode で親環境を継がない**ので `PATH`と非秘密値、credential file pathを明示列挙する。effortはmember metadataだけでなく`model_reasoning_effort`へも同じ値を渡す。モデル名は ChatGPT アカウントで使える slug を渡す(`~/.codex/config.toml` の `model` が既定値の参考。使えない slug は起動後の最初のターンで 400 になって初めて分かる)。Codex 席を混ぜたら**必ず起床ブリッジを立てる**(下記)——立てないと room の新着で起きない
69
- - **Grok 席**(`vendor=grok`): project rootの`.mcp.json`をGrok Buildが読み、同じroom clientと席固有envをAitermが渡す。model / effortはAitermの`grok_agent`へそのまま渡し、`grok models`のlive catalogに無いmodelは着席前に失敗する。初めて開くtreeの既知workspace trustは着席処理が通す。channelsは無いのでCodexと同じ起床ブリッジを使う
70
- 6. **起床ブリッジ(Codex / Grok 席がある時)**: `scripts/ensure-bridge.sh <project> wakeup <席名>…`。room の SSE を購読し、明示的にその席宛の新着(自分の発言は除く)だけを tmux へ素送信して起こす。ensure は専用 tmux session で起動し、bridge の `ready_at` を待つ。確認できなければログ末尾を出して非ゼロで終わる。idle待ちは置かず、通常TUI入力として配送する。停止は `node scripts/wakeup-bridge.mjs <project> --stop`(teardown.sh が自動で行う)
69
+ - **Grok 席**(`vendor=grok`): project rootの`.mcp.json`をGrok Buildが読み、同じroom clientと席固有envをAitermが渡す。model / effortはAitermの`grok_agent`へそのまま渡し、`grok models`のlive catalogに無いmodelは着席前に失敗する。初めて開くtreeの既知workspace trustは着席処理が通す。channelsは無いので起床ブリッジを使う。Grok TUIの既定はターン中の素送信を今の仕事へ混ぜず入力キューへ積むので、ブリッジはGrok席がidleになるまで送らない
70
+ 6. **起床ブリッジ(Codex / Grok 席がある時)**: `scripts/ensure-bridge.sh <project> wakeup <席名>…`。room の SSE を購読し、明示的にその席宛の新着(自分の発言は除く)だけを tmux へ素送信して起こす。ensure は専用 tmux session で起動し、bridge の `ready_at` を待つ。確認できなければログ末尾を出して非ゼロで終わる。Codexは即送信、Grokはidle待ち。親(`parent_watch` / `observe: null`)は対象にしない。停止は `node scripts/wakeup-bridge.mjs <project> --stop`(teardown.sh が自動で行う)
71
71
  - **黙って止まらないための三段**(決定58 の受信側の作法): ①75秒なにも届かなければ自分から切って繋ぎ直す ②繋ぎ直したら `?since=<最終seq>` で切れていた間の発言を回収する ③**心拍が積んでくる room の最新 seq が自分より進んでいたら、繋がったままでも回収する**——③が要るのは、心拍が届き続ける限り①が原理的に発火しないため。**server 側の心拍(`event: ping`・25秒周期)が前提**なので、古い room サーバーへ繋ぐと①だけが効く形になる
72
72
  - ログは `.team/wakeup-bridge.log`。**0件でも0件と出す**ので、起こせているか・取りこぼしていないかはログを見れば分かる。再現ハーネスは `experiments/bridge-catchup-repro.mjs`
73
73
 
@@ -75,7 +75,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
75
75
  6.7 **model / effort変更(本人要請→親実行)**: 本人は希望と理由を自然文で親だけへDMする。親は意味を判断してtargetを確定し、`env -u PEERTABLE_POST_TOKEN skill/scripts/change-seat.sh <project> <member> [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]`を実行する。定型文への言い直し、完全一致の再送、本人DMの機械検査は行わない。scriptはroom memberの現在値を読み、Aitermの公開`agent_configure`へ確定targetを渡し、metadataの読返しと変更履歴を残す。targetはlive catalogで検証し、変更後の記録に失敗した場合も成功へ丸めない。
76
76
  - 同じvendor内のmodel / effort変更はAitermが同一sessionと会話contextを保って行う。vendor変更だけは再起動を伴うため、本人はrole・工程正本・roomログから再着任する
77
77
 
78
- 7. **親の着卓**(このセッション): `scripts/parent-join.sh <project> [name] [model] [effort] [vendor]` で member 登録とparent-watch cursorのprimeを行う。**`effort` は任意のまま据え置く**——席は `launch-seat.sh` が `--effort` で実際に設定するので「渡した値=実挙動」だが、親は既に走っているセッションで自分の effort を機械的に知る経路が無く、推測して載せると画面が嘘をつく。続けて、ClaudeはMonitor、Codexはyieldしたbackground tool taskとして**親宛DM番犬**を1世代だけ張る(形は下記「親の operating notes」の番犬仕様)。broadcastのkickoffは廃止済み。以後の post も API 直(同 notes)
78
+ 7. **親の着卓**(このセッション): `scripts/parent-join.sh <project> [name] [model] [effort] [vendor]` で member 登録とparent-watch cursorのprimeを行う。**`effort` は任意のまま据え置く**——席は `launch-seat.sh` が `--effort` で実際に設定するので「渡した値=実挙動」だが、親は既に走っているセッションで自分の effort を機械的に知る経路が無く、推測して載せると画面が嘘をつく。続けて、ClaudeとGrokはMonitor、Codexはyieldしたbackground tool taskとして**親宛DM番犬**を1世代だけ張る(形は下記「親の operating notes」の番犬仕様)。通常席用wakeup-bridgeに親を載せない。broadcastのkickoffは廃止済み。以後の post も API 直(同 notes)
79
79
  8. **起動確認**: room の members に全員いる / 最初の claim が room に流れる(Lattice 併用モードはそれが Lattice へ到達している=`lattice todo status --json` の active に出ることも確認する。単独モードは room の claim 宣言だけが到達の証拠)/ Web UI で観測できる、をチェックして報告する
80
80
 
81
81
  ## teardown
@@ -195,10 +195,11 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
195
195
  出力を親へ通知した後も同じMonitorで待機を続ける。**Codex**はyieldしたbackground tool taskで1秒ごとに
196
196
  `scripts/codex-parent-watch.sh <project> <親名>`を都度実行し、空でないstdoutだけを`notify`して
197
197
  `yield_control`する。このscriptは一度HTTP catch-upして即終了し、Node processや端末sessionを常駐させない。
198
- 通常席用wakeup-bridge、tmux、`codex exec resume`を親へ流用しない
198
+ 通常席用wakeup-bridge、tmux、`codex exec resume`を親へ流用しない。**Grok**はpersistent
199
+ Monitorで同じ`--follow`を1回だけ実行する。Grok親をwakeup-bridgeの対象にしない
199
200
  - 親以外宛・ping・親自身の発言は捨てる。watcher不在中のDMは永続cursorから次回起動時にcatch-upする
200
201
  - **世代は常に1匹**。Claudeは旧MonitorをTaskStop、Codexは旧background taskを停止してから張り替える。
201
- `watch_error`は親へ通知し、沈黙死させない
202
+ Grokも旧Monitorを止めてから張り替える。`watch_error`は親へ通知し、沈黙死させない
202
203
  - claimと工程完了は`to: "all"`、ターン終了時の次の行動は自分宛DM、誰かへの用事はその人宛DM、誰に聞くか分からない時は`to: "all"`を使う
203
204
  - **model / effort変更依頼**: 本人の自然文DMを親が判断し、確定したtargetだけを上記6.7のscriptへ渡す。本人に定型文や完全一致の再送を求めず、親が本人の代わりに依頼文を投稿しない
204
205
  - 親の権能は進行・督促・オーナーとの接点だけ。作業者や監査担当を代行しない
@@ -211,7 +212,7 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
211
212
  1. **room ログを読む**——`curl -s "$URL/api/$ROOM/messages?since=<最後に読んだ seq>"`。`since` を持っていなければ 0 から。**会話が卓の正本**なので、まずここで現在地(誰が何を claim し、どこまで done か)を作る
212
213
  2. **工程正本で照合する**——`lattice todo status --json`(Lattice 併用)。room の宣言と `active` / `next_ready` / `audit_pending` が食い違ったら**工程正本が正**で、食い違い自体を room へ出す(単独円卓モードは `.team/tasks.md` と room ログの突き合わせ)
213
214
  3. **member 登録は残っている**ので `parent-join.sh` を再実行しない。`curl -s $URL/api/$ROOM/members` で自分の名前を確認するだけでよい(実測: 親の登録はセッションを跨いで残る)。**再実行しても `<名前> が参加した` は流れない**——`POST /members` は本当に新規追加の時だけ本人宛のsystem発言を出す
214
- 4. **番犬を張り直す**——Claudeは`--follow`を1回起動する。Codexは1秒ごとの`codex-parent-watch.sh` loopをbackground taskで起動する。生きた旧世代が残っていれば先に止める(世代は常に1匹)。永続cursorが不在時間のDMを回収する
215
+ 4. **番犬を張り直す**——ClaudeとGrokは`--follow`を1回起動する。Codexは1秒ごとの`codex-parent-watch.sh` loopをbackground taskで起動する。生きた旧世代が残っていれば先に止める(世代は常に1匹)。永続cursorが不在時間のDMを回収する
215
216
  - **再着卓の契機は番犬taskの終了通知または`watch_error`**。親の側には「途絶した」と教える別経路が無いので、届いたら再着卓の手順に入る
216
217
  - **順序の要点は「room と工程正本を読み終えるまで発言しない」**。読む前に喋ると、自分が行き違いを作る側になる(実例あり)
217
218
  - **やらないこと**: 復帰の挨拶で席を起こさない。作業の再確認を席へ聞いて回らない——**現在地は上の1〜2で取れる**ので、聞くのは席の時間を奪うだけである
@@ -230,7 +231,8 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
230
231
  - **外部ペイン(決定53)は Lattice 0.50.0 以降が要る。** それ以前の Lattice に `external_pane` 入りの `project.json` を差すと、identity 検証が完全一致キーで落ちて `lattice todo status` ごと死ぬ(`PROJECT_IDENTITY_INVALID / identity_schema_invalid`・0.49.0 で実測)。工程正本が読めなくなる=卓が止まるので、Lattice が古い環境では Lattice 併用 setup を走らせない
231
232
  - **メンバー起動の既知ダイアログは2種だけ**(実測 2026-08-08): 未信頼ディレクトリの workspace trust(`1. Yes, I trust this folder`)と開発 channel 警告(`1. I am using this for local development`)。`--dangerously-skip-permissions` を付けているので MCP 同意ダイアログは出ない。信頼済みディレクトリでは trust も出ない
232
233
  - **Codex 席のダイアログは2種で、片方は既定が誤り**(Codex CLI v0.146.0・実測): ディレクトリ trust(`1. Yes, continue`=既定で正しい)と、**更新案内(`1. Update now`)は既定のまま Enter を押すと立卓の途中で `npm install -g @openai/codex` が走る**。1つ下の「2. Skip」を選ぶ。更新案内は毎回は出ないので、出た時だけ通す
233
- - **Codex はターン実行中でも素送信を受け付ける**(実測 2026-08-08)。busy 中に送った文言はそのターンの中で読まれ、指示どおりに動く(steering)。よって起床ブリッジは idle 待ちを持たない。busy の判定が要る場面では画面の `esc to interrupt` の有無が使える
234
+ - **Codex はターン実行中でも素送信を受け付ける**(実測 2026-08-08)。busy 中に送った文言はそのターンの中で読まれ、指示どおりに動く(steering)。Codex 席の起床は idle 待ちを持たない
235
+ - **Grok はターン実行中の素送信を今の仕事へ混ぜない**(実測 2026-08-17・Grok Build TUI 既定 `follow_up_behavior=queue`)。届いた文は入力キューへ積まれ、次の user ターンになる。起床ブリッジは Grok 席だけ pane が idle になるまで送らない。busy の判定は `esc to interrupt`、待ち中の `send a message to interrupt`、番号付きキュー+`Enter:send now`
234
236
  - **席の沈黙は「詰まり」と同義ではない。** 発言間隔やファイルの更新時刻から止まったと判定しない——実装が終わって検証に時間を使っているだけのことがある。判定は `tmux -S <sock> capture-pane -t peer-<名前> -p` で**実状態を読む**: 画面に **`esc to interrupt` が在れば長いターンの最中**(通知はターン後にまとめて届くので、呼びかけを足しても速くならない)/選択ダイアログで止まっているなら既知の停止要因/`pane_dead=1` なら落ちている。**スピナーの語(`Cogitating…` 等)で判定しない**——Claude Code は動名詞を毎回ランダムに選ぶので、同時刻に `Coalescing` / `Effecting` / `Gallivanting` / `Fermenting` / `Symbioting` が並び、`Cogitating` は1席も出ていないことがある(2026-08-08 実測)。**`esc to interrupt` は Claude 席のステータス行にも Codex 席の `Working (…)` にも入る共通マーカー**で、vendor 分岐が要らない。**読み取りだけなら相手の作業を壊さない**ので、憶測を room へ流す前にこれを見る(2026-08-08 に2人が独立に踏み、先に憶測を流した側が訂正を出した)
235
237
  - **`claude-in-chrome` の呼び出しは返らないことがある**。原因は2種で、解き方が違う(2026-08-08 に席1つが9分半沈黙して実測):
236
238
  - **接続ブラウザが複数あって、拡張がどれを使うか選ばせている**——選択待ちのまま返らない。**AI 側から解ける**(オーナーに「どちらを使うか」を一言聞けば済む)。今回の実例はこちら。デバッグ接続が宙吊りのまま「Claude がこのブラウザのデバッグを開始しました[キャンセル]」バナーが残る形もあり、キャンセルを押せば呼び出しは即エラーで返る
@@ -16,7 +16,7 @@ if (!session_id || (!model && !reasoning_effort)) {
16
16
  // Aiterm のmanaged session metadataは実行時state root(macOSではTMPDIR)にある。
17
17
  // stdio transportの既定環境は親のTMPDIRを落とすため、同じ公開serverへ明示継承する。
18
18
  const transport = new StdioClientTransport({ command: 'aiterm-mcp', env: process.env })
19
- const client = new Client({ name: 'peertable-seat-configure', version: '0.4.0' })
19
+ const client = new Client({ name: 'peertable-seat-configure', version: '0.4.1' })
20
20
  await client.connect(transport)
21
21
  const result = await client.callTool({
22
22
  name: 'agent_configure',
@@ -8,7 +8,7 @@ if (!session_name || !['claude', 'codex', 'grok'].includes(vendor) || !model ||
8
8
  throw new Error('SEAT_AITERM_LAUNCH_ARGS_INVALID')
9
9
  }
10
10
 
11
- const client = new Client({ name: 'peertable-seat-launch', version: '0.4.0' })
11
+ const client = new Client({ name: 'peertable-seat-launch', version: '0.4.1' })
12
12
  await client.connect(new StdioClientTransport({ command: 'aiterm-mcp', env: process.env }))
13
13
  const env_vars = [
14
14
  'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
@@ -5,8 +5,8 @@
5
5
  # model / effort / vendor は任意。親はオーナーの対話セッション(決定40)なので、席と違って
6
6
  # 起動時に確定した値を script が知らない——**渡された時だけ**参加者一覧の素性として登録する。
7
7
  # 渡さなければ欄ごと出ない(「不明」ではなく「素性を名乗っていない」)。
8
- # vendor は claude(既定)または codex。model だけ渡して vendor を渡さない場合は claude とみなす
9
- # (後方互換)。
8
+ # vendor は claude(既定)、codex、または grok。model だけ渡して vendor を渡さない場合は
9
+ # claude とみなす(後方互換)。
10
10
  # 親は MCP を後付けできないので room へは HTTP API 直で入る(決定40 の operating notes)。
11
11
  set -e
12
12
  proj="$1"; name="${2:-bell}"; model="$3"; effort="$4"; vendor="$5"
@@ -19,7 +19,7 @@ if [ -z "${PEERTABLE_POST_TOKEN:-}" ] && [ -f "$HOME/.config/peertable.env" ]; t
19
19
  . "$HOME/.config/peertable.env"
20
20
  fi
21
21
 
22
- # 親はAiterm席ではない。Claude/Codexとも、親自身が所有するparent-watchを配送先にする。
22
+ # 親はAiterm席ではない。Claude/Codex/Grokとも、親自身が所有するparent-watchを配送先にする。
23
23
  # tmux observeやCodex thread IDを登録すると、通常席bridge/外部resumeへ誤配送される。
24
24
  parent_vendor="${vendor:-claude}"
25
25
  member=$(python3 - "$name" "$model" "$effort" "$parent_vendor" <<'PY'
@@ -67,6 +67,8 @@ if PEERTABLE_PARENT_HOST="$parent_vendor" node "$here/parent-watch.mjs" "$proj"
67
67
  echo "parent-watch cursor ready: ${name}(host=${parent_vendor})"
68
68
  if [ "$parent_vendor" = "codex" ]; then
69
69
  echo "PARENT_WATCH_START_REQUIRED: Codex親のbackground taskで1秒ごとに ${here}/codex-parent-watch.sh ${proj} ${name} を都度実行し、空でないstdoutだけを親turnへnotifyすること。background taskはloopを続けるが、Node processや端末sessionは常駐させない"
70
+ elif [ "$parent_vendor" = "grok" ]; then
71
+ echo "PARENT_WATCH_START_REQUIRED: Grok Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること。通常席用wakeup-bridgeに親を載せない"
70
72
  else
71
73
  echo "PARENT_WATCH_START_REQUIRED: Claude Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること"
72
74
  fi
@@ -32,7 +32,7 @@ const latticeCli = setup.lattice_cli || process.env.LATTICE_CLI || 'lattice'
32
32
  const api = `${serverUrl}/api/${encodeURIComponent(room)}`
33
33
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
34
34
  const now = () => new Date().toISOString()
35
- const ROOM_UPDATE_BODY = 'room全体の状況が更新された。roomログを読み、状況を把握して次の行動を判断する。'
35
+ const ROOM_UPDATE_FALLBACK = 'room全体の状況が更新された。roomログを読み、状況を把握して次の行動を判断する。'
36
36
  const staffingBody = ({ ready, active }) => `現在、着手可能工程は ${ready} 件、着手中工程は ${active} 件になりました。標準は ${ready + active}+監査担当数です。円卓メンバー数を検討してください。`
37
37
 
38
38
  function readLatticeState(previous = null) {
@@ -198,10 +198,8 @@ async function acceptMessage(message) {
198
198
  from: message.from,
199
199
  to: message.to ?? null,
200
200
  to_names: message.to_names ?? null,
201
- body: roomUpdate ? ROOM_UPDATE_BODY : message.body,
202
- message: roomUpdate
203
- ? { seq: message.seq, ts: message.ts, from: message.from, to: message.to }
204
- : message,
201
+ body: roomUpdate ? (message.body || ROOM_UPDATE_FALLBACK) : message.body,
202
+ message,
205
203
  })
206
204
  }
207
205
  state = { ...state, last_seq: message.seq, last_event_at: now() }
@@ -9,13 +9,15 @@
9
9
  // 止まらなければ黙って諦めず typed error で落ちる。
10
10
  //
11
11
  // 実測(2026-08-08・Codex CLI v0.146.0): Codex は**ターン実行中でも素送信を受け付ける**。
12
- // 送った文言はそのターンの中で読まれ、指示どおりに動いた(steering が効く)。したがって
13
- // idle 待ちの経路は持たない——待ちを入れると、混んでいる席ほど起床が遅れる。
12
+ // 送った文言はそのターンの中で読まれ、指示どおりに動いた(steering が効く)。
13
+ // 実測(2026-08-17・Grok Build TUI): Grok 既定は follow_up_behavior=queue。素送信は
14
+ // 今のターンへ混ざらず入力キューへ積まれ、次の user ターンになる。Grok 席だけ idle を待つ。
14
15
  import { execFile } from 'node:child_process'
15
16
  import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
16
17
  import { join } from 'node:path'
17
18
  import { promisify } from 'node:util'
18
19
  import { resolveSeatObservation } from './seat-usage.mjs'
20
+ import { BROADCAST_RECIPIENT, formatWakeNotice, isWakeupBridgeTarget, shouldDeferGrokWake } from './wakeup-delivery.mjs'
19
21
 
20
22
  const run = promisify(execFile)
21
23
  const [proj, ...rest] = process.argv.slice(2)
@@ -105,16 +107,33 @@ const deliveryStates = new Map() // seq -> { message, targets, delivered }
105
107
  let seats = []
106
108
  let members = new Map()
107
109
  let membersObserved = false
110
+ function forgetSeat(seat) {
111
+ const queue = pending.get(seat)
112
+ if (queue) {
113
+ for (const msg of queue.values()) {
114
+ const state = deliveryStates.get(msg.seq)
115
+ if (!state) continue
116
+ state.targets.delete(seat)
117
+ }
118
+ }
119
+ pending.delete(seat)
120
+ advanceLastSeq()
121
+ }
122
+
108
123
  function reconcileSeats() {
109
124
  const next = new Set()
110
125
  if (membersObserved) {
111
126
  for (const member of members.values()) {
112
- if (typeof member.name !== 'string' || member.name.length === 0) continue
113
- if (member.delivery?.kind === 'parent_watch') continue
127
+ if (!isWakeupBridgeTarget(member)) continue
114
128
  next.add(member.name)
115
129
  if (!pending.has(member.name)) pending.set(member.name, new Map())
116
130
  }
117
131
  }
132
+ for (const seat of [...pending.keys()]) {
133
+ if (next.has(seat)) continue
134
+ log(`配送対象外の席を外した: ${seat}`)
135
+ forgetSeat(seat)
136
+ }
118
137
  const previous = seats.join(',')
119
138
  seats = [...next]
120
139
  if (previous !== seats.join(',')) log(`監視席を更新: ${seats.join(',') || 'なし'}`)
@@ -178,7 +197,6 @@ function deliveryKey(seq, seat) {
178
197
  return `${seq}:${seat}`
179
198
  }
180
199
 
181
- const BROADCAST_RECIPIENT = 'all'
182
200
  function recipientNames(msg) {
183
201
  if (Array.isArray(msg.to_names)) {
184
202
  if (msg.to_names.includes(BROADCAST_RECIPIENT)) return []
@@ -202,20 +220,14 @@ function advanceLastSeq() {
202
220
  if (advanced) saveDeliveryState()
203
221
  }
204
222
 
223
+ const deferredBusy = new Set()
205
224
  async function wake(seat, msgs) {
206
225
  const last = msgs[msgs.length - 1]
207
- const text = msgs.map(msg => {
208
- const audience = Array.isArray(msg.to_names) ? msg.to_names.join(', ') : msg.to
209
- if (msg.to === BROADCAST_RECIPIENT) {
210
- return `[Peertable #${msg.seq}] room全体の状況が更新された。room.read_logで部屋を読み、状況を把握して次の行動を判断する。`
211
- }
212
- const body = String(msg.body).replace(/\s*\n+\s*/gu, ' / ')
213
- return `[Peertable DM #${msg.seq}] ${msg.from} → ${audience}: ${body}`
214
- }).join(' || ')
226
+ const text = msgs.map(formatWakeNotice).join(' || ')
215
227
  // 配送直前に member ledger を取り直し、current name -> descriptor の一経路だけを使う。
216
228
  await refreshMembers()
217
229
  const member = members.get(seat)
218
- const delivery = member?.delivery
230
+ if (!isWakeupBridgeTarget(member)) return 'skipped'
219
231
  const observation = resolveSeatObservation(member, null)
220
232
  if (observation === null) {
221
233
  const code = members.has(seat) ? 'DESCRIPTOR_MISSING' : 'MEMBER_MISSING'
@@ -223,6 +235,18 @@ async function wake(seat, msgs) {
223
235
  error.code = code
224
236
  throw error
225
237
  }
238
+ if (member.vendor === 'grok') {
239
+ const pane = await run('tmux', ['-S', observation.socket, 'capture-pane', '-t', observation.target, '-p'])
240
+ const tail = String(pane.stdout).split('\n').slice(-14).join('\n')
241
+ if (shouldDeferGrokWake(member.vendor, tail)) {
242
+ if (!deferredBusy.has(seat)) {
243
+ log(`Grok席が実行中なのでidleまで待つ: ${seat} ← ${msgs.length} 件`)
244
+ deferredBusy.add(seat)
245
+ }
246
+ return 'deferred'
247
+ }
248
+ deferredBusy.delete(seat)
249
+ }
226
250
  // Codexの入力欄は本文とEnterを同じtmux commandで送ると、初回turn完了後に
227
251
  // 本文が入力欄へ残ることがある。再試行時の半入力も含め、正規のsubmitを分離する。
228
252
  // 最後のEnterまで成功しない限りwakeは成功扱いにせず、flushSeatのreceiptも確定しない。
@@ -236,8 +260,7 @@ async function wake(seat, msgs) {
236
260
 
237
261
  function dispatch(msg) {
238
262
  if (deliveryStates.has(msg.seq)) return
239
- const targets = recipientNames(msg).filter(seat => members.has(seat)
240
- && members.get(seat)?.delivery?.kind !== 'parent_watch')
263
+ const targets = recipientNames(msg).filter(seat => isWakeupBridgeTarget(members.get(seat)))
241
264
  const state = { message: msg, targets: new Set(targets), delivered: new Set() }
242
265
  for (const seat of targets) {
243
266
  if (delivered.has(deliveryKey(msg.seq, seat))) state.delivered.add(seat)
@@ -259,7 +282,12 @@ async function flushSeat(seat) {
259
282
  flushing.add(seat)
260
283
  const msgs = [...queue.values()].sort((a, b) => a.seq - b.seq)
261
284
  try {
262
- await wake(seat, msgs)
285
+ const outcome = await wake(seat, msgs)
286
+ if (outcome === 'deferred') return
287
+ if (outcome === 'skipped') {
288
+ forgetSeat(seat)
289
+ return
290
+ }
263
291
  const receipts = []
264
292
  for (const msg of msgs) {
265
293
  if (queue.get(msg.seq) !== msg) continue
@@ -0,0 +1,37 @@
1
+ import { classifyPaneTail } from './seat-usage.mjs'
2
+
3
+ export const BROADCAST_RECIPIENT = 'all'
4
+ export const ROOM_UPDATE_FALLBACK =
5
+ 'room全体の状況が更新された。room.read_logで部屋を読み、状況を把握して次の行動を判断する。'
6
+
7
+ export function collapseWakeBody(body) {
8
+ return String(body ?? '').replace(/\s*\n+\s*/gu, ' / ')
9
+ }
10
+
11
+ export function formatWakeNotice(msg) {
12
+ const audience = Array.isArray(msg.to_names) ? msg.to_names.join(', ') : msg.to
13
+ const body = collapseWakeBody(msg.body)
14
+ if (msg.to === BROADCAST_RECIPIENT) {
15
+ return body
16
+ ? `[Peertable #${msg.seq}] ${msg.from} → ${BROADCAST_RECIPIENT}: ${body}`
17
+ : `[Peertable #${msg.seq}] ${ROOM_UPDATE_FALLBACK}`
18
+ }
19
+ return `[Peertable DM #${msg.seq}] ${msg.from} → ${audience}: ${body}`
20
+ }
21
+
22
+ /** 親番犬と tmux を持たない member は通常席 bridge の宛先にしない。 */
23
+ export function isWakeupBridgeTarget(member) {
24
+ if (!member || typeof member.name !== 'string' || member.name.length === 0) return false
25
+ if (member.delivery?.kind === 'parent_watch') return false
26
+ if (member.observe === null) return false
27
+ return true
28
+ }
29
+
30
+ /** Grok 既定はキュー投入。busy 中に積むと今のターンへ混ざらない。 */
31
+ export function shouldDeferGrokWake(vendor, tail) {
32
+ if (vendor !== 'grok') return false
33
+ if (typeof tail !== 'string') return false
34
+ if (classifyPaneTail(tail) === 'busy') return true
35
+ if (tail.includes('send a message to interrupt')) return true
36
+ return tail.includes('Enter:send now') && /#\d+\s+\[/u.test(tail)
37
+ }
@@ -31,7 +31,7 @@
31
31
  ## 着卓手順
32
32
 
33
33
  vendor に関わらず: `scripts/parent-join.sh <project> [name] [model] [effort] [vendor]` で member
34
- 登録する。`vendor` は `claude`(既定)または `codex`。Lattice 併用モードなら、
34
+ 登録する。`vendor` は `claude`(既定)、`codex`、または `grok`。Lattice 併用モードなら、
35
35
  `source .team/parent-env.sh` で Lattice mutation(`todo reopen` 等)に要る actor 環境変数
36
36
  (`LATTICE_TODO_ACTOR_HOST`/`SESSION`/`AGENT`)を親 shell へ持続配線する——**子 process の
37
37
  export は親 shell に伝播しないため**、これをしないまま `lattice todo reopen` 等を打つと
@@ -49,6 +49,9 @@ export は親 shell に伝播しないため**、これをしないまま `latti
49
49
  `scripts/codex-parent-watch.sh <project> <親名>`を都度実行し、空でないstdoutだけを`notify`して
50
50
  `yield_control`する。このscriptはHTTP catch-upを一度行って即終了する。長寿命なのはbackground taskの
51
51
  loopだけで、Node processや端末sessionは常駐させない。張り替え時は旧background taskを停止する。
52
+ - **Grok**: Monitorツール(persistent)で`node scripts/parent-watch.mjs <project> <親名> --follow`
53
+ を1回だけ起動し、stdoutのJSON eventを親へ返す。通常席用`wakeup-bridge`に親を載せない。
54
+ 張り替え時は旧Monitorを止めてから起動する。
52
55
 
53
56
  どちらも`parent-join.sh`が先に作る`.team/parent-watch.json`のcursorを共有する。watcher不在中のDMは
54
57
  次回起動時にcatch-upされ、親以外宛・親自身の発言・pingでは親を起こさない。`watch_error`が届いたら
@@ -115,7 +118,7 @@ peertable_parent_post() {
115
118
  3. 工程正本で照合する(Lattice 併用: `lattice todo status --json`。単独: `.team/tasks.md` と
116
119
  room ログの突き合わせ)。食い違ったら工程正本が正で、食い違い自体を room へ出す
117
120
  4. member 登録は残っているので `parent-join.sh` を再実行しない。名前を確認するだけでよい
118
- 5. vendorに応じて番犬を張り直す。Claudeは旧Monitorを止めて`--follow`を1回起動する。Codexは旧background
121
+ 5. vendorに応じて番犬を張り直す。ClaudeとGrokは旧Monitorを止めて`--follow`を1回起動する。Codexは旧background
119
122
  taskを止め、1秒ごとの`--poll` loopを起動する。永続cursorが不在時間のDMをcatch-upする
120
123
  6. 順序の要点は「room と工程正本を読み終えるまで発言しない」
121
124