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,113 @@
|
|
|
1
|
+
// 解散はroomと履歴を残す。公開APIで席を止めてから、所有する足場を撤去する。
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, copyFileSync } from 'node:fs'
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { AitermClient } from './aiterm-client.mjs'
|
|
6
|
+
import { RoomApi } from './room-api.mjs'
|
|
7
|
+
import { leaveSeat } from './leave-seat.mjs'
|
|
8
|
+
import { projectPath, readSetup, readJson, runScript, removeExclude, fail } from './project-scaffold.mjs'
|
|
9
|
+
import { packageRoot } from './install-skill.mjs'
|
|
10
|
+
import { resolveLatticeInvocation } from './seat-usage.mjs'
|
|
11
|
+
import { seatSessionId } from './seat-session.mjs'
|
|
12
|
+
|
|
13
|
+
function landingReports(project, state) {
|
|
14
|
+
if (state.mode !== 'lattice') return []
|
|
15
|
+
const run = args => {
|
|
16
|
+
const invocation = resolveLatticeInvocation(process.env.LATTICE_CLI || state.lattice_cli || 'lattice', args)
|
|
17
|
+
return JSON.parse(execFileSync(invocation.command, invocation.argv, { cwd: project, encoding: 'utf8' }))
|
|
18
|
+
}
|
|
19
|
+
const listed = run(['run', 'list', '--json'])
|
|
20
|
+
if (listed.schema !== 'lattice.run_list.v1' || !Array.isArray(listed.active_runs)) fail('PEERTABLE_LANDING_INVALID', 'run listの契約が異なります')
|
|
21
|
+
return listed.active_runs.map(entry => {
|
|
22
|
+
if (!entry?.run_ref) fail('PEERTABLE_LANDING_INVALID', 'run_refがありません')
|
|
23
|
+
const report = run(['run', 'landing', '--run', entry.run_ref])
|
|
24
|
+
if (report.schema !== 'lattice.run_landing_report.v1' || !Array.isArray(report.accepted_receipts)
|
|
25
|
+
|| report.accepted_receipts.some(receipt => typeof receipt.landed !== 'boolean')) fail('PEERTABLE_LANDING_INVALID', 'run landingの契約が異なります')
|
|
26
|
+
return report
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function archiveLog(api, project, room) {
|
|
31
|
+
const { messages } = await api.request('messages')
|
|
32
|
+
const dir = join(project, 'docs', 'archive')
|
|
33
|
+
mkdirSync(dir, { recursive: true })
|
|
34
|
+
const file = join(dir, `room-log_${room}_${new Date().toISOString().replace(/[:.]/g, '-')}.md`)
|
|
35
|
+
const lines = [`# 円卓ログ — room ${room}(全${messages.length}発言)`, '', '解散前の控え。roomと過去ログの原本はサーバーに残ります。', '']
|
|
36
|
+
for (const msg of messages) lines.push(`## [${msg.seq}] ${msg.from} → ${msg.to_names?.join(', ') ?? msg.to} ・ ${msg.ts}`, '', msg.body ?? '(本文欠落)', '')
|
|
37
|
+
writeFileSync(file, lines.join('\n'))
|
|
38
|
+
return file
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function teardownProject(options, dependencies = {}) {
|
|
42
|
+
const project = projectPath(options.project)
|
|
43
|
+
const state = readSetup(project)
|
|
44
|
+
const team = join(project, '.team')
|
|
45
|
+
const env = { ...process.env }
|
|
46
|
+
delete env.PEERTABLE_POST_TOKEN
|
|
47
|
+
const script = dependencies.runScript ?? runScript
|
|
48
|
+
const credential = script('seat-credential.mjs', ['prepare', project, state.room, 'teardown'], { env })
|
|
49
|
+
const api = dependencies.api ?? new RoomApi(state, { credential })
|
|
50
|
+
const aiterm = dependencies.aiterm ?? new AitermClient({ env })
|
|
51
|
+
const steps = []
|
|
52
|
+
const errors = []
|
|
53
|
+
const step = async (name, action) => {
|
|
54
|
+
try { const result = await action(); steps.push({ name, status: 'done', result }); return true }
|
|
55
|
+
catch (error) { errors.push(error); steps.push({ name, status: 'failed', message: error.message }); return false }
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const members = await api.members()
|
|
59
|
+
await aiterm.sessions()
|
|
60
|
+
if (!options.purge) await step('roomログの控え', () => archiveLog(api, project, state.room))
|
|
61
|
+
let stopped = true
|
|
62
|
+
for (const member of members) {
|
|
63
|
+
if (member.delivery?.kind === 'parent_watch' || !seatSessionId(member)) continue
|
|
64
|
+
if (!await step(`席 ${member.name} の退席`, () => leaveSeat(project, member.name, { aiterm }))) stopped = false
|
|
65
|
+
}
|
|
66
|
+
for (const kind of ['wakeup', 'seat-status', 'alarm']) {
|
|
67
|
+
const recordPath = join(team, `${kind}-bridge.json`)
|
|
68
|
+
if (!existsSync(recordPath)) { steps.push({ name: `${kind}停止`, status: 'skipped', reason: '起動記録なし' }); continue }
|
|
69
|
+
const record = readJson(recordPath)
|
|
70
|
+
if (!await step(`${kind}停止`, async () => {
|
|
71
|
+
script(`${kind}-bridge.mjs`, [project, '--stop'], { env })
|
|
72
|
+
const id = record.aiterm_session_id || `peertable-${kind}-${state.room}`
|
|
73
|
+
if ((await aiterm.sessions()).some(session => session.session_id === id)) await aiterm.call('pty_close', { session_id: id })
|
|
74
|
+
if ((await aiterm.sessions()).some(session => session.session_id === id)) fail('PEERTABLE_RUNTIME_STOP_FAILED', `${id} が残っています`)
|
|
75
|
+
})) stopped = false
|
|
76
|
+
}
|
|
77
|
+
await step('Lattice成果の着地確認', () => landingReports(project, state))
|
|
78
|
+
// 停止できなかったruntimeの記録を残し、同じ入口から再実行できる状態を保つ。
|
|
79
|
+
if (!stopped) fail('PEERTABLE_TEARDOWN_STOP_FAILED', '席またはruntimeの停止に失敗しました。.teamを残しています')
|
|
80
|
+
if (errors.length) fail('PEERTABLE_TEARDOWN_INCOMPLETE', 'ログ保存または成果確認に失敗しました。再実行用の.teamを残しています')
|
|
81
|
+
const roomDone = await step(options.purge ? 'room削除' : 'room解散', async () => {
|
|
82
|
+
if (options.purge) {
|
|
83
|
+
await api.request('', { method: 'DELETE' })
|
|
84
|
+
} else {
|
|
85
|
+
await api.request('messages', { method: 'POST', body: { from: 'system', to: 'system', body: `解散。この卓はここまで。参加者: ${members.map(member => member.name).join(', ')}。部屋と過去ログは残ります。` } })
|
|
86
|
+
for (const member of await api.members()) await api.request(`members/${encodeURIComponent(member.name)}`, { method: 'DELETE' })
|
|
87
|
+
await api.request('bridges', { method: 'DELETE' })
|
|
88
|
+
await api.request('archive', { method: 'POST' })
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
if (!roomDone) fail('PEERTABLE_TEARDOWN_ROOM_FAILED', 'roomの撤去に失敗しました。再実行用の.teamを残しています')
|
|
92
|
+
if (state.external_pane) {
|
|
93
|
+
if (state.project_json_preexisting) copyFileSync(join(team, 'project.json.bak'), join(project, '.lattice', 'project.json'))
|
|
94
|
+
else rmSync(join(project, '.lattice', 'project.json'), { force: true })
|
|
95
|
+
}
|
|
96
|
+
const managed = state.room_mcp_managed ?? state.added_root_mcp ?? state.root_mcp_json_fallback ?? false
|
|
97
|
+
if (managed) script('remove-managed-room-mcp.mjs', [project], { env })
|
|
98
|
+
script('ensure-codex-room-mcp.mjs', ['remove', project, packageRoot], { env: { ...env, CODEX_HOME: join(project, '.codex') } })
|
|
99
|
+
if (state.work_order_adapter && !state.runtime_preexisting) rmSync(join(project, '.lattice', 'runtime'), { recursive: true, force: true })
|
|
100
|
+
if (options.purge && !state.lattice_preexisting) rmSync(join(project, '.lattice'), { recursive: true, force: true })
|
|
101
|
+
for (const [flag, rule] of [['added_exclude', '.team/'], ['added_mcp_exclude', '/.mcp.json'], ['added_runtime_exclude', '/.lattice/runtime/']]) {
|
|
102
|
+
if (state[flag]) removeExclude(project, rule)
|
|
103
|
+
}
|
|
104
|
+
rmSync(team, { recursive: true })
|
|
105
|
+
steps.push({ name: 'projectの足場撤去', status: 'done' })
|
|
106
|
+
const result = { schema: 'peertable.teardown-result.v1', status: errors.length ? 'incomplete' : 'done', mode: options.purge ? 'purge' : 'archive', project, steps }
|
|
107
|
+
if (errors.length) throw Object.assign(new AggregateError(errors, '一部の工程に失敗しました'), { code: 'PEERTABLE_TEARDOWN_INCOMPLETE', result })
|
|
108
|
+
return result
|
|
109
|
+
} catch (error) {
|
|
110
|
+
error.result ??= { schema: 'peertable.teardown-result.v1', status: 'incomplete', project, steps }
|
|
111
|
+
throw error
|
|
112
|
+
} finally { if (!dependencies.aiterm) await aiterm.close() }
|
|
113
|
+
}
|
|
@@ -1,411 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# --purge : 何も残さず全部消す(従来の痕跡ゼロ・ゲスト project 向け)
|
|
6
|
-
# 書込トークンは環境変数 PEERTABLE_POST_TOKEN から取る(`~/.config/peertable.env` は export 付きで定義すること)。
|
|
7
|
-
#
|
|
8
|
-
# 撤去は「何が実施され、何が実施されなかったか」を1行ずつ出す。room 削除だけがトークンを要するので、
|
|
9
|
-
# そこが失敗しても残りの撤去は続行し、未実施を明示して非ゼロで終わる(黙って中断しない・決定58)。
|
|
10
|
-
#
|
|
11
|
-
# **既定が archive なのはオーナー裁定(2026-08-09・決定61)**。痕跡ゼロを既定にしていた時代は、
|
|
12
|
-
# 畳んだ瞬間に room の会話ログ(server 側の正本)と Lattice store が消えていた——**卓の議論と
|
|
13
|
-
# 工程の記録は、卓そのものより寿命が長い**。ゲスト project を汚さない不可侵原則は `--purge` が持つ。
|
|
14
|
-
set -e
|
|
15
|
-
proj="$1"
|
|
16
|
-
script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
17
|
-
# shellcheck disable=SC1091
|
|
18
|
-
. "$script_dir/tmux-at.bash"
|
|
19
|
-
peertable_repo=$(cd "$script_dir/../.." && pwd -P)
|
|
20
|
-
mode=archive
|
|
21
|
-
for arg in "${@:2}"; do
|
|
22
|
-
case "$arg" in
|
|
23
|
-
--purge) mode=purge ;;
|
|
24
|
-
--archive) mode=archive ;;
|
|
25
|
-
*) echo "ERROR: 未知の引数: ${arg}(受けるのは --purge / --archive だけ)" >&2; exit 1 ;;
|
|
26
|
-
esac
|
|
27
|
-
done
|
|
28
|
-
state="$proj/.team/setup-state.json"
|
|
29
|
-
room=$(python3 -c "import json;print(json.load(open('$state'))['room'])")
|
|
30
|
-
url=$(python3 -c "import json;print(json.load(open('$state'))['server_url'])")
|
|
31
|
-
added=$(python3 -c "import json;print(json.load(open('$state'))['added_exclude'])")
|
|
32
|
-
lat_pre=$(python3 -c "import json;print(json.load(open('$state'))['lattice_preexisting'])")
|
|
33
|
-
runtime_pre=$(python3 -c "import json;d=json.load(open('$state'));print(d.get('runtime_preexisting', True))")
|
|
34
|
-
added_runtime_ex=$(python3 -c "import json;d=json.load(open('$state'));print(d.get('added_runtime_exclude', False))")
|
|
35
|
-
work_order_adapter=$(python3 -c "import json;d=json.load(open('$state'));print(d.get('work_order_adapter', False))")
|
|
36
|
-
work_order_spool_ref=$(python3 -c "import json;d=json.load(open('$state'));print(d.get('work_order_spool_ref', ''))")
|
|
37
|
-
# 旧 state(added_root_mcp 不在・手動フォールバック時代の root_mcp_json_fallback)も読む
|
|
38
|
-
added_mcp=$(python3 -c "import json;d=json.load(open('$state'));print(d.get('added_root_mcp', d.get('root_mcp_json_fallback', False)))")
|
|
39
|
-
added_mcp_ex=$(python3 -c "import json;d=json.load(open('$state'));print(d.get('added_mcp_exclude', d.get('root_mcp_json_fallback', False)))")
|
|
40
|
-
|
|
41
|
-
fail=0
|
|
42
|
-
did() { echo "teardown: [実施] $*"; }
|
|
43
|
-
skip() { echo "teardown: [スキップ] $*"; }
|
|
44
|
-
miss() { echo "teardown: [未実施] $*" >&2; fail=1; }
|
|
45
|
-
yes_() { [ "$1" = "True" ] || [ "$1" = "true" ]; }
|
|
46
|
-
platform=$(node -p 'process.platform')
|
|
47
|
-
normalize_read_line() {
|
|
48
|
-
if [ "$platform" = win32 ]; then
|
|
49
|
-
printf '%s' "$1" | node "$script_dir/platform/windows/normalize-read-line.mjs"
|
|
50
|
-
else
|
|
51
|
-
printf '%s' "$1"
|
|
52
|
-
fi
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
mcp_remove_action=not-managed
|
|
56
|
-
if yes_ "$added_mcp"; then
|
|
57
|
-
mcp_remove_result=$(node "$script_dir/remove-managed-room-mcp.mjs" "$proj") || exit "$?"
|
|
58
|
-
mcp_remove_action=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["action"])' "$mcp_remove_result")
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
echo "teardown: mode=${mode}(archive=ログとstoreを残す/purge=痕跡ゼロ)"
|
|
62
|
-
|
|
63
|
-
# ---- ログの控え(archive だけ)。room 自体は残るので、これは repo 側の写し ----
|
|
64
|
-
# 失敗しても撤去は続ける(room に原本があるため)。--purge の時だけ「消す前の最後の機会」になる
|
|
65
|
-
log_saved=skip
|
|
66
|
-
if [ "$mode" = archive ]; then
|
|
67
|
-
arc="$proj/docs/archive"
|
|
68
|
-
out="$arc/room-log_${room}_$(date +%Y%m%d-%H%M%S).md"
|
|
69
|
-
mkdir -p "$arc"
|
|
70
|
-
if python3 "$(dirname "$0")/archive-room-log.py" "$url" "$room" "$out"; then
|
|
71
|
-
did "room ログの写しを ${out#"$proj/"} へ(原本は room に残る)"
|
|
72
|
-
log_saved=yes
|
|
73
|
-
else
|
|
74
|
-
miss "room ログの写しに失敗($url/api/$room が読めない)— **原本は room に残っている**ので撤去は続行する"
|
|
75
|
-
log_saved=no
|
|
76
|
-
fi
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
# ---- 席(tmux)の終了。**`.team/` を消す前**に、この room の member だけを畳む ----
|
|
80
|
-
# `peer-*` を全部畳むと、同じマシンの別の卓を巻き込む(bridge が members 起点にしているのと同じ理由)
|
|
81
|
-
sock=$(node "$(dirname "$0")/tmux-socket.mjs")
|
|
82
|
-
members_json=$(python3 "$(dirname "$0")/archive-room-log.py" --members --json "$url" "$room" 2>/dev/null || true)
|
|
83
|
-
seat_names=$(python3 "$(dirname "$0")/archive-room-log.py" --members "$url" "$room" 2>/dev/null || true)
|
|
84
|
-
member_lines=$(python3 -c 'import json,sys; [print(json.dumps(x,separators=(",",":"))) for x in json.loads(sys.stdin.read())]' <<<"$members_json" 2>/dev/null || true)
|
|
85
|
-
if [ -z "$members_json" ]; then
|
|
86
|
-
miss "席の終了 — member 一覧が取れず、畳む相手を特定できない。手で確認: tmux -S \"$sock\" list-sessions | grep peer-"
|
|
87
|
-
else
|
|
88
|
-
closed=0
|
|
89
|
-
while IFS= read -r member; do
|
|
90
|
-
[ -n "$member" ] || continue
|
|
91
|
-
name=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["name"])' "$member")
|
|
92
|
-
target=$(python3 -c 'import json,sys; x=json.loads(sys.argv[1]); print((x.get("observe") or {}).get("tmux_target") or "peer-" + x["name"])' "$member")
|
|
93
|
-
member_sock=$(python3 -c 'import json,sys; x=json.loads(sys.argv[1]); print((x.get("observe") or {}).get("tmux_socket") or sys.argv[2])' "$member" "$sock")
|
|
94
|
-
if tmux_at has-session -t "$target" 2>/dev/null; then
|
|
95
|
-
tmux_at kill-session -t "$target" && closed=$((closed + 1))
|
|
96
|
-
fi
|
|
97
|
-
done <<<"$member_lines"
|
|
98
|
-
if [ "$closed" -gt 0 ]; then did "席の終了(${closed}席)"; else skip "席の終了(生きている席なし)"; fi
|
|
99
|
-
left=$(tmux_at list-sessions 2>/dev/null | grep -c '^peer-' || true)
|
|
100
|
-
[ "${left:-0}" -eq 0 ] || echo "teardown: [注記] 他の卓の peer-* が ${left}件 残っている(この卓のものではないので畳まない)"
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
# Codex / Grok 席の TUI 配達(決定54)。常駐 process なので、`.team/` を消す前に確実に止める
|
|
104
|
-
if [ -f "$proj/.team/wakeup-bridge.json" ]; then
|
|
105
|
-
# 停止に失敗しても **ここで止まらない**。`set -e` で落ちると、t6 の契約(各段の実施・未実施を
|
|
106
|
-
# 1行ずつ出す/黙って中断しない)が丸ごと破れる——[未実施] も [手当] も要約も出ずに撤去が全部残る
|
|
107
|
-
if node "$(dirname "$0")/wakeup-bridge.mjs" "$proj" --stop; then
|
|
108
|
-
did "wakeup-bridge 停止"
|
|
109
|
-
else
|
|
110
|
-
miss "wakeup-bridge 停止に失敗(常駐が残る)— 上の _STOP_FAILED を見て手で止める"
|
|
111
|
-
fi
|
|
112
|
-
else
|
|
113
|
-
skip "wakeup-bridge(起動記録なし)"
|
|
114
|
-
fi
|
|
115
|
-
|
|
116
|
-
# 席の稼働状態ブリッジ。同じく常駐 process なので `.team/` を消す前に止める。
|
|
117
|
-
# ここで止めないと、pid 記録が `.team/` ごと消えて **`--stop` でも止められなくなる**——しかも
|
|
118
|
-
# 「起動記録が無い(既に停止)」と rc=0 で報告する=**止めたと嘘をつく残骸**になる(実測)
|
|
119
|
-
if [ -f "$proj/.team/seat-status-bridge.json" ]; then
|
|
120
|
-
# 停止に失敗しても **ここで止まらない**。`set -e` で落ちると、t6 の契約(各段の実施・未実施を
|
|
121
|
-
# 1行ずつ出す/黙って中断しない)が丸ごと破れる——[未実施] も [手当] も要約も出ずに撤去が全部残る
|
|
122
|
-
if node "$(dirname "$0")/seat-status-bridge.mjs" "$proj" --stop; then
|
|
123
|
-
did "seat-status-bridge 停止"
|
|
124
|
-
else
|
|
125
|
-
miss "seat-status-bridge 停止に失敗(常駐が残る)— 上の _STOP_FAILED を見て手で止める"
|
|
126
|
-
fi
|
|
127
|
-
else
|
|
128
|
-
skip "seat-status-bridge(起動記録なし)"
|
|
129
|
-
fi
|
|
130
|
-
|
|
131
|
-
# 工程変化の目覚ましbridgeも同じWindows processとしてlogを保持する。記録を残したまま
|
|
132
|
-
# `.team/`を消すとalarm-bridge.logがEBUSYになり、MCP/exclude復元の途中でteardownが止まる。
|
|
133
|
-
if [ -f "$proj/.team/alarm-bridge.json" ]; then
|
|
134
|
-
if node "$(dirname "$0")/alarm-bridge.mjs" "$proj" --stop; then
|
|
135
|
-
did "alarm-bridge 停止"
|
|
136
|
-
else
|
|
137
|
-
miss "alarm-bridge 停止に失敗(常駐が残る)— 上の ALARM_BRIDGE_STOP_FAILED を見て手で止める"
|
|
138
|
-
fi
|
|
139
|
-
else
|
|
140
|
-
skip "alarm-bridge(起動記録なし)"
|
|
141
|
-
fi
|
|
142
|
-
|
|
143
|
-
# run-bridge は退役済み(2026-08-22)。旧卓の常駐が残っていれば記録の pid を照合して止める。
|
|
144
|
-
if [ -f "$proj/.team/run-bridge.json" ]; then
|
|
145
|
-
rb_pid=$(python3 -c "import json;print(json.load(open('$proj/.team/run-bridge.json')).get('pid',''))" 2>/dev/null || true)
|
|
146
|
-
if [ -n "$rb_pid" ] && LC_ALL=C /bin/ps -o command= -p "$rb_pid" 2>/dev/null | grep -q "run-bridge.mjs"; then
|
|
147
|
-
kill "$rb_pid" 2>/dev/null || true
|
|
148
|
-
did "run-bridge(退役済み・旧常駐 pid ${rb_pid} を停止)"
|
|
149
|
-
else
|
|
150
|
-
skip "run-bridge(退役済み・生きた常駐なし)"
|
|
151
|
-
fi
|
|
152
|
-
rm -f "$proj/.team/run-bridge.json"
|
|
153
|
-
else
|
|
154
|
-
skip "run-bridge(退役済み)"
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
# run の close は成果が既定branchへ着地した証拠ではない。bridgeを止めてから、`run list`が挙げる
|
|
158
|
-
# **active run** の landing report を読む。**旧版は spool の work order から run を逆算していた**が、
|
|
159
|
-
# 配車が無くなって order が出ないので、装置に直接聞く形へ変えた(改・裁定1)。
|
|
160
|
-
# **`run list` は closed run を返さない**(実装で除外される)ので、ここで見えるのは未 close の分だけ
|
|
161
|
-
# である。closed 済み run の着地は `run close` の返値が landing report を含むので**その時点で読む**。
|
|
162
|
-
# `.lattice/runs` を直に走査して補わない——consumer contract 違反で、旧 orders の保持は配車の復活になる。
|
|
163
|
-
# 全 closed の再列挙が要るなら Lattice 側の公開面を足す別課題であって、ここで迂回実装しない。
|
|
164
|
-
# 未着地・未pushは判断結果なので `lattice run landing` 自体はexit 0を返し、teardownも止めない。
|
|
165
|
-
# **CLI の出力と exit code を先に単独で取る。** `cmd | python3` にすると pipeline の rc は
|
|
166
|
-
# python のものになり、**CLI が rc≠0 で typed error JSON を返しても parser が `active_runs` 欠落を
|
|
167
|
-
# 空配列として飲んで「active run なし」に化ける**(suzune の監査で実測・room [948])。
|
|
168
|
-
# 沈黙する偽 green は、着地の見落としをそのまま「確認済み」に見せる——いちばん避けたい壊れ方である。
|
|
169
|
-
lattice_cli="${LATTICE_CLI:-$(command -v lattice 2>/dev/null || true)}"
|
|
170
|
-
run_list_json=""
|
|
171
|
-
run_list_rc=0
|
|
172
|
-
if [ -n "$lattice_cli" ] && [ -x "$lattice_cli" ]; then
|
|
173
|
-
run_list_json=$(cd "$proj" && "$lattice_cli" run list --json 2>&1) || run_list_rc=$?
|
|
174
|
-
fi
|
|
175
|
-
if [ -z "$lattice_cli" ] || [ ! -x "$lattice_cli" ]; then
|
|
176
|
-
miss "run landing — LATTICE_CLIが実行可能fileを指さず、着地状態を読めない: ${lattice_cli:-未設定}"
|
|
177
|
-
elif [ "$run_list_rc" != "0" ]; then
|
|
178
|
-
miss "run landing — run list が rc=${run_list_rc} で失敗: $(printf '%s' "$run_list_json" | head -c 200)"
|
|
179
|
-
elif ! run_refs=$(printf '%s' "$run_list_json" | python3 -c '
|
|
180
|
-
import json, sys
|
|
181
|
-
|
|
182
|
-
# schema と active_runs を要求する。**typed error JSON も別 schema も黙って空扱いにしない。**
|
|
183
|
-
# f-string の中でバックスラッシュ付きの引用符を使わない——shell の single-quoted `python3 -c`
|
|
184
|
-
# へ `\"` がそのまま届き、**Python が SyntaxError で落ちる**(kanade の監査で実測・room [957])。
|
|
185
|
-
# 値は先に変数へ取り出して、f-string には名前だけを置く。
|
|
186
|
-
raw = sys.stdin.read()
|
|
187
|
-
try:
|
|
188
|
-
listed = json.loads(raw)
|
|
189
|
-
except json.JSONDecodeError as error:
|
|
190
|
-
sys.exit(f"run list がJSONでない: {error}")
|
|
191
|
-
if not isinstance(listed, dict):
|
|
192
|
-
sys.exit(f"run list がobjectでない: {type(listed).__name__}")
|
|
193
|
-
actual_schema = listed.get("schema")
|
|
194
|
-
if actual_schema != "lattice.run_list.v1":
|
|
195
|
-
sys.exit(f"run list の schema が違う: {actual_schema}")
|
|
196
|
-
active = listed.get("active_runs")
|
|
197
|
-
if not isinstance(active, list):
|
|
198
|
-
sys.exit(f"run list に active_runs 配列が無い: {type(active).__name__}")
|
|
199
|
-
# **entry を filter で捨てない。** 1件でも読めない entry があれば、active な run を落として
|
|
200
|
-
# 「なし」に見せることになる(suzune の監査で実測・room [956])。全件を要求して、
|
|
201
|
-
# 満たさなければ非ゼロで落ちる。外部 versioned JSON の境界で fallback しない。
|
|
202
|
-
refs = []
|
|
203
|
-
for index, entry in enumerate(active):
|
|
204
|
-
if not isinstance(entry, dict):
|
|
205
|
-
sys.exit(f"active_runs[{index}] がobjectでない: {type(entry).__name__}")
|
|
206
|
-
ref = entry.get("run_ref")
|
|
207
|
-
if not isinstance(ref, str) or not ref:
|
|
208
|
-
sys.exit(f"active_runs[{index}] に run_ref 文字列が無い")
|
|
209
|
-
refs.append(ref)
|
|
210
|
-
print("\n".join(sorted(refs)))
|
|
211
|
-
' 2>&1); then
|
|
212
|
-
miss "run landing — run listを解釈できない: $(printf '%s' "$run_refs" | head -c 200)"
|
|
213
|
-
elif [ -z "$run_refs" ]; then
|
|
214
|
-
skip "run landing(active runなし)"
|
|
215
|
-
else
|
|
216
|
-
while IFS= read -r run_ref; do
|
|
217
|
-
run_ref=$(normalize_read_line "$run_ref")
|
|
218
|
-
[ -n "$run_ref" ] || continue
|
|
219
|
-
if landing_report=$(cd "$proj" && "$lattice_cli" run landing --run "$run_ref" 2>&1); then
|
|
220
|
-
unlanded_count=""
|
|
221
|
-
if unlanded_count=$(printf '%s' "$landing_report" | python3 -c '
|
|
222
|
-
import json, sys
|
|
223
|
-
|
|
224
|
-
raw = sys.stdin.read()
|
|
225
|
-
try:
|
|
226
|
-
report = json.loads(raw)
|
|
227
|
-
except json.JSONDecodeError as error:
|
|
228
|
-
sys.exit(f"run landing がJSONでない: {error}")
|
|
229
|
-
if not isinstance(report, dict):
|
|
230
|
-
sys.exit(f"run landing がobjectでない: {type(report).__name__}")
|
|
231
|
-
actual_schema = report.get("schema")
|
|
232
|
-
if actual_schema != "lattice.run_landing_report.v1":
|
|
233
|
-
sys.exit(f"run landing の schema が違う: {actual_schema}")
|
|
234
|
-
receipts = report.get("accepted_receipts")
|
|
235
|
-
if not isinstance(receipts, list):
|
|
236
|
-
sys.exit("run landing に accepted_receipts 配列が無い")
|
|
237
|
-
for index, receipt in enumerate(receipts):
|
|
238
|
-
if not isinstance(receipt, dict) or not isinstance(receipt.get("landed"), bool):
|
|
239
|
-
sys.exit(f"accepted_receipts[{index}] の landed が真偽値でない")
|
|
240
|
-
print(sum(1 for receipt in receipts if not receipt["landed"]))
|
|
241
|
-
' 2>&1); then
|
|
242
|
-
did "run landing ${landing_report}"
|
|
243
|
-
if [ "$unlanded_count" != 0 ]; then
|
|
244
|
-
echo "未着地 ${unlanded_count}本: run ${run_ref} の受理済み成果が canonical default branch へ着地していない" >&2
|
|
245
|
-
fi
|
|
246
|
-
else
|
|
247
|
-
miss "run landing $run_ref — reportを解釈できない: ${unlanded_count}"
|
|
248
|
-
fi
|
|
249
|
-
else
|
|
250
|
-
miss "run landing $run_ref — ${landing_report}"
|
|
251
|
-
fi
|
|
252
|
-
done <<EOF
|
|
253
|
-
$run_refs
|
|
254
|
-
EOF
|
|
255
|
-
fi
|
|
256
|
-
|
|
257
|
-
# setupが新しく作ったhost固有runtimeだけを撤去する。既存runtimeは他adapterや進行中runの
|
|
258
|
-
# 所有物を含み得るので触らない。runtimeを先に消してからexcludeを戻し、teardown後に
|
|
259
|
-
# untracked stateが露出する順序逆転を防ぐ。
|
|
260
|
-
# **配車を撤去した後の setup は `.lattice/runtime/` を作らない**ので、新しい卓ではここは常に
|
|
261
|
-
# skip になる。判定を残してあるのは、**配車時代に立てた卓を畳む時**にだけ効くからである
|
|
262
|
-
# (その卓の setup-state は `work_order_adapter: true` を持つ)。
|
|
263
|
-
if yes_ "$work_order_adapter" && ! yes_ "$runtime_pre"; then
|
|
264
|
-
rm -rf "$proj/.lattice/runtime"
|
|
265
|
-
did ".lattice/runtime/ 撤去(配車時代の setup が新規作成したhost固有state)"
|
|
266
|
-
elif yes_ "$work_order_adapter"; then
|
|
267
|
-
skip ".lattice/runtime/(setup 以前から存在)"
|
|
268
|
-
else
|
|
269
|
-
skip ".lattice/runtime/(setup は runtime state を作っていない)"
|
|
270
|
-
fi
|
|
271
|
-
|
|
272
|
-
# 外部ペイン(決定53)。`.team/` を消す前に戻す——退避先が `.team/` の中にある
|
|
273
|
-
ext=$(python3 -c "import json;print(json.load(open('$state')).get('external_pane', False))")
|
|
274
|
-
pj_pre=$(python3 -c "import json;print(json.load(open('$state')).get('project_json_preexisting', False))")
|
|
275
|
-
|
|
276
|
-
# ---- 解散(archive): **部屋は残し、メンバー登録だけ外す** ----
|
|
277
|
-
# 円卓の解散は「部屋を畳む」ではなく「集まりが散る」。部屋は場所であって、次の campaign も
|
|
278
|
-
# 同じ部屋で続く——**過去ログが同じ部屋の履歴として繋がり、部屋は常に一つに見える**
|
|
279
|
-
# (オーナー裁定 2026-08-09・決定61)。参加者一覧だけ空にして、席が戻れば再登録される
|
|
280
|
-
if [ "$mode" = archive ]; then
|
|
281
|
-
if [ -z "${PEERTABLE_POST_TOKEN:-}" ]; then
|
|
282
|
-
miss "メンバー登録の解除 — TOKEN_MISSING: \`~/.config/peertable.env\` の定義が \`export\` 付きでないと子 process へ渡らない"
|
|
283
|
-
echo "teardown: [手当] 参加者は次で外せる: curl -X DELETE \"$url/api/$room/members/<名前>\" -H \"X-Peertable-Token: \$PEERTABLE_POST_TOKEN\"" >&2
|
|
284
|
-
elif [ -z "$seat_names" ]; then
|
|
285
|
-
skip "メンバー登録の解除(一覧が取れていない)"
|
|
286
|
-
else
|
|
287
|
-
# 履歴に解散の区切りを残す。**部屋が続く以上、どこで卓が変わったかが読めないと
|
|
288
|
-
# 過去ログが一続きの会話に見えてしまう**(次の campaign の発言と地続きになる)
|
|
289
|
-
body="解散。この卓はここまで。参加者: ${seat_names}。部屋と過去ログはこのまま残り、次の卓も同じ部屋で続く。"
|
|
290
|
-
python3 -c "
|
|
291
|
-
import json,sys,urllib.parse,urllib.request
|
|
292
|
-
room_path=urllib.parse.quote('$room', safe='')
|
|
293
|
-
req=urllib.request.Request('$url/api/' + room_path + '/messages', method='POST',
|
|
294
|
-
data=json.dumps({'from':'system','to':'system','body':'''$body'''}).encode(),
|
|
295
|
-
headers={'Content-Type':'application/json','X-Peertable-Token':'$PEERTABLE_POST_TOKEN'})
|
|
296
|
-
urllib.request.urlopen(req, timeout=10).read()
|
|
297
|
-
" 2>/dev/null && did "解散の区切りを履歴へ" || skip "解散の区切り(投稿できず・撤去は続行)"
|
|
298
|
-
n=0
|
|
299
|
-
while IFS= read -r member; do
|
|
300
|
-
[ -n "$member" ] || continue
|
|
301
|
-
name=$(python3 -c 'import json,sys,urllib.parse; print(urllib.parse.quote(json.loads(sys.argv[1])["name"], safe=""))' "$member")
|
|
302
|
-
c=$(curl -s -o /dev/null -w '%{http_code}' -X DELETE "$url/api/$room/members/$name" -H "X-Peertable-Token: $PEERTABLE_POST_TOKEN" || true)
|
|
303
|
-
[ "$c" = 200 ] && n=$((n + 1))
|
|
304
|
-
done <<<"$member_lines"
|
|
305
|
-
did "メンバー登録の解除(${n}名)— **部屋と過去ログは残す**(${url}/${room})"
|
|
306
|
-
fi
|
|
307
|
-
if [ -n "${PEERTABLE_POST_TOKEN:-}" ]; then
|
|
308
|
-
bridge_code=$(curl -s -o /dev/null -w '%{http_code}' -X DELETE "$url/api/$room/bridges" -H "X-Peertable-Token: $PEERTABLE_POST_TOKEN" || true)
|
|
309
|
-
if [ "$bridge_code" = 200 ]; then
|
|
310
|
-
did "停止済みbridge台帳の解除(archive roomを未登録表示へ戻す)"
|
|
311
|
-
else
|
|
312
|
-
miss "停止済みbridge台帳の解除に失敗(HTTP ${bridge_code:-000})"
|
|
313
|
-
fi
|
|
314
|
-
# 解散した room を公開一覧(/api/rooms・トップページ主一覧)から外す。外さないと公開面が
|
|
315
|
-
# 過去卓で埋まり、公開トップの live 窓が死卓を選び続ける(2026-08-30 実測)。
|
|
316
|
-
# 次の setup の member 登録が自動で解除するので、復活に手作業は要らない
|
|
317
|
-
arch_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$url/api/$room/archive" -H "X-Peertable-Token: $PEERTABLE_POST_TOKEN" || true)
|
|
318
|
-
if [ "$arch_code" = 200 ]; then
|
|
319
|
-
did "room を公開一覧から archive(個別ページとログは残る)"
|
|
320
|
-
elif [ "$arch_code" = 404 ]; then
|
|
321
|
-
skip "room の archive(server が archive を持たない版・公開一覧に残る)"
|
|
322
|
-
else
|
|
323
|
-
miss "room の archive に失敗(HTTP ${arch_code:-000})"
|
|
324
|
-
fi
|
|
325
|
-
fi
|
|
326
|
-
# room 削除は --purge だけ。トークンを要する唯一の段で、ここだけが外部サービスへの依存境界
|
|
327
|
-
elif [ "$log_saved" = no ]; then
|
|
328
|
-
miss "room 削除 $room — ログを保全できていないので消さない(保全より先に消すと会話は二度と戻らない)"
|
|
329
|
-
elif [ -z "${PEERTABLE_POST_TOKEN:-}" ]; then
|
|
330
|
-
miss "room 削除 $room — TOKEN_MISSING: PEERTABLE_POST_TOKEN が空。\`~/.config/peertable.env\` の定義が \`export\` 付きでないと子 process へ渡らない"
|
|
331
|
-
else
|
|
332
|
-
code=$(curl -s -o /dev/null -w '%{http_code}' -X DELETE "$url/api/$room" -H "X-Peertable-Token: $PEERTABLE_POST_TOKEN" || true)
|
|
333
|
-
[ -n "$code" ] || code=000
|
|
334
|
-
if [ "$code" = 200 ]; then
|
|
335
|
-
did "room 削除 $room (HTTP 200)"
|
|
336
|
-
else
|
|
337
|
-
miss "room 削除 $room (HTTP $code) — 403/401 はトークン不一致、000 は server 不達"
|
|
338
|
-
fi
|
|
339
|
-
fi
|
|
340
|
-
# 未実施なら `.team/` と一緒に room 名も消えるので、後から手で消せる形を先に出す
|
|
341
|
-
[ "$fail" -eq 0 ] || echo "teardown: [手当] room は次で消せる: curl -X DELETE \"$url/api/$room\" -H \"X-Peertable-Token: \$PEERTABLE_POST_TOKEN\"" >&2
|
|
342
|
-
|
|
343
|
-
if yes_ "$ext"; then
|
|
344
|
-
if yes_ "$pj_pre"; then
|
|
345
|
-
cp "$proj/.team/project.json.bak" "$proj/.lattice/project.json"
|
|
346
|
-
did "外部ペイン復元(既存 project.json を書き戻し)"
|
|
347
|
-
else
|
|
348
|
-
rm -f "$proj/.lattice/project.json"
|
|
349
|
-
did "外部ペイン撤去(project.json 削除)"
|
|
350
|
-
fi
|
|
351
|
-
else
|
|
352
|
-
skip "外部ペイン(登録なし)"
|
|
353
|
-
fi
|
|
354
|
-
|
|
355
|
-
if node "$script_dir/ensure-codex-room-mcp.mjs" remove "$proj" "$peertable_repo"; then
|
|
356
|
-
did "Codex project設定からPeertable room MCPを撤去"
|
|
357
|
-
else
|
|
358
|
-
miss "Codex project設定のPeertable room MCPを撤去できない"
|
|
359
|
-
fi
|
|
360
|
-
|
|
361
|
-
rm -rf "$proj/.team"
|
|
362
|
-
did ".team/ 削除"
|
|
363
|
-
|
|
364
|
-
if yes_ "$added_mcp"; then
|
|
365
|
-
did ".mcp.json のPeertable room MCP撤去(${mcp_remove_action})"
|
|
366
|
-
else
|
|
367
|
-
skip ".mcp.json(setup が作っていない)"
|
|
368
|
-
fi
|
|
369
|
-
|
|
370
|
-
if yes_ "$added_mcp_ex"; then
|
|
371
|
-
grep -vx '/\.mcp\.json' "$proj/.git/info/exclude" > "$proj/.git/info/exclude.tmp" || true
|
|
372
|
-
mv "$proj/.git/info/exclude.tmp" "$proj/.git/info/exclude"
|
|
373
|
-
did "exclude から /.mcp.json を撤去"
|
|
374
|
-
else
|
|
375
|
-
skip "exclude の /.mcp.json(setup が足していない)"
|
|
376
|
-
fi
|
|
377
|
-
|
|
378
|
-
if yes_ "$added_runtime_ex"; then
|
|
379
|
-
grep -vx '/\.lattice/runtime/' "$proj/.git/info/exclude" > "$proj/.git/info/exclude.tmp" || true
|
|
380
|
-
mv "$proj/.git/info/exclude.tmp" "$proj/.git/info/exclude"
|
|
381
|
-
did "exclude から /.lattice/runtime/ を撤去"
|
|
382
|
-
else
|
|
383
|
-
skip "exclude の /.lattice/runtime/(setup が足していない)"
|
|
384
|
-
fi
|
|
385
|
-
|
|
386
|
-
if yes_ "$added"; then
|
|
387
|
-
grep -vx '\.team/' "$proj/.git/info/exclude" > "$proj/.git/info/exclude.tmp" || true
|
|
388
|
-
mv "$proj/.git/info/exclude.tmp" "$proj/.git/info/exclude"
|
|
389
|
-
did "exclude から .team/ を撤去"
|
|
390
|
-
else
|
|
391
|
-
skip "exclude の .team/(setup が足していない)"
|
|
392
|
-
fi
|
|
393
|
-
|
|
394
|
-
if [ "$lat_pre" = "True" ] || [ "$lat_pre" = "true" ]; then
|
|
395
|
-
skip ".lattice/(setup 以前から存在)"
|
|
396
|
-
elif [ "$mode" = archive ]; then
|
|
397
|
-
# 工程正本を残すのが archive の本体。**残すだけでは git が知らない**ので、追跡へ入れるのは人の判断
|
|
398
|
-
did ".lattice/ を残す(工程正本・\`lattice todo status\` と \`gantt serve\` が読む)"
|
|
399
|
-
echo "teardown: [注記] .lattice/ は git 追跡外のままなら次の clone に残らない。残すなら commit すること"
|
|
400
|
-
else
|
|
401
|
-
rm -rf "$proj/.lattice"
|
|
402
|
-
did ".lattice/ 削除(setup が作ったもの・--purge)"
|
|
403
|
-
fi
|
|
404
|
-
|
|
405
|
-
if [ "$fail" -ne 0 ]; then
|
|
406
|
-
# 「再実行すればいい」と書かないこと。`.team/` は上で消えているので、2回目は setup-state.json が
|
|
407
|
-
# 読めずに落ちる=**再実行の経路は存在しない**。残っている道は上の [手当] の curl だけ(2026-08-08 実測)
|
|
408
|
-
echo "teardown: 未完了 — 撤去は上のとおり済んでいる。残りは上の [手当] を手で叩くこと(.team/ は削除済みなので teardown.sh の再実行はできない)" >&2
|
|
409
|
-
exit 1
|
|
410
|
-
fi
|
|
411
|
-
echo "teardown done: $proj"
|
|
2
|
+
# 既存の引数を共通の製品入口へ渡す。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
exec node "$(dirname "$0")/legacy-entry.mjs" teardown "$@"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { teardownProject, archiveLog } from './teardown.mjs'
|
|
7
|
+
|
|
8
|
+
function fixture(t, { stopFails = false, roomFails = false } = {}) {
|
|
9
|
+
const project = mkdtempSync(join(tmpdir(), 'peertable-teardown-'))
|
|
10
|
+
t.after(() => rmSync(project, { recursive: true, force: true }))
|
|
11
|
+
mkdirSync(join(project, '.team'))
|
|
12
|
+
writeFileSync(join(project, '.team', 'setup-state.json'), JSON.stringify({ room: 'r', server_url: 'http://example', mode: 'standalone', lattice_preexisting: true }))
|
|
13
|
+
writeFileSync(join(project, '.team', 'alarm-bridge.json'), JSON.stringify({ pid: 99, aiterm_session_id: 'alarm' }))
|
|
14
|
+
writeFileSync(join(project, '.mcp.json'), '{"mcpServers":{"other":{"command":"other"}}}')
|
|
15
|
+
const events = []
|
|
16
|
+
let sessions = [{ session_id: 'alarm' }, { session_id: 'other-project' }]
|
|
17
|
+
return { project, events, dependencies: {
|
|
18
|
+
api: { members: async () => [], request: async (path, options = {}) => {
|
|
19
|
+
events.push(`${options.method || 'GET'} ${path}`)
|
|
20
|
+
if (roomFails && path === 'archive') throw new Error('room通信失敗')
|
|
21
|
+
return { messages: [] }
|
|
22
|
+
} },
|
|
23
|
+
aiterm: { sessions: async () => sessions, call: async (_name, args) => {
|
|
24
|
+
events.push(`close ${args.session_id}`)
|
|
25
|
+
sessions = sessions.filter(item => item.session_id !== args.session_id)
|
|
26
|
+
} },
|
|
27
|
+
runScript: (name, args) => {
|
|
28
|
+
events.push(`${name} ${args.at(-1)}`)
|
|
29
|
+
if (name === 'alarm-bridge.mjs' && stopFails) throw new Error('bridge停止失敗')
|
|
30
|
+
return ''
|
|
31
|
+
},
|
|
32
|
+
}, sessions: () => sessions }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('alarm停止と公開APIの閉鎖を終えてから足場を消し、他projectと設定を保つ', async t => {
|
|
36
|
+
const f = fixture(t)
|
|
37
|
+
const result = await teardownProject({ project: f.project }, f.dependencies)
|
|
38
|
+
assert.equal(result.status, 'done')
|
|
39
|
+
assert.ok(f.events.indexOf('alarm-bridge.mjs --stop') < f.events.indexOf('POST archive'))
|
|
40
|
+
assert.equal(existsSync(join(f.project, '.team')), false)
|
|
41
|
+
assert.equal(readFileSync(join(f.project, '.mcp.json'), 'utf8'), '{"mcpServers":{"other":{"command":"other"}}}')
|
|
42
|
+
assert.deepEqual(f.sessions(), [{ session_id: 'other-project' }])
|
|
43
|
+
})
|
|
44
|
+
for (const failure of ['stopFails', 'roomFails']) test(`${failure}では再実行用の記録を保持する`, async t => {
|
|
45
|
+
const f = fixture(t, { [failure]: true })
|
|
46
|
+
await assert.rejects(teardownProject({ project: f.project }, f.dependencies))
|
|
47
|
+
assert.equal(existsSync(join(f.project, '.team', 'setup-state.json')), true)
|
|
48
|
+
})
|
|
49
|
+
test('ログの控えは原本がroomに残ると明示し、日本語本文を保持する', async t => {
|
|
50
|
+
const f = fixture(t)
|
|
51
|
+
const file = await archiveLog({ request: async () => ({ messages: [{ seq: 1, from: 'alice', to: 'all', ts: 'now', body: '議論を保存' }] }) }, f.project, 'r')
|
|
52
|
+
const log = readFileSync(file, 'utf8')
|
|
53
|
+
assert.match(log, /原本はサーバーに残ります/)
|
|
54
|
+
assert.match(log, /議論を保存/)
|
|
55
|
+
})
|
|
56
|
+
test('ログ保存先のI/O失敗でも再実行でき、roomを先に畳まない', async t => {
|
|
57
|
+
const f = fixture(t)
|
|
58
|
+
writeFileSync(join(f.project, 'docs'), '既存file')
|
|
59
|
+
await assert.rejects(teardownProject({ project: f.project }, f.dependencies), { code: 'PEERTABLE_TEARDOWN_INCOMPLETE' })
|
|
60
|
+
assert.equal(existsSync(join(f.project, '.team', 'setup-state.json')), true)
|
|
61
|
+
assert.equal(f.events.includes('POST archive'), false)
|
|
62
|
+
})
|