pi-brainstorm 0.2.0 → 0.3.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
@@ -10,13 +10,14 @@ The plugin stores each participant's full contribution in a local meeting blackb
10
10
 
11
11
  ## Features
12
12
 
13
- - Multi-model brainstorming with GPT, DeepSeek, and MiniMax style participants.
13
+ - Multi-model brainstorming with configurable participants (default: GPT, DeepSeek, MiniMax).
14
14
  - Debate / battle mode where agents prosecute, stress-test, and challenge positions.
15
+ - Configuration-driven: add, remove, or customize participants via YAML.
15
16
  - Round-by-round summaries focused on consensus, disagreement, and next questions.
16
17
  - Full participant contributions stored as Markdown files under `.pi-meetings/`.
17
18
  - Compact visible cards in the main conversation instead of long pasted transcripts.
18
19
  - JSONL index for lightweight cross-round context.
19
- - Bundled default participant agent definitions for first-time setup.
20
+ - Managed agent files generated from config update one YAML, all agents sync.
20
21
 
21
22
  ## Install
22
23
 
@@ -29,7 +30,7 @@ pi install npm:pi-brainstorm
29
30
  From GitHub:
30
31
 
31
32
  ```bash
32
- pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.2.0
33
+ pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.3.0
33
34
  ```
34
35
 
35
36
  For local development:
@@ -38,15 +39,47 @@ For local development:
38
39
  pi install /Users/jarcis/Project/pi-brainstorm
39
40
  ```
40
41
 
41
- ## Prerequisites
42
+ ## Configuration
43
+
44
+ Participants are defined in YAML. The extension loads config in this order (later overrides earlier):
45
+
46
+ 1. Package default: `config/default.yaml` (shipped with the package)
47
+ 2. User override: `~/.pi/agent/pi-brainstorm.yaml`
48
+ 3. Project override: `.pi-brainstorm.yaml` or `.pi/pi-brainstorm.yaml`
49
+
50
+ Arrays (e.g. `participants`) replace entirely; objects deep-merge.
51
+
52
+ ### Adding a new participant
53
+
54
+ Create `~/.pi/agent/pi-brainstorm.yaml` (user-level) or `.pi-brainstorm.yaml` (project-level):
55
+
56
+ ```yaml
57
+ participants:
58
+ - displayName: Claude
59
+ agentName: claude-brainstormer
60
+ description: Claude brainstorming consultant. Nuanced analyst for multi-model discussion sessions.
61
+ model: anthropic/claude-sonnet-4-20250514:xhigh
62
+ roleTitle: Nuanced Analyst
63
+ rolePrompt: |
64
+ 你是多模型头脑风暴中的分析顾问。擅长细致入微的论证和长篇分析。用中文回答。
65
+ whatYouDo:
66
+ - 提供细致、深入的逐点分析
67
+ - 识别细微差别和边缘情况
68
+ - 撰写结构清晰的长篇论证
69
+ debatePersona:
70
+ label: THE ANALYST
71
+ prompt: |
72
+ DEBATE MODE. You are THE ANALYST. Dissect every argument with precision. Find the weakest link in every chain of reasoning. Use Chinese.
73
+ brainstormRole: Nuanced analyst
74
+ ```
75
+
76
+ To replace the default participants entirely, define a full `participants` list in your override file.
42
77
 
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:
78
+ User-level config can create or update managed agent files in `~/.pi/agent/agents/` after confirmation. Project-level config is used for the current session, but it does not auto-write global agent files; create those agents manually or move the config to `~/.pi/agent/pi-brainstorm.yaml` when you want automatic sync.
44
79
 
45
- - `gpt-brainstormer`
46
- - `deepseek-brainstormer`
47
- - `minimax-brainstormer`
80
+ ### Managed agent files
48
81
 
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.
82
+ Agent files generated by pi-brainstorm contain `<!-- managed-by: pi-brainstorm -->`. These files are overwritten when the config changes. Existing agent files without this marker are never touched.
50
83
 
51
84
  ## Commands
52
85
 
package/README.zh-CN.md CHANGED
@@ -10,13 +10,14 @@ English README: [README.md](./README.md).
10
10
 
11
11
  ## 功能
12
12
 
13
- - 多模型头脑风暴:默认包含 GPT、DeepSeek、MiniMax 风格参与者。
13
+ - 多模型头脑风暴:参与者可配置(默认:GPT、DeepSeek、MiniMax)。
14
14
  - 辩论 / battle 模式:Agent 会攻击、审视、反驳彼此的观点。
15
+ - 配置驱动:通过 YAML 添加、删除或自定义参与者。
15
16
  - 每轮输出聚焦共识、分歧、关键问题和下一步方向。
16
17
  - 参与者完整发言以 Markdown 文件保存到 `.pi-meetings/`。
17
18
  - 主会话中展示紧凑发言卡片,而不是粘贴长篇 transcript。
18
19
  - 使用 JSONL 索引作为轻量级跨轮上下文入口。
19
- - 首次使用时可安装内置的默认参与者 agent 定义。
20
+ - 由配置自动生成托管的 agent 文件——更新一个 YAML,所有 agent 同步。
20
21
 
21
22
  ## 安装
22
23
 
@@ -29,7 +30,7 @@ pi install npm:pi-brainstorm
29
30
  通过 GitHub 安装:
30
31
 
31
32
  ```bash
32
- pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.2.0
33
+ pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.3.0
33
34
  ```
34
35
 
35
36
  本地开发安装:
@@ -38,15 +39,53 @@ pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.2.0
38
39
  pi install /Users/jarcis/Project/pi-brainstorm
39
40
  ```
40
41
 
41
- ## 前置条件
42
+ ## 配置
43
+
44
+ 参与者通过 YAML 定义。插件按以下顺序加载配置(后者覆盖前者):
45
+
46
+ 1. 包默认配置:`config/default.yaml`(随包发布)
47
+ 2. 用户级覆盖:`~/.pi/agent/pi-brainstorm.yaml`
48
+ 3. 项目级覆盖:`.pi-brainstorm.yaml` 或 `.pi/pi-brainstorm.yaml`
49
+
50
+ 数组(如 `participants`)整体替换;对象字段深度合并。
51
+
52
+ ### 添加新参与者
53
+
54
+ 创建 `~/.pi/agent/pi-brainstorm.yaml`(用户级)或 `.pi-brainstorm.yaml`(项目级):
55
+
56
+ ```yaml
57
+ participants:
58
+ - displayName: Claude
59
+ agentName: claude-brainstormer
60
+ description: Claude 头脑风暴顾问。细致入微的分析师,用于多模型讨论。
61
+ model: anthropic/claude-sonnet-4-20250514:xhigh
62
+ roleTitle: 细致分析师
63
+ rolePrompt: |
64
+ 你是多模型头脑风暴中的分析顾问。擅长细致入微的论证和长篇分析。用中文回答。
65
+ whatYouDo:
66
+ - 提供细致、深入的逐点分析
67
+ - 识别细微差别和边缘情况
68
+ - 撰写结构清晰的长篇论证
69
+ debatePersona:
70
+ label: THE ANALYST
71
+ prompt: |
72
+ DEBATE MODE. You are THE ANALYST. Dissect every argument with precision. Find the weakest link in every chain of reasoning. Use Chinese.
73
+ brainstormRole: 细致分析师
74
+ ```
75
+
76
+ 如需完全替换默认参与者,在覆盖文件中定义完整的 `participants` 列表即可。
77
+
78
+ 用户级配置可以在确认后创建或更新 `~/.pi/agent/agents/` 下的受管理 agent 文件。项目级配置只影响当前会话编排,但不会自动写入全局 agent 文件;如果需要自动同步 agent,请手动创建对应 agent,或把配置移到 `~/.pi/agent/pi-brainstorm.yaml`。
42
79
 
43
- 该扩展依赖 pi 中已有的 `subagent` 工具。命令处理器会先创建本地会议记录,然后让主 Agent 调用这些参与者:
80
+ ### 托管的 Agent 文件
81
+
82
+ 由 pi-brainstorm 生成的 agent 文件包含 `<!-- managed-by: pi-brainstorm -->` 标记。当配置变更时,这些文件会被覆盖。不含此标记的已有 agent 文件永远不会被修改。
83
+
84
+ ## 前置条件
44
85
 
45
- - `gpt-brainstormer`
46
- - `deepseek-brainstormer`
47
- - `minimax-brainstormer`
86
+ 该扩展依赖 pi 中已有的 `subagent` 工具。命令处理器会先创建本地会议记录,然后让主 Agent 调用配置中定义的参与者(默认:`gpt-brainstormer`、`deepseek-brainstormer`、`minimax-brainstormer`)。
48
87
 
49
- 第一次使用时,如果这些用户级 agent 不存在,扩展会询问是否把内置默认定义写入 `~/.pi/agent/agents/`。已有同名文件不会被覆盖。
88
+ 第一次使用时,如果这些用户级 agent 不存在,扩展会询问是否写入内置默认定义。已有同名且不含托管标记的文件不会被覆盖。
50
89
 
51
90
  ## 命令
52
91
 
@@ -0,0 +1,58 @@
1
+ # pi-brainstorm default configuration
2
+ # Each participant defines a subagent that joins brainstorming/debate sessions.
3
+ #
4
+ # To override: create ~/.pi/agent/pi-brainstorm.yaml (user-level)
5
+ # or .pi-brainstorm.yaml / .pi/pi-brainstorm.yaml (project-level)
6
+ # Later files override earlier. Arrays replace entirely; objects deep-merge.
7
+
8
+ participants:
9
+ - displayName: GPT
10
+ agentName: gpt-brainstormer
11
+ description: GPT brainstorming consultant. Visionary strategist for multi-model discussion sessions.
12
+ model: vendor-codex/gpt-5.5:xhigh
13
+ roleTitle: Visionary Strategist
14
+ rolePrompt: |
15
+ 你是多模型头脑风暴中的愿景战略家。思考大局,发现别人忽略的机会,将复杂权衡综合为清晰方向。用中文回答。
16
+ whatYouDo:
17
+ - 提出创新的战略方向和解决方案
18
+ - 发现别人忽略的机会和盲点
19
+ - 把零散想法综合成连贯战略
20
+ debatePersona:
21
+ label: THE PROSECUTOR
22
+ prompt: |
23
+ DEBATE MODE. You are THE PROSECUTOR. Attack the other positions ruthlessly. Find every logical flaw, every hidden assumption, every missing edge case. Be relentless. Do not concede easily. Use Chinese.
24
+ brainstormRole: Visionary strategist
25
+
26
+ - displayName: DeepSeek
27
+ agentName: deepseek-brainstormer
28
+ description: DeepSeek brainstorming consultant. Meticulous systems thinker for multi-model discussion sessions.
29
+ model: deepseek/deepseek-v4-pro:xhigh
30
+ roleTitle: Meticulous Systems Thinker
31
+ rolePrompt: |
32
+ 你是多模型头脑风暴中的系统思考者。分析结构、依赖、扩展上限和失败模式。用中文回答。
33
+ whatYouDo:
34
+ - 从结构、依赖和风险角度分析提案
35
+ - 识别隐藏耦合、扩展上限和失败模式
36
+ - 提出具体、可实现、可验证的技术优化方案
37
+ debatePersona:
38
+ label: THE SYSTEMS SKEPTIC
39
+ prompt: |
40
+ DEBATE MODE. You are THE SYSTEMS SKEPTIC. Dissect the structural implications of every proposal. What breaks at scale? Where are the hidden costs? What dependencies create risk? Be precise and unforgiving. Use Chinese.
41
+ brainstormRole: Systems thinker
42
+
43
+ - displayName: MiniMax
44
+ agentName: minimax-brainstormer
45
+ description: MiniMax brainstorming consultant. Creative lateral thinker for multi-model discussion sessions.
46
+ model: minimax-cn/MiniMax-M3:xhigh
47
+ roleTitle: Creative Lateral Thinker
48
+ rolePrompt: |
49
+ 你是多模型头脑风暴中的创意顾问。跳出框框思考,挑战隐性假设,提出非常规方案。用中文回答。
50
+ whatYouDo:
51
+ - 从意想不到的角度切入问题
52
+ - 提出打破常规的创新方案
53
+ - 挑战团队隐性假设
54
+ debatePersona:
55
+ label: THE CONTRARIAN
56
+ prompt: |
57
+ DEBATE MODE. You are THE CONTRARIAN. Take the opposite position from the dominant view. Expose groupthink. Propose radical alternatives. Challenge the fundamental premise if needed. Use Chinese.
58
+ brainstormRole: Creative lateral thinker
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * pi-brainstorm — Multi-model brainstorm/debate extension for Pi
3
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.
4
+ * Runs brainstorm and debate sessions across multiple subagents configured
5
+ * via YAML. Full participant contributions are stored in a local filesystem
6
+ * blackboard, while the main conversation sees compact cards and facilitator
7
+ * synthesis.
7
8
  *
8
9
  * Features:
10
+ * - Configuration-driven participants (YAML)
9
11
  * - meeting_append_entry tool — concurrency-safe append to meeting folder
10
12
  * - meeting_read_index tool — read meeting index
11
13
  * - meeting_read_entry tool — read full entry content
@@ -13,15 +15,629 @@
13
15
  * - /debate command — open-ended multi-agent debate
14
16
  * - meeting-entry message renderer — compact cards with expandable content
15
17
  * - File watcher — auto-posts new entries into the main conversation
18
+ * - Managed agent file generation from config
16
19
  */
17
20
 
18
21
  import * as path from "node:path";
19
22
  import * as fs from "node:fs";
20
23
  import * as fsp from "node:fs/promises";
24
+ import { fileURLToPath } from "node:url";
21
25
  import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
26
  import { Type } from "typebox";
23
27
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
24
28
  import { Text, Box } from "@earendil-works/pi-tui";
29
+ import * as YAML from "yaml";
30
+
31
+ // ────────────────────────────────────────────────────────
32
+ // Types
33
+ // ────────────────────────────────────────────────────────
34
+
35
+ interface DebatePersona {
36
+ label: string;
37
+ prompt: string;
38
+ }
39
+
40
+ interface ParticipantConfig {
41
+ displayName: string;
42
+ agentName: string;
43
+ description?: string;
44
+ model: string;
45
+ roleTitle?: string;
46
+ rolePrompt: string;
47
+ whatYouDo?: string[];
48
+ debatePersona?: DebatePersona;
49
+ brainstormRole?: string;
50
+ tools?: string[];
51
+ }
52
+
53
+ interface BrainstormConfig {
54
+ participants: ParticipantConfig[];
55
+ }
56
+
57
+ // ────────────────────────────────────────────────────────
58
+ // Constants
59
+ // ────────────────────────────────────────────────────────
60
+
61
+ const MANAGED_MARKER = "<!-- managed-by: pi-brainstorm -->";
62
+ const DEFAULT_TOOLS = [
63
+ "read",
64
+ "grep",
65
+ "find",
66
+ "ls",
67
+ "meeting_append_entry",
68
+ "meeting_read_index",
69
+ "meeting_read_entry",
70
+ ];
71
+
72
+ // ────────────────────────────────────────────────────────
73
+ // Config helpers
74
+ // ────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Deep-merge two values. Arrays are replaced entirely; objects are
78
+ * shallow-merged recursively; scalars use the overlay value.
79
+ */
80
+ function deepMerge(base: any, overlay: any): any {
81
+ if (overlay === null || overlay === undefined) return base;
82
+ if (base === null || base === undefined) return overlay;
83
+
84
+ if (Array.isArray(base) && Array.isArray(overlay)) {
85
+ return overlay;
86
+ }
87
+
88
+ if (
89
+ typeof base === "object" &&
90
+ typeof overlay === "object" &&
91
+ !Array.isArray(base) &&
92
+ !Array.isArray(overlay)
93
+ ) {
94
+ const result: Record<string, any> = { ...base };
95
+ for (const key of Object.keys(overlay)) {
96
+ result[key] =
97
+ key in result
98
+ ? deepMerge(result[key], overlay[key])
99
+ : overlay[key];
100
+ }
101
+ return result;
102
+ }
103
+
104
+ return overlay;
105
+ }
106
+
107
+ /**
108
+ * Resolve extension directory from import.meta.url.
109
+ */
110
+ function getExtensionDir(): string {
111
+ return path.dirname(fileURLToPath(import.meta.url));
112
+ }
113
+
114
+ /**
115
+ * Load and merge config from all locations, in priority order (later wins):
116
+ * 1. Package default: config/default.yaml (relative to package root or extension dir)
117
+ * Also try brainstorm.yaml in extension dir (for manual installs)
118
+ * 2. User override: ~/.pi/agent/pi-brainstorm.yaml
119
+ * 3. Project override: {cwd}/.pi-brainstorm.yaml
120
+ * 4. Project override: {cwd}/.pi/pi-brainstorm.yaml
121
+ */
122
+ function hasProjectConfig(cwd: string): boolean {
123
+ if (!cwd) return false;
124
+ return [
125
+ path.join(cwd, ".pi-brainstorm.yaml"),
126
+ path.join(cwd, ".pi", "pi-brainstorm.yaml"),
127
+ ].some((candidate) => fs.existsSync(candidate));
128
+ }
129
+
130
+ function loadConfig(cwd: string): BrainstormConfig {
131
+ const extensionDir = getExtensionDir();
132
+ const packageRoot = path.dirname(extensionDir);
133
+
134
+ // Step 1: package/extension defaults
135
+ const defaultCandidates = [
136
+ path.join(packageRoot, "config", "default.yaml"),
137
+ path.join(extensionDir, "config", "default.yaml"),
138
+ path.join(extensionDir, "brainstorm.yaml"),
139
+ ];
140
+
141
+ let merged: any = {};
142
+ let loadedAny = false;
143
+
144
+ for (const candidate of defaultCandidates) {
145
+ if (fs.existsSync(candidate)) {
146
+ try {
147
+ const raw = fs.readFileSync(candidate, "utf-8");
148
+ const parsed = YAML.parse(raw);
149
+ if (parsed && typeof parsed === "object") {
150
+ merged = deepMerge(merged, parsed);
151
+ loadedAny = true;
152
+ }
153
+ } catch (err: any) {
154
+ throw new Error(
155
+ `Failed to parse config ${candidate}: ${err.message}`
156
+ );
157
+ }
158
+ }
159
+ }
160
+
161
+ if (!loadedAny) {
162
+ throw new Error(
163
+ "No pi-brainstorm config found. Expected config/default.yaml in package root or extension directory."
164
+ );
165
+ }
166
+
167
+ // Step 2: user override
168
+ const userPath = path.join(getAgentDir(), "pi-brainstorm.yaml");
169
+ if (fs.existsSync(userPath)) {
170
+ try {
171
+ const raw = fs.readFileSync(userPath, "utf-8");
172
+ const parsed = YAML.parse(raw);
173
+ if (parsed && typeof parsed === "object") {
174
+ merged = deepMerge(merged, parsed);
175
+ }
176
+ } catch (err: any) {
177
+ throw new Error(
178
+ `Failed to parse user config ${userPath}: ${err.message}`
179
+ );
180
+ }
181
+ }
182
+
183
+ // Step 3: project overrides
184
+ const projectCandidates = cwd
185
+ ? [path.join(cwd, ".pi-brainstorm.yaml"), path.join(cwd, ".pi", "pi-brainstorm.yaml")]
186
+ : [];
187
+
188
+ for (const candidate of projectCandidates) {
189
+ if (fs.existsSync(candidate)) {
190
+ try {
191
+ const raw = fs.readFileSync(candidate, "utf-8");
192
+ const parsed = YAML.parse(raw);
193
+ if (parsed && typeof parsed === "object") {
194
+ merged = deepMerge(merged, parsed);
195
+ }
196
+ } catch (err: any) {
197
+ throw new Error(
198
+ `Failed to parse project config ${candidate}: ${err.message}`
199
+ );
200
+ }
201
+ }
202
+ }
203
+
204
+ return merged as BrainstormConfig;
205
+ }
206
+
207
+ /**
208
+ * Resolve and validate participants for a command invocation.
209
+ * Returns validated participant array; throws with a clear message on failure.
210
+ */
211
+ function isSafeAgentName(value: string): boolean {
212
+ return /^[A-Za-z0-9][A-Za-z0-9_.-]{0,80}$/.test(value);
213
+ }
214
+
215
+ function resolveParticipants(cwd: string): ParticipantConfig[] {
216
+ const config = loadConfig(cwd);
217
+
218
+ if (
219
+ !config.participants ||
220
+ !Array.isArray(config.participants) ||
221
+ config.participants.length === 0
222
+ ) {
223
+ throw new Error(
224
+ "pi-brainstorm config must define at least one participant under 'participants'."
225
+ );
226
+ }
227
+
228
+ const requiredFields: (keyof ParticipantConfig)[] = [
229
+ "displayName",
230
+ "agentName",
231
+ "model",
232
+ "rolePrompt",
233
+ ];
234
+
235
+ for (let i = 0; i < config.participants.length; i++) {
236
+ const p = config.participants[i];
237
+ for (const field of requiredFields) {
238
+ if (!p[field]) {
239
+ throw new Error(
240
+ `Participant at index ${i} is missing required field "${field}".`
241
+ );
242
+ }
243
+ }
244
+ if (typeof p.displayName !== "string" || !p.displayName.trim()) {
245
+ throw new Error(
246
+ `Participant at index ${i} has invalid displayName.`
247
+ );
248
+ }
249
+ if (typeof p.agentName !== "string" || !p.agentName.trim()) {
250
+ throw new Error(
251
+ `Participant at index ${i} has invalid agentName.`
252
+ );
253
+ }
254
+ if (!isSafeAgentName(p.agentName)) {
255
+ throw new Error(
256
+ `Participant "${p.displayName}" has unsafe agentName "${p.agentName}". Use only letters, digits, dot, underscore, and hyphen; it must start with a letter or digit.`
257
+ );
258
+ }
259
+ if (typeof p.model !== "string" || !p.model.trim()) {
260
+ throw new Error(
261
+ `Participant at index ${i} has invalid model.`
262
+ );
263
+ }
264
+ if (typeof p.rolePrompt !== "string" || !p.rolePrompt.trim()) {
265
+ throw new Error(
266
+ `Participant at index ${i} has invalid rolePrompt.`
267
+ );
268
+ }
269
+ }
270
+
271
+ return config.participants;
272
+ }
273
+
274
+ // ────────────────────────────────────────────────────────
275
+ // Agent file generation
276
+ // ────────────────────────────────────────────────────────
277
+
278
+ /**
279
+ * Generate the content of a managed agent markdown file from a participant config.
280
+ */
281
+ function yamlScalar(value: string): string {
282
+ return JSON.stringify(value);
283
+ }
284
+
285
+ function generateAgentFile(participant: ParticipantConfig): string {
286
+ const tools = participant.tools && participant.tools.length > 0
287
+ ? participant.tools
288
+ : DEFAULT_TOOLS;
289
+ const toolsStr = tools.join(", ");
290
+
291
+ const description =
292
+ participant.description ||
293
+ `${participant.displayName} brainstorming consultant.`;
294
+
295
+ const roleTitle = participant.roleTitle
296
+ ? ` - ${participant.roleTitle}`
297
+ : "";
298
+
299
+ const whatYouDoLines = (participant.whatYouDo && participant.whatYouDo.length > 0)
300
+ ? participant.whatYouDo.map((item) => `- ${item}`).join("\n")
301
+ : `- 参与多模型讨论并提供${participant.displayName}视角的分析`;
302
+
303
+ return [
304
+ MANAGED_MARKER,
305
+ "---",
306
+ `name: ${yamlScalar(participant.agentName)}`,
307
+ `description: ${yamlScalar(description)}`,
308
+ `tools: ${toolsStr}`,
309
+ `model: ${yamlScalar(participant.model)}`,
310
+ "---",
311
+ "",
312
+ `# ${participant.displayName} Brainstormer${roleTitle}`,
313
+ "",
314
+ participant.rolePrompt,
315
+ "",
316
+ "## What You Do",
317
+ whatYouDoLines,
318
+ "",
319
+ "## What You Do Not Do",
320
+ "- 写代码或修改项目文件,你只读项目文件",
321
+ "- 委派给其他 Agent",
322
+ "- 在聊天中直接粘贴长篇分析;当明确指示使用 meeting_append_entry 时,必须将完整贡献写入会议黑板,最终回复仅写 WROTE_ENTRY + 一句话摘要",
323
+ "",
324
+ "## Worker Preamble",
325
+ "You are a terminal worker. Work directly with tools. Do NOT spawn sub-agents.",
326
+ "",
327
+ ].join("\n");
328
+ }
329
+
330
+ /**
331
+ * Ensure agent files exist for all configured participants.
332
+ * - Files with the managed marker are overwritten from current config.
333
+ * - Files without the marker are never touched.
334
+ * - Missing files are created after user confirmation.
335
+ *
336
+ * Returns true if all participants have existing agent files (managed or not).
337
+ */
338
+ async function safeWriteAgentFile(targetPath: string, content: string, mode: "create" | "update"): Promise<void> {
339
+ const dir = path.dirname(targetPath);
340
+ assertDirectoryNoSymlink(dir, "agents directory");
341
+ assertPathInside(fs.realpathSync(dir), path.resolve(targetPath), "agent file");
342
+
343
+ if (mode === "create") {
344
+ await fsp.writeFile(targetPath, content, { encoding: "utf-8", flag: "wx" });
345
+ return;
346
+ }
347
+
348
+ assertExistingFileNoSymlink(targetPath, "agent file");
349
+ const tempPath = path.join(
350
+ dir,
351
+ `.${path.basename(targetPath)}.${process.pid}.${Date.now()}.tmp`
352
+ );
353
+ await fsp.writeFile(tempPath, content, { encoding: "utf-8", flag: "wx" });
354
+ try {
355
+ await fsp.rename(tempPath, targetPath);
356
+ } catch (err) {
357
+ try {
358
+ await fsp.unlink(tempPath);
359
+ } catch {
360
+ // Ignore cleanup errors.
361
+ }
362
+ throw err;
363
+ }
364
+ }
365
+
366
+ async function ensureAgentsFromConfig(
367
+ ctx: any,
368
+ participants: ParticipantConfig[],
369
+ options: { allowGlobalWrites: boolean }
370
+ ): Promise<boolean> {
371
+ const agentsDir = path.join(getAgentDir(), "agents");
372
+
373
+ const planned: { filename: string; action: "create" | "update" }[] = [];
374
+ const protectedFiles: string[] = [];
375
+
376
+ for (const p of participants) {
377
+ const filename = `${p.agentName}.md`;
378
+ const targetPath = path.resolve(agentsDir, filename);
379
+ assertPathInside(agentsDir, targetPath, "agent file");
380
+
381
+ if (fs.existsSync(targetPath)) {
382
+ assertExistingFileNoSymlink(targetPath, "agent file");
383
+ const content = fs.readFileSync(targetPath, "utf-8");
384
+ if (content.includes(MANAGED_MARKER)) {
385
+ planned.push({ filename, action: "update" });
386
+ } else {
387
+ protectedFiles.push(filename);
388
+ }
389
+ } else {
390
+ planned.push({ filename, action: "create" });
391
+ }
392
+ }
393
+
394
+ // If nothing to create or update, we're done
395
+ if (planned.length === 0) {
396
+ return true;
397
+ }
398
+
399
+ if (!options.allowGlobalWrites) {
400
+ ctx.ui?.notify?.(
401
+ "Project-level pi-brainstorm config is active. For safety, this command will not create or update global agent files. Create the listed agents manually or move the config to ~/.pi/agent/pi-brainstorm.yaml.",
402
+ "warning"
403
+ );
404
+ return false;
405
+ }
406
+
407
+ // Non-interactive mode
408
+ if (!ctx.hasUI) {
409
+ // Update managed files silently
410
+ await fsp.mkdir(agentsDir, { recursive: true });
411
+ assertDirectoryNoSymlink(agentsDir, "agents directory");
412
+ for (const plan of planned) {
413
+ if (plan.action === "update") {
414
+ const p = participants.find(
415
+ (p) => `${p.agentName}.md` === plan.filename
416
+ )!;
417
+ const targetPath = path.resolve(agentsDir, plan.filename);
418
+ assertPathInside(agentsDir, targetPath, "agent file");
419
+ await safeWriteAgentFile(
420
+ targetPath,
421
+ generateAgentFile(p),
422
+ "update"
423
+ );
424
+ }
425
+ }
426
+ // Report missing
427
+ const missing = planned.filter((p) => p.action === "create");
428
+ if (missing.length > 0) {
429
+ ctx.ui?.notify?.(
430
+ `Missing meeting agents: ${missing.map((m) => m.filename).join(", ")}. Install them under ${agentsDir}.`,
431
+ "warning"
432
+ );
433
+ return false;
434
+ }
435
+ return true;
436
+ }
437
+
438
+ // Interactive mode: ask user
439
+ let message = "";
440
+ if (planned.length > 0) {
441
+ const actionWord = planned.some((p) => p.action === "update")
442
+ ? "created/updated"
443
+ : "created";
444
+ message += `The following agent files will be ${actionWord}:\n`;
445
+ for (const p of planned) {
446
+ message += ` - ${p.filename} (${p.action})\n`;
447
+ }
448
+ }
449
+ if (protectedFiles.length > 0) {
450
+ message += `\nThe following existing files are NOT managed by pi-brainstorm and will be left untouched:\n`;
451
+ for (const f of protectedFiles) {
452
+ message += ` - ${f}\n`;
453
+ }
454
+ }
455
+
456
+ const title = planned.some((p) => p.action === "update")
457
+ ? "Update brainstorm agents?"
458
+ : "Install brainstorm agents?";
459
+
460
+ const ok = await ctx.ui.confirm(
461
+ title,
462
+ message + `\nFiles will be written to ${agentsDir}.`
463
+ );
464
+ if (!ok) return false;
465
+
466
+ await fsp.mkdir(agentsDir, { recursive: true });
467
+ assertDirectoryNoSymlink(agentsDir, "agents directory");
468
+ for (const plan of planned) {
469
+ const p = participants.find(
470
+ (p) => `${p.agentName}.md` === plan.filename
471
+ )!;
472
+ const targetPath = path.resolve(agentsDir, plan.filename);
473
+ assertPathInside(agentsDir, targetPath, "agent file");
474
+ await safeWriteAgentFile(targetPath, generateAgentFile(p), plan.action);
475
+ }
476
+
477
+ ctx.ui.notify(`Updated ${planned.length} agent file(s).`, "info");
478
+ return true;
479
+ }
480
+
481
+ // ────────────────────────────────────────────────────────
482
+ // Prompt builders
483
+ // ────────────────────────────────────────────────────────
484
+
485
+ /**
486
+ * Build the facilitator prompt for /brainstorm.
487
+ */
488
+ function buildBrainstormPrompt(
489
+ topic: string,
490
+ absDir: string,
491
+ participants: ParticipantConfig[]
492
+ ): string {
493
+ const consultantLines = participants
494
+ .map(
495
+ (p) =>
496
+ `- **${p.displayName}**: use the ${p.agentName} subagent. ${p.brainstormRole || p.roleTitle || "Consultant"}.`
497
+ )
498
+ .join("\n");
499
+
500
+ const agentTaskLines = participants
501
+ .map(
502
+ (p) =>
503
+ ` - speaker: "${p.displayName}"`
504
+ )
505
+ .join("\n");
506
+
507
+ return [
508
+ `BLACKBOARD BRAINSTORMING SESSION: ${topic}`,
509
+ "",
510
+ `Meeting folder: \`${absDir}\``,
511
+ "",
512
+ "You are facilitating a round-robin brainstorming session using the MEETING BLACKBOARD.",
513
+ "Each consultant writes their FULL contribution to disk via meeting_append_entry.",
514
+ "",
515
+ "## Consultants (3 rounds)",
516
+ consultantLines,
517
+ "",
518
+ "## CRITICAL INSTRUCTIONS",
519
+ "",
520
+ "### For subagents (include in EVERY task):",
521
+ "1. Write your FULL contribution using the meeting_append_entry tool with:",
522
+ ` - meetingDir: "${absDir}"`,
523
+ " - speaker: your display name, e.g.:",
524
+ agentTaskLines,
525
+ ' - phase: "Round 1", "Round 2", or "Round 3"',
526
+ " - summary: a ONE-SENTENCE summary of your contribution",
527
+ " - content: your FULL analysis in Chinese (中文)",
528
+ "2. After writing, your FINAL ANSWER must be ONLY:",
529
+ " `WROTE_ENTRY: <your one-sentence summary>`",
530
+ "3. DO NOT paste your full analysis into the chat. The main agent and user will read it from the blackboard.",
531
+ "",
532
+ "### For you, the facilitator:",
533
+ "- Do NOT paste participant full text into chat. They are on the blackboard.",
534
+ "- After each round, read the index with meeting_read_index and present a structural overview.",
535
+ "- Optionally read full entries with meeting_read_entry when needed.",
536
+ "- Present each consultant's summary + your structural overview (conflict matrix, consensus table).",
537
+ "- When the user gives feedback, relay it VERBATIM to the consultants in the next round.",
538
+ "",
539
+ "## Protocol",
540
+ "Round 1: Each consultant gives initial analysis on the topic. Run all in parallel.",
541
+ "Round 2: Feed prior discussion back to each. Ask each to challenge the others and propose improvements.",
542
+ "Round 3: Each gives FINAL recommendation, synthesizing the best ideas.",
543
+ "",
544
+ "After Round 3, present the complete structural overview and a synthesized conclusion.",
545
+ "",
546
+ "## IMPORTANT",
547
+ "- All responses in Chinese (中文).",
548
+ "- Save transcript.md and (after user confirms) conclusion.md per the MEETING OUTPUT PROTOCOL.",
549
+ "- The user can intervene at any time to steer the discussion.",
550
+ ].join("\n");
551
+ }
552
+
553
+ /**
554
+ * Build the facilitator prompt for /debate.
555
+ */
556
+ function buildDebatePrompt(
557
+ topic: string,
558
+ absDir: string,
559
+ participants: ParticipantConfig[]
560
+ ): string {
561
+ const debaterLines = participants
562
+ .map((p) => {
563
+ const dp = p.debatePersona;
564
+ if (dp) {
565
+ return `- **${p.displayName}** (${p.agentName}): ${dp.label} — Attack other positions, find flaws, expose assumptions.`;
566
+ }
567
+ return `- **${p.displayName}** (${p.agentName})`;
568
+ })
569
+ .join("\n");
570
+
571
+ // Build per-agent debate task prefixes for the facilitator to include
572
+ const taskPrefixLines = participants
573
+ .map((p) => {
574
+ const dp = p.debatePersona;
575
+ if (dp && dp.prompt) {
576
+ return `**${p.displayName}** (${p.agentName}):\n${dp.prompt}`;
577
+ }
578
+ return `**${p.displayName}** (${p.agentName}): Debate participant.`;
579
+ })
580
+ .join("\n\n");
581
+
582
+ const agentTaskLines = participants
583
+ .map(
584
+ (p) =>
585
+ ` - speaker: "${p.displayName}"`
586
+ )
587
+ .join("\n");
588
+
589
+ return [
590
+ `⚔️ BLACKBOARD DEBATE: ${topic}`,
591
+ "",
592
+ `Meeting folder: \`${absDir}\``,
593
+ "",
594
+ "You are facilitating an OPEN-ENDED debate using the MEETING BLACKBOARD.",
595
+ "Each debater writes their FULL argument to disk via meeting_append_entry.",
596
+ "Continue until the debate CONVERGES or the user intervenes.",
597
+ "",
598
+ "## Debaters (cycling indefinitely)",
599
+ debaterLines,
600
+ "",
601
+ "## DEBATE PERSONAS (include in each subagent task)",
602
+ taskPrefixLines,
603
+ "",
604
+ "## CRITICAL INSTRUCTIONS",
605
+ "",
606
+ "### For subagents (include in EVERY task):",
607
+ "1. Write your FULL contribution using the meeting_append_entry tool with:",
608
+ ` - meetingDir: "${absDir}"`,
609
+ " - speaker: your display name, e.g.:",
610
+ agentTaskLines,
611
+ ' - phase: "Cycle 1", "Cycle 2", etc.',
612
+ " - summary: a ONE-SENTENCE summary of your argument",
613
+ " - content: your FULL argument in Chinese (中文)",
614
+ "2. After writing, your FINAL ANSWER must be ONLY:",
615
+ " `WROTE_ENTRY: <your one-sentence summary>`",
616
+ "3. DO NOT paste your full argument into the chat.",
617
+ "",
618
+ "### Include the FULL VERBATIM prior debate record in each subagent task.",
619
+ "Use meeting_read_index and meeting_read_entry to retrieve the complete debate history.",
620
+ "NEVER summarize or truncate the debate record when passing to subagents.",
621
+ "",
622
+ "### For you, the facilitator:",
623
+ "- Do NOT paste participant full text into chat. They are on the blackboard.",
624
+ "- Cycle through debaters in sequence (chain mode) so each sees all prior entries.",
625
+ "- Read the index with meeting_read_index frequently.",
626
+ "- Read full entries with meeting_read_entry when synthesizing.",
627
+ "- After EACH full cycle (all debaters spoke once), check for CONVERGENCE:",
628
+ " * Do 2+ agents agree on a specific conclusion?",
629
+ " * Did the last cycle introduce any NEW arguments?",
630
+ " * Did anyone explicitly concede?",
631
+ "- If NOT converged: run another cycle. Keep going.",
632
+ "- If converged: present synthesis to me.",
633
+ "",
634
+ "## Rules",
635
+ "- NEVER stop at a predetermined count. Only convergence or user intervention ends this debate.",
636
+ "- All responses in Chinese (中文).",
637
+ "- After convergence, save transcript.md immediately and (after user confirms) conclusion.md per the MEETING OUTPUT PROTOCOL.",
638
+ "- Present: (1) the debate arc, (2) who conceded what, (3) final synthesis.",
639
+ ].join("\n");
640
+ }
25
641
 
26
642
  // ────────────────────────────────────────────────────────
27
643
  // Helpers
@@ -29,12 +645,14 @@ import { Text, Box } from "@earendil-works/pi-tui";
29
645
 
30
646
  /** Sanitize a string for use in filenames (keep letters, digits, hyphens, underscores). */
31
647
  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";
648
+ return (
649
+ raw
650
+ .replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, "_")
651
+ .replace(/_+/g, "_")
652
+ .replace(/^_|_$/g, "")
653
+ .slice(0, 60)
654
+ .toLowerCase() || "unknown"
655
+ );
38
656
  }
39
657
 
40
658
  /** Convert a topic string to a filesystem-safe slug. */
@@ -162,7 +780,11 @@ function startWatching(pi: ExtensionAPI, meetingDir: string): void {
162
780
  try {
163
781
  if (!fs.existsSync(entryPath)) return;
164
782
  assertExistingFileNoSymlink(entryPath, "meeting entry");
165
- assertPathInside(fs.realpathSync(entriesDir), fs.realpathSync(entryPath), "meeting entry real path");
783
+ assertPathInside(
784
+ fs.realpathSync(entriesDir),
785
+ fs.realpathSync(entryPath),
786
+ "meeting entry real path"
787
+ );
166
788
  const parsed = parseEntryFilename(filename);
167
789
  if (!parsed) return;
168
790
  const summary = readEntrySummary(entryPath);
@@ -189,7 +811,7 @@ function startWatching(pi: ExtensionAPI, meetingDir: string): void {
189
811
  );
190
812
  });
191
813
 
192
- watcher.on("error", () => {
814
+ (watcher as any).on?.("error", () => {
193
815
  // Silently handle watcher errors (e.g., directory deleted)
194
816
  });
195
817
 
@@ -221,116 +843,6 @@ interface MeetingManifest {
221
843
  entryCount: number;
222
844
  }
223
845
 
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
846
  async function readManifest(absDir: string): Promise<MeetingManifest | null> {
335
847
  const manifestPath = path.join(absDir, "manifest.json");
336
848
  try {
@@ -428,7 +940,6 @@ export default function (pi: ExtensionAPI) {
428
940
  // Read or create manifest
429
941
  let manifest = await readManifest(absDir);
430
942
  if (!manifest) {
431
- // Should not normally happen — commands seed the manifest
432
943
  manifest = {
433
944
  topic: path.basename(absDir),
434
945
  created: new Date().toISOString(),
@@ -620,7 +1131,11 @@ export default function (pi: ExtensionAPI) {
620
1131
 
621
1132
  try {
622
1133
  assertExistingFileNoSymlink(absEntryPath, "meeting entry");
623
- assertPathInside(fs.realpathSync(absDir), fs.realpathSync(absEntryPath), "meeting entry real path");
1134
+ assertPathInside(
1135
+ fs.realpathSync(absDir),
1136
+ fs.realpathSync(absEntryPath),
1137
+ "meeting entry real path"
1138
+ );
624
1139
  const content = await fsp.readFile(absEntryPath, "utf-8");
625
1140
  return {
626
1141
  content: [
@@ -682,7 +1197,11 @@ export default function (pi: ExtensionAPI) {
682
1197
  try {
683
1198
  assertExistingFileNoSymlink(details.path, "meeting entry");
684
1199
  if (details.meetingDir) {
685
- assertPathInside(fs.realpathSync(details.meetingDir), fs.realpathSync(details.path), "meeting entry real path");
1200
+ assertPathInside(
1201
+ fs.realpathSync(details.meetingDir),
1202
+ fs.realpathSync(details.path),
1203
+ "meeting entry real path"
1204
+ );
686
1205
  }
687
1206
  const fullContent = fs.readFileSync(details.path, "utf-8");
688
1207
  text += `\n\n${theme.fg("dim", fullContent)}`;
@@ -708,7 +1227,21 @@ export default function (pi: ExtensionAPI) {
708
1227
  return;
709
1228
  }
710
1229
 
711
- const agentsReady = await ensureBrainstormAgents(ctx);
1230
+ // Resolve participants from config
1231
+ let participants: ParticipantConfig[];
1232
+ try {
1233
+ participants = resolveParticipants(ctx.cwd);
1234
+ } catch (err: any) {
1235
+ ctx.ui.notify(
1236
+ `pi-brainstorm config error: ${err.message}`,
1237
+ "error"
1238
+ );
1239
+ return;
1240
+ }
1241
+
1242
+ const agentsReady = await ensureAgentsFromConfig(ctx, participants, {
1243
+ allowGlobalWrites: !hasProjectConfig(ctx.cwd),
1244
+ });
712
1245
  if (!agentsReady) return;
713
1246
 
714
1247
  const topic = args.trim();
@@ -723,7 +1256,10 @@ export default function (pi: ExtensionAPI) {
723
1256
  // Create meeting folder structure
724
1257
  await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
725
1258
  assertDirectoryNoSymlink(absDir, "meeting directory");
726
- assertDirectoryNoSymlink(path.join(absDir, "entries"), "entries directory");
1259
+ assertDirectoryNoSymlink(
1260
+ path.join(absDir, "entries"),
1261
+ "entries directory"
1262
+ );
727
1263
 
728
1264
  // Seed manifest
729
1265
  const manifest: MeetingManifest = {
@@ -765,54 +1301,11 @@ export default function (pi: ExtensionAPI) {
765
1301
  );
766
1302
 
767
1303
  // Send orchestration prompt to main agent
1304
+ const promptText = buildBrainstormPrompt(topic, absDir, participants);
768
1305
  pi.sendUserMessage([
769
1306
  {
770
1307
  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"),
1308
+ text: promptText,
816
1309
  },
817
1310
  ]);
818
1311
  },
@@ -829,7 +1322,21 @@ export default function (pi: ExtensionAPI) {
829
1322
  return;
830
1323
  }
831
1324
 
832
- const agentsReady = await ensureBrainstormAgents(ctx);
1325
+ // Resolve participants from config
1326
+ let participants: ParticipantConfig[];
1327
+ try {
1328
+ participants = resolveParticipants(ctx.cwd);
1329
+ } catch (err: any) {
1330
+ ctx.ui.notify(
1331
+ `pi-brainstorm config error: ${err.message}`,
1332
+ "error"
1333
+ );
1334
+ return;
1335
+ }
1336
+
1337
+ const agentsReady = await ensureAgentsFromConfig(ctx, participants, {
1338
+ allowGlobalWrites: !hasProjectConfig(ctx.cwd),
1339
+ });
833
1340
  if (!agentsReady) return;
834
1341
 
835
1342
  const topic = args.trim();
@@ -844,7 +1351,10 @@ export default function (pi: ExtensionAPI) {
844
1351
  // Create meeting folder structure
845
1352
  await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
846
1353
  assertDirectoryNoSymlink(absDir, "meeting directory");
847
- assertDirectoryNoSymlink(path.join(absDir, "entries"), "entries directory");
1354
+ assertDirectoryNoSymlink(
1355
+ path.join(absDir, "entries"),
1356
+ "entries directory"
1357
+ );
848
1358
 
849
1359
  // Seed manifest
850
1360
  const manifest: MeetingManifest = {
@@ -887,58 +1397,11 @@ export default function (pi: ExtensionAPI) {
887
1397
  );
888
1398
 
889
1399
  // Send orchestration prompt to main agent
1400
+ const promptText = buildDebatePrompt(topic, absDir, participants);
890
1401
  pi.sendUserMessage([
891
1402
  {
892
1403
  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"),
1404
+ text: promptText,
942
1405
  },
943
1406
  ]);
944
1407
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-brainstorm",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Multi-model brainstorming and debate sessions for pi subagents.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -26,10 +26,14 @@
26
26
  "homepage": "https://github.com/Jarcis-cy/pi-brainstorm#readme",
27
27
  "files": [
28
28
  "extensions",
29
+ "config",
29
30
  "README.md",
30
31
  "README.zh-CN.md",
31
32
  "LICENSE"
32
33
  ],
34
+ "dependencies": {
35
+ "yaml": "^2.7.0"
36
+ },
33
37
  "peerDependencies": {
34
38
  "@earendil-works/pi-coding-agent": "*",
35
39
  "@earendil-works/pi-tui": "*",