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 +1 -1
- package/room/client.mjs +48 -6
- package/room/server.mjs +18 -9
- package/skill/02_models.snapshot.md +103 -0
- package/skill/SKILL.md +2 -2
- package/skill/scripts/aiterm-launch.mjs +1 -0
- package/skill/scripts/change-seat.sh +2 -2
- package/skill/scripts/ensure-codex-room-mcp.mjs +5 -1
- package/skill/scripts/launch-seat.sh +62 -49
- package/skill/scripts/parent-join.sh +27 -9
- package/skill/scripts/resolve-seat-placement.mjs +114 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.4.
|
|
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
|
+
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
467
|
-
const
|
|
468
|
-
const meta=[
|
|
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)))
|
|
482
|
-
|
|
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>
|
|
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>
|
|
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"
|
|
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"
|
|
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 =
|
|
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>
|
|
4
|
-
#
|
|
5
|
-
# model
|
|
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>
|
|
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"
|
|
16
|
-
[ -n "$proj" ] && [ -n "$name" ]
|
|
17
|
-
shift
|
|
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 "$
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
$(python3 -c 'import json,sys
|
|
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
|
-
|
|
70
|
-
|
|
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 "
|
|
300
|
-
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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
|
-
# 席の素性(
|
|
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,
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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="$
|
|
25
|
-
|
|
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:
|
|
28
|
-
body = {'name': name}
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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 =
|
|
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
|
}
|