peertable 0.4.13 → 0.4.15

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 CHANGED
@@ -107,7 +107,7 @@ Codex では、スキルが所有する room MCP block をプロジェクトの
107
107
 
108
108
  動いており、**自分自身の開発に使っている**。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が止まらないよう直した。
109
109
 
110
- 現在のnpm releaseは **peertable 0.4.13**。
110
+ 現在のnpm releaseは **peertable 0.4.15**。
111
111
 
112
112
  設計文書と決定履歴(**91 決定**)は [docs/plan.md](docs/plan.md)。
113
113
 
package/README.md CHANGED
@@ -143,7 +143,7 @@ It interviews you, names the members, scaffolds `.team/` (charter + roles, isola
143
143
 
144
144
  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.
145
145
 
146
- The current npm release is **peertable 0.4.13**.
146
+ The current npm release is **peertable 0.4.15**.
147
147
 
148
148
  The design document and decision log (**91 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
149
149
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
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
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
11
11
 
12
12
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
13
13
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
14
- const MCP_VERSION = '0.4.13'
14
+ const MCP_VERSION = '0.4.15'
15
15
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
16
16
 
17
17
  const USAGE = `usage:
@@ -70,8 +70,9 @@ const mcp = new Server(
70
70
  `あなたは Peertable room「${ROOM}」のメンバー「${ME}」である。` +
71
71
  '<channel source="room"> の通知は「新着あり」の合図であり、本文は read_unread ツールで読む。' +
72
72
  '発言は post ツールを使う。to: "all" はroom全体、メンバー名はDM、配列は複数人宛である。' +
73
- `ターンを終える直前に必ず post(to: "${ME}", message: "[次の行動] ...") を1回送れ。` +
74
- 'wakeup-bridge はこの自己DMで次ターンを起こす。出さないと席は止まり、次の仕事は来ない。空の終了通知は使うな。',
73
+ `次にやる仕事があるターンを終える直前に post(to: "${ME}", message: "[次の行動] ...") を1回送れ。` +
74
+ 'wakeup-bridge はこの自己DMで次ターンを起こす。仕事があるのに出さないと席は止まる。' +
75
+ '手番が無く待機に入るときは自己DMを出すな。親へ [待機] を一度だけ送り沈黙せよ。空の終了通知は使うな。',
75
76
  },
76
77
  )
77
78
 
@@ -83,7 +84,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
83
84
  name: 'post',
84
85
  description:
85
86
  'room へ通常発言する。to は "all"(room全体)/ メンバー名(DM)/ メンバー名の配列(複数人宛)。' +
86
- `ターン終了時は to を自分の名前(${ME})にし、message を "[次の行動] " で始めよ。これが次ターンの起こしである。`,
87
+ `次の仕事があるターン終了時は to を自分の名前(${ME})にし、message を "[次の行動] " で始めよ。これが次ターンの起こしである。手番が無い待機では自己DMを出すな。`,
87
88
  inputSchema: {
88
89
  type: 'object',
89
90
  properties: {
@@ -297,6 +298,7 @@ async function runDiagnostics(asJson) {
297
298
  'scripts/seat-identity.mjs',
298
299
  'scripts/pid-alive.mjs',
299
300
  'scripts/parent-watch.mjs',
301
+ 'scripts/parent-watch-logic.mjs',
300
302
  'scripts/seat-credential.mjs',
301
303
  'scripts/ensure-room-mcp.mjs',
302
304
  'scripts/leave-seat.sh',
@@ -0,0 +1,11 @@
1
+ export function latticeStaffingChanged(previous, next) {
2
+ if (!next || next.error) return false
3
+ if (!previous || previous.error) return true
4
+ return previous.ready !== next.ready || previous.active !== next.active
5
+ }
6
+
7
+ export function addressedToParent(message, parent) {
8
+ return message?.from !== parent
9
+ && (message?.to === 'all' || message?.to === parent
10
+ || (Array.isArray(message?.to_names) && message.to_names.includes(parent)))
11
+ }
@@ -11,6 +11,7 @@ import { execFileSync } from 'node:child_process'
11
11
  import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
12
12
  import { join } from 'node:path'
13
13
  import { resolveLatticeExecutable } from './seat-usage.mjs'
14
+ import { addressedToParent as messageAddressedToParent, latticeStaffingChanged } from './parent-watch-logic.mjs'
14
15
 
15
16
  const args = process.argv.slice(2)
16
17
  const project = args.shift()
@@ -154,9 +155,17 @@ function releaseLock() {
154
155
  acquireLock()
155
156
  process.on('exit', releaseLock)
156
157
 
157
- const addressedToParent = message => message?.from !== parent
158
- && (message?.to === 'all' || message?.to === parent
159
- || (Array.isArray(message?.to_names) && message.to_names.includes(parent)))
158
+ const addressedToParent = message => messageAddressedToParent(message, parent)
159
+
160
+ function exitWhenParentStdinCloses() {
161
+ if (mode !== '--follow') return
162
+ const stdin = process.stdin
163
+ if (!stdin || stdin.destroyed) return
164
+ const quit = () => process.exit(0)
165
+ stdin.on('end', quit)
166
+ stdin.on('close', quit)
167
+ if (typeof stdin.resume === 'function') stdin.resume()
168
+ }
160
169
 
161
170
  async function writeEvent(event) {
162
171
  await new Promise((resolve, reject) => {
@@ -180,8 +189,7 @@ async function acceptLatticeState(next) {
180
189
  })
181
190
  return true
182
191
  }
183
- const changed = Number.isSafeInteger(previous?.ready) && Number.isSafeInteger(previous?.active)
184
- && previous.ready + previous.active !== next.ready + next.active
192
+ const changed = latticeStaffingChanged(previous, next)
185
193
  state = { ...state, lattice: next, last_event_at: now() }
186
194
  saveState(state)
187
195
  if (!changed) return false
@@ -241,11 +249,29 @@ if (mode === '--next' && (!Number.isFinite(nextWindowMs) || nextWindowMs < 100))
241
249
  process.exit(2)
242
250
  }
243
251
 
252
+ exitWhenParentStdinCloses()
253
+
244
254
  let consecutiveFailures = 0
255
+ let snapshotSent = false
245
256
  for (;;) {
246
257
  const deadline = mode === '--next' ? Date.now() + nextWindowMs : Number.POSITIVE_INFINITY
247
258
  try {
248
259
  if (await catchUp()) process.exit(0)
260
+ if (mode === '--follow' && !snapshotSent) {
261
+ snapshotSent = true
262
+ const lattice = state.lattice
263
+ await writeEvent({
264
+ schema: 'peertable.parent-watch-event.v1',
265
+ type: 'parent_watch_snapshot',
266
+ parent,
267
+ last_seq: state.last_seq,
268
+ ready: lattice?.ready ?? null,
269
+ active: lattice?.active ?? null,
270
+ body: lattice && !lattice.error
271
+ ? staffingBody(lattice)
272
+ : '親番犬を張り直した。room の親宛と工程件数を追従する。',
273
+ })
274
+ }
249
275
  const controller = new AbortController()
250
276
  const remaining = Number.isFinite(deadline) ? Math.max(1, deadline - Date.now()) : null
251
277
  const timer = remaining === null ? null : setTimeout(() => controller.abort(), remaining)
@@ -17,7 +17,7 @@ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from
17
17
  import { join } from 'node:path'
18
18
  import { promisify } from 'node:util'
19
19
  import { resolveSeatObservation, tmuxArgv } from './seat-usage.mjs'
20
- import { BROADCAST_RECIPIENT, formatWakeNotice, isWakeupBridgeTarget, shouldDeferGrokWake } from './wakeup-delivery.mjs'
20
+ import { BROADCAST_RECIPIENT, formatWakeNotice, isIdleSelfWake, isWakeupBridgeTarget, shouldDeferGrokWake } from './wakeup-delivery.mjs'
21
21
 
22
22
  const run = promisify(execFile)
23
23
  const [proj, ...rest] = process.argv.slice(2)
@@ -260,6 +260,11 @@ async function wake(seat, msgs) {
260
260
 
261
261
  function dispatch(msg) {
262
262
  if (deliveryStates.has(msg.seq)) return
263
+ if (isIdleSelfWake(msg)) {
264
+ deliveryStates.set(msg.seq, { message: msg, targets: new Set(), delivered: new Set() })
265
+ advanceLastSeq()
266
+ return
267
+ }
263
268
  const targets = recipientNames(msg).filter(seat => isWakeupBridgeTarget(members.get(seat)))
264
269
  const state = { message: msg, targets: new Set(targets), delivered: new Set() }
265
270
  for (const seat of targets) {
@@ -31,6 +31,27 @@ export function isWakeupBridgeTarget(member) {
31
31
  return true
32
32
  }
33
33
 
34
+ /**
35
+ * 手番の無い待機自己DMは起こさない。
36
+ * 実測 2026-08-20: 監査席が `[次の行動] 変化なし/待機継続` を自分へ送り、
37
+ * wakeup-bridge がそれを次ターンとして注入し、18秒周期で無限自己DMになった。
38
+ * 次の仕事がある自己DM(監査待ちを含む)は起こす。
39
+ */
40
+ export function isIdleSelfWake(msg) {
41
+ const from = msg?.from
42
+ if (typeof from !== 'string' || from.length === 0) return false
43
+ const recipients = Array.isArray(msg.to_names)
44
+ ? msg.to_names
45
+ : typeof msg.to === 'string'
46
+ ? [msg.to]
47
+ : []
48
+ if (recipients.includes(BROADCAST_RECIPIENT)) return false
49
+ if (!recipients.includes(from)) return false
50
+ const body = String(msg.body ?? '')
51
+ if (!body.startsWith('[次の行動]')) return false
52
+ return /変化なし|待機継続|黙って待機/.test(body)
53
+ }
54
+
34
55
  /** Grok 既定はキュー投入。busy 中に積むと今のターンへ混ざらない。 */
35
56
  export function shouldDeferGrokWake(vendor, tail) {
36
57
  if (vendor !== 'grok') return false
@@ -3,7 +3,7 @@
3
3
  これはチーム作業である。自分が引き受けたタスクの完了はミッションの完了ではない。全タスク完了までチームは解散しない。
4
4
 
5
5
  1. 拘束力を持つのは**工程正本への記録**と room ログに残った決定だけ(工程正本=Lattice 併用モードなら Lattice の todo 記録、単独円卓モードなら room の宣言そのもの)。room の `to: "all"` は部屋全体への通常発言、名前指定はDMである
6
- 2. room の宛先規則はこれだけである。claim は `post(to: "all")`、ターン終了時の次の行動は `post(to: "<自分の名前>")`(message は `[次の行動]` で始める。出さないと席は止まる)、工程完了は `post(to: "all")`、誰かへの用事は `post(to: "<相手の名前>")` で送る。誰に聞けばよいか分からないことは `post(to: "all")` で聞く。実装・監査・readyが本当に無い時の待機宣言は最終手段として親だけへDMし、`all`へ送らない。別の通知機構は使わない
6
+ 2. room の宛先規則はこれだけである。claim は `post(to: "all")`、次にやる仕事があるターン終了時の次の行動は `post(to: "<自分の名前>")`(message は `[次の行動]` で始める。仕事があるのに出さないと席は止まる)、工程完了は `post(to: "all")`、誰かへの用事は `post(to: "<相手の名前>")` で送る。誰に聞けばよいか分からないことは `post(to: "all")` で聞く。実装・監査・readyが本当に無い時の待機宣言は最終手段として親だけへDMし、`all`へ送らず、そのあと自分へ `[次の行動]` を送らない。別の通知機構は使わない
7
7
  3. **仕事を選ぶのも始めるのも自分である。** どのモードでも、`read_log`で先行claimを確認してから `post(to: "all", message: "[claim] <タスク>")` を送る。これが唯一の着手通知であり、別の着手通知は送らない。**装置から仕事が降ってくることはないし、着手前に装置の許可を待つこともない**(オーナー裁定 2026-08-09)。Lattice の実行層を使う卓では、その後 `todo start` し、自分で `run intake` して隔離 worktree を受け取る——それは設備の供給であって許可証ではなく、装置が返すのは競合した時の「留まれ」だけである(タスクの呼び名は Lattice 併用モードなら task_id、単独円卓モードなら `.team/tasks.md` の議題名)。同じタスクへの先行 claim があれば取り下げるか `[join] <タスク>` へ切り替える
8
8
  4. 分からないことは room で聞く。台帳はない。自分の変更が他の部位に影響するなら、聞かれる前に影響を受けるメンバーを明示宛先にして通知する
9
9
  5. 判断は情報を持つ者がする。タスクは席ではなく現場。合流(join)は歓迎される。詰まった仲間には目を貸す。親が円卓メンバーを増やす時は、正式な`launch-seat.sh`でAiterm長寿命席として着任させ、native sub-agent・Task・Agentを円卓席の代用にしない。正式着席したメンバーはnative sub-agent、Aiterm外部agent、相談agent、自己実装を自由に選べる。子は自動的に円卓メンバーではなく、工程所有・統合・room報告は着席メンバーが保持する。親は二次委譲の手段を禁止・指定しない。視点が足りない・詰んでいるなら room で報告して援軍(join)を求める
@@ -12,7 +12,8 @@
12
12
 
13
13
  ## 作業ループ
14
14
 
15
- **ターンを終える直前に `post(to: "<自分の名前>", message: "[次の行動] ...")` を1回送れ。** wakeup-bridge はこの自己DMで次ターンを起こす。出さないと席は止まる。空の終了通知は使うな。
15
+ **次にやる仕事があるターンを終える直前に `post(to: "<自分の名前>", message: "[次の行動] ...")` を1回送れ。** wakeup-bridge はこの自己DMで次ターンを起こす。仕事があるのに出さないと席は止まる。空の終了通知は使うな。
16
+ **手番が無く待機に入るときは `[次の行動]` 自己DMを出すな。** 親へ `[待機]` を一度だけ送り、沈黙する。再開は親または他人からの inbound だけ。
16
17
 
17
18
  **探索順は active → ready → 待機である。** まず自分の active 議題を完了させる。無ければ議題を選ぶ。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。ターンを終える時は、次に行う作業または再確認条件を自分へDMする。
18
19
 
@@ -12,11 +12,12 @@
12
12
 
13
13
  ## 作業ループ
14
14
 
15
- **ターンを終える直前に `post(to: "<自分の名前>", message: "[次の行動] ...")` を1回送れ。** wakeup-bridge はこの自己DMで次ターンを起こす。出さないと席は止まる。空の終了通知は使うな。
15
+ **次にやる仕事があるターンを終える直前に `post(to: "<自分の名前>", message: "[次の行動] ...")` を1回送れ。** wakeup-bridge はこの自己DMで次ターンを起こす。仕事があるのに出さないと席は止まる。空の終了通知は使うな。
16
+ **手番が無く待機に入るときは `[次の行動]` 自己DMを出すな。** 親へ `[待機]` を一度だけ送り、沈黙する。再開は親または他人からの inbound だけ。待機自己DMは自分を起こし無限ループになる(2026-08-20 実測)。
16
17
 
17
18
  **作業を選ぶのも始めるのもあなたである。** 装置から仕事が降ってくることはないし、着手前に装置の許可を待つこともない(オーナー裁定 2026-08-09・改・裁定1)。Lattice が居る卓では、着手した後に装置が競合を見て介入してくることがある——それは次の「装置が介入してきた時」に従う。
18
19
 
19
- **探索順は active → ready → 待機である。** まず自分の active 工程を完了させる。無ければ全PLAN横断の ready を自律的に claim する。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。ターンを終える時は、次に行う作業または再確認条件を `post(to: "<自分の名前>", message: "[次の行動] ...")` で自分へDMし、空のturn終了通知は使わない。
20
+ **探索順は active → ready → 待機である。** まず自分の active 工程を完了させる。無ければ全PLAN横断の ready を自律的に claim する。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。待機のあと自分へ `[次の行動]` を送らない。次に行う作業があるターンだけ自己DMする。空の終了通知は使わない。
20
21
 
21
22
  1. `lattice todo status --json` で ready なタスクを見る。{{CLAIM_SCOPE}}
22
23
  2. 憲章の手順で `post(to: "all", message: "[claim] <タスク>")` を一度だけ送る。**この `[claim]` が唯一の着手通知である。別の `[工程着手]` や自動着手通知は送らない。** `[claim]` は独立した1発言で出し、完了報告や他タスクの話と同じ発言に畳まない
@@ -54,8 +54,10 @@ export は親 shell に伝播しないため**、これをしないまま `latti
54
54
  張り替え時は旧Monitorを止めてから起動する。
55
55
 
56
56
  どちらも`parent-join.sh`が先に作る`.team/parent-watch.json`のcursorを共有する。watcher不在中のDMは
57
- 次回起動時にcatch-upされ、親以外宛・親自身の発言・pingでは親を起こさない。`watch_error`が届いたら
58
- 通常のDMとして扱わず、番犬の再着卓を行う。
57
+ 次回起動時にcatch-upされ、親以外宛・親自身の発言・pingでは親を起こさない。Lattice の ready 件数と
58
+ active 件数は合計ではなくそれぞれ変化したら起こす(実装が全部閉じて親手番1件だけになっても起こす)。
59
+ `--follow` は親セッションの stdin が閉じたら終了する。切れた番犬が seq だけ進めて親が起きない状態を作らない。
60
+ `watch_error`が届いたら通常のDMとして扱わず、番犬の再着卓を行う。
59
61
 
60
62
  ## 試験結果の監査
61
63