pi-brainstorm 0.4.2 → 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.2
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.2
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 =
@@ -508,7 +517,7 @@ function buildBrainstormPrompt(
508
517
  return [
509
518
  `BLACKBOARD BRAINSTORMING SESSION: ${topic}`,
510
519
  "",
511
- `Meeting folder: \`${absDir}\``,
520
+ `Initial meeting folder: \`${absDir}\``,
512
521
  "",
513
522
  "You are facilitating a round-robin brainstorming session using the MEETING BLACKBOARD.",
514
523
  "Each consultant writes their FULL contribution to disk via meeting_append_entry.",
@@ -516,34 +525,52 @@ function buildBrainstormPrompt(
516
525
  "## Consultants (3 rounds)",
517
526
  consultantLines,
518
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
+ "",
519
535
  "## CRITICAL INSTRUCTIONS",
520
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
+ "",
521
546
  "### For subagents (include in EVERY task):",
522
- "1. Write your FULL contribution using the meeting_append_entry tool with:",
523
- ` - 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)`,
524
550
  " - speaker: your display name, e.g.:",
525
551
  agentTaskLines,
526
552
  ' - phase: "Round 1", "Round 2", or "Round 3"',
527
553
  " - summary: a ONE-SENTENCE summary of your contribution",
528
554
  " - content: your FULL analysis in Chinese (中文)",
529
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.",
530
- "2. After writing, your FINAL ANSWER must be ONLY:",
556
+ "3. After writing, your FINAL ANSWER must be ONLY:",
531
557
  " `WROTE_ENTRY: <your one-sentence summary>`",
532
- "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.",
533
559
  "",
534
560
  "### For you, the facilitator:",
535
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.",
536
563
  "- After each round, read the index with meeting_read_index and present a structural overview.",
537
564
  "- Optionally read full entries with meeting_read_entry when needed.",
538
565
  "- Present each consultant's summary + your structural overview (conflict matrix, consensus table).",
539
- "- 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.",
540
567
  "",
541
568
  "## Protocol",
542
- "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.",
543
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.",
544
- "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.",
545
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.",
546
- "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.",
547
574
  "",
548
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.",
549
576
  "",
@@ -623,7 +650,7 @@ function buildDebatePrompt(
623
650
  return [
624
651
  `⚔️ BLACKBOARD DEBATE: ${topic}`,
625
652
  "",
626
- `Meeting folder: \`${absDir}\``,
653
+ `Initial meeting folder: \`${absDir}\``,
627
654
  "",
628
655
  "You are facilitating an OPEN-ENDED debate using the MEETING BLACKBOARD.",
629
656
  "Each debater writes their FULL argument to disk via meeting_append_entry.",
@@ -635,26 +662,39 @@ function buildDebatePrompt(
635
662
  "## DEBATE PERSONAS (include in each subagent task)",
636
663
  taskPrefixLines,
637
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
+ "",
638
672
  "## CRITICAL INSTRUCTIONS",
639
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
+ "",
640
682
  "### For subagents (include in EVERY task):",
641
- "1. Write your FULL contribution using the meeting_append_entry tool with:",
642
- ` - 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)`,
643
686
  " - speaker: your display name, e.g.:",
644
687
  agentTaskLines,
645
688
  ' - phase: "Cycle 1", "Cycle 2", etc.',
646
689
  " - summary: a ONE-SENTENCE summary of your argument",
647
690
  " - content: your FULL argument in Chinese (中文)",
648
- "2. After writing, your FINAL ANSWER must be ONLY:",
691
+ "3. After writing, your FINAL ANSWER must be ONLY:",
649
692
  " `WROTE_ENTRY: <your one-sentence summary>`",
650
- "3. DO NOT paste your full argument into the chat.",
651
- "",
652
- "### Include the FULL VERBATIM prior debate record in each subagent task.",
653
- "Use meeting_read_index and meeting_read_entry to retrieve the complete debate history.",
654
- "NEVER summarize or truncate the debate record when passing to subagents.",
693
+ "4. DO NOT paste your full argument into the chat.",
655
694
  "",
656
695
  "### For you, the facilitator:",
657
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.",
658
698
  "- Cycle through debaters in sequence (chain mode) so each sees all prior entries.",
659
699
  "- Read the index with meeting_read_index frequently.",
660
700
  "- Read full entries with meeting_read_entry when synthesizing.",
@@ -720,11 +760,6 @@ function sanitizeFilenamePart(raw: string): string {
720
760
  );
721
761
  }
722
762
 
723
- /** Convert a topic string to a filesystem-safe slug. */
724
- function topicToSlug(topic: string): string {
725
- return sanitizeFilenamePart(topic).slice(0, 40);
726
- }
727
-
728
763
  /** Format today's date as YYYY-MM-DD. */
729
764
  function todayStr(): string {
730
765
  const d = new Date();
@@ -806,6 +841,136 @@ function readEntrySummary(absPath: string): string {
806
841
  }
807
842
  }
808
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
+
809
974
  // ────────────────────────────────────────────────────────
810
975
  // Watcher management
811
976
  // ────────────────────────────────────────────────────────
@@ -903,6 +1068,7 @@ function stopWatching(meetingDir: string): void {
903
1068
 
904
1069
  interface MeetingManifest {
905
1070
  topic: string;
1071
+ title?: string;
906
1072
  created: string;
907
1073
  lastUpdate: string;
908
1074
  entryCount: number;
@@ -945,6 +1111,139 @@ async function getEntryCount(absDir: string): Promise<number> {
945
1111
  // ────────────────────────────────────────────────────────
946
1112
 
947
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
+
948
1247
  // ── Tool: meeting_append_entry ─────────────────────────
949
1248
 
950
1249
  pi.registerTool({
@@ -1310,16 +1609,19 @@ export default function (pi: ExtensionAPI) {
1310
1609
  if (!agentsReady) return;
1311
1610
 
1312
1611
  const topic = args.trim();
1313
- const slug = topicToSlug(topic);
1314
- const dateStr = todayStr();
1315
- const meetingName = `${dateStr}-${slug}`;
1316
- const absDir = validateMeetingDir(
1317
- path.resolve(ctx.cwd, ".pi-meetings", meetingName),
1318
- ctx.cwd
1319
- );
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
+ }
1320
1623
 
1321
- // Create meeting folder structure
1322
- await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
1624
+ // Assertions (createUniqueMeetingDir already created dir + entries)
1323
1625
  assertDirectoryNoSymlink(absDir, "meeting directory");
1324
1626
  assertDirectoryNoSymlink(
1325
1627
  path.join(absDir, "entries"),
@@ -1405,16 +1707,19 @@ export default function (pi: ExtensionAPI) {
1405
1707
  if (!agentsReady) return;
1406
1708
 
1407
1709
  const topic = args.trim();
1408
- const slug = topicToSlug(topic);
1409
- const dateStr = todayStr();
1410
- const meetingName = `${dateStr}-${slug}`;
1411
- const absDir = validateMeetingDir(
1412
- path.resolve(ctx.cwd, ".pi-meetings", meetingName),
1413
- ctx.cwd
1414
- );
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
+ }
1415
1721
 
1416
- // Create meeting folder structure
1417
- await fsp.mkdir(path.join(absDir, "entries"), { recursive: true });
1722
+ // Assertions (createUniqueMeetingDir already created dir + entries)
1418
1723
  assertDirectoryNoSymlink(absDir, "meeting directory");
1419
1724
  assertDirectoryNoSymlink(
1420
1725
  path.join(absDir, "entries"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-brainstorm",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Multi-model brainstorming and debate sessions for pi subagents.",
5
5
  "type": "module",
6
6
  "keywords": [