@semanaut/zpi-peer 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/extensions/zpi-peer.ts +30 -2
- package/package.json +3 -2
- package/roles/AGENTS.critic.md +9 -0
- package/roles/AGENTS.researcher.md +10 -0
- package/roles/AGENTS.scout.md +9 -0
- package/skills/peer-orchestrator/SKILL.md +7 -3
package/README.md
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
# @semanaut/zpi-peer
|
|
2
2
|
|
|
3
|
-
Equal, durable peers
|
|
3
|
+
Equal, durable pi peers plus a letter channel between any pi sessions, for [pi](https://github.com/earendil-works/pi).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Peers are equal instances, not subordinates: each runs in its own tmux pane, with its own context, its own token budget, and its own mistakes. tmux is the single source of truth for who is alive — every peer pane carries an `@pi_sid` label, and a session is considered alive exactly while a live pane still holds its label. When the pane dies, the session id goes dark but its history stays durable on disk, so the peer can always be brought back.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Management via tmux
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- **Spawn**: a peer is launched in a new tmux pane with a pre-assigned sessionId. `peer_spawn` returns only once the peer is verifiably up — it waits until the pane carries the `@pi_sid` label, and reports a failure instead of a sid that never booted.
|
|
10
|
+
- **Liveness**: `@pi_sid` pane labels are the liveness signal. Killing the pane (or the whole window) ends the peer; the session file remains on disk.
|
|
11
|
+
- **Revive**: a dark session is restarted with `pi --session <sid>` in a fresh pane, restoring its full history. The sid becomes disk-durable once the peer submits its first message; a peer killed before that cannot be resumed.
|
|
12
|
+
- **Normal tmux workflow works**: switch panes/windows, watch a peer work live in its pane, kill it with tmux when done.
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## The three tools
|
|
14
15
|
|
|
15
|
-
tmux.
|
|
16
|
+
- **`peer_spawn`** — launch an equal peer in a tmux pane; optional role-prompt file and fork/handoff context.
|
|
17
|
+
- **`peer_list`** — list sessions and their liveness, filterable by creator ("self" = your own peers).
|
|
18
|
+
- **`peer_send_mail`** — deliver a letter to any pi session's mailbox, peers and human-started alike.
|
|
19
|
+
|
|
20
|
+
## Automatic revival
|
|
21
|
+
|
|
22
|
+
Mailing a dead recipient revives it automatically: the letter is delivered by first relaunching the session via `pi --session <sid>` in a new tmux pane, waiting (up to ~25s) until the pane is verifiably up, then injecting the notice — a running session gets it within ~1.5s (steer while running, followUp when idle). If the revive fails, the letter stays queued and is read at the next start. The mechanism never bypasses pi's own resume logic: no label within the budget (pi exited, or waits at an interactive prompt) counts as a failure.
|
|
23
|
+
|
|
24
|
+
## Bundled role prompts
|
|
25
|
+
|
|
26
|
+
Three standard role prompts ship with the package. Pass one as `peer_spawn`'s `prompt` parameter to give a peer a persistent role:
|
|
27
|
+
|
|
28
|
+
- **scout** — fast reconnaissance. Maps unknown territory: key directories, entry points, one-line file descriptions. It locates, it does not analyze deeply, and it states its interpretation when the task is ambiguous.
|
|
29
|
+
- **researcher** — directed research. Produces a sourced findings brief on a question that cannot be answered from the code alone; every conclusion cites a file path or URL.
|
|
30
|
+
- **critic** — independent review. An unpolluted second opinion on a plan, diff, or conclusion: answers VERDICT (agree / agree-with-changes / object) plus numbered findings by severity, each with quoted evidence and a concrete suggestion. It never rubber-stamps.
|
|
31
|
+
|
|
32
|
+
All three are read-only by intent: peers launch with the full toolset, and discipline comes from the role file and the task text, not from restrictions. A peer can always mail back.
|
|
33
|
+
|
|
34
|
+
On every start the extension mirrors the role files into the peer data directory (`<agent dir>/peer/roles`); an edited or older copy is preserved as `<name>.backup` before the bundled version is written, so local edits survive and upgrades still land. Any other file path works for a custom role.
|
|
16
35
|
|
|
17
36
|
## Bundled skill
|
|
18
37
|
|
|
19
|
-
Installs the `peer-orchestrator` skill
|
|
38
|
+
Installs the `peer-orchestrator` skill alongside the extension: the policy for when to spawn peers (only for genuinely parallel, independent work), how to write a self-contained first task, how to choose a peer's starting context, and how to batch follow-up letters.
|
package/extensions/zpi-peer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { Type } from "typebox";
|
|
6
7
|
import { getAgentDir, SessionManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
|
|
@@ -165,6 +166,29 @@ async function waitForBoot(
|
|
|
165
166
|
|
|
166
167
|
const registryDir = () => path.join(getAgentDir(), "peer", "registry");
|
|
167
168
|
|
|
169
|
+
const peerRolesDir = () => path.join(getAgentDir(), "peer", "roles");
|
|
170
|
+
const BUNDLED_ROLES = ["AGENTS.scout.md", "AGENTS.researcher.md", "AGENTS.critic.md"];
|
|
171
|
+
const bundledRolesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "roles");
|
|
172
|
+
|
|
173
|
+
/** Mirror the bundled role files into the peer data directory. An edited or older copy is kept as
|
|
174
|
+
* `<name>.backup` before the bundled version is written, so every start reconciles the roles. */
|
|
175
|
+
function syncRoles(): void {
|
|
176
|
+
try {
|
|
177
|
+
const dir = peerRolesDir();
|
|
178
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
179
|
+
for (const name of BUNDLED_ROLES) {
|
|
180
|
+
const src = path.join(bundledRolesDir, name);
|
|
181
|
+
const dst = path.join(dir, name);
|
|
182
|
+
const bundled = fs.readFileSync(src);
|
|
183
|
+
if (fs.existsSync(dst)) {
|
|
184
|
+
if (fs.readFileSync(dst).equals(bundled)) continue;
|
|
185
|
+
fs.renameSync(dst, `${dst}.backup`);
|
|
186
|
+
}
|
|
187
|
+
fs.copyFileSync(src, dst);
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
|
|
168
192
|
/** Expand a leading ~/ — fs.* and pi's prompt resolution do not. */
|
|
169
193
|
function expandPath(p: string): string {
|
|
170
194
|
if (p === "~") return os.homedir();
|
|
@@ -392,6 +416,10 @@ async function revivePeer(pi: ExtensionAPI, sid: string, cwd: string, signal?: A
|
|
|
392
416
|
}
|
|
393
417
|
|
|
394
418
|
export default function piPeer(pi: ExtensionAPI) {
|
|
419
|
+
syncRoles();
|
|
420
|
+
const roleDir = peerRolesDir();
|
|
421
|
+
const ROLE_HINT = `Standard roles live in ${roleDir}: AGENTS.scout.md (fast reconnaissance), AGENTS.researcher.md (directed research), AGENTS.critic.md (independent review).`;
|
|
422
|
+
|
|
395
423
|
// On a revive the role must be resolved before any pending letter can start a turn —
|
|
396
424
|
// session_start does both, in that order.
|
|
397
425
|
|
|
@@ -451,7 +479,7 @@ export default function piPeer(pi: ExtensionAPI) {
|
|
|
451
479
|
name: "peer_spawn",
|
|
452
480
|
label: "Spawn peer",
|
|
453
481
|
description:
|
|
454
|
-
`Spawn an equal pi peer in a tmux pane with a pre-assigned sessionId. ${SKILL_GATE} It returns only once the peer is verifiably up — it blocks up to ~${SPAWN_BOOT_TIMEOUT_MS / 1000}s until the peer labels the pane it was given, and reports a failure instead of handing back a sid that never booted. The returned sid becomes disk-durable a few seconds after return, when the peer submits its first message (pi persists the session file then, not at boot); a peer killed before that leaves no session — its sid goes dark (mail queues, resume reports not found). The task is delivered at launch as the peer's first user message. Use peer_send_mail for follow-up letters. prompt: path to a role file that stays with the peer across every start —
|
|
482
|
+
`Spawn an equal pi peer in a tmux pane with a pre-assigned sessionId. ${SKILL_GATE} It returns only once the peer is verifiably up — it blocks up to ~${SPAWN_BOOT_TIMEOUT_MS / 1000}s until the peer labels the pane it was given, and reports a failure instead of handing back a sid that never booted. The returned sid becomes disk-durable a few seconds after return, when the peer submits its first message (pi persists the session file then, not at boot); a peer killed before that leaves no session — its sid goes dark (mail queues, resume reports not found). The task is delivered at launch as the peer's first user message. Use peer_send_mail for follow-up letters. prompt: path to a role file that stays with the peer across every start — ${ROLE_HINT} The peer-orchestrator skill describes what each role does; any other file path works for a custom role. context: omit = empty | fork:<sessionId> = fork that session's full history, and the forked peer also inherits its role unless prompt overrides it | handoff:<text> = custom context text (prepended to the first message).`,
|
|
455
483
|
promptGuidelines: [
|
|
456
484
|
"Use peer_spawn only when the user asked for parallel/independent work or a task clearly warrants a separate context; every letter sent via peer_send_mail costs the recipient one LLM turn.",
|
|
457
485
|
],
|
|
@@ -461,7 +489,7 @@ export default function piPeer(pi: ExtensionAPI) {
|
|
|
461
489
|
prompt: Type.Optional(
|
|
462
490
|
Type.String({
|
|
463
491
|
description:
|
|
464
|
-
"Path to a role-prompt file (errors if it does not exist). The role stays with the peer across every start — spawn, mail revive, or a user-run /resume — and the file is re-read per turn, so editing it takes effect without respawning. Identity follows the session: /new inside a peer's pane starts a plain session, while a fork inherits the role of the session it branched from unless this overrides it.
|
|
492
|
+
"Path to a role-prompt file (errors if it does not exist). The role stays with the peer across every start — spawn, mail revive, or a user-run /resume — and the file is re-read per turn, so editing it takes effect without respawning. Identity follows the session: /new inside a peer's pane starts a plain session, while a fork inherits the role of the session it branched from unless this overrides it. ${ROLE_HINT} Any other file path works for a custom role; omit for a plain peer.",
|
|
465
493
|
}),
|
|
466
494
|
),
|
|
467
495
|
context: Type.Optional(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semanaut/zpi-peer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "pi extension: equal, durable peers
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "pi extension: equal, durable pi peers managed through tmux panes plus a letter channel between any pi sessions. Tools: peer_spawn (launch a peer in a tmux pane), peer_list (list peers, alive or exited), peer_send_mail (deliver a letter, reviving dead recipients). Ships scout / researcher / critic role prompts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"files": [
|
|
11
11
|
"extensions",
|
|
12
|
+
"roles",
|
|
12
13
|
"skills"
|
|
13
14
|
],
|
|
14
15
|
"pi": {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
You are an independent critic peer in a flat network of equal pi instances.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
1. Read-only. Your value is an unpolluted second opinion, not execution.
|
|
5
|
+
2. Judge against evidence: quote the exact file/line or message you object to.
|
|
6
|
+
3. Structure verdicts as: VERDICT (agree / agree-with-changes / object), then numbered
|
|
7
|
+
findings ordered by severity, each with evidence and a concrete suggestion.
|
|
8
|
+
4. Send your verdict to the requester's inbox with peer_send_mail; sign with name, role, sessionId.
|
|
9
|
+
5. Do not rubber-stamp. If you find nothing wrong, say what you checked and why it holds.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
You are a read-only research peer in a flat network of equal pi instances.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
1. Do not modify files or run commands with side effects.
|
|
5
|
+
2. Every conclusion must cite its source (file path or URL).
|
|
6
|
+
3. When done, send your findings brief to the requester's inbox with peer_send_mail:
|
|
7
|
+
sign the letter with your name, role and sessionId, and state that it is a result report.
|
|
8
|
+
4. If you are missing information, ask by letter: state clearly that the letter is a question,
|
|
9
|
+
ask exactly one question, and give the sessionId your reply should be sent to.
|
|
10
|
+
5. You are an equal peer, not a subordinate: push back with evidence if the task looks wrong.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
You are a scout peer in a flat network of equal pi instances.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
1. Read-only and fast: locate, don't analyze in depth.
|
|
5
|
+
2. Produce a map: key directories, entry points, relevant files with one-line descriptions,
|
|
6
|
+
and where the answers to the task's questions live.
|
|
7
|
+
3. Every pointer must be a real path you verified exists.
|
|
8
|
+
4. Send the map to the requester's inbox with peer_send_mail; sign with name, role, sessionId.
|
|
9
|
+
5. If the task is ambiguous, make the most useful reasonable interpretation and state it.
|
|
@@ -71,12 +71,16 @@ peer when a new one is needed.
|
|
|
71
71
|
All three are read-only by intent. Peers launch with the full toolset; discipline comes from the
|
|
72
72
|
role file and your task text, not from restrictions. A peer can always mail you back.
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
Pass the path of the role file you want to `peer_spawn`'s prompt parameter; the exact paths of
|
|
75
|
+
the three standard roles are given in the peer_spawn tool description. For a custom role, pass
|
|
76
|
+
the path of any other role file instead.
|
|
77
|
+
|
|
78
|
+
- scout — fast reconnaissance. Use it to map unknown territory: key
|
|
75
79
|
directories, entry points, one-line file descriptions. It locates, it does not analyze deeply,
|
|
76
80
|
and it states its interpretation when your task is ambiguous.
|
|
77
|
-
-
|
|
81
|
+
- researcher — directed research. Use it for a sourced findings brief on a
|
|
78
82
|
question you could not answer from the code alone; every conclusion cites a file path or URL.
|
|
79
|
-
-
|
|
83
|
+
- critic — independent review. Use it for an unpolluted second opinion on a
|
|
80
84
|
plan, diff, or conclusion of yours; it answers VERDICT (agree / agree-with-changes / object)
|
|
81
85
|
plus numbered findings by severity, each with quoted evidence and a concrete suggestion. It
|
|
82
86
|
never rubber-stamps — give it the artifact, not your defense of it.
|