peertable 0.5.0 → 0.6.0
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 +2 -2
- package/README.md +2 -2
- package/package.json +1 -1
- package/room/client.mjs +7 -5
- package/room/server.mjs +18 -7
- package/skill/02_models.snapshot.md +2 -2
- package/skill/SKILL.md +5 -5
- package/skill/scripts/aiterm-launch.mjs +7 -7
- package/skill/scripts/alarm-bridge.mjs +117 -0
- package/skill/scripts/alarm-set.sh +22 -0
- package/skill/scripts/change-seat.sh +25 -25
- package/skill/scripts/ensure-bridge.sh +1 -1
- package/skill/scripts/ensure-codex-room-mcp.mjs +2 -2
- package/skill/scripts/launch-seat.sh +39 -34
- package/skill/scripts/parent-join.sh +19 -18
- package/skill/scripts/resolve-seat-placement.mjs +28 -27
- package/skill/scripts/seat-status-bridge.mjs +34 -0
- package/skill/scripts/seat-usage.mjs +1 -1
- package/skill/scripts/setup.sh +5 -0
- package/skill/scripts/wakeup-bridge.mjs +47 -8
- package/skill/scripts/wakeup-delivery.mjs +4 -4
- package/skill/templates/member.md +3 -1
- package/skill/templates/parent.md +4 -4
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# 席を1つ立てる(tmux 作成 → env 注入 → エージェント起動 → 既知ダイアログ通過 → 着席確認)。
|
|
3
|
-
# usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--
|
|
3
|
+
# usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--harness <harness>] [brief]
|
|
4
4
|
# --roles: 必須。02_models の公式役割。位置引数に公式役割を置いてもよい
|
|
5
5
|
# --model: 任意。省略時は先頭役割の着席可能な1位。指定時は表外でも通す
|
|
6
6
|
# brief: 着席が成立したら送る着任指示(省略時は送らない)
|
|
7
7
|
set -e
|
|
8
8
|
usage() {
|
|
9
|
-
echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--
|
|
9
|
+
echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--harness <harness>] [brief]" >&2
|
|
10
10
|
}
|
|
11
11
|
proj="$1"; name="$2"
|
|
12
12
|
[ -n "$proj" ] && [ -n "$name" ] || { usage; exit 1; }
|
|
13
13
|
shift 2
|
|
14
14
|
brief=""
|
|
15
15
|
roles=""; mission=""
|
|
16
|
-
opt_model="";
|
|
16
|
+
opt_model=""; opt_harness=""; opt_effort=""
|
|
17
17
|
while [ $# -gt 0 ]; do
|
|
18
18
|
case "$1" in
|
|
19
19
|
--roles) roles="${2:-}"; shift 2 ;;
|
|
20
20
|
--mission) mission="${2:-}"; shift 2 ;;
|
|
21
21
|
--model) opt_model="${2:-}"; shift 2 ;;
|
|
22
|
-
--vendor)
|
|
22
|
+
--harness|--vendor) opt_harness="${2:-}"; shift 2 ;; # --vendor は旧名互換
|
|
23
23
|
--effort) opt_effort="${2:-}"; shift 2 ;;
|
|
24
24
|
--*) echo "SEAT_LAUNCH_ARGS_INVALID: 不明な引数 $1" >&2; usage; exit 1 ;;
|
|
25
25
|
*)
|
|
@@ -65,26 +65,26 @@ aiterm_session_id=""
|
|
|
65
65
|
placement_argv=(--roles "$roles")
|
|
66
66
|
[ -n "$opt_model" ] && placement_argv+=(--model "$opt_model")
|
|
67
67
|
[ -n "$opt_effort" ] && placement_argv+=(--effort "$opt_effort")
|
|
68
|
-
[ -n "$
|
|
68
|
+
[ -n "$opt_harness" ] && placement_argv+=(--harness "$opt_harness")
|
|
69
69
|
placement=$(node "$placement_helper" "${placement_argv[@]}") || exit "$?"
|
|
70
|
-
IFS=$'\t' read -r model
|
|
70
|
+
IFS=$'\t' read -r model harness effort role <<EOF
|
|
71
71
|
$(python3 -c 'import json,sys
|
|
72
72
|
p=json.load(sys.stdin)
|
|
73
73
|
s=p.get("settings") or {}
|
|
74
74
|
roles=",".join(p.get("roles") or [])
|
|
75
|
-
print("\t".join((s.get("model") or "", s.get("
|
|
75
|
+
print("\t".join((s.get("model") or "", s.get("harness") or "", s.get("effort") or "", roles)))' <<<"$placement")
|
|
76
76
|
EOF
|
|
77
|
-
echo "SEAT_PLACEMENT: ${role} → ${
|
|
77
|
+
echo "SEAT_PLACEMENT: ${role} → ${harness} / ${model}${effort:+ / $effort}"
|
|
78
78
|
python3 -c 'import json,sys
|
|
79
79
|
p=json.load(sys.stdin)
|
|
80
80
|
for item in p.get("dropped") or []:
|
|
81
81
|
print("SEAT_PLACEMENT_DROPPED: rank %s %s %s" % (item.get("rank"), item.get("reason"), item.get("cell") or ""), file=sys.stderr)
|
|
82
82
|
' <<<"$placement" || true
|
|
83
|
-
case "$
|
|
83
|
+
case "$harness" in
|
|
84
84
|
claude|codex|grok) ;;
|
|
85
|
-
*) echo "
|
|
85
|
+
*) echo "SEAT_LAUNCH_HARNESS_UNSUPPORTED: harness=${harness}" >&2; exit 1 ;;
|
|
86
86
|
esac
|
|
87
|
-
if [ "$
|
|
87
|
+
if [ "$harness" = "claude" ] && [ -n "$effort" ]; then
|
|
88
88
|
case "$effort" in
|
|
89
89
|
low|medium|high|xhigh|max) ;;
|
|
90
90
|
*) echo "unknown effort: ${effort}(claude は low|medium|high|xhigh|max)" >&2; exit 1 ;;
|
|
@@ -221,7 +221,7 @@ fi
|
|
|
221
221
|
# 席を畳む前に測るのが要点である——ここで落ちれば、動いている席を殺さずに済む。
|
|
222
222
|
export PATH="${HOME}/.grok/bin:${PATH}"
|
|
223
223
|
preflight_dir="${TMPDIR:-/tmp}"
|
|
224
|
-
case "$
|
|
224
|
+
case "$harness" in
|
|
225
225
|
claude) preflight_cmd=(claude --model "$model" -p "ping") ;;
|
|
226
226
|
codex) preflight_cmd=(codex exec --model "$model" --skip-git-repo-check "ping") ;;
|
|
227
227
|
grok)
|
|
@@ -245,7 +245,7 @@ case "$vendor" in
|
|
|
245
245
|
echo "grok preflight: GROK_HOME=${grok_home} ${grok_bin} --model ${model} --reasoning-effort ${effort} -p ping"
|
|
246
246
|
preflight_cmd=(env GROK_HOME="$grok_home" "$grok_bin" --model "$model" --reasoning-effort "$effort" -p "ping")
|
|
247
247
|
;;
|
|
248
|
-
*) echo "unknown
|
|
248
|
+
*) echo "unknown harness: ${harness}(claude / codex / grok)" >&2; exit 1 ;;
|
|
249
249
|
esac
|
|
250
250
|
preflight_log=$(mktemp "${TMPDIR:-/tmp}/peertable-preflight.XXXXXX")
|
|
251
251
|
( cd "$preflight_dir" && "${preflight_cmd[@]}" >"$preflight_log" 2>&1 </dev/null ) &
|
|
@@ -264,14 +264,14 @@ while [ $SECONDS -lt $preflight_deadline ]; do
|
|
|
264
264
|
done
|
|
265
265
|
if [ -z "$preflight_rc" ]; then
|
|
266
266
|
kill "$preflight_pid" 2>/dev/null || true
|
|
267
|
-
echo "model preflight が 120 秒で返らない: ${
|
|
267
|
+
echo "model preflight が 120 秒で返らない: ${harness} / ${model}(席は立てない)" >&2
|
|
268
268
|
echo "preflight log: ${preflight_log}" >&2
|
|
269
269
|
cat "$preflight_log" >&2 || true
|
|
270
270
|
[ -n "${grok_home:-}" ] && rm -rf "$grok_home"
|
|
271
271
|
exit 1
|
|
272
272
|
fi
|
|
273
273
|
if [ "$preflight_rc" != 0 ]; then
|
|
274
|
-
echo "model が live で使えない: ${
|
|
274
|
+
echo "model が live で使えない: ${harness} / ${model}(preflight rc=${preflight_rc}・席は立てない)" >&2
|
|
275
275
|
cat "$preflight_log" >&2 || true
|
|
276
276
|
rm -f "$preflight_log"
|
|
277
277
|
[ -n "${grok_home:-}" ] && rm -rf "$grok_home"
|
|
@@ -338,7 +338,7 @@ fi
|
|
|
338
338
|
|
|
339
339
|
# leave-seat が席専用 GROK_HOME を消す。同名再着席では preflight 後に空になるので、
|
|
340
340
|
# live 起動の直前に auth と ui だけを書き直す(user booth MCP は載せない)。
|
|
341
|
-
if [ "$
|
|
341
|
+
if [ "$harness" = grok ]; then
|
|
342
342
|
mkdir -p "$grok_home"
|
|
343
343
|
if [ ! -f "${HOME}/.grok/auth.json" ]; then
|
|
344
344
|
echo "SEAT_GROK_AUTH_MISSING: ${HOME}/.grok/auth.json が無い(席は立てない)" >&2
|
|
@@ -359,7 +359,8 @@ launch_env=(
|
|
|
359
359
|
"PEERTABLE_ROOM=$room"
|
|
360
360
|
"PEERTABLE_MEMBER=$name"
|
|
361
361
|
"PEERTABLE_CREDENTIAL_FILE=$credential_file"
|
|
362
|
-
"
|
|
362
|
+
"PEERTABLE_HARNESS=$harness"
|
|
363
|
+
"PEERTABLE_VENDOR=$harness" # 旧版 room client 互換
|
|
363
364
|
"PEERTABLE_MODEL=$model"
|
|
364
365
|
"PEERTABLE_EFFORT=$effort"
|
|
365
366
|
"PEERTABLE_ROLE=$role"
|
|
@@ -376,13 +377,13 @@ if [ "$mode" = "lattice" ]; then
|
|
|
376
377
|
)
|
|
377
378
|
[ -n "$lattice_cli" ] && launch_env+=("LATTICE_CLI=$lattice_cli")
|
|
378
379
|
fi
|
|
379
|
-
if [ "$
|
|
380
|
+
if [ "$harness" = grok ]; then
|
|
380
381
|
# 実測 2026-08-21: auth の coding_data_retention_opt_out だけでは
|
|
381
382
|
# `Help improve Grok [Opt out] [Opt in]` バナーが残る。席が死んだように見える。
|
|
382
383
|
launch_env+=("GROK_HOME=$grok_home")
|
|
383
384
|
launch_env+=("GROK_PRIVACY_NOTICE_ROLLOUT=0")
|
|
384
385
|
fi
|
|
385
|
-
if [ "$
|
|
386
|
+
if [ "$harness" = codex ]; then
|
|
386
387
|
codex_home="${proj}/.team/seats/${name}.codex"
|
|
387
388
|
mkdir -p "$codex_home"
|
|
388
389
|
if [ -f "${HOME}/.codex/auth.json" ]; then
|
|
@@ -391,7 +392,7 @@ if [ "$vendor" = codex ]; then
|
|
|
391
392
|
fi
|
|
392
393
|
launch_env+=("CODEX_HOME=$codex_home")
|
|
393
394
|
fi
|
|
394
|
-
if [ "$
|
|
395
|
+
if [ "$harness" = codex ] \
|
|
395
396
|
&& ! env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$codex_room_mcp_helper" ensure "$proj" "$peertable_repo"; then
|
|
396
397
|
echo "SEAT_CODEX_ROOM_MCP_INVALID: Codexのproject設定へseat固有room clientを装備できない(席は立てない)" >&2
|
|
397
398
|
exit 1
|
|
@@ -399,7 +400,7 @@ fi
|
|
|
399
400
|
# Aiterm launcherはsession作成後にtyped failureを返すことがある。ここから先は同名memberが
|
|
400
401
|
# 無いことを確認済みなので、launch試行が作ったtmux/memberをon_exitの対象として先に所有する。
|
|
401
402
|
seat_created=true
|
|
402
|
-
launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$
|
|
403
|
+
launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$harness" "$model" "$effort" "$proj" "$brief") || {
|
|
403
404
|
echo "SEAT_AITERM_LAUNCH_FAILED: ${name} をAiterm公開launcherで起動できない(direct CLI fallbackなし)" >&2
|
|
404
405
|
exit 1
|
|
405
406
|
}
|
|
@@ -449,7 +450,7 @@ seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
|
|
|
449
450
|
# Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
|
|
450
451
|
# ヘッダ/trust dialog待機をここへ重ねると、brief turnでヘッダが画面外へ流れた正常席をrollbackする。
|
|
451
452
|
# 必須room MCPの成立は、次の member登録readbackだけで判定する。
|
|
452
|
-
echo "launched: ${sess}(${
|
|
453
|
+
echo "launched: ${sess}(${harness} / ${model}${effort:+ / $effort} / room=${room} / mode=${mode})"
|
|
453
454
|
|
|
454
455
|
# CodexのヘッダはCLIが起動した証拠であって、必須room MCPが初期化された証拠ではない。
|
|
455
456
|
# 無関係MCPのwarningが画面へ出ても、room clientがmember登録まで到達した席だけを着席として扱う。
|
|
@@ -484,7 +485,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
484
485
|
# Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
|
|
485
486
|
# Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
|
|
486
487
|
# 汎用的に承認するfallbackにはしない。承認後のMCP初期化時間は改めて30秒確保する。
|
|
487
|
-
if [ "$
|
|
488
|
+
if [ "$harness" = codex ]; then
|
|
488
489
|
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
489
490
|
if pass_codex_pane "$codex_screen"; then
|
|
490
491
|
room_ready_deadline=$((SECONDS + 90))
|
|
@@ -508,7 +509,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
508
509
|
esac
|
|
509
510
|
fi
|
|
510
511
|
fi
|
|
511
|
-
if [ "$
|
|
512
|
+
if [ "$harness" = grok ] && [ "$grok_trust_accepted" != true ]; then
|
|
512
513
|
grok_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
513
514
|
case "$grok_screen" in
|
|
514
515
|
*"Do you trust the contents of this directory?"*)
|
|
@@ -524,7 +525,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
524
525
|
fi
|
|
525
526
|
# 未信頼ディレクトリでは room MCP 同意より前に workspace trust が出る。既定選択肢
|
|
526
527
|
# (1. Yes, I trust this folder)を Enter で通す。この既知文言だけを一度通す。
|
|
527
|
-
if [ "$
|
|
528
|
+
if [ "$harness" = claude ] && [ "$claude_trust_accepted" != true ]; then
|
|
528
529
|
claude_trust_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
529
530
|
case "$claude_trust_screen" in
|
|
530
531
|
*"Is this a project you created or one you trust"*|*"trust this folder"*)
|
|
@@ -540,7 +541,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
540
541
|
fi
|
|
541
542
|
# aiterm claude_agent は --dangerously-skip-permissions を付けない。project の room MCP
|
|
542
543
|
# 同意が member 登録より前に出る。選択肢2(this and all future)だけを通す。
|
|
543
|
-
if [ "$
|
|
544
|
+
if [ "$harness" = claude ] && [ "$mcp_consent_accepted" != true ]; then
|
|
544
545
|
claude_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
545
546
|
case "$claude_screen" in
|
|
546
547
|
*"New MCP server found in this project: room"*)
|
|
@@ -556,7 +557,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
556
557
|
fi
|
|
557
558
|
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
558
559
|
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 [ "$
|
|
560
|
+
if [ "$harness" = codex ]; then
|
|
560
561
|
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
561
562
|
if ! codex_pane_blocks_ready "$codex_screen"; then
|
|
562
563
|
sleep 1
|
|
@@ -575,7 +576,7 @@ if [ "$room_ready" != true ]; then
|
|
|
575
576
|
exit 1
|
|
576
577
|
fi
|
|
577
578
|
echo "room ready: ${room}/${name}"
|
|
578
|
-
if [ "$
|
|
579
|
+
if [ "$harness" = codex ]; then
|
|
579
580
|
# member 登録後の最初の tool 呼び出しで MCP Allow が出る。出ている間だけ通し、沈黙5秒で抜ける。
|
|
580
581
|
codex_post_ready_deadline=$((SECONDS + 90))
|
|
581
582
|
codex_post_ready_idle=0
|
|
@@ -607,7 +608,7 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
607
608
|
brief_ready=false
|
|
608
609
|
while [ $SECONDS -lt "$brief_ready_deadline" ]; do
|
|
609
610
|
brief_ready_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
610
|
-
if [ "$
|
|
611
|
+
if [ "$harness" != 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
|
|
611
612
|
brief_ready_streak=$((brief_ready_streak + 1))
|
|
612
613
|
if [ "$brief_ready_streak" -ge 3 ]; then
|
|
613
614
|
brief_ready=true
|
|
@@ -691,9 +692,11 @@ if [ -z "$seat_pid" ]; then
|
|
|
691
692
|
# 記録が無ければ席は attach できず、装置の介入は協調 hold のままになる。**黙らない。**
|
|
692
693
|
echo "seat identity を記録できなかった: ${sess} の process group leader を1つに確定できない(席は着席済み)" >&2
|
|
693
694
|
else
|
|
694
|
-
|
|
695
|
+
# aiterm_session_id の書き手はランチャー(launch receipt の所有者)。席の client は
|
|
696
|
+
# Codex closed-mode env で AITERM_SESSION_ID を受け取れないため、ここで台帳へ載せる。
|
|
697
|
+
ident_body=$(python3 - "$name" "$seat_ident" "$aiterm_session_id" <<'PY'
|
|
695
698
|
import json, subprocess, sys
|
|
696
|
-
name, ident_raw = sys.argv[1:
|
|
699
|
+
name, ident_raw, aiterm_session = sys.argv[1:4]
|
|
697
700
|
ident = json.loads(ident_raw)
|
|
698
701
|
if not ident.get('started_identity') or not ident.get('argv') or not ident.get('argv_digest'):
|
|
699
702
|
sys.exit('pid の lstart/args を観測できない')
|
|
@@ -702,6 +705,7 @@ print(json.dumps({
|
|
|
702
705
|
'pid': int(ident['pid']),
|
|
703
706
|
'started_identity': ident['started_identity'],
|
|
704
707
|
'argv_digest': ident['argv_digest'],
|
|
708
|
+
'aiterm_session_id': aiterm_session,
|
|
705
709
|
'identity_recorded_at': subprocess.run(['date', '-u', '+%Y-%m-%dT%H:%M:%S.000Z'],
|
|
706
710
|
capture_output=True, text=True, check=True).stdout.strip(),
|
|
707
711
|
}, ensure_ascii=False))
|
|
@@ -713,7 +717,7 @@ PY
|
|
|
713
717
|
fi
|
|
714
718
|
fi
|
|
715
719
|
|
|
716
|
-
# 素性(
|
|
720
|
+
# 素性(harness/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
|
|
717
721
|
# ランチャーはここで台帳を読み返し、実際に載ったかを確認するだけにする(重複書込の禁止)。
|
|
718
722
|
member_row=$(curl -sf "$url/api/$room/members/$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$name")" || true)
|
|
719
723
|
stored=$(python3 - "$member_row" <<'PY'
|
|
@@ -727,11 +731,12 @@ print('yes' if m.get('model') and m.get('roles') and m.get('aiterm_session_id')
|
|
|
727
731
|
PY
|
|
728
732
|
)
|
|
729
733
|
if [ "$stored" = yes ]; then
|
|
730
|
-
echo "台帳確認: ${
|
|
734
|
+
echo "台帳確認: ${harness} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
|
|
731
735
|
else
|
|
732
736
|
echo "SEAT_LEDGER_INCOMPLETE: 台帳の member 行に素性または本人性が欠けている(席は着席済み。doctor で確認)" >&2
|
|
733
737
|
fi
|
|
734
738
|
|
|
739
|
+
PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" alarm || echo "alarm-bridge の起動確認に失敗した(席は着席済み)" >&2
|
|
735
740
|
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" seat-status; then
|
|
736
741
|
echo "seat-status-bridge: 起動確認済み"
|
|
737
742
|
else
|
|
@@ -740,7 +745,7 @@ fi
|
|
|
740
745
|
|
|
741
746
|
# Claude 席の新着は room client の notifications/claude/channel。TUI 配達は channels を持たない
|
|
742
747
|
# Codex / Grok 席だけ。Claude に立てると channel と二重配達になる。
|
|
743
|
-
if [ "$
|
|
748
|
+
if [ "$harness" = codex ] || [ "$harness" = grok ]; then
|
|
744
749
|
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" wakeup; then
|
|
745
750
|
echo "wakeup-bridge: TUI配達 起動確認済み"
|
|
746
751
|
else
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# 親(ベル等)が room へ着卓する。
|
|
3
|
-
# usage: parent-join.sh <project_dir> [name] [model] [effort] [
|
|
3
|
+
# usage: parent-join.sh <project_dir> [name] [model] [effort] [harness]
|
|
4
4
|
# name 既定は bell。broadcast廃止に伴いkickoff投稿は行わない。
|
|
5
|
-
# model / effort /
|
|
5
|
+
# model / effort / harness は任意。親はオーナーの対話セッション(決定40)なので、席と違って
|
|
6
6
|
# 起動時に確定した値を script が知らない——**渡された時だけ**参加者一覧の素性として登録する。
|
|
7
7
|
# 渡さなければ欄ごと出ない(「不明」ではなく「素性を名乗っていない」)。
|
|
8
|
-
#
|
|
8
|
+
# harness は claude(既定)、codex、または grok。model だけ渡して harness を渡さない場合は
|
|
9
9
|
# claude とみなす(後方互換)。
|
|
10
10
|
# 親は MCP を後付けできないので room へは HTTP API 直で入る(決定40 の operating notes)。
|
|
11
11
|
set -e
|
|
12
|
-
proj="$1"; name="${2:-bell}"; model="$3"; effort="$4";
|
|
12
|
+
proj="$1"; name="${2:-bell}"; model="$3"; effort="$4"; harness="$5"
|
|
13
13
|
mission=""
|
|
14
14
|
if [ "${6:-}" = "--mission" ]; then mission="${7:-}"; fi
|
|
15
15
|
state="$proj/.team/setup-state.json"
|
|
@@ -23,21 +23,22 @@ fi
|
|
|
23
23
|
|
|
24
24
|
# 親はAiterm席ではない。Claude/Codex/Grokとも、親自身が所有するparent-watchを配送先にする。
|
|
25
25
|
# tmux observeやCodex thread IDを登録すると、通常席bridge/外部resumeへ誤配送される。
|
|
26
|
-
|
|
27
|
-
if [ -z "$
|
|
26
|
+
parent_harness="$harness"
|
|
27
|
+
if [ -z "$parent_harness" ] && [ -n "$model" ]; then
|
|
28
28
|
case "$model" in
|
|
29
|
-
gpt-*|o[0-9]*)
|
|
30
|
-
grok*)
|
|
31
|
-
claude*|opus*|sonnet*|haiku*|fable*)
|
|
29
|
+
gpt-*|o[0-9]*) parent_harness=codex ;;
|
|
30
|
+
grok*) parent_harness=grok ;;
|
|
31
|
+
claude*|opus*|sonnet*|haiku*|fable*) parent_harness=claude ;;
|
|
32
32
|
esac
|
|
33
33
|
fi
|
|
34
|
-
member=$(python3 - "$name" "$model" "$effort" "$
|
|
34
|
+
member=$(python3 - "$name" "$model" "$effort" "$parent_harness" "$mission" <<'PY'
|
|
35
35
|
import json, sys
|
|
36
|
-
name, model, effort,
|
|
36
|
+
name, model, effort, harness, mission = sys.argv[1:6]
|
|
37
37
|
# 台帳には canonical 欄だけを登録する(settings / role 単数の重複欄は 2026-08-22 廃止)
|
|
38
38
|
body = {'name': name, 'roles': ['統括']}
|
|
39
|
-
if
|
|
40
|
-
body['
|
|
39
|
+
if harness:
|
|
40
|
+
body['harness'] = harness
|
|
41
|
+
body['vendor'] = harness # 旧版 room server 互換
|
|
41
42
|
if model:
|
|
42
43
|
body['model'] = model
|
|
43
44
|
if effort:
|
|
@@ -45,7 +46,7 @@ if effort:
|
|
|
45
46
|
if mission:
|
|
46
47
|
body['mission'] = mission
|
|
47
48
|
body['observe'] = None
|
|
48
|
-
body['delivery'] = {'kind': 'parent_watch', 'host':
|
|
49
|
+
body['delivery'] = {'kind': 'parent_watch', 'host': harness or ''}
|
|
49
50
|
print(json.dumps(body, ensure_ascii=False))
|
|
50
51
|
PY
|
|
51
52
|
)
|
|
@@ -75,11 +76,11 @@ echo "親の権限境界: 各ToDoのクローズは監査担当が行い、親
|
|
|
75
76
|
|
|
76
77
|
# parent-watchが初回headを固定する。以後のDMはwatcher不在時間を含め
|
|
77
78
|
# 永続cursorからcatch-upされる。host内のbackground task自体は親セッションだけが所有できる。
|
|
78
|
-
if PEERTABLE_PARENT_HOST="$
|
|
79
|
-
echo "parent-watch cursor ready: ${name}(host=${
|
|
80
|
-
if [ "$
|
|
79
|
+
if PEERTABLE_PARENT_HOST="$parent_harness" node "$here/parent-watch.mjs" "$proj" "$name" --prime; then
|
|
80
|
+
echo "parent-watch cursor ready: ${name}(host=${parent_harness})"
|
|
81
|
+
if [ "$parent_harness" = "codex" ]; then
|
|
81
82
|
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は常駐させない"
|
|
82
|
-
elif [ "$
|
|
83
|
+
elif [ "$parent_harness" = "grok" ]; then
|
|
83
84
|
echo "PARENT_WATCH_START_REQUIRED: Grok Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること。通常席用wakeup-bridgeに親を載せない"
|
|
84
85
|
else
|
|
85
86
|
echo "PARENT_WATCH_START_REQUIRED: Claude Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// 02_models.md の役割→1位〜3位から、着席可能な
|
|
2
|
+
// 02_models.md の役割→1位〜3位から、着席可能な harness / model / effort を解決する。
|
|
3
3
|
// 具体モデル名はここに持たない。台帳と順位表をその場で読む。
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
const EFFORT = /×\s*(none|low|medium|high|xhigh|max|ultra)/iu
|
|
10
|
-
const
|
|
10
|
+
const harnessOf = (modelName) => {
|
|
11
11
|
const name = String(modelName).trim()
|
|
12
12
|
if (/^claude\b/iu.test(name)) return 'claude'
|
|
13
13
|
if (/^gpt\b|^gpt-/iu.test(name)) return 'codex'
|
|
@@ -62,9 +62,9 @@ export function parseLedger(markdown) {
|
|
|
62
62
|
return rows.flatMap((row) => {
|
|
63
63
|
const name = row['モデル']
|
|
64
64
|
const slug = slugOf(row['slug'] ?? '')
|
|
65
|
-
const
|
|
66
|
-
if (!name || !slug || !
|
|
67
|
-
return [{ name, slug,
|
|
65
|
+
const harness = harnessOf(name)
|
|
66
|
+
if (!name || !slug || !harness) return []
|
|
67
|
+
return [{ name, slug, harness }]
|
|
68
68
|
})
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -103,7 +103,7 @@ export function listOfficialRoles(markdown) {
|
|
|
103
103
|
.filter(Boolean)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export function
|
|
106
|
+
export function harnessFromSlug(slug) {
|
|
107
107
|
const text = String(slug ?? '').trim().toLowerCase()
|
|
108
108
|
if (!text) return null
|
|
109
109
|
if (/^(gpt-|gpt\b|o[0-9])/u.test(text)) return 'codex'
|
|
@@ -125,8 +125,8 @@ const parseRoleList = (roles) => {
|
|
|
125
125
|
return list
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export function resolveSeatPlacement(role, markdown, { source = '',
|
|
129
|
-
const
|
|
128
|
+
export function resolveSeatPlacement(role, markdown, { source = '', harness = '' } = {}) {
|
|
129
|
+
const wantHarness = String(harness ?? '').trim()
|
|
130
130
|
const wanted = String(role ?? '').trim()
|
|
131
131
|
if (!wanted) {
|
|
132
132
|
return { error: 'SEAT_ROLE_REQUIRED', message: 'role が空(02_models の役割名が要る)' }
|
|
@@ -151,12 +151,12 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
|
|
|
151
151
|
dropped.push({ rank, reason: 'not-in-ledger', cell: parsed.cell })
|
|
152
152
|
continue
|
|
153
153
|
}
|
|
154
|
-
if (
|
|
155
|
-
dropped.push({ rank, reason: `
|
|
154
|
+
if (wantHarness && hit.harness !== wantHarness) {
|
|
155
|
+
dropped.push({ rank, reason: `harness-mismatch(want ${wantHarness})`, cell: parsed.cell })
|
|
156
156
|
continue
|
|
157
157
|
}
|
|
158
158
|
if (hit.slug === 'haiku') {
|
|
159
|
-
return { role: wanted, rank,
|
|
159
|
+
return { role: wanted, rank, harness: hit.harness, model: hit.slug, effort: '', source, dropped }
|
|
160
160
|
}
|
|
161
161
|
if (!parsed.effort) {
|
|
162
162
|
dropped.push({ rank, reason: 'effort-missing', cell: parsed.cell })
|
|
@@ -165,7 +165,7 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
|
|
|
165
165
|
return {
|
|
166
166
|
role: wanted,
|
|
167
167
|
rank,
|
|
168
|
-
|
|
168
|
+
harness: hit.harness,
|
|
169
169
|
model: hit.slug,
|
|
170
170
|
effort: parsed.effort,
|
|
171
171
|
source,
|
|
@@ -174,13 +174,13 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
|
|
|
174
174
|
}
|
|
175
175
|
return {
|
|
176
176
|
error: 'SEAT_PLACEMENT_UNRESOLVABLE',
|
|
177
|
-
message: `${wanted}${
|
|
177
|
+
message: `${wanted}${wantHarness ? `(harness=${wantHarness})` : ''} を着席可能な harness/model/effort へ解決できない`,
|
|
178
178
|
dropped,
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export function resolveSeatIdentity({
|
|
183
|
-
roles, model, effort,
|
|
183
|
+
roles, model, effort, harness, markdown, source = '', allowParentRole = false,
|
|
184
184
|
} = {}) {
|
|
185
185
|
const roleList = parseRoleList(roles)
|
|
186
186
|
if (roleList.length === 0) {
|
|
@@ -204,7 +204,7 @@ export function resolveSeatIdentity({
|
|
|
204
204
|
return {
|
|
205
205
|
roles: roleList,
|
|
206
206
|
settings: {
|
|
207
|
-
|
|
207
|
+
harness: String(harness ?? '').trim(),
|
|
208
208
|
model: '',
|
|
209
209
|
effort: String(effort ?? '').trim(),
|
|
210
210
|
},
|
|
@@ -214,29 +214,29 @@ export function resolveSeatIdentity({
|
|
|
214
214
|
|
|
215
215
|
const requestedModel = String(model ?? '').trim()
|
|
216
216
|
const requestedEffort = String(effort ?? '').trim()
|
|
217
|
-
const
|
|
217
|
+
const requestedHarness = String(harness ?? '').trim()
|
|
218
218
|
if (requestedModel) {
|
|
219
|
-
const
|
|
220
|
-
|| parseLedger(markdown).find((row) => row.slug === requestedModel)?.
|
|
221
|
-
if (!
|
|
219
|
+
const resolvedHarness = requestedHarness || harnessFromSlug(requestedModel)
|
|
220
|
+
|| parseLedger(markdown).find((row) => row.slug === requestedModel)?.harness
|
|
221
|
+
if (!resolvedHarness) {
|
|
222
222
|
return {
|
|
223
|
-
error: '
|
|
224
|
-
message: `model=${requestedModel} の
|
|
223
|
+
error: 'SEAT_HARNESS_UNRESOLVED',
|
|
224
|
+
message: `model=${requestedModel} の harness を推定できない(--harness が要る)`,
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
return {
|
|
228
228
|
roles: roleList,
|
|
229
|
-
settings: {
|
|
229
|
+
settings: { harness: resolvedHarness, model: requestedModel, effort: requestedEffort },
|
|
230
230
|
source,
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
const first = resolveSeatPlacement(roleList[0], markdown, { source,
|
|
234
|
+
const first = resolveSeatPlacement(roleList[0], markdown, { source, harness: requestedHarness })
|
|
235
235
|
if (first.error) return first
|
|
236
236
|
return {
|
|
237
237
|
roles: roleList,
|
|
238
238
|
settings: {
|
|
239
|
-
|
|
239
|
+
harness: first.harness,
|
|
240
240
|
model: first.model,
|
|
241
241
|
effort: requestedEffort || first.effort || '',
|
|
242
242
|
},
|
|
@@ -251,14 +251,15 @@ if (isMain) {
|
|
|
251
251
|
let roles = ''
|
|
252
252
|
let model = ''
|
|
253
253
|
let effort = ''
|
|
254
|
-
let
|
|
254
|
+
let harness = ''
|
|
255
255
|
const positional = []
|
|
256
256
|
for (let i = 0; i < args.length; i += 1) {
|
|
257
257
|
const arg = args[i]
|
|
258
258
|
if (arg === '--roles') { roles = args[++i] ?? ''; continue }
|
|
259
259
|
if (arg === '--model') { model = args[++i] ?? ''; continue }
|
|
260
260
|
if (arg === '--effort') { effort = args[++i] ?? ''; continue }
|
|
261
|
-
|
|
261
|
+
// --vendor は旧名の互換 alias
|
|
262
|
+
if (arg === '--harness' || arg === '--vendor') { harness = args[++i] ?? ''; continue }
|
|
262
263
|
positional.push(arg)
|
|
263
264
|
}
|
|
264
265
|
if (!roles) roles = positional[0] ?? ''
|
|
@@ -270,7 +271,7 @@ if (isMain) {
|
|
|
270
271
|
fail('SEAT_MODELS_DOC_MISSING', `02_models.md が無い: ${doc}`)
|
|
271
272
|
}
|
|
272
273
|
const result = resolveSeatIdentity({
|
|
273
|
-
roles, model, effort,
|
|
274
|
+
roles, model, effort, harness, markdown: readFileSync(doc, 'utf8'), source: doc,
|
|
274
275
|
})
|
|
275
276
|
if (result.error) fail(result.error, result.message)
|
|
276
277
|
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
@@ -146,6 +146,39 @@ async function serverKeepsStatus() {
|
|
|
146
146
|
return supportsMemberObservation(await res.json())
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// ---- 継続番犬(2026-08-22 オーナー設計)----
|
|
150
|
+
// 「AI は ToDo が終わっていなくてもターンを終えてしまう」への機械保証。busy が一定時間
|
|
151
|
+
// 続いた席が停止宣言([待機]/[監査提出]等)なしに idle 化したら、継続指示を配達する。
|
|
152
|
+
// 自己DM(席の自己規律)は保険として残るが、継続の保証はこの番犬が持つ。
|
|
153
|
+
// busy 2分未満は情報読取だけの正当な無宣言ターンがあるため起こさない(誤爆の代償は
|
|
154
|
+
// 待機宣言1ターンで、宣言後の episode は declared 判定で再起こしされない)。
|
|
155
|
+
const STOP_DECLARATION = /\[待機\]|\[監査提出\]|待機します|散会/u
|
|
156
|
+
const NUDGE_MIN_BUSY_MS = 120_000
|
|
157
|
+
const nudgedEpisodes = new Map() // name -> busySince(同一エピソード1回だけ)
|
|
158
|
+
async function nudgeIfDropped(name, busySince) {
|
|
159
|
+
if (!busySince || nudgedEpisodes.get(name) === busySince) return
|
|
160
|
+
if (Date.now() - Date.parse(busySince) < NUDGE_MIN_BUSY_MS) return
|
|
161
|
+
nudgedEpisodes.set(name, busySince)
|
|
162
|
+
let messages
|
|
163
|
+
try {
|
|
164
|
+
messages = (await (await fetch(`${url}/api/${encodeURIComponent(room)}/messages`)).json()).messages ?? []
|
|
165
|
+
} catch { return }
|
|
166
|
+
const since = Date.parse(busySince)
|
|
167
|
+
if (messages.some(m => m.from === name && Date.parse(m.ts) >= since && STOP_DECLARATION.test(m.body ?? ''))) return
|
|
168
|
+
try {
|
|
169
|
+
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/messages`, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { 'X-Peertable-Token': token } : {}) },
|
|
172
|
+
body: JSON.stringify({ from: 'alarm', to: name,
|
|
173
|
+
body: '[継続] 停止宣言なしにターンが終了した。未完の作業があれば続行すること。手番が無いなら規約どおり [待機] を宣言してから沈黙すること。' }),
|
|
174
|
+
})
|
|
175
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
176
|
+
console.error(`seat-status-bridge: 継続番犬が ${name} を起こした(停止宣言なしの busy→idle)`)
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error(`seat-status-bridge: 継続番犬の起こしに失敗: ${e.message}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
const last = new Map() // name -> { status, at }
|
|
150
183
|
let supported = null // server が status を保持する版か(未判定は null)
|
|
151
184
|
const tokenBucket = value => value === null ? null : Math.floor(value / 1_000)
|
|
@@ -221,6 +254,7 @@ async function tick() {
|
|
|
221
254
|
await send(name, observation, observedAt)
|
|
222
255
|
last.set(name, { ...observation, at: now })
|
|
223
256
|
sent++
|
|
257
|
+
if (prev?.status === 'busy' && observation.status === 'idle') await nudgeIfDropped(name, prev.busySince)
|
|
224
258
|
if (changed) console.error(`seat-status-bridge: ${name} → ${observation.status}${prev ? `(${prev.status} から)` : ''}`)
|
|
225
259
|
} catch (e) {
|
|
226
260
|
failed++
|
|
@@ -268,7 +268,7 @@ export function isPaneProcessStopped(panePid, exec = execFileSync) {
|
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* paneのstatus行が公開しているtoken値だけを読む。
|
|
271
|
-
*
|
|
271
|
+
* harness固有のログや課金単価は推測せず、表示が無い席はnullのままにする。
|
|
272
272
|
*/
|
|
273
273
|
export function parsePaneTokenHint(pane) {
|
|
274
274
|
if (typeof pane !== 'string') return null
|
package/skill/scripts/setup.sh
CHANGED
|
@@ -119,6 +119,10 @@ else
|
|
|
119
119
|
cp "$tpl/done.sh" "$tdir/scripts/done.sh" && chmod +x "$tdir/scripts/done.sh"
|
|
120
120
|
cp "$tpl/independence-refresh.sh" "$tdir/scripts/independence-refresh.sh" && chmod +x "$tdir/scripts/independence-refresh.sh"
|
|
121
121
|
fi
|
|
122
|
+
# 目覚まし係の登録口は全モード共通で配る(member.md の待機作法が参照する)
|
|
123
|
+
cp "$repo/skill/scripts/alarm-set.sh" "$tdir/scripts/alarm-set.sh" && chmod +x "$tdir/scripts/alarm-set.sh"
|
|
124
|
+
# member.md が attach 手順で参照する。配り漏れると席が attach できず停止する(実被弾 2026-08-22)
|
|
125
|
+
cp "$repo/skill/scripts/pull-attach-input.mjs" "$tdir/scripts/pull-attach-input.mjs" && chmod +x "$tdir/scripts/pull-attach-input.mjs"
|
|
122
126
|
# room MCP 定義は project root の .mcp.json が正(channels は --mcp-config を解決しない。決定44)
|
|
123
127
|
added_root_mcp=false
|
|
124
128
|
if [ -f "$proj/.mcp.json" ]; then
|
|
@@ -197,6 +201,7 @@ printf '{"room":"%s","server_url":"%s","public_url":"%s","mode":"%s","plan_key":
|
|
|
197
201
|
# 4時間 HTTP 403 を撃ち続け、参加者一覧には点が1つも出なかった=起こしていないのと見分けがつかない)。
|
|
198
202
|
# AI は使わず席へも1バイト送らないので卓の作業を邪魔しない。**書けない時は常駐せずに死ぬ**ので、
|
|
199
203
|
# ここで壊れた常駐が黙って残ることは無い。setup-state.json を読むので、必ずその後で起こす。
|
|
204
|
+
"$repo/skill/scripts/ensure-bridge.sh" "$proj" alarm || echo "alarm-bridge の起動確認に失敗した(席は使える・目覚まし係だけ後で ensure-bridge.sh で立てる)" >&2
|
|
200
205
|
if "$repo/skill/scripts/ensure-bridge.sh" "$proj" seat-status; then
|
|
201
206
|
echo "seat-status-bridge: ready_at を確認した(ログ $tdir/seat-status-bridge.log・停止は teardown が行う)"
|
|
202
207
|
else
|