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 +21 -0
- package/README.md +111 -0
- package/README.zh-CN.md +111 -0
- package/extensions/brainstorm.ts +954 -0
- package/package.json +47 -0
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
|
package/README.zh-CN.md
ADDED
|
@@ -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
|
+
}
|