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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/antigravity.mjs +212 -0
- package/bin/init.mjs +36 -0
- package/bin/oci-sniper.sh +69 -0
- package/bin/total-recall.mjs +229 -0
- package/docs/ARCHITECTURE.md +251 -0
- package/docs/IDE_INTEGRATION.md +112 -0
- package/package.json +71 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
- package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
- package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
- package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
- package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
- package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
- package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
- package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
- package/src/cli/agent-dir.mjs +105 -0
- package/src/cli/backup.mjs +313 -0
- package/src/cli/brain.mjs +224 -0
- package/src/cli/chat.mjs +74 -0
- package/src/cli/config.mjs +247 -0
- package/src/cli/connect.mjs +872 -0
- package/src/cli/daemon.mjs +228 -0
- package/src/cli/deploy-ui.mjs +1793 -0
- package/src/cli/deploy.mjs +1726 -0
- package/src/cli/doctor.mjs +254 -0
- package/src/cli/dream.mjs +80 -0
- package/src/cli/forget.mjs +92 -0
- package/src/cli/friction.mjs +21 -0
- package/src/cli/generate-pat.mjs +65 -0
- package/src/cli/hash-password.mjs +22 -0
- package/src/cli/help.mjs +153 -0
- package/src/cli/import-rules.mjs +102 -0
- package/src/cli/ingest/index.mjs +318 -0
- package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
- package/src/cli/ingest/parsers/google-keep.mjs +147 -0
- package/src/cli/ingest/parsers/search-history.mjs +142 -0
- package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
- package/src/cli/ingest/utils/dedup.mjs +69 -0
- package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
- package/src/cli/ingest.mjs +102 -0
- package/src/cli/init.mjs +736 -0
- package/src/cli/integration-dispatcher.mjs +128 -0
- package/src/cli/lint.mjs +166 -0
- package/src/cli/map.mjs +194 -0
- package/src/cli/migrate.mjs +87 -0
- package/src/cli/rebuild.mjs +88 -0
- package/src/cli/recall.mjs +219 -0
- package/src/cli/relay.mjs +518 -0
- package/src/cli/remember.mjs +306 -0
- package/src/cli/research.mjs +381 -0
- package/src/cli/reset-password.mjs +75 -0
- package/src/cli/restore.mjs +135 -0
- package/src/cli/setup.mjs +616 -0
- package/src/cli/share.mjs +180 -0
- package/src/cli/skill.mjs +313 -0
- package/src/cli/snapshot.mjs +108 -0
- package/src/cli/start.mjs +63 -0
- package/src/cli/status.mjs +296 -0
- package/src/cli/sync.mjs +383 -0
- package/src/cli/uninstall.mjs +613 -0
- package/src/cli/upgrade.mjs +141 -0
- package/src/cli/wizard.html +3682 -0
- package/src/core/blackboard.mjs +54 -0
- package/src/core/clarity-rewriter.mjs +770 -0
- package/src/core/conclusion-writer.mjs +259 -0
- package/src/core/config.mjs +277 -0
- package/src/core/conflict-detector.mjs +536 -0
- package/src/core/crypto.mjs +107 -0
- package/src/core/daemon-control.mjs +158 -0
- package/src/core/daemon-loop.mjs +364 -0
- package/src/core/dream.mjs +347 -0
- package/src/core/drift-detector.mjs +143 -0
- package/src/core/embeddings.mjs +632 -0
- package/src/core/emergency-alerts.mjs +137 -0
- package/src/core/evolution.mjs +107 -0
- package/src/core/fact-seeker.mjs +1602 -0
- package/src/core/friction.mjs +95 -0
- package/src/core/import-rules.mjs +308 -0
- package/src/core/inference-engine.mjs +367 -0
- package/src/core/logger.mjs +75 -0
- package/src/core/memory-layers.mjs +96 -0
- package/src/core/migrate.mjs +116 -0
- package/src/core/notifications.mjs +77 -0
- package/src/core/operation-validator.mjs +272 -0
- package/src/core/optimizer.mjs +330 -0
- package/src/core/pattern_detector.mjs +113 -0
- package/src/core/post-mortem.mjs +413 -0
- package/src/core/protect-instructions.mjs +148 -0
- package/src/core/quick-capture.mjs +145 -0
- package/src/core/research-queue.mjs +431 -0
- package/src/core/research.mjs +461 -0
- package/src/core/runtime.mjs +515 -0
- package/src/core/sandbox.mjs +163 -0
- package/src/core/scheduler.mjs +569 -0
- package/src/core/schema.mjs +333 -0
- package/src/core/search.mjs +219 -0
- package/src/core/semantic-index.mjs +276 -0
- package/src/core/session-watcher.mjs +850 -0
- package/src/core/snapshot.mjs +133 -0
- package/src/core/source-adapters.mjs +1038 -0
- package/src/core/steering.mjs +137 -0
- package/src/core/surface.mjs +444 -0
- package/src/core/task_runner.mjs +125 -0
- package/src/core/tts.mjs +142 -0
- package/src/core/usage-tracker.mjs +349 -0
- package/src/core/vault-cache.mjs +76 -0
- package/src/core/vault.mjs +273 -0
- package/src/core/watchdog.mjs +236 -0
- package/src/server/api.mjs +944 -0
- package/src/server/auth.mjs +410 -0
- package/src/server/index.mjs +868 -0
- package/src/server/keys.mjs +181 -0
- package/src/server/rest.mjs +1652 -0
- package/src/server/route-manifest.json +77 -0
- package/src/server/routes/_shared.mjs +93 -0
- package/src/server/routes/keys.mjs +55 -0
- package/src/server/routes/memory.mjs +249 -0
- package/src/server/routes/sessions.mjs +336 -0
- package/src/server/routes/share.mjs +138 -0
- package/src/server/tools.mjs +618 -0
- package/templates/Caddyfile +37 -0
- package/templates/com.totalrecall.backup.plist +42 -0
- package/templates/com.totalrecall.daemon.plist +40 -0
- package/templates/com.totalrecall.duckdns.plist +23 -0
- package/templates/com.totalrecall.server.plist +46 -0
- package/templates/default-config/budget.yml +6 -0
- package/templates/default-config/runtime.yml +11 -0
- package/templates/default-config/security.yml +53 -0
- package/templates/default-config/voice.yml +34 -0
- package/templates/obsidian-queries/conflicts.md +26 -0
- package/templates/obsidian-queries/daily.md +17 -0
- package/templates/obsidian-queries/index.md +38 -0
- package/templates/obsidian-queries/skills.md +14 -0
- package/templates/onboarding-interview.md +72 -0
- package/templates/skill_engineering.md +40 -0
- package/templates/total-recall-daemon.service +21 -0
- package/templates/total-recall-server.service +20 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import natural from 'natural';
|
|
5
|
+
import { atomicWrite } from './vault.mjs';
|
|
6
|
+
|
|
7
|
+
const JACCARD_THRESHOLD = 0.75;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Jaccard similarity for two sets of tokens.
|
|
11
|
+
*/
|
|
12
|
+
function jaccard(setA, setB) {
|
|
13
|
+
const intersection = new Set([...setA].filter(x => setB.has(x)));
|
|
14
|
+
const union = new Set([...setA, ...setB]);
|
|
15
|
+
if (union.size === 0) return 0;
|
|
16
|
+
return intersection.size / union.size;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Layer 1: Ontology SPO collision check.
|
|
21
|
+
* Checks if Subject, Predicate, and Object match but modality/sentiment flip.
|
|
22
|
+
*/
|
|
23
|
+
export function checkLayer1(candidate, existing) {
|
|
24
|
+
if (
|
|
25
|
+
candidate.subject === existing.subject &&
|
|
26
|
+
candidate.predicate === existing.predicate &&
|
|
27
|
+
candidate.object === existing.object
|
|
28
|
+
) {
|
|
29
|
+
const polarityFlip = candidate.sentiment_polarity !== existing.sentiment_polarity ||
|
|
30
|
+
candidate.modality !== existing.modality;
|
|
31
|
+
if (polarityFlip) {
|
|
32
|
+
return {
|
|
33
|
+
collision: true,
|
|
34
|
+
reason: `Layer 1: Polarity flip on SPO (${candidate.subject} -> ${candidate.predicate} -> ${candidate.object})`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { collision: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Layer 2: Jaccard + Cosine collision check on node body.
|
|
43
|
+
*/
|
|
44
|
+
export function checkLayer2(candidate, existing) {
|
|
45
|
+
const tokenizer = new natural.WordTokenizer();
|
|
46
|
+
const tokensA = new Set(tokenizer.tokenize((candidate.body || '').toLowerCase()));
|
|
47
|
+
const tokensB = new Set(tokenizer.tokenize((existing.body || '').toLowerCase()));
|
|
48
|
+
|
|
49
|
+
const score = jaccard(tokensA, tokensB);
|
|
50
|
+
|
|
51
|
+
if (score >= JACCARD_THRESHOLD) {
|
|
52
|
+
const polarityFlip = candidate.sentiment_polarity !== existing.sentiment_polarity;
|
|
53
|
+
if (polarityFlip) {
|
|
54
|
+
return {
|
|
55
|
+
collision: true,
|
|
56
|
+
similarity: score,
|
|
57
|
+
reason: `Layer 2: Jaccard similarity ${score.toFixed(2)} >= ${JACCARD_THRESHOLD} with polarity flip`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { collision: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run all collision checks for a candidate against the vault.
|
|
66
|
+
*/
|
|
67
|
+
export function detectConflicts(candidate, existingNodes) {
|
|
68
|
+
const conflicts = [];
|
|
69
|
+
|
|
70
|
+
for (const existing of existingNodes) {
|
|
71
|
+
if (existing.status !== 'active') continue;
|
|
72
|
+
|
|
73
|
+
const l1 = checkLayer1(candidate, existing);
|
|
74
|
+
if (l1.collision) {
|
|
75
|
+
conflicts.push({
|
|
76
|
+
type: 'conflict',
|
|
77
|
+
conflict_id: `conflict-${Date.now()}-${crypto.randomInt(0, 1000)}`,
|
|
78
|
+
status: 'pending',
|
|
79
|
+
new_slug: candidate.slug,
|
|
80
|
+
existing_slug: existing.slug,
|
|
81
|
+
similarity: 1.0,
|
|
82
|
+
polarity_flip: true,
|
|
83
|
+
detected_at: new Date().toISOString(),
|
|
84
|
+
reason: l1.reason,
|
|
85
|
+
resolution: null,
|
|
86
|
+
resolved_at: null
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const l2 = checkLayer2(candidate, existing);
|
|
92
|
+
if (l2.collision) {
|
|
93
|
+
conflicts.push({
|
|
94
|
+
type: 'conflict',
|
|
95
|
+
conflict_id: `conflict-${Date.now()}-${crypto.randomInt(0, 1000)}`,
|
|
96
|
+
status: 'pending',
|
|
97
|
+
new_slug: candidate.slug,
|
|
98
|
+
existing_slug: existing.slug,
|
|
99
|
+
similarity: l2.similarity,
|
|
100
|
+
polarity_flip: true,
|
|
101
|
+
detected_at: new Date().toISOString(),
|
|
102
|
+
reason: l2.reason,
|
|
103
|
+
resolution: null,
|
|
104
|
+
resolved_at: null
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return conflicts;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Write conflict record to the memory-inbox/conflicts/ quarantine directory.
|
|
114
|
+
*/
|
|
115
|
+
export function quarantineConflict(conflict, conflictsDir) {
|
|
116
|
+
if (!fs.existsSync(conflictsDir)) {
|
|
117
|
+
fs.mkdirSync(conflictsDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
const filePath = path.join(conflictsDir, `${conflict.conflict_id}.json`);
|
|
120
|
+
atomicWrite(filePath, JSON.stringify(conflict, null, 2));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a conflict.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveConflict(conflictId, resolution, conflictsDir) {
|
|
127
|
+
const filePath = path.join(conflictsDir, `${conflictId}.json`);
|
|
128
|
+
if (!fs.existsSync(filePath)) throw new Error('Conflict not found');
|
|
129
|
+
|
|
130
|
+
const conflict = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
131
|
+
conflict.status = 'resolved';
|
|
132
|
+
conflict.resolution = resolution;
|
|
133
|
+
conflict.resolved_at = new Date().toISOString();
|
|
134
|
+
|
|
135
|
+
atomicWrite(filePath, JSON.stringify(conflict, null, 2));
|
|
136
|
+
return conflict;
|
|
137
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { loadNodes, loadSkills, atomicWrite } from './vault.mjs';
|
|
4
|
+
import {
|
|
5
|
+
buildMemoryLayerIndex,
|
|
6
|
+
inferMemoryLayer,
|
|
7
|
+
} from './memory-layers.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract [[slug]] wikilink references from body text.
|
|
11
|
+
* Native TR link resolution; Obsidian renders them as graph edges.
|
|
12
|
+
*/
|
|
13
|
+
export function extractWikilinks(body) {
|
|
14
|
+
if (!body) return [];
|
|
15
|
+
const matches = body.match(/\[\[([^\]]+)\]\]/g) || [];
|
|
16
|
+
return [...new Set(matches.map(m => m.slice(2, -2).split('|')[0].trim()))];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate an Obsidian Canvas JSON file from active vault nodes.
|
|
21
|
+
* Written to memory-vault/graph.canvas; also readable by /api/graph.
|
|
22
|
+
*/
|
|
23
|
+
function generateCanvas(nodes, vaultDir) {
|
|
24
|
+
const active = nodes.filter(n => n.status === 'active');
|
|
25
|
+
if (active.length === 0) return;
|
|
26
|
+
|
|
27
|
+
const CARD_W = 240, CARD_H = 60, GAP_X = 60, GAP_Y = 30;
|
|
28
|
+
const cols = Math.max(1, Math.ceil(Math.sqrt(active.length)));
|
|
29
|
+
|
|
30
|
+
const canvasNodes = active.map((n, i) => ({
|
|
31
|
+
id: n.slug,
|
|
32
|
+
x: (i % cols) * (CARD_W + GAP_X),
|
|
33
|
+
y: Math.floor(i / cols) * (CARD_H + GAP_Y),
|
|
34
|
+
width: CARD_W,
|
|
35
|
+
height: CARD_H,
|
|
36
|
+
type: 'text',
|
|
37
|
+
text: `**${n.title}**\n_${n.category}_`
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const slugSet = new Set(canvasNodes.map(n => n.id));
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const canvasEdges = [];
|
|
43
|
+
|
|
44
|
+
for (const n of active) {
|
|
45
|
+
const targets = [...(n.related || []), ...extractWikilinks(n.body || '')];
|
|
46
|
+
for (const target of targets) {
|
|
47
|
+
if (!slugSet.has(target) || target === n.slug) continue;
|
|
48
|
+
const key = `${n.slug}→${target}`;
|
|
49
|
+
if (seen.has(key)) continue;
|
|
50
|
+
seen.add(key);
|
|
51
|
+
canvasEdges.push({
|
|
52
|
+
id: `e${canvasEdges.length}`,
|
|
53
|
+
fromNode: n.slug,
|
|
54
|
+
toNode: target,
|
|
55
|
+
fromSide: 'right',
|
|
56
|
+
toSide: 'left'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const canvasPath = path.join(vaultDir, 'graph.canvas');
|
|
62
|
+
atomicWrite(canvasPath, JSON.stringify({ nodes: canvasNodes, edges: canvasEdges }, null, 2));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Legacy injection markers (kept for cleanup of old files) ───
|
|
66
|
+
const INJECTION_BEGIN = '<!-- BEGIN INJECTED MEMORY: do not edit by hand; rebuilt by total-recall surface -->';
|
|
67
|
+
const INJECTION_END = '<!-- END INJECTED MEMORY -->';
|
|
68
|
+
|
|
69
|
+
export function replaceFirstManagedInjectionBlock(raw, injectionBlock) {
|
|
70
|
+
const chunks = raw.match(/[^\n]*(?:\n|$)/g) || [];
|
|
71
|
+
let inFence = false;
|
|
72
|
+
let offset = 0;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
75
|
+
const chunk = chunks[i];
|
|
76
|
+
if (chunk === '') continue;
|
|
77
|
+
const line = chunk.endsWith('\n') ? chunk.slice(0, -1) : chunk;
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
|
|
80
|
+
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
81
|
+
inFence = !inFence;
|
|
82
|
+
offset += chunk.length;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!inFence && line === INJECTION_BEGIN) {
|
|
87
|
+
let endOffset = offset + chunk.length;
|
|
88
|
+
for (let j = i + 1; j < chunks.length; j++) {
|
|
89
|
+
endOffset += chunks[j].length;
|
|
90
|
+
const endLine = chunks[j].endsWith('\n') ? chunks[j].slice(0, -1) : chunks[j];
|
|
91
|
+
if (endLine === INJECTION_END) {
|
|
92
|
+
return `${raw.slice(0, offset)}${injectionBlock}\n${raw.slice(endOffset)}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
offset += chunk.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const DIRECTIVES_BEGIN = '<!-- BEGIN INJECTED ACTIVE DIRECTIVES: do not edit by hand; rebuilt by total-recall surface -->';
|
|
105
|
+
const DIRECTIVES_END = '<!-- END INJECTED ACTIVE DIRECTIVES -->';
|
|
106
|
+
|
|
107
|
+
function extractRuleContent(filePath) {
|
|
108
|
+
if (!fs.existsSync(filePath)) return '';
|
|
109
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
110
|
+
const parts = content.split('---');
|
|
111
|
+
if (parts.length >= 3) {
|
|
112
|
+
return parts.slice(2).join('---').trim();
|
|
113
|
+
}
|
|
114
|
+
return content.trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildRulesBlock(skillsDir, nodes = [], { vaultDir, consumer = 'ide' } = {}) {
|
|
118
|
+
let combined;
|
|
119
|
+
|
|
120
|
+
if (consumer === 'api') {
|
|
121
|
+
// API consumers don't need CLI quickstart docs
|
|
122
|
+
combined = `## Total Recall — Active Memory Context\n\nYour memories and rules are loaded from the active brain vault.\n`;
|
|
123
|
+
} else {
|
|
124
|
+
// CLI quickstart is always included so agents know TR is installed
|
|
125
|
+
combined = `## Total Recall — Sovereign Memory System (Installed)
|
|
126
|
+
|
|
127
|
+
You have Total Recall installed. Use these CLI commands to remember and recall information.
|
|
128
|
+
|
|
129
|
+
### npx total-recall remember <category> "<content>" [options]
|
|
130
|
+
Save rules, preferences, corrections, and facts to permanent memory.
|
|
131
|
+
|
|
132
|
+
**Categories:** invariant, preference, correction, fact, concept, pattern, anti-pattern, decision, lore
|
|
133
|
+
|
|
134
|
+
**Options:**
|
|
135
|
+
--tags, -t <list> Comma-separated tags (e.g. "config,server")
|
|
136
|
+
--importance, -i <1-5> Importance level (default: 3)
|
|
137
|
+
--priority, -p <level> absolute | high | normal | low (default: normal)
|
|
138
|
+
--modality, -m <type> must | must_not | should | should_not | descriptive | preference
|
|
139
|
+
--confidence, -c <0-1> Confidence level (default: 1.0)
|
|
140
|
+
--slug <custom-slug> Custom kebab-case slug
|
|
141
|
+
--title <custom-title> Custom human-readable title
|
|
142
|
+
--status <state> active | draft | archived (default: active)
|
|
143
|
+
--related <list> Comma-separated related slugs
|
|
144
|
+
--global Save to global brain (identity layer)
|
|
145
|
+
--project Save to project brain (context layer)
|
|
146
|
+
|
|
147
|
+
**Examples:**
|
|
148
|
+
npx total-recall remember invariant "Never run tsc directly." --importance 5 --priority absolute
|
|
149
|
+
npx total-recall remember preference "Always use single quotes." --tags "style,js"
|
|
150
|
+
npx total-recall remember fact "The server runs on port 3000." --importance 4
|
|
151
|
+
npx total-recall remember fact "Uses Drizzle ORM" --project
|
|
152
|
+
|
|
153
|
+
### npx total-recall recall "<query>" [options]
|
|
154
|
+
Semantic search across rules, facts, and session history.
|
|
155
|
+
|
|
156
|
+
**Options:**
|
|
157
|
+
--top-k, -k <number> Results to return (default: 5, max: 20)
|
|
158
|
+
--no-sessions, -ns Exclude session chunks, vault only
|
|
159
|
+
--format, -f <type> text (default) or json
|
|
160
|
+
--category, -cat <name> Filter by SSSS category
|
|
161
|
+
--tags, -t <list> Filter by tags
|
|
162
|
+
--modality, -m <type> Filter by modality
|
|
163
|
+
--importance, -i <1-5> Filter by minimum importance
|
|
164
|
+
--global Search global brain only
|
|
165
|
+
--project Search project brain only
|
|
166
|
+
|
|
167
|
+
**Examples:**
|
|
168
|
+
npx total-recall recall "Never run tsc directly"
|
|
169
|
+
npx total-recall recall "Express server port" --top-k 3
|
|
170
|
+
npx total-recall recall "tsc" --category invariants --modality must
|
|
171
|
+
|
|
172
|
+
### npx total-recall forget <slug> [options]
|
|
173
|
+
Delete a memory node by slug. Auto-recompiles surfaces and propagates to project brains.
|
|
174
|
+
|
|
175
|
+
**Options:**
|
|
176
|
+
--global Delete from global brain
|
|
177
|
+
--project Delete from project brain
|
|
178
|
+
--no-compile Skip auto-recompilation after deletion
|
|
179
|
+
|
|
180
|
+
**Examples:**
|
|
181
|
+
npx total-recall forget old-invariant-slug
|
|
182
|
+
npx total-recall forget stale-fact --global
|
|
183
|
+
npx total-recall forget project-specific-node --project
|
|
184
|
+
|
|
185
|
+
### npx total-recall help <topic>
|
|
186
|
+
Query interactive local documentation, VFS specifications, and command references.
|
|
187
|
+
|
|
188
|
+
**Options:**
|
|
189
|
+
--json, -j Emit machine-readable JSON (ideal for programmatic retrieval)
|
|
190
|
+
|
|
191
|
+
**Examples:**
|
|
192
|
+
npx total-recall help connect
|
|
193
|
+
npx total-recall help architecture
|
|
194
|
+
npx total-recall help ssss
|
|
195
|
+
|
|
196
|
+
### npx total-recall --help
|
|
197
|
+
Show all available commands.
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 1. Filter expired rules and auto-archive them
|
|
202
|
+
const now = new Date();
|
|
203
|
+
const isExpired = (n) => n.expires_at && new Date(n.expires_at) <= now;
|
|
204
|
+
|
|
205
|
+
const expiredNodes = nodes.filter(n => n.status === 'active' && isExpired(n));
|
|
206
|
+
for (const expired of expiredNodes) {
|
|
207
|
+
expired.status = 'deprecated';
|
|
208
|
+
console.error(`⏰ Auto-archived expired rule: ${expired.slug}`);
|
|
209
|
+
if (vaultDir) {
|
|
210
|
+
try {
|
|
211
|
+
const nodePath = path.join(vaultDir, expired.category, `${expired.slug}.md`);
|
|
212
|
+
if (fs.existsSync(nodePath)) {
|
|
213
|
+
let content = fs.readFileSync(nodePath, 'utf8');
|
|
214
|
+
content = content.replace(/^status:\s*active$/m, 'status: deprecated');
|
|
215
|
+
atomicWrite(nodePath, content);
|
|
216
|
+
}
|
|
217
|
+
} catch { /* non-fatal: archive is best-effort */ }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 2. Group active (non-expired) rules from SSSS vault nodes
|
|
222
|
+
const invariants = nodes.filter(n => n.category === 'invariants' && n.status === 'active' && !isExpired(n));
|
|
223
|
+
const preferences = nodes.filter(n => n.category === 'preferences' && n.status === 'active' && !isExpired(n));
|
|
224
|
+
const corrections = nodes.filter(n => n.category === 'anti-patterns' && n.status === 'active' && !isExpired(n));
|
|
225
|
+
|
|
226
|
+
const formatNodes = (list) => {
|
|
227
|
+
return list.map(n => {
|
|
228
|
+
const text = n.body || n.content || '';
|
|
229
|
+
return text.startsWith('-') ? text : `- ${text}`;
|
|
230
|
+
}).join('\n');
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (invariants.length > 0) {
|
|
234
|
+
combined += `\n\n## Invariant Rules\n\n${formatNodes(invariants)}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (preferences.length > 0) {
|
|
238
|
+
combined += `\n\n## User Preferences\n\n${formatNodes(preferences)}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (corrections.length > 0) {
|
|
242
|
+
combined += `\n\n## Corrections\n\n${formatNodes(corrections)}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 2. Append legacy rule sheet files if they exist
|
|
246
|
+
if (skillsDir) {
|
|
247
|
+
const rulesDir = path.join(skillsDir, 'total-recall', 'rules');
|
|
248
|
+
const legacyInvariants = extractRuleContent(path.join(rulesDir, 'invariants.md'));
|
|
249
|
+
const legacyPreferences = extractRuleContent(path.join(rulesDir, 'preferences.md'));
|
|
250
|
+
const legacyCorrections = extractRuleContent(path.join(rulesDir, 'corrections.md'));
|
|
251
|
+
|
|
252
|
+
if (legacyInvariants) {
|
|
253
|
+
combined += `\n\n${legacyInvariants}`;
|
|
254
|
+
}
|
|
255
|
+
if (legacyPreferences) {
|
|
256
|
+
combined += `\n\n${legacyPreferences}`;
|
|
257
|
+
}
|
|
258
|
+
if (legacyCorrections) {
|
|
259
|
+
combined += `\n\n${legacyCorrections}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return combined.trim();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function injectDirectives(fileContent, rulesBlock) {
|
|
267
|
+
const beginIdx = fileContent.indexOf(DIRECTIVES_BEGIN);
|
|
268
|
+
const endIdx = fileContent.indexOf(DIRECTIVES_END);
|
|
269
|
+
|
|
270
|
+
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
|
|
271
|
+
return fileContent.slice(0, beginIdx) + DIRECTIVES_BEGIN + '\n' + rulesBlock + '\n' + DIRECTIVES_END + fileContent.slice(endIdx + DIRECTIVES_END.length);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let content = fileContent.trimEnd();
|
|
275
|
+
const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.';
|
|
276
|
+
if (!content.includes('SKILL.md')) {
|
|
277
|
+
content = baseline + '\n\n' + content;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return content.trimEnd() + '\n\n' + DIRECTIVES_BEGIN + '\n' + rulesBlock + '\n' + DIRECTIVES_END + '\n';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Write or update a platform instruction shim with the pointer and active rules.
|
|
285
|
+
*/
|
|
286
|
+
function writeShim(shimPath, skillsDir, nodes = [], { vaultDir } = {}) {
|
|
287
|
+
const shimDir = path.dirname(shimPath);
|
|
288
|
+
const rulesBlock = buildRulesBlock(skillsDir, nodes, { vaultDir });
|
|
289
|
+
const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.\n';
|
|
290
|
+
const fullContent = `${baseline}\n${DIRECTIVES_BEGIN}\n${rulesBlock}\n${DIRECTIVES_END}\n`;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
if (fs.existsSync(shimPath)) {
|
|
294
|
+
const stat = fs.lstatSync(shimPath);
|
|
295
|
+
if (stat.isSymbolicLink()) {
|
|
296
|
+
fs.unlinkSync(shimPath);
|
|
297
|
+
atomicWrite(shimPath, fullContent);
|
|
298
|
+
} else {
|
|
299
|
+
const raw = fs.readFileSync(shimPath, 'utf8');
|
|
300
|
+
let cleaned = raw;
|
|
301
|
+
if (raw.includes(INJECTION_BEGIN)) {
|
|
302
|
+
const replaced = replaceFirstManagedInjectionBlock(raw, '');
|
|
303
|
+
if (replaced !== null) cleaned = replaced;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const updated = injectDirectives(cleaned, rulesBlock);
|
|
307
|
+
atomicWrite(shimPath, updated);
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
if (!fs.existsSync(shimDir)) {
|
|
311
|
+
fs.mkdirSync(shimDir, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
atomicWrite(shimPath, fullContent);
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
// Ignore permission errors
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Map of client names → shim file paths they require.
|
|
322
|
+
*/
|
|
323
|
+
const CLIENT_SHIMS = {
|
|
324
|
+
cursor: ['.cursorrules'],
|
|
325
|
+
'claude-code': ['CLAUDE.md', '.clauderules'],
|
|
326
|
+
antigravity: ['AGENTS.md'],
|
|
327
|
+
gemini: ['GEMINI.md'],
|
|
328
|
+
codex: ['CODEX.md', '.codexrules'],
|
|
329
|
+
vscode: ['.github/copilot-instructions.md', '.vscode/copilot-instructions.md'],
|
|
330
|
+
pi: ['AGENTS.md'],
|
|
331
|
+
aider: ['.aider.rules.md'],
|
|
332
|
+
windsurf: ['.windsurfrules', 'WINDSURF.md'],
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Read the set of connected IDE clients from config/clients.json.
|
|
337
|
+
* Returns a Set of client name strings, or null if the file is
|
|
338
|
+
* missing / empty / malformed (null signals "write all shims").
|
|
339
|
+
*/
|
|
340
|
+
function readConnectedClients(clientsPath) {
|
|
341
|
+
try {
|
|
342
|
+
if (!fs.existsSync(clientsPath)) return null;
|
|
343
|
+
const raw = fs.readFileSync(clientsPath, 'utf8').trim();
|
|
344
|
+
if (!raw) return null;
|
|
345
|
+
const parsed = JSON.parse(raw);
|
|
346
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
|
347
|
+
return new Set(parsed.map(String));
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Compile pointers with active rules directly into root instruction shims.
|
|
355
|
+
*
|
|
356
|
+
* ALL files (including INSTRUCTIONS.md) are user-owned. TR only injects
|
|
357
|
+
* content between the <!-- BEGIN/END INJECTED ACTIVE DIRECTIVES --> markers,
|
|
358
|
+
* preserving any user-written content outside the markers.
|
|
359
|
+
*
|
|
360
|
+
* Only shims for connected clients (per config/clients.json) are written.
|
|
361
|
+
* If clients.json is absent or unreadable, ALL shims are written (backward compat).
|
|
362
|
+
* INSTRUCTIONS.md is always written as the canonical source.
|
|
363
|
+
*/
|
|
364
|
+
function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir } = {}) {
|
|
365
|
+
const agentDir = path.dirname(instructionsFile);
|
|
366
|
+
const baseDir = path.basename(agentDir) === '.agent' ? path.dirname(agentDir) : agentDir;
|
|
367
|
+
|
|
368
|
+
// Always write the canonical INSTRUCTIONS.md
|
|
369
|
+
writeShim(path.join(baseDir, 'INSTRUCTIONS.md'), skillsDir, nodes, { vaultDir });
|
|
370
|
+
|
|
371
|
+
// Determine which client shims to write
|
|
372
|
+
const clientsPath = path.join(baseDir, '.agent', 'config', 'clients.json');
|
|
373
|
+
const connectedClients = readConnectedClients(clientsPath);
|
|
374
|
+
|
|
375
|
+
if (connectedClients === null) {
|
|
376
|
+
// No clients.json → backward compat: write ALL client shims
|
|
377
|
+
for (const files of Object.values(CLIENT_SHIMS)) {
|
|
378
|
+
for (const file of files) {
|
|
379
|
+
writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
// Only write shims for connected clients
|
|
384
|
+
for (const client of connectedClients) {
|
|
385
|
+
const files = CLIENT_SHIMS[client];
|
|
386
|
+
if (!files) continue;
|
|
387
|
+
for (const file of files) {
|
|
388
|
+
writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Main surface compilation entry point.
|
|
396
|
+
*/
|
|
397
|
+
export async function compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile }) {
|
|
398
|
+
const nodes = loadNodes(vaultDir);
|
|
399
|
+
|
|
400
|
+
// 1. Write pointer and active rules to all instruction shims
|
|
401
|
+
compilePointers(instructionsFile, skillsDir, nodes, { vaultDir });
|
|
402
|
+
|
|
403
|
+
// 2. Build derived indexes (powers semantic search API)
|
|
404
|
+
if (!fs.existsSync(derivedDir)) {
|
|
405
|
+
fs.mkdirSync(derivedDir, { recursive: true });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const graphIndex = nodes.map(n => ({
|
|
409
|
+
slug: n.slug,
|
|
410
|
+
title: n.title,
|
|
411
|
+
category: n.category,
|
|
412
|
+
status: n.status,
|
|
413
|
+
confidence: n.confidence,
|
|
414
|
+
memory_layer: inferMemoryLayer(n)
|
|
415
|
+
}));
|
|
416
|
+
atomicWrite(path.join(derivedDir, 'graph-index.jsonl'), graphIndex.map(n => JSON.stringify(n)).join('\n'));
|
|
417
|
+
atomicWrite(
|
|
418
|
+
path.join(derivedDir, 'memory-layers.jsonl'),
|
|
419
|
+
buildMemoryLayerIndex(nodes).map(n => JSON.stringify(n)).join('\n')
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// 3. Build semantic embeddings index if available (fire-and-forget)
|
|
423
|
+
let semanticResult = { indexed: 0, skipped: nodes.length, unavailable: true };
|
|
424
|
+
try {
|
|
425
|
+
const { buildSemanticIndex } = await import('./semantic-index.mjs');
|
|
426
|
+
semanticResult = await buildSemanticIndex(nodes, derivedDir);
|
|
427
|
+
} catch { /* semantic index is optional — never block compile */ }
|
|
428
|
+
|
|
429
|
+
// 4. Generate Obsidian Canvas (fire-and-forget)
|
|
430
|
+
try {
|
|
431
|
+
generateCanvas(nodes, vaultDir);
|
|
432
|
+
} catch { /* non-fatal */ }
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
nodesProcessed: nodes.length,
|
|
436
|
+
skillsInjected: 0, // v2: no more skill injection
|
|
437
|
+
semanticIndexed: semanticResult.indexed,
|
|
438
|
+
semanticUnavailable: semanticResult.unavailable
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
// ─── Legacy exports (kept for backward compatibility) ───
|
|
442
|
+
export function routeNodesToSkills() { return []; }
|
|
443
|
+
export function injectSkills() {}
|
|
444
|
+
export function compileTier1(nodes, instructionsFile) { compilePointers(instructionsFile); }
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
* Task Runner for Total Recall
|
|
9
|
+
* Reads pending tasks from .agent/scheduler/queue/, sorts by priority, and executes them.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function loadPendingTasks(queueDir) {
|
|
13
|
+
if (!fs.existsSync(queueDir)) return [];
|
|
14
|
+
|
|
15
|
+
const tasks = [];
|
|
16
|
+
const files = fs.readdirSync(queueDir);
|
|
17
|
+
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
if (!file.endsWith('.md')) continue;
|
|
20
|
+
const filePath = path.join(queueDir, file);
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
const { data, content } = matter(raw);
|
|
24
|
+
if (data.type === 'task' && data.status === 'pending') {
|
|
25
|
+
tasks.push({ ...data, body: content.trim(), filepath: filePath });
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logger.warn('task-runner', `Failed to parse task ${file}`, { err: err.message });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Sort by priority (higher priority = lower number)
|
|
33
|
+
return tasks.sort((a, b) => a.priority - b.priority);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function updateTaskStatus(task, newStatus, error = null) {
|
|
37
|
+
const { filepath, body, ...frontmatter } = task;
|
|
38
|
+
frontmatter.status = newStatus;
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
frontmatter.error = error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const raw = safeStringify(body || '', frontmatter);
|
|
45
|
+
atomicWrite(filepath, raw);
|
|
46
|
+
return { ...task, status: newStatus };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Executes a single task.
|
|
51
|
+
*/
|
|
52
|
+
export async function executeTask(task, context) {
|
|
53
|
+
logger.info('task-runner', `Executing Task [P${task.priority}]: ${task.target}`);
|
|
54
|
+
updateTaskStatus(task, 'in_progress');
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// In a real implementation, this would route to specific subagents/workflows
|
|
58
|
+
// based on task.category. For now, we simulate execution.
|
|
59
|
+
switch (task.category) {
|
|
60
|
+
case 'skill-engineering':
|
|
61
|
+
await handleSkillEngineering(task, context);
|
|
62
|
+
break;
|
|
63
|
+
case 'memory-maintenance':
|
|
64
|
+
await handleMemoryMaintenance(task, context);
|
|
65
|
+
break;
|
|
66
|
+
case 'system2-deliberation':
|
|
67
|
+
await handleSystem2Deliberation(task, context);
|
|
68
|
+
break;
|
|
69
|
+
case 'proactive-research':
|
|
70
|
+
logger.info('task-runner', 'Handling Deep Research...');
|
|
71
|
+
const { handleProactiveResearch } = await import('./research.mjs');
|
|
72
|
+
const report = await handleProactiveResearch(task, context);
|
|
73
|
+
task.body = `# Deep Research Report\n\n${report}`;
|
|
74
|
+
break;
|
|
75
|
+
case 'self-evaluation':
|
|
76
|
+
case 'exploration':
|
|
77
|
+
logger.info('task-runner', `Handling ${task.category}...`);
|
|
78
|
+
// Simulated work
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
throw new Error(`Unknown task category: ${task.category}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
updateTaskStatus(task, 'done');
|
|
86
|
+
logger.info('task-runner', `Completed Task: ${task.target}`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.error('task-runner', `Failed Task: ${task.target}`, { err: err.message });
|
|
89
|
+
updateTaskStatus(task, 'failed', err.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handleSkillEngineering(task, context) {
|
|
94
|
+
// E.g., spawn an agent with the template to create a skill
|
|
95
|
+
logger.info('task-runner', `Engineering skill: ${task.target}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleMemoryMaintenance(task, context) {
|
|
99
|
+
logger.info('task-runner', `Running memory maintenance for: ${task.target}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function handleSystem2Deliberation(task, context) {
|
|
103
|
+
logger.info('task-runner', `Running System 2 deliberation for: ${task.target}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Main loop for the task runner.
|
|
108
|
+
*/
|
|
109
|
+
export async function runTaskQueue(queueDir, context) {
|
|
110
|
+
logger.info('task-runner', 'Starting Task Runner...');
|
|
111
|
+
const pending = loadPendingTasks(queueDir);
|
|
112
|
+
|
|
113
|
+
if (pending.length === 0) {
|
|
114
|
+
logger.info('task-runner', 'No pending tasks in queue.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
logger.info('task-runner', `Found ${pending.length} pending tasks.`);
|
|
119
|
+
|
|
120
|
+
for (const task of pending) {
|
|
121
|
+
await executeTask(task, context);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.info('task-runner', 'Task Runner queue empty.');
|
|
125
|
+
}
|