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
@@ -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,9 +20,12 @@ 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'
28
+ import { isSeatLaunching } from './seat-launch-phase.mjs'
26
29
  import { bridgeRecordLive } from './bridge-record-live.mjs'
27
30
 
28
31
  const run = promisify(execFile)
@@ -366,31 +369,23 @@ function advanceLastSeq() {
366
369
  }
367
370
 
368
371
  const deferredBusy = new Set()
369
- async function passKnownCodexDialog(member) {
370
- if (memberHarness(member) !== 'codex') return false
372
+ const aiterm = new AitermClient()
373
+ async function passKnownSeatApproval(member, observation) {
371
374
  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
375
+ const session = seatSessionId(member)
376
+ return session ? passSeatApproval(aiterm, session, memberHarness(member), observation) : false
383
377
  }
384
378
 
385
379
  async function wake(seat, msgs) {
380
+ if (isSeatLaunching(proj, seat)) return 'deferred'
386
381
  const last = msgs[msgs.length - 1]
387
382
  const text = msgs.map(formatWakeNotice).join(' || ')
388
383
  // 配送直前に member ledger を取り直し、current name -> descriptor の一経路だけを使う。
389
384
  await refreshMembers()
390
385
  const member = members.get(seat)
391
386
  if (!isWakeupBridgeTarget(member, targetOpts)) return 'skipped'
392
- const observation = resolveSeatObservation(member, null)
393
- if (observation === null) {
387
+ const sessionId = seatSessionId(member)
388
+ if (!sessionId) {
394
389
  const code = members.has(seat) ? 'DESCRIPTOR_MISSING' : 'MEMBER_MISSING'
395
390
  const error = new Error(`${code}: ${seat}`)
396
391
  error.code = code
@@ -401,12 +396,16 @@ async function wake(seat, msgs) {
401
396
  // 所有し、bridge は「誰に・いつ・何を届けるか」と receipt だけを持つ(責務境界。2026-09-04
402
397
  // オーナー裁定: 0.8.51/0.8.52 で bridge に足した fg/stty は aiterm へ移し、ここからは撤去)。
403
398
  // 死んだ席(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 ?? '')
399
+ const observed = await aiterm.observe(sessionId)
400
+ if (!observed.exists || ['dead', 'missing'].includes(observed.state) || observed.harness_alive === false) {
401
+ throw Object.assign(new Error(`SEAT_TUI_GONE: ${seat}`), { code: 'SEAT_TUI_GONE' })
402
+ }
403
+ if (observed.harness_alive === null || observed.state === 'unknown') {
404
+ throw Object.assign(new Error(`SEAT_OBSERVATION_UNKNOWN: ${seat}: ${observed.reason}`), { code: 'SEAT_OBSERVATION_UNKNOWN' })
405
+ }
406
406
  const harness = memberHarness(member)
407
- const tail = screen.split('\n').slice(-14).join('\n')
408
407
  if (harness === 'grok') {
409
- if (shouldDeferGrokWake(harness, tail)) {
408
+ if (shouldDeferGrokWake(harness, observed.state)) {
410
409
  if (!deferredBusy.has(seat)) {
411
410
  log(`Grok席が実行中なのでidleまで待つ: ${seat} ← ${msgs.length} 件`)
412
411
  deferredBusy.add(seat)
@@ -415,13 +414,8 @@ async function wake(seat, msgs) {
415
414
  }
416
415
  deferredBusy.delete(seat)
417
416
  }
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')
417
+ if (await passKnownSeatApproval(member, observed)) return 'deferred'
418
+ const busy = observed.state === 'busy'
425
419
  const deliverScript = fileURLToPath(new URL('./aiterm-deliver.mjs', import.meta.url))
426
420
  const deliverFile = join(proj, '.team', `wakeup-deliver-${process.pid}.txt`)
427
421
  writeFileSync(deliverFile, text)
@@ -616,9 +610,9 @@ setInterval(() => {
616
610
  }
617
611
  }, 2000)
618
612
 
619
- // CodexのMCP/command approvalはroom発言の配達時だけでなく、席自身の最初のtool実行でも
613
+ // 席のtool approvalはroom発言の配達時だけでなく、席自身の最初のtool実行でも
620
614
  // 遅れて現れる。pending DMが無い時も既知dialogだけを処理し続け、ランプがblockedのまま
621
- // 放置される状態を作らない。未知dialogはkeysForCodexPaneがnullを返すため触らない。
615
+ // 放置される状態を作らない。未知dialogはAitermが明示的に拒否する。
622
616
  let dialogSweepRunning = false
623
617
  setInterval(async () => {
624
618
  if (dialogSweepRunning) return
@@ -626,8 +620,8 @@ setInterval(async () => {
626
620
  try {
627
621
  for (const member of members.values()) {
628
622
  if (member.status_effective === 'dead') continue
629
- try { await passKnownCodexDialog(member) } catch (error) {
630
- log(`CODEX_DIALOG_SWEEP_FAILED: ${member.name} ${error.message}`)
623
+ try { await passKnownSeatApproval(member) } catch (error) {
624
+ log(`SEAT_APPROVAL_SWEEP_FAILED: ${member.name} ${error.message}`)
631
625
  }
632
626
  }
633
627
  } finally {