peertable 0.3.2 → 0.3.4
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 +3 -1
- package/README.md +2 -2
- package/package.json +2 -1
- package/room/client.mjs +24 -7
- package/room/server.mjs +23 -7
- package/skill/SKILL.md +71 -2
- package/skill/scripts/archive-room-log.py +59 -0
- package/skill/scripts/run-bridge.mjs +601 -0
- package/skill/scripts/setup.sh +96 -2
- package/skill/scripts/teardown.sh +127 -8
- package/skill/scripts/wakeup-bridge.mjs +3 -1
- package/skill/templates/charter.md +4 -4
- package/skill/templates/member.md +35 -3
package/README.ja.md
CHANGED
|
@@ -79,7 +79,9 @@ claude --mcp-config .team/mcp.json \
|
|
|
79
79
|
|
|
80
80
|
> 円卓を立てて
|
|
81
81
|
|
|
82
|
-
聞き取り・命名・`.team/` の scaffold(プロジェクト本体を汚さない)・Lattice plan 投入(単独モードなら読み取り専用の `.team/tasks.md` 生成)・メンバー起動・親の着卓まで一続き。
|
|
82
|
+
聞き取り・命名・`.team/` の scaffold(プロジェクト本体を汚さない)・Lattice plan 投入(単独モードなら読み取り専用の `.team/tasks.md` 生成)・メンバー起動・親の着卓まで一続き。
|
|
83
|
+
|
|
84
|
+
**teardown は既定で「解散」**——席を畳んでメンバー登録を外し、`.team/` と `.mcp.json` を撤去する。**部屋と過去ログは残る**(部屋は場所であり、次の卓も同じ部屋で続く。過去ログはその部屋の履歴として繋がる)。工程正本 `.lattice/` も残す。**痕跡ゼロに戻したいなら `--purge`**——部屋ごと削除してプロジェクトを diff ゼロへ返す(ゲストのプロジェクトで試した時はこちら)。
|
|
83
85
|
|
|
84
86
|
## 状態
|
|
85
87
|
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ Standalone gives up machine-guaranteed scheduling across tasks — nothing else.
|
|
|
76
76
|
|
|
77
77
|
```
|
|
78
78
|
room/ room server (zero-dependency Node) + per-session MCP channel client
|
|
79
|
-
skill/ "peertable" skill for Claude Code: setup / teardown of a full table
|
|
79
|
+
skill/ "peertable" skill for Claude Code: setup / disband (teardown) of a full table
|
|
80
80
|
docs/ plan.md — the living design document & decision log (Japanese)
|
|
81
81
|
experiments/ verification harnesses (V1 channels, V2 Lattice concurrency, V3 full loop)
|
|
82
82
|
```
|
|
@@ -111,7 +111,7 @@ The `room` entry in `.team/mcp.json` is just `{ "command": "peertable-client" }`
|
|
|
111
111
|
|
|
112
112
|
> 円卓を立てて / "set up a peertable for this project"
|
|
113
113
|
|
|
114
|
-
It interviews you, names the members, scaffolds `.team/` (charter + roles, isolated from your project, `.git/info/exclude`d), seeds the Lattice plan — or writes the read-only `.team/tasks.md` agenda if you chose standalone — launches the member sessions, and seats itself beside the table. `teardown`
|
|
114
|
+
It interviews you, names the members, scaffolds `.team/` (charter + roles, isolated from your project, `.git/info/exclude`d), seeds the Lattice plan — or writes the read-only `.team/tasks.md` agenda if you chose standalone — launches the member sessions, and seats itself beside the table. `teardown` disbands by default: it closes the seats, removes the member registrations, and clears `.team/` — **the room and its history stay** (a room is a place; the next table continues in the same room, so past logs read as that room's history), and the `.lattice/` plan store is kept. Pass `--purge` to delete the room too and restore your project to a zero diff.
|
|
115
115
|
|
|
116
116
|
## Status
|
|
117
117
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "A round table of peer agents. No orchestrator at the head. Turn Claude Code sessions into a team of equal, long-lived peers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"room/client.mjs",
|
|
36
36
|
"room/Dockerfile",
|
|
37
37
|
"skill/",
|
|
38
|
+
"!skill/**/__pycache__/**",
|
|
38
39
|
"README.ja.md"
|
|
39
40
|
],
|
|
40
41
|
"engines": {
|
package/room/client.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
10
10
|
|
|
11
11
|
// client.mjs 側のハードコード版数。package.json の version と一致していることを
|
|
12
12
|
// diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
|
|
13
|
-
const MCP_VERSION = '0.3.
|
|
13
|
+
const MCP_VERSION = '0.3.4'
|
|
14
14
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
15
15
|
|
|
16
16
|
const USAGE = `usage:
|
|
@@ -36,7 +36,10 @@ if (!URL_BASE || !ROOM || !ME) throw new Error('PEERTABLE_URL / PEERTABLE_ROOM /
|
|
|
36
36
|
|
|
37
37
|
const api = p => `${URL_BASE}/api/${ROOM}/${p}`
|
|
38
38
|
const headers = { 'Content-Type': 'application/json', ...(TOKEN ? { 'X-Peertable-Token': TOKEN } : {}) }
|
|
39
|
-
|
|
39
|
+
// 複数人宛は `to_names` を持つ(server が `to` を 'all' へ倒すので、旧 client でも取りこぼさない)。
|
|
40
|
+
// 新 client はここで実宛先だけを見るので、名指しされていない席は起きない。
|
|
41
|
+
const relevant = m => m.from !== ME
|
|
42
|
+
&& (Array.isArray(m.to_names) ? m.to_names.includes(ME) : (m.to === 'all' || m.to === ME))
|
|
40
43
|
|
|
41
44
|
let cursor = 0 // read_unread 用。参加時点から数える
|
|
42
45
|
|
|
@@ -47,7 +50,8 @@ const mcp = new Server(
|
|
|
47
50
|
instructions:
|
|
48
51
|
`あなたは Peertable room「${ROOM}」のメンバー「${ME}」である。` +
|
|
49
52
|
'<channel source="room"> の通知は「新着あり」の合図であり、本文は read_unread ツールで読む。' +
|
|
50
|
-
'発言は post ツール(to: "all" は全員宛、メンバー名で個別宛=DM
|
|
53
|
+
'発言は post ツール(to: "all" は全員宛、メンバー名で個別宛=DM、メンバー名の配列で複数人宛)。' +
|
|
54
|
+
'全員宛は1発言で全席1ターンを焼くので、用件が特定メンバーだけなら配列で名指しする。',
|
|
51
55
|
},
|
|
52
56
|
)
|
|
53
57
|
|
|
@@ -57,11 +61,18 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
57
61
|
tools: [
|
|
58
62
|
{
|
|
59
63
|
name: 'post',
|
|
60
|
-
description: 'room へ発言する。to は "all"
|
|
64
|
+
description: 'room へ発言する。to は "all"(全員宛)/ メンバー名(DM)/ メンバー名の配列(複数人宛)。'
|
|
65
|
+
+ '複数人に別々の用件があるとき、まとめて "all" にしない——名指しの配列にすれば、その人たちだけが起きる',
|
|
61
66
|
inputSchema: {
|
|
62
67
|
type: 'object',
|
|
63
68
|
properties: {
|
|
64
|
-
to: {
|
|
69
|
+
to: {
|
|
70
|
+
anyOf: [
|
|
71
|
+
{ type: 'string' },
|
|
72
|
+
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
73
|
+
],
|
|
74
|
+
description: '"all" / メンバー名 / メンバー名の配列。全員宛は決定・gate状態・全体記録だけに使う',
|
|
75
|
+
},
|
|
65
76
|
message: { type: 'string' },
|
|
66
77
|
},
|
|
67
78
|
required: ['to', 'message'],
|
|
@@ -73,7 +84,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
73
84
|
],
|
|
74
85
|
}))
|
|
75
86
|
|
|
76
|
-
const fmt = m => `[${m.seq}] ${m.from} → ${m.to} (${m.ts}): ${m.body}`
|
|
87
|
+
const fmt = m => `[${m.seq}] ${m.from} → ${Array.isArray(m.to_names) ? m.to_names.join(', ') : m.to} (${m.ts}): ${m.body}`
|
|
77
88
|
|
|
78
89
|
mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
79
90
|
const args = req.params.arguments ?? {}
|
|
@@ -140,7 +151,10 @@ async function subscribe() {
|
|
|
140
151
|
if (!relevant(m)) continue
|
|
141
152
|
await mcp.notification({
|
|
142
153
|
method: 'notifications/claude/channel',
|
|
143
|
-
params: {
|
|
154
|
+
params: {
|
|
155
|
+
content: `room に新着あり(${m.from} → ${Array.isArray(m.to_names) ? m.to_names.join(', ') : m.to})。read_unread で読むこと。`,
|
|
156
|
+
meta: { from: m.from, to: Array.isArray(m.to_names) ? m.to_names.join(',') : m.to, seq: String(m.seq) },
|
|
157
|
+
},
|
|
144
158
|
})
|
|
145
159
|
}
|
|
146
160
|
}
|
|
@@ -222,6 +236,9 @@ async function runDiagnostics(asJson) {
|
|
|
222
236
|
'scripts/make-plan-input.mjs',
|
|
223
237
|
'scripts/parent-join.sh',
|
|
224
238
|
'scripts/wakeup-bridge.mjs',
|
|
239
|
+
'scripts/seat-status-bridge.mjs',
|
|
240
|
+
// teardown の archive(=解散・既定)が呼ぶ。欠けるとログの写しが取れない
|
|
241
|
+
'scripts/archive-room-log.py',
|
|
225
242
|
'templates/gen-plan.mjs',
|
|
226
243
|
'templates/done.sh',
|
|
227
244
|
'templates/charter.md',
|
package/room/server.mjs
CHANGED
|
@@ -45,8 +45,21 @@ function readMessages(room, since = 0) {
|
|
|
45
45
|
.map(l => JSON.parse(l)).filter(m => m.seq > since)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// 宛先の正規化。複数人宛は `to_names` に持ち、`to` は 'all' へ倒す——旧 client は `to === 'all'` で
|
|
49
|
+
// 拾えるので、**取りこぼす側ではなく過剰に受け取る側へ倒れる**。宛先1人は従来どおり `to` だけで、
|
|
50
|
+
// 線の形が変わらない。
|
|
51
|
+
function normalizeAudience(to, toNames) {
|
|
52
|
+
const list = Array.isArray(to) ? to : Array.isArray(toNames) ? toNames : null
|
|
53
|
+
if (list === null) return { to: to ?? 'all', to_names: null }
|
|
54
|
+
if (!list.every(n => typeof n === 'string' && n.length > 0 && n !== 'all')) return { error: 'to_invalid' }
|
|
55
|
+
const names = [...new Set(list)]
|
|
56
|
+
if (names.length === 0) return { error: 'to_invalid' }
|
|
57
|
+
if (names.length === 1) return { to: names[0], to_names: null }
|
|
58
|
+
return { to: 'all', to_names: names }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function post(room, from, to, body, toNames = null) {
|
|
62
|
+
const msg = { seq: ++room.seq, ts: new Date().toISOString(), from, to, body, ...(toNames ? { to_names: toNames } : {}) }
|
|
50
63
|
appendFileSync(room.logPath, JSON.stringify(msg) + '\n')
|
|
51
64
|
const chunk = `data: ${JSON.stringify(msg)}\n\n`
|
|
52
65
|
for (const res of room.streams) res.write(chunk)
|
|
@@ -92,12 +105,14 @@ http.createServer(async (req, res) => {
|
|
|
92
105
|
return json(res, 403, { error: 'token_required' })
|
|
93
106
|
|
|
94
107
|
if (req.method === 'POST' && rest === 'messages') {
|
|
95
|
-
const { from, to, body: text } = JSON.parse(body)
|
|
108
|
+
const { from, to, to_names: toNames, body: text } = JSON.parse(body)
|
|
96
109
|
// 本文が無ければ **書かずに 400**。ここを素通しにすると `JSON.stringify` が欄ごと落として、
|
|
97
110
|
// append-only の正本へ**本文の無い行**が入る——しかも送信側には 200 と seq が返るので
|
|
98
111
|
// 「送れた」と表示される(2026-08-08 に本番で2件実測。消せない)
|
|
99
112
|
if (typeof text !== 'string') return json(res, 400, { error: 'body_required' })
|
|
100
|
-
|
|
113
|
+
const audience = normalizeAudience(to, toNames)
|
|
114
|
+
if (audience.error) return json(res, 400, { error: audience.error })
|
|
115
|
+
return json(res, 200, post(room, from, audience.to, text, audience.to_names))
|
|
101
116
|
}
|
|
102
117
|
if (req.method === 'POST' && rest === 'members') {
|
|
103
118
|
const { name, ...meta } = JSON.parse(body)
|
|
@@ -280,13 +295,14 @@ let last=null,recent=null
|
|
|
280
295
|
function render(m){
|
|
281
296
|
const at=new Date(m.ts)
|
|
282
297
|
if(m.from==='system'){const d=el('div','sys');d.appendChild(el('span','body',m.body));d.appendChild(stamp(at));logEl.appendChild(d);last=null;return}
|
|
283
|
-
const
|
|
284
|
-
const
|
|
298
|
+
const aud=m=>Array.isArray(m.to_names)?m.to_names.join(', '):m.to
|
|
299
|
+
const cont=last&&last.from===m.from&&aud(last)===aud(m)&&at-new Date(last.ts)<300000
|
|
300
|
+
const d=el('div','msg'+(aud(m)!=='all'?' dm':'')+(cont?' cont':''))
|
|
285
301
|
d.style.setProperty('--h',hue(m.from))
|
|
286
302
|
d.appendChild(el('div','av',initial(m.from)))
|
|
287
303
|
const body=el('div','body'),meta=el('div','meta')
|
|
288
304
|
meta.appendChild(el('span','who',m.from))
|
|
289
|
-
if(m
|
|
305
|
+
if(aud(m)!=='all')meta.appendChild(el('span','to','→ '+aud(m)))
|
|
290
306
|
meta.appendChild(stamp(at))
|
|
291
307
|
const bub=el('div','bubble');bub.appendChild(md(m.body))
|
|
292
308
|
body.appendChild(meta);body.appendChild(bub)
|
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: peertable
|
|
3
|
-
description: 任意プロジェクトに Peertable
|
|
3
|
+
description: 任意プロジェクトに Peertable チーム(対等メンバー並列型のマルチエージェント作業システム)を導入・解散する。setup でメンバーセッション群と room を立ち上げ、teardown で席と足場を撤去する(既定は解散——部屋と過去ログは残り、次の卓も同じ部屋で続く。痕跡ゼロにするなら --purge)。「チームで作業して」「円卓を立てて」「peertable setup / teardown」で使う。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Peertable — setup / teardown
|
|
@@ -57,10 +57,79 @@ description: 任意プロジェクトに Peertable チーム(対等メンバ
|
|
|
57
57
|
|
|
58
58
|
## teardown
|
|
59
59
|
|
|
60
|
-
`scripts/teardown.sh <project
|
|
60
|
+
`scripts/teardown.sh <project> [--purge]` が機械部分を**全部**行う(room 名・server URL・作成記録は `.team/setup-state.json` から読むので引数は project だけ。書込トークンは環境変数 `PEERTABLE_POST_TOKEN`)。**席の終了も本 script が行う**——AI が事前に `pty_close` して回る必要はない。
|
|
61
|
+
|
|
62
|
+
**既定は archive(=解散)、`--purge` が痕跡ゼロ**(決定61・オーナー裁定 2026-08-09)。**円卓の解散は「部屋を畳む」ではなく「集まりが散る」**——**部屋は場所であって、次の卓も同じ部屋で続く**。過去ログはその部屋の履歴としてそのまま残り、**部屋は常に一つに見える**。ゲスト project を汚さない不可侵原則は `--purge` が担う。
|
|
63
|
+
|
|
64
|
+
段の順序(**前の段が後の段の前提**):
|
|
65
|
+
1. **room ログの写し**(archive のみ)→ `docs/archive/room-log_<room>_<日時>.md`。**原本は room に残る**ので、これは repo 側の控え(失敗しても撤去は続行する)
|
|
66
|
+
2. **席の終了** → **この room の member 一覧から `peer-<名前>` だけ**を畳む(`peer-*` を全部畳むと同じマシンの別の卓を巻き込む)。他卓の席が残っていれば注記だけ出す
|
|
67
|
+
3. **ブリッジの停止**(起床・稼働状態)→ `.team/` を消す前(pid 記録がその中にある)
|
|
68
|
+
4. **解散**(archive): **履歴へ解散の区切りを1行投稿してから、メンバー登録だけ外す**。**部屋も過去ログも消さない**——区切りが無いと、次の卓の発言が前の卓と地続きに読める/**`--purge`**: room ごと削除(トークンを要する唯一の段)
|
|
69
|
+
5. **外部ペインの復元** → `.team/project.json.bak` が退避先なので `.team/` を消す前
|
|
70
|
+
6. `.team/` 削除 → `.mcp.json` → `.git/info/exclude` の追記行を戻す
|
|
71
|
+
7. **`.lattice/`**: archive では**残す**(`lattice todo status` と `gantt serve` が読む)。`--purge` かつ setup が作ったものなら削除。**残しても git 追跡外なら次の clone に残らない**ので、残すなら commit する(script は注記を出すだけ——他人の repo へ勝手に commit しない)
|
|
72
|
+
|
|
73
|
+
**次の卓を同じ部屋で立てる時は、setup の room 名を前と同じにする**。member は席が戻れば再登録され、履歴は続く。
|
|
74
|
+
|
|
75
|
+
実行後は **`git status`(archive なら `docs/archive/` と `.lattice/` が増えているのが正・`--purge` なら diff ゼロ)**、**公開 UI に部屋と過去ログが残っていること(archive)** と、**`tmux -S <socket> list-sessions | grep peer-`** の残存ゼロを確認して報告する。
|
|
61
76
|
|
|
62
77
|
各段は `[実施] / [スキップ] / [未実施]` を1行ずつ出す。**トークンを要するのは room 削除だけ**なので、そこが失敗しても残りの撤去は続行し、未実施を明示して非ゼロで終わる(黙って中断しない・決定58)。未実施が出ても**撤去そのものは済んでいる**。残りは表示された **[手当] の curl を手で叩く**だけで、`.team/` は既に消えているので **teardown.sh の再実行はできない**(2026-08-08 実測。再実行すると `setup-state.json` が読めず落ちる)。
|
|
63
78
|
|
|
79
|
+
## managed run 経由の配車(Lattice 併用モード・実行層へ載せる卓だけ)
|
|
80
|
+
|
|
81
|
+
卓を Lattice の実行層(managed run・隔離 worktree・実書き込み観測)へ載せた時は、task を claim で取り合わない。**task 選択=Lattice・候補席の選択=bridge・受けるかの決定=席**の3層で、`[配車]` は提示の可視化、席の `[受諾]` で初めて束縛が成立し、`[辞退]` なら別席へ再配車する。**席は動かさない**——自分の project に座ったまま、worktree へ絶対パスで出入りする。cwd と env を動かすと room 接続と MCP 解決が壊れるためで、これは回避策ではなく設計そのものである。
|
|
82
|
+
|
|
83
|
+
- **席と spool は接触しない。** 席が触るのは room と worktree だけで、`.lattice/` の直読み・直書き禁止の契約はそのまま。order は bridge が席の端末へ注入し、report は bridge が書く
|
|
84
|
+
- **席が出す room 語彙は3つだけ**(bridge が行頭一致で機械 parse する。いずれも**独立した1発言**): `[受諾] tN` / `[辞退] tN <理由>` / `[完了] tN`。辞退は正当な選択で、bridge が別の席へ再配車する
|
|
85
|
+
- **席の作法の正本は `templates/member.md` の「配車で来た仕事」節**(注入文の書式・禁止操作・scope 外書込・検証の回し方・成果の正本)。ここに二重化しない
|
|
86
|
+
- 前提は2つで、**どちらも立っていない卓には配車が来ない**(席の作法は無害に眠る): ①Lattice 側 executor adapter の登録と spool dir ②peertable 側の run-bridge 常駐
|
|
87
|
+
- **配車ブリッジの起動**: `PEERTABLE_POST_TOKEN=… nohup node scripts/run-bridge.mjs <project> <spool_dir> <席名>… > <project>/.team/run-bridge.log 2>&1 &`。停止は `node scripts/run-bridge.mjs <project> --stop`(**teardown.sh が自動で行う**)。起床ブリッジと同じ ADR 0157 の作法(pid 記録・起動時に前の記録を掃除・SIGTERM→SIGKILL)で、**席へは1バイトも送らない**——配車は room への投稿で届き、起こすのは channels(Claude 席)と wakeup-bridge(Codex 席)の仕事である
|
|
88
|
+
- **SSE が繋がって頭出しが済むまで配車しない。** 返事を聞けない状態で配車すると、直後の `[受諾]` を既読として捨てる(実測で踏んだ順序)
|
|
89
|
+
- 席が `[辞退]` したら別の席へ配車し直す。**`[受諾]` を受けて初めて report を書く**ので、辞退の窓は受諾より前にしかない(Lattice 側は受諾後の worker pid 変化を hard fail する)
|
|
90
|
+
|
|
91
|
+
運用側が踏みやすい所(実測で確認した挙動):
|
|
92
|
+
|
|
93
|
+
- **worktree は run 終端で `git worktree remove --force` される。** 席の commit は base_sha の子孫だが、木ごと消えた後はどの参照からも辿れない(gc の対象)。**成果の正本は Lattice が撮った observed diff** であって席の commit ではない。着地は run の外の工程で、`[完了]` は着地の宣言ではない
|
|
94
|
+
- **worktree には gitignore 済みの資産が無い**(`node_modules` 等)が、**席に install させない**。checkpoint 観測は `git status --ignored=matching` で撮る(gitignore 経由の scope 迂回を塞ぐ設計)ので、install した file が全部 `undeclared_write` になり、diff entry 上限 256 を超えた時点で観測ごと落ちる(実測: ignored 300本で `diff entry数が上限を超える`)。**依存は install 無しで解決する**——worktree が repo 配下(`<repo>/.lattice/runs/…/tree`)に切られるので、Node の bare specifier 解決が親を遡って canonical の `node_modules` に当たる(repo の外へ置くと `ERR_MODULE_NOT_FOUND`)。当たるのは canonical の版なので、lockfile を動かす task の検証結果は疑う。canonical tree で回させない——測りたい木ではない
|
|
95
|
+
- **`scope_writes` の外への書き込みは黙って弾かれず、`undeclared_write` として観測に出る。** 席へは「隠すな、room で言え」と伝わっている
|
|
96
|
+
|
|
97
|
+
## 線(共有プロトコル)を資源として宣言する(Lattice 併用モード)
|
|
98
|
+
|
|
99
|
+
**path が1つも重ならない2つの task が壊れ合うことがある。** 2026-08-08 の卓で実際に起きた: 片方が SSE のワイヤへ新しい event 種別を足した瞬間、そのストリームを読む側が壊れた。compile から見て完全に独立で、実際そう扱われていた。**依存は path ではなく共有プロトコルにあった。**
|
|
100
|
+
|
|
101
|
+
これを宣言できるのが**線**である。witness set を書く時、path・symbol の owns/reads/writes に加えて `lines` を書く(**省略可。省略=線の宣言なし**)。受理するのは witness set v5 / run_request v5 / boundary manifest v4 以降だけで、旧版へ書けば typed reject になる。
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
"lines": [
|
|
105
|
+
{
|
|
106
|
+
"line_id": "src.runtime-diff-observer.mjs--finding-kind",
|
|
107
|
+
"role": "writes",
|
|
108
|
+
"anchors": [
|
|
109
|
+
{ "kind": "path", "path": "src/runtime-diff-observer.mjs" },
|
|
110
|
+
{ "kind": "symbol", "name": "detectCheckpointFindings", "path": "src/runtime-diff-observer.mjs" }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- **`role`** は `writes`(線の形を変える側)か `reads`(その形に依存する側)。同じ `line_id` を別 task が持つ時、**`writes`×`reads` と `writes`×`writes` は直列化される。並列でいられるのは `reads`×`reads` だけ**——形を変える側が2人居るなら、その2人こそ揃えないと壊れるからである
|
|
117
|
+
- **`line_id` が一致した時だけ交差する。** 機械は anchor の重なりから「同じ線だろう」と推測しない——推測を装置に入れない設計であり、**綴りを揃える責任は宣言する側(AI)にある**
|
|
118
|
+
- **命名は錨から機械的に導く**: `anchors` **先頭**の repo-relative path の `/` を `.` に置換し、必要なら `--<種別>` を suffix する。`line_id` に使える文字は `[0-9A-Za-z._-]`(先頭は英数字・128文字まで)で、`/` も `:` も入らないのでこの置換が要る。**思いつきで名前を付けない**——揃わなければ交差は素通りする
|
|
119
|
+
- **名前を決めるのは最初に宣言した側だけ。** 後から同じ線を宣言する側は**再導出せず、既に在る `line_id` をそのまま写す**。錨が複数ある線で各自が「主たる錨」を選び直すと、同じ線に2つの名前が生まれて交差が消える
|
|
120
|
+
- **綴りが揃わなかった分は実行時が拾う。** 実際の変更 diff を錨の path へ近似して finding にし、その線の読み手を hold 閉包へ入れる。**計画時の宣言と実行時の観測の二段構え**であって、宣言だけで閉じる設計ではない。だから宣言漏れは致命ではないが、**漏れた分は「変更した後」にしか分からない**
|
|
121
|
+
- **錨は同じ repo の relative path だけ**(絶対 path は typed reject・`anchors` は最低1本)。越境 task(別 repo の file)を錨にすると**形式は通る**が、その path はこの repo に存在しないので**実行時の近似は永久に当たらない**。越境の線は「計画時の宣言としてだけ効く」と理解して使う(欠陥ではなく境界)
|
|
122
|
+
- **1つの task が同じ `line_id` を2本書くことはできない**(typed reject)。自分が writer でも reader でもある線は **`writes` を選ぶ**——読むだけの task はその形の変更を知る必要があり、それを教えられるのは writer 側の宣言だけだからである
|
|
123
|
+
|
|
124
|
+
**宣言する時の見つけ方**(席・親のどちらが witness を書く卓でも同じ):
|
|
125
|
+
|
|
126
|
+
1. 自分の変更が**他の誰かが読む形**を変えるかを問う: wire format・event 種別・schema の欄・CLI 出力の key・room の語彙・ファイル書式
|
|
127
|
+
2. 変えるなら `role: "writes"`、その形に依存して読むだけなら `role: "reads"`
|
|
128
|
+
3. 錨は「その形が書かれている file」。symbol 錨も足せるが、**symbol 錨も `path` 必須で、現在の実行時照合はその path 単位である**——同じ path の symbol を足しても近似は細かくならない(人が読む記録と、将来の照合のための宣言として足す)
|
|
129
|
+
4. **迷ったら宣言する。** 宣言は判定を厳しくするだけで、緩めることはできない
|
|
130
|
+
|
|
131
|
+
witness をどう生成するかは**対象 project 側の作法に従う**(Lattice repo なら `.lattice/todo/witness/<plan_key>.json` へ書いて `lattice todo independence compile --plan <key> --input <ref>`)。線はその witness の各 task entry へ足す欄であって、別の置き場を作らない。
|
|
132
|
+
|
|
64
133
|
## 親の operating notes(このセッションの振る舞い)
|
|
65
134
|
|
|
66
135
|
- 親は MCP を後付けできないため room へは HTTP API 直で参加する:
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""room の会話ログを Markdown へ書き出す(teardown の archive モード用)。
|
|
3
|
+
|
|
4
|
+
usage:
|
|
5
|
+
archive-room-log.py <server_url> <room> <out.md> ログを書き出す
|
|
6
|
+
archive-room-log.py --members <server_url> <room> member 名を空白区切りで出す(席を畳む相手)
|
|
7
|
+
|
|
8
|
+
本文は投稿時の Markdown をそのまま置く(引用ブロックで包むと表とコードが崩れる)。
|
|
9
|
+
本文の無い発言(過去に本文なし POST が着地した分)は、欠落と分かる形で残す——
|
|
10
|
+
消すと「そこに発言があった」ことまで消える。
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import urllib.request
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fetch(url, path):
|
|
18
|
+
with urllib.request.urlopen(f"{url.rstrip('/')}{path}", timeout=15) as r:
|
|
19
|
+
return json.load(r)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main(argv):
|
|
23
|
+
if argv[:1] == ["--members"]:
|
|
24
|
+
url, room = argv[1], argv[2]
|
|
25
|
+
members = fetch(url, f"/api/{room}/members")["members"]
|
|
26
|
+
print(" ".join(m["name"] for m in members))
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
url, room, out = argv[0], argv[1], argv[2]
|
|
30
|
+
messages = fetch(url, f"/api/{room}/messages")["messages"]
|
|
31
|
+
lines = [
|
|
32
|
+
f"# 円卓ログ — room `{room}`(全{len(messages)}発言)",
|
|
33
|
+
"",
|
|
34
|
+
"teardown(archive モード)が書き出した正史。room サーバー側は削除済みなので、"
|
|
35
|
+
"この文書が唯一の記録である。",
|
|
36
|
+
"",
|
|
37
|
+
"---",
|
|
38
|
+
"",
|
|
39
|
+
]
|
|
40
|
+
for m in messages:
|
|
41
|
+
body = m.get("body")
|
|
42
|
+
if body is None:
|
|
43
|
+
body = "(本文欠落——本文なし POST が着地した発言。欠落そのものを記録として残す)"
|
|
44
|
+
names = m.get("to_names")
|
|
45
|
+
audience = ", ".join(names) if isinstance(names, list) else m.get("to", "all")
|
|
46
|
+
lines += [f"## [{m['seq']}] {m['from']} → {audience} ・ {m['ts']}", "", body, ""]
|
|
47
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
48
|
+
f.write("\n".join(lines))
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
# 生の traceback を出さない。teardown の画面へ出るのは「何が起きたか」の1行であるべきで、
|
|
54
|
+
# stack trace は読む側に原因を伝えない(呼び出し側は非ゼロだけを見る)
|
|
55
|
+
try:
|
|
56
|
+
sys.exit(main(sys.argv[1:]))
|
|
57
|
+
except Exception as exc: # noqa: BLE001 — 入口なので型を絞らない
|
|
58
|
+
print(f"ARCHIVE_ROOM_LOG_FAILED: {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|