peertable 0.4.30 → 0.4.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.4.30",
3
+ "version": "0.4.32",
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.4.30'
16
+ const MCP_VERSION = '0.4.32'
17
17
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
18
18
 
19
19
  const USAGE = `usage:
@@ -349,8 +349,10 @@ async function runDiagnostics(asJson) {
349
349
  'scripts/parent-watch-logic.mjs',
350
350
  'scripts/seat-credential.mjs',
351
351
  'scripts/ensure-room-mcp.mjs',
352
+ 'scripts/bridge-record-live.mjs',
352
353
  'scripts/leave-seat.sh',
353
354
  'scripts/change-seat.sh',
355
+ 'scripts/set-mission.sh',
354
356
  'scripts/post-message.mjs',
355
357
  // effort 専用の互換入口。change-seat.sh へ委譲するので、どちらが欠けても席設定変更が死ぬ
356
358
  'scripts/change-effort.sh',
package/skill/SKILL.md CHANGED
@@ -74,6 +74,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
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
  - 同じvendor内のmodel / effort変更はAitermが同一sessionと会話contextを保って行う。vendor変更だけは再起動を伴うため、本人はrole・工程正本・roomログから再着任する
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 を足さない。
77
78
 
78
79
  7. **親の着卓**(このセッション): `scripts/parent-join.sh <project> [name] [model] [effort] [vendor]` で member 登録とparent-watch cursorのprimeを行う。**`effort` は任意のまま据え置く**——席は `launch-seat.sh` が `--effort` で実際に設定するので「渡した値=実挙動」だが、親は既に走っているセッションで自分の effort を機械的に知る経路が無く、推測して載せると画面が嘘をつく。続けて、ClaudeとGrokはMonitor、Codexはyieldしたbackground tool taskとして**親宛DM番犬**を1世代だけ張る(形は下記「親の operating notes」の番犬仕様)。通常席用wakeup-bridgeに親を載せない。broadcastのkickoffは廃止済み。以後の post も API 直(同 notes)
79
80
  8. **起動確認**: room の members に全員いる / 最初の claim が room に流れる(Lattice 併用モードはそれが Lattice へ到達している=`lattice todo status --json` の active に出ることも確認する。単独モードは room の claim 宣言だけが到達の証拠)/ Web UI で観測できる、をチェックして報告する
@@ -231,6 +232,7 @@ witness をどう生成するかは**対象 project 側の作法に従う**(La
231
232
  - `--parallel-frontier` が要るのは、**ready が複数あって誰も着手していない frontier の最初の start だけ**(無いと `PARALLEL_DISPATCH_REQUIRED / parallel_frontier_requires_declaration` で弾かれる)。ready が1件だけ、または既に誰かが着手している frontier へ後から乗る場合は素の `start` でよい。フラグが効くのは**取る task が `next_ready` に居る時だけ**で、他人が着手済みの task へ付けると `PARALLEL_DISPATCH_INVALID / parallel_frontier_not_applicable` になる——「フラグが使えない」ではなく「**その task はもう空いていない**」の意味である。記録があるのに対象工程が未宣言・失効なら `todo start` は `INDEPENDENCE_UNVERIFIED` で拒否する(Lattice ADR 0182)。記録が無い plan の start は従来どおり助言だけ
232
233
  - **`done.sh` は feat SHA が `origin/main` の祖先でなければ canonical main へ merge して push する。** 親は着地しない。続けて remaining の independence を compile してから戻る。lattice run receipt の未着地は別軸で、警告のまま
233
234
  - **independence compile は remaining A を含める。** 現在の ready だけを compile すると、次の frontier の start が拒否される。`next_ready` が witness に無い compile 自体も `INDEPENDENCE_READY_UNDECLARED` で拒否する。stale なら席が `.team/scripts/independence-refresh.sh` を打つ。親は compile しない
235
+ - **mission が古くても親は書き換えない。** 工程が変わったら席が `set-mission.sh` を自分で打つ。チップと `[mission]` の1行が正本で、席の再起動はしない
234
236
  - 同時書込は `STORE_WRITE_CONFLICT` 等で明示的に負ける。1〜2 秒待って再実行すれば通る(正常系)
235
237
  - evidence は記述子 JSON。記述子ファイル自体も repo 内相対パスに置く(repo 外絶対パスは INVALID_ARGUMENTS)。`.team/scripts/done.sh` が正規経路。証跡の置き場は **`evidence/<plan_key>/<task_id>.md`**——task_id は campaign を跨いで再利用されるので、平置きにすると前の campaign の監査証跡を上書きで消す(2026-08-08 実測)
236
238
  - **外部ペイン(決定53)は Lattice 0.50.0 以降が要る。** それ以前の Lattice に `external_pane` 入りの `project.json` を差すと、identity 検証が完全一致キーで落ちて `lattice todo status` ごと死ぬ(`PROJECT_IDENTITY_INVALID / identity_schema_invalid`・0.49.0 で実測)。工程正本が読めなくなる=卓が止まるので、Lattice が古い環境では Lattice 併用 setup を走らせない
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // 常駐 bridge の record が「この pid のこの process」を指しているかの境界。
3
+ // Windows は pid を再利用する。alive な pid だけでは本人ではない
4
+ // (2026-08-21 実測: wakeup-bridge.json の pid が oracle-mcp に再利用され、
5
+ // 本物の配達は死んだまま ensure-bridge が起動済みと誤認した)。
6
+ import { readFileSync } from 'node:fs'
7
+ import { resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const STALE_MS = 90_000
11
+
12
+ export function bridgeRecordLive(record, now = Date.now()) {
13
+ if (!record || typeof record !== 'object') return false
14
+ const pid = Number(record.pid)
15
+ if (!Number.isInteger(pid) || pid <= 0) return false
16
+ try { process.kill(pid, 0) } catch { return false }
17
+ const stamp = record.last_progress_at || record.progress_at
18
+ if (typeof stamp !== 'string' || stamp.length === 0) return false
19
+ const at = Date.parse(stamp)
20
+ if (!Number.isFinite(at)) return false
21
+ return (now - at) <= STALE_MS
22
+ }
23
+
24
+ const invokedDirectly = Boolean(process.argv[1])
25
+ && resolve(fileURLToPath(import.meta.url)).toLowerCase() === resolve(process.argv[1]).toLowerCase()
26
+ if (invokedDirectly) {
27
+ const path = process.argv[2]
28
+ if (!path) process.exit(2)
29
+ let record
30
+ try { record = JSON.parse(readFileSync(path, 'utf8')) } catch { process.exit(1) }
31
+ process.exit(bridgeRecordLive(record) ? 0 : 1)
32
+ }
@@ -17,8 +17,7 @@ if [ $# -eq 0 ] && [ -f "$record" ]; then
17
17
  fi
18
18
  fi
19
19
  if [ -f "$record" ]; then
20
- pid=$(node -e 'try{process.stdout.write(String(require(process.argv[1]).pid||""))}catch{}' "$record")
21
- if [ -n "$pid" ] && node "$(dirname "$0")/pid-alive.mjs" "$pid"; then exit 0; fi
20
+ if node "$(dirname "$0")/bridge-record-live.mjs" "$record"; then exit 0; fi
22
21
  if ! "$force" && grep -q 'WRITE_DENIED' "$log" 2>/dev/null; then echo "${name}-bridge: 前回はWRITE_DENIEDで終了。--forceを指定すること" >&2; exit 1; fi
23
22
  fi
24
23
  # shellcheck disable=SC1091
@@ -0,0 +1,107 @@
1
+ #!/bin/bash
2
+ # 席の mission を更新する。再起動しない。
3
+ # usage: set-mission.sh <project_dir> <member> <text>
4
+ #
5
+ # chip(POST /members の mission 欄)と、他席へ届く `[mission] <name>: <text>` の1行が正本。
6
+ # 起動時の PEERTABLE_MISSION env は書き換えない。change-seat.sh には載せない。
7
+ set -eu
8
+
9
+ unset PEERTABLE_POST_TOKEN
10
+ script_dir=$(cd "$(dirname "$0")" && pwd -P)
11
+ credential_helper="${PEERTABLE_CREDENTIAL_HELPER:-$script_dir/seat-credential.mjs}"
12
+
13
+ proj="${1:-}"; name="${2:-}"
14
+ shift 2 2>/dev/null || true
15
+ text="${*:-}"
16
+ [ -n "$proj" ] && [ -n "$name" ] && [ -n "$text" ] || {
17
+ echo "SET_MISSION_ARGS_INVALID: usage: set-mission.sh <project_dir> <member> <text>" >&2
18
+ exit 2
19
+ }
20
+ case "$name" in
21
+ *[!A-Za-z0-9._-]*) echo "SET_MISSION_ARGS_INVALID: member名に使えない文字がある" >&2; exit 2 ;;
22
+ esac
23
+
24
+ state="$proj/.team/setup-state.json"
25
+ [ -f "$state" ] || { echo "SET_MISSION_STATE_MISSING: $state" >&2; exit 1; }
26
+ read -r room url <<EOF
27
+ $(node -e 'const fs=require("fs"); const d=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if(!d.room||!d.server_url) process.exit(1); process.stdout.write(d.room+" "+d.server_url+"\n")' "$state")
28
+ EOF
29
+
30
+ members=$(curl -sf "$url/api/$room/members") || {
31
+ echo "SET_MISSION_ROOM_UNREACHABLE: membersを読めない" >&2; exit 1
32
+ }
33
+ printf '%s' "$members" | node -e '
34
+ const fs=require("fs");
35
+ const name=process.argv[1];
36
+ let members;
37
+ try { members=JSON.parse(fs.readFileSync(0,"utf8")).members||[] } catch { process.exit(2) }
38
+ process.exit(members.some(m => m && m.name===name) ? 0 : 1)
39
+ ' "$name" || {
40
+ echo "SET_MISSION_MEMBER_MISSING: ${name} が room に居ない" >&2
41
+ exit 1
42
+ }
43
+
44
+ credential_file=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" path "$proj" "$room" "$name") || {
45
+ echo "SET_MISSION_CREDENTIAL_MISSING: ${name} のroom credentialを特定できない" >&2; exit 1
46
+ }
47
+
48
+ payload=$(node -e 'process.stdout.write(JSON.stringify({name:process.argv[1],mission:process.argv[2]})+"\n")' "$name" "$text")
49
+ env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
50
+ "$url/api/$room/members" "$payload" >/dev/null || {
51
+ echo "SET_MISSION_METADATA_FAILED: ${name} の mission を room へ書けない" >&2; exit 1
52
+ }
53
+
54
+ members_after=$(curl -sf "$url/api/$room/members") || {
55
+ echo "SET_MISSION_CHANGED_BUT_UNVERIFIED: mission を書いたが members を読めない" >&2; exit 1
56
+ }
57
+ printf '%s' "$members_after" | node -e '
58
+ const fs=require("fs");
59
+ const name=process.argv[1];
60
+ const want=process.argv[2];
61
+ let members;
62
+ try { members=JSON.parse(fs.readFileSync(0,"utf8")).members||[] } catch { process.exit(2) }
63
+ const m=members.find(x => x && x.name===name) || {};
64
+ process.exit(m.mission===want ? 0 : 1)
65
+ ' "$name" "$text" || {
66
+ echo "SET_MISSION_CHANGED_BUT_UNVERIFIED: members の mission が指定文と一致しない" >&2
67
+ exit 1
68
+ }
69
+
70
+ body="[mission] ${name}: ${text}"
71
+ history=$(node "$script_dir/post-message.mjs" "$name" "all" "$body")
72
+ history_response=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" POST \
73
+ "$url/api/$room/messages" "$history") || {
74
+ echo "SET_MISSION_CHANGED_BUT_ANNOUNCE_FAILED: chip は更新済み、[mission] の投稿に失敗" >&2
75
+ exit 1
76
+ }
77
+ history_seq=$(printf '%s' "$history_response" | node -e '
78
+ const fs=require("fs");
79
+ let seq;
80
+ try { seq=JSON.parse(fs.readFileSync(0,"utf8")).seq } catch { process.exit(1) }
81
+ if(!Number.isInteger(seq)) process.exit(1);
82
+ process.stdout.write(String(seq)+"\n");
83
+ ') || {
84
+ echo "SET_MISSION_CHANGED_BUT_ANNOUNCE_FAILED: chip は更新済み、[mission] POST 応答の seq を読めない" >&2
85
+ exit 1
86
+ }
87
+
88
+ messages_after=$(env -u PEERTABLE_POST_TOKEN node "$credential_helper" request "$credential_file" GET \
89
+ "$url/api/$room/messages") || {
90
+ echo "SET_MISSION_CHANGED_BUT_ANNOUNCE_FAILED: chip は更新済み、room 履歴を読み返せない" >&2
91
+ exit 1
92
+ }
93
+ printf '%s' "$messages_after" | node -e '
94
+ const fs=require("fs");
95
+ const seq=process.argv[1];
96
+ const name=process.argv[2];
97
+ const body=process.argv[3];
98
+ let messages;
99
+ try { messages=JSON.parse(fs.readFileSync(0,"utf8")).messages||[] } catch { process.exit(1) }
100
+ const matched=messages.find(m => String(m.seq)===seq);
101
+ process.exit(matched && matched.from===name && matched.to==="all" && matched.body===body ? 0 : 1)
102
+ ' "$history_seq" "$name" "$body" || {
103
+ echo "SET_MISSION_CHANGED_BUT_ANNOUNCE_FAILED: chip は更新済み、[mission] の読返しが一致しない" >&2
104
+ exit 1
105
+ }
106
+
107
+ echo "SET_MISSION_OK: ${name} mission=${text}"
@@ -18,6 +18,7 @@ import { join } from 'node:path'
18
18
  import { promisify } from 'node:util'
19
19
  import { resolveSeatObservation, tmuxArgv } from './seat-usage.mjs'
20
20
  import { BROADCAST_RECIPIENT, formatWakeNotice, isIdleSelfWake, isWakeupBridgeTarget, shouldDeferGrokWake } from './wakeup-delivery.mjs'
21
+ import { bridgeRecordLive } from './bridge-record-live.mjs'
21
22
 
22
23
  const run = promisify(execFile)
23
24
  const [proj, ...rest] = process.argv.slice(2)
@@ -64,10 +65,32 @@ async function acquireStartupLock() {
64
65
  }
65
66
  }
66
67
 
68
+ function readRecord() {
69
+ try { return JSON.parse(readFileSync(record, 'utf8')) } catch { return null }
70
+ }
71
+
72
+ function writeRecord(next) {
73
+ const temp = `${record}.${process.pid}.tmp`
74
+ writeFileSync(temp, JSON.stringify(next) + '\n')
75
+ renameSync(temp, record)
76
+ }
77
+
78
+ function touchProgress() {
79
+ if (!existsSync(record)) return
80
+ const current = readRecord()
81
+ if (!current || current.pid !== process.pid) return
82
+ writeRecord({ ...current, last_progress_at: new Date().toISOString() })
83
+ }
84
+
67
85
  async function stopRecorded() {
68
86
  if (!existsSync(record)) return
69
- const { pid } = JSON.parse(readFileSync(record, 'utf8'))
70
- if (!alive(pid)) { unlinkSync(record); log(`死んだ記録を掃除した(pid ${pid})`); return }
87
+ const saved = readRecord()
88
+ const pid = Number(saved?.pid)
89
+ if (!bridgeRecordLive(saved)) {
90
+ unlinkSync(record)
91
+ log(`他人の pid または死んだ記録を掃除した(pid ${Number.isInteger(pid) ? pid : '不明'})。kill しない`)
92
+ return
93
+ }
71
94
  process.kill(pid, 'SIGTERM')
72
95
  for (let i = 0; i < 25 && alive(pid); i++) await sleep(200)
73
96
  if (alive(pid)) {
@@ -101,9 +124,10 @@ const parentName = (() => {
101
124
  }
102
125
  })()
103
126
  const targetOpts = { parentName }
104
- writeFileSync(record, JSON.stringify({
105
- pid: process.pid, room, server_url: url, requested_seats: requestedSeats, started_at: new Date().toISOString(),
106
- }) + '\n')
127
+ writeRecord({
128
+ pid: process.pid, room, server_url: url, requested_seats: requestedSeats,
129
+ started_at: new Date().toISOString(), last_progress_at: new Date().toISOString(),
130
+ })
107
131
  releaseStartupLock()
108
132
 
109
133
  const cleanup = () => { if (existsSync(record)) unlinkSync(record); process.exit(0) }
@@ -167,10 +191,12 @@ async function refreshMembers() {
167
191
  reconcileSeats()
168
192
  await refreshMembers()
169
193
  function markReady() {
170
- const next = { ...JSON.parse(readFileSync(record, 'utf8')), ready_at: new Date().toISOString() }
171
- const temp = `${record}.tmp`
172
- writeFileSync(temp, JSON.stringify(next) + '\n')
173
- renameSync(temp, record)
194
+ const current = readRecord() || {}
195
+ writeRecord({
196
+ ...current,
197
+ ready_at: new Date().toISOString(),
198
+ last_progress_at: new Date().toISOString(),
199
+ })
174
200
  }
175
201
 
176
202
  function loadDeliveryState() {
@@ -436,6 +462,7 @@ for (;;) {
436
462
  let buf = ''
437
463
  for await (const chunk of res.body) {
438
464
  lastByteAt = Date.now()
465
+ touchProgress()
439
466
  buf += Buffer.from(chunk).toString('utf8')
440
467
  const parts = buf.split('\n\n')
441
468
  buf = parts.pop()
@@ -34,6 +34,18 @@
34
34
  しない。席が再起動された場合は、下の再着任手順でrole・`.team/tasks.md`・roomログから現在地を
35
35
  取り直す。
36
36
 
37
+ ## 工程が変わった時の mission
38
+
39
+ 着席時の mission は起動スナップショットである。議題の塊が次へ進んだら、自分で更新する。
40
+ 親に依頼しない。席は再起動しない。
41
+
42
+ ```
43
+ env -u PEERTABLE_POST_TOKEN "$(npm root -g)/peertable/skill/scripts/set-mission.sh" . "$PEERTABLE_MEMBER" "<新しい使命>"
44
+ ```
45
+
46
+ room の member 欄(チップ)と `[mission] <名前>: <使命>` の全員宛1行が正本になる。
47
+ 起動時の `PEERTABLE_MISSION` env は古いままでよい。
48
+
37
49
  ## 再着任(context が要約されたら)
38
50
 
39
51
  自分の context が要約された(=会話の前半が手元に無い)と気づいたら、実装を続ける前に `.team/roles/member.md` と `.team/CLAUDE.md` を読み直して着任し直し、room へ `[再着任] <名前>` を一行投稿する。進行中 claim の状態は自分の記憶でなく**工程正本で取り直す**——この卓の工程正本は room の宣言だけなので、`read_log` で自分の claim・他人の claim・完了報告を全部照合する(機械に問い合わせる先は無い)。記憶と正本が食い違ったら、正本を正として食い違いを room で報告する。
@@ -38,6 +38,18 @@
38
38
  完全一致の再送は不要で、変更targetは親が判断する。自分でCLI設定を変えたり、broadcastで依頼したり
39
39
  しない。席が再起動された場合は、下の再着任手順でrole・工程正本・roomログから現在地を取り直す。
40
40
 
41
+ ## 工程が変わった時の mission
42
+
43
+ 着席時の mission は起動スナップショットである。campaign / plan が次のフェーズへ進んだら、
44
+ 自分で更新する。親に依頼しない。席は再起動しない。
45
+
46
+ ```
47
+ env -u PEERTABLE_POST_TOKEN "$(npm root -g)/peertable/skill/scripts/set-mission.sh" . "$PEERTABLE_MEMBER" "<新しい使命>"
48
+ ```
49
+
50
+ room の member 欄(チップ)と `[mission] <名前>: <使命>` の全員宛1行が正本になる。
51
+ 起動時の `PEERTABLE_MISSION` env は古いままでよい。
52
+
41
53
  ## Lattice の実行層へ自分の着手を載せる(pull 型・載っている卓だけ)
42
54
 
43
55
  **仕事は降ってこない。** 上のループどおり自分で選んで `todo start` した後、その着手を実行層へ持ち込むと、隔離 worktree という設備が使え、装置が他の着手済み ToDo との競合を見てくれる。**持ち込みは許可申請ではない**——装置は通す/通さないを決めるのではなく、競合した時だけ「留まれ」と言う。載っていない卓ではこの節は静かに眠る。
@@ -21,6 +21,7 @@
21
21
  - 着卓(member 登録)と席数制御(決定68の運用側: ready+active実装ToDo数に合わせて起こす/畳む)
22
22
  - 作業者から監査担当への最終試験結果提出と、監査担当による工程クローズが正本へ記録されたことの観測
23
23
  - 着地と independence compile は席の仕事である。親は代行しない
24
+ - mission の更新は席の仕事である。親は代行しない
24
25
  - 承認 gate・オーナーとの接点、裁定依頼の運搬(自分で判断せずオーナー宛の議題として運ぶ)
25
26
  - model / effort 変更依頼への対応(本人の自然文を親が判断し、確定したtargetだけを
26
27
  `change-seat.sh`へ渡す。定型文への言い直しや完全一致の再送は求めない)