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 +3 -2
- package/README.zh-CN.md +3 -2
- package/extensions/brainstorm.ts +350 -45
- package/package.json +1 -1
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.
|
|
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
|
|
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.
|
|
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`:在主持人或参与者需要时读取某条完整发言。
|
package/extensions/brainstorm.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
523
|
-
|
|
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
|
-
"
|
|
556
|
+
"3. After writing, your FINAL ANSWER must be ONLY:",
|
|
531
557
|
" `WROTE_ENTRY: <your one-sentence summary>`",
|
|
532
|
-
"
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
`
|
|
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.
|
|
642
|
-
|
|
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
|
-
"
|
|
691
|
+
"3. After writing, your FINAL ANSWER must be ONLY:",
|
|
649
692
|
" `WROTE_ENTRY: <your one-sentence summary>`",
|
|
650
|
-
"
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
-
//
|
|
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"),
|