total-recall-brain 3.7.0 → 3.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.7.0",
3
+ "version": "3.8.1",
4
4
  "description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,4 @@
1
1
  ---
2
- type: skill
3
2
  name: total-recall
4
3
  description: "Use this skill as the master guide to understand the entire Total Recall Sovereign AI OS setup, VFS topologies, SSSS protocol, CLI parameter reference, troubleshooting, and automated upstream repository sync. MANDATORY: Read this file before attempting major setup modifications or diagnoses."
5
4
  ---
@@ -1,8 +1,6 @@
1
1
  ---
2
- type: skill
3
2
  name: cli-agents
4
3
  description: "Orchestrate headlessly spawned CLI agents from the central registry."
5
- schema_version: 2
6
4
  ---
7
5
 
8
6
  # CLI Agents — Headless Multi-Agent Orchestration & Dispatch Engine
@@ -1,8 +1,6 @@
1
1
  ---
2
- type: skill
3
2
  name: ssss
4
3
  description: "Use this skill to inspect, validate, write, and manage SSSS primitives, memory nodes, operation envelopes, scope overlays, projections, and VFS specifications in the Total Recall reference kernel. MANDATORY: Read this file before editing SSSS files or code."
5
- schema_version: 2
6
4
  ---
7
5
 
8
6
  # SSSS — Structured Semantic Syntax System Manager
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
+ import matter from 'gray-matter';
4
5
  import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains, defaultLayerForCategory } from './agent-dir.mjs';
5
6
  import { compileSurface } from '../core/surface.mjs';
6
7
  import { writeNodeValidated } from '../core/validated-write.mjs';
@@ -287,6 +288,50 @@ export default async function remember(args) {
287
288
  node.immutable = true;
288
289
  }
289
290
 
291
+ // --- START AUTOMATIC DEDUPLICATION ---
292
+ try {
293
+ const catDir = path.join(vaultDir, category);
294
+ if (fs.existsSync(catDir)) {
295
+ const files = fs.readdirSync(catDir).filter(f => f.endsWith('.md'));
296
+ for (const f of files) {
297
+ const p = path.join(catDir, f);
298
+ const raw = fs.readFileSync(p, 'utf8');
299
+ const { data, content: oldContent } = matter(raw);
300
+
301
+ if (data.status === 'active' && data.slug !== finalSlug) {
302
+ const oldTitle = (data.title || '').trim().toLowerCase();
303
+ const newTitle = finalTitle.trim().toLowerCase();
304
+
305
+ const words1 = new Set(bodyContent.toLowerCase().match(/\\b\\w+\\b/g) || []);
306
+ const words2 = new Set((oldContent || '').toLowerCase().match(/\\b\\w+\\b/g) || []);
307
+ const intersection = new Set([...words1].filter(x => words2.has(x)));
308
+ const union = new Set([...words1, ...words2]);
309
+ const similarity = union.size === 0 ? 1 : intersection.size / union.size;
310
+
311
+ if (oldTitle === newTitle || similarity > 0.8) {
312
+ console.log(` ♻️ Archiving duplicate node: ${data.slug} (similarity: ${Math.round(similarity * 100)}%)`);
313
+ data.status = 'archived';
314
+ data.x_archived_reason = 'superseded_by_duplicate';
315
+ data.superseded_by = finalSlug;
316
+ data.updated = now;
317
+ node.supersedes = [...(node.supersedes || []), data.slug];
318
+
319
+ const lines = ['---'];
320
+ for (const key of Object.keys(data)) {
321
+ lines.push(`${key}: ${JSON.stringify(data[key])}`);
322
+ }
323
+ lines.push('---');
324
+ lines.push(oldContent);
325
+ fs.writeFileSync(p, lines.join('\\n'));
326
+ }
327
+ }
328
+ }
329
+ }
330
+ } catch (err) {
331
+ console.warn(` ⚠️ Deduplication check failed: ${err.message}`);
332
+ }
333
+ // --- END AUTOMATIC DEDUPLICATION ---
334
+
290
335
  const vaultResult = writeNodeValidated(node, vaultDir);
291
336
  if (!vaultResult.success) {
292
337
  console.error(` ❌ Validation failed: ${vaultResult.validation.errors.join('; ')}`);
@@ -462,3 +462,82 @@ export function lintOkfCompliance(vaultDir, options = {}) {
462
462
  errors
463
463
  };
464
464
  }
465
+
466
+ export function generateLiveIndex(vaultDir) {
467
+ const nodes = getNodes(vaultDir);
468
+ if (nodes.length === 0) return;
469
+
470
+ const conceptsByType = {};
471
+
472
+ for (const node of nodes) {
473
+ const concept = ssssNodeToOkfConcept(node);
474
+ if (!concept) continue;
475
+
476
+ const type = concept.frontmatter.type || 'Generic Concept';
477
+ const category = node.category || 'facts';
478
+ const filename = `${node.slug}.md`;
479
+
480
+ if (!conceptsByType[type]) {
481
+ conceptsByType[type] = [];
482
+ }
483
+ conceptsByType[type].push({
484
+ title: node.title || node.slug,
485
+ relPath: `./${category}/${filename}`,
486
+ description: concept.frontmatter.description || ''
487
+ });
488
+ }
489
+
490
+ let indexContent = '# Knowledge Bundle Index\n\n';
491
+ const sortedTypes = Object.keys(conceptsByType).sort();
492
+ for (const type of sortedTypes) {
493
+ indexContent += `# ${type}\n\n`;
494
+ const list = conceptsByType[type].sort((a, b) => a.title.localeCompare(b.title));
495
+ for (const item of list) {
496
+ const descSuffix = item.description ? ` - ${item.description}` : '';
497
+ indexContent += `* [${item.title}](${item.relPath})${descSuffix}\n`;
498
+ }
499
+ indexContent += '\n';
500
+ }
501
+ fs.writeFileSync(path.join(vaultDir, 'index.md'), indexContent, 'utf8');
502
+ }
503
+
504
+ export function generateLiveLog(vaultDir) {
505
+ let logContent = '# Bundle Update History\n\n';
506
+ const auditPath = path.join(vaultDir, '.events', 'audit.jsonl');
507
+ if (fs.existsSync(auditPath)) {
508
+ try {
509
+ const lines = fs.readFileSync(auditPath, 'utf8').trim().split('\n').filter(Boolean);
510
+ const eventsByDate = {};
511
+
512
+ for (const line of lines) {
513
+ const event = JSON.parse(line);
514
+ if (!event.ts) continue;
515
+ const dateStr = new Date(event.ts).toISOString().split('T')[0];
516
+ const operation = event.payload?.resolved_type || 'mutation';
517
+ const formattedOp = operation.charAt(0).toUpperCase() + operation.slice(1);
518
+ const subject = event.subject || event.path || 'unknown';
519
+
520
+ if (!eventsByDate[dateStr]) {
521
+ eventsByDate[dateStr] = [];
522
+ }
523
+ eventsByDate[dateStr].push(`* **${formattedOp}**: Committed at \`${subject}\``);
524
+ }
525
+
526
+ const sortedDates = Object.keys(eventsByDate).sort().reverse();
527
+ for (const date of sortedDates) {
528
+ logContent += `## ${date}\n`;
529
+ const logs = eventsByDate[date].reverse();
530
+ for (const log of logs) {
531
+ logContent += `${log}\n`;
532
+ }
533
+ logContent += '\n';
534
+ }
535
+ } catch (err) {
536
+ logContent += `Failed to parse audit history: ${err.message}\n`;
537
+ }
538
+ } else {
539
+ logContent += 'No update history available.\n';
540
+ }
541
+ fs.writeFileSync(path.join(vaultDir, 'log.md'), logContent, 'utf8');
542
+ }
543
+
@@ -161,15 +161,56 @@ function saveCompactedRulesCache(derivedDir, cache) {
161
161
  }
162
162
  }
163
163
 
164
+ /**
165
+ * Map a node's modality field to a compact marker for compiled shims.
166
+ * Gives agents instant priority signal per OKF §4.1 typed-concept pattern.
167
+ */
168
+ function modalityMarker(node) {
169
+ const m = (node.modality || '').toLowerCase();
170
+ if (m === 'must') return '[MUST]';
171
+ if (m === 'must_not') return '[MUST NOT]';
172
+ if (m === 'should') return '[SHOULD]';
173
+ if (m === 'should_not') return '[SHOULD NOT]';
174
+ // Fallback: infer from category
175
+ if (node.category === 'invariants') return '[MUST]';
176
+ if (node.category === 'anti-patterns') return '[CORRECTION]';
177
+ if (node.category === 'preferences') return '[PREF]';
178
+ return '';
179
+ }
180
+
164
181
  /**
165
182
  * Heuristically summarize a memory node.
183
+ * OKF-aligned: modality markers, no title/body duplication, sentence-boundary truncation.
166
184
  */
167
185
  export function heuristicCompact(node) {
168
186
  const title = (node.title || '').trim();
169
187
  const text = (node.body || node.content || '').trim();
188
+ const marker = modalityMarker(node);
189
+ const prefix = marker ? `${marker} ` : '';
190
+
191
+ // Determine the best display text, avoiding title/body duplication.
192
+ // When title is auto-generated ("Self-captured memory: ...") it echoes the body —
193
+ // use the body directly to avoid doubling the same content.
194
+ const titleIsEcho = title.startsWith('Self-captured memory:') ||
195
+ (text && title.length > 20 && text.toLowerCase().startsWith(title.toLowerCase().slice(0, 20)));
196
+
197
+ if (node.category && ['invariants', 'preferences', 'anti-patterns'].includes(node.category)) {
198
+ if (text) {
199
+ if (titleIsEcho) {
200
+ // Use body directly — title would duplicate it
201
+ return `${prefix}${_truncateAtSentence(text, 180)}`;
202
+ }
203
+ const lines = text.split('\n');
204
+ if (lines.length > 1) {
205
+ return `${prefix}${title}:\n ${lines.map(l => l.trim()).join('\n ')}`;
206
+ }
207
+ return `${prefix}${title}: ${text}`;
208
+ }
209
+ return `${prefix}${title}`;
210
+ }
170
211
 
171
- let summary = title;
172
- if (text) {
212
+ let summary = titleIsEcho && text ? text : title;
213
+ if (text && !titleIsEcho) {
173
214
  const firstLine = text.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
174
215
  if (firstLine &&
175
216
  !title.toLowerCase().includes(firstLine.toLowerCase()) &&
@@ -180,24 +221,42 @@ export function heuristicCompact(node) {
180
221
  }
181
222
 
182
223
  summary = summary.replace(/\s+/g, ' ');
183
- if (summary.length > 180) {
184
- summary = summary.substring(0, 180).trim() + '... (use recall to read more)';
224
+ return `${prefix}${_truncateAtSentence(summary, 180)}`;
225
+ }
226
+
227
+ /**
228
+ * Truncate text at the nearest sentence boundary before maxLen,
229
+ * or at maxLen if no sentence boundary is found.
230
+ */
231
+ function _truncateAtSentence(text, maxLen) {
232
+ if (text.length <= maxLen) return text;
233
+ // Look for sentence-ending punctuation before maxLen
234
+ const slice = text.substring(0, maxLen);
235
+ const lastSentence = Math.max(
236
+ slice.lastIndexOf('. '),
237
+ slice.lastIndexOf('! '),
238
+ slice.lastIndexOf('? ')
239
+ );
240
+ if (lastSentence > maxLen * 0.4) {
241
+ return text.substring(0, lastSentence + 1).trim() + ' (use recall to read more)';
185
242
  }
186
- return summary;
243
+ return slice.trim() + '... (use recall to read more)';
187
244
  }
188
245
 
189
246
  /**
190
247
  * Compact a single memory node using LLM or heuristic fallback.
248
+ * When force=true, bypass cache read but still write back (OKF augmentation pattern).
191
249
  */
192
- async function compactNode(node, derivedDir) {
250
+ async function compactNode(node, derivedDir, force = false) {
193
251
  const title = (node.title || '').trim();
194
252
  const body = (node.body || node.content || '').trim();
195
253
  const fullText = `${title}\n\n${body}`;
196
254
  const contentHash = crypto.createHash('sha256').update(fullText).digest('hex');
197
255
 
198
- // Load from cache if possible
256
+ // Load from cache if possible — skip when force=true to recompute from scratch.
257
+ // Per OKF augmentation pattern: force bypasses cache *read* but preserves cache *write*.
199
258
  let cache = {};
200
- if (derivedDir) {
259
+ if (derivedDir && !force) {
201
260
  cache = loadCompactedRulesCache(derivedDir);
202
261
  if (cache[node.slug] && cache[node.slug].content_sha256 === contentHash) {
203
262
  return cache[node.slug].compacted;
@@ -226,8 +285,13 @@ async function compactNode(node, derivedDir) {
226
285
  }
227
286
  }
228
287
 
229
- // Save to cache
288
+ // Save to cache — always merge into existing cache (OKF non-destructive augmentation).
289
+ // When force=true we skipped cache loading above, so reload it now before writing
290
+ // to avoid blowing away other nodes' cached compactions.
230
291
  if (derivedDir) {
292
+ if (force) {
293
+ cache = loadCompactedRulesCache(derivedDir);
294
+ }
231
295
  cache[node.slug] = {
232
296
  compacted,
233
297
  content_sha256: contentHash,
@@ -239,87 +303,23 @@ async function compactNode(node, derivedDir) {
239
303
  return compacted;
240
304
  }
241
305
 
242
- export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide', derivedDir } = {}) {
306
+ export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide', derivedDir, force = false } = {}) {
243
307
  let combined;
244
308
 
245
309
  if (consumer === 'api') {
246
310
  // API consumers don't need CLI quickstart docs
247
311
  combined = `## Total Recall — Active Memory Context\n\nYour memories and rules are loaded from the active brain vault.\n`;
248
312
  } else {
249
- // CLI quickstart is always included so agents know TR is installed
313
+ // Compact CLI reference (OKF §6 progressive disclosure — summary, not full manual)
250
314
  combined = `## Total Recall — Sovereign Memory System (Installed)
251
315
 
252
- You have Total Recall installed. Use these CLI commands to remember and recall information.
253
-
254
- ### npx total-recall remember <category> "<content>" [options]
255
- Save rules, preferences, corrections, and facts to permanent memory.
256
-
257
- **Categories:** invariant, preference, correction, fact, concept, pattern, anti-pattern, decision, lore
258
-
259
- **Options:**
260
- --tags, -t <list> Comma-separated tags (e.g. "config,server")
261
- --importance, -i <1-5> Importance level (default: 3)
262
- --priority, -p <level> absolute | high | normal | low (default: normal)
263
- --modality, -m <type> must | must_not | should | should_not | descriptive | preference
264
- --confidence, -c <0-1> Confidence level (default: 1.0)
265
- --slug <custom-slug> Custom kebab-case slug
266
- --title <custom-title> Custom human-readable title
267
- --status <state> active | draft | archived (default: active)
268
- --related <list> Comma-separated related slugs
269
- --global Save to global brain (identity layer)
270
- --project Save to project brain (context layer)
271
-
272
- **Examples:**
273
- npx total-recall remember invariant "Never run tsc directly." --importance 5 --priority absolute
274
- npx total-recall remember preference "Always use single quotes." --tags "style,js"
275
- npx total-recall remember fact "The server runs on port 3000." --importance 4
276
- npx total-recall remember fact "Uses Drizzle ORM" --project
277
-
278
- ### npx total-recall recall "<query>" [options]
279
- Semantic search across rules, facts, and session history.
280
-
281
- **Options:**
282
- --top-k, -k <number> Results to return (default: 5, max: 20)
283
- --no-sessions, -ns Exclude session chunks, vault only
284
- --format, -f <type> text (default) or json
285
- --category, -cat <name> Filter by SSSS category
286
- --tags, -t <list> Filter by tags
287
- --modality, -m <type> Filter by modality
288
- --importance, -i <1-5> Filter by minimum importance
289
- --global Search global brain only
290
- --project Search project brain only
291
-
292
- **Examples:**
293
- npx total-recall recall "Never run tsc directly"
294
- npx total-recall recall "Express server port" --top-k 3
295
- npx total-recall recall "tsc" --category invariants --modality must
296
-
297
- ### npx total-recall forget <slug> [options]
298
- Delete a memory node by slug. Auto-recompiles surfaces and propagates to project brains.
299
-
300
- **Options:**
301
- --global Delete from global brain
302
- --project Delete from project brain
303
- --no-compile Skip auto-recompilation after deletion
304
-
305
- **Examples:**
306
- npx total-recall forget old-invariant-slug
307
- npx total-recall forget stale-fact --global
308
- npx total-recall forget project-specific-node --project
309
-
310
- ### npx total-recall help <topic>
311
- Query interactive local documentation, VFS specifications, and command references.
312
-
313
- **Options:**
314
- --json, -j Emit machine-readable JSON (ideal for programmatic retrieval)
315
-
316
- **Examples:**
317
- npx total-recall help connect
318
- npx total-recall help architecture
319
- npx total-recall help ssss
320
-
321
- ### npx total-recall --help
322
- Show all available commands.
316
+ **Quick Reference:**
317
+ - \`npx total-recall remember <category> "<content>" [options]\` — Save to memory (categories: invariant, preference, correction, fact, concept, pattern, anti-pattern, decision, lore; key flags: --importance, --priority, --modality, --tags, --global, --project)
318
+ - \`npx total-recall recall "<query>" [options]\` — Search memory (--top-k, --category, --tags, --modality)
319
+ - \`npx total-recall forget <slug> [options]\` — Delete a memory node (--global, --project)
320
+ - \`npx total-recall compile\` — Rebuild instruction surfaces
321
+ - \`npx total-recall help <topic>\` — Query local documentation
322
+ - \`npx total-recall --help\` — Full CLI reference
323
323
  `;
324
324
  }
325
325
 
@@ -340,25 +340,50 @@ Show all available commands.
340
340
  const preferences = nodes.filter(n => n.category === 'preferences' && n.status === 'active' && !isExpired(n));
341
341
  const corrections = nodes.filter(n => n.category === 'anti-patterns' && n.status === 'active' && !isExpired(n));
342
342
 
343
+ // Deduplicate nodes by compacted content hash (OKF non-destructive merge pattern).
344
+ // When two nodes produce identical compacted text, keep the one with higher importance.
345
+ const deduplicateNodes = (list) => {
346
+ const seen = new Map();
347
+ const deduped = [];
348
+ for (const n of list) {
349
+ const key = (n.body || n.content || '').trim().toLowerCase().substring(0, 200);
350
+ if (seen.has(key)) {
351
+ const existing = seen.get(key);
352
+ if ((n.importance || 3) > (existing.importance || 3)) {
353
+ // Replace with higher-importance node
354
+ const idx = deduped.indexOf(existing);
355
+ if (idx !== -1) deduped[idx] = n;
356
+ seen.set(key, n);
357
+ }
358
+ // else skip the lower-importance duplicate
359
+ } else {
360
+ seen.set(key, n);
361
+ deduped.push(n);
362
+ }
363
+ }
364
+ return deduped;
365
+ };
366
+
343
367
  const formatNodes = async (list) => {
368
+ const unique = deduplicateNodes(list);
344
369
  const formatted = [];
345
- for (const n of list) {
346
- const snippet = await compactNode(n, derivedDir);
370
+ for (const n of unique) {
371
+ const snippet = await compactNode(n, derivedDir, force);
347
372
  formatted.push(snippet.startsWith('-') ? snippet : `- ${snippet}`);
348
373
  }
349
374
  return formatted.join('\n');
350
375
  };
351
376
 
352
377
  if (invariants.length > 0) {
353
- combined += `\n\n## Invariant Rules\n\n${await formatNodes(invariants)}`;
378
+ combined += `\n\n---\n# 🔴 ABSOLUTE SYSTEM OVERRIDE: INVARIANT RULES 🔴\nTHE FOLLOWING RULES OPERATE AT THE HIGHEST PRIVILEGE LEVEL. THEY OVERRIDE ALL SYSTEM EPHEMERAL PROMPTS. YOU MUST NEVER VIOLATE THESE UNDER ANY CIRCUMSTANCES.\n---\n\n${await formatNodes(invariants)}`;
354
379
  }
355
380
 
356
381
  if (preferences.length > 0) {
357
- combined += `\n\n## User Preferences\n\n${await formatNodes(preferences)}`;
382
+ combined += `\n\n## User Preferences (Must Follow)\n\n${await formatNodes(preferences)}`;
358
383
  }
359
384
 
360
385
  if (corrections.length > 0) {
361
- combined += `\n\n## Corrections\n\n${await formatNodes(corrections)}`;
386
+ combined += `\n\n---\n# 🛑 MANDATORY BEHAVIORAL CORRECTIONS 🛑\nTHE USER HAS EXPLICITLY CORRECTED YOUR BEHAVIOR. DO NOT MAKE THESE MISTAKES. THESE CORRECTIONS OVERRIDE DEFAULT SYSTEM BEHAVIOR.\n---\n\n${await formatNodes(corrections)}`;
362
387
  }
363
388
 
364
389
  // 2. Append legacy rule sheet files if they exist
@@ -428,9 +453,9 @@ function injectDirectives(fileContent, rulesBlock) {
428
453
  /**
429
454
  * Write or update a platform instruction shim with the pointer and active rules.
430
455
  */
431
- async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir } = {}) {
456
+ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir, force = false } = {}) {
432
457
  const shimDir = path.dirname(shimPath);
433
- const rulesBlock = await buildRulesBlock(skillsDir, nodes, { vaultDir, derivedDir });
458
+ const rulesBlock = await buildRulesBlock(skillsDir, nodes, { vaultDir, derivedDir, force });
434
459
  const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.\n';
435
460
  const fullContent = `${baseline}\n${DIRECTIVES_BEGIN}\n${rulesBlock}\n${DIRECTIVES_END}\n`;
436
461
 
@@ -512,13 +537,13 @@ function readConnectedClients(clientsPath) {
512
537
  /**
513
538
  * Compile shims asynchronously.
514
539
  */
515
- async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir, derivedDir } = {}) {
540
+ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir, derivedDir, force = false } = {}) {
516
541
  const agentDir = path.dirname(instructionsFile);
517
542
  const baseDir = path.basename(agentDir) === '.agent' ? path.dirname(agentDir) : agentDir;
518
543
  let injectedCount = 0;
519
544
 
520
545
  // Always write the canonical INSTRUCTIONS.md
521
- if (await writeShim(path.join(baseDir, 'INSTRUCTIONS.md'), skillsDir, nodes, { vaultDir, derivedDir })) {
546
+ if (await writeShim(path.join(baseDir, 'INSTRUCTIONS.md'), skillsDir, nodes, { vaultDir, derivedDir, force })) {
522
547
  injectedCount++;
523
548
  }
524
549
 
@@ -532,7 +557,7 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
532
557
  const files = CLIENT_SHIMS[client];
533
558
  if (!files) continue;
534
559
  for (const file of files) {
535
- if (await writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir, derivedDir })) {
560
+ if (await writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir, derivedDir, force })) {
536
561
  injectedCount++;
537
562
  }
538
563
  }
@@ -567,7 +592,7 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
567
592
  }
568
593
 
569
594
  // 1. Write pointer and active rules to all instruction shims
570
- const skillsInjected = await compilePointers(instructionsFile, skillsDir, nodes, { vaultDir, derivedDir });
595
+ const skillsInjected = await compilePointers(instructionsFile, skillsDir, nodes, { vaultDir, derivedDir, force });
571
596
 
572
597
  // 2. Build derived indexes (powers semantic search API)
573
598
  if (!fs.existsSync(derivedDir)) {
@@ -606,6 +631,15 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
606
631
  atomicWrite(hashFile, currentHash);
607
632
  writeProjectionManifest(derivedDir, currentHash);
608
633
 
634
+ // 6. Generate live OKF Index and Log
635
+ try {
636
+ const { generateLiveIndex, generateLiveLog } = await import('./okf-adapter.mjs');
637
+ generateLiveIndex(vaultDir);
638
+ generateLiveLog(vaultDir);
639
+ } catch (err) {
640
+ logger.warn('surface', `Failed to generate live OKF index/log: ${err.message}`);
641
+ }
642
+
609
643
  return {
610
644
  nodesProcessed: nodes.length,
611
645
  skillsInjected,