peertable 0.4.37 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +3 -1
- package/README.md +3 -1
- package/package.json +2 -2
- package/room/Dockerfile +2 -1
- package/room/client.mjs +5 -12
- package/room/server.mjs +121 -39
- package/skill/SKILL.md +13 -8
- package/skill/scripts/alarm-bridge.mjs +117 -0
- package/skill/scripts/alarm-set.sh +22 -0
- package/skill/scripts/change-seat.sh +2 -1
- package/skill/scripts/codex-dialog.mjs +46 -0
- package/skill/scripts/codex-seat-toml.mjs +6 -0
- package/skill/scripts/doctor.sh +185 -0
- package/skill/scripts/ensure-bridge.sh +1 -1
- package/skill/scripts/ensure-codex-room-mcp.mjs +4 -1
- package/skill/scripts/launch-seat.sh +141 -120
- package/skill/scripts/leave-seat.sh +1 -4
- package/skill/scripts/parent-join.sh +22 -9
- package/skill/scripts/parent-watch-logic.mjs +33 -0
- package/skill/scripts/parent-watch.mjs +34 -1
- package/skill/scripts/pull-attach-input.mjs +53 -0
- package/skill/scripts/refresh-seat-identity.mjs +97 -0
- package/skill/scripts/seat-identity.mjs +37 -6
- package/skill/scripts/seat-status-bridge.mjs +34 -0
- package/skill/scripts/seat-usage.mjs +5 -0
- package/skill/scripts/setup.sh +5 -0
- package/skill/scripts/teardown.sh +8 -6
- package/skill/scripts/wakeup-bridge.mjs +91 -2
- package/skill/scripts/wakeup-delivery.mjs +1 -1
- package/skill/templates/member.md +10 -5
- package/skill/scripts/run-bridge.mjs +0 -272
- package/skill/scripts/seat-metadata.mjs +0 -21
|
@@ -107,7 +107,7 @@ sock=""
|
|
|
107
107
|
sess=""
|
|
108
108
|
url=""
|
|
109
109
|
room=""
|
|
110
|
-
|
|
110
|
+
# 席の本人性は room 台帳の member 行が持つ(席file廃止 2026-08-22)。rollback は member DELETE が兼ねる
|
|
111
111
|
cleanup_brief() {
|
|
112
112
|
if [ -n "$brief_file" ]; then rm -f "$brief_file"; fi
|
|
113
113
|
return 0
|
|
@@ -144,13 +144,6 @@ rollback_brief() {
|
|
|
144
144
|
return 1
|
|
145
145
|
fi
|
|
146
146
|
|
|
147
|
-
if [ -n "$seat_file" ] && [ -e "$seat_file" ]; then
|
|
148
|
-
if ! rm -f "$seat_file"; then
|
|
149
|
-
rollback_failed=1
|
|
150
|
-
echo "LAUNCH_BRIEF_ROLLBACK_FAILED: seat identity を撤去できない: ${seat_file}" >&2
|
|
151
|
-
fi
|
|
152
|
-
fi
|
|
153
|
-
|
|
154
147
|
if [ -n "$url" ] && [ -n "$room" ]; then
|
|
155
148
|
if ! encoded_name=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name"); then
|
|
156
149
|
rollback_failed=1
|
|
@@ -305,11 +298,6 @@ if ! node "$room_mcp_helper" "$proj" "$peertable_repo" "$mcp_ownership"; then
|
|
|
305
298
|
echo "SEAT_ROOM_MCP_INVALID: Aiterm席のroom clientをcurrent treeへ束縛できない(席は立てない)" >&2
|
|
306
299
|
exit 1
|
|
307
300
|
fi
|
|
308
|
-
if ! credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" prepare "$proj" "$room" "$name"); then
|
|
309
|
-
echo "SEAT_CREDENTIAL_PREPARE_FAILED: 席別credentialを用意できない(席は立てない)" >&2
|
|
310
|
-
exit 1
|
|
311
|
-
fi
|
|
312
|
-
|
|
313
301
|
# 同名の古いroom memberが残っていると、Codexの新しいroom clientが一度も登録していなくても
|
|
314
302
|
# 下のready確認を通ってしまう。古いmemberを別席のまま黙って消すのは危険なので、起動前に同名を
|
|
315
303
|
# conflictとして拒否し、明示的な退席後に再実行させる。一覧を読めない時も着席前に止める。
|
|
@@ -339,6 +327,15 @@ case "$stale_member_rc" in
|
|
|
339
327
|
;;
|
|
340
328
|
esac
|
|
341
329
|
|
|
330
|
+
# credential は同名掃除の**後**に用意する。leave-seat は member ごと credential file
|
|
331
|
+
# (path は project/room/member から決定的)を撤去するので、先に作ると同名再着席の
|
|
332
|
+
# 初回だけ「消えた credential を席へ渡して client が initialize 前に死ぬ」レースになる
|
|
333
|
+
# (実被弾 2026-08-22: さくら再着席の初回必敗・再走で成功、の正体)。
|
|
334
|
+
if ! credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" prepare "$proj" "$room" "$name"); then
|
|
335
|
+
echo "SEAT_CREDENTIAL_PREPARE_FAILED: 席別credentialを用意できない(席は立てない)" >&2
|
|
336
|
+
exit 1
|
|
337
|
+
fi
|
|
338
|
+
|
|
342
339
|
# leave-seat が席専用 GROK_HOME を消す。同名再着席では preflight 後に空になるので、
|
|
343
340
|
# live 起動の直前に auth と ui だけを書き直す(user booth MCP は載せない)。
|
|
344
341
|
if [ "$vendor" = grok ]; then
|
|
@@ -356,7 +353,6 @@ fi
|
|
|
356
353
|
# direct CLI launch へ戻るfallbackは置かない。Aiterm が作る同名PTYと launch receipt が、
|
|
357
354
|
# 以後の `pty_read` / `agent_configure` / room metadata を同じsessionへ束縛する。
|
|
358
355
|
tmux_at kill-session -t "$sess" 2>/dev/null || true
|
|
359
|
-
rm -f "$proj/.team/seats/$name.json"
|
|
360
356
|
|
|
361
357
|
launch_env=(
|
|
362
358
|
"PEERTABLE_URL=$url"
|
|
@@ -420,7 +416,34 @@ then
|
|
|
420
416
|
exit 1
|
|
421
417
|
fi
|
|
422
418
|
aiterm_session_id="$sess"
|
|
423
|
-
|
|
419
|
+
# **launch が返った事実は brief 配達の証拠にならない。** aiterm は TUI が入力受付前
|
|
420
|
+
# (update/trust ダイアログ表示中など)だと prompt を送らず、receipt の event_cursor=null で
|
|
421
|
+
# それを申告する。読まずに配達済み扱いにすると席が白紙のまま「briefed」と報告される
|
|
422
|
+
# (実被弾 2026-08-22: さくら再着席。席は起動したが着任指示ゼロで放置)。
|
|
423
|
+
# event_cursor が数値 → turn 開始をaitermが確認済み。submit_residue=true → 本文は
|
|
424
|
+
# composer に残ったが Enter が落ちた=後段で Enter だけ打ち直す。null → 未送信=後段で貼る。
|
|
425
|
+
brief_in_composer=false
|
|
426
|
+
if [ -n "$brief" ]; then
|
|
427
|
+
brief_receipt_state=$(python3 - "$launch_receipt" <<'PY'
|
|
428
|
+
import json, sys
|
|
429
|
+
r = json.loads(sys.argv[1])
|
|
430
|
+
cursor = r.get('event_cursor')
|
|
431
|
+
residue = r.get('submit_residue')
|
|
432
|
+
print('dispatched' if cursor is not None and residue is not True
|
|
433
|
+
else 'residue' if residue is True else 'not_sent')
|
|
434
|
+
PY
|
|
435
|
+
)
|
|
436
|
+
case "$brief_receipt_state" in
|
|
437
|
+
dispatched) brief_dispatched=true ;;
|
|
438
|
+
residue)
|
|
439
|
+
brief_in_composer=true
|
|
440
|
+
echo "SEAT_BRIEF_SUBMIT_RESIDUE: brief は入力欄に残り submit が落ちた。着席確認後に submit し直す" >&2
|
|
441
|
+
;;
|
|
442
|
+
*)
|
|
443
|
+
echo "SEAT_BRIEF_LAUNCH_PROMPT_NOT_SENT: aiterm は TUI 入力受付前で brief を送っていない。着席確認後に貼り直す" >&2
|
|
444
|
+
;;
|
|
445
|
+
esac
|
|
446
|
+
fi
|
|
424
447
|
seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
|
|
425
448
|
|
|
426
449
|
# Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
|
|
@@ -441,42 +464,46 @@ codex_hooks_accepted=false
|
|
|
441
464
|
codex_update_accepted=false
|
|
442
465
|
codex_trust_accepted=false
|
|
443
466
|
claude_trust_accepted=false
|
|
467
|
+
codex_dialog_helper="$peertable_script_dir/codex-dialog.mjs"
|
|
468
|
+
pass_codex_pane() {
|
|
469
|
+
local json key
|
|
470
|
+
json=$(printf '%s' "$1" | node "$codex_dialog_helper" || true)
|
|
471
|
+
[ -n "$json" ] && [ "$json" != "null" ] || return 1
|
|
472
|
+
while IFS= read -r key; do
|
|
473
|
+
[ -n "$key" ] || continue
|
|
474
|
+
tmux_at send-keys -t "$sess" "$key" || return 1
|
|
475
|
+
done < <(python3 -c 'import json,sys
|
|
476
|
+
a=json.loads(sys.stdin.read() or "null")
|
|
477
|
+
print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json")
|
|
478
|
+
return 0
|
|
479
|
+
}
|
|
480
|
+
codex_pane_blocks_ready() {
|
|
481
|
+
printf '%s' "$1" | node "$codex_dialog_helper" --ready-ok
|
|
482
|
+
}
|
|
444
483
|
while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
445
484
|
# Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
|
|
446
485
|
# Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
|
|
447
486
|
# 汎用的に承認するfallbackにはしない。承認後のMCP初期化時間は改めて30秒確保する。
|
|
448
487
|
if [ "$vendor" = codex ]; then
|
|
449
488
|
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
450
|
-
if
|
|
451
|
-
|
|
452
|
-
*"Update now"*)
|
|
453
|
-
if tmux_at send-keys -t "$sess" Down && tmux_at send-keys -t "$sess" Enter; then
|
|
454
|
-
codex_update_accepted=true
|
|
455
|
-
room_ready_deadline=$((SECONDS + 90))
|
|
456
|
-
echo "codex update prompt: skipped"
|
|
457
|
-
fi
|
|
458
|
-
;;
|
|
459
|
-
esac
|
|
460
|
-
fi
|
|
461
|
-
if [ "$codex_hooks_accepted" != true ]; then
|
|
489
|
+
if pass_codex_pane "$codex_screen"; then
|
|
490
|
+
room_ready_deadline=$((SECONDS + 90))
|
|
462
491
|
case "$codex_screen" in
|
|
492
|
+
*"Allow the room MCP server to run tool"*) echo "codex mcp allow: always allow" ;;
|
|
463
493
|
*"Hooks need review"*)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
494
|
+
codex_hooks_accepted=true
|
|
495
|
+
echo "codex hooks prompt: trust all"
|
|
496
|
+
;;
|
|
497
|
+
*"Update now"*)
|
|
498
|
+
codex_update_accepted=true
|
|
499
|
+
echo "codex update prompt: skipped"
|
|
469
500
|
;;
|
|
470
|
-
esac
|
|
471
|
-
fi
|
|
472
|
-
if [ "$codex_trust_accepted" != true ]; then
|
|
473
|
-
case "$codex_screen" in
|
|
474
501
|
*"Yes, continue"*)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
502
|
+
codex_trust_accepted=true
|
|
503
|
+
echo "codex directory trust: accepted"
|
|
504
|
+
;;
|
|
505
|
+
*"Would you like to run the following command?"*)
|
|
506
|
+
echo "codex command approval: don't ask again"
|
|
480
507
|
;;
|
|
481
508
|
esac
|
|
482
509
|
fi
|
|
@@ -529,6 +556,13 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
529
556
|
fi
|
|
530
557
|
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
531
558
|
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
|
|
559
|
+
if [ "$vendor" = codex ]; then
|
|
560
|
+
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
561
|
+
if ! codex_pane_blocks_ready "$codex_screen"; then
|
|
562
|
+
sleep 1
|
|
563
|
+
continue
|
|
564
|
+
fi
|
|
565
|
+
fi
|
|
532
566
|
room_ready=true
|
|
533
567
|
break
|
|
534
568
|
fi
|
|
@@ -541,6 +575,24 @@ if [ "$room_ready" != true ]; then
|
|
|
541
575
|
exit 1
|
|
542
576
|
fi
|
|
543
577
|
echo "room ready: ${room}/${name}"
|
|
578
|
+
if [ "$vendor" = codex ]; then
|
|
579
|
+
# member 登録後の最初の tool 呼び出しで MCP Allow が出る。出ている間だけ通し、沈黙5秒で抜ける。
|
|
580
|
+
codex_post_ready_deadline=$((SECONDS + 90))
|
|
581
|
+
codex_post_ready_idle=0
|
|
582
|
+
while [ $SECONDS -lt "$codex_post_ready_deadline" ]; do
|
|
583
|
+
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
584
|
+
if pass_codex_pane "$codex_screen"; then
|
|
585
|
+
case "$codex_screen" in
|
|
586
|
+
*"Allow the room MCP server to run tool"*) echo "codex mcp allow: always allow" ;;
|
|
587
|
+
esac
|
|
588
|
+
codex_post_ready_idle=0
|
|
589
|
+
else
|
|
590
|
+
codex_post_ready_idle=$((codex_post_ready_idle + 1))
|
|
591
|
+
[ "$codex_post_ready_idle" -ge 5 ] && break
|
|
592
|
+
fi
|
|
593
|
+
sleep 1
|
|
594
|
+
done
|
|
595
|
+
fi
|
|
544
596
|
if [ "$brief_dispatched" = true ]; then
|
|
545
597
|
brief_completed=true
|
|
546
598
|
echo "briefed: ${sess}(Aiterm launch prompt)"
|
|
@@ -575,17 +627,20 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
575
627
|
sleep 1
|
|
576
628
|
|
|
577
629
|
brief_before=$(tmux_at capture-pane -S -1000 -t "$sess" -p 2>/dev/null || true)
|
|
578
|
-
|
|
579
|
-
if
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
630
|
+
# submit_residue の席は本文が composer に残っている。貼り直すと二重になるので Enter だけ打つ。
|
|
631
|
+
if [ "$brief_in_composer" != true ]; then
|
|
632
|
+
brief_buffer="peertable-brief-${name}-$$"
|
|
633
|
+
if ! tmux_at load-buffer -b "$brief_buffer" "$brief_file"; then
|
|
634
|
+
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux buffer 読み込みに失敗(席は着席済み)" >&2
|
|
635
|
+
exit 1
|
|
636
|
+
fi
|
|
637
|
+
if ! tmux_at paste-buffer -b "$brief_buffer" -d -t "$sess"; then
|
|
638
|
+
tmux_at delete-buffer -b "$brief_buffer" 2>/dev/null || true
|
|
639
|
+
echo "LAUNCH_BRIEF_SEND_FAILED: brief の tmux paste に失敗(席は着席済み)" >&2
|
|
640
|
+
exit 1
|
|
641
|
+
fi
|
|
642
|
+
sleep 1
|
|
587
643
|
fi
|
|
588
|
-
sleep 1
|
|
589
644
|
if ! tmux_at send-keys -t "$sess" Enter; then
|
|
590
645
|
echo "LAUNCH_BRIEF_SEND_FAILED: brief の submit に失敗(席は着席済み)" >&2
|
|
591
646
|
exit 1
|
|
@@ -614,15 +669,12 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
614
669
|
fi
|
|
615
670
|
fi
|
|
616
671
|
|
|
617
|
-
#
|
|
618
|
-
#
|
|
672
|
+
# 席の本人性(pid / 起動時刻 / argv digest)を room 台帳の member 行へ登録する。
|
|
673
|
+
# member に帰属する情報の正本は台帳だけ(オーナー裁定 2026-08-22・席fileは廃止)。
|
|
619
674
|
# 着席の**後**に取る——起動途中の process を掴むと、ダイアログ通過で子が入れ替わりうる。
|
|
620
|
-
#
|
|
621
|
-
#
|
|
622
|
-
#
|
|
623
|
-
# **raw argv を持たせない**——秘密値はargvから除いたが、将来の引数も含めて複製しない。digest だけを持つ。
|
|
624
|
-
# この file が主張するのは「この pid はこの席だった」という**識別**であって、生死ではない。
|
|
625
|
-
# 生きているかは attach する側(Lattice)が lstart+argv の再観測で確かめる。
|
|
675
|
+
# **raw argv を持たせない**——digest だけを持つ。この記録が主張するのは「この pid は
|
|
676
|
+
# この席だった」という**識別**であって、生死ではない。生きているかは attach する側
|
|
677
|
+
# (Lattice)が lstart+argv の再観測で確かめる。
|
|
626
678
|
seat_pid=""
|
|
627
679
|
pane_pid=$(tmux_at list-panes -t "$sess" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
|
|
628
680
|
seat_ident=""
|
|
@@ -639,82 +691,51 @@ if [ -z "$seat_pid" ]; then
|
|
|
639
691
|
# 記録が無ければ席は attach できず、装置の介入は協調 hold のままになる。**黙らない。**
|
|
640
692
|
echo "seat identity を記録できなかった: ${sess} の process group leader を1つに確定できない(席は着席済み)" >&2
|
|
641
693
|
else
|
|
642
|
-
|
|
643
|
-
#
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if ! python3 - "$proj/.team/seats/$name.json" "$name" "$name" "$seat_ident" <<'PY'
|
|
648
|
-
import hashlib, json, os, subprocess, sys, tempfile
|
|
649
|
-
out, name, session, ident_raw = sys.argv[1:5]
|
|
694
|
+
# aiterm_session_id の書き手はランチャー(launch receipt の所有者)。席の client は
|
|
695
|
+
# Codex closed-mode env で AITERM_SESSION_ID を受け取れないため、ここで台帳へ載せる。
|
|
696
|
+
ident_body=$(python3 - "$name" "$seat_ident" "$aiterm_session_id" <<'PY'
|
|
697
|
+
import json, subprocess, sys
|
|
698
|
+
name, ident_raw, aiterm_session = sys.argv[1:4]
|
|
650
699
|
ident = json.loads(ident_raw)
|
|
651
|
-
|
|
652
|
-
started = ident['started_identity']
|
|
653
|
-
argv = ident['argv']
|
|
654
|
-
if not started or not argv:
|
|
700
|
+
if not ident.get('started_identity') or not ident.get('argv') or not ident.get('argv_digest'):
|
|
655
701
|
sys.exit('pid の lstart/args を観測できない')
|
|
656
|
-
|
|
657
|
-
'argv_digest': hashlib.sha256(argv.encode()).hexdigest(),
|
|
702
|
+
print(json.dumps({
|
|
658
703
|
'name': name,
|
|
659
|
-
'pid': pid,
|
|
660
|
-
'
|
|
661
|
-
|
|
662
|
-
'
|
|
663
|
-
'
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
# 着席直後に席が読むので、部分読取が起きない形にする。
|
|
667
|
-
body = json.dumps(record, ensure_ascii=False, sort_keys=True, separators=(',', ':')) + '\n'
|
|
668
|
-
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(out), prefix='.seat-', suffix='.tmp')
|
|
669
|
-
try:
|
|
670
|
-
with os.fdopen(fd, 'w') as handle:
|
|
671
|
-
handle.write(body)
|
|
672
|
-
handle.flush()
|
|
673
|
-
os.fsync(handle.fileno())
|
|
674
|
-
os.chmod(tmp, 0o600)
|
|
675
|
-
os.replace(tmp, out)
|
|
676
|
-
except BaseException:
|
|
677
|
-
os.unlink(tmp)
|
|
678
|
-
raise
|
|
704
|
+
'pid': int(ident['pid']),
|
|
705
|
+
'started_identity': ident['started_identity'],
|
|
706
|
+
'argv_digest': ident['argv_digest'],
|
|
707
|
+
'aiterm_session_id': aiterm_session,
|
|
708
|
+
'identity_recorded_at': subprocess.run(['date', '-u', '+%Y-%m-%dT%H:%M:%S.000Z'],
|
|
709
|
+
capture_output=True, text=True, check=True).stdout.strip(),
|
|
710
|
+
}, ensure_ascii=False))
|
|
679
711
|
PY
|
|
680
|
-
|
|
681
|
-
|
|
712
|
+
) || ident_body=""
|
|
713
|
+
if [ -z "$ident_body" ] || ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
714
|
+
"$url/api/$room/members" "$ident_body" >/dev/null; then
|
|
715
|
+
echo "seat identity を台帳へ登録できなかった: ${sess}(席は着席済み・attach は協調 hold のままになる)" >&2
|
|
682
716
|
fi
|
|
683
717
|
fi
|
|
684
718
|
|
|
685
|
-
#
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
690
|
-
"$url/api/$room/members" "$meta" >/dev/null; then
|
|
691
|
-
# **200 は保存の証拠にならない**。素性欄を知らない server も 200 {"ok":true} を返して黙って捨てる
|
|
692
|
-
# (登録が `if (!members.has(name))` の no-op になる経路もある)。読み返して実際に載ったかを見る。
|
|
693
|
-
# **パイプと heredoc を同じ stdin へ重ねない**。`curl | python3 - <<'PY'` は
|
|
694
|
-
# 「プログラムを stdin から読む」と「データを stdin から読む」が衝突して、必ず失敗する
|
|
695
|
-
# (そして try/except で包むと、失敗が「保存されていない」という**もっともらしい答え**に化ける。実測)
|
|
696
|
-
listing=$(curl -sf "$url/api/$room/members" || true)
|
|
697
|
-
stored=$(python3 - "$name" "$listing" <<'PY'
|
|
719
|
+
# 素性(vendor/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
|
|
720
|
+
# ランチャーはここで台帳を読み返し、実際に載ったかを確認するだけにする(重複書込の禁止)。
|
|
721
|
+
member_row=$(curl -sf "$url/api/$room/members/$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$name")" || true)
|
|
722
|
+
stored=$(python3 - "$member_row" <<'PY'
|
|
698
723
|
import json, sys
|
|
699
|
-
# 読み返しに失敗しても生の traceback を出さない。ここは「保存されたか」を見るだけの確認段で、
|
|
700
|
-
# 判定不能は「保存されていない」と同じ扱いでよい(席は既に着席している)
|
|
701
724
|
try:
|
|
702
|
-
|
|
725
|
+
m = json.loads(sys.argv[1])['member']
|
|
703
726
|
except Exception:
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
727
|
+
m = {}
|
|
728
|
+
print('yes' if m.get('model') and m.get('roles') and m.get('aiterm_session_id')
|
|
729
|
+
and (m.get('observe') or {}).get('tmux_target') and m.get('pid') else 'no')
|
|
707
730
|
PY
|
|
708
731
|
)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
else
|
|
712
|
-
echo "metadata は保存されなかった: この room サーバーは素性欄を持たない版(席は着席済み・一覧に素性が出ないだけ)" >&2
|
|
713
|
-
fi
|
|
732
|
+
if [ "$stored" = yes ]; then
|
|
733
|
+
echo "台帳確認: ${vendor} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
|
|
714
734
|
else
|
|
715
|
-
echo "
|
|
735
|
+
echo "SEAT_LEDGER_INCOMPLETE: 台帳の member 行に素性または本人性が欠けている(席は着席済み。doctor で確認)" >&2
|
|
716
736
|
fi
|
|
717
737
|
|
|
738
|
+
PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" alarm || echo "alarm-bridge の起動確認に失敗した(席は着席済み)" >&2
|
|
718
739
|
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" seat-status; then
|
|
719
740
|
echo "seat-status-bridge: 起動確認済み"
|
|
720
741
|
else
|
|
@@ -76,10 +76,7 @@ if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_
|
|
|
76
76
|
failed=1
|
|
77
77
|
fi
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
echo "SEAT_LEAVE_IDENTITY_FAILED: $name" >&2
|
|
81
|
-
failed=1
|
|
82
|
-
fi
|
|
79
|
+
# 席の本人性は台帳の member 行が持つ(席file廃止 2026-08-22)。上の member DELETE が identity も撤去する
|
|
83
80
|
rm -rf "$proj/.team/seats/${name}.grok-home"
|
|
84
81
|
rm -rf "$proj/.team/seats/${name}.codex"
|
|
85
82
|
if ! env -u PEERTABLE_POST_TOKEN node "$credential_helper" remove "$proj" "$credential_file"; then
|
|
@@ -34,21 +34,14 @@ fi
|
|
|
34
34
|
member=$(python3 - "$name" "$model" "$effort" "$parent_vendor" "$mission" <<'PY'
|
|
35
35
|
import json, sys
|
|
36
36
|
name, model, effort, vendor, mission = sys.argv[1:6]
|
|
37
|
-
|
|
37
|
+
# 台帳には canonical 欄だけを登録する(settings / role 単数の重複欄は 2026-08-22 廃止)
|
|
38
|
+
body = {'name': name, 'roles': ['統括']}
|
|
38
39
|
if vendor:
|
|
39
40
|
body['vendor'] = vendor
|
|
40
41
|
if model:
|
|
41
42
|
body['model'] = model
|
|
42
|
-
settings = {}
|
|
43
|
-
if vendor:
|
|
44
|
-
settings['vendor'] = vendor
|
|
45
|
-
if model:
|
|
46
|
-
settings['model'] = model
|
|
47
43
|
if effort:
|
|
48
44
|
body['effort'] = effort
|
|
49
|
-
settings['effort'] = effort
|
|
50
|
-
if settings:
|
|
51
|
-
body['settings'] = settings
|
|
52
45
|
if mission:
|
|
53
46
|
body['mission'] = mission
|
|
54
47
|
body['observe'] = None
|
|
@@ -78,6 +71,7 @@ fi
|
|
|
78
71
|
if [ -f "$proj/.team/roles/parent.md" ]; then
|
|
79
72
|
echo "親役割は $proj/.team/roles/parent.md を読むこと"
|
|
80
73
|
fi
|
|
74
|
+
echo "親の権限境界: 各ToDoのクローズは監査担当が行い、親は裁定しない。親の仕事はオーナー窓口・環境修理・終端監査だけ(オーナー裁定 2026-08-22。詳細は skill/SKILL.md「親の operating notes」冒頭)"
|
|
81
75
|
|
|
82
76
|
# parent-watchが初回headを固定する。以後のDMはwatcher不在時間を含め
|
|
83
77
|
# 永続cursorからcatch-upされる。host内のbackground task自体は親セッションだけが所有できる。
|
|
@@ -90,6 +84,25 @@ if PEERTABLE_PARENT_HOST="$parent_vendor" node "$here/parent-watch.mjs" "$proj"
|
|
|
90
84
|
else
|
|
91
85
|
echo "PARENT_WATCH_START_REQUIRED: Claude Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること"
|
|
92
86
|
fi
|
|
87
|
+
# 耳の疎通probe: 番犬が生きていても、その出力を受け取る監視(Monitor等)が現在の
|
|
88
|
+
# 親セッションに繋がっている保証はない(番犬が前セッションの耳へ吠え続け、親宛DMが
|
|
89
|
+
# 全損する実被弾 2026-08-22)。cursor確定後に親宛のprobe DMを1通置き、
|
|
90
|
+
# 「監視イベントとしてこのnonceを受信した」ことを着卓完了の条件にする。
|
|
91
|
+
probe_nonce="$(date +%s)-$$"
|
|
92
|
+
probe_body=$(python3 - "$name" "$probe_nonce" <<'PY'
|
|
93
|
+
import json, sys
|
|
94
|
+
name, nonce = sys.argv[1:3]
|
|
95
|
+
print(json.dumps({'from': 'ear-probe', 'to': name,
|
|
96
|
+
'body': f'[耳疎通probe {nonce}] この通知を監視イベントとして受信できていれば耳は接続済み。受信するまで着卓完了と言わないこと。'}, ensure_ascii=False))
|
|
97
|
+
PY
|
|
98
|
+
)
|
|
99
|
+
if curl -sf -X POST "$url/api/$room/messages" \
|
|
100
|
+
-H "X-Peertable-Token: $PEERTABLE_POST_TOKEN" -H 'content-type: application/json' \
|
|
101
|
+
-d "$probe_body" > /dev/null; then
|
|
102
|
+
echo "EAR_PROBE_SENT: nonce=${probe_nonce}。着卓完了の条件は、上記の監視経由でこのnonceを受信すること。受信できないなら耳は繋がっていない=着卓失敗として扱い、監視を張り直す"
|
|
103
|
+
else
|
|
104
|
+
echo "WARN: EAR_PROBE_POST_FAILED: 耳疎通probeを投稿できない(監視の受信確認ができないまま)" >&2
|
|
105
|
+
fi
|
|
93
106
|
else
|
|
94
107
|
echo "WARN: PARENT_WATCH_PRIME_FAILED: 親(${name})のDM cursorを準備できない" >&2
|
|
95
108
|
fi
|
|
@@ -4,6 +4,39 @@ export function latticeStaffingChanged(previous, next) {
|
|
|
4
4
|
return previous.ready !== next.ready || previous.active !== next.active
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
const STATUS_LABEL = { busy: '作業中', blocked: '承認待ち', idle: '待機', dead: '停止' }
|
|
8
|
+
|
|
9
|
+
// 「工程があるのに作業中の席が1つも無い」が holdMs 続いたら1回だけ警報を出す。
|
|
10
|
+
// observation: { ready, active, workers: [{name, status}] }(membersを読めた時だけ渡す)
|
|
11
|
+
// previous: 保存済み stall 状態({ since, alerted } | null | undefined)
|
|
12
|
+
// 返り値: { stall, event }。event が非null なら親へ届ける。
|
|
13
|
+
export function tableStallUpdate(previous, observation, nowMs, holdMs) {
|
|
14
|
+
if (!observation) return { stall: previous ?? null, event: null }
|
|
15
|
+
const { ready, active, workers } = observation
|
|
16
|
+
const workExists = ready + active > 0
|
|
17
|
+
const anyoneBusy = workers.some(w => w.status === 'busy')
|
|
18
|
+
if (!workExists || anyoneBusy) return { stall: null, event: null }
|
|
19
|
+
const since = previous?.since ?? nowMs
|
|
20
|
+
const alerted = previous?.alerted === true
|
|
21
|
+
if (alerted || nowMs - since < holdMs) {
|
|
22
|
+
return { stall: { since, alerted }, event: null }
|
|
23
|
+
}
|
|
24
|
+
const seatList = workers.length === 0
|
|
25
|
+
? '席が1つもありません'
|
|
26
|
+
: workers.map(w => `${w.name}=${STATUS_LABEL[w.status] ?? w.status ?? '未報告'}`).join('、')
|
|
27
|
+
return {
|
|
28
|
+
stall: { since, alerted: true },
|
|
29
|
+
event: {
|
|
30
|
+
type: 'parent_table_stalled',
|
|
31
|
+
ready,
|
|
32
|
+
active,
|
|
33
|
+
workers,
|
|
34
|
+
body: `警報: 着手可能 ${ready} 件・着手中 ${active} 件の工程があるのに、作業中の席が1つもありません`
|
|
35
|
+
+ `(${Math.round(holdMs / 60000)}分継続)。席: ${seatList}。各席の状態を確認して動かしてください。`,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
export function addressedToParent(message, parent) {
|
|
8
41
|
return message?.from !== parent
|
|
9
42
|
&& (message?.to === 'all' || message?.to === parent
|
|
@@ -11,7 +11,7 @@ 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
13
|
import { resolveLatticeExecutable } from './seat-usage.mjs'
|
|
14
|
-
import { addressedToParent as messageAddressedToParent, latticeStaffingChanged } from './parent-watch-logic.mjs'
|
|
14
|
+
import { addressedToParent as messageAddressedToParent, latticeStaffingChanged, tableStallUpdate } from './parent-watch-logic.mjs'
|
|
15
15
|
|
|
16
16
|
const args = process.argv.slice(2)
|
|
17
17
|
const project = args.shift()
|
|
@@ -231,8 +231,40 @@ async function acceptMessage(message) {
|
|
|
231
231
|
return matched
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// 「工程があるのに作業中の席が1つも無い」を親へ知らせる停滞警報。席の状態は
|
|
235
|
+
// seat-status-bridge が room members へ書く status を使う(親は延べ配信対象外)。
|
|
236
|
+
// members か Lattice を読めない tick は判定を進めず、前回の stall 状態を保つ。
|
|
237
|
+
const stallHoldMs = Number(process.env.PEERTABLE_STALL_ALARM_MS ?? 180_000)
|
|
238
|
+
async function checkStall() {
|
|
239
|
+
const lattice = state.lattice
|
|
240
|
+
if (!lattice || lattice.error !== undefined) return false
|
|
241
|
+
let members
|
|
242
|
+
try { members = (await readJson('/members')).members ?? [] } catch { return false }
|
|
243
|
+
const workers = members
|
|
244
|
+
.filter(m => m.name !== parent && m.delivery?.kind !== 'parent_watch')
|
|
245
|
+
.map(m => ({ name: m.name, status: m.status ?? null }))
|
|
246
|
+
const { stall, event } = tableStallUpdate(
|
|
247
|
+
state.stall ?? null,
|
|
248
|
+
{ ready: lattice.ready, active: lattice.active, workers },
|
|
249
|
+
Date.now(),
|
|
250
|
+
stallHoldMs,
|
|
251
|
+
)
|
|
252
|
+
if (JSON.stringify(stall) !== JSON.stringify(state.stall ?? null)) {
|
|
253
|
+
state = { ...state, stall, last_event_at: event ? now() : state.last_event_at }
|
|
254
|
+
saveState(state)
|
|
255
|
+
}
|
|
256
|
+
if (!event) return false
|
|
257
|
+
await writeEvent({
|
|
258
|
+
schema: 'peertable.parent-watch-event.v1',
|
|
259
|
+
parent,
|
|
260
|
+
...event,
|
|
261
|
+
})
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
|
|
234
265
|
async function catchUp() {
|
|
235
266
|
if (await acceptLatticeState(readLatticeState(state.lattice)) && mode === '--next') return true
|
|
267
|
+
if (await checkStall() && mode === '--next') return true
|
|
236
268
|
const body = await readJson(`/messages?since=${state.last_seq}`)
|
|
237
269
|
for (const message of body.messages ?? []) {
|
|
238
270
|
if (await acceptMessage(message) && mode === '--next') return true
|
|
@@ -295,6 +327,7 @@ for (;;) {
|
|
|
295
327
|
if (eventName === 'ping') {
|
|
296
328
|
const head = Number(data)
|
|
297
329
|
if (await acceptLatticeState(readLatticeState(state.lattice)) && mode === '--next') process.exit(0)
|
|
330
|
+
if (await checkStall() && mode === '--next') process.exit(0)
|
|
298
331
|
if (Number.isSafeInteger(head) && head > state.last_seq && await catchUp()) process.exit(0)
|
|
299
332
|
continue
|
|
300
333
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// attach 入力は room 台帳(member 行の本人性欄)だけから作る。呼び出し側の pid 推定を受け付けない。
|
|
3
|
+
// usage: pull-attach-input.mjs <project_dir> <name> [refusedPid]
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const fail = (code, message) => {
|
|
8
|
+
process.stderr.write(`${code}: ${message}\n`)
|
|
9
|
+
process.exit(2)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const [projectDir, name, maybePid] = process.argv.slice(2)
|
|
13
|
+
if (!projectDir || !name) fail('ATTACH_INPUT_ARGS', 'usage: pull-attach-input.mjs <project_dir> <name> [refusedPid]')
|
|
14
|
+
|
|
15
|
+
let setup
|
|
16
|
+
try {
|
|
17
|
+
setup = JSON.parse(readFileSync(join(resolve(projectDir), '.team', 'setup-state.json'), 'utf8'))
|
|
18
|
+
} catch (error) {
|
|
19
|
+
fail('ATTACH_INPUT_STATE_UNREADABLE', error.message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let member
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`${setup.server_url.replace(/\/$/u, '')}/api/${encodeURIComponent(setup.room)}/members/${encodeURIComponent(name)}`)
|
|
25
|
+
if (!response.ok) fail('ATTACH_INPUT_MEMBER_MISSING', `台帳に ${name} が無い(HTTP ${response.status})`)
|
|
26
|
+
member = (await response.json()).member
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code === undefined) fail('ATTACH_INPUT_LEDGER_UNREACHABLE', String(error?.message ?? error))
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pid = member.pid
|
|
33
|
+
if (!Number.isSafeInteger(pid) || pid < 1) fail('ATTACH_INPUT_NO_PID', '台帳の member 行に pid が無い')
|
|
34
|
+
if (maybePid !== undefined && Number(maybePid) !== pid) {
|
|
35
|
+
fail('WORKER_IDENTITY_MISMATCH',
|
|
36
|
+
`invented pid ${maybePid} is not the ledger pid ${pid}(実行層死亡ではない)`)
|
|
37
|
+
}
|
|
38
|
+
if (!member.started_identity || !member.argv_digest) {
|
|
39
|
+
fail('ATTACH_INPUT_IDENTITY_INCOMPLETE', '台帳の member 行に started_identity / argv_digest が無い')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// `session` は席名。tmux 名(peer-<name>)を入れると Lattice の attach が
|
|
43
|
+
// `WORKER_ACTOR_MISMATCH` で必ず拒否される(actor session は LATTICE_TODO_ACTOR_SESSION=<name>)。
|
|
44
|
+
const input = {
|
|
45
|
+
schema: 'lattice.pull_worker_attach_input.v1',
|
|
46
|
+
name: member.name,
|
|
47
|
+
session: member.name,
|
|
48
|
+
pid,
|
|
49
|
+
started_identity: member.started_identity,
|
|
50
|
+
argv_digest: member.argv_digest,
|
|
51
|
+
recorded_at: member.identity_recorded_at,
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(`${JSON.stringify(input)}\n`)
|