peertable 0.4.16 → 0.4.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
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
@@ -9,10 +9,11 @@ import { execFileSync } from 'node:child_process'
9
9
  import { dirname, join } from 'node:path'
10
10
  import { fileURLToPath } from 'node:url'
11
11
  import { isIdleSelfWake } from '../skill/scripts/wakeup-delivery.mjs'
12
+ import { findModelsDoc, resolveSeatIdentity } from '../skill/scripts/resolve-seat-placement.mjs'
12
13
 
13
14
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
14
15
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
15
- const MCP_VERSION = '0.4.16'
16
+ const MCP_VERSION = '0.4.18'
16
17
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
17
18
 
18
19
  const USAGE = `usage:
@@ -69,6 +70,7 @@ const mcp = new Server(
69
70
  capabilities: { experimental: { 'claude/channel': {} }, tools: {} },
70
71
  instructions:
71
72
  `あなたは Peertable room「${ROOM}」のメンバー「${ME}」である。` +
73
+ 'メンバー一覧の名前・役割・設定・使命を見て連携せよ。members と read_unread 先頭に同じ欄がある。' +
72
74
  '<channel source="room"> の通知は「新着あり」の合図であり、本文は read_unread ツールで読む。' +
73
75
  '発言は post ツールを使う。to: "all" はroom全体、メンバー名はDM、配列は複数人宛である。' +
74
76
  `次にやる仕事があるターンを終える直前に post(to: "${ME}", message: "[次の行動] ...") を1回送れ。` +
@@ -103,11 +105,26 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
103
105
  },
104
106
  { name: 'read_unread', description: 'room全体宛と自分宛の未読メッセージを読む。読んだ位置は記憶される', inputSchema: { type: 'object', properties: {} } },
105
107
  { name: 'read_log', description: 'room ログの直近 count 件を読む(既定 50。全宛先を含む)', inputSchema: { type: 'object', properties: { count: { type: 'number' } } } },
106
- { name: 'members', description: 'room に居るメンバーの一覧', inputSchema: { type: 'object', properties: {} } },
108
+ { name: 'members', description: 'room に居るメンバーの一覧(名前・役割・設定・使命)', inputSchema: { type: 'object', properties: {} } },
107
109
  ],
108
110
  }))
109
111
 
110
112
  const fmt = m => `[${m.seq}] ${m.from} → ${Array.isArray(m.to_names) ? m.to_names.join(', ') : m.to} (${m.ts}): ${m.body}`
113
+ const memberLine = (m) => {
114
+ const rolesText = Array.isArray(m.roles) && m.roles.length ? m.roles.join('・') : (m.role || '')
115
+ const settingsObj = m.settings && typeof m.settings === 'object' ? m.settings : {}
116
+ const settingsText = [settingsObj.model || m.model, settingsObj.effort || m.effort].filter(Boolean).join('×')
117
+ const parts = [m.name]
118
+ if (rolesText) parts.push(`役割=${rolesText}`)
119
+ if (settingsText) parts.push(`設定=${settingsText}`)
120
+ if (m.mission) parts.push(`使命=${m.mission}`)
121
+ return parts.join(' ')
122
+ }
123
+ const rosterText = async () => {
124
+ const { members } = await (await fetch(api('members'))).json()
125
+ if (!members?.length) return '席: (まだ誰も居ない)'
126
+ return `席: ${members.map(memberLine).join(' / ')}`
127
+ }
111
128
 
112
129
  mcp.setRequestHandler(CallToolRequestSchema, async req => {
113
130
  const args = req.params.arguments ?? {}
@@ -124,7 +141,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
124
141
  const { messages } = await (await fetch(api(`messages?since=${cursor}`))).json()
125
142
  if (messages.length) cursor = messages[messages.length - 1].seq
126
143
  const mine = messages.filter(relevant)
127
- return text(mine.length ? mine.map(fmt).join('\n') : '未読なし')
144
+ const roster = await rosterText()
145
+ return text(mine.length ? `${roster}\n${mine.map(fmt).join('\n')}` : `${roster}\n未読なし`)
128
146
  }
129
147
  case 'read_log': {
130
148
  const { messages } = await (await fetch(api('messages'))).json()
@@ -132,7 +150,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
132
150
  }
133
151
  case 'members': {
134
152
  const { members } = await (await fetch(api('members'))).json()
135
- return text(members.map(m => `${m.name}(参加 ${m.joined_at})`).join('\n') || '(誰も居ない)')
153
+ return text(members.map(memberLine).join('\n') || '(誰も居ない)')
136
154
  }
137
155
  default:
138
156
  throw new Error(`unknown tool: ${req.params.name}`)
@@ -177,14 +195,38 @@ function observeSelf() {
177
195
  }
178
196
 
179
197
  const observe = observeSelf()
198
+ const parseRoles = (raw) => String(raw ?? '').split(/[,、]/u).map((item) => item.trim()).filter(Boolean)
199
+ const roles = parseRoles(process.env.PEERTABLE_ROLES || process.env.PEERTABLE_ROLE)
200
+ const settings = Object.fromEntries(Object.entries({
201
+ vendor: process.env.PEERTABLE_VENDOR,
202
+ model: process.env.PEERTABLE_MODEL,
203
+ effort: process.env.PEERTABLE_EFFORT,
204
+ }).filter(([, v]) => v))
180
205
  const IDENTITY = Object.fromEntries(Object.entries({
181
206
  vendor: process.env.PEERTABLE_VENDOR,
182
207
  model: process.env.PEERTABLE_MODEL,
183
208
  effort: process.env.PEERTABLE_EFFORT,
184
- role: process.env.PEERTABLE_ROLE,
209
+ role: roles[0],
210
+ roles,
211
+ settings: Object.keys(settings).length ? settings : undefined,
212
+ mission: process.env.PEERTABLE_MISSION,
185
213
  aiterm_session_id: process.env.AITERM_SESSION_ID,
186
214
  observe,
187
- }).filter(([, v]) => v))
215
+ }).filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)))
216
+ {
217
+ const modelsDoc = findModelsDoc()
218
+ if (!modelsDoc || !existsSync(modelsDoc)) {
219
+ throw new Error('SEAT_MODELS_DOC_MISSING: 02_models.md が見つからない')
220
+ }
221
+ const checked = resolveSeatIdentity({
222
+ roles,
223
+ model: process.env.PEERTABLE_MODEL,
224
+ effort: process.env.PEERTABLE_EFFORT,
225
+ vendor: process.env.PEERTABLE_VENDOR,
226
+ markdown: readFileSync(modelsDoc, 'utf8'),
227
+ })
228
+ if (checked.error) throw new Error(`${checked.error}: ${checked.message}`)
229
+ }
188
230
  await fetch(api('members'), { method: 'POST', headers, body: JSON.stringify({ name: ME, ...IDENTITY }) })
189
231
 
190
232
  // SSE 購読 → 自分に関係する新着だけ一行通知へ変換。切断は外部障害なので再接続する。
package/room/server.mjs CHANGED
@@ -100,7 +100,7 @@ function normalizeAudience(to, toNames) {
100
100
  }
101
101
 
102
102
  // SSE の member イベントで押し込む欄。閲覧者が気づく欄だけに絞る(POST /members 参照)
103
- const MEMBER_EVENT_FIELDS = ['status', 'busy_since', 'vendor', 'model', 'effort', 'role']
103
+ const MEMBER_EVENT_FIELDS = ['status', 'busy_since', 'vendor', 'model', 'effort', 'role', 'roles', 'settings', 'mission']
104
104
 
105
105
  // room ログへは書かない・system 発言も出さない稼働状態の push。post() とは別の経路
106
106
  function emitMember(room, name, meta) {
@@ -267,8 +267,10 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
267
267
  .metapop{position:fixed;z-index:20;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-size:12px;box-shadow:0 6px 20px rgba(0,0,0,.18);max-width:70vw}
268
268
  .metapop .metaname{font-weight:600;margin-bottom:2px}
269
269
  .metapop .metaline{color:var(--dim)}
270
- .chip{position:relative;flex:none;display:flex;align-items:center;gap:7px;padding:3px 11px 3px 3px;border:1px solid var(--line);border-radius:999px;background:var(--surface);font-size:12px;font-weight:600}
271
- .chip .av{width:22px;height:22px;font-size:11px}
270
+ .chip{position:relative;flex:none;display:flex;align-items:flex-start;gap:7px;padding:5px 11px 5px 5px;border:1px solid var(--line);border-radius:12px;background:var(--surface);font-size:12px;font-weight:600}
271
+ .chip .av{width:22px;height:22px;font-size:11px;flex:none}
272
+ .chip .id{display:flex;flex-direction:column;gap:1px;min-width:0;line-height:1.25}
273
+ .chip .roles,.chip .settings,.chip .mission{font-weight:500;font-size:11px;color:var(--dim)}
272
274
  .chip.recent{border-color:hsl(var(--h) var(--sat) var(--edge))}
273
275
  .chip.recent .nm{color:hsl(var(--h) var(--sat) var(--name))}
274
276
  .chip.pulse .av{animation:pulse 1.6s ease-out 2}
@@ -462,10 +464,10 @@ async function refreshMembers(){
462
464
  const c=el('span','chip'+(m.name===recent?' recent':''))
463
465
  const color=avatarColor(m.name)
464
466
  c.style.setProperty('--h',color.h);c.style.setProperty('--av-bg',color.bg);c.style.setProperty('--av-fg',color.fg);c.dataset.name=m.name
465
- // 素性は任意欄。名乗っていない席は行ごと出ない(空欄を「不明」として見せない)。
466
- // vendor/model/effort/role を1行へ畳む(着席ログと読み口を揃える。effort / role の語は出さない)
467
- const idParts=[m.vendor,m.model,m.effort,m.role].filter(Boolean)
468
- const meta=[idParts.length?idParts.join(' / '):null].filter(Boolean)
467
+ const rolesText=Array.isArray(m.roles)&&m.roles.length?m.roles.join('・'):(m.role||'')
468
+ const settingsObj=m.settings&&typeof m.settings==='object'?m.settings:{}
469
+ const settingsText=[settingsObj.model||m.model,settingsObj.effort||m.effort].filter(Boolean).join('×')
470
+ const meta=[rolesText,settingsText,m.mission].filter(Boolean)
469
471
  // 稼働状態は**報告が新しい時だけ**採る。途絶えたら unknown へ落とす——古い状態を出し続けるのが
470
472
  // いちばん悪い(動いていない席を「動いている」と見せる)。閾値は bridge の心拍30秒の3倍
471
473
  const age=m.status_at?Date.now()-Date.parse(m.status_at):Infinity
@@ -478,8 +480,15 @@ async function refreshMembers(){
478
480
  if(Number.isSafeInteger(m.pane_token_hint)&&m.pane_token_hint>=0)usage.push(compactCount(m.pane_token_hint)+' tokens')
479
481
  if(usage.length)meta.push('消費目安 '+usage.join(' / ')+'(pane観測)')
480
482
  c.title=m.name+'(参加 '+new Date(m.joined_at).toLocaleString()+')'+(meta.length?'\\n'+meta.join('\\n'):'')
481
- c.appendChild(el('span','av',initial(m.name)));c.appendChild(el('span','nm',m.name))
482
- if(st)c.appendChild(el('span','st '+st))
483
+ c.appendChild(el('span','av',initial(m.name)))
484
+ const id=el('span','id')
485
+ const nameRow=el('span','nm',m.name)
486
+ if(st)nameRow.appendChild(el('span','st '+st))
487
+ id.appendChild(nameRow)
488
+ if(rolesText)id.appendChild(el('span','roles',rolesText))
489
+ if(settingsText)id.appendChild(el('span','settings',settingsText))
490
+ if(m.mission)id.appendChild(el('span','mission',m.mission))
491
+ c.appendChild(id)
483
492
  // タップ環境には hover が無いので、押した時に同じ内容を出す(ホバーは title が担う)
484
493
  if(meta.length){c.classList.add('has-meta');c.addEventListener('click',ev=>{ev.stopPropagation();showMeta(c,m,meta)})}
485
494
  membersEl.appendChild(c)
@@ -0,0 +1,103 @@
1
+ # 02_models — 役割→モデル×エフォート順位表(唯一の参照点)
2
+
3
+ <!-- 前提: 2026-08-19 更新(Claude 5 / GPT-5.6 / Grok 4.6 世代)。バージョン固定禁止(PLAN 原則9)。モデル名をこの表以外+公認例外(codex/agents/*.toml・.codex-sidecar.yml)に書き散らさない -->
4
+
5
+ 方針: skill・agents・委譲契約・スクリプトは**役割名**でモデルを指し、具体名への解決はこの表だけが担う。本書は役割を先に定義し、役割ごとに適したモデル×effortを1位〜3位で与える。ベンダー別レーンの表構造は撤廃した(オーナー裁定 2026-08-19)。世代交代時は**この1枚+公認例外2種を更新して push すれば全端末が追従**する。更新トリガーはオーナーの宣言(PLAN 原則6)。
6
+
7
+ 背骨: **役割が要求する能力で選び、順位は実測で昇降格する。ただし、現状維持に候補側だけの立証責任を負わせない。** 新しい有力候補は代表実務へ期限・範囲を切って投入し、成功率・手戻り・監査工数・総token・所要時間・quotaで現役と比較する。未検証の現役を「安全」、未検証の新顔を「危険」とは扱わない。
8
+
9
+ 本書の根拠は次の4種を混ぜない。
10
+
11
+ 1. **一次事実**: vendor公式の価格、context、対応effort、live catalog。
12
+ 2. **外部観測**: benchmark、Xの利用報告、独立評価。harness・標本数・再現条件を併記する。
13
+ 3. **dotagents実測**: 当工場のtask、監査通過率、配線実測。一般性能へ拡張しない。
14
+ 4. **運用判断**: 上記を踏まえた配置。客観的に確定した事実を「オーナー裁定」と呼ばない。
15
+
16
+ **親のモデル×effortはオーナーの領分**であり、規範・AIはピンを打ち替えない。親候補と根拠は提示し、子の配置はこの表で解決する。
17
+
18
+ ## 役割の定義(9つ)
19
+
20
+ 1. **統括** — campaign/会話の主体。裁定・契約クリティカルな受入・commit
21
+ 2. **反証** — 実ファイルを読んで主張を殺しにかかる
22
+ 3. **監査・発見** — 自成果物(repo・設計・文書)の欠陥出し。誤検知は後段が裁く(第三者reviewを含む)
23
+ 4. **設計** — 実装前の構造・境界・停止判断の案出し
24
+ 5. **相談** — 実読不要の純推論second opinion
25
+ 6. **実装** — 仕様固定済みのまとまった実装・テスト・移設
26
+ 7. **局所コーディング** — focused test付きの狭い修正・実装
27
+ 8. **軽作業** — 分類・抽出・字面回収
28
+ 9. **調査** — 外部事実の回収〜統合(浅↔深はeffortで刻む)
29
+
30
+ ## 順位表(役割→1位〜3位)
31
+
32
+ 「実測」= dotagents 2026-08-19 役割配置実験([実験記録](../rag/models/role-placement-experiment-20260819.md)、各セルn=1)。
33
+
34
+ | 役割 | 1位 | 2位 | 3位 |
35
+ |---|---|---|---|
36
+ | 統括 | **オーナー指定**(参考候補: Opus 5×high、Grok 4.6×high、Fable 5×highスポット。順位は参考情報でpinはオーナー) | — | — |
37
+ | 反証 | Grok 4.6×high(実測14/14) | Sol×high(実測14/14。同着はGrok優先=オーナー裁定 2026-08-19) | Opus 5×high(実測13/14。唯一の誤りは真だが無害な指摘を殺した方向) |
38
+ | 監査・発見 | Grok 4.6×medium(実測recall 7/10・FP 0) | Sonnet 5×medium(実測6/10+partial 1・FP 0) | Terra×medium(実測6/10・FP 0) |
39
+ | 設計 | Opus 5×high(GDPval-AA・APEX首位 2026-08-19時点) | Grok 4.6×high(独立視点) | Sol×medium(過剰設計報告に注意) |
40
+ | 相談 | ChatGPT(gpt-connector・別quota。[06_gpt-connector.md](06_gpt-connector.md)) | Grok 4.6×medium | Fable 5×highスポット(契約critical) |
41
+ | 実装 | Terra×high(Terminal-Bench 2.1 78.4%) | Sonnet 5×medium〜high | Grok 4.6×medium(repo横断・長時間はhigh) |
42
+ | 局所コーディング | Luna×medium(実測: maxと品質差なし・14/14) | Sonnet 5×medium | Grok 4.6×medium |
43
+ | 軽作業 | Luna×low〜medium | Haiku(Claude枠で閉じる時。effortなし) | Grok 4.6×low |
44
+ | 調査 | Grok 4.6×low(字面)〜medium(統合)。X直結は唯一 | Sonnet 5×medium+Web | Sol/Terraで独立確認 |
45
+
46
+ ### 構造規則(3つ)
47
+
48
+ 1. **反証の1位は、成果を作ったモデルと別ベンダーから選ぶ。**
49
+ 2. **順位は既定であって拘束ではない。** quota逼迫・入口障害・catalog不在時は次順位へ落とし、落とした事実を報告する。存在しないmodel/effortへはfallbackせず明示エラーにする。
50
+ 3. **見逃し対策は2段構造で行う。** finderの増席でなく「疑いを列挙→反証役に裁かせる」工程を挟む(実測: finder 3席が全員見逃した欠陥3件を反証工程が回収した)。
51
+
52
+ ## モデル台帳(slug・価格の解決はここだけ)
53
+
54
+ | モデル | slug | API定価(入力/出力 per Mtok) | context | effort段階 |
55
+ |---|---|---|---|---|
56
+ | Claude Fable 5 | alias `fable` | $10/$50 | 1M | low〜xhigh/max |
57
+ | Claude Opus 5 | alias `opus` | $5/$25 | 1M | low〜xhigh/max |
58
+ | Claude Sonnet 5 | alias `sonnet` | **$2/$10(恒久価格)** | 1M | low〜xhigh/max |
59
+ | Claude Haiku 4.5 | alias `haiku` | $1/$5 | 200K | **effortなし** |
60
+ | GPT-5.6 Sol | `gpt-5.6-sol` | $5/$30(長contextは$10/$45) | 1.05M | none〜max(Codex CLIはultraあり) |
61
+ | GPT-5.6 Terra | `gpt-5.6-terra` | $2/$12(長contextは$4/$18) | 1.05M | 同上 |
62
+ | GPT-5.6 Luna | `gpt-5.6-luna` | $0.20/$1.20(長contextは$0.40/$1.80) | 1.05M | none〜max(ultraなし) |
63
+ | Grok 4.6 | `grok-4.6` | $2/$6(200K超は$4/$12) | 500K | low/medium/high(既定)/xhigh |
64
+
65
+ 価格は標準API定価であり、Claude Code・Codex・Grok Buildのsubscription quotaとは別物。OpenAIの長context課金は階層制(境界と現行値は公式pricingを実行時に確認)。Grok Buildのcatalogに`grok-build-0.1`(Composer 2.5系)の記載が公式docsにあるが、端末live catalogでの実在は未検証——Composer入口はcatalog確認まで`unsupported`のまま、Grokへfallbackしない。根拠は [GPT-5.6](../rag/models/gpt-5.6-family.md)、[Claude 5](../rag/models/claude-5-family.md)、[Grok 4.6](../rag/models/xai-grok46.md)。
66
+
67
+ ### 消費枠(quotaの独立勘定。規則2のfallback判断に使う)
68
+
69
+ Anthropic(Claude Code本体・Agent/Workflow)/OpenAI Codex(Codex CLI・codex-sidecar・aiterm codex_agent)/OpenAI ChatGPT(gpt-connector・API fallback禁止)/xAI(Grok Build・aiterm grok_agent)の4枠は別勘定。同役割の次順位が別枠なら、quota逼迫時のfallbackはコスト増でなく枠の移動になる。
70
+
71
+ ## 入口と使い分け
72
+
73
+ - **Codex親の三入口を分ける**: ① native subagent=repo密結合、② external execution=codex-sidecar/aiterm、③ consultation=gpt-connector。Grok/ComposerはAitermの別vendor入口であり、Codex→Codexの入口判断とは別契約。
74
+ - Aitermの`codex_agent`/`grok_agent`/`claude_agent`はmodelとeffortを毎回明示する。live catalog不在・effort非対応は明示エラーにし、別modelへfallbackしない。
75
+ - **委譲の安全・回収・受入契約は[委譲契約](../shared/orchestrate/delegation-contract.md)が正本**。Aitermの運用型は[aiterm-dispatch](../shared/orchestrate/aiterm-dispatch.md)を正とする。external writerはinstalled→registered→verified→execution-verifiedの最終段だけに置く。
76
+ - codex-sidecarはmodel/effortを毎回明示するか`.codex-sidecar.yml` defaultsへ置く。現行schemaはlow〜xhighでmaxを渡せない。
77
+ - **役割と配置関係の機械可読な対応**は`lib/orchestrate/placement-policy.mjs` v1が固定する。自動ConsultationはAnthropic/OpenAIだけだが、これは現行配線のclosed enumであり、xAIの能力評価ではない。
78
+
79
+ ## effortの規範
80
+
81
+ effortは単調に品質を上げない。まず役割に合うpresetへ置き、同じ代表taskで隣接levelを比べる。失敗後に変えるのはmodel tierかeffortの片方だけにする。
82
+
83
+ | model | 出発点 | 上げ下げの規律 |
84
+ |---|---|---|
85
+ | Opus 5 | **medium**(工程限定の実装・review)、**high**(長期agent・複雑設計) | xhigh/maxは長時間の知識仕事で測定差が出た時だけ。権限境界が緩い工程へ高effortで置かない |
86
+ | Fable 5 | high | 契約criticalのスポット。常用親と同義になる使い方はしない |
87
+ | Sonnet 5 | medium | finderの字面回収だけlow。高effortは代表taskで差が出た時 |
88
+ | Haiku 4.5 | effortなし | 存在しない`haiku×low`を指定しない |
89
+ | Sol/Terra | medium | high/xhighは測定可能な品質差がある時。maxは最難関quality-first。ultraはCodex harnessのmax+自動fan-outであり明示要求時だけ |
90
+ | Luna | **分解済み・仕様固定・focused test付きはmedium**(2026-08-19実測: medium=max同格・14/14) | 分解が甘い仕事はLunaのeffortを上げて救わず別モデルを選ぶ(旧「maxのみ」実測の教訓を適用範囲限定で継承) |
91
+ | Grok 4.6 | low=X/字面回収、medium=統合調査・監査finder、high=統括・反証・長時間agent | xhighは思考loopと遅延の外部報告がある。代表taskでhighを上回った時だけ使う |
92
+
93
+ ## Grok 4.6をどう読むか
94
+
95
+ xAI公式値ではoffice/agentic系で最前線級、DeepSWE・TerminalBenchでは比較対象を下回る。Xには統括・review・長時間実装の成功例と、security誤判定・部分読み完了誤認・思考loopの失敗例が併存する。dotagents実測(2026-08-19)では監査finderのrecall首位(7/10・FP 0)と反証満点(14/14)で、監査・反証・調査の第一候補として実戦配置する。
96
+
97
+ ## 指定と世代交代時の更新手順
98
+
99
+ - Claude Code内はfloating alias(`fable`/`opus`/`sonnet`/`haiku`)だけを使う。Agent/Workflowのmodelとeffortは対応する場合に毎回明示し、Haikuへeffortを付けない。
100
+ - Codexにfloating aliasがないため、`codex/agents/*.toml`と`.codex-sidecar.yml`は具体slugを持つ公認例外。世代交代時は本書と同一commitで更新する。
101
+ - 外部CLIはpinせず`agents-update`でlatest追従する。model live catalogは実行直前に見る。
102
+ - オーナーの世代交代宣言後、`grep -rn "前提:"`で影響面を列挙し、本書、公認例外、focused fixture、RAGを同時更新する。新規候補は小さな実戦比較を行い、失敗も次の配置判断へ残す。
103
+ - 順位の変更は実測(役割配置実験の再走または実戦の成功率・手戻り)を根拠にし、実験記録をRAGへ残してから表を書き換える。
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> <role> [brief]`で起こし、既存席の分担・起床・確認は
31
+ `env -u PEERTABLE_POST_TOKEN scripts/launch-seat.sh <project> <name> --roles <役割> [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
 
@@ -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> <role> [着任指示]` を実行する。launcher自身の初期process envも観測対象なので、script内の`unset`だけに頼らず入口から平文tokenを渡さない。**role は必須**(02_models の役割名。未指定・`worker` 既定・未知の名前は着席前に非ゼロ終了)。model / vendor / effort は順位表から解決する。tmux 作成(aiterm と同じソケットなので、立った席はそのまま `pty_read`/`pty_send` で読める)→ credential file path注入 → 起動 → 既知ダイアログ通過 → 着席確認まで1回で行き、着席しなければ最後の画面を出して非ゼロで落ちる(黙って進まない)
64
+ 5. **メンバー起動**: メンバーごとに `env -u PEERTABLE_POST_TOKEN scripts/launch-seat.sh <project> <name> --roles <役割>[,<役割>...] [--mission <使命>] [着任指示]` を実行する。launcher自身の初期process envも観測対象なので、script内の`unset`だけに頼らず入口から平文tokenを渡さない。**roles は必須**(02_models の公式役割が1つ以上。未指定・`worker`・未知は着席前に非ゼロ終了)。model 省略時は順位表の着席可能な1位。指定時は表外でも通す。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で復元する
@@ -13,6 +13,7 @@ await client.connect(new StdioClientTransport({ command: 'aiterm-mcp', env: proc
13
13
  const env_vars = [
14
14
  'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
15
15
  'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
16
+ 'PEERTABLE_ROLES', 'PEERTABLE_MISSION',
16
17
  'PEERTABLE_TMUX_SOCKET',
17
18
  ]
18
19
  if (process.env.PEERTABLE_PLAN) env_vars.push(
@@ -207,10 +207,10 @@ else
207
207
  exit 1
208
208
  }
209
209
  brief="席設定が変更され(${changes})、席を再起動しました。.team/roles/member.mdと工程正本・roomログから再着任し、進行中taskを続けてください。"
210
- if ! "$launch" "$proj" "$name" "$old_role" "$brief" --model "$model" --vendor "$vendor" --effort "$effort"; then
210
+ if ! "$launch" "$proj" "$name" --roles "$old_role" --model "$model" --effort "$effort" "$brief"; then
211
211
  echo "SEAT_CHANGE_RESTART_FAILED: ${changes}。旧設定(vendor=${old_vendor} / model=${old_model} / effort=${old_effort:-default})へrollbackする" >&2
212
212
  rollback_brief="席設定の変更に失敗して旧設定へrollbackしました。.team/roles/member.mdと工程正本・roomログから再着任してください。"
213
- if "$launch" "$proj" "$name" "$old_role" "$rollback_brief" --model "$old_model" --vendor "$old_vendor" --effort "$old_effort"; then
213
+ if "$launch" "$proj" "$name" --roles "$old_role" --model "$old_model" --effort "$old_effort" "$rollback_brief"; then
214
214
  echo "SEAT_CHANGE_ROLLED_BACK: ${name} は vendor=${old_vendor} / model=${old_model} / effort=${old_effort:-default} で再着席" >&2
215
215
  else
216
216
  echo "SEAT_CHANGE_ROLLBACK_FAILED: ${name} の席を手動で復旧する必要がある" >&2
@@ -24,10 +24,14 @@ const roomHeader = /^\s*\[mcp_servers\.room\]\s*$/mu
24
24
  const seatEnvNames = [
25
25
  'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
26
26
  'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
27
+ 'PEERTABLE_ROLES', 'PEERTABLE_MISSION',
27
28
  'PEERTABLE_PLAN', 'LATTICE_CLI', 'LATTICE_TODO_ACTOR_HOST',
28
29
  'LATTICE_TODO_ACTOR_SESSION', 'LATTICE_TODO_ACTOR_AGENT',
29
30
  ]
30
- const requiredSeatEnv = seatEnvNames.slice(0, 8)
31
+ const requiredSeatEnv = [
32
+ 'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
33
+ 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_ROLE', 'PEERTABLE_ROLES',
34
+ ]
31
35
 
32
36
  function atomicWrite(file, body, mode = 0o644) {
33
37
  mkdirSync(dirname(file), { recursive: true })
@@ -1,41 +1,36 @@
1
1
  #!/bin/bash
2
2
  # 席を1つ立てる(tmux 作成 → env 注入 → エージェント起動 → 既知ダイアログ通過 → 着席確認)。
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
- # brief: 着席が成立したら送る着任指示(省略時は送らない)
7
- #
8
- # room 名・server URL・モード・plan key は <project>/.team/setup-state.json から読む(setup.sh の後に呼ぶ)。
9
- # 書込トークンは helper が ~/.config/peertable.env から席別 0600 file へ移し、pathだけを席へ渡す。
10
- # tmux は aiterm-mcp と同じソケットへ作るので、立てた席はそのまま pty_read / pty_send で読める。
3
+ # usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--vendor <vendor>] [brief]
4
+ # --roles: 必須。02_models の公式役割。位置引数に公式役割を置いてもよい
5
+ # --model: 任意。省略時は先頭役割の着席可能な1位。指定時は表外でも通す
6
+ # brief: 着席が成立したら送る着任指示(省略時は送らない)
11
7
  set -e
12
8
  usage() {
13
- echo "usage: launch-seat.sh <project_dir> <name> <role> [brief] [--model <model> --vendor <vendor> --effort <effort>]" >&2
9
+ echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--vendor <vendor>] [brief]" >&2
14
10
  }
15
- proj="$1"; name="$2"; role="$3"
16
- [ -n "$proj" ] && [ -n "$name" ] && [ -n "$role" ] || { usage; exit 1; }
17
- shift 3
11
+ proj="$1"; name="$2"
12
+ [ -n "$proj" ] && [ -n "$name" ] || { usage; exit 1; }
13
+ shift 2
18
14
  brief=""
15
+ roles=""; mission=""
19
16
  opt_model=""; opt_vendor=""; opt_effort=""
20
17
  while [ $# -gt 0 ]; do
21
18
  case "$1" in
19
+ --roles) roles="${2:-}"; shift 2 ;;
20
+ --mission) mission="${2:-}"; shift 2 ;;
22
21
  --model) opt_model="${2:-}"; shift 2 ;;
23
22
  --vendor) opt_vendor="${2:-}"; shift 2 ;;
24
23
  --effort) opt_effort="${2:-}"; shift 2 ;;
25
24
  --*) echo "SEAT_LAUNCH_ARGS_INVALID: 不明な引数 $1" >&2; usage; exit 1 ;;
26
25
  *)
27
- if [ -z "$brief" ]; then brief="$1"; shift
26
+ if [ -z "$roles" ]; then roles="$1"; shift
27
+ elif [ -z "$brief" ]; then brief="$1"; shift
28
28
  else echo "SEAT_LAUNCH_ARGS_INVALID: 余分な引数 $1" >&2; usage; exit 1
29
29
  fi
30
30
  ;;
31
31
  esac
32
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
- }
38
- fi
33
+ [ -n "$roles" ] || { usage; exit 1; }
39
34
 
40
35
  if [ -n "${PEERTABLE_MEMBER:-}" ]; then
41
36
  echo "SEAT_LAUNCH_DELEGATED_CHILD_FORBIDDEN: PEERTABLE_MEMBER=${PEERTABLE_MEMBER} を継承した呼出元からの席起動を拒否" >&2
@@ -58,21 +53,24 @@ credential_file=""
58
53
  credential_persist=false
59
54
  aiterm_session_id=""
60
55
 
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")
56
+ placement_argv=(--roles "$roles")
57
+ [ -n "$opt_model" ] && placement_argv+=(--model "$opt_model")
58
+ [ -n "$opt_effort" ] && placement_argv+=(--effort "$opt_effort")
59
+ [ -n "$opt_vendor" ] && placement_argv+=(--vendor "$opt_vendor")
60
+ placement=$(node "$placement_helper" "${placement_argv[@]}") || exit "$?"
61
+ IFS=$'\t' read -r model vendor effort role <<EOF
62
+ $(python3 -c 'import json,sys
63
+ p=json.load(sys.stdin)
64
+ s=p.get("settings") or {}
65
+ roles=",".join(p.get("roles") or [])
66
+ print("\t".join((s.get("model") or "", s.get("vendor") or "", s.get("effort") or "", roles)))' <<<"$placement")
68
67
  EOF
69
- echo "SEAT_PLACEMENT: ${role} → ${vendor} / ${model}${effort:+ / $effort}(02_models ${rank}位)"
70
- python3 -c 'import json,sys
68
+ echo "SEAT_PLACEMENT: ${role} → ${vendor} / ${model}${effort:+ / $effort}"
69
+ python3 -c 'import json,sys
71
70
  p=json.load(sys.stdin)
72
71
  for item in p.get("dropped") or []:
73
72
  print("SEAT_PLACEMENT_DROPPED: rank %s %s %s" % (item.get("rank"), item.get("reason"), item.get("cell") or ""), file=sys.stderr)
74
73
  ' <<<"$placement" || true
75
- fi
76
74
  case "$vendor" in
77
75
  claude|codex|grok) ;;
78
76
  *) echo "SEAT_LAUNCH_VENDOR_UNSUPPORTED: vendor=${vendor}" >&2; exit 1 ;;
@@ -296,8 +294,11 @@ PY
296
294
  case "$stale_member_rc" in
297
295
  1) : ;; # 同名memberなし。DELETEを発行せず、そのまま新席を起こす
298
296
  0)
299
- echo "SEAT_ROOM_MEMBER_CONFLICT: 同名room memberが残っているため着席前に停止(明示的に退席してから再実行)" >&2
300
- exit 1
297
+ echo "SEAT_ROOM_MEMBER_RELAUNCH: 同名room memberが残っているので畳んでから起こす: ${name}" >&2
298
+ if ! env -u PEERTABLE_POST_TOKEN "$(dirname "$0")/leave-seat.sh" "$proj" "$name"; then
299
+ echo "SEAT_ROOM_MEMBER_RELAUNCH_FAILED: 同名席を畳めない: ${name}" >&2
300
+ exit 1
301
+ fi
301
302
  ;;
302
303
  *)
303
304
  echo "SEAT_ROOM_MEMBER_STATE_UNREADABLE: room member一覧を読み取れない(席は立てない)" >&2
@@ -320,12 +321,14 @@ launch_env=(
320
321
  "PEERTABLE_MODEL=$model"
321
322
  "PEERTABLE_EFFORT=$effort"
322
323
  "PEERTABLE_ROLE=$role"
324
+ "PEERTABLE_ROLES=$role"
325
+ "PEERTABLE_MISSION=$mission"
323
326
  "PEERTABLE_TMUX_SOCKET=$sock"
324
327
  )
325
328
  if [ "$mode" = "lattice" ]; then
326
329
  launch_env+=(
327
330
  "PEERTABLE_PLAN=$plan"
328
- "LATTICE_TODO_ACTOR_HOST=${LATTICE_TODO_ACTOR_HOST:-mac}"
331
+ "LATTICE_TODO_ACTOR_HOST=${LATTICE_TODO_ACTOR_HOST:-$(hostname | tr -d '\r')}"
329
332
  "LATTICE_TODO_ACTOR_SESSION=$name"
330
333
  "LATTICE_TODO_ACTOR_AGENT=$name"
331
334
  )
@@ -367,7 +370,9 @@ echo "launched: ${sess}(${vendor} / ${model}${effort:+ / $effort} / room=${roo
367
370
  # CodexのヘッダはCLIが起動した証拠であって、必須room MCPが初期化された証拠ではない。
368
371
  # 無関係MCPのwarningが画面へ出ても、room clientがmember登録まで到達した席だけを着席として扱う。
369
372
  # 登録が無ければ「着席済み」と丸めず、on_exitのrollbackへ渡す。
370
- room_ready_deadline=$((SECONDS + 30))
373
+ # brief を launch prompt に載せると初手ターンが MCP 初期化と重なり、30秒では
374
+ # member 登録前に rollback する(2026-08-20 ひなた再着席)。
375
+ room_ready_deadline=$((SECONDS + 90))
371
376
  room_ready=false
372
377
  grok_trust_accepted=false
373
378
  mcp_consent_accepted=false
@@ -385,7 +390,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
385
390
  *"Update now"*)
386
391
  if tmux_at send-keys -t "$sess" Down && tmux_at send-keys -t "$sess" Enter; then
387
392
  codex_update_accepted=true
388
- room_ready_deadline=$((SECONDS + 30))
393
+ room_ready_deadline=$((SECONDS + 90))
389
394
  echo "codex update prompt: skipped"
390
395
  fi
391
396
  ;;
@@ -396,7 +401,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
396
401
  *"Hooks need review"*)
397
402
  if tmux_at send-keys -t "$sess" Down && tmux_at send-keys -t "$sess" Enter; then
398
403
  codex_hooks_accepted=true
399
- room_ready_deadline=$((SECONDS + 30))
404
+ room_ready_deadline=$((SECONDS + 90))
400
405
  echo "codex hooks prompt: trust all"
401
406
  fi
402
407
  ;;
@@ -407,7 +412,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
407
412
  *"Yes, continue"*)
408
413
  if tmux_at send-keys -t "$sess" Enter; then
409
414
  codex_trust_accepted=true
410
- room_ready_deadline=$((SECONDS + 30))
415
+ room_ready_deadline=$((SECONDS + 90))
411
416
  echo "codex directory trust: accepted"
412
417
  fi
413
418
  ;;
@@ -423,7 +428,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
423
428
  exit 1
424
429
  fi
425
430
  grok_trust_accepted=true
426
- room_ready_deadline=$((SECONDS + 30))
431
+ room_ready_deadline=$((SECONDS + 90))
427
432
  echo "grok workspace trust: accepted"
428
433
  ;;
429
434
  esac
@@ -439,7 +444,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
439
444
  exit 1
440
445
  fi
441
446
  mcp_consent_accepted=true
442
- room_ready_deadline=$((SECONDS + 30))
447
+ room_ready_deadline=$((SECONDS + 90))
443
448
  echo "claude room MCP consent: accepted"
444
449
  ;;
445
450
  esac
@@ -543,6 +548,9 @@ fi
543
548
  seat_pid=""
544
549
  pane_pid=$(tmux_at list-panes -t "$sess" -F '#{pane_pid}' 2>/dev/null | head -1 || true)
545
550
  seat_ident=""
551
+ if [ -n "$pane_pid" ] && [ -r "/proc/${pane_pid}/winpid" ]; then
552
+ pane_pid=$(tr -d '[:space:]' < "/proc/${pane_pid}/winpid")
553
+ fi
546
554
  if [ -n "$pane_pid" ]; then
547
555
  seat_ident=$(node "$(dirname "$0")/seat-identity.mjs" "$pane_pid") || seat_ident=""
548
556
  fi
@@ -596,23 +604,28 @@ PY
596
604
  fi
597
605
  fi
598
606
 
599
- # 席の素性(vendor / model / effort / role)を room へ渡す。参加者一覧のホバー表示に使う。
600
- # 席自身の client も起動時に `{name}` だけで登録するので、server 側は
601
- # **欄が無い登録で既存の素性を消さない**(upsert)ことが前提である。
602
- # effort は渡された時だけ入れる——欄が無い=「不明」ではなく「CLI 既定で走っている」。
603
- # ここが失敗しても席は着席済みなので落とさない。ただし黙っては飲まない。
607
+ # 席の素性(roles / settings / mission)を room へ渡す。一覧チップと席の名簿が同じ欄を見る。
604
608
  tmux_ns=$(node "$(dirname "$0")/tmux-socket.mjs" --namespace)
605
- meta=$(python3 - "$name" "$vendor" "$model" "$effort" "$sock" "$sess" "$role" "$aiterm_session_id" "$tmux_ns" <<'PY'
609
+ meta=$(python3 - "$name" "$vendor" "$model" "$effort" "$sock" "$sess" "$role" "$mission" "$aiterm_session_id" "$tmux_ns" <<'PY'
606
610
  import json, sys
607
- name, vendor, model, effort, sock, sess, role, aiterm_session_id, tmux_ns = sys.argv[1:10]
611
+ name, vendor, model, effort, sock, sess, roles_raw, mission, aiterm_session_id, tmux_ns = sys.argv[1:11]
608
612
  observe = {'tmux_socket': sock, 'tmux_target': sess}
609
613
  if tmux_ns:
610
614
  observe['tmux_namespace'] = tmux_ns
611
- body = {'name': name, 'vendor': vendor, 'model': model, 'role': role, 'aiterm_session_id': aiterm_session_id,
612
- 'observe': observe}
615
+ roles = [item for item in roles_raw.split(',') if item]
616
+ settings = {'vendor': vendor, 'model': model}
617
+ if effort:
618
+ settings['effort'] = effort
619
+ body = {
620
+ 'name': name, 'vendor': vendor, 'model': model, 'role': roles[0] if roles else '',
621
+ 'roles': roles, 'settings': settings, 'aiterm_session_id': aiterm_session_id,
622
+ 'observe': observe,
623
+ }
613
624
  if effort:
614
625
  body['effort'] = effort
615
- print(json.dumps(body))
626
+ if mission:
627
+ body['mission'] = mission
628
+ print(json.dumps(body, ensure_ascii=False))
616
629
  PY
617
630
  )
618
631
  if env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
@@ -632,7 +645,7 @@ try:
632
645
  except Exception:
633
646
  members = []
634
647
  m = next((x for x in members if x.get('name') == sys.argv[1]), {})
635
- print('yes' if m.get('model') and m.get('role') and m.get('aiterm_session_id') and m.get('observe', {}).get('tmux_target') else 'no')
648
+ print('yes' if m.get('model') and (m.get('roles') or m.get('role')) and m.get('aiterm_session_id') and m.get('observe', {}).get('tmux_target') else 'no')
636
649
  PY
637
650
  )
638
651
  if [ "$stored" = yes ]; then
@@ -10,6 +10,8 @@
10
10
  # 親は MCP を後付けできないので room へは HTTP API 直で入る(決定40 の operating notes)。
11
11
  set -e
12
12
  proj="$1"; name="${2:-bell}"; model="$3"; effort="$4"; vendor="$5"
13
+ mission=""
14
+ if [ "${6:-}" = "--mission" ]; then mission="${7:-}"; fi
13
15
  state="$proj/.team/setup-state.json"
14
16
  room=$(python3 -c "import json;print(json.load(open('$state'))['room'])")
15
17
  url=$(python3 -c "import json;print(json.load(open('$state'))['server_url'])")
@@ -21,21 +23,37 @@ fi
21
23
 
22
24
  # 親はAiterm席ではない。Claude/Codex/Grokとも、親自身が所有するparent-watchを配送先にする。
23
25
  # tmux observeやCodex thread IDを登録すると、通常席bridge/外部resumeへ誤配送される。
24
- parent_vendor="${vendor:-claude}"
25
- member=$(python3 - "$name" "$model" "$effort" "$parent_vendor" <<'PY'
26
+ parent_vendor="$vendor"
27
+ if [ -z "$parent_vendor" ] && [ -n "$model" ]; then
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 ;;
32
+ esac
33
+ fi
34
+ member=$(python3 - "$name" "$model" "$effort" "$parent_vendor" "$mission" <<'PY'
26
35
  import json, sys
27
- name, model, effort, vendor = sys.argv[1:5]
28
- body = {'name': name}
29
- # 渡された欄だけ載せる。空欄を送ると、素性を持つ既存登録を空で上書きしうる
30
- if model or vendor:
31
- body['vendor'] = vendor or 'claude'
36
+ name, model, effort, vendor, mission = sys.argv[1:6]
37
+ body = {'name': name, 'roles': ['統括'], 'role': '統括'}
38
+ if vendor:
39
+ body['vendor'] = vendor
32
40
  if model:
33
41
  body['model'] = model
42
+ settings = {}
43
+ if vendor:
44
+ settings['vendor'] = vendor
45
+ if model:
46
+ settings['model'] = model
34
47
  if effort:
35
48
  body['effort'] = effort
49
+ settings['effort'] = effort
50
+ if settings:
51
+ body['settings'] = settings
52
+ if mission:
53
+ body['mission'] = mission
36
54
  body['observe'] = None
37
- body['delivery'] = {'kind': 'parent_watch', 'host': vendor}
38
- print(json.dumps(body))
55
+ body['delivery'] = {'kind': 'parent_watch', 'host': vendor or ''}
56
+ print(json.dumps(body, ensure_ascii=False))
39
57
  PY
40
58
  )
41
59
  curl -sf -X POST "$url/api/$room/members" \
@@ -25,6 +25,8 @@ export function findModelsDoc({ env = process.env, exists = existsSync, scriptDi
25
25
  if (env.DOTAGENTS_ROOT) return join(resolve(env.DOTAGENTS_ROOT), 'docs/02_models.md')
26
26
  const sibling = resolve(scriptDir, '../../../dotagents/docs/02_models.md')
27
27
  if (exists(sibling)) return sibling
28
+ const bundled = join(scriptDir, '../02_models.snapshot.md')
29
+ if (exists(bundled)) return bundled
28
30
  return null
29
31
  }
30
32
 
@@ -95,6 +97,34 @@ const matchLedger = (modelKey, ledger) => {
95
97
  return null
96
98
  }
97
99
 
100
+ export function listOfficialRoles(markdown) {
101
+ return parseMarkdownTable(section(markdown, '順位表(役割→1位〜3位)'))
102
+ .map((row) => row['役割'])
103
+ .filter(Boolean)
104
+ }
105
+
106
+ export function vendorFromSlug(slug) {
107
+ const text = String(slug ?? '').trim().toLowerCase()
108
+ if (!text) return null
109
+ if (/^(gpt-|gpt\b|o[0-9])/u.test(text)) return 'codex'
110
+ if (/^(claude|opus|sonnet|haiku|fable)\b/u.test(text)) return 'claude'
111
+ if (/^grok\b/u.test(text)) return 'grok'
112
+ return null
113
+ }
114
+
115
+ const parseRoleList = (roles) => {
116
+ const raw = Array.isArray(roles) ? roles : String(roles ?? '').split(/[,、]/u)
117
+ const seen = new Set()
118
+ const list = []
119
+ for (const item of raw) {
120
+ const role = String(item ?? '').trim()
121
+ if (!role || seen.has(role)) continue
122
+ seen.add(role)
123
+ list.push(role)
124
+ }
125
+ return list
126
+ }
127
+
98
128
  export function resolveSeatPlacement(role, markdown, { source = '' } = {}) {
99
129
  const wanted = String(role ?? '').trim()
100
130
  if (!wanted) {
@@ -144,9 +174,89 @@ export function resolveSeatPlacement(role, markdown, { source = '' } = {}) {
144
174
  }
145
175
  }
146
176
 
177
+ export function resolveSeatIdentity({
178
+ roles, model, effort, vendor, markdown, source = '', allowParentRole = false,
179
+ } = {}) {
180
+ const roleList = parseRoleList(roles)
181
+ if (roleList.length === 0) {
182
+ return { error: 'SEAT_ROLE_REQUIRED', message: 'roles が空(02_models の役割名が1つ以上要る)' }
183
+ }
184
+ const official = listOfficialRoles(markdown)
185
+ const unknown = roleList.filter((role) => !official.includes(role))
186
+ if (unknown.length > 0) {
187
+ return {
188
+ error: 'SEAT_ROLE_UNKNOWN',
189
+ message: `未知の役割: ${unknown.join(', ')}(${official.join(' / ')})`,
190
+ }
191
+ }
192
+ if (roleList.includes('実装') && roleList.includes('反証')) {
193
+ return { error: 'SEAT_ROLE_CONFLICT', message: '同一人物に実装と反証を同時に付けられない' }
194
+ }
195
+ if (!allowParentRole && roleList.includes('統括')) {
196
+ return { error: 'SEAT_ROLE_PARENT_ONLY', message: '統括は親セッションの役割であり席を起こさない' }
197
+ }
198
+ if (allowParentRole && roleList.includes('統括') && !String(model ?? '').trim()) {
199
+ return {
200
+ roles: roleList,
201
+ settings: {
202
+ vendor: String(vendor ?? '').trim(),
203
+ model: '',
204
+ effort: String(effort ?? '').trim(),
205
+ },
206
+ source,
207
+ }
208
+ }
209
+
210
+ const requestedModel = String(model ?? '').trim()
211
+ const requestedEffort = String(effort ?? '').trim()
212
+ const requestedVendor = String(vendor ?? '').trim()
213
+ if (requestedModel) {
214
+ const resolvedVendor = requestedVendor || vendorFromSlug(requestedModel)
215
+ || parseLedger(markdown).find((row) => row.slug === requestedModel)?.vendor
216
+ if (!resolvedVendor) {
217
+ return {
218
+ error: 'SEAT_VENDOR_UNRESOLVED',
219
+ message: `model=${requestedModel} の vendor を推定できない(--vendor が要る)`,
220
+ }
221
+ }
222
+ return {
223
+ roles: roleList,
224
+ settings: { vendor: resolvedVendor, model: requestedModel, effort: requestedEffort },
225
+ source,
226
+ }
227
+ }
228
+
229
+ const first = resolveSeatPlacement(roleList[0], markdown, { source })
230
+ if (first.error) return first
231
+ return {
232
+ roles: roleList,
233
+ settings: {
234
+ vendor: first.vendor,
235
+ model: first.model,
236
+ effort: requestedEffort || first.effort || '',
237
+ },
238
+ source,
239
+ dropped: first.dropped,
240
+ }
241
+ }
242
+
147
243
  const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
148
244
  if (isMain) {
149
- const role = process.argv[2]
245
+ const args = process.argv.slice(2)
246
+ let roles = ''
247
+ let model = ''
248
+ let effort = ''
249
+ let vendor = ''
250
+ const positional = []
251
+ for (let i = 0; i < args.length; i += 1) {
252
+ const arg = args[i]
253
+ if (arg === '--roles') { roles = args[++i] ?? ''; continue }
254
+ if (arg === '--model') { model = args[++i] ?? ''; continue }
255
+ if (arg === '--effort') { effort = args[++i] ?? ''; continue }
256
+ if (arg === '--vendor') { vendor = args[++i] ?? ''; continue }
257
+ positional.push(arg)
258
+ }
259
+ if (!roles) roles = positional[0] ?? ''
150
260
  const doc = findModelsDoc()
151
261
  if (!doc) {
152
262
  fail('SEAT_MODELS_DOC_MISSING', '02_models.md が見つからない(PEERTABLE_MODELS_DOC または DOTAGENTS_ROOT を渡す)')
@@ -154,7 +264,9 @@ if (isMain) {
154
264
  if (!existsSync(doc)) {
155
265
  fail('SEAT_MODELS_DOC_MISSING', `02_models.md が無い: ${doc}`)
156
266
  }
157
- const result = resolveSeatPlacement(role, readFileSync(doc, 'utf8'), { source: doc })
267
+ const result = resolveSeatIdentity({
268
+ roles, model, effort, vendor, markdown: readFileSync(doc, 'utf8'), source: doc,
269
+ })
158
270
  if (result.error) fail(result.error, result.message)
159
271
  process.stdout.write(`${JSON.stringify(result)}\n`)
160
272
  }