@trim21/personal-pi-extensions 0.0.202 → 0.0.203

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
@@ -230,12 +230,12 @@ index.ts —— pi adapter:把 core 接到 pi 的 sendMessage / 生命周
230
230
 
231
231
  ### 工具(LLM 可见)
232
232
 
233
- | 工具 | 作用 |
234
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
235
- | `talk-list-sessions` | 列出其他 session,返回 JSON 数组(`status` / `work_dir` / `id` / `name`);默认只列有心跳的,`includeOffline: true` 列全部 |
236
- | `talk-ask` | 向某个 session 提问并阻塞等待回复(默认 30 分钟超时) |
237
- | `talk-send` | 发送纯文本消息(`to: "*"` 广播所有,`to: "cwd"` 广播同 cwd) |
238
- | `talk-reply` | 回复一个 ask(`replyTo` 为 ask id,显式关联、不推断) |
233
+ | 工具 | 作用 |
234
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
235
+ | `talk-list-sessions` | 列出会话,返回 JSON 数组(`status` / `work_dir` / `id` / `name`,自己带 `self: true`);默认只列有心跳的,`includeOffline: true` 列全部 |
236
+ | `talk-ask` | 向某个 session 提问并阻塞等待回复(默认 30 分钟超时) |
237
+ | `talk-send` | 发送纯文本消息(`to: "*"` 广播所有,`to: "cwd"` 广播同 cwd) |
238
+ | `talk-reply` | 回复一个 ask(`replyTo` 为 ask id,显式关联、不推断) |
239
239
 
240
240
  对端消息自动投递(无需主动拉取):投递方式由 `talk.deliver` 配置,`steer` 在模型工作过程中打断/唤醒,`queue` 排队到 session 下一轮自然 turn 时注入。
241
241
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.202",
3
+ "version": "0.0.203",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -67,6 +67,9 @@
67
67
  "src/opencode-todo.ts",
68
68
  "src/question.ts",
69
69
  "src/talk/index.ts"
70
+ ],
71
+ "skills": [
72
+ "src/talk/skills"
70
73
  ]
71
74
  },
72
75
  "lint-staged": {
package/src/talk/core.ts CHANGED
@@ -449,17 +449,18 @@ export class TalkCore {
449
449
  // ── Tool actions ───────────────────────────────────────────────────────
450
450
 
451
451
  /**
452
- * JSON listing of visible peer sessions. Defaults to sessions whose
453
- * heartbeat is fresh (within LIST_ACTIVE_MS); pass includeOffline to show
454
- * every visible peer regardless of last contact.
452
+ * JSON listing of visible sessions, including self (marked `self: true`).
453
+ * Defaults to sessions whose heartbeat is fresh (within LIST_ACTIVE_MS);
454
+ * pass includeOffline to show every visible peer regardless of last contact.
455
455
  */
456
456
  async list(includeOffline = false): Promise<string> {
457
457
  const self = this.requireSelf();
458
458
  const now = this.now();
459
459
  const all = await listRecords(this.storage);
460
- const records = all.filter(
461
- (r) => this.isPeerVisible(r.cwd) && (includeOffline || now - r.lastSeenAt < LIST_ACTIVE_MS),
462
- );
460
+ const records = all.filter((r) => {
461
+ if (r.addr === self.addr) return !this.dead;
462
+ return this.isPeerVisible(r.cwd) && (includeOffline || now - r.lastSeenAt < LIST_ACTIVE_MS);
463
+ });
463
464
  return formatListing(records, self.addr, (r) => presenceOf(r, now));
464
465
  }
465
466
 
@@ -468,12 +469,11 @@ export class TalkCore {
468
469
  const self = this.requireSelf();
469
470
  const now = this.now();
470
471
  const records = await listRecords(this.storage);
471
- const filtered = records.filter(
472
- (r) =>
473
- r.cwd === cwd &&
474
- this.isPeerVisible(r.cwd) &&
475
- (includeOffline || now - r.lastSeenAt < LIST_ACTIVE_MS),
476
- );
472
+ const filtered = records.filter((r) => {
473
+ if (r.cwd !== cwd) return false;
474
+ if (r.addr === self.addr) return !this.dead;
475
+ return this.isPeerVisible(r.cwd) && (includeOffline || now - r.lastSeenAt < LIST_ACTIVE_MS);
476
+ });
477
477
  return formatListing(filtered, self.addr, (r) => presenceOf(r, now));
478
478
  }
479
479
 
@@ -31,27 +31,30 @@ export function formatDelivery(letter: Letter, now: number = Date.now()): string
31
31
  return `${BOUNDARY_PREAMBLE}\n\n${header}:\n\n${letter.body}\n\n${meta}${hint}`;
32
32
  }
33
33
 
34
- /** One peer session as the model sees it in a listing. `id` is the stable
35
- * pi session uuid; `name` is the display name when one was set. */
34
+ /** One session as the model sees it in a listing. `id` is the stable pi
35
+ * session uuid; `name` is the display name when one was set; `self` marks
36
+ * the calling session itself. */
36
37
  export interface SessionListItem {
37
38
  status: string;
38
39
  work_dir: string;
39
40
  id: string;
40
41
  name?: string;
42
+ self?: boolean;
41
43
  }
42
44
 
43
- /** Machine-readable JSON listing of peer sessions (what the model sees). */
45
+ /** Machine-readable JSON listing of sessions (what the model sees). */
44
46
  export function formatListing(
45
47
  records: SessionRecord[],
46
48
  selfAddr: string,
47
49
  presence: (r: SessionRecord) => Presence,
48
50
  ): string {
49
- const others = records.filter((r) => r.addr !== selfAddr);
50
- if (others.length === 0) return "[]";
51
- const items: SessionListItem[] = others.map((r) => {
51
+ if (records.length === 0) return "[]";
52
+ const items: SessionListItem[] = records.map((r) => {
52
53
  const p = presence(r);
53
54
  const status = p === "live" ? r.status : p === "stalled" ? "not responding" : "offline";
54
- return { status, work_dir: r.cwd, id: r.sessionId, name: r.name };
55
+ const item: SessionListItem = { status, work_dir: r.cwd, id: r.sessionId, name: r.name };
56
+ if (r.addr === selfAddr) item.self = true;
57
+ return item;
55
58
  });
56
59
  return JSON.stringify(items, null, 2);
57
60
  }
package/src/talk/index.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  import * as fs from "node:fs";
12
12
  import * as os from "node:os";
13
13
  import * as path from "node:path";
14
+ import { fileURLToPath } from "node:url";
14
15
 
15
16
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
17
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
@@ -31,6 +32,9 @@ const NOTIFY_TYPE = "talk:notify";
31
32
 
32
33
  const ASK_TIMEOUT_MS = 30 * 60 * 1000;
33
34
 
35
+ /** Guide the model to the multi-agent workflow skill shipped with this package. */
36
+ const SKILL_PATH = fileURLToPath(new URL("skills/multi-agent-dev/SKILL.md", import.meta.url));
37
+
34
38
  function toolResult(text: string) {
35
39
  return { content: [{ type: "text" as const, text }], details: {} };
36
40
  }
@@ -200,6 +204,14 @@ export default function talk(pi: ExtensionAPI) {
200
204
  pi.on("agent_start", () => core.setWorking());
201
205
  pi.on("agent_end", () => core.setIdle());
202
206
  pi.on("agent_settled", () => core.setIdle());
207
+ pi.on("before_agent_start", (event) => {
208
+ // One-line nudge: before coordinating with other pi sessions, read the
209
+ // shipped workflow skill. Skipped when the skill file is absent.
210
+ if (!fs.existsSync(SKILL_PATH)) return;
211
+ return {
212
+ systemPrompt: `${event.systemPrompt}\n\nBefore multi-session collaboration, read ${SKILL_PATH} to understand the talk workflow.`,
213
+ };
214
+ });
203
215
  pi.on("session_info_changed", () => {
204
216
  if (self) core.setSessionName(pi.getSessionName() ?? self.name);
205
217
  });
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: multi-agent-dev
3
+ description: Coordinate multi-agent development across pi sessions using the talk extension. Explains how sessions discover and address each other, when to use talk-send vs talk-ask, and how to split work, exchange information, and review between agents. Use whenever you need to collaborate with other pi sessions or agents on the same machine.
4
+ ---
5
+
6
+ # Multi-Agent Development with Talk
7
+
8
+ ## Concept
9
+
10
+ Multi-agent development runs several independent pi sessions in parallel and coordinates them over **talk**. Every session is a complete agent workspace — its own cwd, conversation history, and context. Talk lets sessions discover each other, exchange messages, ask questions, and sync progress.
11
+
12
+ The core rule: **a peer only knows what you tell it.** Messages must be self-contained — background, goal, and constraints — because the receiving session has none of your context.
13
+
14
+ ## Session model
15
+
16
+ ### Discovery and addressing
17
+
18
+ - `talk-list-sessions` returns sessions as JSON — **your own session is included and marked `self: true`** (also where you learn your own id):
19
+
20
+ ```json
21
+ [
22
+ {
23
+ "status": "idle",
24
+ "work_dir": "/path/to/cwd",
25
+ "id": "0193a2f5-...",
26
+ "name": "...",
27
+ "self": true
28
+ }
29
+ ]
30
+ ```
31
+
32
+ - Addressing is **by session id only**: `talk-send` / `talk-ask` take the full `id` (pi session uuid). Names, paths, and prefixes are not accepted.
33
+ - An unknown or invisible target is refused with `Unknown session id` — always list before sending.
34
+
35
+ ### Status
36
+
37
+ - `idle` / `working` (agent actively running) / `waiting-talk-message` (blocked in `talk-ask` waiting for a reply)
38
+ - `not responding` (heartbeat stale, process alive but unresponsive) / `offline` (process exited or marked dead)
39
+ - The default listing shows only sessions with a heartbeat in the last 15 minutes; pass `includeOffline: true` for everything.
40
+
41
+ ### Visibility
42
+
43
+ - Each workspace controls what it can see via `allowed` in `<cwd>/.pi/talk.json` (path prefixes). Sessions outside the prefixes are neither listed nor addressable.
44
+ - Visibility is one-way: you seeing a session does not mean it sees you.
45
+
46
+ ## Tools
47
+
48
+ | Tool | Purpose |
49
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `talk-list-sessions` | List visible sessions (`id` / `status` / `work_dir` / `name`) |
51
+ | `talk-send` | Send a plain message (async — the main collaboration primitive). `to: "*"` broadcasts, `to: "cwd"` broadcasts to the same directory |
52
+ | `talk-ask` | Ask a question and block for the reply (default 30 min timeout) |
53
+ | `talk-reply` | Reply to a received ask; `replyTo` is the ask id shown in the delivered message |
54
+
55
+ In the TUI: `/talk` lists sessions, `/talk-dead` marks a session as dead (removes it from listings and sweeps it).
56
+
57
+ ## Collaboration workflows
58
+
59
+ ### Split work between sessions
60
+
61
+ 1. `talk-list-sessions` first: see which sessions exist, their `work_dir`, and status.
62
+ 2. Assign work by module/files with `talk-send` — state the scope, boundaries, and expected output.
63
+ 3. Each session completes its slice, then sends the result or a review request.
64
+ 4. Sync progress periodically to avoid overlapping edits.
65
+
66
+ ### Synchronous question/answer (need the answer to continue)
67
+
68
+ - Use `talk-ask` when the next step depends on the peer's information and the peer is reachable.
69
+ - On receiving an ask, reply with `talk-reply` using the `replyTo` id from the delivered message.
70
+ - If two sessions ask each other simultaneously: the later asker yields — answer the peer's ask first, then re-ask.
71
+
72
+ ### Async notifications
73
+
74
+ - Use `talk-send` for heads-ups that do not block: send and keep working.
75
+ - Messages deliver on the next natural turn by default (`queue`); `steer` interrupts the peer immediately — behavior depends on the `talk.deliver` setting.
76
+
77
+ ### Cross-session review
78
+
79
+ - Ask another session to review your changes: `talk-send` the file paths plus a diff summary, request a review, and let it reply.
80
+ - Send paths and summaries, not whole file contents — the peer can `read` them itself.
81
+
82
+ ## Message style
83
+
84
+ - Plain text, ≤32KB. Send a summary and paths, never the full file or large code blocks.
85
+ - Self-contained: background, goal, constraints — the peer has none of your context.
86
+ - Make the ask explicit: "please review", "please implement", or "FYI only".
87
+ - One topic per message so the reply stays focused.
88
+
89
+ ## Pitfalls
90
+
91
+ - **Avoid message loops**: if the peer sent you something or is asking you, answer it before sending new ones. Two agents pinging each other deadlock.
92
+ - **Address from known ids**: only run `talk-list-sessions` to discover sessions or verify an id. If you already hold a valid id (e.g. from an incoming message or a previous listing), send directly — an unknown or invisible id is refused with `Unknown session id`.
93
+ - **Respect status**: asking an offline/not-responding session blocks up to 30 min. Prefer `talk-send` there — the message queues on disk and the peer receives it when it resumes.
94
+ - **Visibility boundary**: you can only collaborate with sessions you can see; invisible sessions are unreachable by design.