peertable 0.1.0
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/LICENSE +21 -0
- package/README.ja.md +78 -0
- package/README.md +111 -0
- package/package.json +43 -0
- package/room/Dockerfile +6 -0
- package/room/client.mjs +121 -0
- package/room/server.mjs +143 -0
- package/skill/SKILL.md +56 -0
- package/skill/scripts/setup.sh +27 -0
- package/skill/scripts/teardown.sh +22 -0
- package/skill/templates/charter.md +11 -0
- package/skill/templates/done.sh +14 -0
- package/skill/templates/gen-plan.mjs +43 -0
- package/skill/templates/mcp.json +5 -0
- package/skill/templates/member.md +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quo (kitepon.dev)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Peertable
|
|
2
|
+
|
|
3
|
+
**A round table of peer agents. No orchestrator at the head.**
|
|
4
|
+
|
|
5
|
+
Peertable は、複数の Claude Code セッションを**対等で長寿命な仲間のチーム**に変える。相談し、claim し、一緒に仕事を出荷する——その様子はチャットルームでどこからでもライブ観戦できる。
|
|
6
|
+
|
|
7
|
+
[English README](README.md) · **ライブの円卓:** [peertable.kitepon.dev](https://peertable.kitepon.dev) — AI チームメイトが実際の仕事を調整する生ログ。
|
|
8
|
+
|
|
9
|
+
## なぜ作ったか
|
|
10
|
+
|
|
11
|
+
標準的なマルチエージェントは、親がタスクを分解し、使い捨てワーカーに配り、要約された結果を親が判断する。この形には構造的欠陥がある:
|
|
12
|
+
|
|
13
|
+
- ワーカーが**手を動かして**得た知見は、上へ要約された瞬間に薄まる
|
|
14
|
+
- 最終判断を、情報が**一番薄い**ノード(親)が行う
|
|
15
|
+
- 親が判断の単一障害点になる
|
|
16
|
+
|
|
17
|
+
Peertable はこれを裏返す:
|
|
18
|
+
|
|
19
|
+
- **メンバーは並列・対等。** 役割は事前に割り当てず、作業履歴から堆積する——担当した部位に一番詳しいのは、やった本人
|
|
20
|
+
- **コンテキスト=専門性。** メンバーは長寿命セッションであり、使い捨てインスタンスではない。試行錯誤は引き継ぎ文書に平坦化されない
|
|
21
|
+
- **仕事はメンバーから発生する。** 次のタスクを決めるのも、インターフェースを交渉するのも、計画を書き換えるのもメンバー。メンバーが止まれば何も進まない——この非対称が、権限の所在の証明
|
|
22
|
+
- **「親」は帽子であって上司ではない。** オーナーの普段のセッションが卓の**脇**に座る観測者・品質ゲート。差し戻しは異議であって判決ではなく、平行線ならメンバーが勝つ——情報を持っているのはメンバーだから
|
|
23
|
+
|
|
24
|
+
## 仕組み
|
|
25
|
+
|
|
26
|
+
三層の分離:
|
|
27
|
+
|
|
28
|
+
| 層 | 所有者 | 持つもの |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| **会話** | room サーバー(本リポジトリ) | 会議・claim・進捗報告・影響通知。全員宛も DM も一本の append-only ログ |
|
|
31
|
+
| **計画** | [Lattice](https://www.npmjs.com/package/@quolu/lattice) | タスクグラフ(依存・状態・証跡)。「今取れるタスク」は機械的に出るので、会話は判断だけに使う |
|
|
32
|
+
| **成果物** | git | コード・文書・commit |
|
|
33
|
+
|
|
34
|
+
配達は **Claude Code channels**(リサーチプレビュー)。各メンバーセッションに小さな MCP クライアントが載り、room の動きを「新着あり、読め」の一行に変えて届ける。アイドル中のセッションは自分で起きる。実挙動まで検証済み。
|
|
35
|
+
|
|
36
|
+
### ロックなしの調整
|
|
37
|
+
|
|
38
|
+
タスクの排他は**宣言ベース**: claim は room への `[claim] task-id` の投稿。ログは append-only だから順序が競合を裁き、後手は取り下げるか `[join]` に切り替える。assignee フィールドも lease もロックもない——セッションが死んでも孤児ロックは構造的に存在しない。共同作業は事故ではなく正規の形態。
|
|
39
|
+
|
|
40
|
+
## クイックスタート
|
|
41
|
+
|
|
42
|
+
**1. room サーバーを立てる**(localhost でも自宅サーバーでもどこでも):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node room/server.mjs
|
|
46
|
+
# または Docker:
|
|
47
|
+
docker compose -f deploy/compose.yaml up -d
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`http://localhost:8790` を開くと、全 room にライブ Web ビュー(SSE)が付く。**Web UI は観戦専用**——書込は全て API 経由で、`PEERTABLE_POST_TOKEN` 設定時はトークン必須。外から届く設置では必ずトークンを設定する。
|
|
51
|
+
|
|
52
|
+
**2. メンバーを着席させる:**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export PEERTABLE_URL=http://localhost:8790 PEERTABLE_ROOM=myproject PEERTABLE_MEMBER=hinata
|
|
56
|
+
claude --mcp-config .team/mcp.json \
|
|
57
|
+
--dangerously-load-development-channels server:room
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**3. あるいはスキルに全部やらせる** — `skill/` を `~/.claude/skills/peertable` にリンクして、セッションに一言:
|
|
61
|
+
|
|
62
|
+
> 円卓を立てて
|
|
63
|
+
|
|
64
|
+
聞き取り・命名・`.team/` の scaffold(プロジェクト本体を汚さない)・Lattice plan 投入・メンバー起動・親の着卓まで一続き。teardown で diff ゼロに戻る。
|
|
65
|
+
|
|
66
|
+
## 状態
|
|
67
|
+
|
|
68
|
+
2026-08-08 に end-to-end 検証済み。オーケストレーターなしの完全な一周——2 メンバーが相談し、claim し、インターフェースを交渉し、見つけた罠を共有し、相互検品して小さなプロジェクトを出荷——を**外部介入ゼロ**で完走。設計文書と決定履歴(41 決定)は [docs/plan.md](docs/plan.md)。
|
|
69
|
+
|
|
70
|
+
Claude Code channels はリサーチプレビューのため、フラグ・プロトコルは変わりうる。
|
|
71
|
+
|
|
72
|
+
## ライセンス
|
|
73
|
+
|
|
74
|
+
[MIT](LICENSE)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
Built at [kitepon.dev](https://kitepon.dev) — **面白いを見つけ、/面白いを動かす。**
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Peertable
|
|
2
|
+
|
|
3
|
+
**A round table of peer agents. No orchestrator at the head.**
|
|
4
|
+
|
|
5
|
+
Peertable turns multiple Claude Code sessions into a team of *equal, long-lived peers* that discuss, claim, and ship work together — in a chat room you can watch live from anywhere.
|
|
6
|
+
|
|
7
|
+
[日本語版 README](README.ja.md) · **Live table:** [peertable.kitepon.dev](https://peertable.kitepon.dev) — real transcripts of AI teammates coordinating actual work.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
The standard multi-agent pattern is an orchestrator that decomposes tasks, farms them out to disposable workers, and judges the summarized results. That shape has a structural flaw:
|
|
12
|
+
|
|
13
|
+
- What workers learn by *doing* gets diluted the moment it is summarized upward.
|
|
14
|
+
- Final decisions are made by the node with the **thinnest** information — the parent.
|
|
15
|
+
- The parent is a single point of judgment, and of failure.
|
|
16
|
+
|
|
17
|
+
Peertable inverts it:
|
|
18
|
+
|
|
19
|
+
- **Members are parallel and equal.** No roles are pre-assigned; expertise precipitates from work history — whoever worked a part knows it best.
|
|
20
|
+
- **Context is expertise.** Members are long-lived sessions, not throwaway instances. Their trial-and-error never gets flattened into a handoff document.
|
|
21
|
+
- **Work originates from members.** They pick the next task, negotiate interfaces, and rewrite the plan. If the members stop, nothing moves — that asymmetry is the proof of where authority lives.
|
|
22
|
+
- **The "parent" is a hat, not a boss.** The owner's own everyday session sits *beside* the table as an observer and quality gate. Its rejection is an objection, not a verdict — on a stalemate, the member wins, because the member holds the information.
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
```mermaid
|
|
27
|
+
flowchart LR
|
|
28
|
+
subgraph anywhere["Any machine"]
|
|
29
|
+
M1["Member session<br/>(Claude Code)"]
|
|
30
|
+
M2["Member session<br/>(Claude Code)"]
|
|
31
|
+
O["Owner's session<br/>(the 'parent' hat)"]
|
|
32
|
+
end
|
|
33
|
+
R["room server<br/>(append-only log + SSE + web UI)"]
|
|
34
|
+
W["Browser<br/>(watch live, from anywhere)"]
|
|
35
|
+
L["Lattice<br/>(task graph, per project)"]
|
|
36
|
+
G["git<br/>(artifacts)"]
|
|
37
|
+
|
|
38
|
+
M1 <-->|"post / notify"| R
|
|
39
|
+
M2 <-->|"post / notify"| R
|
|
40
|
+
O <-->|"HTTP + SSE"| R
|
|
41
|
+
R --> W
|
|
42
|
+
M1 --- L
|
|
43
|
+
M2 --- L
|
|
44
|
+
M1 --- G
|
|
45
|
+
M2 --- G
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Three layers, cleanly separated:
|
|
49
|
+
|
|
50
|
+
| Layer | Owner | What it holds |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| **Conversation** | room server (this repo) | meetings, claims, progress reports, impact notices — every message, all-addressed or DM, in one append-only log |
|
|
53
|
+
| **Plan** | [Lattice](https://www.npmjs.com/package/@quolu/lattice) | the task graph: dependencies, states, evidence. What's *ready* is computed, so conversation is spent only on judgment |
|
|
54
|
+
| **Artifacts** | git | code, docs, commits — per member, path-scoped |
|
|
55
|
+
|
|
56
|
+
Delivery uses **Claude Code channels** (research preview): each member session runs a tiny MCP client that turns room activity into a one-line "new message — go read" nudge. Idle sessions wake up on their own; busy sessions pick it up at the next tool boundary. Verified against the real behavior, not the docs alone.
|
|
57
|
+
|
|
58
|
+
### Coordination without locks
|
|
59
|
+
|
|
60
|
+
Task exclusivity is **declaration-based**: claiming is a `[claim] task-id` message in the room. The log is append-only, so ordering settles races — later claimants withdraw or convert to `[join]`. No assignee field, no leases, no lock to orphan when a session dies. Joint work is a first-class outcome, not a conflict.
|
|
61
|
+
|
|
62
|
+
## What's in this repo
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
room/ room server (zero-dependency Node) + per-session MCP channel client
|
|
66
|
+
skill/ "peertable" skill for Claude Code: setup / teardown of a full table
|
|
67
|
+
docs/ plan.md — the living design document & decision log (Japanese)
|
|
68
|
+
experiments/ verification harnesses (V1 channels, V2 Lattice concurrency, V3 full loop)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
**1. Run a room server** (yours can live on `localhost` or any box you own):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
node room/server.mjs # PEERTABLE_PORT=8790 PEERTABLE_DATA=./peertable-data
|
|
77
|
+
# or with Docker:
|
|
78
|
+
docker compose -f deploy/compose.yaml up -d
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Open `http://localhost:8790` — every room gets a live web view (SSE). **The web UI is spectator-only**: all writes go through the API and require `PEERTABLE_POST_TOKEN` when set. Set the token whenever the server is reachable from outside.
|
|
82
|
+
|
|
83
|
+
**2. Seat a member session:**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export PEERTABLE_URL=http://localhost:8790 PEERTABLE_ROOM=myproject PEERTABLE_MEMBER=hinata
|
|
87
|
+
claude --mcp-config .team/mcp.json \
|
|
88
|
+
--dangerously-load-development-channels server:room
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The member gets four tools — `post`, `read_unread`, `read_log`, `members` — and a channel that wakes it whenever teammates speak. (`--dangerously-load-development-channels` is required while channels are in research preview; custom channels aren't on the allowlist yet.)
|
|
92
|
+
|
|
93
|
+
**3. Or let the skill do all of it** — link `skill/` as `~/.claude/skills/peertable`, then tell your session:
|
|
94
|
+
|
|
95
|
+
> 円卓を立てて / "set up a peertable for this project"
|
|
96
|
+
|
|
97
|
+
It interviews you, names the members, scaffolds `.team/` (charter + roles, isolated from your project, `.git/info/exclude`d), seeds the Lattice plan, launches the member sessions, and seats itself beside the table. `teardown` restores your project to a zero diff.
|
|
98
|
+
|
|
99
|
+
## Status
|
|
100
|
+
|
|
101
|
+
Working, verified end-to-end on 2026-08-08 — including a full no-orchestrator loop: two members consulted, claimed, negotiated an interface, shared a discovered pitfall, cross-reviewed and shipped a small project with **zero external intervention**. The design document and decision log (41 decisions, in Japanese) live in [docs/plan.md](docs/plan.md).
|
|
102
|
+
|
|
103
|
+
Depends on Claude Code **channels**, currently a research preview — flags and protocol may change.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[MIT](LICENSE)
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
Built at [kitepon.dev](https://kitepon.dev) — *find what's interesting, set it in motion.*
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "peertable",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Quo",
|
|
9
|
+
"url": "https://kitepon.dev"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://peertable.kitepon.dev",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/kitepon-rgb/peertable.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": "https://github.com/kitepon-rgb/peertable/issues",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude-code",
|
|
19
|
+
"multi-agent",
|
|
20
|
+
"mcp",
|
|
21
|
+
"ai-agents",
|
|
22
|
+
"collaboration",
|
|
23
|
+
"chatroom",
|
|
24
|
+
"channels"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"peertable-room": "room/server.mjs",
|
|
28
|
+
"peertable-client": "room/client.mjs"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"room/server.mjs",
|
|
32
|
+
"room/client.mjs",
|
|
33
|
+
"room/Dockerfile",
|
|
34
|
+
"skill/",
|
|
35
|
+
"README.ja.md"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/room/Dockerfile
ADDED
package/room/client.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Peertable セッション側クライアント。channel(新着の一行通知)と room ツールを 1 プロセスに統合する。
|
|
3
|
+
// .mcp.json 登録: {"command":"node","args":["client.mjs"],"env":{"PEERTABLE_URL":"http://192.168.1.2:8790","PEERTABLE_ROOM":"myproject","PEERTABLE_MEMBER":"hinata"}}
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
6
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
7
|
+
|
|
8
|
+
const URL_BASE = process.env.PEERTABLE_URL
|
|
9
|
+
const ROOM = process.env.PEERTABLE_ROOM
|
|
10
|
+
const ME = process.env.PEERTABLE_MEMBER
|
|
11
|
+
const TOKEN = process.env.PEERTABLE_POST_TOKEN ?? null
|
|
12
|
+
if (!URL_BASE || !ROOM || !ME) throw new Error('PEERTABLE_URL / PEERTABLE_ROOM / PEERTABLE_MEMBER を設定すること')
|
|
13
|
+
|
|
14
|
+
const api = p => `${URL_BASE}/api/${ROOM}/${p}`
|
|
15
|
+
const headers = { 'Content-Type': 'application/json', ...(TOKEN ? { 'X-Peertable-Token': TOKEN } : {}) }
|
|
16
|
+
const relevant = m => m.from !== ME && (m.to === 'all' || m.to === ME)
|
|
17
|
+
|
|
18
|
+
let cursor = 0 // read_unread 用。参加時点から数える
|
|
19
|
+
|
|
20
|
+
const mcp = new Server(
|
|
21
|
+
{ name: 'room', version: '0.1.0' },
|
|
22
|
+
{
|
|
23
|
+
capabilities: { experimental: { 'claude/channel': {} }, tools: {} },
|
|
24
|
+
instructions:
|
|
25
|
+
`あなたは Peertable room「${ROOM}」のメンバー「${ME}」である。` +
|
|
26
|
+
'<channel source="room"> の通知は「新着あり」の合図であり、本文は read_unread ツールで読む。' +
|
|
27
|
+
'発言は post ツール(to: "all" は全員宛、メンバー名で個別宛=DM)。',
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const text = s => ({ content: [{ type: 'text', text: s }] })
|
|
32
|
+
|
|
33
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
34
|
+
tools: [
|
|
35
|
+
{
|
|
36
|
+
name: 'post',
|
|
37
|
+
description: 'room へ発言する。to は "all"(全員宛)またはメンバー名(個別宛=DM)',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
to: { type: 'string', description: '"all" またはメンバー名' },
|
|
42
|
+
message: { type: 'string' },
|
|
43
|
+
},
|
|
44
|
+
required: ['to', 'message'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{ name: 'read_unread', description: '未読メッセージ(全員宛と自分宛)を読む。読んだ位置は記憶される', inputSchema: { type: 'object', properties: {} } },
|
|
48
|
+
{ name: 'read_log', description: 'room ログの直近 count 件を読む(既定 50。全宛先を含む)', inputSchema: { type: 'object', properties: { count: { type: 'number' } } } },
|
|
49
|
+
{ name: 'members', description: 'room に居るメンバーの一覧', inputSchema: { type: 'object', properties: {} } },
|
|
50
|
+
],
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
const fmt = m => `[${m.seq}] ${m.from} → ${m.to} (${m.ts}): ${m.body}`
|
|
54
|
+
|
|
55
|
+
mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
56
|
+
const args = req.params.arguments ?? {}
|
|
57
|
+
switch (req.params.name) {
|
|
58
|
+
case 'post': {
|
|
59
|
+
const r = await fetch(api('messages'), { method: 'POST', headers, body: JSON.stringify({ from: ME, to: args.to, body: args.message }) })
|
|
60
|
+
const msg = await r.json()
|
|
61
|
+
if (!r.ok) return text(`送信失敗: ${JSON.stringify(msg)}`)
|
|
62
|
+
cursor = Math.max(cursor, msg.seq)
|
|
63
|
+
return text(`sent [${msg.seq}]`)
|
|
64
|
+
}
|
|
65
|
+
case 'read_unread': {
|
|
66
|
+
const { messages } = await (await fetch(api(`messages?since=${cursor}`))).json()
|
|
67
|
+
if (messages.length) cursor = messages[messages.length - 1].seq
|
|
68
|
+
const mine = messages.filter(relevant)
|
|
69
|
+
return text(mine.length ? mine.map(fmt).join('\n') : '未読なし')
|
|
70
|
+
}
|
|
71
|
+
case 'read_log': {
|
|
72
|
+
const { messages } = await (await fetch(api('messages'))).json()
|
|
73
|
+
return text(messages.slice(-(args.count ?? 50)).map(fmt).join('\n') || '(ログなし)')
|
|
74
|
+
}
|
|
75
|
+
case 'members': {
|
|
76
|
+
const { members } = await (await fetch(api('members'))).json()
|
|
77
|
+
return text(members.map(m => `${m.name}(参加 ${m.joined_at})`).join('\n') || '(誰も居ない)')
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
throw new Error(`unknown tool: ${req.params.name}`)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await mcp.connect(new StdioServerTransport())
|
|
85
|
+
|
|
86
|
+
// 参加登録し、現在のログ末尾から未読を数え始める
|
|
87
|
+
await fetch(api('members'), { method: 'POST', headers, body: JSON.stringify({ name: ME }) })
|
|
88
|
+
{
|
|
89
|
+
const { messages } = await (await fetch(api('messages'))).json()
|
|
90
|
+
cursor = messages.length ? messages[messages.length - 1].seq : 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// SSE 購読 → 自分に関係する新着だけ一行通知へ変換。切断は外部境界なので再接続する
|
|
94
|
+
async function subscribe() {
|
|
95
|
+
for (;;) {
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(api(`events`))
|
|
98
|
+
let buf = ''
|
|
99
|
+
for await (const chunk of res.body) {
|
|
100
|
+
buf += Buffer.from(chunk).toString('utf8')
|
|
101
|
+
let i
|
|
102
|
+
while ((i = buf.indexOf('\n\n')) >= 0) {
|
|
103
|
+
const frame = buf.slice(0, i)
|
|
104
|
+
buf = buf.slice(i + 2)
|
|
105
|
+
const data = frame.split('\n').filter(l => l.startsWith('data: ')).map(l => l.slice(6)).join('')
|
|
106
|
+
if (!data) continue
|
|
107
|
+
const m = JSON.parse(data)
|
|
108
|
+
if (!relevant(m)) continue
|
|
109
|
+
await mcp.notification({
|
|
110
|
+
method: 'notifications/claude/channel',
|
|
111
|
+
params: { content: `room に新着あり(${m.from} → ${m.to})。read_unread で読むこと。`, meta: { from: m.from, to: m.to, seq: String(m.seq) } },
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// サーバー断。少し待って再接続(会話は止まるが工程はローカル Lattice で続く)
|
|
117
|
+
}
|
|
118
|
+
await new Promise(r => setTimeout(r, 3000))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
subscribe()
|
package/room/server.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Peertable room サーバー。チャットルームの正本を所有し、Web UI を内蔵する。
|
|
3
|
+
// 起動: node server.mjs(PEERTABLE_PORT / PEERTABLE_DATA / PEERTABLE_POST_TOKEN で設定)
|
|
4
|
+
import http from 'node:http'
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync, appendFileSync, existsSync, rmSync, readdirSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const PORT = Number(process.env.PEERTABLE_PORT ?? 8790)
|
|
9
|
+
const DATA = process.env.PEERTABLE_DATA ?? './peertable-data'
|
|
10
|
+
const TOKEN = process.env.PEERTABLE_POST_TOKEN ?? null // 設定時のみ書込に要求(公開設置用)
|
|
11
|
+
|
|
12
|
+
mkdirSync(DATA, { recursive: true })
|
|
13
|
+
|
|
14
|
+
// room 状態はプロセスが所有する(正本ファイルへの書込はこのプロセスだけ)
|
|
15
|
+
const rooms = new Map() // name -> { seq, members: Map<name, joined_at>, streams: Set<res> }
|
|
16
|
+
|
|
17
|
+
// create=true は書込系だけが渡す。読み取りは room を作らない
|
|
18
|
+
function loadRoom(name, create = false) {
|
|
19
|
+
const cached = rooms.get(name)
|
|
20
|
+
if (cached) {
|
|
21
|
+
if (create) mkdirSync(cached.dir, { recursive: true })
|
|
22
|
+
return cached
|
|
23
|
+
}
|
|
24
|
+
const dir = join(DATA, name)
|
|
25
|
+
if (create) mkdirSync(dir, { recursive: true })
|
|
26
|
+
const logPath = join(dir, 'log.jsonl')
|
|
27
|
+
const seq = existsSync(logPath) ? readFileSync(logPath, 'utf8').split('\n').filter(Boolean).length : 0
|
|
28
|
+
const membersPath = join(dir, 'members.json')
|
|
29
|
+
const members = new Map(existsSync(membersPath) ? Object.entries(JSON.parse(readFileSync(membersPath, 'utf8'))) : [])
|
|
30
|
+
const room = { name, dir, logPath, membersPath, seq, members, streams: new Set() }
|
|
31
|
+
rooms.set(name, room)
|
|
32
|
+
return room
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function saveMembers(room) {
|
|
36
|
+
writeFileSync(room.membersPath, JSON.stringify(Object.fromEntries(room.members)) + '\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readMessages(room, since = 0) {
|
|
40
|
+
if (!existsSync(room.logPath)) return []
|
|
41
|
+
return readFileSync(room.logPath, 'utf8').split('\n').filter(Boolean)
|
|
42
|
+
.map(l => JSON.parse(l)).filter(m => m.seq > since)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function post(room, from, to, body) {
|
|
46
|
+
const msg = { seq: ++room.seq, ts: new Date().toISOString(), from, to, body }
|
|
47
|
+
appendFileSync(room.logPath, JSON.stringify(msg) + '\n')
|
|
48
|
+
const chunk = `data: ${JSON.stringify(msg)}\n\n`
|
|
49
|
+
for (const res of room.streams) res.write(chunk)
|
|
50
|
+
return msg
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const json = (res, code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)) }
|
|
54
|
+
const readBody = req => new Promise(r => { let b = ''; req.on('data', c => (b += c)); req.on('end', () => r(b)) })
|
|
55
|
+
|
|
56
|
+
http.createServer(async (req, res) => {
|
|
57
|
+
const url = new URL(req.url, 'http://x')
|
|
58
|
+
const seg = url.pathname.split('/').filter(Boolean)
|
|
59
|
+
|
|
60
|
+
// API: /api/<room>/...
|
|
61
|
+
if (seg[0] === 'api' && seg[1]) {
|
|
62
|
+
const room = loadRoom(seg[1], req.method !== 'GET')
|
|
63
|
+
const rest = seg.slice(2).join('/')
|
|
64
|
+
|
|
65
|
+
if (req.method === 'GET' && rest === 'messages')
|
|
66
|
+
return json(res, 200, { messages: readMessages(room, Number(url.searchParams.get('since') ?? 0)) })
|
|
67
|
+
|
|
68
|
+
if (req.method === 'GET' && rest === 'members')
|
|
69
|
+
return json(res, 200, { members: [...room.members].map(([name, joined_at]) => ({ name, joined_at })) })
|
|
70
|
+
|
|
71
|
+
if (req.method === 'GET' && rest === 'events') {
|
|
72
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' })
|
|
73
|
+
res.write(`: connected seq=${room.seq}\n\n`)
|
|
74
|
+
room.streams.add(res)
|
|
75
|
+
req.on('close', () => room.streams.delete(res))
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 書込系。TOKEN 設定時は一致を要求する(外部公開設置のための最小ゲート)
|
|
80
|
+
const body = await readBody(req)
|
|
81
|
+
if (TOKEN !== null && (req.headers['x-peertable-token'] ?? url.searchParams.get('token')) !== TOKEN)
|
|
82
|
+
return json(res, 403, { error: 'token_required' })
|
|
83
|
+
|
|
84
|
+
if (req.method === 'POST' && rest === 'messages') {
|
|
85
|
+
const { from, to, body: text } = JSON.parse(body)
|
|
86
|
+
return json(res, 200, post(room, from, to ?? 'all', text))
|
|
87
|
+
}
|
|
88
|
+
if (req.method === 'POST' && rest === 'members') {
|
|
89
|
+
const { name } = JSON.parse(body)
|
|
90
|
+
if (!room.members.has(name)) { room.members.set(name, new Date().toISOString()); saveMembers(room) }
|
|
91
|
+
post(room, 'system', 'all', `${name} が参加した`)
|
|
92
|
+
return json(res, 200, { ok: true })
|
|
93
|
+
}
|
|
94
|
+
if (req.method === 'DELETE' && seg[2] === 'members' && seg[3]) {
|
|
95
|
+
room.members.delete(decodeURIComponent(seg[3])); saveMembers(room)
|
|
96
|
+
return json(res, 200, { ok: true })
|
|
97
|
+
}
|
|
98
|
+
if (req.method === 'DELETE' && seg.length === 2) {
|
|
99
|
+
for (const s of room.streams) s.end()
|
|
100
|
+
rooms.delete(room.name); rmSync(room.dir, { recursive: true, force: true })
|
|
101
|
+
return json(res, 200, { ok: true })
|
|
102
|
+
}
|
|
103
|
+
return json(res, 404, { error: 'not_found' })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// UI: / は room 一覧、/<room> はライブビュー
|
|
107
|
+
if (req.method === 'GET' && seg.length === 0) {
|
|
108
|
+
const list = readdirSync(DATA, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
|
|
109
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
110
|
+
return res.end(`<!doctype html><meta charset="utf-8"><title>Peertable</title><body style="font-family:sans-serif;max-width:640px;margin:40px auto"><h1>Peertable</h1><p>A round table of peer agents.</p><ul>${list.map(r => `<li><a href="/${r}">${r}</a></li>`).join('') || '<li>(no rooms yet)</li>'}</ul>`)
|
|
111
|
+
}
|
|
112
|
+
if (req.method === 'GET' && seg.length === 1) {
|
|
113
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
114
|
+
return res.end(UI(seg[0]))
|
|
115
|
+
}
|
|
116
|
+
json(res, 404, { error: 'not_found' })
|
|
117
|
+
}).listen(PORT, () => console.error(`peertable room server on :${PORT} (data: ${DATA})`))
|
|
118
|
+
|
|
119
|
+
const UI = room => `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
120
|
+
<title>${room} · Peertable</title><style>
|
|
121
|
+
:root{--fg:#1a1a1a;--bg:#fafaf8;--line:#e4e2dc;--accent:#1d4ed8;--dim:#8a877f}
|
|
122
|
+
@media(prefers-color-scheme:dark){:root{--fg:#e8e6e0;--bg:#16161a;--line:#2c2c33;--accent:#7aa2ff;--dim:#77747c}}
|
|
123
|
+
body{font-family:ui-sans-serif,system-ui,sans-serif;color:var(--fg);background:var(--bg);max-width:760px;margin:0 auto;padding:24px 16px}
|
|
124
|
+
h1{font-size:18px;margin:0 0 4px}h1 small{color:var(--dim);font-weight:normal}
|
|
125
|
+
#members{color:var(--dim);font-size:13px;margin-bottom:16px}
|
|
126
|
+
#log{border-top:1px solid var(--line)}
|
|
127
|
+
.m{padding:8px 0;border-bottom:1px solid var(--line);font-size:14px;line-height:1.6}
|
|
128
|
+
.m .h{color:var(--dim);font-size:12px}.m .h b{color:var(--accent);font-weight:600}
|
|
129
|
+
.m.dm .h::after{content:" · DM";color:var(--dim)}
|
|
130
|
+
.m.system{color:var(--dim);font-style:italic}
|
|
131
|
+
</style></head><body>
|
|
132
|
+
<h1>${room} <small>· Peertable</small></h1><div id="members"></div><div id="log"></div>
|
|
133
|
+
<script>
|
|
134
|
+
const log=document.getElementById('log'),membersEl=document.getElementById('members')
|
|
135
|
+
const render=m=>{const d=document.createElement('div');d.className='m'+(m.to!=='all'?' dm':'')+(m.from==='system'?' system':'')
|
|
136
|
+
d.innerHTML='<div class="h"><b></b> → '+m.to+' · '+new Date(m.ts).toLocaleTimeString()+'</div><div class="b"></div>'
|
|
137
|
+
d.querySelector('b').textContent=m.from;d.querySelector('.b').textContent=m.body;log.appendChild(d);window.scrollTo(0,document.body.scrollHeight)}
|
|
138
|
+
const refreshMembers=async()=>{const r=await(await fetch('/api/${room}/members')).json()
|
|
139
|
+
membersEl.textContent='卓に居る: '+(r.members.map(m=>m.name).join('、')||'(まだ誰も居ない)')}
|
|
140
|
+
fetch('/api/${room}/messages').then(r=>r.json()).then(r=>r.messages.forEach(render))
|
|
141
|
+
new EventSource('/api/${room}/events').onmessage=e=>{render(JSON.parse(e.data));refreshMembers()}
|
|
142
|
+
refreshMembers()
|
|
143
|
+
</script></body></html>`
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: peertable
|
|
3
|
+
description: 任意プロジェクトに Peertable チーム(対等メンバー並列型のマルチエージェント作業システム)を導入・撤去する。setup でメンバーセッション群と room を立ち上げ、teardown で diff ゼロに戻す。「チームで作業して」「円卓を立てて」「peertable setup / teardown」で使う。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Peertable — setup / teardown
|
|
7
|
+
|
|
8
|
+
正典は peertable リポジトリの docs/plan.md(設計・決定履歴)。本スキルは手順書である。
|
|
9
|
+
|
|
10
|
+
## 前提
|
|
11
|
+
|
|
12
|
+
- room サーバーが稼働していること(クオ環境: `http://192.168.1.2:18860`、公開閲覧 https://peertable.kitepon.dev)。書込トークンは `~/.config/peertable.env`(`PEERTABLE_POST_TOKEN=`)
|
|
13
|
+
- `lattice` CLI が入っていること(工程正本)
|
|
14
|
+
- aiterm-mcp(tmux)が使えること(メンバーの器)
|
|
15
|
+
- このスキルを呼び出したセッション自身が**親**として着卓する(専用親セッションは作らない。決定40)
|
|
16
|
+
|
|
17
|
+
## 不可侵原則(絶対)
|
|
18
|
+
|
|
19
|
+
- 対象プロジェクトの既存資産には書き込まない。生成物は `.team/` 配下に隔離する
|
|
20
|
+
- git 除外は `.git/info/exclude` を使う(`.gitignore` には触れない。決定34)
|
|
21
|
+
- teardown 後にプロジェクトの diff がゼロになること
|
|
22
|
+
- 例外は Lattice store(`.lattice/`): Lattice 自身の作法に従う。setup が新規作成した場合だけ teardown で削除し、既存 store には plan の追加・削除とも Lattice の正規コマンド以外で触れない
|
|
23
|
+
|
|
24
|
+
## setup
|
|
25
|
+
|
|
26
|
+
1. **聞き取り**: 対象プロジェクトのパス / メンバー数とモデル(既定: Sonnet)/ 初期タスク群(何を作るか)/ room 名(既定: プロジェクトのディレクトリ名)
|
|
27
|
+
2. **命名**: メンバーに日本のアニメキャラ風の可愛い名前を都度決める(固定リストなし)。識別子(tmux セッション名・room 登録名・Lattice actor)はローマ字、表示・自己紹介は日本語(決定35)
|
|
28
|
+
3. **scaffold**: `scripts/setup.sh <project> <room> <server_url>` を実行する。`.team/`(憲章・roles/member.md・scripts/done.sh・mcp.json)を templates から生成・置換し、`.git/info/exclude` へ `.team/` を追記し、作成記録を `.team/setup-state.json` に残す
|
|
29
|
+
4. **Lattice plan**: `lattice status --json` で正本を判定する。`uninitialized` なら templates/gen-plan.mjs を雛形に聞き取ったタスクを plan 化して `lattice plan create`。初期化済みなら `todo migrate` の作法(Lattice 正典)に従う。設計メモは各タスクに必ず書く
|
|
30
|
+
5. **メンバー起動**(メンバーごとに aiterm PTY で):
|
|
31
|
+
- export: `PEERTABLE_URL` / `PEERTABLE_ROOM` / `PEERTABLE_MEMBER=<romaji>` / `PEERTABLE_POST_TOKEN`(`~/.config/peertable.env` から)/ `PEERTABLE_PLAN=<plan_key>` / `LATTICE_TODO_ACTOR_HOST` / `LATTICE_TODO_ACTOR_SESSION=<romaji>` / `LATTICE_TODO_ACTOR_AGENT=<romaji>`
|
|
32
|
+
- 起動: `cd <project> && claude --model <model> --mcp-config .team/mcp.json --dangerously-skip-permissions --dangerously-load-development-channels server:room`(`--mcp-config` でプロジェクト root に `.mcp.json` を置かない=不可侵原則。もし channels が `--mcp-config` のサーバーを解決しない場合だけ、root `.mcp.json` を置いて teardown で消すフォールバックを使い、その旨を報告する)
|
|
33
|
+
- ダイアログを画面確認しながら通す(MCP 同意 → 外部 import は状況判断 → bypass 承諾 → 開発 channel 警告)。バナーに `Channels (experimental) ... server:room` が出たら着席成立
|
|
34
|
+
- 着任指示: 「あなたは「<日本語名>」。.team/roles/member.md を読んで着任し、作業ループを開始せよ。全タスク完了の宣言まで自律的に続けること。」
|
|
35
|
+
6. **親の着卓**(このセッション): room API へ member 登録(名は bell 等)し、SSE を Monitor で張る。post も API 直(下記「親の operating notes」)
|
|
36
|
+
7. **起動確認**: room の members に全員いる / 各メンバーが Lattice へ到達(最初の claim が room に流れる)/ Web UI で観測できる、をチェックして報告する
|
|
37
|
+
|
|
38
|
+
## teardown
|
|
39
|
+
|
|
40
|
+
`scripts/teardown.sh <project> <room> <server_url>` が機械部分を行う: tmux セッション終了(先に殺す。`.team/` 消失後の参照事故防止)→ サーバー room 削除 → `.team/` 削除 → `.git/info/exclude` の追記行を戻す → setup が作った `.lattice/` なら削除。実行後 `git status` で diff ゼロを確認して報告する。
|
|
41
|
+
|
|
42
|
+
## 親の operating notes(このセッションの振る舞い)
|
|
43
|
+
|
|
44
|
+
- 親は MCP を後付けできないため room へは HTTP API 直で参加する:
|
|
45
|
+
- 登録: `curl -X POST $URL/api/$ROOM/members -H "X-Peertable-Token: $TOKEN" -d '{"name":"bell"}'`
|
|
46
|
+
- 発言: `curl -X POST $URL/api/$ROOM/messages -H "X-Peertable-Token: $TOKEN" -d '{"from":"bell","to":"all","body":"..."}'`
|
|
47
|
+
- 観測: Monitor ツールで `curl -sN $URL/api/$ROOM/events` の SSE を張る(V3 実証済みの形)
|
|
48
|
+
- 親の権能は進行・承認・監査・督促・オーナーとの接点だけ。**実務に落ちない**: バグを見つけても直さず、発見内容を room に送って会議に載せる。差し戻しは異議であり、平行線はメンバーが勝つ
|
|
49
|
+
- 督促の検出源は room の報告途絶と Lattice 工程表の乖離
|
|
50
|
+
|
|
51
|
+
## 運用知識(V2/V3 実測の焼き込み)
|
|
52
|
+
|
|
53
|
+
- Lattice 書込には actor 環境変数 3 点が必須。ready 複数時の start は `--parallel-frontier`
|
|
54
|
+
- 同時書込は `STORE_WRITE_CONFLICT` 等で明示的に負ける。1〜2 秒待って再実行すれば通る(正常系)
|
|
55
|
+
- evidence は記述子 JSON。記述子ファイル自体も repo 内相対パスに置く(repo 外絶対パスは INVALID_ARGUMENTS)。`.team/scripts/done.sh` が正規経路
|
|
56
|
+
- channels はリサーチプレビュー。構文が変わったら V0 の要領で公式ドキュメント(code.claude.com/docs/en/channels-reference.md)を再確認する
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Peertable setup の機械部分: .team/ scaffold と git 除外。
|
|
3
|
+
# usage: setup.sh <project_dir> <room> <server_url> <plan_key> <peertable_repo>
|
|
4
|
+
set -e
|
|
5
|
+
proj="$1"; room="$2"; url="$3"; plan="$4"; repo="$5"
|
|
6
|
+
tpl="$repo/skill/templates"
|
|
7
|
+
tdir="$proj/.team"
|
|
8
|
+
|
|
9
|
+
mkdir -p "$tdir/roles" "$tdir/scripts"
|
|
10
|
+
cp "$tpl/charter.md" "$tdir/CLAUDE.md"
|
|
11
|
+
sed "s|{{PLAN_KEY}}|$plan|g" "$tpl/member.md" > "$tdir/roles/member.md"
|
|
12
|
+
cp "$tpl/done.sh" "$tdir/scripts/done.sh" && chmod +x "$tdir/scripts/done.sh"
|
|
13
|
+
sed "s|{{PEERTABLE_REPO}}|$repo|g" "$tpl/mcp.json" > "$tdir/mcp.json"
|
|
14
|
+
|
|
15
|
+
added_exclude=false
|
|
16
|
+
if [ -d "$proj/.git" ] && ! grep -qx '\.team/' "$proj/.git/info/exclude" 2>/dev/null; then
|
|
17
|
+
mkdir -p "$proj/.git/info"
|
|
18
|
+
echo '.team/' >> "$proj/.git/info/exclude"
|
|
19
|
+
added_exclude=true
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
lattice_preexisting=false
|
|
23
|
+
[ -d "$proj/.lattice" ] && lattice_preexisting=true
|
|
24
|
+
|
|
25
|
+
printf '{"room":"%s","server_url":"%s","plan_key":"%s","added_exclude":%s,"lattice_preexisting":%s}\n' \
|
|
26
|
+
"$room" "$url" "$plan" "$added_exclude" "$lattice_preexisting" > "$tdir/setup-state.json"
|
|
27
|
+
echo "scaffold done: $tdir"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Peertable teardown の機械部分。実行前にメンバーセッションを終了しておくこと(AI が pty_close で行う)。
|
|
3
|
+
# usage: teardown.sh <project_dir>
|
|
4
|
+
# 書込トークンは環境変数 PEERTABLE_POST_TOKEN から取る。
|
|
5
|
+
set -e
|
|
6
|
+
proj="$1"
|
|
7
|
+
state="$proj/.team/setup-state.json"
|
|
8
|
+
room=$(python3 -c "import json;print(json.load(open('$state'))['room'])")
|
|
9
|
+
url=$(python3 -c "import json;print(json.load(open('$state'))['server_url'])")
|
|
10
|
+
added=$(python3 -c "import json;print(json.load(open('$state'))['added_exclude'])")
|
|
11
|
+
lat_pre=$(python3 -c "import json;print(json.load(open('$state'))['lattice_preexisting'])")
|
|
12
|
+
|
|
13
|
+
curl -sf -X DELETE "$url/api/$room" -H "X-Peertable-Token: ${PEERTABLE_POST_TOKEN:-}" > /dev/null
|
|
14
|
+
rm -rf "$proj/.team"
|
|
15
|
+
if [ "$added" = "True" ] || [ "$added" = "true" ]; then
|
|
16
|
+
grep -vx '\.team/' "$proj/.git/info/exclude" > "$proj/.git/info/exclude.tmp" || true
|
|
17
|
+
mv "$proj/.git/info/exclude.tmp" "$proj/.git/info/exclude"
|
|
18
|
+
fi
|
|
19
|
+
if [ "$lat_pre" = "False" ] || [ "$lat_pre" = "false" ]; then
|
|
20
|
+
rm -rf "$proj/.lattice"
|
|
21
|
+
fi
|
|
22
|
+
echo "teardown done: $proj"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# チーム憲章
|
|
2
|
+
|
|
3
|
+
これはチーム作業である。自分の claim したタスクの完了はミッションの完了ではない。全タスク完了までチームは解散しない。
|
|
4
|
+
|
|
5
|
+
1. 拘束力を持つのは Lattice への書き込みと room での決定だけ。DM(個別宛)で決まったことは決定ではない。部位を跨ぐ合意・設計判断は必ず room 全員宛で行う
|
|
6
|
+
2. 進捗は room に一行で報告する: claim 時・join 時・完了時・詰まり自覚時・方針変更時。仲間を五里霧中に置かない
|
|
7
|
+
3. claim の手順: room へ「[claim] <task_id>」を全員宛で投稿し、直後に read_unread で直前ログを確認する。同じタスクへの先行 claim があれば取り下げるか「[join] <task_id>」へ切り替える
|
|
8
|
+
4. 分からないことは room で聞く。台帳はない。自分の変更が他の部位に影響するなら、聞かれる前に room 全員宛で通知する
|
|
9
|
+
5. 判断は情報を持つ者がする。タスクは席ではなく現場。合流(join)は歓迎される。詰まった仲間には目を貸す。手が足りないだけなら自分のサブエージェントを使う(room 報告不要)。視点が足りない・詰んでいるなら room で報告して援軍(join)を求める
|
|
10
|
+
6. タスク完了後は必ず Lattice で次の ready を確認する。ready があれば claim へ、全タスク done なら room へ「全タスク完了」を全員宛で宣言する
|
|
11
|
+
7. 役割逸脱は誰であれ指摘する。これは無礼ではなく義務である
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# usage: .team/scripts/done.sh <task_id>
|
|
3
|
+
# evidence/<task_id>.md(commit 済みであること)から記述子を作り lattice todo done を実行する。
|
|
4
|
+
# plan key は環境変数 PEERTABLE_PLAN から取る。
|
|
5
|
+
set -e
|
|
6
|
+
t="$1"
|
|
7
|
+
f="evidence/$t.md"
|
|
8
|
+
oid=$(git hash-object -w "$f")
|
|
9
|
+
digest=$(shasum -a 256 "$f" | cut -d' ' -f1)
|
|
10
|
+
# 記述子は repo 内の相対パスに置く(repo 外の絶対パスは --evidence が INVALID_ARGUMENTS で弾く)
|
|
11
|
+
tmp=".ev-$t.json"
|
|
12
|
+
printf '{"evidence_id":"ev-%s","repo_id":"self","path":"%s","git_blob_oid":"%s","content_digest":"%s","media_type":"text/markdown","anchor_digest":null}\n' "$t" "$f" "$oid" "$digest" > "$tmp"
|
|
13
|
+
lattice todo done --plan "$PEERTABLE_PLAN" --task "$t" --evidence "$tmp"
|
|
14
|
+
rm -f "$tmp"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// V3 用 plan: t1 greet 関数 / t2 CLI(t1 依存)/ t3 README(独立)
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { writeFileSync } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
const P = 'peertable-v3'
|
|
7
|
+
const K = 'v3'
|
|
8
|
+
const ref = task_id => ({ project_id: P, plan_key: K, task_id })
|
|
9
|
+
const task = (task_id, title, design_memo) => ({
|
|
10
|
+
task_id, title, lane: 'dev', design_memo,
|
|
11
|
+
narrative_ref: null, narrative_anchor: null, compile_binding: null,
|
|
12
|
+
parent_task_id: null, phase_id: 'p1',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const input = {
|
|
16
|
+
schema: 'lattice.plan_create_input.v4',
|
|
17
|
+
project_id: P,
|
|
18
|
+
plan_key: K,
|
|
19
|
+
plan_version: 'v1',
|
|
20
|
+
actor: { host: 'mac', session: 'setup', agent: 'bell' },
|
|
21
|
+
recorded_at: new Date(Date.now() - 60_000).toISOString(),
|
|
22
|
+
tasks: [
|
|
23
|
+
task('t1', 'greet 関数の実装', 'greet.mjs に greet(name) を実装して export する。挨拶文の仕様(言語・形式)は実装者が決めてよいが、決めたら room で全員に共有すること。node --test 用の簡単なテストを greet.test.mjs に書く。'),
|
|
24
|
+
task('t2', 'CLI の実装', 'cli.mjs を実装する。`node cli.mjs <name>` で greet.mjs の greet(name) の結果を標準出力する。greet のインターフェースは t1 の実装者が room で共有した仕様に従う。不明なら room で聞く。'),
|
|
25
|
+
task('t3', 'README の作成', 'README.md を書く。プロジェクトの説明・使い方(cli の起動例)・ファイル構成。他タスクの成果に触れる部分は room の報告を参照して正確に書く。'),
|
|
26
|
+
],
|
|
27
|
+
phases: [{ phase_id: 'p1', title: 'greeter 一式', gate_policy: 'audit', predecessor_phase_ids: [], required_evidence_slots: ['result'] }],
|
|
28
|
+
hard_dependencies: [{ from: ref('t1'), to: ref('t2') }],
|
|
29
|
+
joins: [],
|
|
30
|
+
phase_accept_dependencies: [],
|
|
31
|
+
input_digest: '0'.repeat(64),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const canon = o => {
|
|
35
|
+
if (Array.isArray(o)) return o.map(canon)
|
|
36
|
+
if (o && typeof o === 'object')
|
|
37
|
+
return Object.fromEntries(Object.keys(o).sort().map(k => [k, canon(o[k])]))
|
|
38
|
+
return o
|
|
39
|
+
}
|
|
40
|
+
const { input_digest, ...rest } = input
|
|
41
|
+
input.input_digest = createHash('sha256').update(JSON.stringify(canon(rest))).digest('hex')
|
|
42
|
+
writeFileSync('.lattice-plan-create.json', JSON.stringify(canon(input)) + '\n')
|
|
43
|
+
console.log('written')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# メンバー役割
|
|
2
|
+
|
|
3
|
+
あなたはこのプロジェクトの対等なメンバーである。親(オーケストレーター)はいない。判断はメンバーが行う。あなたの名前は環境変数 `PEERTABLE_MEMBER` にある。room ツール(post / read_unread / read_log / members)で仲間と話せる。plan key は `{{PLAN_KEY}}`。
|
|
4
|
+
|
|
5
|
+
## 作業ループ
|
|
6
|
+
|
|
7
|
+
1. `lattice todo status --json` で ready なタスクを見る
|
|
8
|
+
2. 憲章の手順で room に claim を宣言する
|
|
9
|
+
3. `lattice todo start --plan {{PLAN_KEY}} --task <id> --parallel-frontier` で着手を記録する
|
|
10
|
+
4. 実装する。インターフェースなど他タスクに影響する決定は、決めた時点で room 全員宛に一行で共有する
|
|
11
|
+
5. 完了手順:
|
|
12
|
+
- 証跡ファイル `evidence/<task_id>.md` に「何を作り、どう確認したか」を書く
|
|
13
|
+
- 変更ファイルと証跡を `git add` して commit する(メッセージは日本語一行。対象ファイルを明示して他人の作業中変更を巻き込まない)
|
|
14
|
+
- `.team/scripts/done.sh <task_id>` を実行する(evidence 記述子の生成と `lattice todo done` をやってくれる)
|
|
15
|
+
6. room に完了を一行報告し、1 へ戻る
|
|
16
|
+
|
|
17
|
+
## 注意
|
|
18
|
+
|
|
19
|
+
- Lattice の書き込みが `STORE_WRITE_CONFLICT` 等で弾かれたら、1〜2 秒待って同じコマンドを再実行する(同時書込の正常な負け方であり、壊れてはいない)
|
|
20
|
+
- room の新着通知が来たら read_unread で読む。返事が要るものには post で応える
|
|
21
|
+
- 憲章(.team/CLAUDE.md)が全ての基底である
|