@neuroverseos/governance 0.8.0 → 0.8.1

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getLens
3
- } from "./chunk-VGFDMPVB.js";
3
+ } from "./chunk-F2LWMOM5.js";
4
4
  import {
5
5
  loadWorld
6
6
  } from "./chunk-I4RTIMLX.js";
@@ -8,91 +8,166 @@ import {
8
8
  evaluateGuard
9
9
  } from "./chunk-ZAF6JH23.js";
10
10
 
11
+ // src/radiant/core/compress.ts
12
+ function compressWorldmodel(content) {
13
+ const lines = [];
14
+ const missionMatch = content.match(/##\s*Mission\s*\n+(?:<!--[\s\S]*?-->\s*\n+)?(.*?)(?:\n\n|\n##|$)/s);
15
+ if (missionMatch) {
16
+ const mission = missionMatch[1].trim().split("\n")[0];
17
+ lines.push(`Mission: ${mission}`);
18
+ }
19
+ const domainMatches = content.matchAll(/###\s+([^\n]+)/g);
20
+ const domains = [];
21
+ for (const m of domainMatches) {
22
+ const name = m[1].trim();
23
+ if (name !== "Skills" && name !== "Values" && !name.startsWith("####")) {
24
+ domains.push(name);
25
+ }
26
+ }
27
+ if (domains.length > 0) {
28
+ lines.push(`Domains: ${domains.join(", ")}`);
29
+ }
30
+ const invariantSection = content.match(/(?:Invariants|## Invariants|invariants)([\s\S]*?)(?:\n#|\n---|\n\n\n)/i);
31
+ if (invariantSection) {
32
+ const invLines = invariantSection[1].match(/^[-*]\s+`?([^`\n]+)/gm);
33
+ if (invLines) {
34
+ lines.push("\nInvariants:");
35
+ for (const inv of invLines.slice(0, 10)) {
36
+ lines.push(inv.trim());
37
+ }
38
+ }
39
+ }
40
+ const prioritySection = content.match(/(?:Decision Priorities|## Decision Priorities)([\s\S]*?)(?:\n#|\n---|\n\n\n)/i);
41
+ if (prioritySection) {
42
+ const priLines = prioritySection[1].match(/^[-*]\s+.+>.+/gm);
43
+ if (priLines) {
44
+ lines.push("\nPriorities:");
45
+ for (const pri of priLines.slice(0, 10)) {
46
+ lines.push(pri.trim());
47
+ }
48
+ }
49
+ }
50
+ const signalSection = content.match(/(?:## Signals)([\s\S]*?)(?:\n#|\n---|\n\n\n)/i);
51
+ if (signalSection) {
52
+ const sigLines = signalSection[1].match(/^[-*]\s+(\w+)/gm);
53
+ if (sigLines) {
54
+ lines.push(`
55
+ Signals: ${sigLines.map((s) => s.replace(/^[-*]\s+/, "")).join(", ")}`);
56
+ }
57
+ }
58
+ const driftSection = content.match(/(?:Drift Behaviors|## Drift Behaviors)([\s\S]*?)(?:\n#|\n---|\n\n\n)/i);
59
+ if (driftSection) {
60
+ const driftLines = driftSection[1].match(/^[-*]\s+(.+)/gm);
61
+ if (driftLines) {
62
+ lines.push("\nDrift behaviors:");
63
+ for (const d of driftLines.slice(0, 5)) {
64
+ lines.push(d.trim());
65
+ }
66
+ }
67
+ }
68
+ const compressed = lines.join("\n");
69
+ if (compressed.length < 50) {
70
+ return content.slice(0, 2e3) + "\n[truncated]";
71
+ }
72
+ return compressed;
73
+ }
74
+ function compressExocortex(ctx) {
75
+ const lines = [];
76
+ if (ctx.attention) {
77
+ lines.push(`Attention: ${firstMeaningfulLine(ctx.attention)}`);
78
+ }
79
+ if (ctx.goals) {
80
+ lines.push(`Goals: ${firstNLines(ctx.goals, 3)}`);
81
+ }
82
+ if (ctx.sprint) {
83
+ lines.push(`Sprint: ${firstNLines(ctx.sprint, 3)}`);
84
+ }
85
+ if (ctx.identity) {
86
+ lines.push(`Identity: ${firstMeaningfulLine(ctx.identity)}`);
87
+ }
88
+ if (ctx.organization) {
89
+ lines.push(`Org: ${firstMeaningfulLine(ctx.organization)}`);
90
+ }
91
+ return lines.join("\n");
92
+ }
93
+ function compressLens(lens) {
94
+ return {
95
+ evaluationQuestions: lens.primary_frame.evaluation_questions.map((q, i) => `${i + 1}. ${q}`).join("\n"),
96
+ scoringRubric: lens.primary_frame.scoring_rubric,
97
+ forbiddenPhrases: lens.forbidden_phrases.join(", "),
98
+ jargonTranslations: Object.entries(lens.vocabulary.jargon_translations).map(([k, v]) => `${k} \u2192 ${v}`).join("; "),
99
+ strategicPatterns: lens.strategic_patterns.slice(0, 5).join("\n")
100
+ };
101
+ }
102
+ function compressPriorReads(reads) {
103
+ if (reads.length === 0) return "";
104
+ const patternCounts = /* @__PURE__ */ new Map();
105
+ for (const read of reads) {
106
+ for (const name of read.patternNames) {
107
+ patternCounts.set(name, (patternCounts.get(name) ?? 0) + 1);
108
+ }
109
+ }
110
+ const sorted = [...patternCounts.entries()].sort((a, b) => b[1] - a[1]);
111
+ if (sorted.length === 0) {
112
+ return `${reads.length} prior reads, no patterns extracted.`;
113
+ }
114
+ const patternList = sorted.map(([name, count]) => `${name} (${count}x)`).join(", ");
115
+ return `${reads.length} prior reads. Patterns seen: ${patternList}. If these recur, note persistence.`;
116
+ }
117
+ function firstMeaningfulLine(text) {
118
+ const lines = text.split("\n").filter((l) => {
119
+ const t = l.trim();
120
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith("<!--");
121
+ });
122
+ return lines[0]?.slice(0, 200) ?? "";
123
+ }
124
+ function firstNLines(text, n) {
125
+ const lines = text.split("\n").filter((l) => {
126
+ const t = l.trim();
127
+ return t.length > 0 && !t.startsWith("#") && !t.startsWith("<!--");
128
+ });
129
+ return lines.slice(0, n).map((l) => l.slice(0, 150)).join("; ");
130
+ }
131
+
11
132
  // src/radiant/core/prompt.ts
12
133
  function composeSystemPrompt(worldmodelContent, lens) {
13
- const sections = [];
14
- sections.push(
15
- `## Worldmodel
16
-
17
- You are operating inside a governed environment. The worldmodel below
18
- defines the invariants, signals, decision priorities, and behavioral
19
- expectations for this organization. Every response you produce must
20
- be grounded in this worldmodel.
21
-
22
- ` + worldmodelContent
23
- );
24
- const frame = lens.primary_frame;
25
- const questionsBlock = frame.evaluation_questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
26
- const overlapsBlock = frame.overlaps.map(
27
- (o) => `- ${o.domains[0]} + ${o.domains[1]} = **${o.emergent_state}**: ${o.description}`
28
- ).join("\n");
29
- sections.push(
30
- `## How to Think (Analytical Frame: ${lens.name})
31
-
32
- ${frame.scoring_rubric}
134
+ const compressedWorld = compressWorldmodel(worldmodelContent);
135
+ const cl = compressLens(lens);
136
+ const overlapsBlock = lens.primary_frame.overlaps.map((o) => `${o.domains[0]} + ${o.domains[1]} = ${o.emergent_state}`).join("\n");
137
+ return [
138
+ // Section 1: Compressed worldmodel
139
+ `## Worldmodel (compressed)
33
140
 
34
- ### Evaluation questions to reason through
141
+ ${compressedWorld}`,
142
+ // Section 2: Analytical frame (evaluation questions + rubric)
143
+ `## How to Think
35
144
 
36
- ${questionsBlock}
145
+ ${cl.scoringRubric}
37
146
 
38
- ### Overlap emergent states
147
+ Questions:
148
+ ${cl.evaluationQuestions}
39
149
 
40
- ${overlapsBlock}
150
+ Overlaps: ${overlapsBlock}
151
+ Center: ${lens.primary_frame.center_identity}
41
152
 
42
- ### Center identity
43
-
44
- When all dimensions integrate fully: **${frame.center_identity}**. Surface this sparingly \u2014 only when the integration is genuinely complete.`
45
- );
46
- const vocabPreferred = Object.entries(lens.vocabulary.preferred).map(([generic, native]) => `- "${generic}" \u2192 **${native}**`).join("\n");
47
- const vocabArchitecture = lens.vocabulary.architecture.map((t) => `\`${t}\``).join(", ");
48
- const vocabProperNouns = lens.vocabulary.proper_nouns.map((n) => `**${n}**`).join(", ");
49
- const strategicBlock = lens.strategic_patterns.map((p) => `- ${p}`).join("\n");
50
- sections.push(
51
- `## How to Speak (Voice: ${lens.name})
153
+ Translate before output: ${cl.jargonTranslations}`,
154
+ // Section 3: Voice (compressed — register + key rules only)
155
+ `## Voice: ${lens.name}
52
156
 
53
157
  Register: ${lens.voice.register}
54
-
55
- Rules:
56
- - Active voice: ${lens.voice.active_voice}
57
- - Named specificity (people, places, numbers): ${lens.voice.specificity}
58
- - Hype vocabulary: ${lens.voice.hype_vocabulary}
59
- - Hedging / qualified phrasing: ${lens.voice.hedging}
60
- - Playfulness: ${lens.voice.playfulness}
61
- - Close with strategic frame: ${lens.voice.close_with_strategic_frame}
62
- - Honesty about failure: ${lens.voice.honesty_about_failure}
63
-
64
- ### Output translation discipline
158
+ Active voice: ${lens.voice.active_voice}. Specificity: ${lens.voice.specificity}. Hedging: ${lens.voice.hedging}. Hype: ${lens.voice.hype_vocabulary}. Honesty about failure: ${lens.voice.honesty_about_failure}.
65
159
 
66
160
  ${lens.voice.output_translation}
67
161
 
68
- ### Vocabulary
69
-
70
- Proper nouns (use literally): ${vocabProperNouns}
71
-
72
- Preferred term substitutions:
73
- ${vocabPreferred}
74
-
75
- Architecture vocabulary: ${vocabArchitecture}
76
-
77
- ### Strategic decision patterns
78
-
79
- When recommending action, these patterns reflect how this organization resolves tradeoffs:
80
-
81
- ${strategicBlock}`
82
- );
83
- const forbiddenBlock = lens.forbidden_phrases.map((p) => `- "${p}"`).join("\n");
84
- sections.push(
162
+ Strategic patterns:
163
+ ${cl.strategicPatterns}`,
164
+ // Section 4: Guardrails (forbidden phrases as comma-separated, not bulleted)
85
165
  `## Guardrails
86
166
 
87
- Do NOT use any of these phrases in your response. If you catch yourself
88
- reaching for one, rephrase in direct, active, specific language instead.
167
+ Do NOT use: ${cl.forbiddenPhrases}
89
168
 
90
- ${forbiddenBlock}
91
-
92
- If your response would violate a worldmodel invariant, state the conflict
93
- explicitly and propose an alternative that honors the invariant.`
94
- );
95
- return sections.join("\n\n---\n\n");
169
+ If a response would violate a worldmodel invariant, state the conflict and propose an alternative.`
170
+ ].join("\n\n---\n\n");
96
171
  }
97
172
 
98
173
  // src/radiant/core/voice-check.ts
@@ -390,18 +465,481 @@ function createMockGitHubAdapter(fixedEvents) {
390
465
  return async () => fixedEvents;
391
466
  }
392
467
 
393
- // src/radiant/adapters/exocortex.ts
394
- import { readFileSync, existsSync, readdirSync, statSync } from "fs";
395
- import { join, resolve } from "path";
396
- function readExocortex(dirPath) {
468
+ // src/radiant/adapters/discord.ts
469
+ async function fetchDiscordActivity(guildId, token, options = {}) {
470
+ const windowDays = options.windowDays ?? 14;
471
+ const perChannel = options.perChannel ?? 100;
472
+ const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
473
+ const headers = {
474
+ Authorization: `Bot ${token}`,
475
+ "Content-Type": "application/json"
476
+ };
477
+ const channels = await fetchJSON2(
478
+ `https://discord.com/api/v10/guilds/${guildId}/channels`,
479
+ headers
480
+ );
481
+ const textChannels = channels.filter((c) => {
482
+ if (c.type !== 0) return false;
483
+ if (options.channelIds && options.channelIds.length > 0) {
484
+ return options.channelIds.includes(c.id);
485
+ }
486
+ if (options.visibility === "public") {
487
+ return !c.name.startsWith("private-") && !c.nsfw;
488
+ }
489
+ return true;
490
+ });
491
+ const events = [];
492
+ let totalMessages = 0;
493
+ let helpRequests = 0;
494
+ let unresolvedThreads = 0;
495
+ let newcomerMessages = 0;
496
+ const responseTimes = [];
497
+ const participants = /* @__PURE__ */ new Set();
498
+ const knownParticipants = /* @__PURE__ */ new Set();
499
+ const topicCounts = /* @__PURE__ */ new Map();
500
+ for (const channel of textChannels.slice(0, 15)) {
501
+ try {
502
+ const messages = await fetchJSON2(
503
+ `https://discord.com/api/v10/channels/${channel.id}/messages?limit=${perChannel}`,
504
+ headers
505
+ );
506
+ const inWindow = messages.filter(
507
+ (m) => new Date(m.timestamp) >= since
508
+ );
509
+ totalMessages += inWindow.length;
510
+ const topic = channel.name.replace(/-/g, " ");
511
+ topicCounts.set(topic, (topicCounts.get(topic) ?? 0) + inWindow.length);
512
+ for (const msg of inWindow) {
513
+ const actor = mapDiscordUser(msg.author);
514
+ participants.add(actor.id);
515
+ const lowerContent = msg.content.toLowerCase();
516
+ if (lowerContent.includes("help") || lowerContent.includes("stuck") || lowerContent.includes("how do i") || lowerContent.includes("anyone know")) {
517
+ helpRequests++;
518
+ }
519
+ if (msg.referenced_message) {
520
+ const refTime = new Date(msg.referenced_message.timestamp).getTime();
521
+ const msgTime = new Date(msg.timestamp).getTime();
522
+ const diffMinutes = (msgTime - refTime) / 6e4;
523
+ if (diffMinutes > 0 && diffMinutes < 10080) {
524
+ responseTimes.push(diffMinutes);
525
+ }
526
+ }
527
+ events.push({
528
+ id: `discord-${msg.id}`,
529
+ timestamp: msg.timestamp,
530
+ actor,
531
+ kind: "discord_message",
532
+ content: msg.content.slice(0, 500),
533
+ respondsTo: msg.referenced_message ? {
534
+ eventId: `discord-${msg.referenced_message.id}`,
535
+ actor: mapDiscordUser(msg.referenced_message.author)
536
+ } : void 0,
537
+ metadata: {
538
+ channel: channel.name,
539
+ guildId
540
+ }
541
+ });
542
+ }
543
+ } catch {
544
+ }
545
+ }
546
+ const avgResponseMinutes = responseTimes.length > 0 ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : null;
547
+ const topTopics = [...topicCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t]) => t);
548
+ const signals = {
549
+ totalMessages,
550
+ activeChannels: textChannels.length,
551
+ uniqueParticipants: participants.size,
552
+ avgResponseMinutes: avgResponseMinutes ? Math.round(avgResponseMinutes) : null,
553
+ helpRequests,
554
+ unresolvedThreads,
555
+ topTopics,
556
+ newcomerMessages
557
+ };
558
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
559
+ return { events, signals };
560
+ }
561
+ function formatDiscordSignalsForPrompt(signals) {
562
+ if (signals.totalMessages === 0) return "";
563
+ const lines = [
564
+ "## Discord Activity (conversational behavior)",
565
+ "",
566
+ `${signals.totalMessages} messages across ${signals.activeChannels} channels.`,
567
+ `${signals.uniqueParticipants} unique participants.`
568
+ ];
569
+ if (signals.avgResponseMinutes !== null) {
570
+ lines.push(`Average response time: ${signals.avgResponseMinutes} minutes.`);
571
+ }
572
+ if (signals.helpRequests > 0) {
573
+ lines.push(`${signals.helpRequests} help requests detected.`);
574
+ }
575
+ if (signals.unresolvedThreads > 0) {
576
+ lines.push(`${signals.unresolvedThreads} unresolved threads.`);
577
+ }
578
+ if (signals.topTopics.length > 0) {
579
+ lines.push(`Top discussion topics: ${signals.topTopics.join(", ")}.`);
580
+ }
581
+ lines.push("");
582
+ lines.push("Compare conversational activity against GitHub shipping activity.");
583
+ lines.push("Where debates happen in Discord but nothing ships in GitHub, name the gap.");
584
+ lines.push("Where work ships in GitHub but nobody discusses it in Discord, name the visibility gap.");
585
+ return lines.join("\n");
586
+ }
587
+ function mapDiscordUser(user) {
588
+ return {
589
+ id: user.username,
590
+ kind: user.bot ? "bot" : "human",
591
+ name: user.username
592
+ };
593
+ }
594
+ async function fetchJSON2(url, headers) {
595
+ const res = await fetch(url, { headers });
596
+ if (!res.ok) {
597
+ if (res.status === 404 || res.status === 403) return [];
598
+ throw new Error(`Discord API error ${res.status}: ${(await res.text()).slice(0, 300)}`);
599
+ }
600
+ return await res.json();
601
+ }
602
+
603
+ // src/radiant/adapters/slack.ts
604
+ async function fetchSlackActivity(token, options = {}) {
605
+ const windowDays = options.windowDays ?? 14;
606
+ const perChannel = options.perChannel ?? 100;
607
+ const oldest = String(
608
+ Math.floor((Date.now() - windowDays * 24 * 60 * 60 * 1e3) / 1e3)
609
+ );
610
+ const headers = {
611
+ Authorization: `Bearer ${token}`,
612
+ "Content-Type": "application/json"
613
+ };
614
+ const channelsResponse = await fetchSlackAPI("https://slack.com/api/conversations.list?types=public_channel&limit=200", headers);
615
+ let channels = channelsResponse.channels ?? [];
616
+ if (options.channelIds && options.channelIds.length > 0) {
617
+ const ids = new Set(options.channelIds);
618
+ channels = channels.filter((c) => ids.has(c.id));
619
+ }
620
+ if (options.visibility === "public") {
621
+ channels = channels.filter((c) => !c.is_private && !c.is_archived);
622
+ }
623
+ const events = [];
624
+ let totalMessages = 0;
625
+ let reactionCount = 0;
626
+ let unresolvedThreads = 0;
627
+ const responseTimes = [];
628
+ const participants = /* @__PURE__ */ new Set();
629
+ const externalParticipants = /* @__PURE__ */ new Set();
630
+ const channelMessageCounts = /* @__PURE__ */ new Map();
631
+ for (const channel of channels.slice(0, 15)) {
632
+ try {
633
+ const historyResponse = await fetchSlackAPI(
634
+ `https://slack.com/api/conversations.history?channel=${channel.id}&limit=${perChannel}&oldest=${oldest}`,
635
+ headers
636
+ );
637
+ const messages = historyResponse.messages ?? [];
638
+ totalMessages += messages.length;
639
+ channelMessageCounts.set(channel.name, messages.length);
640
+ for (const msg of messages) {
641
+ if (msg.subtype === "channel_join" || msg.subtype === "channel_leave") continue;
642
+ const actor = mapSlackUser(msg.user ?? "unknown");
643
+ participants.add(actor.id);
644
+ if (msg.reactions) {
645
+ reactionCount += msg.reactions.reduce(
646
+ (sum, r) => sum + (r.count ?? 0),
647
+ 0
648
+ );
649
+ }
650
+ if (msg.thread_ts && msg.thread_ts !== msg.ts) {
651
+ const parentTs = parseFloat(msg.thread_ts) * 1e3;
652
+ const msgTs = parseFloat(msg.ts) * 1e3;
653
+ const diffMinutes = (msgTs - parentTs) / 6e4;
654
+ if (diffMinutes > 0 && diffMinutes < 10080) {
655
+ responseTimes.push(diffMinutes);
656
+ }
657
+ }
658
+ if (msg.thread_ts === msg.ts && (!msg.reply_count || msg.reply_count === 0)) {
659
+ if (msg.text && (msg.text.includes("?") || msg.text.toLowerCase().includes("help"))) {
660
+ unresolvedThreads++;
661
+ }
662
+ }
663
+ const timestamp = new Date(parseFloat(msg.ts) * 1e3).toISOString();
664
+ events.push({
665
+ id: `slack-${msg.ts}`,
666
+ timestamp,
667
+ actor,
668
+ kind: "slack_message",
669
+ content: (msg.text ?? "").slice(0, 500),
670
+ respondsTo: msg.thread_ts && msg.thread_ts !== msg.ts ? {
671
+ eventId: `slack-${msg.thread_ts}`,
672
+ actor: { id: "thread-parent", kind: "unknown" }
673
+ } : void 0,
674
+ metadata: {
675
+ channel: channel.name,
676
+ isPrivate: channel.is_private,
677
+ hasReactions: (msg.reactions?.length ?? 0) > 0
678
+ }
679
+ });
680
+ }
681
+ } catch {
682
+ }
683
+ }
684
+ const avgResponseMinutes = responseTimes.length > 0 ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : null;
685
+ const topChannels = [...channelMessageCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name]) => name);
686
+ const signals = {
687
+ totalMessages,
688
+ activeChannels: channelMessageCounts.size,
689
+ uniqueParticipants: participants.size,
690
+ avgResponseMinutes,
691
+ externalParticipants: externalParticipants.size,
692
+ unresolvedThreads,
693
+ topChannels,
694
+ reactionCount
695
+ };
696
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
697
+ return { events, signals };
698
+ }
699
+ function formatSlackSignalsForPrompt(signals) {
700
+ if (signals.totalMessages === 0) return "";
701
+ const lines = [
702
+ "## Slack Activity (external coordination)",
703
+ "",
704
+ `${signals.totalMessages} messages across ${signals.activeChannels} channels.`,
705
+ `${signals.uniqueParticipants} unique participants.`
706
+ ];
707
+ if (signals.avgResponseMinutes !== null) {
708
+ lines.push(`Average thread response time: ${signals.avgResponseMinutes} minutes.`);
709
+ }
710
+ if (signals.unresolvedThreads > 0) {
711
+ lines.push(`${signals.unresolvedThreads} questions/threads with no reply.`);
712
+ }
713
+ if (signals.reactionCount > 0) {
714
+ lines.push(`${signals.reactionCount} reactions (engagement signal).`);
715
+ }
716
+ if (signals.topChannels.length > 0) {
717
+ lines.push(`Most active channels: ${signals.topChannels.join(", ")}.`);
718
+ }
719
+ lines.push("");
720
+ lines.push("Slack carries external coordination \u2014 partner and client communication.");
721
+ lines.push("Compare partner engagement against internal activity. Where partners are");
722
+ lines.push("active but internal follow-through is low, name the gap.");
723
+ return lines.join("\n");
724
+ }
725
+ function mapSlackUser(userId) {
726
+ return {
727
+ id: userId,
728
+ kind: "human",
729
+ name: userId
730
+ };
731
+ }
732
+ async function fetchSlackAPI(url, headers) {
733
+ const res = await fetch(url, { headers });
734
+ if (!res.ok) {
735
+ throw new Error(`Slack API error ${res.status}: ${(await res.text()).slice(0, 300)}`);
736
+ }
737
+ const data = await res.json();
738
+ if (!data.ok) {
739
+ throw new Error(`Slack API error: ${data.error ?? "unknown"}`);
740
+ }
741
+ return data;
742
+ }
743
+
744
+ // src/radiant/adapters/notion.ts
745
+ async function fetchNotionActivity(token, options = {}) {
746
+ const windowDays = options.windowDays ?? 14;
747
+ const maxPages = options.maxPages ?? 100;
748
+ const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
749
+ const headers = {
750
+ Authorization: `Bearer ${token}`,
751
+ "Notion-Version": "2022-06-28",
752
+ "Content-Type": "application/json"
753
+ };
754
+ const searchResponse = await fetchNotionAPI("https://api.notion.com/v1/search", headers, {
755
+ method: "POST",
756
+ body: JSON.stringify({
757
+ filter: { property: "object", value: "page" },
758
+ sort: { direction: "descending", timestamp: "last_edited_time" },
759
+ page_size: maxPages
760
+ })
761
+ });
762
+ const pages = searchResponse.results ?? [];
763
+ const events = [];
764
+ const editors = /* @__PURE__ */ new Set();
765
+ let pagesCreated = 0;
766
+ let pagesUpdated = 0;
767
+ let stalePages = 0;
768
+ const editAges = [];
769
+ const topPages = [];
770
+ const now = Date.now();
771
+ for (const page of pages) {
772
+ const lastEdited = new Date(page.last_edited_time);
773
+ const created = new Date(page.created_time);
774
+ const daysSinceEdit = (now - lastEdited.getTime()) / (24 * 60 * 60 * 1e3);
775
+ editAges.push(daysSinceEdit);
776
+ if (daysSinceEdit > 30) stalePages++;
777
+ const title = extractTitle(page);
778
+ const editorId = page.last_edited_by?.id ?? "unknown";
779
+ editors.add(editorId);
780
+ if (lastEdited >= since) {
781
+ const isNew = created >= since;
782
+ if (isNew) pagesCreated++;
783
+ else pagesUpdated++;
784
+ topPages.push({ title, editedAt: page.last_edited_time });
785
+ events.push({
786
+ id: `notion-${page.id}`,
787
+ timestamp: page.last_edited_time,
788
+ actor: {
789
+ id: editorId,
790
+ kind: "human",
791
+ name: editorId
792
+ },
793
+ kind: isNew ? "doc_created" : "doc_updated",
794
+ content: `${isNew ? "Created" : "Updated"}: ${title}`,
795
+ metadata: {
796
+ pageId: page.id,
797
+ url: page.url,
798
+ createdAt: page.created_time
799
+ }
800
+ });
801
+ }
802
+ }
803
+ const avgDaysSinceEdit = editAges.length > 0 ? Math.round(editAges.reduce((a, b) => a + b, 0) / editAges.length) : null;
804
+ const signals = {
805
+ pagesActive: pagesCreated + pagesUpdated,
806
+ pagesCreated,
807
+ pagesUpdated,
808
+ uniqueEditors: editors.size,
809
+ stalePages,
810
+ avgDaysSinceEdit,
811
+ topPages: topPages.slice(0, 5).map((p) => p.title)
812
+ };
813
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
814
+ return { events, signals };
815
+ }
816
+ function formatNotionSignalsForPrompt(signals) {
817
+ if (signals.pagesActive === 0 && signals.stalePages === 0) return "";
818
+ const lines = [
819
+ "## Notion Activity (documentation behavior)",
820
+ "",
821
+ `${signals.pagesActive} pages active in window (${signals.pagesCreated} created, ${signals.pagesUpdated} updated).`,
822
+ `${signals.uniqueEditors} unique editors.`
823
+ ];
824
+ if (signals.stalePages > 0) {
825
+ lines.push(`${signals.stalePages} pages haven't been touched in 30+ days.`);
826
+ }
827
+ if (signals.avgDaysSinceEdit !== null) {
828
+ lines.push(`Average page age since last edit: ${signals.avgDaysSinceEdit} days.`);
829
+ }
830
+ if (signals.topPages.length > 0) {
831
+ lines.push(`Recently active pages: ${signals.topPages.join(", ")}.`);
832
+ }
833
+ lines.push("");
834
+ lines.push("Documentation is how the team crystallizes and shares knowledge.");
835
+ lines.push("High code velocity + low documentation = building without recording.");
836
+ lines.push("High documentation + low code = planning without shipping.");
837
+ lines.push("Compare Notion activity against GitHub and Discord to find the balance.");
838
+ return lines.join("\n");
839
+ }
840
+ function extractTitle(page) {
841
+ for (const prop of Object.values(page.properties)) {
842
+ if (prop.type === "title" && prop.title) {
843
+ return prop.title.map((t) => t.plain_text).join("") || "Untitled";
844
+ }
845
+ }
846
+ return "Untitled";
847
+ }
848
+ async function fetchNotionAPI(url, headers, init) {
849
+ const res = await fetch(url, {
850
+ method: init?.method ?? "GET",
851
+ headers,
852
+ body: init?.body
853
+ });
854
+ if (!res.ok) {
855
+ throw new Error(
856
+ `Notion API error ${res.status}: ${(await res.text()).slice(0, 300)}`
857
+ );
858
+ }
859
+ return await res.json();
860
+ }
861
+
862
+ // src/radiant/core/discovery.ts
863
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
864
+ import { join, resolve, basename } from "path";
865
+ import { homedir } from "os";
866
+ function discoverWorlds(options) {
867
+ const worlds = [];
868
+ const userDir = options?.userWorldsDir ?? join(homedir(), ".neuroverse", "worlds");
869
+ if (existsSync(userDir)) {
870
+ worlds.push(...loadWorldsFromDir(userDir, "user"));
871
+ }
872
+ if (options?.explicitWorldsDir) {
873
+ worlds.push(...loadWorldsFromDir(options.explicitWorldsDir, "repo"));
874
+ } else if (options?.repoDir) {
875
+ const repoPaths = [
876
+ join(options.repoDir, "worlds"),
877
+ join(options.repoDir, ".neuroverse", "worlds")
878
+ ];
879
+ for (const p of repoPaths) {
880
+ if (existsSync(p)) {
881
+ worlds.push(...loadWorldsFromDir(p, "repo"));
882
+ break;
883
+ }
884
+ }
885
+ }
886
+ const combinedContent = worlds.map((w) => `<!-- world: ${w.name} (${w.source}) -->
887
+ ${w.content}`).join("\n\n---\n\n");
888
+ const summary = worlds.length === 0 ? "no worlds discovered" : worlds.map((w) => `${w.name} (${w.source})`).join(", ");
889
+ return { worlds, combinedContent, summary };
890
+ }
891
+ function formatActiveWorlds(stack) {
892
+ if (stack.worlds.length === 0) return "No worlds loaded.";
893
+ const lines = ["ACTIVE WORLDS", ""];
894
+ for (const w of stack.worlds) {
895
+ const sourceLabel = w.source === "base" ? "universal" : w.source === "user" ? "personal" : "this repo";
896
+ lines.push(` ${w.name} (${sourceLabel})`);
897
+ }
898
+ return lines.join("\n");
899
+ }
900
+ function loadWorldsFromDir(dirPath, source) {
397
901
  const dir = resolve(dirPath);
902
+ if (!existsSync(dir)) return [];
903
+ const stat = statSync(dir);
904
+ if (stat.isFile() && dir.endsWith(".md")) {
905
+ try {
906
+ return [{
907
+ name: basename(dir).replace(/\.worldmodel\.md$/, "").replace(/\.nv-world\.md$/, ""),
908
+ source,
909
+ path: dir,
910
+ content: readFileSync(dir, "utf-8")
911
+ }];
912
+ } catch {
913
+ return [];
914
+ }
915
+ }
916
+ if (!stat.isDirectory()) return [];
917
+ const files = readdirSync(dir).filter(
918
+ (f) => f.endsWith(".worldmodel.md") || f.endsWith(".nv-world.md")
919
+ ).sort();
920
+ return files.map((f) => {
921
+ const fullPath = join(dir, f);
922
+ return {
923
+ name: f.replace(/\.worldmodel\.md$/, "").replace(/\.nv-world\.md$/, ""),
924
+ source,
925
+ path: fullPath,
926
+ content: readFileSync(fullPath, "utf-8")
927
+ };
928
+ });
929
+ }
930
+
931
+ // src/radiant/adapters/exocortex.ts
932
+ import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
933
+ import { join as join2, resolve as resolve2 } from "path";
934
+ function readExocortex(dirPath, repoName) {
935
+ const dir = resolve2(dirPath);
398
936
  let filesLoaded = 0;
399
937
  function tryRead(...paths) {
400
938
  for (const p of paths) {
401
- const full = join(dir, p);
402
- if (existsSync(full)) {
939
+ const full = join2(dir, p);
940
+ if (existsSync2(full)) {
403
941
  try {
404
- const content = readFileSync(full, "utf-8").trim();
942
+ const content = readFileSync2(full, "utf-8").trim();
405
943
  if (content) {
406
944
  filesLoaded++;
407
945
  return content;
@@ -412,16 +950,64 @@ function readExocortex(dirPath) {
412
950
  }
413
951
  return null;
414
952
  }
953
+ const attention = tryRead("attention.md");
954
+ const goals = tryRead("goals.md");
955
+ const identity = tryRead("identity.md", "user.md");
956
+ const organization = tryRead("org/organization.md", "org/src/organization.md");
957
+ const methods = tryRead("org/methods.md", "org/src/methods.md");
958
+ let sprint = null;
959
+ let projectContext = null;
960
+ if (repoName) {
961
+ const projectPaths = [
962
+ repoName,
963
+ repoName.toLowerCase(),
964
+ repoName.replace(/-/g, "_")
965
+ ];
966
+ for (const projectDir of projectPaths) {
967
+ const projectSprint = tryRead(
968
+ `${projectDir}/src/sprint.md`,
969
+ `${projectDir}/sprint.md`
970
+ );
971
+ if (projectSprint) {
972
+ sprint = projectSprint;
973
+ break;
974
+ }
975
+ }
976
+ for (const projectDir of projectPaths) {
977
+ const roadmap = tryRead(
978
+ `${projectDir}/roadmap.md`,
979
+ `${projectDir}/src/roadmap.md`
980
+ );
981
+ if (roadmap) {
982
+ projectContext = roadmap;
983
+ break;
984
+ }
985
+ }
986
+ }
987
+ if (!sprint) {
988
+ sprint = tryRead("sprint.md", "src/sprint.md");
989
+ }
415
990
  const ctx = {
416
- attention: tryRead("attention.md"),
417
- goals: tryRead("goals.md"),
418
- identity: tryRead("identity.md"),
419
- sprint: tryRead("sprint.md", "src/sprint.md"),
420
- organization: tryRead("org/organization.md", "org/src/organization.md"),
421
- methods: tryRead("org/methods.md", "org/src/methods.md"),
991
+ attention,
992
+ goals,
993
+ identity,
994
+ sprint,
995
+ organization,
996
+ methods,
422
997
  source: dir,
423
998
  filesLoaded
424
999
  };
1000
+ if (projectContext && ctx.sprint) {
1001
+ ctx.sprint = `${ctx.sprint}
1002
+
1003
+ ---
1004
+ Project roadmap:
1005
+ ${projectContext}`;
1006
+ } else if (projectContext) {
1007
+ ctx.sprint = `Project roadmap:
1008
+ ${projectContext}`;
1009
+ ctx.filesLoaded++;
1010
+ }
425
1011
  return ctx;
426
1012
  }
427
1013
  function formatExocortexForPrompt(ctx) {
@@ -463,14 +1049,14 @@ ${ctx.methods}`);
463
1049
  return sections.join("\n\n");
464
1050
  }
465
1051
  function readTeamExocortices(teamDir) {
466
- const dir = resolve(teamDir);
467
- if (!existsSync(dir)) return [];
468
- const entries = readdirSync(dir);
1052
+ const dir = resolve2(teamDir);
1053
+ if (!existsSync2(dir)) return [];
1054
+ const entries = readdirSync2(dir);
469
1055
  const results = [];
470
1056
  for (const entry of entries) {
471
- const entryPath = join(dir, entry);
1057
+ const entryPath = join2(dir, entry);
472
1058
  try {
473
- const stat = statSync(entryPath);
1059
+ const stat = statSync2(entryPath);
474
1060
  if (stat.isDirectory()) {
475
1061
  const ctx = readExocortex(entryPath);
476
1062
  if (ctx.filesLoaded > 0) {
@@ -515,14 +1101,14 @@ function summarizeExocortex(ctx) {
515
1101
  }
516
1102
 
517
1103
  // src/radiant/memory/palace.ts
518
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
519
- import { join as join2, resolve as resolve2 } from "path";
1104
+ import { readFileSync as readFileSync3, writeFileSync, mkdirSync, readdirSync as readdirSync3, existsSync as existsSync3 } from "fs";
1105
+ import { join as join3, resolve as resolve3 } from "path";
520
1106
  function writeRead(exocortexDir, frontmatter, text) {
521
- const dir = resolve2(exocortexDir, "radiant", "reads");
1107
+ const dir = resolve3(exocortexDir, "radiant", "reads");
522
1108
  mkdirSync(dir, { recursive: true });
523
1109
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
524
1110
  const filename = `${date}.md`;
525
- const filepath = join2(dir, filename);
1111
+ const filepath = join3(dir, filename);
526
1112
  const content = `${frontmatter}
527
1113
 
528
1114
  ${text}
@@ -531,9 +1117,9 @@ ${text}
531
1117
  return filepath;
532
1118
  }
533
1119
  function updateKnowledge(exocortexDir, persistence, options) {
534
- const dir = resolve2(exocortexDir, "radiant");
1120
+ const dir = resolve3(exocortexDir, "radiant");
535
1121
  mkdirSync(dir, { recursive: true });
536
- const filepath = join2(dir, "knowledge.md");
1122
+ const filepath = join3(dir, "knowledge.md");
537
1123
  const totalReads = options?.totalReads ?? 0;
538
1124
  const existingUntriggered = loadUntriggeredCounts(filepath);
539
1125
  const lines = [
@@ -625,9 +1211,9 @@ function updateKnowledge(exocortexDir, persistence, options) {
625
1211
  }
626
1212
  function loadUntriggeredCounts(filepath) {
627
1213
  const counts = /* @__PURE__ */ new Map();
628
- if (!existsSync2(filepath)) return counts;
1214
+ if (!existsSync3(filepath)) return counts;
629
1215
  try {
630
- const content = readFileSync2(filepath, "utf-8");
1216
+ const content = readFileSync3(filepath, "utf-8");
631
1217
  const match = content.match(
632
1218
  /<!-- untriggered_counts[\s\S]*?-->/
633
1219
  );
@@ -645,13 +1231,13 @@ function loadUntriggeredCounts(filepath) {
645
1231
  return counts;
646
1232
  }
647
1233
  function loadPriorReads(exocortexDir) {
648
- const dir = resolve2(exocortexDir, "radiant", "reads");
649
- if (!existsSync2(dir)) return [];
650
- const files = readdirSync2(dir).filter((f) => f.endsWith(".md")).sort();
1234
+ const dir = resolve3(exocortexDir, "radiant", "reads");
1235
+ if (!existsSync3(dir)) return [];
1236
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".md")).sort();
651
1237
  const reads = [];
652
1238
  for (const filename of files) {
653
- const filepath = join2(dir, filename);
654
- const content = readFileSync2(filepath, "utf-8");
1239
+ const filepath = join3(dir, filename);
1240
+ const content = readFileSync3(filepath, "utf-8");
655
1241
  const date = filename.replace(".md", "");
656
1242
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
657
1243
  const frontmatter = fmMatch ? fmMatch[1] : "";
@@ -1028,15 +1614,17 @@ function buildInterpretationPrompt(input) {
1028
1614
  const eventSample = formatEventSample(input.events, 30);
1029
1615
  const canonicalList = (input.canonicalPatterns ?? []).length > 0 ? `Patterns the organization has already named (use these names if you see them):
1030
1616
  ${input.canonicalPatterns.map((p) => `- ${p}`).join("\n")}` : "No patterns have been named yet. Everything you observe is new.";
1617
+ const compressedWorld = compressWorldmodel(input.worldmodelContent);
1618
+ const cl = compressLens(input.lens);
1031
1619
  const frame = input.lens.primary_frame;
1032
1620
  const evalQuestions = frame.evaluation_questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
1033
- const forbiddenList = input.lens.forbidden_phrases.map((p) => `- "${p}"`).join("\n");
1034
- const jargonTable = Object.entries(input.lens.vocabulary.jargon_translations).map(([internal, plain]) => ` "${internal}" \u2192 "${plain}"`).join("\n");
1621
+ const forbiddenList = cl.forbiddenPhrases;
1622
+ const jargonTable = cl.jargonTranslations;
1035
1623
  return `You are a behavioral intelligence system reading team activity and producing a read for the reader who needs to act on it.
1036
1624
 
1037
- ## Context the reader has loaded
1625
+ ## Worldmodel (compressed)
1038
1626
 
1039
- ${input.worldmodelContent}
1627
+ ${compressedWorld}
1040
1628
 
1041
1629
  ## What happened this window
1042
1630
 
@@ -1227,30 +1815,16 @@ Window: last ${input.windowDays} days \xB7 ${input.eventCount} events
1227
1815
  Lens: ${input.lens.name}`
1228
1816
  );
1229
1817
  if (input.patterns.length > 0) {
1230
- const canonical = input.patterns.filter((p) => p.type === "canonical");
1231
- const candidates = input.patterns.filter((p) => p.type === "candidate");
1232
1818
  let emergentBlock = "EMERGENT\n";
1233
- if (canonical.length > 0) {
1234
- for (const p of canonical) {
1235
- emergentBlock += `
1819
+ for (const p of input.patterns) {
1820
+ emergentBlock += `
1236
1821
  ${p.name}
1237
1822
  `;
1238
- emergentBlock += ` ${p.description}
1823
+ emergentBlock += ` ${p.description}
1239
1824
  `;
1240
- }
1241
- }
1242
- if (candidates.length > 0) {
1243
- emergentBlock += "\n Emergent (candidates \u2014 not yet in worldmodel)\n";
1244
- for (const p of candidates) {
1245
- emergentBlock += `
1246
- ${p.name} (candidate)
1247
- `;
1248
- emergentBlock += ` ${p.description}
1825
+ if (p.evidence.cited_invariant) {
1826
+ emergentBlock += ` Cited invariant: ${p.evidence.cited_invariant}
1249
1827
  `;
1250
- if (p.evidence.cited_invariant) {
1251
- emergentBlock += ` Cited invariant: ${p.evidence.cited_invariant}
1252
- `;
1253
- }
1254
1828
  }
1255
1829
  }
1256
1830
  sections.push(emergentBlock.trimEnd());
@@ -1462,16 +2036,29 @@ function serializeYAML(obj, indent = 0) {
1462
2036
  async function emergent(input) {
1463
2037
  const lens = resolveLens2(input.lensId);
1464
2038
  const windowDays = input.windowDays ?? 14;
2039
+ let worldStack;
2040
+ let worldmodelContent = input.worldmodelContent;
2041
+ if (!worldmodelContent || worldmodelContent.trim() === "") {
2042
+ worldStack = discoverWorlds({ explicitWorldsDir: input.worldPath });
2043
+ worldmodelContent = worldStack.combinedContent;
2044
+ }
1465
2045
  let statedIntent;
1466
2046
  let exocortexContext;
1467
2047
  let priorReadContext = "";
1468
2048
  if (input.exocortexPath) {
1469
- exocortexContext = readExocortex(input.exocortexPath);
1470
- const formatted = formatExocortexForPrompt(exocortexContext);
1471
- if (formatted) statedIntent = formatted;
2049
+ const repoName = input.scope.type === "repo" ? input.scope.repo : void 0;
2050
+ exocortexContext = readExocortex(input.exocortexPath, repoName);
2051
+ const compressed = compressExocortex(exocortexContext);
2052
+ if (compressed) {
2053
+ statedIntent = `## Stated Intent (from exocortex, compressed)
2054
+
2055
+ ${compressed}
2056
+
2057
+ Compare stated intent against actual GitHub activity. Gaps = drift.`;
2058
+ }
1472
2059
  const priorReads = loadPriorReads(input.exocortexPath);
1473
2060
  if (priorReads.length > 0) {
1474
- priorReadContext = formatPriorReadsForPrompt(priorReads);
2061
+ priorReadContext = compressPriorReads(priorReads);
1475
2062
  }
1476
2063
  }
1477
2064
  let events;
@@ -1489,6 +2076,40 @@ async function emergent(input) {
1489
2076
  windowDays
1490
2077
  });
1491
2078
  }
2079
+ let adapterSignals = "";
2080
+ const activeAdapters = ["github"];
2081
+ const discordToken = process.env.DISCORD_TOKEN;
2082
+ const discordGuild = process.env.DISCORD_GUILD_ID;
2083
+ if (discordToken && discordGuild) {
2084
+ try {
2085
+ const discord = await fetchDiscordActivity(discordGuild, discordToken, { windowDays });
2086
+ events.push(...discord.events);
2087
+ adapterSignals += "\n\n" + formatDiscordSignalsForPrompt(discord.signals);
2088
+ activeAdapters.push("discord");
2089
+ } catch {
2090
+ }
2091
+ }
2092
+ const slackToken = process.env.SLACK_TOKEN;
2093
+ if (slackToken) {
2094
+ try {
2095
+ const slack = await fetchSlackActivity(slackToken, { windowDays });
2096
+ events.push(...slack.events);
2097
+ adapterSignals += "\n\n" + formatSlackSignalsForPrompt(slack.signals);
2098
+ activeAdapters.push("slack");
2099
+ } catch {
2100
+ }
2101
+ }
2102
+ const notionToken = process.env.NOTION_TOKEN;
2103
+ if (notionToken) {
2104
+ try {
2105
+ const notion = await fetchNotionActivity(notionToken, { windowDays });
2106
+ events.push(...notion.events);
2107
+ adapterSignals += "\n\n" + formatNotionSignalsForPrompt(notion.signals);
2108
+ activeAdapters.push("notion");
2109
+ } catch {
2110
+ }
2111
+ }
2112
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
1492
2113
  const classified = classifyEvents(events);
1493
2114
  const signals = extractSignals(classified);
1494
2115
  const scores = computeScores(signals, input.worldmodelContent !== "");
@@ -1499,7 +2120,7 @@ async function emergent(input) {
1499
2120
  lens,
1500
2121
  ai: input.ai,
1501
2122
  canonicalPatterns: input.canonicalPatterns,
1502
- statedIntent: statedIntent ? statedIntent + (priorReadContext ? "\n\n" + priorReadContext : "") : priorReadContext || void 0
2123
+ statedIntent: [statedIntent, adapterSignals, priorReadContext].filter(Boolean).join("\n\n") || void 0
1503
2124
  });
1504
2125
  const rewrittenPatterns = patterns.map((p) => lens.rewrite(p));
1505
2126
  const allDescriptions = rewrittenPatterns.map((p) => p.description).join("\n");
@@ -1548,7 +2169,9 @@ async function emergent(input) {
1548
2169
  voiceClean: voiceViolations.length === 0,
1549
2170
  signals,
1550
2171
  scores,
1551
- eventCount: events.length
2172
+ eventCount: events.length,
2173
+ activeAdapters,
2174
+ worldStack
1552
2175
  };
1553
2176
  }
1554
2177
  function computeScores(signals, worldmodelLoaded) {
@@ -1641,6 +2264,10 @@ function createMockAI(fixedResponse) {
1641
2264
  }
1642
2265
 
1643
2266
  export {
2267
+ compressWorldmodel,
2268
+ compressExocortex,
2269
+ compressLens,
2270
+ compressPriorReads,
1644
2271
  composeSystemPrompt,
1645
2272
  checkForbiddenPhrases,
1646
2273
  think,
@@ -1650,6 +2277,14 @@ export {
1650
2277
  fetchGitHubActivity,
1651
2278
  fetchGitHubOrgActivity,
1652
2279
  createMockGitHubAdapter,
2280
+ fetchDiscordActivity,
2281
+ formatDiscordSignalsForPrompt,
2282
+ fetchSlackActivity,
2283
+ formatSlackSignalsForPrompt,
2284
+ fetchNotionActivity,
2285
+ formatNotionSignalsForPrompt,
2286
+ discoverWorlds,
2287
+ formatActiveWorlds,
1653
2288
  readExocortex,
1654
2289
  formatExocortexForPrompt,
1655
2290
  readTeamExocortices,