total-recall-brain 3.33.0 → 3.33.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.
@@ -334,7 +334,13 @@ Connected instruction files contain a small routing bootstrap. Before action,
334
334
  Activation uses existing validated memory tags, not semantic inference or storage
335
335
  scope. Unknown applicability stays required; oversized required sets report
336
336
  not-ready status. Contributions are admitted whole under complete-render token
337
- estimates. User-authored text outside generated blocks remains intact.
337
+ estimates. The 4,000-token default counts the complete serialized response. CLI
338
+ output defaults to text; JSON, supporting knowledge and diagnostics are explicit
339
+ options. A local `context:policy` decision can assign concise directives/actions
340
+ to exact canonical source fingerprints. Invalid, conflicting, expired or stale
341
+ entries fall back to the full original rules. Canonical bodies stay unchanged and
342
+ private curation is excluded from scaffolds. Static surfaces use the same policy.
343
+ User-authored text outside generated blocks remains intact.
338
344
 
339
345
  `recall` defaults to local retrieval. Compilation builds `local-search.json` as a
340
346
  disposable full-text posting/locator projection; matching documents are hydrated
@@ -90,22 +90,35 @@ Autonomously learn and save a new memory node to the vault.
90
90
  ---
91
91
 
92
92
  ### `context`
93
- Assemble required rules and supporting knowledge for the current task before action.
93
+ Assemble applicable required rules for the current task before action.
94
94
 
95
95
  ```sh
96
- total-recall context "task description" --action edit,test --format json
96
+ total-recall context "task description" --action edit,test
97
97
  ```
98
98
 
99
- The CLI defaults to 16,000 estimated tokens for conservative uncurated rule sets;
100
- `--budget 4000` selects a smaller explicit budget. The API defaults to 4,000.
99
+ CLI and API default to 4,000 estimated tokens. The CLI defaults to compact text;
100
+ `--format json` returns a compact machine-readable response. Supporting documents
101
+ are opt-in (`--knowledge`; API `include_knowledge: true`). Diagnostic inventories
102
+ require `--debug --format json` (API `debug: true`), within the same budget.
101
103
  Unknown applicability remains required. `ready: false` and exit 2 mean the full
102
- required set exceeds the budget; never act from a truncated capsule. All headings
103
- and separators count. Refresh on task/action/project/rule/skill changes.
104
+ complete output exceeds the budget; never act from a truncated capsule. Status,
105
+ metadata, JSON escaping, headings and separators all count. Refresh on task/action/project/rule/skill changes.
104
106
  Validated memory tags `context:universal` and `context:action:<action>` curate
105
107
  activation separately from modality. Unknown actions activate rules conservatively.
106
108
  Supported actions: read, edit, test, build, publish, deploy, secrets, network,
107
109
  memory, skills and project. The local CLI works without a server.
108
110
 
111
+ For explicit local curation, save one project `decision` through the CLI with
112
+ tag `context:policy`. Its JSON body contains `rules`, keyed by `project:<slug>`
113
+ or `global:<slug>`. Each entry has `source_hash` (from debug `curation_sources`),
114
+ `actions` (supported actions or `universal`) and a manually verified `directive`.
115
+ Keep every operative constraint; move history and examples to the original,
116
+ retrievable rule. A deliberate local exclusion uses `enabled: false` and a
117
+ nonempty `reason`. Original bodies and global rules remain intact.
118
+ Malformed, conflicting, expired or source-mismatched curation falls back to the
119
+ canonical rule. Never generate applicability from semantic ranking. Curation
120
+ is private runtime state and must not be included in scaffolds or releases.
121
+
109
122
  ### `recall`
110
123
  Search local metadata and indexed full text across enabled memory layers.
111
124
  - **What it does**: Searches a disposable local index and reads only selected Markdown documents. Semantic enrichment is opt-in and exact hits bypass providers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.33.0",
3
+ "version": "3.33.1",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -2,20 +2,23 @@ import path from 'node:path';
2
2
  import { getBothBrains, parseLayerFlag } from './agent-dir.mjs';
3
3
  import { getNodes } from '../core/vault-cache.mjs';
4
4
  import { mergeGlobalRuleNodes, legacyRuleContributions } from '../core/surface.mjs';
5
- import { compileContext } from '../core/context-compiler.mjs';
5
+ import { compileContext, capsuleResponse, renderCapsule } from '../core/context-compiler.mjs';
6
6
  import { isBrainEnabled } from '../core/brain-registry.mjs';
7
7
  import { surfaceInputsHash } from '../core/command-surface.mjs';
8
8
 
9
9
  export default async function context(args = []) {
10
10
  if (!args.length || args.includes('--help')) {
11
- console.log('Usage: total-recall context "task" [--action edit,test,publish] [--budget 16000] [--format json|text] [--project|--global]\nLocal only. Unknown applicability is required. A required-set overflow exits 2: do not act until a complete capsule fits. Refresh on task/action/project or policy version changes.');
11
+ console.log('Usage: total-recall context "task" [--action edit,test,publish] [--budget 4000] [--format text|json] [--knowledge] [--debug] [--project|--global]\nLocal rules only by default. Unknown applicability is required. Overflow exits 2; do not act until the complete output fits. --knowledge adds supporting documents; --debug exposes inventories within the same budget. Refresh on task/action/project or policy changes.');
12
12
  return;
13
13
  }
14
14
  const { layer, remainingArgs } = parseLayerFlag(args);
15
15
  const query = remainingArgs[0];
16
- let actions = [], budget = 16000, format = 'json';
16
+ let actions = [], budget = 4000, format = 'text', includeKnowledge = false, debug = false;
17
17
  for (let i = 1; i < remainingArgs.length; i++) {
18
- const flag = remainingArgs[i], value = remainingArgs[++i];
18
+ const flag = remainingArgs[i];
19
+ if (flag === '--knowledge') { includeKnowledge = true; continue; }
20
+ if (flag === '--debug') { debug = true; continue; }
21
+ const value = remainingArgs[++i];
19
22
  if (flag === '--action') actions = value?.split(',').filter(Boolean) || [];
20
23
  else if (flag === '--budget') budget = Number(value);
21
24
  else if (flag === '--format' && ['json', 'text'].includes(value)) format = value;
@@ -28,8 +31,10 @@ export default async function context(args = []) {
28
31
  const nodes = own ? mergeGlobalRuleNodes(getNodes(path.join(own, 'memory-vault')), global ? getNodes(path.join(global, 'memory-vault')) : []) :
29
32
  getNodes(path.join(global, 'memory-vault')).map(n => ({ ...n, _layer: 'global' }));
30
33
  const skillsDir = path.dirname(own || global);
31
- const result = await compileContext({ query, actions, nodes, budget: { total: budget }, projectRoot: process.cwd(),
34
+ const result = await compileContext({ query, actions, nodes, includeKnowledge, budget: { total: budget }, projectRoot: process.cwd(),
32
35
  contributions: legacyRuleContributions(skillsDir), versionInputs: surfaceInputsHash({ skillsDir }) });
33
- console.log(format === 'json' ? JSON.stringify(result, null, 2) : `${result.ready ? 'READY' : 'NOT READY: required instructions exceed budget'}\n${result.context}`);
34
- if (!result.ready) process.exitCode = 2;
36
+ if (debug && format === 'text') throw new Error('--debug requires --format json');
37
+ const response = capsuleResponse(result, { total: budget, debug, format });
38
+ console.log(renderCapsule(response, format));
39
+ if (!response.ready) process.exitCode = 2;
35
40
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { getNodes } from './vault-cache.mjs';
4
4
  import crypto from 'node:crypto';
5
- import { selectRules, assembleContext } from './context-policy.mjs';
5
+ import { selectRules, assembleContext, curatedRules, ruleFingerprint, RULE_CATEGORIES } from './context-policy.mjs';
6
6
  import {
7
7
  getEmbedding,
8
8
  cosineSimilarity,
@@ -69,21 +69,21 @@ function temporalScore(node) {
69
69
  export async function compileContext({
70
70
  query = '', vaultDir, derivedDir, budget = {}, consumer = 'api',
71
71
  momentumSlugs = [], actions = [], projectRoot = process.cwd(), semantic = false,
72
- nodes, contributions = [], versionInputs = '',
72
+ nodes, contributions = [], versionInputs = '', includeKnowledge = false,
73
73
  } = {}) {
74
74
  const startTime = Date.now();
75
75
  const b = { ...DEFAULT_BUDGET, total: 4000, ...budget };
76
76
  const allNodes = (nodes || getNodes(vaultDir)).map(n => ({ ...n }));
77
- const rules = selectRules(allNodes, { actions, projectRoot });
77
+ const rules = selectRules(curatedRules(allNodes), { actions, projectRoot });
78
78
  const required = rules.map(n => ({ id: `${n._layer || 'project'}:${n.slug}`, required: true,
79
- text: `## ${n.title || n.slug} [${n.slug}]\n\n${(n.body || n.content || '').trim()}` }));
79
+ text: n._directive ? `[${n.slug}] ${n._directive}` : `## ${n.title || n.slug} [${n.slug}]\n\n${(n.body || n.content || '').trim()}` }));
80
80
  let queryEmbedding = null, embeddingsIndex = {};
81
- if (semantic && query) {
81
+ if (includeKnowledge && semantic && query) {
82
82
  try { queryEmbedding = await getEmbedding(String(query)); embeddingsIndex = loadEmbeddingsIndex(derivedDir); } catch { /* local ranking remains usable */ }
83
83
  }
84
84
  const terms = String(query).toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [];
85
85
  const selectedIds = new Set(rules.map(n => n.slug));
86
- const candidates = allNodes.filter(n => n.status === 'active' && !selectedIds.has(n.slug) &&
86
+ const candidates = (includeKnowledge ? allNodes : []).filter(n => n.status === 'active' && !n.tags?.includes('context:policy') && !selectedIds.has(n.slug) &&
87
87
  !['invariants', 'preferences', 'anti-patterns'].includes(n.category)).map(n => {
88
88
  const text = `${n.title || ''} ${n.body || n.content || ''}`.toLowerCase();
89
89
  const overlap = terms.filter(term => text.includes(term)).length;
@@ -98,10 +98,13 @@ export async function compileContext({
98
98
  result.stats.compile_ms = Date.now() - startTime;
99
99
  result.stats.consumer = consumer;
100
100
  result.stats.actions = actions;
101
+ result.stats.curation_sources = allNodes.filter(n => RULE_CATEGORIES.has(n.category)).map(n => ({
102
+ id: `${n._layer || 'project'}:${n.slug}`, source_hash: ruleFingerprint(n),
103
+ }));
101
104
  result.stats.unresolved_contradictions = rules.flatMap(node => (node.contradicts || [])
102
105
  .filter(slug => rules.some(other => other.slug === slug)).map(slug => [node.slug, slug]));
103
106
  result.stats.version = crypto.createHash('sha256').update(JSON.stringify({
104
- policy: 1, projectRoot, actions, versionInputs, nodes: allNodes, capsule: result.stats.version,
107
+ policy: 2, projectRoot, actions, includeKnowledge, versionInputs, nodes: allNodes, capsule: result.stats.version,
105
108
  })).digest('hex');
106
109
  result.stats.mode = queryEmbedding ? 'semantic' : 'local';
107
110
  result.stats.slots = { invariants: { nodes: rules.filter(n => n.category === 'invariants').length,
@@ -110,6 +113,35 @@ export async function compileContext({
110
113
  return result;
111
114
  }
112
115
 
116
+ export function renderCapsule(result, format = 'json') {
117
+ return format === 'text' ? `ready:${result.ready} tokens:${result.stats.total_tokens} version:${result.stats.version}\n${result.context}` : JSON.stringify(result);
118
+ }
119
+
120
+ /** Budget the actual wire representation, including metadata, escaping and status. */
121
+ export function capsuleResponse(result, { total = 4000, debug = false, format = 'json' } = {}) {
122
+ if (!Number.isFinite(total) || total < 1) throw new Error('Context total must be a positive finite token estimate');
123
+ const stats = debug ? { ...result.stats } : {
124
+ version: result.stats.version, mode: result.stats.mode,
125
+ token_measurement: 'estimated_chars_divided_by_four',
126
+ required_count: result.stats.required_ids.length, body_tokens: estimateTokens(result.context),
127
+ };
128
+ const response = { ready: result.ready, context: result.context, stats };
129
+ stats.budget = total;
130
+ // Counts affect serialization themselves. Iterate to a stable representation.
131
+ for (let i = 0; i < 20; i++) {
132
+ const previous = renderCapsule(response, format);
133
+ stats.total_tokens = estimateTokens(previous + '\n');
134
+ if (debug) {
135
+ stats.budget_used = stats.total_tokens;
136
+ stats.budget_remaining = Math.max(0, total - stats.total_tokens);
137
+ }
138
+ stats.overflow_tokens = Math.max(0, stats.total_tokens - total);
139
+ response.ready = result.ready && stats.overflow_tokens === 0;
140
+ if (renderCapsule(response, format) === previous) return response;
141
+ }
142
+ throw new Error('Context output accounting did not converge');
143
+ }
144
+
113
145
  /**
114
146
  * Lightweight variant that returns pre-scored candidates without embedding lookup.
115
147
  * Used for fast context previews and budget estimation.
@@ -5,13 +5,40 @@ export const RULE_CATEGORIES = new Set(['invariants', 'preferences', 'anti-patte
5
5
  export const CONTEXT_ACTIONS = new Set(['read', 'edit', 'test', 'build', 'publish', 'deploy', 'secrets', 'network', 'memory', 'skills', 'project']);
6
6
  export const estimateTokens = text => Math.ceil(String(text || '').length / 4);
7
7
 
8
+ /** Curation is local, explicit and invalidated by any canonical rule change. */
9
+ export const ruleFingerprint = node => crypto.createHash('sha256').update(JSON.stringify(
10
+ Object.fromEntries(Object.entries(node).filter(([key]) => !key.startsWith('_')).sort(([a], [b]) => a.localeCompare(b))),
11
+ )).digest('hex');
12
+
13
+ export function curatedRules(nodes, now = Date.now()) {
14
+ const policies = nodes.filter(n => n._layer !== 'global' && n.category === 'decisions' && n.status === 'active' &&
15
+ !n.superseded_by && !(n.expires_at && Date.parse(n.expires_at) <= now) && n.tags?.includes('context:policy'));
16
+ // Multiple policies are ambiguous, never pick a winner by order or relevance.
17
+ if (policies.length !== 1) return nodes;
18
+ let rules;
19
+ try { rules = JSON.parse(policies[0].body || '').rules; } catch { return nodes; }
20
+ if (!rules || typeof rules !== 'object' || Array.isArray(rules)) return nodes;
21
+ return nodes.map(node => {
22
+ const entry = rules[`${node._layer || 'project'}:${node.slug}`];
23
+ if (!RULE_CATEGORIES.has(node.category) || !entry || entry.source_hash !== ruleFingerprint(node)) return node;
24
+ if (entry.enabled === false && typeof entry.reason === 'string' && entry.reason.trim()) return { ...node, _contextExcluded: true };
25
+ if (entry.enabled !== undefined && entry.enabled !== true) return node;
26
+ if (
27
+ !Array.isArray(entry.actions) || !entry.actions.length || entry.actions.some(a => a !== 'universal' && !CONTEXT_ACTIONS.has(a)) ||
28
+ typeof entry.directive !== 'string' || !entry.directive.trim()) return node;
29
+ const tags = (node.tags || []).filter(t => !t.startsWith('context:action:') && t !== 'context:universal');
30
+ return { ...node, tags: [...tags, ...entry.actions.map(a => a === 'universal' ? 'context:universal' : `context:action:${a}`)],
31
+ _directive: entry.directive.trim() };
32
+ });
33
+ }
34
+
8
35
  /** Applicability is explicit validated memory tags, independent of obligation.
9
36
  * Unknown rules remain required in task capsules. No inference from prose.
10
37
  */
11
38
  export function selectRules(nodes, { actions = [], projectRoot = process.cwd(), bootstrap = false, now = Date.now() } = {}) {
12
39
  const repo = path.basename(projectRoot).toLowerCase();
13
40
  const applicable = nodes.filter(node => {
14
- if (!RULE_CATEGORIES.has(node.category) || node.status !== 'active' || node.superseded_by) return false;
41
+ if (!RULE_CATEGORIES.has(node.category) || node.status !== 'active' || node.superseded_by || node._contextExcluded) return false;
15
42
  if (node.expires_at && Date.parse(node.expires_at) <= now) return false;
16
43
  if (node.repos?.length && !node.repos.some(r => String(r).toLowerCase() === repo)) return false;
17
44
  if (node.project && String(node.project).toLowerCase() !== repo) return false;
@@ -18,7 +18,7 @@ import { brainDir as globalBrainDir, globalAgentDir } from './config.mjs';
18
18
  import { listSurfaceCommands, buildCommandsSection, surfaceInputsHash } from './command-surface.mjs';
19
19
  import { fileURLToPath } from 'url';
20
20
  import { buildLocalSearchIndex } from './fast-recall.mjs';
21
- import { selectRules, assembleContext } from './context-policy.mjs';
21
+ import { selectRules, assembleContext, curatedRules } from './context-policy.mjs';
22
22
 
23
23
  // Long-lived processes (server, daemon, vault watcher) import this module once.
24
24
  // If the rule builder is edited or upgraded after they start, their in-memory
@@ -395,14 +395,14 @@ export async function buildRulesBlock(skillsDir, nodes = [], {
395
395
  consumer = 'ide', derivedDir, vaultDir, projectRoot, actions = [], bootstrap = false, total = bootstrap ? 1000 : 4000,
396
396
  } = {}) {
397
397
  const root = projectRoot || (skillsDir ? path.dirname(path.dirname(skillsDir)) : process.cwd());
398
- const rules = selectRules(nodes, { actions, projectRoot: root, bootstrap });
398
+ const rules = selectRules(curatedRules(nodes), { actions, projectRoot: root, bootstrap });
399
399
  const contributions = [];
400
400
  const counts = category => rules.filter(n => n.category === category).length;
401
401
  const header = bootstrap ? `## Total Recall task routing\n\nBefore taking action, run \`total-recall context "task description" --action <actions>\` using the installed CLI (in a source checkout: \`node bin/total-recall.mjs context ...\`). Read the complete returned capsule and require ready:true. Unknown rule applicability is conservatively required. If it exits 2, stop: the required set exceeds its budget; inspect overflow and explicitly raise the budget or curate applicability before action.\n\nRefresh at every changed task, action or project, and after a memory/skill edit. Do not infer applicability from semantic similarity. CLI retrieval is local and works without the server. Skills have compact entrypoints; load only relevant references. Canonical vault writes use the validated CLI.\n\nDo not act from this bootstrap alone.\n` :
402
402
  `## Active Rules: ${counts('invariants')} invariants, ${counts('preferences')} preferences, ${counts('anti-patterns')} corrections`;
403
403
  contributions.push({ id: 'routing', text: header, required: true });
404
404
  for (const node of rules) contributions.push({ id: `${node._layer || 'project'}:${node.slug}`, required: true,
405
- text: `### ${node.title || node.slug} [${node.slug}]\n\n${node.body || node.content || ''}` });
405
+ text: node._directive ? `[${node.slug}] ${node._directive}` : `### ${node.title || node.slug} [${node.slug}]\n\n${node.body || node.content || ''}` });
406
406
  if (!bootstrap) {
407
407
  if (skillsDir) {
408
408
  contributions.push(...legacyRuleContributions(skillsDir));
@@ -16,13 +16,15 @@ const router = Router();
16
16
 
17
17
  router.post('/api/context', requireAuth, requireScope('memory:read'), async (req, res) => {
18
18
  try {
19
- const { compileContext } = await import('../../core/context-compiler.mjs');
20
- const { query, budget, momentum_slugs, actions } = req.body || {};
19
+ const { compileContext, capsuleResponse } = await import('../../core/context-compiler.mjs');
20
+ const { query, budget, momentum_slugs, actions, include_knowledge, debug } = req.body || {};
21
+ if (include_knowledge !== undefined && typeof include_knowledge !== 'boolean') return badRequest(res, 'include_knowledge must be boolean');
22
+ if (debug !== undefined && typeof debug !== 'boolean') return badRequest(res, 'debug must be boolean');
21
23
  if (actions !== undefined && (!Array.isArray(actions) || actions.some(action => typeof action !== 'string'))) return badRequest(res, 'actions must be an array of strings');
22
24
  const vaultDir = resolveVaultFromQuery(req, { strict: true });
23
25
  const { derivedDir, skillsDir } = pathsForVault(vaultDir);
24
26
  const globalVault = path.join(globalBrainDir, 'memory-vault');
25
- const nodes = path.resolve(vaultDir) === path.resolve(globalVault) ? getNodes(vaultDir) :
27
+ const nodes = path.resolve(vaultDir) === path.resolve(globalVault) ? getNodes(vaultDir).map(n => ({ ...n, _layer: 'global' })) :
26
28
  mergeGlobalRuleNodes(getNodes(vaultDir), getNodes(globalVault));
27
29
  const result = await compileContext({
28
30
  query: query || '',
@@ -32,12 +34,13 @@ router.post('/api/context', requireAuth, requireScope('memory:read'), async (req
32
34
  consumer: 'api',
33
35
  momentumSlugs: momentum_slugs || [],
34
36
  actions: actions || [],
37
+ includeKnowledge: include_knowledge === true,
35
38
  nodes,
36
39
  projectRoot: path.dirname(path.dirname(skillsDir)),
37
40
  contributions: legacyRuleContributions(skillsDir),
38
41
  versionInputs: surfaceInputsHash({ skillsDir }),
39
42
  });
40
- res.json(result);
43
+ res.json(capsuleResponse(result, { total: budget?.total ?? 4000, debug: debug === true }));
41
44
  } catch (err) {
42
45
  serverError(res, err);
43
46
  }