peertable 0.4.11 → 0.4.13

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 CHANGED
@@ -107,9 +107,9 @@ Codex では、スキルが所有する room MCP block をプロジェクトの
107
107
 
108
108
  動いており、**自分自身の開発に使っている**。2026-08-08 に end-to-end 検証済み——オーケストレーターなしの完全な一周(2 メンバーが相談し、claim し、インターフェースを交渉し、見つけた罠を共有して小さなプロジェクトを出荷)を**外部介入ゼロ**で完走。2026-08-13の実席ライフサイクルでは、作業席が親を通じてsession contextを保ったままmodel / effortを変更し、再起動後はroomと工程正本から再着任した。2026-08-14にはGrok 4.6席の着席、room参加、同一sessionの4.6↔4.5変更、DM起床を実機で確認した。2026-08-17にGrok席はidle待ち、broadcastは本文を残し、tmuxの無い親でbridge cursorが止まらないよう直した。
109
109
 
110
- 現在のnpm releaseは **peertable 0.4.11**。
110
+ 現在のnpm releaseは **peertable 0.4.13**。
111
111
 
112
- 設計文書と決定履歴(**89 決定**)は [docs/plan.md](docs/plan.md)。
112
+ 設計文書と決定履歴(**91 決定**)は [docs/plan.md](docs/plan.md)。
113
113
 
114
114
  Claude Code channels はリサーチプレビューのため、フラグ・プロトコルは変わりうる。
115
115
 
package/README.md CHANGED
@@ -143,9 +143,9 @@ It interviews you, names the members, scaffolds `.team/` (charter + roles, isola
143
143
 
144
144
  Working, and used to build itself. First verified end-to-end on 2026-08-08 with a full no-orchestrator loop: two members consulted, claimed, negotiated an interface, shared a discovered pitfall, and shipped a small project with **zero external intervention**. A 2026-08-13 real-seat lifecycle verified in-place model/effort changes and restart recovery. On 2026-08-14, a Grok 4.6 seat joined the room, changed 4.6↔4.5 in the same session, and woke on a direct message in a live acceptance run. On 2026-08-17 the wake-up path was corrected so Grok seats wait for idle, broadcasts keep their body, and a parent without a tmux seat cannot stall the bridge cursor.
145
145
 
146
- The current npm release is **peertable 0.4.11**.
146
+ The current npm release is **peertable 0.4.13**.
147
147
 
148
- The design document and decision log (**89 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
148
+ The design document and decision log (**91 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
149
149
 
150
150
  Depends on Claude Code **channels**, currently a research preview — flags and protocol may change.
151
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "description": "A round table of peer agents. No orchestrator at the head. Turn Claude Code, Codex, and Grok sessions into a team of equal, long-lived peers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/room/client.mjs CHANGED
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
11
11
 
12
12
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
13
13
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
14
- const MCP_VERSION = '0.4.11'
14
+ const MCP_VERSION = '0.4.13'
15
15
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
16
16
 
17
17
  const USAGE = `usage:
@@ -69,7 +69,9 @@ const mcp = new Server(
69
69
  instructions:
70
70
  `あなたは Peertable room「${ROOM}」のメンバー「${ME}」である。` +
71
71
  '<channel source="room"> の通知は「新着あり」の合図であり、本文は read_unread ツールで読む。' +
72
- '発言は post ツールを使う。to: "all" はroom全体、メンバー名はDM、配列は複数人宛である。',
72
+ '発言は post ツールを使う。to: "all" はroom全体、メンバー名はDM、配列は複数人宛である。' +
73
+ `ターンを終える直前に必ず post(to: "${ME}", message: "[次の行動] ...") を1回送れ。` +
74
+ 'wakeup-bridge はこの自己DMで次ターンを起こす。出さないと席は止まり、次の仕事は来ない。空の終了通知は使うな。',
73
75
  },
74
76
  )
75
77
 
@@ -79,7 +81,9 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
79
81
  tools: [
80
82
  {
81
83
  name: 'post',
82
- description: 'room へ通常発言する。to は "all"(room全体)/ メンバー名(DM)/ メンバー名の配列(複数人宛)',
84
+ description:
85
+ 'room へ通常発言する。to は "all"(room全体)/ メンバー名(DM)/ メンバー名の配列(複数人宛)。' +
86
+ `ターン終了時は to を自分の名前(${ME})にし、message を "[次の行動] " で始めよ。これが次ターンの起こしである。`,
83
87
  inputSchema: {
84
88
  type: 'object',
85
89
  properties: {
@@ -287,6 +291,7 @@ async function runDiagnostics(asJson) {
287
291
  'scripts/teardown.sh',
288
292
  'scripts/external-pane.mjs',
289
293
  'scripts/launch-seat.sh',
294
+ 'scripts/resolve-seat-placement.mjs',
290
295
  'scripts/tmux-at.bash',
291
296
  'scripts/tmux-socket.mjs',
292
297
  'scripts/seat-identity.mjs',
package/skill/SKILL.md CHANGED
@@ -28,7 +28,7 @@ description: 任意プロジェクトに Peertable チーム(対等メンバ
28
28
  ## Peertableの正規席と委譲入口
29
29
 
30
30
  Peertableのメンバー席は、aiterm-mcpの外部PTYに長寿命で着席させる。新しい席は
31
- `env -u PEERTABLE_POST_TOKEN scripts/launch-seat.sh <project> <name> <model> <vendor> <effort> [brief]`で起こし、既存席の分担・起床・確認は
31
+ `env -u PEERTABLE_POST_TOKEN scripts/launch-seat.sh <project> <name> <role> [brief]`で起こし、既存席の分担・起床・確認は
32
32
  `mcp__aiterm__pty_read` / `mcp__aiterm__pty_send` / `mcp__aiterm__pty_key`、roomの`read_unread` / `post`、
33
33
  Latticeの`todo`で行う。通常shellのために開いた短命PTYと、メンバーが着席する長寿命PTYは別物である。
34
34
 
@@ -49,7 +49,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
49
49
 
50
50
  手順は **聞き取り → script → 着任指示** の3段である。scripts が機械部分を全部持つので、AI が手で tmux を組み立てることはしない。
51
51
 
52
- 1. **聞き取り**: 対象プロジェクトのパス / **工程正本(`Lattice 併用`=既定 / `単独`)** / メンバー数とモデル・effort(モデル×effortの選定は dotagents `docs/02_models.md` の役割→順位表を正とし、席の主任務の役割(実装・局所コーディング・監査・反証・調査 等)で引く。**effort は既定値を置かない——席ごとに必ず決める**。`launch-seat.sh` effort は必須引数で、未指定は着席前に非ゼロ終了する。決定49改・2026-08-19: 旧「設計か確定実装か」の2値軸と既定Sonnetを廃止し、選定正本を02_models順位表へ移管)/ 初期タスク群(何を作るか)/ room 名(既定: プロジェクトのディレクトリ名)/ **公開URL基底**(Lattice 併用のみ。外部ペインに書く URL。クオ環境は `https://peertable.kitepon.dev`。未指定なら room サーバーの URL がそのまま入る=LAN URL は Lattice を外から見た時に開けない)
52
+ 1. **聞き取り**: 対象プロジェクトのパス / **工程正本(`Lattice 併用`=既定 / `単独`)** / メンバー数と**役割**(dotagents `docs/02_models.md` の役割名そのもの。未指定・未知は着席しない。model×effort `launch-seat.sh` が順位表1位から機械解決する——呼び出し側が model / vendor / effort を渡して正本を迂回しない。決定49改・決定91)/ 初期タスク群(何を作るか)/ room 名(既定: プロジェクトのディレクトリ名)/ **公開URL基底**(Lattice 併用のみ。外部ペインに書く URL。クオ環境は `https://peertable.kitepon.dev`。未指定なら room サーバーの URL がそのまま入る=LAN URL は Lattice を外から見た時に開けない)
53
53
  - **メンバー数の既定**: Lattice 併用なら plan compile 結果の幅(`max_frontier_width`)に合わせる(実測: 幅3→3人、第2 campaign で幅4→4人目追加)。frontier より多い席は最初から遊ぶ。単独モードには frontier が無いので既定の根拠も無く、聞き取りで決める
54
54
  - **運用中のworker席数の標準は「ready+activeな実装ToDo数」(決定68)**: 監査専任席はworker数、reclaim、scale-down候補へ含めず、最終試験結果を待って監査を担う。claimできるToDoが無いworker席は仕事を発明せず、最終手段として親だけへ待機DMする
55
55
  - **モードの選び分け**: タスク間に依存があり並列境界の機械保証が要るなら Lattice 併用。依存の無い小規模作業で、対象プロジェクトに Lattice を持ち込みたくないなら単独。単独で失うのは task 間スケジューリングの機械保証だけで、円卓の核(room・憲章・宣言による協力)は変わらない(決定47)
@@ -61,7 +61,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
61
61
  4. **Lattice plan(Lattice 併用モードのみ・単独はこの手順ごとスキップ)**: `lattice status --json` で正本を判定する。`uninitialized` なら聞き取ったタスクを JSON へ落として `scripts/make-plan-input.mjs <tasks.json> --project <project>` で `plan create` 入力を生成し、`lattice plan create --input .lattice/plan-create.json` を打つ。初期化済みなら `todo migrate` の作法(Lattice 正典)に従う。設計メモは各タスクに必ず書く
62
62
  - `make-plan-input.mjs` が digest 計算と `hard_dependencies` の `(from,to)` 昇順ソートを持つ(**手書きで2回踏んだ罠**。順序が崩れると `INPUT_INVALID / pointer:"/"` としか言われない)。`project_id` の既定は project ディレクトリ名で、`external-pane.mjs` が書く `project.json` の既定と一致させてある——**両者がずれると Lattice が identity 検証で落ちる**
63
63
  - 単独モードのタスク正本は手順3で生成した `.team/tasks.md` だけである。状態(誰が持っているか・何が終わったか)は持たせない——claim と完了は room の宣言だけが正(決定48 の延長)。ミニタスクトラッカーを別途作らない(決定36)
64
- 5. **メンバー起動**: メンバーごとに `env -u PEERTABLE_POST_TOKEN scripts/launch-seat.sh <project> <name> <model> <claude|codex|grok> <effort> [着任指示]` を実行する。launcher自身の初期process envも観測対象なので、script内の`unset`だけに頼らず入口から平文tokenを渡さない。**vendor・effort とも必須引数**(未指定は usage を出して非ゼロ終了。既定値をコードへ埋めない——席を立てる時に決める)。tmux 作成(aiterm と同じソケットなので、立った席はそのまま `pty_read`/`pty_send` で読める)→ credential file path注入 → 起動 → 既知ダイアログ通過 → 着席確認まで1回で行き、着席しなければ最後の画面を出して非ゼロで落ちる(黙って進まない)
64
+ 5. **メンバー起動**: メンバーごとに `env -u PEERTABLE_POST_TOKEN scripts/launch-seat.sh <project> <name> <role> [着任指示]` を実行する。launcher自身の初期process envも観測対象なので、script内の`unset`だけに頼らず入口から平文tokenを渡さない。**role は必須**(02_models の役割名。未指定・`worker` 既定・未知の名前は着席前に非ゼロ終了)。model / vendor / effort は順位表から解決する。tmux 作成(aiterm と同じソケットなので、立った席はそのまま `pty_read`/`pty_send` で読める)→ credential file path注入 → 起動 → 既知ダイアログ通過 → 着席確認まで1回で行き、着席しなければ最後の画面を出して非ゼロで落ちる(黙って進まない)
65
65
  - 起動前に `pty_list` で既存の `peer-*` 席を確認する(前の卓の残骸を99席実測したことがある)。同名の席は launch-seat.sh が落としてから立て直す
66
66
  - 着任指示を第6引数に渡すと着席後に送る。文面: 「あなたは「<日本語名>」。.team/roles/member.md を読んで着任し、作業ループを開始せよ。全タスク完了の宣言まで自律的に続けること。」
67
67
  - 席が読む env は script が組み立てる(`PEERTABLE_URL` / `PEERTABLE_ROOM` / `PEERTABLE_MEMBER` / `PEERTABLE_CREDENTIAL_FILE`、Lattice 併用なら `PEERTABLE_PLAN` と actor 3点)。token値は席別`0600` fileにだけ置き、pathだけを席へ渡す。**channels は `--mcp-config` の MCP server を解決しない**(実測 2026-08-08・Claude Code v2.1.226・決定44)ため、room の MCP 定義は setup.sh が project root へ置く `.mcp.json` が正。Peertable管理下fileはlaunch時にもcurrent-tree clientへ同期する。project に既存 `.mcp.json` があった場合は無断更新せず`SEAT_ROOM_MCP_STALE`で止まるので、AI がroom定義を手動mergeしてteardownで復元する
@@ -16,7 +16,7 @@ if (!session_id || (!model && !reasoning_effort)) {
16
16
  // Aiterm のmanaged session metadataは実行時state root(macOSではTMPDIR)にある。
17
17
  // stdio transportの既定環境は親のTMPDIRを落とすため、同じ公開serverへ明示継承する。
18
18
  const transport = new StdioClientTransport({ command: 'aiterm-mcp', env: process.env })
19
- const client = new Client({ name: 'peertable-seat-configure', version: '0.4.11' })
19
+ const client = new Client({ name: 'peertable-seat-configure', version: '0.4.12' })
20
20
  await client.connect(transport)
21
21
  const result = await client.callTool({
22
22
  name: 'agent_configure',
@@ -8,7 +8,7 @@ if (!session_name || !['claude', 'codex', 'grok'].includes(vendor) || !model ||
8
8
  throw new Error('SEAT_AITERM_LAUNCH_ARGS_INVALID')
9
9
  }
10
10
 
11
- const client = new Client({ name: 'peertable-seat-launch', version: '0.4.11' })
11
+ const client = new Client({ name: 'peertable-seat-launch', version: '0.4.12' })
12
12
  await client.connect(new StdioClientTransport({ command: 'aiterm-mcp', env: process.env }))
13
13
  const env_vars = [
14
14
  'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
@@ -59,9 +59,9 @@ name=sys.argv[1]
59
59
  member=next((m for m in json.load(sys.stdin).get("members",[]) if m.get("name")==name),None)
60
60
  if not member or member.get("vendor") not in ("claude","codex","grok") or not member.get("model"):
61
61
  raise SystemExit(1)
62
- print("\t".join((member["vendor"],member["model"],member.get("effort") or "",member.get("aiterm_session_id") or "")))
62
+ print("\t".join((member["vendor"],member["model"],member.get("effort") or "",member.get("aiterm_session_id") or "",member.get("role") or "")))
63
63
  ' "$name") || { echo "SEAT_CHANGE_MEMBER_METADATA_MISSING: ${name} のvendor/modelが要る" >&2; exit 1; }
64
- IFS=$'\t' read -r old_vendor old_model old_effort aiterm_session_id <<EOF
64
+ IFS=$'\t' read -r old_vendor old_model old_effort aiterm_session_id old_role <<EOF
65
65
  $meta
66
66
  EOF
67
67
 
@@ -202,11 +202,15 @@ else
202
202
  exit 1
203
203
  fi
204
204
  launch="$script_dir/launch-seat.sh"
205
+ [ -n "$old_role" ] || {
206
+ echo "SEAT_CHANGE_ROLE_MISSING: ${name} の role が無い(02_models の役割名が要る)" >&2
207
+ exit 1
208
+ }
205
209
  brief="席設定が変更され(${changes})、席を再起動しました。.team/roles/member.mdと工程正本・roomログから再着任し、進行中taskを続けてください。"
206
- if ! "$launch" "$proj" "$name" "$model" "$vendor" "$effort" "$brief"; then
210
+ if ! "$launch" "$proj" "$name" "$old_role" "$brief" --model "$model" --vendor "$vendor" --effort "$effort"; then
207
211
  echo "SEAT_CHANGE_RESTART_FAILED: ${changes}。旧設定(vendor=${old_vendor} / model=${old_model} / effort=${old_effort:-default})へrollbackする" >&2
208
212
  rollback_brief="席設定の変更に失敗して旧設定へrollbackしました。.team/roles/member.mdと工程正本・roomログから再着任してください。"
209
- if "$launch" "$proj" "$name" "$old_model" "$old_vendor" "$old_effort" "$rollback_brief"; then
213
+ if "$launch" "$proj" "$name" "$old_role" "$rollback_brief" --model "$old_model" --vendor "$old_vendor" --effort "$old_effort"; then
210
214
  echo "SEAT_CHANGE_ROLLED_BACK: ${name} は vendor=${old_vendor} / model=${old_model} / effort=${old_effort:-default} で再着席" >&2
211
215
  else
212
216
  echo "SEAT_CHANGE_ROLLBACK_FAILED: ${name} の席を手動で復旧する必要がある" >&2
@@ -1,25 +1,41 @@
1
1
  #!/bin/bash
2
2
  # 席を1つ立てる(tmux 作成 → env 注入 → エージェント起動 → 既知ダイアログ通過 → 着席確認)。
3
- # usage: launch-seat.sh <project_dir> <name> <model> <vendor> <effort> [brief] [role]
4
- # vendor: claude / codex / grok
5
- # effort: 必須。既定値をコードへ埋めない——席を立てる時に決める(オーナー裁定)
3
+ # usage: launch-seat.sh <project_dir> <name> <role> [brief] [--model <model> --vendor <vendor> --effort <effort>]
4
+ # role: 必須。dotagents docs/02_models.md の役割名(未指定・未知は着席前に拒否)
5
+ # model / vendor / effort: 省略時は 02_models 順位表から機械解決する。上書きするなら3つとも必須
6
6
  # brief: 着席が成立したら送る着任指示(省略時は送らない)
7
7
  #
8
8
  # room 名・server URL・モード・plan key は <project>/.team/setup-state.json から読む(setup.sh の後に呼ぶ)。
9
9
  # 書込トークンは helper が ~/.config/peertable.env から席別 0600 file へ移し、pathだけを席へ渡す。
10
10
  # tmux は aiterm-mcp と同じソケットへ作るので、立てた席はそのまま pty_read / pty_send で読める。
11
11
  set -e
12
- proj="$1"; name="$2"; model="$3"; vendor="$4"; effort="$5"; brief="$6"; role="${7:-worker}"
13
- [ -n "$proj" ] && [ -n "$name" ] && [ -n "$model" ] && [ -n "$vendor" ] && [ -n "$effort" ] || {
14
- echo "usage: launch-seat.sh <project_dir> <name> <model> <vendor> <effort> [brief]" >&2; exit 1;
12
+ usage() {
13
+ echo "usage: launch-seat.sh <project_dir> <name> <role> [brief] [--model <model> --vendor <vendor> --effort <effort>]" >&2
15
14
  }
16
- if [ "$vendor" = "claude" ]; then
17
- case "$effort" in
18
- low|medium|high|xhigh|max) ;;
19
- *) echo "unknown effort: ${effort}(claude は low|medium|high|xhigh|max)" >&2; exit 1 ;;
15
+ proj="$1"; name="$2"; role="$3"
16
+ [ -n "$proj" ] && [ -n "$name" ] && [ -n "$role" ] || { usage; exit 1; }
17
+ shift 3
18
+ brief=""
19
+ opt_model=""; opt_vendor=""; opt_effort=""
20
+ while [ $# -gt 0 ]; do
21
+ case "$1" in
22
+ --model) opt_model="${2:-}"; shift 2 ;;
23
+ --vendor) opt_vendor="${2:-}"; shift 2 ;;
24
+ --effort) opt_effort="${2:-}"; shift 2 ;;
25
+ --*) echo "SEAT_LAUNCH_ARGS_INVALID: 不明な引数 $1" >&2; usage; exit 1 ;;
26
+ *)
27
+ if [ -z "$brief" ]; then brief="$1"; shift
28
+ else echo "SEAT_LAUNCH_ARGS_INVALID: 余分な引数 $1" >&2; usage; exit 1
29
+ fi
30
+ ;;
20
31
  esac
32
+ done
33
+ if [ -n "$opt_model$opt_vendor$opt_effort" ]; then
34
+ [ -n "$opt_model" ] && [ -n "$opt_vendor" ] && [ -n "$opt_effort" ] || {
35
+ echo "SEAT_LAUNCH_PLACEMENT_INCOMPLETE: --model / --vendor / --effort は3つ揃える" >&2
36
+ exit 1
37
+ }
21
38
  fi
22
- case "$role" in worker|auditor) ;; *) echo "unknown seat role: ${role}(worker / auditor)" >&2; exit 1 ;; esac
23
39
 
24
40
  if [ -n "${PEERTABLE_MEMBER:-}" ]; then
25
41
  echo "SEAT_LAUNCH_DELEGATED_CHILD_FORBIDDEN: PEERTABLE_MEMBER=${PEERTABLE_MEMBER} を継承した呼出元からの席起動を拒否" >&2
@@ -33,6 +49,7 @@ credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$(dirname "$0")/seat-credentia
33
49
  room_mcp_helper="${PEERTABLE_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-room-mcp.mjs}"
34
50
  codex_room_mcp_helper="${PEERTABLE_CODEX_ROOM_MCP_HELPER:-$(dirname "$0")/ensure-codex-room-mcp.mjs}"
35
51
  aiterm_launch_helper="${PEERTABLE_AITERM_LAUNCH_HELPER:-$(dirname "$0")/aiterm-launch.mjs}"
52
+ placement_helper="${PEERTABLE_PLACEMENT_HELPER:-$(dirname "$0")/resolve-seat-placement.mjs}"
36
53
  # shellcheck disable=SC1091
37
54
  . "$(dirname "$0")/tmux-at.bash"
38
55
  peertable_repo=$(cd "$(dirname "$0")/../.." && pwd -P)
@@ -41,6 +58,32 @@ credential_file=""
41
58
  credential_persist=false
42
59
  aiterm_session_id=""
43
60
 
61
+ if [ -n "$opt_model" ]; then
62
+ model="$opt_model"; vendor="$opt_vendor"; effort="$opt_effort"
63
+ echo "SEAT_PLACEMENT_OVERRIDE: ${role} → ${vendor} / ${model} / ${effort}"
64
+ else
65
+ placement=$(node "$placement_helper" "$role") || exit "$?"
66
+ IFS=$'\t' read -r model vendor effort rank <<EOF
67
+ $(python3 -c 'import json,sys;p=json.load(sys.stdin);print("\t".join((p["model"],p["vendor"],p.get("effort") or "",str(p["rank"]))))' <<<"$placement")
68
+ EOF
69
+ echo "SEAT_PLACEMENT: ${role} → ${vendor} / ${model}${effort:+ / $effort}(02_models ${rank}位)"
70
+ python3 -c 'import json,sys
71
+ p=json.load(sys.stdin)
72
+ for item in p.get("dropped") or []:
73
+ print("SEAT_PLACEMENT_DROPPED: rank %s %s %s" % (item.get("rank"), item.get("reason"), item.get("cell") or ""), file=sys.stderr)
74
+ ' <<<"$placement" || true
75
+ fi
76
+ case "$vendor" in
77
+ claude|codex|grok) ;;
78
+ *) echo "SEAT_LAUNCH_VENDOR_UNSUPPORTED: vendor=${vendor}" >&2; exit 1 ;;
79
+ esac
80
+ if [ "$vendor" = "claude" ] && [ -n "$effort" ]; then
81
+ case "$effort" in
82
+ low|medium|high|xhigh|max) ;;
83
+ *) echo "unknown effort: ${effort}(claude は low|medium|high|xhigh|max)" >&2; exit 1 ;;
84
+ esac
85
+ fi
86
+
44
87
  # brief は tmux のコマンド引数へ直接載せない。長さを着席前に検証してから一時 file へ置き、
45
88
  # tmux の buffer 経由で貼る。入力を受理できない時は model preflight・tmux・member 登録を
46
89
  # 一つも行わず、何が拒否されたかを機械的に読める形で返す。
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ // 02_models.md の役割→1位〜3位から、着席可能な vendor / model / effort を解決する。
3
+ // 具体モデル名はここに持たない。台帳と順位表をその場で読む。
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import { dirname, join, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const here = dirname(fileURLToPath(import.meta.url))
9
+ const EFFORT = /×\s*(none|low|medium|high|xhigh|max|ultra)/iu
10
+ const vendorOf = (modelName) => {
11
+ const name = String(modelName).trim()
12
+ if (/^claude\b/iu.test(name)) return 'claude'
13
+ if (/^gpt\b|^gpt-/iu.test(name)) return 'codex'
14
+ if (/^grok\b/iu.test(name)) return 'grok'
15
+ return null
16
+ }
17
+
18
+ const fail = (code, message) => {
19
+ process.stderr.write(`${code}: ${message}\n`)
20
+ process.exit(2)
21
+ }
22
+
23
+ export function findModelsDoc({ env = process.env, exists = existsSync, scriptDir = here } = {}) {
24
+ if (env.PEERTABLE_MODELS_DOC) return resolve(env.PEERTABLE_MODELS_DOC)
25
+ if (env.DOTAGENTS_ROOT) return join(resolve(env.DOTAGENTS_ROOT), 'docs/02_models.md')
26
+ const sibling = resolve(scriptDir, '../../../dotagents/docs/02_models.md')
27
+ if (exists(sibling)) return sibling
28
+ return null
29
+ }
30
+
31
+ const stripCell = (cell) => String(cell ?? '').replace(/\*\*/g, '').trim()
32
+
33
+ const parseMarkdownTable = (block) => {
34
+ const lines = block.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('|'))
35
+ if (lines.length < 3) return []
36
+ const headers = lines[0].split('|').slice(1, -1).map((cell) => stripCell(cell))
37
+ return lines.slice(2).map((line) => {
38
+ const cells = line.split('|').slice(1, -1).map((cell) => stripCell(cell))
39
+ return Object.fromEntries(headers.map((header, index) => [header, cells[index] ?? '']))
40
+ })
41
+ }
42
+
43
+ const section = (markdown, heading) => {
44
+ const start = markdown.indexOf(`## ${heading}`)
45
+ if (start < 0) return ''
46
+ const rest = markdown.slice(start)
47
+ const next = rest.search(/\n## /)
48
+ return next < 0 ? rest : rest.slice(0, next)
49
+ }
50
+
51
+ const slugOf = (cell) => {
52
+ const alias = cell.match(/alias\s+`([^`]+)`/u)
53
+ if (alias) return alias[1]
54
+ const slug = cell.match(/`([^`]+)`/u)
55
+ return slug ? slug[1] : null
56
+ }
57
+
58
+ export function parseLedger(markdown) {
59
+ const rows = parseMarkdownTable(section(markdown, 'モデル台帳'))
60
+ return rows.flatMap((row) => {
61
+ const name = row['モデル']
62
+ const slug = slugOf(row['slug'] ?? '')
63
+ const vendor = vendorOf(name)
64
+ if (!name || !slug || !vendor) return []
65
+ return [{ name, slug, vendor }]
66
+ })
67
+ }
68
+
69
+ const parseCell = (cell) => {
70
+ const raw = stripCell(cell)
71
+ if (!raw || raw === '—' || raw === '-') return null
72
+ if (raw.includes('オーナー指定')) return { skip: 'owner-pin', cell: raw }
73
+ if (/^ChatGPT/iu.test(raw) || raw.includes('gpt-connector')) return { skip: 'not-a-seat', cell: raw }
74
+ const effortMatch = raw.match(EFFORT)
75
+ const modelKey = raw.split('×')[0]
76
+ .replace(/([^)]*)/gu, '')
77
+ .replace(/\([^)]*\)/gu, '')
78
+ .trim()
79
+ if (!modelKey) return { skip: 'unparseable', cell: raw }
80
+ return {
81
+ modelKey,
82
+ effort: effortMatch ? effortMatch[1].toLowerCase() : null,
83
+ cell: raw,
84
+ }
85
+ }
86
+
87
+ const matchLedger = (modelKey, ledger) => {
88
+ const key = modelKey.toLowerCase()
89
+ const exact = ledger.find((row) => row.name.toLowerCase() === key)
90
+ if (exact) return exact
91
+ const contained = ledger.filter((row) => row.name.toLowerCase().includes(key) || key.includes(row.name.toLowerCase()))
92
+ if (contained.length === 1) return contained[0]
93
+ const token = ledger.filter((row) => row.name.toLowerCase().split(/\s+/u).includes(key))
94
+ if (token.length === 1) return token[0]
95
+ return null
96
+ }
97
+
98
+ export function resolveSeatPlacement(role, markdown, { source = '' } = {}) {
99
+ const wanted = String(role ?? '').trim()
100
+ if (!wanted) {
101
+ return { error: 'SEAT_ROLE_REQUIRED', message: 'role が空(02_models の役割名が要る)' }
102
+ }
103
+ const ranks = parseMarkdownTable(section(markdown, '順位表(役割→1位〜3位)'))
104
+ const row = ranks.find((item) => item['役割'] === wanted)
105
+ if (!row) {
106
+ const known = ranks.map((item) => item['役割']).filter(Boolean)
107
+ return { error: 'SEAT_ROLE_UNKNOWN', message: `未知の役割: ${wanted}(${known.join(' / ')})` }
108
+ }
109
+ const ledger = parseLedger(markdown)
110
+ const dropped = []
111
+ for (const rank of [1, 2, 3]) {
112
+ const parsed = parseCell(row[`${rank}位`])
113
+ if (!parsed) continue
114
+ if (parsed.skip) {
115
+ dropped.push({ rank, reason: parsed.skip, cell: parsed.cell })
116
+ continue
117
+ }
118
+ const hit = matchLedger(parsed.modelKey, ledger)
119
+ if (!hit) {
120
+ dropped.push({ rank, reason: 'not-in-ledger', cell: parsed.cell })
121
+ continue
122
+ }
123
+ if (hit.slug === 'haiku') {
124
+ return { role: wanted, rank, vendor: hit.vendor, model: hit.slug, effort: '', source, dropped }
125
+ }
126
+ if (!parsed.effort) {
127
+ dropped.push({ rank, reason: 'effort-missing', cell: parsed.cell })
128
+ continue
129
+ }
130
+ return {
131
+ role: wanted,
132
+ rank,
133
+ vendor: hit.vendor,
134
+ model: hit.slug,
135
+ effort: parsed.effort,
136
+ source,
137
+ dropped,
138
+ }
139
+ }
140
+ return {
141
+ error: 'SEAT_PLACEMENT_UNRESOLVABLE',
142
+ message: `${wanted} を着席可能な vendor/model/effort へ解決できない`,
143
+ dropped,
144
+ }
145
+ }
146
+
147
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
148
+ if (isMain) {
149
+ const role = process.argv[2]
150
+ const doc = findModelsDoc()
151
+ if (!doc) {
152
+ fail('SEAT_MODELS_DOC_MISSING', '02_models.md が見つからない(PEERTABLE_MODELS_DOC または DOTAGENTS_ROOT を渡す)')
153
+ }
154
+ if (!existsSync(doc)) {
155
+ fail('SEAT_MODELS_DOC_MISSING', `02_models.md が無い: ${doc}`)
156
+ }
157
+ const result = resolveSeatPlacement(role, readFileSync(doc, 'utf8'), { source: doc })
158
+ if (result.error) fail(result.error, result.message)
159
+ process.stdout.write(`${JSON.stringify(result)}\n`)
160
+ }
@@ -3,7 +3,7 @@
3
3
  これはチーム作業である。自分が引き受けたタスクの完了はミッションの完了ではない。全タスク完了までチームは解散しない。
4
4
 
5
5
  1. 拘束力を持つのは**工程正本への記録**と room ログに残った決定だけ(工程正本=Lattice 併用モードなら Lattice の todo 記録、単独円卓モードなら room の宣言そのもの)。room の `to: "all"` は部屋全体への通常発言、名前指定はDMである
6
- 2. room の宛先規則はこれだけである。claim は `post(to: "all")`、ターン終了時の次の行動は `post(to: "<自分の名前>")`、工程完了は `post(to: "all")`、誰かへの用事は `post(to: "<相手の名前>")` で送る。誰に聞けばよいか分からないことは `post(to: "all")` で聞く。実装・監査・readyが本当に無い時の待機宣言は最終手段として親だけへDMし、`all`へ送らない。別の通知機構は使わない
6
+ 2. room の宛先規則はこれだけである。claim は `post(to: "all")`、ターン終了時の次の行動は `post(to: "<自分の名前>")`(message `[次の行動]` で始める。出さないと席は止まる)、工程完了は `post(to: "all")`、誰かへの用事は `post(to: "<相手の名前>")` で送る。誰に聞けばよいか分からないことは `post(to: "all")` で聞く。実装・監査・readyが本当に無い時の待機宣言は最終手段として親だけへDMし、`all`へ送らない。別の通知機構は使わない
7
7
  3. **仕事を選ぶのも始めるのも自分である。** どのモードでも、`read_log`で先行claimを確認してから `post(to: "all", message: "[claim] <タスク>")` を送る。これが唯一の着手通知であり、別の着手通知は送らない。**装置から仕事が降ってくることはないし、着手前に装置の許可を待つこともない**(オーナー裁定 2026-08-09)。Lattice の実行層を使う卓では、その後 `todo start` し、自分で `run intake` して隔離 worktree を受け取る——それは設備の供給であって許可証ではなく、装置が返すのは競合した時の「留まれ」だけである(タスクの呼び名は Lattice 併用モードなら task_id、単独円卓モードなら `.team/tasks.md` の議題名)。同じタスクへの先行 claim があれば取り下げるか `[join] <タスク>` へ切り替える
8
8
  4. 分からないことは room で聞く。台帳はない。自分の変更が他の部位に影響するなら、聞かれる前に影響を受けるメンバーを明示宛先にして通知する
9
9
  5. 判断は情報を持つ者がする。タスクは席ではなく現場。合流(join)は歓迎される。詰まった仲間には目を貸す。親が円卓メンバーを増やす時は、正式な`launch-seat.sh`でAiterm長寿命席として着任させ、native sub-agent・Task・Agentを円卓席の代用にしない。正式着席したメンバーはnative sub-agent、Aiterm外部agent、相談agent、自己実装を自由に選べる。子は自動的に円卓メンバーではなく、工程所有・統合・room報告は着席メンバーが保持する。親は二次委譲の手段を禁止・指定しない。視点が足りない・詰んでいるなら room で報告して援軍(join)を求める
@@ -12,6 +12,8 @@
12
12
 
13
13
  ## 作業ループ
14
14
 
15
+ **ターンを終える直前に `post(to: "<自分の名前>", message: "[次の行動] ...")` を1回送れ。** wakeup-bridge はこの自己DMで次ターンを起こす。出さないと席は止まる。空の終了通知は使うな。
16
+
15
17
  **探索順は active → ready → 待機である。** まず自分の active 議題を完了させる。無ければ議題を選ぶ。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。ターンを終える時は、次に行う作業または再確認条件を自分へDMする。
16
18
 
17
19
  1. `.team/tasks.md` で議題を見る。`read_log` で既存の claim と完了報告を照合し、まだ誰も持っていないものを選ぶ
@@ -12,6 +12,8 @@
12
12
 
13
13
  ## 作業ループ
14
14
 
15
+ **ターンを終える直前に `post(to: "<自分の名前>", message: "[次の行動] ...")` を1回送れ。** wakeup-bridge はこの自己DMで次ターンを起こす。出さないと席は止まる。空の終了通知は使うな。
16
+
15
17
  **作業を選ぶのも始めるのもあなたである。** 装置から仕事が降ってくることはないし、着手前に装置の許可を待つこともない(オーナー裁定 2026-08-09・改・裁定1)。Lattice が居る卓では、着手した後に装置が競合を見て介入してくることがある——それは次の「装置が介入してきた時」に従う。
16
18
 
17
19
  **探索順は active → ready → 待機である。** まず自分の active 工程を完了させる。無ければ全PLAN横断の ready を自律的に claim する。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。ターンを終える時は、次に行う作業または再確認条件を `post(to: "<自分の名前>", message: "[次の行動] ...")` で自分へDMし、空のturn終了通知は使わない。