peertable 0.8.29 → 0.8.31

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.
Files changed (44) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +128 -128
  3. package/README.md +164 -164
  4. package/package.json +50 -50
  5. package/room/Dockerfile +7 -7
  6. package/room/client.mjs +1 -1
  7. package/room/server.mjs +61 -10
  8. package/skill/02_models.snapshot.md +103 -103
  9. package/skill/SKILL.md +261 -261
  10. package/skill/launchd/dev.kitepon.peertable-bridges.plist +20 -20
  11. package/skill/scripts/alarm-set.sh +0 -0
  12. package/skill/scripts/archive-room-log.py +0 -0
  13. package/skill/scripts/bridge-record-live.mjs +0 -0
  14. package/skill/scripts/change-effort.sh +0 -0
  15. package/skill/scripts/change-seat.sh +0 -0
  16. package/skill/scripts/codex-parent-watch.sh +0 -0
  17. package/skill/scripts/doctor.sh +0 -0
  18. package/skill/scripts/ensure-all-bridges.sh +0 -0
  19. package/skill/scripts/ensure-bridge.sh +0 -0
  20. package/skill/scripts/ensure-codex-room-mcp.mjs +0 -0
  21. package/skill/scripts/ensure-project-runtime.sh +0 -0
  22. package/skill/scripts/ensure-room-mcp.mjs +0 -0
  23. package/skill/scripts/external-pane.mjs +0 -0
  24. package/skill/scripts/kickoff-gate.mjs +0 -0
  25. package/skill/scripts/launch-seat.sh +0 -0
  26. package/skill/scripts/leave-seat.sh +0 -0
  27. package/skill/scripts/make-plan-input.mjs +0 -0
  28. package/skill/scripts/parent-join.sh +0 -0
  29. package/skill/scripts/resolve-seat-placement.mjs +0 -0
  30. package/skill/scripts/resume.sh +0 -0
  31. package/skill/scripts/runtime-contract.test.mjs +11 -0
  32. package/skill/scripts/seat-status-bridge.mjs +0 -0
  33. package/skill/scripts/set-mission.sh +0 -0
  34. package/skill/scripts/setup.sh +0 -0
  35. package/skill/scripts/teardown.sh +20 -0
  36. package/skill/scripts/tmux-at.bash +22 -22
  37. package/skill/scripts/upgrade-team-assets.sh +0 -0
  38. package/skill/scripts/wakeup-bridge.mjs +32 -14
  39. package/skill/templates/charter.md +20 -20
  40. package/skill/templates/mcp.json +5 -5
  41. package/skill/templates/member-standalone.md +60 -60
  42. package/skill/templates/member.md +158 -158
  43. package/skill/templates/parent.md +141 -141
  44. package/skill/templates/tasks.md +8 -8
package/README.md CHANGED
@@ -1,164 +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 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
- 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
-
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.*
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
+ 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
+
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,50 +1,50 @@
1
- {
2
- "name": "peertable",
3
- "version": "0.8.29",
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/message-bounds.mjs",
39
- "room/Dockerfile",
40
- "skill/",
41
- "!skill/**/__pycache__/**",
42
- "README.ja.md"
43
- ],
44
- "engines": {
45
- "node": ">=24"
46
- },
47
- "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.0.0"
49
- }
50
- }
1
+ {
2
+ "name": "peertable",
3
+ "version": "0.8.31",
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/message-bounds.mjs",
39
+ "room/Dockerfile",
40
+ "skill/",
41
+ "!skill/**/__pycache__/**",
42
+ "README.ja.md"
43
+ ],
44
+ "engines": {
45
+ "node": ">=24"
46
+ },
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.0.0"
49
+ }
50
+ }
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
@@ -14,7 +14,7 @@ import { boundedRecent, boundedUnread } from './message-bounds.mjs'
14
14
 
15
15
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
16
16
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
17
- const MCP_VERSION = '0.8.29'
17
+ const MCP_VERSION = '0.8.31'
18
18
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
19
19
 
20
20
  const USAGE = `usage:
package/room/server.mjs CHANGED
@@ -235,6 +235,40 @@ function effectiveStatus(member, bridges, now = Date.now()) {
235
235
  }
236
236
  }
237
237
 
238
+ // 状態点とは別に、閲覧者が「いま何をしているか」を読める短い表示をserverで一元生成する。
239
+ // idle/dead/blocked は過去のmissionや発言を現在形で再表示しない。busyだけ、本人の最新の
240
+ // [次の行動] / [claim] / [引受] / [作業中] を現在作業として採る。
241
+ const ACTIVITY_PREFIX = /^\[(?:次の行動|claim|引受|作業中)\]\s*/i
242
+ function compactActivity(body) {
243
+ const plain = String(body).replace(ACTIVITY_PREFIX, '').replace(/\s+/g, ' ').trim()
244
+ const chars = [...plain]
245
+ return chars.length <= 120 ? plain : `${chars.slice(0, 119).join('')}…`
246
+ }
247
+ function readActivityMessages(room) {
248
+ if (!existsSync(room.logPath)) return { messages: [], readable: true }
249
+ let readable = true
250
+ const messages = []
251
+ for (const line of readFileSync(room.logPath, 'utf8').split('\n').filter(Boolean)) {
252
+ try { messages.push(JSON.parse(line)) } catch { readable = false }
253
+ }
254
+ return { messages, readable }
255
+ }
256
+ function memberActivity(member, effective, activityLog) {
257
+ const status = effective.status_effective
258
+ if (status === 'idle') return { activity_text: '待機中', activity_at: null }
259
+ if (status === 'dead') return { activity_text: 'セッション停止', activity_at: null }
260
+ if (status === 'blocked') return { activity_text: '承認操作待ち', activity_at: null }
261
+ if (status !== 'busy') return { activity_text: '稼働状態を確認できません', activity_at: null }
262
+ for (let index = activityLog.messages.length - 1; index >= 0; index--) {
263
+ const message = activityLog.messages[index]
264
+ if (message.from !== member.name || typeof message.body !== 'string' || !ACTIVITY_PREFIX.test(message.body)) continue
265
+ const text = compactActivity(message.body)
266
+ if (text) return { activity_text: text, activity_at: message.ts ?? null }
267
+ }
268
+ if (!activityLog.readable) return { activity_text: '作業情報を取得できません', activity_at: null }
269
+ return { activity_text: '作業内容未報告', activity_at: null }
270
+ }
271
+
238
272
  // ---- 配送状態の導出(決定102)-----------------------------------------------------
239
273
  // receipt(wakeup-bridge の実投入記録)が正。無い宛先は member 台帳と bridge 台帳から
240
274
  // pending / seat_unavailable / bridge_unavailable を導出する。room_saved だけでは配達と言わない。
@@ -353,12 +387,16 @@ http.createServer(async (req, res) => {
353
387
  if (req.method === 'GET' && rest === 'members') {
354
388
  const now = Date.now()
355
389
  const bridges = bridgeHealth(room.name, now)
390
+ const activityLog = readActivityMessages(room)
356
391
  return json(res, 200, {
357
- members: listMembers(room.name).map(m => ({ ...m, ...effectiveStatus(m, bridges, now) })),
392
+ members: listMembers(room.name).map(m => {
393
+ const effective = effectiveStatus(m, bridges, now)
394
+ return { ...m, ...effective, ...memberActivity(m, effective, activityLog) }
395
+ }),
358
396
  bridges,
359
397
  capabilities: {
360
398
  member_observation_v1: true, member_ledger_v1: true,
361
- effective_status_v1: true, delivery_receipt_v1: true,
399
+ effective_status_v1: true, member_activity_v1: true, delivery_receipt_v1: true,
362
400
  },
363
401
  }, CORS)
364
402
  }
@@ -439,6 +477,13 @@ http.createServer(async (req, res) => {
439
477
  authFailures.delete(`${room.name}/${kind}`)
440
478
  return json(res, 200, { ok: true })
441
479
  }
480
+ // archive teardown はroomと会話ログを残すため、停止済みbridge台帳だけを消す。
481
+ // 消さないと空のarchive roomへstatus_bridge_down警告が恒久表示される。
482
+ if (req.method === 'DELETE' && rest === 'bridges') {
483
+ db.prepare('DELETE FROM bridges WHERE room = ?').run(room.name)
484
+ for (const kind of BRIDGE_KINDS) authFailures.delete(`${room.name}/${kind}`)
485
+ return json(res, 200, { ok: true })
486
+ }
442
487
  // 配送 receipt(決定102)。書き手は wakeup-bridge だけ。同じ (seq, recipient) は最新で上書き
443
488
  // (seat_unavailable → 再試行成功 delivered の遷移を残すため)
444
489
  if (req.method === 'POST' && rest === 'deliveries') {
@@ -524,12 +569,14 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
524
569
  <title>${esc(room)} · Peertable</title>${FAVICON}<style>${STYLE}
525
570
  .top{position:sticky;top:0;z-index:2;background:var(--bg);border-bottom:1px solid var(--line);padding:12px 16px 0}
526
571
  .top>div{max-width:760px;margin:0 auto}
527
- .members{display:flex;gap:6px;overflow-x:auto;padding:10px 0;scrollbar-width:thin}
572
+ .members{display:flex;gap:8px;overflow-x:auto;padding:10px 0;scrollbar-width:thin}
528
573
  .bridgewarn{color:var(--busy);font-size:12px;font-weight:650;padding:0 0 8px}
529
574
  .bridgewarn:empty{display:none}
530
575
  .chip.has-meta{cursor:pointer}
531
576
  /* 稼働状態の点。報告が途絶えたら unknown(中空)へ落として、古い状態を出し続けない */
532
577
  .chip .nm{display:inline-flex;align-items:center;gap:5px}
578
+ .chip .state-label{font-size:10px;font-weight:700;color:var(--dim);white-space:nowrap}
579
+ .chip .activity{display:block;max-width:190px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dim);font-size:10px;font-weight:500}
533
580
  .chip .st{flex:none;display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dim)}
534
581
  .chip .st.busy{background:var(--busy)}
535
582
  .chip .st.idle{background:var(--idle)}
@@ -539,7 +586,7 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
539
586
  .metapop{position:fixed;z-index:20;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-size:12px;box-shadow:0 6px 20px rgba(0,0,0,.18);max-width:70vw}
540
587
  .metapop .metaname{font-weight:600;margin-bottom:2px}
541
588
  .metapop .metaline{color:var(--dim)}
542
- .chip{position:relative;flex:none;display:flex;align-items:center;gap:7px;padding:5px 11px 5px 5px;border:1px solid var(--line);border-radius:12px;background:var(--surface);font-size:12px;font-weight:600}
589
+ .chip{position:relative;flex:none;display:flex;align-items:center;gap:7px;min-width:150px;max-width:230px;padding:6px 11px 6px 6px;border:1px solid var(--line);border-radius:12px;background:var(--surface);font-size:12px;font-weight:600}
543
590
  .chip .av{width:22px;height:22px;font-size:11px;flex:none}
544
591
  .chip .id{display:flex;flex-direction:column;gap:1px;min-width:0;line-height:1.25}
545
592
  .chip.recent{border-color:hsl(var(--h) var(--sat) var(--edge))}
@@ -755,8 +802,10 @@ async function refreshMembers(){
755
802
  // 閲覧面ごとに独自の鮮度判定を持つと、MCP と Web UI で違う判定になり誤認が再発する
756
803
  const st=m.status_effective??null
757
804
  if(st)c.classList.add('is-'+st)
805
+ const stateText=st?({busy:'作業中',idle:'待機',dead:'停止',blocked:'承認待ち',unknown:'不明'}[st]??st):'不明'
758
806
  const reason={status_unreported:'未報告(seat-status bridge が動いていない)',status_heartbeat_stale:'状態heartbeatが途絶えている',status_bridge_down:'状態bridgeが停止(status_bridge_down)',status_bridge_unreported:'状態bridgeが未登録',bridge_auth_failed:'bridge認証失敗(403)'}[m.status_reason]
759
- meta.push(st?'状態 '+({busy:'作業中',idle:'待機',dead:'停止',blocked:'承認待ち',unknown:'不明'}[st]??st)+(reason?'('+reason+')':''):'状態 未取得(旧serverは実効状態を返さない)')
807
+ meta.push(st?'状態 '+stateText+(reason?'('+reason+')':''):'状態 未取得(旧serverは実効状態を返さない)')
808
+ meta.push('現在 '+(m.activity_text??'未取得(旧server)'))
760
809
  const usage=[]
761
810
  const busyAge=m.busy_since?Date.now()-Date.parse(m.busy_since):NaN
762
811
  if((st==='busy'||st==='blocked')&&Number.isFinite(busyAge)&&busyAge>=0)usage.push('継続 '+elapsed(busyAge))
@@ -766,12 +815,12 @@ async function refreshMembers(){
766
815
  c.appendChild(el('span','av',initial(m.name)))
767
816
  const id=el('span','id')
768
817
  const nameRow=el('span','nm',m.name)
769
- // チップの常設表示は「アバター・名前・◉」だけ。◉は状態が新鮮な時だけ色が付き、
770
- // 途絶・未報告は中空リング=bridge が動いていないことがひと目で分かる。
771
- // roles / settings / mission は title(ホバー)とタップ popover にだけ出す。
772
818
  nameRow.appendChild(el('span','st '+(st??'unknown')))
819
+ nameRow.appendChild(el('span','state-label',stateText))
773
820
  id.appendChild(nameRow)
821
+ id.appendChild(el('span','activity',m.activity_text??'現在作業未取得'))
774
822
  c.appendChild(id)
823
+ c.setAttribute('aria-label',m.name+'、'+stateText+'、'+(m.activity_text??'現在作業未取得'))
775
824
  // タップ環境には hover が無いので、押した時に同じ内容を出す(ホバーは title が担う)
776
825
  if(meta.length){c.classList.add('has-meta');c.addEventListener('click',ev=>{ev.stopPropagation();showMeta(c,m,meta)})}
777
826
  membersEl.appendChild(c)
@@ -821,6 +870,8 @@ function celebrate(name){
821
870
  }
822
871
  const BEAT=${HEARTBEAT_MS}
823
872
  let lastSeq=0,lastBeat=Date.now(),es=null,emptyEl=null,firstLoad=true,catching=false,memberDebounce=null
873
+ const scheduleMemberRefresh=()=>{clearTimeout(memberDebounce);memberDebounce=setTimeout(refreshMembers,150)}
874
+ const isActivityMessage=m=>m.from!=='system'&&typeof m.body==='string'&&/^(?:\[(?:次の行動|claim|引受|作業中)\])/i.test(m.body)
824
875
  // seq で二重描画を弾く。張り直し後の追いつきと SSE の新着が重なっても同じ発言は1回しか出ない
825
876
  function apply(m,live=false){
826
877
  if(m.seq<=lastSeq)return false
@@ -859,12 +910,12 @@ function connect(){
859
910
  // 場合は、この差分だけが手掛かりになる
860
911
  es.addEventListener('ping',e=>{lastBeat=Date.now();if(Number(e.data)>lastSeq)catchUp()})
861
912
  // 稼働状態・素性の変化。既存の refreshMembers() を150msデバウンスで呼ぶ(部分更新は実装しない)
862
- es.addEventListener('member',()=>{clearTimeout(memberDebounce);memberDebounce=setTimeout(refreshMembers,150)})
913
+ es.addEventListener('member',scheduleMemberRefresh)
863
914
  es.onmessage=e=>{
864
915
  lastBeat=Date.now()
865
916
  const m=JSON.parse(e.data),stick=nearBottom()
866
917
  if(!apply(m,true))return
867
- if(m.from==='system')refreshMembers();else{markActive(m.from);if(isCompletion(m))celebrate(m.from)}
918
+ if(m.from==='system')refreshMembers();else{markActive(m.from);if(isActivityMessage(m))scheduleMemberRefresh();if(isCompletion(m))celebrate(m.from)}
868
919
  if(stick)window.scrollTo(0,document.body.scrollHeight)
869
920
  syncToBottom()
870
921
  }