@neuroverseos/governance 0.7.0 → 0.8.0

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.
@@ -46,10 +46,18 @@ __export(radiant_exports, {
46
46
  createMockGitHubAdapter: () => createMockGitHubAdapter,
47
47
  emergent: () => emergent,
48
48
  extractSignals: () => extractSignals,
49
+ fetchDiscordActivity: () => fetchDiscordActivity,
49
50
  fetchGitHubActivity: () => fetchGitHubActivity,
51
+ fetchGitHubOrgActivity: () => fetchGitHubOrgActivity,
52
+ fetchNotionActivity: () => fetchNotionActivity,
53
+ fetchSlackActivity: () => fetchSlackActivity,
54
+ formatDiscordSignalsForPrompt: () => formatDiscordSignalsForPrompt,
50
55
  formatExocortexForPrompt: () => formatExocortexForPrompt,
56
+ formatNotionSignalsForPrompt: () => formatNotionSignalsForPrompt,
51
57
  formatPriorReadsForPrompt: () => formatPriorReadsForPrompt,
52
58
  formatScope: () => formatScope,
59
+ formatSlackSignalsForPrompt: () => formatSlackSignalsForPrompt,
60
+ formatTeamExocorticesForPrompt: () => formatTeamExocorticesForPrompt,
53
61
  getLens: () => getLens,
54
62
  interpretPatterns: () => interpretPatterns,
55
63
  isPresent: () => isPresent,
@@ -58,8 +66,10 @@ __export(radiant_exports, {
58
66
  listLenses: () => listLenses,
59
67
  loadPriorReads: () => loadPriorReads,
60
68
  parseRepoScope: () => parseRepoScope,
69
+ parseScope: () => parseScope,
61
70
  presenceAverage: () => presenceAverage,
62
71
  readExocortex: () => readExocortex,
72
+ readTeamExocortices: () => readTeamExocortices,
63
73
  render: () => render,
64
74
  scoreComposite: () => scoreComposite,
65
75
  scoreCyber: () => scoreCyber,
@@ -847,17 +857,30 @@ function createMockAI(fixedResponse) {
847
857
  }
848
858
 
849
859
  // src/radiant/core/scopes.ts
850
- function parseRepoScope(scope) {
860
+ function parseScope(scope) {
851
861
  const cleaned = scope.replace(/^https?:\/\//, "").replace(/^github\.com\//, "").replace(/\.git$/, "").replace(/\/$/, "");
852
- const parts = cleaned.split("/");
853
- if (parts.length < 2 || !parts[0] || !parts[1]) {
862
+ const parts = cleaned.split("/").filter(Boolean);
863
+ if (parts.length === 0 || !parts[0]) {
864
+ throw new Error(
865
+ `Cannot parse scope: "${scope}". Expected "owner/repo" or "owner".`
866
+ );
867
+ }
868
+ if (parts.length === 1) {
869
+ return { type: "org", owner: parts[0] };
870
+ }
871
+ return { type: "repo", owner: parts[0], repo: parts[1] };
872
+ }
873
+ function parseRepoScope(scope) {
874
+ const parsed = parseScope(scope);
875
+ if (parsed.type === "org") {
854
876
  throw new Error(
855
- `Cannot parse repo scope: "${scope}". Expected "owner/repo" or a GitHub URL.`
877
+ `Expected "owner/repo" but got org-level scope "${parsed.owner}". Use parseScope() for org-level.`
856
878
  );
857
879
  }
858
- return { owner: parts[0], repo: parts[1] };
880
+ return parsed;
859
881
  }
860
882
  function formatScope(scope) {
883
+ if (scope.type === "org") return `${scope.owner} (org)`;
861
884
  return `${scope.owner}/${scope.repo}`;
862
885
  }
863
886
 
@@ -1041,6 +1064,40 @@ async function fetchJSON(url, headers) {
1041
1064
  }
1042
1065
  return await res.json();
1043
1066
  }
1067
+ async function fetchGitHubOrgActivity(scope, token, options = {}) {
1068
+ const perPage = options.perPage ?? 100;
1069
+ const headers = {
1070
+ Authorization: `token ${token}`,
1071
+ Accept: "application/vnd.github.v3+json",
1072
+ "User-Agent": "neuroverseos-radiant"
1073
+ };
1074
+ const repos = await fetchJSON(
1075
+ `https://api.github.com/orgs/${scope.owner}/repos?sort=pushed&direction=desc&per_page=${perPage}`,
1076
+ headers
1077
+ );
1078
+ const windowDays = options.windowDays ?? 14;
1079
+ const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
1080
+ const activeRepos = repos.filter(
1081
+ (r) => new Date(r.pushed_at) >= since
1082
+ );
1083
+ const cappedRepos = activeRepos.slice(0, 10);
1084
+ const allEvents = [];
1085
+ const repoNames = [];
1086
+ for (const repo of cappedRepos) {
1087
+ const [owner, repoName] = repo.full_name.split("/");
1088
+ try {
1089
+ const repoScope = { type: "repo", owner, repo: repoName };
1090
+ const events = await fetchGitHubActivity(repoScope, token, options);
1091
+ allEvents.push(...events);
1092
+ if (events.length > 0) repoNames.push(repo.full_name);
1093
+ } catch {
1094
+ }
1095
+ }
1096
+ allEvents.sort(
1097
+ (a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)
1098
+ );
1099
+ return { events: allEvents, repos: repoNames };
1100
+ }
1044
1101
  function createMockGitHubAdapter(fixedEvents) {
1045
1102
  return async () => fixedEvents;
1046
1103
  }
@@ -1117,6 +1174,46 @@ ${ctx.methods}`);
1117
1174
  }
1118
1175
  return sections.join("\n\n");
1119
1176
  }
1177
+ function readTeamExocortices(teamDir) {
1178
+ const dir = (0, import_path.resolve)(teamDir);
1179
+ if (!(0, import_fs.existsSync)(dir)) return [];
1180
+ const entries = (0, import_fs.readdirSync)(dir);
1181
+ const results = [];
1182
+ for (const entry of entries) {
1183
+ const entryPath = (0, import_path.join)(dir, entry);
1184
+ try {
1185
+ const stat = (0, import_fs.statSync)(entryPath);
1186
+ if (stat.isDirectory()) {
1187
+ const ctx = readExocortex(entryPath);
1188
+ if (ctx.filesLoaded > 0) {
1189
+ results.push({ name: entry, context: ctx });
1190
+ }
1191
+ }
1192
+ } catch {
1193
+ }
1194
+ }
1195
+ return results;
1196
+ }
1197
+ function formatTeamExocorticesForPrompt(team) {
1198
+ if (team.length === 0) return "";
1199
+ const sections = [
1200
+ "## Team Intent (cross-exocortex read)",
1201
+ "",
1202
+ `Reading ${team.length} team members' exocortices. Compare each person's`,
1203
+ "stated intent against the observed activity AND against each other.",
1204
+ "Surface: duplicate focus, missing coverage, silent pivots,",
1205
+ "and areas where no one is carrying the work.",
1206
+ ""
1207
+ ];
1208
+ for (const { name, context } of team) {
1209
+ sections.push(`### ${name}`);
1210
+ if (context.attention) sections.push(`**Attention:** ${context.attention.split("\n")[0]}`);
1211
+ if (context.goals) sections.push(`**Goals:** ${context.goals.split("\n").slice(0, 3).join("; ")}`);
1212
+ if (context.sprint) sections.push(`**Sprint:** ${context.sprint.split("\n").slice(0, 3).join("; ")}`);
1213
+ sections.push("");
1214
+ }
1215
+ return sections.join("\n");
1216
+ }
1120
1217
  function summarizeExocortex(ctx) {
1121
1218
  if (ctx.filesLoaded === 0) return "no exocortex files found";
1122
1219
  const loaded = [];
@@ -1129,6 +1226,400 @@ function summarizeExocortex(ctx) {
1129
1226
  return `${loaded.join(", ")} (${ctx.filesLoaded} files)`;
1130
1227
  }
1131
1228
 
1229
+ // src/radiant/adapters/discord.ts
1230
+ async function fetchDiscordActivity(guildId, token, options = {}) {
1231
+ const windowDays = options.windowDays ?? 14;
1232
+ const perChannel = options.perChannel ?? 100;
1233
+ const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
1234
+ const headers = {
1235
+ Authorization: `Bot ${token}`,
1236
+ "Content-Type": "application/json"
1237
+ };
1238
+ const channels = await fetchJSON2(
1239
+ `https://discord.com/api/v10/guilds/${guildId}/channels`,
1240
+ headers
1241
+ );
1242
+ const textChannels = channels.filter((c) => {
1243
+ if (c.type !== 0) return false;
1244
+ if (options.channelIds && options.channelIds.length > 0) {
1245
+ return options.channelIds.includes(c.id);
1246
+ }
1247
+ if (options.visibility === "public") {
1248
+ return !c.name.startsWith("private-") && !c.nsfw;
1249
+ }
1250
+ return true;
1251
+ });
1252
+ const events = [];
1253
+ let totalMessages = 0;
1254
+ let helpRequests = 0;
1255
+ let unresolvedThreads = 0;
1256
+ let newcomerMessages = 0;
1257
+ const responseTimes = [];
1258
+ const participants = /* @__PURE__ */ new Set();
1259
+ const knownParticipants = /* @__PURE__ */ new Set();
1260
+ const topicCounts = /* @__PURE__ */ new Map();
1261
+ for (const channel of textChannels.slice(0, 15)) {
1262
+ try {
1263
+ const messages = await fetchJSON2(
1264
+ `https://discord.com/api/v10/channels/${channel.id}/messages?limit=${perChannel}`,
1265
+ headers
1266
+ );
1267
+ const inWindow = messages.filter(
1268
+ (m) => new Date(m.timestamp) >= since
1269
+ );
1270
+ totalMessages += inWindow.length;
1271
+ const topic = channel.name.replace(/-/g, " ");
1272
+ topicCounts.set(topic, (topicCounts.get(topic) ?? 0) + inWindow.length);
1273
+ for (const msg of inWindow) {
1274
+ const actor = mapDiscordUser(msg.author);
1275
+ participants.add(actor.id);
1276
+ const lowerContent = msg.content.toLowerCase();
1277
+ if (lowerContent.includes("help") || lowerContent.includes("stuck") || lowerContent.includes("how do i") || lowerContent.includes("anyone know")) {
1278
+ helpRequests++;
1279
+ }
1280
+ if (msg.referenced_message) {
1281
+ const refTime = new Date(msg.referenced_message.timestamp).getTime();
1282
+ const msgTime = new Date(msg.timestamp).getTime();
1283
+ const diffMinutes = (msgTime - refTime) / 6e4;
1284
+ if (diffMinutes > 0 && diffMinutes < 10080) {
1285
+ responseTimes.push(diffMinutes);
1286
+ }
1287
+ }
1288
+ events.push({
1289
+ id: `discord-${msg.id}`,
1290
+ timestamp: msg.timestamp,
1291
+ actor,
1292
+ kind: "discord_message",
1293
+ content: msg.content.slice(0, 500),
1294
+ respondsTo: msg.referenced_message ? {
1295
+ eventId: `discord-${msg.referenced_message.id}`,
1296
+ actor: mapDiscordUser(msg.referenced_message.author)
1297
+ } : void 0,
1298
+ metadata: {
1299
+ channel: channel.name,
1300
+ guildId
1301
+ }
1302
+ });
1303
+ }
1304
+ } catch {
1305
+ }
1306
+ }
1307
+ const avgResponseMinutes = responseTimes.length > 0 ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : null;
1308
+ const topTopics = [...topicCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t]) => t);
1309
+ const signals = {
1310
+ totalMessages,
1311
+ activeChannels: textChannels.length,
1312
+ uniqueParticipants: participants.size,
1313
+ avgResponseMinutes: avgResponseMinutes ? Math.round(avgResponseMinutes) : null,
1314
+ helpRequests,
1315
+ unresolvedThreads,
1316
+ topTopics,
1317
+ newcomerMessages
1318
+ };
1319
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
1320
+ return { events, signals };
1321
+ }
1322
+ function formatDiscordSignalsForPrompt(signals) {
1323
+ if (signals.totalMessages === 0) return "";
1324
+ const lines = [
1325
+ "## Discord Activity (conversational behavior)",
1326
+ "",
1327
+ `${signals.totalMessages} messages across ${signals.activeChannels} channels.`,
1328
+ `${signals.uniqueParticipants} unique participants.`
1329
+ ];
1330
+ if (signals.avgResponseMinutes !== null) {
1331
+ lines.push(`Average response time: ${signals.avgResponseMinutes} minutes.`);
1332
+ }
1333
+ if (signals.helpRequests > 0) {
1334
+ lines.push(`${signals.helpRequests} help requests detected.`);
1335
+ }
1336
+ if (signals.unresolvedThreads > 0) {
1337
+ lines.push(`${signals.unresolvedThreads} unresolved threads.`);
1338
+ }
1339
+ if (signals.topTopics.length > 0) {
1340
+ lines.push(`Top discussion topics: ${signals.topTopics.join(", ")}.`);
1341
+ }
1342
+ lines.push("");
1343
+ lines.push("Compare conversational activity against GitHub shipping activity.");
1344
+ lines.push("Where debates happen in Discord but nothing ships in GitHub, name the gap.");
1345
+ lines.push("Where work ships in GitHub but nobody discusses it in Discord, name the visibility gap.");
1346
+ return lines.join("\n");
1347
+ }
1348
+ function mapDiscordUser(user) {
1349
+ return {
1350
+ id: user.username,
1351
+ kind: user.bot ? "bot" : "human",
1352
+ name: user.username
1353
+ };
1354
+ }
1355
+ async function fetchJSON2(url, headers) {
1356
+ const res = await fetch(url, { headers });
1357
+ if (!res.ok) {
1358
+ if (res.status === 404 || res.status === 403) return [];
1359
+ throw new Error(`Discord API error ${res.status}: ${(await res.text()).slice(0, 300)}`);
1360
+ }
1361
+ return await res.json();
1362
+ }
1363
+
1364
+ // src/radiant/adapters/slack.ts
1365
+ async function fetchSlackActivity(token, options = {}) {
1366
+ const windowDays = options.windowDays ?? 14;
1367
+ const perChannel = options.perChannel ?? 100;
1368
+ const oldest = String(
1369
+ Math.floor((Date.now() - windowDays * 24 * 60 * 60 * 1e3) / 1e3)
1370
+ );
1371
+ const headers = {
1372
+ Authorization: `Bearer ${token}`,
1373
+ "Content-Type": "application/json"
1374
+ };
1375
+ const channelsResponse = await fetchSlackAPI("https://slack.com/api/conversations.list?types=public_channel&limit=200", headers);
1376
+ let channels = channelsResponse.channels ?? [];
1377
+ if (options.channelIds && options.channelIds.length > 0) {
1378
+ const ids = new Set(options.channelIds);
1379
+ channels = channels.filter((c) => ids.has(c.id));
1380
+ }
1381
+ if (options.visibility === "public") {
1382
+ channels = channels.filter((c) => !c.is_private && !c.is_archived);
1383
+ }
1384
+ const events = [];
1385
+ let totalMessages = 0;
1386
+ let reactionCount = 0;
1387
+ let unresolvedThreads = 0;
1388
+ const responseTimes = [];
1389
+ const participants = /* @__PURE__ */ new Set();
1390
+ const externalParticipants = /* @__PURE__ */ new Set();
1391
+ const channelMessageCounts = /* @__PURE__ */ new Map();
1392
+ for (const channel of channels.slice(0, 15)) {
1393
+ try {
1394
+ const historyResponse = await fetchSlackAPI(
1395
+ `https://slack.com/api/conversations.history?channel=${channel.id}&limit=${perChannel}&oldest=${oldest}`,
1396
+ headers
1397
+ );
1398
+ const messages = historyResponse.messages ?? [];
1399
+ totalMessages += messages.length;
1400
+ channelMessageCounts.set(channel.name, messages.length);
1401
+ for (const msg of messages) {
1402
+ if (msg.subtype === "channel_join" || msg.subtype === "channel_leave") continue;
1403
+ const actor = mapSlackUser(msg.user ?? "unknown");
1404
+ participants.add(actor.id);
1405
+ if (msg.reactions) {
1406
+ reactionCount += msg.reactions.reduce(
1407
+ (sum, r) => sum + (r.count ?? 0),
1408
+ 0
1409
+ );
1410
+ }
1411
+ if (msg.thread_ts && msg.thread_ts !== msg.ts) {
1412
+ const parentTs = parseFloat(msg.thread_ts) * 1e3;
1413
+ const msgTs = parseFloat(msg.ts) * 1e3;
1414
+ const diffMinutes = (msgTs - parentTs) / 6e4;
1415
+ if (diffMinutes > 0 && diffMinutes < 10080) {
1416
+ responseTimes.push(diffMinutes);
1417
+ }
1418
+ }
1419
+ if (msg.thread_ts === msg.ts && (!msg.reply_count || msg.reply_count === 0)) {
1420
+ if (msg.text && (msg.text.includes("?") || msg.text.toLowerCase().includes("help"))) {
1421
+ unresolvedThreads++;
1422
+ }
1423
+ }
1424
+ const timestamp = new Date(parseFloat(msg.ts) * 1e3).toISOString();
1425
+ events.push({
1426
+ id: `slack-${msg.ts}`,
1427
+ timestamp,
1428
+ actor,
1429
+ kind: "slack_message",
1430
+ content: (msg.text ?? "").slice(0, 500),
1431
+ respondsTo: msg.thread_ts && msg.thread_ts !== msg.ts ? {
1432
+ eventId: `slack-${msg.thread_ts}`,
1433
+ actor: { id: "thread-parent", kind: "unknown" }
1434
+ } : void 0,
1435
+ metadata: {
1436
+ channel: channel.name,
1437
+ isPrivate: channel.is_private,
1438
+ hasReactions: (msg.reactions?.length ?? 0) > 0
1439
+ }
1440
+ });
1441
+ }
1442
+ } catch {
1443
+ }
1444
+ }
1445
+ const avgResponseMinutes = responseTimes.length > 0 ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : null;
1446
+ const topChannels = [...channelMessageCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name]) => name);
1447
+ const signals = {
1448
+ totalMessages,
1449
+ activeChannels: channelMessageCounts.size,
1450
+ uniqueParticipants: participants.size,
1451
+ avgResponseMinutes,
1452
+ externalParticipants: externalParticipants.size,
1453
+ unresolvedThreads,
1454
+ topChannels,
1455
+ reactionCount
1456
+ };
1457
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
1458
+ return { events, signals };
1459
+ }
1460
+ function formatSlackSignalsForPrompt(signals) {
1461
+ if (signals.totalMessages === 0) return "";
1462
+ const lines = [
1463
+ "## Slack Activity (external coordination)",
1464
+ "",
1465
+ `${signals.totalMessages} messages across ${signals.activeChannels} channels.`,
1466
+ `${signals.uniqueParticipants} unique participants.`
1467
+ ];
1468
+ if (signals.avgResponseMinutes !== null) {
1469
+ lines.push(`Average thread response time: ${signals.avgResponseMinutes} minutes.`);
1470
+ }
1471
+ if (signals.unresolvedThreads > 0) {
1472
+ lines.push(`${signals.unresolvedThreads} questions/threads with no reply.`);
1473
+ }
1474
+ if (signals.reactionCount > 0) {
1475
+ lines.push(`${signals.reactionCount} reactions (engagement signal).`);
1476
+ }
1477
+ if (signals.topChannels.length > 0) {
1478
+ lines.push(`Most active channels: ${signals.topChannels.join(", ")}.`);
1479
+ }
1480
+ lines.push("");
1481
+ lines.push("Slack carries external coordination \u2014 partner and client communication.");
1482
+ lines.push("Compare partner engagement against internal activity. Where partners are");
1483
+ lines.push("active but internal follow-through is low, name the gap.");
1484
+ return lines.join("\n");
1485
+ }
1486
+ function mapSlackUser(userId) {
1487
+ return {
1488
+ id: userId,
1489
+ kind: "human",
1490
+ name: userId
1491
+ };
1492
+ }
1493
+ async function fetchSlackAPI(url, headers) {
1494
+ const res = await fetch(url, { headers });
1495
+ if (!res.ok) {
1496
+ throw new Error(`Slack API error ${res.status}: ${(await res.text()).slice(0, 300)}`);
1497
+ }
1498
+ const data = await res.json();
1499
+ if (!data.ok) {
1500
+ throw new Error(`Slack API error: ${data.error ?? "unknown"}`);
1501
+ }
1502
+ return data;
1503
+ }
1504
+
1505
+ // src/radiant/adapters/notion.ts
1506
+ async function fetchNotionActivity(token, options = {}) {
1507
+ const windowDays = options.windowDays ?? 14;
1508
+ const maxPages = options.maxPages ?? 100;
1509
+ const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
1510
+ const headers = {
1511
+ Authorization: `Bearer ${token}`,
1512
+ "Notion-Version": "2022-06-28",
1513
+ "Content-Type": "application/json"
1514
+ };
1515
+ const searchResponse = await fetchNotionAPI("https://api.notion.com/v1/search", headers, {
1516
+ method: "POST",
1517
+ body: JSON.stringify({
1518
+ filter: { property: "object", value: "page" },
1519
+ sort: { direction: "descending", timestamp: "last_edited_time" },
1520
+ page_size: maxPages
1521
+ })
1522
+ });
1523
+ const pages = searchResponse.results ?? [];
1524
+ const events = [];
1525
+ const editors = /* @__PURE__ */ new Set();
1526
+ let pagesCreated = 0;
1527
+ let pagesUpdated = 0;
1528
+ let stalePages = 0;
1529
+ const editAges = [];
1530
+ const topPages = [];
1531
+ const now = Date.now();
1532
+ for (const page of pages) {
1533
+ const lastEdited = new Date(page.last_edited_time);
1534
+ const created = new Date(page.created_time);
1535
+ const daysSinceEdit = (now - lastEdited.getTime()) / (24 * 60 * 60 * 1e3);
1536
+ editAges.push(daysSinceEdit);
1537
+ if (daysSinceEdit > 30) stalePages++;
1538
+ const title = extractTitle(page);
1539
+ const editorId = page.last_edited_by?.id ?? "unknown";
1540
+ editors.add(editorId);
1541
+ if (lastEdited >= since) {
1542
+ const isNew = created >= since;
1543
+ if (isNew) pagesCreated++;
1544
+ else pagesUpdated++;
1545
+ topPages.push({ title, editedAt: page.last_edited_time });
1546
+ events.push({
1547
+ id: `notion-${page.id}`,
1548
+ timestamp: page.last_edited_time,
1549
+ actor: {
1550
+ id: editorId,
1551
+ kind: "human",
1552
+ name: editorId
1553
+ },
1554
+ kind: isNew ? "doc_created" : "doc_updated",
1555
+ content: `${isNew ? "Created" : "Updated"}: ${title}`,
1556
+ metadata: {
1557
+ pageId: page.id,
1558
+ url: page.url,
1559
+ createdAt: page.created_time
1560
+ }
1561
+ });
1562
+ }
1563
+ }
1564
+ const avgDaysSinceEdit = editAges.length > 0 ? Math.round(editAges.reduce((a, b) => a + b, 0) / editAges.length) : null;
1565
+ const signals = {
1566
+ pagesActive: pagesCreated + pagesUpdated,
1567
+ pagesCreated,
1568
+ pagesUpdated,
1569
+ uniqueEditors: editors.size,
1570
+ stalePages,
1571
+ avgDaysSinceEdit,
1572
+ topPages: topPages.slice(0, 5).map((p) => p.title)
1573
+ };
1574
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
1575
+ return { events, signals };
1576
+ }
1577
+ function formatNotionSignalsForPrompt(signals) {
1578
+ if (signals.pagesActive === 0 && signals.stalePages === 0) return "";
1579
+ const lines = [
1580
+ "## Notion Activity (documentation behavior)",
1581
+ "",
1582
+ `${signals.pagesActive} pages active in window (${signals.pagesCreated} created, ${signals.pagesUpdated} updated).`,
1583
+ `${signals.uniqueEditors} unique editors.`
1584
+ ];
1585
+ if (signals.stalePages > 0) {
1586
+ lines.push(`${signals.stalePages} pages haven't been touched in 30+ days.`);
1587
+ }
1588
+ if (signals.avgDaysSinceEdit !== null) {
1589
+ lines.push(`Average page age since last edit: ${signals.avgDaysSinceEdit} days.`);
1590
+ }
1591
+ if (signals.topPages.length > 0) {
1592
+ lines.push(`Recently active pages: ${signals.topPages.join(", ")}.`);
1593
+ }
1594
+ lines.push("");
1595
+ lines.push("Documentation is how the team crystallizes and shares knowledge.");
1596
+ lines.push("High code velocity + low documentation = building without recording.");
1597
+ lines.push("High documentation + low code = planning without shipping.");
1598
+ lines.push("Compare Notion activity against GitHub and Discord to find the balance.");
1599
+ return lines.join("\n");
1600
+ }
1601
+ function extractTitle(page) {
1602
+ for (const prop of Object.values(page.properties)) {
1603
+ if (prop.type === "title" && prop.title) {
1604
+ return prop.title.map((t) => t.plain_text).join("") || "Untitled";
1605
+ }
1606
+ }
1607
+ return "Untitled";
1608
+ }
1609
+ async function fetchNotionAPI(url, headers, init) {
1610
+ const res = await fetch(url, {
1611
+ method: init?.method ?? "GET",
1612
+ headers,
1613
+ body: init?.body
1614
+ });
1615
+ if (!res.ok) {
1616
+ throw new Error(
1617
+ `Notion API error ${res.status}: ${(await res.text()).slice(0, 300)}`
1618
+ );
1619
+ }
1620
+ return await res.json();
1621
+ }
1622
+
1132
1623
  // src/radiant/core/patterns.ts
1133
1624
  async function interpretPatterns(input) {
1134
1625
  const prompt = buildInterpretationPrompt(input);
@@ -1200,6 +1691,14 @@ ${jargonTable}
1200
1691
 
1201
1692
  For example: don't say "update the worldmodel." Say "add a line to your strategy file."
1202
1693
 
1694
+ ## When the same invariant keeps firing
1695
+
1696
+ If the prior read history or the current evidence shows the same worldmodel invariant being triggered repeatedly (by the same side \u2014 human or AI), name it in MEANING and ask the real question:
1697
+
1698
+ "This invariant has been tested N times across M reads. Always on the [human/AI] side. Either the team needs alignment on WHY this rule exists \u2014 or the team is telling you something the worldmodel hasn't absorbed yet."
1699
+
1700
+ Don't just say "invariant held." Say what it means that people keep pushing against the same wall.
1701
+
1203
1702
  ## Health is a valid read
1204
1703
 
1205
1704
  If the activity is healthy and aligned with the worldmodel, SAY SO. Don't fabricate problems. Over-prescription is a voice failure. Legitimate outputs include:
@@ -2655,7 +3154,7 @@ function verdictToEvent(status, intent) {
2655
3154
  async function loadWorldFromDirectory(dirPath) {
2656
3155
  const { readFile } = await import("fs/promises");
2657
3156
  const { join: join3 } = await import("path");
2658
- const { readdirSync: readdirSync2 } = await import("fs");
3157
+ const { readdirSync: readdirSync3 } = await import("fs");
2659
3158
  async function readJson(filename) {
2660
3159
  const filePath = join3(dirPath, filename);
2661
3160
  try {
@@ -2688,7 +3187,7 @@ async function loadWorldFromDirectory(dirPath) {
2688
3187
  const rules = [];
2689
3188
  try {
2690
3189
  const rulesDir = join3(dirPath, "rules");
2691
- const ruleFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".json")).sort();
3190
+ const ruleFiles = readdirSync3(rulesDir).filter((f) => f.endsWith(".json")).sort();
2692
3191
  for (const file of ruleFiles) {
2693
3192
  try {
2694
3193
  const content = await readFile(join3(rulesDir, file), "utf-8");
@@ -3090,9 +3589,21 @@ async function emergent(input) {
3090
3589
  priorReadContext = formatPriorReadsForPrompt(priorReads);
3091
3590
  }
3092
3591
  }
3093
- const events = await fetchGitHubActivity(input.scope, input.githubToken, {
3094
- windowDays
3095
- });
3592
+ let events;
3593
+ let orgRepos;
3594
+ if (input.scope.type === "org") {
3595
+ const orgResult = await fetchGitHubOrgActivity(
3596
+ input.scope,
3597
+ input.githubToken,
3598
+ { windowDays }
3599
+ );
3600
+ events = orgResult.events;
3601
+ orgRepos = orgResult.repos;
3602
+ } else {
3603
+ events = await fetchGitHubActivity(input.scope, input.githubToken, {
3604
+ windowDays
3605
+ });
3606
+ }
3096
3607
  const classified = classifyEvents(events);
3097
3608
  const signals = extractSignals(classified);
3098
3609
  const scores = computeScores(signals, input.worldmodelContent !== "");
@@ -3223,10 +3734,18 @@ var RADIANT_PACKAGE_VERSION = "0.0.0";
3223
3734
  createMockGitHubAdapter,
3224
3735
  emergent,
3225
3736
  extractSignals,
3737
+ fetchDiscordActivity,
3226
3738
  fetchGitHubActivity,
3739
+ fetchGitHubOrgActivity,
3740
+ fetchNotionActivity,
3741
+ fetchSlackActivity,
3742
+ formatDiscordSignalsForPrompt,
3227
3743
  formatExocortexForPrompt,
3744
+ formatNotionSignalsForPrompt,
3228
3745
  formatPriorReadsForPrompt,
3229
3746
  formatScope,
3747
+ formatSlackSignalsForPrompt,
3748
+ formatTeamExocorticesForPrompt,
3230
3749
  getLens,
3231
3750
  interpretPatterns,
3232
3751
  isPresent,
@@ -3235,8 +3754,10 @@ var RADIANT_PACKAGE_VERSION = "0.0.0";
3235
3754
  listLenses,
3236
3755
  loadPriorReads,
3237
3756
  parseRepoScope,
3757
+ parseScope,
3238
3758
  presenceAverage,
3239
3759
  readExocortex,
3760
+ readTeamExocortices,
3240
3761
  render,
3241
3762
  scoreComposite,
3242
3763
  scoreCyber,