peertable 0.4.36 → 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 +39 -2
- 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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 卓の健全性を機械判定して表示し、--repair 指定時だけ死んでいるブリッジを立て直す。
|
|
3
|
+
# usage: doctor.sh <project_dir> [--repair]
|
|
4
|
+
#
|
|
5
|
+
# 判定するのは5点だけ:
|
|
6
|
+
# 1. room サーバー到達性(GET /api/<room>/summary)
|
|
7
|
+
# 2. 台帳(room member 行)の素性・本人性の完全性
|
|
8
|
+
# 3. 各席の tmux セッション(peer-<name>)の存在と、席file の pid+lstart による本人性
|
|
9
|
+
# 4. 2ブリッジ(wakeup / seat-status)の record 生存と、record にある識別情報だけで
|
|
10
|
+
# 判定できる範囲の本人性・鮮度(判定できないものは「判定不能」と正直に出す。偽の生存判定を作らない)
|
|
11
|
+
# 5. Lattice 併用モード(mode=lattice)なら `lattice status --json` の state / active_runs
|
|
12
|
+
#
|
|
13
|
+
# 各行は OK / NG / REPAIRED / 判定不能 のいずれかで始まる。--repair は NG のブリッジだけ
|
|
14
|
+
# ensure-bridge.sh で立て直す。**席の再起動はしない**——席は人の判断が要るので、NG 表示に
|
|
15
|
+
# launch-seat.sh を促す一言を添えるだけに留める。
|
|
16
|
+
#
|
|
17
|
+
# 終了コード: 全 OK(判定不能を含む)= 0、NG が1件でもあれば 1(--repair で全部直れば 0)。
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
proj="${1:-}"
|
|
21
|
+
repair=false
|
|
22
|
+
if [ "${2:-}" = "--repair" ]; then repair=true; fi
|
|
23
|
+
if [ -z "$proj" ]; then
|
|
24
|
+
echo "usage: doctor.sh <project_dir> [--repair]" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
proj=$(cd "$proj" && pwd)
|
|
28
|
+
script_dir=$(cd "$(dirname "$0")" && pwd)
|
|
29
|
+
setup="$proj/.team/setup-state.json"
|
|
30
|
+
if [ ! -f "$setup" ]; then
|
|
31
|
+
echo "NG .team/setup-state.json が無い(${proj} は peertable setup 済みか確認せよ)" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
DOCTOR_PROJ="$proj" DOCTOR_SCRIPT_DIR="$script_dir" DOCTOR_REPAIR="$repair" \
|
|
36
|
+
node --input-type=module <<'NODE'
|
|
37
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
38
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
39
|
+
import { join } from 'node:path'
|
|
40
|
+
import { pathToFileURL } from 'node:url'
|
|
41
|
+
|
|
42
|
+
const proj = process.env.DOCTOR_PROJ
|
|
43
|
+
const scriptDir = process.env.DOCTOR_SCRIPT_DIR
|
|
44
|
+
const repair = process.env.DOCTOR_REPAIR === 'true'
|
|
45
|
+
const team = join(proj, '.team')
|
|
46
|
+
|
|
47
|
+
const { bridgeRecordLive } = await import(pathToFileURL(join(scriptDir, 'bridge-record-live.mjs')))
|
|
48
|
+
const { observePidCommand } = await import(pathToFileURL(join(scriptDir, 'refresh-seat-identity.mjs')))
|
|
49
|
+
const { resolveTmuxSocket, tmuxArgv } = await import(pathToFileURL(join(scriptDir, 'seat-usage.mjs')))
|
|
50
|
+
|
|
51
|
+
let ng = 0
|
|
52
|
+
function line(level, text) {
|
|
53
|
+
console.log(`${level} ${text}`)
|
|
54
|
+
if (level === 'NG') ng = 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const setup = JSON.parse(readFileSync(join(team, 'setup-state.json'), 'utf8'))
|
|
58
|
+
const { room, server_url: url, mode } = setup
|
|
59
|
+
|
|
60
|
+
// 1. room サーバー到達性
|
|
61
|
+
let summary = null
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/summary`)
|
|
64
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
65
|
+
summary = await res.json()
|
|
66
|
+
line('OK', `room 到達: ${url} room=${room} seq=${summary.seq} member_count=${summary.member_count}`)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
line('NG', `room 到達不可: ${url} room=${room}(${error.message})`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// members は以降の突合・席チェックにも使う
|
|
72
|
+
let members = []
|
|
73
|
+
let membersOk = false
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/members`)
|
|
76
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
77
|
+
const body = await res.json()
|
|
78
|
+
members = Array.isArray(body.members) ? body.members : []
|
|
79
|
+
membersOk = true
|
|
80
|
+
} catch (error) {
|
|
81
|
+
line('NG', `room members を取得できない(${error.message})`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. 台帳の完全性。tmux 席を持つ member(親以外)は本人性欄(pid 等)も台帳に載っている
|
|
85
|
+
// はず(席file は 2026-08-22 廃止・正本は member 行だけ)
|
|
86
|
+
function hasDescriptor(member) {
|
|
87
|
+
return Boolean(member?.observe && typeof member.observe === 'object'
|
|
88
|
+
&& typeof member.observe.tmux_target === 'string' && member.observe.tmux_target.length > 0)
|
|
89
|
+
}
|
|
90
|
+
const seatRows = new Map()
|
|
91
|
+
if (membersOk) {
|
|
92
|
+
const seated = members.filter(hasDescriptor)
|
|
93
|
+
const missingIdentity = seated.filter(m => !Number.isSafeInteger(m.pid) || !m.started_identity || !m.argv_digest)
|
|
94
|
+
for (const m of seated) seatRows.set(m.name, m)
|
|
95
|
+
if (missingIdentity.length === 0) {
|
|
96
|
+
line('OK', `台帳の member 行に素性・本人性が揃っている(${seated.length} 席)`)
|
|
97
|
+
} else {
|
|
98
|
+
line('NG', `台帳の本人性欄が欠けている member: ${missingIdentity.map(m => m.name).join(',')}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. 各席の tmux セッション(peer-<name>)の存在と、台帳の pid+lstart による本人性
|
|
103
|
+
for (const [name, seat] of seatRows) {
|
|
104
|
+
const member = seat
|
|
105
|
+
const socket = member?.observe?.tmux_socket || resolveTmuxSocket(process.env).socket
|
|
106
|
+
const target = member?.observe?.tmux_target || `peer-${name}`
|
|
107
|
+
let sessionExists = false
|
|
108
|
+
try {
|
|
109
|
+
execFileSync('tmux', tmuxArgv(['has-session', '-t', target], { socket }), { stdio: 'ignore' })
|
|
110
|
+
sessionExists = true
|
|
111
|
+
} catch { /* has-session は非ゼロ終了で無しを表す */ }
|
|
112
|
+
if (!sessionExists) {
|
|
113
|
+
line('NG', `席 ${name}: tmux セッション ${target} が無い(launch-seat.sh で立て直す)`)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
let identity = null
|
|
117
|
+
let identityError = null
|
|
118
|
+
try { identity = observePidCommand(seat.pid) } catch (error) { identityError = error }
|
|
119
|
+
if (identityError) {
|
|
120
|
+
line('NG', `席 ${name}: pid ${seat.pid} を観測できない(${identityError.message})(launch-seat.sh で立て直す)`)
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (identity.started_identity !== seat.started_identity) {
|
|
124
|
+
line('NG', `席 ${name}: pid ${seat.pid} は再利用されている(lstart不一致・本人ではない)(launch-seat.sh で立て直す)`)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
line('OK', `席 ${name}: tmux ${target} 生存・pid ${seat.pid} 本人性確認`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. 2ブリッジ(run-bridge は 2026-08-22 退役)。record の形式が違うので判定できる範囲だけ判定する
|
|
131
|
+
function judgeWakeup() {
|
|
132
|
+
const path = join(team, 'wakeup-bridge.json')
|
|
133
|
+
if (!existsSync(path)) return { level: 'NG', text: 'wakeup-bridge: record が無い(起動していない)' }
|
|
134
|
+
const record = JSON.parse(readFileSync(path, 'utf8'))
|
|
135
|
+
if (bridgeRecordLive(record)) {
|
|
136
|
+
const age = Math.round((Date.now() - Date.parse(record.last_progress_at)) / 1000)
|
|
137
|
+
return { level: 'OK', text: `wakeup-bridge: 生存 pid=${record.pid} last_progress ${age}秒前` }
|
|
138
|
+
}
|
|
139
|
+
return { level: 'NG', text: `wakeup-bridge: pid=${record.pid} が死んでいるか last_progress_at が古い` }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function judgeSeatStatusBridge() {
|
|
143
|
+
const path = join(team, 'seat-status-bridge.json')
|
|
144
|
+
if (!existsSync(path)) return { level: 'NG', text: 'seat-status-bridge: record が無い(起動していない)' }
|
|
145
|
+
const record = JSON.parse(readFileSync(path, 'utf8'))
|
|
146
|
+
let alive = false
|
|
147
|
+
try { process.kill(record.pid, 0); alive = true } catch { /* 死んでいる */ }
|
|
148
|
+
if (!alive) return { level: 'NG', text: `seat-status-bridge: pid=${record.pid} が死んでいる` }
|
|
149
|
+
return { level: '判定不能', text: `seat-status-bridge: pid=${record.pid} は生存しているが record に lstart が無く本人性を確認できない` }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const [name, judge] of [['wakeup', judgeWakeup], ['seat-status', judgeSeatStatusBridge]]) {
|
|
153
|
+
let result = judge()
|
|
154
|
+
if (result.level === 'NG' && repair) {
|
|
155
|
+
const res = spawnSync(join(scriptDir, 'ensure-bridge.sh'), [proj, name], { encoding: 'utf8' })
|
|
156
|
+
if (res.status === 0) {
|
|
157
|
+
const after = judge()
|
|
158
|
+
if (after.level !== 'NG') { line('REPAIRED', `${name}-bridge: 立て直した(${after.text})`); continue }
|
|
159
|
+
line('NG', `${name}-bridge: 立て直したが依然として不健全(${after.text})`)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const detail = (res.stderr || res.stdout || '').trim().split('\n').slice(-3).join(' / ')
|
|
163
|
+
line('NG', `${name}-bridge: 立て直しに失敗(${detail || `exit ${res.status}`})`)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
line(result.level, result.text)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 5. Lattice 併用モードの工程正本
|
|
170
|
+
if (mode === 'lattice') {
|
|
171
|
+
const latticeCli = process.env.LATTICE_CLI || (typeof setup.lattice_cli === 'string' && setup.lattice_cli) || 'lattice'
|
|
172
|
+
try {
|
|
173
|
+
const command = process.platform === 'win32' && !/\.(cmd|bat|exe)$/i.test(latticeCli) && existsSync(`${latticeCli}.cmd`)
|
|
174
|
+
? `${latticeCli}.cmd`
|
|
175
|
+
: latticeCli
|
|
176
|
+
const out = execFileSync(command, ['status', '--json'], { cwd: proj, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 })
|
|
177
|
+
const status = JSON.parse(out)
|
|
178
|
+
line('OK', `Lattice: state=${status.state} active_runs=${(status.active_runs ?? []).length}`)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
line('NG', `Lattice status を取得できない(${String(error.message ?? error).split('\n')[0]})`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
process.exit(ng)
|
|
185
|
+
NODE
|
|
@@ -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)
|
|
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
|
|
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
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from 'node:fs'
|
|
7
7
|
import { dirname, join, resolve } from 'node:path'
|
|
8
8
|
import { execFileSync } from 'node:child_process'
|
|
9
|
+
import { repairSplicedBegin } from './codex-seat-toml.mjs'
|
|
9
10
|
|
|
10
11
|
const fail = (code, message) => {
|
|
11
12
|
process.stderr.write(`${code}: ${message}\n`)
|
|
@@ -71,6 +72,8 @@ function expectedBlock(addedNewline) {
|
|
|
71
72
|
if (process.env.PATH) explicitEnv.unshift(`PATH = ${JSON.stringify(process.env.PATH)}`)
|
|
72
73
|
return [
|
|
73
74
|
`# BEGIN PEERTABLE ROOM MCP added_newline=${addedNewline ? '1' : '0'}`,
|
|
75
|
+
'approval_policy = "never"',
|
|
76
|
+
'sandbox_mode = "danger-full-access"',
|
|
74
77
|
'[mcp_servers.room]',
|
|
75
78
|
'command = "node"',
|
|
76
79
|
`args = [${JSON.stringify(client)}]`,
|
|
@@ -120,7 +123,7 @@ try {
|
|
|
120
123
|
const missing = requiredSeatEnv.filter((name) => !process.env[name])
|
|
121
124
|
if (missing.length > 0)
|
|
122
125
|
fail('SEAT_CODEX_ROOM_MCP_ENV_MISSING', `seat環境が無い: ${missing.join(',')}`)
|
|
123
|
-
const current = existsSync(configFile) ? readFileSync(configFile, 'utf8') : ''
|
|
126
|
+
const current = repairSplicedBegin(existsSync(configFile) ? readFileSync(configFile, 'utf8') : '')
|
|
124
127
|
const range = markerRange(current)
|
|
125
128
|
let next
|
|
126
129
|
if (range) {
|
|
@@ -107,7 +107,7 @@ sock=""
|
|
|
107
107
|
sess=""
|
|
108
108
|
url=""
|
|
109
109
|
room=""
|
|
110
|
-
|
|
110
|
+
# 席の本人性は room 台帳の member 行が持つ(席file廃止 2026-08-22)。rollback は member DELETE が兼ねる
|
|
111
111
|
cleanup_brief() {
|
|
112
112
|
if [ -n "$brief_file" ]; then rm -f "$brief_file"; fi
|
|
113
113
|
return 0
|
|
@@ -144,13 +144,6 @@ rollback_brief() {
|
|
|
144
144
|
return 1
|
|
145
145
|
fi
|
|
146
146
|
|
|
147
|
-
if [ -n "$seat_file" ] && [ -e "$seat_file" ]; then
|
|
148
|
-
if ! rm -f "$seat_file"; then
|
|
149
|
-
rollback_failed=1
|
|
150
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: seat identity を撤去できない: ${seat_file}" >&2
|
|
151
|
-
fi
|
|
152
|
-
fi
|
|
153
|
-
|
|
154
147
|
if [ -n "$url" ] && [ -n "$room" ]; then
|
|
155
148
|
if ! encoded_name=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name"); then
|
|
156
149
|
rollback_failed=1
|
|
@@ -305,11 +298,6 @@ if ! node "$room_mcp_helper" "$proj" "$peertable_repo" "$mcp_ownership"; then
|
|
|
305
298
|
echo "SEAT_ROOM_MCP_INVALID: Aiterm席のroom clientをcurrent treeへ束縛できない(席は立てない)" >&2
|
|
306
299
|
exit 1
|
|
307
300
|
fi
|
|
308
|
-
if ! credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" prepare "$proj" "$room" "$name"); then
|
|
309
|
-
echo "SEAT_CREDENTIAL_PREPARE_FAILED: 席別credentialを用意できない(席は立てない)" >&2
|
|
310
|
-
exit 1
|
|
311
|
-
fi
|
|
312
|
-
|
|
313
301
|
# 同名の古いroom memberが残っていると、Codexの新しいroom clientが一度も登録していなくても
|
|
314
302
|
# 下のready確認を通ってしまう。古いmemberを別席のまま黙って消すのは危険なので、起動前に同名を
|
|
315
303
|
# conflictとして拒否し、明示的な退席後に再実行させる。一覧を読めない時も着席前に止める。
|
|
@@ -339,6 +327,15 @@ case "$stale_member_rc" in
|
|
|
339
327
|
;;
|
|
340
328
|
esac
|
|
341
329
|
|
|
330
|
+
# credential は同名掃除の**後**に用意する。leave-seat は member ごと credential file
|
|
331
|
+
# (path は project/room/member から決定的)を撤去するので、先に作ると同名再着席の
|
|
332
|
+
# 初回だけ「消えた credential を席へ渡して client が initialize 前に死ぬ」レースになる
|
|
333
|
+
# (実被弾 2026-08-22: さくら再着席の初回必敗・再走で成功、の正体)。
|
|
334
|
+
if ! credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" prepare "$proj" "$room" "$name"); then
|
|
335
|
+
echo "SEAT_CREDENTIAL_PREPARE_FAILED: 席別credentialを用意できない(席は立てない)" >&2
|
|
336
|
+
exit 1
|
|
337
|
+
fi
|
|
338
|
+
|
|
342
339
|
# leave-seat が席専用 GROK_HOME を消す。同名再着席では preflight 後に空になるので、
|
|
343
340
|
# live 起動の直前に auth と ui だけを書き直す(user booth MCP は載せない)。
|
|
344
341
|
if [ "$vendor" = grok ]; then
|
|
@@ -356,7 +353,6 @@ fi
|
|
|
356
353
|
# direct CLI launch へ戻るfallbackは置かない。Aiterm が作る同名PTYと launch receipt が、
|
|
357
354
|
# 以後の `pty_read` / `agent_configure` / room metadata を同じsessionへ束縛する。
|
|
358
355
|
tmux_at kill-session -t "$sess" 2>/dev/null || true
|
|
359
|
-
rm -f "$proj/.team/seats/$name.json"
|
|
360
356
|
|
|
361
357
|
launch_env=(
|
|
362
358
|
"PEERTABLE_URL=$url"
|
|
@@ -420,7 +416,34 @@ then
|
|
|
420
416
|
exit 1
|
|
421
417
|
fi
|
|
422
418
|
aiterm_session_id="$sess"
|
|
423
|
-
|
|
419
|
+
# **launch が返った事実は brief 配達の証拠にならない。** aiterm は TUI が入力受付前
|
|
420
|
+
# (update/trust ダイアログ表示中など)だと prompt を送らず、receipt の event_cursor=null で
|
|
421
|
+
# それを申告する。読まずに配達済み扱いにすると席が白紙のまま「briefed」と報告される
|
|
422
|
+
# (実被弾 2026-08-22: さくら再着席。席は起動したが着任指示ゼロで放置)。
|
|
423
|
+
# event_cursor が数値 → turn 開始をaitermが確認済み。submit_residue=true → 本文は
|
|
424
|
+
# composer に残ったが Enter が落ちた=後段で Enter だけ打ち直す。null → 未送信=後段で貼る。
|
|
425
|
+
brief_in_composer=false
|
|
426
|
+
if [ -n "$brief" ]; then
|
|
427
|
+
brief_receipt_state=$(python3 - "$launch_receipt" <<'PY'
|
|
428
|
+
import json, sys
|
|
429
|
+
r = json.loads(sys.argv[1])
|
|
430
|
+
cursor = r.get('event_cursor')
|
|
431
|
+
residue = r.get('submit_residue')
|
|
432
|
+
print('dispatched' if cursor is not None and residue is not True
|
|
433
|
+
else 'residue' if residue is True else 'not_sent')
|
|
434
|
+
PY
|
|
435
|
+
)
|
|
436
|
+
case "$brief_receipt_state" in
|
|
437
|
+
dispatched) brief_dispatched=true ;;
|
|
438
|
+
residue)
|
|
439
|
+
brief_in_composer=true
|
|
440
|
+
echo "SEAT_BRIEF_SUBMIT_RESIDUE: brief は入力欄に残り submit が落ちた。着席確認後に submit し直す" >&2
|
|
441
|
+
;;
|
|
442
|
+
*)
|
|
443
|
+
echo "SEAT_BRIEF_LAUNCH_PROMPT_NOT_SENT: aiterm は TUI 入力受付前で brief を送っていない。着席確認後に貼り直す" >&2
|
|
444
|
+
;;
|
|
445
|
+
esac
|
|
446
|
+
fi
|
|
424
447
|
seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
|
|
425
448
|
|
|
426
449
|
# Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
|
|
@@ -441,42 +464,46 @@ codex_hooks_accepted=false
|
|
|
441
464
|
codex_update_accepted=false
|
|
442
465
|
codex_trust_accepted=false
|
|
443
466
|
claude_trust_accepted=false
|
|
467
|
+
codex_dialog_helper="$peertable_script_dir/codex-dialog.mjs"
|
|
468
|
+
pass_codex_pane() {
|
|
469
|
+
local json key
|
|
470
|
+
json=$(printf '%s' "$1" | node "$codex_dialog_helper" || true)
|
|
471
|
+
[ -n "$json" ] && [ "$json" != "null" ] || return 1
|
|
472
|
+
while IFS= read -r key; do
|
|
473
|
+
[ -n "$key" ] || continue
|
|
474
|
+
tmux_at send-keys -t "$sess" "$key" || return 1
|
|
475
|
+
done < <(python3 -c 'import json,sys
|
|
476
|
+
a=json.loads(sys.stdin.read() or "null")
|
|
477
|
+
print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json")
|
|
478
|
+
return 0
|
|
479
|
+
}
|
|
480
|
+
codex_pane_blocks_ready() {
|
|
481
|
+
printf '%s' "$1" | node "$codex_dialog_helper" --ready-ok
|
|
482
|
+
}
|
|
444
483
|
while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
445
484
|
# Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
|
|
446
485
|
# Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
|
|
447
486
|
# 汎用的に承認するfallbackにはしない。承認後のMCP初期化時間は改めて30秒確保する。
|
|
448
487
|
if [ "$vendor" = codex ]; then
|
|
449
488
|
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
450
|
-
if
|
|
451
|
-
|
|
452
|
-
*"Update now"*)
|
|
453
|
-
if tmux_at send-keys -t "$sess" Down && tmux_at send-keys -t "$sess" Enter; then
|
|
454
|
-
codex_update_accepted=true
|
|
455
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
456
|
-
echo "codex update prompt: skipped"
|
|
457
|
-
fi
|
|
458
|
-
;;
|
|
459
|
-
esac
|
|
460
|
-
fi
|
|
461
|
-
if [ "$codex_hooks_accepted" != true ]; then
|
|
489
|
+
if pass_codex_pane "$codex_screen"; then
|
|
490
|
+
room_ready_deadline=$((SECONDS + 90))
|
|
462
491
|
case "$codex_screen" in
|
|
492
|
+
*"Allow the room MCP server to run tool"*) echo "codex mcp allow: always allow" ;;
|
|
463
493
|
*"Hooks need review"*)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
494
|
+
codex_hooks_accepted=true
|
|
495
|
+
echo "codex hooks prompt: trust all"
|
|
496
|
+
;;
|
|
497
|
+
*"Update now"*)
|
|
498
|
+
codex_update_accepted=true
|
|
499
|
+
echo "codex update prompt: skipped"
|
|
469
500
|
;;
|
|
470
|
-
esac
|
|
471
|
-
fi
|
|
472
|
-
if [ "$codex_trust_accepted" != true ]; then
|
|
473
|
-
case "$codex_screen" in
|
|
474
501
|
*"Yes, continue"*)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
502
|
+
codex_trust_accepted=true
|
|
503
|
+
echo "codex directory trust: accepted"
|
|
504
|
+
;;
|
|
505
|
+
*"Would you like to run the following command?"*)
|
|
506
|
+
echo "codex command approval: don't ask again"
|
|
480
507
|
;;
|
|
481
508
|
esac
|
|
482
509
|
fi
|
|
@@ -529,6 +556,13 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
529
556
|
fi
|
|
530
557
|
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
531
558
|
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
|
+
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
561
|
+
if ! codex_pane_blocks_ready "$codex_screen"; then
|
|
562
|
+
sleep 1
|
|
563
|
+
continue
|
|
564
|
+
fi
|
|
565
|
+
fi
|
|
532
566
|
room_ready=true
|
|
533
567
|
break
|
|
534
568
|
fi
|
|
@@ -541,6 +575,24 @@ if [ "$room_ready" != true ]; then
|
|
|
541
575
|
exit 1
|
|
542
576
|
fi
|
|
543
577
|
echo "room ready: ${room}/${name}"
|
|
578
|
+
if [ "$vendor" = codex ]; then
|
|
579
|
+
# member 登録後の最初の tool 呼び出しで MCP Allow が出る。出ている間だけ通し、沈黙5秒で抜ける。
|
|
580
|
+
codex_post_ready_deadline=$((SECONDS + 90))
|
|
581
|
+
codex_post_ready_idle=0
|
|
582
|
+
while [ $SECONDS -lt "$codex_post_ready_deadline" ]; do
|
|
583
|
+
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
584
|
+
if pass_codex_pane "$codex_screen"; then
|
|
585
|
+
case "$codex_screen" in
|
|
586
|
+
*"Allow the room MCP server to run tool"*) echo "codex mcp allow: always allow" ;;
|
|
587
|
+
esac
|
|
588
|
+
codex_post_ready_idle=0
|
|
589
|
+
else
|
|
590
|
+
codex_post_ready_idle=$((codex_post_ready_idle + 1))
|
|
591
|
+
[ "$codex_post_ready_idle" -ge 5 ] && break
|
|
592
|
+
fi
|
|
593
|
+
sleep 1
|
|
594
|
+
done
|
|
595
|
+
fi
|
|
544
596
|
if [ "$brief_dispatched" = true ]; then
|
|
545
597
|
brief_completed=true
|
|
546
598
|
echo "briefed: ${sess}(Aiterm launch prompt)"
|
|
@@ -575,17 +627,20 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
575
627
|
sleep 1
|
|
576
628
|
|
|
577
629
|
brief_before=$(tmux_at capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
|
|
578
|
-
|
|
579
|
-
if
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
630
|
+
# submit_residue の席は本文が composer に残っている。貼り直すと二重になるので Enter だけ打つ。
|
|
631
|
+
if [ "$brief_in_composer" != true ]; then
|
|
632
|
+
brief_buffer="peertable-brief-${name}-$$"
|
|
633
|
+
if ! tmux_at load-buffer -b "$brief_buffer" "$brief_file"; then
|
|
634
|
+
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux buffer 読み込みに失敗(席は着席済み)" >&2
|
|
635
|
+
exit 1
|
|
636
|
+
fi
|
|
637
|
+
if ! tmux_at paste-buffer -b "$brief_buffer" -d -t "$sess"; then
|
|
638
|
+
tmux_at delete-buffer -b "$brief_buffer" 2>/dev/null || true
|
|
639
|
+
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux paste に失敗(席は着席済み)" >&2
|
|
640
|
+
exit 1
|
|
641
|
+
fi
|
|
642
|
+
sleep 1
|
|
587
643
|
fi
|
|
588
|
-
sleep 1
|
|
589
644
|
if ! tmux_at send-keys -t "$sess" Enter; then
|
|
590
645
|
echo "LAUNCH_BRIEF_SEND_FAILED: brief の submit に失敗(席は着席済み)" >&2
|
|
591
646
|
exit 1
|
|
@@ -614,15 +669,12 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
614
669
|
fi
|
|
615
670
|
fi
|
|
616
671
|
|
|
617
|
-
#
|
|
618
|
-
#
|
|
672
|
+
# 席の本人性(pid / 起動時刻 / argv digest)を room 台帳の member 行へ登録する。
|
|
673
|
+
# member に帰属する情報の正本は台帳だけ(オーナー裁定 2026-08-22・席fileは廃止)。
|
|
619
674
|
# 着席の**後**に取る——起動途中の process を掴むと、ダイアログ通過で子が入れ替わりうる。
|
|
620
|
-
#
|
|
621
|
-
#
|
|
622
|
-
#
|
|
623
|
-
# **raw argv を持たせない**——秘密値はargvから除いたが、将来の引数も含めて複製しない。digest だけを持つ。
|
|
624
|
-
# この file が主張するのは「この pid はこの席だった」という**識別**であって、生死ではない。
|
|
625
|
-
# 生きているかは attach する側(Lattice)が lstart+argv の再観測で確かめる。
|
|
675
|
+
# **raw argv を持たせない**——digest だけを持つ。この記録が主張するのは「この pid は
|
|
676
|
+
# この席だった」という**識別**であって、生死ではない。生きているかは attach する側
|
|
677
|
+
# (Lattice)が lstart+argv の再観測で確かめる。
|
|
626
678
|
seat_pid=""
|
|
627
679
|
pane_pid=$(tmux_at list-panes -t "$sess" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
|
|
628
680
|
seat_ident=""
|
|
@@ -639,80 +691,45 @@ if [ -z "$seat_pid" ]; then
|
|
|
639
691
|
# 記録が無ければ席は attach できず、装置の介入は協調 hold のままになる。**黙らない。**
|
|
640
692
|
echo "seat identity を記録できなかった: ${sess} の process group leader を1つに確定できない(席は着席済み)" >&2
|
|
641
693
|
else
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
# 上の env_line が入れる `LATTICE_TODO_ACTOR_SESSION=$name` である。tmux 識別子を混ぜると
|
|
646
|
-
# attach が必ず `WORKER_ACTOR_MISMATCH` で拒否される(mio の監査で発覚・room [937])。
|
|
647
|
-
if ! python3 - "$proj/.team/seats/$name.json" "$name" "$name" "$seat_ident" <<'PY'
|
|
648
|
-
import hashlib, json, os, subprocess, sys, tempfile
|
|
649
|
-
out, name, session, ident_raw = sys.argv[1:5]
|
|
694
|
+
ident_body=$(python3 - "$name" "$seat_ident" <<'PY'
|
|
695
|
+
import json, subprocess, sys
|
|
696
|
+
name, ident_raw = sys.argv[1:3]
|
|
650
697
|
ident = json.loads(ident_raw)
|
|
651
|
-
|
|
652
|
-
started = ident['started_identity']
|
|
653
|
-
argv = ident['argv']
|
|
654
|
-
if not started or not argv:
|
|
698
|
+
if not ident.get('started_identity') or not ident.get('argv') or not ident.get('argv_digest'):
|
|
655
699
|
sys.exit('pid の lstart/args を観測できない')
|
|
656
|
-
|
|
657
|
-
'argv_digest': hashlib.sha256(argv.encode()).hexdigest(),
|
|
700
|
+
print(json.dumps({
|
|
658
701
|
'name': name,
|
|
659
|
-
'pid': pid,
|
|
660
|
-
'
|
|
661
|
-
|
|
662
|
-
'
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
# canonical JSON(key 昇順・空白なし)+ 0600 + 一時file→fsync→rename で原子的に置く。
|
|
666
|
-
# 着席直後に席が読むので、部分読取が起きない形にする。
|
|
667
|
-
body = json.dumps(record, ensure_ascii=False, sort_keys=True, separators=(',', ':')) + '\n'
|
|
668
|
-
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(out), prefix='.seat-', suffix='.tmp')
|
|
669
|
-
try:
|
|
670
|
-
with os.fdopen(fd, 'w') as handle:
|
|
671
|
-
handle.write(body)
|
|
672
|
-
handle.flush()
|
|
673
|
-
os.fsync(handle.fileno())
|
|
674
|
-
os.chmod(tmp, 0o600)
|
|
675
|
-
os.replace(tmp, out)
|
|
676
|
-
except BaseException:
|
|
677
|
-
os.unlink(tmp)
|
|
678
|
-
raise
|
|
702
|
+
'pid': int(ident['pid']),
|
|
703
|
+
'started_identity': ident['started_identity'],
|
|
704
|
+
'argv_digest': ident['argv_digest'],
|
|
705
|
+
'identity_recorded_at': subprocess.run(['date', '-u', '+%Y-%m-%dT%H:%M:%S.000Z'],
|
|
706
|
+
capture_output=True, text=True, check=True).stdout.strip(),
|
|
707
|
+
}, ensure_ascii=False))
|
|
679
708
|
PY
|
|
680
|
-
|
|
681
|
-
|
|
709
|
+
) || ident_body=""
|
|
710
|
+
if [ -z "$ident_body" ] || ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
711
|
+
"$url/api/$room/members" "$ident_body" >/dev/null; then
|
|
712
|
+
echo "seat identity を台帳へ登録できなかった: ${sess}(席は着席済み・attach は協調 hold のままになる)" >&2
|
|
682
713
|
fi
|
|
683
714
|
fi
|
|
684
715
|
|
|
685
|
-
#
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
690
|
-
"$url/api/$room/members" "$meta" >/dev/null; then
|
|
691
|
-
# **200 は保存の証拠にならない**。素性欄を知らない server も 200 {"ok":true} を返して黙って捨てる
|
|
692
|
-
# (登録が `if (!members.has(name))` の no-op になる経路もある)。読み返して実際に載ったかを見る。
|
|
693
|
-
# **パイプと heredoc を同じ stdin へ重ねない**。`curl | python3 - <<'PY'` は
|
|
694
|
-
# 「プログラムを stdin から読む」と「データを stdin から読む」が衝突して、必ず失敗する
|
|
695
|
-
# (そして try/except で包むと、失敗が「保存されていない」という**もっともらしい答え**に化ける。実測)
|
|
696
|
-
listing=$(curl -sf "$url/api/$room/members" || true)
|
|
697
|
-
stored=$(python3 - "$name" "$listing" <<'PY'
|
|
716
|
+
# 素性(vendor/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
|
|
717
|
+
# ランチャーはここで台帳を読み返し、実際に載ったかを確認するだけにする(重複書込の禁止)。
|
|
718
|
+
member_row=$(curl -sf "$url/api/$room/members/$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$name")" || true)
|
|
719
|
+
stored=$(python3 - "$member_row" <<'PY'
|
|
698
720
|
import json, sys
|
|
699
|
-
# 読み返しに失敗しても生の traceback を出さない。ここは「保存されたか」を見るだけの確認段で、
|
|
700
|
-
# 判定不能は「保存されていない」と同じ扱いでよい(席は既に着席している)
|
|
701
721
|
try:
|
|
702
|
-
|
|
722
|
+
m = json.loads(sys.argv[1])['member']
|
|
703
723
|
except Exception:
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
724
|
+
m = {}
|
|
725
|
+
print('yes' if m.get('model') and m.get('roles') and m.get('aiterm_session_id')
|
|
726
|
+
and (m.get('observe') or {}).get('tmux_target') and m.get('pid') else 'no')
|
|
707
727
|
PY
|
|
708
728
|
)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
else
|
|
712
|
-
echo "metadata は保存されなかった: この room サーバーは素性欄を持たない版(席は着席済み・一覧に素性が出ないだけ)" >&2
|
|
713
|
-
fi
|
|
729
|
+
if [ "$stored" = yes ]; then
|
|
730
|
+
echo "台帳確認: ${vendor} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
|
|
714
731
|
else
|
|
715
|
-
echo "
|
|
732
|
+
echo "SEAT_LEDGER_INCOMPLETE: 台帳の member 行に素性または本人性が欠けている(席は着席済み。doctor で確認)" >&2
|
|
716
733
|
fi
|
|
717
734
|
|
|
718
735
|
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" seat-status; then
|
|
@@ -76,10 +76,7 @@ if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_
|
|
|
76
76
|
failed=1
|
|
77
77
|
fi
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
echo "SEAT_LEAVE_IDENTITY_FAILED: $name" >&2
|
|
81
|
-
failed=1
|
|
82
|
-
fi
|
|
79
|
+
# 席の本人性は台帳の member 行が持つ(席file廃止 2026-08-22)。上の member DELETE が identity も撤去する
|
|
83
80
|
rm -rf "$proj/.team/seats/${name}.grok-home"
|
|
84
81
|
rm -rf "$proj/.team/seats/${name}.codex"
|
|
85
82
|
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" remove "$proj" "$credential_file"; then
|