peertable 0.3.8 → 0.3.10
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 +8 -4
- package/README.md +8 -4
- package/package.json +1 -1
- package/room/client.mjs +36 -56
- package/room/server.mjs +77 -16
- package/skill/SKILL.md +73 -34
- package/skill/scripts/aiterm-configure.mjs +31 -0
- package/skill/scripts/aiterm-launch.mjs +29 -0
- package/skill/scripts/archive-room-log.py +3 -1
- package/skill/scripts/change-effort.sh +5 -129
- package/skill/scripts/change-seat.sh +253 -0
- package/skill/scripts/codex-parent-watch.sh +8 -0
- package/skill/scripts/ensure-bridge.sh +6 -2
- package/skill/scripts/ensure-codex-room-mcp.mjs +152 -0
- package/skill/scripts/ensure-room-mcp.mjs +52 -0
- package/skill/scripts/launch-seat.sh +392 -94
- package/skill/scripts/leave-seat.sh +90 -0
- package/skill/scripts/parent-join.sh +46 -6
- package/skill/scripts/parent-watch.mjs +286 -0
- package/skill/scripts/seat-credential.mjs +118 -0
- package/skill/scripts/seat-status-bridge.mjs +10 -3
- package/skill/scripts/seat-usage.mjs +44 -3
- package/skill/scripts/setup.sh +30 -3
- package/skill/scripts/teardown.sh +12 -3
- package/skill/scripts/todo-extraction-from-plan.mjs +199 -0
- package/skill/scripts/upgrade-team-assets.sh +224 -0
- package/skill/scripts/wakeup-bridge.mjs +245 -58
- package/skill/templates/charter.md +12 -6
- package/skill/templates/done.sh +379 -36
- package/skill/templates/mcp.json +1 -1
- package/skill/templates/member-standalone.md +24 -14
- package/skill/templates/member.md +31 -33
- package/skill/templates/parent.md +128 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# 席を1つ立てる(tmux 作成 → env 注入 → エージェント起動 → 既知ダイアログ通過 → 着席確認)。
|
|
3
|
-
# usage: launch-seat.sh <project_dir> <name> <model> <vendor> <effort> [brief]
|
|
3
|
+
# usage: launch-seat.sh <project_dir> <name> <model> <vendor> <effort> [brief] [role]
|
|
4
4
|
# vendor: claude / codex
|
|
5
5
|
# effort: 必須。既定値をコードへ埋めない——席を立てる時に決める(オーナー裁定)
|
|
6
6
|
# brief: 着席が成立したら送る着任指示(省略時は送らない)
|
|
7
7
|
#
|
|
8
8
|
# room 名・server URL・モード・plan key は <project>/.team/setup-state.json から読む(setup.sh の後に呼ぶ)。
|
|
9
|
-
#
|
|
9
|
+
# 書込トークンは helper が ~/.config/peertable.env から席別 0600 file へ移し、pathだけを席へ渡す。
|
|
10
10
|
# tmux は aiterm-mcp と同じソケットへ作るので、立てた席はそのまま pty_read / pty_send で読める。
|
|
11
11
|
set -e
|
|
12
|
-
proj="$1"; name="$2"; model="$3"; vendor="$4"; effort="$5"; brief="$6"
|
|
12
|
+
proj="$1"; name="$2"; model="$3"; vendor="$4"; effort="$5"; brief="$6"; role="${7:-worker}"
|
|
13
13
|
[ -n "$proj" ] && [ -n "$name" ] && [ -n "$model" ] && [ -n "$vendor" ] && [ -n "$effort" ] || {
|
|
14
14
|
echo "usage: launch-seat.sh <project_dir> <name> <model> <vendor> <effort> [brief]" >&2; exit 1;
|
|
15
15
|
}
|
|
@@ -19,6 +19,195 @@ if [ "$vendor" = "claude" ]; then
|
|
|
19
19
|
*) echo "unknown effort: ${effort}(claude は low|medium|high|xhigh|max)" >&2; exit 1 ;;
|
|
20
20
|
esac
|
|
21
21
|
fi
|
|
22
|
+
case "$role" in worker|auditor) ;; *) echo "unknown seat role: ${role}(worker / auditor)" >&2; exit 1 ;; esac
|
|
23
|
+
|
|
24
|
+
if [ -n "${PEERTABLE_MEMBER:-}" ]; then
|
|
25
|
+
echo "SEAT_LAUNCH_DELEGATED_CHILD_FORBIDDEN: PEERTABLE_MEMBER=${PEERTABLE_MEMBER} を継承した呼出元からの席起動を拒否" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# 呼出元が古い手順でtokenをexportしていても、preflight・tmux・model CLIへ継承しない。
|
|
30
|
+
# 値の解決はcredential helperだけが設定fileから行う。env/argvへのfallbackは持たない。
|
|
31
|
+
unset PEERTABLE_POST_TOKEN
|
|
32
|
+
credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$(dirname "$0")/seat-credential.mjs}"
|
|
33
|
+
room_mcp_helper="${PEERTABLE_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-room-mcp.mjs}"
|
|
34
|
+
codex_room_mcp_helper="${PEERTABLE_CODEX_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-codex-room-mcp.mjs}"
|
|
35
|
+
aiterm_launch_helper="${PEERTABLE_AITERM_LAUNCH_HELPER:-$(dirname "$0")/aiterm-launch.mjs}"
|
|
36
|
+
peertable_repo=$(cd "$(dirname "$0")/../.." && pwd -P)
|
|
37
|
+
peertable_client="$peertable_repo/room/client.mjs"
|
|
38
|
+
credential_file=""
|
|
39
|
+
credential_persist=false
|
|
40
|
+
aiterm_session_id=""
|
|
41
|
+
|
|
42
|
+
# brief は tmux のコマンド引数へ直接載せない。長さを着席前に検証してから一時 file へ置き、
|
|
43
|
+
# tmux の buffer 経由で貼る。入力を受理できない時は model preflight・tmux・member 登録を
|
|
44
|
+
# 一つも行わず、何が拒否されたかを機械的に読める形で返す。
|
|
45
|
+
brief_file=""
|
|
46
|
+
brief_max_bytes=65536
|
|
47
|
+
brief_completed=false
|
|
48
|
+
brief_dispatched=false
|
|
49
|
+
seat_created=false
|
|
50
|
+
rollback_done=false
|
|
51
|
+
# ready を観測できないだけの不確実性は、投入後の真失敗と分ける。これは
|
|
52
|
+
# Aiterm から手動 dispatch できる空席として残し、席数へ成功着任とは数えない。
|
|
53
|
+
brief_not_ready=false
|
|
54
|
+
sock=""
|
|
55
|
+
sess=""
|
|
56
|
+
url=""
|
|
57
|
+
room=""
|
|
58
|
+
seat_file="$proj/.team/seats/$name.json"
|
|
59
|
+
cleanup_brief() {
|
|
60
|
+
if [ -n "$brief_file" ]; then rm -f "$brief_file"; fi
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
# brief を受け付けて送信した後に turn が始まらなかった場合は、作りかけの席を残さない。
|
|
64
|
+
# tmux を先に落とし、client が再登録しない状態にしてから room member を解除する。
|
|
65
|
+
# DELETE は idempotent だが、一覧の読み返しまで通らなければ rollback 成功とは言わない。
|
|
66
|
+
rollback_brief() {
|
|
67
|
+
local original_rc="${1:-1}"
|
|
68
|
+
local rollback_failed=0
|
|
69
|
+
local encoded_name member_code listing
|
|
70
|
+
|
|
71
|
+
if [ -n "$sock" ] && [ -n "$sess" ] && [ "$seat_created" = true ]; then
|
|
72
|
+
if tmux -S "$sock" has-session -t "$sess" 2>/dev/null; then
|
|
73
|
+
if ! tmux -S "$sock" kill-session -t "$sess" 2>/dev/null; then
|
|
74
|
+
rollback_failed=1
|
|
75
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session を停止できない: ${sess}" >&2
|
|
76
|
+
elif tmux -S "$sock" has-session -t "$sess" 2>/dev/null; then
|
|
77
|
+
rollback_failed=1
|
|
78
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session が停止後も残っている: ${sess}" >&2
|
|
79
|
+
fi
|
|
80
|
+
elif tmux -S "$sock" list-sessions >/dev/null 2>&1; then
|
|
81
|
+
: # serverへ到達でき、対象sessionが無い
|
|
82
|
+
else
|
|
83
|
+
rollback_failed=1
|
|
84
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session を観測できない: ${sess}" >&2
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
if [ "$rollback_failed" -ne 0 ]; then
|
|
89
|
+
# live clientを止めたと確認できない時は、member/identity/credentialを先に消して
|
|
90
|
+
# rollback済みに見せない。credential cleanupはon_exitにもさせない。
|
|
91
|
+
credential_persist=true
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
if [ -n "$seat_file" ] && [ -e "$seat_file" ]; then
|
|
96
|
+
if ! rm -f "$seat_file"; then
|
|
97
|
+
rollback_failed=1
|
|
98
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: seat identity を撤去できない: ${seat_file}" >&2
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if [ -n "$url" ] && [ -n "$room" ]; then
|
|
103
|
+
if ! encoded_name=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name"); then
|
|
104
|
+
rollback_failed=1
|
|
105
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: member 名を URL 化できない: ${name}" >&2
|
|
106
|
+
else
|
|
107
|
+
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" DELETE \
|
|
108
|
+
"$url/api/$room/members/$encoded_name" >/dev/null; then
|
|
109
|
+
rollback_failed=1
|
|
110
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: room member を解除できない" >&2
|
|
111
|
+
fi
|
|
112
|
+
listing=$(curl -sf "$url/api/$room/members" || true)
|
|
113
|
+
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
|
|
114
|
+
rollback_failed=1
|
|
115
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: room member の解除を読み返せない: ${name}" >&2
|
|
116
|
+
fi
|
|
117
|
+
fi
|
|
118
|
+
else
|
|
119
|
+
rollback_failed=1
|
|
120
|
+
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: room の rollback 境界が未解決" >&2
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if [ "$rollback_failed" -ne 0 ]; then
|
|
124
|
+
return 1
|
|
125
|
+
fi
|
|
126
|
+
echo "LAUNCH_BRIEF_ROLLED_BACK: ${sess}(tmux / room member / seat identity を撤去)" >&2
|
|
127
|
+
return "$original_rc"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
on_exit() {
|
|
131
|
+
local exit_rc=$?
|
|
132
|
+
local rollback_rc
|
|
133
|
+
trap - EXIT
|
|
134
|
+
cleanup_brief
|
|
135
|
+
if [ "$seat_created" = true ] && [ "$brief_completed" != true ] && [ "$brief_not_ready" != true ] && [ "$rollback_done" != true ]; then
|
|
136
|
+
rollback_done=true
|
|
137
|
+
if rollback_brief "$exit_rc"; then
|
|
138
|
+
:
|
|
139
|
+
else
|
|
140
|
+
rollback_rc=$?
|
|
141
|
+
[ "$exit_rc" -eq 0 ] && exit_rc="$rollback_rc"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
if [ -n "$credential_file" ] && [ "$credential_persist" != true ]; then
|
|
145
|
+
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" remove "$proj" "$credential_file"; then
|
|
146
|
+
echo "SEAT_CREDENTIAL_ROLLBACK_FAILED: ${credential_file}" >&2
|
|
147
|
+
[ "$exit_rc" -eq 0 ] && exit_rc=1
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
exit "$exit_rc"
|
|
151
|
+
}
|
|
152
|
+
trap on_exit EXIT
|
|
153
|
+
if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
154
|
+
brief_bytes=$(printf '%s' "$brief" | LC_ALL=C wc -c | tr -d '[:space:]')
|
|
155
|
+
case "$brief_bytes" in
|
|
156
|
+
''|*[!0-9]*) echo "LAUNCH_BRIEF_INVALID: brief の byte 長を測定できない(席は立てない)" >&2; exit 2 ;;
|
|
157
|
+
esac
|
|
158
|
+
if [ "$brief_bytes" -gt "$brief_max_bytes" ]; then
|
|
159
|
+
echo "LAUNCH_BRIEF_TOO_LONG: brief が ${brief_bytes} bytes(上限 ${brief_max_bytes} bytes・席は立てない)" >&2
|
|
160
|
+
exit 2
|
|
161
|
+
fi
|
|
162
|
+
if ! brief_file=$(mktemp "${TMPDIR:-/tmp}/peertable-brief.XXXXXX"); then
|
|
163
|
+
echo "LAUNCH_BRIEF_PREPARE_FAILED: brief の輸送 file を作れない(席は立てない)" >&2
|
|
164
|
+
exit 2
|
|
165
|
+
fi
|
|
166
|
+
if ! printf '%s' "$brief" >"$brief_file"; then
|
|
167
|
+
echo "LAUNCH_BRIEF_PREPARE_FAILED: brief を輸送 file へ書けない(席は立てない)" >&2
|
|
168
|
+
exit 2
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# **画面の文字列は「model が使えること」を意味しない。** 2026-08-11 実測: fable-5 の席は
|
|
173
|
+
# Claude の channels バナーが出たので下の着席判定を通ったが、その後の入力は全て
|
|
174
|
+
# model unavailable で 0 秒失敗し、席は一度も仕事をできなかった(room [11])。
|
|
175
|
+
# バナーは CLI が起動したことしか言わないので、**live に応答するかは非対話入口で先に測る**。
|
|
176
|
+
# 席を畳む前に測るのが要点である——ここで落ちれば、動いている席を殺さずに済む。
|
|
177
|
+
preflight_dir="${TMPDIR:-/tmp}"
|
|
178
|
+
case "$vendor" in
|
|
179
|
+
claude) preflight_cmd=(claude --model "$model" -p "ping") ;;
|
|
180
|
+
codex) preflight_cmd=(codex exec --model "$model" --skip-git-repo-check "ping") ;;
|
|
181
|
+
*) echo "unknown vendor: ${vendor}(claude / codex)" >&2; exit 1 ;;
|
|
182
|
+
esac
|
|
183
|
+
preflight_log=$(mktemp "${TMPDIR:-/tmp}/peertable-preflight.XXXXXX")
|
|
184
|
+
( cd "$preflight_dir" && "${preflight_cmd[@]}" >"$preflight_log" 2>&1 </dev/null ) &
|
|
185
|
+
preflight_pid=$!
|
|
186
|
+
# 外部 CLI が黙って固まる場合に立卓ごと止めないための締切(外部境界なので上限を置く)。
|
|
187
|
+
preflight_deadline=$((SECONDS + 120))
|
|
188
|
+
preflight_rc=""
|
|
189
|
+
while [ $SECONDS -lt $preflight_deadline ]; do
|
|
190
|
+
if ! kill -0 "$preflight_pid" 2>/dev/null; then
|
|
191
|
+
# `set -e` の下では、失敗した子を素の `wait` で待つとそこで script ごと死ぬ。
|
|
192
|
+
# 条件式として使い、rc を自分で受ける(=失敗を握らず、メッセージを出して落とすため)
|
|
193
|
+
if wait "$preflight_pid"; then preflight_rc=0; else preflight_rc=$?; fi
|
|
194
|
+
break
|
|
195
|
+
fi
|
|
196
|
+
sleep 2
|
|
197
|
+
done
|
|
198
|
+
if [ -z "$preflight_rc" ]; then
|
|
199
|
+
kill "$preflight_pid" 2>/dev/null || true
|
|
200
|
+
echo "model preflight が 120 秒で返らない: ${vendor} / ${model}(席は立てない)" >&2
|
|
201
|
+
rm -f "$preflight_log"
|
|
202
|
+
exit 1
|
|
203
|
+
fi
|
|
204
|
+
if [ "$preflight_rc" != 0 ]; then
|
|
205
|
+
echo "model が live で使えない: ${vendor} / ${model}(preflight rc=${preflight_rc}・席は立てない)" >&2
|
|
206
|
+
tail -5 "$preflight_log" >&2
|
|
207
|
+
rm -f "$preflight_log"
|
|
208
|
+
exit 1
|
|
209
|
+
fi
|
|
210
|
+
rm -f "$preflight_log"
|
|
22
211
|
|
|
23
212
|
sock=$(node "$(dirname "$0")/tmux-socket.mjs")
|
|
24
213
|
sess="peer-$name"
|
|
@@ -30,95 +219,195 @@ EOF
|
|
|
30
219
|
# (release 前の source tree では、PATH の install は pull 系 command を持たない)。
|
|
31
220
|
# 無い卓(旧 setup-state)では空になり、席は既定どおり `lattice` を使う。
|
|
32
221
|
lattice_cli=$(python3 -c "import json;print(json.load(open('$state')).get('lattice_cli') or '')")
|
|
222
|
+
if [ "$mode" = lattice ] && [ -z "$lattice_cli" ]; then
|
|
223
|
+
lattice_cli="${LATTICE_CLI:-lattice}"
|
|
224
|
+
fi
|
|
33
225
|
|
|
34
|
-
|
|
35
|
-
|
|
226
|
+
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')")
|
|
227
|
+
if ! node "$room_mcp_helper" "$proj" "$peertable_repo" "$mcp_ownership"; then
|
|
228
|
+
echo "SEAT_ROOM_MCP_INVALID: Aiterm席のroom clientをcurrent treeへ束縛できない(席は立てない)" >&2
|
|
229
|
+
exit 1
|
|
230
|
+
fi
|
|
231
|
+
if ! credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" prepare "$proj" "$room" "$name"); then
|
|
232
|
+
echo "SEAT_CREDENTIAL_PREPARE_FAILED: 席別credentialを用意できない(席は立てない)" >&2
|
|
233
|
+
exit 1
|
|
36
234
|
fi
|
|
37
235
|
|
|
38
|
-
#
|
|
39
|
-
|
|
236
|
+
# 同名の古いroom memberが残っていると、Codexの新しいroom clientが一度も登録していなくても
|
|
237
|
+
# 下のready確認を通ってしまう。古いmemberを別席のまま黙って消すのは危険なので、起動前に同名を
|
|
238
|
+
# conflictとして拒否し、明示的な退席後に再実行させる。一覧を読めない時も着席前に止める。
|
|
239
|
+
stale_members=$(curl -sf "$url/api/$room/members" || true)
|
|
240
|
+
stale_member_rc=0
|
|
241
|
+
python3 - "$name" "$stale_members" <<'PY' || stale_member_rc=$?
|
|
242
|
+
import json
|
|
243
|
+
import sys
|
|
244
|
+
try:
|
|
245
|
+
members = json.loads(sys.argv[2])['members']
|
|
246
|
+
except Exception:
|
|
247
|
+
raise SystemExit(2)
|
|
248
|
+
raise SystemExit(0 if any(member.get('name') == sys.argv[1] for member in members) else 1)
|
|
249
|
+
PY
|
|
250
|
+
case "$stale_member_rc" in
|
|
251
|
+
1) : ;; # 同名memberなし。DELETEを発行せず、そのまま新席を起こす
|
|
252
|
+
0)
|
|
253
|
+
echo "SEAT_ROOM_MEMBER_CONFLICT: 同名room memberが残っているため着席前に停止(明示的に退席してから再実行)" >&2
|
|
254
|
+
exit 1
|
|
255
|
+
;;
|
|
256
|
+
*)
|
|
257
|
+
echo "SEAT_ROOM_MEMBER_STATE_UNREADABLE: room member一覧を読み取れない(席は立てない)" >&2
|
|
258
|
+
exit 1
|
|
259
|
+
;;
|
|
260
|
+
esac
|
|
40
261
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
262
|
+
# 前の卓の残骸を回収してから、Aiterm の公開 agent launcher を唯一の席起動経路として呼ぶ。
|
|
263
|
+
# direct CLI launch へ戻るfallbackは置かない。Aiterm が作る同名PTYと launch receipt が、
|
|
264
|
+
# 以後の `pty_read` / `agent_configure` / room metadata を同じsessionへ束縛する。
|
|
265
|
+
tmux -S "$sock" kill-session -t "$sess" 2>/dev/null || true
|
|
44
266
|
rm -f "$proj/.team/seats/$name.json"
|
|
45
|
-
tmux -S "$sock" new-session -d -s "$sess" -x 200 -y 50 -c "$proj"
|
|
46
|
-
# Codex の closed env には launcher 自身でなく、今作った session の識別子を渡す。
|
|
47
|
-
# 自己申告の observe が別 pane を指すと、稼働状態・起床とも別席を誤操作する。
|
|
48
|
-
seat_tmux=$(tmux -S "$sock" display-message -p -t "$sess" '#{socket_path}')
|
|
49
|
-
seat_tmux_pane=$(tmux -S "$sock" display-message -p -t "$sess" '#{pane_id}')
|
|
50
267
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
268
|
+
launch_env=(
|
|
269
|
+
"PEERTABLE_URL=$url"
|
|
270
|
+
"PEERTABLE_ROOM=$room"
|
|
271
|
+
"PEERTABLE_MEMBER=$name"
|
|
272
|
+
"PEERTABLE_CREDENTIAL_FILE=$credential_file"
|
|
273
|
+
"PEERTABLE_VENDOR=$vendor"
|
|
274
|
+
"PEERTABLE_MODEL=$model"
|
|
275
|
+
"PEERTABLE_EFFORT=$effort"
|
|
276
|
+
"PEERTABLE_ROLE=$role"
|
|
277
|
+
"PEERTABLE_TMUX_SOCKET=$sock"
|
|
278
|
+
)
|
|
55
279
|
if [ "$mode" = "lattice" ]; then
|
|
56
|
-
|
|
57
|
-
|
|
280
|
+
launch_env+=(
|
|
281
|
+
"PEERTABLE_PLAN=$plan"
|
|
282
|
+
"LATTICE_TODO_ACTOR_HOST=${LATTICE_TODO_ACTOR_HOST:-mac}"
|
|
283
|
+
"LATTICE_TODO_ACTOR_SESSION=$name"
|
|
284
|
+
"LATTICE_TODO_ACTOR_AGENT=$name"
|
|
285
|
+
)
|
|
286
|
+
[ -n "$lattice_cli" ] && launch_env+=("LATTICE_CLI=$lattice_cli")
|
|
287
|
+
fi
|
|
288
|
+
if [ "$vendor" = codex ] \
|
|
289
|
+
&& ! env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$codex_room_mcp_helper" ensure "$proj" "$peertable_repo"; then
|
|
290
|
+
echo "SEAT_CODEX_ROOM_MCP_INVALID: Codexのproject設定へseat固有room clientを装備できない(席は立てない)" >&2
|
|
291
|
+
exit 1
|
|
292
|
+
fi
|
|
293
|
+
# Aiterm launcherはsession作成後にtyped failureを返すことがある。ここから先は同名memberが
|
|
294
|
+
# 無いことを確認済みなので、launch試行が作ったtmux/memberをon_exitの対象として先に所有する。
|
|
295
|
+
seat_created=true
|
|
296
|
+
launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$vendor" "$model" "$effort" "$proj" "$brief") || {
|
|
297
|
+
echo "SEAT_AITERM_LAUNCH_FAILED: ${name} をAiterm公開launcherで起動できない(direct CLI fallbackなし)" >&2
|
|
298
|
+
exit 1
|
|
299
|
+
}
|
|
300
|
+
if ! python3 - "$launch_receipt" "$sess" <<'PY'
|
|
301
|
+
import json, sys
|
|
302
|
+
try:
|
|
303
|
+
receipt = json.loads(sys.argv[1])
|
|
304
|
+
except Exception:
|
|
305
|
+
raise SystemExit(1)
|
|
306
|
+
raise SystemExit(0 if receipt.get('schema') == 'aiterm.agent-launch-result.v1' and receipt.get('session_id') == sys.argv[2] else 1)
|
|
307
|
+
PY
|
|
308
|
+
then
|
|
309
|
+
echo "SEAT_AITERM_LAUNCH_RECEIPT_INVALID: ${name} のsession_idを確定できない" >&2
|
|
310
|
+
exit 1
|
|
58
311
|
fi
|
|
59
|
-
|
|
60
|
-
|
|
312
|
+
aiterm_session_id="$sess"
|
|
313
|
+
brief_dispatched=true
|
|
314
|
+
seat_tmux=$(tmux -S "$sock" display-message -p -t "$sess" '#{socket_path}')
|
|
61
315
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cmd="$cmd --dangerously-skip-permissions --dangerously-load-development-channels server:room"
|
|
67
|
-
;;
|
|
68
|
-
codex)
|
|
69
|
-
# Codex には channels が無いので room は stdio MCP として差す。
|
|
70
|
-
# `[mcp_servers.X.env]` は closed mode(親 env を継がない)ので全変数を明示列挙する
|
|
71
|
-
# (caveat `codex-cli-v0-130-0-mcp-servers-x-env-block-is-closed-mode-parent-env-not-inherited`)。
|
|
72
|
-
envtbl="PATH=\\\"$PATH\\\",PEERTABLE_URL=\\\"$url\\\",PEERTABLE_ROOM=\\\"$room\\\",PEERTABLE_MEMBER=\\\"$name\\\",PEERTABLE_POST_TOKEN=\\\"$PEERTABLE_POST_TOKEN\\\",TMUX=\\\"$seat_tmux\\\",TMUX_PANE=\\\"$seat_tmux_pane\\\""
|
|
73
|
-
cmd="codex --model $model -C $proj --dangerously-bypass-approvals-and-sandbox"
|
|
74
|
-
# Codexのeffortは環境変数だけでは適用されない。member metadataへ表示する値と、
|
|
75
|
-
# 実際の推論設定を同じ引数から渡して食い違わせない。
|
|
76
|
-
[ -n "$effort" ] && cmd="$cmd -c 'model_reasoning_effort=\"$effort\"'"
|
|
77
|
-
cmd="$cmd -c 'mcp_servers.room.command=\"peertable-client\"'"
|
|
78
|
-
cmd="$cmd -c \"mcp_servers.room.env={$envtbl}\""
|
|
79
|
-
;;
|
|
80
|
-
# 変数展開の直後に全角括弧を置かない(bash が高位バイトを変数名の一部として食い、
|
|
81
|
-
# 変数が空になって黙って情報が消える。2026-08-08 実測)。必ず ${var} で閉じる
|
|
82
|
-
*) echo "unknown vendor: ${vendor}(claude / codex)" >&2; exit 1 ;;
|
|
83
|
-
esac
|
|
84
|
-
tmux -S "$sock" send-keys -t "$sess" "$cmd" Enter
|
|
85
|
-
|
|
86
|
-
# 既知ダイアログ(実測 2026-08-08・Claude Code v2.1.226 / Codex CLI v0.146.0):
|
|
87
|
-
# claude ① 未信頼ディレクトリの workspace trust「1. Yes, I trust this folder」
|
|
88
|
-
# ② 開発 channel 警告「1. I am using this for local development」
|
|
89
|
-
# codex ① ディレクトリ trust「1. Yes, continue」
|
|
90
|
-
# いずれも既定の選択肢が正なので、文言を確認してから Enter を送る。信頼済みディレクトリでは
|
|
91
|
-
# trust が出ないので、出た時だけ通す(順不同・出ないものは待たない)。
|
|
92
|
-
# 着席の判定は claude=channels バナー / codex=セッションヘッダ。出なければ画面ごと出して落ちる。
|
|
93
|
-
deadline=$((SECONDS + 90))
|
|
94
|
-
seated=false
|
|
95
|
-
while [ $SECONDS -lt $deadline ]; do
|
|
96
|
-
screen=$(tmux -S "$sock" capture-pane -t "$sess" -p)
|
|
97
|
-
if [ "$vendor" = "claude" ]; then
|
|
98
|
-
case "$screen" in *"Channels (experimental)"*"server:room"*) seated=true; break ;; esac
|
|
99
|
-
else
|
|
100
|
-
case "$screen" in *"OpenAI Codex (v"*) seated=true; break ;; esac
|
|
101
|
-
fi
|
|
102
|
-
case "$screen" in
|
|
103
|
-
# Codex の更新案内だけは既定(1. Update now)が誤り——立卓の途中で
|
|
104
|
-
# `npm install -g @openai/codex` が走る。1つ下の「2. Skip」を選ぶ
|
|
105
|
-
*"1. Update now"*)
|
|
106
|
-
tmux -S "$sock" send-keys -t "$sess" Down
|
|
107
|
-
sleep 1
|
|
108
|
-
tmux -S "$sock" send-keys -t "$sess" Enter ;;
|
|
109
|
-
*"1. Yes, I trust this folder"*|*"1. I am using this for local development"*|*"1. Yes, continue"*)
|
|
110
|
-
tmux -S "$sock" send-keys -t "$sess" Enter ;;
|
|
111
|
-
esac
|
|
112
|
-
sleep 2
|
|
113
|
-
done
|
|
316
|
+
# Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
|
|
317
|
+
# ヘッダ/trust dialog待機をここへ重ねると、brief turnでヘッダが画面外へ流れた正常席をrollbackする。
|
|
318
|
+
# 必須room MCPの成立は、次の member登録readbackだけで判定する。
|
|
319
|
+
echo "launched: ${sess}(${vendor} / ${model}${effort:+ / $effort} / room=${room} / mode=${mode})"
|
|
114
320
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
321
|
+
# CodexのヘッダはCLIが起動した証拠であって、必須room MCPが初期化された証拠ではない。
|
|
322
|
+
# 無関係MCPのwarningが画面へ出ても、room clientがmember登録まで到達した席だけを着席として扱う。
|
|
323
|
+
# 登録が無ければ「着席済み」と丸めず、on_exitのrollbackへ渡す。
|
|
324
|
+
room_ready_deadline=$((SECONDS + 30))
|
|
325
|
+
room_ready=false
|
|
326
|
+
while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
327
|
+
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
328
|
+
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
|
|
329
|
+
room_ready=true
|
|
330
|
+
break
|
|
331
|
+
fi
|
|
332
|
+
sleep 1
|
|
333
|
+
done
|
|
334
|
+
if [ "$room_ready" != true ]; then
|
|
335
|
+
echo "SEAT_ROOM_MCP_NOT_READY: room member登録を観測できない(無関係MCP warningとroom不成立を分離。席をrollbackする)" >&2
|
|
118
336
|
exit 1
|
|
119
337
|
fi
|
|
338
|
+
echo "room ready: ${room}/${name}"
|
|
339
|
+
if [ "$brief_dispatched" = true ]; then
|
|
340
|
+
brief_completed=true
|
|
341
|
+
echo "briefed: ${sess}(Aiterm launch prompt)"
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
345
|
+
# Codex はヘッダを描いた後も MCP 初期化を続ける。Aiterm の ready 契約に合わせ、
|
|
346
|
+
# 同じ可視 pane 内に入力候補行とモデルフッタがある構造を連続して観測する。
|
|
347
|
+
# hook / MCP warning の更新で画面全体が変わっても、入力欄周辺が ready なら通す。
|
|
348
|
+
brief_ready_deadline=$((SECONDS + 90))
|
|
349
|
+
brief_ready_streak=0
|
|
350
|
+
brief_ready=false
|
|
351
|
+
while [ $SECONDS -lt "$brief_ready_deadline" ]; do
|
|
352
|
+
brief_ready_screen=$(tmux -S "$sock" capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
353
|
+
if [ "$vendor" != codex ] || printf '%s\n' "$brief_ready_screen" | python3 -c 'import sys; lines=sys.stdin.read().splitlines()[-24:]; has_prompt=any(line.strip() == "›" or line.lstrip().startswith("› ") for line in lines); has_footer=any("gpt-" in line and "·" in line for line in lines); raise SystemExit(0 if has_prompt and has_footer else 1)'; then
|
|
354
|
+
brief_ready_streak=$((brief_ready_streak + 1))
|
|
355
|
+
if [ "$brief_ready_streak" -ge 3 ]; then
|
|
356
|
+
brief_ready=true
|
|
357
|
+
break
|
|
358
|
+
fi
|
|
359
|
+
else
|
|
360
|
+
brief_ready_streak=0
|
|
361
|
+
fi
|
|
362
|
+
sleep 1
|
|
363
|
+
done
|
|
364
|
+
if [ "$brief_ready" != true ]; then
|
|
365
|
+
brief_not_ready=true
|
|
366
|
+
echo "LAUNCH_BRIEF_NOT_READY: brief を受け付ける入力 prompt を観測できない(brief未投入・空席を保持。Aiterm手動dispatch対象)" >&2
|
|
367
|
+
else
|
|
368
|
+
# prompt の描画とキー入力受理の境界を分ける。Codex のTUIが入力欄を
|
|
369
|
+
# mountした直後に paste と Enter を同一tickで受けると、Enterだけ落ちる。
|
|
370
|
+
sleep 1
|
|
371
|
+
|
|
372
|
+
brief_before=$(tmux -S "$sock" capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
|
|
373
|
+
brief_buffer="peertable-brief-${name}-$$"
|
|
374
|
+
if ! tmux -S "$sock" load-buffer -b "$brief_buffer" "$brief_file"; then
|
|
375
|
+
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux buffer 読み込みに失敗(席は着席済み)" >&2
|
|
376
|
+
exit 1
|
|
377
|
+
fi
|
|
378
|
+
if ! tmux -S "$sock" paste-buffer -b "$brief_buffer" -d -t "$sess"; then
|
|
379
|
+
tmux -S "$sock" delete-buffer -b "$brief_buffer" 2>/dev/null || true
|
|
380
|
+
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux paste に失敗(席は着席済み)" >&2
|
|
381
|
+
exit 1
|
|
382
|
+
fi
|
|
383
|
+
sleep 1
|
|
384
|
+
if ! tmux -S "$sock" send-keys -t "$sess" Enter; then
|
|
385
|
+
echo "LAUNCH_BRIEF_SEND_FAILED: brief の submit に失敗(席は着席済み)" >&2
|
|
386
|
+
exit 1
|
|
387
|
+
fi
|
|
120
388
|
|
|
121
|
-
|
|
389
|
+
# 入力欄へ置けた事実だけでは着任成功としない。既存の席状態判定と同じ live marker が、
|
|
390
|
+
# brief 投入後に画面へ現れたことを観測する。高速な fake/CLI の残像を拾わないよう、投入前の
|
|
391
|
+
# 画面と異なることも同時に要求する。dispatch は Aiterm の手動送信と同じく、この1回だけ行う。
|
|
392
|
+
brief_deadline=$((SECONDS + 30))
|
|
393
|
+
brief_turn_started=false
|
|
394
|
+
while [ $SECONDS -lt "$brief_deadline" ]; do
|
|
395
|
+
brief_screen=$(tmux -S "$sock" capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
|
|
396
|
+
case "$brief_screen" in
|
|
397
|
+
*"esc to interrupt"*)
|
|
398
|
+
if [ "$brief_screen" != "$brief_before" ]; then brief_turn_started=true; break; fi
|
|
399
|
+
;;
|
|
400
|
+
esac
|
|
401
|
+
sleep 1
|
|
402
|
+
done
|
|
403
|
+
if [ "$brief_turn_started" != true ]; then
|
|
404
|
+
echo "LAUNCH_BRIEF_TURN_NOT_STARTED: brief 投入後の turn 開始を観測できない(席は着席済み)" >&2
|
|
405
|
+
exit 1
|
|
406
|
+
fi
|
|
407
|
+
brief_completed=true
|
|
408
|
+
echo "briefed: $sess"
|
|
409
|
+
fi
|
|
410
|
+
fi
|
|
122
411
|
|
|
123
412
|
# 席の素性を `.team/seats/<name>.json` へ置く。**席が自分の pid を知るための唯一の経路**である
|
|
124
413
|
# (Lattice の `run intake attach` は expected identity を要求し、pid を推定しない)。
|
|
@@ -126,8 +415,7 @@ echo "seated: ${sess}(${vendor} / ${model}${effort:+ / $effort} / room=${room}
|
|
|
126
415
|
#
|
|
127
416
|
# 持たせるのは6欄だけで、**`lattice.pull_worker_attach_input.v1` の exact 集合から `schema` を
|
|
128
417
|
# 除いたもの**と一致する。席は読んで `schema` を被せるだけで attach input になる(変換不要)。
|
|
129
|
-
# **raw argv
|
|
130
|
-
# 保存すれば秘密の複製になる(2026-08-09 実測)。digest だけを持つ。
|
|
418
|
+
# **raw argv を持たせない**——秘密値はargvから除いたが、将来の引数も含めて複製しない。digest だけを持つ。
|
|
131
419
|
# この file が主張するのは「この pid はこの席だった」という**識別**であって、生死ではない。
|
|
132
420
|
# 生きているかは attach する側(Lattice)が lstart+argv の再観測で確かめる。
|
|
133
421
|
seat_pid=""
|
|
@@ -192,17 +480,18 @@ fi
|
|
|
192
480
|
# **欄が無い登録で既存の素性を消さない**(upsert)ことが前提である。
|
|
193
481
|
# effort は渡された時だけ入れる——欄が無い=「不明」ではなく「CLI 既定で走っている」。
|
|
194
482
|
# ここが失敗しても席は着席済みなので落とさない。ただし黙っては飲まない。
|
|
195
|
-
meta=$(python3 - "$name" "$vendor" "$model" "$effort" "$sock" "$sess" <<'PY'
|
|
483
|
+
meta=$(python3 - "$name" "$vendor" "$model" "$effort" "$sock" "$sess" "$role" "$aiterm_session_id" <<'PY'
|
|
196
484
|
import json, sys
|
|
197
|
-
name, vendor, model, effort, sock, sess = sys.argv[1:
|
|
198
|
-
body = {'name': name, 'vendor': vendor, 'model': model, '
|
|
485
|
+
name, vendor, model, effort, sock, sess, role, aiterm_session_id = sys.argv[1:9]
|
|
486
|
+
body = {'name': name, 'vendor': vendor, 'model': model, 'role': role, 'aiterm_session_id': aiterm_session_id,
|
|
487
|
+
'observe': {'tmux_socket': sock, 'tmux_target': sess}}
|
|
199
488
|
if effort:
|
|
200
489
|
body['effort'] = effort
|
|
201
490
|
print(json.dumps(body))
|
|
202
491
|
PY
|
|
203
492
|
)
|
|
204
|
-
if
|
|
205
|
-
|
|
493
|
+
if env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
494
|
+
"$url/api/$room/members" "$meta" >/dev/null; then
|
|
206
495
|
# **200 は保存の証拠にならない**。素性欄を知らない server も 200 {"ok":true} を返して黙って捨てる
|
|
207
496
|
# (登録が `if (!members.has(name))` の no-op になる経路もある)。読み返して実際に載ったかを見る。
|
|
208
497
|
# **パイプと heredoc を同じ stdin へ重ねない**。`curl | python3 - <<'PY'` は
|
|
@@ -218,7 +507,7 @@ try:
|
|
|
218
507
|
except Exception:
|
|
219
508
|
members = []
|
|
220
509
|
m = next((x for x in members if x.get('name') == sys.argv[1]), {})
|
|
221
|
-
print('yes' if m.get('model') and m.get('observe', {}).get('tmux_target') else 'no')
|
|
510
|
+
print('yes' if m.get('model') and m.get('role') and m.get('aiterm_session_id') and m.get('observe', {}).get('tmux_target') else 'no')
|
|
222
511
|
PY
|
|
223
512
|
)
|
|
224
513
|
if [ "$stored" = yes ]; then
|
|
@@ -230,16 +519,25 @@ else
|
|
|
230
519
|
echo "metadata の登録に失敗した: 席は着席済みで、参加者一覧に素性が出ないだけ(room の到達性とトークンを確認)" >&2
|
|
231
520
|
fi
|
|
232
521
|
|
|
233
|
-
if "$(dirname "$0")/ensure-bridge.sh" "$proj" seat-status; then
|
|
522
|
+
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" seat-status; then
|
|
234
523
|
echo "seat-status-bridge: 起動確認済み"
|
|
235
524
|
else
|
|
236
525
|
echo "seat-status-bridge の起動確認に失敗した(席は着席済み)" >&2
|
|
237
526
|
fi
|
|
238
527
|
|
|
239
|
-
if [
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
528
|
+
if [ "$vendor" = codex ]; then
|
|
529
|
+
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" wakeup; then
|
|
530
|
+
echo "wakeup-bridge: 起動確認済み"
|
|
531
|
+
else
|
|
532
|
+
echo "SEAT_WAKEUP_BRIDGE_NOT_READY: Codex席の起床bridgeを準備できない" >&2
|
|
533
|
+
exit 1
|
|
534
|
+
fi
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
if [ -z "$brief" ]; then brief_completed=true; fi
|
|
538
|
+
credential_persist=true
|
|
539
|
+
if [ "$brief_not_ready" = true ]; then
|
|
540
|
+
# 空席の後段セットアップ(identity / metadata / bridge)まで済ませたうえで、
|
|
541
|
+
# 呼び出し側にはready未確認を非0で返す。席はAiterm手動dispatchへ引き継ぐ。
|
|
542
|
+
exit 1
|
|
245
543
|
fi
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 1席だけを通常退席させ、runtime credentialまで同じ境界で撤去する。
|
|
3
|
+
# usage: leave-seat.sh <project_dir> <member>
|
|
4
|
+
set -u
|
|
5
|
+
|
|
6
|
+
unset PEERTABLE_POST_TOKEN
|
|
7
|
+
proj="${1:-}"
|
|
8
|
+
name="${2:-}"
|
|
9
|
+
[ -n "$proj" ] && [ -n "$name" ] || {
|
|
10
|
+
echo "SEAT_LEAVE_ARGS_INVALID: leave-seat.sh <project_dir> <member>" >&2
|
|
11
|
+
exit 2
|
|
12
|
+
}
|
|
13
|
+
case "$name" in
|
|
14
|
+
*[!A-Za-z0-9._:-]*) echo "SEAT_LEAVE_ARGS_INVALID: member名に使えない文字がある" >&2; exit 2 ;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
18
|
+
credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$script_dir/seat-credential.mjs}"
|
|
19
|
+
state="$proj/.team/setup-state.json"
|
|
20
|
+
[ -f "$state" ] || { echo "SEAT_LEAVE_STATE_MISSING: $state" >&2; exit 1; }
|
|
21
|
+
read -r room url <<EOF
|
|
22
|
+
$(python3 -c "import json;d=json.load(open('$state'));print(d['room'],d['server_url'])")
|
|
23
|
+
EOF
|
|
24
|
+
|
|
25
|
+
credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" path "$proj" "$room" "$name") || exit 1
|
|
26
|
+
# 旧席などでcredentialだけ欠けている時は、sessionを止める前に復元する。
|
|
27
|
+
# ここで失敗すれば席は生きたままなので、member登録だけ残る半退席を作らない。
|
|
28
|
+
if [ ! -r "$credential_file" ] || [ ! -s "$credential_file" ]; then
|
|
29
|
+
credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" prepare "$proj" "$room" "$name") || {
|
|
30
|
+
echo "SEAT_LEAVE_CREDENTIAL_PREPARE_FAILED: $name" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
}
|
|
33
|
+
fi
|
|
34
|
+
default_socket=$(node "$script_dir/tmux-socket.mjs" 2>/dev/null || true)
|
|
35
|
+
members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
36
|
+
read -r member_socket target <<EOF
|
|
37
|
+
$(python3 - "$name" "$default_socket" "$members" <<'PY'
|
|
38
|
+
import json, sys
|
|
39
|
+
name, default_socket, raw = sys.argv[1:4]
|
|
40
|
+
try:
|
|
41
|
+
member = next((x for x in json.loads(raw).get('members', []) if x.get('name') == name), {})
|
|
42
|
+
except Exception:
|
|
43
|
+
member = {}
|
|
44
|
+
observe = member.get('observe') or {}
|
|
45
|
+
print(observe.get('tmux_socket') or default_socket, observe.get('tmux_target') or f'peer-{name}')
|
|
46
|
+
PY
|
|
47
|
+
)
|
|
48
|
+
EOF
|
|
49
|
+
|
|
50
|
+
failed=0
|
|
51
|
+
if [ -z "$member_socket" ]; then
|
|
52
|
+
echo "SEAT_LEAVE_SESSION_UNREADABLE: tmux socketを解決できない" >&2
|
|
53
|
+
failed=1
|
|
54
|
+
elif tmux -S "$member_socket" has-session -t "$target" 2>/dev/null; then
|
|
55
|
+
if ! tmux -S "$member_socket" kill-session -t "$target"; then
|
|
56
|
+
echo "SEAT_LEAVE_SESSION_FAILED: $target" >&2
|
|
57
|
+
failed=1
|
|
58
|
+
elif tmux -S "$member_socket" has-session -t "$target" 2>/dev/null; then
|
|
59
|
+
echo "SEAT_LEAVE_SESSION_FAILED: $target が撤去後も残っている" >&2
|
|
60
|
+
failed=1
|
|
61
|
+
fi
|
|
62
|
+
elif tmux -S "$member_socket" list-sessions >/dev/null 2>&1; then
|
|
63
|
+
: # serverへ到達でき、対象sessionが無い
|
|
64
|
+
else
|
|
65
|
+
echo "SEAT_LEAVE_SESSION_UNREADABLE: $member_socket" >&2
|
|
66
|
+
failed=1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# live clientを止めたと確認できない時は、member/identity/credentialを先に消して
|
|
70
|
+
# 「退席済み」に見せない。観測面を残したままtyped failureで止める。
|
|
71
|
+
[ "$failed" -eq 0 ] || exit 1
|
|
72
|
+
|
|
73
|
+
encoded_name=$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$name")
|
|
74
|
+
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" DELETE \
|
|
75
|
+
"$url/api/$room/members/$encoded_name" >/dev/null; then
|
|
76
|
+
echo "SEAT_LEAVE_MEMBER_FAILED: $name" >&2
|
|
77
|
+
failed=1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
if ! rm -f "$proj/.team/seats/$name.json"; then
|
|
81
|
+
echo "SEAT_LEAVE_IDENTITY_FAILED: $name" >&2
|
|
82
|
+
failed=1
|
|
83
|
+
fi
|
|
84
|
+
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" remove "$proj" "$credential_file"; then
|
|
85
|
+
echo "SEAT_LEAVE_CREDENTIAL_FAILED: $name" >&2
|
|
86
|
+
failed=1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
[ "$failed" -eq 0 ] || exit 1
|
|
90
|
+
echo "SEAT_LEAVE_OK: ${name}(session / room member / seat identity / credential を撤去)"
|