peertable 0.5.1 → 0.6.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
@@ -73,13 +73,13 @@ docker compose -f deploy/compose.yaml up -d
73
73
 
74
74
  `http://localhost:8790` を開くと、全 room にライブ Web ビュー(SSE)が付く。**Web UI は観戦専用**——書込は全て API 経由で、`PEERTABLE_POST_TOKEN` 設定時はトークン必須。外から届く設置では必ずトークンを設定する。
75
75
 
76
- ライブビューはメンバーごとに vendor / model / effort / role と**稼働状態**(作業中・待機・**承認待ち**(許可ダイアログで止まっている)・停止)を出す。作業中の席はアイコンが動き、完了宣言(`[done]` / `[完了]` / `受理:` 等)の瞬間に席の上へ印が浮く。状態変化は SSE で押し込むので、30秒の再取得を待たず観測周期(約8秒)で切り替わる。発言にはログ番号(`[123]`)が付き、ライブ新着はブロック単位で現れる。**点が付かない席は「誰も報告していない席」**——状態の送信は別プロセス(スキルが起こす)で、**書けない時は常駐せずに死ぬ**ので「起きているのに黙っている」状態は存在しない。
76
+ ライブビューはメンバーごとに harness / model / effort / role と**稼働状態**(作業中・待機・**承認待ち**(許可ダイアログで止まっている)・停止)を出す。作業中の席はアイコンが動き、完了宣言(`[done]` / `[完了]` / `受理:` 等)の瞬間に席の上へ印が浮く。状態変化は SSE で押し込むので、30秒の再取得を待たず観測周期(約8秒)で切り替わる。発言にはログ番号(`[123]`)が付き、ライブ新着はブロック単位で現れる。**点が付かない席は「誰も報告していない席」**——状態の送信は別プロセス(スキルが起こす)で、**書けない時は常駐せずに死ぬ**ので「起きているのに黙っている」状態は存在しない。
77
77
 
78
78
  観測先は**席自身が名乗る**(`observe: {tmux_socket, tmux_target}`)。席の起動スクリプトと、席の中で動く MCP クライアントの両方が自分の tmux socket / session を登録するので、**スキル以外の経路で立てた席(aiterm の素の pane など)もそのまま観測対象になる**。表示名から `peer-<名前>` を推測しないので、任意のセッション名で立てた席が消える問題は起きない。名乗っていない古い席だけが従来の推測へ落ちる。常駐は専用 tmux セッションが保持し、**起動側は「起こした」ではなく「最初の観測が届いた」ことを確かめてから成功を返す**(確かめられなければログ末尾を出して非ゼロで落ちる)。
79
79
 
80
80
  API: `GET /api/<room>/messages` / `members` / `members/<name>` / `summary`(約120バイト・`seq`・`last_ts`・`member_count`)/ `events`(SSE)、`POST /api/<room>/messages` / `members`。
81
81
 
82
- **部屋がメンバーの唯一の台帳である。** メンバーに帰属する情報——素性(vendor / model / effort / roles / mission)・観測先(`observe`)・稼働状態・プロセス本人性(pid / 起動時刻 / argv digest)——は room サーバー内蔵の SQLite(`node:sqlite`・`/data/room.db`・Node 24+ 必須)の**1行**に全部入る。欄ごとに書き手は1人(素性=席自身の MCP クライアント、本人性=ランチャー、状態=状態ブリッジ)。席ファイルも重複欄も無く、全ての読者は台帳を読む。旧 `members.json` は初回起動で一度だけ取り込まれる。
82
+ **部屋がメンバーの唯一の台帳である。** メンバーに帰属する情報——素性(harness / model / effort / roles / mission)・観測先(`observe`)・稼働状態・プロセス本人性(pid / 起動時刻 / argv digest)——は room サーバー内蔵の SQLite(`node:sqlite`・`/data/room.db`・Node 24+ 必須)の**1行**に全部入る。欄ごとに書き手は1人(素性=席自身の MCP クライアント、本人性=ランチャー、状態=状態ブリッジ)。席ファイルも重複欄も無く、全ての読者は台帳を読む。旧 `members.json` は初回起動で一度だけ取り込まれる。
83
83
 
84
84
  **2. Claude Code のメンバーを着席させる。** room の MCP 定義は**プロジェクト root の `.mcp.json`** に置く:
85
85
 
package/README.md CHANGED
@@ -109,13 +109,13 @@ docker compose -f deploy/compose.yaml up -d
109
109
 
110
110
  Open `http://localhost:8790` — every room gets a live web view (SSE). **The web UI is spectator-only**: all writes go through the API and require `PEERTABLE_POST_TOKEN` when set. Set the token whenever the server is reachable from outside.
111
111
 
112
- The live view shows, per member: vendor / model / reasoning effort / role, and a **working state** — 作業中 (busy) · 待機 (idle) · 承認待ち (blocked on a permission prompt) · 停止 (dead). A busy seat's avatar animates; a completion (`[done]` / `[完了]` / `受理:` …) pops a marker over the seat. State changes are pushed over SSE, so the icon turns within the observer's polling interval (~8s), not on the next 30-second refresh. Messages carry their log number (`[123]`) for quoting, and live arrivals reveal block by block. **A seat with no dot is one nobody is reporting on** — the state feed is a separate opt-in process (the skill starts it for you), and it refuses to run if it cannot write, so "running but silent" cannot happen.
112
+ The live view shows, per member: harness / model / reasoning effort / role, and a **working state** — 作業中 (busy) · 待機 (idle) · 承認待ち (blocked on a permission prompt) · 停止 (dead). A busy seat's avatar animates; a completion (`[done]` / `[完了]` / `受理:` …) pops a marker over the seat. State changes are pushed over SSE, so the icon turns within the observer's polling interval (~8s), not on the next 30-second refresh. Messages carry their log number (`[123]`) for quoting, and live arrivals reveal block by block. **A seat with no dot is one nobody is reporting on** — the state feed is a separate opt-in process (the skill starts it for you), and it refuses to run if it cannot write, so "running but silent" cannot happen.
113
113
 
114
114
  Seats **declare where to watch them** (`observe: {tmux_socket, tmux_target}`). Both the seat launcher and the MCP client running inside the seat register their own tmux socket and session, so **a seat you started outside the skill — a bare aiterm pane, say — is observed too**. Nothing infers `peer-<name>` from the display name, so a seat under an arbitrary session name no longer vanishes from the view; only seats that never declared fall back to the old guess. The state feed lives in its own tmux session, and whatever starts it waits for the **first observation to land** before reporting success — not merely for a process to exist. If it never lands, the starter prints the log tail and exits non-zero.
115
115
 
116
116
  Endpoints: `GET /api/<room>/messages` · `GET /api/<room>/members` · `GET /api/<room>/members/<name>` · `GET /api/<room>/summary` (≈120 bytes: `seq`, `last_ts`, `member_count`) · `GET /api/<room>/events` (SSE) · `POST /api/<room>/messages` · `POST /api/<room>/members`.
117
117
 
118
- **The room is the single member ledger.** Everything that belongs to a member — identity (vendor / model / effort / roles / mission), where to observe it (`observe`), its working state, and its process identity (pid / start time / argv digest) — lives in one SQLite row inside the room server (`node:sqlite`, `/data/room.db`; requires Node 24+). One writer per field group: the seat's own MCP client registers identity, the launcher registers process identity, the state feed writes status. No seat files, no duplicated fields — every consumer reads the ledger. A legacy `members.json` is imported once on first boot.
118
+ **The room is the single member ledger.** Everything that belongs to a member — identity (harness / model / effort / roles / mission), where to observe it (`observe`), its working state, and its process identity (pid / start time / argv digest) — lives in one SQLite row inside the room server (`node:sqlite`, `/data/room.db`; requires Node 24+). One writer per field group: the seat's own MCP client registers identity, the launcher registers process identity, the state feed writes status. No seat files, no duplicated fields — every consumer reads the ledger. A legacy `members.json` is imported once on first boot.
119
119
 
120
120
  **2. Seat a Claude Code member session.** The room MCP definition must live in the **project-root `.mcp.json`**:
121
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.5.1",
3
+ "version": "0.6.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
@@ -13,7 +13,7 @@ import { findModelsDoc, resolveSeatIdentity } from '../skill/scripts/resolve-sea
13
13
 
14
14
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
15
15
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
16
- const MCP_VERSION = '0.5.1'
16
+ const MCP_VERSION = '0.6.1'
17
17
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
18
18
 
19
19
  const USAGE = `usage:
@@ -159,7 +159,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
159
159
  await mcp.connect(new StdioServerTransport())
160
160
 
161
161
  // 参加登録し、現在のログ末尾から未読を数え始める。
162
- // 素性(vendor/model/effort/role)は launch-seat.sh が env へ入れる。**登録のたびに載せる**——
162
+ // 素性(harness/model/effort/role)は launch-seat.sh が env へ入れる。**登録のたびに載せる**——
163
163
  // 登録は client の起動ごとに繰り返し起きるので、1回きりの経路に置くと
164
164
  // member の状態が失われた時に二度と戻らない(server 側は渡された欄だけ更新する upsert)
165
165
  function observeSelf() {
@@ -204,7 +204,10 @@ const roles = parseRoles(process.env.PEERTABLE_ROLES || process.env.PEERTABLE_RO
204
204
  // 台帳(room server の SQLite)へは canonical 欄だけを登録する。settings / role(単数)の
205
205
  // 重複欄は 2026-08-22 に廃止——同一情報の重複管理禁止(オーナー裁定)。
206
206
  const IDENTITY = Object.fromEntries(Object.entries({
207
- vendor: process.env.PEERTABLE_VENDOR,
207
+ // 正本は harness。旧 launcher(PEERTABLE_VENDOR だけを置く版)からの起動も受ける。
208
+ harness: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
209
+ // 旧版 room server(vendor 列世代)へ登録しても素性が落ちないための互換併記。
210
+ vendor: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
208
211
  model: process.env.PEERTABLE_MODEL,
209
212
  effort: process.env.PEERTABLE_EFFORT,
210
213
  roles,
@@ -220,7 +223,7 @@ const IDENTITY = Object.fromEntries(Object.entries({
220
223
  roles,
221
224
  model: process.env.PEERTABLE_MODEL,
222
225
  effort: process.env.PEERTABLE_EFFORT,
223
- vendor: process.env.PEERTABLE_VENDOR,
226
+ harness: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
224
227
  markdown: readFileSync(modelsDoc, 'utf8'),
225
228
  })
226
229
  if (checked.error) throw new Error(`${checked.error}: ${checked.message}`)
package/room/server.mjs CHANGED
@@ -40,7 +40,7 @@ db.exec(`
40
40
  room TEXT NOT NULL,
41
41
  name TEXT NOT NULL,
42
42
  joined_at TEXT NOT NULL,
43
- vendor TEXT, model TEXT, effort TEXT,
43
+ harness TEXT, model TEXT, effort TEXT,
44
44
  roles TEXT,
45
45
  mission TEXT,
46
46
  delivery TEXT,
@@ -53,8 +53,15 @@ db.exec(`
53
53
  )
54
54
  `)
55
55
 
56
+ // 既存 DB(旧 vendor 列世代)は一度だけ harness 列を足して値を引き継ぐ。旧列は rollback 用に残す。
57
+ {
58
+ const columns = db.prepare('PRAGMA table_info(members)').all().map((c) => c.name)
59
+ if (!columns.includes('harness')) db.exec('ALTER TABLE members ADD COLUMN harness TEXT')
60
+ if (columns.includes('vendor')) db.exec('UPDATE members SET harness = vendor WHERE harness IS NULL')
61
+ }
62
+
56
63
  const MEMBER_COLUMNS = [
57
- 'joined_at', 'vendor', 'model', 'effort', 'roles', 'mission', 'delivery', 'observe',
64
+ 'joined_at', 'harness', 'model', 'effort', 'roles', 'mission', 'delivery', 'observe',
58
65
  'aiterm_session_id', 'status', 'status_at', 'busy_since', 'pane_token_hint', 'usage_source',
59
66
  'pid', 'started_identity', 'argv_digest', 'identity_recorded_at',
60
67
  ]
@@ -67,6 +74,8 @@ function rowToMember(row) {
67
74
  if (value === null || value === undefined) continue
68
75
  member[column] = MEMBER_JSON_COLUMNS.has(column) ? JSON.parse(value) : value
69
76
  }
77
+ // 旧読者(member.vendor を読む旧版 bridge/skill)向けの互換 mirror。正本は harness。
78
+ if (member.harness !== undefined) member.vendor = member.harness
70
79
  return member
71
80
  }
72
81
  const listMembers = roomName => db.prepare('SELECT * FROM members WHERE room = ? ORDER BY joined_at').all(roomName).map(rowToMember)
@@ -85,15 +94,17 @@ function putMember(roomName, member) {
85
94
  VALUES (:room, :name, ${MEMBER_COLUMNS.map(c => `:${c}`).join(', ')})`).run(values)
86
95
  }
87
96
 
88
- // 旧形式(settings/role の重複欄)を canonical へ畳む。台帳には canonical だけを置く。
97
+ // 旧形式(settings/role の重複欄・旧 vendor 欄)を canonical へ畳む。台帳には canonical だけを置く。
89
98
  function normalizeMemberMeta(meta) {
90
- const { role, settings, ...rest } = meta
99
+ const { role, settings, vendor, ...rest } = meta
91
100
  const out = { ...rest }
92
101
  if (out.roles === undefined && role) out.roles = [role]
102
+ if (out.harness === undefined && vendor !== undefined) out.harness = vendor
93
103
  if (settings && typeof settings === 'object') {
94
- for (const key of ['vendor', 'model', 'effort']) {
104
+ for (const key of ['harness', 'model', 'effort']) {
95
105
  if (out[key] === undefined && settings[key] !== undefined) out[key] = settings[key]
96
106
  }
107
+ if (out.harness === undefined && settings.vendor !== undefined) out.harness = settings.vendor
97
108
  }
98
109
  for (const key of Object.keys(out)) if (out[key] === undefined) delete out[key]
99
110
  return out
@@ -173,7 +184,7 @@ function normalizeAudience(to, toNames) {
173
184
 
174
185
  // SSE の member イベントで押し込む欄。閲覧者が気づく欄だけに絞る(POST /members 参照)。
175
186
  // roles は配列なので JSON 比較で差分を見る
176
- const MEMBER_EVENT_FIELDS = ['status', 'busy_since', 'vendor', 'model', 'effort', 'roles', 'mission']
187
+ const MEMBER_EVENT_FIELDS = ['status', 'busy_since', 'harness', 'model', 'effort', 'roles', 'mission']
177
188
  const memberFieldChanged = (before, after) =>
178
189
  MEMBER_EVENT_FIELDS.some(f => JSON.stringify(before?.[f] ?? null) !== JSON.stringify(after?.[f] ?? null))
179
190
 
@@ -272,7 +283,7 @@ http.createServer(async (req, res) => {
272
283
  }
273
284
  if (req.method === 'POST' && rest === 'members') {
274
285
  const { name, ...meta } = JSON.parse(body)
275
- // 素性(vendor/model/effort/role)や稼働状態は、名前以外の欄をそのまま任意欄として持つ。
286
+ // 素性(harness/model/effort/role)や稼働状態は、名前以外の欄をそのまま任意欄として持つ。
276
287
  // **渡された欄だけ更新し、渡されなかった欄は既存を保つ**——席の client は `{name}` だけで
277
288
  // 登録するので、これが無いと再接続のたびに素性が消える。`joined_at` は最初の登録を保つ。
278
289
  // observe:null は「取れなかった」であって「記述子を消せ」ではない。上書きすると
@@ -8,7 +8,7 @@
8
8
 
9
9
  本書の根拠は次の4種を混ぜない。
10
10
 
11
- 1. **一次事実**: vendor公式の価格、context、対応effort、live catalog。
11
+ 1. **一次事実**: model提供元公式の価格、context、対応effort、live catalog。
12
12
  2. **外部観測**: benchmark、Xの利用報告、独立評価。harness・標本数・再現条件を併記する。
13
13
  3. **dotagents実測**: 当工場のtask、監査通過率、配線実測。一般性能へ拡張しない。
14
14
  4. **運用判断**: 上記を踏まえた配置。客観的に確定した事実を「オーナー裁定」と呼ばない。
@@ -70,7 +70,7 @@ Anthropic(Claude Code本体・Agent/Workflow)/OpenAI Codex(Codex CLI・c
70
70
 
71
71
  ## 入口と使い分け
72
72
 
73
- - **Codex親の三入口を分ける**: ① native subagent=repo密結合、② external execution=codex-sidecar/aiterm、③ consultation=gpt-connector。Grok/ComposerはAitermの別vendor入口であり、Codex→Codexの入口判断とは別契約。
73
+ - **Codex親の三入口を分ける**: ① native subagent=repo密結合、② external execution=codex-sidecar/aiterm、③ consultation=gpt-connector。Grok/ComposerはAitermの別harness入口であり、Codex→Codexの入口判断とは別契約。
74
74
  - Aitermの`codex_agent`/`grok_agent`/`claude_agent`はmodelとeffortを毎回明示する。live catalog不在・effort非対応は明示エラーにし、別modelへfallbackしない。
75
75
  - **委譲の安全・回収・受入契約は[委譲契約](../shared/orchestrate/delegation-contract.md)が正本**。Aitermの運用型は[aiterm-dispatch](../shared/orchestrate/aiterm-dispatch.md)を正とする。external writerはinstalled→registered→verified→execution-verifiedの最終段だけに置く。
76
76
  - codex-sidecarはmodel/effortを毎回明示するか`.codex-sidecar.yml` defaultsへ置く。現行schemaはlow〜xhighでmaxを渡せない。
package/skill/SKILL.md CHANGED
@@ -49,7 +49,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
49
49
 
50
50
  手順は **聞き取り → script → 着任指示** の3段である。scripts が機械部分を全部持つので、AI が手で tmux を組み立てることはしない。
51
51
 
52
- 1. **聞き取り**: 対象プロジェクトのパス / **工程正本(`Lattice 併用`=既定 / `単独`)** / メンバー数と**役割**(dotagents `docs/02_models.md` の役割名そのもの。未指定・未知は着席しない。model×effort は `launch-seat.sh` が順位表1位から機械解決する——呼び出し側が model / vendor / effort を渡して正本を迂回しない。決定49改・決定91)/ 初期タスク群(何を作るか)/ room 名(既定: プロジェクトのディレクトリ名)/ **公開URL基底**(Lattice 併用のみ。外部ペインに書く URL。クオ環境は `https://peertable.kitepon.dev`。未指定なら room サーバーの URL がそのまま入る=LAN URL は Lattice を外から見た時に開けない)
52
+ 1. **聞き取り**: 対象プロジェクトのパス / **工程正本(`Lattice 併用`=既定 / `単独`)** / メンバー数と**役割**(dotagents `docs/02_models.md` の役割名そのもの。未指定・未知は着席しない。model×effort は `launch-seat.sh` が順位表1位から機械解決する——呼び出し側が model / harness / effort を渡して正本を迂回しない。決定49改・決定91)/ 初期タスク群(何を作るか)/ room 名(既定: プロジェクトのディレクトリ名)/ **公開URL基底**(Lattice 併用のみ。外部ペインに書く URL。クオ環境は `https://peertable.kitepon.dev`。未指定なら room サーバーの URL がそのまま入る=LAN URL は Lattice を外から見た時に開けない)
53
53
  - **メンバー数の既定**: Lattice 併用なら plan compile 結果の幅(`max_frontier_width`)に合わせる(実測: 幅3→3人、第2 campaign で幅4→4人目追加)。frontier より多い席は最初から遊ぶ。単独モードには frontier が無いので既定の根拠も無く、聞き取りで決める
54
54
  - **運用中のworker席数の標準は「ready+activeな実装ToDo数」(決定68)**: 監査専任席はworker数、reclaim、scale-down候補へ含めず、最終試験結果を待って監査を担う。claimできるToDoが無いworker席は仕事を発明せず、最終手段として親だけへ待機DMする
55
55
  - **モードの選び分け**: タスク間に依存があり並列境界の機械保証が要るなら Lattice 併用。依存の無い小規模作業で、対象プロジェクトに Lattice を持ち込みたくないなら単独。単独で失うのは task 間スケジューリングの機械保証だけで、円卓の核(room・憲章・宣言による協力)は変わらない(決定47)
@@ -65,18 +65,18 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
65
65
  - 起動前に `pty_list` で既存の `peer-*` 席を確認する(前の卓の残骸を99席実測したことがある)。同名の席は launch-seat.sh が落としてから立て直す
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
- - **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 になって初めて分かる)。**DM を席 TUI へ入れる配達は Codex / Grok だけ**(下記)——Claude 席は room client が `notifications/claude/channel` を送る(8/12 に送信ループを削った回帰を 0.4.5 で戻した)。Codex は MCP 通知だけではターンを始めないので、room に残すだけでは席に届いたことにならない
69
- - **Grok 席**(`vendor=grok`): project rootの`.mcp.json`をGrok Buildが読み、同じroom clientと席固有envをAitermが渡す。**user `~/.grok/config.toml` の booth/lattice 等 MCP は載せない**——席専用 `GROK_HOME`(`.team/seats/<name>.grok-home`、auth.json と ui だけ)を preflight と live 席の両方へ渡す。通らなければ席を立てず、別 model へ落とさない。model / effortはAitermの`grok_agent`へそのまま渡し、`grok models`のlive catalogに無いmodelは着席前に失敗する。初めて開くtreeの既知workspace trustは着席処理が通す。channelsは無いので同じ TUI 配達を使う。Grok TUIの既定はターン中の素送信を今の仕事へ混ぜず入力キューへ積むので、配達はGrok席がidleになるまで送らない
68
+ - **Codex 席**(`harness=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 になって初めて分かる)。**DM を席 TUI へ入れる配達は Codex / Grok だけ**(下記)——Claude 席は room client が `notifications/claude/channel` を送る(8/12 に送信ループを削った回帰を 0.4.5 で戻した)。Codex は MCP 通知だけではターンを始めないので、room に残すだけでは席に届いたことにならない
69
+ - **Grok 席**(`harness=grok`): project rootの`.mcp.json`をGrok Buildが読み、同じroom clientと席固有envをAitermが渡す。**user `~/.grok/config.toml` の booth/lattice 等 MCP は載せない**——席専用 `GROK_HOME`(`.team/seats/<name>.grok-home`、auth.json と ui だけ)を preflight と live 席の両方へ渡す。通らなければ席を立てず、別 model へ落とさない。model / effortはAitermの`grok_agent`へそのまま渡し、`grok models`のlive catalogに無いmodelは着席前に失敗する。初めて開くtreeの既知workspace trustは着席処理が通す。channelsは無いので同じ TUI 配達を使う。Grok TUIの既定はターン中の素送信を今の仕事へ混ぜず入力キューへ積むので、配達はGrok席がidleになるまで送らない
70
70
  6. **DM の TUI 配達(Codex / Grok 席がある時)**: `scripts/ensure-bridge.sh <project> wakeup <席名>…`。room の SSE を購読し、明示的にその席宛の新着(自分の発言は除く)だけを席の TUI へ素送信する。専用の起床デーモンではない——Codex / Grok が MCP 通知からターンを始めないための配達口である。ensure は専用 tmux session で起動し、bridge の `ready_at` を待つ。確認できなければログ末尾を出して非ゼロで終わる。**pid が生きているだけでは本人ではない**(Windows は pid を再利用する。2026-08-21 実測: 死んだ bridge の pid が oracle-mcp に再利用され、ensure が起動済みと誤認、全員宛キックオフが席へ届かなかった)。live 判定は record の `last_progress_at` が新しいこと。偽の pid は kill しない。Codexは即送信、Grokはidle待ち。親(`parent_watch`)は対象にしない。`observe` 欠落は対象外ではなく配達失敗としてログに出し、再試行する。停止は `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件と出す**ので、TUI へ入れているか・取りこぼしていないかはログを見れば分かる。再現ハーネスは `experiments/bridge-catchup-repro.mjs`
73
73
 
74
74
  6.5 **席の稼働状態ブリッジ(setup.sh が自動で起こす)**: 手順3の scaffold が `ensure-bridge.sh <project> seat-status` で起こすので、**AI が手で叩く段は無い**。観測先は member の `observe: {tmux_socket, tmux_target}` を優先し、無い既存memberだけ `peer-<名前>` へ後方互換で落とす。socket は明示env → aiterm POSIX既定 → 検証済み`/tmp/aiterm-*.sock`1本 → 既定パスの順で解決し、bashからは`tmux-socket.mjs`を呼ぶ。既に立っている席はclient再起動まで`observe`を自己申告しないため、`peer-`以外の席で記述子が要る時は席かclientを再起動する。WSLではbridgeが読む**WSL側**の版が正で、Windows側だけの更新では直らない。
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
- - 同じvendor内のmodel / effort変更はAitermが同一sessionと会話contextを保って行う。vendor変更だけは再起動を伴うため、本人はrole・工程正本・roomログから再着任する
76
+ - 同じharness内のmodel / effort変更はAitermが同一sessionと会話contextを保って行う。harness変更だけは再起動を伴うため、本人はrole・工程正本・roomログから再着任する
77
77
  6.8 **mission 更新(席が自分で実行)**: 工程が変わったら席が `env -u PEERTABLE_POST_TOKEN skill/scripts/set-mission.sh <project> <name> <text>` を打つ。`POST /members` で chip を更新し、`[mission] <name>: <text>` を全員へ1行出す。席は再起動しない。親は代行しない。`change-seat.sh` に mission を足さない。
78
78
 
79
- 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」の番犬仕様)。**着卓完了の条件は、parent-join が投稿する耳疎通probe(`EAR_PROBE_SENT` の nonce)を自分の監視イベントとして受信すること**——番犬プロセスの生存は耳の証拠にならない(前セッションの耳へ吠え続け親宛DMが全損した実被弾 2026-08-22)。受信できないなら監視を張り直す。通常席用wakeup-bridgeに親を載せない。broadcastのkickoffは廃止済み。以後の post も API 直(同 notes)
79
+ 7. **親の着卓**(このセッション): `scripts/parent-join.sh <project> [name] [model] [effort] [harness]` で member 登録とparent-watch cursorのprimeを行う。**`effort` は任意のまま据え置く**——席は `launch-seat.sh` が `--effort` で実際に設定するので「渡した値=実挙動」だが、親は既に走っているセッションで自分の effort を機械的に知る経路が無く、推測して載せると画面が嘘をつく。続けて、ClaudeとGrokはMonitor、Codexはyieldしたbackground tool taskとして**親宛DM番犬**を1世代だけ張る(形は下記「親の operating notes」の番犬仕様)。**着卓完了の条件は、parent-join が投稿する耳疎通probe(`EAR_PROBE_SENT` の nonce)を自分の監視イベントとして受信すること**——番犬プロセスの生存は耳の証拠にならない(前セッションの耳へ吠え続け親宛DMが全損した実被弾 2026-08-22)。受信できないなら監視を張り直す。通常席用wakeup-bridgeに親を載せない。broadcastのkickoffは廃止済み。以後の post も API 直(同 notes)
80
80
  8. **起動確認**: room の members に全員いる / 最初の claim が room に流れる(Lattice 併用モードはそれが Lattice へ到達している=`lattice todo status --json` の active に出ることも確認する。単独モードは room の claim 宣言だけが到達の証拠)/ Web UI で観測できる、をチェックして報告する
81
81
 
82
82
  ## teardown
@@ -3,8 +3,8 @@
3
3
  import { Client } from '@modelcontextprotocol/sdk/client/index.js'
4
4
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
5
5
 
6
- const [session_name, vendor, model, reasoning_effort, cwd, prompt = ''] = process.argv.slice(2)
7
- if (!session_name || !['claude', 'codex', 'grok'].includes(vendor) || !model || !reasoning_effort || !cwd) {
6
+ const [session_name, harness, model, reasoning_effort, cwd, prompt = ''] = process.argv.slice(2)
7
+ if (!session_name || !['claude', 'codex', 'grok'].includes(harness) || !model || !reasoning_effort || !cwd) {
8
8
  throw new Error('SEAT_AITERM_LAUNCH_ARGS_INVALID')
9
9
  }
10
10
 
@@ -12,7 +12,7 @@ const client = new Client({ name: 'peertable-seat-launch', version: '0.4.12' })
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',
15
- 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
15
+ 'PEERTABLE_HARNESS', 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
16
16
  'PEERTABLE_ROLES', 'PEERTABLE_MISSION',
17
17
  'PEERTABLE_TMUX_SOCKET',
18
18
  ]
@@ -20,13 +20,13 @@ if (process.env.PEERTABLE_PLAN) env_vars.push(
20
20
  'PEERTABLE_PLAN', 'LATTICE_CLI', 'LATTICE_TODO_ACTOR_HOST',
21
21
  'LATTICE_TODO_ACTOR_SESSION', 'LATTICE_TODO_ACTOR_AGENT',
22
22
  )
23
- if (vendor === 'grok') {
23
+ if (harness === 'grok') {
24
24
  if (process.env.GROK_HOME) env_vars.push('GROK_HOME')
25
25
  env_vars.push('GROK_PRIVACY_NOTICE_ROLLOUT')
26
26
  }
27
- if (vendor === 'codex' && process.env.CODEX_HOME) env_vars.push('CODEX_HOME')
27
+ if (harness === 'codex' && process.env.CODEX_HOME) env_vars.push('CODEX_HOME')
28
28
  const result = await client.callTool({
29
- name: `${vendor}_agent`,
29
+ name: `${harness}_agent`,
30
30
  arguments: {
31
31
  session_name,
32
32
  cwd,
@@ -39,7 +39,7 @@ const result = await client.callTool({
39
39
  await client.close()
40
40
  if (result.isError) throw new Error(result.content?.[0]?.text ?? 'SEAT_AITERM_LAUNCH_FAILED')
41
41
  const receipt = result.structuredContent ?? JSON.parse(result.content?.[0]?.text ?? '{}')
42
- if (receipt?.schema !== 'aiterm.agent-launch-result.v1' || receipt.provider !== vendor || receipt.session_id !== session_name) {
42
+ if (receipt?.schema !== 'aiterm.agent-launch-result.v1' || receipt.provider !== harness || receipt.session_id !== session_name) {
43
43
  throw new Error('SEAT_AITERM_LAUNCH_RECEIPT_INVALID')
44
44
  }
45
45
  console.log(JSON.stringify(receipt))
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
- # 席の vendor / model / effort を変更する。
3
- # usage: change-seat.sh <project_dir> <member> [--vendor <vendor>] [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]
2
+ # 席の harness / model / effort を変更する。
3
+ # usage: change-seat.sh <project_dir> <member> [--harness <harness>] [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]
4
4
  #
5
5
  # **自然文の依頼を再解釈しない。** 依頼の意味・本人の意図・変更してよい局面を判断するのは親である AI で、
6
6
  # この script が受け取るのは親が確定した target だけである(旧 change-effort.sh の
@@ -22,10 +22,10 @@ credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$script_dir/seat-credential.mj
22
22
 
23
23
  proj="${1:-}"; name="${2:-}"
24
24
  shift 2 2>/dev/null || true
25
- opt_vendor=""; opt_model=""; opt_effort=""; parent="bell"; reason=""
25
+ opt_harness=""; opt_model=""; opt_effort=""; parent="bell"; reason=""
26
26
  while [ $# -gt 0 ]; do
27
27
  case "$1" in
28
- --vendor) opt_vendor="${2:-}"; shift 2 || true ;;
28
+ --harness|--vendor) opt_harness="${2:-}"; shift 2 || true ;; # --vendor は旧名互換
29
29
  --model) opt_model="${2:-}"; shift 2 || true ;;
30
30
  --effort) opt_effort="${2:-}"; shift 2 || true ;;
31
31
  --parent) parent="${2:-}"; shift 2 || true ;;
@@ -34,7 +34,7 @@ while [ $# -gt 0 ]; do
34
34
  esac
35
35
  done
36
36
  [ -n "$proj" ] && [ -n "$name" ] || {
37
- echo "SEAT_CHANGE_ARGS_INVALID: usage: change-seat.sh <project_dir> <member> [--vendor <vendor>] [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]" >&2
37
+ echo "SEAT_CHANGE_ARGS_INVALID: usage: change-seat.sh <project_dir> <member> [--harness <harness>] [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]" >&2
38
38
  exit 2
39
39
  }
40
40
  [ -n "$opt_model" ] || [ -n "$opt_effort" ] || {
@@ -57,22 +57,22 @@ meta=$(printf '%s' "$members" | python3 -c '
57
57
  import json,sys
58
58
  name=sys.argv[1]
59
59
  member=next((m for m in json.load(sys.stdin).get("members",[]) if m.get("name")==name),None)
60
- if not member or member.get("vendor") not in ("claude","codex","grok") or not member.get("model"):
60
+ if not member or (member.get("harness") or member.get("vendor")) not in ("claude","codex","grok") or not member.get("model"):
61
61
  raise SystemExit(1)
62
62
  roles=member.get("roles") or []
63
- print("\t".join((member["vendor"],member["model"],member.get("effort") or "",member.get("aiterm_session_id") or "",",".join(roles))))
64
- ' "$name") || { echo "SEAT_CHANGE_MEMBER_METADATA_MISSING: ${name} のvendor/modelが要る" >&2; exit 1; }
65
- IFS=$'\t' read -r old_vendor old_model old_effort aiterm_session_id old_role <<EOF
63
+ print("\t".join(((member.get("harness") or member.get("vendor")),member["model"],member.get("effort") or "",member.get("aiterm_session_id") or "",",".join(roles))))
64
+ ' "$name") || { echo "SEAT_CHANGE_MEMBER_METADATA_MISSING: ${name} のharness/modelが要る" >&2; exit 1; }
65
+ IFS=$'\t' read -r old_harness old_model old_effort aiterm_session_id old_role <<EOF
66
66
  $meta
67
67
  EOF
68
68
 
69
- vendor="${opt_vendor:-$old_vendor}"
70
- case "$vendor" in
69
+ harness="${opt_harness:-$old_harness}"
70
+ case "$harness" in
71
71
  claude|codex|grok) ;;
72
- *) echo "SEAT_CHANGE_VENDOR_UNSUPPORTED: vendor=${vendor}(claude / codex / grok のみ)" >&2; exit 2 ;;
72
+ *) echo "SEAT_CHANGE_HARNESS_UNSUPPORTED: harness=${harness}(claude / codex / grok のみ)" >&2; exit 2 ;;
73
73
  esac
74
- if [ "$vendor" != "$old_vendor" ] && { [ -z "$opt_model" ] || [ -z "$opt_effort" ]; }; then
75
- echo "SEAT_CHANGE_ARGS_INVALID: vendor変更には --model と --effort の明示指定が要る" >&2
74
+ if [ "$harness" != "$old_harness" ] && { [ -z "$opt_model" ] || [ -z "$opt_effort" ]; }; then
75
+ echo "SEAT_CHANGE_ARGS_INVALID: harness変更には --model と --effort の明示指定が要る" >&2
76
76
  exit 2
77
77
  fi
78
78
  model="${opt_model:-$old_model}"
@@ -83,7 +83,7 @@ effort="${opt_effort:-$old_effort}"
83
83
  echo "SEAT_CHANGE_EFFORT_UNKNOWN: ${name} の現在effortがmetadataに無い。--effort を明示する" >&2; exit 1
84
84
  }
85
85
 
86
- if [ "$model" = "$old_model" ] && [ "$effort" = "$old_effort" ] && [ "$vendor" = "$old_vendor" ]; then
86
+ if [ "$model" = "$old_model" ] && [ "$effort" = "$old_effort" ] && [ "$harness" = "$old_harness" ]; then
87
87
  echo "SEAT_CHANGE_NOOP: ${name} は既に model=${model} / effort=${effort}(再起動しない)"
88
88
  exit 0
89
89
  fi
@@ -102,7 +102,7 @@ case "$screen" in
102
102
  esac
103
103
 
104
104
  # target の検証は live 面だけを使い、古くなる hardcode を足さない。
105
- case "$vendor" in
105
+ case "$harness" in
106
106
  claude)
107
107
  # Claude には非破壊で引ける model catalog が無い(`--help` の alias 例は catalog ではなく、
108
108
  # 実際 2026-08-11 に `fable` は例に載ったまま live では unavailable だった)。
@@ -166,7 +166,7 @@ raise SystemExit(0 if model in models else 1)
166
166
  esac
167
167
 
168
168
  changes=""
169
- [ "$vendor" = "$old_vendor" ] || changes="vendor ${old_vendor} → ${vendor}"
169
+ [ "$harness" = "$old_harness" ] || changes="harness ${old_harness} → ${harness}"
170
170
  [ "$model" = "$old_model" ] || {
171
171
  [ -z "$changes" ] || changes="$changes / "
172
172
  changes="${changes}model ${old_model} → ${model}"
@@ -180,7 +180,7 @@ credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" path "$p
180
180
  echo "SEAT_CHANGE_CREDENTIAL_MISSING: ${name} のroom credentialを特定できない" >&2; exit 1
181
181
  }
182
182
 
183
- if [ "$vendor" = "$old_vendor" ]; then
183
+ if [ "$harness" = "$old_harness" ]; then
184
184
  [ -n "$aiterm_session_id" ] || {
185
185
  echo "SEAT_CHANGE_AITERM_SESSION_MISSING: ${name} にAiterm managed session_idが無い" >&2; exit 1
186
186
  }
@@ -190,7 +190,7 @@ if [ "$vendor" = "$old_vendor" ]; then
190
190
  node "$script_dir/aiterm-configure.mjs" "${configure_args[@]}" >/dev/null || {
191
191
  echo "SEAT_CHANGE_AITERM_CONFIGURE_FAILED: ${name} の設定は変更していない" >&2; exit 1
192
192
  }
193
- identity=$(python3 -c 'import json,sys;print(json.dumps({"name":sys.argv[1],"vendor":sys.argv[2],"model":sys.argv[3],"effort":sys.argv[4],"aiterm_session_id":sys.argv[5]}))' "$name" "$vendor" "$model" "$effort" "$aiterm_session_id")
193
+ identity=$(python3 -c 'import json,sys;print(json.dumps({"name":sys.argv[1],"harness":sys.argv[2],"vendor":sys.argv[2],"model":sys.argv[3],"effort":sys.argv[4],"aiterm_session_id":sys.argv[5]}))' "$name" "$harness" "$model" "$effort" "$aiterm_session_id")
194
194
  env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
195
195
  "$url/api/$room/members" "$identity" >/dev/null || {
196
196
  echo "SEAT_CHANGE_CHANGED_BUT_METADATA_FAILED: ${name} は設定済み、room metadataを同期できない" >&2; exit 1
@@ -209,10 +209,10 @@ else
209
209
  }
210
210
  brief="席設定が変更され(${changes})、席を再起動しました。.team/roles/member.mdと工程正本・roomログから再着任し、進行中taskを続けてください。"
211
211
  if ! "$launch" "$proj" "$name" --roles "$old_role" --model "$model" --effort "$effort" "$brief"; then
212
- echo "SEAT_CHANGE_RESTART_FAILED: ${changes}。旧設定(vendor=${old_vendor} / model=${old_model} / effort=${old_effort:-default})へrollbackする" >&2
212
+ echo "SEAT_CHANGE_RESTART_FAILED: ${changes}。旧設定(harness=${old_harness} / model=${old_model} / effort=${old_effort:-default})へrollbackする" >&2
213
213
  rollback_brief="席設定の変更に失敗して旧設定へrollbackしました。.team/roles/member.mdと工程正本・roomログから再着任してください。"
214
214
  if "$launch" "$proj" "$name" --roles "$old_role" --model "$old_model" --effort "$old_effort" "$rollback_brief"; then
215
- echo "SEAT_CHANGE_ROLLED_BACK: ${name} は vendor=${old_vendor} / model=${old_model} / effort=${old_effort:-default} で再着席" >&2
215
+ echo "SEAT_CHANGE_ROLLED_BACK: ${name} は harness=${old_harness} / model=${old_model} / effort=${old_effort:-default} で再着席" >&2
216
216
  else
217
217
  echo "SEAT_CHANGE_ROLLBACK_FAILED: ${name} の席を手動で復旧する必要がある" >&2
218
218
  fi
@@ -226,11 +226,11 @@ members_after=$(curl -sf "$url/api/$room/members") || {
226
226
  }
227
227
  if ! printf '%s' "$members_after" | python3 -c '
228
228
  import json,sys
229
- name,vendor,model,effort=sys.argv[1:5]
229
+ name,harness,model,effort=sys.argv[1:5]
230
230
  m=next((m for m in json.load(sys.stdin).get("members",[]) if m.get("name")==name),{})
231
- raise SystemExit(0 if m.get("vendor")==vendor and m.get("model")==model and m.get("effort")==effort else 1)
232
- ' "$name" "$vendor" "$model" "$effort"; then
233
- echo "SEAT_CHANGE_CHANGED_BUT_UNVERIFIED: 席は再起動済み、member metadataが vendor=${vendor} / model=${model} / effort=${effort} でない" >&2
231
+ raise SystemExit(0 if (m.get("harness") or m.get("vendor"))==harness and m.get("model")==model and m.get("effort")==effort else 1)
232
+ ' "$name" "$harness" "$model" "$effort"; then
233
+ echo "SEAT_CHANGE_CHANGED_BUT_UNVERIFIED: 席は再起動済み、member metadataが harness=${harness} / model=${model} / effort=${effort} でない" >&2
234
234
  exit 1
235
235
  fi
236
236
 
@@ -24,14 +24,14 @@ const endMarker = '# END PEERTABLE ROOM MCP'
24
24
  const roomHeader = /^\s*\[mcp_servers\.room\]\s*$/mu
25
25
  const seatEnvNames = [
26
26
  'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
27
- 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
27
+ 'PEERTABLE_HARNESS', 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
28
28
  'PEERTABLE_ROLES', 'PEERTABLE_MISSION',
29
29
  'PEERTABLE_PLAN', 'LATTICE_CLI', 'LATTICE_TODO_ACTOR_HOST',
30
30
  'LATTICE_TODO_ACTOR_SESSION', 'LATTICE_TODO_ACTOR_AGENT',
31
31
  ]
32
32
  const requiredSeatEnv = [
33
33
  'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
34
- 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_ROLE', 'PEERTABLE_ROLES',
34
+ 'PEERTABLE_HARNESS', 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_ROLE', 'PEERTABLE_ROLES',
35
35
  ]
36
36
 
37
37
  function atomicWrite(file, body, mode = 0o644) {
@@ -1,25 +1,25 @@
1
1
  #!/bin/bash
2
2
  # 席を1つ立てる(tmux 作成 → env 注入 → エージェント起動 → 既知ダイアログ通過 → 着席確認)。
3
- # usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--vendor <vendor>] [brief]
3
+ # usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--harness <harness>] [brief]
4
4
  # --roles: 必須。02_models の公式役割。位置引数に公式役割を置いてもよい
5
5
  # --model: 任意。省略時は先頭役割の着席可能な1位。指定時は表外でも通す
6
6
  # brief: 着席が成立したら送る着任指示(省略時は送らない)
7
7
  set -e
8
8
  usage() {
9
- echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--vendor <vendor>] [brief]" >&2
9
+ echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--harness <harness>] [brief]" >&2
10
10
  }
11
11
  proj="$1"; name="$2"
12
12
  [ -n "$proj" ] && [ -n "$name" ] || { usage; exit 1; }
13
13
  shift 2
14
14
  brief=""
15
15
  roles=""; mission=""
16
- opt_model=""; opt_vendor=""; opt_effort=""
16
+ opt_model=""; opt_harness=""; opt_effort=""
17
17
  while [ $# -gt 0 ]; do
18
18
  case "$1" in
19
19
  --roles) roles="${2:-}"; shift 2 ;;
20
20
  --mission) mission="${2:-}"; shift 2 ;;
21
21
  --model) opt_model="${2:-}"; shift 2 ;;
22
- --vendor) opt_vendor="${2:-}"; shift 2 ;;
22
+ --harness|--vendor) opt_harness="${2:-}"; shift 2 ;; # --vendor は旧名互換
23
23
  --effort) opt_effort="${2:-}"; shift 2 ;;
24
24
  --*) echo "SEAT_LAUNCH_ARGS_INVALID: 不明な引数 $1" >&2; usage; exit 1 ;;
25
25
  *)
@@ -65,26 +65,26 @@ aiterm_session_id=""
65
65
  placement_argv=(--roles "$roles")
66
66
  [ -n "$opt_model" ] && placement_argv+=(--model "$opt_model")
67
67
  [ -n "$opt_effort" ] && placement_argv+=(--effort "$opt_effort")
68
- [ -n "$opt_vendor" ] && placement_argv+=(--vendor "$opt_vendor")
68
+ [ -n "$opt_harness" ] && placement_argv+=(--harness "$opt_harness")
69
69
  placement=$(node "$placement_helper" "${placement_argv[@]}") || exit "$?"
70
- IFS=$'\t' read -r model vendor effort role <<EOF
70
+ IFS=$'\t' read -r model harness effort role <<EOF
71
71
  $(python3 -c 'import json,sys
72
72
  p=json.load(sys.stdin)
73
73
  s=p.get("settings") or {}
74
74
  roles=",".join(p.get("roles") or [])
75
- print("\t".join((s.get("model") or "", s.get("vendor") or "", s.get("effort") or "", roles)))' <<<"$placement")
75
+ print("\t".join((s.get("model") or "", s.get("harness") or "", s.get("effort") or "", roles)))' <<<"$placement")
76
76
  EOF
77
- echo "SEAT_PLACEMENT: ${role} → ${vendor} / ${model}${effort:+ / $effort}"
77
+ echo "SEAT_PLACEMENT: ${role} → ${harness} / ${model}${effort:+ / $effort}"
78
78
  python3 -c 'import json,sys
79
79
  p=json.load(sys.stdin)
80
80
  for item in p.get("dropped") or []:
81
81
  print("SEAT_PLACEMENT_DROPPED: rank %s %s %s" % (item.get("rank"), item.get("reason"), item.get("cell") or ""), file=sys.stderr)
82
82
  ' <<<"$placement" || true
83
- case "$vendor" in
83
+ case "$harness" in
84
84
  claude|codex|grok) ;;
85
- *) echo "SEAT_LAUNCH_VENDOR_UNSUPPORTED: vendor=${vendor}" >&2; exit 1 ;;
85
+ *) echo "SEAT_LAUNCH_HARNESS_UNSUPPORTED: harness=${harness}" >&2; exit 1 ;;
86
86
  esac
87
- if [ "$vendor" = "claude" ] && [ -n "$effort" ]; then
87
+ if [ "$harness" = "claude" ] && [ -n "$effort" ]; then
88
88
  case "$effort" in
89
89
  low|medium|high|xhigh|max) ;;
90
90
  *) echo "unknown effort: ${effort}(claude は low|medium|high|xhigh|max)" >&2; exit 1 ;;
@@ -221,7 +221,7 @@ fi
221
221
  # 席を畳む前に測るのが要点である——ここで落ちれば、動いている席を殺さずに済む。
222
222
  export PATH="${HOME}/.grok/bin:${PATH}"
223
223
  preflight_dir="${TMPDIR:-/tmp}"
224
- case "$vendor" in
224
+ case "$harness" in
225
225
  claude) preflight_cmd=(claude --model "$model" -p "ping") ;;
226
226
  codex) preflight_cmd=(codex exec --model "$model" --skip-git-repo-check "ping") ;;
227
227
  grok)
@@ -245,7 +245,7 @@ case "$vendor" in
245
245
  echo "grok preflight: GROK_HOME=${grok_home} ${grok_bin} --model ${model} --reasoning-effort ${effort} -p ping"
246
246
  preflight_cmd=(env GROK_HOME="$grok_home" "$grok_bin" --model "$model" --reasoning-effort "$effort" -p "ping")
247
247
  ;;
248
- *) echo "unknown vendor: ${vendor}(claude / codex / grok)" >&2; exit 1 ;;
248
+ *) echo "unknown harness: ${harness}(claude / codex / grok)" >&2; exit 1 ;;
249
249
  esac
250
250
  preflight_log=$(mktemp "${TMPDIR:-/tmp}/peertable-preflight.XXXXXX")
251
251
  ( cd "$preflight_dir" && "${preflight_cmd[@]}" >"$preflight_log" 2>&1 </dev/null ) &
@@ -264,14 +264,14 @@ while [ $SECONDS -lt $preflight_deadline ]; do
264
264
  done
265
265
  if [ -z "$preflight_rc" ]; then
266
266
  kill "$preflight_pid" 2>/dev/null || true
267
- echo "model preflight が 120 秒で返らない: ${vendor} / ${model}(席は立てない)" >&2
267
+ echo "model preflight が 120 秒で返らない: ${harness} / ${model}(席は立てない)" >&2
268
268
  echo "preflight log: ${preflight_log}" >&2
269
269
  cat "$preflight_log" >&2 || true
270
270
  [ -n "${grok_home:-}" ] && rm -rf "$grok_home"
271
271
  exit 1
272
272
  fi
273
273
  if [ "$preflight_rc" != 0 ]; then
274
- echo "model が live で使えない: ${vendor} / ${model}(preflight rc=${preflight_rc}・席は立てない)" >&2
274
+ echo "model が live で使えない: ${harness} / ${model}(preflight rc=${preflight_rc}・席は立てない)" >&2
275
275
  cat "$preflight_log" >&2 || true
276
276
  rm -f "$preflight_log"
277
277
  [ -n "${grok_home:-}" ] && rm -rf "$grok_home"
@@ -338,7 +338,7 @@ fi
338
338
 
339
339
  # leave-seat が席専用 GROK_HOME を消す。同名再着席では preflight 後に空になるので、
340
340
  # live 起動の直前に auth と ui だけを書き直す(user booth MCP は載せない)。
341
- if [ "$vendor" = grok ]; then
341
+ if [ "$harness" = grok ]; then
342
342
  mkdir -p "$grok_home"
343
343
  if [ ! -f "${HOME}/.grok/auth.json" ]; then
344
344
  echo "SEAT_GROK_AUTH_MISSING: ${HOME}/.grok/auth.json が無い(席は立てない)" >&2
@@ -359,7 +359,8 @@ launch_env=(
359
359
  "PEERTABLE_ROOM=$room"
360
360
  "PEERTABLE_MEMBER=$name"
361
361
  "PEERTABLE_CREDENTIAL_FILE=$credential_file"
362
- "PEERTABLE_VENDOR=$vendor"
362
+ "PEERTABLE_HARNESS=$harness"
363
+ "PEERTABLE_VENDOR=$harness" # 旧版 room client 互換
363
364
  "PEERTABLE_MODEL=$model"
364
365
  "PEERTABLE_EFFORT=$effort"
365
366
  "PEERTABLE_ROLE=$role"
@@ -376,13 +377,13 @@ if [ "$mode" = "lattice" ]; then
376
377
  )
377
378
  [ -n "$lattice_cli" ] && launch_env+=("LATTICE_CLI=$lattice_cli")
378
379
  fi
379
- if [ "$vendor" = grok ]; then
380
+ if [ "$harness" = grok ]; then
380
381
  # 実測 2026-08-21: auth の coding_data_retention_opt_out だけでは
381
382
  # `Help improve Grok [Opt out] [Opt in]` バナーが残る。席が死んだように見える。
382
383
  launch_env+=("GROK_HOME=$grok_home")
383
384
  launch_env+=("GROK_PRIVACY_NOTICE_ROLLOUT=0")
384
385
  fi
385
- if [ "$vendor" = codex ]; then
386
+ if [ "$harness" = codex ]; then
386
387
  codex_home="${proj}/.team/seats/${name}.codex"
387
388
  mkdir -p "$codex_home"
388
389
  if [ -f "${HOME}/.codex/auth.json" ]; then
@@ -391,7 +392,7 @@ if [ "$vendor" = codex ]; then
391
392
  fi
392
393
  launch_env+=("CODEX_HOME=$codex_home")
393
394
  fi
394
- if [ "$vendor" = codex ] \
395
+ if [ "$harness" = codex ] \
395
396
  && ! env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$codex_room_mcp_helper" ensure "$proj" "$peertable_repo"; then
396
397
  echo "SEAT_CODEX_ROOM_MCP_INVALID: Codexのproject設定へseat固有room clientを装備できない(席は立てない)" >&2
397
398
  exit 1
@@ -399,7 +400,7 @@ fi
399
400
  # Aiterm launcherはsession作成後にtyped failureを返すことがある。ここから先は同名memberが
400
401
  # 無いことを確認済みなので、launch試行が作ったtmux/memberをon_exitの対象として先に所有する。
401
402
  seat_created=true
402
- launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$vendor" "$model" "$effort" "$proj" "$brief") || {
403
+ launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$harness" "$model" "$effort" "$proj" "$brief") || {
403
404
  echo "SEAT_AITERM_LAUNCH_FAILED: ${name} をAiterm公開launcherで起動できない(direct CLI fallbackなし)" >&2
404
405
  exit 1
405
406
  }
@@ -449,7 +450,7 @@ seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
449
450
  # Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
450
451
  # ヘッダ/trust dialog待機をここへ重ねると、brief turnでヘッダが画面外へ流れた正常席をrollbackする。
451
452
  # 必須room MCPの成立は、次の member登録readbackだけで判定する。
452
- echo "launched: ${sess}(${vendor} / ${model}${effort:+ / $effort} / room=${room} / mode=${mode})"
453
+ echo "launched: ${sess}(${harness} / ${model}${effort:+ / $effort} / room=${room} / mode=${mode})"
453
454
 
454
455
  # CodexのヘッダはCLIが起動した証拠であって、必須room MCPが初期化された証拠ではない。
455
456
  # 無関係MCPのwarningが画面へ出ても、room clientがmember登録まで到達した席だけを着席として扱う。
@@ -484,7 +485,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
484
485
  # Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
485
486
  # Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
486
487
  # 汎用的に承認するfallbackにはしない。承認後のMCP初期化時間は改めて30秒確保する。
487
- if [ "$vendor" = codex ]; then
488
+ if [ "$harness" = codex ]; then
488
489
  codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
489
490
  if pass_codex_pane "$codex_screen"; then
490
491
  room_ready_deadline=$((SECONDS + 90))
@@ -508,7 +509,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
508
509
  esac
509
510
  fi
510
511
  fi
511
- if [ "$vendor" = grok ] && [ "$grok_trust_accepted" != true ]; then
512
+ if [ "$harness" = grok ] && [ "$grok_trust_accepted" != true ]; then
512
513
  grok_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
513
514
  case "$grok_screen" in
514
515
  *"Do you trust the contents of this directory?"*)
@@ -524,7 +525,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
524
525
  fi
525
526
  # 未信頼ディレクトリでは room MCP 同意より前に workspace trust が出る。既定選択肢
526
527
  # (1. Yes, I trust this folder)を Enter で通す。この既知文言だけを一度通す。
527
- if [ "$vendor" = claude ] && [ "$claude_trust_accepted" != true ]; then
528
+ if [ "$harness" = claude ] && [ "$claude_trust_accepted" != true ]; then
528
529
  claude_trust_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
529
530
  case "$claude_trust_screen" in
530
531
  *"Is this a project you created or one you trust"*|*"trust this folder"*)
@@ -540,7 +541,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
540
541
  fi
541
542
  # aiterm claude_agent は --dangerously-skip-permissions を付けない。project の room MCP
542
543
  # 同意が member 登録より前に出る。選択肢2(this and all future)だけを通す。
543
- if [ "$vendor" = claude ] && [ "$mcp_consent_accepted" != true ]; then
544
+ if [ "$harness" = claude ] && [ "$mcp_consent_accepted" != true ]; then
544
545
  claude_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
545
546
  case "$claude_screen" in
546
547
  *"New MCP server found in this project: room"*)
@@ -556,7 +557,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
556
557
  fi
557
558
  room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
558
559
  if printf '%s' "$room_members" | python3 -c 'import json,sys; name=sys.argv[1]; members=json.load(sys.stdin).get("members",[]); raise SystemExit(0 if any(m.get("name") == name for m in members) else 1)' "$name"; then
559
- if [ "$vendor" = codex ]; then
560
+ if [ "$harness" = codex ]; then
560
561
  codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
561
562
  if ! codex_pane_blocks_ready "$codex_screen"; then
562
563
  sleep 1
@@ -575,7 +576,7 @@ if [ "$room_ready" != true ]; then
575
576
  exit 1
576
577
  fi
577
578
  echo "room ready: ${room}/${name}"
578
- if [ "$vendor" = codex ]; then
579
+ if [ "$harness" = codex ]; then
579
580
  # member 登録後の最初の tool 呼び出しで MCP Allow が出る。出ている間だけ通し、沈黙5秒で抜ける。
580
581
  codex_post_ready_deadline=$((SECONDS + 90))
581
582
  codex_post_ready_idle=0
@@ -607,7 +608,7 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
607
608
  brief_ready=false
608
609
  while [ $SECONDS -lt "$brief_ready_deadline" ]; do
609
610
  brief_ready_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
610
- if [ "$vendor" != codex ] || printf '%s\n' "$brief_ready_screen" | python3 -c 'import sys; lines=sys.stdin.read().splitlines()[-24:]; has_prompt=any(line.strip() == "›" or line.lstrip().startswith("› ") for line in lines); has_footer=any("gpt-" in line and "·" in line for line in lines); raise SystemExit(0 if has_prompt and has_footer else 1)'; then
611
+ if [ "$harness" != codex ] || printf '%s\n' "$brief_ready_screen" | python3 -c 'import sys; lines=sys.stdin.read().splitlines()[-24:]; has_prompt=any(line.strip() == "›" or line.lstrip().startswith("› ") for line in lines); has_footer=any("gpt-" in line and "·" in line for line in lines); raise SystemExit(0 if has_prompt and has_footer else 1)'; then
611
612
  brief_ready_streak=$((brief_ready_streak + 1))
612
613
  if [ "$brief_ready_streak" -ge 3 ]; then
613
614
  brief_ready=true
@@ -716,7 +717,7 @@ PY
716
717
  fi
717
718
  fi
718
719
 
719
- # 素性(vendor/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
720
+ # 素性(harness/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
720
721
  # ランチャーはここで台帳を読み返し、実際に載ったかを確認するだけにする(重複書込の禁止)。
721
722
  member_row=$(curl -sf "$url/api/$room/members/$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$name")" || true)
722
723
  stored=$(python3 - "$member_row" <<'PY'
@@ -730,7 +731,7 @@ print('yes' if m.get('model') and m.get('roles') and m.get('aiterm_session_id')
730
731
  PY
731
732
  )
732
733
  if [ "$stored" = yes ]; then
733
- echo "台帳確認: ${vendor} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
734
+ echo "台帳確認: ${harness} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
734
735
  else
735
736
  echo "SEAT_LEDGER_INCOMPLETE: 台帳の member 行に素性または本人性が欠けている(席は着席済み。doctor で確認)" >&2
736
737
  fi
@@ -744,7 +745,7 @@ fi
744
745
 
745
746
  # Claude 席の新着は room client の notifications/claude/channel。TUI 配達は channels を持たない
746
747
  # Codex / Grok 席だけ。Claude に立てると channel と二重配達になる。
747
- if [ "$vendor" = codex ] || [ "$vendor" = grok ]; then
748
+ if [ "$harness" = codex ] || [ "$harness" = grok ]; then
748
749
  if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" wakeup; then
749
750
  echo "wakeup-bridge: TUI配達 起動確認済み"
750
751
  else
@@ -1,15 +1,15 @@
1
1
  #!/bin/bash
2
2
  # 親(ベル等)が room へ着卓する。
3
- # usage: parent-join.sh <project_dir> [name] [model] [effort] [vendor]
3
+ # usage: parent-join.sh <project_dir> [name] [model] [effort] [harness]
4
4
  # name 既定は bell。broadcast廃止に伴いkickoff投稿は行わない。
5
- # model / effort / vendor は任意。親はオーナーの対話セッション(決定40)なので、席と違って
5
+ # model / effort / harness は任意。親はオーナーの対話セッション(決定40)なので、席と違って
6
6
  # 起動時に確定した値を script が知らない——**渡された時だけ**参加者一覧の素性として登録する。
7
7
  # 渡さなければ欄ごと出ない(「不明」ではなく「素性を名乗っていない」)。
8
- # vendor は claude(既定)、codex、または grok。model だけ渡して vendor を渡さない場合は
8
+ # harness は claude(既定)、codex、または grok。model だけ渡して harness を渡さない場合は
9
9
  # claude とみなす(後方互換)。
10
10
  # 親は MCP を後付けできないので room へは HTTP API 直で入る(決定40 の operating notes)。
11
11
  set -e
12
- proj="$1"; name="${2:-bell}"; model="$3"; effort="$4"; vendor="$5"
12
+ proj="$1"; name="${2:-bell}"; model="$3"; effort="$4"; harness="$5"
13
13
  mission=""
14
14
  if [ "${6:-}" = "--mission" ]; then mission="${7:-}"; fi
15
15
  state="$proj/.team/setup-state.json"
@@ -23,21 +23,22 @@ fi
23
23
 
24
24
  # 親はAiterm席ではない。Claude/Codex/Grokとも、親自身が所有するparent-watchを配送先にする。
25
25
  # tmux observeやCodex thread IDを登録すると、通常席bridge/外部resumeへ誤配送される。
26
- parent_vendor="$vendor"
27
- if [ -z "$parent_vendor" ] && [ -n "$model" ]; then
26
+ parent_harness="$harness"
27
+ if [ -z "$parent_harness" ] && [ -n "$model" ]; then
28
28
  case "$model" in
29
- gpt-*|o[0-9]*) parent_vendor=codex ;;
30
- grok*) parent_vendor=grok ;;
31
- claude*|opus*|sonnet*|haiku*|fable*) parent_vendor=claude ;;
29
+ gpt-*|o[0-9]*) parent_harness=codex ;;
30
+ grok*) parent_harness=grok ;;
31
+ claude*|opus*|sonnet*|haiku*|fable*) parent_harness=claude ;;
32
32
  esac
33
33
  fi
34
- member=$(python3 - "$name" "$model" "$effort" "$parent_vendor" "$mission" <<'PY'
34
+ member=$(python3 - "$name" "$model" "$effort" "$parent_harness" "$mission" <<'PY'
35
35
  import json, sys
36
- name, model, effort, vendor, mission = sys.argv[1:6]
36
+ name, model, effort, harness, mission = sys.argv[1:6]
37
37
  # 台帳には canonical 欄だけを登録する(settings / role 単数の重複欄は 2026-08-22 廃止)
38
38
  body = {'name': name, 'roles': ['統括']}
39
- if vendor:
40
- body['vendor'] = vendor
39
+ if harness:
40
+ body['harness'] = harness
41
+ body['vendor'] = harness # 旧版 room server 互換
41
42
  if model:
42
43
  body['model'] = model
43
44
  if effort:
@@ -45,7 +46,7 @@ if effort:
45
46
  if mission:
46
47
  body['mission'] = mission
47
48
  body['observe'] = None
48
- body['delivery'] = {'kind': 'parent_watch', 'host': vendor or ''}
49
+ body['delivery'] = {'kind': 'parent_watch', 'host': harness or ''}
49
50
  print(json.dumps(body, ensure_ascii=False))
50
51
  PY
51
52
  )
@@ -75,11 +76,11 @@ echo "親の権限境界: 各ToDoのクローズは監査担当が行い、親
75
76
 
76
77
  # parent-watchが初回headを固定する。以後のDMはwatcher不在時間を含め
77
78
  # 永続cursorからcatch-upされる。host内のbackground task自体は親セッションだけが所有できる。
78
- if PEERTABLE_PARENT_HOST="$parent_vendor" node "$here/parent-watch.mjs" "$proj" "$name" --prime; then
79
- echo "parent-watch cursor ready: ${name}(host=${parent_vendor})"
80
- if [ "$parent_vendor" = "codex" ]; then
79
+ if PEERTABLE_PARENT_HOST="$parent_harness" node "$here/parent-watch.mjs" "$proj" "$name" --prime; then
80
+ echo "parent-watch cursor ready: ${name}(host=${parent_harness})"
81
+ if [ "$parent_harness" = "codex" ]; then
81
82
  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は常駐させない"
82
- elif [ "$parent_vendor" = "grok" ]; then
83
+ elif [ "$parent_harness" = "grok" ]; then
83
84
  echo "PARENT_WATCH_START_REQUIRED: Grok Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること。通常席用wakeup-bridgeに親を載せない"
84
85
  else
85
86
  echo "PARENT_WATCH_START_REQUIRED: Claude Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // 02_models.md の役割→1位〜3位から、着席可能な vendor / model / effort を解決する。
2
+ // 02_models.md の役割→1位〜3位から、着席可能な harness / model / effort を解決する。
3
3
  // 具体モデル名はここに持たない。台帳と順位表をその場で読む。
4
4
  import { existsSync, readFileSync } from 'node:fs'
5
5
  import { dirname, join, resolve } from 'node:path'
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'
7
7
 
8
8
  const here = dirname(fileURLToPath(import.meta.url))
9
9
  const EFFORT = /×\s*(none|low|medium|high|xhigh|max|ultra)/iu
10
- const vendorOf = (modelName) => {
10
+ const harnessOf = (modelName) => {
11
11
  const name = String(modelName).trim()
12
12
  if (/^claude\b/iu.test(name)) return 'claude'
13
13
  if (/^gpt\b|^gpt-/iu.test(name)) return 'codex'
@@ -62,9 +62,9 @@ export function parseLedger(markdown) {
62
62
  return rows.flatMap((row) => {
63
63
  const name = row['モデル']
64
64
  const slug = slugOf(row['slug'] ?? '')
65
- const vendor = vendorOf(name)
66
- if (!name || !slug || !vendor) return []
67
- return [{ name, slug, vendor }]
65
+ const harness = harnessOf(name)
66
+ if (!name || !slug || !harness) return []
67
+ return [{ name, slug, harness }]
68
68
  })
69
69
  }
70
70
 
@@ -103,7 +103,7 @@ export function listOfficialRoles(markdown) {
103
103
  .filter(Boolean)
104
104
  }
105
105
 
106
- export function vendorFromSlug(slug) {
106
+ export function harnessFromSlug(slug) {
107
107
  const text = String(slug ?? '').trim().toLowerCase()
108
108
  if (!text) return null
109
109
  if (/^(gpt-|gpt\b|o[0-9])/u.test(text)) return 'codex'
@@ -125,8 +125,8 @@ const parseRoleList = (roles) => {
125
125
  return list
126
126
  }
127
127
 
128
- export function resolveSeatPlacement(role, markdown, { source = '', vendor = '' } = {}) {
129
- const wantVendor = String(vendor ?? '').trim()
128
+ export function resolveSeatPlacement(role, markdown, { source = '', harness = '' } = {}) {
129
+ const wantHarness = String(harness ?? '').trim()
130
130
  const wanted = String(role ?? '').trim()
131
131
  if (!wanted) {
132
132
  return { error: 'SEAT_ROLE_REQUIRED', message: 'role が空(02_models の役割名が要る)' }
@@ -151,12 +151,12 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
151
151
  dropped.push({ rank, reason: 'not-in-ledger', cell: parsed.cell })
152
152
  continue
153
153
  }
154
- if (wantVendor && hit.vendor !== wantVendor) {
155
- dropped.push({ rank, reason: `vendor-mismatch(want ${wantVendor})`, cell: parsed.cell })
154
+ if (wantHarness && hit.harness !== wantHarness) {
155
+ dropped.push({ rank, reason: `harness-mismatch(want ${wantHarness})`, cell: parsed.cell })
156
156
  continue
157
157
  }
158
158
  if (hit.slug === 'haiku') {
159
- return { role: wanted, rank, vendor: hit.vendor, model: hit.slug, effort: '', source, dropped }
159
+ return { role: wanted, rank, harness: hit.harness, model: hit.slug, effort: '', source, dropped }
160
160
  }
161
161
  if (!parsed.effort) {
162
162
  dropped.push({ rank, reason: 'effort-missing', cell: parsed.cell })
@@ -165,7 +165,7 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
165
165
  return {
166
166
  role: wanted,
167
167
  rank,
168
- vendor: hit.vendor,
168
+ harness: hit.harness,
169
169
  model: hit.slug,
170
170
  effort: parsed.effort,
171
171
  source,
@@ -174,13 +174,13 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
174
174
  }
175
175
  return {
176
176
  error: 'SEAT_PLACEMENT_UNRESOLVABLE',
177
- message: `${wanted}${wantVendor ? `(vendor=${wantVendor})` : ''} を着席可能な vendor/model/effort へ解決できない`,
177
+ message: `${wanted}${wantHarness ? `(harness=${wantHarness})` : ''} を着席可能な harness/model/effort へ解決できない`,
178
178
  dropped,
179
179
  }
180
180
  }
181
181
 
182
182
  export function resolveSeatIdentity({
183
- roles, model, effort, vendor, markdown, source = '', allowParentRole = false,
183
+ roles, model, effort, harness, markdown, source = '', allowParentRole = false,
184
184
  } = {}) {
185
185
  const roleList = parseRoleList(roles)
186
186
  if (roleList.length === 0) {
@@ -204,7 +204,7 @@ export function resolveSeatIdentity({
204
204
  return {
205
205
  roles: roleList,
206
206
  settings: {
207
- vendor: String(vendor ?? '').trim(),
207
+ harness: String(harness ?? '').trim(),
208
208
  model: '',
209
209
  effort: String(effort ?? '').trim(),
210
210
  },
@@ -214,29 +214,29 @@ export function resolveSeatIdentity({
214
214
 
215
215
  const requestedModel = String(model ?? '').trim()
216
216
  const requestedEffort = String(effort ?? '').trim()
217
- const requestedVendor = String(vendor ?? '').trim()
217
+ const requestedHarness = String(harness ?? '').trim()
218
218
  if (requestedModel) {
219
- const resolvedVendor = requestedVendor || vendorFromSlug(requestedModel)
220
- || parseLedger(markdown).find((row) => row.slug === requestedModel)?.vendor
221
- if (!resolvedVendor) {
219
+ const resolvedHarness = requestedHarness || harnessFromSlug(requestedModel)
220
+ || parseLedger(markdown).find((row) => row.slug === requestedModel)?.harness
221
+ if (!resolvedHarness) {
222
222
  return {
223
- error: 'SEAT_VENDOR_UNRESOLVED',
224
- message: `model=${requestedModel} の vendor を推定できない(--vendor が要る)`,
223
+ error: 'SEAT_HARNESS_UNRESOLVED',
224
+ message: `model=${requestedModel} の harness を推定できない(--harness が要る)`,
225
225
  }
226
226
  }
227
227
  return {
228
228
  roles: roleList,
229
- settings: { vendor: resolvedVendor, model: requestedModel, effort: requestedEffort },
229
+ settings: { harness: resolvedHarness, model: requestedModel, effort: requestedEffort },
230
230
  source,
231
231
  }
232
232
  }
233
233
 
234
- const first = resolveSeatPlacement(roleList[0], markdown, { source, vendor: requestedVendor })
234
+ const first = resolveSeatPlacement(roleList[0], markdown, { source, harness: requestedHarness })
235
235
  if (first.error) return first
236
236
  return {
237
237
  roles: roleList,
238
238
  settings: {
239
- vendor: first.vendor,
239
+ harness: first.harness,
240
240
  model: first.model,
241
241
  effort: requestedEffort || first.effort || '',
242
242
  },
@@ -251,14 +251,15 @@ if (isMain) {
251
251
  let roles = ''
252
252
  let model = ''
253
253
  let effort = ''
254
- let vendor = ''
254
+ let harness = ''
255
255
  const positional = []
256
256
  for (let i = 0; i < args.length; i += 1) {
257
257
  const arg = args[i]
258
258
  if (arg === '--roles') { roles = args[++i] ?? ''; continue }
259
259
  if (arg === '--model') { model = args[++i] ?? ''; continue }
260
260
  if (arg === '--effort') { effort = args[++i] ?? ''; continue }
261
- if (arg === '--vendor') { vendor = args[++i] ?? ''; continue }
261
+ // --vendor は旧名の互換 alias
262
+ if (arg === '--harness' || arg === '--vendor') { harness = args[++i] ?? ''; continue }
262
263
  positional.push(arg)
263
264
  }
264
265
  if (!roles) roles = positional[0] ?? ''
@@ -270,7 +271,7 @@ if (isMain) {
270
271
  fail('SEAT_MODELS_DOC_MISSING', `02_models.md が無い: ${doc}`)
271
272
  }
272
273
  const result = resolveSeatIdentity({
273
- roles, model, effort, vendor, markdown: readFileSync(doc, 'utf8'), source: doc,
274
+ roles, model, effort, harness, markdown: readFileSync(doc, 'utf8'), source: doc,
274
275
  })
275
276
  if (result.error) fail(result.error, result.message)
276
277
  process.stdout.write(`${JSON.stringify(result)}\n`)
@@ -268,7 +268,7 @@ export function isPaneProcessStopped(panePid, exec = execFileSync) {
268
268
 
269
269
  /**
270
270
  * paneのstatus行が公開しているtoken値だけを読む。
271
- * vendor固有のログや課金単価は推測せず、表示が無い席はnullのままにする。
271
+ * harness固有のログや課金単価は推測せず、表示が無い席はnullのままにする。
272
272
  */
273
273
  export function parsePaneTokenHint(pane) {
274
274
  if (typeof pane !== 'string') return null
@@ -18,7 +18,7 @@ import { join } from 'node:path'
18
18
  import { promisify } from 'node:util'
19
19
  import { resolveSeatObservation, tmuxArgv } from './seat-usage.mjs'
20
20
  import { keysForCodexPane } from './codex-dialog.mjs'
21
- import { BROADCAST_RECIPIENT, formatWakeNotice, isIdleSelfWake, isWakeupBridgeTarget, shouldDeferGrokWake } from './wakeup-delivery.mjs'
21
+ import { BROADCAST_RECIPIENT, formatWakeNotice, isIdleSelfWake, isWakeupBridgeTarget, memberHarness, shouldDeferGrokWake } from './wakeup-delivery.mjs'
22
22
  import { bridgeRecordLive } from './bridge-record-live.mjs'
23
23
 
24
24
  const run = promisify(execFile)
@@ -136,7 +136,7 @@ process.on('SIGTERM', cleanup)
136
136
  process.on('SIGINT', cleanup)
137
137
 
138
138
  // 席ごとに未配達を溜めて、2秒ごとにまとめて1回起こす(連投で席を何度も起こさない)。
139
- // pending は room message の宛先名から作る。起動引数や vendor は候補集合を決めない。
139
+ // pending は room message の宛先名から作る。起動引数や harness は候補集合を決めない。
140
140
  const pending = new Map() // name -> Map<seq, message>
141
141
  const deliveryStates = new Map() // seq -> { message, targets, delivered }
142
142
  let seats = []
@@ -310,7 +310,7 @@ async function wake(seat, msgs) {
310
310
  }
311
311
  const pane = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
312
312
  const screen = String(pane.stdout ?? '')
313
- if (member.vendor === 'codex') {
313
+ if (memberHarness(member) === 'codex') {
314
314
  const dialog = keysForCodexPane(screen)
315
315
  if (dialog) {
316
316
  for (const key of dialog.keys) {
@@ -320,9 +320,9 @@ async function wake(seat, msgs) {
320
320
  return 'deferred'
321
321
  }
322
322
  }
323
- if (member.vendor === 'grok') {
323
+ if (memberHarness(member) === 'grok') {
324
324
  const tail = screen.split('\n').slice(-14).join('\n')
325
- if (shouldDeferGrokWake(member.vendor, tail)) {
325
+ if (shouldDeferGrokWake(memberHarness(member), tail)) {
326
326
  if (!deferredBusy.has(seat)) {
327
327
  log(`Grok席が実行中なのでidleまで待つ: ${seat} ← ${msgs.length} 件`)
328
328
  deferredBusy.add(seat)
@@ -352,7 +352,7 @@ async function wake(seat, msgs) {
352
352
  // ターンを中断**していた(実被弾 2026-08-22: 監査席が2秒ごとに中断され、監査が進まなかった)。
353
353
  // Claude 席は「受理された兆候」を成功とみなし、回復キーに Escape を使わない。
354
354
  const CLAUDE_ACCEPTED = /paste again to expand|\[Pasted text|esc to interrupt|✻|✽|✳|·\s*\w+ing…/u
355
- const isClaude = member.vendor === 'claude'
355
+ const isClaude = memberHarness(member) === 'claude'
356
356
  for (let attempt = 0; attempt < 3; attempt++) {
357
357
  await sleep(1200)
358
358
  const after = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
@@ -4,6 +4,11 @@ export const BROADCAST_RECIPIENT = 'all'
4
4
  export const ROOM_UPDATE_FALLBACK =
5
5
  'room全体の状況が更新された。room.read_logで部屋を読み、状況を把握して次の行動を判断する。'
6
6
 
7
+ /** member素性の正本はharness。旧server応答(vendorだけ)も受ける。 */
8
+ export function memberHarness(member) {
9
+ return member?.harness ?? member?.vendor
10
+ }
11
+
7
12
  export function collapseWakeBody(body) {
8
13
  return String(body ?? '').replace(/\s*\n+\s*/gu, ' / ')
9
14
  }
@@ -36,8 +41,8 @@ export function isWakeupBridgeTarget(member, options = {}) {
36
41
  && observe.tmux_target,
37
42
  )
38
43
  if (hasPane) return true
39
- const vendor = member.vendor
40
- return vendor === 'codex' || vendor === 'grok'
44
+ const harness = memberHarness(member)
45
+ return harness === 'codex' || harness === 'grok'
41
46
  }
42
47
 
43
48
  /**
@@ -62,8 +67,8 @@ export function isIdleSelfWake(msg) {
62
67
  }
63
68
 
64
69
  /** Grok 既定はキュー投入。busy 中に積むと今のターンへ混ざらない。 */
65
- export function shouldDeferGrokWake(vendor, tail) {
66
- if (vendor !== 'grok') return false
70
+ export function shouldDeferGrokWake(harness, tail) {
71
+ if (harness !== 'grok') return false
67
72
  if (typeof tail !== 'string') return false
68
73
  if (classifyPaneTail(tail) === 'busy') return true
69
74
  if (tail.includes('send a message to interrupt')) return true
@@ -34,14 +34,14 @@
34
34
 
35
35
  ## 着卓手順
36
36
 
37
- vendor に関わらず: `scripts/parent-join.sh <project> [name] [model] [effort] [vendor]` で member
38
- 登録する。`vendor` は `claude`(既定)、`codex`、または `grok`。Lattice 併用モードなら、
37
+ harness に関わらず: `scripts/parent-join.sh <project> [name] [model] [effort] [harness]` で member
38
+ 登録する。`harness` は `claude`(既定)、`codex`、または `grok`。Lattice 併用モードなら、
39
39
  `source .team/parent-env.sh` で Lattice mutation(`todo reopen` 等)に要る actor 環境変数
40
40
  (`LATTICE_TODO_ACTOR_HOST`/`SESSION`/`AGENT`)を親 shell へ持続配線する——**子 process の
41
41
  export は親 shell に伝播しないため**、これをしないまま `lattice todo reopen` 等を打つと
42
42
  `ACTOR_UNRESOLVED`(`missing_environment=[...]`)で無変更停止する(実測: owner裁定[46]④)。
43
43
 
44
- ## 新着の検知(room追従は共通、親への通知だけvendor別)
44
+ ## 新着の検知(room追従は共通、親への通知だけharness別)
45
45
 
46
46
  `scripts/parent-watch.mjs <project> <親名>` がroom SSE、heartbeat、再接続catch-up、宛先判定、
47
47
  永続cursorを一括所有する。stdoutへ出る`peertable.parent-watch-event.v1`はDM本文そのものであり、
@@ -129,7 +129,7 @@ peertable_parent_post() {
129
129
  3. 工程正本で照合する(Lattice 併用: `lattice todo status --json`。単独: `.team/tasks.md` と
130
130
  room ログの突き合わせ)。食い違ったら工程正本が正で、食い違い自体を room へ出す
131
131
  4. member 登録は残っているので `parent-join.sh` を再実行しない。名前を確認するだけでよい
132
- 5. vendorに応じて番犬を張り直す。ClaudeとGrokは旧Monitorを止めて`--follow`を1回起動する。Codexは旧background
132
+ 5. harnessに応じて番犬を張り直す。ClaudeとGrokは旧Monitorを止めて`--follow`を1回起動する。Codexは旧background
133
133
  taskを止め、1秒ごとの`--poll` loopを起動する。永続cursorが不在時間のDMをcatch-upする
134
134
  6. 順序の要点は「room と工程正本を読み終えるまで発言しない」
135
135