@semanaut/zpi-peer 2.1.0 → 2.5.0

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 CHANGED
@@ -11,11 +11,18 @@ Peers are equal instances, not subordinates: each runs in its own tmux pane, wit
11
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
12
  - **Normal tmux workflow works**: switch panes/windows, watch a peer work live in its pane, kill it with tmux when done.
13
13
 
14
- ## The three tools
14
+ ## The four tools
15
15
 
16
- - **`peer_spawn`** — launch an equal peer in a tmux pane; optional role-prompt file and fork/handoff context.
16
+ - **`peer_spawn(name, task, prompt?, context?, model?)`** — launch an equal peer in a tmux pane. The peer is named `[peer]<name>`; the name is the peer's identity carrier, so make it declare function, responsibility, and state. Optional role-prompt file and fork/handoff context.
17
17
  - **`peer_list`** — list sessions and their contact counts, filterable by contact ("self" = the sessions you have spawned or exchanged letters with; any sessionId = who that session communicated with).
18
- - **`peer_send_mail`** — deliver a letter to any pi session's mailbox, peers and human-started alike.
18
+ - **`peer_send_mail`** — deliver a letter to any pi session's mailbox, peers and human-started alike; a dead recipient is revived automatically.
19
+ - **`peer_name`** — read or set a session's own name. Omit the argument to read the name as it exists on disk (the last `session_info` entry — the same value every external reader sees); pass a name to set it, effective immediately. The tool never touches another session: renaming another peer goes through a letter, so the peer also learns why its name changed.
20
+
21
+ ## Data layout
22
+
23
+ - `<agent dir>/peer/registry/<sessionId>/role.json` — role record (exists only for sessions with a role; plain peers carry none).
24
+ - `<agent dir>/peer/registry/<sessionId>/contacts.txt` — append-only contact log, one sessionId per line.
25
+ - `<agent dir>/peer/letters/<sessionId>/` — the session's mailbox (`pending-*.md` letters awaiting delivery).
19
26
 
20
27
  ## Compatibility
21
28
 
@@ -41,4 +48,4 @@ On every start the extension mirrors the bundled role files into the peer data d
41
48
 
42
49
  ## Bundled skill
43
50
 
44
- 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.
51
+ Installs the `peer-orchestrator` skill alongside the extension: the policy for when to spawn peers (only for genuinely parallel, independent work), how to name them (the name is the identity carrier — function, responsibility, state — and must be kept in sync as the peer's state changes), how to write a self-contained first task, how to choose a peer's starting context, and how to batch follow-up letters.
@@ -189,13 +189,15 @@ function expandPath(p: string): string {
189
189
 
190
190
  // Peer role identity lives ONLY here, never in session files — a fork cannot pollute it by
191
191
  // file copying, and no scan can miss it. A record exists only for sessions WITH a role;
192
- // plain peers carry no .json at all. Legacy records with creatorSessionId are ignored on read.
192
+ // plain peers carry no role.json at all.
193
+ const sessionDir = (sid: string) => path.join(registryDir(), sid);
194
+ const roleFile = (sid: string) => path.join(sessionDir(sid), "role.json");
195
+
193
196
  function writePeerRecord(sid: string, rec: PeerRecord): void {
194
- fs.mkdirSync(registryDir(), { recursive: true });
195
- const file = path.join(registryDir(), `${sid}.json`);
196
- const tmp = `${file}.tmp`;
197
+ fs.mkdirSync(sessionDir(sid), { recursive: true });
198
+ const tmp = `${roleFile(sid)}.tmp`;
197
199
  fs.writeFileSync(tmp, JSON.stringify({ appendPrompt: expandPath(rec.appendPrompt) }));
198
- fs.renameSync(tmp, file); // tmp+rename: readers never see a half-written record
200
+ fs.renameSync(tmp, roleFile(sid)); // tmp+rename: readers never see a half-written record
199
201
  }
200
202
 
201
203
  // Role records are write-once (the role path never changes), so this cache (negatives included) never goes stale.
@@ -206,14 +208,14 @@ function readPeerRecord(sid: string): PeerRecord | null {
206
208
  if (hit !== undefined) return hit;
207
209
  let rec: PeerRecord | null = null;
208
210
  try {
209
- const raw = JSON.parse(fs.readFileSync(path.join(registryDir(), `${sid}.json`), "utf-8"));
211
+ const raw = JSON.parse(fs.readFileSync(roleFile(sid), "utf-8"));
210
212
  if (raw && typeof raw.appendPrompt === "string") rec = { appendPrompt: raw.appendPrompt };
211
213
  } catch {}
212
214
  recordCache.set(sid, rec);
213
215
  return rec;
214
216
  }
215
217
 
216
- const contactsFile = (sid: string) => path.join(registryDir(), `${sid}.contacts`);
218
+ const contactsFile = (sid: string) => path.join(sessionDir(sid), "contacts.txt");
217
219
 
218
220
  /** Append-only contact log: one sessionId per line, both directions written by whoever acts
219
221
  * (spawn or letter). A single short O_APPEND write is atomic under the kernel's inode lock, so
@@ -222,7 +224,8 @@ const contactsFile = (sid: string) => path.join(registryDir(), `${sid}.contacts`
222
224
  * duplicates are harmless because reads dedupe. */
223
225
  function addContact(a: string, b: string): void {
224
226
  try {
225
- fs.mkdirSync(registryDir(), { recursive: true });
227
+ fs.mkdirSync(sessionDir(a), { recursive: true });
228
+ fs.mkdirSync(sessionDir(b), { recursive: true });
226
229
  fs.appendFileSync(contactsFile(a), `${b}\n`);
227
230
  fs.appendFileSync(contactsFile(b), `${a}\n`);
228
231
  } catch {} // best effort: a lost line only narrows a future peer_list filter
@@ -238,9 +241,11 @@ function readContacts(sid: string): string[] {
238
241
  return [...new Set(raw.split("\n").filter((l) => UUID_RE.test(l)))];
239
242
  }
240
243
 
244
+ // Removes the whole session folder: spawn-failure paths call this before any contact exists,
245
+ // so the folder holds at most role.json — a sid that never booted never lingers.
241
246
  function deletePeerRecord(sid: string): void {
242
247
  try {
243
- fs.unlinkSync(path.join(registryDir(), `${sid}.json`));
248
+ fs.rmSync(sessionDir(sid), { recursive: true, force: true });
244
249
  } catch {}
245
250
  recordCache.delete(sid);
246
251
  }
@@ -275,6 +280,40 @@ function findSessionById(records: SessionRecord[], sessionId: string): SessionRe
275
280
  return records.find((r) => r.sessionId === sessionId);
276
281
  }
277
282
 
283
+ /** This session's file, located the way the tools tell models to: the <timestamp>_<sid>.jsonl file somewhere under <agentDir>/sessions/. */
284
+ function findSessionFile(sid: string): string | null {
285
+ const root = path.join(getAgentDir(), "sessions");
286
+ try {
287
+ for (const dir of fs.readdirSync(root, { withFileTypes: true })) {
288
+ if (!dir.isDirectory()) continue;
289
+ for (const f of fs.readdirSync(path.join(root, dir.name))) {
290
+ if (f.endsWith(`_${sid}.jsonl`)) return path.join(root, dir.name, f);
291
+ }
292
+ }
293
+ } catch {}
294
+ return null;
295
+ }
296
+
297
+ /** The name as every external reader sees it: the LAST session_info entry of the session file.
298
+ * The file is the single source of truth — pi's in-memory copy is deliberately never consulted. */
299
+ function readNameFromFile(sid: string): { name: string | null; file: string | null } {
300
+ const file = findSessionFile(sid);
301
+ if (!file) return { name: null, file: null };
302
+ let name: string | null = null;
303
+ try {
304
+ for (const line of fs.readFileSync(file, "utf-8").split("\n")) {
305
+ if (!line) continue;
306
+ try {
307
+ const entry = JSON.parse(line);
308
+ if (entry?.type === "session_info") name = typeof entry.name === "string" ? entry.name : null;
309
+ } catch {} // a torn trailing line is not a rename; the only writer of the file is pi itself
310
+ }
311
+ } catch {
312
+ return { name: null, file };
313
+ }
314
+ return { name, file };
315
+ }
316
+
278
317
  /** Uniform failed-tool result: "<code>: <detail>" text plus structured { code, message, …extra }. */
279
318
  function errResult(code: string, detail: string, extra?: Record<string, unknown>) {
280
319
  const message = `${code}: ${detail}`;
@@ -457,7 +496,7 @@ export default function piPeer(pi: ExtensionAPI) {
457
496
  if (event.reason !== "fork" || !event.previousSessionFile) return;
458
497
  const file = path.basename(event.previousSessionFile);
459
498
  const srcSid = file.endsWith(".jsonl") ? file.slice(0, -".jsonl".length).split("_").pop() : undefined;
460
- if (!srcSid || fs.existsSync(path.join(registryDir(), `${ownSid}.json`))) return;
499
+ if (!srcSid || fs.existsSync(roleFile(ownSid))) return;
461
500
  const src = readPeerRecord(srcSid);
462
501
  if (!src) return;
463
502
  writePeerRecord(ownSid, { appendPrompt: src.appendPrompt }); // role only — contacts are never inherited
@@ -527,7 +566,7 @@ export default function piPeer(pi: ExtensionAPI) {
527
566
  "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.",
528
567
  ],
529
568
  parameters: Type.Object({
530
- topic: Type.String({ description: "Session topic; the peer is named '[peer]<topic>'" }),
569
+ name: Type.String({ description: "Session name seed; the peer is named '[peer]<name>'" }),
531
570
  task: Type.String({ description: "Self-contained first message; delivered at launch as the peer's first user message" }),
532
571
  prompt: Type.Optional(
533
572
  Type.String({
@@ -551,7 +590,7 @@ export default function piPeer(pi: ExtensionAPI) {
551
590
 
552
591
  // Pre-assigned id: --session-id creates exactly this session; the wait after launch only confirms it.
553
592
  const sid = uuidv7();
554
- const name = `[peer]${params.topic}`.slice(0, 60);
593
+ const name = `[peer]${params.name}`;
555
594
 
556
595
  let mode = "empty";
557
596
  let forkSid: string | undefined;
@@ -756,4 +795,49 @@ export default function piPeer(pi: ExtensionAPI) {
756
795
  );
757
796
  },
758
797
  });
798
+
799
+ // Self-only by design: a rename of ANOTHER session is a request, not a write — it goes through a
800
+ // letter (peer_send_mail), so the recipient also learns WHY its name changed. The read path is
801
+ // the file tail on purpose: it is the same value every external reader (peer_list) sees.
802
+ pi.registerTool({
803
+ name: "peer_name",
804
+ label: "Read or set own session name",
805
+ description:
806
+ "Read or set THIS session's name — a peer's identity carrier: it declares what this session does, what it is responsible for, and its current state; peer_list shows it first, and it is often the only thing a reader sees before deciding to mail, fork, or skip. " + SKILL_GATE + " Omit name to read the current name exactly as it exists on disk — the last session_info entry of this session's file, the same value every external reader sees; a session with no name entry reads as unnamed. Pass name to set it — effective immediately for this process and on disk. This tool never touches another session: to change another session's name (dead or alive — peer_send_mail revives a dead recipient), send a letter stating the reason and the new name; the recipient renames itself.",
807
+ promptGuidelines: [
808
+ "A name change is almost always a state change: update your own name with peer_name in the same run that changes your state (retirement, redirection, reassignment) — never rename another session, ask it by letter.",
809
+ ],
810
+ parameters: Type.Object({
811
+ name: Type.Optional(
812
+ Type.String({
813
+ description: "New name for this session — function, responsibility, and state. Omit to read the current name.",
814
+ }),
815
+ ),
816
+ }),
817
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
818
+ const sid = ctx.sessionManager.getSessionId();
819
+ if (params.name === undefined) {
820
+ const { name, file } = readNameFromFile(sid);
821
+ const text = name
822
+ ? `this session's name: ${name}`
823
+ : file
824
+ ? "this session has no name on disk yet — set one with peer_name (function, responsibility, state)."
825
+ : "this session's file is not on disk yet (pi persists it with this session's first submitted message); a name set now takes effect in this process and lands on disk with the file.";
826
+ return { content: [{ type: "text" as const, text }], details: { name, sessionId: sid, file } };
827
+ }
828
+ const name = params.name.trim();
829
+ if (!name) {
830
+ return errResult(
831
+ "bad_name",
832
+ "a name cannot be empty or whitespace-only — pass a non-empty name, or omit it to read the current name.",
833
+ );
834
+ }
835
+ pi.setSessionName(name);
836
+ const message = `session renamed: ${name} — effective immediately for this process and on disk; peer_list and every file reader see it from now on`;
837
+ return {
838
+ content: [{ type: "text" as const, text: message }],
839
+ details: { name, sessionId: sid, message },
840
+ };
841
+ },
842
+ });
759
843
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@semanaut/zpi-peer",
3
- "version": "2.1.0",
3
+ "version": "2.5.0",
4
4
  "piVersion": "1.3.0",
5
- "description": "pi extension: equal, durable pi peers managed through tmux panes plus a letter channel between any pi sessions. tmux runs on Unix-like systems (Linux, macOS, BSD; Windows via WSL/Cygwin); native Windows is not supported. Tools: peer_spawn (launch a peer in a tmux pane), peer_list (list sessions with their contact counts), peer_send_mail (deliver a letter, reviving dead recipients). Ships investigator / strategist / auditor role prompts; developed against the pi version given by the piVersion field",
5
+ "description": "pi extension: equal, durable pi peers managed through tmux panes plus a letter channel between any pi sessions. tmux runs on Unix-like systems (Linux, macOS, BSD; Windows via WSL/Cygwin); native Windows is not supported. Tools: peer_spawn (launch a peer in a tmux pane), peer_list (list sessions with their contact counts), peer_send_mail (deliver a letter, reviving dead recipients), peer_name (read or set a session's name — the peer's identity carrier). Ships investigator / strategist / auditor role prompts; developed against the pi version given by the piVersion field",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "pi-package"
@@ -1,117 +1,63 @@
1
1
  ---
2
2
  name: peer-orchestrator
3
- description: Policy for creating and steering equal pi peers. Load when considering peer_spawn — deciding to run directions in parallel, choosing a peer's starting context, or writing its first task.
3
+ description: Policy for creating and steering equal pi peers. Load when considering peer_spawn, peer_list, peer_send_mail, or peer_name — deciding to run directions in parallel, choosing a peer's starting context, writing its first task, or reading/changing a session's name.
4
4
  ---
5
5
 
6
6
  # Peer Orchestrator
7
7
 
8
8
  ## Mental model
9
- Peers are equal instances, not subordinates: you have no control over them, only a launch and a
10
- messaging channel. Each peer has its own context, its own token budget, its own mistakes.
11
- Work directly by default; spawn a peer only when the user asked for parallel/independent work,
12
- or a task clearly warrants a separate context. Never spawn for a single lookup or a file read.
9
+ Peers are equal instances, not subordinates: you have no control over them, only a launch and a messaging channel. Each peer has its own context, its own token budget, its own mistakes.
10
+ Work directly by default; spawn a peer only when the user asked for parallel/independent work, or a task clearly warrants a separate context. Never spawn for a single lookup or a file read.
13
11
 
14
- ## The three tools
12
+ ## The four tools
15
13
 
16
- ### `peer_spawn(topic, task, prompt?, context?, model?)`
17
- Creates a peer in its own tmux pane and returns its sessionId immediately — no handshake. The peer
18
- is named `[peer]<topic>`, receives `task` as its first message, and starts working as soon as it
19
- boots. Use `peer_send_mail` for everything after that.
14
+ ### `peer_spawn(name, task, prompt?, context?, model?)`
15
+ Creates a peer in its own tmux pane and returns its sessionId immediately — no handshake. You name it at spawn — the peer is named `[peer]<name>` — it receives `task` as its first message, and starts working as soon as it boots. Use `peer_send_mail` for everything after that.
20
16
 
21
17
  ### `peer_send_mail(to, summary, message)`
22
- Delivers a letter to any session's mailbox as a .md file with a frontmatter header (from + summary).
23
- The recipient's poll injects only the summary plus the file path — never the full text; the recipient
24
- reads the full letter itself and deletes the file afterwards. Notices reach a running session within
25
- ~1.5s — via steer while it is running, via followUp when it is idle — and pending notices merge into one
26
- message. A dead recipient is revived automatically — the call may block up to ~25s and tells you
27
- whether the revive succeeded; on failure the letter stays queued and is read whenever that session
28
- next runs.
29
- The summary is all the recipient sees up front: state who you are (name/sessionId) and what the
30
- letter is about. The letter body must be self-contained: state whether a reply is needed and where
31
- to send it. Batch related matters into one letter — every letter costs the recipient a turn.
18
+ Delivers a letter to any session's mailbox as a .md file with a frontmatter header (from + summary). The recipient's poll injects only the summary plus the file path; the recipient reads the full letter itself and deletes the file afterwards.
19
+ The summary is all the recipient sees up front: state who you are (name/sessionId) and what the letter is about. The letter body must be self-contained: state whether a reply is needed and where to send it. Batch related matters into one letter.
20
+ Notices reach a running session within seconds and pending notices merge into one message. A dead recipient is revived automatically — the call may block up to ~25s and tells you whether the revive succeeded; on failure the letter stays queued and is read whenever that session next runs.
32
21
 
33
22
  ### `peer_list(contact?, page?)`
34
- Lists sessions with their contact count, cwd, and sessionId — newest first, 15 per page; the
35
- first line summarises the total count and the current page (pass `page` for older ones). Run it
36
- before spawning: reusing or forking an existing peer beats recreating one — a session's whole
37
- history is on disk whether it is running or not, so mail it instead of spawning a duplicate. Use
38
- `contact: "self"` to see the sessions you have spawned or exchanged letters with (either
39
- direction counts); pass any other sessionId to inspect who that session has communicated with.
23
+ Lists sessions with their contact count, cwd, and sessionId — newest first, 15 per page; the first line summarises the total count and the current page (pass `page` for older ones).
24
+ Use `contact: "self"` to see the sessions you have spawned or exchanged letters with (either direction counts); pass any other sessionId to inspect who that session has communicated with.
25
+ Run it before spawning: reusing or forking an existing peer beats recreating one — a session's whole history is on disk whether it is running or not, so mail it instead of spawning a duplicate.
26
+
27
+ ### `peer_name(name?)`
28
+ Reads or sets THIS session's name. Omit `name` to read: the value returned is the name as it exists on disk; a session with no name entry reads as unnamed. Pass `name` to set: effective immediately.
29
+ Name is shown in `peer_list` as the first field and is the only thing a reader sees before deciding to mail, so the name should declare and stay consistent with exact function, responsibility, and state.
30
+ Your name is your own responsibility. Whenever your state changes — retirement, a new direction, a changed task — update your name with `peer_name` in the same run, so that other peers are not misled.
40
31
 
41
32
  ## Writing a spawn
42
33
 
43
- ### `topic`
44
- The peer's name is `[peer]<topic>` (capped at 60 chars). Name the peer after its task or
45
- responsibility in detail — a reader should understand what the peer does from the topic alone,
46
- without opening its task. Use a semantic, mission-descriptive name, not a vague handle: e.g.
47
- `investigate-auth-latency-and-analyze-causes`, `分析8月英伟达股价波动情况以判断未来走势` — not `task1`,
48
- `helper`, or a bare `refactor-auth`.
34
+ ### `name`
35
+ The peer's name is `[peer]<name>`. The name is the peer's identity carrier — see `peer_name`. A compliant name declares function, responsibility, and state, separated by `|`: e.g. `investigate-auth-latency-and-root-causes | owns the auth-latency diagnosis report for the main session | active, one-shot mission`, `宏观与全球央行跟踪 | 负责 economy 日报的宏观板块 | 活跃,每日例行`, `英伟达股价走势分析 | 已交付一次性走势判断报告 | 已退役 2026-10-19` — not `task1`, `helper`, a bare `refactor-auth`, or a function-only name like `investigate-auth-latency-and-analyze-causes` (no responsibility, no state).
49
36
 
50
37
  ### `task`
51
- The task is the peer's entire brief — it has no other context. State: the goal, the boundaries,
52
- the expected output format, and where to send the result (your own sessionId; the peer learns it
53
- from what you write). Make it self-contained: the peer must be able to finish without asking back.
38
+ The task is the peer's entire brief — it has no other context. State: the goal, the boundaries, the expected output format, and where to send the result (your own sessionId; the peer learns it from what you write). Make it self-contained: the peer must be able to finish without asking back.
54
39
 
55
40
  ### `context`
56
41
  - omit — empty context; cheapest; the default for a self-contained task.
57
- - `fork:<sessionId>` — inherit that session's full history, and the forked peer also inherits its
58
- role (unless you pass a different `prompt`, or the source has no role — then nothing is inherited);
59
- only when the peer truly needs everything you know (expensive).
60
- - `handoff:<text>` — you write exactly what the peer needs; best token/value ratio for a large
61
- context. To pass on your own context, write the handoff yourself — do NOT reuse your /compact
62
- summary: it is a self-continuation artifact and is stale by everything that happened after it.
42
+ - `fork:<sessionId>` — inherit that session's full history, and the forked peer also inherits its role (unless you pass a different `prompt`, or the source has no role — then nothing is inherited); only when the peer truly needs everything you know (expensive).
43
+ - `handoff:<text>` — you write exactly what the peer needs; best token/value ratio for a large context. To pass on your own context, write the handoff yourself — do NOT reuse your /compact summary: it is a self-continuation artifact and is stale by everything that happened after it.
63
44
 
64
45
  ### `prompt` (role)
65
- Pass the path to a role file — no need to read it first; `peer_spawn` only checks it exists. The
66
- role stays with that session across every start (fresh spawn, revive after a letter, or a manual
67
- `/resume`), and editing the role file takes effect without respawning. Identity follows the
68
- session, never the process: `/new` inside a peer's pane starts a plain session, while a fork
69
- inherits the role of the session it branched from (unless you pass `prompt`) — spawn a proper
70
- peer when a new one is needed.
71
-
72
- **Pass a role whenever one fits the mission.** It is the main lever you have for shaping a peer's
73
- stance, and it costs one path string. Leave it empty only when no standard role fits and the task
74
- text alone can carry the discipline you need — a plain peer has no role discipline at all.
75
-
76
- ## What a role is here
77
-
78
- Peers are flat and equal: same toolset, same permissions, no hierarchy. A role therefore cannot be
79
- a permission slice — it is a **responsibility and a stance**, expressed as what the peer produces
80
- and what it refuses to do. Two consequences:
81
-
82
- - A role must change the output contract or the discipline, not the subject matter. "Review the
83
- frontend" is a task; "verify, audit, then criticize" is a role. Subject variation belongs in `task`.
84
- - A role never implies rank. No role is an overseer of another, and a role file must not tell a
85
- peer to direct, schedule, or supervise other peers. Every peer can spawn and mail every other.
86
-
87
- Peers launch with the full toolset: whatever discipline a role shows comes from its stance and its
88
- task text, never from restrictions. A peer can always mail you back.
46
+ Path to a role file. Its text is appended to the peer's system prompt on every turn — it survives every restart (spawn, revive, manual resume), and editing the file takes effect without respawning. Pass one whenever it fits the mission; omitted, the peer runs with no discipline beyond its task.
89
47
 
90
48
  ## The standard roles
91
49
 
92
- Pass the path of the role file you want to `peer_spawn`'s prompt parameter; the exact paths are
93
- given in the peer_spawn tool description. For a custom role, pass the path of any other role file.
94
-
95
- - **investigator** — find out what is true. Merges codebase reconnaissance with outside research:
96
- a map of where things are, or a sourced brief on what the evidence says. It cites every path and
97
- URL it actually checked, and marks the boundary between what it verified and what it infers. Use
98
- it whenever you need facts you do not have.
99
- - **strategist** — make the call. Given a problem, or given alternatives someone else produced, it
100
- weighs them and commits: real options with what each buys and costs, the axis being balanced, one
101
- recommendation definite enough to be wrong, and the fact that would flip it. Decisive, not
102
- analytic — it decides, then says what would change its mind. It proposes; it does not plan
103
- execution steps or direct other peers. Use it before committing to an approach.
104
- - **auditor** — expose what does not hold, and dare to say so. It verifies by actually doing:
105
- re-runs, reproduces, re-derives — then audits the path actually taken (including what was
106
- skipped), names the risks, and only then criticizes: VERDICT (agree / agree-with-changes /
107
- object) plus numbered findings by severity, each with evidence and a concrete suggestion. It
108
- pours cold water on warm consensus and never rubber-stamps — give it the artifact, not your
109
- defense of it.
50
+ A role is a stance, not a rank and not permissions. It changes how the peer works — what it produces and what it refuses — never what it works on: "review the frontend" is a task, "verify, audit, then criticize" is a role. It never grants control over other peers: every peer stays equal.
51
+ Pass the path of the role file you want to `peer_spawn`'s prompt parameter; the exact paths are given in the peer_spawn tool description. For a custom role, pass the path of any other role file.
52
+
53
+ - **investigator** — find out what is true. Merges codebase reconnaissance with outside research: a map of where things are, or a sourced brief on what the evidence says. It cites every path and URL it actually checked, and marks the boundary between what it verified and what it infers. Use it whenever you need facts you do not have.
54
+ - **strategist** — make the call. Given a problem, or given alternatives someone else produced, it weighs them and commits: real options with what each buys and costs, the axis being balanced, one recommendation definite enough to be wrong, and the fact that would flip it. Decisive, not analytic — it decides, then says what would change its mind. It proposes; it does not plan execution steps or direct other peers. Use it before committing to an approach.
55
+ - **auditor** — expose what does not hold, and dare to say so. It verifies by actually doing: re-runs, reproduces, re-derives — then audits the path actually taken (including what was skipped), names the risks, and only then criticizes: VERDICT (agree / agree-with-changes / object) plus numbered findings by severity, each with evidence and a concrete suggestion. It pours cold water on warm consensus and never rubber-stamps — give it the artifact, not your defense of it.
110
56
 
111
57
  ## Conventions
112
58
  - One direction per peer; a peer with two missions does neither well.
113
59
  - Tell the peer where to reply (your sessionId) in the task itself.
114
60
  - Waiting for a peer's report does not require you to stay active: an incoming letter is injected into your context and starts a new conversation turn by itself, even after your output has ended. Finish your reply and stop; holding the turn open (timed pauses, re-checks, sleep) only burns time and tokens.
115
61
  - No acknowledgements or small talk by letter; write only when there is substance.
116
- - A peer that goes quiet is not stuck — letters merge while it is busy; wait for its report before
117
- nudging, and nudge with new information, not with "status?".
62
+ - Keep the name truthful: a name that has drifted from the peer's real state is a bug — update it with `peer_name` in the same run that changes the state.
63
+ - A peer that goes quiet is not stuck — letters merge while it is busy; wait for its report before nudging, and nudge with new information, not with "status?".