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.
- package/package.json +6 -5
- package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
- package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/SKILL.md +3 -2
- package/scaffold/.agent/skills/total-recall/skills/{research → tr-research}/SKILL.md +2 -1
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/SKILL.md +3 -2
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/evals/evals.json +3 -3
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-add.sh +1 -1
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-delete.sh +1 -1
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/create-skill.sh +3 -3
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/find-skills.mjs +25 -1
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/install-skill.mjs +117 -0
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/scan-skill.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/SKILL.md +58 -15
- package/src/cli/connect.mjs +6 -59
- package/src/cli/init.mjs +42 -2
- package/src/cli/skill-projection.mjs +214 -0
- package/src/cli/skill.mjs +44 -18
- package/src/core/daemon-loop.mjs +13 -2
- package/src/core/operation-validator.mjs +19 -1
- package/src/core/research.mjs +47 -9
- package/src/core/runtime.mjs +14 -7
- package/src/core/schema.mjs +23 -1
- package/src/server/api.mjs +2 -2
- package/src/server/rest.mjs +3 -3
- package/src/server/routes/skills.mjs +25 -4
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/agents.yml +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/evals/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/antigravity.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/claude.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/codex.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/gemini.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/scripts/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/subagents/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/assets/skill-template/SKILL.md +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/learnings/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/references/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/enforce-skill-optimization.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/skill-watcher.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/subagents/audit-all-skills.md +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/evals/evals.json +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/references/CHANGELOG.md +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/references/ssss-spec.md +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/scripts/validate-schema.mjs +0 -0
- /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
|
-
|
|
13
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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>');
|
package/src/core/daemon-loop.mjs
CHANGED
|
@@ -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
|
|
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}`);
|
package/src/core/research.mjs
CHANGED
|
@@ -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 =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 =
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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;
|
package/src/core/runtime.mjs
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
|
|
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
|
|
package/src/core/schema.mjs
CHANGED
|
@@ -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,
|
package/src/server/api.mjs
CHANGED
|
@@ -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)}`;
|
package/src/server/rest.mjs
CHANGED
|
@@ -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) => {
|