peertable 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/package.json +1 -1
- package/room/client.mjs +7 -4
- package/room/server.mjs +18 -7
- package/skill/02_models.snapshot.md +2 -2
- package/skill/SKILL.md +5 -5
- package/skill/scripts/aiterm-launch.mjs +7 -7
- package/skill/scripts/change-seat.sh +25 -25
- package/skill/scripts/ensure-codex-room-mcp.mjs +2 -2
- package/skill/scripts/launch-seat.sh +33 -32
- package/skill/scripts/parent-join.sh +19 -18
- package/skill/scripts/resolve-seat-placement.mjs +28 -27
- package/skill/scripts/seat-usage.mjs +1 -1
- package/skill/scripts/wakeup-bridge.mjs +5 -5
- package/skill/scripts/wakeup-delivery.mjs +4 -4
- package/skill/templates/parent.md +4 -4
package/README.ja.md
CHANGED
|
@@ -73,13 +73,13 @@ docker compose -f deploy/compose.yaml up -d
|
|
|
73
73
|
|
|
74
74
|
`http://localhost:8790` を開くと、全 room にライブ Web ビュー(SSE)が付く。**Web UI は観戦専用**——書込は全て API 経由で、`PEERTABLE_POST_TOKEN` 設定時はトークン必須。外から届く設置では必ずトークンを設定する。
|
|
75
75
|
|
|
76
|
-
ライブビューはメンバーごとに
|
|
76
|
+
ライブビューはメンバーごとに harness / model / effort / role と**稼働状態**(作業中・待機・**承認待ち**(許可ダイアログで止まっている)・停止)を出す。作業中の席はアイコンが動き、完了宣言(`[done]` / `[完了]` / `受理:` 等)の瞬間に席の上へ印が浮く。状態変化は SSE で押し込むので、30秒の再取得を待たず観測周期(約8秒)で切り替わる。発言にはログ番号(`[123]`)が付き、ライブ新着はブロック単位で現れる。**点が付かない席は「誰も報告していない席」**——状態の送信は別プロセス(スキルが起こす)で、**書けない時は常駐せずに死ぬ**ので「起きているのに黙っている」状態は存在しない。
|
|
77
77
|
|
|
78
78
|
観測先は**席自身が名乗る**(`observe: {tmux_socket, tmux_target}`)。席の起動スクリプトと、席の中で動く MCP クライアントの両方が自分の tmux socket / session を登録するので、**スキル以外の経路で立てた席(aiterm の素の pane など)もそのまま観測対象になる**。表示名から `peer-<名前>` を推測しないので、任意のセッション名で立てた席が消える問題は起きない。名乗っていない古い席だけが従来の推測へ落ちる。常駐は専用 tmux セッションが保持し、**起動側は「起こした」ではなく「最初の観測が届いた」ことを確かめてから成功を返す**(確かめられなければログ末尾を出して非ゼロで落ちる)。
|
|
79
79
|
|
|
80
80
|
API: `GET /api/<room>/messages` / `members` / `members/<name>` / `summary`(約120バイト・`seq`・`last_ts`・`member_count`)/ `events`(SSE)、`POST /api/<room>/messages` / `members`。
|
|
81
81
|
|
|
82
|
-
**部屋がメンバーの唯一の台帳である。** メンバーに帰属する情報——素性(
|
|
82
|
+
**部屋がメンバーの唯一の台帳である。** メンバーに帰属する情報——素性(harness / model / effort / roles / mission)・観測先(`observe`)・稼働状態・プロセス本人性(pid / 起動時刻 / argv digest)——は room サーバー内蔵の SQLite(`node:sqlite`・`/data/room.db`・Node 24+ 必須)の**1行**に全部入る。欄ごとに書き手は1人(素性=席自身の MCP クライアント、本人性=ランチャー、状態=状態ブリッジ)。席ファイルも重複欄も無く、全ての読者は台帳を読む。旧 `members.json` は初回起動で一度だけ取り込まれる。
|
|
83
83
|
|
|
84
84
|
**2. Claude Code のメンバーを着席させる。** room の MCP 定義は**プロジェクト root の `.mcp.json`** に置く:
|
|
85
85
|
|
package/README.md
CHANGED
|
@@ -109,13 +109,13 @@ docker compose -f deploy/compose.yaml up -d
|
|
|
109
109
|
|
|
110
110
|
Open `http://localhost:8790` — every room gets a live web view (SSE). **The web UI is spectator-only**: all writes go through the API and require `PEERTABLE_POST_TOKEN` when set. Set the token whenever the server is reachable from outside.
|
|
111
111
|
|
|
112
|
-
The live view shows, per member:
|
|
112
|
+
The live view shows, per member: harness / model / reasoning effort / role, and a **working state** — 作業中 (busy) · 待機 (idle) · 承認待ち (blocked on a permission prompt) · 停止 (dead). A busy seat's avatar animates; a completion (`[done]` / `[完了]` / `受理:` …) pops a marker over the seat. State changes are pushed over SSE, so the icon turns within the observer's polling interval (~8s), not on the next 30-second refresh. Messages carry their log number (`[123]`) for quoting, and live arrivals reveal block by block. **A seat with no dot is one nobody is reporting on** — the state feed is a separate opt-in process (the skill starts it for you), and it refuses to run if it cannot write, so "running but silent" cannot happen.
|
|
113
113
|
|
|
114
114
|
Seats **declare where to watch them** (`observe: {tmux_socket, tmux_target}`). Both the seat launcher and the MCP client running inside the seat register their own tmux socket and session, so **a seat you started outside the skill — a bare aiterm pane, say — is observed too**. Nothing infers `peer-<name>` from the display name, so a seat under an arbitrary session name no longer vanishes from the view; only seats that never declared fall back to the old guess. The state feed lives in its own tmux session, and whatever starts it waits for the **first observation to land** before reporting success — not merely for a process to exist. If it never lands, the starter prints the log tail and exits non-zero.
|
|
115
115
|
|
|
116
116
|
Endpoints: `GET /api/<room>/messages` · `GET /api/<room>/members` · `GET /api/<room>/members/<name>` · `GET /api/<room>/summary` (≈120 bytes: `seq`, `last_ts`, `member_count`) · `GET /api/<room>/events` (SSE) · `POST /api/<room>/messages` · `POST /api/<room>/members`.
|
|
117
117
|
|
|
118
|
-
**The room is the single member ledger.** Everything that belongs to a member — identity (
|
|
118
|
+
**The room is the single member ledger.** Everything that belongs to a member — identity (harness / model / effort / roles / mission), where to observe it (`observe`), its working state, and its process identity (pid / start time / argv digest) — lives in one SQLite row inside the room server (`node:sqlite`, `/data/room.db`; requires Node 24+). One writer per field group: the seat's own MCP client registers identity, the launcher registers process identity, the state feed writes status. No seat files, no duplicated fields — every consumer reads the ledger. A legacy `members.json` is imported once on first boot.
|
|
119
119
|
|
|
120
120
|
**2. Seat a Claude Code member session.** The room MCP definition must live in the **project-root `.mcp.json`**:
|
|
121
121
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
@@ -13,7 +13,7 @@ import { findModelsDoc, resolveSeatIdentity } from '../skill/scripts/resolve-sea
|
|
|
13
13
|
|
|
14
14
|
// client.mjs 側のハードコード版数。package.json の version と一致していることを
|
|
15
15
|
// diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
|
|
16
|
-
const MCP_VERSION = '0.
|
|
16
|
+
const MCP_VERSION = '0.6.0'
|
|
17
17
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
18
|
|
|
19
19
|
const USAGE = `usage:
|
|
@@ -159,7 +159,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
159
159
|
await mcp.connect(new StdioServerTransport())
|
|
160
160
|
|
|
161
161
|
// 参加登録し、現在のログ末尾から未読を数え始める。
|
|
162
|
-
// 素性(
|
|
162
|
+
// 素性(harness/model/effort/role)は launch-seat.sh が env へ入れる。**登録のたびに載せる**——
|
|
163
163
|
// 登録は client の起動ごとに繰り返し起きるので、1回きりの経路に置くと
|
|
164
164
|
// member の状態が失われた時に二度と戻らない(server 側は渡された欄だけ更新する upsert)
|
|
165
165
|
function observeSelf() {
|
|
@@ -204,7 +204,10 @@ const roles = parseRoles(process.env.PEERTABLE_ROLES || process.env.PEERTABLE_RO
|
|
|
204
204
|
// 台帳(room server の SQLite)へは canonical 欄だけを登録する。settings / role(単数)の
|
|
205
205
|
// 重複欄は 2026-08-22 に廃止——同一情報の重複管理禁止(オーナー裁定)。
|
|
206
206
|
const IDENTITY = Object.fromEntries(Object.entries({
|
|
207
|
-
|
|
207
|
+
// 正本は harness。旧 launcher(PEERTABLE_VENDOR だけを置く版)からの起動も受ける。
|
|
208
|
+
harness: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
|
|
209
|
+
// 旧版 room server(vendor 列世代)へ登録しても素性が落ちないための互換併記。
|
|
210
|
+
vendor: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
|
|
208
211
|
model: process.env.PEERTABLE_MODEL,
|
|
209
212
|
effort: process.env.PEERTABLE_EFFORT,
|
|
210
213
|
roles,
|
|
@@ -220,7 +223,7 @@ const IDENTITY = Object.fromEntries(Object.entries({
|
|
|
220
223
|
roles,
|
|
221
224
|
model: process.env.PEERTABLE_MODEL,
|
|
222
225
|
effort: process.env.PEERTABLE_EFFORT,
|
|
223
|
-
|
|
226
|
+
harness: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
|
|
224
227
|
markdown: readFileSync(modelsDoc, 'utf8'),
|
|
225
228
|
})
|
|
226
229
|
if (checked.error) throw new Error(`${checked.error}: ${checked.message}`)
|
package/room/server.mjs
CHANGED
|
@@ -40,7 +40,7 @@ db.exec(`
|
|
|
40
40
|
room TEXT NOT NULL,
|
|
41
41
|
name TEXT NOT NULL,
|
|
42
42
|
joined_at TEXT NOT NULL,
|
|
43
|
-
|
|
43
|
+
harness TEXT, model TEXT, effort TEXT,
|
|
44
44
|
roles TEXT,
|
|
45
45
|
mission TEXT,
|
|
46
46
|
delivery TEXT,
|
|
@@ -53,8 +53,15 @@ db.exec(`
|
|
|
53
53
|
)
|
|
54
54
|
`)
|
|
55
55
|
|
|
56
|
+
// 既存 DB(旧 vendor 列世代)は一度だけ harness 列を足して値を引き継ぐ。旧列は rollback 用に残す。
|
|
57
|
+
{
|
|
58
|
+
const columns = db.prepare('PRAGMA table_info(members)').all().map((c) => c.name)
|
|
59
|
+
if (!columns.includes('harness')) db.exec('ALTER TABLE members ADD COLUMN harness TEXT')
|
|
60
|
+
if (columns.includes('vendor')) db.exec('UPDATE members SET harness = vendor WHERE harness IS NULL')
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
const MEMBER_COLUMNS = [
|
|
57
|
-
'joined_at', '
|
|
64
|
+
'joined_at', 'harness', 'model', 'effort', 'roles', 'mission', 'delivery', 'observe',
|
|
58
65
|
'aiterm_session_id', 'status', 'status_at', 'busy_since', 'pane_token_hint', 'usage_source',
|
|
59
66
|
'pid', 'started_identity', 'argv_digest', 'identity_recorded_at',
|
|
60
67
|
]
|
|
@@ -67,6 +74,8 @@ function rowToMember(row) {
|
|
|
67
74
|
if (value === null || value === undefined) continue
|
|
68
75
|
member[column] = MEMBER_JSON_COLUMNS.has(column) ? JSON.parse(value) : value
|
|
69
76
|
}
|
|
77
|
+
// 旧読者(member.vendor を読む旧版 bridge/skill)向けの互換 mirror。正本は harness。
|
|
78
|
+
if (member.harness !== undefined) member.vendor = member.harness
|
|
70
79
|
return member
|
|
71
80
|
}
|
|
72
81
|
const listMembers = roomName => db.prepare('SELECT * FROM members WHERE room = ? ORDER BY joined_at').all(roomName).map(rowToMember)
|
|
@@ -85,15 +94,17 @@ function putMember(roomName, member) {
|
|
|
85
94
|
VALUES (:room, :name, ${MEMBER_COLUMNS.map(c => `:${c}`).join(', ')})`).run(values)
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
// 旧形式(settings/role
|
|
97
|
+
// 旧形式(settings/role の重複欄・旧 vendor 欄)を canonical へ畳む。台帳には canonical だけを置く。
|
|
89
98
|
function normalizeMemberMeta(meta) {
|
|
90
|
-
const { role, settings, ...rest } = meta
|
|
99
|
+
const { role, settings, vendor, ...rest } = meta
|
|
91
100
|
const out = { ...rest }
|
|
92
101
|
if (out.roles === undefined && role) out.roles = [role]
|
|
102
|
+
if (out.harness === undefined && vendor !== undefined) out.harness = vendor
|
|
93
103
|
if (settings && typeof settings === 'object') {
|
|
94
|
-
for (const key of ['
|
|
104
|
+
for (const key of ['harness', 'model', 'effort']) {
|
|
95
105
|
if (out[key] === undefined && settings[key] !== undefined) out[key] = settings[key]
|
|
96
106
|
}
|
|
107
|
+
if (out.harness === undefined && settings.vendor !== undefined) out.harness = settings.vendor
|
|
97
108
|
}
|
|
98
109
|
for (const key of Object.keys(out)) if (out[key] === undefined) delete out[key]
|
|
99
110
|
return out
|
|
@@ -173,7 +184,7 @@ function normalizeAudience(to, toNames) {
|
|
|
173
184
|
|
|
174
185
|
// SSE の member イベントで押し込む欄。閲覧者が気づく欄だけに絞る(POST /members 参照)。
|
|
175
186
|
// roles は配列なので JSON 比較で差分を見る
|
|
176
|
-
const MEMBER_EVENT_FIELDS = ['status', 'busy_since', '
|
|
187
|
+
const MEMBER_EVENT_FIELDS = ['status', 'busy_since', 'harness', 'model', 'effort', 'roles', 'mission']
|
|
177
188
|
const memberFieldChanged = (before, after) =>
|
|
178
189
|
MEMBER_EVENT_FIELDS.some(f => JSON.stringify(before?.[f] ?? null) !== JSON.stringify(after?.[f] ?? null))
|
|
179
190
|
|
|
@@ -272,7 +283,7 @@ http.createServer(async (req, res) => {
|
|
|
272
283
|
}
|
|
273
284
|
if (req.method === 'POST' && rest === 'members') {
|
|
274
285
|
const { name, ...meta } = JSON.parse(body)
|
|
275
|
-
// 素性(
|
|
286
|
+
// 素性(harness/model/effort/role)や稼働状態は、名前以外の欄をそのまま任意欄として持つ。
|
|
276
287
|
// **渡された欄だけ更新し、渡されなかった欄は既存を保つ**——席の client は `{name}` だけで
|
|
277
288
|
// 登録するので、これが無いと再接続のたびに素性が消える。`joined_at` は最初の登録を保つ。
|
|
278
289
|
// observe:null は「取れなかった」であって「記述子を消せ」ではない。上書きすると
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
本書の根拠は次の4種を混ぜない。
|
|
10
10
|
|
|
11
|
-
1. **一次事実**:
|
|
11
|
+
1. **一次事実**: model提供元公式の価格、context、対応effort、live catalog。
|
|
12
12
|
2. **外部観測**: benchmark、Xの利用報告、独立評価。harness・標本数・再現条件を併記する。
|
|
13
13
|
3. **dotagents実測**: 当工場のtask、監査通過率、配線実測。一般性能へ拡張しない。
|
|
14
14
|
4. **運用判断**: 上記を踏まえた配置。客観的に確定した事実を「オーナー裁定」と呼ばない。
|
|
@@ -70,7 +70,7 @@ Anthropic(Claude Code本体・Agent/Workflow)/OpenAI Codex(Codex CLI・c
|
|
|
70
70
|
|
|
71
71
|
## 入口と使い分け
|
|
72
72
|
|
|
73
|
-
- **Codex親の三入口を分ける**: ① native subagent=repo密結合、② external execution=codex-sidecar/aiterm、③ consultation=gpt-connector。Grok/ComposerはAitermの別
|
|
73
|
+
- **Codex親の三入口を分ける**: ① native subagent=repo密結合、② external execution=codex-sidecar/aiterm、③ consultation=gpt-connector。Grok/ComposerはAitermの別harness入口であり、Codex→Codexの入口判断とは別契約。
|
|
74
74
|
- Aitermの`codex_agent`/`grok_agent`/`claude_agent`はmodelとeffortを毎回明示する。live catalog不在・effort非対応は明示エラーにし、別modelへfallbackしない。
|
|
75
75
|
- **委譲の安全・回収・受入契約は[委譲契約](../shared/orchestrate/delegation-contract.md)が正本**。Aitermの運用型は[aiterm-dispatch](../shared/orchestrate/aiterm-dispatch.md)を正とする。external writerはinstalled→registered→verified→execution-verifiedの最終段だけに置く。
|
|
76
76
|
- codex-sidecarはmodel/effortを毎回明示するか`.codex-sidecar.yml` defaultsへ置く。現行schemaはlow〜xhighでmaxを渡せない。
|
package/skill/SKILL.md
CHANGED
|
@@ -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 併用`=既定 / `単独`)** / メンバー数と**役割**(dotagents `docs/02_models.md` の役割名そのもの。未指定・未知は着席しない。model×effort は `launch-seat.sh` が順位表1位から機械解決する——呼び出し側が model /
|
|
52
|
+
1. **聞き取り**: 対象プロジェクトのパス / **工程正本(`Lattice 併用`=既定 / `単独`)** / メンバー数と**役割**(dotagents `docs/02_models.md` の役割名そのもの。未指定・未知は着席しない。model×effort は `launch-seat.sh` が順位表1位から機械解決する——呼び出し側が model / harness / 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)
|
|
@@ -65,18 +65,18 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
|
|
|
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で復元する
|
|
68
|
-
- **Codex 席**(`
|
|
69
|
-
- **Grok 席**(`
|
|
68
|
+
- **Codex 席**(`harness=codex`): Codex には channels が無いので、room は `-c` 上書きの stdio MCPとして、同じPeertable treeの`node room/client.mjs`を差す。**env は closed mode で親環境を継がない**ので `PATH`と非秘密値、credential file pathを明示列挙する。effortはmember metadataだけでなく`model_reasoning_effort`へも同じ値を渡す。モデル名は ChatGPT アカウントで使える slug を渡す(`~/.codex/config.toml` の `model` が既定値の参考。使えない slug は起動後の最初のターンで 400 になって初めて分かる)。**DM を席 TUI へ入れる配達は Codex / Grok だけ**(下記)——Claude 席は room client が `notifications/claude/channel` を送る(8/12 に送信ループを削った回帰を 0.4.5 で戻した)。Codex は MCP 通知だけではターンを始めないので、room に残すだけでは席に届いたことにならない
|
|
69
|
+
- **Grok 席**(`harness=grok`): project rootの`.mcp.json`をGrok Buildが読み、同じroom clientと席固有envをAitermが渡す。**user `~/.grok/config.toml` の booth/lattice 等 MCP は載せない**——席専用 `GROK_HOME`(`.team/seats/<name>.grok-home`、auth.json と ui だけ)を preflight と live 席の両方へ渡す。通らなければ席を立てず、別 model へ落とさない。model / effortはAitermの`grok_agent`へそのまま渡し、`grok models`のlive catalogに無いmodelは着席前に失敗する。初めて開くtreeの既知workspace trustは着席処理が通す。channelsは無いので同じ TUI 配達を使う。Grok TUIの既定はターン中の素送信を今の仕事へ混ぜず入力キューへ積むので、配達はGrok席がidleになるまで送らない
|
|
70
70
|
6. **DM の TUI 配達(Codex / Grok 席がある時)**: `scripts/ensure-bridge.sh <project> wakeup <席名>…`。room の SSE を購読し、明示的にその席宛の新着(自分の発言は除く)だけを席の TUI へ素送信する。専用の起床デーモンではない——Codex / Grok が MCP 通知からターンを始めないための配達口である。ensure は専用 tmux session で起動し、bridge の `ready_at` を待つ。確認できなければログ末尾を出して非ゼロで終わる。**pid が生きているだけでは本人ではない**(Windows は pid を再利用する。2026-08-21 実測: 死んだ bridge の pid が oracle-mcp に再利用され、ensure が起動済みと誤認、全員宛キックオフが席へ届かなかった)。live 判定は record の `last_progress_at` が新しいこと。偽の pid は kill しない。Codexは即送信、Grokはidle待ち。親(`parent_watch`)は対象にしない。`observe` 欠落は対象外ではなく配達失敗としてログに出し、再試行する。停止は `node scripts/wakeup-bridge.mjs <project> --stop`(teardown.sh が自動で行う)
|
|
71
71
|
- **黙って止まらないための三段**(決定58 の受信側の作法): ①75秒なにも届かなければ自分から切って繋ぎ直す ②繋ぎ直したら `?since=<最終seq>` で切れていた間の発言を回収する ③**心拍が積んでくる room の最新 seq が自分より進んでいたら、繋がったままでも回収する**——③が要るのは、心拍が届き続ける限り①が原理的に発火しないため。**server 側の心拍(`event: ping`・25秒周期)が前提**なので、古い room サーバーへ繋ぐと①だけが効く形になる
|
|
72
72
|
- ログは `.team/wakeup-bridge.log`。**0件でも0件と出す**ので、TUI へ入れているか・取りこぼしていないかはログを見れば分かる。再現ハーネスは `experiments/bridge-catchup-repro.mjs`
|
|
73
73
|
|
|
74
74
|
6.5 **席の稼働状態ブリッジ(setup.sh が自動で起こす)**: 手順3の scaffold が `ensure-bridge.sh <project> seat-status` で起こすので、**AI が手で叩く段は無い**。観測先は member の `observe: {tmux_socket, tmux_target}` を優先し、無い既存memberだけ `peer-<名前>` へ後方互換で落とす。socket は明示env → aiterm POSIX既定 → 検証済み`/tmp/aiterm-*.sock`1本 → 既定パスの順で解決し、bashからは`tmux-socket.mjs`を呼ぶ。既に立っている席はclient再起動まで`observe`を自己申告しないため、`peer-`以外の席で記述子が要る時は席かclientを再起動する。WSLではbridgeが読む**WSL側**の版が正で、Windows側だけの更新では直らない。
|
|
75
75
|
6.7 **model / effort変更(本人要請→親実行)**: 本人は希望と理由を自然文で親だけへDMする。親は意味を判断してtargetを確定し、`env -u PEERTABLE_POST_TOKEN skill/scripts/change-seat.sh <project> <member> [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]`を実行する。定型文への言い直し、完全一致の再送、本人DMの機械検査は行わない。scriptはroom memberの現在値を読み、Aitermの公開`agent_configure`へ確定targetを渡し、metadataの読返しと変更履歴を残す。targetはlive catalogで検証し、変更後の記録に失敗した場合も成功へ丸めない。
|
|
76
|
-
- 同じ
|
|
76
|
+
- 同じharness内のmodel / effort変更はAitermが同一sessionと会話contextを保って行う。harness変更だけは再起動を伴うため、本人はrole・工程正本・roomログから再着任する
|
|
77
77
|
6.8 **mission 更新(席が自分で実行)**: 工程が変わったら席が `env -u PEERTABLE_POST_TOKEN skill/scripts/set-mission.sh <project> <name> <text>` を打つ。`POST /members` で chip を更新し、`[mission] <name>: <text>` を全員へ1行出す。席は再起動しない。親は代行しない。`change-seat.sh` に mission を足さない。
|
|
78
78
|
|
|
79
|
-
7. **親の着卓**(このセッション): `scripts/parent-join.sh <project> [name] [model] [effort] [
|
|
79
|
+
7. **親の着卓**(このセッション): `scripts/parent-join.sh <project> [name] [model] [effort] [harness]` で member 登録とparent-watch cursorのprimeを行う。**`effort` は任意のまま据え置く**——席は `launch-seat.sh` が `--effort` で実際に設定するので「渡した値=実挙動」だが、親は既に走っているセッションで自分の effort を機械的に知る経路が無く、推測して載せると画面が嘘をつく。続けて、ClaudeとGrokはMonitor、Codexはyieldしたbackground tool taskとして**親宛DM番犬**を1世代だけ張る(形は下記「親の operating notes」の番犬仕様)。**着卓完了の条件は、parent-join が投稿する耳疎通probe(`EAR_PROBE_SENT` の nonce)を自分の監視イベントとして受信すること**——番犬プロセスの生存は耳の証拠にならない(前セッションの耳へ吠え続け親宛DMが全損した実被弾 2026-08-22)。受信できないなら監視を張り直す。通常席用wakeup-bridgeに親を載せない。broadcastのkickoffは廃止済み。以後の post も API 直(同 notes)
|
|
80
80
|
8. **起動確認**: room の members に全員いる / 最初の claim が room に流れる(Lattice 併用モードはそれが Lattice へ到達している=`lattice todo status --json` の active に出ることも確認する。単独モードは room の claim 宣言だけが到達の証拠)/ Web UI で観測できる、をチェックして報告する
|
|
81
81
|
|
|
82
82
|
## teardown
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
4
4
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
5
5
|
|
|
6
|
-
const [session_name,
|
|
7
|
-
if (!session_name || !['claude', 'codex', 'grok'].includes(
|
|
6
|
+
const [session_name, harness, model, reasoning_effort, cwd, prompt = ''] = process.argv.slice(2)
|
|
7
|
+
if (!session_name || !['claude', 'codex', 'grok'].includes(harness) || !model || !reasoning_effort || !cwd) {
|
|
8
8
|
throw new Error('SEAT_AITERM_LAUNCH_ARGS_INVALID')
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ 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',
|
|
15
|
-
'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
|
|
15
|
+
'PEERTABLE_HARNESS', 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
|
|
16
16
|
'PEERTABLE_ROLES', 'PEERTABLE_MISSION',
|
|
17
17
|
'PEERTABLE_TMUX_SOCKET',
|
|
18
18
|
]
|
|
@@ -20,13 +20,13 @@ if (process.env.PEERTABLE_PLAN) env_vars.push(
|
|
|
20
20
|
'PEERTABLE_PLAN', 'LATTICE_CLI', 'LATTICE_TODO_ACTOR_HOST',
|
|
21
21
|
'LATTICE_TODO_ACTOR_SESSION', 'LATTICE_TODO_ACTOR_AGENT',
|
|
22
22
|
)
|
|
23
|
-
if (
|
|
23
|
+
if (harness === 'grok') {
|
|
24
24
|
if (process.env.GROK_HOME) env_vars.push('GROK_HOME')
|
|
25
25
|
env_vars.push('GROK_PRIVACY_NOTICE_ROLLOUT')
|
|
26
26
|
}
|
|
27
|
-
if (
|
|
27
|
+
if (harness === 'codex' && process.env.CODEX_HOME) env_vars.push('CODEX_HOME')
|
|
28
28
|
const result = await client.callTool({
|
|
29
|
-
name: `${
|
|
29
|
+
name: `${harness}_agent`,
|
|
30
30
|
arguments: {
|
|
31
31
|
session_name,
|
|
32
32
|
cwd,
|
|
@@ -39,7 +39,7 @@ const result = await client.callTool({
|
|
|
39
39
|
await client.close()
|
|
40
40
|
if (result.isError) throw new Error(result.content?.[0]?.text ?? 'SEAT_AITERM_LAUNCH_FAILED')
|
|
41
41
|
const receipt = result.structuredContent ?? JSON.parse(result.content?.[0]?.text ?? '{}')
|
|
42
|
-
if (receipt?.schema !== 'aiterm.agent-launch-result.v1' || receipt.provider !==
|
|
42
|
+
if (receipt?.schema !== 'aiterm.agent-launch-result.v1' || receipt.provider !== harness || receipt.session_id !== session_name) {
|
|
43
43
|
throw new Error('SEAT_AITERM_LAUNCH_RECEIPT_INVALID')
|
|
44
44
|
}
|
|
45
45
|
console.log(JSON.stringify(receipt))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# 席の
|
|
3
|
-
# usage: change-seat.sh <project_dir> <member> [--
|
|
2
|
+
# 席の harness / model / effort を変更する。
|
|
3
|
+
# usage: change-seat.sh <project_dir> <member> [--harness <harness>] [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]
|
|
4
4
|
#
|
|
5
5
|
# **自然文の依頼を再解釈しない。** 依頼の意味・本人の意図・変更してよい局面を判断するのは親である AI で、
|
|
6
6
|
# この script が受け取るのは親が確定した target だけである(旧 change-effort.sh の
|
|
@@ -22,10 +22,10 @@ credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$script_dir/seat-credential.mj
|
|
|
22
22
|
|
|
23
23
|
proj="${1:-}"; name="${2:-}"
|
|
24
24
|
shift 2 2>/dev/null || true
|
|
25
|
-
|
|
25
|
+
opt_harness=""; opt_model=""; opt_effort=""; parent="bell"; reason=""
|
|
26
26
|
while [ $# -gt 0 ]; do
|
|
27
27
|
case "$1" in
|
|
28
|
-
--vendor)
|
|
28
|
+
--harness|--vendor) opt_harness="${2:-}"; shift 2 || true ;; # --vendor は旧名互換
|
|
29
29
|
--model) opt_model="${2:-}"; shift 2 || true ;;
|
|
30
30
|
--effort) opt_effort="${2:-}"; shift 2 || true ;;
|
|
31
31
|
--parent) parent="${2:-}"; shift 2 || true ;;
|
|
@@ -34,7 +34,7 @@ while [ $# -gt 0 ]; do
|
|
|
34
34
|
esac
|
|
35
35
|
done
|
|
36
36
|
[ -n "$proj" ] && [ -n "$name" ] || {
|
|
37
|
-
echo "SEAT_CHANGE_ARGS_INVALID: usage: change-seat.sh <project_dir> <member> [--
|
|
37
|
+
echo "SEAT_CHANGE_ARGS_INVALID: usage: change-seat.sh <project_dir> <member> [--harness <harness>] [--model <model>] [--effort <effort>] [--parent <name>] [--reason <text>]" >&2
|
|
38
38
|
exit 2
|
|
39
39
|
}
|
|
40
40
|
[ -n "$opt_model" ] || [ -n "$opt_effort" ] || {
|
|
@@ -57,22 +57,22 @@ meta=$(printf '%s' "$members" | python3 -c '
|
|
|
57
57
|
import json,sys
|
|
58
58
|
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
|
-
if not member or member.get("vendor") not in ("claude","codex","grok") or not member.get("model"):
|
|
60
|
+
if not member or (member.get("harness") or member.get("vendor")) not in ("claude","codex","grok") or not member.get("model"):
|
|
61
61
|
raise SystemExit(1)
|
|
62
62
|
roles=member.get("roles") or []
|
|
63
|
-
print("\t".join((member
|
|
64
|
-
' "$name") || { echo "SEAT_CHANGE_MEMBER_METADATA_MISSING: ${name} の
|
|
65
|
-
IFS=$'\t' read -r
|
|
63
|
+
print("\t".join(((member.get("harness") or member.get("vendor")),member["model"],member.get("effort") or "",member.get("aiterm_session_id") or "",",".join(roles))))
|
|
64
|
+
' "$name") || { echo "SEAT_CHANGE_MEMBER_METADATA_MISSING: ${name} のharness/modelが要る" >&2; exit 1; }
|
|
65
|
+
IFS=$'\t' read -r old_harness old_model old_effort aiterm_session_id old_role <<EOF
|
|
66
66
|
$meta
|
|
67
67
|
EOF
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
case "$
|
|
69
|
+
harness="${opt_harness:-$old_harness}"
|
|
70
|
+
case "$harness" in
|
|
71
71
|
claude|codex|grok) ;;
|
|
72
|
-
*) echo "
|
|
72
|
+
*) echo "SEAT_CHANGE_HARNESS_UNSUPPORTED: harness=${harness}(claude / codex / grok のみ)" >&2; exit 2 ;;
|
|
73
73
|
esac
|
|
74
|
-
if [ "$
|
|
75
|
-
echo "SEAT_CHANGE_ARGS_INVALID:
|
|
74
|
+
if [ "$harness" != "$old_harness" ] && { [ -z "$opt_model" ] || [ -z "$opt_effort" ]; }; then
|
|
75
|
+
echo "SEAT_CHANGE_ARGS_INVALID: harness変更には --model と --effort の明示指定が要る" >&2
|
|
76
76
|
exit 2
|
|
77
77
|
fi
|
|
78
78
|
model="${opt_model:-$old_model}"
|
|
@@ -83,7 +83,7 @@ effort="${opt_effort:-$old_effort}"
|
|
|
83
83
|
echo "SEAT_CHANGE_EFFORT_UNKNOWN: ${name} の現在effortがmetadataに無い。--effort を明示する" >&2; exit 1
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
if [ "$model" = "$old_model" ] && [ "$effort" = "$old_effort" ] && [ "$
|
|
86
|
+
if [ "$model" = "$old_model" ] && [ "$effort" = "$old_effort" ] && [ "$harness" = "$old_harness" ]; then
|
|
87
87
|
echo "SEAT_CHANGE_NOOP: ${name} は既に model=${model} / effort=${effort}(再起動しない)"
|
|
88
88
|
exit 0
|
|
89
89
|
fi
|
|
@@ -102,7 +102,7 @@ case "$screen" in
|
|
|
102
102
|
esac
|
|
103
103
|
|
|
104
104
|
# target の検証は live 面だけを使い、古くなる hardcode を足さない。
|
|
105
|
-
case "$
|
|
105
|
+
case "$harness" in
|
|
106
106
|
claude)
|
|
107
107
|
# Claude には非破壊で引ける model catalog が無い(`--help` の alias 例は catalog ではなく、
|
|
108
108
|
# 実際 2026-08-11 に `fable` は例に載ったまま live では unavailable だった)。
|
|
@@ -166,7 +166,7 @@ raise SystemExit(0 if model in models else 1)
|
|
|
166
166
|
esac
|
|
167
167
|
|
|
168
168
|
changes=""
|
|
169
|
-
[ "$
|
|
169
|
+
[ "$harness" = "$old_harness" ] || changes="harness ${old_harness} → ${harness}"
|
|
170
170
|
[ "$model" = "$old_model" ] || {
|
|
171
171
|
[ -z "$changes" ] || changes="$changes / "
|
|
172
172
|
changes="${changes}model ${old_model} → ${model}"
|
|
@@ -180,7 +180,7 @@ credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" path "$p
|
|
|
180
180
|
echo "SEAT_CHANGE_CREDENTIAL_MISSING: ${name} のroom credentialを特定できない" >&2; exit 1
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
if [ "$
|
|
183
|
+
if [ "$harness" = "$old_harness" ]; then
|
|
184
184
|
[ -n "$aiterm_session_id" ] || {
|
|
185
185
|
echo "SEAT_CHANGE_AITERM_SESSION_MISSING: ${name} にAiterm managed session_idが無い" >&2; exit 1
|
|
186
186
|
}
|
|
@@ -190,7 +190,7 @@ if [ "$vendor" = "$old_vendor" ]; then
|
|
|
190
190
|
node "$script_dir/aiterm-configure.mjs" "${configure_args[@]}" >/dev/null || {
|
|
191
191
|
echo "SEAT_CHANGE_AITERM_CONFIGURE_FAILED: ${name} の設定は変更していない" >&2; exit 1
|
|
192
192
|
}
|
|
193
|
-
identity=$(python3 -c 'import json,sys;print(json.dumps({"name":sys.argv[1],"vendor":sys.argv[2],"model":sys.argv[3],"effort":sys.argv[4],"aiterm_session_id":sys.argv[5]}))' "$name" "$
|
|
193
|
+
identity=$(python3 -c 'import json,sys;print(json.dumps({"name":sys.argv[1],"harness":sys.argv[2],"vendor":sys.argv[2],"model":sys.argv[3],"effort":sys.argv[4],"aiterm_session_id":sys.argv[5]}))' "$name" "$harness" "$model" "$effort" "$aiterm_session_id")
|
|
194
194
|
env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
|
|
195
195
|
"$url/api/$room/members" "$identity" >/dev/null || {
|
|
196
196
|
echo "SEAT_CHANGE_CHANGED_BUT_METADATA_FAILED: ${name} は設定済み、room metadataを同期できない" >&2; exit 1
|
|
@@ -209,10 +209,10 @@ else
|
|
|
209
209
|
}
|
|
210
210
|
brief="席設定が変更され(${changes})、席を再起動しました。.team/roles/member.mdと工程正本・roomログから再着任し、進行中taskを続けてください。"
|
|
211
211
|
if ! "$launch" "$proj" "$name" --roles "$old_role" --model "$model" --effort "$effort" "$brief"; then
|
|
212
|
-
echo "SEAT_CHANGE_RESTART_FAILED: ${changes}。旧設定(
|
|
212
|
+
echo "SEAT_CHANGE_RESTART_FAILED: ${changes}。旧設定(harness=${old_harness} / model=${old_model} / effort=${old_effort:-default})へrollbackする" >&2
|
|
213
213
|
rollback_brief="席設定の変更に失敗して旧設定へrollbackしました。.team/roles/member.mdと工程正本・roomログから再着任してください。"
|
|
214
214
|
if "$launch" "$proj" "$name" --roles "$old_role" --model "$old_model" --effort "$old_effort" "$rollback_brief"; then
|
|
215
|
-
echo "SEAT_CHANGE_ROLLED_BACK: ${name} は
|
|
215
|
+
echo "SEAT_CHANGE_ROLLED_BACK: ${name} は harness=${old_harness} / model=${old_model} / effort=${old_effort:-default} で再着席" >&2
|
|
216
216
|
else
|
|
217
217
|
echo "SEAT_CHANGE_ROLLBACK_FAILED: ${name} の席を手動で復旧する必要がある" >&2
|
|
218
218
|
fi
|
|
@@ -226,11 +226,11 @@ members_after=$(curl -sf "$url/api/$room/members") || {
|
|
|
226
226
|
}
|
|
227
227
|
if ! printf '%s' "$members_after" | python3 -c '
|
|
228
228
|
import json,sys
|
|
229
|
-
name,
|
|
229
|
+
name,harness,model,effort=sys.argv[1:5]
|
|
230
230
|
m=next((m for m in json.load(sys.stdin).get("members",[]) if m.get("name")==name),{})
|
|
231
|
-
raise SystemExit(0 if m.get("vendor")==
|
|
232
|
-
' "$name" "$
|
|
233
|
-
echo "SEAT_CHANGE_CHANGED_BUT_UNVERIFIED: 席は再起動済み、member metadataが
|
|
231
|
+
raise SystemExit(0 if (m.get("harness") or m.get("vendor"))==harness and m.get("model")==model and m.get("effort")==effort else 1)
|
|
232
|
+
' "$name" "$harness" "$model" "$effort"; then
|
|
233
|
+
echo "SEAT_CHANGE_CHANGED_BUT_UNVERIFIED: 席は再起動済み、member metadataが harness=${harness} / model=${model} / effort=${effort} でない" >&2
|
|
234
234
|
exit 1
|
|
235
235
|
fi
|
|
236
236
|
|
|
@@ -24,14 +24,14 @@ const endMarker = '# END PEERTABLE ROOM MCP'
|
|
|
24
24
|
const roomHeader = /^\s*\[mcp_servers\.room\]\s*$/mu
|
|
25
25
|
const seatEnvNames = [
|
|
26
26
|
'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
|
|
27
|
-
'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
|
|
27
|
+
'PEERTABLE_HARNESS', 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT', 'PEERTABLE_ROLE',
|
|
28
28
|
'PEERTABLE_ROLES', 'PEERTABLE_MISSION',
|
|
29
29
|
'PEERTABLE_PLAN', 'LATTICE_CLI', 'LATTICE_TODO_ACTOR_HOST',
|
|
30
30
|
'LATTICE_TODO_ACTOR_SESSION', 'LATTICE_TODO_ACTOR_AGENT',
|
|
31
31
|
]
|
|
32
32
|
const requiredSeatEnv = [
|
|
33
33
|
'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
|
|
34
|
-
'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_ROLE', 'PEERTABLE_ROLES',
|
|
34
|
+
'PEERTABLE_HARNESS', 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_ROLE', 'PEERTABLE_ROLES',
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
function atomicWrite(file, body, mode = 0o644) {
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# 席を1つ立てる(tmux 作成 → env 注入 → エージェント起動 → 既知ダイアログ通過 → 着席確認)。
|
|
3
|
-
# usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--
|
|
3
|
+
# usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--harness <harness>] [brief]
|
|
4
4
|
# --roles: 必須。02_models の公式役割。位置引数に公式役割を置いてもよい
|
|
5
5
|
# --model: 任意。省略時は先頭役割の着席可能な1位。指定時は表外でも通す
|
|
6
6
|
# brief: 着席が成立したら送る着任指示(省略時は送らない)
|
|
7
7
|
set -e
|
|
8
8
|
usage() {
|
|
9
|
-
echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--
|
|
9
|
+
echo "usage: launch-seat.sh <project_dir> <name> --roles <role>[,<role>...] [--mission <text>] [--model <slug>] [--effort <effort>] [--harness <harness>] [brief]" >&2
|
|
10
10
|
}
|
|
11
11
|
proj="$1"; name="$2"
|
|
12
12
|
[ -n "$proj" ] && [ -n "$name" ] || { usage; exit 1; }
|
|
13
13
|
shift 2
|
|
14
14
|
brief=""
|
|
15
15
|
roles=""; mission=""
|
|
16
|
-
opt_model="";
|
|
16
|
+
opt_model=""; opt_harness=""; opt_effort=""
|
|
17
17
|
while [ $# -gt 0 ]; do
|
|
18
18
|
case "$1" in
|
|
19
19
|
--roles) roles="${2:-}"; shift 2 ;;
|
|
20
20
|
--mission) mission="${2:-}"; shift 2 ;;
|
|
21
21
|
--model) opt_model="${2:-}"; shift 2 ;;
|
|
22
|
-
--vendor)
|
|
22
|
+
--harness|--vendor) opt_harness="${2:-}"; shift 2 ;; # --vendor は旧名互換
|
|
23
23
|
--effort) opt_effort="${2:-}"; shift 2 ;;
|
|
24
24
|
--*) echo "SEAT_LAUNCH_ARGS_INVALID: 不明な引数 $1" >&2; usage; exit 1 ;;
|
|
25
25
|
*)
|
|
@@ -65,26 +65,26 @@ aiterm_session_id=""
|
|
|
65
65
|
placement_argv=(--roles "$roles")
|
|
66
66
|
[ -n "$opt_model" ] && placement_argv+=(--model "$opt_model")
|
|
67
67
|
[ -n "$opt_effort" ] && placement_argv+=(--effort "$opt_effort")
|
|
68
|
-
[ -n "$
|
|
68
|
+
[ -n "$opt_harness" ] && placement_argv+=(--harness "$opt_harness")
|
|
69
69
|
placement=$(node "$placement_helper" "${placement_argv[@]}") || exit "$?"
|
|
70
|
-
IFS=$'\t' read -r model
|
|
70
|
+
IFS=$'\t' read -r model harness effort role <<EOF
|
|
71
71
|
$(python3 -c 'import json,sys
|
|
72
72
|
p=json.load(sys.stdin)
|
|
73
73
|
s=p.get("settings") or {}
|
|
74
74
|
roles=",".join(p.get("roles") or [])
|
|
75
|
-
print("\t".join((s.get("model") or "", s.get("
|
|
75
|
+
print("\t".join((s.get("model") or "", s.get("harness") or "", s.get("effort") or "", roles)))' <<<"$placement")
|
|
76
76
|
EOF
|
|
77
|
-
echo "SEAT_PLACEMENT: ${role} → ${
|
|
77
|
+
echo "SEAT_PLACEMENT: ${role} → ${harness} / ${model}${effort:+ / $effort}"
|
|
78
78
|
python3 -c 'import json,sys
|
|
79
79
|
p=json.load(sys.stdin)
|
|
80
80
|
for item in p.get("dropped") or []:
|
|
81
81
|
print("SEAT_PLACEMENT_DROPPED: rank %s %s %s" % (item.get("rank"), item.get("reason"), item.get("cell") or ""), file=sys.stderr)
|
|
82
82
|
' <<<"$placement" || true
|
|
83
|
-
case "$
|
|
83
|
+
case "$harness" in
|
|
84
84
|
claude|codex|grok) ;;
|
|
85
|
-
*) echo "
|
|
85
|
+
*) echo "SEAT_LAUNCH_HARNESS_UNSUPPORTED: harness=${harness}" >&2; exit 1 ;;
|
|
86
86
|
esac
|
|
87
|
-
if [ "$
|
|
87
|
+
if [ "$harness" = "claude" ] && [ -n "$effort" ]; then
|
|
88
88
|
case "$effort" in
|
|
89
89
|
low|medium|high|xhigh|max) ;;
|
|
90
90
|
*) echo "unknown effort: ${effort}(claude は low|medium|high|xhigh|max)" >&2; exit 1 ;;
|
|
@@ -221,7 +221,7 @@ fi
|
|
|
221
221
|
# 席を畳む前に測るのが要点である——ここで落ちれば、動いている席を殺さずに済む。
|
|
222
222
|
export PATH="${HOME}/.grok/bin:${PATH}"
|
|
223
223
|
preflight_dir="${TMPDIR:-/tmp}"
|
|
224
|
-
case "$
|
|
224
|
+
case "$harness" in
|
|
225
225
|
claude) preflight_cmd=(claude --model "$model" -p "ping") ;;
|
|
226
226
|
codex) preflight_cmd=(codex exec --model "$model" --skip-git-repo-check "ping") ;;
|
|
227
227
|
grok)
|
|
@@ -245,7 +245,7 @@ case "$vendor" in
|
|
|
245
245
|
echo "grok preflight: GROK_HOME=${grok_home} ${grok_bin} --model ${model} --reasoning-effort ${effort} -p ping"
|
|
246
246
|
preflight_cmd=(env GROK_HOME="$grok_home" "$grok_bin" --model "$model" --reasoning-effort "$effort" -p "ping")
|
|
247
247
|
;;
|
|
248
|
-
*) echo "unknown
|
|
248
|
+
*) echo "unknown harness: ${harness}(claude / codex / grok)" >&2; exit 1 ;;
|
|
249
249
|
esac
|
|
250
250
|
preflight_log=$(mktemp "${TMPDIR:-/tmp}/peertable-preflight.XXXXXX")
|
|
251
251
|
( cd "$preflight_dir" && "${preflight_cmd[@]}" >"$preflight_log" 2>&1 </dev/null ) &
|
|
@@ -264,14 +264,14 @@ while [ $SECONDS -lt $preflight_deadline ]; do
|
|
|
264
264
|
done
|
|
265
265
|
if [ -z "$preflight_rc" ]; then
|
|
266
266
|
kill "$preflight_pid" 2>/dev/null || true
|
|
267
|
-
echo "model preflight が 120 秒で返らない: ${
|
|
267
|
+
echo "model preflight が 120 秒で返らない: ${harness} / ${model}(席は立てない)" >&2
|
|
268
268
|
echo "preflight log: ${preflight_log}" >&2
|
|
269
269
|
cat "$preflight_log" >&2 || true
|
|
270
270
|
[ -n "${grok_home:-}" ] && rm -rf "$grok_home"
|
|
271
271
|
exit 1
|
|
272
272
|
fi
|
|
273
273
|
if [ "$preflight_rc" != 0 ]; then
|
|
274
|
-
echo "model が live で使えない: ${
|
|
274
|
+
echo "model が live で使えない: ${harness} / ${model}(preflight rc=${preflight_rc}・席は立てない)" >&2
|
|
275
275
|
cat "$preflight_log" >&2 || true
|
|
276
276
|
rm -f "$preflight_log"
|
|
277
277
|
[ -n "${grok_home:-}" ] && rm -rf "$grok_home"
|
|
@@ -338,7 +338,7 @@ fi
|
|
|
338
338
|
|
|
339
339
|
# leave-seat が席専用 GROK_HOME を消す。同名再着席では preflight 後に空になるので、
|
|
340
340
|
# live 起動の直前に auth と ui だけを書き直す(user booth MCP は載せない)。
|
|
341
|
-
if [ "$
|
|
341
|
+
if [ "$harness" = grok ]; then
|
|
342
342
|
mkdir -p "$grok_home"
|
|
343
343
|
if [ ! -f "${HOME}/.grok/auth.json" ]; then
|
|
344
344
|
echo "SEAT_GROK_AUTH_MISSING: ${HOME}/.grok/auth.json が無い(席は立てない)" >&2
|
|
@@ -359,7 +359,8 @@ launch_env=(
|
|
|
359
359
|
"PEERTABLE_ROOM=$room"
|
|
360
360
|
"PEERTABLE_MEMBER=$name"
|
|
361
361
|
"PEERTABLE_CREDENTIAL_FILE=$credential_file"
|
|
362
|
-
"
|
|
362
|
+
"PEERTABLE_HARNESS=$harness"
|
|
363
|
+
"PEERTABLE_VENDOR=$harness" # 旧版 room client 互換
|
|
363
364
|
"PEERTABLE_MODEL=$model"
|
|
364
365
|
"PEERTABLE_EFFORT=$effort"
|
|
365
366
|
"PEERTABLE_ROLE=$role"
|
|
@@ -376,13 +377,13 @@ if [ "$mode" = "lattice" ]; then
|
|
|
376
377
|
)
|
|
377
378
|
[ -n "$lattice_cli" ] && launch_env+=("LATTICE_CLI=$lattice_cli")
|
|
378
379
|
fi
|
|
379
|
-
if [ "$
|
|
380
|
+
if [ "$harness" = grok ]; then
|
|
380
381
|
# 実測 2026-08-21: auth の coding_data_retention_opt_out だけでは
|
|
381
382
|
# `Help improve Grok [Opt out] [Opt in]` バナーが残る。席が死んだように見える。
|
|
382
383
|
launch_env+=("GROK_HOME=$grok_home")
|
|
383
384
|
launch_env+=("GROK_PRIVACY_NOTICE_ROLLOUT=0")
|
|
384
385
|
fi
|
|
385
|
-
if [ "$
|
|
386
|
+
if [ "$harness" = codex ]; then
|
|
386
387
|
codex_home="${proj}/.team/seats/${name}.codex"
|
|
387
388
|
mkdir -p "$codex_home"
|
|
388
389
|
if [ -f "${HOME}/.codex/auth.json" ]; then
|
|
@@ -391,7 +392,7 @@ if [ "$vendor" = codex ]; then
|
|
|
391
392
|
fi
|
|
392
393
|
launch_env+=("CODEX_HOME=$codex_home")
|
|
393
394
|
fi
|
|
394
|
-
if [ "$
|
|
395
|
+
if [ "$harness" = codex ] \
|
|
395
396
|
&& ! env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$codex_room_mcp_helper" ensure "$proj" "$peertable_repo"; then
|
|
396
397
|
echo "SEAT_CODEX_ROOM_MCP_INVALID: Codexのproject設定へseat固有room clientを装備できない(席は立てない)" >&2
|
|
397
398
|
exit 1
|
|
@@ -399,7 +400,7 @@ fi
|
|
|
399
400
|
# Aiterm launcherはsession作成後にtyped failureを返すことがある。ここから先は同名memberが
|
|
400
401
|
# 無いことを確認済みなので、launch試行が作ったtmux/memberをon_exitの対象として先に所有する。
|
|
401
402
|
seat_created=true
|
|
402
|
-
launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$
|
|
403
|
+
launch_receipt=$(env -u PEERTABLE_POST_TOKEN "${launch_env[@]}" node "$aiterm_launch_helper" "$sess" "$harness" "$model" "$effort" "$proj" "$brief") || {
|
|
403
404
|
echo "SEAT_AITERM_LAUNCH_FAILED: ${name} をAiterm公開launcherで起動できない(direct CLI fallbackなし)" >&2
|
|
404
405
|
exit 1
|
|
405
406
|
}
|
|
@@ -449,7 +450,7 @@ seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
|
|
|
449
450
|
# Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
|
|
450
451
|
# ヘッダ/trust dialog待機をここへ重ねると、brief turnでヘッダが画面外へ流れた正常席をrollbackする。
|
|
451
452
|
# 必須room MCPの成立は、次の member登録readbackだけで判定する。
|
|
452
|
-
echo "launched: ${sess}(${
|
|
453
|
+
echo "launched: ${sess}(${harness} / ${model}${effort:+ / $effort} / room=${room} / mode=${mode})"
|
|
453
454
|
|
|
454
455
|
# CodexのヘッダはCLIが起動した証拠であって、必須room MCPが初期化された証拠ではない。
|
|
455
456
|
# 無関係MCPのwarningが画面へ出ても、room clientがmember登録まで到達した席だけを着席として扱う。
|
|
@@ -484,7 +485,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
484
485
|
# Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
|
|
485
486
|
# Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
|
|
486
487
|
# 汎用的に承認するfallbackにはしない。承認後のMCP初期化時間は改めて30秒確保する。
|
|
487
|
-
if [ "$
|
|
488
|
+
if [ "$harness" = codex ]; then
|
|
488
489
|
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
489
490
|
if pass_codex_pane "$codex_screen"; then
|
|
490
491
|
room_ready_deadline=$((SECONDS + 90))
|
|
@@ -508,7 +509,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
508
509
|
esac
|
|
509
510
|
fi
|
|
510
511
|
fi
|
|
511
|
-
if [ "$
|
|
512
|
+
if [ "$harness" = grok ] && [ "$grok_trust_accepted" != true ]; then
|
|
512
513
|
grok_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
513
514
|
case "$grok_screen" in
|
|
514
515
|
*"Do you trust the contents of this directory?"*)
|
|
@@ -524,7 +525,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
524
525
|
fi
|
|
525
526
|
# 未信頼ディレクトリでは room MCP 同意より前に workspace trust が出る。既定選択肢
|
|
526
527
|
# (1. Yes, I trust this folder)を Enter で通す。この既知文言だけを一度通す。
|
|
527
|
-
if [ "$
|
|
528
|
+
if [ "$harness" = claude ] && [ "$claude_trust_accepted" != true ]; then
|
|
528
529
|
claude_trust_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
529
530
|
case "$claude_trust_screen" in
|
|
530
531
|
*"Is this a project you created or one you trust"*|*"trust this folder"*)
|
|
@@ -540,7 +541,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
540
541
|
fi
|
|
541
542
|
# aiterm claude_agent は --dangerously-skip-permissions を付けない。project の room MCP
|
|
542
543
|
# 同意が member 登録より前に出る。選択肢2(this and all future)だけを通す。
|
|
543
|
-
if [ "$
|
|
544
|
+
if [ "$harness" = claude ] && [ "$mcp_consent_accepted" != true ]; then
|
|
544
545
|
claude_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
545
546
|
case "$claude_screen" in
|
|
546
547
|
*"New MCP server found in this project: room"*)
|
|
@@ -556,7 +557,7 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
|
|
|
556
557
|
fi
|
|
557
558
|
room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
|
|
558
559
|
if printf '%s' "$room_members" | python3 -c 'import json,sys; name=sys.argv[1]; members=json.load(sys.stdin).get("members",[]); raise SystemExit(0 if any(m.get("name") == name for m in members) else 1)' "$name"; then
|
|
559
|
-
if [ "$
|
|
560
|
+
if [ "$harness" = codex ]; then
|
|
560
561
|
codex_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
561
562
|
if ! codex_pane_blocks_ready "$codex_screen"; then
|
|
562
563
|
sleep 1
|
|
@@ -575,7 +576,7 @@ if [ "$room_ready" != true ]; then
|
|
|
575
576
|
exit 1
|
|
576
577
|
fi
|
|
577
578
|
echo "room ready: ${room}/${name}"
|
|
578
|
-
if [ "$
|
|
579
|
+
if [ "$harness" = codex ]; then
|
|
579
580
|
# member 登録後の最初の tool 呼び出しで MCP Allow が出る。出ている間だけ通し、沈黙5秒で抜ける。
|
|
580
581
|
codex_post_ready_deadline=$((SECONDS + 90))
|
|
581
582
|
codex_post_ready_idle=0
|
|
@@ -607,7 +608,7 @@ if [ -n "$brief" ] && [ "$brief_dispatched" != true ]; then
|
|
|
607
608
|
brief_ready=false
|
|
608
609
|
while [ $SECONDS -lt "$brief_ready_deadline" ]; do
|
|
609
610
|
brief_ready_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
|
|
610
|
-
if [ "$
|
|
611
|
+
if [ "$harness" != codex ] || printf '%s\n' "$brief_ready_screen" | python3 -c 'import sys; lines=sys.stdin.read().splitlines()[-24:]; has_prompt=any(line.strip() == "›" or line.lstrip().startswith("› ") for line in lines); has_footer=any("gpt-" in line and "·" in line for line in lines); raise SystemExit(0 if has_prompt and has_footer else 1)'; then
|
|
611
612
|
brief_ready_streak=$((brief_ready_streak + 1))
|
|
612
613
|
if [ "$brief_ready_streak" -ge 3 ]; then
|
|
613
614
|
brief_ready=true
|
|
@@ -716,7 +717,7 @@ PY
|
|
|
716
717
|
fi
|
|
717
718
|
fi
|
|
718
719
|
|
|
719
|
-
# 素性(
|
|
720
|
+
# 素性(harness/model/roles/mission/observe/aiterm ID)の書き手は**席の room client だけ**。
|
|
720
721
|
# ランチャーはここで台帳を読み返し、実際に載ったかを確認するだけにする(重複書込の禁止)。
|
|
721
722
|
member_row=$(curl -sf "$url/api/$room/members/$(python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1],safe=""))' "$name")" || true)
|
|
722
723
|
stored=$(python3 - "$member_row" <<'PY'
|
|
@@ -730,7 +731,7 @@ print('yes' if m.get('model') and m.get('roles') and m.get('aiterm_session_id')
|
|
|
730
731
|
PY
|
|
731
732
|
)
|
|
732
733
|
if [ "$stored" = yes ]; then
|
|
733
|
-
echo "台帳確認: ${
|
|
734
|
+
echo "台帳確認: ${harness} / ${model}${effort:+ / $effort}${role:+ / $role}(素性・本人性とも登録済み)"
|
|
734
735
|
else
|
|
735
736
|
echo "SEAT_LEDGER_INCOMPLETE: 台帳の member 行に素性または本人性が欠けている(席は着席済み。doctor で確認)" >&2
|
|
736
737
|
fi
|
|
@@ -744,7 +745,7 @@ fi
|
|
|
744
745
|
|
|
745
746
|
# Claude 席の新着は room client の notifications/claude/channel。TUI 配達は channels を持たない
|
|
746
747
|
# Codex / Grok 席だけ。Claude に立てると channel と二重配達になる。
|
|
747
|
-
if [ "$
|
|
748
|
+
if [ "$harness" = codex ] || [ "$harness" = grok ]; then
|
|
748
749
|
if PEERTABLE_CREDENTIAL_FILE="$credential_file" "$(dirname "$0")/ensure-bridge.sh" "$proj" wakeup; then
|
|
749
750
|
echo "wakeup-bridge: TUI配達 起動確認済み"
|
|
750
751
|
else
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# 親(ベル等)が room へ着卓する。
|
|
3
|
-
# usage: parent-join.sh <project_dir> [name] [model] [effort] [
|
|
3
|
+
# usage: parent-join.sh <project_dir> [name] [model] [effort] [harness]
|
|
4
4
|
# name 既定は bell。broadcast廃止に伴いkickoff投稿は行わない。
|
|
5
|
-
# model / effort /
|
|
5
|
+
# model / effort / harness は任意。親はオーナーの対話セッション(決定40)なので、席と違って
|
|
6
6
|
# 起動時に確定した値を script が知らない——**渡された時だけ**参加者一覧の素性として登録する。
|
|
7
7
|
# 渡さなければ欄ごと出ない(「不明」ではなく「素性を名乗っていない」)。
|
|
8
|
-
#
|
|
8
|
+
# harness は claude(既定)、codex、または grok。model だけ渡して harness を渡さない場合は
|
|
9
9
|
# claude とみなす(後方互換)。
|
|
10
10
|
# 親は MCP を後付けできないので room へは HTTP API 直で入る(決定40 の operating notes)。
|
|
11
11
|
set -e
|
|
12
|
-
proj="$1"; name="${2:-bell}"; model="$3"; effort="$4";
|
|
12
|
+
proj="$1"; name="${2:-bell}"; model="$3"; effort="$4"; harness="$5"
|
|
13
13
|
mission=""
|
|
14
14
|
if [ "${6:-}" = "--mission" ]; then mission="${7:-}"; fi
|
|
15
15
|
state="$proj/.team/setup-state.json"
|
|
@@ -23,21 +23,22 @@ fi
|
|
|
23
23
|
|
|
24
24
|
# 親はAiterm席ではない。Claude/Codex/Grokとも、親自身が所有するparent-watchを配送先にする。
|
|
25
25
|
# tmux observeやCodex thread IDを登録すると、通常席bridge/外部resumeへ誤配送される。
|
|
26
|
-
|
|
27
|
-
if [ -z "$
|
|
26
|
+
parent_harness="$harness"
|
|
27
|
+
if [ -z "$parent_harness" ] && [ -n "$model" ]; then
|
|
28
28
|
case "$model" in
|
|
29
|
-
gpt-*|o[0-9]*)
|
|
30
|
-
grok*)
|
|
31
|
-
claude*|opus*|sonnet*|haiku*|fable*)
|
|
29
|
+
gpt-*|o[0-9]*) parent_harness=codex ;;
|
|
30
|
+
grok*) parent_harness=grok ;;
|
|
31
|
+
claude*|opus*|sonnet*|haiku*|fable*) parent_harness=claude ;;
|
|
32
32
|
esac
|
|
33
33
|
fi
|
|
34
|
-
member=$(python3 - "$name" "$model" "$effort" "$
|
|
34
|
+
member=$(python3 - "$name" "$model" "$effort" "$parent_harness" "$mission" <<'PY'
|
|
35
35
|
import json, sys
|
|
36
|
-
name, model, effort,
|
|
36
|
+
name, model, effort, harness, mission = sys.argv[1:6]
|
|
37
37
|
# 台帳には canonical 欄だけを登録する(settings / role 単数の重複欄は 2026-08-22 廃止)
|
|
38
38
|
body = {'name': name, 'roles': ['統括']}
|
|
39
|
-
if
|
|
40
|
-
body['
|
|
39
|
+
if harness:
|
|
40
|
+
body['harness'] = harness
|
|
41
|
+
body['vendor'] = harness # 旧版 room server 互換
|
|
41
42
|
if model:
|
|
42
43
|
body['model'] = model
|
|
43
44
|
if effort:
|
|
@@ -45,7 +46,7 @@ if effort:
|
|
|
45
46
|
if mission:
|
|
46
47
|
body['mission'] = mission
|
|
47
48
|
body['observe'] = None
|
|
48
|
-
body['delivery'] = {'kind': 'parent_watch', 'host':
|
|
49
|
+
body['delivery'] = {'kind': 'parent_watch', 'host': harness or ''}
|
|
49
50
|
print(json.dumps(body, ensure_ascii=False))
|
|
50
51
|
PY
|
|
51
52
|
)
|
|
@@ -75,11 +76,11 @@ echo "親の権限境界: 各ToDoのクローズは監査担当が行い、親
|
|
|
75
76
|
|
|
76
77
|
# parent-watchが初回headを固定する。以後のDMはwatcher不在時間を含め
|
|
77
78
|
# 永続cursorからcatch-upされる。host内のbackground task自体は親セッションだけが所有できる。
|
|
78
|
-
if PEERTABLE_PARENT_HOST="$
|
|
79
|
-
echo "parent-watch cursor ready: ${name}(host=${
|
|
80
|
-
if [ "$
|
|
79
|
+
if PEERTABLE_PARENT_HOST="$parent_harness" node "$here/parent-watch.mjs" "$proj" "$name" --prime; then
|
|
80
|
+
echo "parent-watch cursor ready: ${name}(host=${parent_harness})"
|
|
81
|
+
if [ "$parent_harness" = "codex" ]; then
|
|
81
82
|
echo "PARENT_WATCH_START_REQUIRED: Codex親のbackground taskで1秒ごとに ${here}/codex-parent-watch.sh ${proj} ${name} を都度実行し、空でないstdoutだけを親turnへnotifyすること。background taskはloopを続けるが、Node processや端末sessionは常駐させない"
|
|
82
|
-
elif [ "$
|
|
83
|
+
elif [ "$parent_harness" = "grok" ]; then
|
|
83
84
|
echo "PARENT_WATCH_START_REQUIRED: Grok Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること。通常席用wakeup-bridgeに親を載せない"
|
|
84
85
|
else
|
|
85
86
|
echo "PARENT_WATCH_START_REQUIRED: Claude Monitor(persistent)で ${here}/parent-watch.mjs ${proj} ${name} --follow を1回だけ起動し、通知後も同じMonitorで待機を続けること"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// 02_models.md の役割→1位〜3位から、着席可能な
|
|
2
|
+
// 02_models.md の役割→1位〜3位から、着席可能な harness / model / effort を解決する。
|
|
3
3
|
// 具体モデル名はここに持たない。台帳と順位表をその場で読む。
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
const EFFORT = /×\s*(none|low|medium|high|xhigh|max|ultra)/iu
|
|
10
|
-
const
|
|
10
|
+
const harnessOf = (modelName) => {
|
|
11
11
|
const name = String(modelName).trim()
|
|
12
12
|
if (/^claude\b/iu.test(name)) return 'claude'
|
|
13
13
|
if (/^gpt\b|^gpt-/iu.test(name)) return 'codex'
|
|
@@ -62,9 +62,9 @@ export function parseLedger(markdown) {
|
|
|
62
62
|
return rows.flatMap((row) => {
|
|
63
63
|
const name = row['モデル']
|
|
64
64
|
const slug = slugOf(row['slug'] ?? '')
|
|
65
|
-
const
|
|
66
|
-
if (!name || !slug || !
|
|
67
|
-
return [{ name, slug,
|
|
65
|
+
const harness = harnessOf(name)
|
|
66
|
+
if (!name || !slug || !harness) return []
|
|
67
|
+
return [{ name, slug, harness }]
|
|
68
68
|
})
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -103,7 +103,7 @@ export function listOfficialRoles(markdown) {
|
|
|
103
103
|
.filter(Boolean)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export function
|
|
106
|
+
export function harnessFromSlug(slug) {
|
|
107
107
|
const text = String(slug ?? '').trim().toLowerCase()
|
|
108
108
|
if (!text) return null
|
|
109
109
|
if (/^(gpt-|gpt\b|o[0-9])/u.test(text)) return 'codex'
|
|
@@ -125,8 +125,8 @@ const parseRoleList = (roles) => {
|
|
|
125
125
|
return list
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export function resolveSeatPlacement(role, markdown, { source = '',
|
|
129
|
-
const
|
|
128
|
+
export function resolveSeatPlacement(role, markdown, { source = '', harness = '' } = {}) {
|
|
129
|
+
const wantHarness = String(harness ?? '').trim()
|
|
130
130
|
const wanted = String(role ?? '').trim()
|
|
131
131
|
if (!wanted) {
|
|
132
132
|
return { error: 'SEAT_ROLE_REQUIRED', message: 'role が空(02_models の役割名が要る)' }
|
|
@@ -151,12 +151,12 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
|
|
|
151
151
|
dropped.push({ rank, reason: 'not-in-ledger', cell: parsed.cell })
|
|
152
152
|
continue
|
|
153
153
|
}
|
|
154
|
-
if (
|
|
155
|
-
dropped.push({ rank, reason: `
|
|
154
|
+
if (wantHarness && hit.harness !== wantHarness) {
|
|
155
|
+
dropped.push({ rank, reason: `harness-mismatch(want ${wantHarness})`, cell: parsed.cell })
|
|
156
156
|
continue
|
|
157
157
|
}
|
|
158
158
|
if (hit.slug === 'haiku') {
|
|
159
|
-
return { role: wanted, rank,
|
|
159
|
+
return { role: wanted, rank, harness: hit.harness, model: hit.slug, effort: '', source, dropped }
|
|
160
160
|
}
|
|
161
161
|
if (!parsed.effort) {
|
|
162
162
|
dropped.push({ rank, reason: 'effort-missing', cell: parsed.cell })
|
|
@@ -165,7 +165,7 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
|
|
|
165
165
|
return {
|
|
166
166
|
role: wanted,
|
|
167
167
|
rank,
|
|
168
|
-
|
|
168
|
+
harness: hit.harness,
|
|
169
169
|
model: hit.slug,
|
|
170
170
|
effort: parsed.effort,
|
|
171
171
|
source,
|
|
@@ -174,13 +174,13 @@ export function resolveSeatPlacement(role, markdown, { source = '', vendor = ''
|
|
|
174
174
|
}
|
|
175
175
|
return {
|
|
176
176
|
error: 'SEAT_PLACEMENT_UNRESOLVABLE',
|
|
177
|
-
message: `${wanted}${
|
|
177
|
+
message: `${wanted}${wantHarness ? `(harness=${wantHarness})` : ''} を着席可能な harness/model/effort へ解決できない`,
|
|
178
178
|
dropped,
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export function resolveSeatIdentity({
|
|
183
|
-
roles, model, effort,
|
|
183
|
+
roles, model, effort, harness, markdown, source = '', allowParentRole = false,
|
|
184
184
|
} = {}) {
|
|
185
185
|
const roleList = parseRoleList(roles)
|
|
186
186
|
if (roleList.length === 0) {
|
|
@@ -204,7 +204,7 @@ export function resolveSeatIdentity({
|
|
|
204
204
|
return {
|
|
205
205
|
roles: roleList,
|
|
206
206
|
settings: {
|
|
207
|
-
|
|
207
|
+
harness: String(harness ?? '').trim(),
|
|
208
208
|
model: '',
|
|
209
209
|
effort: String(effort ?? '').trim(),
|
|
210
210
|
},
|
|
@@ -214,29 +214,29 @@ export function resolveSeatIdentity({
|
|
|
214
214
|
|
|
215
215
|
const requestedModel = String(model ?? '').trim()
|
|
216
216
|
const requestedEffort = String(effort ?? '').trim()
|
|
217
|
-
const
|
|
217
|
+
const requestedHarness = String(harness ?? '').trim()
|
|
218
218
|
if (requestedModel) {
|
|
219
|
-
const
|
|
220
|
-
|| parseLedger(markdown).find((row) => row.slug === requestedModel)?.
|
|
221
|
-
if (!
|
|
219
|
+
const resolvedHarness = requestedHarness || harnessFromSlug(requestedModel)
|
|
220
|
+
|| parseLedger(markdown).find((row) => row.slug === requestedModel)?.harness
|
|
221
|
+
if (!resolvedHarness) {
|
|
222
222
|
return {
|
|
223
|
-
error: '
|
|
224
|
-
message: `model=${requestedModel} の
|
|
223
|
+
error: 'SEAT_HARNESS_UNRESOLVED',
|
|
224
|
+
message: `model=${requestedModel} の harness を推定できない(--harness が要る)`,
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
return {
|
|
228
228
|
roles: roleList,
|
|
229
|
-
settings: {
|
|
229
|
+
settings: { harness: resolvedHarness, model: requestedModel, effort: requestedEffort },
|
|
230
230
|
source,
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
const first = resolveSeatPlacement(roleList[0], markdown, { source,
|
|
234
|
+
const first = resolveSeatPlacement(roleList[0], markdown, { source, harness: requestedHarness })
|
|
235
235
|
if (first.error) return first
|
|
236
236
|
return {
|
|
237
237
|
roles: roleList,
|
|
238
238
|
settings: {
|
|
239
|
-
|
|
239
|
+
harness: first.harness,
|
|
240
240
|
model: first.model,
|
|
241
241
|
effort: requestedEffort || first.effort || '',
|
|
242
242
|
},
|
|
@@ -251,14 +251,15 @@ if (isMain) {
|
|
|
251
251
|
let roles = ''
|
|
252
252
|
let model = ''
|
|
253
253
|
let effort = ''
|
|
254
|
-
let
|
|
254
|
+
let harness = ''
|
|
255
255
|
const positional = []
|
|
256
256
|
for (let i = 0; i < args.length; i += 1) {
|
|
257
257
|
const arg = args[i]
|
|
258
258
|
if (arg === '--roles') { roles = args[++i] ?? ''; continue }
|
|
259
259
|
if (arg === '--model') { model = args[++i] ?? ''; continue }
|
|
260
260
|
if (arg === '--effort') { effort = args[++i] ?? ''; continue }
|
|
261
|
-
|
|
261
|
+
// --vendor は旧名の互換 alias
|
|
262
|
+
if (arg === '--harness' || arg === '--vendor') { harness = args[++i] ?? ''; continue }
|
|
262
263
|
positional.push(arg)
|
|
263
264
|
}
|
|
264
265
|
if (!roles) roles = positional[0] ?? ''
|
|
@@ -270,7 +271,7 @@ if (isMain) {
|
|
|
270
271
|
fail('SEAT_MODELS_DOC_MISSING', `02_models.md が無い: ${doc}`)
|
|
271
272
|
}
|
|
272
273
|
const result = resolveSeatIdentity({
|
|
273
|
-
roles, model, effort,
|
|
274
|
+
roles, model, effort, harness, markdown: readFileSync(doc, 'utf8'), source: doc,
|
|
274
275
|
})
|
|
275
276
|
if (result.error) fail(result.error, result.message)
|
|
276
277
|
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
@@ -268,7 +268,7 @@ export function isPaneProcessStopped(panePid, exec = execFileSync) {
|
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* paneのstatus行が公開しているtoken値だけを読む。
|
|
271
|
-
*
|
|
271
|
+
* harness固有のログや課金単価は推測せず、表示が無い席はnullのままにする。
|
|
272
272
|
*/
|
|
273
273
|
export function parsePaneTokenHint(pane) {
|
|
274
274
|
if (typeof pane !== 'string') return null
|
|
@@ -136,7 +136,7 @@ process.on('SIGTERM', cleanup)
|
|
|
136
136
|
process.on('SIGINT', cleanup)
|
|
137
137
|
|
|
138
138
|
// 席ごとに未配達を溜めて、2秒ごとにまとめて1回起こす(連投で席を何度も起こさない)。
|
|
139
|
-
// pending は room message の宛先名から作る。起動引数や
|
|
139
|
+
// pending は room message の宛先名から作る。起動引数や harness は候補集合を決めない。
|
|
140
140
|
const pending = new Map() // name -> Map<seq, message>
|
|
141
141
|
const deliveryStates = new Map() // seq -> { message, targets, delivered }
|
|
142
142
|
let seats = []
|
|
@@ -310,7 +310,7 @@ async function wake(seat, msgs) {
|
|
|
310
310
|
}
|
|
311
311
|
const pane = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
|
|
312
312
|
const screen = String(pane.stdout ?? '')
|
|
313
|
-
if (member.vendor === 'codex') {
|
|
313
|
+
if ((member.harness ?? member.vendor) === 'codex') {
|
|
314
314
|
const dialog = keysForCodexPane(screen)
|
|
315
315
|
if (dialog) {
|
|
316
316
|
for (const key of dialog.keys) {
|
|
@@ -320,9 +320,9 @@ async function wake(seat, msgs) {
|
|
|
320
320
|
return 'deferred'
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
-
if (member.vendor === 'grok') {
|
|
323
|
+
if ((member.harness ?? member.vendor) === 'grok') {
|
|
324
324
|
const tail = screen.split('\n').slice(-14).join('\n')
|
|
325
|
-
if (shouldDeferGrokWake(member.vendor, tail)) {
|
|
325
|
+
if (shouldDeferGrokWake((member.harness ?? member.vendor), tail)) {
|
|
326
326
|
if (!deferredBusy.has(seat)) {
|
|
327
327
|
log(`Grok席が実行中なのでidleまで待つ: ${seat} ← ${msgs.length} 件`)
|
|
328
328
|
deferredBusy.add(seat)
|
|
@@ -352,7 +352,7 @@ async function wake(seat, msgs) {
|
|
|
352
352
|
// ターンを中断**していた(実被弾 2026-08-22: 監査席が2秒ごとに中断され、監査が進まなかった)。
|
|
353
353
|
// Claude 席は「受理された兆候」を成功とみなし、回復キーに Escape を使わない。
|
|
354
354
|
const CLAUDE_ACCEPTED = /paste again to expand|\[Pasted text|esc to interrupt|✻|✽|✳|·\s*\w+ing…/u
|
|
355
|
-
const isClaude = member.vendor === 'claude'
|
|
355
|
+
const isClaude = (member.harness ?? member.vendor) === 'claude'
|
|
356
356
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
357
357
|
await sleep(1200)
|
|
358
358
|
const after = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
|
|
@@ -36,8 +36,8 @@ export function isWakeupBridgeTarget(member, options = {}) {
|
|
|
36
36
|
&& observe.tmux_target,
|
|
37
37
|
)
|
|
38
38
|
if (hasPane) return true
|
|
39
|
-
const
|
|
40
|
-
return
|
|
39
|
+
const harness = member.harness ?? member.vendor // 旧server互換
|
|
40
|
+
return harness === 'codex' || harness === 'grok'
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -62,8 +62,8 @@ export function isIdleSelfWake(msg) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** Grok 既定はキュー投入。busy 中に積むと今のターンへ混ざらない。 */
|
|
65
|
-
export function shouldDeferGrokWake(
|
|
66
|
-
if (
|
|
65
|
+
export function shouldDeferGrokWake(harness, tail) {
|
|
66
|
+
if (harness !== 'grok') return false
|
|
67
67
|
if (typeof tail !== 'string') return false
|
|
68
68
|
if (classifyPaneTail(tail) === 'busy') return true
|
|
69
69
|
if (tail.includes('send a message to interrupt')) return true
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
|
|
35
35
|
## 着卓手順
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
登録する。`
|
|
37
|
+
harness に関わらず: `scripts/parent-join.sh <project> [name] [model] [effort] [harness]` で member
|
|
38
|
+
登録する。`harness` は `claude`(既定)、`codex`、または `grok`。Lattice 併用モードなら、
|
|
39
39
|
`source .team/parent-env.sh` で Lattice mutation(`todo reopen` 等)に要る actor 環境変数
|
|
40
40
|
(`LATTICE_TODO_ACTOR_HOST`/`SESSION`/`AGENT`)を親 shell へ持続配線する——**子 process の
|
|
41
41
|
export は親 shell に伝播しないため**、これをしないまま `lattice todo reopen` 等を打つと
|
|
42
42
|
`ACTOR_UNRESOLVED`(`missing_environment=[...]`)で無変更停止する(実測: owner裁定[46]④)。
|
|
43
43
|
|
|
44
|
-
## 新着の検知(room追従は共通、親への通知だけ
|
|
44
|
+
## 新着の検知(room追従は共通、親への通知だけharness別)
|
|
45
45
|
|
|
46
46
|
`scripts/parent-watch.mjs <project> <親名>` がroom SSE、heartbeat、再接続catch-up、宛先判定、
|
|
47
47
|
永続cursorを一括所有する。stdoutへ出る`peertable.parent-watch-event.v1`はDM本文そのものであり、
|
|
@@ -129,7 +129,7 @@ peertable_parent_post() {
|
|
|
129
129
|
3. 工程正本で照合する(Lattice 併用: `lattice todo status --json`。単独: `.team/tasks.md` と
|
|
130
130
|
room ログの突き合わせ)。食い違ったら工程正本が正で、食い違い自体を room へ出す
|
|
131
131
|
4. member 登録は残っているので `parent-join.sh` を再実行しない。名前を確認するだけでよい
|
|
132
|
-
5.
|
|
132
|
+
5. harnessに応じて番犬を張り直す。ClaudeとGrokは旧Monitorを止めて`--follow`を1回起動する。Codexは旧background
|
|
133
133
|
taskを止め、1秒ごとの`--poll` loopを起動する。永続cursorが不在時間のDMをcatch-upする
|
|
134
134
|
6. 順序の要点は「room と工程正本を読み終えるまで発言しない」
|
|
135
135
|
|