role-os 2.2.0 → 2.3.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/src/hooks.mjs CHANGED
@@ -322,7 +322,7 @@ function generateSessionStartScript() {
322
322
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
323
323
  import { join } from "node:path";
324
324
 
325
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
325
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
326
326
  const cwd = input.cwd || process.cwd();
327
327
  const stateDir = join(cwd, ".claude", "hooks");
328
328
  mkdirSync(stateDir, { recursive: true });
@@ -356,7 +356,7 @@ function generatePromptSubmitScript() {
356
356
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
357
357
  import { join } from "node:path";
358
358
 
359
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
359
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
360
360
  const cwd = input.cwd || process.cwd();
361
361
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
362
362
 
@@ -389,7 +389,7 @@ function generatePreToolUseScript() {
389
389
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
390
390
  import { join } from "node:path";
391
391
 
392
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
392
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
393
393
  const cwd = input.cwd || process.cwd();
394
394
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
395
395
 
@@ -421,7 +421,7 @@ function generateSubagentStartScript() {
421
421
  import { readFileSync, existsSync } from "node:fs";
422
422
  import { join } from "node:path";
423
423
 
424
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
424
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
425
425
  const cwd = input.cwd || process.cwd();
426
426
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
427
427
 
@@ -444,7 +444,7 @@ function generateStopScript() {
444
444
  import { readFileSync, existsSync } from "node:fs";
445
445
  import { join } from "node:path";
446
446
 
447
- const input = JSON.parse(readFileSync("/dev/stdin", "utf-8").toString() || "{}");
447
+ const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
448
448
  const cwd = input.cwd || process.cwd();
449
449
  const statePath = join(cwd, ".claude", "hooks", "session-state.json");
450
450
 
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Artifact Evidence Analyzer
3
+ *
4
+ * Connects: retrieval bundle → prompt knowledge block → artifact output
5
+ *
6
+ * Deterministic chain-of-custody checks:
7
+ * - Which retrieved references appear in the artifact?
8
+ * - Which artifact claims are backed by retrieved evidence?
9
+ * - Did the artifact cite material when it should?
10
+ * - Did it invent unsupported references?
11
+ * - Did posture status affect artifact behavior?
12
+ *
13
+ * No LLM judge vibes. Just structural truth checks.
14
+ */
15
+
16
+ // ── ArtifactEvidenceReport ──────────────────────────────────────────
17
+
18
+ /**
19
+ * @typedef {Object} ArtifactEvidenceReport
20
+ * @property {string} role_id
21
+ * @property {string} task_id
22
+ * @property {string} knowledge_status - strong | weak | stale | conflicted | none
23
+ *
24
+ * @property {RetrievedReferenceCheck[]} reference_checks
25
+ * @property {number} references_available - total retrieved references
26
+ * @property {number} references_used - references found in artifact
27
+ * @property {number} references_missed - available but not used
28
+ * @property {number} unsupported_references - artifact cited something not in bundle
29
+ * @property {number} reference_use_ratio - references_used / references_available
30
+ *
31
+ * @property {PostureComplianceCheck} posture_compliance
32
+ * @property {DistinctivenessSignal[]} distinctiveness_signals
33
+ * @property {DriftViolation[]} drift_violations
34
+ *
35
+ * @property {string} verdict - pass | warn | fail
36
+ * @property {string[]} reasons
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} RetrievedReferenceCheck
41
+ * @property {string} reference - citation reference from bundle
42
+ * @property {string} chunk_id
43
+ * @property {boolean} found_in_artifact
44
+ * @property {string[]} match_locations - where in artifact it was found
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} PostureComplianceCheck
49
+ * @property {string} status - strong | weak | stale | conflicted | none
50
+ * @property {boolean} compliant
51
+ * @property {string[]} expected_signals - what should appear given posture
52
+ * @property {string[]} found_signals - what actually appeared
53
+ * @property {string[]} missing_signals - expected but absent
54
+ */
55
+
56
+ /**
57
+ * @typedef {Object} DistinctivenessSignal
58
+ * @property {string} signal - what was checked
59
+ * @property {boolean} present
60
+ * @property {string} [evidence] - where in artifact
61
+ */
62
+
63
+ /**
64
+ * @typedef {Object} DriftViolation
65
+ * @property {string} violation - what was found
66
+ * @property {string} evidence - the offending text
67
+ * @property {string} rule - which anti-goal was violated
68
+ */
69
+
70
+ // ── Posture Signal Expectations ─────────────────────────────────────
71
+
72
+ const POSTURE_SIGNALS = {
73
+ strong: {
74
+ // Strong posture: artifact should reference sources and make definitive claims
75
+ expected: ["per ", "according to", "documented in", "as identified", "findings", "recommendation"],
76
+ banned: ["insufficient evidence", "unable to assess", "lack sufficient data"],
77
+ },
78
+ weak: {
79
+ expected: ["limited evidence", "partial", "insufficient", "uncertain", "preliminary"],
80
+ banned: [],
81
+ },
82
+ stale: {
83
+ expected: ["outdated", "stale", "may no longer", "as of", "dated", "older"],
84
+ banned: [],
85
+ },
86
+ conflicted: {
87
+ expected: ["disagree", "conflict", "contradict", "contested", "divergent"],
88
+ banned: ["all sources agree", "clear answer"],
89
+ // Note: "consensus" alone is too broad — "false consensus" is actually correct behavior.
90
+ // Only ban unqualified consensus claims.
91
+ },
92
+ none: {
93
+ expected: [],
94
+ banned: ["retrieved evidence", "according to retrieved", "knowledge base"],
95
+ },
96
+ };
97
+
98
+ // ── Role Distinctiveness Profiles ───────────────────────────────────
99
+
100
+ const ROLE_PROFILES = {
101
+ "product-strategist": {
102
+ expected_signals: [
103
+ "user value", "tradeoff", "scope", "adoption", "leverage",
104
+ "opportunity cost", "strategic", "outcome", "positioning",
105
+ ],
106
+ anti_signals: [
107
+ "CVE", "exploit", "injection", "threat model",
108
+ "handbook structure", "navigation pattern",
109
+ "verdict", "accept", "reject",
110
+ ],
111
+ },
112
+ "security-reviewer": {
113
+ expected_signals: [
114
+ "vulnerability", "threat", "exploit", "attack", "risk",
115
+ "mitigation", "access control", "injection", "auth",
116
+ "security", "trust boundary",
117
+ ],
118
+ anti_signals: [
119
+ "market share", "competitor", "pricing",
120
+ "user value", "adoption friction",
121
+ "handbook", "progressive disclosure",
122
+ ],
123
+ },
124
+ "competitive-analyst": {
125
+ expected_signals: [
126
+ "competitor", "market", "differentiation", "pricing",
127
+ "substitute", "switching cost", "moat", "disadvantage",
128
+ "landscape", "ecosystem",
129
+ ],
130
+ anti_signals: [
131
+ "CVE", "exploit", "injection",
132
+ "handbook", "navigation",
133
+ "verdict", "reject", "quality bar",
134
+ ],
135
+ },
136
+ "docs-architect": {
137
+ expected_signals: [
138
+ "structure", "navigation", "hierarchy", "section",
139
+ "handbook", "documentation", "getting started",
140
+ "progressive disclosure", "cross-reference",
141
+ ],
142
+ anti_signals: [
143
+ "CVE", "exploit", "threat model",
144
+ "market share", "competitor", "pricing",
145
+ "verdict", "reject",
146
+ ],
147
+ },
148
+ "critic-reviewer": {
149
+ expected_signals: [
150
+ "verdict", "accept", "reject", "quality bar",
151
+ "evidence", "contract", "completeness",
152
+ "precedent", "failure pattern", "drift",
153
+ ],
154
+ anti_signals: [
155
+ "market share", "competitor",
156
+ "handbook structure",
157
+ "exploit", "CVE",
158
+ ],
159
+ },
160
+ };
161
+
162
+ // ── Main Analyzer ───────────────────────────────────────────────────
163
+
164
+ /**
165
+ * Analyze an artifact against its retrieval bundle for evidence use.
166
+ *
167
+ * @param {Object} options
168
+ * @param {string} options.role_id
169
+ * @param {string} options.task_id
170
+ * @param {string} options.artifact_text - the role's output artifact
171
+ * @param {Object|null} options.packet_knowledge - packet.knowledge
172
+ * @param {string[]} [options.known_external_refs] - references not in bundle that are still valid
173
+ * @returns {ArtifactEvidenceReport}
174
+ */
175
+ export function analyzeArtifactEvidence({
176
+ role_id,
177
+ task_id,
178
+ artifact_text,
179
+ packet_knowledge,
180
+ known_external_refs = [],
181
+ }) {
182
+ const reasons = [];
183
+ const artifactLower = artifact_text.toLowerCase();
184
+ const status = packet_knowledge?.status ?? "none";
185
+ const bundle = packet_knowledge?.retrieval_bundle;
186
+
187
+ // ── Reference Checks ────────────────────────────────────────────
188
+ const referenceChecks = [];
189
+ let refsAvailable = 0;
190
+ let refsUsed = 0;
191
+
192
+ if (bundle?.selected) {
193
+ refsAvailable = bundle.selected.length;
194
+
195
+ for (const chunk of bundle.selected) {
196
+ const ref = chunk.citation?.reference ?? chunk.chunk_id;
197
+ const refLower = ref.toLowerCase();
198
+
199
+ // Check for exact reference, partial title match, or chunk content echo
200
+ const locations = [];
201
+ if (artifactLower.includes(refLower)) {
202
+ locations.push("exact-reference");
203
+ }
204
+ // Check for title fragment
205
+ if (chunk.title && artifactLower.includes(chunk.title.toLowerCase())) {
206
+ locations.push("title-match");
207
+ }
208
+ // Check for source ID reference
209
+ if (artifactLower.includes(chunk.source_id.toLowerCase())) {
210
+ locations.push("source-id");
211
+ }
212
+ // Check for key content phrases (first 50 chars of content)
213
+ const contentSnippet = chunk.content.slice(0, 50).toLowerCase();
214
+ if (contentSnippet.length > 20 && artifactLower.includes(contentSnippet)) {
215
+ locations.push("content-echo");
216
+ }
217
+
218
+ const found = locations.length > 0;
219
+ if (found) refsUsed++;
220
+
221
+ referenceChecks.push({
222
+ reference: ref,
223
+ chunk_id: chunk.chunk_id,
224
+ found_in_artifact: found,
225
+ match_locations: locations,
226
+ });
227
+ }
228
+ }
229
+
230
+ const refsMissed = refsAvailable - refsUsed;
231
+ const refUseRatio = refsAvailable > 0 ? refsUsed / refsAvailable : 0;
232
+
233
+ // ── Unsupported Reference Detection ─────────────────────────────
234
+ // Look for citation-like patterns that don't match any retrieved reference
235
+ const citationPatterns = extractCitationPatterns(artifact_text);
236
+ const knownRefs = new Set([
237
+ ...(bundle?.selected?.map((c) => (c.citation?.reference ?? c.chunk_id).toLowerCase()) ?? []),
238
+ ...(bundle?.selected?.map((c) => c.title?.toLowerCase()).filter(Boolean) ?? []),
239
+ ...(bundle?.selected?.map((c) => c.source_id.toLowerCase()) ?? []),
240
+ ...known_external_refs.map((r) => r.toLowerCase()),
241
+ ]);
242
+
243
+ const unsupportedRefs = citationPatterns.filter(
244
+ (p) => !knownRefs.has(p.toLowerCase()) && !isGenericPhrase(p)
245
+ );
246
+
247
+ // ── Posture Compliance ──────────────────────────────────────────
248
+ const postureCompliance = checkPostureCompliance(artifactLower, status);
249
+
250
+ // ── Distinctiveness Signals ─────────────────────────────────────
251
+ const distinctivenessSignals = checkDistinctiveness(artifactLower, role_id);
252
+
253
+ // ── Drift Violations ────────────────────────────────────────────
254
+ const driftViolations = checkDrift(artifactLower, role_id);
255
+
256
+ // ── Verdict ─────────────────────────────────────────────────────
257
+ let verdict = "pass";
258
+
259
+ if (status !== "none" && refsAvailable > 0 && refUseRatio < 0.1) {
260
+ verdict = "fail";
261
+ reasons.push(`Retrieved evidence mostly unused (${refsUsed}/${refsAvailable} = ${(refUseRatio * 100).toFixed(0)}%)`);
262
+ }
263
+
264
+ if (unsupportedRefs.length > 2) {
265
+ verdict = verdict === "fail" ? "fail" : "warn";
266
+ reasons.push(`${unsupportedRefs.length} unsupported references in artifact`);
267
+ }
268
+
269
+ if (!postureCompliance.compliant) {
270
+ verdict = verdict === "fail" ? "fail" : "warn";
271
+ reasons.push(`Posture compliance failed: missing ${postureCompliance.missing_signals.join(", ")}`);
272
+ }
273
+
274
+ if (driftViolations.length > 0) {
275
+ verdict = "fail";
276
+ reasons.push(`${driftViolations.length} drift violation(s): ${driftViolations.map((d) => d.violation).join(", ")}`);
277
+ }
278
+
279
+ const expectedSignalsHit = distinctivenessSignals.filter((s) => s.present).length;
280
+ const totalExpected = distinctivenessSignals.length;
281
+ if (totalExpected > 0 && expectedSignalsHit / totalExpected < 0.3) {
282
+ verdict = verdict === "fail" ? "fail" : "warn";
283
+ reasons.push(`Low distinctiveness: only ${expectedSignalsHit}/${totalExpected} expected signals found`);
284
+ }
285
+
286
+ if (verdict === "pass") {
287
+ reasons.push("Evidence chain intact, posture compliant, role-distinct");
288
+ }
289
+
290
+ return {
291
+ role_id,
292
+ task_id,
293
+ knowledge_status: status,
294
+ reference_checks: referenceChecks,
295
+ references_available: refsAvailable,
296
+ references_used: refsUsed,
297
+ references_missed: refsMissed,
298
+ unsupported_references: unsupportedRefs.length,
299
+ reference_use_ratio: refUseRatio,
300
+ posture_compliance: postureCompliance,
301
+ distinctiveness_signals: distinctivenessSignals,
302
+ drift_violations: driftViolations,
303
+ verdict,
304
+ reasons,
305
+ };
306
+ }
307
+
308
+ // ── Posture Compliance Checker ──────────────────────────────────────
309
+
310
+ function checkPostureCompliance(artifactLower, status) {
311
+ const profile = POSTURE_SIGNALS[status];
312
+ if (!profile) {
313
+ return { status, compliant: true, expected_signals: [], found_signals: [], missing_signals: [] };
314
+ }
315
+
316
+ const found = [];
317
+ const missing = [];
318
+
319
+ for (const signal of profile.expected) {
320
+ if (artifactLower.includes(signal.toLowerCase())) {
321
+ found.push(signal);
322
+ } else {
323
+ missing.push(signal);
324
+ }
325
+ }
326
+
327
+ // For posture compliance, finding at least one expected signal is enough
328
+ // (except "none" which has no expected signals)
329
+ const compliant = profile.expected.length === 0 || found.length > 0;
330
+
331
+ // Check banned phrases
332
+ const bannedFound = [];
333
+ for (const banned of profile.banned) {
334
+ if (artifactLower.includes(banned.toLowerCase())) {
335
+ bannedFound.push(banned);
336
+ }
337
+ }
338
+
339
+ return {
340
+ status,
341
+ compliant: compliant && bannedFound.length === 0,
342
+ expected_signals: profile.expected,
343
+ found_signals: found,
344
+ missing_signals: missing.length > 0 && found.length === 0 ? missing : [],
345
+ banned_violations: bannedFound,
346
+ };
347
+ }
348
+
349
+ // ── Distinctiveness Checker ─────────────────────────────────────────
350
+
351
+ function checkDistinctiveness(artifactLower, roleId) {
352
+ const profile = ROLE_PROFILES[roleId];
353
+ if (!profile) return [];
354
+
355
+ return profile.expected_signals.map((signal) => ({
356
+ signal,
357
+ present: artifactLower.includes(signal.toLowerCase()),
358
+ evidence: artifactLower.includes(signal.toLowerCase())
359
+ ? `Contains "${signal}"`
360
+ : undefined,
361
+ }));
362
+ }
363
+
364
+ // ── Drift Checker ───────────────────────────────────────────────────
365
+
366
+ function checkDrift(artifactLower, roleId) {
367
+ const profile = ROLE_PROFILES[roleId];
368
+ if (!profile) return [];
369
+
370
+ const violations = [];
371
+ for (const antiSignal of profile.anti_signals) {
372
+ if (artifactLower.includes(antiSignal.toLowerCase())) {
373
+ violations.push({
374
+ violation: `Contains "${antiSignal}" — outside role mandate`,
375
+ evidence: antiSignal,
376
+ rule: `anti_signal: ${antiSignal}`,
377
+ });
378
+ }
379
+ }
380
+
381
+ return violations;
382
+ }
383
+
384
+ // ── Helpers ─────────────────────────────────────────────────────────
385
+
386
+ /**
387
+ * Extract citation-like patterns from artifact text.
388
+ * Looks for: [brackets], "Source: ...", "Reference: ...", "per ...", "according to ..."
389
+ */
390
+ function extractCitationPatterns(text) {
391
+ const patterns = [];
392
+
393
+ // Bracketed references: [Something]
394
+ const bracketMatches = text.match(/\[([^\]]{5,80})\]/g) ?? [];
395
+ for (const m of bracketMatches) {
396
+ patterns.push(m.slice(1, -1));
397
+ }
398
+
399
+ // "Source: X" or "Reference: X"
400
+ const sourceMatches = text.match(/(?:source|reference|per|according to):?\s+([^\n.]{5,80})/gi) ?? [];
401
+ for (const m of sourceMatches) {
402
+ const cleaned = m.replace(/^(?:source|reference|per|according to):?\s+/i, "").trim();
403
+ if (cleaned) patterns.push(cleaned);
404
+ }
405
+
406
+ return patterns;
407
+ }
408
+
409
+ /**
410
+ * Filter out generic phrases that look like citations but aren't.
411
+ */
412
+ function isGenericPhrase(text) {
413
+ const generics = [
414
+ "see above", "see below", "as noted", "as mentioned", "emphasis added",
415
+ "bold mine", "note", "important", "warning", "example",
416
+ "preferred", "authoritative", "fresh", "stale", "aging",
417
+ ];
418
+ const lower = text.toLowerCase();
419
+ return generics.some((g) => lower.includes(g)) || text.length < 8;
420
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Attach retrieval provenance to Role OS evidence items.
3
+ *
4
+ * When a role cites retrieved knowledge in its output, this wires
5
+ * the citation back to the retrieval bundle for full traceability.
6
+ */
7
+
8
+ /**
9
+ * Create an evidence provenance record from a retrieved chunk.
10
+ *
11
+ * @param {Object} chunk - A RetrievedChunk from the bundle
12
+ * @returns {Object} EvidenceProvenance
13
+ */
14
+ export function provenanceFromChunk(chunk) {
15
+ return {
16
+ source_id: chunk.source_id,
17
+ document_id: chunk.document_id,
18
+ chunk_id: chunk.chunk_id,
19
+ trust_tier: chunk.metadata?.trust_tier ?? "general",
20
+ freshness_status: chunk.metadata?.freshness?.status ?? "undated",
21
+ citation_reference: chunk.citation?.reference ?? `chunk:${chunk.chunk_id}`,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Attach provenance to an existing evidence item.
27
+ *
28
+ * @param {Object} evidenceItem - Role OS evidence entry (mutable)
29
+ * @param {Object} chunk - The RetrievedChunk that backs this evidence
30
+ * @returns {Object} The mutated evidence item
31
+ */
32
+ export function attachProvenance(evidenceItem, chunk) {
33
+ evidenceItem.provenance = provenanceFromChunk(chunk);
34
+ return evidenceItem;
35
+ }
36
+
37
+ /**
38
+ * Bulk-attach provenance to evidence items that match retrieved chunks by reference.
39
+ *
40
+ * @param {Object[]} evidenceItems - Array of evidence entries
41
+ * @param {Object} bundle - RetrievalBundle
42
+ * @returns {Object[]} Updated evidence items (mutated in place)
43
+ */
44
+ export function reconcileEvidenceWithBundle(evidenceItems, bundle) {
45
+ if (!bundle?.selected?.length) return evidenceItems;
46
+
47
+ // Build lookup by citation reference
48
+ const chunkByRef = new Map();
49
+ for (const chunk of bundle.selected) {
50
+ if (chunk.citation?.reference) {
51
+ chunkByRef.set(chunk.citation.reference, chunk);
52
+ }
53
+ }
54
+
55
+ for (const item of evidenceItems) {
56
+ if (item.reference && chunkByRef.has(item.reference)) {
57
+ attachProvenance(item, chunkByRef.get(item.reference));
58
+ }
59
+ }
60
+
61
+ return evidenceItems;
62
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Attach a retrieval bundle to a Role OS packet.
3
+ *
4
+ * Adds packet.knowledge with the bundle and its status.
5
+ * The prompt builder consumes this — retrieval never touches prompts directly.
6
+ */
7
+
8
+ /**
9
+ * Attach retrieval results to a packet object.
10
+ *
11
+ * @param {Object} packet - Role OS packet (mutable)
12
+ * @param {Object} bundle - RetrievalBundle from retrieveForDispatch
13
+ * @param {string} status - Knowledge status: strong | weak | stale | conflicted | none
14
+ * @returns {Object} The mutated packet (for chaining)
15
+ */
16
+ export function attachBundleToPacket(packet, bundle, status) {
17
+ packet.knowledge = {
18
+ retrieval_bundle: bundle,
19
+ status,
20
+ };
21
+ return packet;
22
+ }
23
+
24
+ /**
25
+ * Check if a packet has knowledge attached.
26
+ *
27
+ * @param {Object} packet
28
+ * @returns {boolean}
29
+ */
30
+ export function hasKnowledge(packet) {
31
+ return packet.knowledge != null && packet.knowledge.status !== "none";
32
+ }
33
+
34
+ /**
35
+ * Get the knowledge status from a packet.
36
+ *
37
+ * @param {Object} packet
38
+ * @returns {string} One of: strong | weak | stale | conflicted | none
39
+ */
40
+ export function getKnowledgeStatus(packet) {
41
+ return packet.knowledge?.status ?? "none";
42
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Fallback policy for degraded retrieval scenarios.
3
+ *
4
+ * Explicit governance — no silent garbage.
5
+ * Every degraded state has a named action and a message.
6
+ */
7
+
8
+ /**
9
+ * Evaluate the fallback state for a retrieval bundle.
10
+ *
11
+ * @param {Object} bundle - RetrievalBundle
12
+ * @param {Object|null} overlay - RoleOverlay or null
13
+ * @returns {{ state: string, action: string, message: string }}
14
+ */
15
+ export function applyFallbackPolicy(bundle, overlay) {
16
+ // No overlay → shared corpus only
17
+ if (!overlay) {
18
+ return {
19
+ state: "no_overlay",
20
+ action: "continue",
21
+ message: `No overlay for role ${bundle.role_id} — using shared corpus only`,
22
+ };
23
+ }
24
+
25
+ // No selected chunks at all
26
+ if (bundle.selected.length === 0) {
27
+ return {
28
+ state: "no_strong_match",
29
+ action: "warn",
30
+ message: "No chunks selected — bundle is empty",
31
+ };
32
+ }
33
+
34
+ // Check for forbidden source hits
35
+ if (bundle.summary.forbidden_hits > 0) {
36
+ // Forbidden sources were removed, but log the diagnostic
37
+ return {
38
+ state: "forbidden_hit",
39
+ action: "continue",
40
+ message: `${bundle.summary.forbidden_hits} forbidden source(s) removed from results`,
41
+ };
42
+ }
43
+
44
+ // Check for stale-dominant results
45
+ const totalRelevant = bundle.summary.selected_count + bundle.summary.stale_count;
46
+ if (totalRelevant > 0 && bundle.summary.stale_count / totalRelevant > 0.5) {
47
+ return {
48
+ state: "stale_dominant",
49
+ action: "warn",
50
+ message: `${bundle.summary.stale_count} of ${totalRelevant} relevant candidates are stale`,
51
+ };
52
+ }
53
+
54
+ // Check for conflicting evidence
55
+ const conflictWarning = bundle.warnings?.find((w) => w.code === "CONFLICTING_EVIDENCE");
56
+ if (conflictWarning) {
57
+ return {
58
+ state: "conflicting",
59
+ action: "warn",
60
+ message: conflictWarning.message,
61
+ };
62
+ }
63
+
64
+ // Check for weak trust posture
65
+ if (bundle.provenance.trust_posture === "weak") {
66
+ return {
67
+ state: "no_strong_match",
68
+ action: "warn",
69
+ message: "All selected chunks are low-trust — bundle marked weak",
70
+ };
71
+ }
72
+
73
+ // Healthy
74
+ return {
75
+ state: "healthy",
76
+ action: "continue",
77
+ message: "Retrieval healthy",
78
+ };
79
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Role OS Knowledge Integration Layer
3
+ *
4
+ * Wires knowledge-core retrieval into Role OS dispatch, packet, and evidence systems.
5
+ * Phase 2: live retrieval via configureKnowledge().
6
+ */
7
+
8
+ export { resolveOverlay, hasOverlay } from "./resolve-overlay.mjs";
9
+ export { retrieveForDispatch, configureKnowledge, isKnowledgeConfigured } from "./retrieve-for-dispatch.mjs";
10
+ export { attachBundleToPacket, hasKnowledge, getKnowledgeStatus } from "./attach-bundle-to-packet.mjs";
11
+ export { provenanceFromChunk, attachProvenance, reconcileEvidenceWithBundle } from "./attach-bundle-to-evidence.mjs";
12
+ export { applyFallbackPolicy } from "./fallback-policy.mjs";
13
+ export { renderKnowledgeBlock, knowledgeManifestSummary } from "./render-knowledge-block.mjs";
14
+ export { analyzeArtifactEvidence } from "./analyze-artifact-evidence.mjs";