@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 +103 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/SECURITY.md +27 -0
- package/bin/ensemble.mjs +217 -0
- package/docs/ADAPTERS.md +159 -0
- package/docs/AUDIT.md +20 -0
- package/docs/CLAUDE_AGENT_TEAMS.md +149 -0
- package/docs/LANDSCAPE.md +452 -0
- package/docs/QUICKSTART.md +149 -0
- package/docs/ROADMAP.md +313 -0
- package/docs/RUNTIME_RECIPES.md +111 -0
- package/docs/SPEC.md +132 -0
- package/examples/claude-agent-teams-lead.md +38 -0
- package/examples/ensemble-tmux +136 -0
- package/extensions/pi-ensemble.ts +212 -0
- package/lib/core.mjs +517 -0
- package/package.json +54 -0
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
|
+
[](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.
|
package/bin/ensemble.mjs
ADDED
|
@@ -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
|
+
}
|
package/docs/ADAPTERS.md
ADDED
|
@@ -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.
|