peertable 0.4.37 → 0.5.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 +3 -1
- package/README.md +3 -1
- package/package.json +2 -2
- package/room/Dockerfile +2 -1
- package/room/client.mjs +5 -11
- package/room/server.mjs +121 -39
- package/skill/SKILL.md +13 -8
- package/skill/scripts/change-seat.sh +2 -1
- package/skill/scripts/codex-dialog.mjs +46 -0
- package/skill/scripts/codex-seat-toml.mjs +6 -0
- package/skill/scripts/doctor.sh +185 -0
- package/skill/scripts/ensure-bridge.sh +1 -1
- package/skill/scripts/ensure-codex-room-mcp.mjs +4 -1
- package/skill/scripts/launch-seat.sh +137 -120
- package/skill/scripts/leave-seat.sh +1 -4
- package/skill/scripts/parent-join.sh +22 -9
- package/skill/scripts/parent-watch-logic.mjs +33 -0
- package/skill/scripts/parent-watch.mjs +34 -1
- package/skill/scripts/pull-attach-input.mjs +53 -0
- package/skill/scripts/refresh-seat-identity.mjs +97 -0
- package/skill/scripts/seat-identity.mjs +37 -6
- package/skill/scripts/seat-usage.mjs +5 -0
- package/skill/scripts/teardown.sh +8 -6
- package/skill/scripts/wakeup-bridge.mjs +52 -2
- package/skill/scripts/wakeup-delivery.mjs +1 -1
- package/skill/templates/member.md +7 -4
- package/skill/scripts/run-bridge.mjs +0 -272
- package/skill/scripts/seat-metadata.mjs +0 -21
package/README.ja.md
CHANGED
|
@@ -77,7 +77,9 @@ docker compose -f deploy/compose.yaml up -d
|
|
|
77
77
|
|
|
78
78
|
観測先は**席自身が名乗る**(`observe: {tmux_socket, tmux_target}`)。席の起動スクリプトと、席の中で動く MCP クライアントの両方が自分の tmux socket / session を登録するので、**スキル以外の経路で立てた席(aiterm の素の pane など)もそのまま観測対象になる**。表示名から `peer-<名前>` を推測しないので、任意のセッション名で立てた席が消える問題は起きない。名乗っていない古い席だけが従来の推測へ落ちる。常駐は専用 tmux セッションが保持し、**起動側は「起こした」ではなく「最初の観測が届いた」ことを確かめてから成功を返す**(確かめられなければログ末尾を出して非ゼロで落ちる)。
|
|
79
79
|
|
|
80
|
-
API: `GET /api/<room>/messages` / `members` / `summary`(約120バイト・`seq`・`last_ts`・`member_count`)/ `events`(SSE)、`POST /api/<room>/messages` / `members`。
|
|
80
|
+
API: `GET /api/<room>/messages` / `members` / `members/<name>` / `summary`(約120バイト・`seq`・`last_ts`・`member_count`)/ `events`(SSE)、`POST /api/<room>/messages` / `members`。
|
|
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` は初回起動で一度だけ取り込まれる。
|
|
81
83
|
|
|
82
84
|
**2. Claude Code のメンバーを着席させる。** room の MCP 定義は**プロジェクト root の `.mcp.json`** に置く:
|
|
83
85
|
|
package/README.md
CHANGED
|
@@ -113,7 +113,9 @@ The live view shows, per member: vendor / model / reasoning effort / role, and a
|
|
|
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
|
-
Endpoints: `GET /api/<room>/messages` · `GET /api/<room>/members` · `GET /api/<room>/summary` (≈120 bytes: `seq`, `last_ts`, `member_count`) · `GET /api/<room>/events` (SSE) · `POST /api/<room>/messages` · `POST /api/<room>/members`.
|
|
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
|
+
|
|
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.
|
|
117
119
|
|
|
118
120
|
**2. Seat a Claude Code member session.** The room MCP definition must live in the **project-root `.mcp.json`**:
|
|
119
121
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"README.ja.md"
|
|
42
42
|
],
|
|
43
43
|
"engines": {
|
|
44
|
-
"node": ">=
|
|
44
|
+
"node": ">=24"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
package/room/Dockerfile
CHANGED
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.
|
|
16
|
+
const MCP_VERSION = '0.5.0'
|
|
17
17
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
18
|
|
|
19
19
|
const USAGE = `usage:
|
|
@@ -111,9 +111,8 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
111
111
|
|
|
112
112
|
const fmt = m => `[${m.seq}] ${m.from} → ${Array.isArray(m.to_names) ? m.to_names.join(', ') : m.to} (${m.ts}): ${m.body}`
|
|
113
113
|
const memberLine = (m) => {
|
|
114
|
-
const rolesText = Array.isArray(m.roles) && m.roles.length ? m.roles.join('・') :
|
|
115
|
-
const
|
|
116
|
-
const settingsText = [settingsObj.model || m.model, settingsObj.effort || m.effort].filter(Boolean).join('×')
|
|
114
|
+
const rolesText = Array.isArray(m.roles) && m.roles.length ? m.roles.join('・') : ''
|
|
115
|
+
const settingsText = [m.model, m.effort].filter(Boolean).join('×')
|
|
117
116
|
const parts = [m.name]
|
|
118
117
|
if (rolesText) parts.push(`役割=${rolesText}`)
|
|
119
118
|
if (settingsText) parts.push(`設定=${settingsText}`)
|
|
@@ -202,18 +201,13 @@ function observeSelf() {
|
|
|
202
201
|
const observe = observeSelf()
|
|
203
202
|
const parseRoles = (raw) => String(raw ?? '').split(/[,、]/u).map((item) => item.trim()).filter(Boolean)
|
|
204
203
|
const roles = parseRoles(process.env.PEERTABLE_ROLES || process.env.PEERTABLE_ROLE)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
model: process.env.PEERTABLE_MODEL,
|
|
208
|
-
effort: process.env.PEERTABLE_EFFORT,
|
|
209
|
-
}).filter(([, v]) => v))
|
|
204
|
+
// 台帳(room server の SQLite)へは canonical 欄だけを登録する。settings / role(単数)の
|
|
205
|
+
// 重複欄は 2026-08-22 に廃止——同一情報の重複管理禁止(オーナー裁定)。
|
|
210
206
|
const IDENTITY = Object.fromEntries(Object.entries({
|
|
211
207
|
vendor: process.env.PEERTABLE_VENDOR,
|
|
212
208
|
model: process.env.PEERTABLE_MODEL,
|
|
213
209
|
effort: process.env.PEERTABLE_EFFORT,
|
|
214
|
-
role: roles[0],
|
|
215
210
|
roles,
|
|
216
|
-
settings: Object.keys(settings).length ? settings : undefined,
|
|
217
211
|
mission: process.env.PEERTABLE_MISSION,
|
|
218
212
|
aiterm_session_id: process.env.AITERM_SESSION_ID,
|
|
219
213
|
observe,
|
package/room/server.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Peertable room サーバー。チャットルームの正本を所有し、Web UI を内蔵する。
|
|
3
3
|
// 起動: node server.mjs(PEERTABLE_PORT / PEERTABLE_DATA / PEERTABLE_POST_TOKEN で設定)
|
|
4
4
|
import http from 'node:http'
|
|
5
|
-
import { mkdirSync, readFileSync,
|
|
5
|
+
import { mkdirSync, readFileSync, appendFileSync, existsSync, rmSync, readdirSync, renameSync } from 'node:fs'
|
|
6
6
|
import { dirname, join } from 'node:path'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
8
9
|
|
|
9
10
|
const SERVER_USAGE = 'usage: peertable-room\n設定は環境変数: PEERTABLE_PORT(既定8790)/ PEERTABLE_DATA(既定./peertable-data)/ PEERTABLE_POST_TOKEN(設定時のみ書込に要求)\n'
|
|
10
11
|
const serverArg = process.argv[2]
|
|
@@ -29,8 +30,77 @@ const HEARTBEAT_MS = 25000 // SSE 心拍。中間の proxy が落とす前・cli
|
|
|
29
30
|
|
|
30
31
|
mkdirSync(DATA, { recursive: true })
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
|
|
33
|
+
// **member の唯一の台帳**(オーナー裁定 2026-08-22)。member に帰属する情報は全部この
|
|
34
|
+
// SQLite の1行へ入れ、席file・settings欄・role単数欄などへの重複管理を禁止する。
|
|
35
|
+
// 読者・書き手は全員この台帳(HTTP API 経由)だけを見る。
|
|
36
|
+
const db = new DatabaseSync(join(DATA, 'room.db'))
|
|
37
|
+
db.exec(`
|
|
38
|
+
PRAGMA journal_mode=WAL;
|
|
39
|
+
CREATE TABLE IF NOT EXISTS members (
|
|
40
|
+
room TEXT NOT NULL,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
joined_at TEXT NOT NULL,
|
|
43
|
+
vendor TEXT, model TEXT, effort TEXT,
|
|
44
|
+
roles TEXT,
|
|
45
|
+
mission TEXT,
|
|
46
|
+
delivery TEXT,
|
|
47
|
+
observe TEXT,
|
|
48
|
+
aiterm_session_id TEXT,
|
|
49
|
+
status TEXT, status_at TEXT, busy_since TEXT,
|
|
50
|
+
pane_token_hint INTEGER, usage_source TEXT,
|
|
51
|
+
pid INTEGER, started_identity TEXT, argv_digest TEXT, identity_recorded_at TEXT,
|
|
52
|
+
PRIMARY KEY (room, name)
|
|
53
|
+
)
|
|
54
|
+
`)
|
|
55
|
+
|
|
56
|
+
const MEMBER_COLUMNS = [
|
|
57
|
+
'joined_at', 'vendor', 'model', 'effort', 'roles', 'mission', 'delivery', 'observe',
|
|
58
|
+
'aiterm_session_id', 'status', 'status_at', 'busy_since', 'pane_token_hint', 'usage_source',
|
|
59
|
+
'pid', 'started_identity', 'argv_digest', 'identity_recorded_at',
|
|
60
|
+
]
|
|
61
|
+
const MEMBER_JSON_COLUMNS = new Set(['roles', 'delivery', 'observe'])
|
|
62
|
+
|
|
63
|
+
function rowToMember(row) {
|
|
64
|
+
const member = { name: row.name }
|
|
65
|
+
for (const column of MEMBER_COLUMNS) {
|
|
66
|
+
const value = row[column]
|
|
67
|
+
if (value === null || value === undefined) continue
|
|
68
|
+
member[column] = MEMBER_JSON_COLUMNS.has(column) ? JSON.parse(value) : value
|
|
69
|
+
}
|
|
70
|
+
return member
|
|
71
|
+
}
|
|
72
|
+
const listMembers = roomName => db.prepare('SELECT * FROM members WHERE room = ? ORDER BY joined_at').all(roomName).map(rowToMember)
|
|
73
|
+
const getMember = (roomName, name) => {
|
|
74
|
+
const row = db.prepare('SELECT * FROM members WHERE room = ? AND name = ?').get(roomName, name)
|
|
75
|
+
return row ? rowToMember(row) : null
|
|
76
|
+
}
|
|
77
|
+
function putMember(roomName, member) {
|
|
78
|
+
const values = { room: roomName, name: member.name }
|
|
79
|
+
for (const column of MEMBER_COLUMNS) {
|
|
80
|
+
const value = member[column]
|
|
81
|
+
values[column] = value === undefined || value === null ? null
|
|
82
|
+
: MEMBER_JSON_COLUMNS.has(column) ? JSON.stringify(value) : value
|
|
83
|
+
}
|
|
84
|
+
db.prepare(`INSERT OR REPLACE INTO members (room, name, ${MEMBER_COLUMNS.join(', ')})
|
|
85
|
+
VALUES (:room, :name, ${MEMBER_COLUMNS.map(c => `:${c}`).join(', ')})`).run(values)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 旧形式(settings/role の重複欄)を canonical へ畳む。台帳には canonical だけを置く。
|
|
89
|
+
function normalizeMemberMeta(meta) {
|
|
90
|
+
const { role, settings, ...rest } = meta
|
|
91
|
+
const out = { ...rest }
|
|
92
|
+
if (out.roles === undefined && role) out.roles = [role]
|
|
93
|
+
if (settings && typeof settings === 'object') {
|
|
94
|
+
for (const key of ['vendor', 'model', 'effort']) {
|
|
95
|
+
if (out[key] === undefined && settings[key] !== undefined) out[key] = settings[key]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const key of Object.keys(out)) if (out[key] === undefined) delete out[key]
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// room 状態はプロセスが所有する(member 正本は上の SQLite、ログは append-only file)
|
|
103
|
+
const rooms = new Map() // name -> { seq, streams: Set<res> }
|
|
34
104
|
|
|
35
105
|
// create=true は書込系だけが渡す。読み取りは room を作らない
|
|
36
106
|
function loadRoom(name, create = false) {
|
|
@@ -48,21 +118,23 @@ function loadRoom(name, create = false) {
|
|
|
48
118
|
// 壊れた末尾行で throw すると loadRoom は全エンドポイントの入口なので巻き添えで死ぬ→null に落とす
|
|
49
119
|
let lastTs = null
|
|
50
120
|
if (lines.length) { try { lastTs = JSON.parse(lines.at(-1)).ts ?? null } catch { lastTs = null } }
|
|
121
|
+
// 旧 members.json は一度だけ台帳へ取り込み、以後は台帳だけを正とする(file は .migrated へ退避)
|
|
51
122
|
const membersPath = join(dir, 'members.json')
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
123
|
+
if (existsSync(membersPath)) {
|
|
124
|
+
const stored = Object.entries(JSON.parse(readFileSync(membersPath, 'utf8')))
|
|
125
|
+
for (const [memberName, value] of stored) {
|
|
126
|
+
if (getMember(name, memberName)) continue
|
|
127
|
+
const meta = typeof value === 'string' ? { joined_at: value } : value
|
|
128
|
+
putMember(name, { name: memberName, joined_at: meta.joined_at ?? new Date().toISOString(),
|
|
129
|
+
...normalizeMemberMeta(meta) })
|
|
130
|
+
}
|
|
131
|
+
renameSync(membersPath, `${membersPath}.migrated`)
|
|
57
132
|
}
|
|
133
|
+
const room = { name, dir, logPath, seq, last_ts: lastTs, streams: new Set() }
|
|
58
134
|
rooms.set(name, room)
|
|
59
135
|
return room
|
|
60
136
|
}
|
|
61
137
|
|
|
62
|
-
function saveMembers(room) {
|
|
63
|
-
writeFileSync(room.membersPath, JSON.stringify(Object.fromEntries(room.members)) + '\n')
|
|
64
|
-
}
|
|
65
|
-
|
|
66
138
|
function readMessages(room, since = 0) {
|
|
67
139
|
if (!existsSync(room.logPath)) return []
|
|
68
140
|
return readFileSync(room.logPath, 'utf8').split('\n').filter(Boolean)
|
|
@@ -76,7 +148,7 @@ const recipientError = (code, message) => ({
|
|
|
76
148
|
const WAITING_WORDS = ['待機する', '待機します', '待機。']
|
|
77
149
|
|
|
78
150
|
function parentName(room) {
|
|
79
|
-
return
|
|
151
|
+
return listMembers(room.name).find(m => m.delivery?.kind === 'parent_watch')?.name
|
|
80
152
|
}
|
|
81
153
|
|
|
82
154
|
// `all` は room 全体、名前は DM、配列は明示した複数人宛。いずれも同じ通常発言である。
|
|
@@ -99,8 +171,11 @@ function normalizeAudience(to, toNames) {
|
|
|
99
171
|
return { to: null, to_names: names }
|
|
100
172
|
}
|
|
101
173
|
|
|
102
|
-
// SSE の member イベントで押し込む欄。閲覧者が気づく欄だけに絞る(POST /members
|
|
103
|
-
|
|
174
|
+
// SSE の member イベントで押し込む欄。閲覧者が気づく欄だけに絞る(POST /members 参照)。
|
|
175
|
+
// roles は配列なので JSON 比較で差分を見る
|
|
176
|
+
const MEMBER_EVENT_FIELDS = ['status', 'busy_since', 'vendor', 'model', 'effort', 'roles', 'mission']
|
|
177
|
+
const memberFieldChanged = (before, after) =>
|
|
178
|
+
MEMBER_EVENT_FIELDS.some(f => JSON.stringify(before?.[f] ?? null) !== JSON.stringify(after?.[f] ?? null))
|
|
104
179
|
|
|
105
180
|
// room ログへは書かない・system 発言も出さない稼働状態の push。post() とは別の経路
|
|
106
181
|
function emitMember(room, name, meta) {
|
|
@@ -152,13 +227,20 @@ http.createServer(async (req, res) => {
|
|
|
152
227
|
|
|
153
228
|
if (req.method === 'GET' && rest === 'members')
|
|
154
229
|
return json(res, 200, {
|
|
155
|
-
members:
|
|
156
|
-
capabilities: { member_observation_v1: true },
|
|
230
|
+
members: listMembers(room.name),
|
|
231
|
+
capabilities: { member_observation_v1: true, member_ledger_v1: true },
|
|
157
232
|
}, CORS)
|
|
158
233
|
|
|
234
|
+
// 単独 member の読み出し。席の attach 入力・診断はここから取る(席fileは廃止・台帳が唯一の正)
|
|
235
|
+
if (req.method === 'GET' && seg[2] === 'members' && seg[3]) {
|
|
236
|
+
const member = getMember(room.name, decodeURIComponent(seg[3]))
|
|
237
|
+
if (!member) return json(res, 404, { error: 'member_not_found' }, CORS)
|
|
238
|
+
return json(res, 200, { member }, CORS)
|
|
239
|
+
}
|
|
240
|
+
|
|
159
241
|
if (req.method === 'GET' && rest === 'summary')
|
|
160
242
|
return json(res, 200, {
|
|
161
|
-
schema: 'peertable.summary.v1', room: room.name, seq: room.seq, last_ts: room.last_ts, member_count: room.
|
|
243
|
+
schema: 'peertable.summary.v1', room: room.name, seq: room.seq, last_ts: room.last_ts, member_count: listMembers(room.name).length,
|
|
162
244
|
}, CORS)
|
|
163
245
|
|
|
164
246
|
if (req.method === 'GET' && rest === 'events') {
|
|
@@ -195,27 +277,28 @@ http.createServer(async (req, res) => {
|
|
|
195
277
|
// 登録するので、これが無いと再接続のたびに素性が消える。`joined_at` は最初の登録を保つ。
|
|
196
278
|
// observe:null は「取れなかった」であって「記述子を消せ」ではない。上書きすると
|
|
197
279
|
// wakeup-bridge が席を飛ばす(2026-08-20 なぎ)。
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
280
|
+
const normalized = normalizeMemberMeta(meta)
|
|
281
|
+
const known = getMember(room.name, name)
|
|
282
|
+
if (normalized.observe == null && known?.observe) delete normalized.observe
|
|
283
|
+
const merged = { name, joined_at: known?.joined_at ?? new Date().toISOString(), ...known, ...normalized }
|
|
284
|
+
putMember(room.name, merged)
|
|
203
285
|
// **system 発言は本当に新規の時だけ**。欄の更新で「参加した」を流すと、状態を数秒ごとに
|
|
204
286
|
// 送る消費者が卓の全席を起こし続ける(既存メンバーへの再 POST で実測・room [285])
|
|
205
287
|
if (!known) post(room, 'system', name, `${name} が参加した`)
|
|
206
288
|
// 閲覧者が気づく欄が変わった時だけ member イベントを流す。status_at(心拍のたび必ず変わる)と
|
|
207
289
|
// pane_token_hint(作業中に刻み変わる)は対象外——含めると数秒ごとに再描画が走り稼働アニメがちらつく
|
|
208
|
-
else if (
|
|
290
|
+
else if (memberFieldChanged(known, merged))
|
|
209
291
|
emitMember(room, name, merged)
|
|
210
292
|
return json(res, 200, { ok: true })
|
|
211
293
|
}
|
|
212
294
|
if (req.method === 'DELETE' && seg[2] === 'members' && seg[3]) {
|
|
213
|
-
|
|
295
|
+
db.prepare('DELETE FROM members WHERE room = ? AND name = ?').run(room.name, decodeURIComponent(seg[3]))
|
|
214
296
|
return json(res, 200, { ok: true })
|
|
215
297
|
}
|
|
216
298
|
if (req.method === 'DELETE' && seg.length === 2) {
|
|
217
299
|
for (const s of room.streams) s.end()
|
|
218
300
|
rooms.delete(room.name); rmSync(room.dir, { recursive: true, force: true })
|
|
301
|
+
db.prepare('DELETE FROM members WHERE room = ?').run(room.name)
|
|
219
302
|
return json(res, 200, { ok: true })
|
|
220
303
|
}
|
|
221
304
|
return json(res, 404, { error: 'not_found' })
|
|
@@ -244,8 +327,8 @@ const FAVICON = `<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://
|
|
|
244
327
|
// 発言者ごとの色は名前ハッシュ(--h)から作る。彩度・明度だけテーマで持ち替えれば dark/light 両方が成立する
|
|
245
328
|
const STYLE = `
|
|
246
329
|
*{box-sizing:border-box}
|
|
247
|
-
:root{color-scheme:light dark;--fg:#1a1a1a;--bg:#f7f6f3;--surface:#fff;--line:#e4e2dc;--accent:#1d4ed8;--dim:#8a877f;--sat:55%;--lum:45%;--name:32%;--tint:96%;--edge:82%;--busy:#
|
|
248
|
-
@media(prefers-color-scheme:dark){:root{--fg:#e8e6e0;--bg:#141418;--surface:#1e1e24;--line:#2c2c33;--accent:#7aa2ff;--dim:#8b8892;--sat:48%;--lum:56%;--name:75%;--tint:18%;--edge:32%;--busy:#
|
|
330
|
+
:root{color-scheme:light dark;--fg:#1a1a1a;--bg:#f7f6f3;--surface:#fff;--line:#e4e2dc;--accent:#1d4ed8;--dim:#8a877f;--sat:55%;--lum:45%;--name:32%;--tint:96%;--edge:82%;--busy:#d03b3b;--idle:#1f9d55;--dead:#57534e;--blocked:#d97706}
|
|
331
|
+
@media(prefers-color-scheme:dark){:root{--fg:#e8e6e0;--bg:#141418;--surface:#1e1e24;--line:#2c2c33;--accent:#7aa2ff;--dim:#8b8892;--sat:48%;--lum:56%;--name:75%;--tint:18%;--edge:32%;--busy:#ff6b6b;--idle:#3ecf7e;--dead:#6f6c76;--blocked:#f0a63a}}
|
|
249
332
|
body{margin:0;font-family:ui-sans-serif,system-ui,-apple-system,"Hiragino Kaku Gothic ProN","Noto Sans JP",sans-serif;font-size:14px;line-height:1.65;color:var(--fg);background:var(--bg)}
|
|
250
333
|
a{color:var(--accent)}
|
|
251
334
|
.brand{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:650;letter-spacing:.01em}
|
|
@@ -261,7 +344,8 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
|
|
|
261
344
|
.members{display:flex;gap:6px;overflow-x:auto;padding:10px 0;scrollbar-width:thin}
|
|
262
345
|
.chip.has-meta{cursor:pointer}
|
|
263
346
|
/* 稼働状態の点。報告が途絶えたら unknown(中空)へ落として、古い状態を出し続けない */
|
|
264
|
-
.chip .
|
|
347
|
+
.chip .nm{display:inline-flex;align-items:center;gap:5px}
|
|
348
|
+
.chip .st{flex:none;display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dim)}
|
|
265
349
|
.chip .st.busy{background:var(--busy)}
|
|
266
350
|
.chip .st.idle{background:var(--idle)}
|
|
267
351
|
.chip .st.dead{background:var(--dead)}
|
|
@@ -270,10 +354,9 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
|
|
|
270
354
|
.metapop{position:fixed;z-index:20;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-size:12px;box-shadow:0 6px 20px rgba(0,0,0,.18);max-width:70vw}
|
|
271
355
|
.metapop .metaname{font-weight:600;margin-bottom:2px}
|
|
272
356
|
.metapop .metaline{color:var(--dim)}
|
|
273
|
-
.chip{position:relative;flex:none;display:flex;align-items:
|
|
357
|
+
.chip{position:relative;flex:none;display:flex;align-items:center;gap:7px;padding:5px 11px 5px 5px;border:1px solid var(--line);border-radius:12px;background:var(--surface);font-size:12px;font-weight:600}
|
|
274
358
|
.chip .av{width:22px;height:22px;font-size:11px;flex:none}
|
|
275
359
|
.chip .id{display:flex;flex-direction:column;gap:1px;min-width:0;line-height:1.25}
|
|
276
|
-
.chip .roles,.chip .settings,.chip .mission{font-weight:500;font-size:11px;color:var(--dim)}
|
|
277
360
|
.chip.recent{border-color:hsl(var(--h) var(--sat) var(--edge))}
|
|
278
361
|
.chip.recent .nm{color:hsl(var(--h) var(--sat) var(--name))}
|
|
279
362
|
.chip.pulse .av{animation:pulse 1.6s ease-out 2}
|
|
@@ -284,7 +367,7 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
|
|
|
284
367
|
@keyframes seat-working{0%,100%{transform:translateY(0) rotate(-2deg)}50%{transform:translateY(-2px) rotate(2deg)}}
|
|
285
368
|
@keyframes seat-beat{from{transform:scale(.78);opacity:.7}to{transform:scale(1.28);opacity:1}}
|
|
286
369
|
@keyframes seat-blocked{0%,100%{opacity:1}50%{opacity:.5}}
|
|
287
|
-
.victory{position:fixed;z-index:30;left:var(--victory-x);top:var(--victory-y);pointer-events:none;color:var(--
|
|
370
|
+
.victory{position:fixed;z-index:30;left:var(--victory-x);top:var(--victory-y);pointer-events:none;color:var(--idle);font-size:13px;font-weight:800;letter-spacing:.03em;text-shadow:0 1px 0 var(--surface);animation:victory-rise 1.45s cubic-bezier(.18,.8,.3,1) both}
|
|
288
371
|
@keyframes victory-rise{0%{opacity:0;transform:translate(-50%,8px) scale(.75)}18%{opacity:1;transform:translate(-50%,-7px) scale(1.08)}72%{opacity:1;transform:translate(-50%,-15px) scale(1)}100%{opacity:0;transform:translate(-50%,-27px) scale(.92)}}
|
|
289
372
|
@keyframes pulse{from{box-shadow:0 0 0 0 hsl(var(--h) var(--sat) var(--lum)/.6)}to{box-shadow:0 0 0 9px hsl(var(--h) var(--sat) var(--lum)/0)}}
|
|
290
373
|
.log{max-width:760px;margin:0 auto;padding:14px 16px 40px;display:flex;flex-direction:column;gap:10px}
|
|
@@ -467,16 +550,15 @@ async function refreshMembers(){
|
|
|
467
550
|
const c=el('span','chip'+(m.name===recent?' recent':''))
|
|
468
551
|
const color=avatarColor(m.name)
|
|
469
552
|
c.style.setProperty('--h',color.h);c.style.setProperty('--av-bg',color.bg);c.style.setProperty('--av-fg',color.fg);c.dataset.name=m.name
|
|
470
|
-
const rolesText=Array.isArray(m.roles)&&m.roles.length?m.roles.join('・'):
|
|
471
|
-
const
|
|
472
|
-
const settingsText=[settingsObj.model||m.model,settingsObj.effort||m.effort].filter(Boolean).join('×')
|
|
553
|
+
const rolesText=Array.isArray(m.roles)&&m.roles.length?m.roles.join('・'):''
|
|
554
|
+
const settingsText=[m.model,m.effort].filter(Boolean).join('×')
|
|
473
555
|
const meta=[rolesText,settingsText,m.mission].filter(Boolean)
|
|
474
556
|
// 稼働状態は**報告が新しい時だけ**採る。途絶えたら unknown へ落とす——古い状態を出し続けるのが
|
|
475
557
|
// いちばん悪い(動いていない席を「動いている」と見せる)。閾値は bridge の心拍30秒の3倍
|
|
476
558
|
const age=m.status_at?Date.now()-Date.parse(m.status_at):Infinity
|
|
477
559
|
const st=(m.status&&age<STATUS_STALE_MS)?m.status:(m.status?'unknown':null)
|
|
478
560
|
if(st)c.classList.add('is-'+st)
|
|
479
|
-
|
|
561
|
+
meta.push(st?'状態 '+({busy:'作業中',idle:'待機',dead:'停止',blocked:'承認待ち',unknown:'不明(報告が途絶えている)'}[st]??st):'状態 未報告(seat-status bridge が動いていない)')
|
|
480
562
|
const usage=[]
|
|
481
563
|
const busyAge=m.busy_since?Date.now()-Date.parse(m.busy_since):NaN
|
|
482
564
|
if((st==='busy'||st==='blocked')&&Number.isFinite(busyAge)&&busyAge>=0)usage.push('継続 '+elapsed(busyAge))
|
|
@@ -486,11 +568,11 @@ async function refreshMembers(){
|
|
|
486
568
|
c.appendChild(el('span','av',initial(m.name)))
|
|
487
569
|
const id=el('span','id')
|
|
488
570
|
const nameRow=el('span','nm',m.name)
|
|
489
|
-
|
|
571
|
+
// チップの常設表示は「アバター・名前・◉」だけ。◉は状態が新鮮な時だけ色が付き、
|
|
572
|
+
// 途絶・未報告は中空リング=bridge が動いていないことがひと目で分かる。
|
|
573
|
+
// roles / settings / mission は title(ホバー)とタップ popover にだけ出す。
|
|
574
|
+
nameRow.appendChild(el('span','st '+(st??'unknown')))
|
|
490
575
|
id.appendChild(nameRow)
|
|
491
|
-
if(rolesText)id.appendChild(el('span','roles',rolesText))
|
|
492
|
-
if(settingsText)id.appendChild(el('span','settings',settingsText))
|
|
493
|
-
if(m.mission)id.appendChild(el('span','mission',m.mission))
|
|
494
576
|
c.appendChild(id)
|
|
495
577
|
// タップ環境には hover が無いので、押した時に同じ内容を出す(ホバーは title が担う)
|
|
496
578
|
if(meta.length){c.classList.add('has-meta');c.addEventListener('click',ev=>{ev.stopPropagation();showMeta(c,m,meta)})}
|
package/skill/SKILL.md
CHANGED
|
@@ -76,7 +76,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
|
|
|
76
76
|
- 同じvendor内のmodel / effort変更はAitermが同一sessionと会話contextを保って行う。vendor変更だけは再起動を伴うため、本人は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
|
|
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)
|
|
80
80
|
8. **起動確認**: room の members に全員いる / 最初の claim が room に流れる(Lattice 併用モードはそれが Lattice へ到達している=`lattice todo status --json` の active に出ることも確認する。単独モードは room の claim 宣言だけが到達の証拠)/ Web UI で観測できる、をチェックして報告する
|
|
81
81
|
|
|
82
82
|
## teardown
|
|
@@ -101,6 +101,10 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
|
|
|
101
101
|
|
|
102
102
|
各段は `[実施] / [スキップ] / [未実施]` を1行ずつ出す。**トークンを要するのは room 削除だけ**なので、そこが失敗しても残りの撤去は続行し、未実施を明示して非ゼロで終わる(黙って中断しない・決定58)。未実施が出ても**撤去そのものは済んでいる**。残りは表示された **[手当] の curl を手で叩く**だけで、`.team/` は既に消えているので **teardown.sh の再実行はできない**(2026-08-08 実測。再実行すると `setup-state.json` が読めず落ちる)。
|
|
103
103
|
|
|
104
|
+
## doctor
|
|
105
|
+
|
|
106
|
+
卓の再開・引き継ぎ・「動いてるか分からない」時は、最初に `scripts/doctor.sh <project> [--repair]` を打つ。room 到達性・台帳(member 行)の素性と本人性の完全性・各席の tmux セッションと pid+lstart による本人性・2ブリッジ(wakeup/seat-status)の生存と本人性・(Lattice 併用モードなら)工程正本の state を1行ずつ機械判定する。判定できない項目は偽の生存判定を作らず「判定不能」と出す。`--repair` は死んでいるブリッジだけ `ensure-bridge.sh` で立て直す——**席の再起動はしない**(人の判断が要るため。NG 表示に `launch-seat.sh` を促す一言が付くだけ)。
|
|
107
|
+
|
|
104
108
|
## Lattice の実行層へ席の着手を載せる(pull 型・Lattice 併用モードだけ)
|
|
105
109
|
|
|
106
110
|
**装置は仕事を配らない。** 2026-08-09 のオーナー裁定(改・裁定1)で、Lattice が task を選んで席へ配る向きは撤回された——**作業を選ぶのも始めるのも席(AI)**であり、装置がやるのは**着手済み ToDo 同士の競合判定と介入だけ**である。着手前に装置の許可を待つ面も作らない。旧版にあった `[配車]` / `[受諾]` / `[辞退]` の3層は無くなり、**claim は従来どおり割当の主体に戻った**(決定25 のとおり)。
|
|
@@ -113,11 +117,8 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
|
|
|
113
117
|
- **worktree と lease は設備の供給である。** 席が要求すれば出てくるもので、出してもらうものではない
|
|
114
118
|
- **席と spool は接触しない。** 席が触るのは room と worktree だけで、`.lattice/` の直読み・直書き禁止の契約はそのまま
|
|
115
119
|
- **席の作法の正本は `templates/member.md` の「Lattice の実行層へ自分の着手を載せる」節**(intake→intervention 判定→attach→作業→監査担当が `done.sh`→intake 席が accept の順・1席1 intake・禁止操作・検証の回し方・成果の正本)。ここに二重化しない
|
|
116
|
-
- **席は自分の pid を装置へ渡す(attach)。** その pid は
|
|
117
|
-
- **run-bridge
|
|
118
|
-
- **ブリッジの起動**: `scripts/ensure-bridge.sh <project> run [--lattice <path>]`。停止は `node scripts/run-bridge.mjs <project> --stop`(**teardown.sh が自動で行う**)。**spool dir も席名も取らない**(配車をしないので要らない。渡すと typed error で落ちる)。ADR 0157 の作法(pid 記録・起動時に前の記録を掃除・SIGTERM→SIGKILL)はそのまま
|
|
119
|
-
- `--lattice <path>` は release 前の source tree を実測する時に使う。**PATH の install は version 表示が同じでも未 publish の schema を読めない**(2026-08-09 に卓で3件実測)
|
|
120
|
-
- bridge が返すのは2つだけで**どちらも読み取り**: `[run] <run-id> intake=[…] accepted=[…] hold=[…] closed=…`(変化時だけ)と、`hold` になった intake をその作業を始めた席宛へ返す `[介入]`
|
|
120
|
+
- **席は自分の pid を装置へ渡す(attach)。** その pid は room 台帳の member 行(`launch-seat.sh` が着席直後に登録する本人性欄)が持ち、席は `pull-attach-input.mjs` で読むだけで attach input になる(席file は 2026-08-22 廃止・member に帰属する情報の正本は台帳だけ)。**raw argv を保存しない**——token値をargvから除いた後も、将来の引数を無条件に複製しない。持つのはdigestだけ。Lattice の再観測は `/bin/ps -o command=`。pid/lstart が一致して digest だけ違うときは親が `skill/scripts/refresh-seat-identity.mjs <project> <name>` で揃える。席は台帳の本人性欄を書き換えない
|
|
121
|
+
- **run-bridge は退役した(2026-08-22・オーナー裁定)。** 介入(hold)は席が自分の Lattice コマンド応答(intake / attach / accept / `run intake intervention`)で受け取る——これが唯一の経路である。装置の介入を DM で先回り通知する中継は、凍った席には届かず、届いた席を退席させ、死んだ席の shell へ打鍵する事故だけを生んだので廃止した。ブリッジは wakeup と seat-status の2本だけ
|
|
121
122
|
|
|
122
123
|
運用側が踏みやすい所(実測で確認した挙動):
|
|
123
124
|
|
|
@@ -188,6 +189,7 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
|
|
|
188
189
|
|
|
189
190
|
## 親の operating notes(このセッションの振る舞い)
|
|
190
191
|
|
|
192
|
+
- **親の権限境界(最初に読む・オーナー裁定 2026-08-22)**: 円卓は対等メンバーの自律で回り、各ToDoのクローズは監査担当が行う。親は裁定者ではない——親が卓上で工程手順・着地方法・完了可否を裁定しない。親がやってよいのは、オーナー窓口・環境修理(ブリッジ・CLI・席の器)・campaign 終端の最終監査だけ。実装の代行も裁定の差し込みも、席の正典(roles/member.md)と衝突する「親のバグ」として扱う(実被弾 2026-08-22: Grok 親の実装代行と Fable 親の着地裁定が、監査担当の正規クローズと二重に衝突した)
|
|
191
193
|
- 親は MCP を後付けできないため room へは HTTP API 直で参加する:
|
|
192
194
|
- 登録: `curl -X POST $URL/api/$ROOM/members -H "X-Peertable-Token: $TOKEN" -d '{"name":"bell"}'`
|
|
193
195
|
- 発言: `node skill/scripts/post-message.mjs bell <宛先> '<本文>' | curl -X POST $URL/api/$ROOM/messages -H "X-Peertable-Token: $TOKEN" -H "Content-Type: application/json" --data-binary @-`。Windows の `python3 -c json.dumps` は stdout が cp932 になり日本語本文が部屋へ壊れて保存される。複数人は`to`へ名前の配列(JSON)
|
|
@@ -202,6 +204,9 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
|
|
|
202
204
|
通常席用wakeup-bridge、tmux、`codex exec resume`を親へ流用しない。**Grok**はpersistent
|
|
203
205
|
Monitorで同じ`--follow`を1回だけ実行する。Grok親をwakeup-bridgeの対象にしない
|
|
204
206
|
- 親以外宛・ping・親自身の発言は捨てる。watcher不在中のDMは永続cursorから次回起動時にcatch-upする
|
|
207
|
+
- **停滞警報**: 番犬は「着手可能・着手中の工程があるのに作業中(busy)の席が1つも無い」状態が
|
|
208
|
+
3分(`PEERTABLE_STALL_ALARM_MS`)続くと `parent_table_stalled` を1回出す(状態が変わるまで再警報しない)。
|
|
209
|
+
親はこれを受けたら席の状態一覧を実観測し、止まっている原因(brief不達・承認待ち・hold)を特定して動かす
|
|
205
210
|
- **世代は常に1匹**。Claudeは旧MonitorをTaskStop、Codexは旧background taskを停止してから張り替える。
|
|
206
211
|
Grokも旧Monitorを止めてから張り替える。`watch_error`は親へ通知し、沈黙死させない
|
|
207
212
|
- claimと工程完了は`to: "all"`、ターン終了時の次の行動は自分宛DM、誰かへの用事はその人宛DM、誰に聞くか分からない時は`to: "all"`を使う
|
|
@@ -216,7 +221,7 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
|
|
|
216
221
|
1. **room ログを読む**——`curl -s "$URL/api/$ROOM/messages?since=<最後に読んだ seq>"`。`since` を持っていなければ 0 から。**会話が卓の正本**なので、まずここで現在地(誰が何を claim し、どこまで done か)を作る
|
|
217
222
|
2. **工程正本で照合する**——`lattice todo status --json`(Lattice 併用)。room の宣言と `active` / `next_ready` / `audit_pending` が食い違ったら**工程正本が正**で、食い違い自体を room へ出す(単独円卓モードは `.team/tasks.md` と room ログの突き合わせ)
|
|
218
223
|
3. **member 登録は残っている**ので `parent-join.sh` を再実行しない。`curl -s $URL/api/$ROOM/members` で自分の名前を確認するだけでよい(実測: 親の登録はセッションを跨いで残る)。**再実行しても `<名前> が参加した` は流れない**——`POST /members` は本当に新規追加の時だけ本人宛のsystem発言を出す
|
|
219
|
-
4. **番犬を張り直す**——ClaudeとGrokは`--follow`を1回起動する。Codexは1秒ごとの`codex-parent-watch.sh` loopをbackground taskで起動する。生きた旧世代が残っていれば先に止める(世代は常に1匹)。永続cursorが不在時間のDM
|
|
224
|
+
4. **番犬を張り直す**——ClaudeとGrokは`--follow`を1回起動する。Codexは1秒ごとの`codex-parent-watch.sh` loopをbackground taskで起動する。生きた旧世代が残っていれば先に止める(世代は常に1匹)。永続cursorが不在時間のDMを回収する。**張り直したら耳の疎通を実測する**——自分宛のprobe DM(差出人は自分以外なら何でもよい)を1通投稿し、それが監視イベントとして届くのを確認するまで再着卓完了と言わない
|
|
220
225
|
- **再着卓の契機は番犬taskの終了通知または`watch_error`**。親の側には「途絶した」と教える別経路が無いので、届いたら再着卓の手順に入る
|
|
221
226
|
- **順序の要点は「room と工程正本を読み終えるまで発言しない」**。読む前に喋ると、自分が行き違いを作る側になる(実例あり)
|
|
222
227
|
- **やらないこと**: 復帰の挨拶で席を起こさない。作業の再確認を席へ聞いて回らない——**現在地は上の1〜2で取れる**ので、聞くのは席の時間を奪うだけである
|
|
@@ -238,7 +243,7 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
|
|
|
238
243
|
- evidence は記述子 JSON。記述子ファイル自体も repo 内相対パスに置く(repo 外絶対パスは INVALID_ARGUMENTS)。`.team/scripts/done.sh` が正規経路。証跡の置き場は **`evidence/<plan_key>/<task_id>.md`**——task_id は campaign を跨いで再利用されるので、平置きにすると前の campaign の監査証跡を上書きで消す(2026-08-08 実測)
|
|
239
244
|
- **外部ペイン(決定53)は Lattice 0.50.0 以降が要る。** それ以前の Lattice に `external_pane` 入りの `project.json` を差すと、identity 検証が完全一致キーで落ちて `lattice todo status` ごと死ぬ(`PROJECT_IDENTITY_INVALID / identity_schema_invalid`・0.49.0 で実測)。工程正本が読めなくなる=卓が止まるので、Lattice が古い環境では Lattice 併用 setup を走らせない
|
|
240
245
|
- **メンバー起動の既知ダイアログは2種だけ**(実測 2026-08-08): 未信頼ディレクトリの workspace trust(`1. Yes, I trust this folder`)と開発 channel 警告(`1. I am using this for local development`)。`--dangerously-skip-permissions` を付けているので MCP 同意ダイアログは出ない。信頼済みディレクトリでは trust も出ない
|
|
241
|
-
- **Codex 席のダイアログは
|
|
246
|
+
- **Codex 席のダイアログは5種**(trust / update skip / hooks / **MCP Allow** / **command approval**)。更新案内と hooks の既定は誤り(Codex CLI v0.146.0 および 2026-08-20 実測): ディレクトリ trust(`1. Yes, continue`=既定で正しい)。**更新案内(`1. Update now`)は既定のまま Enter を押すと立卓の途中で `npm install -g @openai/codex` が走る**ので「2. Skip」。**`Hooks need review` は room MCP 初期化より前に出る**(`SEAT_ROOM_MCP_NOT_READY` のまま rollback)。**`Allow the room MCP server to run tool` は member 登録の後、最初の tool 呼び出しで出る**(2026-08-22 ひなた/さくら。launch が ready と報告したあと画面に残る=仕組みの欠陥)。**`Would you like to run the following command?` は approval_policy=never でもターン中に出る**(2026-08-22 ひなた/さくら。wakeup-bridge が MCP Allow 以外へ素送信すると承認画面へ DM が混ざる)。`launch-seat.sh` は trust / skip / Trust all に加え Always allow(Down×2+Enter)と command approval(Down+Enter=don't ask again)を通し、member 登録だけでは ready にしない。席 CODEX_HOME は `approval_policy = "never"`。wakeup-bridge は既知ダイアログを通してから素送信する。失敗時は rollback 前に `.team/<name>-pane-on-fail.txt` を残す。待ち時間のポーリングで代用しない
|
|
242
247
|
- **Codex はターン実行中でも素送信を受け付ける**(実測 2026-08-08)。busy 中に送った文言はそのターンの中で読まれ、指示どおりに動く(steering)。Codex 席への TUI 配達は idle 待ちを持たない
|
|
243
248
|
- **Grok はターン実行中の素送信を今の仕事へ混ぜない**(実測 2026-08-17・Grok Build TUI 既定 `follow_up_behavior=queue`)。届いた文は入力キューへ積まれ、次の user ターンになる。TUI 配達は Grok 席だけ pane が idle になるまで送らない。busy の判定は `esc to interrupt` に加え、Grok 固有の `Waiting for response` / `Responding…` / `[stop]` / 未完了 `[hooks: 1/3]`(実測 2026-08-21。Grok は `esc to interrupt` を出さないので、これを見ないと作業中が待機に見える)、待ち中の `send a message to interrupt`、番号付きキュー+`Enter:send now`
|
|
244
249
|
- **Grok の `Help improve Grok [Opt out] [Opt in]` バナーは席を死んだように見せる**(実測 2026-08-21。auth の `coding_data_retention_opt_out` だけでは消えない)。`launch-seat.sh` は `GROK_PRIVACY_NOTICE_ROLLOUT=0` を着席 env へ渡す。キー送信では消えない。既存席は再着席するまで残る
|
|
@@ -59,7 +59,8 @@ 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
60
|
if not member 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))))
|
|
63
64
|
' "$name") || { echo "SEAT_CHANGE_MEMBER_METADATA_MISSING: ${name} のvendor/modelが要る" >&2; exit 1; }
|
|
64
65
|
IFS=$'\t' read -r old_vendor old_model old_effort aiterm_session_id old_role <<EOF
|
|
65
66
|
$meta
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Codex TUI の既知ダイアログ → 送るキー。画面文言だけで判定する。未知の確認は通さない。
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
export const MCP_ALLOW_NEEDLE = 'Allow the room MCP server to run tool'
|
|
7
|
+
export const MCP_ALWAYS_ALLOW = 'Always allow'
|
|
8
|
+
export const COMMAND_APPROVAL_NEEDLE = 'Would you like to run the following command?'
|
|
9
|
+
export const COMMAND_APPROVAL_DONT_ASK = "don't ask again"
|
|
10
|
+
|
|
11
|
+
export function keysForCodexPane(screen) {
|
|
12
|
+
const text = String(screen ?? '')
|
|
13
|
+
if (text.includes(MCP_ALLOW_NEEDLE) && text.includes(MCP_ALWAYS_ALLOW)) {
|
|
14
|
+
return { kind: 'mcp-allow', keys: ['Down', 'Down', 'Enter'] }
|
|
15
|
+
}
|
|
16
|
+
if (text.includes(COMMAND_APPROVAL_NEEDLE) && text.toLowerCase().includes(COMMAND_APPROVAL_DONT_ASK)) {
|
|
17
|
+
return { kind: 'command-approval', keys: ['Down', 'Enter'] }
|
|
18
|
+
}
|
|
19
|
+
if (text.includes('Hooks need review')) {
|
|
20
|
+
return { kind: 'hooks', keys: ['Down', 'Enter'] }
|
|
21
|
+
}
|
|
22
|
+
if (text.includes('Update now')) {
|
|
23
|
+
return { kind: 'update', keys: ['Down', 'Enter'] }
|
|
24
|
+
}
|
|
25
|
+
if (text.includes('Yes, continue') && text.includes('Do you trust the contents of this directory')) {
|
|
26
|
+
return { kind: 'trust', keys: ['Enter'] }
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function blocksCodexReady(screen) {
|
|
32
|
+
const kind = keysForCodexPane(screen)?.kind
|
|
33
|
+
return kind === 'mcp-allow' || kind === 'command-approval'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isMain = Boolean(process.argv[1]) && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
37
|
+
if (isMain) {
|
|
38
|
+
const chunks = []
|
|
39
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
40
|
+
const screen = Buffer.concat(chunks).toString('utf8')
|
|
41
|
+
const action = keysForCodexPane(screen)
|
|
42
|
+
if (process.argv.includes('--ready-ok')) {
|
|
43
|
+
process.exit(blocksCodexReady(screen) ? 2 : 0)
|
|
44
|
+
}
|
|
45
|
+
process.stdout.write(`${JSON.stringify(action)}\n`)
|
|
46
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function repairSplicedBegin(text) {
|
|
2
|
+
return String(text ?? '').replace(
|
|
3
|
+
/^# BEGIN PEERTABLE ROOM MCP\napproval_policy = "never"\nsandbox_mode = "danger-full-access" added_newline=([01])\n/mu,
|
|
4
|
+
'# BEGIN PEERTABLE ROOM MCP added_newline=$1\napproval_policy = "never"\nsandbox_mode = "danger-full-access"\n',
|
|
5
|
+
)
|
|
6
|
+
}
|