peertable 0.3.9 → 0.3.10
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 +6 -4
- package/README.md +6 -4
- package/package.json +1 -1
- package/room/client.mjs +36 -56
- package/room/server.mjs +77 -16
- package/skill/SKILL.md +73 -35
- package/skill/scripts/aiterm-configure.mjs +31 -0
- package/skill/scripts/aiterm-launch.mjs +29 -0
- package/skill/scripts/archive-room-log.py +3 -1
- package/skill/scripts/change-effort.sh +5 -129
- package/skill/scripts/change-seat.sh +253 -0
- package/skill/scripts/codex-parent-watch.sh +8 -0
- package/skill/scripts/ensure-bridge.sh +6 -2
- package/skill/scripts/ensure-codex-room-mcp.mjs +152 -0
- package/skill/scripts/ensure-room-mcp.mjs +52 -0
- package/skill/scripts/launch-seat.sh +392 -94
- package/skill/scripts/leave-seat.sh +90 -0
- package/skill/scripts/parent-join.sh +46 -6
- package/skill/scripts/parent-watch.mjs +286 -0
- package/skill/scripts/seat-credential.mjs +118 -0
- package/skill/scripts/seat-status-bridge.mjs +10 -3
- package/skill/scripts/seat-usage.mjs +44 -3
- package/skill/scripts/setup.sh +30 -3
- package/skill/scripts/teardown.sh +12 -3
- package/skill/scripts/todo-extraction-from-plan.mjs +199 -0
- package/skill/scripts/upgrade-team-assets.sh +224 -0
- package/skill/scripts/wakeup-bridge.mjs +245 -58
- package/skill/templates/charter.md +12 -6
- package/skill/templates/done.sh +379 -36
- package/skill/templates/mcp.json +1 -1
- package/skill/templates/member-standalone.md +24 -14
- package/skill/templates/member.md +31 -33
- package/skill/templates/parent.md +128 -0
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# 工程の記録は、卓そのものより寿命が長い**。ゲスト project を汚さない不可侵原則は `--purge` が持つ。
|
|
14
14
|
set -e
|
|
15
15
|
proj="$1"
|
|
16
|
+
script_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
17
|
+
peertable_repo=$(cd "$script_dir/../.." && pwd -P)
|
|
16
18
|
mode=archive
|
|
17
19
|
for arg in "${@:2}"; do
|
|
18
20
|
case "$arg" in
|
|
@@ -255,8 +257,9 @@ if [ "$mode" = archive ]; then
|
|
|
255
257
|
# 過去ログが一続きの会話に見えてしまう**(次の campaign の発言と地続きになる)
|
|
256
258
|
body="解散。この卓はここまで。参加者: ${seat_names}。部屋と過去ログはこのまま残り、次の卓も同じ部屋で続く。"
|
|
257
259
|
python3 -c "
|
|
258
|
-
import json,sys,urllib.request
|
|
259
|
-
|
|
260
|
+
import json,sys,urllib.parse,urllib.request
|
|
261
|
+
room_path=urllib.parse.quote('$room', safe='')
|
|
262
|
+
req=urllib.request.Request('$url/api/' + room_path + '/messages', method='POST',
|
|
260
263
|
data=json.dumps({'from':'system','to':'system','body':'''$body'''}).encode(),
|
|
261
264
|
headers={'Content-Type':'application/json','X-Peertable-Token':'$PEERTABLE_POST_TOKEN'})
|
|
262
265
|
urllib.request.urlopen(req, timeout=10).read()
|
|
@@ -268,7 +271,7 @@ urllib.request.urlopen(req, timeout=10).read()
|
|
|
268
271
|
c=$(curl -s -o /dev/null -w '%{http_code}' -X DELETE "$url/api/$room/members/$name" -H "X-Peertable-Token: $PEERTABLE_POST_TOKEN" || true)
|
|
269
272
|
[ "$c" = 200 ] && n=$((n + 1))
|
|
270
273
|
done <<<"$member_lines"
|
|
271
|
-
did "メンバー登録の解除(${n}名)— **部屋と過去ログは残す**($url/$room)"
|
|
274
|
+
did "メンバー登録の解除(${n}名)— **部屋と過去ログは残す**(${url}/${room})"
|
|
272
275
|
fi
|
|
273
276
|
# room 削除は --purge だけ。トークンを要する唯一の段で、ここだけが外部サービスへの依存境界
|
|
274
277
|
elif [ "$log_saved" = no ]; then
|
|
@@ -299,6 +302,12 @@ else
|
|
|
299
302
|
skip "外部ペイン(登録なし)"
|
|
300
303
|
fi
|
|
301
304
|
|
|
305
|
+
if node "$script_dir/ensure-codex-room-mcp.mjs" remove "$proj" "$peertable_repo"; then
|
|
306
|
+
did "Codex project設定からPeertable room MCPを撤去"
|
|
307
|
+
else
|
|
308
|
+
miss "Codex project設定のPeertable room MCPを撤去できない"
|
|
309
|
+
fi
|
|
310
|
+
|
|
302
311
|
rm -rf "$proj/.team"
|
|
303
312
|
did ".team/ 削除"
|
|
304
313
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 稼働中 campaign へ独立欠陥 ToDo を軽量に追加するための正規入口。
|
|
3
|
+
//
|
|
4
|
+
// 背景: main plan の migrate 時に narrative_ref へ #L 行番号を持たせなかったため、
|
|
5
|
+
// `lattice todo split` が `predecessor_source_inventory_unavailable` で機構的に失敗した
|
|
6
|
+
// (実測 2026-08-11。docs/plan_peertable-autonomy-runtime-fx-20260811.md f4)。`todo revise` は
|
|
7
|
+
// 使えるが desired_plan 全体・source_cutover_batch を要求する重量級 API で、発見者が親裁定なしに
|
|
8
|
+
// 選べる手段ではない。このツールは「計画 Markdown に `### <task_id> <title>` 見出しで task を書く
|
|
9
|
+
// →本ツールで extraction.json を自動生成→`lattice todo migrate` で新規 companion plan として起票」
|
|
10
|
+
// という軽量な代替経路を提供する。既存 plan への task 追加は、新規 companion plan を作り
|
|
11
|
+
// `lattice todo dependency connect` で前提へ接続する(このツールは新規 plan の migrate 入力生成
|
|
12
|
+
// だけを担う。稼働中 plan への revise は対象外)。
|
|
13
|
+
//
|
|
14
|
+
// usage:
|
|
15
|
+
// node todo-extraction-from-plan.mjs <plan.md> <plan_key> --project <project_id> --agent <name>
|
|
16
|
+
// [--session <name>] [--host <name>] [--lane <lane>] [--out <path>]
|
|
17
|
+
//
|
|
18
|
+
// 計画 Markdown の規約: `# <root見出し>` → `## <section見出し>` → `### <task_id> <title>` の3階層。
|
|
19
|
+
// 各 `###` 見出しの本文(次の `#`/`##`/`###` 見出しまで)が design_memo になる。
|
|
20
|
+
import { accessSync, constants, existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
21
|
+
import { execFileSync } from 'node:child_process'
|
|
22
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
23
|
+
|
|
24
|
+
function usage(message) {
|
|
25
|
+
if (message) console.error(`ERROR: ${message}`)
|
|
26
|
+
console.error('usage: todo-extraction-from-plan.mjs <plan.md> <plan_key> --project <id> --agent <name> [--session <name>] [--host <name>] [--lane <lane>] [--out <path>]')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fail(code, message, nextAction) {
|
|
31
|
+
console.error(`${code}: ${message}`)
|
|
32
|
+
if (nextAction) console.error(`next: ${nextAction}`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2)
|
|
37
|
+
if (args.length < 2 || args[0].startsWith('--')) usage('plan.md と plan_key は最初の2つの位置引数')
|
|
38
|
+
const [planPath, planKey, ...rest] = args
|
|
39
|
+
const opts = { project: null, agent: null, session: null, host: 'mac', lane: 'defect', out: null }
|
|
40
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
41
|
+
const key = rest[i]?.replace(/^--/, '')
|
|
42
|
+
if (!(key in opts)) usage(`unknown flag: ${rest[i]}`)
|
|
43
|
+
opts[key] = rest[i + 1]
|
|
44
|
+
}
|
|
45
|
+
if (!opts.project) usage('--project は必須')
|
|
46
|
+
if (!opts.agent) usage('--agent は必須')
|
|
47
|
+
if (!opts.session) opts.session = opts.agent
|
|
48
|
+
|
|
49
|
+
let absolutePlanPath
|
|
50
|
+
try {
|
|
51
|
+
absolutePlanPath = realpathSync(resolve(planPath))
|
|
52
|
+
} catch {
|
|
53
|
+
fail('PLAN_NOT_FOUND', `${planPath} を実在fileとして解決できない`, '実在する計画Markdownを指定する')
|
|
54
|
+
}
|
|
55
|
+
let repoRoot
|
|
56
|
+
try {
|
|
57
|
+
repoRoot = realpathSync(execFileSync('git', ['-C', dirname(absolutePlanPath), 'rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim())
|
|
58
|
+
} catch {
|
|
59
|
+
fail('PLAN_REPO_UNRESOLVED', `${planPath} の所属git repoを解決できない`, 'git管理下の計画Markdownを指定する')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const setupStatePath = join(repoRoot, '.team', 'setup-state.json')
|
|
63
|
+
let latticeCli = process.env.LATTICE_CLI?.trim() ?? ''
|
|
64
|
+
let latticeCliSource = 'LATTICE_CLI'
|
|
65
|
+
if (!latticeCli) {
|
|
66
|
+
latticeCliSource = setupStatePath
|
|
67
|
+
if (!existsSync(setupStatePath)) {
|
|
68
|
+
fail(
|
|
69
|
+
'LATTICE_CLI_UNRESOLVED',
|
|
70
|
+
`LATTICE_CLIが未指定で ${setupStatePath} も存在しない`,
|
|
71
|
+
'Peertable setupの正規手順で .team/setup-state.json を生成するか、LATTICE_CLI=<実行可能file> を明示する',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
let setupState
|
|
75
|
+
try {
|
|
76
|
+
setupState = JSON.parse(readFileSync(setupStatePath, 'utf8'))
|
|
77
|
+
} catch {
|
|
78
|
+
fail(
|
|
79
|
+
'PEERTABLE_SETUP_STATE_INVALID',
|
|
80
|
+
`${setupStatePath} をJSONとして読めない`,
|
|
81
|
+
'Peertable setupの正規手順で setup-state.json を再生成する',
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
latticeCli = typeof setupState.lattice_cli === 'string' ? setupState.lattice_cli.trim() : ''
|
|
85
|
+
if (!latticeCli) {
|
|
86
|
+
fail(
|
|
87
|
+
'LATTICE_CLI_UNRESOLVED',
|
|
88
|
+
`${setupStatePath} に lattice_cli が無い`,
|
|
89
|
+
'Peertable setupの正規手順で lattice_cli を記録するか、LATTICE_CLI=<実行可能file> を明示する',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
latticeCli = realpathSync(latticeCli)
|
|
95
|
+
accessSync(latticeCli, constants.X_OK)
|
|
96
|
+
} catch {
|
|
97
|
+
fail(
|
|
98
|
+
'LATTICE_CLI_INVALID',
|
|
99
|
+
`${latticeCliSource} の lattice CLI が実行可能fileではない: ${latticeCli}`,
|
|
100
|
+
latticeCliSource === 'LATTICE_CLI'
|
|
101
|
+
? 'LATTICE_CLI=<実行可能な lattice CLI> を指定する'
|
|
102
|
+
: 'Peertable setupの正規手順で setup-state.json の lattice_cli を再生成する',
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
const latticePkgSrc = join(dirname(dirname(latticeCli)), 'src', 'todo-contracts.mjs')
|
|
106
|
+
const { todoSelfDigest } = await import(latticePkgSrc)
|
|
107
|
+
|
|
108
|
+
const planPathFromRoot = relative(repoRoot, absolutePlanPath)
|
|
109
|
+
if (isAbsolute(planPathFromRoot) || planPathFromRoot.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || planPathFromRoot === '..') {
|
|
110
|
+
fail('PLAN_OUTSIDE_REPO', `${planPath} は ${repoRoot} の外にある`, 'repo内の計画Markdownを指定する')
|
|
111
|
+
}
|
|
112
|
+
const relPlanPath = execFileSync('git', ['-C', repoRoot, 'ls-files', '--full-name', planPathFromRoot], { encoding: 'utf8' }).trim()
|
|
113
|
+
if (!relPlanPath) usage(`${planPath} は git 管理下にない(先に commit すること)`)
|
|
114
|
+
const dirty = execFileSync('git', ['-C', repoRoot, 'status', '--porcelain', '--', relPlanPath], { encoding: 'utf8' }).trim()
|
|
115
|
+
if (dirty) usage(`${relPlanPath} に未 commit の変更がある。先に commit してから実行すること`)
|
|
116
|
+
const sourceCommit = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
|
|
117
|
+
|
|
118
|
+
const text = readFileSync(join(repoRoot, relPlanPath), 'utf8')
|
|
119
|
+
const lines = text.split('\n')
|
|
120
|
+
|
|
121
|
+
let rootHeading = null
|
|
122
|
+
let sectionHeading = null
|
|
123
|
+
const tasks = []
|
|
124
|
+
let current = null // { taskId, title, startLine, headingPath }
|
|
125
|
+
|
|
126
|
+
const flush = (endLineExclusive) => {
|
|
127
|
+
if (!current) return
|
|
128
|
+
const bodyLines = lines.slice(current.startLine, endLineExclusive)
|
|
129
|
+
while (bodyLines.length && bodyLines[0].trim() === '') bodyLines.shift()
|
|
130
|
+
while (bodyLines.length && bodyLines.at(-1).trim() === '') bodyLines.pop()
|
|
131
|
+
const designMemo = bodyLines.join('\n').trim()
|
|
132
|
+
if (!designMemo) usage(`task ${current.taskId} の design_memo が空`)
|
|
133
|
+
tasks.push({ ...current, designMemo })
|
|
134
|
+
current = null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
138
|
+
const line = lines[i]
|
|
139
|
+
const h1 = /^# (.+)$/.exec(line)
|
|
140
|
+
const h2 = /^## (.+)$/.exec(line)
|
|
141
|
+
const h3 = /^### (\S+) (.+)$/.exec(line)
|
|
142
|
+
if (h1) { flush(i); rootHeading = h1[1]; sectionHeading = null; continue }
|
|
143
|
+
if (h2) { flush(i); sectionHeading = h2[1]; continue }
|
|
144
|
+
if (h3) {
|
|
145
|
+
flush(i)
|
|
146
|
+
if (!rootHeading || !sectionHeading) usage(`行${i + 1}: task見出しの前に # と ## の見出しが要る`)
|
|
147
|
+
current = {
|
|
148
|
+
taskId: h3[1], title: h3[2], startLine: i + 1,
|
|
149
|
+
headingPath: [rootHeading, sectionHeading, `${h3[1]} ${h3[2]}`],
|
|
150
|
+
headingLine: i + 1,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
flush(lines.length)
|
|
155
|
+
if (tasks.length === 0) usage('`### <task_id> <title>` 見出しが1つも見つからない')
|
|
156
|
+
|
|
157
|
+
const migrationContext = {
|
|
158
|
+
carry_over_ref: null, condition: null, evidence_refs: [], external_canonical_ref: null,
|
|
159
|
+
h_required: false, notes: [],
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const extraction = {
|
|
163
|
+
actor: { agent: opts.agent, host: opts.host, session: opts.session },
|
|
164
|
+
hard_dependencies: [],
|
|
165
|
+
joins: [],
|
|
166
|
+
plan_key: planKey,
|
|
167
|
+
plan_version: 'v1',
|
|
168
|
+
project_id: opts.project,
|
|
169
|
+
recorded_at: new Date(Date.now() - 60_000).toISOString().replace(/\.\d+Z$/, (m) => m.length === 5 ? m : '.000Z'),
|
|
170
|
+
schema: 'lattice.todo_extraction.v3',
|
|
171
|
+
tasks: tasks.map(({ taskId, title, headingPath, headingLine, designMemo }) => ({
|
|
172
|
+
compile_binding: null,
|
|
173
|
+
completion: null,
|
|
174
|
+
design_memo: designMemo,
|
|
175
|
+
disposition: 'register_pending',
|
|
176
|
+
lane: opts.lane,
|
|
177
|
+
migration_context: migrationContext,
|
|
178
|
+
narrative_ref: relPlanPath,
|
|
179
|
+
source: {
|
|
180
|
+
checkbox_state: 'absent',
|
|
181
|
+
heading_path: headingPath,
|
|
182
|
+
markdown_depth: 3,
|
|
183
|
+
origin_line: headingLine,
|
|
184
|
+
origin_plan_ref: relPlanPath,
|
|
185
|
+
parent_task_id: null,
|
|
186
|
+
source_commit: sourceCommit,
|
|
187
|
+
},
|
|
188
|
+
start: null,
|
|
189
|
+
task_id: taskId,
|
|
190
|
+
title,
|
|
191
|
+
})),
|
|
192
|
+
extraction_digest: '0'.repeat(64),
|
|
193
|
+
}
|
|
194
|
+
extraction.extraction_digest = todoSelfDigest(extraction, 'extraction_digest')
|
|
195
|
+
|
|
196
|
+
const outPath = opts.out ?? join(repoRoot, `.lattice/extraction-${planKey}.json`)
|
|
197
|
+
writeFileSync(outPath, JSON.stringify(extraction, null, 2) + '\n')
|
|
198
|
+
console.log(`written: ${outPath} (${tasks.length} tasks: ${tasks.map((t) => t.taskId).join(', ')})`)
|
|
199
|
+
console.log(`next: lattice todo migrate --input ${outPath} --dry-run --json`)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 既存卓へ、Peertable が所有する generated asset だけを現行 template へ同期する。
|
|
3
|
+
# usage: upgrade-team-assets.sh <project_dir>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ "$#" -ne 1 ]; then
|
|
7
|
+
echo 'PEERTABLE_UPGRADE_USAGE: upgrade-team-assets.sh <project_dir>' >&2
|
|
8
|
+
exit 2
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
12
|
+
repo_dir=$(CDPATH= cd -- "$script_dir/../.." && pwd)
|
|
13
|
+
|
|
14
|
+
node --input-type=module - "$1" "$repo_dir" <<'NODE'
|
|
15
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
17
|
+
import { dirname, join, resolve } from 'node:path'
|
|
18
|
+
|
|
19
|
+
const projectArg = process.argv[2]
|
|
20
|
+
const repoDir = process.argv[3]
|
|
21
|
+
|
|
22
|
+
function fail(code, detail) {
|
|
23
|
+
console.error(JSON.stringify({
|
|
24
|
+
schema: 'peertable.generated_assets_upgrade_result.v1',
|
|
25
|
+
result: 'rejected',
|
|
26
|
+
code,
|
|
27
|
+
detail,
|
|
28
|
+
}))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lstatOrNull(path) {
|
|
33
|
+
try {
|
|
34
|
+
return lstatSync(path)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.code === 'ENOENT') return null
|
|
37
|
+
fail('PEERTABLE_UPGRADE_PATH_UNREADABLE', `${path}: ${error.message}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requireRegularFile(path, code, label) {
|
|
42
|
+
const stat = lstatOrNull(path)
|
|
43
|
+
if (!stat || stat.isSymbolicLink() || !stat.isFile()) {
|
|
44
|
+
fail(code, `${label}: ${path}`)
|
|
45
|
+
}
|
|
46
|
+
return stat
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateDirectory(path, required, label) {
|
|
50
|
+
const stat = lstatOrNull(path)
|
|
51
|
+
if (!stat) {
|
|
52
|
+
if (required) fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `${label} が無い: ${path}`)
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
56
|
+
fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `${label} がsymlinkまたはdirectoryでない: ${path}`)
|
|
57
|
+
}
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateTargetPath(project, relativePath) {
|
|
62
|
+
const parts = relativePath.split('/')
|
|
63
|
+
let current = project
|
|
64
|
+
for (const part of parts.slice(0, -1)) {
|
|
65
|
+
current = join(current, part)
|
|
66
|
+
validateDirectory(current, part === '.team', `管理asset親 ${part}`)
|
|
67
|
+
}
|
|
68
|
+
const target = join(project, relativePath)
|
|
69
|
+
const stat = lstatOrNull(target)
|
|
70
|
+
if (stat && (stat.isSymbolicLink() || !stat.isFile())) {
|
|
71
|
+
fail('PEERTABLE_GENERATED_ASSET_UNSAFE_PATH', `管理assetがsymlinkまたはregular fileでない: ${target}`)
|
|
72
|
+
}
|
|
73
|
+
return { target, stat }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function templateText(repo, relativePath) {
|
|
77
|
+
const path = join(repo, relativePath)
|
|
78
|
+
requireRegularFile(path, 'PEERTABLE_GENERATED_ASSET_TEMPLATE_INVALID', 'template')
|
|
79
|
+
try {
|
|
80
|
+
return readFileSync(path)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
fail('PEERTABLE_GENERATED_ASSET_TEMPLATE_INVALID', `${path}: ${error.message}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sha256(content) {
|
|
87
|
+
return createHash('sha256').update(content).digest('hex')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderMember(template, state) {
|
|
91
|
+
if (state.mode === 'standalone') return template
|
|
92
|
+
const phases = state.phases
|
|
93
|
+
const scope = phases.length === 0
|
|
94
|
+
? 'この卓の claim 範囲は plan 全体(phase 指定なしで立っている)。'
|
|
95
|
+
: `**この卓の claim 範囲は phase ${phases.join(' ')} の task だけ**。範囲外の phase の task は、ready に見えていても取らない——同じ plan へ別 campaign が相乗りしている時、範囲外を取ると他卓の工程を横取りする(越境が2回実測されたことへの対処)。範囲外に手を入れる必要が出たら room へ出して裁定を仰ぐ。`
|
|
96
|
+
return template
|
|
97
|
+
.toString('utf8')
|
|
98
|
+
.replaceAll('{{PLAN_KEY}}', state.plan_key)
|
|
99
|
+
.replaceAll('{{CLAIM_SCOPE}}', scope)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadState(project) {
|
|
103
|
+
const path = join(project, '.team', 'setup-state.json')
|
|
104
|
+
requireRegularFile(path, 'PEERTABLE_SETUP_STATE_INVALID', 'setup-state.json')
|
|
105
|
+
let state
|
|
106
|
+
try {
|
|
107
|
+
state = JSON.parse(readFileSync(path, 'utf8'))
|
|
108
|
+
} catch (error) {
|
|
109
|
+
fail('PEERTABLE_SETUP_STATE_INVALID', `${path}: JSONを読めない: ${error.message}`)
|
|
110
|
+
}
|
|
111
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
112
|
+
fail('PEERTABLE_SETUP_STATE_INVALID', 'setup-state.json はobjectでなければならない')
|
|
113
|
+
}
|
|
114
|
+
if (!['lattice', 'standalone'].includes(state.mode)) {
|
|
115
|
+
fail('PEERTABLE_SETUP_STATE_INVALID', `modeが不正: ${String(state.mode)}`)
|
|
116
|
+
}
|
|
117
|
+
if (typeof state.room !== 'string' || !state.room || typeof state.server_url !== 'string' || !state.server_url) {
|
|
118
|
+
fail('PEERTABLE_SETUP_STATE_INVALID', 'room/server_url が不正')
|
|
119
|
+
}
|
|
120
|
+
if (typeof state.plan_key !== 'string') {
|
|
121
|
+
fail('PEERTABLE_SETUP_STATE_INVALID', 'plan_key がstringでない')
|
|
122
|
+
}
|
|
123
|
+
if (state.mode === 'lattice' && !state.plan_key) {
|
|
124
|
+
fail('PEERTABLE_SETUP_STATE_INVALID', 'lattice mode の plan_key が空')
|
|
125
|
+
}
|
|
126
|
+
if (state.phases !== undefined && (!Array.isArray(state.phases) || state.phases.some(phase => typeof phase !== 'string' || !phase))) {
|
|
127
|
+
fail('PEERTABLE_SETUP_STATE_INVALID', 'phases がstring配列でない')
|
|
128
|
+
}
|
|
129
|
+
return { ...state, phases: state.phases ?? [] }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeAtomically(target, content, mode) {
|
|
133
|
+
const temporary = join(dirname(target), `.${target.split('/').at(-1)}.upgrade-${process.pid}-${randomBytes(6).toString('hex')}.tmp`)
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(temporary, content, { mode })
|
|
136
|
+
chmodSync(temporary, mode)
|
|
137
|
+
renameSync(temporary, target)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
try { lstatSync(temporary); unlinkSync(temporary) } catch {}
|
|
140
|
+
fail('PEERTABLE_GENERATED_ASSET_WRITE_FAILED', `${target}: ${error.message}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function main() {
|
|
145
|
+
if (!projectArg || !repoDir) fail('PEERTABLE_UPGRADE_USAGE', 'project_dir が無い')
|
|
146
|
+
const projectStat = lstatOrNull(projectArg)
|
|
147
|
+
if (!projectStat || projectStat.isSymbolicLink() || !projectStat.isDirectory()) {
|
|
148
|
+
fail('PEERTABLE_UPGRADE_PROJECT_INVALID', `project directoryが不正: ${projectArg}`)
|
|
149
|
+
}
|
|
150
|
+
const project = resolve(projectArg)
|
|
151
|
+
const repo = resolve(repoDir)
|
|
152
|
+
validateDirectory(join(project, '.team'), true, '.team')
|
|
153
|
+
validateDirectory(join(repo, 'skill'), true, 'Peertable repo')
|
|
154
|
+
const state = loadState(project)
|
|
155
|
+
|
|
156
|
+
const common = [
|
|
157
|
+
['.team/CLAUDE.md', 'skill/templates/charter.md', 0o644],
|
|
158
|
+
['.team/roles/parent.md', 'skill/templates/parent.md', 0o644],
|
|
159
|
+
]
|
|
160
|
+
const modeSpecific = state.mode === 'lattice'
|
|
161
|
+
? [
|
|
162
|
+
['.team/roles/member.md', 'skill/templates/member.md', 0o644],
|
|
163
|
+
['.team/scripts/done.sh', 'skill/templates/done.sh', 0o755],
|
|
164
|
+
]
|
|
165
|
+
: [['.team/roles/member.md', 'skill/templates/member-standalone.md', 0o644]]
|
|
166
|
+
const definitions = [...common, ...modeSpecific]
|
|
167
|
+
|
|
168
|
+
const prepared = definitions.map(([relativePath, sourcePath, mode]) => {
|
|
169
|
+
const { target, stat } = validateTargetPath(project, relativePath)
|
|
170
|
+
const source = templateText(repo, sourcePath)
|
|
171
|
+
const content = relativePath === '.team/roles/member.md' && state.mode === 'lattice'
|
|
172
|
+
? Buffer.from(renderMember(source, state), 'utf8')
|
|
173
|
+
: source
|
|
174
|
+
return { relativePath, sourcePath, target, stat, content, mode }
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const changes = prepared.map(item => {
|
|
178
|
+
if (!item.stat) return { path: item.relativePath, action: 'created', sha256: sha256(item.content) }
|
|
179
|
+
const current = readFileSync(item.target)
|
|
180
|
+
const mode = item.stat.mode & 0o777
|
|
181
|
+
if (current.equals(item.content) && mode === item.mode) {
|
|
182
|
+
return { path: item.relativePath, action: 'unchanged', sha256: sha256(item.content) }
|
|
183
|
+
}
|
|
184
|
+
return { path: item.relativePath, action: current.equals(item.content) ? 'mode-updated' : 'updated', sha256: sha256(item.content) }
|
|
185
|
+
})
|
|
186
|
+
const obsolete = ['.team/scripts/start.sh', '.team/scripts/start-event.mjs']
|
|
187
|
+
.map(relativePath => ({ relativePath, ...validateTargetPath(project, relativePath) }))
|
|
188
|
+
|
|
189
|
+
// 全対象の安全性・template・差分を先に確定してから、管理allowlistだけへ書く。
|
|
190
|
+
for (const [index, item] of prepared.entries()) {
|
|
191
|
+
const change = changes[index]
|
|
192
|
+
if (change.action === 'unchanged') continue
|
|
193
|
+
const parent = dirname(item.target)
|
|
194
|
+
mkdirSync(parent, { recursive: true, mode: 0o755 })
|
|
195
|
+
if (change.action === 'mode-updated') chmodSync(item.target, item.mode)
|
|
196
|
+
else writeAtomically(item.target, item.content, item.mode)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const removed = []
|
|
200
|
+
for (const { relativePath, target, stat } of obsolete) {
|
|
201
|
+
if (!stat) continue
|
|
202
|
+
unlinkSync(target)
|
|
203
|
+
removed.push(relativePath)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(JSON.stringify({
|
|
207
|
+
schema: 'peertable.generated_assets_upgrade_result.v1',
|
|
208
|
+
result: 'ok',
|
|
209
|
+
project,
|
|
210
|
+
mode: state.mode,
|
|
211
|
+
managed: prepared.map(item => ({ path: item.relativePath, source: item.sourcePath, sha256: sha256(item.content) })),
|
|
212
|
+
changes,
|
|
213
|
+
removed,
|
|
214
|
+
changed_count: changes.filter(change => change.action !== 'unchanged').length + removed.length,
|
|
215
|
+
}))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
main()
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error?.code && typeof error.message === 'string') fail('PEERTABLE_GENERATED_ASSET_UPGRADE_FAILED', error.message)
|
|
222
|
+
fail('PEERTABLE_GENERATED_ASSET_UPGRADE_FAILED', String(error))
|
|
223
|
+
}
|
|
224
|
+
NODE
|