peertable 0.8.55 → 0.8.57

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.
Files changed (75) hide show
  1. package/README.ja.md +32 -33
  2. package/README.md +36 -37
  3. package/package.json +3 -1
  4. package/room/client.mjs +24 -33
  5. package/room/server.mjs +1 -1
  6. package/skill/SKILL.md +69 -94
  7. package/skill/scripts/aiterm-client.mjs +48 -0
  8. package/skill/scripts/aiterm-deliver.mjs +3 -1
  9. package/skill/scripts/change-seat.mjs +86 -0
  10. package/skill/scripts/change-seat.sh +3 -276
  11. package/skill/scripts/change-seat.test.mjs +129 -0
  12. package/skill/scripts/cli.mjs +77 -0
  13. package/skill/scripts/doctor.mjs +53 -0
  14. package/skill/scripts/doctor.sh +2 -213
  15. package/skill/scripts/ensure-all-bridges.mjs +42 -0
  16. package/skill/scripts/ensure-all-bridges.sh +3 -80
  17. package/skill/scripts/ensure-bridge.mjs +11 -0
  18. package/skill/scripts/ensure-bridge.sh +2 -95
  19. package/skill/scripts/ensure-codex-room-mcp.mjs +1 -1
  20. package/skill/scripts/ensure-project-runtime.mjs +85 -0
  21. package/skill/scripts/ensure-project-runtime.sh +2 -11
  22. package/skill/scripts/ensure-project-runtime.test.mjs +40 -0
  23. package/skill/scripts/install-skill.mjs +89 -0
  24. package/skill/scripts/install-skill.test.mjs +84 -0
  25. package/skill/scripts/launch-seat.mjs +163 -0
  26. package/skill/scripts/launch-seat.sh +3 -797
  27. package/skill/scripts/launch-seat.test.mjs +174 -0
  28. package/skill/scripts/leave-seat.mjs +44 -0
  29. package/skill/scripts/leave-seat.sh +3 -92
  30. package/skill/scripts/leave-seat.test.mjs +64 -0
  31. package/skill/scripts/legacy-entry.mjs +33 -0
  32. package/skill/scripts/platform/windows/resolve-lattice-command.mjs +12 -1
  33. package/skill/scripts/project-runtime.mjs +95 -0
  34. package/skill/scripts/project-runtime.test.mjs +29 -0
  35. package/skill/scripts/project-scaffold.mjs +179 -0
  36. package/skill/scripts/project-scaffold.test.mjs +129 -0
  37. package/skill/scripts/refresh-seat-identity.mjs +11 -8
  38. package/skill/scripts/remove-managed-room-mcp.mjs +9 -4
  39. package/skill/scripts/resume.sh +2 -225
  40. package/skill/scripts/room-api.mjs +28 -0
  41. package/skill/scripts/room-public-session.test.mjs +73 -0
  42. package/skill/scripts/runtime-contract.test.mjs +13 -146
  43. package/skill/scripts/runtime-launch-command.mjs +19 -0
  44. package/skill/scripts/runtime-launch-command.test.mjs +32 -0
  45. package/skill/scripts/seat-approval.mjs +24 -0
  46. package/skill/scripts/seat-approval.test.mjs +43 -0
  47. package/skill/scripts/seat-identity.mjs +2 -71
  48. package/skill/scripts/seat-launch-phase.mjs +11 -0
  49. package/skill/scripts/seat-observer.mjs +54 -0
  50. package/skill/scripts/seat-observer.test.mjs +58 -0
  51. package/skill/scripts/seat-session.mjs +17 -0
  52. package/skill/scripts/seat-status-bridge.mjs +33 -206
  53. package/skill/scripts/seat-usage.mjs +3 -334
  54. package/skill/scripts/setup.sh +3 -215
  55. package/skill/scripts/teardown.mjs +113 -0
  56. package/skill/scripts/teardown.sh +3 -410
  57. package/skill/scripts/teardown.test.mjs +62 -0
  58. package/skill/scripts/upgrade-team-assets.mjs +268 -0
  59. package/skill/scripts/upgrade-team-assets.sh +2 -282
  60. package/skill/scripts/wakeup-bridge.mjs +26 -32
  61. package/skill/scripts/wakeup-delivery.mjs +4 -14
  62. package/skill/templates/charter.md +1 -1
  63. package/skill/templates/member-standalone.md +1 -1
  64. package/skill/templates/member.md +3 -3
  65. package/skill/scripts/agent-pane-status.mjs +0 -6
  66. package/skill/scripts/aiterm-configure.mjs +0 -31
  67. package/skill/scripts/aiterm-launch.mjs +0 -64
  68. package/skill/scripts/aiterm-send.mjs +0 -22
  69. package/skill/scripts/claude-dialog.mjs +0 -26
  70. package/skill/scripts/codex-dialog.mjs +0 -67
  71. package/skill/scripts/platform/windows/build-bridge-command.mjs +0 -52
  72. package/skill/scripts/seat-input.mjs +0 -36
  73. package/skill/scripts/tmux-at.bash +0 -22
  74. package/skill/scripts/tmux-socket.mjs +0 -19
  75. package/skill/scripts/vendors/grok/pane-status.mjs +0 -30
@@ -1,109 +1,14 @@
1
- import { createHash } from 'node:crypto'
2
- import { existsSync, readFileSync, readdirSync } from 'node:fs'
3
- import { execFileSync } from 'node:child_process'
4
- import { homedir, tmpdir } from 'node:os'
5
- import path, { join } from 'node:path'
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
6
4
  import { resolveWindowsCommand, resolveWindowsLatticeCommand } from './platform/windows/resolve-lattice-command.mjs'
7
- import { classifyGrokPaneTail, isGrokPrivacyBanner } from './vendors/grok/pane-status.mjs'
8
-
9
- const TOKEN_HINT = /[↓↑]\s*([0-9]+(?:\.[0-9]+)?)\s*([kKmM]?)\s*tokens\b/gu
10
5
 
11
6
  const POST_TOKEN_LINE = /^\s*(?:export\s+)?PEERTABLE_POST_TOKEN\s*=\s*(.*)$/
12
7
 
13
- const TOKEN_MULTIPLIER = Object.freeze({
14
- '': 1,
15
- k: 1_000,
16
- m: 1_000_000,
17
- })
18
-
19
8
  export function supportsMemberObservation(payload) {
20
9
  return payload?.capabilities?.member_observation_v1 === true
21
10
  }
22
11
 
23
- /** aiterm-mcp の POSIX 既定ソケット規則。Windows の path.join で `/tmp` を壊さない。 */
24
- export function defaultTmuxSocket(env) {
25
- return path.posix.join(env.TMPDIR || '/tmp', 'claude-tmux-sockets', 'claude.sock')
26
- }
27
-
28
- const defaultTmuxProbe = {
29
- serverAlive(socket) {
30
- try {
31
- execFileSync('tmux', ['-S', socket, 'ls'], { stdio: 'ignore' })
32
- return true
33
- } catch {
34
- return false
35
- }
36
- },
37
- listAitermSockets() {
38
- try {
39
- return readdirSync('/tmp')
40
- .filter(name => /^aiterm-.*\.sock$/u.test(name))
41
- .map(name => join('/tmp', name))
42
- } catch {
43
- return []
44
- }
45
- },
46
- }
47
-
48
- /**
49
- * 明示設定、既定、検証済み発見の順で tmux socket を決める。
50
- * 見つからない既定値は launch-seat.sh が新しい server を作るために必要なので返す。
51
- */
52
- export function resolveTmuxSocket(env, probe = defaultTmuxProbe) {
53
- if (env.PEERTABLE_TMUX_SOCKET) {
54
- return { socket: env.PEERTABLE_TMUX_SOCKET, source: 'explicit', candidates: [], error: null }
55
- }
56
- const fallback = defaultTmuxSocket(env)
57
- if (probe.serverAlive(fallback)) {
58
- return { socket: fallback, source: 'default', candidates: [], error: null }
59
- }
60
- const candidates = probe.listAitermSockets().filter(socket => probe.serverAlive(socket))
61
- if (candidates.length === 1) {
62
- return { socket: candidates[0], source: 'discovered', candidates, error: null }
63
- }
64
- if (candidates.length > 1) {
65
- return {
66
- socket: null,
67
- source: 'none',
68
- candidates,
69
- error: { code: 'PEERTABLE_TMUX_SOCKET_AMBIGUOUS', message: '検証済み aiterm socket が複数あります' },
70
- }
71
- }
72
- return { socket: fallback, source: 'default', candidates: [], error: null }
73
- }
74
-
75
- /**
76
- * Windows の psmux は -S を黙って既定 namespace へ落とす(aiterm 実測 2026-08-16)。
77
- * aiterm と同じ -L namespace を使う。名前の式は aiterm-mcp core.js の WIN_NS と一致させる。
78
- */
79
- export function usesPsmuxNamespace(env = process.env, platform = process.platform) {
80
- if (env.PEERTABLE_FORCE_POSIX_TMUX === '1') return false
81
- if (env.PEERTABLE_TMUX_L || env.AITERM_PSMUX_NS) return true
82
- return platform === 'win32'
83
- }
84
-
85
- export function aitermPsmuxNamespace(env = process.env, platform = process.platform) {
86
- if (env.PEERTABLE_TMUX_L) return env.PEERTABLE_TMUX_L
87
- if (env.AITERM_PSMUX_NS) return env.AITERM_PSMUX_NS
88
- const isWin = platform === 'win32'
89
- const root = env.TMPDIR ?? (isWin ? env.TEMP ?? tmpdir() : '/tmp')
90
- const joinPath = isWin ? path.win32.join : path.posix.join
91
- const sockdir = joinPath(root, 'claude-tmux-sockets')
92
- return `aiterm-${createHash('sha1').update(sockdir).digest('hex').slice(0, 12)}`
93
- }
94
-
95
- /** tmux/psmux へ渡す接続引数。POSIX は -S socket、Windows は -L namespace。 */
96
- export function tmuxArgv(extraArgs = [], { socket, env = process.env, platform = process.platform } = {}) {
97
- if (usesPsmuxNamespace(env, platform)) {
98
- return ['-L', aitermPsmuxNamespace(env, platform), ...extraArgs]
99
- }
100
- return ['-S', socket, ...extraArgs]
101
- }
102
-
103
- /**
104
- * npm の extensionless shim は Windows の execFile で ENOENT。
105
- * 同じnpm binにある .ps1 shimをPowerShell 7で起動し、cmd.exeへfallbackしない。
106
- */
107
12
  export function resolveLatticeExecutable(cli, { platform = process.platform, exists = existsSync } = {}) {
108
13
  if (typeof cli !== 'string' || !cli) return { command: cli, argv: ['todo', 'status', '--json'] }
109
14
  if (platform !== 'win32') return { command: cli, argv: ['todo', 'status', '--json'] }
@@ -118,18 +23,6 @@ export function resolveLatticeInvocation(cli, argv, {
118
23
  return resolveWindowsCommand(cli, argv, { platform, exists, pwsh })
119
24
  }
120
25
 
121
- /** member の自己申告を優先し、無い既存 member だけ旧 session 名へ互換フォールバックする。 */
122
- export function resolveSeatObservation(member, defaultSocket) {
123
- const observe = member?.observe
124
- if (observe && typeof observe === 'object' && typeof observe.tmux_target === 'string' && observe.tmux_target) {
125
- const socket = observe.tmux_socket || defaultSocket
126
- return socket ? { socket, target: observe.tmux_target, source: 'descriptor' } : null
127
- }
128
- return defaultSocket && member?.name
129
- ? { socket: defaultSocket, target: `peer-${member.name}`, source: 'legacy' }
130
- : null
131
- }
132
-
133
26
  /**
134
27
  * `~/.config/peertable.env` の本文から書込トークンを読む。**`export` の有無に依存しない。**
135
28
  * 2026-08-10 実測: `export` を落とした設定ファイルを `source` した shell から `nohup node …` で
@@ -178,133 +71,6 @@ export function decideBridgeContinuation({ attempted, failed, provenWritable, fa
178
71
  return { verdict: next >= limit ? 'unreachable' : 'degraded', provenWritable, failedTicks: next }
179
72
  }
180
73
 
181
- /**
182
- * tmux セッションが見つからない席の扱いを決める。**tmux 席を持たない member(親など)は
183
- * 一度も観測できない**ので `null`(送らない)。**過去に観測できていた席が消えたら実際に落ちた**
184
- * ので `dead` を返す。previous は直前に送信できた観測(`{status,...}` か undefined)。
185
- */
186
- export function deriveMissingSession(previous) {
187
- return previous ? { status: 'dead', busySince: null, paneTokenHint: null } : null
188
- }
189
-
190
- // launch-seat.sh:74-77 と docs/plan.md §11 が実測で記録した既知ダイアログ文言の集合
191
- export const BLOCKED_MARKERS = [
192
- '1. Yes, I trust this folder',
193
- '1. I am using this for local development',
194
- '1. Yes, continue',
195
- 'Do you want to proceed?',
196
- // Codex v0.148 のコマンド承認ダイアログ(2026-08-22 実測: 承認待ちの席が idle と
197
- // 誤表示され、オーナーが「動いているのに表示が変わらない」と気づいた)
198
- 'Would you like to run the following command?',
199
- '1. Yes, proceed',
200
- 'Press enter to confirm or esc to cancel',
201
- ]
202
-
203
- export { isGrokPrivacyBanner }
204
-
205
- // 承認画面や接続失敗は折返し・キュー表示で14行より上へ押し出される。現在のturnを
206
- // 覆いながら、古いscrollback全体を状態に混ぜない固定窓として32行を読む。
207
- export const PANE_STATUS_TAIL_LINES = 32
208
-
209
- export function paneStatusTail(pane) {
210
- return String(pane ?? '').split('\n').slice(-PANE_STATUS_TAIL_LINES).join('\n')
211
- }
212
-
213
- /**
214
- * pane 末尾の生文字列から画面状態を判定する。判定順は busy → blocked → idle
215
- * (承認プロンプト表示中は `esc to interrupt` が消えるので busy を先に見る)。
216
- */
217
- export function classifyPaneTail(tail) {
218
- if (typeof tail !== 'string') return 'idle'
219
- if (tail.includes('esc to interrupt')) return 'busy'
220
- // Claude Code 2.1 / Fable TUI は思考中に esc to interrupt を出さない(2026-08-19 実測)。
221
- // 動名詞(Sprouting / Wibbling 等)は毎回変わるので使わない。
222
- // 思考中の固定句に加え、ツール実行中は経過時間付きスピナー行と /btw の固定ヒントが出る。
223
- if (tail.includes('thinking with') || tail.includes('almost done thinking')) return 'busy'
224
- if (tail.includes("without interrupting Claude's current work")) return 'busy'
225
- if (tail.includes('Calling tools')) return 'busy'
226
- if (/…\s*\(\d+(?:m \d+)?s\b/u.test(tail)) return 'busy'
227
- const grokStatus = classifyGrokPaneTail(tail)
228
- if (grokStatus !== null) return grokStatus
229
- if (BLOCKED_MARKERS.some(marker => tail.includes(marker))) return 'blocked'
230
- return 'idle'
231
- }
232
-
233
- /**
234
- * pane 内で実行中の CLI プロセスの pid(`#{pane_pid}`)を読む。tmux pane 自体は生きたまま
235
- * (`pane_dead=0`)、中の CLI プロセスだけが停止する局面があるため、pane の生死とは別に
236
- * このプロセス自身の状態を見る必要がある。
237
- */
238
- export function tmuxPanePid(socket, target, exec = execFileSync) {
239
- try {
240
- const out = exec('tmux', tmuxArgv(['list-panes', '-t', target, '-F', '#{pane_pid}'], { socket }), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
241
- const pid = out.trim().split('\n')[0]
242
- return pid && /^[0-9]+$/.test(pid) ? pid : null
243
- } catch {
244
- return null
245
- }
246
- }
247
-
248
- /**
249
- * pane プロセス(またはその直接の子)が stopped(`ps` stat の先頭が `T`)かどうかを見る。
250
- * 実測(2026-08-11, bell/suzune): Lattice pull run の accept hold は attach 済み worker
251
- * (pane の子で pid===pgid のもの)へ SIGSTOP を送る。tmux pane は shell も含めて生きたまま
252
- * (`pane_dead=0`)で、末尾の画面は停止直前のまま残るため、`classifyPaneTail` だけでは
253
- * `idle` と誤判定する。
254
- */
255
- /**
256
- * paneのshellだけが生きていて中のharnessが死んだ席をdeadと判定する素材(実被弾 2026-08-26:
257
- * mioのCodexが落ちて素のbashが露出、ランプはidle表示のまま番犬DMがshellへ打鍵され続けた)。
258
- * pane processがshell(bash/zsh/sh系)で、子プロセスが1つも居なければ、席のharnessは存在しない。
259
- * pane processがharness本体(shellでない)の場合はこの判定の対象外——それが生きていれば席は生きている。
260
- */
261
- export function isPaneHarnessMissing(psRows, panePid) {
262
- if (!panePid) return false
263
- let command = null
264
- let hasChild = false
265
- for (const row of psRows) {
266
- const m = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(row)
267
- if (!m) continue
268
- if (m[1] === String(panePid)) command = m[3].trim()
269
- if (m[2] === String(panePid)) hasChild = true
270
- }
271
- if (command === null) return false
272
- const bin = command.split(/\s+/)[0].split('/').pop()
273
- if (!/^(?:-?bash|-?zsh|-?sh|-?dash|-?ksh)$/.test(bin)) return false
274
- return !hasChild
275
- }
276
-
277
- export function isPaneProcessStopped(panePid, exec = execFileSync) {
278
- if (!panePid) return false
279
- try {
280
- const out = exec('ps', ['-o', 'pid=,ppid=,stat=', '-A'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
281
- return out.split('\n').some((line) => {
282
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)/u.exec(line)
283
- if (!match) return false
284
- const [, pid, ppid, stat] = match
285
- return (pid === String(panePid) || ppid === String(panePid)) && stat.startsWith('T')
286
- })
287
- } catch {
288
- return false
289
- }
290
- }
291
-
292
- /**
293
- * paneのstatus行が公開しているtoken値だけを読む。
294
- * harness固有のログや課金単価は推測せず、表示が無い席はnullのままにする。
295
- */
296
- export function parsePaneTokenHint(pane) {
297
- if (typeof pane !== 'string') return null
298
- let latest = null
299
- for (const match of pane.matchAll(TOKEN_HINT)) {
300
- const multiplier = TOKEN_MULTIPLIER[match[2].toLowerCase()]
301
- const value = Math.round(Number(match[1]) * multiplier)
302
- if (Number.isSafeInteger(value) && value >= 0) latest = value
303
- }
304
- return latest
305
- }
306
-
307
- // ---- claim巡回番犬の純関数(seat-status-bridge.mjsが使う。testはここからimportする)----
308
74
  export const STOP_DECLARATION = /\[待機\]|\[監査提出\]|待機します|散会/u
309
75
 
310
76
  export function patrolTargets({ activeTasks, messages, statusOf, lastBusyStartAt, now, lastNag, nagIntervalMs }) {
@@ -368,103 +134,6 @@ export function combineSeatLamp(paneStatus, job) {
368
134
  return paneStatus // 'dead' 等はそのまま
369
135
  }
370
136
 
371
- // ---- 預け仕事セッションの帰属(2026-08-26 オーナー裁定: 規約名でなくOSの継承機構で辿る)----
372
- // 正はsession envのPEERTABLE_MEMBER——席のシェルが持つ値をtmuxのupdate-environmentが作成時に
373
- // 自動で写したもので、席は名前も申告も要らない。envが無い既存セッションだけ、旧来の
374
- // `peer-<name>-` 前置の最長一致で拾う(後方互換。stamp導入前に立った常駐を殺さないため)。
375
- // 席セッションそのもの(`peer-<name>` 完全一致)は預け仕事ではない。
376
- export function attributeJobSession({ session, memberNames, env }) {
377
- if (memberNames.includes(session.replace(/^peer-/, ''))) return null
378
- if (env !== null && env !== undefined && memberNames.includes(env)) return env
379
- let best = null
380
- for (const n of memberNames) {
381
- if (session.startsWith(`peer-${n}-`) && (best === null || n.length > best.length)) best = n
382
- }
383
- return best
384
- }
385
-
386
- // paneプロセスの子孫に実働(CPU消費)が居るかの判定素材。psの `pid ppid pcpu` 出力を受け取り、
387
- // root配下の子孫(root自身は除く)に pcpu >= 閾値 が居れば true。席がCodex内蔵background terminal等、
388
- // tmuxセッションを作らずに走らせた預け仕事を、プロセスツリーの実観測で拾う(2026-08-25 オーナー裁定
389
- //「子プロセスも見る」)。nohup等でツリーから切り離された仕事はここでは見えない——それは
390
- // peer-<name>-job* セッション慣例の側が受け持つ(二本立て)。
391
- export function hasActiveDescendant(psRows, rootPid, { minCpu = 1.0, minAgeGapSeconds = 60 } = {}) {
392
- const parseEtime = (raw) => {
393
- // ps etime: [[dd-]hh:]mm:ss
394
- const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(raw)
395
- if (!m) return null
396
- return (Number(m[1] ?? 0) * 86400) + (Number(m[2] ?? 0) * 3600) + (Number(m[3]) * 60) + Number(m[4])
397
- }
398
- const children = new Map()
399
- const cpu = new Map()
400
- const age = new Map()
401
- for (const row of psRows) {
402
- const m = /^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+(\S+)/.exec(row)
403
- if (!m) continue
404
- const [pid, ppid, pcpu] = [Number(m[1]), Number(m[2]), Number(m[3])]
405
- if (!children.has(ppid)) children.set(ppid, [])
406
- children.get(ppid).push(pid)
407
- cpu.set(pid, pcpu)
408
- age.set(pid, parseEtime(m[4]))
409
- }
410
- const rootAge = age.get(rootPid)
411
- const queue = [...(children.get(rootPid) ?? [])]
412
- while (queue.length) {
413
- const pid = queue.pop()
414
- // 席と同時に起動したプロセスは足場(CLI本体・MCP server群)であって預け仕事ではない。
415
- // 足場はアイドルでも1%前後のCPUを食い、ランプを恒常的に点滅させる誤検知源になる
416
- // (実被弾 2026-08-25: 監査待ちのkoharuが「ずっとアクティブ」に見えた)。
417
- // 「paneより有意に後から生まれた」ことだけを預け仕事の観測条件にする——起動時刻は
418
- // 観測できる事実であり、コマンド名の恣意的なリストを持たない。
419
- const laterBorn = rootAge != null && age.get(pid) != null && rootAge - age.get(pid) >= minAgeGapSeconds
420
- if (laterBorn && (cpu.get(pid) ?? 0) >= minCpu) return true
421
- queue.push(...(children.get(pid) ?? []))
422
- }
423
- return false
424
- }
425
-
426
- // pane子孫(filter該当分)の累積CPU時間合計(秒)。pcpu%は減衰平均でIO待ち主体のジョブが0.0へ
427
- // 丸まる(実被弾 2026-08-25: 毎分数百リクエストの収集がpcpu 0.0で「非稼働」表示)。
428
- // 累積CPU時間は実カウンタで、働いていれば周期間で必ず増える——「変化が発生しているか」を見る。
429
- // psの `pid ppid time etime` 出力を受ける。filterは(childAgeSec, rootAgeSec)=>boolで足場除外等に使う。
430
- export function subtreeCpuSeconds(psRows, rootPid, { includeChild = () => true, includeRoot = false } = {}) {
431
- // 小数部を捨てない: 30秒周期で数msずつ進む常駐ジョブは、秒未満を切ると差分が永久に0になり
432
- // 「生きているのに稼働として観測されない」(実被弾 2026-08-26: mioのp5daily監督ループ)
433
- const parseClock = (raw) => {
434
- const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)$/.exec(raw)
435
- if (!m) return null
436
- return (Number(m[1] ?? 0) * 86400) + (Number(m[2] ?? 0) * 3600) + (Number(m[3]) * 60) + Number(m[4])
437
- }
438
- const children = new Map()
439
- const rec = new Map()
440
- for (const row of psRows) {
441
- const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/.exec(row)
442
- if (!m) continue
443
- const [pid, ppid] = [Number(m[1]), Number(m[2])]
444
- if (!children.has(ppid)) children.set(ppid, [])
445
- children.get(ppid).push(pid)
446
- rec.set(pid, { cpu: parseClock(m[3]), age: parseClock(m[4]) })
447
- }
448
- const rootAge = rec.get(rootPid)?.age ?? null
449
- let total = 0
450
- // 席paneでは root(harness本体)は常時CPUを進めるので数えない(足場除外と同じ理由)。
451
- // jobセッションでは root(pane pid)そのものが預け仕事の本体なので数える——数えないと
452
- // 監督ループ型のジョブが丸ごと不可視になる(実被弾 2026-08-26: mioのp5dailyがidle表示)
453
- if (includeRoot) total += rec.get(rootPid)?.cpu ?? 0
454
- const queue = [...(children.get(rootPid) ?? [])]
455
- while (queue.length) {
456
- const pid = queue.pop()
457
- const r = rec.get(pid)
458
- if (r && includeChild(r.age, rootAge)) total += r.cpu ?? 0
459
- queue.push(...(children.get(pid) ?? []))
460
- }
461
- return total
462
- }
463
-
464
- // ---- 親番犬の「起こす価値」判定 ----
465
- // 2026-08-29裁定「番犬が必要な時だけ呼べ」で[待機]DMを沈黙させていたが、
466
- // 2026-08-30裁定「待機にお前が気付けないのが問題だ。待機宣言は届くようにしろ」で撤回。
467
- // 親宛DMは待機宣言を含めて全件起こす。room全体発言だけは従来どおり選別する。
468
137
  export function parentWatchShouldNotify(message, roomUpdate) {
469
138
  const body = String(message?.body ?? '')
470
139
  if (roomUpdate) return /全タスク完了|\[オーナー宛/u.test(body)
@@ -1,216 +1,4 @@
1
1
  #!/bin/bash
2
- # Peertable setup の機械部分: .team/ scaffold と git 除外。
3
- # usage: setup.sh <project_dir> <room> <server_url> <plan_key|-> <peertable_repo> [tasks_file] [--phase <id>]...
4
- # plan_key `-` を渡すと単独円卓モード(工程正本を持たない。決定47)。
5
- # 単独モードでは tasks_file(聞き取ったタスクを書いた本文)が必須で、議題表 .team/tasks.md になる。
6
- # --phase は複数指定可。指定すると卓の claim 範囲がその phase の task に限られる。
7
- # 指定なしは plan 全体。他 campaign と同じ plan へ相乗りする時に、範囲外 phase の越境を止めるためのもの。
8
- set -e
9
- proj="$1"; room="$2"; url="$3"; plan="$4"; repo="$5"
10
- [ $# -ge 5 ] && shift 5 || shift $#
11
- # 第6引数の tasks_file は単独円卓モードだけが使う。`--` で始まるものはオプションなので
12
- # 位置引数として食わない——食うと Lattice 併用モードの `… <repo> --phase p2` が
13
- # 「未知の引数: p2」という原因を指さないエラーで落ちる(2026-08-08 実測・kotoha 監査)。
14
- tasks=""
15
- if [ $# -gt 0 ]; then
16
- case "$1" in
17
- -) shift ;; # 明示的な「tasks_file 無し」
18
- -*) ;; # オプション(綴り誤りも含む)。下のループで typed に落とす
19
- *) tasks="$1"; shift ;; # tasks_file
20
- esac
21
- fi
22
-
23
- phases=()
24
- while [ $# -gt 0 ]; do
25
- case "$1" in
26
- --phase)
27
- shift
28
- [ -n "$1" ] || { echo "ERROR: --phase には phase id が要る" >&2; exit 1; }
29
- case "$1" in
30
- *[!A-Za-z0-9._-]*) echo "ERROR: phase id に使えない文字がある: $1" >&2; exit 1 ;;
31
- esac
32
- phases+=("$1")
33
- ;;
34
- *) echo "ERROR: 未知の引数: $1(受けるのは --phase <id> だけ。tasks_file は単独円卓モード専用で、オプションより前に置く)" >&2; exit 1 ;;
35
- esac
36
- shift
37
- done
38
- tpl="$repo/skill/templates"
39
- tdir="$proj/.team"
40
-
41
- if [ "$plan" = "-" ] || [ -z "$plan" ]; then
42
- mode=standalone; plan=""
43
- # 単独円卓モードに phase は無い(工程正本を持たないため)。黙って無視せず止める
44
- [ ${#phases[@]} -eq 0 ] || { echo "ERROR: 単独円卓モードに --phase は使えない(工程正本を持たないため)" >&2; exit 1; }
45
- # 引数の検証は project へ何か置く前に済ませる(不可侵原則: 半端な .team/ を残さない)
46
- if [ -z "$tasks" ] || [ ! -f "$tasks" ]; then
47
- echo "ERROR: 単独円卓モードは議題表の本文ファイル(第6引数)が必須: setup.sh ... - <peertable_repo> <tasks_file>" >&2
48
- exit 1
49
- fi
50
- else
51
- mode=lattice
52
- fi
53
-
54
- # `.team/` は setup が所有する使い捨ての生成領域だが、既存 project の資産を
55
- # その名前だけで所有物だと決めてはいけない。追跡済み file は project の意図した資産、
56
- # 未追跡の残りは前卓の残骸か利用者の資産かを setup だけでは判定できない。
57
- # どちらも write 前に typed conflict として止め、退避・削除・上書きはしない。
58
- team_dir="$proj/.team"
59
- team_existing=""
60
- if [ -L "$team_dir" ] || { [ -e "$team_dir" ] && [ ! -d "$team_dir" ]; }; then
61
- team_existing="$team_dir"
62
- elif [ -d "$team_dir" ]; then
63
- team_existing=$(find "$team_dir" -mindepth 1 -print | sort)
64
- fi
65
- team_tracked=""
66
- if git -C "$proj" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
67
- team_tracked=$(git -C "$proj" ls-files --cached -- .team)
68
- fi
69
- if [ -n "$team_existing" ] || [ -n "$team_tracked" ]; then
70
- echo "PEERTABLE_SETUP_TEAM_CONFLICT: .team/ に既存資産があるため、setup は書き込まない" >&2
71
- if [ -n "$team_tracked" ]; then
72
- echo " tracked(project所有として扱い、上書きしない):" >&2
73
- printf ' %s\n' "$team_tracked" >&2
74
- fi
75
- if [ -n "$team_existing" ]; then
76
- echo " existing(前卓の残骸か利用者資産かを推測せず、触らない):" >&2
77
- printf ' %s\n' "$team_existing" >&2
78
- fi
79
- exit 1
80
- fi
81
-
82
- # Lattice 併用モードは、公開CLIをprojectへ何か置く前に確定する。通常はglobal installされた
83
- # lattice を使い、release前のsource treeを実測する時だけ `LATTICE_CLI` で同じtreeのbinを明示する。
84
- # **work-order adapter binary はもう要らない**(配車を撤去したので登録しない)。ここで見るのは
85
- # CLI の存在だけで、席が `todo status` / `run intake` を叩ける前提の確認である。
86
- lattice_cli=""
87
- if [ "$mode" = "lattice" ]; then
88
- lattice_cli="${LATTICE_CLI:-$(command -v lattice 2>/dev/null || true)}"
89
- [ -n "$lattice_cli" ] || { echo "ERROR: lattice CLI が見つからない" >&2; exit 1; }
90
- [ -x "$lattice_cli" ] || { echo "ERROR: lattice CLI が実行可能fileでない: $lattice_cli" >&2; exit 1; }
91
- lattice_cli=$(node -e 'process.stdout.write(require("node:fs").realpathSync(process.argv[1]))' "$lattice_cli")
92
-
93
- # config_refはgit root相対の公開契約。subdirectoryをprojectとして受けると別の
94
- # `.lattice/` を作ってしまうので、黙って親repoへ登録せずtypedに止める。
95
- project_root=$(node -e 'process.stdout.write(require("node:fs").realpathSync(process.argv[1]))' "$proj")
96
- git_root=$(git -C "$proj" rev-parse --show-toplevel 2>/dev/null || true)
97
- [ -n "$git_root" ] || { echo "ERROR: Lattice 併用モードのprojectはgit repositoryでなければならない: $proj" >&2; exit 1; }
98
- git_root=$(node -e 'process.stdout.write(require("node:fs").realpathSync(process.argv[1]))' "$git_root")
99
- [ "$project_root" = "$git_root" ] || {
100
- echo "ERROR: project_dirはgit rootを指さなければならない: project=$project_root git_root=$git_root" >&2
101
- exit 1
102
- }
103
- fi
104
-
105
- mkdir -p "$tdir/roles" "$tdir/scripts"
106
- cp "$tpl/charter.md" "$tdir/CLAUDE.md"
107
- cp "$tpl/parent.md" "$tdir/roles/parent.md"
108
- if [ "$mode" = "standalone" ]; then
109
- cp "$tpl/member-standalone.md" "$tdir/roles/member.md"
110
- cat "$tpl/tasks.md" "$tasks" > "$tdir/tasks.md"
111
- else
112
- # claim 範囲は席へ渡す文書に焼き込む。範囲の出典を「誰かの記憶」でなく role 文書にする
113
- if [ ${#phases[@]} -eq 0 ]; then
114
- scope="この卓の claim 範囲は plan 全体(phase 指定なしで立っている)。"
115
- else
116
- scope="**この卓の claim 範囲は phase ${phases[*]} の task だけ**。範囲外の phase の task は、ready に見えていても取らない——同じ plan へ別 campaign が相乗りしている時、範囲外を取ると他卓の工程を横取りする(越境が2回実測されたことへの対処)。範囲外に手を入れる必要が出たら room へ出して裁定を仰ぐ。"
117
- fi
118
- sed -e "s|{{PLAN_KEY}}|$plan|g" -e "s|{{CLAIM_SCOPE}}|$scope|g" "$tpl/member.md" > "$tdir/roles/member.md"
119
- cp "$tpl/done.sh" "$tdir/scripts/done.sh" && chmod +x "$tdir/scripts/done.sh"
120
- cp "$tpl/independence-refresh.sh" "$tdir/scripts/independence-refresh.sh" && chmod +x "$tdir/scripts/independence-refresh.sh"
121
- fi
122
- # 目覚まし係の登録口は全モード共通で配る(member.md の待機作法が参照する)
123
- cp "$repo/skill/scripts/alarm-set.sh" "$tdir/scripts/alarm-set.sh" && chmod +x "$tdir/scripts/alarm-set.sh"
124
- cp "$repo/skill/scripts/alarm-write.mjs" "$tdir/scripts/alarm-write.mjs" && chmod +x "$tdir/scripts/alarm-write.mjs"
125
- cp "$repo/skill/scripts/alarm-condition.mjs" "$tdir/scripts/alarm-condition.mjs" && chmod +x "$tdir/scripts/alarm-condition.mjs"
126
- # member.md が attach 手順で参照する。配り漏れると席が attach できず停止する(実被弾 2026-08-22)
127
- cp "$repo/skill/scripts/pull-attach-input.mjs" "$tdir/scripts/pull-attach-input.mjs" && chmod +x "$tdir/scripts/pull-attach-input.mjs"
128
- # room MCP 定義は project root の .mcp.json が正(channels は --mcp-config を解決しない。決定44)
129
- added_root_mcp=false
130
- if [ -f "$proj/.mcp.json" ]; then
131
- echo "WARN: $proj/.mcp.json が既に存在する。上書きしない。room の server 定義を手動 merge し、teardown で復元すること" >&2
132
- else
133
- sed "s|{{PEERTABLE_REPO}}|$repo|g" "$tpl/mcp.json" > "$proj/.mcp.json"
134
- added_root_mcp=true
135
- fi
136
-
137
- added_exclude=false
138
- if [ -d "$proj/.git" ] && ! grep -qx '\.team/' "$proj/.git/info/exclude" 2>/dev/null; then
139
- mkdir -p "$proj/.git/info"
140
- echo '.team/' >> "$proj/.git/info/exclude"
141
- added_exclude=true
142
- fi
143
- added_mcp_exclude=false
144
- if [ "$added_root_mcp" = "true" ] && [ -d "$proj/.git" ] && ! grep -qx '/\.mcp\.json' "$proj/.git/info/exclude" 2>/dev/null; then
145
- mkdir -p "$proj/.git/info"
146
- echo '/.mcp.json' >> "$proj/.git/info/exclude"
147
- added_mcp_exclude=true
148
- fi
149
-
150
- lattice_preexisting=false
151
- [ -d "$proj/.lattice" ] && lattice_preexisting=true
152
- runtime_preexisting=false
153
- [ -d "$proj/.lattice/runtime" ] && runtime_preexisting=true
154
-
155
- # adapter registry/config/spool はhost固有のruntime stateであり、sourceとして追跡しない。
156
- # `.lattice/`の一部を正本として追跡するprojectでもruntimeだけをroot相対で除外する。
157
- added_runtime_exclude=false
158
- if [ "$mode" = "lattice" ] && [ -d "$proj/.git" ] \
159
- && ! grep -qx '/\.lattice/runtime/' "$proj/.git/info/exclude" 2>/dev/null; then
160
- mkdir -p "$proj/.git/info"
161
- echo '/.lattice/runtime/' >> "$proj/.git/info/exclude"
162
- added_runtime_exclude=true
163
- fi
164
-
165
- # **機械配車の口はもう作らない。** 2026-08-09 のオーナー裁定(改・裁定1)で、Lattice が席へ
166
- # 仕事を配る向きは撤回された。席は自分で `todo start` してから `run intake` するので、
167
- # work-order adapter の登録も spool(orders/reports)も要らない。**setup がここで adapter を
168
- # 必須化していると、adapter binary が無い環境で新しい卓が立たなくなる**(配車をしないのに)。
169
- # 既存 project に前の卓が作った registry が残っていても触らない——他 adapter や進行中 run の
170
- # 所有物を含みうるので、setup が消す対象ではない。
171
- work_order_adapter=false
172
- work_order_spool_ref=""
173
-
174
- # Lattice 併用モードだけ、工程表の右ペインへ円卓を差す(決定53・明示的コネクタ)。
175
- # 公開URL基底は `PEERTABLE_PUBLIC_URL`(クオ環境: https://peertable.kitepon.dev)。
176
- # 未設定なら room サーバーの URL をそのまま使う——LAN URL は Lattice を外から見た時に開けないので、
177
- # 書いた URL は必ず標準エラーへ出す。
178
- external_pane=false
179
- project_json_preexisting=false
180
- public_url=""
181
- if [ "$mode" = "lattice" ]; then
182
- public_url="${PEERTABLE_PUBLIC_URL:-$url}"
183
- project_json_preexisting=$(node "$repo/skill/scripts/external-pane.mjs" "$proj" "$room" "$public_url")
184
- external_pane=true
185
- fi
186
-
187
- # phases は追加キー(既存の読み手は .get で読むので壊れない)。空配列=plan 全体
188
- phases_json="[]"
189
- if [ ${#phases[@]} -gt 0 ]; then
190
- phases_json=$(printf '"%s",' "${phases[@]}")
191
- phases_json="[${phases_json%,}]"
192
- fi
193
-
194
- # **解決した CLI の実 path を残す。** 残さないと、席も teardown も PATH の `lattice` へ逸れる。
195
- # release 前の source tree を実測する卓では、それは**pull 系 command を持たない古い install**で、
196
- # 手順どおり打っても届かない(suzune の監査で実測・room [1037])。
197
- if [ "$(node -p 'process.platform')" = win32 ]; then
198
- node "$repo/skill/scripts/platform/windows/write-setup-state.mjs" "$tdir/setup-state.json" \
199
- "$room" "$url" "$public_url" "$mode" "$plan" "$phases_json" "$added_exclude" \
200
- "$lattice_preexisting" "$runtime_preexisting" "$added_runtime_exclude" "$added_root_mcp" \
201
- "$added_mcp_exclude" "$external_pane" "$project_json_preexisting" "$work_order_adapter" \
202
- "$work_order_spool_ref" "$lattice_cli"
203
- else
204
- 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' \
205
- "$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"
206
- fi
207
-
208
- # 稼働状態ブリッジを起こす。**teardown が止めるのと対称にする**——「起こすかは卓の任意」だった間、
209
- # 手で `nohup` する経路が2つの失敗を招いた: 起こし忘れと、**トークンを持たないシェルで起こす**
210
- # (2026-08-10 実測: `export` 欠落の設定ファイルを source した shell から起こした常駐が、
211
- # 4時間 HTTP 403 を撃ち続け、参加者一覧には点が1つも出なかった=起こしていないのと見分けがつかない)。
212
- # AI は使わず席へも1バイト送らないので卓の作業を邪魔しない。**書けない時は常駐せずに死ぬ**ので、
213
- # ここで壊れた常駐が黙って残ることは無い。setup-state.json を読むので、必ずその後で起こす。
214
- "$repo/skill/scripts/ensure-project-runtime.sh" "$proj"
215
-
216
- echo "scaffold done: $tdir"
2
+ # 既存の引数を共通の製品入口へ渡す。
3
+ set -euo pipefail
4
+ exec node "$(dirname "$0")/legacy-entry.mjs" setup "$@"