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.
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/reference/cli-reference.md +19 -6
- package/package.json +1 -1
- package/src/cli/context.mjs +12 -7
- package/src/core/context-compiler.mjs +39 -7
- package/src/core/context-policy.mjs +28 -1
- package/src/core/surface.mjs +3 -3
- package/src/server/routes/context.mjs +7 -4
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
96
|
+
total-recall context "task description" --action edit,test
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
`--
|
|
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
|
-
|
|
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
package/src/cli/context.mjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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]
|
|
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
|
-
|
|
34
|
-
|
|
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:
|
|
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;
|
package/src/core/surface.mjs
CHANGED
|
@@ -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
|
}
|