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 +3 -2
- package/README.zh-CN.md +3 -2
- package/extensions/brainstorm.ts +352 -46
- 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 =
|
|
@@ -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
|
-
`
|
|
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.
|
|
522
|
-
|
|
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
|
-
"
|
|
556
|
+
"3. After writing, your FINAL ANSWER must be ONLY:",
|
|
530
557
|
" `WROTE_ENTRY: <your one-sentence summary>`",
|
|
531
|
-
"
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
`
|
|
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.
|
|
641
|
-
|
|
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
|
-
"
|
|
691
|
+
"3. After writing, your FINAL ANSWER must be ONLY:",
|
|
648
692
|
" `WROTE_ENTRY: <your one-sentence summary>`",
|
|
649
|
-
"
|
|
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
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
//
|
|
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"),
|