@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.
@@ -13,17 +13,21 @@ import {
13
13
  emergent,
14
14
  extractSignals,
15
15
  fetchGitHubActivity,
16
+ fetchGitHubOrgActivity,
16
17
  formatExocortexForPrompt,
17
18
  formatPriorReadsForPrompt,
18
19
  formatScope,
20
+ formatTeamExocorticesForPrompt,
19
21
  interpretPatterns,
20
22
  isPresent,
21
23
  isScored,
22
24
  isSentinel,
23
25
  loadPriorReads,
24
26
  parseRepoScope,
27
+ parseScope,
25
28
  presenceAverage,
26
29
  readExocortex,
30
+ readTeamExocortices,
27
31
  render,
28
32
  scoreComposite,
29
33
  scoreCyber,
@@ -33,7 +37,7 @@ import {
33
37
  think,
34
38
  updateKnowledge,
35
39
  writeRead
36
- } from "../chunk-T6EQ7ZBG.js";
40
+ } from "../chunk-MC6O5GV5.js";
37
41
  import {
38
42
  LENSES,
39
43
  aukiBuilderLens,
@@ -45,6 +49,400 @@ import "../chunk-ZAF6JH23.js";
45
49
  import "../chunk-QLPTHTVB.js";
46
50
  import "../chunk-QWGCMQQD.js";
47
51
 
52
+ // src/radiant/adapters/discord.ts
53
+ async function fetchDiscordActivity(guildId, token, options = {}) {
54
+ const windowDays = options.windowDays ?? 14;
55
+ const perChannel = options.perChannel ?? 100;
56
+ const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
57
+ const headers = {
58
+ Authorization: `Bot ${token}`,
59
+ "Content-Type": "application/json"
60
+ };
61
+ const channels = await fetchJSON(
62
+ `https://discord.com/api/v10/guilds/${guildId}/channels`,
63
+ headers
64
+ );
65
+ const textChannels = channels.filter((c) => {
66
+ if (c.type !== 0) return false;
67
+ if (options.channelIds && options.channelIds.length > 0) {
68
+ return options.channelIds.includes(c.id);
69
+ }
70
+ if (options.visibility === "public") {
71
+ return !c.name.startsWith("private-") && !c.nsfw;
72
+ }
73
+ return true;
74
+ });
75
+ const events = [];
76
+ let totalMessages = 0;
77
+ let helpRequests = 0;
78
+ let unresolvedThreads = 0;
79
+ let newcomerMessages = 0;
80
+ const responseTimes = [];
81
+ const participants = /* @__PURE__ */ new Set();
82
+ const knownParticipants = /* @__PURE__ */ new Set();
83
+ const topicCounts = /* @__PURE__ */ new Map();
84
+ for (const channel of textChannels.slice(0, 15)) {
85
+ try {
86
+ const messages = await fetchJSON(
87
+ `https://discord.com/api/v10/channels/${channel.id}/messages?limit=${perChannel}`,
88
+ headers
89
+ );
90
+ const inWindow = messages.filter(
91
+ (m) => new Date(m.timestamp) >= since
92
+ );
93
+ totalMessages += inWindow.length;
94
+ const topic = channel.name.replace(/-/g, " ");
95
+ topicCounts.set(topic, (topicCounts.get(topic) ?? 0) + inWindow.length);
96
+ for (const msg of inWindow) {
97
+ const actor = mapDiscordUser(msg.author);
98
+ participants.add(actor.id);
99
+ const lowerContent = msg.content.toLowerCase();
100
+ if (lowerContent.includes("help") || lowerContent.includes("stuck") || lowerContent.includes("how do i") || lowerContent.includes("anyone know")) {
101
+ helpRequests++;
102
+ }
103
+ if (msg.referenced_message) {
104
+ const refTime = new Date(msg.referenced_message.timestamp).getTime();
105
+ const msgTime = new Date(msg.timestamp).getTime();
106
+ const diffMinutes = (msgTime - refTime) / 6e4;
107
+ if (diffMinutes > 0 && diffMinutes < 10080) {
108
+ responseTimes.push(diffMinutes);
109
+ }
110
+ }
111
+ events.push({
112
+ id: `discord-${msg.id}`,
113
+ timestamp: msg.timestamp,
114
+ actor,
115
+ kind: "discord_message",
116
+ content: msg.content.slice(0, 500),
117
+ respondsTo: msg.referenced_message ? {
118
+ eventId: `discord-${msg.referenced_message.id}`,
119
+ actor: mapDiscordUser(msg.referenced_message.author)
120
+ } : void 0,
121
+ metadata: {
122
+ channel: channel.name,
123
+ guildId
124
+ }
125
+ });
126
+ }
127
+ } catch {
128
+ }
129
+ }
130
+ const avgResponseMinutes = responseTimes.length > 0 ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : null;
131
+ const topTopics = [...topicCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t]) => t);
132
+ const signals = {
133
+ totalMessages,
134
+ activeChannels: textChannels.length,
135
+ uniqueParticipants: participants.size,
136
+ avgResponseMinutes: avgResponseMinutes ? Math.round(avgResponseMinutes) : null,
137
+ helpRequests,
138
+ unresolvedThreads,
139
+ topTopics,
140
+ newcomerMessages
141
+ };
142
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
143
+ return { events, signals };
144
+ }
145
+ function formatDiscordSignalsForPrompt(signals) {
146
+ if (signals.totalMessages === 0) return "";
147
+ const lines = [
148
+ "## Discord Activity (conversational behavior)",
149
+ "",
150
+ `${signals.totalMessages} messages across ${signals.activeChannels} channels.`,
151
+ `${signals.uniqueParticipants} unique participants.`
152
+ ];
153
+ if (signals.avgResponseMinutes !== null) {
154
+ lines.push(`Average response time: ${signals.avgResponseMinutes} minutes.`);
155
+ }
156
+ if (signals.helpRequests > 0) {
157
+ lines.push(`${signals.helpRequests} help requests detected.`);
158
+ }
159
+ if (signals.unresolvedThreads > 0) {
160
+ lines.push(`${signals.unresolvedThreads} unresolved threads.`);
161
+ }
162
+ if (signals.topTopics.length > 0) {
163
+ lines.push(`Top discussion topics: ${signals.topTopics.join(", ")}.`);
164
+ }
165
+ lines.push("");
166
+ lines.push("Compare conversational activity against GitHub shipping activity.");
167
+ lines.push("Where debates happen in Discord but nothing ships in GitHub, name the gap.");
168
+ lines.push("Where work ships in GitHub but nobody discusses it in Discord, name the visibility gap.");
169
+ return lines.join("\n");
170
+ }
171
+ function mapDiscordUser(user) {
172
+ return {
173
+ id: user.username,
174
+ kind: user.bot ? "bot" : "human",
175
+ name: user.username
176
+ };
177
+ }
178
+ async function fetchJSON(url, headers) {
179
+ const res = await fetch(url, { headers });
180
+ if (!res.ok) {
181
+ if (res.status === 404 || res.status === 403) return [];
182
+ throw new Error(`Discord API error ${res.status}: ${(await res.text()).slice(0, 300)}`);
183
+ }
184
+ return await res.json();
185
+ }
186
+
187
+ // src/radiant/adapters/slack.ts
188
+ async function fetchSlackActivity(token, options = {}) {
189
+ const windowDays = options.windowDays ?? 14;
190
+ const perChannel = options.perChannel ?? 100;
191
+ const oldest = String(
192
+ Math.floor((Date.now() - windowDays * 24 * 60 * 60 * 1e3) / 1e3)
193
+ );
194
+ const headers = {
195
+ Authorization: `Bearer ${token}`,
196
+ "Content-Type": "application/json"
197
+ };
198
+ const channelsResponse = await fetchSlackAPI("https://slack.com/api/conversations.list?types=public_channel&limit=200", headers);
199
+ let channels = channelsResponse.channels ?? [];
200
+ if (options.channelIds && options.channelIds.length > 0) {
201
+ const ids = new Set(options.channelIds);
202
+ channels = channels.filter((c) => ids.has(c.id));
203
+ }
204
+ if (options.visibility === "public") {
205
+ channels = channels.filter((c) => !c.is_private && !c.is_archived);
206
+ }
207
+ const events = [];
208
+ let totalMessages = 0;
209
+ let reactionCount = 0;
210
+ let unresolvedThreads = 0;
211
+ const responseTimes = [];
212
+ const participants = /* @__PURE__ */ new Set();
213
+ const externalParticipants = /* @__PURE__ */ new Set();
214
+ const channelMessageCounts = /* @__PURE__ */ new Map();
215
+ for (const channel of channels.slice(0, 15)) {
216
+ try {
217
+ const historyResponse = await fetchSlackAPI(
218
+ `https://slack.com/api/conversations.history?channel=${channel.id}&limit=${perChannel}&oldest=${oldest}`,
219
+ headers
220
+ );
221
+ const messages = historyResponse.messages ?? [];
222
+ totalMessages += messages.length;
223
+ channelMessageCounts.set(channel.name, messages.length);
224
+ for (const msg of messages) {
225
+ if (msg.subtype === "channel_join" || msg.subtype === "channel_leave") continue;
226
+ const actor = mapSlackUser(msg.user ?? "unknown");
227
+ participants.add(actor.id);
228
+ if (msg.reactions) {
229
+ reactionCount += msg.reactions.reduce(
230
+ (sum, r) => sum + (r.count ?? 0),
231
+ 0
232
+ );
233
+ }
234
+ if (msg.thread_ts && msg.thread_ts !== msg.ts) {
235
+ const parentTs = parseFloat(msg.thread_ts) * 1e3;
236
+ const msgTs = parseFloat(msg.ts) * 1e3;
237
+ const diffMinutes = (msgTs - parentTs) / 6e4;
238
+ if (diffMinutes > 0 && diffMinutes < 10080) {
239
+ responseTimes.push(diffMinutes);
240
+ }
241
+ }
242
+ if (msg.thread_ts === msg.ts && (!msg.reply_count || msg.reply_count === 0)) {
243
+ if (msg.text && (msg.text.includes("?") || msg.text.toLowerCase().includes("help"))) {
244
+ unresolvedThreads++;
245
+ }
246
+ }
247
+ const timestamp = new Date(parseFloat(msg.ts) * 1e3).toISOString();
248
+ events.push({
249
+ id: `slack-${msg.ts}`,
250
+ timestamp,
251
+ actor,
252
+ kind: "slack_message",
253
+ content: (msg.text ?? "").slice(0, 500),
254
+ respondsTo: msg.thread_ts && msg.thread_ts !== msg.ts ? {
255
+ eventId: `slack-${msg.thread_ts}`,
256
+ actor: { id: "thread-parent", kind: "unknown" }
257
+ } : void 0,
258
+ metadata: {
259
+ channel: channel.name,
260
+ isPrivate: channel.is_private,
261
+ hasReactions: (msg.reactions?.length ?? 0) > 0
262
+ }
263
+ });
264
+ }
265
+ } catch {
266
+ }
267
+ }
268
+ const avgResponseMinutes = responseTimes.length > 0 ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : null;
269
+ const topChannels = [...channelMessageCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name]) => name);
270
+ const signals = {
271
+ totalMessages,
272
+ activeChannels: channelMessageCounts.size,
273
+ uniqueParticipants: participants.size,
274
+ avgResponseMinutes,
275
+ externalParticipants: externalParticipants.size,
276
+ unresolvedThreads,
277
+ topChannels,
278
+ reactionCount
279
+ };
280
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
281
+ return { events, signals };
282
+ }
283
+ function formatSlackSignalsForPrompt(signals) {
284
+ if (signals.totalMessages === 0) return "";
285
+ const lines = [
286
+ "## Slack Activity (external coordination)",
287
+ "",
288
+ `${signals.totalMessages} messages across ${signals.activeChannels} channels.`,
289
+ `${signals.uniqueParticipants} unique participants.`
290
+ ];
291
+ if (signals.avgResponseMinutes !== null) {
292
+ lines.push(`Average thread response time: ${signals.avgResponseMinutes} minutes.`);
293
+ }
294
+ if (signals.unresolvedThreads > 0) {
295
+ lines.push(`${signals.unresolvedThreads} questions/threads with no reply.`);
296
+ }
297
+ if (signals.reactionCount > 0) {
298
+ lines.push(`${signals.reactionCount} reactions (engagement signal).`);
299
+ }
300
+ if (signals.topChannels.length > 0) {
301
+ lines.push(`Most active channels: ${signals.topChannels.join(", ")}.`);
302
+ }
303
+ lines.push("");
304
+ lines.push("Slack carries external coordination \u2014 partner and client communication.");
305
+ lines.push("Compare partner engagement against internal activity. Where partners are");
306
+ lines.push("active but internal follow-through is low, name the gap.");
307
+ return lines.join("\n");
308
+ }
309
+ function mapSlackUser(userId) {
310
+ return {
311
+ id: userId,
312
+ kind: "human",
313
+ name: userId
314
+ };
315
+ }
316
+ async function fetchSlackAPI(url, headers) {
317
+ const res = await fetch(url, { headers });
318
+ if (!res.ok) {
319
+ throw new Error(`Slack API error ${res.status}: ${(await res.text()).slice(0, 300)}`);
320
+ }
321
+ const data = await res.json();
322
+ if (!data.ok) {
323
+ throw new Error(`Slack API error: ${data.error ?? "unknown"}`);
324
+ }
325
+ return data;
326
+ }
327
+
328
+ // src/radiant/adapters/notion.ts
329
+ async function fetchNotionActivity(token, options = {}) {
330
+ const windowDays = options.windowDays ?? 14;
331
+ const maxPages = options.maxPages ?? 100;
332
+ const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
333
+ const headers = {
334
+ Authorization: `Bearer ${token}`,
335
+ "Notion-Version": "2022-06-28",
336
+ "Content-Type": "application/json"
337
+ };
338
+ const searchResponse = await fetchNotionAPI("https://api.notion.com/v1/search", headers, {
339
+ method: "POST",
340
+ body: JSON.stringify({
341
+ filter: { property: "object", value: "page" },
342
+ sort: { direction: "descending", timestamp: "last_edited_time" },
343
+ page_size: maxPages
344
+ })
345
+ });
346
+ const pages = searchResponse.results ?? [];
347
+ const events = [];
348
+ const editors = /* @__PURE__ */ new Set();
349
+ let pagesCreated = 0;
350
+ let pagesUpdated = 0;
351
+ let stalePages = 0;
352
+ const editAges = [];
353
+ const topPages = [];
354
+ const now = Date.now();
355
+ for (const page of pages) {
356
+ const lastEdited = new Date(page.last_edited_time);
357
+ const created = new Date(page.created_time);
358
+ const daysSinceEdit = (now - lastEdited.getTime()) / (24 * 60 * 60 * 1e3);
359
+ editAges.push(daysSinceEdit);
360
+ if (daysSinceEdit > 30) stalePages++;
361
+ const title = extractTitle(page);
362
+ const editorId = page.last_edited_by?.id ?? "unknown";
363
+ editors.add(editorId);
364
+ if (lastEdited >= since) {
365
+ const isNew = created >= since;
366
+ if (isNew) pagesCreated++;
367
+ else pagesUpdated++;
368
+ topPages.push({ title, editedAt: page.last_edited_time });
369
+ events.push({
370
+ id: `notion-${page.id}`,
371
+ timestamp: page.last_edited_time,
372
+ actor: {
373
+ id: editorId,
374
+ kind: "human",
375
+ name: editorId
376
+ },
377
+ kind: isNew ? "doc_created" : "doc_updated",
378
+ content: `${isNew ? "Created" : "Updated"}: ${title}`,
379
+ metadata: {
380
+ pageId: page.id,
381
+ url: page.url,
382
+ createdAt: page.created_time
383
+ }
384
+ });
385
+ }
386
+ }
387
+ const avgDaysSinceEdit = editAges.length > 0 ? Math.round(editAges.reduce((a, b) => a + b, 0) / editAges.length) : null;
388
+ const signals = {
389
+ pagesActive: pagesCreated + pagesUpdated,
390
+ pagesCreated,
391
+ pagesUpdated,
392
+ uniqueEditors: editors.size,
393
+ stalePages,
394
+ avgDaysSinceEdit,
395
+ topPages: topPages.slice(0, 5).map((p) => p.title)
396
+ };
397
+ events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
398
+ return { events, signals };
399
+ }
400
+ function formatNotionSignalsForPrompt(signals) {
401
+ if (signals.pagesActive === 0 && signals.stalePages === 0) return "";
402
+ const lines = [
403
+ "## Notion Activity (documentation behavior)",
404
+ "",
405
+ `${signals.pagesActive} pages active in window (${signals.pagesCreated} created, ${signals.pagesUpdated} updated).`,
406
+ `${signals.uniqueEditors} unique editors.`
407
+ ];
408
+ if (signals.stalePages > 0) {
409
+ lines.push(`${signals.stalePages} pages haven't been touched in 30+ days.`);
410
+ }
411
+ if (signals.avgDaysSinceEdit !== null) {
412
+ lines.push(`Average page age since last edit: ${signals.avgDaysSinceEdit} days.`);
413
+ }
414
+ if (signals.topPages.length > 0) {
415
+ lines.push(`Recently active pages: ${signals.topPages.join(", ")}.`);
416
+ }
417
+ lines.push("");
418
+ lines.push("Documentation is how the team crystallizes and shares knowledge.");
419
+ lines.push("High code velocity + low documentation = building without recording.");
420
+ lines.push("High documentation + low code = planning without shipping.");
421
+ lines.push("Compare Notion activity against GitHub and Discord to find the balance.");
422
+ return lines.join("\n");
423
+ }
424
+ function extractTitle(page) {
425
+ for (const prop of Object.values(page.properties)) {
426
+ if (prop.type === "title" && prop.title) {
427
+ return prop.title.map((t) => t.plain_text).join("") || "Untitled";
428
+ }
429
+ }
430
+ return "Untitled";
431
+ }
432
+ async function fetchNotionAPI(url, headers, init) {
433
+ const res = await fetch(url, {
434
+ method: init?.method ?? "GET",
435
+ headers,
436
+ body: init?.body
437
+ });
438
+ if (!res.ok) {
439
+ throw new Error(
440
+ `Notion API error ${res.status}: ${(await res.text()).slice(0, 300)}`
441
+ );
442
+ }
443
+ return await res.json();
444
+ }
445
+
48
446
  // src/radiant/index.ts
49
447
  var RADIANT_PACKAGE_VERSION = "0.0.0";
50
448
  export {
@@ -64,10 +462,18 @@ export {
64
462
  createMockGitHubAdapter,
65
463
  emergent,
66
464
  extractSignals,
465
+ fetchDiscordActivity,
67
466
  fetchGitHubActivity,
467
+ fetchGitHubOrgActivity,
468
+ fetchNotionActivity,
469
+ fetchSlackActivity,
470
+ formatDiscordSignalsForPrompt,
68
471
  formatExocortexForPrompt,
472
+ formatNotionSignalsForPrompt,
69
473
  formatPriorReadsForPrompt,
70
474
  formatScope,
475
+ formatSlackSignalsForPrompt,
476
+ formatTeamExocorticesForPrompt,
71
477
  getLens,
72
478
  interpretPatterns,
73
479
  isPresent,
@@ -76,8 +482,10 @@ export {
76
482
  listLenses,
77
483
  loadPriorReads,
78
484
  parseRepoScope,
485
+ parseScope,
79
486
  presenceAverage,
80
487
  readExocortex,
488
+ readTeamExocortices,
81
489
  render,
82
490
  scoreComposite,
83
491
  scoreCyber,
@@ -3,7 +3,7 @@ import {
3
3
  emergent,
4
4
  parseRepoScope,
5
5
  think
6
- } from "./chunk-T6EQ7ZBG.js";
6
+ } from "./chunk-MC6O5GV5.js";
7
7
  import "./chunk-VGFDMPVB.js";
8
8
  import "./chunk-I4RTIMLX.js";
9
9
  import "./chunk-ZAF6JH23.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuroverseos/governance",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Deterministic governance engine for AI agents — enforce worlds (permanent rules) and plans (mission constraints) with full audit trace",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",