peertable 0.8.55 → 0.8.57
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 +32 -33
- package/README.md +36 -37
- package/package.json +3 -1
- package/room/client.mjs +24 -33
- package/room/server.mjs +1 -1
- package/skill/SKILL.md +69 -94
- package/skill/scripts/aiterm-client.mjs +48 -0
- package/skill/scripts/aiterm-deliver.mjs +3 -1
- package/skill/scripts/change-seat.mjs +86 -0
- package/skill/scripts/change-seat.sh +3 -276
- package/skill/scripts/change-seat.test.mjs +129 -0
- package/skill/scripts/cli.mjs +77 -0
- package/skill/scripts/doctor.mjs +53 -0
- package/skill/scripts/doctor.sh +2 -213
- package/skill/scripts/ensure-all-bridges.mjs +42 -0
- package/skill/scripts/ensure-all-bridges.sh +3 -80
- package/skill/scripts/ensure-bridge.mjs +11 -0
- package/skill/scripts/ensure-bridge.sh +2 -95
- package/skill/scripts/ensure-codex-room-mcp.mjs +1 -1
- package/skill/scripts/ensure-project-runtime.mjs +85 -0
- package/skill/scripts/ensure-project-runtime.sh +2 -11
- package/skill/scripts/ensure-project-runtime.test.mjs +40 -0
- package/skill/scripts/install-skill.mjs +89 -0
- package/skill/scripts/install-skill.test.mjs +84 -0
- package/skill/scripts/launch-seat.mjs +163 -0
- package/skill/scripts/launch-seat.sh +3 -797
- package/skill/scripts/launch-seat.test.mjs +174 -0
- package/skill/scripts/leave-seat.mjs +44 -0
- package/skill/scripts/leave-seat.sh +3 -92
- package/skill/scripts/leave-seat.test.mjs +64 -0
- package/skill/scripts/legacy-entry.mjs +33 -0
- package/skill/scripts/platform/windows/resolve-lattice-command.mjs +12 -1
- package/skill/scripts/project-runtime.mjs +95 -0
- package/skill/scripts/project-runtime.test.mjs +29 -0
- package/skill/scripts/project-scaffold.mjs +179 -0
- package/skill/scripts/project-scaffold.test.mjs +129 -0
- package/skill/scripts/refresh-seat-identity.mjs +11 -8
- package/skill/scripts/remove-managed-room-mcp.mjs +9 -4
- package/skill/scripts/resume.sh +2 -225
- package/skill/scripts/room-api.mjs +28 -0
- package/skill/scripts/room-public-session.test.mjs +73 -0
- package/skill/scripts/runtime-contract.test.mjs +13 -146
- package/skill/scripts/runtime-launch-command.mjs +19 -0
- package/skill/scripts/runtime-launch-command.test.mjs +32 -0
- package/skill/scripts/seat-approval.mjs +24 -0
- package/skill/scripts/seat-approval.test.mjs +43 -0
- package/skill/scripts/seat-identity.mjs +2 -71
- package/skill/scripts/seat-launch-phase.mjs +11 -0
- package/skill/scripts/seat-observer.mjs +54 -0
- package/skill/scripts/seat-observer.test.mjs +58 -0
- package/skill/scripts/seat-session.mjs +17 -0
- package/skill/scripts/seat-status-bridge.mjs +33 -206
- package/skill/scripts/seat-usage.mjs +3 -334
- package/skill/scripts/setup.sh +3 -215
- package/skill/scripts/teardown.mjs +113 -0
- package/skill/scripts/teardown.sh +3 -410
- package/skill/scripts/teardown.test.mjs +62 -0
- package/skill/scripts/upgrade-team-assets.mjs +268 -0
- package/skill/scripts/upgrade-team-assets.sh +2 -282
- package/skill/scripts/wakeup-bridge.mjs +26 -32
- package/skill/scripts/wakeup-delivery.mjs +4 -14
- package/skill/templates/charter.md +1 -1
- package/skill/templates/member-standalone.md +1 -1
- package/skill/templates/member.md +3 -3
- package/skill/scripts/agent-pane-status.mjs +0 -6
- package/skill/scripts/aiterm-configure.mjs +0 -31
- package/skill/scripts/aiterm-launch.mjs +0 -64
- package/skill/scripts/aiterm-send.mjs +0 -22
- package/skill/scripts/claude-dialog.mjs +0 -26
- package/skill/scripts/codex-dialog.mjs +0 -67
- package/skill/scripts/platform/windows/build-bridge-command.mjs +0 -52
- package/skill/scripts/seat-input.mjs +0 -36
- package/skill/scripts/tmux-at.bash +0 -22
- package/skill/scripts/tmux-socket.mjs +0 -19
- package/skill/scripts/vendors/grok/pane-status.mjs +0 -30
|
@@ -1,798 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# --model: 任意。省略時は先頭役割の着席可能な1位。指定時は表外でも通す
|
|
6
|
-
# brief: 着席が成立したら送る着任指示(省略時は送らない)
|
|
7
|
-
set -e
|
|
8
|
-
usage() {
|
|
9
|
-
echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--harness <harness>] [brief]" >&2
|
|
10
|
-
}
|
|
11
|
-
proj="$1"; name="$2"
|
|
12
|
-
[ -n "$proj" ] && [ -n "$name" ] || { usage; exit 1; }
|
|
13
|
-
shift 2
|
|
14
|
-
brief=""
|
|
15
|
-
roles=""; mission=""
|
|
16
|
-
opt_model=""; opt_harness=""; opt_effort=""
|
|
17
|
-
while [ $# -gt 0 ]; do
|
|
18
|
-
case "$1" in
|
|
19
|
-
--roles) roles="${2:-}"; shift 2 ;;
|
|
20
|
-
--mission) mission="${2:-}"; shift 2 ;;
|
|
21
|
-
--model) opt_model="${2:-}"; shift 2 ;;
|
|
22
|
-
--harness|--vendor) opt_harness="${2:-}"; shift 2 ;; # --vendor は旧名互換
|
|
23
|
-
--effort) opt_effort="${2:-}"; shift 2 ;;
|
|
24
|
-
--*) echo "SEAT_LAUNCH_ARGS_INVALID: 不明な引数 $1" >&2; usage; exit 1 ;;
|
|
25
|
-
*)
|
|
26
|
-
if [ -z "$roles" ]; then roles="$1"; shift
|
|
27
|
-
elif [ -z "$brief" ]; then brief="$1"; shift
|
|
28
|
-
else echo "SEAT_LAUNCH_ARGS_INVALID: 余分な引数 $1" >&2; usage; exit 1
|
|
29
|
-
fi
|
|
30
|
-
;;
|
|
31
|
-
esac
|
|
32
|
-
done
|
|
33
|
-
[ -n "$roles" ] || { usage; exit 1; }
|
|
34
|
-
|
|
35
|
-
if [ -n "${PEERTABLE_MEMBER:-}" ]; then
|
|
36
|
-
echo "SEAT_LAUNCH_DELEGATED_CHILD_FORBIDDEN: PEERTABLE_MEMBER=${PEERTABLE_MEMBER} を継承した呼出元からの席起動を拒否" >&2
|
|
37
|
-
exit 1
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
# 呼出元が古い手順でtokenをexportしていても、preflight・tmux・model CLIへ継承しない。
|
|
41
|
-
# 値の解決はcredential helperだけが設定fileから行う。env/argvへのfallbackは持たない。
|
|
42
|
-
unset PEERTABLE_POST_TOKEN
|
|
43
|
-
credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$(dirname "$0")/seat-credential.mjs}"
|
|
44
|
-
room_mcp_helper="${PEERTABLE_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-room-mcp.mjs}"
|
|
45
|
-
codex_room_mcp_helper="${PEERTABLE_CODEX_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-codex-room-mcp.mjs}"
|
|
46
|
-
aiterm_launch_helper="${PEERTABLE_AITERM_LAUNCH_HELPER:-$(dirname "$0")/aiterm-launch.mjs}"
|
|
47
|
-
placement_helper="${PEERTABLE_PLACEMENT_HELPER:-$(dirname "$0")/resolve-seat-placement.mjs}"
|
|
48
|
-
# shellcheck disable=SC1091
|
|
49
|
-
. "$(dirname "$0")/tmux-at.bash"
|
|
50
|
-
# 先に script 自身の物理パスを確定してから遡る。~/.claude/skills/peertable が
|
|
51
|
-
# <repo>/skill へのシンボリックリンクなので、`dirname "$0"` へ `../..` を付けてから
|
|
52
|
-
# 一度に cd すると、bash が `..` をリンク先ではなく論理パス上で畳み、
|
|
53
|
-
# repo ではなく ~/.claude/skills を指す(skill 経由=正規の呼び方だけで壊れる)。
|
|
54
|
-
peertable_script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
55
|
-
peertable_repo="${PEERTABLE_REPO:-$(cd "$peertable_script_dir/../.." && pwd -P)}"
|
|
56
|
-
peertable_client="$peertable_repo/room/client.mjs"
|
|
57
|
-
if [ ! -f "$peertable_client" ]; then
|
|
58
|
-
echo "SEAT_PEERTABLE_TREE_UNRESOLVED: room client が見つからない: $peertable_client(script=$0)" >&2
|
|
59
|
-
exit 1
|
|
60
|
-
fi
|
|
61
|
-
credential_file=""
|
|
62
|
-
credential_persist=false
|
|
63
|
-
aiterm_session_id=""
|
|
64
|
-
|
|
65
|
-
placement_argv=(--roles "$roles")
|
|
66
|
-
[ -n "$opt_model" ] && placement_argv+=(--model "$opt_model")
|
|
67
|
-
[ -n "$opt_effort" ] && placement_argv+=(--effort "$opt_effort")
|
|
68
|
-
[ -n "$opt_harness" ] && placement_argv+=(--harness "$opt_harness")
|
|
69
|
-
placement=$(node "$placement_helper" "${placement_argv[@]}") || exit "$?"
|
|
70
|
-
IFS=$'\t' read -r model harness effort role <<EOF
|
|
71
|
-
$(python3 -c 'import json,sys
|
|
72
|
-
p=json.load(sys.stdin)
|
|
73
|
-
s=p.get("settings") or {}
|
|
74
|
-
roles=",".join(p.get("roles") or [])
|
|
75
|
-
print("\t".join((s.get("model") or "", s.get("harness") or "", s.get("effort") or "", roles)))' <<<"$placement")
|
|
76
|
-
EOF
|
|
77
|
-
echo "SEAT_PLACEMENT: ${role} → ${harness} / ${model}${effort:+ / $effort}"
|
|
78
|
-
python3 -c 'import json,sys
|
|
79
|
-
p=json.load(sys.stdin)
|
|
80
|
-
for item in p.get("dropped") or []:
|
|
81
|
-
print("SEAT_PLACEMENT_DROPPED: rank %s %s %s" % (item.get("rank"), item.get("reason"), item.get("cell") or ""), file=sys.stderr)
|
|
82
|
-
' <<<"$placement" || true
|
|
83
|
-
case "$harness" in
|
|
84
|
-
claude|codex|grok) ;;
|
|
85
|
-
*) echo "SEAT_LAUNCH_HARNESS_UNSUPPORTED: harness=${harness}" >&2; exit 1 ;;
|
|
86
|
-
esac
|
|
87
|
-
if [ "$harness" = "claude" ] && [ -n "$effort" ]; then
|
|
88
|
-
case "$effort" in
|
|
89
|
-
low|medium|high|xhigh|max) ;;
|
|
90
|
-
*) echo "unknown effort: ${effort}(claude は low|medium|high|xhigh|max)" >&2; exit 1 ;;
|
|
91
|
-
esac
|
|
92
|
-
fi
|
|
93
|
-
|
|
94
|
-
# brief は tmux のコマンド引数へ直接載せない。長さを着席前に検証してから一時 file へ置き、
|
|
95
|
-
# tmux の buffer 経由で貼る。入力を受理できない時は model preflight・tmux・member 登録を
|
|
96
|
-
# 一つも行わず、何が拒否されたかを機械的に読める形で返す。
|
|
97
|
-
brief_file=""
|
|
98
|
-
brief_max_bytes=65536
|
|
99
|
-
brief_completed=false
|
|
100
|
-
brief_dispatched=false
|
|
101
|
-
brief_sent=false
|
|
102
|
-
brief_event_cursor=""
|
|
103
|
-
seat_created=false
|
|
104
|
-
rollback_done=false
|
|
105
|
-
sock=""
|
|
106
|
-
sess=""
|
|
107
|
-
url=""
|
|
108
|
-
room=""
|
|
109
|
-
# 席の本人性は room 台帳の member 行が持つ(席file廃止 2026-08-22)。rollback は member DELETE が兼ねる
|
|
110
|
-
cleanup_brief() {
|
|
111
|
-
if [ -n "$brief_file" ]; then rm -f "$brief_file"; fi
|
|
112
|
-
return 0
|
|
113
|
-
}
|
|
114
|
-
# brief を受け付けて送信した後に turn が始まらなかった場合は、作りかけの席を残さない。
|
|
115
|
-
# tmux を先に落とし、client が再登録しない状態にしてから room member を解除する。
|
|
116
|
-
# DELETE は idempotent だが、一覧の読み返しまで通らなければ rollback 成功とは言わない。
|
|
117
|
-
rollback_brief() {
|
|
118
|
-
local original_rc="${1:-1}"
|
|
119
|
-
local rollback_failed=0
|
|
120
|
-
local encoded_name member_code listing
|
|
121
|
-
|
|
122
|
-
if [ -n "$sess" ] && [ "$seat_created" = true ]; then
|
|
123
|
-
if tmux_at has-session -t "$sess" 2>/dev/null; then
|
|
124
|
-
if ! tmux_at kill-session -t "$sess" 2>/dev/null; then
|
|
125
|
-
rollback_failed=1
|
|
126
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session を停止できない: ${sess}" >&2
|
|
127
|
-
elif tmux_at has-session -t "$sess" 2>/dev/null; then
|
|
128
|
-
rollback_failed=1
|
|
129
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session が停止後も残っている: ${sess}" >&2
|
|
130
|
-
fi
|
|
131
|
-
elif tmux_at list-sessions >/dev/null 2>&1; then
|
|
132
|
-
: # serverへ到達でき、対象sessionが無い
|
|
133
|
-
else
|
|
134
|
-
rollback_failed=1
|
|
135
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session を観測できない: ${sess}" >&2
|
|
136
|
-
fi
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
if [ "$rollback_failed" -ne 0 ]; then
|
|
140
|
-
# live clientを止めたと確認できない時は、member/identity/credentialを先に消して
|
|
141
|
-
# rollback済みに見せない。credential cleanupはon_exitにもさせない。
|
|
142
|
-
credential_persist=true
|
|
143
|
-
return 1
|
|
144
|
-
fi
|
|
145
|
-
|
|
146
|
-
if [ -n "$url" ] && [ -n "$room" ]; then
|
|
147
|
-
if ! encoded_name=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name"); then
|
|
148
|
-
rollback_failed=1
|
|
149
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: member 名を URL 化できない: ${name}" >&2
|
|
150
|
-
else
|
|
151
|
-
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" DELETE \
|
|
152
|
-
"$url/api/$room/members/$encoded_name" >/dev/null; then
|
|
153
|
-
rollback_failed=1
|
|
154
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: room member を解除できない" >&2
|
|
155
|
-
fi
|
|
156
|
-
listing=$(curl -sf "$url/api/$room/members" || true)
|
|
157
|
-
if ! printf '%s' "$listing" | python3 -c 'import json,sys; name=sys.argv[1]; members=json.load(sys.stdin).get("members",[]); raise SystemExit(0 if not any(m.get("name") == name for m in members) else 1)' "$name"; then
|
|
158
|
-
rollback_failed=1
|
|
159
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: room member の解除を読み返せない: ${name}" >&2
|
|
160
|
-
fi
|
|
161
|
-
fi
|
|
162
|
-
else
|
|
163
|
-
rollback_failed=1
|
|
164
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: room の rollback 境界が未解決" >&2
|
|
165
|
-
fi
|
|
166
|
-
|
|
167
|
-
if [ "$rollback_failed" -ne 0 ]; then
|
|
168
|
-
return 1
|
|
169
|
-
fi
|
|
170
|
-
echo "LAUNCH_BRIEF_ROLLED_BACK: ${sess}(tmux / room member / seat identity を撤去)" >&2
|
|
171
|
-
return "$original_rc"
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
on_exit() {
|
|
175
|
-
local exit_rc=$?
|
|
176
|
-
local rollback_rc
|
|
177
|
-
trap - EXIT
|
|
178
|
-
cleanup_brief
|
|
179
|
-
if [ "$seat_created" = true ] && [ "$brief_completed" != true ] && [ "$rollback_done" != true ]; then
|
|
180
|
-
rollback_done=true
|
|
181
|
-
if rollback_brief "$exit_rc"; then
|
|
182
|
-
:
|
|
183
|
-
else
|
|
184
|
-
rollback_rc=$?
|
|
185
|
-
[ "$exit_rc" -eq 0 ] && exit_rc="$rollback_rc"
|
|
186
|
-
fi
|
|
187
|
-
fi
|
|
188
|
-
if [ -n "$credential_file" ] && [ "$credential_persist" != true ]; then
|
|
189
|
-
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" remove "$proj" "$credential_file"; then
|
|
190
|
-
echo "SEAT_CREDENTIAL_ROLLBACK_FAILED: ${credential_file}" >&2
|
|
191
|
-
[ "$exit_rc" -eq 0 ] && exit_rc=1
|
|
192
|
-
fi
|
|
193
|
-
fi
|
|
194
|
-
exit "$exit_rc"
|
|
195
|
-
}
|
|
196
|
-
trap on_exit EXIT
|
|
197
|
-
if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
198
|
-
brief_bytes=$(printf '%s' "$brief" | LC_ALL=C wc -c | tr -d '[:space:]')
|
|
199
|
-
case "$brief_bytes" in
|
|
200
|
-
''|*[!0-9]*) echo "LAUNCH_BRIEF_INVALID: brief の byte 長を測定できない(席は立てない)" >&2; exit 2 ;;
|
|
201
|
-
esac
|
|
202
|
-
if [ "$brief_bytes" -gt "$brief_max_bytes" ]; then
|
|
203
|
-
echo "LAUNCH_BRIEF_TOO_LONG: brief が ${brief_bytes} bytes(上限 ${brief_max_bytes} bytes・席は立てない)" >&2
|
|
204
|
-
exit 2
|
|
205
|
-
fi
|
|
206
|
-
if ! brief_file=$(mktemp "${TMPDIR:-/tmp}/peertable-brief.XXXXXX"); then
|
|
207
|
-
echo "LAUNCH_BRIEF_PREPARE_FAILED: brief の輸送 file を作れない(席は立てない)" >&2
|
|
208
|
-
exit 2
|
|
209
|
-
fi
|
|
210
|
-
if ! printf '%s' "$brief" >"$brief_file"; then
|
|
211
|
-
echo "LAUNCH_BRIEF_PREPARE_FAILED: brief を輸送 file へ書けない(席は立てない)" >&2
|
|
212
|
-
exit 2
|
|
213
|
-
fi
|
|
214
|
-
fi
|
|
215
|
-
|
|
216
|
-
# 既存卓も正規着座入口の一回で現行generated assetへ収束させる。alarm輸送helperや
|
|
217
|
-
# member規約の更新を利用側の事前resume順序へ委ねない。
|
|
218
|
-
"$peertable_script_dir/upgrade-team-assets.sh" "$proj"
|
|
219
|
-
|
|
220
|
-
# **画面の文字列は「model が使えること」を意味しない。** 2026-08-11 実測: fable-5 の席は
|
|
221
|
-
# Claude の channels バナーが出たので下の着席判定を通ったが、その後の入力は全て
|
|
222
|
-
# model unavailable で 0 秒失敗し、席は一度も仕事をできなかった(room [11])。
|
|
223
|
-
# バナーは CLI が起動したことしか言わないので、**live に応答するかは非対話入口で先に測る**。
|
|
224
|
-
# 席を畳む前に測るのが要点である——ここで落ちれば、動いている席を殺さずに済む。
|
|
225
|
-
export PATH="${HOME}/.grok/bin:${PATH}"
|
|
226
|
-
preflight_dir="${TMPDIR:-/tmp}"
|
|
227
|
-
case "$harness" in
|
|
228
|
-
claude) preflight_cmd=(claude --model "$model" -p "ping") ;;
|
|
229
|
-
codex) preflight_cmd=(codex exec --model "$model" --skip-git-repo-check "ping") ;;
|
|
230
|
-
grok)
|
|
231
|
-
grok_bin=$(command -v grok || true)
|
|
232
|
-
if [ -z "$grok_bin" ]; then
|
|
233
|
-
echo "SEAT_GROK_CLI_MISSING: grok が PATH に無い(席は立てない)" >&2
|
|
234
|
-
exit 1
|
|
235
|
-
fi
|
|
236
|
-
# 通常 GROK_HOME の config は booth 等 MCP を起動する。preflight がそれを待つと
|
|
237
|
-
# 120 秒で空ログのまま死ぬ(2026-08-20)。live 席も同じ config を読むと
|
|
238
|
-
# lattice MCP が数分固まる。認証だけ借りて MCP 無しの席専用 HOME を残す。
|
|
239
|
-
grok_home="${proj}/.team/seats/${name}.grok-home"
|
|
240
|
-
rm -rf "$grok_home"
|
|
241
|
-
mkdir -p "$grok_home"
|
|
242
|
-
if [ ! -f "${HOME}/.grok/auth.json" ]; then
|
|
243
|
-
echo "SEAT_GROK_AUTH_MISSING: ${HOME}/.grok/auth.json が無い(席は立てない)" >&2
|
|
244
|
-
exit 1
|
|
245
|
-
fi
|
|
246
|
-
cp "${HOME}/.grok/auth.json" "${grok_home}/auth.json"
|
|
247
|
-
node "$peertable_script_dir/grok-seat-config.mjs" "$proj" "${grok_home}/config.toml"
|
|
248
|
-
echo "grok preflight: GROK_HOME=${grok_home} ${grok_bin} --model ${model} --reasoning-effort ${effort} -p ping"
|
|
249
|
-
preflight_cmd=(env GROK_HOME="$grok_home" "$grok_bin" --model "$model" --reasoning-effort "$effort" -p "ping")
|
|
250
|
-
;;
|
|
251
|
-
*) echo "unknown harness: ${harness}(claude / codex / grok)" >&2; exit 1 ;;
|
|
252
|
-
esac
|
|
253
|
-
preflight_log=$(mktemp "${TMPDIR:-/tmp}/peertable-preflight.XXXXXX")
|
|
254
|
-
( cd "$preflight_dir" && "${preflight_cmd[@]}" >"$preflight_log" 2>&1 </dev/null ) &
|
|
255
|
-
preflight_pid=$!
|
|
256
|
-
# 外部 CLI が黙って固まる場合に立卓ごと止めないための締切(外部境界なので上限を置く)。
|
|
257
|
-
preflight_deadline=$((SECONDS + 120))
|
|
258
|
-
preflight_rc=""
|
|
259
|
-
while [ $SECONDS -lt $preflight_deadline ]; do
|
|
260
|
-
if ! kill -0 "$preflight_pid" 2>/dev/null; then
|
|
261
|
-
# `set -e` の下では、失敗した子を素の `wait` で待つとそこで script ごと死ぬ。
|
|
262
|
-
# 条件式として使い、rc を自分で受ける(=失敗を握らず、メッセージを出して落とすため)
|
|
263
|
-
if wait "$preflight_pid"; then preflight_rc=0; else preflight_rc=$?; fi
|
|
264
|
-
break
|
|
265
|
-
fi
|
|
266
|
-
sleep 2
|
|
267
|
-
done
|
|
268
|
-
if [ -z "$preflight_rc" ]; then
|
|
269
|
-
kill "$preflight_pid" 2>/dev/null || true
|
|
270
|
-
echo "model preflight が 120 秒で返らない: ${harness} / ${model}(席は立てない)" >&2
|
|
271
|
-
echo "preflight log: ${preflight_log}" >&2
|
|
272
|
-
cat "$preflight_log" >&2 || true
|
|
273
|
-
[ -n "${grok_home:-}" ] && rm -rf "$grok_home"
|
|
274
|
-
exit 1
|
|
275
|
-
fi
|
|
276
|
-
if [ "$preflight_rc" != 0 ]; then
|
|
277
|
-
echo "model が live で使えない: ${harness} / ${model}(preflight rc=${preflight_rc}・席は立てない)" >&2
|
|
278
|
-
cat "$preflight_log" >&2 || true
|
|
279
|
-
rm -f "$preflight_log"
|
|
280
|
-
[ -n "${grok_home:-}" ] && rm -rf "$grok_home"
|
|
281
|
-
exit 1
|
|
282
|
-
fi
|
|
283
|
-
rm -f "$preflight_log"
|
|
284
|
-
|
|
285
|
-
sock=$(node "$(dirname "$0")/tmux-socket.mjs")
|
|
286
|
-
sess="peer-$name"
|
|
287
|
-
state="$proj/.team/setup-state.json"
|
|
288
|
-
read -r room url mode plan <<EOF
|
|
289
|
-
$(python3 -c "import json;d=json.load(open('$state'));print(d['room'],d['server_url'],d['mode'],d.get('plan_key') or '-')")
|
|
290
|
-
EOF
|
|
291
|
-
# setup が解決した CLI の実 path。**席が PATH の `lattice` へ逸れないため**に渡す
|
|
292
|
-
# (release 前の source tree では、PATH の install は pull 系 command を持たない)。
|
|
293
|
-
# 無い卓(旧 setup-state)では空になり、席は既定どおり `lattice` を使う。
|
|
294
|
-
lattice_cli=$(python3 -c "import json;print(json.load(open('$state')).get('lattice_cli') or '')")
|
|
295
|
-
if [ "$mode" = lattice ] && [ -z "$lattice_cli" ]; then
|
|
296
|
-
lattice_cli="${LATTICE_CLI:-lattice}"
|
|
297
|
-
fi
|
|
298
|
-
|
|
299
|
-
mcp_ownership=$(python3 -c "import json;d=json.load(open('$state'));print('managed' if d.get('added_root_mcp', d.get('root_mcp_json_fallback', False)) else 'preexisting')")
|
|
300
|
-
if ! node "$room_mcp_helper" "$proj" "$peertable_repo" "$mcp_ownership"; then
|
|
301
|
-
echo "SEAT_ROOM_MCP_INVALID: Aiterm席のroom clientをcurrent treeへ束縛できない(席は立てない)" >&2
|
|
302
|
-
exit 1
|
|
303
|
-
fi
|
|
304
|
-
# 同名の古いroom memberが残っていると、Codexの新しいroom clientが一度も登録していなくても
|
|
305
|
-
# 下のready確認を通ってしまう。古いmemberを別席のまま黙って消すのは危険なので、起動前に同名を
|
|
306
|
-
# conflictとして拒否し、明示的な退席後に再実行させる。一覧を読めない時も着席前に止める。
|
|
307
|
-
stale_members=$(curl -sf "$url/api/$room/members" || true)
|
|
308
|
-
stale_member_rc=0
|
|
309
|
-
python3 - "$name" "$stale_members" <<'PY' || stale_member_rc=$?
|
|
310
|
-
import json
|
|
311
|
-
import sys
|
|
312
|
-
try:
|
|
313
|
-
members = json.loads(sys.argv[2])['members']
|
|
314
|
-
except Exception:
|
|
315
|
-
raise SystemExit(2)
|
|
316
|
-
raise SystemExit(0 if any(member.get('name') == sys.argv[1] for member in members) else 1)
|
|
317
|
-
PY
|
|
318
|
-
case "$stale_member_rc" in
|
|
319
|
-
1) : ;; # 同名memberなし。DELETEを発行せず、そのまま新席を起こす
|
|
320
|
-
0)
|
|
321
|
-
echo "SEAT_ROOM_MEMBER_RELAUNCH: 同名room memberが残っているので畳んでから起こす: ${name}" >&2
|
|
322
|
-
if ! env -u PEERTABLE_POST_TOKEN "$(dirname "$0")/leave-seat.sh" "$proj" "$name"; then
|
|
323
|
-
echo "SEAT_ROOM_MEMBER_RELAUNCH_FAILED: 同名席を畳めない: ${name}" >&2
|
|
324
|
-
exit 1
|
|
325
|
-
fi
|
|
326
|
-
;;
|
|
327
|
-
*)
|
|
328
|
-
echo "SEAT_ROOM_MEMBER_STATE_UNREADABLE: room member一覧を読み取れない(席は立てない)" >&2
|
|
329
|
-
exit 1
|
|
330
|
-
;;
|
|
331
|
-
esac
|
|
332
|
-
|
|
333
|
-
# credential は同名掃除の**後**に用意する。leave-seat は member ごと credential file
|
|
334
|
-
# (path は project/room/member から決定的)を撤去するので、先に作ると同名再着席の
|
|
335
|
-
# 初回だけ「消えた credential を席へ渡して client が initialize 前に死ぬ」レースになる
|
|
336
|
-
# (実被弾 2026-08-22: さくら再着席の初回必敗・再走で成功、の正体)。
|
|
337
|
-
if ! credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" prepare "$proj" "$room" "$name"); then
|
|
338
|
-
echo "SEAT_CREDENTIAL_PREPARE_FAILED: 席別credentialを用意できない(席は立てない)" >&2
|
|
339
|
-
exit 1
|
|
340
|
-
fi
|
|
341
|
-
|
|
342
|
-
# leave-seat が席専用 GROK_HOME を消す。同名再着席では preflight 後に空になるので、
|
|
343
|
-
# live 起動の直前に auth と ui だけを書き直す(user booth MCP は載せない)。
|
|
344
|
-
if [ "$harness" = grok ]; then
|
|
345
|
-
mkdir -p "$grok_home"
|
|
346
|
-
if [ ! -f "${HOME}/.grok/auth.json" ]; then
|
|
347
|
-
echo "SEAT_GROK_AUTH_MISSING: ${HOME}/.grok/auth.json が無い(席は立てない)" >&2
|
|
348
|
-
exit 1
|
|
349
|
-
fi
|
|
350
|
-
cp "${HOME}/.grok/auth.json" "${grok_home}/auth.json"
|
|
351
|
-
chmod 600 "${grok_home}/auth.json" 2>/dev/null || true
|
|
352
|
-
node "$peertable_script_dir/grok-seat-config.mjs" "$proj" "${grok_home}/config.toml"
|
|
353
|
-
fi
|
|
354
|
-
|
|
355
|
-
# 前の卓の残骸を回収してから、Aiterm の公開 agent launcher を唯一の席起動経路として呼ぶ。
|
|
356
|
-
# direct CLI launch へ戻るfallbackは置かない。Aiterm が作る同名PTYと launch receipt が、
|
|
357
|
-
# 以後の `pty_read` / `agent_configure` / room metadata を同じsessionへ束縛する。
|
|
358
|
-
tmux_at kill-session -t "$sess" 2>/dev/null || true
|
|
359
|
-
|
|
360
|
-
launch_env=(
|
|
361
|
-
"PEERTABLE_URL=$url"
|
|
362
|
-
"PEERTABLE_ROOM=$room"
|
|
363
|
-
"PEERTABLE_MEMBER=$name"
|
|
364
|
-
"PEERTABLE_CREDENTIAL_FILE=$credential_file"
|
|
365
|
-
"PEERTABLE_HARNESS=$harness"
|
|
366
|
-
"PEERTABLE_VENDOR=$harness" # 旧版 room client 互換
|
|
367
|
-
"PEERTABLE_MODEL=$model"
|
|
368
|
-
"PEERTABLE_EFFORT=$effort"
|
|
369
|
-
"PEERTABLE_ROLE=$role"
|
|
370
|
-
"PEERTABLE_ROLES=$role"
|
|
371
|
-
"PEERTABLE_MISSION=$mission"
|
|
372
|
-
"PEERTABLE_TMUX_SOCKET=$sock"
|
|
373
|
-
)
|
|
374
|
-
if [ "$mode" = "lattice" ]; then
|
|
375
|
-
launch_env+=(
|
|
376
|
-
"PEERTABLE_PLAN=$plan"
|
|
377
|
-
"LATTICE_TODO_ACTOR_HOST=${LATTICE_TODO_ACTOR_HOST:-$(hostname | tr -d '\r')}"
|
|
378
|
-
"LATTICE_TODO_ACTOR_SESSION=$name"
|
|
379
|
-
"LATTICE_TODO_ACTOR_AGENT=$name"
|
|
380
|
-
)
|
|
381
|
-
[ -n "$lattice_cli" ] && launch_env+=("LATTICE_CLI=$lattice_cli")
|
|
382
|
-
fi
|
|
383
|
-
if [ "$harness" = grok ]; then
|
|
384
|
-
# 実測 2026-08-21: auth の coding_data_retention_opt_out だけでは
|
|
385
|
-
# `Help improve Grok [Opt out] [Opt in]` バナーが残る。席が死んだように見える。
|
|
386
|
-
launch_env+=("GROK_HOME=$grok_home")
|
|
387
|
-
launch_env+=("GROK_PRIVACY_NOTICE_ROLLOUT=0")
|
|
388
|
-
fi
|
|
389
|
-
if [ "$harness" = codex ]; then
|
|
390
|
-
codex_home="${proj}/.team/seats/${name}.codex"
|
|
391
|
-
mkdir -p "$codex_home"
|
|
392
|
-
if [ -f "${HOME}/.codex/auth.json" ]; then
|
|
393
|
-
cp "${HOME}/.codex/auth.json" "${codex_home}/auth.json"
|
|
394
|
-
chmod 600 "${codex_home}/auth.json" 2>/dev/null || true
|
|
395
|
-
fi
|
|
396
|
-
launch_env+=("CODEX_HOME=$codex_home")
|
|
397
|
-
fi
|
|
398
|
-
if [ "$harness" = codex ] \
|
|
399
|
-
&& ! env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$codex_room_mcp_helper" ensure "$proj" "$peertable_repo"; then
|
|
400
|
-
echo "SEAT_CODEX_ROOM_MCP_INVALID: Codexのproject設定へseat固有room clientを装備できない(席は立てない)" >&2
|
|
401
|
-
exit 1
|
|
402
|
-
fi
|
|
403
|
-
# Aiterm launcherはsession作成後にtyped failureを返すことがある。ここから先は同名memberが
|
|
404
|
-
# 無いことを確認済みなので、launch試行が作ったtmux/memberをon_exitの対象として先に所有する。
|
|
405
|
-
seat_created=true
|
|
406
|
-
launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$harness" "$model" "$effort" "$proj" "$brief") || {
|
|
407
|
-
echo "SEAT_AITERM_LAUNCH_FAILED: ${name} をAiterm公開launcherで起動できない(direct CLI fallbackなし)" >&2
|
|
408
|
-
exit 1
|
|
409
|
-
}
|
|
410
|
-
if ! python3 - "$launch_receipt" "$sess" <<'PY'
|
|
411
|
-
import json, sys
|
|
412
|
-
try:
|
|
413
|
-
receipt = json.loads(sys.argv[1])
|
|
414
|
-
except Exception:
|
|
415
|
-
raise SystemExit(1)
|
|
416
|
-
raise SystemExit(0 if receipt.get('schema') == 'aiterm.agent-launch-result.v1' and receipt.get('session_id') == sys.argv[2] else 1)
|
|
417
|
-
PY
|
|
418
|
-
then
|
|
419
|
-
echo "SEAT_AITERM_LAUNCH_RECEIPT_INVALID: ${name} のsession_idを確定できない" >&2
|
|
420
|
-
exit 1
|
|
421
|
-
fi
|
|
422
|
-
aiterm_session_id="$sess"
|
|
423
|
-
# **launch が返った事実は brief 配達の証拠にならない。** aiterm は TUI が入力受付前
|
|
424
|
-
# (update/trust ダイアログ表示中など)だと prompt を送らず、receipt の event_cursor=null で
|
|
425
|
-
# それを申告する。読まずに配達済み扱いにすると席が白紙のまま「briefed」と報告される
|
|
426
|
-
# (実被弾 2026-08-22: さくら再着席。席は起動したが着任指示ゼロで放置)。
|
|
427
|
-
# event_cursor が数値 → turn 開始をaitermが確認済み。submit_residue=true → 本文は
|
|
428
|
-
# composer に残ったが Enter が落ちた=後段で Enter だけ打ち直す。null → 未送信=後段で貼る。
|
|
429
|
-
brief_in_composer=false
|
|
430
|
-
if [ -n "$brief" ]; then
|
|
431
|
-
brief_receipt_state=$(python3 - "$launch_receipt" <<'PY'
|
|
432
|
-
import json, sys
|
|
433
|
-
r = json.loads(sys.argv[1])
|
|
434
|
-
cursor = r.get('event_cursor')
|
|
435
|
-
residue = r.get('submit_residue')
|
|
436
|
-
print('dispatched' if cursor is not None and residue is not True
|
|
437
|
-
else 'residue' if residue is True else 'not_sent')
|
|
438
|
-
PY
|
|
439
|
-
)
|
|
440
|
-
case "$brief_receipt_state" in
|
|
441
|
-
dispatched)
|
|
442
|
-
brief_dispatched=true
|
|
443
|
-
brief_sent=true
|
|
444
|
-
brief_event_cursor=$(python3 -c 'import json,sys;print(json.loads(sys.argv[1]).get("event_cursor", ""))' "$launch_receipt")
|
|
445
|
-
;;
|
|
446
|
-
residue)
|
|
447
|
-
brief_in_composer=true
|
|
448
|
-
brief_event_cursor=$(python3 -c 'import json,sys;print(json.loads(sys.argv[1]).get("event_cursor", ""))' "$launch_receipt")
|
|
449
|
-
echo "SEAT_BRIEF_SUBMIT_RESIDUE: brief は入力欄に残り submit が落ちた。着席確認後に submit し直す" >&2
|
|
450
|
-
;;
|
|
451
|
-
*)
|
|
452
|
-
echo "SEAT_BRIEF_LAUNCH_PROMPT_NOT_SENT: aiterm は TUI 入力受付前で brief を送っていない。着席確認後に貼り直す" >&2
|
|
453
|
-
;;
|
|
454
|
-
esac
|
|
455
|
-
fi
|
|
456
|
-
seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
|
|
457
|
-
|
|
458
|
-
# 席が作る派生tmuxセッションへ PEERTABLE_MEMBER を自動継承させる(2026-08-26 オーナー裁定:
|
|
459
|
-
# 帰属は規約名でなくOSの継承機構で辿る)。tmuxは既定でclientのenvを剥ぐので、update-environmentへ
|
|
460
|
-
# 1回だけ載せる。これで席発のセッションは名前が何でも、session envのPEERTABLE_MEMBERで持ち主が読める
|
|
461
|
-
if ! tmux_at show-options -g update-environment 2>/dev/null | grep -q 'PEERTABLE_MEMBER'; then
|
|
462
|
-
tmux_at set-option -ga update-environment ' PEERTABLE_MEMBER' || \
|
|
463
|
-
echo "SEAT_ENV_STAMP_UNAVAILABLE: update-environment を設定できず、派生セッションの帰属はsession envに載らない" >&2
|
|
464
|
-
fi
|
|
465
|
-
|
|
466
|
-
# Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
|
|
467
|
-
# ヘッダ/trust dialog待機をここへ重ねると、brief turnでヘッダが画面外へ流れた正常席をrollbackする。
|
|
468
|
-
# 必須room MCPの成立は、次の member登録readbackだけで判定する。
|
|
469
|
-
echo "launched: ${sess}(${harness} / ${model}${effort:+ / $effort} / room=${room} / mode=${mode})"
|
|
470
|
-
|
|
471
|
-
# CodexのヘッダはCLIが起動した証拠であって、必須room MCPが初期化された証拠ではない。
|
|
472
|
-
# 無関係MCPのwarningが画面へ出ても、room clientがmember登録まで到達した席だけを着席として扱う。
|
|
473
|
-
# 登録が無ければ「着席済み」と丸めず、on_exitのrollbackへ渡す。
|
|
474
|
-
# brief を launch prompt に載せると初手ターンが MCP 初期化と重なり、30秒では
|
|
475
|
-
# member 登録前に rollback する(2026-08-20 ひなた再着席)。
|
|
476
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
477
|
-
room_ready=false
|
|
478
|
-
grok_trust_accepted=false
|
|
479
|
-
mcp_consent_accepted=false
|
|
480
|
-
codex_hooks_accepted=false
|
|
481
|
-
codex_update_accepted=false
|
|
482
|
-
codex_trust_accepted=false
|
|
483
|
-
claude_trust_accepted=false
|
|
484
|
-
codex_dialog_helper="$peertable_script_dir/codex-dialog.mjs"
|
|
485
|
-
claude_dialog_helper="$peertable_script_dir/claude-dialog.mjs"
|
|
486
|
-
pass_codex_pane() {
|
|
487
|
-
local keys key
|
|
488
|
-
keys=$(printf '%s' "$1" | node "$codex_dialog_helper" --keys || true)
|
|
489
|
-
[ -n "$keys" ] || return 1
|
|
490
|
-
sleep .3
|
|
491
|
-
while IFS= read -r key; do
|
|
492
|
-
[ -n "$key" ] || continue
|
|
493
|
-
tmux_at send-keys -t "$sess" "$key" || return 1
|
|
494
|
-
sleep .3
|
|
495
|
-
sleep .3
|
|
496
|
-
done <<<"$keys"
|
|
497
|
-
return 0
|
|
498
|
-
}
|
|
499
|
-
codex_pane_blocks_ready() {
|
|
500
|
-
printf '%s' "$1" | node "$codex_dialog_helper" --ready-ok
|
|
501
|
-
}
|
|
502
|
-
pass_claude_mcp_pane() {
|
|
503
|
-
local keys key
|
|
504
|
-
keys=$(printf '%s' "$1" | node "$claude_dialog_helper" --keys || true)
|
|
505
|
-
[ -n "$keys" ] || return 1
|
|
506
|
-
while IFS= read -r key; do
|
|
507
|
-
[ -n "$key" ] || continue
|
|
508
|
-
tmux_at send-keys -t "$sess" "$key" || return 1
|
|
509
|
-
done <<<"$keys"
|
|
510
|
-
return 0
|
|
511
|
-
}
|
|
512
|
-
while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
513
|
-
# Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
|
|
514
|
-
# Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
|
|
515
|
-
# 汎用的に承認するfallbackにはしない。承認後のMCP初期化時間は改めて30秒確保する。
|
|
516
|
-
if [ "$harness" = codex ]; then
|
|
517
|
-
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
518
|
-
if pass_codex_pane "$codex_screen"; then
|
|
519
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
520
|
-
case "$codex_screen" in
|
|
521
|
-
*"Allow the room MCP server to run tool"*) echo "codex mcp allow: always allow" ;;
|
|
522
|
-
*"Hooks need review"*)
|
|
523
|
-
codex_hooks_accepted=true
|
|
524
|
-
echo "codex hooks prompt: trust all"
|
|
525
|
-
;;
|
|
526
|
-
*"Update now"*)
|
|
527
|
-
codex_update_accepted=true
|
|
528
|
-
echo "codex update prompt: skipped"
|
|
529
|
-
;;
|
|
530
|
-
*"Yes, continue"*)
|
|
531
|
-
codex_trust_accepted=true
|
|
532
|
-
echo "codex directory trust: accepted"
|
|
533
|
-
;;
|
|
534
|
-
*"Would you like to run the following command?"*)
|
|
535
|
-
echo "codex command approval: don't ask again"
|
|
536
|
-
;;
|
|
537
|
-
esac
|
|
538
|
-
fi
|
|
539
|
-
fi
|
|
540
|
-
if [ "$harness" = grok ] && [ "$grok_trust_accepted" != true ]; then
|
|
541
|
-
grok_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
542
|
-
case "$grok_screen" in
|
|
543
|
-
*"Do you trust the contents of this directory?"*)
|
|
544
|
-
if ! tmux_at send-keys -t "$sess" y; then
|
|
545
|
-
echo "SEAT_GROK_TRUST_FAILED: workspace trustへ応答できない" >&2
|
|
546
|
-
exit 1
|
|
547
|
-
fi
|
|
548
|
-
grok_trust_accepted=true
|
|
549
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
550
|
-
echo "grok workspace trust: accepted"
|
|
551
|
-
;;
|
|
552
|
-
esac
|
|
553
|
-
fi
|
|
554
|
-
# 未信頼ディレクトリでは room MCP 同意より前に workspace trust が出る。既定選択肢
|
|
555
|
-
# (1. Yes, I trust this folder)を Enter で通す。この既知文言だけを一度通す。
|
|
556
|
-
if [ "$harness" = claude ] && [ "$claude_trust_accepted" != true ]; then
|
|
557
|
-
claude_trust_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
558
|
-
case "$claude_trust_screen" in
|
|
559
|
-
*"Is this a project you created or one you trust"*|*"trust this folder"*)
|
|
560
|
-
if ! tmux_at send-keys -t "$sess" Enter; then
|
|
561
|
-
echo "SEAT_CLAUDE_TRUST_FAILED: workspace trustへ応答できない" >&2
|
|
562
|
-
exit 1
|
|
563
|
-
fi
|
|
564
|
-
claude_trust_accepted=true
|
|
565
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
566
|
-
echo "claude workspace trust: accepted"
|
|
567
|
-
;;
|
|
568
|
-
esac
|
|
569
|
-
fi
|
|
570
|
-
# aiterm claude_agent は --dangerously-skip-permissions を付けない。project の room MCP
|
|
571
|
-
# 同意が member 登録より前に出る。選択肢2(this and all future)だけを通す。
|
|
572
|
-
if [ "$harness" = claude ] && [ "$mcp_consent_accepted" != true ]; then
|
|
573
|
-
claude_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
574
|
-
if pass_claude_mcp_pane "$claude_screen"; then
|
|
575
|
-
mcp_consent_accepted=true
|
|
576
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
577
|
-
echo "claude room MCP consent: accepted"
|
|
578
|
-
fi
|
|
579
|
-
fi
|
|
580
|
-
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
581
|
-
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
|
|
582
|
-
if [ "$harness" = codex ]; then
|
|
583
|
-
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
584
|
-
if ! codex_pane_blocks_ready "$codex_screen"; then
|
|
585
|
-
sleep 1
|
|
586
|
-
continue
|
|
587
|
-
fi
|
|
588
|
-
fi
|
|
589
|
-
room_ready=true
|
|
590
|
-
break
|
|
591
|
-
fi
|
|
592
|
-
sleep 1
|
|
593
|
-
done
|
|
594
|
-
if [ "$room_ready" != true ]; then
|
|
595
|
-
echo "SEAT_ROOM_MCP_NOT_READY: room member登録を観測できない(無関係MCP warningとroom不成立を分離。席をrollbackする)" >&2
|
|
596
|
-
tmux_at capture-pane -t "$sess" -p >"$proj/.team/${name}-pane-on-fail.txt" 2>/dev/null || true
|
|
597
|
-
echo "pane saved: $proj/.team/${name}-pane-on-fail.txt" >&2
|
|
598
|
-
exit 1
|
|
599
|
-
fi
|
|
600
|
-
echo "room ready: ${room}/${name}"
|
|
601
|
-
|
|
602
|
-
# ここから先のdialog処理・稼働状態・DM配送に必要なruntimeは、利用側へ順序を
|
|
603
|
-
# 委ねず正規着座入口が現行Peertable版へ収束させる。
|
|
604
|
-
PEERTABLE_CREDENTIAL_FILE="$credential_file" "$peertable_script_dir/ensure-project-runtime.sh" "$proj"
|
|
605
|
-
|
|
606
|
-
if [ "$harness" = codex ]; then
|
|
607
|
-
# member 登録後の最初の tool 呼び出しで MCP Allow が出る。出ている間だけ通し、沈黙5秒で抜ける。
|
|
608
|
-
codex_post_ready_deadline=$((SECONDS + 90))
|
|
609
|
-
codex_post_ready_idle=0
|
|
610
|
-
while [ $SECONDS -lt "$codex_post_ready_deadline" ]; do
|
|
611
|
-
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
612
|
-
if pass_codex_pane "$codex_screen"; then
|
|
613
|
-
case "$codex_screen" in
|
|
614
|
-
*"Allow the room MCP server to run tool"*) echo "codex mcp allow: always allow" ;;
|
|
615
|
-
esac
|
|
616
|
-
codex_post_ready_idle=0
|
|
617
|
-
else
|
|
618
|
-
codex_post_ready_idle=$((codex_post_ready_idle + 1))
|
|
619
|
-
[ "$codex_post_ready_idle" -ge 5 ] && break
|
|
620
|
-
fi
|
|
621
|
-
sleep 1
|
|
622
|
-
done
|
|
623
|
-
fi
|
|
624
|
-
if [ "$brief_dispatched" = true ]; then
|
|
625
|
-
echo "briefed: ${sess}(Aiterm launch prompt)"
|
|
626
|
-
fi
|
|
627
|
-
|
|
628
|
-
if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
629
|
-
if [ "$brief_in_composer" != true ]; then
|
|
630
|
-
# TUIの入力可能判定とsubmitはAitermの公開 pty_send が所有する。ここで独自の
|
|
631
|
-
# prompt連続観測を重ねると、正常なTUI描画差だけでdispatch前に席を落とす。
|
|
632
|
-
if brief_send_receipt=$(node "$peertable_script_dir/aiterm-send.mjs" "$sess" "$brief_file"); then
|
|
633
|
-
brief_sent=true
|
|
634
|
-
brief_event_cursor=$(python3 -c 'import json,sys;print(json.loads(sys.argv[1]).get("event_cursor", ""))' "$brief_send_receipt")
|
|
635
|
-
echo "briefed: $sess(Aiterm pty_send)"
|
|
636
|
-
else
|
|
637
|
-
echo "LAUNCH_BRIEF_SEND_FAILED: Aiterm pty_sendでbriefをdispatchできない" >&2
|
|
638
|
-
exit 1
|
|
639
|
-
fi
|
|
640
|
-
else
|
|
641
|
-
# Codex はヘッダを描いた後も MCP 初期化を続ける。Aiterm の ready 契約に合わせ、
|
|
642
|
-
# 同じ可視 pane 内に入力候補行とモデルフッタがある構造を連続して観測する。
|
|
643
|
-
# hook / MCP warning の更新で画面全体が変わっても、入力欄周辺が ready なら通す。
|
|
644
|
-
brief_ready_deadline=$((SECONDS + 90))
|
|
645
|
-
brief_ready_streak=0
|
|
646
|
-
brief_ready=false
|
|
647
|
-
while [ $SECONDS -lt "$brief_ready_deadline" ]; do
|
|
648
|
-
brief_ready_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
649
|
-
if [ "$harness" != codex ] || printf '%s\n' "$brief_ready_screen" | python3 -c 'import sys; lines=sys.stdin.read().splitlines()[-24:]; has_prompt=any(line.strip() == "›" or line.lstrip().startswith("› ") for line in lines); has_footer=any("gpt-" in line and "·" in line for line in lines); raise SystemExit(0 if has_prompt and has_footer else 1)'; then
|
|
650
|
-
brief_ready_streak=$((brief_ready_streak + 1))
|
|
651
|
-
if [ "$brief_ready_streak" -ge 3 ]; then
|
|
652
|
-
brief_ready=true
|
|
653
|
-
break
|
|
654
|
-
fi
|
|
655
|
-
else
|
|
656
|
-
brief_ready_streak=0
|
|
657
|
-
fi
|
|
658
|
-
sleep 1
|
|
659
|
-
done
|
|
660
|
-
if [ "$brief_ready" != true ]; then
|
|
661
|
-
tmux_at capture-pane -t "$sess" -p >"$proj/.team/${name}-pane-on-fail.txt" 2>/dev/null || true
|
|
662
|
-
echo "LAUNCH_BRIEF_NOT_READY: brief を受け付ける入力promptを観測できない" >&2
|
|
663
|
-
echo "pane saved: $proj/.team/${name}-pane-on-fail.txt" >&2
|
|
664
|
-
exit 1
|
|
665
|
-
else
|
|
666
|
-
# prompt の描画とキー入力受理の境界を分ける。Codex のTUIが入力欄を
|
|
667
|
-
# mountした直後に paste と Enter を同一tickで受けると、Enterだけ落ちる。
|
|
668
|
-
sleep 1
|
|
669
|
-
|
|
670
|
-
# submit_residue の席は本文が composer に残っている。貼り直すと二重になるので Enter だけ打つ。
|
|
671
|
-
if [ "$brief_in_composer" != true ]; then
|
|
672
|
-
brief_buffer="peertable-brief-${name}-$$"
|
|
673
|
-
if ! tmux_at load-buffer -b "$brief_buffer" "$brief_file"; then
|
|
674
|
-
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux buffer 読み込みに失敗(席は着席済み)" >&2
|
|
675
|
-
exit 1
|
|
676
|
-
fi
|
|
677
|
-
if ! tmux_at paste-buffer -b "$brief_buffer" -d -t "$sess"; then
|
|
678
|
-
tmux_at delete-buffer -b "$brief_buffer" 2>/dev/null || true
|
|
679
|
-
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux paste に失敗(席は着席済み)" >&2
|
|
680
|
-
exit 1
|
|
681
|
-
fi
|
|
682
|
-
sleep 1
|
|
683
|
-
fi
|
|
684
|
-
if ! tmux_at send-keys -t "$sess" Enter; then
|
|
685
|
-
echo "LAUNCH_BRIEF_SEND_FAILED: brief の submit に失敗(席は着席済み)" >&2
|
|
686
|
-
exit 1
|
|
687
|
-
fi
|
|
688
|
-
|
|
689
|
-
brief_sent=true
|
|
690
|
-
echo "briefed: $sess"
|
|
691
|
-
fi
|
|
692
|
-
fi
|
|
693
|
-
fi
|
|
694
|
-
|
|
695
|
-
# 「promptを渡した」ではなく、全harness共通で実ターンが始まったことを成功条件にする。
|
|
696
|
-
# 1秒未満で完了するturnはAitermの完了正本で拾い、通常turnは現在paneのbusyランプで拾う。
|
|
697
|
-
if [ -n "$brief" ] && [ "$brief_sent" = true ]; then
|
|
698
|
-
command -v aiterm-wait >/dev/null 2>&1 || { echo "LAUNCH_AITERM_WAIT_MISSING: 実ターン開始を確認できない" >&2; exit 1; }
|
|
699
|
-
brief_active_deadline=$((SECONDS + 60))
|
|
700
|
-
brief_turn_observed=false
|
|
701
|
-
while [ $SECONDS -lt "$brief_active_deadline" ]; do
|
|
702
|
-
brief_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
703
|
-
if [ "$harness" = codex ] && pass_codex_pane "$brief_screen"; then
|
|
704
|
-
sleep 1
|
|
705
|
-
continue
|
|
706
|
-
fi
|
|
707
|
-
brief_pane_status=$(printf '%s' "$brief_screen" | node "$peertable_script_dir/agent-pane-status.mjs")
|
|
708
|
-
if [ "$brief_pane_status" = busy ]; then
|
|
709
|
-
brief_turn_observed=true
|
|
710
|
-
break
|
|
711
|
-
fi
|
|
712
|
-
if [ -n "$brief_event_cursor" ]; then
|
|
713
|
-
wait_receipt=$(aiterm-wait --session "$sess" --cursor "$brief_event_cursor" --timeout 0 2>/dev/null || true)
|
|
714
|
-
if printf '%s' "$wait_receipt" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{try{process.exit(JSON.parse(s).outcome==="done"?0:1)}catch{process.exit(1)}})'; then
|
|
715
|
-
brief_turn_observed=true
|
|
716
|
-
break
|
|
717
|
-
fi
|
|
718
|
-
fi
|
|
719
|
-
sleep 1
|
|
720
|
-
done
|
|
721
|
-
if [ "$brief_turn_observed" != true ]; then
|
|
722
|
-
tmux_at capture-pane -t "$sess" -p >"$proj/.team/${name}-pane-on-fail.txt" 2>/dev/null || true
|
|
723
|
-
echo "LAUNCH_BRIEF_TURN_NOT_STARTED: Aiterm dispatch後に${harness}の実ターン開始も完了も観測できない" >&2
|
|
724
|
-
echo "pane saved: $proj/.team/${name}-pane-on-fail.txt" >&2
|
|
725
|
-
exit 1
|
|
726
|
-
fi
|
|
727
|
-
brief_completed=true
|
|
728
|
-
echo "active: $sess(実ターン開始を観測)"
|
|
729
|
-
fi
|
|
730
|
-
|
|
731
|
-
# 席の本人性(pid / 起動時刻 / argv digest)を room 台帳の member 行へ登録する。
|
|
732
|
-
# member に帰属する情報の正本は台帳だけ(オーナー裁定 2026-08-22・席fileは廃止)。
|
|
733
|
-
# 着席の**後**に取る——起動途中の process を掴むと、ダイアログ通過で子が入れ替わりうる。
|
|
734
|
-
# **raw argv を持たせない**——digest だけを持つ。この記録が主張するのは「この pid は
|
|
735
|
-
# この席だった」という**識別**であって、生死ではない。生きているかは attach する側
|
|
736
|
-
# (Lattice)が lstart+argv の再観測で確かめる。
|
|
737
|
-
seat_pid=""
|
|
738
|
-
pane_pid=$(tmux_at list-panes -t "$sess" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
|
|
739
|
-
seat_ident=""
|
|
740
|
-
if [ -n "$pane_pid" ] && [ -r "/proc/${pane_pid}/winpid" ]; then
|
|
741
|
-
pane_pid=$(tr -d '[:space:]' < "/proc/${pane_pid}/winpid")
|
|
742
|
-
fi
|
|
743
|
-
if [ -n "$pane_pid" ]; then
|
|
744
|
-
seat_ident=$(node "$(dirname "$0")/seat-identity.mjs" "$pane_pid") || seat_ident=""
|
|
745
|
-
fi
|
|
746
|
-
if [ -n "$seat_ident" ]; then
|
|
747
|
-
seat_pid=$(python3 -c "import json,sys;print(json.load(sys.stdin)['pid'])" <<<"$seat_ident")
|
|
748
|
-
fi
|
|
749
|
-
if [ -z "$seat_pid" ]; then
|
|
750
|
-
# 記録が無ければ席は attach できず、装置の介入は協調 hold のままになる。**黙らない。**
|
|
751
|
-
echo "seat identity を記録できなかった: ${sess} の process group leader を1つに確定できない(席は着席済み)" >&2
|
|
752
|
-
else
|
|
753
|
-
# aiterm_session_id の書き手はランチャー(launch receipt の所有者)。席の client は
|
|
754
|
-
# Codex closed-mode env で AITERM_SESSION_ID を受け取れないため、ここで台帳へ載せる。
|
|
755
|
-
ident_body=$(python3 - "$name" "$seat_ident" "$aiterm_session_id" <<'PY'
|
|
756
|
-
import json, subprocess, sys
|
|
757
|
-
name, ident_raw, aiterm_session = sys.argv[1:4]
|
|
758
|
-
ident = json.loads(ident_raw)
|
|
759
|
-
if not ident.get('started_identity') or not ident.get('argv') or not ident.get('argv_digest'):
|
|
760
|
-
sys.exit('pid の lstart/args を観測できない')
|
|
761
|
-
print(json.dumps({
|
|
762
|
-
'name': name,
|
|
763
|
-
'pid': int(ident['pid']),
|
|
764
|
-
'started_identity': ident['started_identity'],
|
|
765
|
-
'argv_digest': ident['argv_digest'],
|
|
766
|
-
'aiterm_session_id': aiterm_session,
|
|
767
|
-
'identity_recorded_at': subprocess.run(['date', '-u', '+%Y-%m-%dT%H:%M:%S.000Z'],
|
|
768
|
-
capture_output=True, text=True, check=True).stdout.strip(),
|
|
769
|
-
}, ensure_ascii=False))
|
|
770
|
-
PY
|
|
771
|
-
) || ident_body=""
|
|
772
|
-
if [ -z "$ident_body" ] || ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
773
|
-
"$url/api/$room/members" "$ident_body" >/dev/null; then
|
|
774
|
-
echo "seat identity を台帳へ登録できなかった: ${sess}(席は着席済み・attach は協調 hold のままになる)" >&2
|
|
775
|
-
fi
|
|
776
|
-
fi
|
|
777
|
-
|
|
778
|
-
# 素性(harness/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
|
|
779
|
-
# ランチャーはここで台帳を読み返し、実際に載ったかを確認するだけにする(重複書込の禁止)。
|
|
780
|
-
member_row=$(curl -sf "$url/api/$room/members/$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$name")" || true)
|
|
781
|
-
stored=$(python3 - "$member_row" <<'PY'
|
|
782
|
-
import json, sys
|
|
783
|
-
try:
|
|
784
|
-
m = json.loads(sys.argv[1])['member']
|
|
785
|
-
except Exception:
|
|
786
|
-
m = {}
|
|
787
|
-
print('yes' if m.get('model') and m.get('roles') and m.get('aiterm_session_id')
|
|
788
|
-
and (m.get('observe') or {}).get('tmux_target') and m.get('pid') else 'no')
|
|
789
|
-
PY
|
|
790
|
-
)
|
|
791
|
-
if [ "$stored" = yes ]; then
|
|
792
|
-
echo "台帳確認: ${harness} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
|
|
793
|
-
else
|
|
794
|
-
echo "SEAT_LEDGER_INCOMPLETE: 台帳の member 行に素性または本人性が欠けている(席は着席済み。doctor で確認)" >&2
|
|
795
|
-
fi
|
|
796
|
-
|
|
797
|
-
if [ -z "$brief" ]; then brief_completed=true; fi
|
|
798
|
-
credential_persist=true
|
|
2
|
+
# 既存の引数を共通の製品入口へ渡す。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
exec node "$(dirname "$0")/legacy-entry.mjs" launch-seat "$@"
|