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.
@@ -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>] [--vendor <vendor>] [brief]
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>] [--vendor <vendor>] [brief]" >&2
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=""; opt_vendor=""; opt_effort=""
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) opt_vendor="${2:-}"; shift 2 ;;
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 "$opt_vendor" ] && placement_argv+=(--vendor "$opt_vendor")
68
+ [ -n "$opt_harness" ] && placement_argv+=(--harness "$opt_harness")
69
69
  placement=$(node "$placement_helper" "${placement_argv[@]}") || exit "$?"
70
- IFS=$'\t' read -r model vendor effort role <<EOF
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("vendor") or "", s.get("effort") or "", roles)))' <<<"$placement")
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} → ${vendor} / ${model}${effort:+ / $effort}"
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 "$vendor" in
83
+ case "$harness" in
84
84
  claude|codex|grok) ;;
85
- *) echo "SEAT_LAUNCH_VENDOR_UNSUPPORTED: vendor=${vendor}" >&2; exit 1 ;;
85
+ *) echo "SEAT_LAUNCH_HARNESS_UNSUPPORTED: harness=${harness}" >&2; exit 1 ;;
86
86
  esac
87
- if [ "$vendor" = "claude" ] && [ -n "$effort" ]; then
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 "$vendor" in
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 vendor: ${vendor}(claude / codex / grok)" >&2; exit 1 ;;
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 秒で返らない: ${vendor} / ${model}(席は立てない)" >&2
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 で使えない: ${vendor} / ${model}(preflight rc=${preflight_rc}・席は立てない)" >&2
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 [ "$vendor" = grok ]; then
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
- "PEERTABLE_VENDOR=$vendor"
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 [ "$vendor" = grok ]; then
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 [ "$vendor" = codex ]; then
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 [ "$vendor" = codex ] \
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" "$vendor" "$model" "$effort" "$proj" "$brief") || {
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}(${vendor} / ${model}${effort:+ / $effort} / room=${room} / mode=${mode})"
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 [ "$vendor" = codex ]; then
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 [ "$vendor" = grok ] && [ "$grok_trust_accepted" != true ]; then
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 [ "$vendor" = claude ] && [ "$claude_trust_accepted" != true ]; then
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 [ "$vendor" = claude ] && [ "$mcp_consent_accepted" != true ]; then
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 [ "$vendor" = codex ]; then
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 [ "$vendor" = codex ]; then
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 [ "$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
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
- ident_body=$(python3 - "$name" "$seat_ident" <<'PY'
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:3]
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
- # 素性(vendor/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
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 "台帳確認: ${vendor} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
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 [ "$vendor" = codex ] || [ "$vendor" = grok ]; then
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] [vendor]
3
+ # usage: parent-join.sh <project_dir> [name] [model] [effort] [harness]
4
4
  # name 既定は bell。broadcast廃止に伴いkickoff投稿は行わない。
5
- # model / effort / vendor は任意。親はオーナーの対話セッション(決定40)なので、席と違って
5
+ # model / effort / harness は任意。親はオーナーの対話セッション(決定40)なので、席と違って
6
6
  # 起動時に確定した値を script が知らない——**渡された時だけ**参加者一覧の素性として登録する。
7
7
  # 渡さなければ欄ごと出ない(「不明」ではなく「素性を名乗っていない」)。
8
- # vendor は claude(既定)、codex、または grok。model だけ渡して vendor を渡さない場合は
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"; vendor="$5"
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
- parent_vendor="$vendor"
27
- if [ -z "$parent_vendor" ] && [ -n "$model" ]; then
26
+ parent_harness="$harness"
27
+ if [ -z "$parent_harness" ] && [ -n "$model" ]; then
28
28
  case "$model" in
29
- gpt-*|o[0-9]*) parent_vendor=codex ;;
30
- grok*) parent_vendor=grok ;;
31
- claude*|opus*|sonnet*|haiku*|fable*) parent_vendor=claude ;;
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" "$parent_vendor" "$mission" <<'PY'
34
+ member=$(python3 - "$name" "$model" "$effort" "$parent_harness" "$mission" <<'PY'
35
35
  import json, sys
36
- name, model, effort, vendor, mission = sys.argv[1:6]
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 vendor:
40
- body['vendor'] = vendor
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': vendor or ''}
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="$parent_vendor" node "$here/parent-watch.mjs" "$proj" "$name" --prime; then
79
- echo "parent-watch cursor ready: ${name}(host=${parent_vendor})"
80
- if [ "$parent_vendor" = "codex" ]; then
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 [ "$parent_vendor" = "grok" ]; then
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位から、着席可能な vendor / model / effort を解決する。
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 vendorOf = (modelName) => {
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 vendor = vendorOf(name)
66
- if (!name || !slug || !vendor) return []
67
- return [{ name, slug, vendor }]
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 vendorFromSlug(slug) {
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 = '', vendor = '' } = {}) {
129
- const wantVendor = String(vendor ?? '').trim()
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 (wantVendor && hit.vendor !== wantVendor) {
155
- dropped.push({ rank, reason: `vendor-mismatch(want ${wantVendor})`, cell: parsed.cell })
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, vendor: hit.vendor, model: hit.slug, effort: '', source, dropped }
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
- vendor: hit.vendor,
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}${wantVendor ? `(vendor=${wantVendor})` : ''} を着席可能な vendor/model/effort へ解決できない`,
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, vendor, markdown, source = '', allowParentRole = false,
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
- vendor: String(vendor ?? '').trim(),
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 requestedVendor = String(vendor ?? '').trim()
217
+ const requestedHarness = String(harness ?? '').trim()
218
218
  if (requestedModel) {
219
- const resolvedVendor = requestedVendor || vendorFromSlug(requestedModel)
220
- || parseLedger(markdown).find((row) => row.slug === requestedModel)?.vendor
221
- if (!resolvedVendor) {
219
+ const resolvedHarness = requestedHarness || harnessFromSlug(requestedModel)
220
+ || parseLedger(markdown).find((row) => row.slug === requestedModel)?.harness
221
+ if (!resolvedHarness) {
222
222
  return {
223
- error: 'SEAT_VENDOR_UNRESOLVED',
224
- message: `model=${requestedModel} の vendor を推定できない(--vendor が要る)`,
223
+ error: 'SEAT_HARNESS_UNRESOLVED',
224
+ message: `model=${requestedModel} の harness を推定できない(--harness が要る)`,
225
225
  }
226
226
  }
227
227
  return {
228
228
  roles: roleList,
229
- settings: { vendor: resolvedVendor, model: requestedModel, effort: requestedEffort },
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, vendor: requestedVendor })
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
- vendor: first.vendor,
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 vendor = ''
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
- if (arg === '--vendor') { vendor = args[++i] ?? ''; continue }
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, vendor, markdown: readFileSync(doc, 'utf8'), source: doc,
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
- * vendor固有のログや課金単価は推測せず、表示が無い席はnullのままにする。
271
+ * harness固有のログや課金単価は推測せず、表示が無い席はnullのままにする。
272
272
  */
273
273
  export function parsePaneTokenHint(pane) {
274
274
  if (typeof pane !== 'string') return null
@@ -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