total-recall-brain 3.1.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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/antigravity.mjs +212 -0
  4. package/bin/init.mjs +36 -0
  5. package/bin/oci-sniper.sh +69 -0
  6. package/bin/total-recall.mjs +229 -0
  7. package/docs/ARCHITECTURE.md +251 -0
  8. package/docs/IDE_INTEGRATION.md +112 -0
  9. package/package.json +71 -0
  10. package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
  11. package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
  12. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
  13. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
  14. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
  15. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
  16. package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
  17. package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
  18. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
  19. package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
  20. package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
  21. package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
  22. package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
  23. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
  24. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
  25. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
  26. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
  27. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
  28. package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
  29. package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
  30. package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
  31. package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
  32. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
  33. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
  34. package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
  35. package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
  36. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
  37. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
  38. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
  39. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
  40. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
  41. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
  42. package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
  43. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
  44. package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
  45. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
  46. package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
  47. package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
  48. package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
  49. package/src/cli/agent-dir.mjs +105 -0
  50. package/src/cli/backup.mjs +313 -0
  51. package/src/cli/brain.mjs +224 -0
  52. package/src/cli/chat.mjs +74 -0
  53. package/src/cli/config.mjs +247 -0
  54. package/src/cli/connect.mjs +872 -0
  55. package/src/cli/daemon.mjs +228 -0
  56. package/src/cli/deploy-ui.mjs +1793 -0
  57. package/src/cli/deploy.mjs +1726 -0
  58. package/src/cli/doctor.mjs +254 -0
  59. package/src/cli/dream.mjs +80 -0
  60. package/src/cli/forget.mjs +92 -0
  61. package/src/cli/friction.mjs +21 -0
  62. package/src/cli/generate-pat.mjs +65 -0
  63. package/src/cli/hash-password.mjs +22 -0
  64. package/src/cli/help.mjs +153 -0
  65. package/src/cli/import-rules.mjs +102 -0
  66. package/src/cli/ingest/index.mjs +318 -0
  67. package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
  68. package/src/cli/ingest/parsers/google-keep.mjs +147 -0
  69. package/src/cli/ingest/parsers/search-history.mjs +142 -0
  70. package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
  71. package/src/cli/ingest/utils/dedup.mjs +69 -0
  72. package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
  73. package/src/cli/ingest.mjs +102 -0
  74. package/src/cli/init.mjs +736 -0
  75. package/src/cli/integration-dispatcher.mjs +128 -0
  76. package/src/cli/lint.mjs +166 -0
  77. package/src/cli/map.mjs +194 -0
  78. package/src/cli/migrate.mjs +87 -0
  79. package/src/cli/rebuild.mjs +88 -0
  80. package/src/cli/recall.mjs +219 -0
  81. package/src/cli/relay.mjs +518 -0
  82. package/src/cli/remember.mjs +306 -0
  83. package/src/cli/research.mjs +381 -0
  84. package/src/cli/reset-password.mjs +75 -0
  85. package/src/cli/restore.mjs +135 -0
  86. package/src/cli/setup.mjs +616 -0
  87. package/src/cli/share.mjs +180 -0
  88. package/src/cli/skill.mjs +313 -0
  89. package/src/cli/snapshot.mjs +108 -0
  90. package/src/cli/start.mjs +63 -0
  91. package/src/cli/status.mjs +296 -0
  92. package/src/cli/sync.mjs +383 -0
  93. package/src/cli/uninstall.mjs +613 -0
  94. package/src/cli/upgrade.mjs +141 -0
  95. package/src/cli/wizard.html +3682 -0
  96. package/src/core/blackboard.mjs +54 -0
  97. package/src/core/clarity-rewriter.mjs +770 -0
  98. package/src/core/conclusion-writer.mjs +259 -0
  99. package/src/core/config.mjs +277 -0
  100. package/src/core/conflict-detector.mjs +536 -0
  101. package/src/core/crypto.mjs +107 -0
  102. package/src/core/daemon-control.mjs +158 -0
  103. package/src/core/daemon-loop.mjs +364 -0
  104. package/src/core/dream.mjs +347 -0
  105. package/src/core/drift-detector.mjs +143 -0
  106. package/src/core/embeddings.mjs +632 -0
  107. package/src/core/emergency-alerts.mjs +137 -0
  108. package/src/core/evolution.mjs +107 -0
  109. package/src/core/fact-seeker.mjs +1602 -0
  110. package/src/core/friction.mjs +95 -0
  111. package/src/core/import-rules.mjs +308 -0
  112. package/src/core/inference-engine.mjs +367 -0
  113. package/src/core/logger.mjs +75 -0
  114. package/src/core/memory-layers.mjs +96 -0
  115. package/src/core/migrate.mjs +116 -0
  116. package/src/core/notifications.mjs +77 -0
  117. package/src/core/operation-validator.mjs +272 -0
  118. package/src/core/optimizer.mjs +330 -0
  119. package/src/core/pattern_detector.mjs +113 -0
  120. package/src/core/post-mortem.mjs +413 -0
  121. package/src/core/protect-instructions.mjs +148 -0
  122. package/src/core/quick-capture.mjs +145 -0
  123. package/src/core/research-queue.mjs +431 -0
  124. package/src/core/research.mjs +461 -0
  125. package/src/core/runtime.mjs +515 -0
  126. package/src/core/sandbox.mjs +163 -0
  127. package/src/core/scheduler.mjs +569 -0
  128. package/src/core/schema.mjs +333 -0
  129. package/src/core/search.mjs +219 -0
  130. package/src/core/semantic-index.mjs +276 -0
  131. package/src/core/session-watcher.mjs +850 -0
  132. package/src/core/snapshot.mjs +133 -0
  133. package/src/core/source-adapters.mjs +1038 -0
  134. package/src/core/steering.mjs +137 -0
  135. package/src/core/surface.mjs +444 -0
  136. package/src/core/task_runner.mjs +125 -0
  137. package/src/core/tts.mjs +142 -0
  138. package/src/core/usage-tracker.mjs +349 -0
  139. package/src/core/vault-cache.mjs +76 -0
  140. package/src/core/vault.mjs +273 -0
  141. package/src/core/watchdog.mjs +236 -0
  142. package/src/server/api.mjs +944 -0
  143. package/src/server/auth.mjs +410 -0
  144. package/src/server/index.mjs +868 -0
  145. package/src/server/keys.mjs +181 -0
  146. package/src/server/rest.mjs +1652 -0
  147. package/src/server/route-manifest.json +77 -0
  148. package/src/server/routes/_shared.mjs +93 -0
  149. package/src/server/routes/keys.mjs +55 -0
  150. package/src/server/routes/memory.mjs +249 -0
  151. package/src/server/routes/sessions.mjs +336 -0
  152. package/src/server/routes/share.mjs +138 -0
  153. package/src/server/tools.mjs +618 -0
  154. package/templates/Caddyfile +37 -0
  155. package/templates/com.totalrecall.backup.plist +42 -0
  156. package/templates/com.totalrecall.daemon.plist +40 -0
  157. package/templates/com.totalrecall.duckdns.plist +23 -0
  158. package/templates/com.totalrecall.server.plist +46 -0
  159. package/templates/default-config/budget.yml +6 -0
  160. package/templates/default-config/runtime.yml +11 -0
  161. package/templates/default-config/security.yml +53 -0
  162. package/templates/default-config/voice.yml +34 -0
  163. package/templates/obsidian-queries/conflicts.md +26 -0
  164. package/templates/obsidian-queries/daily.md +17 -0
  165. package/templates/obsidian-queries/index.md +38 -0
  166. package/templates/obsidian-queries/skills.md +14 -0
  167. package/templates/onboarding-interview.md +72 -0
  168. package/templates/skill_engineering.md +40 -0
  169. package/templates/total-recall-daemon.service +21 -0
  170. package/templates/total-recall-server.service +20 -0
@@ -0,0 +1,113 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { atomicWrite, safeStringify } from './vault.mjs';
5
+ import { logger } from './logger.mjs';
6
+
7
+ /**
8
+ * Pattern Detection Module
9
+ * Scans session logs for repeated failures, questions, or missing knowledge,
10
+ * and generates Proactive Research or Skill Engineering tasks.
11
+ */
12
+
13
+ export function scanForMissingKnowledge(sessionsDir, existingSkills) {
14
+ if (!fs.existsSync(sessionsDir)) return [];
15
+
16
+ const missingKnowledgeSignals = [];
17
+ const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
18
+
19
+ for (const file of files) {
20
+ const lines = fs.readFileSync(path.join(sessionsDir, file), 'utf8').split('\n').filter(Boolean);
21
+ for (const line of lines) {
22
+ try {
23
+ const entry = JSON.parse(line);
24
+ if (entry.type !== 'observation' && entry.type !== 'task') continue;
25
+
26
+ const content = (entry.content || '').toLowerCase();
27
+
28
+ // Heuristic: Does the agent repeatedly fail to find a tool or skill?
29
+ if (
30
+ content.includes('i don\'t know how to') ||
31
+ content.includes('missing skill') ||
32
+ content.includes('no instructions found for') ||
33
+ content.includes('unfamiliar with')
34
+ ) {
35
+ missingKnowledgeSignals.push({
36
+ topic: extractTopic(content),
37
+ session: entry.id,
38
+ ts: entry.ts,
39
+ raw: entry.content
40
+ });
41
+ }
42
+ } catch (err) {
43
+ // Skip malformed
44
+ }
45
+ }
46
+ }
47
+
48
+ return aggregateSignals(missingKnowledgeSignals, existingSkills);
49
+ }
50
+
51
+ /**
52
+ * Very basic heuristic to pull out the topic from a failure message.
53
+ */
54
+ function extractTopic(text) {
55
+ const match = text.match(/(?:missing skill|no instructions found for|unfamiliar with)\s+([a-zA-Z0-9_-]+)/i);
56
+ return match ? match[1].toLowerCase() : 'unknown-topic';
57
+ }
58
+
59
+ /**
60
+ * Group signals and threshold them to generate tasks.
61
+ */
62
+ function aggregateSignals(signals, existingSkills) {
63
+ const frequency = new Map();
64
+ for (const sig of signals) {
65
+ if (sig.topic === 'unknown-topic') continue;
66
+ // Don't generate tasks for skills that already exist
67
+ if (existingSkills.some(s => s.name === sig.topic)) continue;
68
+
69
+ const count = frequency.get(sig.topic) || 0;
70
+ frequency.set(sig.topic, count + 1);
71
+ }
72
+
73
+ const tasks = [];
74
+ for (const [topic, count] of frequency.entries()) {
75
+ if (count >= 2) { // Threshold: User/Agent tripped over this 2+ times
76
+ tasks.push(generateSkillTask(topic, count));
77
+ }
78
+ }
79
+
80
+ return tasks;
81
+ }
82
+
83
+ function generateSkillTask(topic, count) {
84
+ const slug = `learn-${topic}-${Date.now().toString(36)}`;
85
+ return {
86
+ type: 'task',
87
+ slug,
88
+ priority: 50, // Mid-priority
89
+ category: 'skill-engineering',
90
+ target: `skills/${topic}-expert.md`,
91
+ estimated_calls: 15,
92
+ deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
93
+ created_by: 'pattern-detector',
94
+ reason: `Agent failed to execute workflows related to '${topic}' ${count} times. Missing skill.`,
95
+ status: 'pending',
96
+ progress: 0,
97
+ body: `## Objective\nResearch and build a new skill module for ${topic}.\n\n## Steps\n1. Web search ${topic} docs.\n2. Write draft SKILL.md.\n3. Validate against SSSS schema.`
98
+ };
99
+ }
100
+
101
+ export function writeTasksToQueue(tasks, queueDir) {
102
+ if (!fs.existsSync(queueDir)) {
103
+ fs.mkdirSync(queueDir, { recursive: true });
104
+ }
105
+
106
+ for (const task of tasks) {
107
+ const filePath = path.join(queueDir, `${task.slug}.md`);
108
+ const { body, slug, ...frontmatter } = task;
109
+ const raw = safeStringify(body, frontmatter);
110
+ atomicWrite(filePath, raw);
111
+ logger.info('pattern-detector', `Created Task: ${slug}`);
112
+ }
113
+ }
@@ -0,0 +1,413 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import crypto from 'crypto';
5
+ import { callLocalRuntime, loadRuntimeConfig } from './runtime.mjs';
6
+ import { atomicWrite, loadNodes, safeStringify } from './vault.mjs';
7
+ import { logger } from './logger.mjs';
8
+ import { brainDir } from './config.mjs';
9
+
10
+ /**
11
+ * Total Recall Session Post-Mortem Engine
12
+ *
13
+ * After each ingested session, the local LLM reads the transcript and extracts:
14
+ * 1. New patterns/preferences observed (→ conscious layer)
15
+ * 2. New facts mentioned (→ research layer, draft status)
16
+ * 3. Skill gaps (→ skill engineering tasks)
17
+ * 4. Rule violations (→ compliance auditor)
18
+ *
19
+ * All outputs are proposals or draft nodes — never direct mutations.
20
+ */
21
+
22
+ // ─── Constants ──────────────────────────────────────────────────────────────────
23
+
24
+ const MAX_TRANSCRIPT_CHARS = 12000; // Don't overwhelm the local LLM
25
+ const MAX_ENTRIES_PER_SESSION = 100;
26
+
27
+ // ─── Prompts ────────────────────────────────────────────────────────────────────
28
+
29
+ const POST_MORTEM_SYSTEM = `You are a Session Post-Mortem Analyst for a sovereign AI memory system called Total Recall.
30
+
31
+ Your job is to read a conversation transcript between a user and an AI agent, then extract structured insights.
32
+
33
+ You MUST output valid JSON matching this schema:
34
+ {
35
+ "patterns": [
36
+ { "title": "string", "description": "string", "layer": "conscious" }
37
+ ],
38
+ "facts": [
39
+ { "title": "string", "description": "string", "layer": "research" }
40
+ ],
41
+ "skill_gaps": [
42
+ { "topic": "string", "description": "string" }
43
+ ],
44
+ "violations": [
45
+ { "rule": "string", "violation": "string", "suggestion": "string" }
46
+ ]
47
+ }
48
+
49
+ Guidelines:
50
+ - "patterns" are user preferences, coding conventions, or workflow habits observed.
51
+ - "facts" are specific technical facts mentioned (APIs, tool versions, library behavior).
52
+ - "skill_gaps" are topics where the agent struggled or had to improvise.
53
+ - "violations" are cases where the agent failed to follow established rules.
54
+ - If a category has no items, use an empty array [].
55
+ - Output ONLY valid JSON, no markdown fences, no explanation.`;
56
+
57
+ function buildPostMortemPrompt(transcript, rules) {
58
+ let prompt = `## Conversation Transcript\n${transcript}\n`;
59
+ if (rules.length > 0) {
60
+ prompt += `\n## Active Rules (check for violations)\n`;
61
+ for (const rule of rules) {
62
+ prompt += `- ${rule.title} (modality: ${rule.modality})\n`;
63
+ }
64
+ }
65
+ prompt += '\nExtract patterns, facts, skill gaps, and rule violations as JSON.';
66
+ return prompt;
67
+ }
68
+
69
+ // ─── Session Reading ────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Read a session JSONL file and produce a readable transcript.
73
+ */
74
+ export function readSessionTranscript(sessionPath) {
75
+ if (!fs.existsSync(sessionPath)) return '';
76
+
77
+ const raw = fs.readFileSync(sessionPath, 'utf8');
78
+ const lines = raw.split('\n').filter(Boolean);
79
+ const entries = [];
80
+
81
+ for (const line of lines.slice(0, MAX_ENTRIES_PER_SESSION)) {
82
+ try {
83
+ const entry = JSON.parse(line);
84
+ const role = (entry.role || 'assistant').toUpperCase();
85
+ const content = (entry.content || '').slice(0, 2000);
86
+ if (content.trim()) {
87
+ entries.push(`[${role}]: ${content}`);
88
+ }
89
+ } catch {
90
+ // skip malformed
91
+ }
92
+ }
93
+
94
+ const transcript = entries.join('\n\n');
95
+ // Truncate to avoid overwhelming the model
96
+ return transcript.slice(0, MAX_TRANSCRIPT_CHARS);
97
+ }
98
+
99
+ // ─── Post-Mortem Execution ──────────────────────────────────────────────────────
100
+
101
+ /**
102
+ * Run a post-mortem analysis on a single session file.
103
+ *
104
+ * @param {string} sessionPath Path to the session JSONL file
105
+ * @param {object} opts
106
+ * @param {string} opts.vaultDir Path to memory vault
107
+ * @param {string} opts.inboxDir Path to memory inbox
108
+ * @param {string} opts.runtimeConfig Local LLM runtime config
109
+ * @returns {object} { patterns, facts, skill_gaps, violations, error? }
110
+ */
111
+ export async function runPostMortem(sessionPath, { vaultDir, inboxDir, runtimeConfig }) {
112
+ const transcript = readSessionTranscript(sessionPath);
113
+ if (!transcript || transcript.length < 50) {
114
+ return { patterns: [], facts: [], skill_gaps: [], violations: [], skipped: 'too-short' };
115
+ }
116
+
117
+ // Load active rules for violation checking
118
+ const nodes = loadNodes(vaultDir);
119
+ const rules = nodes.filter(n =>
120
+ n.status === 'active' &&
121
+ n.priority === 'absolute' &&
122
+ (n.modality === 'must' || n.modality === 'must_not')
123
+ );
124
+
125
+ const prompt = buildPostMortemPrompt(transcript, rules);
126
+
127
+ let result;
128
+ try {
129
+ const rawResponse = await callLocalRuntime(prompt, POST_MORTEM_SYSTEM, runtimeConfig);
130
+ // Extract JSON from the response
131
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
132
+ if (!jsonMatch) {
133
+ throw new Error('No JSON found in LLM response');
134
+ }
135
+ result = JSON.parse(jsonMatch[0]);
136
+ } catch (err) {
137
+ logger.info({
138
+ subsystem: 'post-mortem',
139
+ message: `LLM analysis failed for ${path.basename(sessionPath)}: ${err.message}`,
140
+ });
141
+ return { patterns: [], facts: [], skill_gaps: [], violations: [], error: err.message };
142
+ }
143
+
144
+ // Normalize the result
145
+ const patterns = Array.isArray(result.patterns) ? result.patterns : [];
146
+ const facts = Array.isArray(result.facts) ? result.facts : [];
147
+ const skillGaps = Array.isArray(result.skill_gaps) ? result.skill_gaps : [];
148
+ const violations = Array.isArray(result.violations) ? result.violations : [];
149
+
150
+ // Write extractions to appropriate locations
151
+ const sessionSlug = path.basename(sessionPath, '.jsonl');
152
+
153
+ if (!fs.existsSync(inboxDir)) {
154
+ fs.mkdirSync(inboxDir, { recursive: true });
155
+ }
156
+
157
+ // Write patterns as conscious-layer draft nodes
158
+ for (const pattern of patterns) {
159
+ writeDraftNode(inboxDir, {
160
+ title: pattern.title || 'Observed Pattern',
161
+ body: pattern.description || '',
162
+ category: 'patterns',
163
+ layer: 'conscious',
164
+ tags: ['post-mortem', 'pattern', 'auto-extracted'],
165
+ sourceSession: sessionSlug,
166
+ });
167
+ }
168
+
169
+ // Write facts as research-layer draft nodes
170
+ for (const fact of facts) {
171
+ writeDraftNode(inboxDir, {
172
+ title: fact.title || 'Observed Fact',
173
+ body: fact.description || '',
174
+ category: 'facts',
175
+ layer: 'research',
176
+ tags: ['post-mortem', 'fact', 'auto-extracted'],
177
+ sourceSession: sessionSlug,
178
+ });
179
+ }
180
+
181
+ // Write skill gaps as tasks
182
+ const queueDir = path.join(brainDir, 'scheduler', 'queue');
183
+ if (!fs.existsSync(queueDir)) {
184
+ fs.mkdirSync(queueDir, { recursive: true });
185
+ }
186
+
187
+ for (const gap of skillGaps) {
188
+ writeSkillGapTask(queueDir, {
189
+ topic: gap.topic || 'unknown',
190
+ description: gap.description || '',
191
+ sourceSession: sessionSlug,
192
+ });
193
+ }
194
+
195
+ logger.info({
196
+ subsystem: 'post-mortem',
197
+ message: `Session ${sessionSlug}: ${patterns.length} patterns, ${facts.length} facts, ${skillGaps.length} gaps, ${violations.length} violations`,
198
+ });
199
+
200
+ // Feed session transcript to the research agenda for topic inference
201
+ try {
202
+ const { ingestSessionTopics } = await import('./fact-seeker.mjs');
203
+ const addedTopics = await ingestSessionTopics(transcript, runtimeConfig);
204
+ if (addedTopics.length > 0) {
205
+ logger.info({
206
+ subsystem: 'post-mortem',
207
+ message: `Research agenda: +${addedTopics.length} topics from session "${sessionSlug}"`,
208
+ });
209
+ }
210
+ } catch (err) {
211
+ // Non-fatal — topic inference failure shouldn't block post-mortem results
212
+ logger.info({ subsystem: 'post-mortem', message: `Topic inference skipped: ${err.message}` });
213
+ }
214
+
215
+ return { patterns, facts, skill_gaps: skillGaps, violations };
216
+ }
217
+
218
+ // ─── Draft Node Writers ─────────────────────────────────────────────────────────
219
+
220
+ function writeDraftNode(inboxDir, { title, body, category, layer, tags, sourceSession }) {
221
+ const slug = `pm-${category}-${crypto.randomBytes(4).toString('hex')}`;
222
+ const now = new Date().toISOString();
223
+
224
+ const frontmatter = {
225
+ type: 'memory',
226
+ slug,
227
+ category,
228
+ title,
229
+ status: 'draft',
230
+ confidence: 0.6,
231
+ importance: 3,
232
+ created: now,
233
+ updated: now,
234
+ last_accessed: now,
235
+ source: {
236
+ type: 'dream-cycle',
237
+ session_id: sourceSession,
238
+ agent: 'post-mortem-engine',
239
+ evidence_count: 1,
240
+ },
241
+ supersedes: [],
242
+ superseded_by: null,
243
+ contradicts: [],
244
+ tags,
245
+ related: [],
246
+ routes_to_skills: [],
247
+ sentiment_polarity: 'descriptive',
248
+ sentiment_target: category,
249
+ modality: 'should',
250
+ subject: 'agent',
251
+ predicate: 'observe',
252
+ object: category,
253
+ decay: { half_life_days: 90, access_count: 1 },
254
+ schema_version: 2,
255
+ x_memory_layer: layer,
256
+ x_temporal_context: now,
257
+ x_citations: [{
258
+ source: 'post-mortem-engine',
259
+ title: `Extracted from session post-mortem: ${sourceSession}`,
260
+ url: `session://${sourceSession}`,
261
+ published: now,
262
+ relevance: 1.0,
263
+ accessed: now
264
+ }]
265
+ };
266
+
267
+ const raw = safeStringify(body, frontmatter);
268
+ atomicWrite(path.join(inboxDir, `${slug}.md`), raw);
269
+ }
270
+
271
+ function writeSkillGapTask(queueDir, { topic, description, sourceSession }) {
272
+ const slug = `skill-gap-${topic}-${crypto.randomBytes(3).toString('hex')}`;
273
+ const data = {
274
+ type: 'task',
275
+ slug,
276
+ priority: 40,
277
+ category: 'skill-engineering',
278
+ status: 'pending',
279
+ created_by: 'post-mortem-engine',
280
+ source_session: sourceSession,
281
+ reason: `Agent struggled with topic "${topic}" during a conversation.`,
282
+ };
283
+
284
+ const body = `## Objective\nThe agent encountered a skill gap for "${topic}".\n\n${description}\n\n## Steps\n1. Research the topic.\n2. Check if an existing skill covers it.\n3. If not, draft a new SKILL.md.`;
285
+
286
+ atomicWrite(path.join(queueDir, `${slug}.md`), safeStringify(body, data));
287
+ }
288
+
289
+ // ─── Rule Compliance Auditor ────────────────────────────────────────────────────
290
+
291
+ const COMPLIANCE_SYSTEM = `You are a Rule Compliance Auditor for a sovereign AI memory system.
292
+
293
+ Given a list of rules and a conversation transcript, identify any rules the agent violated.
294
+
295
+ Output valid JSON matching this schema:
296
+ {
297
+ "compliant": boolean,
298
+ "violations": [
299
+ {
300
+ "rule_slug": "string",
301
+ "rule_title": "string",
302
+ "violation_description": "string",
303
+ "severity": "low" | "medium" | "high",
304
+ "suggested_rule_improvement": "string"
305
+ }
306
+ ]
307
+ }
308
+
309
+ If the agent followed all rules, set "compliant": true and "violations": [].
310
+ Output ONLY valid JSON.`;
311
+
312
+ /**
313
+ * Run a compliance audit on a session transcript against active rules.
314
+ *
315
+ * @param {string} sessionPath Path to session JSONL file
316
+ * @param {object} opts
317
+ * @param {string} opts.vaultDir Path to memory vault
318
+ * @param {string} opts.runtimeConfig Local LLM runtime config
319
+ * @returns {object} { compliant, violations, error? }
320
+ */
321
+ export async function runComplianceAudit(sessionPath, { vaultDir, runtimeConfig }) {
322
+ const transcript = readSessionTranscript(sessionPath);
323
+ if (!transcript || transcript.length < 50) {
324
+ return { compliant: true, violations: [], skipped: 'too-short' };
325
+ }
326
+
327
+ const nodes = loadNodes(vaultDir);
328
+ const rules = nodes.filter(n =>
329
+ n.status === 'active' &&
330
+ (n.modality === 'must' || n.modality === 'must_not')
331
+ );
332
+
333
+ if (rules.length === 0) {
334
+ return { compliant: true, violations: [], skipped: 'no-rules' };
335
+ }
336
+
337
+ const rulesText = rules.map(r =>
338
+ `[${r.slug}] ${r.modality.toUpperCase()}: ${r.title}\n ${(r.body || '').slice(0, 300)}`
339
+ ).join('\n\n');
340
+
341
+ const prompt = `## Rules\n${rulesText}\n\n## Session Transcript\n${transcript}\n\nAudit this session for rule violations.`;
342
+
343
+ try {
344
+ const rawResponse = await callLocalRuntime(prompt, COMPLIANCE_SYSTEM, runtimeConfig);
345
+ const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
346
+ if (!jsonMatch) throw new Error('No JSON found in LLM response');
347
+
348
+ const result = JSON.parse(jsonMatch[0]);
349
+ const violations = Array.isArray(result.violations) ? result.violations : [];
350
+
351
+ // Bump importance for violated rules
352
+ if (violations.length > 0) {
353
+ await escalateViolatedRules(violations, nodes, vaultDir);
354
+ }
355
+
356
+ logger.info({
357
+ subsystem: 'compliance-auditor',
358
+ message: `Session ${path.basename(sessionPath)}: ${violations.length > 0 ? `${violations.length} violations` : 'COMPLIANT'}`,
359
+ });
360
+
361
+ return { compliant: violations.length === 0, violations };
362
+ } catch (err) {
363
+ logger.info({
364
+ subsystem: 'compliance-auditor',
365
+ message: `Audit failed for ${path.basename(sessionPath)}: ${err.message}`,
366
+ });
367
+ return { compliant: true, violations: [], error: err.message };
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Escalate rules that get violated frequently.
373
+ * Bumps importance and confidence. After 3+ violations, promotes to priority: absolute.
374
+ */
375
+ async function escalateViolatedRules(violations, nodes, vaultDir) {
376
+ for (const v of violations) {
377
+ const node = nodes.find(n => n.slug === v.rule_slug);
378
+ if (!node || !node._filepath) continue;
379
+
380
+ try {
381
+ const raw = fs.readFileSync(node._filepath, 'utf8');
382
+ const { data, content } = matter(raw);
383
+
384
+ // Track violation count
385
+ const violationCount = (data.x_violation_count || 0) + 1;
386
+ data.x_violation_count = violationCount;
387
+
388
+ // Bump importance (max 5)
389
+ if (data.importance < 5) {
390
+ data.importance = Math.min(5, data.importance + 1);
391
+ }
392
+
393
+ // Bump confidence (max 1.0)
394
+ if (data.confidence < 1.0) {
395
+ data.confidence = Math.min(1.0, data.confidence + 0.05);
396
+ }
397
+
398
+ // Auto-escalate to priority: absolute after 3+ violations
399
+ if (violationCount >= 3 && data.priority !== 'absolute') {
400
+ data.priority = 'absolute';
401
+ logger.info({
402
+ subsystem: 'compliance-auditor',
403
+ message: `ESCALATED rule "${data.slug}" to priority: absolute after ${violationCount} violations.`,
404
+ });
405
+ }
406
+
407
+ data.updated = new Date().toISOString();
408
+ atomicWrite(node._filepath, safeStringify(content, data));
409
+ } catch {
410
+ // Skip if we can't update
411
+ }
412
+ }
413
+ }
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { logger } from './logger.mjs';
5
+
6
+ /**
7
+ * Automatically detects direct manual additions/modifications to any compiled
8
+ * instruction files or shims in the active IDE workspace or agent directory.
9
+ * Converts any custom additions into permanent SSSS memory vault invariants
10
+ * with priority: absolute and modality: must, ensuring they are never overwritten.
11
+ *
12
+ * @param {object} params
13
+ * @param {string} params.vaultDir
14
+ * @param {string} params.derivedDir
15
+ * @param {string} params.instructionsFile
16
+ * @returns {{ imported: number, slugs: string[] }}
17
+ */
18
+ export function protectIDEInstructions({ vaultDir, derivedDir, instructionsFile }) {
19
+ try {
20
+ const backupFile = path.join(derivedDir, 'INSTRUCTIONS.system.md');
21
+
22
+ // If no backup of system-compiled instructions exists, we cannot perform diff analysis
23
+ if (!fs.existsSync(backupFile)) {
24
+ return { imported: 0, slugs: [] };
25
+ }
26
+
27
+ const systemContent = fs.readFileSync(backupFile, 'utf8').trim();
28
+ if (!systemContent) {
29
+ return { imported: 0, slugs: [] };
30
+ }
31
+
32
+ const systemLinesSet = new Set(systemContent.split('\n').map(l => l.trim()));
33
+
34
+ // Collect possible instruction/shim files in both compile target and active workspace cwd
35
+ const candidates = [
36
+ instructionsFile,
37
+ path.join(process.cwd(), 'INSTRUCTIONS.md'),
38
+ path.join(process.cwd(), '.cursorrules'),
39
+ path.join(process.cwd(), 'GEMINI.md'),
40
+ path.join(process.cwd(), 'CLAUDE.md'),
41
+ path.join(process.cwd(), '.clauderules'),
42
+ path.join(process.cwd(), 'AGENTS.md'),
43
+ path.join(process.cwd(), 'WINDSURF.md'),
44
+ path.join(process.cwd(), '.windsurfrules')
45
+ ];
46
+
47
+ // Resolve symbolic links to obtain unique real file paths
48
+ const uniquePaths = new Set();
49
+ for (const f of candidates) {
50
+ if (!fs.existsSync(f)) continue;
51
+ try {
52
+ const real = fs.realpathSync(f);
53
+ uniquePaths.add(real);
54
+ } catch {
55
+ uniquePaths.add(path.resolve(f));
56
+ }
57
+ }
58
+
59
+ const importedSlugs = [];
60
+
61
+ for (const file of uniquePaths) {
62
+ let content;
63
+ try {
64
+ content = fs.readFileSync(file, 'utf8').trim();
65
+ } catch {
66
+ continue;
67
+ }
68
+
69
+ if (!content || content === systemContent) {
70
+ continue;
71
+ }
72
+
73
+ // Split by line to identify manual additions
74
+ const currentLines = content.split('\n').map(l => l.trim());
75
+ const newLines = [];
76
+
77
+ for (const line of currentLines) {
78
+ if (!line) continue;
79
+
80
+ // Skip lines that exist in the system-generated template
81
+ if (systemLinesSet.has(line)) continue;
82
+
83
+ // Skip comments and tables, but preserve custom headers and blockquotes
84
+ if (line.startsWith('<!--')) continue;
85
+ if (line.includes('BEGIN INJECTED MEMORY') || line.includes('END INJECTED MEMORY')) continue;
86
+ if (line.startsWith('|') && line.includes('Topic')) continue; // Table header
87
+ if (line.startsWith('|') && line.includes('---')) continue; // Table separator
88
+ if (line.startsWith('|') && line.includes('.md')) continue; // Table row
89
+
90
+ newLines.push(line);
91
+ }
92
+
93
+ if (newLines.length === 0) {
94
+ continue;
95
+ }
96
+
97
+ // Group manual rules and create a stable node slug using text hash
98
+ const cleanInstructionText = newLines.join('\n');
99
+ const contentHash = crypto.createHash('sha256').update(cleanInstructionText).digest('hex').slice(0, 8);
100
+ const slug = `inviolable-ide-instruction-${contentHash}`;
101
+
102
+ const nodeFile = path.join(vaultDir, 'invariants', `${slug}.md`);
103
+
104
+ // If the node already exists, we do not duplicate it
105
+ if (fs.existsSync(nodeFile)) {
106
+ continue;
107
+ }
108
+
109
+ // Ensure invariants folder exists
110
+ fs.mkdirSync(path.join(vaultDir, 'invariants'), { recursive: true });
111
+
112
+ const firstLineClean = newLines[0]
113
+ .replace(/^[-*•+\d.]\s+/, '') // strip bullet markers
114
+ .replace(/"/g, '\\"')
115
+ .slice(0, 50);
116
+
117
+ const nodeContent = `---
118
+ type: memory
119
+ slug: ${slug}
120
+ title: "Inviolable IDE Instruction: ${firstLineClean}"
121
+ category: invariants
122
+ status: active
123
+ priority: absolute
124
+ modality: must
125
+ confidence: 1.0
126
+ importance: critical
127
+ tags:
128
+ - inviolable
129
+ - ide-edit
130
+ created: "${new Date().toISOString()}"
131
+ updated: "${new Date().toISOString()}"
132
+ ---
133
+
134
+ ${cleanInstructionText}
135
+ `;
136
+
137
+ fs.writeFileSync(nodeFile, nodeContent, 'utf8');
138
+ importedSlugs.push(slug);
139
+
140
+ logger.info('kernel', `Auto-captured manual IDE instruction edit in ${path.basename(file)}! Promoted new rule to canonical invariant: ${slug}`);
141
+ }
142
+
143
+ return { imported: importedSlugs.length, slugs: importedSlugs };
144
+ } catch (err) {
145
+ logger.error('kernel', `Failed to run IDE instruction protection check: ${err.message}`);
146
+ return { imported: 0, slugs: [] };
147
+ }
148
+ }