peertable 0.7.1 → 0.8.1
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 +7 -7
- package/README.md +7 -7
- package/package.json +1 -1
- package/room/client.mjs +10 -3
- package/room/server.mjs +13 -3
- package/skill/SKILL.md +3 -3
- package/skill/scripts/aiterm-launch.mjs +20 -1
- package/skill/scripts/doctor.sh +22 -0
- package/skill/scripts/platform/windows/normalize-read-line.mjs +7 -0
- package/skill/scripts/platform/windows/write-setup-state.mjs +31 -0
- package/skill/scripts/resolve-seat-placement.mjs +7 -2
- package/skill/scripts/seat-status-bridge.mjs +111 -2
- package/skill/scripts/seat-usage.mjs +72 -0
- package/skill/scripts/setup.sh +10 -2
- package/skill/scripts/teardown.sh +9 -0
- package/skill/scripts/tmux-at.bash +10 -10
- package/skill/scripts/wakeup-bridge.mjs +35 -1
- package/skill/templates/member.md +12 -2
package/README.ja.md
CHANGED
|
@@ -95,11 +95,11 @@ claude --dangerously-load-development-channels server:room
|
|
|
95
95
|
|
|
96
96
|
**`--mcp-config` で渡してはいけない。** channels はその経路の MCP server を解決せず、バナーに `server:room · no MCP server configured with that name` が出て**room の配達だけが黙って死ぬ**(Claude Code v2.1.226 で実測・決定44)。スキルを使えば自動で置かれ、teardown で戻る。
|
|
97
97
|
|
|
98
|
-
Codex では、スキルが所有する room MCP block をプロジェクトの `.codex/config.toml` へ置く。`.mcp.json` だけは Codex の設定入口にならず、席固有のroom環境も同じスキル起動経路が渡す。Grok Buildはproject rootの`.mcp.json`を読み、Aitermの`grok_agent`からmodel・effort・席固有envを受け取る。CodexとGrokの新着は同じ経路で席の TUI へ入る。Codexは即送信(ターン中のsteering)。Grok TUIはターン中の素送信を次のuserターンへ積むので、配達はidleを待ってから送る。親は通常席の TUI 配達に載せない——ClaudeとGrok親は`parent-watch --follow`、Codex親はpoll。
|
|
99
|
-
|
|
100
|
-
Windows工場hostはPowerShell 7(`pwsh.exe`)を前提とし、5.1しかなければMicrosoft公式installer/package managerで7を導入してから使う。永続PTYはAitermが所有し、psmuxはそのWindows backendであってshellではない。Peertableに残るmux直接観測はAiterm公開APIへ移行中であり、psmuxを一般の製品前提にはしない。
|
|
101
|
-
|
|
102
|
-
既存roomの`resume.sh`は、最初にPeertable所有generated assetとroot room MCP blockを現行package treeへ更新する。利用者が先に持っていた`.mcp.json`は書き換えず、room blockの明示mergeを要求する。
|
|
98
|
+
Codex では、スキルが所有する room MCP block をプロジェクトの `.codex/config.toml` へ置く。`.mcp.json` だけは Codex の設定入口にならず、席固有のroom環境も同じスキル起動経路が渡す。Grok Buildはproject rootの`.mcp.json`を読み、Aitermの`grok_agent`からmodel・effort・席固有envを受け取る。CodexとGrokの新着は同じ経路で席の TUI へ入る。Codexは即送信(ターン中のsteering)。Grok TUIはターン中の素送信を次のuserターンへ積むので、配達はidleを待ってから送る。親は通常席の TUI 配達に載せない——ClaudeとGrok親は`parent-watch --follow`、Codex親はpoll。
|
|
99
|
+
|
|
100
|
+
Windows工場hostはPowerShell 7(`pwsh.exe`)を前提とし、5.1しかなければMicrosoft公式installer/package managerで7を導入してから使う。永続PTYはAitermが所有し、psmuxはそのWindows backendであってshellではない。Peertableに残るmux直接観測はAiterm公開APIへ移行中であり、psmuxを一般の製品前提にはしない。
|
|
101
|
+
|
|
102
|
+
既存roomの`resume.sh`は、最初にPeertable所有generated assetとroot room MCP blockを現行package treeへ更新する。利用者が先に持っていた`.mcp.json`は書き換えず、room blockの明示mergeを要求する。
|
|
103
103
|
|
|
104
104
|
**3. あるいはスキルに全部やらせる** — `skill/` を `~/.claude/skills/peertable` にリンクして、セッションに一言:
|
|
105
105
|
|
|
@@ -113,9 +113,9 @@ Windows工場hostはPowerShell 7(`pwsh.exe`)を前提とし、5.1しかな
|
|
|
113
113
|
|
|
114
114
|
動いており、**自分自身の開発に使っている**。2026-08-08 に end-to-end 検証済み——オーケストレーターなしの完全な一周(2 メンバーが相談し、claim し、インターフェースを交渉し、見つけた罠を共有して小さなプロジェクトを出荷)を**外部介入ゼロ**で完走。2026-08-13の実席ライフサイクルでは、作業席が親を通じてsession contextを保ったままmodel / effortを変更し、再起動後はroomと工程正本から再着任した。2026-08-14にはGrok 4.6席の着席、room参加、同一sessionの4.6↔4.5変更、DM起床を実機で確認した。2026-08-17にGrok席はidle待ち、broadcastは本文を残し、tmuxの無い親でbridge cursorが止まらないよう直した。
|
|
115
115
|
|
|
116
|
-
現在のnpm releaseは **peertable 0.7.1**。
|
|
116
|
+
現在のnpm releaseは **peertable 0.7.1**。
|
|
117
117
|
|
|
118
|
-
設計文書と決定履歴(**106 決定**)は [docs/plan.md](docs/plan.md)。
|
|
118
|
+
設計文書と決定履歴(**106 決定**)は [docs/plan.md](docs/plan.md)。
|
|
119
119
|
|
|
120
120
|
Claude Code channels はリサーチプレビューのため、フラグ・プロトコルは変わりうる。
|
|
121
121
|
|
package/README.md
CHANGED
|
@@ -133,11 +133,11 @@ claude --dangerously-load-development-channels server:room
|
|
|
133
133
|
|
|
134
134
|
The member gets five tools — `post`, `read_unread`, `read_log`, `members`, `delivery_status` — and a channel that wakes it whenever teammates address it. `post` returns `room_saved` plus a per-recipient `delivery` breakdown (delivered / pending / seat_unavailable / bridge_unavailable / failed): saving to the room is not the same fact as reaching a seat's TUI, and `delivered` is only ever written by the wakeup bridge after the injection actually lands. `members` includes each seat's server-computed effective status (fresh / stale / bridge down / auth failed) and bridge health. (`--dangerously-load-development-channels` is required while channels are in research preview; custom channels aren't on the allowlist yet.)
|
|
135
135
|
|
|
136
|
-
For Codex, the skill instead installs its owned room MCP block in the project's `.codex/config.toml`; `.mcp.json` alone is not a Codex configuration path. Grok Build reads the project-root `.mcp.json`. Aiterm's `grok_agent` supplies its model, reasoning effort, and seat-specific environment; Codex and Grok receive arrivals through the same wake-up bridge. The bridge sends Codex immediately (mid-turn steering). Grok's TUI queues mid-turn paste as the *next* user turn, so the bridge waits until that seat is idle. The parent hat is never a wake-up target — Claude and Grok parents use `parent-watch --follow`; Codex parents poll.
|
|
137
|
-
|
|
138
|
-
On Windows factory hosts, PowerShell 7 (`pwsh.exe`) is required; install it through Microsoft's official installer or package manager before operating the table. Aiterm owns persistent PTYs and uses psmux as its Windows backend. psmux is a terminal/session multiplexer, not a shell. Peertable is migrating its remaining legacy mux observations to Aiterm's public API and does not make psmux a general product prerequisite.
|
|
139
|
-
|
|
140
|
-
`resume.sh` first upgrades Peertable-owned generated assets and the Peertable-owned root room MCP block to the current package tree. A project-owned pre-existing `.mcp.json` is never rewritten; merge its room block explicitly.
|
|
136
|
+
For Codex, the skill instead installs its owned room MCP block in the project's `.codex/config.toml`; `.mcp.json` alone is not a Codex configuration path. Grok Build reads the project-root `.mcp.json`. Aiterm's `grok_agent` supplies its model, reasoning effort, and seat-specific environment; Codex and Grok receive arrivals through the same wake-up bridge. The bridge sends Codex immediately (mid-turn steering). Grok's TUI queues mid-turn paste as the *next* user turn, so the bridge waits until that seat is idle. The parent hat is never a wake-up target — Claude and Grok parents use `parent-watch --follow`; Codex parents poll.
|
|
137
|
+
|
|
138
|
+
On Windows factory hosts, PowerShell 7 (`pwsh.exe`) is required; install it through Microsoft's official installer or package manager before operating the table. Aiterm owns persistent PTYs and uses psmux as its Windows backend. psmux is a terminal/session multiplexer, not a shell. Peertable is migrating its remaining legacy mux observations to Aiterm's public API and does not make psmux a general product prerequisite.
|
|
139
|
+
|
|
140
|
+
`resume.sh` first upgrades Peertable-owned generated assets and the Peertable-owned root room MCP block to the current package tree. A project-owned pre-existing `.mcp.json` is never rewritten; merge its room block explicitly.
|
|
141
141
|
|
|
142
142
|
**3. Or let the skill do all of it** — link `skill/` as `~/.claude/skills/peertable`, then tell your session:
|
|
143
143
|
|
|
@@ -149,9 +149,9 @@ It interviews you, names the members, scaffolds `.team/` (charter + roles, isola
|
|
|
149
149
|
|
|
150
150
|
Working, and used to build itself. First verified end-to-end on 2026-08-08 with a full no-orchestrator loop: two members consulted, claimed, negotiated an interface, shared a discovered pitfall, and shipped a small project with **zero external intervention**. A 2026-08-13 real-seat lifecycle verified in-place model/effort changes and restart recovery. On 2026-08-14, a Grok 4.6 seat joined the room, changed 4.6↔4.5 in the same session, and woke on a direct message in a live acceptance run. On 2026-08-17 the wake-up path was corrected so Grok seats wait for idle, broadcasts keep their body, and a parent without a tmux seat cannot stall the bridge cursor.
|
|
151
151
|
|
|
152
|
-
The current npm release is **peertable 0.7.1**.
|
|
152
|
+
The current npm release is **peertable 0.7.1**.
|
|
153
153
|
|
|
154
|
-
The design document and decision log (**106 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
|
|
154
|
+
The design document and decision log (**106 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
|
|
155
155
|
|
|
156
156
|
Depends on Claude Code **channels**, currently a research preview — flags and protocol may change.
|
|
157
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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.8.1'
|
|
17
17
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
18
|
|
|
19
19
|
const USAGE = `usage:
|
|
@@ -63,6 +63,9 @@ const relevant = m => Array.isArray(m.to_names)
|
|
|
63
63
|
: m.to === ME || (m.to === 'all' && m.from !== ME)
|
|
64
64
|
|
|
65
65
|
let cursor = 0 // read_unread 用。参加時点から数える
|
|
66
|
+
// 読了ack専用。read_unreadが実際にメッセージを返した時だけ進む(参加時の末尾代入では進まない)。
|
|
67
|
+
// cursorとの分離は反証3(2026-08-25): 再接続直後のcursorは「未読の起点」であって読了の証明ではない。
|
|
68
|
+
let ackSeq = 0
|
|
66
69
|
|
|
67
70
|
const mcp = new Server(
|
|
68
71
|
{ name: 'room', version: MCP_VERSION },
|
|
@@ -159,7 +162,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
159
162
|
const args = req.params.arguments ?? {}
|
|
160
163
|
switch (req.params.name) {
|
|
161
164
|
case 'post': {
|
|
162
|
-
|
|
165
|
+
// read_seq: read_unreadが実際に返した最終seqだけを相乗り申告する(読了ack・2026-08-25)。
|
|
166
|
+
// 「読んだ後にターンが生きて投稿まで到達した」の証明であり、wakeup-bridgeはこの値以下を再配達しない。
|
|
167
|
+
// cursorは流用しない——cursorは参加時に末尾seqへ代入される未読起点であり、再接続直後に
|
|
168
|
+
// 「読んでいないものを読了申告」して配達待ちの未読を捨てる(反証3・2026-08-25)。
|
|
169
|
+
const r = await fetch(api('messages'), { method: 'POST', headers, body: JSON.stringify({ from: ME, to: args.to, body: args.message, ...(ackSeq > 0 ? { read_seq: ackSeq } : {}) }) })
|
|
163
170
|
const msg = await r.json()
|
|
164
171
|
if (!r.ok) return { isError: true, ...text(`送信失敗: ${JSON.stringify(msg)}`) }
|
|
165
172
|
// cursor は触らない。自分の発言は relevant で除外されるので進める必要が無く、
|
|
@@ -176,7 +183,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
176
183
|
}
|
|
177
184
|
case 'read_unread': {
|
|
178
185
|
const { messages } = await (await fetch(api(`messages?since=${cursor}`))).json()
|
|
179
|
-
if (messages.length) cursor = messages[messages.length - 1].seq
|
|
186
|
+
if (messages.length) { cursor = messages[messages.length - 1].seq; ackSeq = cursor }
|
|
180
187
|
const mine = messages.filter(relevant)
|
|
181
188
|
const roster = await rosterText()
|
|
182
189
|
return text(mine.length ? `${roster}\n${mine.map(fmt).join('\n')}` : `${roster}\n未読なし`)
|
package/room/server.mjs
CHANGED
|
@@ -71,13 +71,14 @@ db.exec(`
|
|
|
71
71
|
{
|
|
72
72
|
const columns = db.prepare('PRAGMA table_info(members)').all().map((c) => c.name)
|
|
73
73
|
if (!columns.includes('harness')) db.exec('ALTER TABLE members ADD COLUMN harness TEXT')
|
|
74
|
+
if (!columns.includes('read_seq')) db.exec('ALTER TABLE members ADD COLUMN read_seq INTEGER')
|
|
74
75
|
if (columns.includes('vendor')) db.exec('UPDATE members SET harness = vendor WHERE harness IS NULL')
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
const MEMBER_COLUMNS = [
|
|
78
79
|
'joined_at', 'harness', 'model', 'effort', 'roles', 'mission', 'delivery', 'observe',
|
|
79
80
|
'aiterm_session_id', 'status', 'status_at', 'busy_since', 'pane_token_hint', 'usage_source',
|
|
80
|
-
'pid', 'started_identity', 'argv_digest', 'identity_recorded_at',
|
|
81
|
+
'pid', 'started_identity', 'argv_digest', 'identity_recorded_at', 'read_seq',
|
|
81
82
|
]
|
|
82
83
|
const MEMBER_JSON_COLUMNS = new Set(['roles', 'delivery', 'observe'])
|
|
83
84
|
|
|
@@ -110,7 +111,8 @@ function putMember(roomName, member) {
|
|
|
110
111
|
|
|
111
112
|
// 旧形式(settings/role の重複欄・旧 vendor 欄)を canonical へ畳む。台帳には canonical だけを置く。
|
|
112
113
|
function normalizeMemberMeta(meta) {
|
|
113
|
-
|
|
114
|
+
// read_seq はserver所有欄。正規経路(POST /messages の相乗りack)以外から書かせない(反証4)
|
|
115
|
+
const { role, settings, vendor, read_seq: _readSeq, ...rest } = meta
|
|
114
116
|
const out = { ...rest }
|
|
115
117
|
if (out.roles === undefined && role) out.roles = [role]
|
|
116
118
|
if (out.harness === undefined && vendor !== undefined) out.harness = vendor
|
|
@@ -407,7 +409,15 @@ http.createServer(async (req, res) => {
|
|
|
407
409
|
}
|
|
408
410
|
|
|
409
411
|
if (req.method === 'POST' && rest === 'messages') {
|
|
410
|
-
const { from, to, to_names: toNames, body: text } = JSON.parse(body)
|
|
412
|
+
const { from, to, to_names: toNames, body: text, read_seq: readSeq } = JSON.parse(body)
|
|
413
|
+
// 読了ack(2026-08-25): 席のclientがread_unreadで取得済みの最終seqを、次の自分の投稿に
|
|
414
|
+
// 相乗りで申告する。「読んだ後にターンが生きて投稿まで到達した」が読了の証明であり、
|
|
415
|
+
// read_unread直後のturn死でメッセージが失われる形(読んだ扱いだが未処理)を作らない。
|
|
416
|
+
// 単調増加のみ受理。wakeup-bridgeは注入直前にこの値を照会し、申告済みseq以下を再配達しない。
|
|
417
|
+
if (Number.isSafeInteger(readSeq) && readSeq > 0 && typeof from === 'string' && from.length > 0) {
|
|
418
|
+
db.prepare('UPDATE members SET read_seq = MAX(COALESCE(read_seq, 0), ?) WHERE room = ? AND name = ?')
|
|
419
|
+
.run(readSeq, room.name, from)
|
|
420
|
+
}
|
|
411
421
|
// 本文が無ければ **書かずに 400**。ここを素通しにすると `JSON.stringify` が欄ごと落として、
|
|
412
422
|
// append-only の正本へ**本文の無い行**が入る——しかも送信側には 200 と seq が返るので
|
|
413
423
|
// 「送れた」と表示される(2026-08-08 に本番で2件実測。消せない)
|
package/skill/SKILL.md
CHANGED
|
@@ -82,7 +82,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
|
|
|
82
82
|
|
|
83
83
|
## resume(既存 room の再稼働・決定105)
|
|
84
84
|
|
|
85
|
-
過去ログを残した同じ room を現行工程へ接続し直す時は、setup.sh でなく `scripts/resume.sh <project> [--plan <plan_key>] [--phase <id>]... [--no-probe]` を打つ。既存 `.team/` と room を前提に、①Peertable所有generated asset/root room MCPを現行treeへ更新 ②room 確認 ③plan 再束縛(setup-state.json・roles/member.md・外部ペイン)④死んだ bridge 記録の除去 ⑤台帳の現行メンバー構成から死んでいる席だけ launch-seat.sh で再起動(roles の無い member は typed error で止まる)⑥3 bridge の再起動 ⑦全席の fresh heartbeat 読み戻し ⑧probe DM の delivered receipt 確認、までを一回で行う。利用者が先に持っていた`.mcp.json`は更新せず、room blockの手動mergeを要求する。手書きのメンバー一覧・個別再起動 script に依存しない。親の再着卓(parent-join / 番犬)は「親の再着卓」の手順で別途行う。軽い健全性確認だけなら従来どおり `doctor.sh` を使う。
|
|
85
|
+
過去ログを残した同じ room を現行工程へ接続し直す時は、setup.sh でなく `scripts/resume.sh <project> [--plan <plan_key>] [--phase <id>]... [--no-probe]` を打つ。既存 `.team/` と room を前提に、①Peertable所有generated asset/root room MCPを現行treeへ更新 ②room 確認 ③plan 再束縛(setup-state.json・roles/member.md・外部ペイン)④死んだ bridge 記録の除去 ⑤台帳の現行メンバー構成から死んでいる席だけ launch-seat.sh で再起動(roles の無い member は typed error で止まる)⑥3 bridge の再起動 ⑦全席の fresh heartbeat 読み戻し ⑧probe DM の delivered receipt 確認、までを一回で行う。利用者が先に持っていた`.mcp.json`は更新せず、room blockの手動mergeを要求する。手書きのメンバー一覧・個別再起動 script に依存しない。親の再着卓(parent-join / 番犬)は「親の再着卓」の手順で別途行う。軽い健全性確認だけなら従来どおり `doctor.sh` を使う。
|
|
86
86
|
|
|
87
87
|
## teardown
|
|
88
88
|
|
|
@@ -214,7 +214,7 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
|
|
|
214
214
|
親はこれを受けたら席の状態一覧を実観測し、止まっている原因(brief不達・承認待ち・hold)を特定して動かす
|
|
215
215
|
- **世代は常に1匹**。Claudeは旧MonitorをTaskStop、Codexは旧background taskを停止してから張り替える。
|
|
216
216
|
Grokも旧Monitorを止めてから張り替える。`watch_error`は親へ通知し、沈黙死させない
|
|
217
|
-
-
|
|
217
|
+
- `to: "all"`は5類型(claim宣言/完了・クローズ通知+着手可の統合1通/共有リソース占有・解放/全席の前提を変える環境事実/親の進行権能)だけとし、判定は「知らない席は次の行動を間違えるか」の一問で行う(2026-08-25 オーナー裁定。正本はtemplates/member.md)。それ以外は宛先DM、進捗報告・了解は投稿不要。同時のto:allは1通へ統合(claimは独立のまま)。ターン終了時の次の行動は自分宛DM
|
|
218
218
|
- **model / effort変更依頼**: 本人の自然文DMを親が判断し、確定したtargetだけを上記6.7のscriptへ渡す。本人に定型文や完全一致の再送を求めず、親が本人の代わりに依頼文を投稿しない
|
|
219
219
|
- 親の権能は進行・督促・オーナーとの接点だけ。作業者や監査担当を代行しない
|
|
220
220
|
- **作業者は自ら必要な試験と自己監査を行い、工程を次に進めてよい水準まで完成させる。** 完成したら証跡へ記したものと同じ最終的な試験内容と試験結果を監査担当へ渡し、自分では工程をクローズしない
|
|
@@ -230,7 +230,7 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
|
|
|
230
230
|
- **再着卓の契機は番犬taskの終了通知または`watch_error`**。親の側には「途絶した」と教える別経路が無いので、届いたら再着卓の手順に入る
|
|
231
231
|
- **順序の要点は「room と工程正本を読み終えるまで発言しない」**。読む前に喋ると、自分が行き違いを作る側になる(実例あり)
|
|
232
232
|
- **やらないこと**: 復帰の挨拶で席を起こさない。作業の再確認を席へ聞いて回らない——**現在地は上の1〜2で取れる**ので、聞くのは席の時間を奪うだけである
|
|
233
|
-
- **宛先の規律**:
|
|
233
|
+
- **宛先の規律**: `to: "all"`は5類型(claim宣言/完了・クローズ通知+着手可の統合1通/共有リソース占有・解放/全席の前提を変える環境事実/親の進行権能)だけとし、判定は「知らない席は次の行動を間違えるか」の一問で行う(2026-08-25 オーナー裁定。正本はtemplates/member.md)。それ以外は宛先DM、進捗報告・了解は投稿不要。同時のto:allは1通へ統合(claimは独立のまま)。ターン終了時の次の行動は自分宛DM。別の通知機構は置かない
|
|
234
234
|
- 督促の検出源は room の報告途絶と Lattice 工程表の乖離。**単独円卓モードでは工程表が無いので、検出源は `.team/tasks.md` の議題と room ログの照合だけになる**——完走の判定も同じで、全議題に完了報告が揃ったことを親が room ログで確認し、散会を宣言する(この確認と宣言が単独モードの done gate である)
|
|
235
235
|
- **席の縮退も親の進行権能**(散会と同じ性質。決定51): frontier が細って遊休席が出たら親が畳む。順序を守る——①対象席へ名指しで通告 ②本人に WIP と未報告の作業が無いことを確認する(本人が「まだ持っている」と言えば畳まない。判断は情報を持つ本人がする)③`env -u PEERTABLE_POST_TOKEN scripts/leave-seat.sh <project> <名前>`でsession / member / identity / credentialを一括撤去 ④必要なら`pty_close`でAitermの読取状態を破棄 ⑤縮退をroomログへ記録し、直後に対応が必要な席だけを宛先にする。**本人の確認より先にmemberを消すと最後の報告を出せない**
|
|
236
236
|
- **再着任表明(`[再着任] <名前>`)の受け方**: 確認するのはその席の claim 状態と工程正本の齟齬だけ。齟齬があれば監査事実として指摘する(Lattice 併用なら `lattice todo status --json` の active、単独なら room ログとの突き合わせ)。齟齬が無ければ受理も激励もせず黙って通す——1発言=全席1ターンであり、儀礼の返事は卓の燃料を焼くだけ。**代わりに作業を思い出させようとしない**(実務へ落ちる)
|
|
@@ -37,7 +37,26 @@ const result = await client.callTool({
|
|
|
37
37
|
},
|
|
38
38
|
})
|
|
39
39
|
await client.close()
|
|
40
|
-
if (result.isError)
|
|
40
|
+
if (result.isError) {
|
|
41
|
+
const text = result.content?.[0]?.text ?? ''
|
|
42
|
+
// aiterm v0.29.0からready gate失敗(起動時ダイアログ等でprompt未送信)は明示エラーになった。
|
|
43
|
+
// Peertableの席起動は「未送信→ダイアログ自動通過→着任指示を後段で貼る」を正規手順として
|
|
44
|
+
// 持っているので、sessionが残るこの失敗だけはnot_sent受領として続行する。
|
|
45
|
+
if (text.includes('initial_prompt=not_sent') && text.includes(`session_id: ${session_name}`)) {
|
|
46
|
+
console.log(JSON.stringify({
|
|
47
|
+
schema: 'aiterm.agent-launch-result.v1',
|
|
48
|
+
provider: harness,
|
|
49
|
+
harness: `${harness === 'claude' ? 'claude-code' : `${harness}-cli`}`,
|
|
50
|
+
session_id: session_name,
|
|
51
|
+
managed_completion: true,
|
|
52
|
+
event_cursor: null,
|
|
53
|
+
wait_command: null,
|
|
54
|
+
submit_residue: null,
|
|
55
|
+
}))
|
|
56
|
+
process.exit(0)
|
|
57
|
+
}
|
|
58
|
+
throw new Error(text || 'SEAT_AITERM_LAUNCH_FAILED')
|
|
59
|
+
}
|
|
41
60
|
const receipt = result.structuredContent ?? JSON.parse(result.content?.[0]?.text ?? '{}')
|
|
42
61
|
if (receipt?.schema !== 'aiterm.agent-launch-result.v1' || receipt.provider !== harness || receipt.session_id !== session_name) {
|
|
43
62
|
throw new Error('SEAT_AITERM_LAUNCH_RECEIPT_INVALID')
|
package/skill/scripts/doctor.sh
CHANGED
|
@@ -166,6 +166,28 @@ for (const [name, judge] of [['wakeup', judgeWakeup], ['seat-status', judgeSeatS
|
|
|
166
166
|
line(result.level, result.text)
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// 4.5 端末別の装置可用性(silent absence禁止・2026-08-25 オーナー裁定):
|
|
170
|
+
// この端末で「動かない」なら動かないと表示する。黙ってMacだけで動く状態を作らない。
|
|
171
|
+
{
|
|
172
|
+
// job観測(アクティブランプへの預け仕事合成): tmux list-sessions がこの端末で成立するか
|
|
173
|
+
try {
|
|
174
|
+
const probe = spawnSync('tmux', ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' })
|
|
175
|
+
if (probe.error) throw probe.error
|
|
176
|
+
line('OK', `job観測: tmux list-sessions 実行可(ランプ合成は有効。jobは peer-<name>-job* セッションで走らせること)`)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
line('NG', `job観測: tmux list-sessions が失敗——預け仕事のランプ合成はこの端末で無効(${String(error.message ?? error).split('\n')[0]})`)
|
|
179
|
+
}
|
|
180
|
+
// 読了ack: server members に read_seq 欄が観測できるか(席が一度もackしていない間は判定できない)
|
|
181
|
+
try {
|
|
182
|
+
const res = JSON.parse(execFileSync('curl', ['-s', `${url}/api/${encodeURIComponent(room)}/members`], { encoding: 'utf8' }))
|
|
183
|
+
const withAck = (res.members ?? []).filter(m => m.read_seq !== undefined).length
|
|
184
|
+
if (withAck > 0) line('OK', `読了ack: server対応を確認(read_seq保持 ${withAck} 席)`)
|
|
185
|
+
else line('判定不能', '読了ack: read_seq を持つ席がまだ無い(旧serverか、席が一度も read_unread→post していないか区別できない)')
|
|
186
|
+
} catch (error) {
|
|
187
|
+
line('NG', `読了ack: members を読めない(${String(error.message ?? error).split('\n')[0]})`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
169
191
|
// 5. Lattice 併用モードの工程正本
|
|
170
192
|
if (mode === 'lattice') {
|
|
171
193
|
const latticeCli = process.env.LATTICE_CLI || (typeof setup.lattice_cli === 'string' && setup.lattice_cli) || 'lattice'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
const chunks = []
|
|
5
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
6
|
+
const value = Buffer.concat(chunks).toString('utf8')
|
|
7
|
+
process.stdout.write(value.endsWith('\r') ? value.slice(0, -1) : value)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
const [
|
|
5
|
+
output, room, serverUrl, publicUrl, mode, planKey, phasesJson,
|
|
6
|
+
addedExclude, latticePreexisting, runtimePreexisting, addedRuntimeExclude,
|
|
7
|
+
addedRootMcp, addedMcpExclude, externalPane, projectJsonPreexisting,
|
|
8
|
+
workOrderAdapter, workOrderSpoolRef, latticeCli,
|
|
9
|
+
] = process.argv.slice(2)
|
|
10
|
+
|
|
11
|
+
const bool = (value) => value === 'true'
|
|
12
|
+
const state = {
|
|
13
|
+
room,
|
|
14
|
+
server_url: serverUrl,
|
|
15
|
+
public_url: publicUrl,
|
|
16
|
+
mode,
|
|
17
|
+
plan_key: planKey,
|
|
18
|
+
phases: JSON.parse(phasesJson),
|
|
19
|
+
added_exclude: bool(addedExclude),
|
|
20
|
+
lattice_preexisting: bool(latticePreexisting),
|
|
21
|
+
runtime_preexisting: bool(runtimePreexisting),
|
|
22
|
+
added_runtime_exclude: bool(addedRuntimeExclude),
|
|
23
|
+
added_root_mcp: bool(addedRootMcp),
|
|
24
|
+
added_mcp_exclude: bool(addedMcpExclude),
|
|
25
|
+
external_pane: bool(externalPane),
|
|
26
|
+
project_json_preexisting: bool(projectJsonPreexisting),
|
|
27
|
+
work_order_adapter: bool(workOrderAdapter),
|
|
28
|
+
work_order_spool_ref: workOrderSpoolRef,
|
|
29
|
+
lattice_cli: latticeCli,
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(output, `${JSON.stringify(state)}\n`, 'utf8')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// 02_models.md の役割→1位〜3位から、着席可能な harness / model / effort を解決する。
|
|
3
3
|
// 具体モデル名はここに持たない。台帳と順位表をその場で読む。
|
|
4
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
|
|
@@ -245,7 +245,12 @@ export function resolveSeatIdentity({
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
// realpath比較にする——symlink経由(例: ~/.claude/skills/peertable → 実体tree)で呼ばれると
|
|
249
|
+
// パス文字列比較が外れ、mainが走らず「exit 0・出力なし」で沈黙する(実被弾 2026-08-25:
|
|
250
|
+
// launch-seat.sh が SEAT_LAUNCH_HARNESS_UNSUPPORTED: harness= で落ち、原因が見えなかった)。
|
|
251
|
+
const isMain = process.argv[1] && (() => {
|
|
252
|
+
try { return realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url)) } catch { return false }
|
|
253
|
+
})()
|
|
249
254
|
if (isMain) {
|
|
250
255
|
const args = process.argv.slice(2)
|
|
251
256
|
let roles = ''
|
|
@@ -27,7 +27,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
27
27
|
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
28
28
|
import { join } from 'node:path'
|
|
29
29
|
|
|
30
|
-
import { classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
|
|
30
|
+
import { STOP_DECLARATION, combineSeatLamp, hasActiveDescendant, patrolTargets, classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
|
|
31
31
|
|
|
32
32
|
const args = process.argv.slice(2)
|
|
33
33
|
const proj = args[0]
|
|
@@ -123,6 +123,39 @@ function readSeat(member, previous, observedAt) {
|
|
|
123
123
|
return { status, busySince, paneTokenHint: parsePaneTokenHint(tail) }
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// ---- 預け仕事(jobセッション)の観測(2026-08-25 オーナー裁定)----
|
|
127
|
+
// 席が常駐・長時間ジョブを `peer-<名前>-job*` の名前のtmuxセッションで走らせる取り決めを前提に、
|
|
128
|
+
// その実物を毎周期観測してランプへ合成する。登録・宣言は一切見ない——観測だけ。
|
|
129
|
+
// active判定は「画面内容が前周期から変わった」こと。出力を出さない計算中はidle表示になるが、
|
|
130
|
+
// それは観測できる事実の正直な表示であり、推測で点滅させない。
|
|
131
|
+
const jobPaneHash = new Map() // `${name}:${session}` -> { hash, changedAt }
|
|
132
|
+
let jobObserveFailureLogged = false
|
|
133
|
+
function observeJob(socket, name) {
|
|
134
|
+
const listed = tmux(socket, 'list-sessions', '-F', '#{session_name}')
|
|
135
|
+
if (listed === null) {
|
|
136
|
+
// 「jobなし」と「観測手段が壊れている」を同じ顔にしない(silent absence禁止・2026-08-25 オーナー裁定)。
|
|
137
|
+
// この端末でjob観測が成立しない場合、ランプ合成は席paneのみで動いていることを明示的に吠える。
|
|
138
|
+
if (!jobObserveFailureLogged) {
|
|
139
|
+
jobObserveFailureLogged = true
|
|
140
|
+
console.error('JOB_OBSERVE_UNAVAILABLE: tmux list-sessions が失敗するため、預け仕事のランプ合成はこの端末で無効(席paneのみで表示)。以後この警告は繰り返さない')
|
|
141
|
+
}
|
|
142
|
+
return { alive: false, active: false }
|
|
143
|
+
}
|
|
144
|
+
jobObserveFailureLogged = false
|
|
145
|
+
const sessions = listed.split('\n').filter(s => s.startsWith(`peer-${name}-job`))
|
|
146
|
+
if (sessions.length === 0) return { alive: false, active: false }
|
|
147
|
+
let active = false
|
|
148
|
+
for (const session of sessions) {
|
|
149
|
+
const pane = tmux(socket, 'capture-pane', '-t', session, '-p')
|
|
150
|
+
if (pane === null) continue
|
|
151
|
+
const hash = String(pane.length) + ':' + pane.slice(-256)
|
|
152
|
+
const key = `${name}:${session}`
|
|
153
|
+
const prev = jobPaneHash.get(key)
|
|
154
|
+
if (!prev || prev.hash !== hash) { jobPaneHash.set(key, { hash }); active = true }
|
|
155
|
+
}
|
|
156
|
+
return { alive: true, active }
|
|
157
|
+
}
|
|
158
|
+
|
|
126
159
|
async function send(name, observation, observedAt) {
|
|
127
160
|
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/members`, {
|
|
128
161
|
method: 'POST',
|
|
@@ -152,7 +185,6 @@ async function serverKeepsStatus() {
|
|
|
152
185
|
// 自己DM(席の自己規律)は保険として残るが、継続の保証はこの番犬が持つ。
|
|
153
186
|
// busy 2分未満は情報読取だけの正当な無宣言ターンがあるため起こさない(誤爆の代償は
|
|
154
187
|
// 待機宣言1ターンで、宣言後の episode は declared 判定で再起こしされない)。
|
|
155
|
-
const STOP_DECLARATION = /\[待機\]|\[監査提出\]|待機します|散会/u
|
|
156
188
|
const NUDGE_MIN_BUSY_MS = 120_000
|
|
157
189
|
const nudgedEpisodes = new Map() // name -> busySince(同一エピソード1回だけ)
|
|
158
190
|
async function nudgeIfDropped(name, busySince) {
|
|
@@ -179,6 +211,64 @@ async function nudgeIfDropped(name, busySince) {
|
|
|
179
211
|
}
|
|
180
212
|
}
|
|
181
213
|
|
|
214
|
+
// ---- claim巡回番犬(2026-08-25 オーナー設計)----
|
|
215
|
+
// 「activeなclaimを保有する席が、有効な待機宣言なしにidleでいる」ことを**周期的に**検査して起こす。
|
|
216
|
+
// 上のbusy→idle遷移番犬は2分未満のターンで作業を落とした席を構造的に見ない(実被弾 2026-08-25:
|
|
217
|
+
// 目覚ましで起床→1分未満で「再開します」と宣言だけしてidle化。以後どの装置も起こさなかった)。
|
|
218
|
+
// 待機宣言の有効性: 宣言より後にその席宛の名指しメッセージ(目覚まし・DM)が届いたら失効。
|
|
219
|
+
// 目覚ましで起こされた席の古い宣言文面を、寝ている根拠にしない(同実被弾: #105宣言→#106起床→就寝)。
|
|
220
|
+
// to:"all" は失効させない——親の待機宣言運用(起きても返信不要)と両立させるため。
|
|
221
|
+
const PATROL_INTERVAL_MS = 30_000
|
|
222
|
+
const PATROL_NAG_INTERVAL_MS = 300_000 // 条件が続く席へは5分間隔で再吠えする(1回きりにしない)
|
|
223
|
+
const patrolLastNag = new Map() // seat -> epoch_ms
|
|
224
|
+
const busyStartedAt = new Map() // seat -> epoch_ms(このbridgeプロセスが観測した最後のターン開始)
|
|
225
|
+
let lastPatrolAt = 0
|
|
226
|
+
async function patrolClaims() {
|
|
227
|
+
if (setup.mode !== 'lattice' || !setup.plan_key) return
|
|
228
|
+
const now = Date.now()
|
|
229
|
+
if (now - lastPatrolAt < PATROL_INTERVAL_MS) return
|
|
230
|
+
lastPatrolAt = now
|
|
231
|
+
let activeTasks
|
|
232
|
+
try {
|
|
233
|
+
// doctor.sh と同じ解決規則(LATTICE_CLI env → setup記録 → 既定 'lattice'、Windowsは .cmd 解決)
|
|
234
|
+
const latticeCli = process.env.LATTICE_CLI || (typeof setup.lattice_cli === 'string' && setup.lattice_cli) || 'lattice'
|
|
235
|
+
const latticeCommand = process.platform === 'win32' && !/\.(cmd|bat|exe)$/i.test(latticeCli) && existsSync(`${latticeCli}.cmd`)
|
|
236
|
+
? `${latticeCli}.cmd`
|
|
237
|
+
: latticeCli
|
|
238
|
+
const out = execFileSync(latticeCommand, ['todo', 'status', '--json'], { cwd: proj, encoding: 'utf8' })
|
|
239
|
+
activeTasks = (JSON.parse(out).active_set ?? []).map(t => t.task_id)
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error(`seat-status-bridge: claim巡回がLatticeを読めない(今周期は検査しない): ${e.message.split('\n')[0]}`)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
if (!activeTasks.length) return
|
|
245
|
+
let messages
|
|
246
|
+
try {
|
|
247
|
+
messages = (await (await fetch(`${url}/api/${encodeURIComponent(room)}/messages`)).json()).messages ?? []
|
|
248
|
+
} catch { return }
|
|
249
|
+
const targets = patrolTargets({
|
|
250
|
+
activeTasks, messages, statusOf: seat => last.get(seat)?.status ?? null,
|
|
251
|
+
lastBusyStartAt: seat => busyStartedAt.get(seat) ?? null,
|
|
252
|
+
now, lastNag: patrolLastNag, nagIntervalMs: PATROL_NAG_INTERVAL_MS,
|
|
253
|
+
})
|
|
254
|
+
for (const { seat, task } of targets) {
|
|
255
|
+
patrolLastNag.set(seat, now)
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/messages`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: { 'Content-Type': 'application/json', ...(token ? { 'X-Peertable-Token': token } : {}) },
|
|
260
|
+
body: JSON.stringify({ from: 'alarm', to: seat,
|
|
261
|
+
body: `[継続] あなたがclaim中の工程 ${task} が着手中のまま、ターン終了後の待機宣言なしに席が停止している。作業を続行するか、外部待ちなら目覚まし条件を登録して [待機] を宣言してから終えること。` }),
|
|
262
|
+
})
|
|
263
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
264
|
+
console.error(`seat-status-bridge: claim巡回が ${seat} を起こした(${task} を保有したまま無宣言idle)`)
|
|
265
|
+
} catch (e) {
|
|
266
|
+
patrolLastNag.delete(seat)
|
|
267
|
+
console.error(`seat-status-bridge: claim巡回の起こしに失敗: ${e.message}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
182
272
|
// bridge 心拍(決定103)。server の bridge 台帳へ 30 秒ごとに送る。途絶=status_bridge_down、
|
|
183
273
|
// 403=server 側が bridge_auth_failed として観測する。旧 server(404)へは送り続けない
|
|
184
274
|
let lastBridgeBeatAt = 0
|
|
@@ -270,6 +360,23 @@ async function tick() {
|
|
|
270
360
|
const observation = readSeat(member, prev, observedAt)
|
|
271
361
|
// tmux 席を持たない member(親など)は一度も観測できないので送らない(deriveMissingSession が null を返す)
|
|
272
362
|
if (observation === null) { skipped++; continue }
|
|
363
|
+
{
|
|
364
|
+
const target = resolveSeatObservation(member, null) ?? resolveSeatObservation(member, defaultSocket())
|
|
365
|
+
if (target !== null) {
|
|
366
|
+
const job = observeJob(target.socket, member.name)
|
|
367
|
+
// 席paneの子孫プロセス(Codex内蔵background terminal等)の実働もjob稼働として合成する
|
|
368
|
+
if (!job.active && observation.status === 'idle') {
|
|
369
|
+
const panePid = tmuxPanePid(target.socket, target.target)
|
|
370
|
+
if (panePid) {
|
|
371
|
+
try {
|
|
372
|
+
const rows = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu='], { encoding: 'utf8' }).split('\n')
|
|
373
|
+
if (hasActiveDescendant(rows, Number(panePid))) job.active = true
|
|
374
|
+
} catch { /* psが失敗する端末では子孫観測なしで続行(named session観測は生きている) */ }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
observation.status = combineSeatLamp(observation.status, job)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
273
380
|
const changed = !prev || prev.status !== observation.status
|
|
274
381
|
|| prev.busySince !== observation.busySince
|
|
275
382
|
// token表示は実行中に細かく増える。1k未満の差で8秒ごとにPOSTせず、表示精度に合う粒度で送る。
|
|
@@ -280,6 +387,7 @@ async function tick() {
|
|
|
280
387
|
await send(name, observation, observedAt)
|
|
281
388
|
last.set(name, { ...observation, at: now })
|
|
282
389
|
sent++
|
|
390
|
+
if ((observation.status === 'busy' || observation.status === 'blocked') && prev?.status !== 'busy' && prev?.status !== 'blocked') busyStartedAt.set(name, now)
|
|
283
391
|
if (prev?.status === 'busy' && observation.status === 'idle') await nudgeIfDropped(name, prev.busySince)
|
|
284
392
|
if (changed) console.error(`seat-status-bridge: ${name} → ${observation.status}${prev ? `(${prev.status} から)` : ''}`)
|
|
285
393
|
} catch (e) {
|
|
@@ -289,6 +397,7 @@ async function tick() {
|
|
|
289
397
|
}
|
|
290
398
|
// 0件でも0件と言う(条件付きログにしない。沈黙する失敗を作らない・決定58)
|
|
291
399
|
console.error(`seat-status-bridge: ${members.length} 席を見て ${sent} 件送った(tmux席を持たず観測対象外: ${skipped})`)
|
|
400
|
+
await patrolClaims()
|
|
292
401
|
return { attempted: sent + failed, failed }
|
|
293
402
|
}
|
|
294
403
|
|
|
@@ -280,3 +280,75 @@ export function parsePaneTokenHint(pane) {
|
|
|
280
280
|
}
|
|
281
281
|
return latest
|
|
282
282
|
}
|
|
283
|
+
|
|
284
|
+
// ---- claim巡回番犬の純関数(seat-status-bridge.mjsが使う。testはここからimportする)----
|
|
285
|
+
export const STOP_DECLARATION = /\[待機\]|\[監査提出\]|待機します|散会/u
|
|
286
|
+
|
|
287
|
+
export function patrolTargets({ activeTasks, messages, statusOf, lastBusyStartAt, now, lastNag, nagIntervalMs }) {
|
|
288
|
+
const owner = new Map() // task_id -> 最新のclaim発言者
|
|
289
|
+
for (const m of messages) {
|
|
290
|
+
const match = /^\[claim\]\s+(\S+)/u.exec(m.body ?? '')
|
|
291
|
+
if (match) owner.set(match[1], m.from)
|
|
292
|
+
}
|
|
293
|
+
const targets = []
|
|
294
|
+
for (const task of activeTasks) {
|
|
295
|
+
const seat = owner.get(task)
|
|
296
|
+
if (!seat || statusOf(seat) !== 'idle') continue
|
|
297
|
+
// 待機の判定は観測ベース(2026-08-25 オーナー裁定): 「最後のターン終了より後に待機宣言があるか」
|
|
298
|
+
// だけを見る。宛先による失効・TTLのようなメッセージ解釈の条件は持たない。
|
|
299
|
+
// ターンを終えたのに宣言せず黙った席(実被弾: 起こされて→落として→古い宣言の裏で就寝)は
|
|
300
|
+
// 宣言がターン終了より古いので確実に引っかかる。
|
|
301
|
+
let declTs = 0
|
|
302
|
+
for (const m of messages) {
|
|
303
|
+
if (m.from === seat && STOP_DECLARATION.test(m.body ?? '')) declTs = Date.parse(m.ts ?? '') || declTs
|
|
304
|
+
}
|
|
305
|
+
const busyStart = lastBusyStartAt(seat)
|
|
306
|
+
// 錨はターン「開始」(2026-08-25 実被弾 #120: 終了錨だと「宣言→数秒後にターン終了」の正常系が
|
|
307
|
+
// 常に古い判定を食らい誤爆する)。「最後のターンの開始より後に宣言した」=そのターンが宣言を
|
|
308
|
+
// 残したこと。起床ターンが無宣言で終わると、手持ちの宣言は前ターン開始より古くなり必ず捕まる。
|
|
309
|
+
// bridge再起動等でターン履歴が無い席は、宣言の存在だけで正当とみなす(誤爆しない安全側)
|
|
310
|
+
const declared = busyStart == null ? declTs > 0 : declTs >= busyStart - 10_000
|
|
311
|
+
if (declared) continue
|
|
312
|
+
if (now - (lastNag.get(seat) ?? 0) < nagIntervalMs) continue
|
|
313
|
+
if (!targets.some(t => t.seat === seat)) targets.push({ seat, task })
|
|
314
|
+
}
|
|
315
|
+
return targets
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
// ---- ランプ合成(2026-08-25 オーナー裁定: ドットは1個、席と預け仕事を合成する)----
|
|
320
|
+
// 点滅(busy) = 席のターン実行中 または jobセッションの画面が動いている
|
|
321
|
+
// 点灯(idle) = どのプロセスかは生きているが、何も動いていない
|
|
322
|
+
// 白抜き(dead/unknown相当) = 何も無い
|
|
323
|
+
// blockedは「存在するが承認待ちで詰まっている」の特殊表示としてそのまま通す。
|
|
324
|
+
export function combineSeatLamp(paneStatus, job) {
|
|
325
|
+
if (job?.active) return 'busy'
|
|
326
|
+
if (paneStatus === 'busy' || paneStatus === 'blocked') return paneStatus
|
|
327
|
+
if (paneStatus === 'idle' || job?.alive) return 'idle'
|
|
328
|
+
return paneStatus // 'dead' 等はそのまま
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// paneプロセスの子孫に実働(CPU消費)が居るかの判定素材。psの `pid ppid pcpu` 出力を受け取り、
|
|
332
|
+
// root配下の子孫(root自身は除く)に pcpu >= 閾値 が居れば true。席がCodex内蔵background terminal等、
|
|
333
|
+
// tmuxセッションを作らずに走らせた預け仕事を、プロセスツリーの実観測で拾う(2026-08-25 オーナー裁定
|
|
334
|
+
//「子プロセスも見る」)。nohup等でツリーから切り離された仕事はここでは見えない——それは
|
|
335
|
+
// peer-<name>-job* セッション慣例の側が受け持つ(二本立て)。
|
|
336
|
+
export function hasActiveDescendant(psRows, rootPid, { minCpu = 1.0 } = {}) {
|
|
337
|
+
const children = new Map()
|
|
338
|
+
const cpu = new Map()
|
|
339
|
+
for (const row of psRows) {
|
|
340
|
+
const m = /^\s*(\d+)\s+(\d+)\s+([\d.]+)/.exec(row)
|
|
341
|
+
if (!m) continue
|
|
342
|
+
const [pid, ppid, pcpu] = [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
343
|
+
if (!children.has(ppid)) children.set(ppid, [])
|
|
344
|
+
children.get(ppid).push(pid)
|
|
345
|
+
cpu.set(pid, pcpu)
|
|
346
|
+
}
|
|
347
|
+
const queue = [...(children.get(rootPid) ?? [])]
|
|
348
|
+
while (queue.length) {
|
|
349
|
+
const pid = queue.pop()
|
|
350
|
+
if ((cpu.get(pid) ?? 0) >= minCpu) return true
|
|
351
|
+
queue.push(...(children.get(pid) ?? []))
|
|
352
|
+
}
|
|
353
|
+
return false
|
|
354
|
+
}
|
package/skill/scripts/setup.sh
CHANGED
|
@@ -192,8 +192,16 @@ fi
|
|
|
192
192
|
# **解決した CLI の実 path を残す。** 残さないと、席も teardown も PATH の `lattice` へ逸れる。
|
|
193
193
|
# release 前の source tree を実測する卓では、それは**pull 系 command を持たない古い install**で、
|
|
194
194
|
# 手順どおり打っても届かない(suzune の監査で実測・room [1037])。
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
if [ "$(node -p 'process.platform')" = win32 ]; then
|
|
196
|
+
node "$repo/skill/scripts/platform/windows/write-setup-state.mjs" "$tdir/setup-state.json" \
|
|
197
|
+
"$room" "$url" "$public_url" "$mode" "$plan" "$phases_json" "$added_exclude" \
|
|
198
|
+
"$lattice_preexisting" "$runtime_preexisting" "$added_runtime_exclude" "$added_root_mcp" \
|
|
199
|
+
"$added_mcp_exclude" "$external_pane" "$project_json_preexisting" "$work_order_adapter" \
|
|
200
|
+
"$work_order_spool_ref" "$lattice_cli"
|
|
201
|
+
else
|
|
202
|
+
printf '{"room":"%s","server_url":"%s","public_url":"%s","mode":"%s","plan_key":"%s","phases":%s,"added_exclude":%s,"lattice_preexisting":%s,"runtime_preexisting":%s,"added_runtime_exclude":%s,"added_root_mcp":%s,"added_mcp_exclude":%s,"external_pane":%s,"project_json_preexisting":%s,"work_order_adapter":%s,"work_order_spool_ref":"%s","lattice_cli":"%s"}\n' \
|
|
203
|
+
"$room" "$url" "$public_url" "$mode" "$plan" "$phases_json" "$added_exclude" "$lattice_preexisting" "$runtime_preexisting" "$added_runtime_exclude" "$added_root_mcp" "$added_mcp_exclude" "$external_pane" "$project_json_preexisting" "$work_order_adapter" "$work_order_spool_ref" "$lattice_cli" > "$tdir/setup-state.json"
|
|
204
|
+
fi
|
|
197
205
|
|
|
198
206
|
# 稼働状態ブリッジを起こす。**teardown が止めるのと対称にする**——「起こすかは卓の任意」だった間、
|
|
199
207
|
# 手で `nohup` する経路が2つの失敗を招いた: 起こし忘れと、**トークンを持たないシェルで起こす**
|
|
@@ -43,6 +43,14 @@ did() { echo "teardown: [実施] $*"; }
|
|
|
43
43
|
skip() { echo "teardown: [スキップ] $*"; }
|
|
44
44
|
miss() { echo "teardown: [未実施] $*" >&2; fail=1; }
|
|
45
45
|
yes_() { [ "$1" = "True" ] || [ "$1" = "true" ]; }
|
|
46
|
+
platform=$(node -p 'process.platform')
|
|
47
|
+
normalize_read_line() {
|
|
48
|
+
if [ "$platform" = win32 ]; then
|
|
49
|
+
printf '%s' "$1" | node "$script_dir/platform/windows/normalize-read-line.mjs"
|
|
50
|
+
else
|
|
51
|
+
printf '%s' "$1"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
46
54
|
|
|
47
55
|
mcp_remove_action=not-managed
|
|
48
56
|
if yes_ "$added_mcp"; then
|
|
@@ -194,6 +202,7 @@ elif [ -z "$run_refs" ]; then
|
|
|
194
202
|
skip "run landing(active runなし)"
|
|
195
203
|
else
|
|
196
204
|
while IFS= read -r run_ref; do
|
|
205
|
+
run_ref=$(normalize_read_line "$run_ref")
|
|
197
206
|
[ -n "$run_ref" ] || continue
|
|
198
207
|
if landing_report=$(cd "$proj" && "$lattice_cli" run landing --run "$run_ref" 2>&1); then
|
|
199
208
|
unlanded_count=""
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# sourced by launch-seat / leave-seat / teardown / ensure-bridge / change-seat
|
|
2
|
-
# POSIX は tmux -S <sock>。Windows psmux は -L <aiterm-ns>(-S は既定 namespace へ落ちる)。
|
|
3
|
-
_peertable_tmux_scripts=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
4
|
-
tmux_at() {
|
|
5
|
-
local prefix
|
|
6
|
-
prefix=$(node "$_peertable_tmux_scripts/tmux-socket.mjs" --prefix) || return 1
|
|
7
|
-
# shellcheck disable=SC2206
|
|
8
|
-
local -a conn=($prefix)
|
|
9
|
-
command tmux "${conn[@]}" "$@"
|
|
10
|
-
}
|
|
1
|
+
# sourced by launch-seat / leave-seat / teardown / ensure-bridge / change-seat
|
|
2
|
+
# POSIX は tmux -S <sock>。Windows psmux は -L <aiterm-ns>(-S は既定 namespace へ落ちる)。
|
|
3
|
+
_peertable_tmux_scripts=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
4
|
+
tmux_at() {
|
|
5
|
+
local prefix
|
|
6
|
+
prefix=$(node "$_peertable_tmux_scripts/tmux-socket.mjs" --prefix) || return 1
|
|
7
|
+
# shellcheck disable=SC2206
|
|
8
|
+
local -a conn=($prefix)
|
|
9
|
+
command tmux "${conn[@]}" "$@"
|
|
10
|
+
}
|
|
@@ -469,8 +469,42 @@ async function flushSeat(seat) {
|
|
|
469
469
|
const queue = pending.get(seat)
|
|
470
470
|
if (!queue || queue.size === 0) return
|
|
471
471
|
flushing.add(seat)
|
|
472
|
-
|
|
472
|
+
let msgs = []
|
|
473
473
|
try {
|
|
474
|
+
// 読了ack照会(2026-08-25): 席のclientはread_unreadで取得した最終seqを次の自分の投稿へ
|
|
475
|
+
// 相乗り申告し、serverがmember.read_seqへ単調保存する。注入直前に照会し、申告済みseq以下は
|
|
476
|
+
// 再配達しない(席が読んで返答済みの既読群を再注入して起こした実被弾 2026-08-25 poly卓)。
|
|
477
|
+
// 読了直後・投稿前にターンが死んだ席はackが無いので従来どおり再配達される=安全網は保つ。
|
|
478
|
+
// refreshMembers失敗時は直前観測のread_seqを使う=古い分は再配達側へ倒れる(安全方向)。
|
|
479
|
+
await refreshMembers()
|
|
480
|
+
const ackedSeq = Number(members.get(seat)?.read_seq ?? 0)
|
|
481
|
+
if (ackedSeq > 0) {
|
|
482
|
+
const acked = [...queue.values()].filter(msg => msg.seq <= ackedSeq)
|
|
483
|
+
if (acked.length > 0) {
|
|
484
|
+
for (const msg of acked) {
|
|
485
|
+
const state = deliveryStates.get(msg.seq)
|
|
486
|
+
if (state) state.delivered.add(seat)
|
|
487
|
+
delivered.add(deliveryKey(msg.seq, seat))
|
|
488
|
+
}
|
|
489
|
+
// durable保存失敗時はin-memory印を巻き戻す(TUI成功経路517–525と同じ契約。反証1)
|
|
490
|
+
try {
|
|
491
|
+
saveDeliveryState()
|
|
492
|
+
} catch (error) {
|
|
493
|
+
for (const msg of acked) {
|
|
494
|
+
deliveryStates.get(msg.seq)?.delivered.delete(seat)
|
|
495
|
+
delivered.delete(deliveryKey(msg.seq, seat))
|
|
496
|
+
}
|
|
497
|
+
throw error
|
|
498
|
+
}
|
|
499
|
+
for (const msg of acked) queue.delete(msg.seq)
|
|
500
|
+
// 席は本文を自分で読了済み。配達保証(決定102)はacked_read理由のdeliveredで満たす
|
|
501
|
+
for (const msg of acked) await postReceipt(msg.seq, seat, 'delivered', 'acked_read')
|
|
502
|
+
log(`既読ackで配達を省略: ${seat} ← ${acked.length} 件(read_seq=${ackedSeq})`)
|
|
503
|
+
advanceLastSeq()
|
|
504
|
+
}
|
|
505
|
+
if (queue.size === 0) return
|
|
506
|
+
}
|
|
507
|
+
msgs = [...queue.values()].sort((a, b) => a.seq - b.seq)
|
|
474
508
|
const outcome = await wake(seat, msgs)
|
|
475
509
|
if (outcome === 'deferred') return
|
|
476
510
|
if (outcome === 'skipped') {
|
|
@@ -21,16 +21,26 @@
|
|
|
21
21
|
|
|
22
22
|
**kickoff・名指しの依頼DMには、まず `[引受] <要旨>` を room へ返してから着手する(決定104)。** 親はこの引受発言と配送 receipt が揃うまで依頼を未着手として扱う——黙って作業を始めると、依頼が届いていないと判定され再送・再依頼が起きる。
|
|
23
23
|
|
|
24
|
+
**常駐・長時間ジョブは `peer-<自分の名前>-job<任意suffix>` という名前のtmuxセッションで走らせる(2026-08-25 オーナー裁定)。** `nohup`等で切り離すと観測不能になり、あなたのランプが「何もしていない」表示になる。jobセッションで走らせれば、稼働状況が自分のアクティブランプへ合成される(jobが動いていれば点滅)。複数ジョブは別suffixで並走してよい。ジョブ終了の目覚まし登録は従来どおり行う。
|
|
25
|
+
|
|
24
26
|
**探索順は active → ready → 待機である。** まず自分の active 工程を完了させる。無ければ全PLAN横断の ready を自律的に claim する。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。待機のあと自分へ `[次の行動]` を送らない。次に行う作業があるターンだけ自己DMする。空の終了通知は使わない。
|
|
25
27
|
|
|
28
|
+
**`to: "all"` を使ってよいのは次の5類型だけである(2026-08-25 オーナー裁定)。** 判定は「この情報を知らない席は、次の行動を間違えるか」の一問で行う。間違えないなら宛先DMにする。to:all は1通ごとに全席のターンを1つずつ消費する。
|
|
29
|
+
- (a) claim宣言(独立1発言の規定は従来どおり)
|
|
30
|
+
- (b) 工程の完了・クローズ通知(監査担当の「次の工程に着手可」はこの1通に統合し、別送しない)
|
|
31
|
+
- (c) 共有リソースの占有・解放(ブラウザ・ポート・常駐プロセス・repo全体に触る操作の前後)
|
|
32
|
+
- (d) 全席の作業前提を変える環境事実(自工程の方針・進捗の変更は含まない)
|
|
33
|
+
- (e) 親の進行権能(kickoff・待機宣言・停止・散会・縮退)
|
|
34
|
+
進捗・状況報告(「〜します」「継続中」)と了解・謝辞は、to:all はもちろん投稿そのものが不要である。同じタイミングで送る to:all が2件あるなら1通に統合する(claim だけは独立のまま)。
|
|
35
|
+
|
|
26
36
|
1. `lattice todo status --json` で ready なタスクを見る。{{CLAIM_SCOPE}}
|
|
27
37
|
2. 憲章の手順で `post(to: "all", message: "[claim] <タスク>")` を一度だけ送る。**この `[claim]` が唯一の着手通知である。別の `[工程着手]` や自動着手通知は送らない。** `[claim]` は独立した1発言で出し、完了報告や他タスクの話と同じ発言に畳まない
|
|
28
38
|
3. `lattice todo start --plan {{PLAN_KEY}} --task <id>` で着手を記録する。**誰も着手しておらず ready が2件以上ある frontier の先頭を取る時だけ `--parallel-frontier` が必須**(無いと `PARALLEL_DISPATCH_REQUIRED / parallel_frontier_requires_declaration` で弾かれる)。ready が1件だけ、または既に誰かが着手している frontier へ後から乗る時は素の start でよい。**`INDEPENDENCE_UNVERIFIED` で落ちたら実装に入らない。** 記録があるのにこの工程が未宣言・失効なので、親を待たない。canonical の cwd で `.team/scripts/independence-refresh.sh {{PLAN_KEY}}` を打ち、remaining A が witness に無ければ自分で足してから再 compile し、もう一度 `todo start` する
|
|
29
39
|
4. 実装し、自ら必要な試験と自己監査を行う。工程を次に進めてよい水準まで自分の責任で完成させる。着手後に先行工程由来の不具合が判明しても、先行工程をreopenせず、前担当者へ戻さず、修正工程も追加しない。現在の工程を成立させる修正として自ら直し、最終試験結果へ含める。誰かに用事がある時はその相手へDMし、誰に聞けばよいか分からない時は `post(to: "all")` で聞く
|
|
30
40
|
5. 証跡ファイル `evidence/{{PLAN_KEY}}/<task_id>.md` に、最終的な試験内容と試験結果を含めて「何を作り、どう確認したか」を書き、変更ファイルと証跡だけをcommitする
|
|
31
|
-
6.
|
|
41
|
+
6. その証跡と同じ最終試験内容・結果を監査担当へ渡す。宛先は members ツールで役割=監査・発見の席を確認して名指しDMで送る(親やallへ投げない)。作業者自身は `.team/scripts/done.sh` や `lattice todo done` を実行しない
|
|
32
42
|
7. 監査担当として結果を受け取った場合は、提出された試験内容と試験結果が元PLAN・工程正本・受入条件に照らして妥当か判断する。試験を再実行せず、個人の思想や計画外の改善を完了条件へ加えない
|
|
33
|
-
8. 妥当なら監査担当が `.team/scripts/done.sh <task_id> --plan <plan_key>` で工程をクローズする。クローズに親・オーナーの裁定は要らず、待たない——親が卓上で別の手順を言っていても、監査担当のクローズ権限が優先する。親は campaign 終端の最終監査で全量を見る(オーナー裁定 2026-08-22)。未着地の feat は `done.sh` が canonical main へ merge して push する。親へ着地を依頼しない。`done.sh` は証跡と同じ本文を Lattice の `test_result` へ記録し、remaining の並列記録も更新してから戻る。done
|
|
43
|
+
8. 妥当なら監査担当が `.team/scripts/done.sh <task_id> --plan <plan_key>` で工程をクローズする。クローズに親・オーナーの裁定は要らず、待たない——親が卓上で別の手順を言っていても、監査担当のクローズ権限が優先する。親は campaign 終端の最終監査で全量を見る(オーナー裁定 2026-08-22)。未着地の feat は `done.sh` が canonical main へ merge して push する。親へ着地を依頼しない。`done.sh` は証跡と同じ本文を Lattice の `test_result` へ記録し、remaining の並列記録も更新してから戻る。done を読返してから、クローズと着手可能の更新を1通で `post(to: "all", message: "[クローズ] <task_id>。次の工程に着手可")` と通知し、具体的な次工程は指示しない(クローズ通知と着手可通知を別送しない)
|
|
34
44
|
9. 不合格なら、現在モデルでの修正機会は1回だけとする。再び不合格になったら親へmodel変更を依頼し、`Luna → Terra → Sol`の順で一段昇格する。自分で席設定を変えない
|
|
35
45
|
10. 作業者は監査担当によるクローズを確認し、工程正本から次のreadyを選ぶ
|
|
36
46
|
11. **claimできるToDoが無いなら仕事を発明しない(決定68)。** 依頼されていない監査・他席への状況照会・正典の自主レビューを暇つぶしに始めない。縮退の打診が来たらWIP棚卸しを正直に返す
|