total-recall-brain 3.9.0 → 3.10.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 (45) hide show
  1. package/package.json +6 -5
  2. package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
  3. package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/SKILL.md +3 -2
  4. package/scaffold/.agent/skills/total-recall/skills/{research → tr-research}/SKILL.md +2 -1
  5. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/SKILL.md +3 -2
  6. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/evals/evals.json +3 -3
  7. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-add.sh +1 -1
  8. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-delete.sh +1 -1
  9. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/create-skill.sh +3 -3
  10. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/find-skills.mjs +25 -1
  11. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/install-skill.mjs +117 -0
  12. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/scan-skill.mjs +20 -0
  13. package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/SKILL.md +58 -15
  14. package/src/cli/connect.mjs +6 -59
  15. package/src/cli/init.mjs +42 -2
  16. package/src/cli/skill-projection.mjs +214 -0
  17. package/src/cli/skill.mjs +44 -18
  18. package/src/core/daemon-loop.mjs +13 -2
  19. package/src/core/operation-validator.mjs +19 -1
  20. package/src/core/research.mjs +47 -9
  21. package/src/core/runtime.mjs +14 -7
  22. package/src/core/schema.mjs +23 -1
  23. package/src/server/api.mjs +2 -2
  24. package/src/server/rest.mjs +3 -3
  25. package/src/server/routes/skills.mjs +25 -4
  26. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/agents.yml +0 -0
  27. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/evals/.gitkeep +0 -0
  28. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/antigravity.txt +0 -0
  29. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/claude.txt +0 -0
  30. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/codex.txt +0 -0
  31. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/gemini.txt +0 -0
  32. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/scripts/.gitkeep +0 -0
  33. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/subagents/.gitkeep +0 -0
  34. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/assets/skill-template/SKILL.md +0 -0
  35. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/learnings/.gitkeep +0 -0
  36. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/references/.gitkeep +0 -0
  37. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/.gitkeep +0 -0
  38. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/enforce-skill-optimization.mjs +0 -0
  39. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/skill-watcher.mjs +0 -0
  40. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/subagents/audit-all-skills.md +0 -0
  41. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/evals/evals.json +0 -0
  42. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/references/CHANGELOG.md +0 -0
  43. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/references/ssss-spec.md +0 -0
  44. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/scripts/validate-schema.mjs +0 -0
  45. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/subagents/audit-ssss-node.md +0 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Scope-aware skill → slash-command projection.
3
+ *
4
+ * A "repo skill" is any <skills-root>/<name>/SKILL.md directory (the Agent
5
+ * Skills standard). Each IDE turns such a directory into a /<name> slash
6
+ * command, but every IDE looks in a *different* place, and the right place
7
+ * depends on the *scope* of the skill:
8
+ *
9
+ * PROJECT skills (authored in <repo>/.agent/skills or <repo>/.agents/skills)
10
+ * → project into the *repo's* IDE skill dirs, and only for IDEs actually
11
+ * in use in this repo (project marker dir present, or current process is
12
+ * that IDE). They belong to this repo, not the whole machine.
13
+ *
14
+ * GLOBAL skills (authored in ~/.agent/skills)
15
+ * → project into each IDE's *global* skill dir, for IDEs installed on this
16
+ * machine. They are available in every repo.
17
+ *
18
+ * This mirrors the brain model: `init --project` ⇒ project skills; `init`
19
+ * (global) ⇒ global skills. Codex has no project-local skills dir (it only
20
+ * reads ~/.codex/skills globally), so it participates in GLOBAL projection only;
21
+ * pushing project skills to Codex is opt-in via `connect codex`.
22
+ */
23
+ import fs from 'node:fs';
24
+ import os from 'node:os';
25
+ import path from 'node:path';
26
+
27
+ /**
28
+ * IDE skill descriptors. Each IDE declares how it is detected and where its
29
+ * skills live, per scope:
30
+ * project: { dir, markers, env } — repo-relative dest; detected by repo
31
+ * marker dirs or current-process env vars. null ⇒ no project skills.
32
+ * global: { dir, markers } — home-relative dest; detected by home
33
+ * marker dirs (i.e. the IDE is installed). null ⇒ no global skills.
34
+ */
35
+ export const IDE_SKILLS = [
36
+ {
37
+ id: 'claude-code',
38
+ label: 'Claude Code',
39
+ clients: ['claude-code'],
40
+ project: { dir: ['.claude', 'skills'], markers: ['.claude'], env: ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'] },
41
+ global: { dir: ['.claude', 'skills'], markers: ['.claude'] }
42
+ },
43
+ {
44
+ id: 'agents',
45
+ label: 'Antigravity / Gemini',
46
+ clients: ['antigravity', 'gemini'],
47
+ // Antigravity & Gemini both read <repo>/.agents/skills as the workspace
48
+ // Agent-Skills location. Their *global* surface is ~/.gemini/commands
49
+ // (markdown commands), wired by the slash-command writer, not here.
50
+ project: { dir: ['.agents', 'skills'], markers: ['.agents', '.gemini'], env: ['ANTIGRAVITY', 'GEMINI_CLI'] },
51
+ global: null
52
+ },
53
+ {
54
+ id: 'codex',
55
+ label: 'Codex',
56
+ clients: ['codex'],
57
+ // Codex only discovers skills globally under ~/.codex/skills. There is no
58
+ // project-local dir, so project skills are opt-in (namespaced) via connect.
59
+ project: null,
60
+ global: { dir: ['.codex', 'skills'], markers: ['.codex'] }
61
+ }
62
+ ];
63
+
64
+ function pathExists(filePath) {
65
+ try {
66
+ fs.lstatSync(filePath);
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Skill source roots, in priority order. The canonical Total Recall location
75
+ * (.agent/skills, singular) wins, but we also scan the Antigravity-native
76
+ * .agents/skills (plural) so a skill authored in either place is discovered.
77
+ */
78
+ export function skillSourceRoots(agentDir) {
79
+ const roots = [path.join(agentDir, 'skills')];
80
+ const parent = path.dirname(agentDir);
81
+ if (path.basename(agentDir) === '.agent') {
82
+ roots.push(path.join(parent, '.agents', 'skills'));
83
+ }
84
+ return roots;
85
+ }
86
+
87
+ /**
88
+ * Discover repo skills to expose as slash commands.
89
+ * Every <root>/<name>/ directory containing a SKILL.md is a skill. When the
90
+ * same name appears in more than one source root, the canonical root wins.
91
+ */
92
+ export function discoverRepoSkills(agentDir) {
93
+ const seen = new Map();
94
+ for (const skillsRoot of skillSourceRoots(agentDir)) {
95
+ if (!fs.existsSync(skillsRoot)) continue;
96
+ let entries;
97
+ try {
98
+ entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
99
+ } catch {
100
+ continue;
101
+ }
102
+ for (const entry of entries) {
103
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
104
+ if (seen.has(entry.name)) continue;
105
+ const skillDir = path.join(skillsRoot, entry.name);
106
+ if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) continue;
107
+ seen.set(entry.name, { name: entry.name, skillDir });
108
+ }
109
+ }
110
+ return [...seen.values()];
111
+ }
112
+
113
+ /**
114
+ * Symlink each discovered skill dir into one IDE's Agent-Skills directory.
115
+ * Idempotent, self-healing, and strictly additive: a symlink that is broken or
116
+ * stale is refreshed even without --force; a *real* (non-symlink) entry is
117
+ * never clobbered unless --force (so a user's own skill always wins a name
118
+ * collision); a skill whose source already *is* the destination is left alone.
119
+ */
120
+ export function projectSkillsAsCommands(destDir, skills, opts = {}) {
121
+ fs.mkdirSync(destDir, { recursive: true });
122
+ const written = [];
123
+ for (const skill of skills) {
124
+ const linkPath = path.join(destDir, skill.name);
125
+ const wantTarget = path.resolve(skill.skillDir);
126
+
127
+ if (path.resolve(linkPath) === wantTarget) {
128
+ written.push({ name: skill.name, action: 'source' });
129
+ continue;
130
+ }
131
+
132
+ if (pathExists(linkPath)) {
133
+ const stat = fs.lstatSync(linkPath);
134
+ if (stat.isSymbolicLink()) {
135
+ const current = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
136
+ if (current === wantTarget && fs.existsSync(linkPath) && !opts.force) {
137
+ written.push({ name: skill.name, action: 'exists' });
138
+ continue;
139
+ }
140
+ fs.unlinkSync(linkPath); // stale, broken, or --force → refresh
141
+ } else if (!opts.force) {
142
+ written.push({ name: skill.name, action: 'skipped' }); // user's real file/dir wins
143
+ continue;
144
+ } else {
145
+ fs.rmSync(linkPath, { recursive: true, force: true });
146
+ }
147
+ }
148
+ fs.symlinkSync(skill.skillDir, linkPath);
149
+ written.push({ name: skill.name, action: 'linked' });
150
+ }
151
+ return written;
152
+ }
153
+
154
+ /**
155
+ * Is an IDE target active for the given scope?
156
+ * project → repo has a marker dir, or the current process is this IDE (env)
157
+ * global → the IDE is installed (home marker dir present)
158
+ */
159
+ function isTargetActive(target, scope, cwd, env) {
160
+ const spec = target[scope];
161
+ if (!spec) return false;
162
+ if (scope === 'project') {
163
+ if ((spec.markers || []).some(m => pathExists(path.join(cwd, m)))) return true;
164
+ if ((spec.env || []).some(v => env[v] != null && env[v] !== '')) return true;
165
+ return false;
166
+ }
167
+ // global
168
+ return (spec.markers || []).some(m => pathExists(path.join(os.homedir(), m)));
169
+ }
170
+
171
+ /**
172
+ * Resolve which IDE targets are active (and where they project to) for a scope.
173
+ * Returns every target annotated with { active, destDir } so callers can both
174
+ * wire the active ones and advertise the inactive ones as opt-in.
175
+ */
176
+ export function detectActiveSkillTargets({ scope, cwd = process.cwd(), env = process.env }) {
177
+ return IDE_SKILLS.map(target => {
178
+ const spec = target[scope];
179
+ const active = isTargetActive(target, scope, cwd, env);
180
+ let destDir = null;
181
+ if (spec) {
182
+ destDir = scope === 'global'
183
+ ? path.join(os.homedir(), ...spec.dir)
184
+ : path.join(cwd, ...spec.dir);
185
+ }
186
+ return { id: target.id, label: target.label, clients: target.clients, scope, supported: !!spec, active, destDir };
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Project discovered skills into every *active* IDE skill dir for one scope.
192
+ * Used by `init`: project skills on `init --project`, global skills on `init`.
193
+ *
194
+ * @returns {{ scope, skills, wired: Array, available: Array }}
195
+ * wired — targets we projected into (active + supported)
196
+ * available — supported-but-inactive targets, surfaced as opt-in `connect`
197
+ */
198
+ export function projectSkillsForScope({ scope, cwd = process.cwd(), agentDir, opts = {}, env = process.env }) {
199
+ const skills = discoverRepoSkills(agentDir);
200
+ const targets = detectActiveSkillTargets({ scope, cwd, env });
201
+ const wired = [];
202
+ const available = [];
203
+
204
+ for (const target of targets) {
205
+ if (!target.supported) continue;
206
+ if (target.active && skills.length > 0) {
207
+ const results = projectSkillsAsCommands(target.destDir, skills, opts);
208
+ wired.push({ ...target, results });
209
+ } else if (!target.active) {
210
+ available.push(target);
211
+ }
212
+ }
213
+ return { scope, skills, wired, available };
214
+ }
package/src/cli/skill.mjs CHANGED
@@ -1,16 +1,30 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
  import { spawnSync } from 'node:child_process';
5
+ import { resolveAgentDir, parseLayerFlag } from './agent-dir.mjs';
5
6
 
6
7
  // Helper to resolve agent directories dynamically
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  const ROOT = path.resolve(__dirname, '../../');
9
- const agentDir = path.join(ROOT, '.agent');
10
- const skillsDir = path.join(agentDir, 'skills');
11
10
 
12
- // Direct dynamic imports for scripts layer to preserve environment paths
13
- const scriptsPath = path.join(agentDir, 'skills', 'total-recall', 'skills', 'skill', 'scripts');
11
+ export function resolveSkillScriptPath(scriptName, { agentDir = resolveAgentDir(), root = ROOT } = {}) {
12
+ const candidates = [
13
+ path.join(agentDir, 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
14
+ path.join(agentDir, 'skills', 'total-recall', 'skills', 'skill', 'scripts', scriptName),
15
+ path.join(root, 'scaffold', '.agent', 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
16
+ path.join(root, 'scaffold', '.agent', 'skills', 'total-recall', 'skills', 'skill', 'scripts', scriptName),
17
+ ];
18
+ const found = candidates.find(candidate => fs.existsSync(candidate));
19
+ if (!found) {
20
+ throw new Error(`Could not find bundled skill helper "${scriptName}". Checked: ${candidates.join(', ')}`);
21
+ }
22
+ return found;
23
+ }
24
+
25
+ async function importSkillScript(scriptName, options) {
26
+ return import(pathToFileURL(resolveSkillScriptPath(scriptName, options)).href);
27
+ }
14
28
 
15
29
  function printHelp() {
16
30
  console.log(`
@@ -28,6 +42,8 @@ function printHelp() {
28
42
  remove <name> Safely delete a skill package and hot-recompile shims (alias: rm)
29
43
 
30
44
  Options:
45
+ --global Target the global brain (~/.agent)
46
+ --project Target the current repo's project brain
31
47
  --help, -h Show this help menu
32
48
 
33
49
  Examples:
@@ -40,15 +56,24 @@ function printHelp() {
40
56
  }
41
57
 
42
58
  export default async function skillCli(args) {
43
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
59
+ const { layer, remainingArgs } = parseLayerFlag(args);
60
+ if (remainingArgs.length === 0 || remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
44
61
  printHelp();
45
62
  return;
46
63
  }
47
64
 
48
- const command = args[0];
65
+ let agentDir;
66
+ try {
67
+ agentDir = resolveAgentDir(layer);
68
+ } catch (err) {
69
+ console.error(`❌ ${err.message}`);
70
+ process.exit(1);
71
+ }
72
+ const skillsDir = path.join(agentDir, 'skills');
73
+ const command = remainingArgs[0];
49
74
 
50
75
  if (command === 'find') {
51
- const query = args[1];
76
+ const query = remainingArgs[1];
52
77
  if (!query) {
53
78
  console.error('❌ You must provide a search query.');
54
79
  console.error('Usage: npx total-recall skill find <query>');
@@ -56,7 +81,7 @@ export default async function skillCli(args) {
56
81
  }
57
82
 
58
83
  try {
59
- const { searchAndSort } = await import('../../.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs');
84
+ const { searchAndSort } = await importSkillScript('find-skills.mjs', { agentDir });
60
85
  const results = searchAndSort(query);
61
86
 
62
87
  if (results.length === 0) {
@@ -100,7 +125,7 @@ export default async function skillCli(args) {
100
125
  }
101
126
 
102
127
  else if (command === 'install' || command === 'add' || command === 'load') {
103
- const pkg = args[1];
128
+ const pkg = remainingArgs[1];
104
129
  if (!pkg) {
105
130
  console.error('❌ You must provide a skill package to load.');
106
131
  console.error('Usage: npx total-recall skill load <owner/repo@skill>');
@@ -108,8 +133,8 @@ export default async function skillCli(args) {
108
133
  }
109
134
 
110
135
  try {
111
- const { installSkill } = await import('../../.agent/skills/total-recall/skills/skill/scripts/install-skill.mjs');
112
- const res = installSkill(pkg);
136
+ const { installSkill } = await importSkillScript('install-skill.mjs', { agentDir });
137
+ const res = installSkill(pkg, { cwd: process.cwd(), agentDir });
113
138
  if (!res.success) {
114
139
  process.exit(1);
115
140
  }
@@ -120,7 +145,7 @@ export default async function skillCli(args) {
120
145
  }
121
146
 
122
147
  else if (command === 'scan') {
123
- const target = args[1];
148
+ const target = remainingArgs[1];
124
149
  if (!target) {
125
150
  console.error('❌ You must specify a local skill directory to scan.');
126
151
  console.error('Usage: npx total-recall skill scan <skill-name>');
@@ -130,7 +155,7 @@ export default async function skillCli(args) {
130
155
  const targetDir = path.resolve(skillsDir, target);
131
156
 
132
157
  try {
133
- const { runScan } = await import('../../.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs');
158
+ const { runScan } = await importSkillScript('scan-skill.mjs', { agentDir });
134
159
  const result = runScan(targetDir);
135
160
  process.exit(result.success ? 0 : 1);
136
161
  } catch (err) {
@@ -176,7 +201,7 @@ export default async function skillCli(args) {
176
201
  }
177
202
 
178
203
  else if (command === 'remove' || command === 'rm') {
179
- const target = args[1];
204
+ const target = remainingArgs[1];
180
205
  if (!target) {
181
206
  console.error('❌ You must specify a skill name to remove.');
182
207
  console.error('Usage: npx total-recall skill remove <skill-name>');
@@ -197,7 +222,8 @@ export default async function skillCli(args) {
197
222
  console.log('🔄 Hot-recompiling active shims to reflect changes...');
198
223
  const child = spawnSync('npx', ['total-recall', 'compile'], {
199
224
  encoding: 'utf8',
200
- cwd: ROOT
225
+ cwd: process.cwd(),
226
+ env: { ...process.env, AGENT_DIR: agentDir },
201
227
  });
202
228
 
203
229
  if (child.error || child.status !== 0) {
@@ -208,7 +234,7 @@ export default async function skillCli(args) {
208
234
  }
209
235
 
210
236
  else if (command === 'create' || command === 'new') {
211
- const target = args[1];
237
+ const target = remainingArgs[1];
212
238
  if (!target) {
213
239
  console.error('❌ You must specify a skill name to create.');
214
240
  console.error('Usage: npx total-recall skill create <skill-name>');
@@ -285,7 +311,7 @@ Provide a high-level explanation of the skill's capabilities and context.
285
311
  }
286
312
 
287
313
  else if (command === 'edit') {
288
- const target = args[1];
314
+ const target = remainingArgs[1];
289
315
  if (!target) {
290
316
  console.error('❌ You must specify a skill name to edit.');
291
317
  console.error('Usage: npx total-recall skill edit <skill-name>');
@@ -293,6 +293,14 @@ process.on('SIGINT', () => {
293
293
  running = false;
294
294
  });
295
295
 
296
+ process.on('uncaughtException', (err) => {
297
+ logger.error({ subsystem: 'daemon-loop', message: `Uncaught Exception (suppressed to keep daemon alive): ${err.stack || err.message}` });
298
+ });
299
+
300
+ process.on('unhandledRejection', (reason, promise) => {
301
+ logger.error({ subsystem: 'daemon-loop', message: `Unhandled Rejection (suppressed to keep daemon alive): ${reason}` });
302
+ });
303
+
296
304
  async function main() {
297
305
  logger.info({
298
306
  subsystem: 'daemon-loop',
@@ -416,8 +424,11 @@ async function main() {
416
424
  // If the task was skipped because it requires an LLM, DO NOT complete the research project.
417
425
  if (result.skippedLLM) {
418
426
  patch.status = 'pending';
419
- } else if (result.success && result.factSlug) {
420
- if (task._research_phase === 'acquisition') {
427
+ } else if (result.success) {
428
+ if (task._research_phase === 'acquisition' && !result.factSlug) {
429
+ patch.status = 'failed';
430
+ patch.notes = 'No insights found during acquisition.';
431
+ } else if (task._research_phase === 'acquisition') {
421
432
  patch.status = 'pending';
422
433
  patch.research_phase = 'deliberation';
423
434
  patch.completed_at = new Date().toISOString();
@@ -7,6 +7,7 @@ import {
7
7
  OperationEnvelopeSchema,
8
8
  PatchEnvelopeSchema,
9
9
  EventEnvelopeSchema,
10
+ DeleteEnvelopeSchema,
10
11
  } from './schema.mjs';
11
12
  import { atomicWrite, safeStringify } from './vault.mjs';
12
13
  import { logger } from './logger.mjs';
@@ -109,7 +110,7 @@ export function processOperation(envelope, vaultRoot, options = {}) {
109
110
  const warnings = [];
110
111
 
111
112
  // Stage 1: Envelope Validation
112
- const schemaMap = { operation: OperationEnvelopeSchema, patch: PatchEnvelopeSchema, event: EventEnvelopeSchema };
113
+ const schemaMap = { operation: OperationEnvelopeSchema, patch: PatchEnvelopeSchema, event: EventEnvelopeSchema, delete: DeleteEnvelopeSchema };
113
114
  const envSchema = schemaMap[envelope.type];
114
115
  if (!envSchema) return makeErrorResponse(envelope.type || 'unknown', operationId, envelope.path || '', [`Unknown envelope type: ${envelope.type}`]);
115
116
  const envResult = envSchema.safeParse(envelope);
@@ -189,6 +190,17 @@ export function processOperation(envelope, vaultRoot, options = {}) {
189
190
  }
190
191
  if (envelope.type === 'event') { try { JSON.parse(envelope.content); } catch { errors.push('Event content must be valid JSON.'); } }
191
192
 
193
+ if (envelope.type === 'delete') {
194
+ const absPath = resolveVfsPath(vaultRoot, envelope.path);
195
+ if (!fs.existsSync(absPath)) { errors.push(`Target file does not exist for delete: ${envelope.path}`); }
196
+ else {
197
+ const { data } = matter(fs.readFileSync(absPath, 'utf8'));
198
+ resolvedType = data.type || null;
199
+ // §6.2: append-type documents are immutable and may not be deleted.
200
+ if (APPEND_TYPES.has(resolvedType)) errors.push(`Append-type '${resolvedType}' may not be deleted.`);
201
+ }
202
+ }
203
+
192
204
  const isValid = errors.length === 0;
193
205
  if (envelope.dry_run || !isValid) {
194
206
  const resp = { success: isValid, type: envelope.type, operation_id: operationId, path: envelope.path, committed_at: null, dry_run: !!envelope.dry_run, validation: { valid: isValid, type: resolvedType, errors, warnings } };
@@ -230,6 +242,12 @@ export function processOperation(envelope, vaultRoot, options = {}) {
230
242
  const logDir = eventLogDir || path.join(vaultRoot, '.events');
231
243
  if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
232
244
  fs.appendFileSync(path.join(logDir, `${envelope.workspace_id}.jsonl`), JSON.stringify({ event_id: operationId, event_type: 'operation-audit', ts: committedAt, path: envelope.path, content: envelope.content, idempotency_key: envelope.idempotency_key }) + '\n');
245
+ } else if (envelope.type === 'delete') {
246
+ fs.rmSync(absPath, { force: true });
247
+ // §6.2: a delete emits an auditable deletion event so history is never lost.
248
+ const logDir = eventLogDir || path.join(vaultRoot, '.events');
249
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
250
+ fs.appendFileSync(path.join(logDir, `${envelope.workspace_id}.jsonl`), JSON.stringify({ event_id: operationId, event_type: 'delete', ts: committedAt, path: envelope.path, resolved_type: resolvedType, idempotency_key: envelope.idempotency_key }) + '\n');
233
251
  }
234
252
  } catch (err) {
235
253
  logger.error('operation-validator', `Commit failed: ${err.message}`);
@@ -109,11 +109,25 @@ async function planResearchQueries(task, context) {
109
109
  const today = getLocalizedDateTime();
110
110
  const cutoff = context.runtimeConfig?.training_cutoff || 'January 2025';
111
111
 
112
- const planSystem = `You are a Research Planner. Today's date and time is ${today}. The model's training data cutoff is ${cutoff}.
113
- Decompose the research objective into 3 distinct, specific search queries. Prioritize queries targeting fresh, timely information that has arisen or been updated since the training cutoff.
114
- Output ONLY valid JSON: { "queries": ["query 1", "query 2", "query 3"], "source_hints": { "query 1": ["brave-search", "arxiv"] } }`;
115
-
116
- const planPrompt = `Research Objective: ${task.target}\nDetails: ${task.body || ''}\nGenerate 3 targeted search queries targeting active, timely information post-${cutoff}.`;
112
+ const planSystem = `<system_instructions>
113
+ You are a Research Planner.
114
+ - Decompose the research objective into 3 distinct, specific search queries.
115
+ - Prioritize queries targeting fresh, timely information that has arisen or been updated since the training cutoff.
116
+ - ALWAYS think inside a <scratchpad> block first to outline your queries.
117
+ - Your final output MUST be ONLY valid JSON matching this schema: { "queries": ["query 1", "query 2", "query 3"], "source_hints": { "query 1": ["brave-search", "arxiv"] } }
118
+ </system_instructions>
119
+
120
+ <context>
121
+ Today's date and time is ${today}.
122
+ The model's training data cutoff is ${cutoff}.
123
+ </context>`;
124
+
125
+ const planPrompt = `<user_goal>
126
+ Research Objective: ${task.target}
127
+ Details: ${task.body || 'None'}
128
+ </user_goal>
129
+
130
+ Generate 3 targeted search queries targeting active, timely information post-${cutoff}.`;
117
131
 
118
132
  try {
119
133
  const raw = await callLocalRuntime(planPrompt, planSystem, context.runtimeConfig);
@@ -436,12 +450,36 @@ async function synthesizeLocally(task, results, runtimeConfig) {
436
450
  .map(r => `[${r.source}] ${r.title}: ${r.snippet?.slice(0, 300)}`)
437
451
  .join('\n');
438
452
 
439
- const system = `You are a Deep Research Synthesizer. Today's date and time is ${today}. The model's training data cutoff is ${cutoff}.
440
- Synthesize research results into a concise report. Cite sources inline [Source: URL]. Prioritize fresh, timely information published after the cutoff.`;
441
- const prompt = `Topic: "${task.target}"\n\nResults:\n${sourceSummary.slice(0, 6000)}`;
453
+ const system = `<system_instructions>
454
+ You are a Deep Research Synthesizer.
455
+ - Synthesize research results into a concise report.
456
+ - Cite sources inline [Source: URL].
457
+ - Prioritize fresh, timely information published after the cutoff.
458
+ - ALWAYS use a <scratchpad> block first to outline your synthesis and evaluate source reliability before writing the final report.
459
+ </system_instructions>
460
+
461
+ <context>
462
+ Today's date and time is ${today}.
463
+ The model's training data cutoff is ${cutoff}.
464
+ </context>`;
465
+
466
+ const prompt = `<user_goal>
467
+ Topic: "${task.target}"
468
+ </user_goal>
469
+
470
+ <retrieved_docs>
471
+ ${sourceSummary.slice(0, 6000)}
472
+ </retrieved_docs>
473
+
474
+ Please synthesize the research results.`;
442
475
 
443
476
  try {
444
- return callLocalRuntime(prompt, system, runtimeConfig);
477
+ let report = await callLocalRuntime(prompt, system, runtimeConfig);
478
+ // Remove the scratchpad from the final stored report
479
+ if (report) {
480
+ report = report.replace(/<scratchpad>[\s\S]*?<\/scratchpad>/gi, '').trim();
481
+ }
482
+ return report;
445
483
  } catch (err) {
446
484
  logger.error('research: callLocalRuntime failed during synthesis', { err: err.message });
447
485
  return null;
@@ -35,8 +35,8 @@ export function findBinaryInPath(binaryName) {
35
35
  * Total Recall Runtime — CLI Agent Dispatch
36
36
  *
37
37
  * All reasoning tasks are dispatched to headless CLI agents.
38
- * Agent registry lives in the cli-agents skill:
39
- * .agent/skills/total-recall/skills/cli-agents/agents.yml
38
+ * Agent registry lives in the tr-cli-agents skill:
39
+ * .agent/skills/total-recall/skills/tr-cli-agents/agents.yml
40
40
  */
41
41
 
42
42
  // ─── Default agent registry (used when agents.yml doesn't exist) ─────────────
@@ -51,11 +51,18 @@ const DEFAULT_AGENTS = [
51
51
  * Resolve the path to the cli-agents config file.
52
52
  */
53
53
  function getAgentsConfigPath() {
54
- const skillPath = path.join(agentDir, 'skills', 'total-recall', 'skills', 'cli-agents', 'agents.yml');
55
- if (fs.existsSync(skillPath)) return skillPath;
56
- // Fallback: check if it's a standalone skill (not nested in meta-skill)
57
- const standalonePath = path.join(agentDir, 'skills', 'cli-agents', 'agents.yml');
58
- if (fs.existsSync(standalonePath)) return standalonePath;
54
+ // The cli-agents skill was namespaced to tr-cli-agents; check the new name
55
+ // first, then fall back to the legacy name for repos initialized before the
56
+ // rename, in both the nested (meta-skill) and standalone locations.
57
+ const candidates = [
58
+ path.join(agentDir, 'skills', 'total-recall', 'skills', 'tr-cli-agents', 'agents.yml'),
59
+ path.join(agentDir, 'skills', 'total-recall', 'skills', 'cli-agents', 'agents.yml'),
60
+ path.join(agentDir, 'skills', 'tr-cli-agents', 'agents.yml'),
61
+ path.join(agentDir, 'skills', 'cli-agents', 'agents.yml')
62
+ ];
63
+ for (const p of candidates) {
64
+ if (fs.existsSync(p)) return p;
65
+ }
59
66
  return null;
60
67
  }
61
68
 
@@ -284,11 +284,23 @@ export const EventEnvelopeSchema = z.object({
284
284
  intent: z.string().optional(),
285
285
  });
286
286
 
287
+ // The canonical fourth envelope type (SSSS spec §6.2): remove a replace-type VFS
288
+ // file. Carries no `content`; the deletion is recorded as an auditable event so
289
+ // history is never lost. Append-type documents may not be deleted.
290
+ export const DeleteEnvelopeSchema = z.object({
291
+ type: z.literal('delete'),
292
+ idempotency_key: z.string().min(8),
293
+ path: z.string(),
294
+ workspace_id: z.string(),
295
+ lease_id: z.string().optional(),
296
+ intent: z.string().optional(),
297
+ });
298
+
287
299
  // ─── Operation Response (§6.4 of the SSSS spec) ────────────────────────────
288
300
 
289
301
  export const OperationResponseSchema = z.object({
290
302
  success: z.boolean(),
291
- type: z.enum(['operation', 'patch', 'event']),
303
+ type: z.enum(['operation', 'patch', 'event', 'delete']),
292
304
  operation_id: z.string(),
293
305
  path: z.string(),
294
306
  committed_at: z.string().datetime().nullable(),
@@ -712,6 +724,15 @@ export const CommerceCatalogSchema = z.object({
712
724
  db_id: z.string().optional().nullable(),
713
725
  });
714
726
 
727
+ export const EmailAccountSchema = z.object({
728
+ type: z.literal("email_account"),
729
+ email_address: z.string(),
730
+ display_name: z.string(),
731
+ provider: z.string(),
732
+ status: z.string(),
733
+ workspace_id: z.string().optional().nullable(),
734
+ metadata: z.record(z.unknown()).optional().nullable(),
735
+ });
715
736
  export const ProductSchema = z.object({
716
737
  type: z.literal('product'),
717
738
  name: z.string(),
@@ -828,6 +849,7 @@ export const SSSS_SCHEMAS = {
828
849
  account_memory: AccountMemorySchema,
829
850
  account_workflow: AccountWorkflowSchema,
830
851
  workspace_transfer: WorkspaceTransferSchema,
852
+ email_account: EmailAccountSchema,
831
853
  extension: ExtensionSchema,
832
854
  phone_number: PhoneNumberSchema,
833
855
  domain: DomainSchema,
@@ -176,7 +176,7 @@ function resolveRequestedModel(requestedModel, runtimeConfig) {
176
176
  }
177
177
 
178
178
  function ssssReferenceDir() {
179
- return path.join(SKILLS_DIR, 'total-recall', 'skills', 'ssss', 'references');
179
+ return path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references');
180
180
  }
181
181
 
182
182
  function listSsssReferences(req) {
@@ -510,7 +510,7 @@ You have a REAL browser AND full desktop control. Use them. Navigate, click, typ
510
510
  baseSystemPrompt += `\n\n=== TIER 1 HOT MEMORY INSTRUCTIONS ===\n${compressInstructions(instructions)}`;
511
511
  }
512
512
 
513
- const ssssPath = path.join(AGENT_DIR, 'skills', 'total-recall', 'skills', 'ssss', 'SKILL.md');
513
+ const ssssPath = path.join(AGENT_DIR, 'skills', 'total-recall', 'skills', 'tr-ssss', 'SKILL.md');
514
514
  if (fs.existsSync(ssssPath)) {
515
515
  const ssssContent = fs.readFileSync(ssssPath, 'utf8');
516
516
  baseSystemPrompt += `\n\n=== STRUCTURED SEMANTIC SYNTAX SYSTEM (SSSS) DOCUMENTATION ===\nYou are the orchestrator of this system. Here is the architectural specification:\n${compressSsssSkill(ssssContent)}`;
@@ -1520,7 +1520,7 @@ function absoluteUrl(req, routePath) {
1520
1520
  }
1521
1521
 
1522
1522
  function ssssReferenceDir() {
1523
- return path.join(SKILLS_DIR, 'total-recall', 'skills', 'ssss', 'references');
1523
+ return path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references');
1524
1524
  }
1525
1525
 
1526
1526
  function listSsssReferences(req) {
@@ -1561,7 +1561,7 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
1561
1561
  name: 'ssss-skill',
1562
1562
  url: absoluteUrl(req, '/api/ssss/skill/ssss'),
1563
1563
  ...(() => {
1564
- const r = readTextResource(path.join(SKILLS_DIR, 'total-recall', 'skills', 'ssss', 'SKILL.md'), 'ssss-skill');
1564
+ const r = readTextResource(path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'SKILL.md'), 'ssss-skill');
1565
1565
  return r ? { sha256: r.sha256, bytes: r.bytes, modified: r.modified } : { sha256: null, bytes: 0, modified: null };
1566
1566
  })()
1567
1567
  },
@@ -1588,7 +1588,7 @@ router.get('/api/ssss/instructions', requireAuth, requireScope('ssss:read', 'ins
1588
1588
  });
1589
1589
 
1590
1590
  router.get('/api/ssss/skill/ssss', requireAuth, requireScope('ssss:read'), (_req, res) => {
1591
- return sendTextResource(res, path.join(SKILLS_DIR, 'total-recall', 'skills', 'ssss', 'SKILL.md'), 'ssss-skill');
1591
+ return sendTextResource(res, path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'SKILL.md'), 'ssss-skill');
1592
1592
  });
1593
1593
 
1594
1594
  router.get('/api/ssss/spec', requireAuth, requireScope('ssss:read'), (_req, res) => {