pi-brainstorm 0.4.1 → 0.4.3

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
@@ -36,7 +36,7 @@ pi install npm:pi-brainstorm
36
36
  From GitHub:
37
37
 
38
38
  ```bash
39
- pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.4.1
39
+ pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.4.3
40
40
  ```
41
41
 
42
42
  For local development:
@@ -131,8 +131,9 @@ The blackboard files are the source of truth for the session. The facilitator ca
131
131
 
132
132
  ## Tools
133
133
 
134
- The extension registers three tools for participants and the facilitator:
134
+ The extension registers four tools:
135
135
 
136
+ - `meeting_rename` (facilitator-only) renames the meeting directory to a human-readable title before Round 1 starts.
136
137
  - `meeting_append_entry` writes a full participant contribution to disk and returns only a short reference.
137
138
  - `meeting_read_index` lists entries by id, speaker, phase, summary, and path.
138
139
  - `meeting_read_entry` reads one full entry when the facilitator or a participant needs it.
package/README.zh-CN.md CHANGED
@@ -36,7 +36,7 @@ pi install npm:pi-brainstorm
36
36
  通过 GitHub 安装:
37
37
 
38
38
  ```bash
39
- pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.4.1
39
+ pi install git:github.com/Jarcis-cy/pi-brainstorm@v0.4.3
40
40
  ```
41
41
 
42
42
  本地开发安装:
@@ -133,8 +133,9 @@ pi install npm:@narumitw/pi-subagents
133
133
 
134
134
  ## 工具
135
135
 
136
- 扩展注册三个工具:
136
+ 扩展注册四个工具:
137
137
 
138
+ - `meeting_rename`(仅主持人):在 Round 1 开始前将会议目录重命名为人类可读的标题。
138
139
  - `meeting_append_entry`:把参与者完整发言写入磁盘,只返回短引用。
139
140
  - `meeting_read_index`:读取条目索引,包含 id、speaker、phase、summary、path。
140
141
  - `meeting_read_entry`:在主持人或参与者需要时读取某条完整发言。
@@ -59,14 +59,17 @@ interface BrainstormConfig {
59
59
  // ────────────────────────────────────────────────────────
60
60
 
61
61
  const MANAGED_MARKER = "<!-- managed-by: pi-brainstorm -->";
62
+ const MEETING_TOOLS = [
63
+ "meeting_append_entry",
64
+ "meeting_read_index",
65
+ "meeting_read_entry",
66
+ ];
62
67
  const DEFAULT_TOOLS = [
63
68
  "read",
64
69
  "grep",
65
70
  "find",
66
71
  "ls",
67
- "meeting_append_entry",
68
- "meeting_read_index",
69
- "meeting_read_entry",
72
+ ...MEETING_TOOLS,
70
73
  ];
71
74
 
72
75
  // ────────────────────────────────────────────────────────
@@ -283,9 +286,15 @@ function yamlScalar(value: string): string {
283
286
  }
284
287
 
285
288
  function generateAgentFile(participant: ParticipantConfig): string {
286
- const tools = participant.tools && participant.tools.length > 0
289
+ const baseTools = participant.tools && participant.tools.length > 0
287
290
  ? participant.tools
288
291
  : DEFAULT_TOOLS;
292
+ // Always include meeting tools even when participant overrides defaults
293
+ const toolsSet = new Set(baseTools);
294
+ for (const mt of MEETING_TOOLS) {
295
+ toolsSet.add(mt);
296
+ }
297
+ const tools = [...toolsSet];
289
298
  const toolsStr = tools.join(", ");
290
299
 
291
300
  const description =
@@ -301,7 +310,6 @@ function generateAgentFile(participant: ParticipantConfig): string {
301
310
  : `- 参与多模型讨论并提供${participant.displayName}视角的分析`;
302
311
 
303
312
  return [
304
- MANAGED_MARKER,
305
313
  "---",
306
314
  `name: ${yamlScalar(participant.agentName)}`,
307
315
  `description: ${yamlScalar(description)}`,
@@ -309,6 +317,8 @@ function generateAgentFile(participant: ParticipantConfig): string {
309
317
  `model: ${yamlScalar(participant.model)}`,
310
318
  "---",
311
319
  "",
320
+ MANAGED_MARKER,
321
+ "",
312
322
  `# ${participant.displayName} Brainstormer${roleTitle}`,
313
323
  "",
314
324
  participant.rolePrompt,
@@ -507,7 +517,7 @@ function buildBrainstormPrompt(
507
517
  return [
508
518
  `BLACKBOARD BRAINSTORMING SESSION: ${topic}`,
509
519
  "",
510
- `Meeting folder: \`${absDir}\``,
520
+ `Initial meeting folder: \`${absDir}\``,
511
521
  "",
512
522
  "You are facilitating a round-robin brainstorming session using the MEETING BLACKBOARD.",
513
523
  "Each consultant writes their FULL contribution to disk via meeting_append_entry.",
@@ -515,34 +525,52 @@ function buildBrainstormPrompt(
515
525
  "## Consultants (3 rounds)",
516
526
  consultantLines,
517
527
  "",
528
+ "## PRE-ROUND STEP — Assign a Human-Readable Title",
529
+ "",
530
+ `Before Round 1, choose a concise human-readable meeting title for "${topic}" and call:`,
531
+ ` meeting_rename({ meetingDir: "${absDir}", title: "<your concise title>" })`,
532
+ "",
533
+ "Use the returned `newMeetingDir` for ALL subsequent meeting tools and subagent tasks. If rename fails, continue with the original meetingDir.",
534
+ "",
518
535
  "## CRITICAL INSTRUCTIONS",
519
536
  "",
537
+ "### Blackboard-first — do NOT paste prior participant text into subagent tasks",
538
+ "",
539
+ "The meeting blackboard is the single source of truth. Subagent tasks must direct participants to READ from the blackboard, not receive pasted history.",
540
+ "",
541
+ "- Round 1: subagents write initial analysis. No prior entries to read.",
542
+ "- Round 2: tell subagents to call meeting_read_index and meeting_read_entry on the current meetingDir to read Round 1 entries before responding.",
543
+ "- Round 3: tell subagents to call meeting_read_index and meeting_read_entry to read ALL prior round entries AND any User feedback entries from the blackboard.",
544
+ "- User feedback: if the user provides feedback that exists only in chat, APPEND it to the blackboard as `speaker: \"User\", phase: \"Feedback after Round N\"`, then tell participants to read it from the blackboard.",
545
+ "",
520
546
  "### For subagents (include in EVERY task):",
521
- "1. Write your FULL contribution using the meeting_append_entry tool with:",
522
- ` - meetingDir: "${absDir}"`,
547
+ "1. When the task asks for prior context, read it from the blackboard using meeting_read_index and meeting_read_entry. Round 1 has no prior entries to read.",
548
+ "2. Write your FULL contribution using the meeting_append_entry tool with:",
549
+ ` - meetingDir: the current meetingDir (use the one returned by meeting_rename if rename succeeded)`,
523
550
  " - speaker: your display name, e.g.:",
524
551
  agentTaskLines,
525
552
  ' - phase: "Round 1", "Round 2", or "Round 3"',
526
553
  " - summary: a ONE-SENTENCE summary of your contribution",
527
554
  " - content: your FULL analysis in Chinese (中文)",
528
555
  " - content must contain only the participant's analysis. Do not include wrapper tags, hidden thinking markers, tool-call text, or WROTE_ENTRY text inside content.",
529
- "2. After writing, your FINAL ANSWER must be ONLY:",
556
+ "3. After writing, your FINAL ANSWER must be ONLY:",
530
557
  " `WROTE_ENTRY: <your one-sentence summary>`",
531
- "3. DO NOT paste your full analysis into the chat. The main agent and user will read it from the blackboard.",
558
+ "4. DO NOT paste your full analysis into the chat. The main agent and user will read it from the blackboard.",
532
559
  "",
533
560
  "### For you, the facilitator:",
534
561
  "- Do NOT paste participant full text into chat. They are on the blackboard.",
562
+ "- Do NOT paste prior round content into subagent tasks. Tell subagents to read the blackboard.",
535
563
  "- After each round, read the index with meeting_read_index and present a structural overview.",
536
564
  "- Optionally read full entries with meeting_read_entry when needed.",
537
565
  "- Present each consultant's summary + your structural overview (conflict matrix, consensus table).",
538
- "- When the user gives feedback, relay it VERBATIM to the consultants in the next round.",
566
+ "- When the user gives feedback, append it to the blackboard as a User entry, then tell participants to read it.",
539
567
  "",
540
568
  "## Protocol",
541
- "Round 1: Each consultant gives initial analysis on the topic. Run all in parallel.",
569
+ "Round 1: Each consultant gives initial analysis on the topic. Run all in parallel. Subagents do not need to read prior entries.",
542
570
  "After Round 1: read the index, present summaries plus a structural overview, then STOP. Ask the user for feedback or permission to continue. Do NOT start Round 2 in the same assistant turn.",
543
- "Round 2: only after the user replies, feed Round 1 plus the user's VERBATIM feedback back to each consultant. Ask each to challenge the others and propose improvements.",
571
+ "Round 2: only after the user replies, tell each subagent to call meeting_read_index and meeting_read_entry to read Round 1 entries, then challenge the others and propose improvements. If user feedback exists, append it to the blackboard first.",
544
572
  "After Round 2: read the index, present summaries plus an updated structural overview, then STOP. Ask the user for feedback or permission to continue. Do NOT start Round 3 in the same assistant turn.",
545
- "Round 3: only after the user replies, feed all prior rounds plus the user's VERBATIM feedback back to each consultant. Each gives FINAL recommendation, synthesizing the best ideas.",
573
+ "Round 3: only after the user replies, tell each subagent to read ALL prior round entries AND user feedback entries from the blackboard (via meeting_read_index and meeting_read_entry). Each gives FINAL recommendation, synthesizing the best ideas.",
546
574
  "",
547
575
  "After Round 3, present the complete structural overview and ask whether to write the final conclusion. Only write conclusion.md after the user confirms.",
548
576
  "",
@@ -622,7 +650,7 @@ function buildDebatePrompt(
622
650
  return [
623
651
  `⚔️ BLACKBOARD DEBATE: ${topic}`,
624
652
  "",
625
- `Meeting folder: \`${absDir}\``,
653
+ `Initial meeting folder: \`${absDir}\``,
626
654
  "",
627
655
  "You are facilitating an OPEN-ENDED debate using the MEETING BLACKBOARD.",
628
656
  "Each debater writes their FULL argument to disk via meeting_append_entry.",
@@ -634,26 +662,39 @@ function buildDebatePrompt(
634
662
  "## DEBATE PERSONAS (include in each subagent task)",
635
663
  taskPrefixLines,
636
664
  "",
665
+ "## PRE-ROUND STEP — Assign a Human-Readable Title",
666
+ "",
667
+ `Before Cycle 1, choose a concise human-readable meeting title for "${topic}" and call:`,
668
+ ` meeting_rename({ meetingDir: "${absDir}", title: "<your concise title>" })`,
669
+ "",
670
+ "Use the returned `newMeetingDir` for ALL subsequent meeting tools and subagent tasks. If rename fails, continue with the original meetingDir.",
671
+ "",
637
672
  "## CRITICAL INSTRUCTIONS",
638
673
  "",
674
+ "### Blackboard-first — do NOT paste prior participant text into subagent tasks",
675
+ "",
676
+ "The meeting blackboard is the single source of truth. Subagent tasks must direct participants to READ from the blackboard, not receive pasted history.",
677
+ "",
678
+ "- Each subagent must call meeting_read_index and meeting_read_entry on the current meetingDir to read the complete debate history before writing.",
679
+ "- NEVER summarize or truncate the debate record when passing to subagents — tell them to read it from the blackboard.",
680
+ "- If the user provides feedback in chat, APPEND it to the blackboard as `speaker: \"User\", phase: \"Feedback\"`, then tell debaters to read it.",
681
+ "",
639
682
  "### For subagents (include in EVERY task):",
640
- "1. Write your FULL contribution using the meeting_append_entry tool with:",
641
- ` - meetingDir: "${absDir}"`,
683
+ "1. First, read the complete debate history from the blackboard using meeting_read_index and meeting_read_entry.",
684
+ "2. Write your FULL contribution using the meeting_append_entry tool with:",
685
+ ` - meetingDir: the current meetingDir (use the one returned by meeting_rename if rename succeeded)`,
642
686
  " - speaker: your display name, e.g.:",
643
687
  agentTaskLines,
644
688
  ' - phase: "Cycle 1", "Cycle 2", etc.',
645
689
  " - summary: a ONE-SENTENCE summary of your argument",
646
690
  " - content: your FULL argument in Chinese (中文)",
647
- "2. After writing, your FINAL ANSWER must be ONLY:",
691
+ "3. After writing, your FINAL ANSWER must be ONLY:",
648
692
  " `WROTE_ENTRY: <your one-sentence summary>`",
649
- "3. DO NOT paste your full argument into the chat.",
650
- "",
651
- "### Include the FULL VERBATIM prior debate record in each subagent task.",
652
- "Use meeting_read_index and meeting_read_entry to retrieve the complete debate history.",
653
- "NEVER summarize or truncate the debate record when passing to subagents.",
693
+ "4. DO NOT paste your full argument into the chat.",
654
694
  "",
655
695
  "### For you, the facilitator:",
656
696
  "- Do NOT paste participant full text into chat. They are on the blackboard.",
697
+ "- Do NOT paste prior debate history into subagent tasks. Tell subagents to read the blackboard.",
657
698
  "- Cycle through debaters in sequence (chain mode) so each sees all prior entries.",
658
699
  "- Read the index with meeting_read_index frequently.",
659
700
  "- Read full entries with meeting_read_entry when synthesizing.",
@@ -719,11 +760,6 @@ function sanitizeFilenamePart(raw: string): string {
719
760
  );
720
761
  }
721
762
 
722
- /** Convert a topic string to a filesystem-safe slug. */
723
- function topicToSlug(topic: string): string {
724
- return sanitizeFilenamePart(topic).slice(0, 40);
725
- }
726
-
727
763
  /** Format today's date as YYYY-MM-DD. */
728
764
  function todayStr(): string {
729
765
  const d = new Date();
@@ -805,6 +841,136 @@ function readEntrySummary(absPath: string): string {
805
841
  }
806
842
  }
807
843
 
844
+ /** Strip CR/LF/control chars, collapse whitespace, trim, max ~80 chars. */
845
+ function sanitizeHumanTitle(raw: string): string {
846
+ return (
847
+ raw
848
+ .replace(/[\r\n\x00-\x1f\x7f]/g, " ")
849
+ .replace(/\s+/g, " ")
850
+ .trim()
851
+ .slice(0, 80) || "Untitled meeting"
852
+ );
853
+ }
854
+
855
+ /** Return current time as HHMMSS. */
856
+ function timeStr(): string {
857
+ const d = new Date();
858
+ const hh = String(d.getHours()).padStart(2, "0");
859
+ const mm = String(d.getMinutes()).padStart(2, "0");
860
+ const ss = String(d.getSeconds()).padStart(2, "0");
861
+ return `${hh}${mm}${ss}`;
862
+ }
863
+
864
+ /** Generate initial meeting name for a kind. */
865
+ function initialMeetingName(kind: "brainstorm" | "debate"): string {
866
+ return `${todayStr()}-${kind}-${timeStr()}`;
867
+ }
868
+
869
+ /**
870
+ * Create a unique meeting directory under cwd/.pi-meetings/.
871
+ * Tries baseName, then baseName-2 through baseName-50.
872
+ * Returns the absolute directory and the actual meeting name used.
873
+ */
874
+ async function createUniqueMeetingDir(
875
+ cwd: string,
876
+ baseName: string
877
+ ): Promise<{ absDir: string; meetingName: string }> {
878
+ const meetingsRoot = path.resolve(cwd, ".pi-meetings");
879
+
880
+ // validateMeetingDir ensures root exists
881
+ const candidates: string[] = [baseName];
882
+ for (let suffix = 2; suffix <= 50; suffix++) {
883
+ candidates.push(`${baseName}-${suffix}`);
884
+ }
885
+
886
+ for (const candidate of candidates) {
887
+ const absDir = path.resolve(cwd, ".pi-meetings", candidate);
888
+ // Validate resolves & ensures root
889
+ validateMeetingDir(absDir, cwd);
890
+ if (!fs.existsSync(absDir)) {
891
+ await fsp.mkdir(absDir);
892
+ assertDirectoryNoSymlink(absDir, "meeting directory");
893
+ const entriesDir = path.join(absDir, "entries");
894
+ await fsp.mkdir(entriesDir);
895
+ assertDirectoryNoSymlink(entriesDir, "entries directory");
896
+ return { absDir, meetingName: candidate };
897
+ }
898
+ }
899
+
900
+ throw new Error(
901
+ `Could not create unique meeting directory under .pi-meetings/ for base name "${baseName}". Tried up to ${baseName}-50.`
902
+ );
903
+ }
904
+
905
+ /** Check whether a meeting already has entries. */
906
+ async function meetingHasEntries(absDir: string): Promise<boolean> {
907
+ const manifest = await readManifest(absDir);
908
+ if (manifest && manifest.entryCount > 0) return true;
909
+
910
+ const count = await getEntryCount(absDir);
911
+ if (count > 0) return true;
912
+
913
+ const entriesDir = path.join(absDir, "entries");
914
+ if (fs.existsSync(entriesDir)) {
915
+ try {
916
+ const files = await fsp.readdir(entriesDir);
917
+ if (files.some((f) => f.endsWith(".md"))) return true;
918
+ } catch {
919
+ // ignore
920
+ }
921
+ }
922
+
923
+ return false;
924
+ }
925
+
926
+ /** Replace the first markdown heading in blackboard.md, preserving Meeting/Debate prefix. */
927
+ function replaceBlackboardHeading(absDir: string, title: string): void {
928
+ const blackboardPath = path.join(absDir, "blackboard.md");
929
+ assertWritableFilePath(blackboardPath, absDir, "meeting blackboard");
930
+
931
+ let content = fs.readFileSync(blackboardPath, "utf-8");
932
+ content = content.replace(
933
+ /^# (Meeting|Debate): .*$/m,
934
+ `# $1: ${title}`
935
+ );
936
+ fs.writeFileSync(blackboardPath, content, "utf-8");
937
+ }
938
+
939
+ /**
940
+ * Move all direct children from oldAbsDir into a newly-created targetAbsDir,
941
+ * then remove oldAbsDir. Rejects if target already exists. Rejects symlinks
942
+ * among old dir's direct children.
943
+ */
944
+ async function moveMeetingDirectoryNoOverwrite(
945
+ oldAbsDir: string,
946
+ targetAbsDir: string
947
+ ): Promise<void> {
948
+ if (fs.existsSync(targetAbsDir)) {
949
+ throw new Error(`Target directory already exists: ${targetAbsDir}`);
950
+ }
951
+
952
+ const children = await fsp.readdir(oldAbsDir, { withFileTypes: true });
953
+ for (const child of children) {
954
+ if (child.isSymbolicLink()) {
955
+ throw new Error(
956
+ `Symlink detected in meeting directory: ${path.join(oldAbsDir, child.name)}`
957
+ );
958
+ }
959
+ }
960
+
961
+ await fsp.mkdir(targetAbsDir);
962
+ assertDirectoryNoSymlink(targetAbsDir, "target meeting directory");
963
+
964
+ for (const child of children) {
965
+ await fsp.rename(
966
+ path.join(oldAbsDir, child.name),
967
+ path.join(targetAbsDir, child.name)
968
+ );
969
+ }
970
+
971
+ await fsp.rmdir(oldAbsDir);
972
+ }
973
+
808
974
  // ────────────────────────────────────────────────────────
809
975
  // Watcher management
810
976
  // ────────────────────────────────────────────────────────
@@ -902,6 +1068,7 @@ function stopWatching(meetingDir: string): void {
902
1068
 
903
1069
  interface MeetingManifest {
904
1070
  topic: string;
1071
+ title?: string;
905
1072
  created: string;
906
1073
  lastUpdate: string;
907
1074
  entryCount: number;
@@ -944,6 +1111,139 @@ async function getEntryCount(absDir: string): Promise<number> {
944
1111
  // ────────────────────────────────────────────────────────
945
1112
 
946
1113
  export default function (pi: ExtensionAPI) {
1114
+ // ── Tool: meeting_rename ─────────────────────────────
1115
+
1116
+ pi.registerTool({
1117
+ name: "meeting_rename",
1118
+ label: "Meeting Rename",
1119
+ description:
1120
+ "Rename a meeting directory to a human-readable title. Facilitator-only. " +
1121
+ "Only works when the meeting has no entries yet (before Round 1 starts).",
1122
+ promptSnippet:
1123
+ "meeting_rename({ meetingDir, title }) — rename meeting to human-readable title",
1124
+ parameters: Type.Object({
1125
+ meetingDir: Type.String({
1126
+ description: "Absolute path to the current meeting directory",
1127
+ }),
1128
+ title: Type.String({
1129
+ description: "Human-readable meeting title (max ~80 chars)",
1130
+ }),
1131
+ }),
1132
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1133
+ const cwd = ctx.cwd;
1134
+ const oldAbsDir = validateMeetingDir(params.meetingDir, cwd);
1135
+
1136
+ const manifestPath = path.join(oldAbsDir, "manifest.json");
1137
+ assertWritableFilePath(manifestPath, oldAbsDir, "manifest");
1138
+
1139
+ return withFileMutationQueue(manifestPath, async () => {
1140
+ // Reject if meeting already has entries
1141
+ const hasEntries = await meetingHasEntries(oldAbsDir);
1142
+ if (hasEntries) {
1143
+ return {
1144
+ content: [
1145
+ {
1146
+ type: "text" as const,
1147
+ text: "Cannot rename meeting: entries already exist. meeting_rename only works on empty meetings (before Round 1 starts).",
1148
+ },
1149
+ ],
1150
+ details: {},
1151
+ isError: true,
1152
+ };
1153
+ }
1154
+
1155
+ // Sanitize title
1156
+ const sanitizedTitle = sanitizeHumanTitle(params.title);
1157
+
1158
+ // Derive date prefix from old basename
1159
+ const oldBase = path.basename(oldAbsDir);
1160
+ const dateMatch = oldBase.match(/^\d{4}-\d{2}-\d{2}/);
1161
+ const datePrefix = dateMatch ? dateMatch[0] : todayStr();
1162
+
1163
+ // Build target base name
1164
+ const titleSlug = sanitizeFilenamePart(sanitizedTitle).slice(0, 40);
1165
+ const baseTarget = `${datePrefix}-${titleSlug}`;
1166
+
1167
+ // Find a non-existing suffix
1168
+ let targetMeetingName = baseTarget;
1169
+ let targetAbsDir = path.resolve(cwd, ".pi-meetings", targetMeetingName);
1170
+
1171
+ if (fs.existsSync(targetAbsDir)) {
1172
+ let found = false;
1173
+ for (let suffix = 2; suffix <= 50; suffix++) {
1174
+ const candidate = `${baseTarget}-${suffix}`;
1175
+ const candidateAbsDir = path.resolve(cwd, ".pi-meetings", candidate);
1176
+ if (!fs.existsSync(candidateAbsDir)) {
1177
+ targetMeetingName = candidate;
1178
+ targetAbsDir = candidateAbsDir;
1179
+ found = true;
1180
+ break;
1181
+ }
1182
+ }
1183
+ if (!found) {
1184
+ return {
1185
+ content: [
1186
+ {
1187
+ type: "text" as const,
1188
+ text: `Cannot rename meeting: no available target name under .pi-meetings/. Tried ${baseTarget} through ${baseTarget}-50.`,
1189
+ },
1190
+ ],
1191
+ details: {},
1192
+ isError: true,
1193
+ };
1194
+ }
1195
+ }
1196
+
1197
+ // Validate target
1198
+ validateMeetingDir(targetAbsDir, cwd);
1199
+
1200
+ // Stop watching old dir
1201
+ stopWatching(oldAbsDir);
1202
+
1203
+ try {
1204
+ await moveMeetingDirectoryNoOverwrite(oldAbsDir, targetAbsDir);
1205
+ } catch (err: any) {
1206
+ // Restart watcher on old dir on failure
1207
+ startWatching(pi, oldAbsDir);
1208
+ throw err;
1209
+ }
1210
+
1211
+ try {
1212
+ // Update manifest with new title
1213
+ const manifest = await readManifest(targetAbsDir);
1214
+ if (manifest) {
1215
+ manifest.title = sanitizedTitle;
1216
+ manifest.lastUpdate = new Date().toISOString();
1217
+ await writeManifest(targetAbsDir, manifest);
1218
+ }
1219
+
1220
+ // Replace blackboard heading
1221
+ replaceBlackboardHeading(targetAbsDir, sanitizedTitle);
1222
+ } catch (err) {
1223
+ startWatching(pi, targetAbsDir);
1224
+ throw err;
1225
+ }
1226
+
1227
+ // Start watching new dir
1228
+ startWatching(pi, targetAbsDir);
1229
+
1230
+ return {
1231
+ content: [
1232
+ {
1233
+ type: "text" as const,
1234
+ text: `Meeting renamed. New meetingDir: ${targetAbsDir}`,
1235
+ },
1236
+ ],
1237
+ details: {
1238
+ oldMeetingDir: oldAbsDir,
1239
+ newMeetingDir: targetAbsDir,
1240
+ title: sanitizedTitle,
1241
+ },
1242
+ };
1243
+ });
1244
+ },
1245
+ });
1246
+
947
1247
  // ── Tool: meeting_append_entry ─────────────────────────
948
1248
 
949
1249
  pi.registerTool({
@@ -1309,16 +1609,19 @@ export default function (pi: ExtensionAPI) {
1309
1609
  if (!agentsReady) return;
1310
1610
 
1311
1611
  const topic = args.trim();
1312
- const slug = topicToSlug(topic);
1313
- const dateStr = todayStr();
1314
- const meetingName = `${dateStr}-${slug}`;
1315
- const absDir = validateMeetingDir(
1316
- path.resolve(ctx.cwd, ".pi-meetings", meetingName),
1317
- ctx.cwd
1318
- );
1612
+ let absDir: string;
1613
+ let meetingName: string;
1614
+ try {
1615
+ ({ absDir, meetingName } = await createUniqueMeetingDir(
1616
+ ctx.cwd,
1617
+ initialMeetingName("brainstorm")
1618
+ ));
1619
+ } catch (err: any) {
1620
+ ctx.ui.notify(`Failed to create meeting folder: ${err.message}`, "error");
1621
+ return;
1622
+ }
1319
1623
 
1320
- // Create meeting folder structure
1321
- await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
1624
+ // Assertions (createUniqueMeetingDir already created dir + entries)
1322
1625
  assertDirectoryNoSymlink(absDir, "meeting directory");
1323
1626
  assertDirectoryNoSymlink(
1324
1627
  path.join(absDir, "entries"),
@@ -1404,16 +1707,19 @@ export default function (pi: ExtensionAPI) {
1404
1707
  if (!agentsReady) return;
1405
1708
 
1406
1709
  const topic = args.trim();
1407
- const slug = topicToSlug(topic);
1408
- const dateStr = todayStr();
1409
- const meetingName = `${dateStr}-${slug}`;
1410
- const absDir = validateMeetingDir(
1411
- path.resolve(ctx.cwd, ".pi-meetings", meetingName),
1412
- ctx.cwd
1413
- );
1710
+ let absDir: string;
1711
+ let meetingName: string;
1712
+ try {
1713
+ ({ absDir, meetingName } = await createUniqueMeetingDir(
1714
+ ctx.cwd,
1715
+ initialMeetingName("debate")
1716
+ ));
1717
+ } catch (err: any) {
1718
+ ctx.ui.notify(`Failed to create meeting folder: ${err.message}`, "error");
1719
+ return;
1720
+ }
1414
1721
 
1415
- // Create meeting folder structure
1416
- await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
1722
+ // Assertions (createUniqueMeetingDir already created dir + entries)
1417
1723
  assertDirectoryNoSymlink(absDir, "meeting directory");
1418
1724
  assertDirectoryNoSymlink(
1419
1725
  path.join(absDir, "entries"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-brainstorm",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Multi-model brainstorming and debate sessions for pi subagents.",
5
5
  "type": "module",
6
6
  "keywords": [