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.
Files changed (38) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +122 -120
  3. package/README.md +158 -156
  4. package/package.json +49 -49
  5. package/room/Dockerfile +6 -6
  6. package/room/client.mjs +6 -1
  7. package/skill/SKILL.md +245 -243
  8. package/skill/scripts/aiterm-configure.mjs +1 -1
  9. package/skill/scripts/aiterm-launch.mjs +1 -1
  10. package/skill/scripts/archive-room-log.py +0 -0
  11. package/skill/scripts/change-effort.sh +0 -0
  12. package/skill/scripts/change-seat.sh +4 -2
  13. package/skill/scripts/codex-parent-watch.sh +0 -0
  14. package/skill/scripts/ensure-bridge.sh +4 -2
  15. package/skill/scripts/ensure-codex-room-mcp.mjs +0 -0
  16. package/skill/scripts/ensure-room-mcp.mjs +0 -0
  17. package/skill/scripts/external-pane.mjs +0 -0
  18. package/skill/scripts/launch-seat.sh +54 -32
  19. package/skill/scripts/leave-seat.sh +7 -8
  20. package/skill/scripts/make-plan-input.mjs +0 -0
  21. package/skill/scripts/parent-join.sh +5 -3
  22. package/skill/scripts/parent-watch.mjs +7 -6
  23. package/skill/scripts/seat-identity.mjs +58 -0
  24. package/skill/scripts/seat-status-bridge.mjs +2 -2
  25. package/skill/scripts/seat-usage.mjs +50 -6
  26. package/skill/scripts/setup.sh +0 -0
  27. package/skill/scripts/teardown.sh +5 -3
  28. package/skill/scripts/tmux-at.bash +10 -0
  29. package/skill/scripts/tmux-socket.mjs +13 -4
  30. package/skill/scripts/upgrade-team-assets.sh +0 -0
  31. package/skill/scripts/wakeup-bridge.mjs +49 -21
  32. package/skill/scripts/wakeup-delivery.mjs +37 -0
  33. package/skill/templates/charter.md +20 -20
  34. package/skill/templates/mcp.json +5 -5
  35. package/skill/templates/member-standalone.md +43 -43
  36. package/skill/templates/member.md +127 -127
  37. package/skill/templates/parent.md +131 -128
  38. 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.0' })
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.0' })
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
- tmux -S "$sock" has-session -t "$sess" 2>/dev/null || {
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=$(tmux -S "$sock" capture-pane -t "$sess" -p -S -25 2>/dev/null) || {
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
- tmux -S "$sock" kill-session -t "$session" 2>/dev/null || true
43
- tmux -S "$sock" new-session -d -s "$session" "env${env_prefix} node $(dirname "$0")/$script $(printf '%q ' "$proj" "$@") >> $(printf '%q' "$log") 2>&1"
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 "$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
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 tmux -S "$sock" has-session -t "$sess" 2>/dev/null; then
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 tmux -S "$sock" list-sessions >/dev/null 2>&1; then
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
- tmux -S "$sock" kill-session -t "$sess" 2>/dev/null || true
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=$(tmux -S "$sock" display-message -p -t "$sess" '#{socket_path}')
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=$(tmux -S "$sock" capture-pane -t "$sess" -p 2>/dev/null || true)
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 ! tmux -S "$sock" send-keys -t "$sess" y; then
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=$(tmux -S "$sock" capture-pane -t "$sess" -p 2>/dev/null || true)
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=$(tmux -S "$sock" capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
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 ! tmux -S "$sock" load-buffer -b "$brief_buffer" "$brief_file"; then
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 ! tmux -S "$sock" paste-buffer -b "$brief_buffer" -d -t "$sess"; then
398
- tmux -S "$sock" delete-buffer -b "$brief_buffer" 2>/dev/null || true
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 ! tmux -S "$sock" send-keys -t "$sess" Enter; then
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=$(tmux -S "$sock" capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
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=$(tmux -S "$sock" list-panes -t "$sess" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
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
- # pane の子で pid===pgid のものが席本体。pane_pid(shell)を渡すと Lattice の直接 OS 観測が
444
- # 「worker process group を無関係 process と共有している」で正しく落ちる。
445
- # **1件でなければ推測で選ばない**(run-bridge.mjs seatWorkerPid と同じ規律)。
446
- leaders=$(ps -Ao pid=,ppid=,pgid= | awk -v p="$pane_pid" '$2==p && $1==$3 {print $1}')
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" "$seat_pid" <<'PY'
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, pid = sys.argv[1:5]
461
- pid = int(pid)
462
- started = subprocess.run(['/bin/ps', '-o', 'lstart=', '-p', str(pid)],
463
- capture_output=True, text=True, check=True).stdout.strip()
464
- argv = subprocess.run(['/bin/ps', '-o', 'args=', '-p', str(pid)],
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
- meta=$(python3 - "$name" "$vendor" "$model" "$effort" "$sock" "$sess" "$role" "$aiterm_session_id" <<'PY'
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:9]
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': {'tmux_socket': sock, 'tmux_target': sess}}
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 [ -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
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 tmux -S "$member_socket" has-session -t "$target" 2>/dev/null; then
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 tmux -S "$member_socket" list-sessions >/dev/null 2>&1; then
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(既定)または codex。model だけ渡して 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 ROOM_UPDATE_BODY = 'room全体の状況が更新された。roomログを読み、状況を把握して次の行動を判断する。'
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 status = JSON.parse(execFileSync(latticeCli, ['todo', 'status', '--json'], {
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 ? ROOM_UPDATE_BODY : message.body,
202
- message: roomUpdate
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', ['-S', socket, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) } catch { return null } }
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 { readFileSync, readdirSync } from 'node:fs'
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', ['-S', socket, 'list-panes', '-t', target, '-F', '#{pane_pid}'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
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 {
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 tmux -S "$member_sock" has-session -t "$target" 2>/dev/null; then
79
- tmux -S "$member_sock" kill-session -t "$target" && closed=$((closed + 1))
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=$(tmux -S "$sock" list-sessions 2>/dev/null | grep -c '^peer-' || true)
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('--source')) console.log(result.source)
10
- else console.log(result.socket)
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