role-os 2.2.1 → 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.
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Knowledge Block Renderer — transforms a retrieval bundle into a prompt fragment.
3
+ *
4
+ * This is a pure function: same input → same output. No retrieval logic.
5
+ * No raw JSON dump. No ad hoc search. Just governed prompt construction.
6
+ *
7
+ * The block has 4 sections:
8
+ * 1. Knowledge posture (one line)
9
+ * 2. Retrieved evidence (top N excerpts with citations)
10
+ * 3. Warnings / constraints
11
+ * 4. Usage law (status-specific behavioral rules)
12
+ */
13
+
14
+ // ── Configuration ───────────────────────────────────────────────────
15
+
16
+ const MAX_EVIDENCE_CHUNKS = 6;
17
+ const MAX_EXCERPT_LENGTH = 200;
18
+
19
+ // ── Status-Specific Usage Law ───────────────────────────────────────
20
+
21
+ const USAGE_LAW = {
22
+ strong: [
23
+ "Prioritize retrieved evidence when making claims in your domain.",
24
+ "Cite retrieved references when making specific substantive claims.",
25
+ "Do not invent sources that were not retrieved.",
26
+ ],
27
+ weak: [
28
+ "Treat retrieved evidence as partial — it may not cover the full picture.",
29
+ "Avoid overclaiming based on limited evidence.",
30
+ "Escalate uncertainty where it affects your deliverable.",
31
+ "Do not invent sources that were not retrieved.",
32
+ ],
33
+ stale: [
34
+ "Retrieved evidence may be outdated — note possible staleness in your analysis.",
35
+ "Do not present stale evidence as current truth without qualification.",
36
+ "Flag any claims that depend on time-sensitive data.",
37
+ "Do not invent sources that were not retrieved.",
38
+ ],
39
+ conflicted: [
40
+ "Retrieved sources contain conflicting evidence.",
41
+ "Surface the conflict explicitly — do not flatten disagreement into fake consensus.",
42
+ "Acknowledge which claims are contested and by what sources.",
43
+ "Do not invent sources that were not retrieved.",
44
+ ],
45
+ none: null, // No knowledge block rendered
46
+ };
47
+
48
+ // ── Main Renderer ───────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Render a knowledge prompt block from packet.knowledge.
52
+ *
53
+ * @param {Object|null} packetKnowledge - packet.knowledge (with retrieval_bundle and status)
54
+ * @returns {string|null} Prompt block string, or null if no knowledge to render
55
+ */
56
+ export function renderKnowledgeBlock(packetKnowledge) {
57
+ if (!packetKnowledge || packetKnowledge.status === "none") {
58
+ return null;
59
+ }
60
+
61
+ const { retrieval_bundle: bundle, status } = packetKnowledge;
62
+ if (!bundle || !bundle.selected?.length) {
63
+ return null;
64
+ }
65
+
66
+ const sections = [];
67
+
68
+ // 1. Posture line
69
+ sections.push(`## Retrieved Knowledge\n\nKnowledge status: **${status}**`);
70
+
71
+ // 2. Evidence excerpts
72
+ const evidenceBlock = renderEvidenceBlock(bundle.selected);
73
+ if (evidenceBlock) {
74
+ sections.push(evidenceBlock);
75
+ }
76
+
77
+ // 3. Warnings
78
+ const warningsBlock = renderWarningsBlock(bundle.warnings);
79
+ if (warningsBlock) {
80
+ sections.push(warningsBlock);
81
+ }
82
+
83
+ // 4. Usage law
84
+ const lawBlock = renderUsageLaw(status);
85
+ if (lawBlock) {
86
+ sections.push(lawBlock);
87
+ }
88
+
89
+ return sections.join("\n\n");
90
+ }
91
+
92
+ // ── Evidence Renderer ───────────────────────────────────────────────
93
+
94
+ /**
95
+ * Render the top N evidence excerpts.
96
+ */
97
+ function renderEvidenceBlock(selected) {
98
+ if (!selected?.length) return null;
99
+
100
+ const top = selected.slice(0, MAX_EVIDENCE_CHUNKS);
101
+ const lines = ["### Evidence"];
102
+
103
+ for (let i = 0; i < top.length; i++) {
104
+ const chunk = top[i];
105
+ const trustLabel = chunk.metadata?.trust_tier ?? "general";
106
+ const freshnessLabel = chunk.metadata?.freshness?.status ?? "undated";
107
+ const citation = chunk.citation?.reference ?? chunk.chunk_id;
108
+ const excerpt = truncateExcerpt(chunk.content);
109
+
110
+ lines.push(`${i + 1}. [${trustLabel} | ${freshnessLabel}] ${citation}`);
111
+ lines.push(` "${excerpt}"`);
112
+ }
113
+
114
+ return lines.join("\n");
115
+ }
116
+
117
+ // ── Warnings Renderer ───────────────────────────────────────────────
118
+
119
+ /**
120
+ * Render retrieval warnings as constraints.
121
+ */
122
+ function renderWarningsBlock(warnings) {
123
+ if (!warnings?.length) return null;
124
+
125
+ const meaningful = warnings.filter(
126
+ (w) => w.code !== "FORBIDDEN_SOURCE_HIT" // governance working, not a user-facing warning
127
+ );
128
+
129
+ if (!meaningful.length) return null;
130
+
131
+ const lines = ["### Warnings"];
132
+ for (const warning of meaningful) {
133
+ lines.push(`- ${formatWarning(warning)}`);
134
+ }
135
+
136
+ return lines.join("\n");
137
+ }
138
+
139
+ /**
140
+ * Format a warning into human-readable text.
141
+ */
142
+ function formatWarning(warning) {
143
+ switch (warning.code) {
144
+ case "NO_HIGH_TRUST_MATCH":
145
+ return "No authoritative sources matched — evidence quality is limited.";
146
+ case "ONLY_SHARED_CORPUS":
147
+ return "No role-specific evidence found — results are from shared corpus only.";
148
+ case "STALE_DOMINANT":
149
+ return "Most retrieved evidence is outdated — treat with caution.";
150
+ case "LOW_DIVERSITY":
151
+ return "Evidence comes from a single source — limited perspective.";
152
+ case "CONFLICTING_EVIDENCE":
153
+ return `Conflicting evidence: ${warning.message}`;
154
+ default:
155
+ return warning.message;
156
+ }
157
+ }
158
+
159
+ // ── Usage Law Renderer ──────────────────────────────────────────────
160
+
161
+ /**
162
+ * Render status-specific usage law.
163
+ */
164
+ function renderUsageLaw(status) {
165
+ const laws = USAGE_LAW[status];
166
+ if (!laws) return null;
167
+
168
+ const lines = ["### Knowledge Use Rules"];
169
+ for (const law of laws) {
170
+ lines.push(`- ${law}`);
171
+ }
172
+
173
+ return lines.join("\n");
174
+ }
175
+
176
+ // ── Helpers ─────────────────────────────────────────────────────────
177
+
178
+ /**
179
+ * Truncate content to MAX_EXCERPT_LENGTH, preserving word boundaries.
180
+ */
181
+ function truncateExcerpt(content) {
182
+ if (!content) return "";
183
+ if (content.length <= MAX_EXCERPT_LENGTH) return content;
184
+
185
+ const truncated = content.slice(0, MAX_EXCERPT_LENGTH);
186
+ const lastSpace = truncated.lastIndexOf(" ");
187
+ return (lastSpace > MAX_EXCERPT_LENGTH * 0.7 ? truncated.slice(0, lastSpace) : truncated) + "...";
188
+ }
189
+
190
+ // ── Manifest Summary ────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Generate a compact knowledge summary for the dispatch manifest.
194
+ * Not full excerpts — just posture truth.
195
+ *
196
+ * @param {Object|null} packetKnowledge
197
+ * @returns {Object|null} Compact summary for manifest, or null
198
+ */
199
+ export function knowledgeManifestSummary(packetKnowledge) {
200
+ if (!packetKnowledge || packetKnowledge.status === "none") {
201
+ return null;
202
+ }
203
+
204
+ const { retrieval_bundle: bundle, status } = packetKnowledge;
205
+ if (!bundle) return null;
206
+
207
+ return {
208
+ status,
209
+ selected_count: bundle.selected?.length ?? 0,
210
+ trust_posture: bundle.provenance?.trust_posture ?? "weak",
211
+ freshness_posture: bundle.provenance?.freshness_posture ?? "stale",
212
+ warning_codes: (bundle.warnings ?? []).map((w) => w.code),
213
+ rerank_strategy: bundle.summary?.rerank_strategy ?? "unknown",
214
+ };
215
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Resolve a role overlay from knowledge/roles/*.json.
3
+ *
4
+ * Returns the overlay config for a given role, or null if no overlay exists.
5
+ * Roles without overlays fall back to shared-corpus-only retrieval.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join, resolve, dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+
14
+ // Default overlay search paths (relative to knowledge-core root)
15
+ const OVERLAY_PATHS = [
16
+ // Local knowledge-core checkout (development)
17
+ join(resolve(__dirname, "..", ".."), "knowledge-core", "knowledge", "roles"),
18
+ // Fallback: role-os local knowledge dir
19
+ join(resolve(__dirname, ".."), "knowledge", "roles"),
20
+ ];
21
+
22
+ /**
23
+ * Resolve the overlay for a role.
24
+ *
25
+ * @param {string} roleId - Role identifier (e.g. "security-reviewer")
26
+ * @param {Object} [options]
27
+ * @param {string[]} [options.searchPaths] - Override overlay search paths
28
+ * @returns {{ overlay: object, path: string } | null}
29
+ */
30
+ export function resolveOverlay(roleId, options = {}) {
31
+ const paths = options.searchPaths || OVERLAY_PATHS;
32
+
33
+ for (const dir of paths) {
34
+ const filePath = join(dir, `${roleId}.json`);
35
+ if (existsSync(filePath)) {
36
+ try {
37
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
38
+ if (data.version !== "1.0") {
39
+ console.warn(`[knowledge] Overlay ${roleId}: unsupported version ${data.version}`);
40
+ return null;
41
+ }
42
+ if (data.role_id !== roleId) {
43
+ console.warn(`[knowledge] Overlay file ${roleId}.json has mismatched role_id: ${data.role_id}`);
44
+ return null;
45
+ }
46
+ return { overlay: data, path: filePath };
47
+ } catch (e) {
48
+ console.warn(`[knowledge] Failed to parse overlay for ${roleId}:`, e.message);
49
+ return null;
50
+ }
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Check if a role has an overlay available.
59
+ *
60
+ * @param {string} roleId
61
+ * @param {Object} [options]
62
+ * @returns {boolean}
63
+ */
64
+ export function hasOverlay(roleId, options = {}) {
65
+ return resolveOverlay(roleId, options) !== null;
66
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Retrieve knowledge for a dispatch step.
3
+ *
4
+ * This is the integration seam between Role OS dispatch and knowledge-core.
5
+ * Phase 2: wired to the real retrieval pipeline.
6
+ *
7
+ * When a corpus store is available, runs the full pipeline:
8
+ * task + overlay + corpus → candidates → filter → rerank → bundle
9
+ *
10
+ * When no corpus is available, returns a governed stub (graceful degradation).
11
+ */
12
+
13
+ import { resolveOverlay } from "./resolve-overlay.mjs";
14
+ import { applyFallbackPolicy } from "./fallback-policy.mjs";
15
+
16
+ // ── Corpus Store Singleton ──────────────────────────────────────────
17
+ // Lazy-loaded. Role OS sets this via configureKnowledge().
18
+ let _store = null;
19
+ let _retrieveFn = null;
20
+
21
+ /**
22
+ * Configure the knowledge subsystem with a live corpus store.
23
+ * Call once at startup (e.g., in session init or dispatch init).
24
+ *
25
+ * @param {Object} options
26
+ * @param {Object} options.store - CorpusStore instance from knowledge-core
27
+ * @param {Function} options.retrieve - retrieve() function from knowledge-core
28
+ */
29
+ export function configureKnowledge({ store, retrieve }) {
30
+ _store = store;
31
+ _retrieveFn = retrieve;
32
+ }
33
+
34
+ /**
35
+ * Check if knowledge subsystem is configured with a live corpus.
36
+ */
37
+ export function isKnowledgeConfigured() {
38
+ return _store !== null && _retrieveFn !== null;
39
+ }
40
+
41
+ /**
42
+ * @typedef {Object} RetrieveOptions
43
+ * @property {string} roleId - Role identifier
44
+ * @property {string} taskText - Task description from packet
45
+ * @property {string} [packetContextSummary] - Optional packet context
46
+ * @property {Object} [routeSignals] - Route scoring signals
47
+ */
48
+
49
+ /**
50
+ * Retrieve knowledge for a role's dispatch step.
51
+ *
52
+ * @param {RetrieveOptions} options
53
+ * @returns {Promise<{ bundle: object, status: string, fallback: object }>}
54
+ */
55
+ export async function retrieveForDispatch({ roleId, taskText, packetContextSummary, routeSignals }) {
56
+ const overlayResult = resolveOverlay(roleId);
57
+ const overlay = overlayResult?.overlay ?? null;
58
+
59
+ // ── Live retrieval (Phase 2) ────────────────────────────────────
60
+ if (_store && _retrieveFn) {
61
+ const bundle = await _retrieveFn({
62
+ store: _store,
63
+ roleId,
64
+ taskText,
65
+ overlay,
66
+ packetContextSummary,
67
+ lexicalOnly: true, // Phase 2: lexical-only until embeddings are populated
68
+ });
69
+
70
+ const fallback = applyFallbackPolicy(bundle, overlay);
71
+
72
+ return {
73
+ bundle,
74
+ status: deriveStatus(fallback),
75
+ fallback,
76
+ };
77
+ }
78
+
79
+ // ── Stub fallback (no corpus configured) ────────────────────────
80
+ const bundle = buildStubBundle(roleId, taskText, overlayResult);
81
+ const fallback = applyFallbackPolicy(bundle, overlay);
82
+
83
+ return {
84
+ bundle,
85
+ status: deriveStatus(fallback),
86
+ fallback,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Derive packet knowledge status from fallback state.
92
+ */
93
+ function deriveStatus(fallback) {
94
+ switch (fallback.state) {
95
+ case "healthy": return "strong";
96
+ case "no_overlay": return "none";
97
+ case "no_strong_match": return "weak";
98
+ case "stale_dominant": return "stale";
99
+ case "conflicting": return "conflicted";
100
+ case "forbidden_hit": return "strong"; // forbidden sources removed = governance working
101
+ default: return "weak";
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Build a stub retrieval bundle when no corpus is available.
107
+ */
108
+ function buildStubBundle(roleId, taskText, overlayResult) {
109
+ const hasOverlayData = overlayResult !== null;
110
+
111
+ return {
112
+ version: "1.0",
113
+ role_id: roleId,
114
+ query: {
115
+ task_text: taskText,
116
+ lexical_query: [],
117
+ semantic_query: [],
118
+ applied_overlay_rules: hasOverlayData ? [`overlay:${roleId}`] : [],
119
+ applied_filters: [],
120
+ generated_at: new Date().toISOString(),
121
+ },
122
+ summary: {
123
+ total_candidates: 0,
124
+ selected_count: 0,
125
+ stale_count: 0,
126
+ forbidden_hits: 0,
127
+ trust_tier_breakdown: { authoritative: 0, preferred: 0, general: 0, untrusted: 0 },
128
+ source_breakdown: {},
129
+ rerank_strategy: "stub-no-corpus",
130
+ },
131
+ selected: [],
132
+ rejected: [],
133
+ provenance: {
134
+ source_ids: [],
135
+ document_ids: [],
136
+ trust_posture: "weak",
137
+ freshness_posture: "stale",
138
+ },
139
+ warnings: [
140
+ { code: "ONLY_SHARED_CORPUS", message: "No corpus configured — stub bundle returned" },
141
+ ],
142
+ diagnostics: {
143
+ latency_ms: 0,
144
+ candidate_pool_size: 0,
145
+ deduped_count: 0,
146
+ dropped_forbidden_count: 0,
147
+ dropped_stale_count: 0,
148
+ },
149
+ };
150
+ }
@@ -75,7 +75,10 @@ export function createRun(missionKey, taskDescription, options = {}) {
75
75
  let steps;
76
76
  const dd = mission.dynamicDispatch;
77
77
 
78
- if (dd && options.manifest) {
78
+ if (missionKey === "dogfood-swarm" && dd && options.manifest) {
79
+ // Swarm dispatch — build staged domain steps from swarm manifest
80
+ steps = buildSwarmSteps(mission, options.manifest);
81
+ } else if (dd && options.manifest) {
79
82
  // Dynamic dispatch — build steps from manifest
80
83
  steps = buildDynamicSteps(mission, options.manifest);
81
84
  } else {
@@ -105,6 +108,7 @@ export function createRun(missionKey, taskDescription, options = {}) {
105
108
  completionReport: null,
106
109
  dynamicDispatch: dd && options.manifest ? true : false,
107
110
  manifest: options.manifest || null,
111
+ knowledge: options.knowledge || null, // Phase 5: PacketKnowledge from retrieval
108
112
  };
109
113
  }
110
114
 
@@ -191,6 +195,93 @@ function buildDynamicSteps(mission, manifest) {
191
195
  return steps;
192
196
  }
193
197
 
198
+ /**
199
+ * Build steps from swarm manifest for dogfood-swarm missions.
200
+ * Creates domain agent steps per stage with coordinator gates.
201
+ * @param {Object} mission
202
+ * @param {Object} manifest - The swarm-manifest.json content
203
+ * @returns {MissionStep[]}
204
+ */
205
+ function buildSwarmSteps(mission, manifest) {
206
+ const steps = [];
207
+ const domains = manifest.domains || [];
208
+ const stages = manifest.stages || ["health-a", "health-b", "health-c", "feature"];
209
+ const waveLoops = mission.waveLoops || [];
210
+
211
+ // For each stage, create domain agent steps + coordinator gate
212
+ for (const stage of stages) {
213
+ const loopDef = waveLoops.find(w => w.stage === stage);
214
+
215
+ // One step per domain agent
216
+ for (const domain of domains) {
217
+ steps.push({
218
+ role: domain.role,
219
+ produces: "wave-report",
220
+ consumedBy: "Swarm Coordinator",
221
+ domain: domain.id,
222
+ stage,
223
+ waveIteration: 0,
224
+ patterns: domain.patterns,
225
+ status: "pending",
226
+ artifact: null,
227
+ artifactValidation: null,
228
+ note: null,
229
+ startedAt: null,
230
+ completedAt: null,
231
+ });
232
+ }
233
+
234
+ // Coordinator gate step after domain agents
235
+ steps.push({
236
+ role: "Swarm Coordinator",
237
+ produces: "swarm-gate",
238
+ consumedBy: stage === stages[stages.length - 1] ? "Swarm Synthesizer" : domains[0]?.role || null,
239
+ stage,
240
+ isGate: true,
241
+ exitCondition: loopDef?.exitCondition || null,
242
+ maxIterations: loopDef?.maxIterations || 1,
243
+ buildGate: loopDef?.buildGate ?? true,
244
+ userApproval: loopDef?.userApproval ?? false,
245
+ lens: loopDef?.lens || null,
246
+ status: "pending",
247
+ artifact: null,
248
+ artifactValidation: null,
249
+ note: null,
250
+ startedAt: null,
251
+ completedAt: null,
252
+ });
253
+ }
254
+
255
+ // Final stage: Synthesizer + Critic
256
+ steps.push({
257
+ role: "Swarm Synthesizer",
258
+ produces: "swarm-final-report",
259
+ consumedBy: "Critic Reviewer",
260
+ stage: "final",
261
+ status: "pending",
262
+ artifact: null,
263
+ artifactValidation: null,
264
+ note: null,
265
+ startedAt: null,
266
+ completedAt: null,
267
+ });
268
+
269
+ steps.push({
270
+ role: "Critic Reviewer",
271
+ produces: "review-verdict",
272
+ consumedBy: null,
273
+ stage: "final",
274
+ status: "pending",
275
+ artifact: null,
276
+ artifactValidation: null,
277
+ note: null,
278
+ startedAt: null,
279
+ completedAt: null,
280
+ });
281
+
282
+ return steps;
283
+ }
284
+
194
285
  // ── Step through a run ──────────────────────────────────────────────────────
195
286
 
196
287
  /**
@@ -391,6 +482,7 @@ export function generateCompletionReport(run) {
391
482
  status: s.status,
392
483
  hasArtifact: !!s.artifact,
393
484
  note: s.note,
485
+ knowledge: s.knowledge || null, // Phase 5: per-step knowledge posture
394
486
  }));
395
487
 
396
488
  const isComplete = run.status === "completed";
@@ -413,6 +505,13 @@ export function generateCompletionReport(run) {
413
505
  artifactChain,
414
506
  escalationCount: run.escalations.length,
415
507
  escalations: run.escalations,
508
+ knowledge: run.knowledge ? {
509
+ status: run.knowledge.status,
510
+ selected_count: run.knowledge.retrieval_bundle?.selected?.length ?? 0,
511
+ trust_posture: run.knowledge.retrieval_bundle?.provenance?.trust_posture ?? "unknown",
512
+ freshness_posture: run.knowledge.retrieval_bundle?.provenance?.freshness_posture ?? "unknown",
513
+ warning_codes: (run.knowledge.retrieval_bundle?.warnings ?? []).map((w) => w.code),
514
+ } : null,
416
515
  honestPartial: isPartial || isFailed ? mission.honestPartial : null,
417
516
  verdict: isComplete
418
517
  ? "Mission completed — all artifacts produced, all steps passed."
@@ -458,7 +557,20 @@ export function formatCompletionReport(report) {
458
557
  step.status === "blocked" ? "[-]" : "[ ]";
459
558
  const artifact = step.hasArtifact ? ` → ${step.produces}` : "";
460
559
  const note = step.note ? ` (${step.note})` : "";
461
- lines.push(` ${icon} ${step.role}${artifact}${note}`);
560
+ const kStatus = step.knowledge ? ` [knowledge: ${step.knowledge.status}]` : "";
561
+ lines.push(` ${icon} ${step.role}${artifact}${kStatus}${note}`);
562
+ }
563
+
564
+ // Knowledge posture (Phase 5)
565
+ if (report.knowledge) {
566
+ lines.push("");
567
+ lines.push("## Knowledge");
568
+ lines.push(` Status: ${report.knowledge.status}`);
569
+ lines.push(` Evidence: ${report.knowledge.selected_count} chunks selected`);
570
+ lines.push(` Trust: ${report.knowledge.trust_posture} | Freshness: ${report.knowledge.freshness_posture}`);
571
+ if (report.knowledge.warning_codes.length > 0) {
572
+ lines.push(` Warnings: ${report.knowledge.warning_codes.join(", ")}`);
573
+ }
462
574
  }
463
575
 
464
576
  if (report.escalationCount > 0) {