pi-brainstorm 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jarcis-cy
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,111 @@
1
+ # pi-brainstorm
2
+
3
+ Multi-model brainstorming and debate sessions for [pi](https://github.com/earendil-works/pi-coding-agent).
4
+
5
+ `pi-brainstorm` lets different models from different providers join the same discussion. Use `/brainstorm` for a structured round-robin ideation session, or `/debate` for a more adversarial battle where agents challenge each other's assumptions until the discussion converges.
6
+
7
+ The plugin stores each participant's full contribution in a local meeting blackboard under `.pi-meetings/...`. The blackboard is an implementation feature: it keeps the main chat focused on compact cards, summaries, conflicts, and conclusions while preserving the complete transcript on disk.
8
+
9
+ 中文说明见 [README.zh-CN.md](./README.zh-CN.md).
10
+
11
+ ## Features
12
+
13
+ - Multi-model brainstorming with GPT, DeepSeek, and MiniMax style participants.
14
+ - Debate / battle mode where agents prosecute, stress-test, and challenge positions.
15
+ - Round-by-round summaries focused on consensus, disagreement, and next questions.
16
+ - Full participant contributions stored as Markdown files under `.pi-meetings/`.
17
+ - Compact visible cards in the main conversation instead of long pasted transcripts.
18
+ - JSONL index for lightweight cross-round context.
19
+ - Bundled default participant agent definitions for first-time setup.
20
+
21
+ ## Install
22
+
23
+ From npm:
24
+
25
+ ```bash
26
+ pi install npm:pi-brainstorm
27
+ ```
28
+
29
+ From GitHub:
30
+
31
+ ```bash
32
+ pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.2.0
33
+ ```
34
+
35
+ For local development:
36
+
37
+ ```bash
38
+ pi install /Users/jarcis/Project/pi-brainstorm
39
+ ```
40
+
41
+ ## Prerequisites
42
+
43
+ This extension expects pi's `subagent` tool to be available. The command handler creates the local meeting record and then instructs the main agent to run:
44
+
45
+ - `gpt-brainstormer`
46
+ - `deepseek-brainstormer`
47
+ - `minimax-brainstormer`
48
+
49
+ On first use, if any of these user-level agents are missing, the extension asks before writing bundled defaults to `~/.pi/agent/agents/`. Existing files are never overwritten.
50
+
51
+ ## Commands
52
+
53
+ ```text
54
+ /brainstorm <topic>
55
+ /debate <topic>
56
+ ```
57
+
58
+ `/brainstorm` starts a three-round multi-model brainstorming session.
59
+
60
+ `/debate` starts an open-ended multi-agent battle that should continue until convergence or user intervention.
61
+
62
+ ## How It Works
63
+
64
+ ```text
65
+ participant model -> meeting_append_entry -> .pi-meetings/... files
66
+ participant model -> short WROTE_ENTRY summary -> facilitator context
67
+ file watcher -> compact visible card -> main chat
68
+ facilitator -> consensus / disagreement / final synthesis
69
+ ```
70
+
71
+ The blackboard files are the source of truth for the session. The facilitator can read the index or specific entries when producing summaries.
72
+
73
+ ## Tools
74
+
75
+ The extension registers three tools for participants and the facilitator:
76
+
77
+ - `meeting_append_entry` writes a full participant contribution to disk and returns only a short reference.
78
+ - `meeting_read_index` lists entries by id, speaker, phase, summary, and path.
79
+ - `meeting_read_entry` reads one full entry when the facilitator or a participant needs it.
80
+
81
+ ## Files
82
+
83
+ Each session writes an append-only folder under the current working directory:
84
+
85
+ ```text
86
+ .pi-meetings/YYYY-MM-DD-topic/
87
+ manifest.json
88
+ index.jsonl
89
+ blackboard.md
90
+ entries/
91
+ 0001-gpt-round_1.md
92
+ 0002-deepseek-round_1.md
93
+ ```
94
+
95
+ `blackboard.md` is the full transcript. `index.jsonl` is the compact context entry point. Full participant text lives in `entries/`.
96
+
97
+ ## Safety
98
+
99
+ The extension validates that meeting paths stay under the current workspace's `.pi-meetings/` directory and rejects symlinked meeting paths, entry files, index files, and blackboard files. Entry writes use exclusive creation to avoid overwriting existing files.
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ npm pack --dry-run
105
+ ```
106
+
107
+ The extension is TypeScript loaded by pi through its extension loader. Runtime dependencies imported from pi (`@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, and `typebox`) are declared as peer dependencies per pi package guidance.
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,111 @@
1
+ # pi-brainstorm
2
+
3
+ 面向 [pi](https://github.com/earendil-works/pi-coding-agent) 的多模型头脑风暴与辩论插件。
4
+
5
+ `pi-brainstorm` 让来自不同供应商的不同模型参与同一场讨论。使用 `/brainstorm` 启动结构化的多轮头脑风暴;使用 `/debate` 启动更强对抗性的 battle,让不同 Agent 互相质疑假设、攻击薄弱点,直到讨论收敛。
6
+
7
+ 插件会把每个参与者的完整发言保存在 `.pi-meetings/...` 下的本地会议黑板中。黑板是实现亮点,不是主要卖点:它让主会话保持简洁,只展示短卡片、主持人总结、共识、分歧和最终结论,同时完整 transcript 仍然保存在磁盘上。
8
+
9
+ English README: [README.md](./README.md).
10
+
11
+ ## 功能
12
+
13
+ - 多模型头脑风暴:默认包含 GPT、DeepSeek、MiniMax 风格参与者。
14
+ - 辩论 / battle 模式:Agent 会攻击、审视、反驳彼此的观点。
15
+ - 每轮输出聚焦共识、分歧、关键问题和下一步方向。
16
+ - 参与者完整发言以 Markdown 文件保存到 `.pi-meetings/`。
17
+ - 主会话中展示紧凑发言卡片,而不是粘贴长篇 transcript。
18
+ - 使用 JSONL 索引作为轻量级跨轮上下文入口。
19
+ - 首次使用时可安装内置的默认参与者 agent 定义。
20
+
21
+ ## 安装
22
+
23
+ 通过 npm 安装:
24
+
25
+ ```bash
26
+ pi install npm:pi-brainstorm
27
+ ```
28
+
29
+ 通过 GitHub 安装:
30
+
31
+ ```bash
32
+ pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.2.0
33
+ ```
34
+
35
+ 本地开发安装:
36
+
37
+ ```bash
38
+ pi install /Users/jarcis/Project/pi-brainstorm
39
+ ```
40
+
41
+ ## 前置条件
42
+
43
+ 该扩展依赖 pi 中已有的 `subagent` 工具。命令处理器会先创建本地会议记录,然后让主 Agent 调用这些参与者:
44
+
45
+ - `gpt-brainstormer`
46
+ - `deepseek-brainstormer`
47
+ - `minimax-brainstormer`
48
+
49
+ 第一次使用时,如果这些用户级 agent 不存在,扩展会询问是否把内置默认定义写入 `~/.pi/agent/agents/`。已有同名文件不会被覆盖。
50
+
51
+ ## 命令
52
+
53
+ ```text
54
+ /brainstorm <主题>
55
+ /debate <主题>
56
+ ```
57
+
58
+ `/brainstorm` 启动三轮多模型头脑风暴。
59
+
60
+ `/debate` 启动开放式多 Agent battle,直到收敛或用户介入为止。
61
+
62
+ ## 工作方式
63
+
64
+ ```text
65
+ 参与者模型 -> meeting_append_entry -> .pi-meetings/... 文件
66
+ 参与者模型 -> 短 WROTE_ENTRY 摘要 -> 主持人上下文
67
+ 文件 watcher -> 短卡片 -> 主会话可见区域
68
+ 主持人 -> 共识 / 分歧 / 最终综合
69
+ ```
70
+
71
+ 黑板文件是会话事实源。主持人可以按需读取索引或具体条目,然后生成结构化总结。
72
+
73
+ ## 工具
74
+
75
+ 扩展注册三个工具:
76
+
77
+ - `meeting_append_entry`:把参与者完整发言写入磁盘,只返回短引用。
78
+ - `meeting_read_index`:读取条目索引,包含 id、speaker、phase、summary、path。
79
+ - `meeting_read_entry`:在主持人或参与者需要时读取某条完整发言。
80
+
81
+ ## 文件结构
82
+
83
+ 每次会话会在当前工作目录下创建 append-only 目录:
84
+
85
+ ```text
86
+ .pi-meetings/YYYY-MM-DD-topic/
87
+ manifest.json
88
+ index.jsonl
89
+ blackboard.md
90
+ entries/
91
+ 0001-gpt-round_1.md
92
+ 0002-deepseek-round_1.md
93
+ ```
94
+
95
+ `blackboard.md` 是完整 transcript。`index.jsonl` 是紧凑索引。参与者全文放在 `entries/`。
96
+
97
+ ## 安全边界
98
+
99
+ 扩展会校验会议路径必须留在当前工作区的 `.pi-meetings/` 下,并拒绝符号链接形式的会议目录、entry 文件、index 文件和 blackboard 文件。entry 写入使用 exclusive creation,避免覆盖已有文件。
100
+
101
+ ## 开发
102
+
103
+ ```bash
104
+ npm pack --dry-run
105
+ ```
106
+
107
+ 该扩展由 pi 的扩展加载器直接加载 TypeScript。运行时从 pi 引入的 `@earendil-works/pi-coding-agent`、`@earendil-works/pi-tui` 和 `typebox` 按 pi package 规范声明为 peer dependencies。
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,954 @@
1
+ /**
2
+ * pi-brainstorm — Multi-model brainstorm/debate extension for Pi
3
+ *
4
+ * Runs brainstorm and battle-style debate sessions across multiple subagents.
5
+ * Full participant contributions are stored in a local filesystem blackboard,
6
+ * while the main conversation sees compact cards and facilitator synthesis.
7
+ *
8
+ * Features:
9
+ * - meeting_append_entry tool — concurrency-safe append to meeting folder
10
+ * - meeting_read_index tool — read meeting index
11
+ * - meeting_read_entry tool — read full entry content
12
+ * - /brainstorm command — multi-model brainstorming
13
+ * - /debate command — open-ended multi-agent debate
14
+ * - meeting-entry message renderer — compact cards with expandable content
15
+ * - File watcher — auto-posts new entries into the main conversation
16
+ */
17
+
18
+ import * as path from "node:path";
19
+ import * as fs from "node:fs";
20
+ import * as fsp from "node:fs/promises";
21
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
+ import { Type } from "typebox";
23
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
24
+ import { Text, Box } from "@earendil-works/pi-tui";
25
+
26
+ // ────────────────────────────────────────────────────────
27
+ // Helpers
28
+ // ────────────────────────────────────────────────────────
29
+
30
+ /** Sanitize a string for use in filenames (keep letters, digits, hyphens, underscores). */
31
+ function sanitizeFilenamePart(raw: string): string {
32
+ return raw
33
+ .replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, "_")
34
+ .replace(/_+/g, "_")
35
+ .replace(/^_|_$/g, "")
36
+ .slice(0, 60)
37
+ .toLowerCase() || "unknown";
38
+ }
39
+
40
+ /** Convert a topic string to a filesystem-safe slug. */
41
+ function topicToSlug(topic: string): string {
42
+ return sanitizeFilenamePart(topic).slice(0, 40);
43
+ }
44
+
45
+ /** Format today's date as YYYY-MM-DD. */
46
+ function todayStr(): string {
47
+ const d = new Date();
48
+ const yyyy = d.getFullYear();
49
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
50
+ const dd = String(d.getDate()).padStart(2, "0");
51
+ return `${yyyy}-${mm}-${dd}`;
52
+ }
53
+
54
+ /** Validate that meetingDir is a subdirectory of cwd/.pi-meetings. Returns the resolved absolute path. */
55
+ function validateMeetingDir(meetingDir: string, cwd: string): string {
56
+ const resolved = path.resolve(cwd, meetingDir);
57
+ const meetingsRoot = path.resolve(cwd, ".pi-meetings");
58
+ if (!fs.existsSync(meetingsRoot)) {
59
+ fs.mkdirSync(meetingsRoot, { recursive: true });
60
+ }
61
+ assertDirectoryNoSymlink(meetingsRoot, "meetings root");
62
+ assertPathInside(meetingsRoot, resolved, "meetingDir");
63
+ const rootReal = fs.realpathSync(meetingsRoot);
64
+ const targetReal = fs.existsSync(resolved)
65
+ ? fs.realpathSync(resolved)
66
+ : fs.realpathSync(path.dirname(resolved));
67
+ assertPathInside(rootReal, targetReal, "meetingDir real path");
68
+ return resolved;
69
+ }
70
+
71
+ function assertPathInside(baseDir: string, targetPath: string, label: string): void {
72
+ const rel = path.relative(baseDir, targetPath);
73
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
74
+ throw new Error(`${label} must stay under ${baseDir}, got ${targetPath}`);
75
+ }
76
+ }
77
+
78
+ function assertDirectoryNoSymlink(dirPath: string, label: string): void {
79
+ const stat = fs.lstatSync(dirPath);
80
+ if (stat.isSymbolicLink()) {
81
+ throw new Error(`${label} must not be a symbolic link: ${dirPath}`);
82
+ }
83
+ if (!stat.isDirectory()) {
84
+ throw new Error(`${label} must be a directory: ${dirPath}`);
85
+ }
86
+ }
87
+
88
+ function assertExistingFileNoSymlink(filePath: string, label: string): void {
89
+ const stat = fs.lstatSync(filePath);
90
+ if (stat.isSymbolicLink()) {
91
+ throw new Error(`${label} must not be a symbolic link: ${filePath}`);
92
+ }
93
+ if (!stat.isFile()) {
94
+ throw new Error(`${label} must be a regular file: ${filePath}`);
95
+ }
96
+ }
97
+
98
+ function assertWritableFilePath(filePath: string, baseDir: string, label: string): void {
99
+ assertPathInside(baseDir, filePath, label);
100
+ if (fs.existsSync(filePath)) {
101
+ assertExistingFileNoSymlink(filePath, label);
102
+ assertPathInside(fs.realpathSync(baseDir), fs.realpathSync(filePath), `${label} real path`);
103
+ }
104
+ }
105
+
106
+ /** Parse entry filename like "0001-gpt-round-1.md" into parts. */
107
+ function parseEntryFilename(
108
+ filename: string
109
+ ): { id: string; speaker: string; phase: string } | null {
110
+ const match = filename.match(/^(\d{4})-(.+)-(.+)\.md$/);
111
+ if (!match) return null;
112
+ return { id: match[1], speaker: match[2], phase: match[3] };
113
+ }
114
+
115
+ /** Read the first heading from an entry file to use as display summary. */
116
+ function readEntrySummary(absPath: string): string {
117
+ try {
118
+ const content = fs.readFileSync(absPath, "utf-8");
119
+ const m = content.match(/^#\s*(.+)$/m);
120
+ return m ? m[1].trim() : path.basename(absPath, ".md");
121
+ } catch {
122
+ return path.basename(absPath, ".md");
123
+ }
124
+ }
125
+
126
+ // ────────────────────────────────────────────────────────
127
+ // Watcher management
128
+ // ────────────────────────────────────────────────────────
129
+
130
+ /** Active watchers: meetingDir (absolute) → { watcher, debounce timers } */
131
+ const activeWatchers = new Map<
132
+ string,
133
+ { watcher: fs.FSWatcher; debounceTimers: Map<string, NodeJS.Timeout> }
134
+ >();
135
+
136
+ function startWatching(pi: ExtensionAPI, meetingDir: string): void {
137
+ const absDir = meetingDir;
138
+ const entriesDir = path.join(absDir, "entries");
139
+
140
+ // Ensure entries directory exists
141
+ fs.mkdirSync(entriesDir, { recursive: true });
142
+ assertDirectoryNoSymlink(absDir, "meeting directory");
143
+ assertDirectoryNoSymlink(entriesDir, "entries directory");
144
+
145
+ // Stop any existing watcher for this meeting
146
+ stopWatching(absDir);
147
+
148
+ const debounceTimers = new Map<string, NodeJS.Timeout>();
149
+
150
+ const watcher = fs.watch(entriesDir, (_eventType, filename) => {
151
+ if (!filename || !filename.endsWith(".md")) return;
152
+
153
+ // Debounce: wait 300ms before processing to let the file be fully written
154
+ const existing = debounceTimers.get(filename);
155
+ if (existing) clearTimeout(existing);
156
+
157
+ debounceTimers.set(
158
+ filename,
159
+ setTimeout(() => {
160
+ debounceTimers.delete(filename);
161
+ const entryPath = path.join(entriesDir, filename);
162
+ try {
163
+ if (!fs.existsSync(entryPath)) return;
164
+ assertExistingFileNoSymlink(entryPath, "meeting entry");
165
+ assertPathInside(fs.realpathSync(entriesDir), fs.realpathSync(entryPath), "meeting entry real path");
166
+ const parsed = parseEntryFilename(filename);
167
+ if (!parsed) return;
168
+ const summary = readEntrySummary(entryPath);
169
+
170
+ pi.sendMessage(
171
+ {
172
+ customType: "meeting-entry",
173
+ content: `${parsed.speaker} · ${parsed.phase}`,
174
+ display: true,
175
+ details: {
176
+ path: entryPath,
177
+ speaker: parsed.speaker,
178
+ phase: parsed.phase,
179
+ summary,
180
+ meetingDir: absDir,
181
+ },
182
+ },
183
+ { deliverAs: "steer" }
184
+ );
185
+ } catch {
186
+ // Silently ignore watcher errors
187
+ }
188
+ }, 300)
189
+ );
190
+ });
191
+
192
+ watcher.on("error", () => {
193
+ // Silently handle watcher errors (e.g., directory deleted)
194
+ });
195
+
196
+ activeWatchers.set(absDir, { watcher, debounceTimers });
197
+ }
198
+
199
+ function stopWatching(meetingDir: string): void {
200
+ const entry = activeWatchers.get(meetingDir);
201
+ if (!entry) return;
202
+ for (const timer of entry.debounceTimers.values()) {
203
+ clearTimeout(timer);
204
+ }
205
+ try {
206
+ entry.watcher.close();
207
+ } catch {
208
+ // Ignore close errors
209
+ }
210
+ activeWatchers.delete(meetingDir);
211
+ }
212
+
213
+ // ────────────────────────────────────────────────────────
214
+ // Manifest helpers
215
+ // ────────────────────────────────────────────────────────
216
+
217
+ interface MeetingManifest {
218
+ topic: string;
219
+ created: string;
220
+ lastUpdate: string;
221
+ entryCount: number;
222
+ }
223
+
224
+ const BRAINSTORM_AGENT_FILES: Record<string, string> = {
225
+ "gpt-brainstormer.md": `---
226
+ name: gpt-brainstormer
227
+ description: GPT brainstorming consultant. Visionary strategist for multi-model discussion sessions.
228
+ tools: read, grep, find, ls, meeting_append_entry, meeting_read_index, meeting_read_entry
229
+ model: vendor-codex/gpt-5.5:xhigh
230
+ ---
231
+
232
+ # GPT Brainstormer - Visionary Strategist
233
+
234
+ 你是多模型头脑风暴中的愿景战略家。思考大局,发现别人忽略的机会,将复杂权衡综合为清晰方向。用中文回答。
235
+
236
+ ## What You Do
237
+ - 提出创新的战略方向和解决方案
238
+ - 发现别人忽略的机会和盲点
239
+ - 把零散想法综合成连贯战略
240
+
241
+ ## What You Do Not Do
242
+ - 写代码或修改项目文件,你只读项目文件
243
+ - 委派给其他 Agent
244
+ - 在聊天中直接粘贴长篇分析;当明确指示使用 meeting_append_entry 时,必须将完整贡献写入会议黑板,最终回复仅写 WROTE_ENTRY + 一句话摘要
245
+
246
+ ## Worker Preamble
247
+ You are a terminal worker. Work directly with tools. Do NOT spawn sub-agents.
248
+ `,
249
+ "deepseek-brainstormer.md": `---
250
+ name: deepseek-brainstormer
251
+ description: DeepSeek brainstorming consultant. Meticulous systems thinker for multi-model discussion sessions.
252
+ tools: read, grep, find, ls, meeting_append_entry, meeting_read_index, meeting_read_entry
253
+ model: deepseek/deepseek-v4-pro:xhigh
254
+ ---
255
+
256
+ # DeepSeek Brainstormer - Meticulous Systems Thinker
257
+
258
+ 你是多模型头脑风暴中的系统思考者。分析结构、依赖、扩展上限和失败模式。用中文回答。
259
+
260
+ ## What You Do
261
+ - 从结构、依赖和风险角度分析提案
262
+ - 识别隐藏耦合、扩展上限和失败模式
263
+ - 提出具体、可实现、可验证的技术优化方案
264
+
265
+ ## What You Do Not Do
266
+ - 写代码或修改项目文件,你只读项目文件
267
+ - 委派给其他 Agent
268
+ - 在聊天中直接粘贴长篇分析;当明确指示使用 meeting_append_entry 时,必须将完整贡献写入会议黑板,最终回复仅写 WROTE_ENTRY + 一句话摘要
269
+
270
+ ## Worker Preamble
271
+ You are a terminal worker. Work directly with tools. Do NOT spawn sub-agents.
272
+ `,
273
+ "minimax-brainstormer.md": `---
274
+ name: minimax-brainstormer
275
+ description: MiniMax brainstorming consultant. Creative lateral thinker for multi-model discussion sessions.
276
+ tools: read, grep, find, ls, meeting_append_entry, meeting_read_index, meeting_read_entry
277
+ model: minimax-cn/MiniMax-M3:xhigh
278
+ ---
279
+
280
+ # MiniMax Brainstormer - Creative Lateral Thinker
281
+
282
+ 你是多模型头脑风暴中的创意顾问。跳出框框思考,挑战隐性假设,提出非常规方案。用中文回答。
283
+
284
+ ## What You Do
285
+ - 从意想不到的角度切入问题
286
+ - 提出打破常规的创新方案
287
+ - 挑战团队隐性假设
288
+
289
+ ## What You Do Not Do
290
+ - 写代码或修改项目文件,你只读项目文件
291
+ - 委派给其他 Agent
292
+ - 在聊天中直接粘贴长篇分析;当明确指示使用 meeting_append_entry 时,必须将完整贡献写入会议黑板,最终回复仅写 WROTE_ENTRY + 一句话摘要
293
+
294
+ ## Worker Preamble
295
+ You are a terminal worker. Work directly with tools. Do NOT spawn sub-agents.
296
+ `,
297
+ };
298
+
299
+ async function ensureBrainstormAgents(ctx: any): Promise<boolean> {
300
+ const agentsDir = path.join(getAgentDir(), "agents");
301
+ const missing = Object.keys(BRAINSTORM_AGENT_FILES).filter(
302
+ (filename) => !fs.existsSync(path.join(agentsDir, filename))
303
+ );
304
+ if (missing.length === 0) return true;
305
+
306
+ if (!ctx.hasUI) {
307
+ ctx.ui?.notify?.(
308
+ `Missing meeting agents: ${missing.join(", ")}. Install them under ${agentsDir}.`,
309
+ "warning"
310
+ );
311
+ return false;
312
+ }
313
+
314
+ const ok = await ctx.ui.confirm(
315
+ "Install meeting brainstorm agents?",
316
+ `The blackboard meeting commands need these user-level agents:\n${missing
317
+ .map((name) => `- ${name}`)
318
+ .join("\n")}\n\nThey will be created under ${agentsDir}. Existing files are not overwritten.`
319
+ );
320
+ if (!ok) return false;
321
+
322
+ await fsp.mkdir(agentsDir, { recursive: true });
323
+ for (const filename of missing) {
324
+ const target = path.join(agentsDir, filename);
325
+ await fsp.writeFile(target, BRAINSTORM_AGENT_FILES[filename], {
326
+ encoding: "utf-8",
327
+ flag: "wx",
328
+ });
329
+ }
330
+ ctx.ui.notify(`Installed ${missing.length} meeting agent(s).`, "info");
331
+ return true;
332
+ }
333
+
334
+ async function readManifest(absDir: string): Promise<MeetingManifest | null> {
335
+ const manifestPath = path.join(absDir, "manifest.json");
336
+ try {
337
+ assertWritableFilePath(manifestPath, absDir, "manifest");
338
+ const raw = await fsp.readFile(manifestPath, "utf-8");
339
+ return JSON.parse(raw) as MeetingManifest;
340
+ } catch {
341
+ return null;
342
+ }
343
+ }
344
+
345
+ async function writeManifest(
346
+ absDir: string,
347
+ manifest: MeetingManifest
348
+ ): Promise<void> {
349
+ const manifestPath = path.join(absDir, "manifest.json");
350
+ assertWritableFilePath(manifestPath, absDir, "manifest");
351
+ await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
352
+ }
353
+
354
+ async function getEntryCount(absDir: string): Promise<number> {
355
+ const indexJsonlPath = path.join(absDir, "index.jsonl");
356
+ try {
357
+ assertWritableFilePath(indexJsonlPath, absDir, "meeting index");
358
+ const raw = await fsp.readFile(indexJsonlPath, "utf-8");
359
+ if (!raw.trim()) return 0;
360
+ return raw.split("\n").filter((l) => l.trim()).length;
361
+ } catch {
362
+ return 0;
363
+ }
364
+ }
365
+
366
+ // ────────────────────────────────────────────────────────
367
+ // Main Extension
368
+ // ────────────────────────────────────────────────────────
369
+
370
+ export default function (pi: ExtensionAPI) {
371
+ // ── Tool: meeting_append_entry ─────────────────────────
372
+
373
+ pi.registerTool({
374
+ name: "meeting_append_entry",
375
+ label: "Meeting Append Entry",
376
+ description:
377
+ "Append a contribution to a meeting blackboard. Write your FULL brainstorm/debate response to disk. " +
378
+ "This is append-only and concurrency-safe. After writing, reply ONLY with WROTE_ENTRY plus a one-sentence summary — do NOT paste the full content into chat.",
379
+ promptSnippet:
380
+ "meeting_append_entry({ meetingDir, speaker, phase, title?, summary, content }) — write full contribution to meeting blackboard",
381
+ promptGuidelines: [
382
+ "Use meeting_append_entry to write your FULL brainstorm/debate contribution to disk. Then reply ONLY with 'WROTE_ENTRY: <one-sentence summary>' — do NOT repeat the full content in chat.",
383
+ ],
384
+ parameters: Type.Object({
385
+ meetingDir: Type.String({
386
+ description:
387
+ "Absolute path to the meeting directory (e.g., /path/to/.pi-meetings/2026-06-28-my-topic)",
388
+ }),
389
+ speaker: Type.String({
390
+ description: "Speaker identifier (e.g., GPT, DeepSeek, MiniMax)",
391
+ }),
392
+ phase: Type.String({
393
+ description: "Meeting phase (e.g., Round 1, Round 2, Final)",
394
+ }),
395
+ title: Type.Optional(
396
+ Type.String({
397
+ description: "Optional title for this entry",
398
+ })
399
+ ),
400
+ summary: Type.String({
401
+ description:
402
+ "One-sentence summary of this contribution (shown in meeting index)",
403
+ }),
404
+ content: Type.String({
405
+ description:
406
+ "FULL contribution content in Markdown. This is the complete text that will be stored on disk.",
407
+ }),
408
+ }),
409
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
410
+ const cwd = ctx.cwd;
411
+ const absDir = validateMeetingDir(params.meetingDir, cwd);
412
+
413
+ // Sanitize filename parts
414
+ const speakerSlug = sanitizeFilenamePart(params.speaker);
415
+ const phaseSlug = sanitizeFilenamePart(params.phase);
416
+
417
+ // Use withFileMutationQueue on the manifest to serialize writes for this meeting
418
+ assertDirectoryNoSymlink(absDir, "meeting directory");
419
+ const manifestPath = path.join(absDir, "manifest.json");
420
+ assertWritableFilePath(manifestPath, absDir, "manifest");
421
+
422
+ return withFileMutationQueue(manifestPath, async () => {
423
+ // Ensure directories exist
424
+ const entriesDir = path.join(absDir, "entries");
425
+ await fsp.mkdir(entriesDir, { recursive: true });
426
+ assertDirectoryNoSymlink(entriesDir, "entries directory");
427
+
428
+ // Read or create manifest
429
+ let manifest = await readManifest(absDir);
430
+ if (!manifest) {
431
+ // Should not normally happen — commands seed the manifest
432
+ manifest = {
433
+ topic: path.basename(absDir),
434
+ created: new Date().toISOString(),
435
+ lastUpdate: new Date().toISOString(),
436
+ entryCount: 0,
437
+ };
438
+ await writeManifest(absDir, manifest);
439
+ }
440
+
441
+ // Determine entry number from existing index
442
+ const currentCount = await getEntryCount(absDir);
443
+ const entryId = String(currentCount + 1).padStart(4, "0");
444
+ const entryFilename = `${entryId}-${speakerSlug}-${phaseSlug}.md`;
445
+ const entryRelPath = path.join("entries", entryFilename);
446
+ const entryAbsPath = path.join(absDir, entryRelPath);
447
+ assertWritableFilePath(entryAbsPath, absDir, "meeting entry");
448
+
449
+ // Format the entry file content
450
+ const heading = params.title
451
+ ? `# ${params.speaker} (${params.phase}): ${params.title}\n\n`
452
+ : `# ${params.speaker} (${params.phase}): ${params.summary}\n\n`;
453
+ const entryContent = heading + params.content;
454
+
455
+ // Write entry file. wx prevents overwriting pre-existing files or symlinks.
456
+ await fsp.writeFile(entryAbsPath, entryContent, {
457
+ encoding: "utf-8",
458
+ flag: "wx",
459
+ });
460
+
461
+ // Append to index.jsonl
462
+ const indexEntry = {
463
+ id: entryId,
464
+ speaker: params.speaker,
465
+ phase: params.phase,
466
+ title: params.title ?? null,
467
+ summary: params.summary,
468
+ path: entryRelPath,
469
+ timestamp: new Date().toISOString(),
470
+ };
471
+ const indexJsonlPath = path.join(absDir, "index.jsonl");
472
+ assertWritableFilePath(indexJsonlPath, absDir, "meeting index");
473
+ await fsp.appendFile(
474
+ indexJsonlPath,
475
+ JSON.stringify(indexEntry) + "\n",
476
+ "utf-8"
477
+ );
478
+
479
+ // Append to blackboard.md
480
+ const blackboardPath = path.join(absDir, "blackboard.md");
481
+ assertWritableFilePath(blackboardPath, absDir, "meeting blackboard");
482
+ const blackboardEntry = [
483
+ "",
484
+ `## ${params.speaker} (${params.phase}): ${params.title ?? params.summary}`,
485
+ "",
486
+ params.content,
487
+ "",
488
+ "---",
489
+ "",
490
+ ].join("\n");
491
+ await fsp.appendFile(blackboardPath, blackboardEntry, "utf-8");
492
+
493
+ // Update manifest
494
+ manifest.lastUpdate = new Date().toISOString();
495
+ manifest.entryCount = currentCount + 1;
496
+ await writeManifest(absDir, manifest);
497
+
498
+ // Return only short reference — NOT full content
499
+ return {
500
+ content: [
501
+ {
502
+ type: "text" as const,
503
+ text: `Entry ${entryId} written: ${entryRelPath} — ${params.summary}`,
504
+ },
505
+ ],
506
+ details: {
507
+ id: entryId,
508
+ path: entryRelPath,
509
+ summary: params.summary,
510
+ },
511
+ };
512
+ });
513
+ },
514
+ });
515
+
516
+ // ── Tool: meeting_read_index ──────────────────────────
517
+
518
+ pi.registerTool({
519
+ name: "meeting_read_index",
520
+ label: "Meeting Read Index",
521
+ description:
522
+ "Read the index of all entries in a meeting blackboard. Returns the list of entries with id, speaker, phase, title, summary, and path.",
523
+ promptSnippet:
524
+ "meeting_read_index({ meetingDir, limit? }) — list meeting entries",
525
+ parameters: Type.Object({
526
+ meetingDir: Type.String({
527
+ description:
528
+ "Absolute path to the meeting directory (e.g., /path/to/.pi-meetings/2026-06-28-my-topic)",
529
+ }),
530
+ limit: Type.Optional(
531
+ Type.Number({
532
+ description:
533
+ "Maximum number of entries to return (most recent first). Omit for all.",
534
+ })
535
+ ),
536
+ }),
537
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
538
+ const absDir = validateMeetingDir(params.meetingDir, ctx.cwd);
539
+ const indexJsonlPath = path.join(absDir, "index.jsonl");
540
+
541
+ try {
542
+ assertWritableFilePath(indexJsonlPath, absDir, "meeting index");
543
+ const raw = await fsp.readFile(indexJsonlPath, "utf-8");
544
+ const lines = raw
545
+ .split("\n")
546
+ .filter((l) => l.trim())
547
+ .map((l) => JSON.parse(l));
548
+
549
+ const result = params.limit
550
+ ? lines.slice(-params.limit).reverse()
551
+ : [...lines].reverse();
552
+
553
+ return {
554
+ content: [
555
+ {
556
+ type: "text" as const,
557
+ text:
558
+ `Meeting index (${result.length} of ${lines.length} entries):\n` +
559
+ result
560
+ .map(
561
+ (e) =>
562
+ ` [${e.id}] ${e.speaker} · ${e.phase} — ${e.summary}`
563
+ )
564
+ .join("\n"),
565
+ },
566
+ ],
567
+ details: { entries: result },
568
+ };
569
+ } catch {
570
+ return {
571
+ content: [
572
+ {
573
+ type: "text" as const,
574
+ text: "Meeting index is empty or does not exist yet.",
575
+ },
576
+ ],
577
+ details: { entries: [] },
578
+ };
579
+ }
580
+ },
581
+ });
582
+
583
+ // ── Tool: meeting_read_entry ──────────────────────────
584
+
585
+ pi.registerTool({
586
+ name: "meeting_read_entry",
587
+ label: "Meeting Read Entry",
588
+ description:
589
+ "Read the full content of a specific entry from a meeting blackboard. Use this to get the complete text of a participant's contribution.",
590
+ promptSnippet:
591
+ "meeting_read_entry({ meetingDir, entryPath }) — read full entry content",
592
+ parameters: Type.Object({
593
+ meetingDir: Type.String({
594
+ description:
595
+ "Absolute path to the meeting directory (e.g., /path/to/.pi-meetings/2026-06-28-my-topic)",
596
+ }),
597
+ entryPath: Type.String({
598
+ description:
599
+ "Relative path to the entry file within the meeting (e.g., entries/0001-gpt-round-1.md)",
600
+ }),
601
+ }),
602
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
603
+ const absDir = validateMeetingDir(params.meetingDir, ctx.cwd);
604
+ const absEntryPath = path.resolve(absDir, params.entryPath);
605
+
606
+ // Validate that entryPath doesn't escape the meeting directory
607
+ const rel = path.relative(absDir, absEntryPath);
608
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
609
+ return {
610
+ content: [
611
+ {
612
+ type: "text" as const,
613
+ text: `Invalid entryPath: ${params.entryPath} escapes meeting directory.`,
614
+ },
615
+ ],
616
+ details: {},
617
+ isError: true,
618
+ };
619
+ }
620
+
621
+ try {
622
+ assertExistingFileNoSymlink(absEntryPath, "meeting entry");
623
+ assertPathInside(fs.realpathSync(absDir), fs.realpathSync(absEntryPath), "meeting entry real path");
624
+ const content = await fsp.readFile(absEntryPath, "utf-8");
625
+ return {
626
+ content: [
627
+ {
628
+ type: "text" as const,
629
+ text: `Entry: ${rel}\n\n${content}`,
630
+ },
631
+ ],
632
+ details: { path: rel, content },
633
+ };
634
+ } catch {
635
+ return {
636
+ content: [
637
+ {
638
+ type: "text" as const,
639
+ text: `Entry not found: ${params.entryPath}`,
640
+ },
641
+ ],
642
+ details: {},
643
+ isError: true,
644
+ };
645
+ }
646
+ },
647
+ });
648
+
649
+ // ── Message Renderer: meeting-entry ───────────────────
650
+
651
+ pi.registerMessageRenderer(
652
+ "meeting-entry",
653
+ (message, { expanded }, theme) => {
654
+ const details = message.details as
655
+ | {
656
+ path?: string;
657
+ speaker?: string;
658
+ phase?: string;
659
+ summary?: string;
660
+ meetingDir?: string;
661
+ }
662
+ | undefined;
663
+
664
+ const contentStr: string =
665
+ typeof message.content === "string"
666
+ ? message.content
667
+ : (message.content as Array<{ type: string; text?: string }>)
668
+ .map((c) => (c.type === "text" ? (c.text ?? "") : ""))
669
+ .join("");
670
+
671
+ const speaker = details?.speaker ?? "Unknown";
672
+ const phase = details?.phase ?? "";
673
+ const summary = details?.summary ?? contentStr;
674
+
675
+ // Compact view: speaker · phase — summary
676
+ let text = theme.fg("accent", `▸ ${speaker}`);
677
+ if (phase) text += theme.fg("dim", ` · ${phase}`);
678
+ text += `\n${theme.fg("muted", summary)}`;
679
+
680
+ // Expanded view: read full content from disk
681
+ if (expanded && details?.path) {
682
+ try {
683
+ assertExistingFileNoSymlink(details.path, "meeting entry");
684
+ if (details.meetingDir) {
685
+ assertPathInside(fs.realpathSync(details.meetingDir), fs.realpathSync(details.path), "meeting entry real path");
686
+ }
687
+ const fullContent = fs.readFileSync(details.path, "utf-8");
688
+ text += `\n\n${theme.fg("dim", fullContent)}`;
689
+ } catch {
690
+ text += `\n\n${theme.fg("error", "(entry file not found)")}`;
691
+ }
692
+ }
693
+
694
+ const box = new Box(1, 1, (t: string) => theme.bg("customMessageBg", t));
695
+ box.addChild(new Text(text, 0, 0));
696
+ return box;
697
+ }
698
+ );
699
+
700
+ // ── Command: /brainstorm ─────────────────────────────
701
+
702
+ pi.registerCommand("brainstorm", {
703
+ description:
704
+ "Start a multi-model brainstorming session on a topic",
705
+ handler: async (args, ctx) => {
706
+ if (!args || !args.trim()) {
707
+ ctx.ui.notify("Usage: /brainstorm <topic>", "warning");
708
+ return;
709
+ }
710
+
711
+ const agentsReady = await ensureBrainstormAgents(ctx);
712
+ if (!agentsReady) return;
713
+
714
+ const topic = args.trim();
715
+ const slug = topicToSlug(topic);
716
+ const dateStr = todayStr();
717
+ const meetingName = `${dateStr}-${slug}`;
718
+ const absDir = validateMeetingDir(
719
+ path.resolve(ctx.cwd, ".pi-meetings", meetingName),
720
+ ctx.cwd
721
+ );
722
+
723
+ // Create meeting folder structure
724
+ await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
725
+ assertDirectoryNoSymlink(absDir, "meeting directory");
726
+ assertDirectoryNoSymlink(path.join(absDir, "entries"), "entries directory");
727
+
728
+ // Seed manifest
729
+ const manifest: MeetingManifest = {
730
+ topic,
731
+ created: new Date().toISOString(),
732
+ lastUpdate: new Date().toISOString(),
733
+ entryCount: 0,
734
+ };
735
+ await writeManifest(absDir, manifest);
736
+
737
+ // Seed index.jsonl (empty)
738
+ const indexJsonlPath = path.join(absDir, "index.jsonl");
739
+ assertWritableFilePath(indexJsonlPath, absDir, "meeting index");
740
+ if (!fs.existsSync(indexJsonlPath)) {
741
+ await fsp.writeFile(indexJsonlPath, "", "utf-8");
742
+ }
743
+
744
+ // Seed blackboard.md header
745
+ const blackboardPath = path.join(absDir, "blackboard.md");
746
+ assertWritableFilePath(blackboardPath, absDir, "meeting blackboard");
747
+ const blackboardHeader = [
748
+ `# Meeting: ${topic}`,
749
+ `> Created: ${new Date().toISOString()}`,
750
+ `> Type: Brainstorming (3 rounds)`,
751
+ "",
752
+ "---",
753
+ "",
754
+ ].join("\n");
755
+ await fsp.writeFile(blackboardPath, blackboardHeader, "utf-8");
756
+
757
+ // Start watcher
758
+ startWatching(pi, absDir);
759
+
760
+ ctx.ui.notify(
761
+ `🧠 Multi-model brainstorm: ${meetingName}\n` +
762
+ ` Folder: .pi-meetings/${meetingName}/\n` +
763
+ ` Watcher active — entries will appear as cards`,
764
+ "info"
765
+ );
766
+
767
+ // Send orchestration prompt to main agent
768
+ pi.sendUserMessage([
769
+ {
770
+ type: "text" as const,
771
+ text: [
772
+ `BLACKBOARD BRAINSTORMING SESSION: ${topic}`,
773
+ "",
774
+ `Meeting folder: \`${absDir}\``,
775
+ "",
776
+ "You are facilitating a round-robin brainstorming session using the MEETING BLACKBOARD.",
777
+ "Each consultant writes their FULL contribution to disk via meeting_append_entry.",
778
+ "",
779
+ "## Consultants (3 rounds)",
780
+ "- **GPT**: use the gpt-brainstormer subagent. Visionary strategist.",
781
+ "- **DeepSeek**: use the deepseek-brainstormer subagent. Systems thinker.",
782
+ "- **MiniMax**: use the minimax-brainstormer subagent. Creative lateral thinker.",
783
+ "",
784
+ "## CRITICAL INSTRUCTIONS",
785
+ "",
786
+ "### For subagents (include in EVERY task):",
787
+ "1. Write your FULL contribution using the meeting_append_entry tool with:",
788
+ ` - meetingDir: "${absDir}"`,
789
+ " - speaker: your name (GPT, DeepSeek, or MiniMax)",
790
+ ' - phase: "Round 1", "Round 2", or "Round 3"',
791
+ " - summary: a ONE-SENTENCE summary of your contribution",
792
+ " - content: your FULL analysis in Chinese (中文)",
793
+ "2. After writing, your FINAL ANSWER must be ONLY:",
794
+ " `WROTE_ENTRY: <your one-sentence summary>`",
795
+ "3. DO NOT paste your full analysis into the chat. The main agent and user will read it from the blackboard.",
796
+ "",
797
+ "### For you, the facilitator:",
798
+ "- Do NOT paste participant full text into chat. They are on the blackboard.",
799
+ "- After each round, read the index with meeting_read_index and present a structural overview.",
800
+ "- Optionally read full entries with meeting_read_entry when needed.",
801
+ "- Present each consultant's summary + your structural overview (conflict matrix, consensus table).",
802
+ "- When the user gives feedback, relay it VERBATIM to the consultants in the next round.",
803
+ "",
804
+ "## Protocol",
805
+ "Round 1: Each consultant gives initial analysis on the topic. Run all 3 in parallel.",
806
+ "Round 2: Feed prior discussion back to each. Ask each to challenge the others and propose improvements.",
807
+ "Round 3: Each gives FINAL recommendation, synthesizing the best ideas.",
808
+ "",
809
+ "After Round 3, present the complete structural overview and a synthesized conclusion.",
810
+ "",
811
+ "## IMPORTANT",
812
+ "- All responses in Chinese (中文).",
813
+ "- Save transcript.md and (after user confirms) conclusion.md per the MEETING OUTPUT PROTOCOL.",
814
+ "- The user can intervene at any time to steer the discussion.",
815
+ ].join("\n"),
816
+ },
817
+ ]);
818
+ },
819
+ });
820
+
821
+ // ── Command: /debate ─────────────────────────────────
822
+
823
+ pi.registerCommand("debate", {
824
+ description:
825
+ "Start a multi-agent debate battle — runs until convergence",
826
+ handler: async (args, ctx) => {
827
+ if (!args || !args.trim()) {
828
+ ctx.ui.notify("Usage: /debate <topic>", "warning");
829
+ return;
830
+ }
831
+
832
+ const agentsReady = await ensureBrainstormAgents(ctx);
833
+ if (!agentsReady) return;
834
+
835
+ const topic = args.trim();
836
+ const slug = topicToSlug(topic);
837
+ const dateStr = todayStr();
838
+ const meetingName = `${dateStr}-${slug}`;
839
+ const absDir = validateMeetingDir(
840
+ path.resolve(ctx.cwd, ".pi-meetings", meetingName),
841
+ ctx.cwd
842
+ );
843
+
844
+ // Create meeting folder structure
845
+ await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
846
+ assertDirectoryNoSymlink(absDir, "meeting directory");
847
+ assertDirectoryNoSymlink(path.join(absDir, "entries"), "entries directory");
848
+
849
+ // Seed manifest
850
+ const manifest: MeetingManifest = {
851
+ topic,
852
+ created: new Date().toISOString(),
853
+ lastUpdate: new Date().toISOString(),
854
+ entryCount: 0,
855
+ };
856
+ await writeManifest(absDir, manifest);
857
+
858
+ // Seed index.jsonl (empty)
859
+ const indexJsonlPath = path.join(absDir, "index.jsonl");
860
+ assertWritableFilePath(indexJsonlPath, absDir, "meeting index");
861
+ if (!fs.existsSync(indexJsonlPath)) {
862
+ await fsp.writeFile(indexJsonlPath, "", "utf-8");
863
+ }
864
+
865
+ // Seed blackboard.md header
866
+ const blackboardPath = path.join(absDir, "blackboard.md");
867
+ assertWritableFilePath(blackboardPath, absDir, "meeting blackboard");
868
+ const blackboardHeader = [
869
+ `# Debate: ${topic}`,
870
+ `> Created: ${new Date().toISOString()}`,
871
+ `> Type: Open-ended debate (until convergence)`,
872
+ "",
873
+ "---",
874
+ "",
875
+ ].join("\n");
876
+ await fsp.writeFile(blackboardPath, blackboardHeader, "utf-8");
877
+
878
+ // Start watcher
879
+ startWatching(pi, absDir);
880
+
881
+ ctx.ui.notify(
882
+ `⚔️ Multi-agent debate: ${meetingName}\n` +
883
+ ` Folder: .pi-meetings/${meetingName}/\n` +
884
+ ` Watcher active — entries will appear as cards\n` +
885
+ ` Open-ended — runs until convergence or you intervene`,
886
+ "info"
887
+ );
888
+
889
+ // Send orchestration prompt to main agent
890
+ pi.sendUserMessage([
891
+ {
892
+ type: "text" as const,
893
+ text: [
894
+ `⚔️ BLACKBOARD DEBATE: ${topic}`,
895
+ "",
896
+ `Meeting folder: \`${absDir}\``,
897
+ "",
898
+ "You are facilitating an OPEN-ENDED debate using the MEETING BLACKBOARD.",
899
+ "Each debater writes their FULL argument to disk via meeting_append_entry.",
900
+ "Continue until the debate CONVERGES or the user intervenes.",
901
+ "",
902
+ "## Debaters (cycling indefinitely)",
903
+ "- **GPT** (gpt-brainstormer): THE PROSECUTOR — Attack other positions ruthlessly. Find every logical flaw, hidden assumption, and missing edge case.",
904
+ "- **DeepSeek** (deepseek-brainstormer): THE SYSTEMS SKEPTIC — Dissect structural implications. What breaks at scale? Where are the hidden costs?",
905
+ "- **MiniMax** (minimax-brainstormer): THE CONTRARIAN — Take the opposite position. Expose groupthink. Propose radical alternatives.",
906
+ "",
907
+ "## CRITICAL INSTRUCTIONS",
908
+ "",
909
+ "### For subagents (include in EVERY task):",
910
+ "1. Write your FULL contribution using the meeting_append_entry tool with:",
911
+ ` - meetingDir: "${absDir}"`,
912
+ " - speaker: your name (GPT, DeepSeek, or MiniMax)",
913
+ ' - phase: "Cycle 1", "Cycle 2", etc.',
914
+ " - summary: a ONE-SENTENCE summary of your argument",
915
+ " - content: your FULL argument in Chinese (中文)",
916
+ "2. After writing, your FINAL ANSWER must be ONLY:",
917
+ " `WROTE_ENTRY: <your one-sentence summary>`",
918
+ "3. DO NOT paste your full argument into the chat.",
919
+ "",
920
+ "### Include the FULL VERBATIM prior debate record in each subagent task.",
921
+ "Use meeting_read_index and meeting_read_entry to retrieve the complete debate history.",
922
+ "NEVER summarize or truncate the debate record when passing to subagents.",
923
+ "",
924
+ "### For you, the facilitator:",
925
+ "- Do NOT paste participant full text into chat. They are on the blackboard.",
926
+ "- Run debaters in CHAIN mode (one at a time, each sees all prior entries).",
927
+ "- Read the index with meeting_read_index frequently.",
928
+ "- Read full entries with meeting_read_entry when synthesizing.",
929
+ "- After EACH full cycle (all 3 spoke), check for CONVERGENCE:",
930
+ " * Do 2+ agents agree on a specific conclusion?",
931
+ " * Did the last cycle introduce any NEW arguments?",
932
+ " * Did anyone explicitly concede?",
933
+ "- If NOT converged: run another cycle. Keep going.",
934
+ "- If converged: present synthesis to me.",
935
+ "",
936
+ "## Rules",
937
+ "- NEVER stop at a predetermined count. Only convergence or user intervention ends this debate.",
938
+ "- All responses in Chinese (中文).",
939
+ "- After convergence, save transcript.md immediately and (after user confirms) conclusion.md per the MEETING OUTPUT PROTOCOL.",
940
+ "- Present: (1) the debate arc, (2) who conceded what, (3) final synthesis.",
941
+ ].join("\n"),
942
+ },
943
+ ]);
944
+ },
945
+ });
946
+
947
+ // ── Cleanup on session shutdown ───────────────────────
948
+
949
+ pi.on("session_shutdown", () => {
950
+ for (const dir of activeWatchers.keys()) {
951
+ stopWatching(dir);
952
+ }
953
+ });
954
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "pi-brainstorm",
3
+ "version": "0.2.0",
4
+ "description": "Multi-model brainstorming and debate sessions for pi subagents.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-extension",
10
+ "brainstorm",
11
+ "debate",
12
+ "battle",
13
+ "multi-model",
14
+ "subagents",
15
+ "blackboard"
16
+ ],
17
+ "author": "Jarcis-cy",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Jarcis-cy/pi-brainstorm.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/Jarcis-cy/pi-brainstorm/issues"
25
+ },
26
+ "homepage": "https://github.com/Jarcis-cy/pi-brainstorm#readme",
27
+ "files": [
28
+ "extensions",
29
+ "README.md",
30
+ "README.zh-CN.md",
31
+ "LICENSE"
32
+ ],
33
+ "peerDependencies": {
34
+ "@earendil-works/pi-coding-agent": "*",
35
+ "@earendil-works/pi-tui": "*",
36
+ "typebox": "*"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^26.0.1",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "pi": {
43
+ "extensions": [
44
+ "./extensions"
45
+ ]
46
+ }
47
+ }