peertable 0.5.0 → 0.6.0

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.0",
3
+ "version": "0.6.0",
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.0'
16
+ const MCP_VERSION = '0.6.0'
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,12 +204,14 @@ 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,
211
214
  mission: process.env.PEERTABLE_MISSION,
212
- aiterm_session_id: process.env.AITERM_SESSION_ID,
213
215
  observe,
214
216
  }).filter(([, v]) => v != null && v !== '' && !(Array.isArray(v) && v.length === 0)))
215
217
  {
@@ -221,7 +223,7 @@ const IDENTITY = Object.fromEntries(Object.entries({
221
223
  roles,
222
224
  model: process.env.PEERTABLE_MODEL,
223
225
  effort: process.env.PEERTABLE_EFFORT,
224
- vendor: process.env.PEERTABLE_VENDOR,
226
+ harness: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
225
227
  markdown: readFileSync(modelsDoc, 'utf8'),
226
228
  })
227
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))
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // 目覚まし係(2026-08-22 オーナー設計)。AI ではない常駐スクリプト。
3
+ //
4
+ // 席は「待機からの解放条件を判定するスクリプト」を登録してから寝る(alarm-set.sh)。
5
+ // 本常駐が高頻度でそのスクリプトを回し、exit 0 になったら登録席へ
6
+ // 「[待機解放条件成立] <note>」の DM を room へ置く(配達は wakeup-bridge の正規経路)。
7
+ // 条件の約束が席の揮発文脈でなく装置に住むので、依頼相手の死・レース負け・宛先不在・
8
+ // 外部/時刻条件・相互待機のどれでも待機が永眠しない。
9
+ //
10
+ // usage: alarm-bridge.mjs <project_dir> 起動(nohup で常駐)
11
+ // alarm-bridge.mjs <project_dir> --stop 停止
12
+ //
13
+ // 登録: .team/alarms/<id>.json
14
+ // { "seat": "ren", "note": "転写job j42 完了", "script": "test -f ~/asr/jobs/j42/result.json",
15
+ // "interval_s": 10, "created_at": "..." }
16
+ // スクリプトは bash -c で実行される(timeout 30秒)。exit 0 = 条件成立。
17
+ // 成立したら DM を置いて登録 file を削除する。スクリプトのエラーは成立と区別してログへ出す。
18
+ import { execFile } from 'node:child_process'
19
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
20
+ import { join } from 'node:path'
21
+ import { promisify } from 'node:util'
22
+
23
+ import { resolvePostToken } from './seat-usage.mjs'
24
+ import { observePidCommand } from './seat-identity.mjs'
25
+
26
+ const run = promisify(execFile)
27
+ const [proj, flag] = process.argv.slice(2)
28
+ if (!proj) { console.error('usage: alarm-bridge.mjs <project_dir> [--stop]'); process.exit(1) }
29
+
30
+ const team = join(proj, '.team')
31
+ const alarmsDir = join(team, 'alarms')
32
+ const record = join(team, 'alarm-bridge.json')
33
+ const alive = pid => { try { process.kill(pid, 0); return true } catch { return false } }
34
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
35
+ const log = line => console.error(`[${new Date().toISOString()}] ${line}`)
36
+
37
+ // 停止・再認証は run-bridge と同じ作法(pid+lstart 照合。pid 再利用先を殺さない)
38
+ function stopRecorded(strict) {
39
+ if (!existsSync(record)) { if (strict) log('起動記録が無い(既に停止)'); return }
40
+ const stored = JSON.parse(readFileSync(record, 'utf8'))
41
+ let facts = null
42
+ try { facts = observePidCommand(stored.pid) } catch { /* 観測不能=死亡扱い */ }
43
+ const same = facts && stored.start_identity !== undefined
44
+ && facts.started_identity === stored.start_identity && facts.argv.includes('alarm-bridge.mjs')
45
+ if (!same) { unlinkSync(record); log(`死んだ記録を掃除した(pid ${stored.pid})`); return }
46
+ process.kill(stored.pid, 'SIGTERM')
47
+ log(`前の目覚まし係を停止した(pid ${stored.pid})`)
48
+ try { unlinkSync(record) } catch { /* 本人のcleanupと競合してよい */ }
49
+ }
50
+ stopRecorded(flag === '--stop')
51
+ if (flag === '--stop') process.exit(0)
52
+
53
+ const setup = JSON.parse(readFileSync(join(team, 'setup-state.json'), 'utf8'))
54
+ const { room, server_url: url } = setup
55
+ const token = resolvePostToken(process.env)
56
+ if (!token) { console.error('ALARM_BRIDGE_TOKEN_MISSING: 書込トークンが無い'); process.exit(1) }
57
+ mkdirSync(alarmsDir, { recursive: true })
58
+
59
+ const self = observePidCommand(process.pid)
60
+ writeFileSync(record, JSON.stringify({
61
+ pid: process.pid, start_identity: self.started_identity, room, server_url: url,
62
+ started_at: new Date().toISOString(), ready_at: new Date().toISOString(),
63
+ }) + '\n')
64
+ const cleanup = () => { try { unlinkSync(record) } catch {} process.exit(0) }
65
+ process.on('SIGTERM', cleanup)
66
+ process.on('SIGINT', cleanup)
67
+
68
+ async function wake(seat, note) {
69
+ const res = await fetch(`${url}/api/${encodeURIComponent(room)}/messages`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json', 'X-Peertable-Token': token },
72
+ body: JSON.stringify({ from: 'alarm', to: seat, body: `[待機解放条件成立] ${note}` }),
73
+ })
74
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
75
+ }
76
+
77
+ const nextDue = new Map() // file -> epoch_ms
78
+ async function tick() {
79
+ let entries
80
+ try { entries = readdirSync(alarmsDir).filter(f => f.endsWith('.json')) } catch { return }
81
+ for (const entry of entries) {
82
+ const file = join(alarmsDir, entry)
83
+ const due = nextDue.get(file) ?? 0
84
+ if (Date.now() < due) continue
85
+ let reg
86
+ try { reg = JSON.parse(readFileSync(file, 'utf8')) } catch { continue } // 書込途中は次周へ
87
+ if (typeof reg.seat !== 'string' || typeof reg.script !== 'string' || !reg.seat || !reg.script) {
88
+ log(`ALARM_REGISTRATION_INVALID: ${entry}(seat/script が無い)を撤去する`)
89
+ try { unlinkSync(file) } catch {}
90
+ continue
91
+ }
92
+ nextDue.set(file, Date.now() + Math.max(2, Number(reg.interval_s) || 10) * 1000)
93
+ let met = false
94
+ try {
95
+ await run('bash', ['-c', reg.script], { timeout: 30_000, maxBuffer: 1024 * 1024 })
96
+ met = true
97
+ } catch (e) {
98
+ // exit 非0 = 条件未成立(正常)。timeout/spawn 失敗はログへ(成立と区別し、握りつぶさない)
99
+ if (e.killed || e.code === 'ENOENT') log(`条件スクリプトの実行失敗(${entry}): ${e.message}`)
100
+ }
101
+ if (!met) continue
102
+ try {
103
+ await wake(reg.seat, reg.note || entry)
104
+ unlinkSync(file)
105
+ nextDue.delete(file)
106
+ log(`起床: ${reg.seat} ← ${reg.note || entry}`)
107
+ } catch (e) {
108
+ log(`起床の投函に失敗(次周期に再試行): ${e.message}`)
109
+ }
110
+ }
111
+ }
112
+
113
+ log(`alarm-bridge start: room=${room} project=${proj} pid=${process.pid}`)
114
+ for (;;) {
115
+ try { await tick() } catch (e) { log(`tick 失敗(続行): ${e.message}`) }
116
+ await sleep(2000)
117
+ }
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ # 目覚まし係へ待機解放条件を登録する(席が待機に入る前に使う)。
3
+ # usage: alarm-set.sh <project_dir> <seat> <note> <script...>
4
+ # script は bash -c で実行され、exit 0 で条件成立とみなされる。
5
+ # **script はシングルクォートで渡すこと**。ダブルクォートだと `$()` や変数が登録側の
6
+ # shell で先に展開され、判定コマンドでなく展開結果が登録される(かえで実測 2026-08-22)。
7
+ # 成立すると席へ「[待機解放条件成立] <note>」が配達され、登録は自動で消える。
8
+ set -eu
9
+ proj="${1:?project_dir}"; seat="${2:?seat}"; note="${3:?note}"; shift 3
10
+ script="$*"
11
+ [ -n "$script" ] || { echo "ALARM_SET_SCRIPT_REQUIRED: 条件スクリプトが空" >&2; exit 2; }
12
+ dir="$proj/.team/alarms"
13
+ mkdir -p "$dir"
14
+ id="$(date +%s)-$$"
15
+ python3 - "$dir/$id.json" "$seat" "$note" "$script" <<'PY'
16
+ import json, sys
17
+ out, seat, note, script = sys.argv[1:5]
18
+ with open(out, 'w') as f:
19
+ json.dump({'seat': seat, 'note': note, 'script': script, 'interval_s': 10,
20
+ 'created_at': __import__('datetime').datetime.utcnow().isoformat() + 'Z'}, f, ensure_ascii=False)
21
+ PY
22
+ echo "ALARM_SET_OK: ${dir}/${id}.json(seat=${seat} note=${note})"
@@ -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
 
@@ -3,7 +3,7 @@
3
3
  set -euo pipefail
4
4
 
5
5
  proj="$1"; name="$2"; shift 2
6
- case "$name" in seat-status) script="seat-status-bridge.mjs" ;; wakeup) script="wakeup-bridge.mjs" ;; run) echo "ENSURE_BRIDGE_RETIRED: run-bridge は退役した(2026-08-22)。介入は席が自分の Lattice コマンド応答で受け取る" >&2; exit 1 ;; *) echo "usage: ensure-bridge.sh <project> <seat-status|wakeup> [args...]" >&2; exit 1 ;; esac
6
+ case "$name" in seat-status) script="seat-status-bridge.mjs" ;; wakeup) script="wakeup-bridge.mjs" ;; alarm) script="alarm-bridge.mjs" ;; run) echo "ENSURE_BRIDGE_RETIRED: run-bridge は退役した(2026-08-22)。介入は席が自分の Lattice コマンド応答で受け取る" >&2; exit 1 ;; *) echo "usage: ensure-bridge.sh <project> <seat-status|wakeup|alarm> [args...]" >&2; exit 1 ;; esac
7
7
  team="$proj/.team"; record="$team/$name-bridge.json"; log="$team/$name-bridge.log"
8
8
  force=false
9
9
  if [ "${1:-}" = "--force" ]; then force=true; shift; fi
@@ -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) {