peertable 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/package.json +1 -1
- package/room/client.mjs +7 -5
- 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/alarm-bridge.mjs +117 -0
- package/skill/scripts/alarm-set.sh +22 -0
- package/skill/scripts/change-seat.sh +25 -25
- package/skill/scripts/ensure-bridge.sh +1 -1
- package/skill/scripts/ensure-codex-room-mcp.mjs +2 -2
- package/skill/scripts/launch-seat.sh +39 -34
- package/skill/scripts/parent-join.sh +19 -18
- package/skill/scripts/resolve-seat-placement.mjs +28 -27
- package/skill/scripts/seat-status-bridge.mjs +34 -0
- package/skill/scripts/seat-usage.mjs +1 -1
- package/skill/scripts/setup.sh +5 -0
- package/skill/scripts/wakeup-bridge.mjs +47 -8
- package/skill/scripts/wakeup-delivery.mjs +4 -4
- package/skill/templates/member.md +3 -1
- 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,12 +204,14 @@ 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,
|
|
211
214
|
mission: process.env.PEERTABLE_MISSION,
|
|
212
|
-
aiterm_session_id: process.env.AITERM_SESSION_ID,
|
|
213
215
|
observe,
|
|
214
216
|
}).filter(([, v]) => v != null && v !== '' && !(Array.isArray(v) && v.length === 0)))
|
|
215
217
|
{
|
|
@@ -221,7 +223,7 @@ const IDENTITY = Object.fromEntries(Object.entries({
|
|
|
221
223
|
roles,
|
|
222
224
|
model: process.env.PEERTABLE_MODEL,
|
|
223
225
|
effort: process.env.PEERTABLE_EFFORT,
|
|
224
|
-
|
|
226
|
+
harness: process.env.PEERTABLE_HARNESS ?? process.env.PEERTABLE_VENDOR,
|
|
225
227
|
markdown: readFileSync(modelsDoc, 'utf8'),
|
|
226
228
|
})
|
|
227
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))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 目覚まし係(2026-08-22 オーナー設計)。AI ではない常駐スクリプト。
|
|
3
|
+
//
|
|
4
|
+
// 席は「待機からの解放条件を判定するスクリプト」を登録してから寝る(alarm-set.sh)。
|
|
5
|
+
// 本常駐が高頻度でそのスクリプトを回し、exit 0 になったら登録席へ
|
|
6
|
+
// 「[待機解放条件成立] <note>」の DM を room へ置く(配達は wakeup-bridge の正規経路)。
|
|
7
|
+
// 条件の約束が席の揮発文脈でなく装置に住むので、依頼相手の死・レース負け・宛先不在・
|
|
8
|
+
// 外部/時刻条件・相互待機のどれでも待機が永眠しない。
|
|
9
|
+
//
|
|
10
|
+
// usage: alarm-bridge.mjs <project_dir> 起動(nohup で常駐)
|
|
11
|
+
// alarm-bridge.mjs <project_dir> --stop 停止
|
|
12
|
+
//
|
|
13
|
+
// 登録: .team/alarms/<id>.json
|
|
14
|
+
// { "seat": "ren", "note": "転写job j42 完了", "script": "test -f ~/asr/jobs/j42/result.json",
|
|
15
|
+
// "interval_s": 10, "created_at": "..." }
|
|
16
|
+
// スクリプトは bash -c で実行される(timeout 30秒)。exit 0 = 条件成立。
|
|
17
|
+
// 成立したら DM を置いて登録 file を削除する。スクリプトのエラーは成立と区別してログへ出す。
|
|
18
|
+
import { execFile } from 'node:child_process'
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
import { promisify } from 'node:util'
|
|
22
|
+
|
|
23
|
+
import { resolvePostToken } from './seat-usage.mjs'
|
|
24
|
+
import { observePidCommand } from './seat-identity.mjs'
|
|
25
|
+
|
|
26
|
+
const run = promisify(execFile)
|
|
27
|
+
const [proj, flag] = process.argv.slice(2)
|
|
28
|
+
if (!proj) { console.error('usage: alarm-bridge.mjs <project_dir> [--stop]'); process.exit(1) }
|
|
29
|
+
|
|
30
|
+
const team = join(proj, '.team')
|
|
31
|
+
const alarmsDir = join(team, 'alarms')
|
|
32
|
+
const record = join(team, 'alarm-bridge.json')
|
|
33
|
+
const alive = pid => { try { process.kill(pid, 0); return true } catch { return false } }
|
|
34
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
35
|
+
const log = line => console.error(`[${new Date().toISOString()}] ${line}`)
|
|
36
|
+
|
|
37
|
+
// 停止・再認証は run-bridge と同じ作法(pid+lstart 照合。pid 再利用先を殺さない)
|
|
38
|
+
function stopRecorded(strict) {
|
|
39
|
+
if (!existsSync(record)) { if (strict) log('起動記録が無い(既に停止)'); return }
|
|
40
|
+
const stored = JSON.parse(readFileSync(record, 'utf8'))
|
|
41
|
+
let facts = null
|
|
42
|
+
try { facts = observePidCommand(stored.pid) } catch { /* 観測不能=死亡扱い */ }
|
|
43
|
+
const same = facts && stored.start_identity !== undefined
|
|
44
|
+
&& facts.started_identity === stored.start_identity && facts.argv.includes('alarm-bridge.mjs')
|
|
45
|
+
if (!same) { unlinkSync(record); log(`死んだ記録を掃除した(pid ${stored.pid})`); return }
|
|
46
|
+
process.kill(stored.pid, 'SIGTERM')
|
|
47
|
+
log(`前の目覚まし係を停止した(pid ${stored.pid})`)
|
|
48
|
+
try { unlinkSync(record) } catch { /* 本人のcleanupと競合してよい */ }
|
|
49
|
+
}
|
|
50
|
+
stopRecorded(flag === '--stop')
|
|
51
|
+
if (flag === '--stop') process.exit(0)
|
|
52
|
+
|
|
53
|
+
const setup = JSON.parse(readFileSync(join(team, 'setup-state.json'), 'utf8'))
|
|
54
|
+
const { room, server_url: url } = setup
|
|
55
|
+
const token = resolvePostToken(process.env)
|
|
56
|
+
if (!token) { console.error('ALARM_BRIDGE_TOKEN_MISSING: 書込トークンが無い'); process.exit(1) }
|
|
57
|
+
mkdirSync(alarmsDir, { recursive: true })
|
|
58
|
+
|
|
59
|
+
const self = observePidCommand(process.pid)
|
|
60
|
+
writeFileSync(record, JSON.stringify({
|
|
61
|
+
pid: process.pid, start_identity: self.started_identity, room, server_url: url,
|
|
62
|
+
started_at: new Date().toISOString(), ready_at: new Date().toISOString(),
|
|
63
|
+
}) + '\n')
|
|
64
|
+
const cleanup = () => { try { unlinkSync(record) } catch {} process.exit(0) }
|
|
65
|
+
process.on('SIGTERM', cleanup)
|
|
66
|
+
process.on('SIGINT', cleanup)
|
|
67
|
+
|
|
68
|
+
async function wake(seat, note) {
|
|
69
|
+
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/messages`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json', 'X-Peertable-Token': token },
|
|
72
|
+
body: JSON.stringify({ from: 'alarm', to: seat, body: `[待機解放条件成立] ${note}` }),
|
|
73
|
+
})
|
|
74
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nextDue = new Map() // file -> epoch_ms
|
|
78
|
+
async function tick() {
|
|
79
|
+
let entries
|
|
80
|
+
try { entries = readdirSync(alarmsDir).filter(f => f.endsWith('.json')) } catch { return }
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const file = join(alarmsDir, entry)
|
|
83
|
+
const due = nextDue.get(file) ?? 0
|
|
84
|
+
if (Date.now() < due) continue
|
|
85
|
+
let reg
|
|
86
|
+
try { reg = JSON.parse(readFileSync(file, 'utf8')) } catch { continue } // 書込途中は次周へ
|
|
87
|
+
if (typeof reg.seat !== 'string' || typeof reg.script !== 'string' || !reg.seat || !reg.script) {
|
|
88
|
+
log(`ALARM_REGISTRATION_INVALID: ${entry}(seat/script が無い)を撤去する`)
|
|
89
|
+
try { unlinkSync(file) } catch {}
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
nextDue.set(file, Date.now() + Math.max(2, Number(reg.interval_s) || 10) * 1000)
|
|
93
|
+
let met = false
|
|
94
|
+
try {
|
|
95
|
+
await run('bash', ['-c', reg.script], { timeout: 30_000, maxBuffer: 1024 * 1024 })
|
|
96
|
+
met = true
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// exit 非0 = 条件未成立(正常)。timeout/spawn 失敗はログへ(成立と区別し、握りつぶさない)
|
|
99
|
+
if (e.killed || e.code === 'ENOENT') log(`条件スクリプトの実行失敗(${entry}): ${e.message}`)
|
|
100
|
+
}
|
|
101
|
+
if (!met) continue
|
|
102
|
+
try {
|
|
103
|
+
await wake(reg.seat, reg.note || entry)
|
|
104
|
+
unlinkSync(file)
|
|
105
|
+
nextDue.delete(file)
|
|
106
|
+
log(`起床: ${reg.seat} ← ${reg.note || entry}`)
|
|
107
|
+
} catch (e) {
|
|
108
|
+
log(`起床の投函に失敗(次周期に再試行): ${e.message}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
log(`alarm-bridge start: room=${room} project=${proj} pid=${process.pid}`)
|
|
114
|
+
for (;;) {
|
|
115
|
+
try { await tick() } catch (e) { log(`tick 失敗(続行): ${e.message}`) }
|
|
116
|
+
await sleep(2000)
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 目覚まし係へ待機解放条件を登録する(席が待機に入る前に使う)。
|
|
3
|
+
# usage: alarm-set.sh <project_dir> <seat> <note> <script...>
|
|
4
|
+
# script は bash -c で実行され、exit 0 で条件成立とみなされる。
|
|
5
|
+
# **script はシングルクォートで渡すこと**。ダブルクォートだと `$()` や変数が登録側の
|
|
6
|
+
# shell で先に展開され、判定コマンドでなく展開結果が登録される(かえで実測 2026-08-22)。
|
|
7
|
+
# 成立すると席へ「[待機解放条件成立] <note>」が配達され、登録は自動で消える。
|
|
8
|
+
set -eu
|
|
9
|
+
proj="${1:?project_dir}"; seat="${2:?seat}"; note="${3:?note}"; shift 3
|
|
10
|
+
script="$*"
|
|
11
|
+
[ -n "$script" ] || { echo "ALARM_SET_SCRIPT_REQUIRED: 条件スクリプトが空" >&2; exit 2; }
|
|
12
|
+
dir="$proj/.team/alarms"
|
|
13
|
+
mkdir -p "$dir"
|
|
14
|
+
id="$(date +%s)-$$"
|
|
15
|
+
python3 - "$dir/$id.json" "$seat" "$note" "$script" <<'PY'
|
|
16
|
+
import json, sys
|
|
17
|
+
out, seat, note, script = sys.argv[1:5]
|
|
18
|
+
with open(out, 'w') as f:
|
|
19
|
+
json.dump({'seat': seat, 'note': note, 'script': script, 'interval_s': 10,
|
|
20
|
+
'created_at': __import__('datetime').datetime.utcnow().isoformat() + 'Z'}, f, ensure_ascii=False)
|
|
21
|
+
PY
|
|
22
|
+
echo "ALARM_SET_OK: ${dir}/${id}.json(seat=${seat} note=${note})"
|
|
@@ -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
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
5
|
proj="$1"; name="$2"; shift 2
|
|
6
|
-
case "$name" in seat-status) script="seat-status-bridge.mjs" ;; wakeup) script="wakeup-bridge.mjs" ;; run) echo "ENSURE_BRIDGE_RETIRED: run-bridge は退役した(2026-08-22)。介入は席が自分の Lattice コマンド応答で受け取る" >&2; exit 1 ;; *) echo "usage: ensure-bridge.sh <project> <seat-status|wakeup> [args...]" >&2; exit 1 ;; esac
|
|
6
|
+
case "$name" in seat-status) script="seat-status-bridge.mjs" ;; wakeup) script="wakeup-bridge.mjs" ;; alarm) script="alarm-bridge.mjs" ;; run) echo "ENSURE_BRIDGE_RETIRED: run-bridge は退役した(2026-08-22)。介入は席が自分の Lattice コマンド応答で受け取る" >&2; exit 1 ;; *) echo "usage: ensure-bridge.sh <project> <seat-status|wakeup|alarm> [args...]" >&2; exit 1 ;; esac
|
|
7
7
|
team="$proj/.team"; record="$team/$name-bridge.json"; log="$team/$name-bridge.log"
|
|
8
8
|
force=false
|
|
9
9
|
if [ "${1:-}" = "--force" ]; then force=true; shift; fi
|
|
@@ -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) {
|