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.
- package/README.ja.md +32 -33
- package/README.md +36 -37
- package/package.json +3 -1
- package/room/client.mjs +24 -33
- package/room/server.mjs +1 -1
- package/skill/SKILL.md +69 -94
- package/skill/scripts/aiterm-client.mjs +48 -0
- package/skill/scripts/aiterm-deliver.mjs +3 -1
- package/skill/scripts/change-seat.mjs +86 -0
- package/skill/scripts/change-seat.sh +3 -276
- package/skill/scripts/change-seat.test.mjs +129 -0
- package/skill/scripts/cli.mjs +77 -0
- package/skill/scripts/doctor.mjs +53 -0
- package/skill/scripts/doctor.sh +2 -213
- package/skill/scripts/ensure-all-bridges.mjs +42 -0
- package/skill/scripts/ensure-all-bridges.sh +3 -80
- package/skill/scripts/ensure-bridge.mjs +11 -0
- package/skill/scripts/ensure-bridge.sh +2 -95
- package/skill/scripts/ensure-codex-room-mcp.mjs +1 -1
- package/skill/scripts/ensure-project-runtime.mjs +85 -0
- package/skill/scripts/ensure-project-runtime.sh +2 -11
- package/skill/scripts/ensure-project-runtime.test.mjs +40 -0
- package/skill/scripts/install-skill.mjs +89 -0
- package/skill/scripts/install-skill.test.mjs +84 -0
- package/skill/scripts/launch-seat.mjs +163 -0
- package/skill/scripts/launch-seat.sh +3 -797
- package/skill/scripts/launch-seat.test.mjs +174 -0
- package/skill/scripts/leave-seat.mjs +44 -0
- package/skill/scripts/leave-seat.sh +3 -92
- package/skill/scripts/leave-seat.test.mjs +64 -0
- package/skill/scripts/legacy-entry.mjs +33 -0
- package/skill/scripts/platform/windows/resolve-lattice-command.mjs +12 -1
- package/skill/scripts/project-runtime.mjs +95 -0
- package/skill/scripts/project-runtime.test.mjs +29 -0
- package/skill/scripts/project-scaffold.mjs +179 -0
- package/skill/scripts/project-scaffold.test.mjs +129 -0
- package/skill/scripts/refresh-seat-identity.mjs +11 -8
- package/skill/scripts/remove-managed-room-mcp.mjs +9 -4
- package/skill/scripts/resume.sh +2 -225
- package/skill/scripts/room-api.mjs +28 -0
- package/skill/scripts/room-public-session.test.mjs +73 -0
- package/skill/scripts/runtime-contract.test.mjs +13 -146
- package/skill/scripts/runtime-launch-command.mjs +19 -0
- package/skill/scripts/runtime-launch-command.test.mjs +32 -0
- package/skill/scripts/seat-approval.mjs +24 -0
- package/skill/scripts/seat-approval.test.mjs +43 -0
- package/skill/scripts/seat-identity.mjs +2 -71
- package/skill/scripts/seat-launch-phase.mjs +11 -0
- package/skill/scripts/seat-observer.mjs +54 -0
- package/skill/scripts/seat-observer.test.mjs +58 -0
- package/skill/scripts/seat-session.mjs +17 -0
- package/skill/scripts/seat-status-bridge.mjs +33 -206
- package/skill/scripts/seat-usage.mjs +3 -334
- package/skill/scripts/setup.sh +3 -215
- package/skill/scripts/teardown.mjs +113 -0
- package/skill/scripts/teardown.sh +3 -410
- package/skill/scripts/teardown.test.mjs +62 -0
- package/skill/scripts/upgrade-team-assets.mjs +268 -0
- package/skill/scripts/upgrade-team-assets.sh +2 -282
- package/skill/scripts/wakeup-bridge.mjs +26 -32
- package/skill/scripts/wakeup-delivery.mjs +4 -14
- package/skill/templates/charter.md +1 -1
- package/skill/templates/member-standalone.md +1 -1
- package/skill/templates/member.md +3 -3
- package/skill/scripts/agent-pane-status.mjs +0 -6
- package/skill/scripts/aiterm-configure.mjs +0 -31
- package/skill/scripts/aiterm-launch.mjs +0 -64
- package/skill/scripts/aiterm-send.mjs +0 -22
- package/skill/scripts/claude-dialog.mjs +0 -26
- package/skill/scripts/codex-dialog.mjs +0 -67
- package/skill/scripts/platform/windows/build-bridge-command.mjs +0 -52
- package/skill/scripts/seat-input.mjs +0 -36
- package/skill/scripts/tmux-at.bash +0 -22
- package/skill/scripts/tmux-socket.mjs +0 -19
- 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
|
-
#
|
|
3
|
-
|
|
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 {
|
|
24
|
-
import {
|
|
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
|
-
|
|
370
|
-
|
|
372
|
+
const aiterm = new AitermClient()
|
|
373
|
+
async function passKnownSeatApproval(member, observation) {
|
|
371
374
|
if (!isWakeupBridgeTarget(member, targetOpts)) return false
|
|
372
|
-
const
|
|
373
|
-
|
|
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
|
|
393
|
-
if (
|
|
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
|
|
405
|
-
|
|
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,
|
|
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
|
|
419
|
-
const
|
|
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
|
-
//
|
|
613
|
+
// 席のtool approvalはroom発言の配達時だけでなく、席自身の最初のtool実行でも
|
|
620
614
|
// 遅れて現れる。pending DMが無い時も既知dialogだけを処理し続け、ランプがblockedのまま
|
|
621
|
-
// 放置される状態を作らない。未知dialogは
|
|
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
|
|
630
|
-
log(`
|
|
623
|
+
try { await passKnownSeatApproval(member) } catch (error) {
|
|
624
|
+
log(`SEAT_APPROVAL_SWEEP_FAILED: ${member.name} ${error.message}`)
|
|
631
625
|
}
|
|
632
626
|
}
|
|
633
627
|
} finally {
|