peertable 0.4.0 → 0.4.2
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 -120
- package/README.md +158 -156
- package/package.json +49 -49
- package/room/Dockerfile +6 -6
- package/room/client.mjs +6 -1
- package/skill/SKILL.md +245 -243
- 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 +5 -3
- package/skill/scripts/parent-watch.mjs +7 -6
- package/skill/scripts/seat-identity.mjs +58 -0
- package/skill/scripts/seat-status-bridge.mjs +2 -2
- package/skill/scripts/seat-usage.mjs +50 -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 +49 -21
- package/skill/scripts/wakeup-delivery.mjs +37 -0
- 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 -128
- 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.2' })
|
|
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.2' })
|
|
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
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# model / effort / vendor は任意。親はオーナーの対話セッション(決定40)なので、席と違って
|
|
6
6
|
# 起動時に確定した値を script が知らない——**渡された時だけ**参加者一覧の素性として登録する。
|
|
7
7
|
# 渡さなければ欄ごと出ない(「不明」ではなく「素性を名乗っていない」)。
|
|
8
|
-
# vendor は claude
|
|
9
|
-
#
|
|
8
|
+
# vendor は claude(既定)、codex、または grok。model だけ渡して vendor を渡さない場合は
|
|
9
|
+
# claude とみなす(後方互換)。
|
|
10
10
|
# 親は MCP を後付けできないので room へは HTTP API 直で入る(決定40 の operating notes)。
|
|
11
11
|
set -e
|
|
12
12
|
proj="$1"; name="${2:-bell}"; model="$3"; effort="$4"; vendor="$5"
|
|
@@ -19,7 +19,7 @@ if [ -z "${PEERTABLE_POST_TOKEN:-}" ] && [ -f "$HOME/.config/peertable.env" ]; t
|
|
|
19
19
|
. "$HOME/.config/peertable.env"
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
-
# 親はAiterm席ではない。Claude/Codexとも、親自身が所有するparent-watchを配送先にする。
|
|
22
|
+
# 親はAiterm席ではない。Claude/Codex/Grokとも、親自身が所有するparent-watchを配送先にする。
|
|
23
23
|
# tmux observeやCodex thread IDを登録すると、通常席bridge/外部resumeへ誤配送される。
|
|
24
24
|
parent_vendor="${vendor:-claude}"
|
|
25
25
|
member=$(python3 - "$name" "$model" "$effort" "$parent_vendor" <<'PY'
|
|
@@ -67,6 +67,8 @@ if PEERTABLE_PARENT_HOST="$parent_vendor" node "$here/parent-watch.mjs" "$proj"
|
|
|
67
67
|
echo "parent-watch cursor ready: ${name}(host=${parent_vendor})"
|
|
68
68
|
if [ "$parent_vendor" = "codex" ]; then
|
|
69
69
|
echo "PARENT_WATCH_START_REQUIRED: Codex親のbackground taskで1秒ごとに ${here}/codex-parent-watch.sh ${proj} ${name} を都度実行し、空でないstdoutだけを親turnへnotifyすること。background taskはloopを続けるが、Node processや端末sessionは常駐させない"
|
|
70
|
+
elif [ "$parent_vendor" = "grok" ]; then
|
|
71
|
+
echo "PARENT_WATCH_START_REQUIRED: Grok Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること。通常席用wakeup-bridgeに親を載せない"
|
|
70
72
|
else
|
|
71
73
|
echo "PARENT_WATCH_START_REQUIRED: Claude Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること"
|
|
72
74
|
fi
|
|
@@ -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()
|
|
@@ -32,7 +33,7 @@ const latticeCli = setup.lattice_cli || process.env.LATTICE_CLI || 'lattice'
|
|
|
32
33
|
const api = `${serverUrl}/api/${encodeURIComponent(room)}`
|
|
33
34
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
34
35
|
const now = () => new Date().toISOString()
|
|
35
|
-
const
|
|
36
|
+
const ROOM_UPDATE_FALLBACK = 'room全体の状況が更新された。roomログを読み、状況を把握して次の行動を判断する。'
|
|
36
37
|
const staffingBody = ({ ready, active }) => `現在、着手可能工程は ${ready} 件、着手中工程は ${active} 件になりました。標準は ${ready + active}+監査担当数です。円卓メンバー数を検討してください。`
|
|
37
38
|
|
|
38
39
|
function readLatticeState(previous = null) {
|
|
@@ -43,10 +44,12 @@ 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, ['todo', 'status', '--json'], {
|
|
47
49
|
cwd: project,
|
|
48
50
|
encoding: 'utf8',
|
|
49
51
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
shell: lattice.shell,
|
|
50
53
|
}))
|
|
51
54
|
if (!Array.isArray(status.next_ready) || !Array.isArray(status.active_set)) {
|
|
52
55
|
throw new Error('invalid schema')
|
|
@@ -198,10 +201,8 @@ async function acceptMessage(message) {
|
|
|
198
201
|
from: message.from,
|
|
199
202
|
to: message.to ?? null,
|
|
200
203
|
to_names: message.to_names ?? null,
|
|
201
|
-
body: roomUpdate ?
|
|
202
|
-
message
|
|
203
|
-
? { seq: message.seq, ts: message.ts, from: message.from, to: message.to }
|
|
204
|
-
: message,
|
|
204
|
+
body: roomUpdate ? (message.body || ROOM_UPDATE_FALLBACK) : message.body,
|
|
205
|
+
message,
|
|
205
206
|
})
|
|
206
207
|
}
|
|
207
208
|
state = { ...state, last_seq: message.seq, last_event_at: now() }
|
|
@@ -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,49 @@ 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 経由で起動する。
|
|
104
|
+
*/
|
|
105
|
+
export function resolveLatticeExecutable(cli, { platform = process.platform, exists = existsSync } = {}) {
|
|
106
|
+
if (typeof cli !== 'string' || !cli) return { command: cli, shell: false }
|
|
107
|
+
if (platform !== 'win32') return { command: cli, shell: false }
|
|
108
|
+
const lower = cli.toLowerCase()
|
|
109
|
+
if (lower.endsWith('.cmd') || lower.endsWith('.bat') || lower.endsWith('.exe')) {
|
|
110
|
+
return { command: cli, shell: true }
|
|
111
|
+
}
|
|
112
|
+
if (exists(`${cli}.cmd`)) return { command: `${cli}.cmd`, shell: true }
|
|
113
|
+
return { command: cli, shell: true }
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
/** member の自己申告を優先し、無い既存 member だけ旧 session 名へ互換フォールバックする。 */
|
|
73
117
|
export function resolveSeatObservation(member, defaultSocket) {
|
|
74
118
|
const observe = member?.observe
|
|
@@ -164,7 +208,7 @@ export function classifyPaneTail(tail) {
|
|
|
164
208
|
*/
|
|
165
209
|
export function tmuxPanePid(socket, target, exec = execFileSync) {
|
|
166
210
|
try {
|
|
167
|
-
const out = exec('tmux', ['
|
|
211
|
+
const out = exec('tmux', tmuxArgv(['list-panes', '-t', target, '-F', '#{pane_pid}'], { socket }), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
168
212
|
const pid = out.trim().split('\n')[0]
|
|
169
213
|
return pid && /^[0-9]+$/.test(pid) ? pid : null
|
|
170
214
|
} 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
|