peertable 0.8.54 → 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 +12 -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 +7 -18
  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,89 @@
1
+ #!/usr/bin/env node
2
+ // skill配置だけを所有する。project、room、AIの設定ファイルは変更しない。
3
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, symlinkSync, unlinkSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { dirname, join, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ export const clients = Object.freeze(['claude', 'codex', 'grok', 'cursor'])
9
+ export const packageRoot = realpathSync(fileURLToPath(new URL('../../', import.meta.url)))
10
+ const fail = (code, message) => Object.assign(new Error(`${code}: ${message}`), { code })
11
+ const stat = path => {
12
+ try { return lstatSync(path) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
13
+ }
14
+
15
+ function ownedLink(target, source) {
16
+ if (resolve(target) === source) return true
17
+ // 旧手動配置はPeertable packageのskillへのリンクだけを移行する。
18
+ if (target.split(/[\\/]/u).at(-1) !== 'skill') return false
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(join(dirname(target), 'package.json'), 'utf8'))
21
+ return pkg.name === 'peertable' && existsSync(join(target, 'SKILL.md'))
22
+ } catch (error) {
23
+ if (error.code === 'ENOENT' || error instanceof SyntaxError) return false
24
+ throw error
25
+ }
26
+ }
27
+
28
+ export function inspectSkill({ root = packageRoot, home = homedir(), targets } = {}) {
29
+ const source = realpathSync(join(root, 'skill'))
30
+ const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version
31
+ const selected = targets?.length ? targets : clients.filter(client => existsSync(join(home, `.${client}`)))
32
+ for (const client of selected) {
33
+ if (!clients.includes(client)) throw fail('PEERTABLE_SKILL_TARGET_UNKNOWN', client)
34
+ }
35
+ const entries = [...new Set(selected)].map(client => {
36
+ const path = join(home, `.${client}`, 'skills', 'peertable')
37
+ const info = stat(path)
38
+ if (!info) return { client, path, status: 'missing' }
39
+ if (!info.isSymbolicLink()) return { client, path, status: 'conflict' }
40
+ const target = resolve(dirname(path), readlinkSync(path))
41
+ if (target === source) return { client, path, status: 'current' }
42
+ return { client, path, status: ownedLink(target, source) ? 'outdated' : 'conflict' }
43
+ })
44
+ return { schema: 'peertable.skill-install.v1', version, source, targets: entries,
45
+ status: !entries.length ? 'not_detected' : entries.every(entry => entry.status === 'current') ? 'ready' : 'required' }
46
+ }
47
+
48
+ export function installSkill(options = {}) {
49
+ const result = inspectSkill(options)
50
+ // 外部の既存資産との衝突は、どの導入先へも書き込む前に知らせる。
51
+ const conflicts = result.targets.filter(entry => entry.status === 'conflict')
52
+ if (conflicts.length) throw fail('PEERTABLE_SKILL_CONFLICT', conflicts.map(entry => entry.path).join(', '))
53
+ for (const entry of result.targets) {
54
+ if (entry.status === 'current') continue
55
+ mkdirSync(dirname(entry.path), { recursive: true })
56
+ // symlinkだけを取り替え、旧リンク先のsourceは一切変更しない。
57
+ if (entry.status === 'outdated') unlinkSync(entry.path)
58
+ symlinkSync(result.source, entry.path, process.platform === 'win32' ? 'junction' : 'dir')
59
+ entry.status = entry.status === 'missing' ? 'installed' : 'updated'
60
+ if (realpathSync(entry.path) !== result.source) throw fail('PEERTABLE_SKILL_VERIFY_FAILED', entry.path)
61
+ }
62
+ result.status = result.targets.length ? 'ready' : 'not_detected'
63
+ return result
64
+ }
65
+
66
+ export function skillCommand(args) {
67
+ const targets = []
68
+ let check = false
69
+ for (let index = 0; index < args.length; index += 1) {
70
+ const arg = args[index]
71
+ if (arg === '--json') continue
72
+ if (arg === '--check') { check = true; continue }
73
+ if (arg === '--target' && args[index + 1]) { targets.push(args[++index]); continue }
74
+ throw fail('PEERTABLE_SKILL_ARGUMENT_INVALID', arg)
75
+ }
76
+ return check ? inspectSkill({ targets }) : installSkill({ targets })
77
+ }
78
+
79
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
80
+ try {
81
+ if (process.argv.includes('--postinstall') && process.env.npm_config_global !== 'true') process.exit(0)
82
+ const result = skillCommand(process.argv.slice(2).filter(arg => arg !== '--postinstall'))
83
+ console.log(JSON.stringify(result))
84
+ if (process.argv.includes('--check') && result.status !== 'ready') process.exitCode = 1
85
+ } catch (error) {
86
+ console.error(JSON.stringify({ schema: 'peertable.error.v1', code: error.code ?? 'PEERTABLE_SKILL_INSTALL_FAILED', message: error.message }))
87
+ process.exitCode = 1
88
+ }
89
+ }
@@ -0,0 +1,84 @@
1
+ import assert from 'node:assert/strict'
2
+ import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+ import test from 'node:test'
6
+ import { inspectSkill, installSkill } from './install-skill.mjs'
7
+
8
+ function fixture(t) {
9
+ const dir = mkdtempSync(join(tmpdir(), 'peertable-skill-'))
10
+ t.after(() => rmSync(dir, { recursive: true, force: true }))
11
+ const home = join(dir, 'home with spaces')
12
+ mkdirSync(home)
13
+ function pkg(name, version) {
14
+ const root = join(dir, name)
15
+ mkdirSync(join(root, 'skill'), { recursive: true })
16
+ writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'peertable', version }))
17
+ writeFileSync(join(root, 'skill', 'SKILL.md'), `---\nname: peertable\n---\n${version}\n`)
18
+ return root
19
+ }
20
+ return { home, pkg, root: pkg('package 1', '1.0.0') }
21
+ }
22
+
23
+ test('初回配置と再実行で同じskillを公開しAI設定とprojectを保持する', t => {
24
+ const { root, home } = fixture(t)
25
+ const targets = ['claude', 'codex', 'grok', 'cursor']
26
+ for (const client of targets) {
27
+ mkdirSync(join(home, `.${client}`))
28
+ writeFileSync(join(home, `.${client}`, 'config.toml'), '# 独自設定\n')
29
+ }
30
+ mkdirSync(join(home, 'project', '.team'), { recursive: true })
31
+ writeFileSync(join(home, 'project', '.team', 'setup-state.json'), '{"room":"既存room"}\n')
32
+ const first = installSkill({ root, home })
33
+ assert.equal(first.status, 'ready')
34
+ assert.deepEqual(first.targets.map(entry => entry.status), targets.map(() => 'installed'))
35
+ const inodes = first.targets.map(entry => lstatSync(entry.path).ino)
36
+ const second = installSkill({ root, home })
37
+ assert.deepEqual(second.targets.map(entry => entry.status), targets.map(() => 'current'))
38
+ assert.deepEqual(second.targets.map(entry => lstatSync(entry.path).ino), inodes)
39
+ for (const entry of first.targets) {
40
+ assert.equal(realpathSync(entry.path), realpathSync(join(root, 'skill')))
41
+ assert.equal(readFileSync(join(home, `.${entry.client}`, 'config.toml'), 'utf8'), '# 独自設定\n')
42
+ }
43
+ assert.equal(readFileSync(join(home, 'project', '.team', 'setup-state.json'), 'utf8'), '{"room":"既存room"}\n')
44
+ })
45
+
46
+ test('更新は旧Peertableへの手動リンクを移行し旧sourceを保持する', t => {
47
+ const { root, home, pkg } = fixture(t)
48
+ installSkill({ root, home, targets: ['codex'] })
49
+ const updatedRoot = pkg('package 2', '2.0.0')
50
+ const result = installSkill({ root: updatedRoot, home, targets: ['codex'] })
51
+ assert.equal(result.targets[0].status, 'updated')
52
+ assert.equal(readFileSync(join(home, '.codex', 'skills', 'peertable', 'SKILL.md'), 'utf8'), '---\nname: peertable\n---\n2.0.0\n')
53
+ assert.equal(readFileSync(join(root, 'skill', 'SKILL.md'), 'utf8'), '---\nname: peertable\n---\n1.0.0\n')
54
+ })
55
+
56
+ test('独自ディレクトリとの衝突は全導入先への書込みより前に止まる', t => {
57
+ const { root, home } = fixture(t)
58
+ const custom = join(home, '.codex', 'skills', 'peertable')
59
+ mkdirSync(custom, { recursive: true })
60
+ writeFileSync(join(custom, 'SKILL.md'), '利用者の独自skill')
61
+ assert.throws(() => installSkill({ root, home, targets: ['claude', 'codex'] }), { code: 'PEERTABLE_SKILL_CONFLICT' })
62
+ assert.equal(readFileSync(join(custom, 'SKILL.md'), 'utf8'), '利用者の独自skill')
63
+ assert.throws(() => lstatSync(join(home, '.claude')), { code: 'ENOENT' })
64
+ })
65
+
66
+ test('別製品へのリンクは上書きせず、診断は読み取りだけを行う', t => {
67
+ const { root, home } = fixture(t)
68
+ const custom = join(home, 'custom')
69
+ mkdirSync(custom)
70
+ const destination = join(home, '.grok', 'skills', 'peertable')
71
+ mkdirSync(join(home, '.grok', 'skills'), { recursive: true })
72
+ symlinkSync(custom, destination, process.platform === 'win32' ? 'junction' : 'dir')
73
+ assert.equal(inspectSkill({ root, home }).targets[0].status, 'conflict')
74
+ assert.throws(() => installSkill({ root, home }), { code: 'PEERTABLE_SKILL_CONFLICT' })
75
+ assert.equal(realpathSync(destination), realpathSync(custom))
76
+ })
77
+
78
+ test('未検出AIを作らず明示targetだけを導入する', t => {
79
+ const { root, home } = fixture(t)
80
+ assert.equal(installSkill({ root, home }).status, 'not_detected')
81
+ assert.throws(() => installSkill({ root, home, targets: ['unknown'] }), { code: 'PEERTABLE_SKILL_TARGET_UNKNOWN' })
82
+ assert.equal(installSkill({ root, home, targets: ['cursor'] }).targets[0].client, 'cursor')
83
+ assert.throws(() => lstatSync(join(home, '.codex')), { code: 'ENOENT' })
84
+ })
@@ -0,0 +1,156 @@
1
+ // 正規着座。TUIの準備・入力・観測はAiterm、席の設定・台帳はPeertableが所有する。
2
+ import { execFileSync, spawn } from 'node:child_process'
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
4
+ import { homedir, hostname, tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { AitermClient } from './aiterm-client.mjs'
7
+ import { RoomApi } from './room-api.mjs'
8
+ import { passSeatApproval } from './seat-approval.mjs'
9
+ import { leaveSeat } from './leave-seat.mjs'
10
+ import { ensureProjectRuntime } from './ensure-project-runtime.mjs'
11
+ import { projectPath, readSetup, runScript, fail } from './project-scaffold.mjs'
12
+ import { packageRoot } from './install-skill.mjs'
13
+ import { resolveWindowsCommand } from './platform/windows/resolve-lattice-command.mjs'
14
+
15
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
16
+ const harnessIds = { claude: 'claude-code', codex: 'codex-cli', grok: 'grok-cli' }
17
+
18
+ function prepareHarness(project, name, harness, env, script, home) {
19
+ if (harness === 'grok' || harness === 'codex') {
20
+ const key = harness === 'grok' ? 'GROK_HOME' : 'CODEX_HOME'
21
+ env[key] = join(project, '.team', 'seats', name + (harness === 'grok' ? '.grok-home' : '.codex'))
22
+ mkdirSync(env[key], { recursive: true })
23
+ const auth = join(home, `.${harness}`, 'auth.json')
24
+ if (existsSync(auth)) {
25
+ copyFileSync(auth, join(env[key], 'auth.json'))
26
+ chmodSync(join(env[key], 'auth.json'), 0o600)
27
+ } else if (harness === 'grok') fail('SEAT_GROK_AUTH_MISSING', 'Grokの認証がありません')
28
+ if (harness === 'grok') script('grok-seat-config.mjs', [project, join(env[key], 'config.toml')], { env })
29
+ else script('ensure-codex-room-mcp.mjs', ['ensure', project, packageRoot], { env })
30
+ }
31
+ }
32
+
33
+ export async function launchSeat(options, dependencies = {}) {
34
+ if (process.env.PEERTABLE_MEMBER) fail('SEAT_LAUNCH_DELEGATED_CHILD_FORBIDDEN', '席から別席は起動できません')
35
+ const project = projectPath(options.project)
36
+ const { name, brief = '' } = options
37
+ if (!name || !/^[A-Za-z0-9._:-]+$/.test(name)) fail('SEAT_LAUNCH_ARGS_INVALID', '有効な席名を指定してください')
38
+ if (!options.roles) fail('SEAT_LAUNCH_ARGS_INVALID', '役割を指定してください')
39
+ if (Buffer.byteLength(brief) > 65536) fail('LAUNCH_BRIEF_TOO_LONG', '着任指示の上限は65536 bytesです')
40
+ const env = { ...process.env }
41
+ const home = dependencies.home ?? homedir()
42
+ const script = dependencies.runScript ?? runScript
43
+ const execute = dependencies.execFileSync ?? execFileSync
44
+ const leave = dependencies.leaveSeat ?? leaveSeat
45
+ const ensureRuntime = dependencies.ensureProjectRuntime ?? ensureProjectRuntime
46
+ delete env.PEERTABLE_POST_TOKEN
47
+ delete env.PEERTABLE_TMUX_SOCKET
48
+ const placementArgs = ['--roles', options.roles]
49
+ for (const key of ['model', 'effort', 'harness']) if (options[key]) placementArgs.push(`--${key}`, options[key])
50
+ const placement = JSON.parse(script('resolve-seat-placement.mjs', placementArgs, { env }))
51
+ const { model, harness, effort } = placement.settings
52
+ if (!harnessIds[harness]) fail('SEAT_LAUNCH_HARNESS_UNSUPPORTED', harness)
53
+ script('upgrade-team-assets.mjs', [project], { env })
54
+ const state = readSetup(project)
55
+ // 非対話のモデル実測は、既存席を畳む前に行う。
56
+ const preflightArgs = harness === 'codex' ? ['exec', '--model', model, '--skip-git-repo-check', 'ping']
57
+ : harness === 'grok' ? ['--model', model, '--reasoning-effort', effort, '-p', 'ping'] : ['--model', model, '-p', 'ping']
58
+ const preflight = (dependencies.resolveCommand ?? resolveWindowsCommand)(harness, preflightArgs)
59
+ const preflightEnv = { ...env }
60
+ let preflightHome
61
+ try {
62
+ if (harness === 'grok') {
63
+ preflightHome = mkdtempSync(join(tmpdir(), 'peertable-model-'))
64
+ copyFileSync(join(home, '.grok', 'auth.json'), join(preflightHome, 'auth.json'))
65
+ chmodSync(join(preflightHome, 'auth.json'), 0o600)
66
+ preflightEnv.GROK_HOME = preflightHome
67
+ script('grok-seat-config.mjs', [project, join(preflightHome, 'config.toml')], { env: preflightEnv })
68
+ }
69
+ execute(preflight.command, preflight.argv, { cwd: tmpdir(), env: preflightEnv, encoding: 'utf8', timeout: 120_000, stdio: ['ignore', 'pipe', 'pipe'] })
70
+ } finally { if (preflightHome) rmSync(preflightHome, { recursive: true, force: true }) }
71
+
72
+ // 登録前に中断した席も、公開envで同じroomの本人と確認して撤去する。
73
+ await leave(project, name)
74
+ const credential = script('seat-credential.mjs', ['prepare', project, state.room, name], { env })
75
+ Object.assign(env, {
76
+ PEERTABLE_URL: state.server_url, PEERTABLE_ROOM: state.room, PEERTABLE_MEMBER: name,
77
+ PEERTABLE_CREDENTIAL_FILE: credential, PEERTABLE_HARNESS: harness, PEERTABLE_VENDOR: harness,
78
+ PEERTABLE_MODEL: model, PEERTABLE_EFFORT: effort || '', PEERTABLE_ROLE: placement.roles[0],
79
+ PEERTABLE_ROLES: placement.roles.join(','), PEERTABLE_MISSION: options.mission || '',
80
+ })
81
+ if (state.mode === 'lattice') Object.assign(env, {
82
+ PEERTABLE_PLAN: state.plan_key, LATTICE_CLI: state.lattice_cli || env.LATTICE_CLI || 'lattice',
83
+ LATTICE_TODO_ACTOR_HOST: env.LATTICE_TODO_ACTOR_HOST || hostname(),
84
+ LATTICE_TODO_ACTOR_SESSION: name, LATTICE_TODO_ACTOR_AGENT: name,
85
+ })
86
+ const api = dependencies.api ?? new RoomApi(state, { credential })
87
+ const aiterm = dependencies.aiterm ?? new AitermClient({ env })
88
+ const sessionId = `peer-${name}`
89
+ let launched = false
90
+ try {
91
+ prepareHarness(project, name, harness, env, script, home)
92
+ const before = await aiterm.sessions()
93
+ if (before.some(session => session.session_id === sessionId)) fail('SEAT_SESSION_CONFLICT', `${sessionId} が既に存在します`)
94
+ launched = true
95
+ const launch = await aiterm.structured('agent_launch', {
96
+ session_name: sessionId, harness: harnessIds[harness], model, reasoning_effort: effort,
97
+ cwd: project, trust_project: true,
98
+ env_vars: [
99
+ 'PEERTABLE_URL', 'PEERTABLE_ROOM', 'PEERTABLE_MEMBER', 'PEERTABLE_CREDENTIAL_FILE',
100
+ 'PEERTABLE_HARNESS', 'PEERTABLE_VENDOR', 'PEERTABLE_MODEL', 'PEERTABLE_EFFORT',
101
+ 'PEERTABLE_ROLE', 'PEERTABLE_ROLES', 'PEERTABLE_MISSION', 'PEERTABLE_PLAN',
102
+ 'LATTICE_CLI', 'LATTICE_TODO_ACTOR_HOST', 'LATTICE_TODO_ACTOR_SESSION', 'LATTICE_TODO_ACTOR_AGENT',
103
+ 'GROK_HOME', 'CODEX_HOME',
104
+ ].filter(key => env[key] !== undefined),
105
+ }, 'aiterm.agent-launch-result.v1')
106
+ if (launch.session_id !== sessionId || launch.startup?.status !== 'ready') {
107
+ fail('SEAT_STARTUP_NOT_READY', `${name} の起動準備が完了していません: ${launch.startup?.reason ?? 'startup応答なし'}`)
108
+ }
109
+ const readyDeadline = Date.now() + 90_000
110
+ for (;;) {
111
+ await passSeatApproval(aiterm, sessionId, harness)
112
+ const registered = (await api.members()).find(member => member.name === name)
113
+ if (registered) break
114
+ if (Date.now() >= readyDeadline) fail('SEAT_ROOM_MCP_NOT_READY', `${name} のroom参加登録を確認できません`)
115
+ await delay(1000)
116
+ }
117
+ const observed = await aiterm.observe(sessionId)
118
+ if (!observed.process_identity) fail('SEAT_IDENTITY_UNAVAILABLE', `${name} の本人性を確認できません`)
119
+ await api.request('members', { method: 'POST', body: {
120
+ name, ...observed.process_identity, identity_recorded_at: observed.observed_at,
121
+ } })
122
+ await ensureRuntime(project, { env })
123
+ let turnStarted = false
124
+ if (brief) {
125
+ const receipt = await aiterm.structured('pty_send', { session_id: sessionId, text: brief }, 'aiterm.pty-send-result.v1')
126
+ if (receipt.mode !== 'agent_dispatch' || receipt.submit_residue || receipt.event_cursor == null || !receipt.wait_process) {
127
+ fail('LAUNCH_BRIEF_SEND_FAILED', `${name} の着任指示が成立していません`)
128
+ }
129
+ let completion = null
130
+ const wp = receipt.wait_process
131
+ const waiter = (dependencies.spawn ?? spawn)(wp.executable, wp.args, { detached: true, stdio: 'ignore', env })
132
+ waiter.on('error', error => { completion = { error } })
133
+ waiter.on('exit', code => { completion = { code } })
134
+ waiter.unref()
135
+ const deadline = Date.now() + 60_000
136
+ for (;;) {
137
+ if (completion?.error) throw completion.error
138
+ if (completion && completion.code !== 0) fail('LAUNCH_BRIEF_TURN_FAILED', `完了観測がexit ${completion.code}を返しました`)
139
+ const current = await aiterm.observe(sessionId)
140
+ if (current.state === 'busy' || completion?.code === 0) { turnStarted = true; break }
141
+ await passSeatApproval(aiterm, sessionId, harness)
142
+ if (Date.now() >= deadline) fail('LAUNCH_BRIEF_TURN_NOT_STARTED', `${name} の実ターン開始を確認できません`)
143
+ await delay(1000)
144
+ }
145
+ }
146
+ const stored = (await api.members()).find(member => member.name === name)
147
+ if (!stored?.model || !stored.roles?.length || !stored.aiterm_session_id || !stored.pid) fail('SEAT_LEDGER_INCOMPLETE', `${name} の台帳が不完全です`)
148
+ return { schema: 'peertable.seat-launch.v1', status: 'ready', member: name, session_id: sessionId, placement, turn_started: turnStarted }
149
+ } catch (error) {
150
+ if (launched) {
151
+ try { await leave(project, name, { aiterm, api }) }
152
+ catch (rollback) { throw new AggregateError([error, rollback], `着座失敗後の撤去も失敗しました: ${error.message}; ${rollback.message}`) }
153
+ } else script('seat-credential.mjs', ['remove', project, credential], { env })
154
+ throw error
155
+ } finally { if (!dependencies.aiterm) await aiterm.close() }
156
+ }