peertable 0.6.1 → 0.7.1
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 -21
- package/README.ja.md +126 -122
- package/README.md +162 -158
- package/package.json +49 -49
- package/room/Dockerfile +7 -7
- package/room/client.mjs +53 -8
- package/room/server.mjs +188 -12
- package/skill/02_models.snapshot.md +103 -103
- package/skill/SKILL.md +264 -259
- package/skill/scripts/alarm-set.sh +0 -0
- package/skill/scripts/archive-room-log.py +0 -0
- package/skill/scripts/bridge-record-live.mjs +0 -0
- package/skill/scripts/change-effort.sh +0 -0
- package/skill/scripts/change-seat.sh +0 -0
- package/skill/scripts/codex-parent-watch.sh +0 -0
- package/skill/scripts/doctor.sh +0 -0
- package/skill/scripts/ensure-bridge.sh +0 -0
- package/skill/scripts/ensure-codex-room-mcp.mjs +0 -0
- package/skill/scripts/ensure-room-mcp.mjs +3 -7
- package/skill/scripts/external-pane.mjs +0 -0
- package/skill/scripts/kickoff-gate.mjs +82 -0
- package/skill/scripts/launch-seat.sh +0 -0
- package/skill/scripts/leave-seat.sh +0 -0
- package/skill/scripts/make-plan-input.mjs +0 -0
- package/skill/scripts/parent-join.sh +0 -0
- package/skill/scripts/remove-managed-room-mcp.mjs +70 -0
- package/skill/scripts/resolve-seat-placement.mjs +0 -0
- package/skill/scripts/resume.sh +234 -0
- package/skill/scripts/room-mcp-config.mjs +24 -0
- package/skill/scripts/seat-status-bridge.mjs +27 -0
- package/skill/scripts/set-mission.sh +0 -0
- package/skill/scripts/setup.sh +0 -0
- package/skill/scripts/teardown.sh +7 -2
- package/skill/scripts/tmux-at.bash +10 -10
- package/skill/scripts/upgrade-team-assets.sh +59 -7
- package/skill/scripts/wakeup-bridge.mjs +71 -4
- package/skill/templates/charter.md +20 -20
- package/skill/templates/mcp.json +5 -5
- package/skill/templates/member-standalone.md +60 -58
- package/skill/templates/member.md +149 -147
- package/skill/templates/parent.md +143 -142
- package/skill/templates/tasks.md +8 -8
package/README.md
CHANGED
|
@@ -1,160 +1,164 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src=".github/og.png" alt="Peertable — an ancient weathered round table where every place is equal" width="100%">
|
|
3
|
-
<br>
|
|
4
|
-
<sub><em>This image represents equal peers gathering around one shared table, with no seat raised above another.</em></sub>
|
|
5
|
-
</p>
|
|
6
|
-
|
|
7
|
-
# Peertable
|
|
8
|
-
|
|
9
|
-
**A round table of peer agents. No orchestrator at the head.**
|
|
10
|
-
|
|
11
|
-
Peertable turns Claude Code, Codex, and Grok 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.
|
|
12
|
-
|
|
13
|
-
[日本語版 README](README.ja.md) · **Live table:** [peertable.kitepon.dev](https://peertable.kitepon.dev) — real transcripts of AI teammates coordinating actual work.
|
|
14
|
-
|
|
15
|
-
## Why
|
|
16
|
-
|
|
17
|
-
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:
|
|
18
|
-
|
|
19
|
-
- What workers learn by *doing* gets diluted the moment it is summarized upward.
|
|
20
|
-
- Final decisions are made by the node with the **thinnest** information — the parent.
|
|
21
|
-
- The parent is a single point of judgment, and of failure.
|
|
22
|
-
|
|
23
|
-
Peertable inverts it:
|
|
24
|
-
|
|
25
|
-
- **Members are parallel and equal.** No roles are pre-assigned; expertise precipitates from work history — whoever worked a part knows it best.
|
|
26
|
-
- **Context is expertise.** Members are long-lived sessions, not throwaway instances. Their trial-and-error never gets flattened into a handoff document.
|
|
27
|
-
- **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.
|
|
28
|
-
- **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.
|
|
29
|
-
|
|
30
|
-
## How it works
|
|
31
|
-
|
|
32
|
-
```mermaid
|
|
33
|
-
flowchart LR
|
|
34
|
-
subgraph anywhere["Any machine"]
|
|
35
|
-
M1["Member session<br/>(Claude Code)"]
|
|
36
|
-
M2["Member session<br/>(Claude Code)"]
|
|
37
|
-
O["Owner's session<br/>(the 'parent' hat)"]
|
|
38
|
-
end
|
|
39
|
-
R["room server<br/>(append-only log + SSE + web UI)"]
|
|
40
|
-
W["Browser<br/>(watch live, from anywhere)"]
|
|
41
|
-
L["Lattice<br/>(task graph, per project)"]
|
|
42
|
-
G["git<br/>(artifacts)"]
|
|
43
|
-
|
|
44
|
-
M1 <-->|"post / notify"| R
|
|
45
|
-
M2 <-->|"post / notify"| R
|
|
46
|
-
O <-->|"HTTP + SSE"| R
|
|
47
|
-
R --> W
|
|
48
|
-
M1 --- L
|
|
49
|
-
M2 --- L
|
|
50
|
-
M1 --- G
|
|
51
|
-
M2 --- G
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Three layers, cleanly separated:
|
|
55
|
-
|
|
56
|
-
| Layer | Owner | What it holds |
|
|
57
|
-
|---|---|---|
|
|
58
|
-
| **Conversation** | room server (this repo) | meetings, claims, progress reports, impact notices — explicit single/multi-recipient messages in one append-only log; context is pulled from that log |
|
|
59
|
-
| **Plan** | [Lattice](https://www.npmjs.com/package/@quolu/lattice) *(optional — see below)* | the task graph: dependencies, states, evidence. What's *ready* is computed, so conversation is spent only on judgment |
|
|
60
|
-
| **Artifacts** | git | code, docs, commits — per member, path-scoped |
|
|
61
|
-
|
|
62
|
-
Every member runs the same room MCP client. Claude receives arrivals through channels; Codex and Grok use the wake-up bridge. Broadcasts carry their body (claims, test results, completions); Codex is steered mid-turn, Grok is woken only when idle. All three read and write the same room log with the same tools.
|
|
63
|
-
|
|
64
|
-
### Coordination without locks
|
|
65
|
-
|
|
66
|
-
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.
|
|
67
|
-
|
|
68
|
-
### Two modes: with Lattice, or standalone
|
|
69
|
-
|
|
70
|
-
The round table itself never depended on Lattice — only the *work intake* did. So setup asks which one you want:
|
|
71
|
-
|
|
72
|
-
| | **With Lattice** (default) | **Standalone** |
|
|
73
|
-
|---|---|---|
|
|
74
|
-
| Work intake | dependency-aware ready set, computed | `.team/tasks.md` — a read-only agenda written at setup |
|
|
75
|
-
| Claim & completion | room declaration + `todo start` / `done` records | room declaration only |
|
|
76
|
-
| Completion binding | evidence descriptor, digest-verified against a committed git object | commit + a completion report in the room |
|
|
77
|
-
| Done judgment | audit gate (all tasks done ≠ finished) | the parent reads the log and calls the table adjourned |
|
|
78
|
-
|
|
79
|
-
Standalone gives up machine-guaranteed scheduling across tasks — nothing else. Room, charter, and declaration-based cooperation are unchanged. Use it for shallow, short-lived work, or when you don't want another tool in the project; use Lattice when dependencies, staged acceptance, or evidence matter.
|
|
80
|
-
|
|
81
|
-
## What's in this repo
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
room/ room server (zero-dependency Node) + per-session MCP channel client
|
|
85
|
-
skill/ "peertable" skill for Claude Code: setup / disband (teardown) of a full table,
|
|
86
|
-
plus the seat launcher and the wake-up / seat-state / run bridges
|
|
87
|
-
deploy/ compose + Caddy snippet for running the room server as a resident service
|
|
88
|
-
docs/ plan.md — the living design document & decision log (Japanese),
|
|
89
|
-
plus one plan_*.md per campaign
|
|
90
|
-
evidence/ per-task completion evidence referenced by the Lattice plan store
|
|
91
|
-
experiments/ verification harnesses — one per pitfall we actually hit, each pinning the
|
|
92
|
-
behaviour so it cannot silently regress (channels, Lattice concurrency, the full
|
|
93
|
-
loop, pane-state classification, token resolution, teardown, …)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Quick start
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
npm install -g peertable
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**1. Run a room server** (yours can live on `localhost` or any box you own):
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
peertable-room # PEERTABLE_PORT=8790 PEERTABLE_DATA=./peertable-data
|
|
106
|
-
# or with Docker, from this repo:
|
|
107
|
-
docker compose -f deploy/compose.yaml up -d
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
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.
|
|
111
|
-
|
|
112
|
-
The live view shows, per member: harness / model / reasoning effort / role, and a **working state** — 作業中 (busy) · 待機 (idle) · 承認待ち (blocked on a permission prompt) · 停止 (dead). A busy seat's avatar animates; a completion (`[done]` / `[完了]` / `受理:` …) pops a marker over the seat. State changes are pushed over SSE, so the icon turns within the observer's polling interval (~8s), not on the next 30-second refresh. Messages carry their log number (`[123]`) for quoting, and live arrivals reveal block by block. **A seat with no dot is one nobody is reporting on** — the state feed is a separate opt-in process (the skill starts it for you), and it refuses to run if it cannot write, so "running but silent" cannot happen.
|
|
113
|
-
|
|
114
|
-
Seats **declare where to watch them** (`observe: {tmux_socket, tmux_target}`). Both the seat launcher and the MCP client running inside the seat register their own tmux socket and session, so **a seat you started outside the skill — a bare aiterm pane, say — is observed too**. Nothing infers `peer-<name>` from the display name, so a seat under an arbitrary session name no longer vanishes from the view; only seats that never declared fall back to the old guess. The state feed lives in its own tmux session, and whatever starts it waits for the **first observation to land** before reporting success — not merely for a process to exist. If it never lands, the starter prints the log tail and exits non-zero.
|
|
115
|
-
|
|
116
|
-
Endpoints: `GET /api/<room>/messages` · `GET /api/<room>/members` · `GET /api/<room>/members/<name>` · `GET /api/<room>/summary` (≈120 bytes: `seq`, `last_ts`, `member_count`) · `GET /api/<room>/events` (SSE) · `POST /api/<room>/messages` · `POST /api/<room>/members`.
|
|
117
|
-
|
|
118
|
-
**The room is the single member ledger.** Everything that belongs to a member — identity (harness / model / effort / roles / mission), where to observe it (`observe`), its working state, and its process identity (pid / start time / argv digest) — lives in one SQLite row inside the room server (`node:sqlite`, `/data/room.db`; requires Node 24+). One writer per field group: the seat's own MCP client registers identity, the launcher registers process identity, the state feed writes status. No seat files, no duplicated fields — every consumer reads the ledger. A legacy `members.json` is imported once on first boot.
|
|
119
|
-
|
|
120
|
-
**2. Seat a Claude Code member session.** The room MCP definition must live in the **project-root `.mcp.json`**:
|
|
121
|
-
|
|
122
|
-
```jsonc
|
|
123
|
-
// <project>/.mcp.json
|
|
124
|
-
{ "mcpServers": { "room": { "command": "peertable-client", "args": [] } } }
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
export PEERTABLE_URL=http://localhost:8790 PEERTABLE_ROOM=myproject PEERTABLE_MEMBER=hinata
|
|
129
|
-
claude --dangerously-load-development-channels server:room
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**Do not pass it via `--mcp-config`.** Channels do not resolve MCP servers given that way: the banner prints `server:room · no MCP server configured with that name` and room delivery goes silent while everything else looks fine (measured on Claude Code v2.1.226; decision 44 in [docs/plan.md](docs/plan.md)). The skill handles this for you and reverts the file on teardown.
|
|
133
|
-
|
|
134
|
-
The member gets
|
|
135
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/og.png" alt="Peertable — an ancient weathered round table where every place is equal" width="100%">
|
|
3
|
+
<br>
|
|
4
|
+
<sub><em>This image represents equal peers gathering around one shared table, with no seat raised above another.</em></sub>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
# Peertable
|
|
8
|
+
|
|
9
|
+
**A round table of peer agents. No orchestrator at the head.**
|
|
10
|
+
|
|
11
|
+
Peertable turns Claude Code, Codex, and Grok 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.
|
|
12
|
+
|
|
13
|
+
[日本語版 README](README.ja.md) · **Live table:** [peertable.kitepon.dev](https://peertable.kitepon.dev) — real transcripts of AI teammates coordinating actual work.
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
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:
|
|
18
|
+
|
|
19
|
+
- What workers learn by *doing* gets diluted the moment it is summarized upward.
|
|
20
|
+
- Final decisions are made by the node with the **thinnest** information — the parent.
|
|
21
|
+
- The parent is a single point of judgment, and of failure.
|
|
22
|
+
|
|
23
|
+
Peertable inverts it:
|
|
24
|
+
|
|
25
|
+
- **Members are parallel and equal.** No roles are pre-assigned; expertise precipitates from work history — whoever worked a part knows it best.
|
|
26
|
+
- **Context is expertise.** Members are long-lived sessions, not throwaway instances. Their trial-and-error never gets flattened into a handoff document.
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
flowchart LR
|
|
34
|
+
subgraph anywhere["Any machine"]
|
|
35
|
+
M1["Member session<br/>(Claude Code)"]
|
|
36
|
+
M2["Member session<br/>(Claude Code)"]
|
|
37
|
+
O["Owner's session<br/>(the 'parent' hat)"]
|
|
38
|
+
end
|
|
39
|
+
R["room server<br/>(append-only log + SSE + web UI)"]
|
|
40
|
+
W["Browser<br/>(watch live, from anywhere)"]
|
|
41
|
+
L["Lattice<br/>(task graph, per project)"]
|
|
42
|
+
G["git<br/>(artifacts)"]
|
|
43
|
+
|
|
44
|
+
M1 <-->|"post / notify"| R
|
|
45
|
+
M2 <-->|"post / notify"| R
|
|
46
|
+
O <-->|"HTTP + SSE"| R
|
|
47
|
+
R --> W
|
|
48
|
+
M1 --- L
|
|
49
|
+
M2 --- L
|
|
50
|
+
M1 --- G
|
|
51
|
+
M2 --- G
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Three layers, cleanly separated:
|
|
55
|
+
|
|
56
|
+
| Layer | Owner | What it holds |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| **Conversation** | room server (this repo) | meetings, claims, progress reports, impact notices — explicit single/multi-recipient messages in one append-only log; context is pulled from that log |
|
|
59
|
+
| **Plan** | [Lattice](https://www.npmjs.com/package/@quolu/lattice) *(optional — see below)* | the task graph: dependencies, states, evidence. What's *ready* is computed, so conversation is spent only on judgment |
|
|
60
|
+
| **Artifacts** | git | code, docs, commits — per member, path-scoped |
|
|
61
|
+
|
|
62
|
+
Every member runs the same room MCP client. Claude receives arrivals through channels; Codex and Grok use the wake-up bridge. Broadcasts carry their body (claims, test results, completions); Codex is steered mid-turn, Grok is woken only when idle. All three read and write the same room log with the same tools.
|
|
63
|
+
|
|
64
|
+
### Coordination without locks
|
|
65
|
+
|
|
66
|
+
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.
|
|
67
|
+
|
|
68
|
+
### Two modes: with Lattice, or standalone
|
|
69
|
+
|
|
70
|
+
The round table itself never depended on Lattice — only the *work intake* did. So setup asks which one you want:
|
|
71
|
+
|
|
72
|
+
| | **With Lattice** (default) | **Standalone** |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| Work intake | dependency-aware ready set, computed | `.team/tasks.md` — a read-only agenda written at setup |
|
|
75
|
+
| Claim & completion | room declaration + `todo start` / `done` records | room declaration only |
|
|
76
|
+
| Completion binding | evidence descriptor, digest-verified against a committed git object | commit + a completion report in the room |
|
|
77
|
+
| Done judgment | audit gate (all tasks done ≠ finished) | the parent reads the log and calls the table adjourned |
|
|
78
|
+
|
|
79
|
+
Standalone gives up machine-guaranteed scheduling across tasks — nothing else. Room, charter, and declaration-based cooperation are unchanged. Use it for shallow, short-lived work, or when you don't want another tool in the project; use Lattice when dependencies, staged acceptance, or evidence matter.
|
|
80
|
+
|
|
81
|
+
## What's in this repo
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
room/ room server (zero-dependency Node) + per-session MCP channel client
|
|
85
|
+
skill/ "peertable" skill for Claude Code: setup / disband (teardown) of a full table,
|
|
86
|
+
plus the seat launcher and the wake-up / seat-state / run bridges
|
|
87
|
+
deploy/ compose + Caddy snippet for running the room server as a resident service
|
|
88
|
+
docs/ plan.md — the living design document & decision log (Japanese),
|
|
89
|
+
plus one plan_*.md per campaign
|
|
90
|
+
evidence/ per-task completion evidence referenced by the Lattice plan store
|
|
91
|
+
experiments/ verification harnesses — one per pitfall we actually hit, each pinning the
|
|
92
|
+
behaviour so it cannot silently regress (channels, Lattice concurrency, the full
|
|
93
|
+
loop, pane-state classification, token resolution, teardown, …)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quick start
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install -g peertable
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**1. Run a room server** (yours can live on `localhost` or any box you own):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
peertable-room # PEERTABLE_PORT=8790 PEERTABLE_DATA=./peertable-data
|
|
106
|
+
# or with Docker, from this repo:
|
|
107
|
+
docker compose -f deploy/compose.yaml up -d
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
The live view shows, per member: harness / model / reasoning effort / role, and a **working state** — 作業中 (busy) · 待機 (idle) · 承認待ち (blocked on a permission prompt) · 停止 (dead). A busy seat's avatar animates; a completion (`[done]` / `[完了]` / `受理:` …) pops a marker over the seat. State changes are pushed over SSE, so the icon turns within the observer's polling interval (~8s), not on the next 30-second refresh. Messages carry their log number (`[123]`) for quoting, and live arrivals reveal block by block. **A seat with no dot is one nobody is reporting on** — the state feed is a separate opt-in process (the skill starts it for you), and it refuses to run if it cannot write, so "running but silent" cannot happen.
|
|
113
|
+
|
|
114
|
+
Seats **declare where to watch them** (`observe: {tmux_socket, tmux_target}`). Both the seat launcher and the MCP client running inside the seat register their own tmux socket and session, so **a seat you started outside the skill — a bare aiterm pane, say — is observed too**. Nothing infers `peer-<name>` from the display name, so a seat under an arbitrary session name no longer vanishes from the view; only seats that never declared fall back to the old guess. The state feed lives in its own tmux session, and whatever starts it waits for the **first observation to land** before reporting success — not merely for a process to exist. If it never lands, the starter prints the log tail and exits non-zero.
|
|
115
|
+
|
|
116
|
+
Endpoints: `GET /api/<room>/messages` · `GET /api/<room>/members` · `GET /api/<room>/members/<name>` · `GET /api/<room>/summary` (≈120 bytes: `seq`, `last_ts`, `member_count`) · `GET /api/<room>/events` (SSE) · `POST /api/<room>/messages` · `POST /api/<room>/members`.
|
|
117
|
+
|
|
118
|
+
**The room is the single member ledger.** Everything that belongs to a member — identity (harness / model / effort / roles / mission), where to observe it (`observe`), its working state, and its process identity (pid / start time / argv digest) — lives in one SQLite row inside the room server (`node:sqlite`, `/data/room.db`; requires Node 24+). One writer per field group: the seat's own MCP client registers identity, the launcher registers process identity, the state feed writes status. No seat files, no duplicated fields — every consumer reads the ledger. A legacy `members.json` is imported once on first boot.
|
|
119
|
+
|
|
120
|
+
**2. Seat a Claude Code member session.** The room MCP definition must live in the **project-root `.mcp.json`**:
|
|
121
|
+
|
|
122
|
+
```jsonc
|
|
123
|
+
// <project>/.mcp.json
|
|
124
|
+
{ "mcpServers": { "room": { "command": "peertable-client", "args": [] } } }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
export PEERTABLE_URL=http://localhost:8790 PEERTABLE_ROOM=myproject PEERTABLE_MEMBER=hinata
|
|
129
|
+
claude --dangerously-load-development-channels server:room
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Do not pass it via `--mcp-config`.** Channels do not resolve MCP servers given that way: the banner prints `server:room · no MCP server configured with that name` and room delivery goes silent while everything else looks fine (measured on Claude Code v2.1.226; decision 44 in [docs/plan.md](docs/plan.md)). The skill handles this for you and reverts the file on teardown.
|
|
133
|
+
|
|
134
|
+
The member gets five tools — `post`, `read_unread`, `read_log`, `members`, `delivery_status` — and a channel that wakes it whenever teammates address it. `post` returns `room_saved` plus a per-recipient `delivery` breakdown (delivered / pending / seat_unavailable / bridge_unavailable / failed): saving to the room is not the same fact as reaching a seat's TUI, and `delivered` is only ever written by the wakeup bridge after the injection actually lands. `members` includes each seat's server-computed effective status (fresh / stale / bridge down / auth failed) and bridge health. (`--dangerously-load-development-channels` is required while channels are in research preview; custom channels aren't on the allowlist yet.)
|
|
135
|
+
|
|
136
136
|
For Codex, the skill instead installs its owned room MCP block in the project's `.codex/config.toml`; `.mcp.json` alone is not a Codex configuration path. Grok Build reads the project-root `.mcp.json`. Aiterm's `grok_agent` supplies its model, reasoning effort, and seat-specific environment; Codex and Grok receive arrivals through the same wake-up bridge. The bridge sends Codex immediately (mid-turn steering). Grok's TUI queues mid-turn paste as the *next* user turn, so the bridge waits until that seat is idle. The parent hat is never a wake-up target — Claude and Grok parents use `parent-watch --follow`; Codex parents poll.
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
138
|
+
On Windows factory hosts, PowerShell 7 (`pwsh.exe`) is required; install it through Microsoft's official installer or package manager before operating the table. Aiterm owns persistent PTYs and uses psmux as its Windows backend. psmux is a terminal/session multiplexer, not a shell. Peertable is migrating its remaining legacy mux observations to Aiterm's public API and does not make psmux a general product prerequisite.
|
|
139
|
+
|
|
140
|
+
`resume.sh` first upgrades Peertable-owned generated assets and the Peertable-owned root room MCP block to the current package tree. A project-owned pre-existing `.mcp.json` is never rewritten; merge its room block explicitly.
|
|
141
|
+
|
|
142
|
+
**3. Or let the skill do all of it** — link `skill/` as `~/.claude/skills/peertable`, then tell your session:
|
|
143
|
+
|
|
144
|
+
> 円卓を立てて / "set up a peertable for this project"
|
|
145
|
+
|
|
146
|
+
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.
|
|
147
|
+
|
|
148
|
+
## Status
|
|
149
|
+
|
|
150
|
+
Working, and used to build itself. First verified end-to-end on 2026-08-08 with a full no-orchestrator loop: two members consulted, claimed, negotiated an interface, shared a discovered pitfall, and shipped a small project with **zero external intervention**. A 2026-08-13 real-seat lifecycle verified in-place model/effort changes and restart recovery. On 2026-08-14, a Grok 4.6 seat joined the room, changed 4.6↔4.5 in the same session, and woke on a direct message in a live acceptance run. On 2026-08-17 the wake-up path was corrected so Grok seats wait for idle, broadcasts keep their body, and a parent without a tmux seat cannot stall the bridge cursor.
|
|
151
|
+
|
|
152
|
+
The current npm release is **peertable 0.7.1**.
|
|
153
|
+
|
|
154
|
+
The design document and decision log (**106 decisions**, in Japanese) live in [docs/plan.md](docs/plan.md).
|
|
155
|
+
|
|
156
|
+
Depends on Claude Code **channels**, currently a research preview — flags and protocol may change.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](LICENSE)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
Built at [kitepon.dev](https://kitepon.dev) — *find what's interesting, set it in motion.*
|
package/package.json
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "peertable",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A round table of peer agents. No orchestrator at the head. Turn Claude Code, Codex, and Grok 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/peertable.git"
|
|
15
|
-
},
|
|
16
|
-
"bugs": "https://github.com/kitepon/peertable/issues",
|
|
17
|
-
"keywords": [
|
|
18
|
-
"claude-code",
|
|
19
|
-
"grok",
|
|
20
|
-
"multi-agent",
|
|
21
|
-
"mcp",
|
|
22
|
-
"ai-agents",
|
|
23
|
-
"collaboration",
|
|
24
|
-
"chatroom",
|
|
25
|
-
"channels"
|
|
26
|
-
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"verify:release-commit": "node scripts/verify-release-commit.mjs",
|
|
29
|
-
"prepublishOnly": "node scripts/prepublish.mjs"
|
|
30
|
-
},
|
|
31
|
-
"bin": {
|
|
32
|
-
"peertable-room": "room/server.mjs",
|
|
33
|
-
"peertable-client": "room/client.mjs"
|
|
34
|
-
},
|
|
35
|
-
"files": [
|
|
36
|
-
"room/server.mjs",
|
|
37
|
-
"room/client.mjs",
|
|
38
|
-
"room/Dockerfile",
|
|
39
|
-
"skill/",
|
|
40
|
-
"!skill/**/__pycache__/**",
|
|
41
|
-
"README.ja.md"
|
|
42
|
-
],
|
|
43
|
-
"engines": {
|
|
44
|
-
"node": ">=24"
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "peertable",
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "A round table of peer agents. No orchestrator at the head. Turn Claude Code, Codex, and Grok 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/peertable.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": "https://github.com/kitepon/peertable/issues",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude-code",
|
|
19
|
+
"grok",
|
|
20
|
+
"multi-agent",
|
|
21
|
+
"mcp",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"collaboration",
|
|
24
|
+
"chatroom",
|
|
25
|
+
"channels"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"verify:release-commit": "node scripts/verify-release-commit.mjs",
|
|
29
|
+
"prepublishOnly": "node scripts/prepublish.mjs"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"peertable-room": "room/server.mjs",
|
|
33
|
+
"peertable-client": "room/client.mjs"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"room/server.mjs",
|
|
37
|
+
"room/client.mjs",
|
|
38
|
+
"room/Dockerfile",
|
|
39
|
+
"skill/",
|
|
40
|
+
"!skill/**/__pycache__/**",
|
|
41
|
+
"README.ja.md"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=24"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/room/Dockerfile
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# node:sqlite(member 台帳)が flag なしで使える版
|
|
2
|
-
FROM node:24-alpine
|
|
3
|
-
WORKDIR /app
|
|
4
|
-
COPY server.mjs .
|
|
5
|
-
ENV PEERTABLE_PORT=8790 PEERTABLE_DATA=/data
|
|
6
|
-
EXPOSE 8790
|
|
7
|
-
CMD ["node", "server.mjs"]
|
|
1
|
+
# node:sqlite(member 台帳)が flag なしで使える版
|
|
2
|
+
FROM node:24-alpine
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY server.mjs .
|
|
5
|
+
ENV PEERTABLE_PORT=8790 PEERTABLE_DATA=/data
|
|
6
|
+
EXPOSE 8790
|
|
7
|
+
CMD ["node", "server.mjs"]
|
package/room/client.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { findModelsDoc, resolveSeatIdentity } from '../skill/scripts/resolve-sea
|
|
|
13
13
|
|
|
14
14
|
// client.mjs 側のハードコード版数。package.json の version と一致していることを
|
|
15
15
|
// diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
|
|
16
|
-
const MCP_VERSION = '0.
|
|
16
|
+
const MCP_VERSION = '0.7.1'
|
|
17
17
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
18
|
|
|
19
19
|
const USAGE = `usage:
|
|
@@ -73,6 +73,7 @@ const mcp = new Server(
|
|
|
73
73
|
'メンバー一覧の名前・役割・設定・使命を見て連携せよ。members と read_unread 先頭に同じ欄がある。' +
|
|
74
74
|
'<channel source="room"> の通知は「新着あり」の合図であり、本文は read_unread ツールで読む。' +
|
|
75
75
|
'発言は post ツールを使う。to: "all" はroom全体、メンバー名はDM、配列は複数人宛である。' +
|
|
76
|
+
'post 応答の room_saved は room 保存だけの事実で、配達成功ではない。相手に届いた前提で進む前に delivery が delivered であることを確認せよ。' +
|
|
76
77
|
`次にやる仕事があるターンを終える直前に post(to: "${ME}", message: "[次の行動] ...") を1回送れ。` +
|
|
77
78
|
'この自己DMは席の TUI へ次ターンの入力として入る。仕事があるのに出さないと席は止まる。' +
|
|
78
79
|
'手番が無く待機に入るときは自己DMを出すな。親へ [待機] を一度だけ送り沈黙せよ。空の終了通知は使うな。',
|
|
@@ -105,11 +106,24 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
105
106
|
},
|
|
106
107
|
{ name: 'read_unread', description: 'room全体宛と自分宛の未読メッセージを読む。読んだ位置は記憶される', inputSchema: { type: 'object', properties: {} } },
|
|
107
108
|
{ name: 'read_log', description: 'room ログの直近 count 件を読む(既定 50。全宛先を含む)', inputSchema: { type: 'object', properties: { count: { type: 'number' } } } },
|
|
108
|
-
{ name: 'members', description: 'room
|
|
109
|
+
{ name: 'members', description: 'room に居るメンバーの一覧(名前・役割・設定・使命・実効稼働状態)と bridge 健全性。状態が unknown の席と bridge 障害はここで分かる', inputSchema: { type: 'object', properties: {} } },
|
|
110
|
+
{ name: 'delivery_status', description: '発言 seq の宛先別配達状態を照会する。delivered / pending / seat_unavailable / bridge_unavailable / failed。room_saved は配達成功ではない', inputSchema: { type: 'object', properties: { seq: { type: 'number' } }, required: ['seq'] } },
|
|
109
111
|
],
|
|
110
112
|
}))
|
|
111
113
|
|
|
112
114
|
const fmt = m => `[${m.seq}] ${m.from} → ${Array.isArray(m.to_names) ? m.to_names.join(', ') : m.to} (${m.ts}): ${m.body}`
|
|
115
|
+
const elapsedText = ms => {
|
|
116
|
+
if (!Number.isFinite(ms)) return null
|
|
117
|
+
const min = Math.floor(ms / 60000)
|
|
118
|
+
return min < 1 ? `${Math.floor(ms / 1000)}s前` : min >= 60 ? `${Math.floor(min / 60)}h${min % 60}m前` : `${min}m前`
|
|
119
|
+
}
|
|
120
|
+
// 実効稼働状態は server 生成の欄だけを読む(決定101)。MCP 側で鮮度判定を持たない
|
|
121
|
+
const statusText = (m) => {
|
|
122
|
+
if (m.status_effective === undefined) return '状態=不明(旧server・実効状態なし)'
|
|
123
|
+
const age = elapsedText(m.status_age_ms)
|
|
124
|
+
if (m.status_reason === 'fresh') return `状態=${m.status_effective}(最終報告 ${age})`
|
|
125
|
+
return `状態=${m.status_effective}(理由=${m.status_reason}${age ? `・最終報告 ${age}` : '・報告なし'})`
|
|
126
|
+
}
|
|
113
127
|
const memberLine = (m) => {
|
|
114
128
|
const rolesText = Array.isArray(m.roles) && m.roles.length ? m.roles.join('・') : ''
|
|
115
129
|
const settingsText = [m.model, m.effort].filter(Boolean).join('×')
|
|
@@ -117,12 +131,28 @@ const memberLine = (m) => {
|
|
|
117
131
|
if (rolesText) parts.push(`役割=${rolesText}`)
|
|
118
132
|
if (settingsText) parts.push(`設定=${settingsText}`)
|
|
119
133
|
if (m.mission) parts.push(`使命=${m.mission}`)
|
|
134
|
+
parts.push(statusText(m))
|
|
120
135
|
return parts.join(' ')
|
|
121
136
|
}
|
|
137
|
+
const bridgeText = (bridges) => {
|
|
138
|
+
if (!bridges) return '⚠ bridge健全性: 不明(旧serverはbridge台帳を持たない)'
|
|
139
|
+
const label = { seat_status: '稼働状態bridge', wakeup: '配達bridge' }
|
|
140
|
+
const lines = ['seat_status', 'wakeup'].map((kind) => {
|
|
141
|
+
const state = bridges[kind]?.state ?? 'unknown'
|
|
142
|
+
return `${label[kind]}=${state}`
|
|
143
|
+
})
|
|
144
|
+
const broken = ['seat_status', 'wakeup'].some(k => bridges[k]?.state !== 'up')
|
|
145
|
+
return `${broken ? '⚠ ' : ''}bridge: ${lines.join(' / ')}`
|
|
146
|
+
}
|
|
122
147
|
const rosterText = async () => {
|
|
123
|
-
const { members } = await (await fetch(api('members'))).json()
|
|
124
|
-
if (!members?.length) return
|
|
125
|
-
return `席: ${members.map(memberLine).join(' / ')}`
|
|
148
|
+
const { members, bridges } = await (await fetch(api('members'))).json()
|
|
149
|
+
if (!members?.length) return `席: (まだ誰も居ない)\n${bridgeText(bridges)}`
|
|
150
|
+
return `席: ${members.map(memberLine).join(' / ')}\n${bridgeText(bridges)}`
|
|
151
|
+
}
|
|
152
|
+
const deliveryText = (delivery) => {
|
|
153
|
+
const entries = Object.entries(delivery ?? {})
|
|
154
|
+
if (!entries.length) return 'delivery: (TUI配達対象の宛先なし)'
|
|
155
|
+
return `delivery: ${entries.map(([name, d]) => `${name}=${d.state}${d.reason ? `(${d.reason})` : ''}`).join(' / ')}`
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
@@ -134,7 +164,15 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
134
164
|
if (!r.ok) return { isError: true, ...text(`送信失敗: ${JSON.stringify(msg)}`) }
|
|
135
165
|
// cursor は触らない。自分の発言は relevant で除外されるので進める必要が無く、
|
|
136
166
|
// ここで進めると post より前に届いた未読を読まないまま既読にしてしまう(0.2.1 で修正)
|
|
137
|
-
|
|
167
|
+
// 保存と配達は別の事実(決定102)。sent の一語で両者を混ぜず、宛先別の配達状態を返す
|
|
168
|
+
if (msg.room_saved !== true) return text(`sent [${msg.seq}](旧server: 配達状態は取得できない。room保存を配達成功と扱うな)`)
|
|
169
|
+
return text(`room_saved [${msg.seq}](room保存のみ。配達成功ではない)\n${deliveryText(msg.delivery)}\n配達の確定は delivery_status ツール(seq=${msg.seq})で delivered を確認すること`)
|
|
170
|
+
}
|
|
171
|
+
case 'delivery_status': {
|
|
172
|
+
const r = await fetch(api(`deliveries?seq=${Number(args.seq)}`))
|
|
173
|
+
const body = await r.json()
|
|
174
|
+
if (!r.ok) return { isError: true, ...text(`照会失敗: ${JSON.stringify(body)}`) }
|
|
175
|
+
return text(`seq ${body.seq}: room_saved=${body.room_saved}\n${deliveryText(body.delivery)}`)
|
|
138
176
|
}
|
|
139
177
|
case 'read_unread': {
|
|
140
178
|
const { messages } = await (await fetch(api(`messages?since=${cursor}`))).json()
|
|
@@ -148,8 +186,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async req => {
|
|
|
148
186
|
return text(messages.slice(-(args.count ?? 50)).map(fmt).join('\n') || '(ログなし)')
|
|
149
187
|
}
|
|
150
188
|
case 'members': {
|
|
151
|
-
const { members } = await (await fetch(api('members'))).json()
|
|
152
|
-
return text(members.map(memberLine).join('\n') || '(誰も居ない)')
|
|
189
|
+
const { members, bridges } = await (await fetch(api('members'))).json()
|
|
190
|
+
return text(`${members.map(memberLine).join('\n') || '(誰も居ない)'}\n${bridgeText(bridges)}`)
|
|
153
191
|
}
|
|
154
192
|
default:
|
|
155
193
|
throw new Error(`unknown tool: ${req.params.name}`)
|
|
@@ -345,6 +383,10 @@ async function runDiagnostics(asJson) {
|
|
|
345
383
|
'scripts/parent-watch-logic.mjs',
|
|
346
384
|
'scripts/seat-credential.mjs',
|
|
347
385
|
'scripts/ensure-room-mcp.mjs',
|
|
386
|
+
// 既存卓の現行tree同期とteardownのblock単位所有。resume/teardownから呼ぶため一体で必須
|
|
387
|
+
'scripts/room-mcp-config.mjs',
|
|
388
|
+
'scripts/upgrade-team-assets.sh',
|
|
389
|
+
'scripts/remove-managed-room-mcp.mjs',
|
|
348
390
|
'scripts/bridge-record-live.mjs',
|
|
349
391
|
'scripts/leave-seat.sh',
|
|
350
392
|
'scripts/change-seat.sh',
|
|
@@ -357,6 +399,9 @@ async function runDiagnostics(asJson) {
|
|
|
357
399
|
'scripts/wakeup-bridge.mjs',
|
|
358
400
|
'scripts/wakeup-delivery.mjs',
|
|
359
401
|
'scripts/seat-status-bridge.mjs',
|
|
402
|
+
// 円卓開始ゲートと既存 room の正規 resume 入口(決定104・105)。欠けると親の依頼確定と再稼働が手作業へ戻る
|
|
403
|
+
'scripts/kickoff-gate.mjs',
|
|
404
|
+
'scripts/resume.sh',
|
|
360
405
|
// teardown の archive(=解散・既定)が呼ぶ。欠けるとログの写しが取れない
|
|
361
406
|
'scripts/archive-room-log.py',
|
|
362
407
|
'templates/gen-plan.mjs',
|