total-recall-brain 3.41.2 → 3.41.4
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/bin/total-recall.mjs +19 -4
- package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
- package/frontend/dist/assets/{index-BtuiZqwP.js → index-CRzjQLu7.js} +7 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +8 -5
- package/frontend/src/api/auth.ts +0 -3
- package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
- package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
- package/frontend/src/pages/LoginPage.tsx +1 -41
- package/frontend/src/pages/PluginsPage.tsx +14 -7
- package/package.json +7 -5
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +23 -8
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +37 -10
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +986 -280
- package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +25 -10
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
- package/src/cli/command.mjs +23 -18
- package/src/cli/config.mjs +3 -0
- package/src/cli/connect.mjs +12 -116
- package/src/cli/daemon.mjs +23 -1
- package/src/cli/harness.mjs +0 -1
- package/src/cli/plugin/copies.mjs +8 -5
- package/src/cli/plugin/create.mjs +56 -3
- package/src/cli/plugin/index.mjs +40 -5
- package/src/cli/plugin/monetization.mjs +232 -12
- package/src/cli/preflight.mjs +161 -0
- package/src/cli/rebuild.mjs +22 -0
- package/src/cli/reset-password.mjs +3 -2
- package/src/cli/skill.mjs +8 -2
- package/src/cli/status.mjs +25 -7
- package/src/core/automatic-brain-updates.mjs +9 -3
- package/src/core/brain-updates.mjs +63 -5
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/context-compiler.mjs +8 -1
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/global-brain-updates.mjs +21 -1
- package/src/core/mesh-brain-updates.mjs +5 -2
- package/src/core/meta-harness.mjs +32 -79
- package/src/core/plugin-dependencies.mjs +6 -1
- package/src/core/plugin-runner.mjs +6 -1
- package/src/core/plugin-store.mjs +67 -1
- package/src/core/plugin-updates.mjs +1 -1
- package/src/core/portable-skills-maintenance.mjs +19 -0
- package/src/core/preflight-gate.mjs +77 -0
- package/src/core/preflight-receipt.mjs +123 -0
- package/src/core/required-plugins.mjs +7 -0
- package/src/core/runtime.mjs +0 -3
- package/src/core/skills-registry.mjs +33 -3
- package/src/core/surface.mjs +38 -1
- package/src/server/auth.mjs +41 -1
- package/src/server/memory-app.mjs +2 -1
- package/src/server/routes/auth.mjs +25 -1
- package/src/server/routes/plugins.mjs +17 -4
|
@@ -22,6 +22,47 @@ const isLink = file => { try { return fs.lstatSync(file).isSymbolicLink(); } cat
|
|
|
22
22
|
export const brainUpdateLockPath = owner => path.join(os.tmpdir(),
|
|
23
23
|
`total-recall-update-${createHash('sha256').update(owner).digest('hex')}.lock`);
|
|
24
24
|
|
|
25
|
+
const pause = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
26
|
+
const alive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Take a per-owner update lock. The daemon's hourly `update --automatic` and a
|
|
30
|
+
* manual `update --all-brains` take the same locks, and a plain exclusive create
|
|
31
|
+
* failed the manual run outright with EEXIST. A lock whose process is gone is
|
|
32
|
+
* reclaimed; a live holder is waited for, up to `waitMs`.
|
|
33
|
+
*/
|
|
34
|
+
export function acquireBrainUpdateLock(lock, { waitMs = 600000, pollMs = 1000, isAlive = alive, sleep = pause } = {}) {
|
|
35
|
+
const deadline = Date.now() + waitMs;
|
|
36
|
+
for (;;) {
|
|
37
|
+
try {
|
|
38
|
+
const fd = fs.openSync(lock, 'wx', 0o600);
|
|
39
|
+
fs.writeFileSync(fd, String(process.pid));
|
|
40
|
+
return fd;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code !== 'EEXIST') throw error;
|
|
43
|
+
}
|
|
44
|
+
let holder, age;
|
|
45
|
+
try {
|
|
46
|
+
holder = Number.parseInt(fs.readFileSync(lock, 'utf8').trim(), 10);
|
|
47
|
+
age = Date.now() - fs.statSync(lock).mtimeMs;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') continue; // released between the create and the read
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
// A creator writes its pid right after the exclusive create; an empty lock
|
|
53
|
+
// younger than a few seconds is still being taken, not abandoned. Content
|
|
54
|
+
// that is not a pid names an unknown owner and is respected.
|
|
55
|
+
const content = holder ? 'pid' : fs.statSync(lock).size ? 'unknown' : 'empty';
|
|
56
|
+
const stale = content === 'pid' ? !isAlive(holder) : content === 'empty' && age > 5000;
|
|
57
|
+
if (stale) {
|
|
58
|
+
try { fs.unlinkSync(lock); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (Date.now() >= deadline) throw new Error(`Another update (${holder ? `pid ${holder}` : 'unknown owner'}) still holds ${lock}`);
|
|
62
|
+
sleep(pollMs);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
25
66
|
export function installedBrainPackage(root, key) {
|
|
26
67
|
try {
|
|
27
68
|
const paths = createRequire(path.join(root, 'package.json')).resolve.paths(key) || [];
|
|
@@ -44,7 +85,14 @@ export function discoverBrainConsumers(roots, { onError } = {}) {
|
|
|
44
85
|
let pkg;
|
|
45
86
|
try { pkg = read(file); }
|
|
46
87
|
catch (error) { if (onError) { onError(real, error); return; } throw error; }
|
|
47
|
-
|
|
88
|
+
// A plugin copied into a shared plugin folder is installed without its dev
|
|
89
|
+
// dependencies, on purpose: full dev trees there filled a disk. Counting its
|
|
90
|
+
// devDependencies made every such plugin a permanently failing consumer, and
|
|
91
|
+
// "fixing" it would mean installing exactly what must never be installed.
|
|
92
|
+
const sharedPluginCopy = fs.existsSync(path.join(real, 'plugin.json'))
|
|
93
|
+
&& real.split(path.sep).join('/').includes('/skills/total-recall/plugins/');
|
|
94
|
+
const deps = sections.filter(section => !(sharedPluginCopy && section === 'devDependencies'))
|
|
95
|
+
.flatMap(section => Object.entries(pkg[section] || {})
|
|
48
96
|
.filter(([key]) => BRAIN_PACKAGES.includes(canonical(key)) && canonical(key) !== pkg.name)
|
|
49
97
|
.map(([key, declared]) => ({ section, key, package: canonical(key), declared,
|
|
50
98
|
...installedBrainPackage(real, key) })));
|
|
@@ -168,8 +216,7 @@ export async function updateAllBrains(opts = {}) {
|
|
|
168
216
|
isLink(path.join(consumer.root, 'node_modules')) || consumer.deps.some(dep =>
|
|
169
217
|
dep.version !== versions[dep.package] || !dep.file?.startsWith(path.join(group.owner, 'node_modules') + path.sep)));
|
|
170
218
|
try {
|
|
171
|
-
lockFd =
|
|
172
|
-
fs.writeFileSync(lockFd, String(process.pid));
|
|
219
|
+
lockFd = acquireBrainUpdateLock(lock, opts.lockWaitMs === undefined ? {} : { waitMs: opts.lockWaitMs });
|
|
173
220
|
for (const name of ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml']) {
|
|
174
221
|
const file = path.join(group.owner, name);
|
|
175
222
|
saved.set(file, fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null);
|
|
@@ -198,11 +245,22 @@ export async function updateAllBrains(opts = {}) {
|
|
|
198
245
|
if (install.status !== 0) throw new Error(install.error?.message || install.stderr?.trim().slice(-1500) || `Install exited ${install.status}`);
|
|
199
246
|
for (const { consumer, row } of group.consumers) {
|
|
200
247
|
row.packages = row.packages.map(dep => ({ ...dep, installed: installedBrainPackage(consumer.root, dep.key).version }));
|
|
248
|
+
// A consumer may legitimately resolve the package from its own node_modules — nested plugin
|
|
249
|
+
// directories do exactly that. Requiring the owner's hoisted copy reported a correct install
|
|
250
|
+
// as a failure and rolled the whole batch back, which is why 30 consumers never updated.
|
|
251
|
+
const allowedRoots = [
|
|
252
|
+
path.join(group.owner, 'node_modules') + path.sep,
|
|
253
|
+
path.join(consumer.root, 'node_modules') + path.sep,
|
|
254
|
+
];
|
|
255
|
+
const placed = (file) => Boolean(file) && allowedRoots.some((root) => file.startsWith(root));
|
|
201
256
|
const bad = consumer.deps.find(dep => {
|
|
202
257
|
const actual = installedBrainPackage(consumer.root, dep.key);
|
|
203
|
-
return actual.version !== versions[dep.package] || !actual.file
|
|
258
|
+
return actual.version !== versions[dep.package] || !placed(actual.file);
|
|
204
259
|
});
|
|
205
|
-
|
|
260
|
+
// A link is only a fault when it escapes every legitimate root; a self-contained one is fine.
|
|
261
|
+
const linkPath = path.join(consumer.root, 'node_modules');
|
|
262
|
+
const linked = isLink(linkPath) && !placed(path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath)));
|
|
263
|
+
if (bad || linked) throw new Error(`Installed release verification failed: ${consumer.root}${bad ? ` (${bad.key})` : ' (shared link)'}`);
|
|
206
264
|
row.status = 'updated';
|
|
207
265
|
}
|
|
208
266
|
} catch (error) {
|
|
@@ -139,8 +139,15 @@ export async function compileContext({
|
|
|
139
139
|
return result;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Second line of every text capsule, so output that is truncated or piped through
|
|
144
|
+
* `head` still carries the rule (the rule once sat at line 544 of the output and
|
|
145
|
+
* an agent that cut the output at 80 lines never saw it).
|
|
146
|
+
*/
|
|
147
|
+
export const PREFLIGHT_NOTICE = 'PREFLIGHT: before any other tool call, read this whole capsule (never pipe or truncate it), then run: total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
|
|
148
|
+
|
|
142
149
|
export function renderCapsule(result, format = 'json') {
|
|
143
|
-
return format === 'text' ? `ready:${result.ready} tokens:${result.stats.total_tokens} version:${result.stats.version}\n${result.context}` : JSON.stringify(result);
|
|
150
|
+
return format === 'text' ? `ready:${result.ready} tokens:${result.stats.total_tokens} version:${result.stats.version}\n${PREFLIGHT_NOTICE}\n${result.context}` : JSON.stringify(result);
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/** Budget the actual wire representation, including metadata, escaping and status. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Default runtime foundation and routine agent-development tools. */
|
|
2
2
|
export const DEFAULT_PLUGIN_IDS = Object.freeze([
|
|
3
|
-
'auth', 'mesh', 'openwiki', 'composable-cli', '
|
|
3
|
+
'auth', 'mesh', 'openwiki', 'composable-cli', 'meta-harness',
|
|
4
4
|
'code-quality', 'security', 'embeddings', 'git-sentinel',
|
|
5
5
|
]);
|
|
@@ -6,6 +6,24 @@ import {BRAIN_PACKAGES} from './brain-updates.mjs';
|
|
|
6
6
|
import {comparePackageVersions} from './package-version.mjs';
|
|
7
7
|
import {updateSourceBrainPackage} from './source-brain-updates.mjs';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* A global link that points at a source checkout of `name` under another
|
|
11
|
+
* directory name (the package was once published as `total-recall`). Missing it
|
|
12
|
+
* made the update `npm install -g` over the bin and replace the operator's
|
|
13
|
+
* source checkout with a registry copy.
|
|
14
|
+
*/
|
|
15
|
+
export function findLinkedSourceCheckout(root,name){
|
|
16
|
+
let entries=[];try{entries=fs.readdirSync(root,{withFileTypes:true});}catch{return null;}
|
|
17
|
+
for(const entry of entries){
|
|
18
|
+
if(!entry.isSymbolicLink())continue;
|
|
19
|
+
try{
|
|
20
|
+
const dir=fs.realpathSync(path.join(root,entry.name));
|
|
21
|
+
if(JSON.parse(fs.readFileSync(path.join(dir,'package.json'),'utf8')).name===name&&fs.existsSync(path.join(dir,'.git')))return dir;
|
|
22
|
+
}catch{}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
export function updateGlobalBrainPackages({versions,exec=spawnSync,sourceUpdate=updateSourceBrainPackage}={}) {
|
|
10
28
|
const prefix=exec('npm',['root','--global'],{encoding:'utf8',timeout:15000});
|
|
11
29
|
if(prefix.status!==0)return {failed:1,results:[{status:'failed',error:'Cannot resolve global npm root'}]};
|
|
@@ -13,7 +31,9 @@ export function updateGlobalBrainPackages({versions,exec=spawnSync,sourceUpdate=
|
|
|
13
31
|
const results=[];
|
|
14
32
|
const install=[];
|
|
15
33
|
for(const name of BRAIN_PACKAGES){
|
|
16
|
-
|
|
34
|
+
let directory=path.join(root,name);
|
|
35
|
+
if(!fs.existsSync(directory))directory=findLinkedSourceCheckout(root,name)||directory;
|
|
36
|
+
const file=path.join(directory,'package.json');
|
|
17
37
|
let installed;try{installed=JSON.parse(fs.readFileSync(file,'utf8')).version;}catch{}
|
|
18
38
|
const row={package:name,installed:installed || null,latest:versions[name],status:'up_to_date'};results.push(row);
|
|
19
39
|
if(installed===versions[name])continue;
|
|
@@ -15,14 +15,17 @@ export async function updateMeshBrainPackages({version,versions,listNodes,exec,n
|
|
|
15
15
|
// Read the durable report first, then launch only when stale or failed.
|
|
16
16
|
const observed=await exec(node.ip,'cd "$HOME" && total-recall update --status --json',{timeoutMs:20000});
|
|
17
17
|
let report;try{report=JSON.parse(observed.stdout).last;}catch{}
|
|
18
|
-
const
|
|
18
|
+
const localFailed=report?(report.local_failed ?? Math.max(0,(report.failed || 0)-(report.mesh?.failed || 0))):null;
|
|
19
|
+
const current=observed.success && report && localFailed===0 && now()>=report.checkedAt && now()-report.checkedAt<2*60*60*1000 && report.versions?.['total-recall-brain']===version && (!versions?.['@gregiteen/ssss-cli'] || report.versions?.['@gregiteen/ssss-cli']===versions['@gregiteen/ssss-cli']);
|
|
19
20
|
if(current){row.status='verified';row.report=report;continue;}
|
|
20
21
|
// npm exec pins the verified release so an old installed CLI can bootstrap
|
|
21
22
|
// this capability. The remote command owns its lock, verification and report.
|
|
22
23
|
const command=`cd "$HOME" && nohup npm exec --yes --package=total-recall-brain@${version} -- total-recall update --automatic --local-only --json </dev/null >/tmp/total-recall-automatic-update.log 2>&1 &`;
|
|
23
24
|
const launched=await exec(node.ip,command,{timeoutMs:20000});
|
|
24
25
|
row.status=launched.success?'dispatched':'failed';
|
|
25
|
-
row.error
|
|
26
|
+
row.error=!launched.success?launched.stderr || 'Mesh maintenance dispatch failed'
|
|
27
|
+
:localFailed?`Peer reports ${localFailed} local maintenance failure(s); re-dispatched`
|
|
28
|
+
:'Awaiting independently verified remote report';
|
|
26
29
|
}catch(error){row.status='failed';row.error=error.message;}
|
|
27
30
|
}
|
|
28
31
|
// A dispatched process is not a verified update. Keep it visible for retries.
|
|
@@ -49,15 +49,6 @@ export const HARNESS_SPECS = {
|
|
|
49
49
|
execType: 'subcommand',
|
|
50
50
|
description: 'Autonomous program synthesis and sandbox execution.'
|
|
51
51
|
},
|
|
52
|
-
gemini: {
|
|
53
|
-
id: 'gemini',
|
|
54
|
-
name: 'Google Gemini CLI',
|
|
55
|
-
binary: 'gemini',
|
|
56
|
-
category: 'fast_utility',
|
|
57
|
-
defaultFlags: ['--sandbox=false', '--yolo', '-o', 'json'],
|
|
58
|
-
execType: 'flag_last',
|
|
59
|
-
description: 'High-speed utility completions and tool chaining.'
|
|
60
|
-
},
|
|
61
52
|
ollama: {
|
|
62
53
|
id: 'ollama',
|
|
63
54
|
name: 'Ollama Local LLM',
|
|
@@ -246,14 +237,6 @@ export async function detectHarnessesWithAuth({ readSecret = readStoreSecret } =
|
|
|
246
237
|
h.authed = true;
|
|
247
238
|
h.authRoute = 'cached-xai-auth';
|
|
248
239
|
}
|
|
249
|
-
} else if (h.id === 'gemini') {
|
|
250
|
-
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
251
|
-
h.authed = true;
|
|
252
|
-
h.authRoute = 'env-api-key';
|
|
253
|
-
} else if (fs.existsSync(path.join(os.homedir(), '.gemini'))) {
|
|
254
|
-
h.authed = true;
|
|
255
|
-
h.authRoute = 'cached-google-signin';
|
|
256
|
-
}
|
|
257
240
|
} else if (h.id === 'hermes') {
|
|
258
241
|
if (process.env.HERMES_API_KEY) {
|
|
259
242
|
h.authed = true;
|
|
@@ -471,6 +454,9 @@ export function getInstalledHarnessVersion(harnessId) {
|
|
|
471
454
|
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
472
455
|
if (m) return m[1];
|
|
473
456
|
} else if (harnessId === 'codex') {
|
|
457
|
+
const res = spawnSync('codex', ['--version'], { timeout: 1500, encoding: 'utf8' });
|
|
458
|
+
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
459
|
+
if (m) return m[1];
|
|
474
460
|
const candidatePaths = [
|
|
475
461
|
path.join(os.homedir(), '.nvm/versions/node/v24.12.0/lib/node_modules/@openai/codex/package.json'),
|
|
476
462
|
'/usr/local/lib/node_modules/@openai/codex/package.json',
|
|
@@ -496,10 +482,6 @@ export function getInstalledHarnessVersion(harnessId) {
|
|
|
496
482
|
const res = spawnSync('ollama', ['--version'], { timeout: 1500, encoding: 'utf8' });
|
|
497
483
|
const m = (res.stdout || res.stderr || '').match(/(\d+\.\d+\.\d+)/);
|
|
498
484
|
if (m) return m[1];
|
|
499
|
-
} else if (harnessId === 'gemini') {
|
|
500
|
-
const res = spawnSync('gemini', ['--version'], { timeout: 800, encoding: 'utf8' });
|
|
501
|
-
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
502
|
-
if (m) return m[1];
|
|
503
485
|
} else if (harnessId === 'hermes') {
|
|
504
486
|
const res = spawnSync('hermes', ['--version'], { timeout: 1500, encoding: 'utf8' });
|
|
505
487
|
const m = (res.stdout || '').match(/(\d+\.\d+\.\d+)/);
|
|
@@ -550,19 +532,24 @@ export async function getLatestHarnessVersion(harnessId, installedVersion = 'unk
|
|
|
550
532
|
if (d.version) latest = d.version;
|
|
551
533
|
}
|
|
552
534
|
} else if (harnessId === 'agy') {
|
|
553
|
-
|
|
535
|
+
try {
|
|
536
|
+
const res = spawnSync('agy', ['changelog'], { timeout: 2000, encoding: 'utf8' });
|
|
537
|
+
const m = (res.stdout || '').match(/^(\d+\.\d+\.\d+):/m);
|
|
538
|
+
if (m) latest = m[1];
|
|
539
|
+
else latest = installedVersion !== 'unknown' ? installedVersion : '1.4.12';
|
|
540
|
+
} catch {
|
|
541
|
+
latest = installedVersion !== 'unknown' ? installedVersion : '1.4.12';
|
|
542
|
+
}
|
|
554
543
|
} else if (harnessId === 'grok') {
|
|
555
|
-
latest = '0.2.118';
|
|
544
|
+
latest = installedVersion !== 'unknown' ? installedVersion : '0.2.118';
|
|
556
545
|
} else if (harnessId === 'ollama') {
|
|
557
|
-
latest = '
|
|
558
|
-
} else if (harnessId === 'gemini') {
|
|
559
|
-
latest = '0.3.0';
|
|
546
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
560
547
|
} else if (harnessId === 'hermes') {
|
|
561
|
-
latest = '
|
|
548
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
562
549
|
} else if (harnessId === 'dsh') {
|
|
563
|
-
latest = '
|
|
550
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
564
551
|
} else if (harnessId === 'openclaw') {
|
|
565
|
-
latest = '
|
|
552
|
+
latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
|
|
566
553
|
}
|
|
567
554
|
} catch {}
|
|
568
555
|
|
|
@@ -593,9 +580,9 @@ export async function queryHarnessUsage() {
|
|
|
593
580
|
status: 'not_installed',
|
|
594
581
|
identity: '—',
|
|
595
582
|
plan: '—',
|
|
596
|
-
rolling5hRemainingPct: '
|
|
597
|
-
weeklyRemainingPct: '
|
|
598
|
-
quotaRemaining: '
|
|
583
|
+
rolling5hRemainingPct: '—',
|
|
584
|
+
weeklyRemainingPct: '—',
|
|
585
|
+
quotaRemaining: 'not installed'
|
|
599
586
|
});
|
|
600
587
|
continue;
|
|
601
588
|
}
|
|
@@ -606,9 +593,13 @@ export async function queryHarnessUsage() {
|
|
|
606
593
|
|
|
607
594
|
let identity = 'Detected local install';
|
|
608
595
|
let plan = 'Standard';
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
596
|
+
// Never invent a quota. These fields held hardcoded literals ('100%', '98%', '85%') that read
|
|
597
|
+
// like measurements but came from no source at all — no provider usage endpoint was called, so
|
|
598
|
+
// the table reported fictional headroom for every harness. A figure appears here only when a
|
|
599
|
+
// real source produced it; otherwise the value is honestly unknown.
|
|
600
|
+
let quotaRemaining = 'unknown — no usage endpoint integrated';
|
|
601
|
+
let rolling5hRemainingPct = '—';
|
|
602
|
+
let weeklyRemainingPct = '—';
|
|
612
603
|
let isAuthed = h.authed;
|
|
613
604
|
let authRoute = h.authRoute;
|
|
614
605
|
|
|
@@ -620,9 +611,6 @@ export async function queryHarnessUsage() {
|
|
|
620
611
|
if (acc.active) identity = acc.active;
|
|
621
612
|
}
|
|
622
613
|
plan = 'Google AI Ultra / Pro';
|
|
623
|
-
rolling5hRemainingPct = '100%';
|
|
624
|
-
weeklyRemainingPct = '98%';
|
|
625
|
-
quotaRemaining = '5h window: 100% | Weekly compute: 98%';
|
|
626
614
|
} catch {}
|
|
627
615
|
} else if (h.id === 'claude') {
|
|
628
616
|
try {
|
|
@@ -632,9 +620,6 @@ export async function queryHarnessUsage() {
|
|
|
632
620
|
if (cData.userID) identity = `User: ${cData.userID.slice(0, 8)}...`;
|
|
633
621
|
plan = cData.opusProMigrationComplete ? 'Claude Pro / Max (Opus enabled)' : 'Claude Pro';
|
|
634
622
|
}
|
|
635
|
-
rolling5hRemainingPct = '85%';
|
|
636
|
-
weeklyRemainingPct = '92%';
|
|
637
|
-
quotaRemaining = '5h window: 85% remaining | Weekly cap: 92% remaining';
|
|
638
623
|
} catch {}
|
|
639
624
|
} else if (h.id === 'codex') {
|
|
640
625
|
try {
|
|
@@ -659,15 +644,7 @@ export async function queryHarnessUsage() {
|
|
|
659
644
|
}
|
|
660
645
|
plan = 'ChatGPT Subscription (GPT-4o/Codex)';
|
|
661
646
|
}
|
|
662
|
-
if (isAuthed)
|
|
663
|
-
rolling5hRemainingPct = '94%';
|
|
664
|
-
weeklyRemainingPct = '96%';
|
|
665
|
-
quotaRemaining = '5h reasoning: 94% remaining | Weekly quota: 96% remaining';
|
|
666
|
-
} else {
|
|
667
|
-
rolling5hRemainingPct = '0% (expired)';
|
|
668
|
-
weeklyRemainingPct = '0% (expired)';
|
|
669
|
-
quotaRemaining = 'Authentication expired';
|
|
670
|
-
}
|
|
647
|
+
if (!isAuthed) quotaRemaining = 'unavailable — authentication expired';
|
|
671
648
|
} catch {}
|
|
672
649
|
} else if (h.id === 'grok') {
|
|
673
650
|
try {
|
|
@@ -685,27 +662,7 @@ export async function queryHarnessUsage() {
|
|
|
685
662
|
}
|
|
686
663
|
plan = 'Grok Build Tier';
|
|
687
664
|
}
|
|
688
|
-
if (isAuthed)
|
|
689
|
-
rolling5hRemainingPct = '100%';
|
|
690
|
-
weeklyRemainingPct = '100%';
|
|
691
|
-
quotaRemaining = '5h rate window: 100% | Weekly allowance: 100%';
|
|
692
|
-
} else {
|
|
693
|
-
rolling5hRemainingPct = '0% (expired)';
|
|
694
|
-
weeklyRemainingPct = '0% (expired)';
|
|
695
|
-
quotaRemaining = 'Token expired (refresh needed)';
|
|
696
|
-
}
|
|
697
|
-
} catch {}
|
|
698
|
-
} else if (h.id === 'gemini') {
|
|
699
|
-
try {
|
|
700
|
-
const accPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
701
|
-
if (fs.existsSync(accPath)) {
|
|
702
|
-
const acc = JSON.parse(fs.readFileSync(accPath, 'utf8'));
|
|
703
|
-
if (acc.active) identity = acc.active;
|
|
704
|
-
}
|
|
705
|
-
plan = 'Google AI API / Subscription';
|
|
706
|
-
rolling5hRemainingPct = '100%';
|
|
707
|
-
weeklyRemainingPct = '100%';
|
|
708
|
-
quotaRemaining = 'Standard rate limits active';
|
|
665
|
+
if (!isAuthed) quotaRemaining = 'unavailable — token expired (refresh needed)';
|
|
709
666
|
} catch {}
|
|
710
667
|
} else if (h.id === 'ollama') {
|
|
711
668
|
identity = 'Local Machine';
|
|
@@ -716,9 +673,7 @@ export async function queryHarnessUsage() {
|
|
|
716
673
|
} else if (h.id === 'hermes') {
|
|
717
674
|
identity = 'Hermes Supervisor';
|
|
718
675
|
plan = 'Autonomous Agent Runtime';
|
|
719
|
-
|
|
720
|
-
weeklyRemainingPct = '100%';
|
|
721
|
-
quotaRemaining = 'Active broker & gateway';
|
|
676
|
+
quotaRemaining = 'local runtime — no metered quota';
|
|
722
677
|
} else if (h.id === 'dsh') {
|
|
723
678
|
let model = 'deepseek/deepseek-v4.1-flash';
|
|
724
679
|
try {
|
|
@@ -729,9 +684,9 @@ export async function queryHarnessUsage() {
|
|
|
729
684
|
} catch {}
|
|
730
685
|
identity = model;
|
|
731
686
|
plan = 'DeepSeek Dedicated Harness';
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
quotaRemaining = '
|
|
687
|
+
// dsh bills through OpenRouter metered credit and publishes no quota figures of its own, so
|
|
688
|
+
// none are shown. Point the reader at the one place the real number exists.
|
|
689
|
+
quotaRemaining = 'metered — see OpenRouter credits';
|
|
735
690
|
} else if (h.id === 'openclaw') {
|
|
736
691
|
let channelsStr = 'telegram, qqbot';
|
|
737
692
|
try {
|
|
@@ -745,8 +700,6 @@ export async function queryHarnessUsage() {
|
|
|
745
700
|
}
|
|
746
701
|
} catch {}
|
|
747
702
|
plan = 'Multi-Channel Gateway';
|
|
748
|
-
rolling5hRemainingPct = '100%';
|
|
749
|
-
weeklyRemainingPct = '100%';
|
|
750
703
|
quotaRemaining = `Channels: ${channelsStr}`;
|
|
751
704
|
}
|
|
752
705
|
|
|
@@ -13,7 +13,12 @@ export function preparePluginDependencies(pluginDir) {
|
|
|
13
13
|
if (name === host.name) source = PACKAGE_ROOT;
|
|
14
14
|
else {
|
|
15
15
|
const alias = Object.entries(host.dependencies || {}).find(([, spec]) => typeof spec === 'string' && spec.startsWith(`npm:${name}@`))?.[0];
|
|
16
|
-
|
|
16
|
+
const targetPkg = alias || name;
|
|
17
|
+
try {
|
|
18
|
+
source = path.dirname(require.resolve(targetPkg));
|
|
19
|
+
} catch {
|
|
20
|
+
source = path.dirname(require.resolve(`${targetPkg}/package.json`));
|
|
21
|
+
}
|
|
17
22
|
while (true) {
|
|
18
23
|
const file = path.join(source, 'package.json');
|
|
19
24
|
if (fs.existsSync(file) && JSON.parse(fs.readFileSync(file, 'utf8')).name === name) break;
|
|
@@ -56,7 +56,12 @@ export function runPluginCommand(plugin, options = {}) {
|
|
|
56
56
|
if (!pluginExecutionEnabled(plugin, { projectRoot: cwd }) && !['status', 'health', 'jobs', '--help', '-h', 'help'].includes(operation)) {
|
|
57
57
|
return Promise.reject(new Error(`Plugin ${plugin.id} is inactive; run total-recall plugin on ${plugin.id}`));
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
// Manifests declare this as `timeoutMs`; the runner only read `timeout_ms`, so every declared
|
|
60
|
+
// timeout was ignored and each command fell back to DEFAULT_TIMEOUT_MS (30s). That killed work
|
|
61
|
+
// that legitimately takes longer — `mesh nodes` and `tr-transcribe transcribe` (model load) both
|
|
62
|
+
// died at 30s while their manifests asked for far more. Accept both spellings.
|
|
63
|
+
const declaration = plugin?.manifest?.cli?.subcommands?.find(item => item.name === operation);
|
|
64
|
+
const declaredTimeout = declaration?.timeout_ms ?? declaration?.timeoutMs;
|
|
60
65
|
const timeoutMs = requestedTimeoutMs ?? declaredTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
61
66
|
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 600000) return Promise.reject(new Error('Plugin timeout must be 1..600000 ms'));
|
|
62
67
|
const handlerRel = plugin?.manifest?.cli?.handler;
|
|
@@ -417,6 +417,67 @@ export async function installPluginDependencies(dest, id) {
|
|
|
417
417
|
return { installed: true, packages };
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
const REPLACEMENT_NAME = /^(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
421
|
+
|
|
422
|
+
/** Where `install --replace` keeps the copy it replaced, beside a plugins directory. */
|
|
423
|
+
export function replacementArchiveDir(pluginsDir) {
|
|
424
|
+
return path.join(path.dirname(pluginsDir), 'memory-derived', 'plugin-replacements');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function directoryBytes(dir) {
|
|
428
|
+
let total = 0;
|
|
429
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
430
|
+
const full = path.join(dir, entry.name);
|
|
431
|
+
if (entry.isDirectory()) total += directoryBytes(full);
|
|
432
|
+
else if (entry.isFile()) total += fs.statSync(full).size;
|
|
433
|
+
}
|
|
434
|
+
return total;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Bound the copies `install --replace` keeps. Every replacement archives the
|
|
439
|
+
* previous install, dependencies included, and nothing removed them, so the
|
|
440
|
+
* hourly automatic plugin update grew the archive without limit: 73 copies
|
|
441
|
+
* filled a laptop disk. Keep the newest `keep` copies of each plugin (or of
|
|
442
|
+
* `id` only), without node_modules, which a rollback reinstalls. A rename
|
|
443
|
+
* keeps a directory's old mtime, so the copy just archived is named in
|
|
444
|
+
* `prefer` and always counts as the newest.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} archiveDir
|
|
447
|
+
* @param {{ id?: string, keep?: number, apply?: boolean, prefer?: string }} [opts]
|
|
448
|
+
*/
|
|
449
|
+
export function pruneReplacementArchives(archiveDir, { id, keep = 1, apply = true, prefer } = {}) {
|
|
450
|
+
if (!fs.existsSync(archiveDir)) return { archiveDir, kept: [], removed: [], stripped: [], bytesFreed: 0 };
|
|
451
|
+
const byId = new Map();
|
|
452
|
+
for (const name of fs.readdirSync(archiveDir)) {
|
|
453
|
+
const m = REPLACEMENT_NAME.exec(name);
|
|
454
|
+
const full = path.join(archiveDir, name);
|
|
455
|
+
if (!m || (id && m[1] !== id) || !fs.statSync(full).isDirectory()) continue;
|
|
456
|
+
(byId.get(m[1]) || byId.set(m[1], []).get(m[1])).push({ name, full, mtime: fs.statSync(full).mtimeMs });
|
|
457
|
+
}
|
|
458
|
+
const kept = [], removed = [], stripped = [];
|
|
459
|
+
let bytesFreed = 0;
|
|
460
|
+
for (const copies of byId.values()) {
|
|
461
|
+
copies.sort((a, b) => (b.name === prefer) - (a.name === prefer) || b.mtime - a.mtime);
|
|
462
|
+
copies.forEach((copy, index) => {
|
|
463
|
+
if (index >= keep) {
|
|
464
|
+
bytesFreed += directoryBytes(copy.full);
|
|
465
|
+
if (apply) fs.rmSync(copy.full, { recursive: true, force: true });
|
|
466
|
+
removed.push(copy.name);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
kept.push(copy.name);
|
|
470
|
+
const modules = path.join(copy.full, 'node_modules');
|
|
471
|
+
if (fs.existsSync(modules)) {
|
|
472
|
+
bytesFreed += directoryBytes(modules);
|
|
473
|
+
if (apply) fs.rmSync(modules, { recursive: true, force: true });
|
|
474
|
+
stripped.push(copy.name);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return { archiveDir, kept, removed, stripped, bytesFreed };
|
|
479
|
+
}
|
|
480
|
+
|
|
420
481
|
/**
|
|
421
482
|
* Install a plugin.
|
|
422
483
|
* @param {string} source bundled id | public share URL | `peer:<host>/<id>` | git URL | local path
|
|
@@ -439,7 +500,7 @@ export async function installPlugin(source, options = {}) {
|
|
|
439
500
|
const prepareDestination=id=>{
|
|
440
501
|
const target=path.join(pluginsDir,id);
|
|
441
502
|
if(!options.replace || !fs.existsSync(target))return assertNotInstalled(id,pluginsDir);
|
|
442
|
-
const archive=path.join(
|
|
503
|
+
const archive=path.join(replacementArchiveDir(pluginsDir),`${id}-${crypto.randomUUID()}`);
|
|
443
504
|
fs.mkdirSync(path.dirname(archive),{recursive:true});
|
|
444
505
|
fs.renameSync(target,archive);previousDirectory={target,archive};
|
|
445
506
|
};
|
|
@@ -524,6 +585,11 @@ export async function installPlugin(source, options = {}) {
|
|
|
524
585
|
throw new Error(`Plugin files were written but its record could not be: ${err.message}`);
|
|
525
586
|
}
|
|
526
587
|
|
|
588
|
+
// The install succeeded: keep only this replacement's copy of the plugin, without dependencies.
|
|
589
|
+
if (previousDirectory) {
|
|
590
|
+
pruneReplacementArchives(replacementArchiveDir(pluginsDir), { id: manifest.id, keep: 1, prefer: path.basename(previousDirectory.archive) });
|
|
591
|
+
}
|
|
592
|
+
|
|
527
593
|
// A batch of installs (automatic updates) compiles once after the batch instead.
|
|
528
594
|
const compilation = options.recompile === false ? { skipped: true } : await recompileAfterPluginMutation(projectRoot, isGlobal);
|
|
529
595
|
return {
|
|
@@ -50,7 +50,7 @@ export async function checkPlugin(plugin, { gitHead = remoteGitHead, peerHash =
|
|
|
50
50
|
return { status: 'pinned', ref: source.ref };
|
|
51
51
|
case 'bundled': {
|
|
52
52
|
const shipped = bundled().find(p => p.id === id);
|
|
53
|
-
if (!shipped) return { status: 'failed', error: `Bundled plugin '${id}' is no longer shipped in this package` };
|
|
53
|
+
if (!shipped) return { status: 'failed', error: `Bundled plugin '${id}' is no longer shipped in this package; reinstall it from its git repository with: total-recall plugin install <git-url> --global --replace` };
|
|
54
54
|
return hashPluginDir(shipped.dir).sha256 === plugin.installed_sha256 ? { status: 'current' } : { status: 'behind', install: id };
|
|
55
55
|
}
|
|
56
56
|
case 'git': {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Existing daemon maintenance verifies explicitly enrolled owner skill packages. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadRegistry } from './skills-registry.mjs';
|
|
5
|
+
import { runBounded } from './startup-health.mjs';
|
|
6
|
+
|
|
7
|
+
export async function maintainPortableSkills({ brainDir, apply = true, run = runBounded } = {}) {
|
|
8
|
+
const entry = loadRegistry(brainDir).skills.ssss;
|
|
9
|
+
if (!entry || !entry.layered || entry.source_type !== 'portable-owner-package') return { skipped: true, reason: 'no-enrolled-portable-source', failed: 0, results: [] };
|
|
10
|
+
const script = path.join(entry.source_path, 'core/scripts/maintain.mjs');
|
|
11
|
+
if (!fs.existsSync(script)) return { failed: 1, results: [], error: 'Registered portable source maintenance helper is missing' };
|
|
12
|
+
const result = await run(process.execPath, [script, '--source', entry.source_path, ...(apply ? ['--apply'] : [])], { timeoutMs: 5 * 60 * 1000, maxBytes: 8 * 1024 * 1024 });
|
|
13
|
+
try {
|
|
14
|
+
const report = JSON.parse(result.output);
|
|
15
|
+
return { ...report, failed: Math.max(report.failed || 0, result.code === 0 ? 0 : 1) };
|
|
16
|
+
} catch {
|
|
17
|
+
return { failed: 1, results: [], error: result.reason || result.output || 'Portable skill verification returned no report' };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-neutral gate decision. Every harness adapter normalises its native
|
|
3
|
+
* hook payload into `{ tool, command, path, cwd }` and asks `decide`; the answer
|
|
4
|
+
* is the same in every harness.
|
|
5
|
+
*
|
|
6
|
+
* Rules, in order:
|
|
7
|
+
* 1. Tools that only display or discover (`meta`, `ui`) are always open.
|
|
8
|
+
* 2. Reads of skill files are open: reading them is how the preflight is earned.
|
|
9
|
+
* 3. A single plain Total Recall preflight, context, recall or help command is
|
|
10
|
+
* open, so the gate can always be satisfied or repaired. Chaining, piping,
|
|
11
|
+
* substitution and redirects are not plain, so the exemption cannot carry
|
|
12
|
+
* other work and a capsule cannot be truncated through a pipe.
|
|
13
|
+
* 4. Everything else needs a fresh receipt for the directory.
|
|
14
|
+
* 5. With a receipt, a repository-wide source sweep still needs a recent
|
|
15
|
+
* Total Recall consult (the rule DSH enforced alone until now).
|
|
16
|
+
* Any error inside the gate denies: most harnesses fail open when a hook crashes.
|
|
17
|
+
*/
|
|
18
|
+
import { ACK_COMMAND } from './preflight-receipt.mjs';
|
|
19
|
+
|
|
20
|
+
export const TOOL_CLASSES = ['shell', 'read', 'write', 'search', 'web', 'mcp', 'agent', 'meta', 'ui', 'other'];
|
|
21
|
+
export const SWEEP_CONSULT_MS = 15 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
const SKILL_PATH = /\/\.(agent|claude)\/skills\//;
|
|
24
|
+
const SATISFYING = /^(cd\s+\S+\s*&&\s*)?(npx\s+)?(total-recall|node\s+\S*bin\/total-recall\.mjs)\s+(tr-preflight|preflight|context|recall|help|--help)(\s+[^;&|`<>$\n]*)?$/;
|
|
25
|
+
|
|
26
|
+
/** True when the whole command is one plain Total Recall invocation that satisfies or repairs the gate. */
|
|
27
|
+
export function isSatisfyingCommand(command) {
|
|
28
|
+
return SATISFYING.test(String(command || '').trim());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A repository-wide search used to answer a question, as the DSH `tr-first.sh` rule defined it. */
|
|
32
|
+
export function isRepoSweep(command) {
|
|
33
|
+
const c = String(command || '');
|
|
34
|
+
if (c.includes('total-recall')) return false;
|
|
35
|
+
const sweep = /(^|[\s;&|(])(rg|ripgrep|ag)\s/.test(c)
|
|
36
|
+
|| /(^|[\s;&|(])grep\s+(-[A-Za-z]*[rR][A-Za-z]*|--recursive)(\s|$)/.test(c)
|
|
37
|
+
|| /(^|[\s;&|(])find\s+(\.|\/Users)/.test(c)
|
|
38
|
+
|| /(^|[\s;&|(])git\s+grep(\s|$)/.test(c);
|
|
39
|
+
if (!sweep) return false;
|
|
40
|
+
if (/\|\s*(grep|rg|ag)(\s|$)/.test(c)) return false; // filtering command output is not a source question
|
|
41
|
+
if (/node_modules|\.agent\/skills|\/tmp\/|\.git\//.test(c)) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const allow = (rule) => ({ decision: 'allow', rule });
|
|
46
|
+
const deny = (rule, reason, fix = ACK_COMMAND) => ({ decision: 'deny', rule, reason, fix });
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {{ tool: string, command?: string, path?: string, cwd?: string }} call
|
|
50
|
+
* @param {{ check: () => { ok: boolean, problems: string[] }, recentConsult?: () => boolean, readablePaths?: RegExp[] }} deps
|
|
51
|
+
*/
|
|
52
|
+
export function decide(call, deps) {
|
|
53
|
+
try {
|
|
54
|
+
const tool = TOOL_CLASSES.includes(call?.tool) ? call.tool : 'other';
|
|
55
|
+
if (tool === 'meta' || tool === 'ui') return allow(`class:${tool}`);
|
|
56
|
+
if (tool === 'read') {
|
|
57
|
+
const target = String(call.path || '');
|
|
58
|
+
if (SKILL_PATH.test(target)) return allow('read:skill-file');
|
|
59
|
+
// Persisted output of an allowed command is readable; each adapter names its host's paths (audit F-019).
|
|
60
|
+
if ((deps.readablePaths || []).some((re) => re.test(target))) return allow('read:persisted-output');
|
|
61
|
+
}
|
|
62
|
+
if (tool === 'shell' && isSatisfyingCommand(call.command)) return allow('shell:satisfying-command');
|
|
63
|
+
|
|
64
|
+
const receipt = deps.check();
|
|
65
|
+
if (!receipt.ok) {
|
|
66
|
+
return deny('receipt', `TR-PREFLIGHT gate: ${receipt.problems[0] || 'check failed'} (directory: ${call.cwd || 'unknown'}). `
|
|
67
|
+
+ 'No work may start until it passes. Read the complete context capsule (never pipe or truncate it), then acknowledge.');
|
|
68
|
+
}
|
|
69
|
+
if (tool === 'shell' && isRepoSweep(call.command) && !(deps.recentConsult ? deps.recentConsult() : false)) {
|
|
70
|
+
return deny('sweep', 'TR-PREFLIGHT gate: do not sweep the repository for an answer; Total Recall owns it. Run `total-recall help "<task>"` to find the owning command, or `total-recall recall "<topic>" --local`, then repeat this call.',
|
|
71
|
+
'total-recall help "<task>"');
|
|
72
|
+
}
|
|
73
|
+
return allow('receipt:fresh');
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return deny('gate-error', `TR-PREFLIGHT gate error: ${err.message}. The gate fails closed.`);
|
|
76
|
+
}
|
|
77
|
+
}
|