@neuroverseos/governance 0.9.0 → 0.10.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.
- package/README.md +1 -1
- package/dist/{chunk-3ZWU7C43.js → chunk-BZYQHJDM.js} +469 -9
- package/dist/cli/neuroverse.cjs +777 -20
- package/dist/cli/radiant.cjs +772 -13
- package/dist/cli/radiant.d.cts +44 -1
- package/dist/cli/radiant.d.ts +44 -1
- package/dist/cli/radiant.js +294 -6
- package/dist/radiant/index.cjs +474 -9
- package/dist/radiant/index.d.cts +160 -10
- package/dist/radiant/index.d.ts +160 -10
- package/dist/radiant/index.js +11 -1
- package/dist/{server-JKUBUK5H.js → server-EI5JCIBU.js} +1 -1
- package/examples/radiant-weekly-workflow.yml +4 -1
- package/package.json +1 -1
package/dist/radiant/index.cjs
CHANGED
|
@@ -51,15 +51,19 @@ __export(radiant_exports, {
|
|
|
51
51
|
detectOrgExtendsSpec: () => detectOrgExtendsSpec,
|
|
52
52
|
discoverWorlds: () => discoverWorlds,
|
|
53
53
|
emergent: () => emergent,
|
|
54
|
+
extractDeclaredVocabulary: () => extractDeclaredVocabulary,
|
|
54
55
|
extractSignals: () => extractSignals,
|
|
55
56
|
fetchDiscordActivity: () => fetchDiscordActivity,
|
|
56
57
|
fetchGitHubActivity: () => fetchGitHubActivity,
|
|
57
58
|
fetchGitHubOrgActivity: () => fetchGitHubOrgActivity,
|
|
59
|
+
fetchLinearActivity: () => fetchLinearActivity,
|
|
58
60
|
fetchNotionActivity: () => fetchNotionActivity,
|
|
59
61
|
fetchSlackActivity: () => fetchSlackActivity,
|
|
62
|
+
filterEventsByUser: () => filterEventsByUser,
|
|
60
63
|
formatActiveWorlds: () => formatActiveWorlds,
|
|
61
64
|
formatDiscordSignalsForPrompt: () => formatDiscordSignalsForPrompt,
|
|
62
65
|
formatExocortexForPrompt: () => formatExocortexForPrompt,
|
|
66
|
+
formatLinearSignalsForPrompt: () => formatLinearSignalsForPrompt,
|
|
63
67
|
formatNotionSignalsForPrompt: () => formatNotionSignalsForPrompt,
|
|
64
68
|
formatPriorReadsForPrompt: () => formatPriorReadsForPrompt,
|
|
65
69
|
formatScope: () => formatScope,
|
|
@@ -75,6 +79,7 @@ __export(radiant_exports, {
|
|
|
75
79
|
listLenses: () => listLenses,
|
|
76
80
|
loadExtendsConfig: () => loadExtendsConfig,
|
|
77
81
|
loadPriorReads: () => loadPriorReads,
|
|
82
|
+
matchDeclaredPattern: () => matchDeclaredPattern,
|
|
78
83
|
parseExtendsSpec: () => parseExtendsSpec,
|
|
79
84
|
parseRemoteUrl: () => parseRemoteUrl,
|
|
80
85
|
parseRepoScope: () => parseRepoScope,
|
|
@@ -2040,11 +2045,398 @@ async function fetchNotionAPI(url, headers, init) {
|
|
|
2040
2045
|
return await res.json();
|
|
2041
2046
|
}
|
|
2042
2047
|
|
|
2048
|
+
// src/radiant/adapters/linear.ts
|
|
2049
|
+
async function fetchLinearActivity(apiKey, options = {}) {
|
|
2050
|
+
const windowDays = options.windowDays ?? 14;
|
|
2051
|
+
const maxIssues = options.maxIssues ?? 200;
|
|
2052
|
+
const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
|
|
2053
|
+
const sinceIso = since.toISOString();
|
|
2054
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2055
|
+
const teamFilter = options.teamIds && options.teamIds.length > 0 ? `team: { id: { in: [${options.teamIds.map((t) => JSON.stringify(t)).join(", ")}] } }` : "";
|
|
2056
|
+
const issuesQuery = `
|
|
2057
|
+
query RadiantIssues($since: DateTimeOrDuration!, $first: Int!) {
|
|
2058
|
+
issues(
|
|
2059
|
+
filter: {
|
|
2060
|
+
updatedAt: { gte: $since }
|
|
2061
|
+
${teamFilter}
|
|
2062
|
+
}
|
|
2063
|
+
first: $first
|
|
2064
|
+
orderBy: updatedAt
|
|
2065
|
+
) {
|
|
2066
|
+
nodes {
|
|
2067
|
+
id
|
|
2068
|
+
identifier
|
|
2069
|
+
title
|
|
2070
|
+
url
|
|
2071
|
+
createdAt
|
|
2072
|
+
updatedAt
|
|
2073
|
+
completedAt
|
|
2074
|
+
canceledAt
|
|
2075
|
+
state { name type }
|
|
2076
|
+
assignee { id name email }
|
|
2077
|
+
creator { id name }
|
|
2078
|
+
team { id name }
|
|
2079
|
+
project { id name }
|
|
2080
|
+
cycle { id number startsAt endsAt }
|
|
2081
|
+
comments(first: 20) {
|
|
2082
|
+
nodes { id body createdAt user { id name } }
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
`;
|
|
2088
|
+
const cyclesQuery = `
|
|
2089
|
+
query RadiantCycles($since: DateTimeOrDuration!, $now: DateTimeOrDuration!) {
|
|
2090
|
+
cycles(
|
|
2091
|
+
filter: { endsAt: { gte: $since, lte: $now } }
|
|
2092
|
+
first: 20
|
|
2093
|
+
) {
|
|
2094
|
+
nodes {
|
|
2095
|
+
id
|
|
2096
|
+
number
|
|
2097
|
+
startsAt
|
|
2098
|
+
endsAt
|
|
2099
|
+
issueCountHistory
|
|
2100
|
+
completedIssueCountHistory
|
|
2101
|
+
team { id name }
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
`;
|
|
2106
|
+
const [issuesResponse, cyclesResponse] = await Promise.all([
|
|
2107
|
+
fetchLinearGraphQL(apiKey, issuesQuery, {
|
|
2108
|
+
since: sinceIso,
|
|
2109
|
+
first: maxIssues
|
|
2110
|
+
}),
|
|
2111
|
+
fetchLinearGraphQL(apiKey, cyclesQuery, {
|
|
2112
|
+
since: sinceIso,
|
|
2113
|
+
now: nowIso
|
|
2114
|
+
})
|
|
2115
|
+
]);
|
|
2116
|
+
const rawIssues = issuesResponse.issues?.nodes ?? [];
|
|
2117
|
+
const rawCycles = cyclesResponse.cycles?.nodes ?? [];
|
|
2118
|
+
const events = [];
|
|
2119
|
+
const assignees = /* @__PURE__ */ new Set();
|
|
2120
|
+
const projects = /* @__PURE__ */ new Map();
|
|
2121
|
+
let issuesCreated = 0;
|
|
2122
|
+
let issuesCompleted = 0;
|
|
2123
|
+
let issuesOpen = 0;
|
|
2124
|
+
let issuesStalled = 0;
|
|
2125
|
+
let commentsTotal = 0;
|
|
2126
|
+
const now = Date.now();
|
|
2127
|
+
const stallThresholdMs = 14 * 24 * 60 * 60 * 1e3;
|
|
2128
|
+
for (const issue of rawIssues) {
|
|
2129
|
+
const created = new Date(issue.createdAt);
|
|
2130
|
+
const updated = new Date(issue.updatedAt);
|
|
2131
|
+
const completed = issue.completedAt ? new Date(issue.completedAt) : null;
|
|
2132
|
+
const assigneeId = issue.assignee?.id ?? "unassigned";
|
|
2133
|
+
if (assigneeId !== "unassigned") assignees.add(assigneeId);
|
|
2134
|
+
if (issue.project) {
|
|
2135
|
+
projects.set(issue.project.name, (projects.get(issue.project.name) ?? 0) + 1);
|
|
2136
|
+
}
|
|
2137
|
+
const actor = {
|
|
2138
|
+
id: assigneeId,
|
|
2139
|
+
kind: "human",
|
|
2140
|
+
name: issue.assignee?.name ?? "unassigned"
|
|
2141
|
+
};
|
|
2142
|
+
if (created >= since) {
|
|
2143
|
+
issuesCreated++;
|
|
2144
|
+
events.push({
|
|
2145
|
+
id: `linear-created-${issue.id}`,
|
|
2146
|
+
timestamp: issue.createdAt,
|
|
2147
|
+
actor: {
|
|
2148
|
+
id: issue.creator?.id ?? "unknown",
|
|
2149
|
+
kind: "human",
|
|
2150
|
+
name: issue.creator?.name ?? "unknown"
|
|
2151
|
+
},
|
|
2152
|
+
kind: "issue_created",
|
|
2153
|
+
content: `[${issue.identifier}] ${issue.title}`,
|
|
2154
|
+
metadata: {
|
|
2155
|
+
issueId: issue.id,
|
|
2156
|
+
url: issue.url,
|
|
2157
|
+
team: issue.team?.name,
|
|
2158
|
+
project: issue.project?.name,
|
|
2159
|
+
state: issue.state?.name
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
if (completed && completed >= since) {
|
|
2164
|
+
issuesCompleted++;
|
|
2165
|
+
events.push({
|
|
2166
|
+
id: `linear-completed-${issue.id}`,
|
|
2167
|
+
timestamp: issue.completedAt,
|
|
2168
|
+
actor,
|
|
2169
|
+
kind: "issue_completed",
|
|
2170
|
+
content: `[${issue.identifier}] ${issue.title}`,
|
|
2171
|
+
metadata: {
|
|
2172
|
+
issueId: issue.id,
|
|
2173
|
+
url: issue.url,
|
|
2174
|
+
team: issue.team?.name,
|
|
2175
|
+
project: issue.project?.name,
|
|
2176
|
+
cycleDays: issue.cycle?.startsAt && issue.completedAt ? Math.round(
|
|
2177
|
+
(new Date(issue.completedAt).getTime() - new Date(issue.cycle.startsAt).getTime()) / (24 * 60 * 60 * 1e3)
|
|
2178
|
+
) : null
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
if (!completed && !issue.canceledAt) {
|
|
2183
|
+
issuesOpen++;
|
|
2184
|
+
const isInProgress = issue.state?.type === "started";
|
|
2185
|
+
const idleMs = now - updated.getTime();
|
|
2186
|
+
if (isInProgress && idleMs > stallThresholdMs) issuesStalled++;
|
|
2187
|
+
}
|
|
2188
|
+
for (const comment of issue.comments?.nodes ?? []) {
|
|
2189
|
+
const commentedAt = new Date(comment.createdAt);
|
|
2190
|
+
if (commentedAt < since) continue;
|
|
2191
|
+
commentsTotal++;
|
|
2192
|
+
events.push({
|
|
2193
|
+
id: `linear-comment-${comment.id}`,
|
|
2194
|
+
timestamp: comment.createdAt,
|
|
2195
|
+
actor: {
|
|
2196
|
+
id: comment.user?.id ?? "unknown",
|
|
2197
|
+
kind: "human",
|
|
2198
|
+
name: comment.user?.name ?? "unknown"
|
|
2199
|
+
},
|
|
2200
|
+
kind: "issue_comment",
|
|
2201
|
+
content: comment.body.slice(0, 280),
|
|
2202
|
+
metadata: {
|
|
2203
|
+
issueId: issue.id,
|
|
2204
|
+
issueIdentifier: issue.identifier,
|
|
2205
|
+
url: issue.url
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
let cycleCompletionRate = null;
|
|
2211
|
+
if (rawCycles.length > 0) {
|
|
2212
|
+
const rates = [];
|
|
2213
|
+
for (const cycle of rawCycles) {
|
|
2214
|
+
const committed = cycle.issueCountHistory?.at(0) ?? 0;
|
|
2215
|
+
const completed = cycle.completedIssueCountHistory?.at(-1) ?? 0;
|
|
2216
|
+
if (committed > 0) rates.push(completed / committed);
|
|
2217
|
+
}
|
|
2218
|
+
if (rates.length > 0) {
|
|
2219
|
+
cycleCompletionRate = Math.round(rates.reduce((a, b) => a + b, 0) / rates.length * 100) / 100;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
const topProjects = [...projects.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name]) => name);
|
|
2223
|
+
const signals = {
|
|
2224
|
+
issuesCreated,
|
|
2225
|
+
issuesCompleted,
|
|
2226
|
+
issuesOpen,
|
|
2227
|
+
issuesStalled,
|
|
2228
|
+
cycleCompletionRate,
|
|
2229
|
+
uniqueAssignees: assignees.size,
|
|
2230
|
+
commentsTotal,
|
|
2231
|
+
topProjects
|
|
2232
|
+
};
|
|
2233
|
+
events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
2234
|
+
return { events, signals };
|
|
2235
|
+
}
|
|
2236
|
+
function formatLinearSignalsForPrompt(signals) {
|
|
2237
|
+
if (signals.issuesCreated === 0 && signals.issuesCompleted === 0 && signals.issuesOpen === 0) {
|
|
2238
|
+
return "";
|
|
2239
|
+
}
|
|
2240
|
+
const lines = [
|
|
2241
|
+
"## Linear Activity (planned work vs. shipped outcome)",
|
|
2242
|
+
"",
|
|
2243
|
+
`${signals.issuesCreated} issues created, ${signals.issuesCompleted} completed in window.`,
|
|
2244
|
+
`${signals.issuesOpen} issues still open.`
|
|
2245
|
+
];
|
|
2246
|
+
if (signals.issuesStalled > 0) {
|
|
2247
|
+
lines.push(
|
|
2248
|
+
`${signals.issuesStalled} in-progress issues haven't moved in 14+ days (stalled).`
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
if (signals.cycleCompletionRate !== null) {
|
|
2252
|
+
const pct = Math.round(signals.cycleCompletionRate * 100);
|
|
2253
|
+
lines.push(`Cycles ended in window completed ${pct}% of what was committed.`);
|
|
2254
|
+
}
|
|
2255
|
+
if (signals.uniqueAssignees > 0) {
|
|
2256
|
+
lines.push(`${signals.uniqueAssignees} unique assignees active.`);
|
|
2257
|
+
}
|
|
2258
|
+
if (signals.commentsTotal > 0) {
|
|
2259
|
+
lines.push(`${signals.commentsTotal} comments across issues in window.`);
|
|
2260
|
+
}
|
|
2261
|
+
if (signals.topProjects.length > 0) {
|
|
2262
|
+
lines.push(`Most active projects: ${signals.topProjects.join(", ")}.`);
|
|
2263
|
+
}
|
|
2264
|
+
lines.push("");
|
|
2265
|
+
lines.push("Linear is where the team states what it will build.");
|
|
2266
|
+
lines.push("GitHub is where the team reveals what actually got built.");
|
|
2267
|
+
lines.push("Low completion rate + high creation rate = planning faster than shipping.");
|
|
2268
|
+
lines.push("High stalled count = commitments made but not honored.");
|
|
2269
|
+
lines.push("Compare Linear signals against GitHub to find the stated-vs-shipped gap.");
|
|
2270
|
+
return lines.join("\n");
|
|
2271
|
+
}
|
|
2272
|
+
async function fetchLinearGraphQL(apiKey, query, variables) {
|
|
2273
|
+
const res = await fetch("https://api.linear.app/graphql", {
|
|
2274
|
+
method: "POST",
|
|
2275
|
+
headers: {
|
|
2276
|
+
// Linear accepts the raw API key in Authorization with no "Bearer" prefix.
|
|
2277
|
+
Authorization: apiKey,
|
|
2278
|
+
"Content-Type": "application/json"
|
|
2279
|
+
},
|
|
2280
|
+
body: JSON.stringify({ query, variables })
|
|
2281
|
+
});
|
|
2282
|
+
if (!res.ok) {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
`Linear API error ${res.status}: ${(await res.text()).slice(0, 300)}`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
const json = await res.json();
|
|
2288
|
+
if (json.errors && json.errors.length > 0) {
|
|
2289
|
+
throw new Error(
|
|
2290
|
+
`Linear GraphQL errors: ${json.errors.map((e) => e.message).join("; ")}`
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
if (!json.data) {
|
|
2294
|
+
throw new Error("Linear API returned no data");
|
|
2295
|
+
}
|
|
2296
|
+
return json.data;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// src/radiant/core/vocabulary.ts
|
|
2300
|
+
function extractDeclaredVocabulary(worldmodelContent) {
|
|
2301
|
+
const aligned = extractSection(worldmodelContent, "Aligned Behaviors").map(
|
|
2302
|
+
(b) => parseBehavior(b, "aligned")
|
|
2303
|
+
);
|
|
2304
|
+
const drift = extractSection(worldmodelContent, "Drift Behaviors").map(
|
|
2305
|
+
(b) => parseBehavior(b, "drift")
|
|
2306
|
+
);
|
|
2307
|
+
const allNames = [...aligned, ...drift].map((p) => p.name);
|
|
2308
|
+
return { aligned, drift, allNames };
|
|
2309
|
+
}
|
|
2310
|
+
function extractSection(content, header) {
|
|
2311
|
+
const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2312
|
+
const pattern = new RegExp(
|
|
2313
|
+
`##\\s+${escaped}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`,
|
|
2314
|
+
"i"
|
|
2315
|
+
);
|
|
2316
|
+
const match = content.match(pattern);
|
|
2317
|
+
if (!match) return [];
|
|
2318
|
+
const body = match[1];
|
|
2319
|
+
const bullets = body.match(/^[-*]\s+.+$/gm);
|
|
2320
|
+
if (!bullets) return [];
|
|
2321
|
+
return bullets.map((b) => b.replace(/^[-*]\s+/, "").trim()).filter((b) => b.length > 0 && !b.startsWith("<!--"));
|
|
2322
|
+
}
|
|
2323
|
+
function parseBehavior(bullet, kind) {
|
|
2324
|
+
const explicit = bullet.match(
|
|
2325
|
+
/^`?([a-z][a-z0-9_]*)`?\s+[—\u2014-]\s+(.+)$/i
|
|
2326
|
+
);
|
|
2327
|
+
if (explicit && isSnakeCase(explicit[1])) {
|
|
2328
|
+
return {
|
|
2329
|
+
name: explicit[1].toLowerCase(),
|
|
2330
|
+
prose: explicit[2].trim(),
|
|
2331
|
+
kind
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
return { name: snakeCaseName(bullet), prose: bullet, kind };
|
|
2335
|
+
}
|
|
2336
|
+
function isSnakeCase(s) {
|
|
2337
|
+
return /^[a-z][a-z0-9_]*$/.test(s);
|
|
2338
|
+
}
|
|
2339
|
+
function snakeCaseName(s) {
|
|
2340
|
+
const base = s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2341
|
+
if (base.length <= 60) return base;
|
|
2342
|
+
const truncated = base.slice(0, 60);
|
|
2343
|
+
const lastUnderscore = truncated.lastIndexOf("_");
|
|
2344
|
+
return lastUnderscore > 20 ? truncated.slice(0, lastUnderscore) : truncated;
|
|
2345
|
+
}
|
|
2346
|
+
function matchDeclaredPattern(candidateName, candidateDescription, vocabulary) {
|
|
2347
|
+
const candidateText = `${candidateName.replace(/_/g, " ")} ${candidateDescription}`;
|
|
2348
|
+
const candidateWords = contentWords(candidateText);
|
|
2349
|
+
if (candidateWords.size === 0) return null;
|
|
2350
|
+
let best = null;
|
|
2351
|
+
for (const pattern of [...vocabulary.aligned, ...vocabulary.drift]) {
|
|
2352
|
+
const proseWords = contentWords(pattern.prose);
|
|
2353
|
+
if (proseWords.size === 0) continue;
|
|
2354
|
+
let shared = 0;
|
|
2355
|
+
for (const w of proseWords) {
|
|
2356
|
+
if (candidateWords.has(w)) shared++;
|
|
2357
|
+
}
|
|
2358
|
+
const coverage = shared / proseWords.size;
|
|
2359
|
+
if (shared >= 2 && coverage >= 0.3) {
|
|
2360
|
+
if (!best || coverage > best.score) {
|
|
2361
|
+
best = { pattern, score: coverage };
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return best?.pattern ?? null;
|
|
2366
|
+
}
|
|
2367
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2368
|
+
"about",
|
|
2369
|
+
"after",
|
|
2370
|
+
"against",
|
|
2371
|
+
"among",
|
|
2372
|
+
"around",
|
|
2373
|
+
"because",
|
|
2374
|
+
"been",
|
|
2375
|
+
"before",
|
|
2376
|
+
"being",
|
|
2377
|
+
"between",
|
|
2378
|
+
"both",
|
|
2379
|
+
"could",
|
|
2380
|
+
"does",
|
|
2381
|
+
"doing",
|
|
2382
|
+
"during",
|
|
2383
|
+
"each",
|
|
2384
|
+
"from",
|
|
2385
|
+
"further",
|
|
2386
|
+
"have",
|
|
2387
|
+
"having",
|
|
2388
|
+
"into",
|
|
2389
|
+
"itself",
|
|
2390
|
+
"most",
|
|
2391
|
+
"nor",
|
|
2392
|
+
"only",
|
|
2393
|
+
"other",
|
|
2394
|
+
"over",
|
|
2395
|
+
"same",
|
|
2396
|
+
"should",
|
|
2397
|
+
"some",
|
|
2398
|
+
"such",
|
|
2399
|
+
"than",
|
|
2400
|
+
"that",
|
|
2401
|
+
"their",
|
|
2402
|
+
"them",
|
|
2403
|
+
"then",
|
|
2404
|
+
"there",
|
|
2405
|
+
"these",
|
|
2406
|
+
"they",
|
|
2407
|
+
"this",
|
|
2408
|
+
"those",
|
|
2409
|
+
"through",
|
|
2410
|
+
"under",
|
|
2411
|
+
"until",
|
|
2412
|
+
"very",
|
|
2413
|
+
"were",
|
|
2414
|
+
"what",
|
|
2415
|
+
"when",
|
|
2416
|
+
"where",
|
|
2417
|
+
"which",
|
|
2418
|
+
"while",
|
|
2419
|
+
"will",
|
|
2420
|
+
"with",
|
|
2421
|
+
"without",
|
|
2422
|
+
"would",
|
|
2423
|
+
"your",
|
|
2424
|
+
"yours"
|
|
2425
|
+
]);
|
|
2426
|
+
function contentWords(text) {
|
|
2427
|
+
const words = text.toLowerCase().match(/[a-z][a-z0-9_]+/g) ?? [];
|
|
2428
|
+
return new Set(words.filter((w) => w.length > 3 && !STOPWORDS.has(w)));
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2043
2431
|
// src/radiant/core/patterns.ts
|
|
2044
2432
|
async function interpretPatterns(input) {
|
|
2045
2433
|
const prompt = buildInterpretationPrompt(input);
|
|
2046
2434
|
const raw = await input.ai.complete(prompt, "Analyze the activity and produce the read.");
|
|
2047
|
-
const
|
|
2435
|
+
const canonicalNames = [
|
|
2436
|
+
...input.canonicalPatterns ?? [],
|
|
2437
|
+
...input.declaredVocabulary?.allNames ?? []
|
|
2438
|
+
];
|
|
2439
|
+
const parsed = parseInterpretation(raw, canonicalNames, input.declaredVocabulary);
|
|
2048
2440
|
return {
|
|
2049
2441
|
patterns: parsed.patterns,
|
|
2050
2442
|
meaning: parsed.meaning,
|
|
@@ -2055,8 +2447,10 @@ async function interpretPatterns(input) {
|
|
|
2055
2447
|
function buildInterpretationPrompt(input) {
|
|
2056
2448
|
const signalSummary = formatSignalSummary(input.signals);
|
|
2057
2449
|
const eventSample = formatEventSample(input.events, 30);
|
|
2058
|
-
const canonicalList = (
|
|
2059
|
-
|
|
2450
|
+
const canonicalList = formatDeclaredVocabulary(
|
|
2451
|
+
input.declaredVocabulary,
|
|
2452
|
+
input.canonicalPatterns ?? []
|
|
2453
|
+
);
|
|
2060
2454
|
const compressedWorld = compressWorldmodel(input.worldmodelContent);
|
|
2061
2455
|
const cl = compressLens(input.lens);
|
|
2062
2456
|
const frame = input.lens.primary_frame;
|
|
@@ -2167,6 +2561,44 @@ Only recommend a move when the evidence actually calls for one.
|
|
|
2167
2561
|
Do NOT use these phrases anywhere in your output:
|
|
2168
2562
|
${forbiddenList}`;
|
|
2169
2563
|
}
|
|
2564
|
+
function formatDeclaredVocabulary(vocabulary, extraNames) {
|
|
2565
|
+
const aligned = vocabulary?.aligned ?? [];
|
|
2566
|
+
const drift = vocabulary?.drift ?? [];
|
|
2567
|
+
if (aligned.length === 0 && drift.length === 0 && extraNames.length === 0) {
|
|
2568
|
+
return 'No patterns have been named yet. Everything you observe is new \u2014 mark it type: "candidate".';
|
|
2569
|
+
}
|
|
2570
|
+
const parts = [];
|
|
2571
|
+
parts.push("## Declared vocabulary (use these names when you see matching evidence)");
|
|
2572
|
+
parts.push("");
|
|
2573
|
+
parts.push(
|
|
2574
|
+
'The worldmodel declares the patterns below. If your observation matches one of these, use the EXACT snake_case name shown and mark type: "canonical" \u2014 do not invent a new name for something that already has one.'
|
|
2575
|
+
);
|
|
2576
|
+
parts.push("");
|
|
2577
|
+
if (aligned.length > 0) {
|
|
2578
|
+
parts.push("### Aligned behaviors (positive patterns)");
|
|
2579
|
+
for (const p of aligned) {
|
|
2580
|
+
parts.push(`- \`${p.name}\` \u2014 ${p.prose}`);
|
|
2581
|
+
}
|
|
2582
|
+
parts.push("");
|
|
2583
|
+
}
|
|
2584
|
+
if (drift.length > 0) {
|
|
2585
|
+
parts.push("### Drift behaviors (negative patterns)");
|
|
2586
|
+
for (const p of drift) {
|
|
2587
|
+
parts.push(`- \`${p.name}\` \u2014 ${p.prose}`);
|
|
2588
|
+
}
|
|
2589
|
+
parts.push("");
|
|
2590
|
+
}
|
|
2591
|
+
if (extraNames.length > 0) {
|
|
2592
|
+
parts.push(
|
|
2593
|
+
`Additional canonical names (from prior runs or caller): ${extraNames.join(", ")}`
|
|
2594
|
+
);
|
|
2595
|
+
parts.push("");
|
|
2596
|
+
}
|
|
2597
|
+
parts.push(
|
|
2598
|
+
'If you observe something genuinely new that matches NO declared pattern, mark it type: "candidate" with a freshly-invented snake_case name.'
|
|
2599
|
+
);
|
|
2600
|
+
return parts.join("\n");
|
|
2601
|
+
}
|
|
2170
2602
|
function formatSignalSummary(signals) {
|
|
2171
2603
|
const lines = [];
|
|
2172
2604
|
const domains = ["life", "cyber", "joint"];
|
|
@@ -2192,7 +2624,7 @@ function formatEventSample(events, maxEvents) {
|
|
|
2192
2624
|
"${content}"`;
|
|
2193
2625
|
}).join("\n");
|
|
2194
2626
|
}
|
|
2195
|
-
function parseInterpretation(raw, canonicalNames) {
|
|
2627
|
+
function parseInterpretation(raw, canonicalNames, vocabulary) {
|
|
2196
2628
|
let meaning = "";
|
|
2197
2629
|
let move = "";
|
|
2198
2630
|
let patternsArray = [];
|
|
@@ -2222,14 +2654,23 @@ function parseInterpretation(raw, canonicalNames) {
|
|
|
2222
2654
|
const patterns = [];
|
|
2223
2655
|
for (const item of patternsArray) {
|
|
2224
2656
|
if (!isPatternLike(item)) continue;
|
|
2225
|
-
const
|
|
2657
|
+
const rawName = String(item.name ?? "unnamed");
|
|
2658
|
+
const description = String(item.description ?? "");
|
|
2226
2659
|
const ev = item.evidence;
|
|
2227
|
-
|
|
2660
|
+
let name = rawName;
|
|
2661
|
+
let isCanonical = item.type === "canonical" || canonicalSet.has(rawName.toLowerCase());
|
|
2662
|
+
if (!isCanonical && vocabulary) {
|
|
2663
|
+
const matched = matchDeclaredPattern(rawName, description, vocabulary);
|
|
2664
|
+
if (matched) {
|
|
2665
|
+
name = matched.name;
|
|
2666
|
+
isCanonical = true;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2228
2669
|
patterns.push({
|
|
2229
|
-
name
|
|
2670
|
+
name,
|
|
2230
2671
|
type: isCanonical ? "canonical" : "candidate",
|
|
2231
|
-
declaredAs: isCanonical ?
|
|
2232
|
-
description
|
|
2672
|
+
declaredAs: isCanonical ? name : void 0,
|
|
2673
|
+
description,
|
|
2233
2674
|
evidence: {
|
|
2234
2675
|
signals: Array.isArray(ev?.signals) ? ev.signals.map(String) : [],
|
|
2235
2676
|
events: Array.isArray(ev?.events) ? ev.events.map(String) : [],
|
|
@@ -4408,10 +4849,24 @@ Compare stated intent against actual GitHub activity. Gaps = drift.`;
|
|
|
4408
4849
|
} catch {
|
|
4409
4850
|
}
|
|
4410
4851
|
}
|
|
4852
|
+
const linearKey = process.env.LINEAR_API_KEY;
|
|
4853
|
+
if (linearKey) {
|
|
4854
|
+
try {
|
|
4855
|
+
const linear = await fetchLinearActivity(linearKey, { windowDays });
|
|
4856
|
+
events.push(...linear.events);
|
|
4857
|
+
adapterSignals += "\n\n" + formatLinearSignalsForPrompt(linear.signals);
|
|
4858
|
+
activeAdapters.push("linear");
|
|
4859
|
+
} catch {
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4411
4862
|
events.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
4863
|
+
if (input.personalUser) {
|
|
4864
|
+
events = filterEventsByUser(events, input.personalUser);
|
|
4865
|
+
}
|
|
4412
4866
|
const classified = classifyEvents(events);
|
|
4413
4867
|
const signals = extractSignals(classified);
|
|
4414
4868
|
const scores = computeScores(signals, input.worldmodelContent !== "");
|
|
4869
|
+
const declaredVocabulary = extractDeclaredVocabulary(worldmodelContent);
|
|
4415
4870
|
const { patterns, meaning, move } = await interpretPatterns({
|
|
4416
4871
|
signals,
|
|
4417
4872
|
events: classified,
|
|
@@ -4419,6 +4874,7 @@ Compare stated intent against actual GitHub activity. Gaps = drift.`;
|
|
|
4419
4874
|
lens,
|
|
4420
4875
|
ai: input.ai,
|
|
4421
4876
|
canonicalPatterns: input.canonicalPatterns,
|
|
4877
|
+
declaredVocabulary,
|
|
4422
4878
|
statedIntent: [statedIntent, adapterSignals, priorReadContext].filter(Boolean).join("\n\n") || void 0
|
|
4423
4879
|
});
|
|
4424
4880
|
const rewrittenPatterns = patterns.map((p) => lens.rewrite(p));
|
|
@@ -4473,6 +4929,10 @@ Compare stated intent against actual GitHub activity. Gaps = drift.`;
|
|
|
4473
4929
|
worldStack
|
|
4474
4930
|
};
|
|
4475
4931
|
}
|
|
4932
|
+
function filterEventsByUser(events, username) {
|
|
4933
|
+
const target = username.toLowerCase();
|
|
4934
|
+
return events.filter((e) => e.actor.id.toLowerCase() === target);
|
|
4935
|
+
}
|
|
4476
4936
|
function computeScores(signals, worldmodelLoaded) {
|
|
4477
4937
|
const gate = DEFAULT_EVIDENCE_GATE;
|
|
4478
4938
|
const lifeSignals = signals.filter((s) => s.domain === "life");
|
|
@@ -4546,15 +5006,19 @@ var RADIANT_PACKAGE_VERSION = "0.0.0";
|
|
|
4546
5006
|
detectOrgExtendsSpec,
|
|
4547
5007
|
discoverWorlds,
|
|
4548
5008
|
emergent,
|
|
5009
|
+
extractDeclaredVocabulary,
|
|
4549
5010
|
extractSignals,
|
|
4550
5011
|
fetchDiscordActivity,
|
|
4551
5012
|
fetchGitHubActivity,
|
|
4552
5013
|
fetchGitHubOrgActivity,
|
|
5014
|
+
fetchLinearActivity,
|
|
4553
5015
|
fetchNotionActivity,
|
|
4554
5016
|
fetchSlackActivity,
|
|
5017
|
+
filterEventsByUser,
|
|
4555
5018
|
formatActiveWorlds,
|
|
4556
5019
|
formatDiscordSignalsForPrompt,
|
|
4557
5020
|
formatExocortexForPrompt,
|
|
5021
|
+
formatLinearSignalsForPrompt,
|
|
4558
5022
|
formatNotionSignalsForPrompt,
|
|
4559
5023
|
formatPriorReadsForPrompt,
|
|
4560
5024
|
formatScope,
|
|
@@ -4570,6 +5034,7 @@ var RADIANT_PACKAGE_VERSION = "0.0.0";
|
|
|
4570
5034
|
listLenses,
|
|
4571
5035
|
loadExtendsConfig,
|
|
4572
5036
|
loadPriorReads,
|
|
5037
|
+
matchDeclaredPattern,
|
|
4573
5038
|
parseExtendsSpec,
|
|
4574
5039
|
parseRemoteUrl,
|
|
4575
5040
|
parseRepoScope,
|