@sztlink/pi-ensemble 0.1.0-alpha.12

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/CHANGELOG.md ADDED
@@ -0,0 +1,103 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0-alpha.12
4
+
5
+ - Documented neutral-root runtime usage: keep Pi/Claude in their natural runtime root and point to the canonical ledger with `PI_ENSEMBLE_ROOT` or `--root`.
6
+ - Updated tmux wake adapter prompt to use `PI_ENSEMBLE_ROOT=... ensemble ...` instead of requiring `cd` into the ledger root.
7
+ - Refreshed GitHub install docs for the latest alpha.
8
+
9
+ ## v0.1.0-alpha.11
10
+
11
+ - Added lightweight message lifecycle without adding orchestration:
12
+ - generated message ids on `ensemble send`;
13
+ - inbox headers include `{#msg_...}` anchors;
14
+ - new `ensemble ack MESSAGE_ID` audit event;
15
+ - new `ensemble done MESSAGE_ID` resolution audit event;
16
+ - new `ensemble messages [--open]` read-only lifecycle view;
17
+ - `overview` includes recent open messages.
18
+ - Added matching Pi command/tool actions and tests.
19
+
20
+ ## v0.1.0-alpha.10
21
+
22
+ - Added `ensemble doctor` read-only ledger health checks:
23
+ - required protocol files;
24
+ - config protocol version;
25
+ - audit JSONL parse issues;
26
+ - agent name validity;
27
+ - unread/retained inbox summaries;
28
+ - claim path/owner sanity;
29
+ - nested `.pi-ensemble` detection for root-confusion dogfood.
30
+ - Added matching Pi command/tool action and tests.
31
+
32
+ ## v0.1.0-alpha.9
33
+
34
+ - Added dogfood ergonomics for retained inboxes:
35
+ - per-agent `lastReadAt` state;
36
+ - `unread` and `stale` counts in status/overview JSON;
37
+ - `ensemble inbox --since-last-read` for focused new-message reads without clearing history.
38
+ - Updated overview text to show `total` vs `unread` instead of treating all retained messages as urgent.
39
+ - Updated tmux wake prompts to suggest `--since-last-read`, reducing duplicate/manual relay friction.
40
+
41
+ ## v0.1.0-alpha.8
42
+
43
+ - Added canonical root overrides for nested workspaces:
44
+ - CLI `--root PATH`;
45
+ - `PI_ENSEMBLE_ROOT` environment variable;
46
+ - Pi tool `root` parameter;
47
+ - Pi slash command `--root PATH`.
48
+ - Documented root resolution to avoid accidental use of nested `.pi-ensemble/` ledgers.
49
+
50
+ ## v0.1.0-alpha.7 — publish candidate
51
+
52
+ - Documented GitHub Pi package install path.
53
+ - Added changelog and release framing for first public alpha.
54
+ - Confirmed project-local Pi install from `git:github.com/sztlink/pi-ensemble@v0.1.0-alpha.6` in a clean temp workspace.
55
+
56
+ ## v0.1.0-alpha.6
57
+
58
+ - Added read-only observability:
59
+ - `ensemble overview`
60
+ - `ensemble timeline`
61
+ - Added matching Pi tool/command actions.
62
+ - Expanded tests for overview/timeline.
63
+
64
+ ## v0.1.0-alpha.5
65
+
66
+ - Added ledger inspection commands:
67
+ - `ensemble claims`
68
+ - `ensemble audit`
69
+ - Added protocol version to `status`.
70
+ - Expanded tests for audit and claims.
71
+
72
+ ## v0.1.0-alpha.4
73
+
74
+ - Documented Claude Code Agent Teams interop.
75
+ - Added runtime recipes for Pi, Claude Code, Codex/generic terminal agents, shell scripts, CI/watchers, and tmux.
76
+ - Added Claude lead-session prompt example.
77
+
78
+ ## v0.1.0-alpha.3
79
+
80
+ - Formalized adapter contract.
81
+ - Added public `examples/ensemble-tmux` adapter.
82
+ - Made tmux wake prompts shell-safe with `#` prefix.
83
+
84
+ ## v0.1.0-alpha.2
85
+
86
+ - Hardened core ledger:
87
+ - agent name validation;
88
+ - message type validation;
89
+ - JSON outputs for adapter-facing commands;
90
+ - claim conflict protection and force override;
91
+ - blackboard recovery.
92
+ - Added Node test suite.
93
+ - Added quickstart and expanded spec.
94
+
95
+ ## v0.1.0-alpha.1
96
+
97
+ - Added hybrid runtime adapter documentation.
98
+ - Bumped alpha after documenting tmux/Pi/Claude boundaries.
99
+
100
+ ## v0.1.0-alpha.0
101
+
102
+ - Initial public alpha.
103
+ - CLI, core file protocol, Pi extension, blackboard, inboxes, claims, audit log, docs, and security boundary.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 szt.link
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.md ADDED
@@ -0,0 +1,142 @@
1
+ # pi-ensemble
2
+
3
+ [![CI](https://github.com/sztlink/pi-ensemble/actions/workflows/ci.yml/badge.svg)](https://github.com/sztlink/pi-ensemble/actions/workflows/ci.yml)
4
+
5
+ Shared workspace coordination ledger for parallel coding agents.
6
+
7
+ `pi-ensemble` is a small local coordination ledger: blackboard + mailbox + claims + audit for developers who run multiple coding agents side by side. It is designed for Pi, Claude Code, Codex, or any terminal agent that can read and write files.
8
+
9
+ It is **not** a daemon, process supervisor, remote-control system, or agent auto-runner.
10
+
11
+ ## Why
12
+
13
+ When multiple coding agents work in parallel, the human becomes the relay: copy message here, paste result there, remember who owns which worktree. `pi-ensemble` turns that relay into a transparent local file protocol:
14
+
15
+ ```txt
16
+ .pi-ensemble/
17
+ config.yaml
18
+ blackboard.md
19
+ agents/<name>/inbox.md
20
+ agents/<name>/state.json
21
+ worktrees.json
22
+ audit.jsonl
23
+ ```
24
+
25
+ Files are the protocol. If the tool disappears, the state is still readable.
26
+
27
+ ## Quickstart
28
+
29
+ See [`docs/QUICKSTART.md`](docs/QUICKSTART.md) for the shortest path. See [`docs/CLAUDE_AGENT_TEAMS.md`](docs/CLAUDE_AGENT_TEAMS.md) and [`docs/RUNTIME_RECIPES.md`](docs/RUNTIME_RECIPES.md) for interop patterns.
30
+
31
+ ## Install
32
+
33
+ ### Pi package from GitHub
34
+
35
+ ```bash
36
+ pi install git:github.com/sztlink/pi-ensemble@v0.1.0-alpha.12
37
+ ```
38
+
39
+ Reload Pi or start a new session, then run:
40
+
41
+ ```txt
42
+ /ensemble status
43
+ ```
44
+
45
+ ### Local development
46
+
47
+ ```bash
48
+ cd /path/to/repo
49
+ pi install /absolute/path/to/pi-ensemble
50
+ ```
51
+
52
+ ### CLI only
53
+
54
+ ```bash
55
+ node /absolute/path/to/pi-ensemble/bin/ensemble.mjs init
56
+ ```
57
+
58
+ ### npm
59
+
60
+ The npm package is not published yet. Once published:
61
+
62
+ ```bash
63
+ pi install npm:@sztlink/pi-ensemble@alpha
64
+ ```
65
+
66
+ ## CLI
67
+
68
+ Use `--root PATH` or `PI_ENSEMBLE_ROOT=/path/to/workspace` when running from nested repositories, subdirectories, or neutral runtime roots. This lets Pi/Claude stay in their natural session root while sharing one canonical ledger elsewhere.
69
+
70
+ ```bash
71
+ ensemble --root /path/to/workspace init [--agent pi]
72
+ ensemble --root /path/to/workspace status
73
+ ensemble note "message" [--from pi]
74
+ ensemble send claude "handoff" [--from pi] [--type handoff]
75
+ ensemble ack msg_xxx [--from claude] [--body "received"]
76
+ ensemble done msg_xxx [--from pi] [--body "resolved"]
77
+ ensemble messages [--open] [--limit 50] [--json]
78
+ ensemble inbox [--agent pi] [--no-clear] [--since-last-read] [--clear] [--json]
79
+ ensemble board [--json]
80
+ ensemble claims [--json]
81
+ ensemble audit [--limit 50] [--json]
82
+ ensemble timeline [--limit 50] [--json]
83
+ ensemble overview [--limit 10] [--json]
84
+ ensemble doctor [--json]
85
+ ensemble claim ./worktree-or-path [--agent pi] [--force] [--json]
86
+ ensemble release ./worktree-or-path [--agent pi] [--force] [--json]
87
+ ```
88
+
89
+ Allowed message types: `note`, `handoff`, `question`, `result`, `ack`.
90
+
91
+ Inbox reads update per-agent `lastReadAt`. Use `--since-last-read` for focused wakeups: it prints only new messages, marks them read, and keeps retained history in `inbox.md`. `overview` reports both total retained messages and unread counts.
92
+
93
+ Use `ensemble doctor` when a workflow feels off: it checks required files, protocol version, audit log parse health, claims, agent names, inbox state, and nested `.pi-ensemble` folders that can cause root confusion.
94
+
95
+ Every `send` returns a message id and writes an inbox anchor like `{#msg_...}`. Use `ack` and `done` for lightweight traceability of handoffs/questions. They append audit events only; they do not schedule, route, or supervise agents.
96
+
97
+ Canonical/root override examples:
98
+
99
+ ```bash
100
+ # Neutral-root runtime: stay wherever the agent naturally starts, point at the ledger.
101
+ PI_ENSEMBLE_ROOT=/home/aya/implante ensemble overview
102
+ PI_ENSEMBLE_ROOT=/home/aya/implante ensemble inbox --agent claude --since-last-read
103
+ PI_ENSEMBLE_ROOT=/home/aya/implante ensemble send pi "Result: ..." --from claude --type result
104
+
105
+ # Equivalent explicit flag:
106
+ ensemble --root /home/aya/implante inbox --agent pi --since-last-read
107
+ ```
108
+
109
+ ## Pi commands
110
+
111
+ When installed as a Pi package, the extension exposes:
112
+
113
+ ```txt
114
+ /ensemble init
115
+ /ensemble status
116
+ /ensemble note <message>
117
+ /ensemble send <agent> <message> [--type note|handoff|question|result|ack]
118
+ /ensemble inbox
119
+ /ensemble board
120
+ /ensemble claim <path>
121
+ /ensemble release <path>
122
+ ```
123
+
124
+ It also exposes an `ensemble` tool for the parent agent to perform the same file-only operations.
125
+
126
+ ## v0.1 boundaries
127
+
128
+ See [`SECURITY.md`](SECURITY.md). In short: no network, no spawning, no command execution, no credentials, no hidden persistence, no remote sessions, no automatic routing.
129
+
130
+ ## Hybrid runtimes
131
+
132
+ Pi can use the package extension and tool directly. Claude Code can participate directly or through a lead session that also uses Agent Teams internally. Codex and other terminal agents can participate through the same CLI/files. Tmux wakeups should remain an adapter outside the core protocol. See [`docs/ADAPTERS.md`](docs/ADAPTERS.md) and [`examples/ensemble-tmux`](examples/ensemble-tmux).
133
+
134
+ ## Relationship to existing workflows
135
+
136
+ `pi-ensemble` generalizes a simple bridge pattern: blackboard for durable shared facts, inboxes for handoffs, audit log for traceability. Integrations with tmux, watchers, or external dashboards should remain outside v0.1.
137
+
138
+ See [`docs/LANDSCAPE.md`](docs/LANDSCAPE.md) for a benchmark of related Claude Code, Pi, tmux, and terminal-agent orchestrators and why `pi-ensemble` stays smaller: local file protocol, not mission control. See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the repositioning from would-be orchestrator toward neutral ledger + adapter protocol.
139
+
140
+ ## Repository decision
141
+
142
+ Recommended public home: `sztlink/pi-ensemble` as a standalone repository, not inside a benchmark or application repo. The protocol is generic and should not inherit TurboQuant-specific context.
package/SECURITY.md ADDED
@@ -0,0 +1,27 @@
1
+ # Security Principles — pi-ensemble
2
+
3
+ pi-ensemble is a local coordination layer for coding agents. These principles are fixed for v0.1; any violation is a bug, not a feature.
4
+
5
+ ## What pi-ensemble does
6
+
7
+ - Reads and writes local files inside `.pi-ensemble/`.
8
+ - Provides structured handoff, mailbox, blackboard, and worktree-claim files.
9
+ - Logs inter-agent events to `audit.jsonl` for human review.
10
+
11
+ ## What pi-ensemble never does in v0.1
12
+
13
+ 1. **No network** — no HTTP, sockets, cloud sync, or webhooks.
14
+ 2. **No process spawning** — never creates, wakes, kills, or supervises agents.
15
+ 3. **No code execution** — never runs commands on behalf of an agent.
16
+ 4. **No credentials** — never handles, stores, extracts, or proxies secrets.
17
+ 5. **No hidden persistence** — no daemons, cron jobs, launch agents, or systemd units.
18
+ 6. **No remote sessions** — designed for single-user local development only.
19
+ 7. **No automatic routing** — humans decide which agent receives which handoff.
20
+
21
+ ## Threat model
22
+
23
+ An actor who can write to `.pi-ensemble/` can place messages in agent inboxes. Treat inboxes and the blackboard like source files: review them before acting, keep normal filesystem permissions, and do not store secrets there.
24
+
25
+ ## Reporting
26
+
27
+ If you discover behavior that violates these principles, open an issue or remove the package. The safe failure mode is to delete `.pi-ensemble/` and recreate it.
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ack,
4
+ claim,
5
+ claims,
6
+ defaultAgent,
7
+ doctor,
8
+ done,
9
+ init,
10
+ messages,
11
+ note,
12
+ overview,
13
+ readAudit,
14
+ readBoard,
15
+ readInbox,
16
+ release,
17
+ requireWorkspaceRoot,
18
+ send,
19
+ status,
20
+ timeline,
21
+ } from '../lib/core.mjs';
22
+
23
+ function usage() {
24
+ console.log(`pi-ensemble
25
+
26
+ Usage:
27
+ ensemble [--root PATH] init [--agent NAME]
28
+ ensemble [--root PATH] status
29
+ ensemble note MESSAGE [--from NAME] [--json]
30
+ ensemble send AGENT MESSAGE [--from NAME] [--type note|handoff|question|result|ack] [--json]
31
+ ensemble ack MESSAGE_ID [--from NAME] [--body TEXT] [--json]
32
+ ensemble done MESSAGE_ID [--from NAME] [--body TEXT] [--json]
33
+ ensemble messages [--limit N] [--open] [--json]
34
+ ensemble inbox [--agent NAME] [--no-clear] [--since-last-read] [--clear] [--json]
35
+ ensemble board [--json]
36
+ ensemble claims [--json]
37
+ ensemble audit [--limit N] [--json]
38
+ ensemble timeline [--limit N] [--json]
39
+ ensemble overview [--limit N] [--json]
40
+ ensemble doctor [--json]
41
+ ensemble claim PATH [--agent NAME] [--force] [--json]
42
+ ensemble release PATH [--agent NAME] [--force] [--json]
43
+ `);
44
+ }
45
+
46
+ function takeFlag(args, name, fallback = undefined) {
47
+ const i = args.indexOf(name);
48
+ if (i === -1) return fallback;
49
+ const value = args[i + 1];
50
+ args.splice(i, 2);
51
+ return value ?? fallback;
52
+ }
53
+
54
+ function hasFlag(args, name) {
55
+ const i = args.indexOf(name);
56
+ if (i === -1) return false;
57
+ args.splice(i, 1);
58
+ return true;
59
+ }
60
+
61
+ let explicitRoot;
62
+
63
+ function root() {
64
+ return requireWorkspaceRoot(explicitRoot || process.env.PI_ENSEMBLE_ROOT || process.cwd());
65
+ }
66
+
67
+ function initRoot() {
68
+ return explicitRoot || process.env.PI_ENSEMBLE_ROOT || process.cwd();
69
+ }
70
+
71
+ function printJson(value) {
72
+ console.log(JSON.stringify(value, null, 2));
73
+ }
74
+
75
+ function formatTimeline(rows) {
76
+ return rows.map(row => `${row.ts ?? '?'} ${row.action ?? '?'} — ${row.summary}`).join('\n') + (rows.length ? '\n' : '');
77
+ }
78
+
79
+ function formatOverview(value) {
80
+ const lines = [];
81
+ lines.push(`root: ${value.root}`);
82
+ lines.push(`version: ${value.version}`);
83
+ lines.push(`agents: ${value.agents.map(a => `${a.agent}(${a.pending} total, ${a.unread} unread)`).join(', ') || 'none'}`);
84
+ lines.push(`unread: ${value.unread.map(a => a.agent).join(', ') || 'none'}`);
85
+ lines.push(`retained: ${value.stale.map(a => a.agent).join(', ') || 'none'}`);
86
+ lines.push(`claims: ${value.claims.length}`);
87
+ for (const claim of value.claims) lines.push(` - ${claim.agent}: ${claim.path}`);
88
+ lines.push(`open messages: ${value.openMessages.length}`);
89
+ for (const message of value.openMessages) lines.push(` - ${message.messageId}: ${message.from} → ${message.to} [${message.type}] ${message.status}`);
90
+ lines.push('recent:');
91
+ for (const row of value.recent) lines.push(` - ${row.ts ?? '?'} ${row.action ?? '?'} — ${row.summary}`);
92
+ return lines.join('\n') + '\n';
93
+ }
94
+
95
+ function formatMessages(rows) {
96
+ return rows.map(row => `${row.messageId} ${row.status} — ${row.from ?? '?'} → ${row.to ?? '?'} [${row.type ?? '?'}]${row.doneBy ? ` done by ${row.doneBy}` : ''}`).join('\n') + (rows.length ? '\n' : '');
97
+ }
98
+
99
+ function formatDoctor(value) {
100
+ const lines = [];
101
+ lines.push(`root: ${value.root}`);
102
+ lines.push(`ok: ${value.ok ? 'yes' : 'no'}`);
103
+ lines.push(`summary: ${value.summary.pass} pass, ${value.summary.info} info, ${value.summary.warn} warn, ${value.summary.fail} fail`);
104
+ for (const check of value.checks) {
105
+ const mark = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : check.status === 'warn' ? '!' : 'i';
106
+ lines.push(`${mark} ${check.name}: ${check.message}`);
107
+ if (check.details) lines.push(` details: ${JSON.stringify(check.details)}`);
108
+ }
109
+ return lines.join('\n') + '\n';
110
+ }
111
+
112
+ try {
113
+ const args = process.argv.slice(2);
114
+ explicitRoot = takeFlag(args, '--root', undefined);
115
+ const cmd = args.shift() || 'help';
116
+ if (!explicitRoot) explicitRoot = takeFlag(args, '--root', undefined);
117
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
118
+ usage();
119
+ } else if (cmd === 'init') {
120
+ const agent = takeFlag(args, '--agent', defaultAgent());
121
+ const r = init(initRoot(), { agent });
122
+ console.log(`initialized ${r.dir} (agent: ${r.agent})`);
123
+ } else if (cmd === 'status') {
124
+ const s = status(root());
125
+ printJson(s);
126
+ } else if (cmd === 'note') {
127
+ const from = takeFlag(args, '--from', defaultAgent());
128
+ const json = hasFlag(args, '--json');
129
+ const body = args.join(' ');
130
+ const result = note(root(), { from, body });
131
+ json ? printJson(result) : console.log('noted');
132
+ } else if (cmd === 'send') {
133
+ const from = takeFlag(args, '--from', defaultAgent());
134
+ const type = takeFlag(args, '--type', 'handoff');
135
+ const json = hasFlag(args, '--json');
136
+ const to = args.shift();
137
+ const body = args.join(' ');
138
+ const result = send(root(), { from, to, type, body });
139
+ json ? printJson(result) : console.log(`sent to ${to}: ${result.messageId}`);
140
+ } else if (cmd === 'ack') {
141
+ const from = takeFlag(args, '--from', defaultAgent());
142
+ const body = takeFlag(args, '--body', '');
143
+ const json = hasFlag(args, '--json');
144
+ const messageId = args.shift();
145
+ const result = ack(root(), { from, messageId, body: body || args.join(' ') });
146
+ json ? printJson(result) : console.log(`acked ${messageId}`);
147
+ } else if (cmd === 'done') {
148
+ const from = takeFlag(args, '--from', defaultAgent());
149
+ const body = takeFlag(args, '--body', '');
150
+ const json = hasFlag(args, '--json');
151
+ const messageId = args.shift();
152
+ const result = done(root(), { from, messageId, body: body || args.join(' ') });
153
+ json ? printJson(result) : console.log(`resolved ${messageId}`);
154
+ } else if (cmd === 'messages') {
155
+ const json = hasFlag(args, '--json');
156
+ const open = hasFlag(args, '--open');
157
+ const limit = Number(takeFlag(args, '--limit', '50'));
158
+ const result = messages(root(), { limit: Number.isFinite(limit) ? limit : 50, open });
159
+ json ? printJson(result) : process.stdout.write(formatMessages(result));
160
+ } else if (cmd === 'inbox') {
161
+ const agent = takeFlag(args, '--agent', defaultAgent());
162
+ const sinceLastRead = hasFlag(args, '--since-last-read');
163
+ const clearFlag = hasFlag(args, '--clear');
164
+ const noClear = hasFlag(args, '--no-clear');
165
+ const json = hasFlag(args, '--json');
166
+ const clear = clearFlag ? true : sinceLastRead ? false : !noClear;
167
+ const content = readInbox(root(), { agent, clear, sinceLastRead });
168
+ json ? printJson({ agent, clear, sinceLastRead, content }) : process.stdout.write(content);
169
+ } else if (cmd === 'board') {
170
+ const json = hasFlag(args, '--json');
171
+ const content = readBoard(root());
172
+ json ? printJson({ content }) : process.stdout.write(content);
173
+ } else if (cmd === 'claims') {
174
+ const json = hasFlag(args, '--json');
175
+ const result = claims(root());
176
+ json ? printJson(result) : printJson(result);
177
+ } else if (cmd === 'audit') {
178
+ const json = hasFlag(args, '--json');
179
+ const limit = Number(takeFlag(args, '--limit', '50'));
180
+ const result = readAudit(root(), { limit: Number.isFinite(limit) ? limit : 50 });
181
+ json ? printJson(result) : process.stdout.write(result.map(r => JSON.stringify(r)).join('\n') + (result.length ? '\n' : ''));
182
+ } else if (cmd === 'timeline') {
183
+ const json = hasFlag(args, '--json');
184
+ const limit = Number(takeFlag(args, '--limit', '50'));
185
+ const result = timeline(root(), { limit: Number.isFinite(limit) ? limit : 50 });
186
+ json ? printJson(result) : process.stdout.write(formatTimeline(result));
187
+ } else if (cmd === 'overview') {
188
+ const json = hasFlag(args, '--json');
189
+ const limit = Number(takeFlag(args, '--limit', '10'));
190
+ const result = overview(root(), { limit: Number.isFinite(limit) ? limit : 10 });
191
+ json ? printJson(result) : process.stdout.write(formatOverview(result));
192
+ } else if (cmd === 'doctor') {
193
+ const json = hasFlag(args, '--json');
194
+ const result = doctor(root());
195
+ json ? printJson(result) : process.stdout.write(formatDoctor(result));
196
+ } else if (cmd === 'claim') {
197
+ const agent = takeFlag(args, '--agent', defaultAgent());
198
+ const force = hasFlag(args, '--force');
199
+ const json = hasFlag(args, '--json');
200
+ const targetPath = args.join(' ');
201
+ const result = claim(root(), { agent, targetPath, force });
202
+ json ? printJson(result) : console.log(`claimed ${targetPath}`);
203
+ } else if (cmd === 'release') {
204
+ const agent = takeFlag(args, '--agent', defaultAgent());
205
+ const force = hasFlag(args, '--force');
206
+ const json = hasFlag(args, '--json');
207
+ const targetPath = args.join(' ');
208
+ const result = release(root(), { agent, targetPath, force });
209
+ json ? printJson(result) : console.log(`released ${targetPath}`);
210
+ } else {
211
+ usage();
212
+ process.exitCode = 2;
213
+ }
214
+ } catch (err) {
215
+ console.error(err instanceof Error ? err.message : String(err));
216
+ process.exitCode = 1;
217
+ }
@@ -0,0 +1,159 @@
1
+ # pi-ensemble adapters
2
+
3
+ `pi-ensemble` core is intentionally file-only. It does not spawn agents, send tmux keys, open sockets, or supervise processes.
4
+
5
+ Adapters sit beside the core ledger to connect specific runtimes to the same files.
6
+
7
+ ## Adapter contract
8
+
9
+ Adapters MAY:
10
+
11
+ - call the `ensemble` CLI or tool;
12
+ - write normal protocol messages through `note`, `send`, `claim`, and `release`;
13
+ - read `status`, `board`, `inbox`, `worktrees.json`, and `audit.jsonl`;
14
+ - keep runtime-specific local config outside the core protocol;
15
+ - wake panes, render dashboards, mirror events, or bridge other queues.
16
+
17
+ Adapters MUST:
18
+
19
+ - keep durable coordination state in `.pi-ensemble/`;
20
+ - put long messages in inbox/files, not in transport-specific prompts;
21
+ - preserve human readability;
22
+ - leave an audit trail for state-changing protocol operations;
23
+ - tolerate missing agents/inboxes by creating or reporting them explicitly;
24
+ - fail closed when a target runtime/pane is missing.
25
+
26
+ Adapters MUST NOT:
27
+
28
+ - treat tmux panes, process ids, session ids, provider/model names, or launcher state as core protocol fields;
29
+ - store secrets in `.pi-ensemble/`;
30
+ - hide coordination state in adapter-only databases;
31
+ - mutate core files in shapes that the CLI cannot understand;
32
+ - make the core depend on any specific runtime.
33
+
34
+ Heuristic:
35
+
36
+ ```txt
37
+ Needed to reconstruct ownership, decision, or outcome later? -> core ledger.
38
+ Only needed to operate one runtime right now? -> adapter config/state.
39
+ ```
40
+
41
+ ## Safe wake pattern
42
+
43
+ Transport prompts are lossy and runtime-specific. The safe pattern is:
44
+
45
+ ```bash
46
+ ensemble send claude-lead "full handoff body" --from pi --type handoff
47
+ # out-of-band wake carries only a short pointer
48
+ ```
49
+
50
+ For tmux, wake text should be shell-safe. Prefix with `#` so accidental paste into a shell pane is a harmless comment, while Pi/Claude still receive a readable markdown-style prompt:
51
+
52
+ ```txt
53
+ # pi-ensemble: new inbox item. Run: PI_ENSEMBLE_ROOT=/repo ensemble inbox --agent claude-lead --since-last-read
54
+ ```
55
+
56
+ Do not paste long instructions through tmux. Put them in the inbox.
57
+
58
+ ## Pi adapter
59
+
60
+ Install as a Pi package:
61
+
62
+ ```bash
63
+ pi install /path/to/pi-ensemble
64
+ # or, after publication:
65
+ pi install git:github.com/sztlink/pi-ensemble@v0.1.0-alpha.2
66
+ ```
67
+
68
+ After `/reload` or a new Pi session, Pi exposes:
69
+
70
+ ```txt
71
+ /ensemble status
72
+ /ensemble inbox
73
+ /ensemble note <message>
74
+ /ensemble send <agent> <message> [--type ...]
75
+ ```
76
+
77
+ The registered `ensemble` tool exposes the same operations to the parent agent.
78
+
79
+ Pi-specific guidance:
80
+
81
+ - Pi can act as maestro, but that is a usage pattern, not a core feature.
82
+ - Pi should record only durable facts, claims, handoffs, and outcomes.
83
+ - Pi should use external orchestrators such as Claude Agent Teams when they already solve runtime-level parallelism.
84
+
85
+ ## Claude Code / Agent Teams adapter
86
+
87
+ Claude Code does not need a native plugin for v0.1. It participates through the CLI and files:
88
+
89
+ ```bash
90
+ # From the project root:
91
+ ensemble status
92
+ ensemble inbox --agent claude-lead --since-last-read
93
+ ensemble note "durable fact" --from claude-lead
94
+ ensemble send pi "handoff" --from claude-lead --type handoff
95
+ ensemble claim ./path --agent claude-lead
96
+ ensemble release ./path --agent claude-lead
97
+
98
+ # Or from a neutral runtime root while using a canonical ledger elsewhere:
99
+ PI_ENSEMBLE_ROOT=~/implante ensemble overview
100
+ PI_ENSEMBLE_ROOT=~/implante ensemble inbox --agent claude-lead --since-last-read
101
+ PI_ENSEMBLE_ROOT=~/implante ensemble send pi "Result: ..." --from claude-lead --type result
102
+ ```
103
+
104
+ Recommended Claude lead-session habit:
105
+
106
+ 1. On start, inspect `ensemble overview` from the project root, or `PI_ENSEMBLE_ROOT=/canonical/root ensemble overview` from a neutral runtime root.
107
+ 2. If the lead has unread inbox items, read with `--since-last-read` first.
108
+ 3. If useful, use Claude Code Agent Teams internally.
109
+ 4. Mirror only durable milestones to `pi-ensemble`:
110
+ - accepted task frame;
111
+ - claimed files/worktrees;
112
+ - result pointers;
113
+ - final handoff;
114
+ - blockers requiring another runtime.
115
+ 5. Keep ephemeral intra-team chatter inside Claude's team system.
116
+
117
+ ## tmux adapter
118
+
119
+ Tmux is a transport/wake layer, not part of the core protocol.
120
+
121
+ An example adapter lives at:
122
+
123
+ ```txt
124
+ examples/ensemble-tmux
125
+ ```
126
+
127
+ It does exactly two durable things:
128
+
129
+ 1. Writes the real message to `.pi-ensemble/` using the CLI.
130
+ 2. Pastes a short shell-safe wake prompt into the target pane.
131
+
132
+ Example:
133
+
134
+ ```bash
135
+ examples/ensemble-tmux send claude-lead \
136
+ "Please read your inbox and run the requested review." \
137
+ --from pi \
138
+ --type handoff
139
+ ```
140
+
141
+ This preserves the invariant: deleting the adapter still leaves the collaboration legible in `.pi-ensemble/`.
142
+
143
+ ## Dashboard / observability adapters
144
+
145
+ Dashboards should be read-only by default:
146
+
147
+ - render `blackboard.md`, inbox total/unread/stale counts, claims, and audit timeline;
148
+ - do not become the source of truth;
149
+ - if they mutate state, they should do so by invoking the same CLI/tool operations as everyone else.
150
+
151
+ ## Queue / bridge adapters
152
+
153
+ Adapters for other message queues may mirror into `pi-ensemble`, but should avoid duplicating whole private transcripts. Mirror only:
154
+
155
+ - external event id / URL / file pointer;
156
+ - sender and recipient;
157
+ - type (`handoff`, `question`, `result`, `ack`, `note`);
158
+ - durable summary;
159
+ - result pointer.