peertable 0.8.55 → 0.8.56

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 (74) 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 +156 -0
  26. package/skill/scripts/launch-seat.sh +3 -797
  27. package/skill/scripts/launch-seat.test.mjs +159 -0
  28. package/skill/scripts/leave-seat.mjs +42 -0
  29. package/skill/scripts/leave-seat.sh +3 -92
  30. package/skill/scripts/leave-seat.test.mjs +53 -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-observer.mjs +54 -0
  49. package/skill/scripts/seat-observer.test.mjs +58 -0
  50. package/skill/scripts/seat-session.mjs +17 -0
  51. package/skill/scripts/seat-status-bridge.mjs +33 -206
  52. package/skill/scripts/seat-usage.mjs +3 -334
  53. package/skill/scripts/setup.sh +3 -215
  54. package/skill/scripts/teardown.mjs +113 -0
  55. package/skill/scripts/teardown.sh +3 -410
  56. package/skill/scripts/teardown.test.mjs +62 -0
  57. package/skill/scripts/upgrade-team-assets.mjs +268 -0
  58. package/skill/scripts/upgrade-team-assets.sh +2 -282
  59. package/skill/scripts/wakeup-bridge.mjs +24 -32
  60. package/skill/scripts/wakeup-delivery.mjs +4 -14
  61. package/skill/templates/charter.md +1 -1
  62. package/skill/templates/member-standalone.md +1 -1
  63. package/skill/templates/member.md +3 -3
  64. package/skill/scripts/agent-pane-status.mjs +0 -6
  65. package/skill/scripts/aiterm-configure.mjs +0 -31
  66. package/skill/scripts/aiterm-launch.mjs +0 -64
  67. package/skill/scripts/aiterm-send.mjs +0 -22
  68. package/skill/scripts/claude-dialog.mjs +0 -26
  69. package/skill/scripts/codex-dialog.mjs +0 -67
  70. package/skill/scripts/platform/windows/build-bridge-command.mjs +0 -52
  71. package/skill/scripts/seat-input.mjs +0 -36
  72. package/skill/scripts/tmux-at.bash +0 -22
  73. package/skill/scripts/tmux-socket.mjs +0 -19
  74. package/skill/scripts/vendors/grok/pane-status.mjs +0 -30
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ // 既存卓のPeertable所有assetを現行packageへ同期する。
3
+ import { chmodSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
4
+ import { createHash, randomBytes } from 'node:crypto'
5
+ import { basename, dirname, join, resolve } from 'node:path'
6
+ import { pathToFileURL, fileURLToPath } from 'node:url'
7
+
8
+ const projectArg = process.argv[2]
9
+ const repoDir = fileURLToPath(new URL('../../', import.meta.url))
10
+ const { expectedRoomMcp, isExpectedRoomMcp } = await import(pathToFileURL(
11
+ join(repoDir, 'skill', 'scripts', 'room-mcp-config.mjs')))
12
+
13
+ function fail(code, detail) {
14
+ console.error(JSON.stringify({
15
+ schema: 'peertable.generated_assets_upgrade_result.v2',
16
+ result: 'rejected',
17
+ code,
18
+ detail,
19
+ }))
20
+ process.exit(1)
21
+ }
22
+
23
+ function lstatOrNull(path) {
24
+ try {
25
+ return lstatSync(path)
26
+ } catch (error) {
27
+ if (error?.code === 'ENOENT') return null
28
+ fail('PEERTABLE_UPGRADE_PATH_UNREADABLE', `${path}: ${error.message}`)
29
+ }
30
+ }
31
+
32
+ function requireRegularFile(path, code, label) {
33
+ const stat = lstatOrNull(path)
34
+ if (!stat || stat.isSymbolicLink() || !stat.isFile()) {
35
+ fail(code, `${label}: ${path}`)
36
+ }
37
+ return stat
38
+ }
39
+
40
+ function validateDirectory(path, required, label) {
41
+ const stat = lstatOrNull(path)
42
+ if (!stat) {
43
+ if (required) fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `${label} が無い: ${path}`)
44
+ return false
45
+ }
46
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
47
+ fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `${label} がsymlinkまたはdirectoryでない: ${path}`)
48
+ }
49
+ return true
50
+ }
51
+
52
+ function validateTargetPath(project, relativePath) {
53
+ const parts = relativePath.split('/')
54
+ let current = project
55
+ for (const part of parts.slice(0, -1)) {
56
+ current = join(current, part)
57
+ validateDirectory(current, part === '.team', `管理asset親 ${part}`)
58
+ }
59
+ const target = join(project, relativePath)
60
+ const stat = lstatOrNull(target)
61
+ if (stat && (stat.isSymbolicLink() || !stat.isFile())) {
62
+ fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `管理assetがsymlinkまたはregular fileでない: ${target}`)
63
+ }
64
+ return { target, stat }
65
+ }
66
+
67
+ function templateText(repo, relativePath) {
68
+ const path = join(repo, relativePath)
69
+ requireRegularFile(path, 'PEERTABLE_GENERATED_ASSET_TEMPLATE_INVALID', 'template')
70
+ try {
71
+ return readFileSync(path)
72
+ } catch (error) {
73
+ fail('PEERTABLE_GENERATED_ASSET_TEMPLATE_INVALID', `${path}: ${error.message}`)
74
+ }
75
+ }
76
+
77
+ function sha256(content) {
78
+ return createHash('sha256').update(content).digest('hex')
79
+ }
80
+
81
+ function renderMember(template, state) {
82
+ if (state.mode === 'standalone') return template
83
+ const phases = state.phases
84
+ const scope = phases.length === 0
85
+ ? 'この卓の claim 範囲は plan 全体(phase 指定なしで立っている)。'
86
+ : `**この卓の claim 範囲は phase ${phases.join(' ')} の task だけ**。範囲外の phase の task は、ready に見えていても取らない——同じ plan へ別 campaign が相乗りしている時、範囲外を取ると他卓の工程を横取りする(越境が2回実測されたことへの対処)。範囲外に手を入れる必要が出たら room へ出して裁定を仰ぐ。`
87
+ return template
88
+ .toString('utf8')
89
+ .replaceAll('{{PLAN_KEY}}', state.plan_key)
90
+ .replaceAll('{{CLAIM_SCOPE}}', scope)
91
+ }
92
+
93
+ function loadState(project) {
94
+ const path = join(project, '.team', 'setup-state.json')
95
+ requireRegularFile(path, 'PEERTABLE_SETUP_STATE_INVALID', 'setup-state.json')
96
+ let state
97
+ try {
98
+ state = JSON.parse(readFileSync(path, 'utf8'))
99
+ } catch (error) {
100
+ fail('PEERTABLE_SETUP_STATE_INVALID', `${path}: JSONを読めない: ${error.message}`)
101
+ }
102
+ if (!state || typeof state !== 'object' || Array.isArray(state)) {
103
+ fail('PEERTABLE_SETUP_STATE_INVALID', 'setup-state.json はobjectでなければならない')
104
+ }
105
+ if (!['lattice', 'standalone'].includes(state.mode)) {
106
+ fail('PEERTABLE_SETUP_STATE_INVALID', `modeが不正: ${String(state.mode)}`)
107
+ }
108
+ if (typeof state.room !== 'string' || !state.room || typeof state.server_url !== 'string' || !state.server_url) {
109
+ fail('PEERTABLE_SETUP_STATE_INVALID', 'room/server_url が不正')
110
+ }
111
+ if (typeof state.plan_key !== 'string') {
112
+ fail('PEERTABLE_SETUP_STATE_INVALID', 'plan_key がstringでない')
113
+ }
114
+ if (state.mode === 'lattice' && !state.plan_key) {
115
+ fail('PEERTABLE_SETUP_STATE_INVALID', 'lattice mode の plan_key が空')
116
+ }
117
+ if (state.phases !== undefined && (!Array.isArray(state.phases) || state.phases.some(phase => typeof phase !== 'string' || !phase))) {
118
+ fail('PEERTABLE_SETUP_STATE_INVALID', 'phases がstring配列でない')
119
+ }
120
+ for (const key of ['added_root_mcp', 'root_mcp_json_fallback']) {
121
+ if (state[key] !== undefined && typeof state[key] !== 'boolean') {
122
+ fail('PEERTABLE_SETUP_STATE_INVALID', `${key} がbooleanでない`)
123
+ }
124
+ }
125
+ return { ...state, phases: state.phases ?? [],
126
+ root_mcp_managed: state.room_mcp_managed ?? state.added_root_mcp ?? state.root_mcp_json_fallback ?? false }
127
+ }
128
+
129
+ function prepareRoomMcp(project, repo, state) {
130
+ const relativePath = '.mcp.json'
131
+ const { target, stat } = validateTargetPath(project, relativePath)
132
+ if (!state.root_mcp_managed && !stat) {
133
+ fail('PEERTABLE_PREEXISTING_MCP_STALE', `${target}: 既存room MCPが無い。手動mergeが必要`)
134
+ }
135
+ let config = { mcpServers: {} }
136
+ if (stat) {
137
+ try {
138
+ config = JSON.parse(readFileSync(target, 'utf8'))
139
+ } catch (error) {
140
+ fail(state.root_mcp_managed ? 'PEERTABLE_MANAGED_MCP_INVALID' : 'PEERTABLE_PREEXISTING_MCP_STALE',
141
+ `${target}: JSONを読めない: ${error.message}`)
142
+ }
143
+ }
144
+ if (!config || typeof config !== 'object' || Array.isArray(config)
145
+ || (config.mcpServers !== undefined
146
+ && (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)))) {
147
+ fail(state.root_mcp_managed ? 'PEERTABLE_MANAGED_MCP_INVALID' : 'PEERTABLE_PREEXISTING_MCP_STALE',
148
+ `${target}: root/mcpServersがobjectでない`)
149
+ }
150
+ const expected = expectedRoomMcp(repo)
151
+ if (!state.root_mcp_managed) {
152
+ if (!isExpectedRoomMcp(config.mcpServers?.room, expected)) {
153
+ fail('PEERTABLE_PREEXISTING_MCP_STALE', `${target}: room blockを現行treeへ手動mergeする必要がある`)
154
+ }
155
+ return { item: null,
156
+ report: { path: relativePath, ownership: 'preexisting', action: 'unchanged-preexisting' } }
157
+ }
158
+ config.mcpServers ??= {}
159
+ config.mcpServers.room = expected
160
+ const item = { relativePath, sourcePath: 'room/client.mjs', target, stat,
161
+ content: Buffer.from(`${JSON.stringify(config, null, 2)}\n`, 'utf8'), mode: stat ? stat.mode & 0o777 : 0o644 }
162
+ return { item, report: { path: relativePath, ownership: 'managed', action: null } }
163
+ }
164
+
165
+ function writeAtomically(target, content, mode) {
166
+ const temporary = join(dirname(target), `.${basename(target)}.upgrade-${process.pid}-${randomBytes(6).toString('hex')}.tmp`)
167
+ try {
168
+ writeFileSync(temporary, content, { mode })
169
+ chmodSync(temporary, mode)
170
+ renameSync(temporary, target)
171
+ } catch (error) {
172
+ try { lstatSync(temporary); unlinkSync(temporary) } catch {}
173
+ fail('PEERTABLE_GENERATED_ASSET_WRITE_FAILED', `${target}: ${error.message}`)
174
+ }
175
+ }
176
+
177
+ function main() {
178
+ if (!projectArg || !repoDir) fail('PEERTABLE_UPGRADE_USAGE', 'project_dir が無い')
179
+ const projectStat = lstatOrNull(projectArg)
180
+ if (!projectStat || projectStat.isSymbolicLink() || !projectStat.isDirectory()) {
181
+ fail('PEERTABLE_UPGRADE_PROJECT_INVALID', `project directoryが不正: ${projectArg}`)
182
+ }
183
+ const project = resolve(projectArg)
184
+ const repo = resolve(repoDir)
185
+ validateDirectory(join(project, '.team'), true, '.team')
186
+ validateDirectory(join(repo, 'skill'), true, 'Peertable repo')
187
+ const state = loadState(project)
188
+
189
+ const common = [
190
+ ['.team/CLAUDE.md', 'skill/templates/charter.md', 0o644],
191
+ ['.team/roles/parent.md', 'skill/templates/parent.md', 0o644],
192
+ ['.team/scripts/alarm-set.sh', 'skill/scripts/alarm-set.sh', 0o755],
193
+ ['.team/scripts/alarm-write.mjs', 'skill/scripts/alarm-write.mjs', 0o755],
194
+ ['.team/scripts/alarm-condition.mjs', 'skill/scripts/alarm-condition.mjs', 0o755],
195
+ ['.team/scripts/pull-attach-input.mjs', 'skill/scripts/pull-attach-input.mjs', 0o755],
196
+ ]
197
+ const modeSpecific = state.mode === 'lattice'
198
+ ? [
199
+ ['.team/roles/member.md', 'skill/templates/member.md', 0o644],
200
+ ['.team/scripts/done.sh', 'skill/templates/done.sh', 0o755],
201
+ ['.team/scripts/independence-refresh.sh', 'skill/templates/independence-refresh.sh', 0o755],
202
+ ]
203
+ : [['.team/roles/member.md', 'skill/templates/member-standalone.md', 0o644]]
204
+ const definitions = [...common, ...modeSpecific]
205
+
206
+ const prepared = definitions.map(([relativePath, sourcePath, mode]) => {
207
+ const { target, stat } = validateTargetPath(project, relativePath)
208
+ const source = templateText(repo, sourcePath)
209
+ const content = relativePath === '.team/roles/member.md' && state.mode === 'lattice'
210
+ ? Buffer.from(renderMember(source, state), 'utf8')
211
+ : source
212
+ return { relativePath, sourcePath, target, stat, content, mode }
213
+ })
214
+ const roomMcp = prepareRoomMcp(project, repo, state)
215
+ if (roomMcp.item) prepared.push(roomMcp.item)
216
+
217
+ const changes = prepared.map(item => {
218
+ if (!item.stat) return { path: item.relativePath, action: 'created', sha256: sha256(item.content) }
219
+ const current = readFileSync(item.target)
220
+ const mode = item.stat.mode & 0o777
221
+ const modeMatches = process.platform === 'win32' || mode === item.mode
222
+ if (current.equals(item.content) && modeMatches) {
223
+ return { path: item.relativePath, action: 'unchanged', sha256: sha256(item.content) }
224
+ }
225
+ return { path: item.relativePath, action: current.equals(item.content) ? 'mode-updated' : 'updated', sha256: sha256(item.content) }
226
+ })
227
+ const obsolete = ['.team/scripts/start.sh', '.team/scripts/start-event.mjs']
228
+ .map(relativePath => ({ relativePath, ...validateTargetPath(project, relativePath) }))
229
+
230
+ // 全対象の安全性・template・差分を先に確定してから、管理allowlistだけへ書く。
231
+ for (const [index, item] of prepared.entries()) {
232
+ const change = changes[index]
233
+ if (change.action === 'unchanged') continue
234
+ const parent = dirname(item.target)
235
+ mkdirSync(parent, { recursive: true, mode: 0o755 })
236
+ if (change.action === 'mode-updated') chmodSync(item.target, item.mode)
237
+ else writeAtomically(item.target, item.content, item.mode)
238
+ }
239
+
240
+ const removed = []
241
+ for (const { relativePath, target, stat } of obsolete) {
242
+ if (!stat) continue
243
+ unlinkSync(target)
244
+ removed.push(relativePath)
245
+ }
246
+
247
+ console.log(JSON.stringify({
248
+ schema: 'peertable.generated_assets_upgrade_result.v2',
249
+ result: 'ok',
250
+ project,
251
+ mode: state.mode,
252
+ managed: prepared.map(item => ({ path: item.relativePath, source: item.sourcePath, sha256: sha256(item.content) })),
253
+ changes,
254
+ room_mcp: roomMcp.item
255
+ ? { ...roomMcp.report,
256
+ action: changes.find(change => change.path === roomMcp.item.relativePath).action }
257
+ : roomMcp.report,
258
+ removed,
259
+ changed_count: changes.filter(change => change.action !== 'unchanged').length + removed.length,
260
+ }))
261
+ }
262
+
263
+ try {
264
+ main()
265
+ } catch (error) {
266
+ if (error?.code && typeof error.message === 'string') fail('PEERTABLE_GENERATED_ASSET_UPGRADE_FAILED', error.message)
267
+ fail('PEERTABLE_GENERATED_ASSET_UPGRADE_FAILED', String(error))
268
+ }
@@ -1,283 +1,3 @@
1
1
  #!/bin/bash
2
- # 既存卓へ、Peertable が所有する generated assetとroot room MCPだけを現行treeへ同期する。
3
- # usage: upgrade-team-assets.sh <project_dir>
4
- set -euo pipefail
5
-
6
- if [ "$#" -ne 1 ]; then
7
- echo 'PEERTABLE_UPGRADE_USAGE: upgrade-team-assets.sh <project_dir>' >&2
8
- exit 2
9
- fi
10
-
11
- # skill dir は ~/.claude/skills/peertable のような symlink 経由で呼ばれるので、$0 を実体へ解決してから repo を求める
12
- # (解決しないと ../.. が ~/.claude/skills になり room-mcp-config.mjs を見失う。2026-09-04 実測)
13
- script_dir=$(CDPATH= cd -- "$(dirname -- "$(readlink -f -- "$0")")" && pwd)
14
- repo_dir=$(CDPATH= cd -- "$script_dir/../.." && pwd)
15
-
16
- node --input-type=module - "$1" "$repo_dir" <<'NODE'
17
- import { chmodSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
18
- import { createHash, randomBytes } from 'node:crypto'
19
- import { basename, dirname, join, resolve } from 'node:path'
20
- import { pathToFileURL } from 'node:url'
21
-
22
- const projectArg = process.argv[2]
23
- const repoDir = process.argv[3]
24
- const { expectedRoomMcp, isExpectedRoomMcp } = await import(pathToFileURL(
25
- join(repoDir, 'skill', 'scripts', 'room-mcp-config.mjs')))
26
-
27
- function fail(code, detail) {
28
- console.error(JSON.stringify({
29
- schema: 'peertable.generated_assets_upgrade_result.v2',
30
- result: 'rejected',
31
- code,
32
- detail,
33
- }))
34
- process.exit(1)
35
- }
36
-
37
- function lstatOrNull(path) {
38
- try {
39
- return lstatSync(path)
40
- } catch (error) {
41
- if (error?.code === 'ENOENT') return null
42
- fail('PEERTABLE_UPGRADE_PATH_UNREADABLE', `${path}: ${error.message}`)
43
- }
44
- }
45
-
46
- function requireRegularFile(path, code, label) {
47
- const stat = lstatOrNull(path)
48
- if (!stat || stat.isSymbolicLink() || !stat.isFile()) {
49
- fail(code, `${label}: ${path}`)
50
- }
51
- return stat
52
- }
53
-
54
- function validateDirectory(path, required, label) {
55
- const stat = lstatOrNull(path)
56
- if (!stat) {
57
- if (required) fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `${label} が無い: ${path}`)
58
- return false
59
- }
60
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
61
- fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `${label} がsymlinkまたはdirectoryでない: ${path}`)
62
- }
63
- return true
64
- }
65
-
66
- function validateTargetPath(project, relativePath) {
67
- const parts = relativePath.split('/')
68
- let current = project
69
- for (const part of parts.slice(0, -1)) {
70
- current = join(current, part)
71
- validateDirectory(current, part === '.team', `管理asset親 ${part}`)
72
- }
73
- const target = join(project, relativePath)
74
- const stat = lstatOrNull(target)
75
- if (stat && (stat.isSymbolicLink() || !stat.isFile())) {
76
- fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `管理assetがsymlinkまたはregular fileでない: ${target}`)
77
- }
78
- return { target, stat }
79
- }
80
-
81
- function templateText(repo, relativePath) {
82
- const path = join(repo, relativePath)
83
- requireRegularFile(path, 'PEERTABLE_GENERATED_ASSET_TEMPLATE_INVALID', 'template')
84
- try {
85
- return readFileSync(path)
86
- } catch (error) {
87
- fail('PEERTABLE_GENERATED_ASSET_TEMPLATE_INVALID', `${path}: ${error.message}`)
88
- }
89
- }
90
-
91
- function sha256(content) {
92
- return createHash('sha256').update(content).digest('hex')
93
- }
94
-
95
- function renderMember(template, state) {
96
- if (state.mode === 'standalone') return template
97
- const phases = state.phases
98
- const scope = phases.length === 0
99
- ? 'この卓の claim 範囲は plan 全体(phase 指定なしで立っている)。'
100
- : `**この卓の claim 範囲は phase ${phases.join(' ')} の task だけ**。範囲外の phase の task は、ready に見えていても取らない——同じ plan へ別 campaign が相乗りしている時、範囲外を取ると他卓の工程を横取りする(越境が2回実測されたことへの対処)。範囲外に手を入れる必要が出たら room へ出して裁定を仰ぐ。`
101
- return template
102
- .toString('utf8')
103
- .replaceAll('{{PLAN_KEY}}', state.plan_key)
104
- .replaceAll('{{CLAIM_SCOPE}}', scope)
105
- }
106
-
107
- function loadState(project) {
108
- const path = join(project, '.team', 'setup-state.json')
109
- requireRegularFile(path, 'PEERTABLE_SETUP_STATE_INVALID', 'setup-state.json')
110
- let state
111
- try {
112
- state = JSON.parse(readFileSync(path, 'utf8'))
113
- } catch (error) {
114
- fail('PEERTABLE_SETUP_STATE_INVALID', `${path}: JSONを読めない: ${error.message}`)
115
- }
116
- if (!state || typeof state !== 'object' || Array.isArray(state)) {
117
- fail('PEERTABLE_SETUP_STATE_INVALID', 'setup-state.json はobjectでなければならない')
118
- }
119
- if (!['lattice', 'standalone'].includes(state.mode)) {
120
- fail('PEERTABLE_SETUP_STATE_INVALID', `modeが不正: ${String(state.mode)}`)
121
- }
122
- if (typeof state.room !== 'string' || !state.room || typeof state.server_url !== 'string' || !state.server_url) {
123
- fail('PEERTABLE_SETUP_STATE_INVALID', 'room/server_url が不正')
124
- }
125
- if (typeof state.plan_key !== 'string') {
126
- fail('PEERTABLE_SETUP_STATE_INVALID', 'plan_key がstringでない')
127
- }
128
- if (state.mode === 'lattice' && !state.plan_key) {
129
- fail('PEERTABLE_SETUP_STATE_INVALID', 'lattice mode の plan_key が空')
130
- }
131
- if (state.phases !== undefined && (!Array.isArray(state.phases) || state.phases.some(phase => typeof phase !== 'string' || !phase))) {
132
- fail('PEERTABLE_SETUP_STATE_INVALID', 'phases がstring配列でない')
133
- }
134
- for (const key of ['added_root_mcp', 'root_mcp_json_fallback']) {
135
- if (state[key] !== undefined && typeof state[key] !== 'boolean') {
136
- fail('PEERTABLE_SETUP_STATE_INVALID', `${key} がbooleanでない`)
137
- }
138
- }
139
- return { ...state, phases: state.phases ?? [],
140
- root_mcp_managed: state.added_root_mcp ?? state.root_mcp_json_fallback ?? false }
141
- }
142
-
143
- function prepareRoomMcp(project, repo, state) {
144
- const relativePath = '.mcp.json'
145
- const { target, stat } = validateTargetPath(project, relativePath)
146
- if (!state.root_mcp_managed && !stat) {
147
- fail('PEERTABLE_PREEXISTING_MCP_STALE', `${target}: 既存room MCPが無い。手動mergeが必要`)
148
- }
149
- let config = { mcpServers: {} }
150
- if (stat) {
151
- try {
152
- config = JSON.parse(readFileSync(target, 'utf8'))
153
- } catch (error) {
154
- fail(state.root_mcp_managed ? 'PEERTABLE_MANAGED_MCP_INVALID' : 'PEERTABLE_PREEXISTING_MCP_STALE',
155
- `${target}: JSONを読めない: ${error.message}`)
156
- }
157
- }
158
- if (!config || typeof config !== 'object' || Array.isArray(config)
159
- || (config.mcpServers !== undefined
160
- && (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)))) {
161
- fail(state.root_mcp_managed ? 'PEERTABLE_MANAGED_MCP_INVALID' : 'PEERTABLE_PREEXISTING_MCP_STALE',
162
- `${target}: root/mcpServersがobjectでない`)
163
- }
164
- const expected = expectedRoomMcp(repo)
165
- if (!state.root_mcp_managed) {
166
- if (!isExpectedRoomMcp(config.mcpServers?.room, expected)) {
167
- fail('PEERTABLE_PREEXISTING_MCP_STALE', `${target}: room blockを現行treeへ手動mergeする必要がある`)
168
- }
169
- return { item: null,
170
- report: { path: relativePath, ownership: 'preexisting', action: 'unchanged-preexisting' } }
171
- }
172
- config.mcpServers ??= {}
173
- config.mcpServers.room = expected
174
- const item = { relativePath, sourcePath: 'room/client.mjs', target, stat,
175
- content: Buffer.from(`${JSON.stringify(config, null, 2)}\n`, 'utf8'), mode: stat ? stat.mode & 0o777 : 0o644 }
176
- return { item, report: { path: relativePath, ownership: 'managed', action: null } }
177
- }
178
-
179
- function writeAtomically(target, content, mode) {
180
- const temporary = join(dirname(target), `.${basename(target)}.upgrade-${process.pid}-${randomBytes(6).toString('hex')}.tmp`)
181
- try {
182
- writeFileSync(temporary, content, { mode })
183
- chmodSync(temporary, mode)
184
- renameSync(temporary, target)
185
- } catch (error) {
186
- try { lstatSync(temporary); unlinkSync(temporary) } catch {}
187
- fail('PEERTABLE_GENERATED_ASSET_WRITE_FAILED', `${target}: ${error.message}`)
188
- }
189
- }
190
-
191
- function main() {
192
- if (!projectArg || !repoDir) fail('PEERTABLE_UPGRADE_USAGE', 'project_dir が無い')
193
- const projectStat = lstatOrNull(projectArg)
194
- if (!projectStat || projectStat.isSymbolicLink() || !projectStat.isDirectory()) {
195
- fail('PEERTABLE_UPGRADE_PROJECT_INVALID', `project directoryが不正: ${projectArg}`)
196
- }
197
- const project = resolve(projectArg)
198
- const repo = resolve(repoDir)
199
- validateDirectory(join(project, '.team'), true, '.team')
200
- validateDirectory(join(repo, 'skill'), true, 'Peertable repo')
201
- const state = loadState(project)
202
-
203
- const common = [
204
- ['.team/CLAUDE.md', 'skill/templates/charter.md', 0o644],
205
- ['.team/roles/parent.md', 'skill/templates/parent.md', 0o644],
206
- ['.team/scripts/alarm-set.sh', 'skill/scripts/alarm-set.sh', 0o755],
207
- ['.team/scripts/alarm-write.mjs', 'skill/scripts/alarm-write.mjs', 0o755],
208
- ['.team/scripts/alarm-condition.mjs', 'skill/scripts/alarm-condition.mjs', 0o755],
209
- ['.team/scripts/pull-attach-input.mjs', 'skill/scripts/pull-attach-input.mjs', 0o755],
210
- ]
211
- const modeSpecific = state.mode === 'lattice'
212
- ? [
213
- ['.team/roles/member.md', 'skill/templates/member.md', 0o644],
214
- ['.team/scripts/done.sh', 'skill/templates/done.sh', 0o755],
215
- ['.team/scripts/independence-refresh.sh', 'skill/templates/independence-refresh.sh', 0o755],
216
- ]
217
- : [['.team/roles/member.md', 'skill/templates/member-standalone.md', 0o644]]
218
- const definitions = [...common, ...modeSpecific]
219
-
220
- const prepared = definitions.map(([relativePath, sourcePath, mode]) => {
221
- const { target, stat } = validateTargetPath(project, relativePath)
222
- const source = templateText(repo, sourcePath)
223
- const content = relativePath === '.team/roles/member.md' && state.mode === 'lattice'
224
- ? Buffer.from(renderMember(source, state), 'utf8')
225
- : source
226
- return { relativePath, sourcePath, target, stat, content, mode }
227
- })
228
- const roomMcp = prepareRoomMcp(project, repo, state)
229
- if (roomMcp.item) prepared.push(roomMcp.item)
230
-
231
- const changes = prepared.map(item => {
232
- if (!item.stat) return { path: item.relativePath, action: 'created', sha256: sha256(item.content) }
233
- const current = readFileSync(item.target)
234
- const mode = item.stat.mode & 0o777
235
- const modeMatches = process.platform === 'win32' || mode === item.mode
236
- if (current.equals(item.content) && modeMatches) {
237
- return { path: item.relativePath, action: 'unchanged', sha256: sha256(item.content) }
238
- }
239
- return { path: item.relativePath, action: current.equals(item.content) ? 'mode-updated' : 'updated', sha256: sha256(item.content) }
240
- })
241
- const obsolete = ['.team/scripts/start.sh', '.team/scripts/start-event.mjs']
242
- .map(relativePath => ({ relativePath, ...validateTargetPath(project, relativePath) }))
243
-
244
- // 全対象の安全性・template・差分を先に確定してから、管理allowlistだけへ書く。
245
- for (const [index, item] of prepared.entries()) {
246
- const change = changes[index]
247
- if (change.action === 'unchanged') continue
248
- const parent = dirname(item.target)
249
- mkdirSync(parent, { recursive: true, mode: 0o755 })
250
- if (change.action === 'mode-updated') chmodSync(item.target, item.mode)
251
- else writeAtomically(item.target, item.content, item.mode)
252
- }
253
-
254
- const removed = []
255
- for (const { relativePath, target, stat } of obsolete) {
256
- if (!stat) continue
257
- unlinkSync(target)
258
- removed.push(relativePath)
259
- }
260
-
261
- console.log(JSON.stringify({
262
- schema: 'peertable.generated_assets_upgrade_result.v2',
263
- result: 'ok',
264
- project,
265
- mode: state.mode,
266
- managed: prepared.map(item => ({ path: item.relativePath, source: item.sourcePath, sha256: sha256(item.content) })),
267
- changes,
268
- room_mcp: roomMcp.item
269
- ? { ...roomMcp.report,
270
- action: changes.find(change => change.path === roomMcp.item.relativePath).action }
271
- : roomMcp.report,
272
- removed,
273
- changed_count: changes.filter(change => change.action !== 'unchanged').length + removed.length,
274
- }))
275
- }
276
-
277
- try {
278
- main()
279
- } catch (error) {
280
- if (error?.code && typeof error.message === 'string') fail('PEERTABLE_GENERATED_ASSET_UPGRADE_FAILED', error.message)
281
- fail('PEERTABLE_GENERATED_ASSET_UPGRADE_FAILED', String(error))
282
- }
283
- NODE
2
+ # 旧入口互換。処理の正本はOS共通のNode実装。
3
+ exec node "$(dirname "$0")/upgrade-team-assets.mjs" "$@"
@@ -20,8 +20,10 @@ import {
20
20
  import { join } from 'node:path'
21
21
  import { promisify } from 'node:util'
22
22
  import { fileURLToPath } from 'node:url'
23
- import { classifyPaneTail, resolvePostToken, resolveSeatObservation, tmuxArgv } from './seat-usage.mjs'
24
- import { keysForCodexPane } from './codex-dialog.mjs'
23
+ import { resolvePostToken } from './seat-usage.mjs'
24
+ import { AitermClient } from './aiterm-client.mjs'
25
+ import { seatSessionId } from './seat-session.mjs'
26
+ import { passSeatApproval } from './seat-approval.mjs'
25
27
  import { BROADCAST_RECIPIENT, formatWakeNotice, isIdleSelfWake, isWakeupBridgeTarget, memberHarness, shouldDeferGrokWake } from './wakeup-delivery.mjs'
26
28
  import { bridgeRecordLive } from './bridge-record-live.mjs'
27
29
 
@@ -366,20 +368,11 @@ function advanceLastSeq() {
366
368
  }
367
369
 
368
370
  const deferredBusy = new Set()
369
- async function passKnownCodexDialog(member) {
370
- if (memberHarness(member) !== 'codex') return false
371
+ const aiterm = new AitermClient()
372
+ async function passKnownSeatApproval(member, observation) {
371
373
  if (!isWakeupBridgeTarget(member, targetOpts)) return false
372
- const observation = resolveSeatObservation(member, null)
373
- if (observation === null) return false
374
- const pane = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
375
- const dialog = keysForCodexPane(String(pane.stdout ?? ''))
376
- if (!dialog) return false
377
- for (const key of dialog.keys) {
378
- await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, key], { socket: observation.socket }))
379
- await sleep(150)
380
- }
381
- log(`Codex ${dialog.kind} を通した: ${member.name}`)
382
- return true
374
+ const session = seatSessionId(member)
375
+ return session ? passSeatApproval(aiterm, session, memberHarness(member), observation) : false
383
376
  }
384
377
 
385
378
  async function wake(seat, msgs) {
@@ -389,8 +382,8 @@ async function wake(seat, msgs) {
389
382
  await refreshMembers()
390
383
  const member = members.get(seat)
391
384
  if (!isWakeupBridgeTarget(member, targetOpts)) return 'skipped'
392
- const observation = resolveSeatObservation(member, null)
393
- if (observation === null) {
385
+ const sessionId = seatSessionId(member)
386
+ if (!sessionId) {
394
387
  const code = members.has(seat) ? 'DESCRIPTOR_MISSING' : 'MEMBER_MISSING'
395
388
  const error = new Error(`${code}: ${seat}`)
396
389
  error.code = code
@@ -401,12 +394,16 @@ async function wake(seat, msgs) {
401
394
  // 所有し、bridge は「誰に・いつ・何を届けるか」と receipt だけを持つ(責務境界。2026-09-04
402
395
  // オーナー裁定: 0.8.51/0.8.52 で bridge に足した fg/stty は aiterm へ移し、ここからは撤去)。
403
396
  // 死んだ席(agent 不在の素の shell)へは aiterm の ready gate が入力受付不能として送らない。
404
- const pane = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
405
- const screen = String(pane.stdout ?? '')
397
+ const observed = await aiterm.observe(sessionId)
398
+ if (!observed.exists || ['dead', 'missing'].includes(observed.state) || observed.harness_alive === false) {
399
+ throw Object.assign(new Error(`SEAT_TUI_GONE: ${seat}`), { code: 'SEAT_TUI_GONE' })
400
+ }
401
+ if (observed.harness_alive === null || observed.state === 'unknown') {
402
+ throw Object.assign(new Error(`SEAT_OBSERVATION_UNKNOWN: ${seat}: ${observed.reason}`), { code: 'SEAT_OBSERVATION_UNKNOWN' })
403
+ }
406
404
  const harness = memberHarness(member)
407
- const tail = screen.split('\n').slice(-14).join('\n')
408
405
  if (harness === 'grok') {
409
- if (shouldDeferGrokWake(harness, tail)) {
406
+ if (shouldDeferGrokWake(harness, observed.state)) {
410
407
  if (!deferredBusy.has(seat)) {
411
408
  log(`Grok席が実行中なのでidleまで待つ: ${seat} ← ${msgs.length} 件`)
412
409
  deferredBusy.add(seat)
@@ -415,13 +412,8 @@ async function wake(seat, msgs) {
415
412
  }
416
413
  deferredBusy.delete(seat)
417
414
  }
418
- if (await passKnownCodexDialog(member)) return 'deferred'
419
- const sessionId = typeof member.aiterm_session_id === 'string' && member.aiterm_session_id
420
- ? member.aiterm_session_id
421
- : observation.target
422
- // Codex は実行中ターンへ steer(0.153 は「次の tool call 後に送る」キューへ積む)、idle なら dispatch。
423
- // 実行中の判定は画面の実行中マーカーだけ(読むだけで、状態を変えない)。
424
- const busy = classifyPaneTail(tail) === 'busy' || tail.includes('esc to interrupt')
415
+ if (await passKnownSeatApproval(member, observed)) return 'deferred'
416
+ const busy = observed.state === 'busy'
425
417
  const deliverScript = fileURLToPath(new URL('./aiterm-deliver.mjs', import.meta.url))
426
418
  const deliverFile = join(proj, '.team', `wakeup-deliver-${process.pid}.txt`)
427
419
  writeFileSync(deliverFile, text)
@@ -616,9 +608,9 @@ setInterval(() => {
616
608
  }
617
609
  }, 2000)
618
610
 
619
- // CodexのMCP/command approvalはroom発言の配達時だけでなく、席自身の最初のtool実行でも
611
+ // 席のtool approvalはroom発言の配達時だけでなく、席自身の最初のtool実行でも
620
612
  // 遅れて現れる。pending DMが無い時も既知dialogだけを処理し続け、ランプがblockedのまま
621
- // 放置される状態を作らない。未知dialogはkeysForCodexPaneがnullを返すため触らない。
613
+ // 放置される状態を作らない。未知dialogはAitermが明示的に拒否する。
622
614
  let dialogSweepRunning = false
623
615
  setInterval(async () => {
624
616
  if (dialogSweepRunning) return
@@ -626,8 +618,8 @@ setInterval(async () => {
626
618
  try {
627
619
  for (const member of members.values()) {
628
620
  if (member.status_effective === 'dead') continue
629
- try { await passKnownCodexDialog(member) } catch (error) {
630
- log(`CODEX_DIALOG_SWEEP_FAILED: ${member.name} ${error.message}`)
621
+ try { await passKnownSeatApproval(member) } catch (error) {
622
+ log(`SEAT_APPROVAL_SWEEP_FAILED: ${member.name} ${error.message}`)
631
623
  }
632
624
  }
633
625
  } finally {