peertable 0.4.1 → 0.4.3
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/LICENSE +21 -21
- package/README.ja.md +122 -122
- package/README.md +158 -158
- package/package.json +49 -49
- package/room/Dockerfile +6 -6
- package/room/client.mjs +5 -1
- package/skill/SKILL.md +245 -245
- package/skill/scripts/aiterm-configure.mjs +1 -1
- package/skill/scripts/aiterm-launch.mjs +1 -1
- package/skill/scripts/archive-room-log.py +0 -0
- package/skill/scripts/change-effort.sh +0 -0
- package/skill/scripts/change-seat.sh +4 -2
- package/skill/scripts/codex-parent-watch.sh +0 -0
- package/skill/scripts/ensure-bridge.sh +4 -2
- package/skill/scripts/ensure-codex-room-mcp.mjs +0 -0
- package/skill/scripts/ensure-room-mcp.mjs +0 -0
- package/skill/scripts/external-pane.mjs +0 -0
- package/skill/scripts/launch-seat.sh +54 -32
- package/skill/scripts/leave-seat.sh +7 -8
- package/skill/scripts/make-plan-input.mjs +0 -0
- package/skill/scripts/parent-join.sh +0 -0
- package/skill/scripts/parent-watch.mjs +3 -1
- package/skill/scripts/seat-identity.mjs +58 -0
- package/skill/scripts/seat-status-bridge.mjs +2 -2
- package/skill/scripts/seat-usage.mjs +51 -6
- package/skill/scripts/setup.sh +0 -0
- package/skill/scripts/teardown.sh +5 -3
- package/skill/scripts/tmux-at.bash +10 -0
- package/skill/scripts/tmux-socket.mjs +13 -4
- package/skill/scripts/upgrade-team-assets.sh +0 -0
- package/skill/scripts/wakeup-bridge.mjs +5 -5
- package/skill/templates/charter.md +20 -20
- package/skill/templates/mcp.json +5 -5
- package/skill/templates/member-standalone.md +43 -43
- package/skill/templates/member.md +127 -127
- package/skill/templates/parent.md +131 -131
- package/skill/templates/tasks.md +8 -8
|
@@ -16,7 +16,7 @@ if (!session_id || (!model && !reasoning_effort)) {
|
|
|
16
16
|
// Aiterm のmanaged session metadataは実行時state root(macOSではTMPDIR)にある。
|
|
17
17
|
// stdio transportの既定環境は親のTMPDIRを落とすため、同じ公開serverへ明示継承する。
|
|
18
18
|
const transport = new StdioClientTransport({ command: 'aiterm-mcp', env: process.env })
|
|
19
|
-
const client = new Client({ name: 'peertable-seat-configure', version: '0.4.
|
|
19
|
+
const client = new Client({ name: 'peertable-seat-configure', version: '0.4.3' })
|
|
20
20
|
await client.connect(transport)
|
|
21
21
|
const result = await client.callTool({
|
|
22
22
|
name: 'agent_configure',
|
|
@@ -8,7 +8,7 @@ if (!session_name || !['claude', 'codex', 'grok'].includes(vendor) || !model ||
|
|
|
8
8
|
throw new Error('SEAT_AITERM_LAUNCH_ARGS_INVALID')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const client = new Client({ name: 'peertable-seat-launch', version: '0.4.
|
|
11
|
+
const client = new Client({ name: 'peertable-seat-launch', version: '0.4.3' })
|
|
12
12
|
await client.connect(new StdioClientTransport({ command: 'aiterm-mcp', env: process.env }))
|
|
13
13
|
const env_vars = [
|
|
14
14
|
'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
|
|
File without changes
|
|
File without changes
|
|
@@ -16,6 +16,8 @@ set -eu
|
|
|
16
16
|
# token値はこの制御processや再起動する席へ継承しない。launch後のroom記録も席別file経由で行う。
|
|
17
17
|
unset PEERTABLE_POST_TOKEN
|
|
18
18
|
script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
19
|
+
# shellcheck disable=SC1091
|
|
20
|
+
. "$script_dir/tmux-at.bash"
|
|
19
21
|
credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$script_dir/seat-credential.mjs}"
|
|
20
22
|
|
|
21
23
|
proj="${1:-}"; name="${2:-}"
|
|
@@ -87,10 +89,10 @@ fi
|
|
|
87
89
|
|
|
88
90
|
sock="${PEERTABLE_TMUX_SOCKET:-${TMPDIR:-/tmp/}claude-tmux-sockets/claude.sock}"
|
|
89
91
|
sess="peer-$name"
|
|
90
|
-
|
|
92
|
+
tmux_at has-session -t "$sess" 2>/dev/null || {
|
|
91
93
|
echo "SEAT_CHANGE_SEAT_MISSING: ${sess}" >&2; exit 1;
|
|
92
94
|
}
|
|
93
|
-
screen=$(
|
|
95
|
+
screen=$(tmux_at capture-pane -t "$sess" -p -S -25 2>/dev/null) || {
|
|
94
96
|
echo "SEAT_CHANGE_SEAT_UNREADABLE: $sess" >&2; exit 1;
|
|
95
97
|
}
|
|
96
98
|
# busy の判定文字列は seat-status-bridge と同じ(Claude のステータス行にも Codex の `Working (…)` にも出る)
|
|
File without changes
|
|
@@ -21,6 +21,8 @@ if [ -f "$record" ]; then
|
|
|
21
21
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then exit 0; fi
|
|
22
22
|
if ! "$force" && grep -q 'WRITE_DENIED' "$log" 2>/dev/null; then echo "${name}-bridge: 前回はWRITE_DENIEDで終了。--forceを指定すること" >&2; exit 1; fi
|
|
23
23
|
fi
|
|
24
|
+
# shellcheck disable=SC1091
|
|
25
|
+
. "$(dirname "$0")/tmux-at.bash"
|
|
24
26
|
sock=$(node "$(dirname "$0")/tmux-socket.mjs")
|
|
25
27
|
room=$(node -e 'process.stdout.write(require(process.argv[1]).room)' "$team/setup-state.json")
|
|
26
28
|
session="peertable-${name}-${room}"
|
|
@@ -39,8 +41,8 @@ for v in PEERTABLE_TMUX_SOCKET PEERTABLE_POST_TOKEN PEERTABLE_CREDENTIAL_FILE PE
|
|
|
39
41
|
done
|
|
40
42
|
# **session が在るだけでは常駐が生きている証拠にならない**(中の node だけ死んで殻が残る)。
|
|
41
43
|
# 上で pid 生存を確かめて here まで来た=生きていないので、殻は畳んでから立て直す。
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
tmux_at kill-session -t "$session" 2>/dev/null || true
|
|
45
|
+
tmux_at new-session -d -s "$session" "env${env_prefix} node $(dirname "$0")/$script $(printf '%q ' "$proj" "$@") >> $(printf '%q' "$log") 2>&1"
|
|
44
46
|
for _ in $(seq 1 30); do
|
|
45
47
|
if [ -f "$record" ] && node -e 'process.exit(require(process.argv[1]).ready_at?0:1)' "$record"; then
|
|
46
48
|
# **末尾は本物の改行にする。** `"\\n"` と書くと JS がリテラルの `\`+`n` を足し、
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -33,6 +33,8 @@ credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$(dirname "$0")/seat-credentia
|
|
|
33
33
|
room_mcp_helper="${PEERTABLE_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-room-mcp.mjs}"
|
|
34
34
|
codex_room_mcp_helper="${PEERTABLE_CODEX_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-codex-room-mcp.mjs}"
|
|
35
35
|
aiterm_launch_helper="${PEERTABLE_AITERM_LAUNCH_HELPER:-$(dirname "$0")/aiterm-launch.mjs}"
|
|
36
|
+
# shellcheck disable=SC1091
|
|
37
|
+
. "$(dirname "$0")/tmux-at.bash"
|
|
36
38
|
peertable_repo=$(cd "$(dirname "$0")/../.." && pwd -P)
|
|
37
39
|
peertable_client="$peertable_repo/room/client.mjs"
|
|
38
40
|
credential_file=""
|
|
@@ -68,16 +70,16 @@ rollback_brief() {
|
|
|
68
70
|
local rollback_failed=0
|
|
69
71
|
local encoded_name member_code listing
|
|
70
72
|
|
|
71
|
-
if [ -n "$
|
|
72
|
-
if
|
|
73
|
-
if !
|
|
73
|
+
if [ -n "$sess" ] && [ "$seat_created" = true ]; then
|
|
74
|
+
if tmux_at has-session -t "$sess" 2>/dev/null; then
|
|
75
|
+
if ! tmux_at kill-session -t "$sess" 2>/dev/null; then
|
|
74
76
|
rollback_failed=1
|
|
75
77
|
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session を停止できない: ${sess}" >&2
|
|
76
|
-
elif
|
|
78
|
+
elif tmux_at has-session -t "$sess" 2>/dev/null; then
|
|
77
79
|
rollback_failed=1
|
|
78
80
|
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: tmux session が停止後も残っている: ${sess}" >&2
|
|
79
81
|
fi
|
|
80
|
-
elif
|
|
82
|
+
elif tmux_at list-sessions >/dev/null 2>&1; then
|
|
81
83
|
: # serverへ到達でき、対象sessionが無い
|
|
82
84
|
else
|
|
83
85
|
rollback_failed=1
|
|
@@ -263,7 +265,7 @@ esac
|
|
|
263
265
|
# 前の卓の残骸を回収してから、Aiterm の公開 agent launcher を唯一の席起動経路として呼ぶ。
|
|
264
266
|
# direct CLI launch へ戻るfallbackは置かない。Aiterm が作る同名PTYと launch receipt が、
|
|
265
267
|
# 以後の `pty_read` / `agent_configure` / room metadata を同じsessionへ束縛する。
|
|
266
|
-
|
|
268
|
+
tmux_at kill-session -t "$sess" 2>/dev/null || true
|
|
267
269
|
rm -f "$proj/.team/seats/$name.json"
|
|
268
270
|
|
|
269
271
|
launch_env=(
|
|
@@ -312,7 +314,7 @@ then
|
|
|
312
314
|
fi
|
|
313
315
|
aiterm_session_id="$sess"
|
|
314
316
|
brief_dispatched=true
|
|
315
|
-
seat_tmux=$(
|
|
317
|
+
seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
|
|
316
318
|
|
|
317
319
|
# Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
|
|
318
320
|
# ヘッダ/trust dialog待機をここへ重ねると、brief turnでヘッダが画面外へ流れた正常席をrollbackする。
|
|
@@ -325,15 +327,16 @@ echo "launched: ${sess}(${vendor} / ${model}${effort:+ / $effort} / room=${roo
|
|
|
325
327
|
room_ready_deadline=$((SECONDS + 30))
|
|
326
328
|
room_ready=false
|
|
327
329
|
grok_trust_accepted=false
|
|
330
|
+
mcp_consent_accepted=false
|
|
328
331
|
while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
329
332
|
# Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
|
|
330
333
|
# Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
|
|
331
334
|
# 汎用的に承認するfallbackにはしない。承認後のMCP初期化時間は改めて30秒確保する。
|
|
332
335
|
if [ "$vendor" = grok ] && [ "$grok_trust_accepted" != true ]; then
|
|
333
|
-
grok_screen=$(
|
|
336
|
+
grok_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
334
337
|
case "$grok_screen" in
|
|
335
338
|
*"Do you trust the contents of this directory?"*)
|
|
336
|
-
if !
|
|
339
|
+
if ! tmux_at send-keys -t "$sess" y; then
|
|
337
340
|
echo "SEAT_GROK_TRUST_FAILED: workspace trustへ応答できない" >&2
|
|
338
341
|
exit 1
|
|
339
342
|
fi
|
|
@@ -343,6 +346,22 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
343
346
|
;;
|
|
344
347
|
esac
|
|
345
348
|
fi
|
|
349
|
+
# aiterm claude_agent は --dangerously-skip-permissions を付けない。project の room MCP
|
|
350
|
+
# 同意が member 登録より前に出る。選択肢2(this and all future)だけを通す。
|
|
351
|
+
if [ "$vendor" = claude ] && [ "$mcp_consent_accepted" != true ]; then
|
|
352
|
+
claude_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
353
|
+
case "$claude_screen" in
|
|
354
|
+
*"New MCP server found in this project: room"*)
|
|
355
|
+
if ! tmux_at send-keys -t "$sess" Down || ! tmux_at send-keys -t "$sess" Enter; then
|
|
356
|
+
echo "SEAT_CLAUDE_MCP_CONSENT_FAILED: room MCP 同意へ応答できない" >&2
|
|
357
|
+
exit 1
|
|
358
|
+
fi
|
|
359
|
+
mcp_consent_accepted=true
|
|
360
|
+
room_ready_deadline=$((SECONDS + 30))
|
|
361
|
+
echo "claude room MCP consent: accepted"
|
|
362
|
+
;;
|
|
363
|
+
esac
|
|
364
|
+
fi
|
|
346
365
|
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
347
366
|
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
|
|
348
367
|
room_ready=true
|
|
@@ -368,7 +387,7 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
368
387
|
brief_ready_streak=0
|
|
369
388
|
brief_ready=false
|
|
370
389
|
while [ $SECONDS -lt "$brief_ready_deadline" ]; do
|
|
371
|
-
brief_ready_screen=$(
|
|
390
|
+
brief_ready_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
372
391
|
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
|
|
373
392
|
brief_ready_streak=$((brief_ready_streak + 1))
|
|
374
393
|
if [ "$brief_ready_streak" -ge 3 ]; then
|
|
@@ -388,19 +407,19 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
388
407
|
# mountした直後に paste と Enter を同一tickで受けると、Enterだけ落ちる。
|
|
389
408
|
sleep 1
|
|
390
409
|
|
|
391
|
-
brief_before=$(
|
|
410
|
+
brief_before=$(tmux_at capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
|
|
392
411
|
brief_buffer="peertable-brief-${name}-$$"
|
|
393
|
-
if !
|
|
412
|
+
if ! tmux_at load-buffer -b "$brief_buffer" "$brief_file"; then
|
|
394
413
|
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux buffer 読み込みに失敗(席は着席済み)" >&2
|
|
395
414
|
exit 1
|
|
396
415
|
fi
|
|
397
|
-
if !
|
|
398
|
-
|
|
416
|
+
if ! tmux_at paste-buffer -b "$brief_buffer" -d -t "$sess"; then
|
|
417
|
+
tmux_at delete-buffer -b "$brief_buffer" 2>/dev/null || true
|
|
399
418
|
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux paste に失敗(席は着席済み)" >&2
|
|
400
419
|
exit 1
|
|
401
420
|
fi
|
|
402
421
|
sleep 1
|
|
403
|
-
if !
|
|
422
|
+
if ! tmux_at send-keys -t "$sess" Enter; then
|
|
404
423
|
echo "LAUNCH_BRIEF_SEND_FAILED: brief の submit に失敗(席は着席済み)" >&2
|
|
405
424
|
exit 1
|
|
406
425
|
fi
|
|
@@ -411,7 +430,7 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
411
430
|
brief_deadline=$((SECONDS + 30))
|
|
412
431
|
brief_turn_started=false
|
|
413
432
|
while [ $SECONDS -lt "$brief_deadline" ]; do
|
|
414
|
-
brief_screen=$(
|
|
433
|
+
brief_screen=$(tmux_at capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
|
|
415
434
|
case "$brief_screen" in
|
|
416
435
|
*"esc to interrupt"*)
|
|
417
436
|
if [ "$brief_screen" != "$brief_before" ]; then brief_turn_started=true; break; fi
|
|
@@ -438,13 +457,13 @@ fi
|
|
|
438
457
|
# この file が主張するのは「この pid はこの席だった」という**識別**であって、生死ではない。
|
|
439
458
|
# 生きているかは attach する側(Lattice)が lstart+argv の再観測で確かめる。
|
|
440
459
|
seat_pid=""
|
|
441
|
-
pane_pid=$(
|
|
460
|
+
pane_pid=$(tmux_at list-panes -t "$sess" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
|
|
461
|
+
seat_ident=""
|
|
442
462
|
if [ -n "$pane_pid" ]; then
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if [ "$(printf '%s\n' "$leaders" | grep -c .)" = "1" ]; then seat_pid=$(printf '%s' "$leaders" | tr -d ' \n'); fi
|
|
463
|
+
seat_ident=$(node "$(dirname "$0")/seat-identity.mjs" "$pane_pid" 2>/dev/null || true)
|
|
464
|
+
fi
|
|
465
|
+
if [ -n "$seat_ident" ]; then
|
|
466
|
+
seat_pid=$(python3 -c "import json,sys;print(json.load(sys.stdin)['pid'])" <<<"$seat_ident")
|
|
448
467
|
fi
|
|
449
468
|
if [ -z "$seat_pid" ]; then
|
|
450
469
|
# 記録が無ければ席は attach できず、装置の介入は協調 hold のままになる。**黙らない。**
|
|
@@ -455,14 +474,13 @@ else
|
|
|
455
474
|
# `input.session === actor.session` を要求し(`runtime-pull-intake.mjs:674`)、actor session は
|
|
456
475
|
# 上の env_line が入れる `LATTICE_TODO_ACTOR_SESSION=$name` である。tmux 識別子を混ぜると
|
|
457
476
|
# attach が必ず `WORKER_ACTOR_MISMATCH` で拒否される(mio の監査で発覚・room [937])。
|
|
458
|
-
if ! python3 - "$proj/.team/seats/$name.json" "$name" "$name" "$
|
|
477
|
+
if ! python3 - "$proj/.team/seats/$name.json" "$name" "$name" "$seat_ident" <<'PY'
|
|
459
478
|
import hashlib, json, os, subprocess, sys, tempfile
|
|
460
|
-
out, name, session,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
argv =
|
|
465
|
-
capture_output=True, text=True, check=True).stdout.strip()
|
|
479
|
+
out, name, session, ident_raw = sys.argv[1:5]
|
|
480
|
+
ident = json.loads(ident_raw)
|
|
481
|
+
pid = int(ident['pid'])
|
|
482
|
+
started = ident['started_identity']
|
|
483
|
+
argv = ident['argv']
|
|
466
484
|
if not started or not argv:
|
|
467
485
|
sys.exit('pid の lstart/args を観測できない')
|
|
468
486
|
record = {
|
|
@@ -499,11 +517,15 @@ fi
|
|
|
499
517
|
# **欄が無い登録で既存の素性を消さない**(upsert)ことが前提である。
|
|
500
518
|
# effort は渡された時だけ入れる——欄が無い=「不明」ではなく「CLI 既定で走っている」。
|
|
501
519
|
# ここが失敗しても席は着席済みなので落とさない。ただし黙っては飲まない。
|
|
502
|
-
|
|
520
|
+
tmux_ns=$(node "$(dirname "$0")/tmux-socket.mjs" --namespace)
|
|
521
|
+
meta=$(python3 - "$name" "$vendor" "$model" "$effort" "$sock" "$sess" "$role" "$aiterm_session_id" "$tmux_ns" <<'PY'
|
|
503
522
|
import json, sys
|
|
504
|
-
name, vendor, model, effort, sock, sess, role, aiterm_session_id = sys.argv[1:
|
|
523
|
+
name, vendor, model, effort, sock, sess, role, aiterm_session_id, tmux_ns = sys.argv[1:10]
|
|
524
|
+
observe = {'tmux_socket': sock, 'tmux_target': sess}
|
|
525
|
+
if tmux_ns:
|
|
526
|
+
observe['tmux_namespace'] = tmux_ns
|
|
505
527
|
body = {'name': name, 'vendor': vendor, 'model': model, 'role': role, 'aiterm_session_id': aiterm_session_id,
|
|
506
|
-
'observe':
|
|
528
|
+
'observe': observe}
|
|
507
529
|
if effort:
|
|
508
530
|
body['effort'] = effort
|
|
509
531
|
print(json.dumps(body))
|
|
@@ -15,6 +15,8 @@ case "$name" in
|
|
|
15
15
|
esac
|
|
16
16
|
|
|
17
17
|
script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
18
|
+
# shellcheck disable=SC1091
|
|
19
|
+
. "$script_dir/tmux-at.bash"
|
|
18
20
|
credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$script_dir/seat-credential.mjs}"
|
|
19
21
|
state="$proj/.team/setup-state.json"
|
|
20
22
|
[ -f "$state" ] || { echo "SEAT_LEAVE_STATE_MISSING: $state" >&2; exit 1; }
|
|
@@ -48,21 +50,18 @@ PY
|
|
|
48
50
|
EOF
|
|
49
51
|
|
|
50
52
|
failed=0
|
|
51
|
-
if
|
|
52
|
-
|
|
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
|
|
53
|
+
if tmux_at has-session -t "$target" 2>/dev/null; then
|
|
54
|
+
if ! tmux_at kill-session -t "$target"; then
|
|
56
55
|
echo "SEAT_LEAVE_SESSION_FAILED: $target" >&2
|
|
57
56
|
failed=1
|
|
58
|
-
elif
|
|
57
|
+
elif tmux_at has-session -t "$target" 2>/dev/null; then
|
|
59
58
|
echo "SEAT_LEAVE_SESSION_FAILED: $target が撤去後も残っている" >&2
|
|
60
59
|
failed=1
|
|
61
60
|
fi
|
|
62
|
-
elif
|
|
61
|
+
elif tmux_at list-sessions >/dev/null 2>&1; then
|
|
63
62
|
: # serverへ到達でき、対象sessionが無い
|
|
64
63
|
else
|
|
65
|
-
echo "SEAT_LEAVE_SESSION_UNREADABLE: $member_socket" >&2
|
|
64
|
+
echo "SEAT_LEAVE_SESSION_UNREADABLE: ${member_socket:-tmux}" >&2
|
|
66
65
|
failed=1
|
|
67
66
|
fi
|
|
68
67
|
|
|
File without changes
|
|
File without changes
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { execFileSync } from 'node:child_process'
|
|
11
11
|
import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { join } from 'node:path'
|
|
13
|
+
import { resolveLatticeExecutable } from './seat-usage.mjs'
|
|
13
14
|
|
|
14
15
|
const args = process.argv.slice(2)
|
|
15
16
|
const project = args.shift()
|
|
@@ -43,7 +44,8 @@ function readLatticeState(previous = null) {
|
|
|
43
44
|
const source = `${info.mtimeMs}:${info.size}`
|
|
44
45
|
if (previous?.source === source && previous.error === undefined) return previous
|
|
45
46
|
try {
|
|
46
|
-
const
|
|
47
|
+
const lattice = resolveLatticeExecutable(latticeCli)
|
|
48
|
+
const status = JSON.parse(execFileSync(lattice.command, lattice.argv, {
|
|
47
49
|
cwd: project,
|
|
48
50
|
encoding: 'utf8',
|
|
49
51
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pane_pid から Lattice attach 用の pid / lstart / argv を1件に決める。
|
|
3
|
+
// POSIX は ps。Windows は Win32_Process(ps -Ao は Git Bash で unknown option -- A)。
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
const panePid = process.argv[2]
|
|
7
|
+
if (!/^\d+$/.test(panePid || '')) {
|
|
8
|
+
console.error('usage: seat-identity.mjs <pane_pid>')
|
|
9
|
+
process.exit(2)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AGENT = /(?:claude|codex|grok|composer)(?:\.cmd|\.exe)?(?:\s|$)/iu
|
|
13
|
+
|
|
14
|
+
function posixIdentity(pid) {
|
|
15
|
+
const table = execFileSync('ps', ['-Ao', 'pid=,ppid=,pgid='], { encoding: 'utf8' })
|
|
16
|
+
const leaders = table.split('\n').flatMap((line) => {
|
|
17
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s*$/u.exec(line)
|
|
18
|
+
if (!match) return []
|
|
19
|
+
const [, child, ppid, pgid] = match
|
|
20
|
+
return ppid === String(pid) && child === pgid ? [child] : []
|
|
21
|
+
})
|
|
22
|
+
const chosen = leaders.length === 1 ? leaders[0] : String(pid)
|
|
23
|
+
const started = execFileSync('/bin/ps', ['-o', 'lstart=', '-p', chosen], { encoding: 'utf8' }).trim()
|
|
24
|
+
const argv = execFileSync('/bin/ps', ['-o', 'args=', '-p', chosen], { encoding: 'utf8' }).trim()
|
|
25
|
+
if (!started || !argv) throw new Error('pid の lstart/args を観測できない')
|
|
26
|
+
return { pid: Number(chosen), started_identity: started, argv }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function winQuery(filter) {
|
|
30
|
+
const script = `Get-CimInstance Win32_Process -Filter ${JSON.stringify(filter)} | Select-Object ProcessId,ParentProcessId,CreationDate,CommandLine | ConvertTo-Json -Compress`
|
|
31
|
+
const out = execFileSync('powershell.exe', ['-NoProfile', '-Command', script], { encoding: 'utf8' }).trim()
|
|
32
|
+
if (!out) return []
|
|
33
|
+
const parsed = JSON.parse(out)
|
|
34
|
+
return Array.isArray(parsed) ? parsed : [parsed]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function winIdentity(pid) {
|
|
38
|
+
const self = winQuery(`ProcessId=${pid}`)[0]
|
|
39
|
+
if (!self) throw new Error('Win32_Process を観測できない')
|
|
40
|
+
const children = winQuery(`ParentProcessId=${pid}`)
|
|
41
|
+
const agentKids = children.filter((row) => AGENT.test(String(row.CommandLine || '')))
|
|
42
|
+
let chosen = self
|
|
43
|
+
if (AGENT.test(String(self.CommandLine || ''))) chosen = self
|
|
44
|
+
else if (agentKids.length === 1) chosen = agentKids[0]
|
|
45
|
+
else if (children.length === 1) chosen = children[0]
|
|
46
|
+
const started = chosen.CreationDate ? new Date(chosen.CreationDate).toISOString() : ''
|
|
47
|
+
const argv = String(chosen.CommandLine || '').trim()
|
|
48
|
+
if (!started || !argv) throw new Error('pid の CreationDate/CommandLine を観測できない')
|
|
49
|
+
return { pid: Number(chosen.ProcessId), started_identity: started, argv }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const ident = process.platform === 'win32' ? winIdentity(panePid) : posixIdentity(panePid)
|
|
54
|
+
process.stdout.write(`${JSON.stringify(ident)}\n`)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(error.message || error)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
@@ -26,7 +26,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
26
26
|
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
27
27
|
import { join } from 'node:path'
|
|
28
28
|
|
|
29
|
-
import { classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxPanePid } from './seat-usage.mjs'
|
|
29
|
+
import { classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
|
|
30
30
|
|
|
31
31
|
const args = process.argv.slice(2)
|
|
32
32
|
const proj = args[0]
|
|
@@ -87,7 +87,7 @@ const defaultSocket = () => {
|
|
|
87
87
|
if (defaultSocketResolution === null) defaultSocketResolution = resolveTmuxSocket(process.env)
|
|
88
88
|
return defaultSocketResolution.socket
|
|
89
89
|
}
|
|
90
|
-
const tmux = (socket, ...a) => { try { return execFileSync('tmux',
|
|
90
|
+
const tmux = (socket, ...a) => { try { return execFileSync('tmux', tmuxArgv(a, { socket }), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) } catch { return null } }
|
|
91
91
|
|
|
92
92
|
// 監視するのは room の members に居る席だけ。tmux の `peer-*` を全部拾うと、同じマシンで走る別の卓を晒す
|
|
93
93
|
async function seats() {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
3
|
import { execFileSync } from 'node:child_process'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
4
|
+
import { homedir, tmpdir } from 'node:os'
|
|
5
|
+
import path, { join } from 'node:path'
|
|
5
6
|
|
|
6
7
|
const TOKEN_HINT = /[↓↑]\s*([0-9]+(?:\.[0-9]+)?)\s*([kKmM]?)\s*tokens\b/gu
|
|
7
8
|
|
|
@@ -17,9 +18,9 @@ export function supportsMemberObservation(payload) {
|
|
|
17
18
|
return payload?.capabilities?.member_observation_v1 === true
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/** aiterm-mcp の POSIX 既定ソケット規則。 */
|
|
21
|
+
/** aiterm-mcp の POSIX 既定ソケット規則。Windows の path.join で `/tmp` を壊さない。 */
|
|
21
22
|
export function defaultTmuxSocket(env) {
|
|
22
|
-
return join(env.TMPDIR || '/tmp', 'claude-tmux-sockets', 'claude.sock')
|
|
23
|
+
return path.posix.join(env.TMPDIR || '/tmp', 'claude-tmux-sockets', 'claude.sock')
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const defaultTmuxProbe = {
|
|
@@ -69,6 +70,50 @@ export function resolveTmuxSocket(env, probe = defaultTmuxProbe) {
|
|
|
69
70
|
return { socket: fallback, source: 'default', candidates: [], error: null }
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Windows の psmux は -S を黙って既定 namespace へ落とす(aiterm 実測 2026-08-16)。
|
|
75
|
+
* aiterm と同じ -L namespace を使う。名前の式は aiterm-mcp core.js の WIN_NS と一致させる。
|
|
76
|
+
*/
|
|
77
|
+
export function usesPsmuxNamespace(env = process.env, platform = process.platform) {
|
|
78
|
+
if (env.PEERTABLE_FORCE_POSIX_TMUX === '1') return false
|
|
79
|
+
if (env.PEERTABLE_TMUX_L || env.AITERM_PSMUX_NS) return true
|
|
80
|
+
return platform === 'win32'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function aitermPsmuxNamespace(env = process.env, platform = process.platform) {
|
|
84
|
+
if (env.PEERTABLE_TMUX_L) return env.PEERTABLE_TMUX_L
|
|
85
|
+
if (env.AITERM_PSMUX_NS) return env.AITERM_PSMUX_NS
|
|
86
|
+
const isWin = platform === 'win32'
|
|
87
|
+
const root = env.TMPDIR ?? (isWin ? env.TEMP ?? tmpdir() : '/tmp')
|
|
88
|
+
const joinPath = isWin ? path.win32.join : path.posix.join
|
|
89
|
+
const sockdir = joinPath(root, 'claude-tmux-sockets')
|
|
90
|
+
return `aiterm-${createHash('sha1').update(sockdir).digest('hex').slice(0, 12)}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** tmux/psmux へ渡す接続引数。POSIX は -S socket、Windows は -L namespace。 */
|
|
94
|
+
export function tmuxArgv(extraArgs = [], { socket, env = process.env, platform = process.platform } = {}) {
|
|
95
|
+
if (usesPsmuxNamespace(env, platform)) {
|
|
96
|
+
return ['-L', aitermPsmuxNamespace(env, platform), ...extraArgs]
|
|
97
|
+
}
|
|
98
|
+
return ['-S', socket, ...extraArgs]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* npm の extensionless shim は Windows の execFile で ENOENT。
|
|
103
|
+
* 隣の .cmd を cmd.exe /c で起動する(shell:true は DEP0190 かつ引数連結)。
|
|
104
|
+
*/
|
|
105
|
+
export function resolveLatticeExecutable(cli, { platform = process.platform, exists = existsSync } = {}) {
|
|
106
|
+
if (typeof cli !== 'string' || !cli) return { command: cli, argv: ['todo', 'status', '--json'] }
|
|
107
|
+
if (platform !== 'win32') return { command: cli, argv: ['todo', 'status', '--json'] }
|
|
108
|
+
const lower = cli.toLowerCase()
|
|
109
|
+
const cmd = lower.endsWith('.cmd') || lower.endsWith('.bat') || lower.endsWith('.exe')
|
|
110
|
+
? cli
|
|
111
|
+
: exists(`${cli}.cmd`)
|
|
112
|
+
? `${cli}.cmd`
|
|
113
|
+
: cli
|
|
114
|
+
return { command: process.env.ComSpec || 'cmd.exe', argv: ['/d', '/c', cmd, 'todo', 'status', '--json'] }
|
|
115
|
+
}
|
|
116
|
+
|
|
72
117
|
/** member の自己申告を優先し、無い既存 member だけ旧 session 名へ互換フォールバックする。 */
|
|
73
118
|
export function resolveSeatObservation(member, defaultSocket) {
|
|
74
119
|
const observe = member?.observe
|
|
@@ -164,7 +209,7 @@ export function classifyPaneTail(tail) {
|
|
|
164
209
|
*/
|
|
165
210
|
export function tmuxPanePid(socket, target, exec = execFileSync) {
|
|
166
211
|
try {
|
|
167
|
-
const out = exec('tmux', ['
|
|
212
|
+
const out = exec('tmux', tmuxArgv(['list-panes', '-t', target, '-F', '#{pane_pid}'], { socket }), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
168
213
|
const pid = out.trim().split('\n')[0]
|
|
169
214
|
return pid && /^[0-9]+$/.test(pid) ? pid : null
|
|
170
215
|
} catch {
|
package/skill/scripts/setup.sh
CHANGED
|
File without changes
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
set -e
|
|
15
15
|
proj="$1"
|
|
16
16
|
script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
17
|
+
# shellcheck disable=SC1091
|
|
18
|
+
. "$script_dir/tmux-at.bash"
|
|
17
19
|
peertable_repo=$(cd "$script_dir/../.." && pwd -P)
|
|
18
20
|
mode=archive
|
|
19
21
|
for arg in "${@:2}"; do
|
|
@@ -75,12 +77,12 @@ else
|
|
|
75
77
|
name=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["name"])' "$member")
|
|
76
78
|
target=$(python3 -c 'import json,sys; x=json.loads(sys.argv[1]); print((x.get("observe") or {}).get("tmux_target") or "peer-" + x["name"])' "$member")
|
|
77
79
|
member_sock=$(python3 -c 'import json,sys; x=json.loads(sys.argv[1]); print((x.get("observe") or {}).get("tmux_socket") or sys.argv[2])' "$member" "$sock")
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
+
if tmux_at has-session -t "$target" 2>/dev/null; then
|
|
81
|
+
tmux_at kill-session -t "$target" && closed=$((closed + 1))
|
|
80
82
|
fi
|
|
81
83
|
done <<<"$member_lines"
|
|
82
84
|
if [ "$closed" -gt 0 ]; then did "席の終了(${closed}席)"; else skip "席の終了(生きている席なし)"; fi
|
|
83
|
-
left=$(
|
|
85
|
+
left=$(tmux_at list-sessions 2>/dev/null | grep -c '^peer-' || true)
|
|
84
86
|
[ "${left:-0}" -eq 0 ] || echo "teardown: [注記] 他の卓の peer-* が ${left}件 残っている(この卓のものではないので畳まない)"
|
|
85
87
|
fi
|
|
86
88
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# sourced by launch-seat / leave-seat / teardown / ensure-bridge / change-seat
|
|
2
|
+
# POSIX は tmux -S <sock>。Windows psmux は -L <aiterm-ns>(-S は既定 namespace へ落ちる)。
|
|
3
|
+
_peertable_tmux_scripts=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
4
|
+
tmux_at() {
|
|
5
|
+
local prefix
|
|
6
|
+
prefix=$(node "$_peertable_tmux_scripts/tmux-socket.mjs" --prefix) || return 1
|
|
7
|
+
# shellcheck disable=SC2206
|
|
8
|
+
local -a conn=($prefix)
|
|
9
|
+
command tmux "${conn[@]}" "$@"
|
|
10
|
+
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolveTmuxSocket } from './seat-usage.mjs'
|
|
2
|
+
import { aitermPsmuxNamespace, resolveTmuxSocket, tmuxArgv, usesPsmuxNamespace } from './seat-usage.mjs'
|
|
3
3
|
|
|
4
4
|
const result = resolveTmuxSocket(process.env)
|
|
5
|
-
if (result.error?.code === 'PEERTABLE_TMUX_SOCKET_AMBIGUOUS') {
|
|
5
|
+
if (result.error?.code === 'PEERTABLE_TMUX_SOCKET_AMBIGUOUS' && !usesPsmuxNamespace(process.env)) {
|
|
6
6
|
console.error(`${result.error.code}: ${result.candidates.join(', ')}`)
|
|
7
7
|
process.exit(1)
|
|
8
8
|
}
|
|
9
|
-
if (process.argv.includes('--
|
|
10
|
-
|
|
9
|
+
if (process.argv.includes('--prefix')) {
|
|
10
|
+
console.log(tmuxArgv([], { socket: result.socket }).join(' '))
|
|
11
|
+
} else if (process.argv.includes('--namespace')) {
|
|
12
|
+
console.log(usesPsmuxNamespace(process.env) ? aitermPsmuxNamespace(process.env) : '')
|
|
13
|
+
} else if (process.argv.includes('--source')) {
|
|
14
|
+
console.log(usesPsmuxNamespace(process.env) ? 'psmux-namespace' : result.source)
|
|
15
|
+
} else if (usesPsmuxNamespace(process.env)) {
|
|
16
|
+
console.log(aitermPsmuxNamespace(process.env))
|
|
17
|
+
} else {
|
|
18
|
+
console.log(result.socket)
|
|
19
|
+
}
|
|
File without changes
|
|
@@ -16,7 +16,7 @@ import { execFile } from 'node:child_process'
|
|
|
16
16
|
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
18
|
import { promisify } from 'node:util'
|
|
19
|
-
import { resolveSeatObservation } from './seat-usage.mjs'
|
|
19
|
+
import { resolveSeatObservation, tmuxArgv } from './seat-usage.mjs'
|
|
20
20
|
import { BROADCAST_RECIPIENT, formatWakeNotice, isWakeupBridgeTarget, shouldDeferGrokWake } from './wakeup-delivery.mjs'
|
|
21
21
|
|
|
22
22
|
const run = promisify(execFile)
|
|
@@ -236,7 +236,7 @@ async function wake(seat, msgs) {
|
|
|
236
236
|
throw error
|
|
237
237
|
}
|
|
238
238
|
if (member.vendor === 'grok') {
|
|
239
|
-
const pane = await run('tmux', ['
|
|
239
|
+
const pane = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
|
|
240
240
|
const tail = String(pane.stdout).split('\n').slice(-14).join('\n')
|
|
241
241
|
if (shouldDeferGrokWake(member.vendor, tail)) {
|
|
242
242
|
if (!deferredBusy.has(seat)) {
|
|
@@ -250,11 +250,11 @@ async function wake(seat, msgs) {
|
|
|
250
250
|
// Codexの入力欄は本文とEnterを同じtmux commandで送ると、初回turn完了後に
|
|
251
251
|
// 本文が入力欄へ残ることがある。再試行時の半入力も含め、正規のsubmitを分離する。
|
|
252
252
|
// 最後のEnterまで成功しない限りwakeは成功扱いにせず、flushSeatのreceiptも確定しない。
|
|
253
|
-
await run('tmux', ['
|
|
253
|
+
await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, 'C-u'], { socket: observation.socket }))
|
|
254
254
|
await sleep(100)
|
|
255
|
-
await run('tmux', ['
|
|
255
|
+
await run('tmux', tmuxArgv(['send-keys', '-l', '-t', observation.target, text], { socket: observation.socket }))
|
|
256
256
|
await sleep(750)
|
|
257
|
-
await run('tmux', ['
|
|
257
|
+
await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, 'Enter'], { socket: observation.socket }))
|
|
258
258
|
log(`起こした: ${seat} ← ${msgs.length} 件(最新 seq ${last.seq})`)
|
|
259
259
|
}
|
|
260
260
|
|