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.
Files changed (128) hide show
  1. package/bin/total-recall.mjs +19 -4
  2. package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
  3. package/frontend/dist/assets/{index-BtuiZqwP.js → index-CRzjQLu7.js} +7 -7
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +8 -5
  6. package/frontend/src/api/auth.ts +0 -3
  7. package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
  9. package/frontend/src/pages/LoginPage.tsx +1 -41
  10. package/frontend/src/pages/PluginsPage.tsx +14 -7
  11. package/package.json +7 -5
  12. package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
  27. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
  33. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
  38. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
  42. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +23 -8
  43. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
  44. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
  46. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
  47. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  48. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
  49. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
  50. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
  51. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
  52. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +37 -10
  84. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +986 -280
  85. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
  86. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +25 -10
  87. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +6 -2
  88. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  89. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
  90. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
  91. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
  92. package/src/cli/command.mjs +23 -18
  93. package/src/cli/config.mjs +3 -0
  94. package/src/cli/connect.mjs +12 -116
  95. package/src/cli/daemon.mjs +23 -1
  96. package/src/cli/harness.mjs +0 -1
  97. package/src/cli/plugin/copies.mjs +8 -5
  98. package/src/cli/plugin/create.mjs +56 -3
  99. package/src/cli/plugin/index.mjs +40 -5
  100. package/src/cli/plugin/monetization.mjs +232 -12
  101. package/src/cli/preflight.mjs +161 -0
  102. package/src/cli/rebuild.mjs +22 -0
  103. package/src/cli/reset-password.mjs +3 -2
  104. package/src/cli/skill.mjs +8 -2
  105. package/src/cli/status.mjs +25 -7
  106. package/src/core/automatic-brain-updates.mjs +9 -3
  107. package/src/core/brain-updates.mjs +63 -5
  108. package/src/core/cli-commands.mjs +2 -0
  109. package/src/core/context-compiler.mjs +8 -1
  110. package/src/core/default-plugins.mjs +1 -1
  111. package/src/core/global-brain-updates.mjs +21 -1
  112. package/src/core/mesh-brain-updates.mjs +5 -2
  113. package/src/core/meta-harness.mjs +32 -79
  114. package/src/core/plugin-dependencies.mjs +6 -1
  115. package/src/core/plugin-runner.mjs +6 -1
  116. package/src/core/plugin-store.mjs +67 -1
  117. package/src/core/plugin-updates.mjs +1 -1
  118. package/src/core/portable-skills-maintenance.mjs +19 -0
  119. package/src/core/preflight-gate.mjs +77 -0
  120. package/src/core/preflight-receipt.mjs +123 -0
  121. package/src/core/required-plugins.mjs +7 -0
  122. package/src/core/runtime.mjs +0 -3
  123. package/src/core/skills-registry.mjs +33 -3
  124. package/src/core/surface.mjs +38 -1
  125. package/src/server/auth.mjs +41 -1
  126. package/src/server/memory-app.mjs +2 -1
  127. package/src/server/routes/auth.mjs +25 -1
  128. 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
- const deps = sections.flatMap(section => Object.entries(pkg[section] || {})
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 = fs.openSync(lock, 'wx', 0o600);
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?.startsWith(path.join(group.owner, 'node_modules') + path.sep);
258
+ return actual.version !== versions[dep.package] || !placed(actual.file);
204
259
  });
205
- if (bad || isLink(path.join(consumer.root, 'node_modules'))) throw new Error(`Installed release verification failed: ${consumer.root}${bad ? ` (${bad.key})` : ' (shared link)'}`);
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) {
@@ -22,6 +22,8 @@ export const CLI_COMMANDS = {
22
22
  edit: 'edit.mjs',
23
23
  recall: 'recall.mjs',
24
24
  context: 'context.mjs',
25
+ preflight: 'preflight.mjs',
26
+ 'tr-preflight': 'preflight.mjs',
25
27
  'context-stats': 'context-stats.mjs',
26
28
  rules: 'rules.mjs',
27
29
  search: 'recall.mjs',
@@ -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', 'cli-agents',
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
- const directory=path.join(root,name),file=path.join(directory,'package.json');
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 current=observed.success && report && !report.failed && 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']);
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=launched.success?'Awaiting independently verified remote report':launched.stderr || 'Mesh maintenance dispatch failed';
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
- latest = '1.3.1';
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 = '0.24.0';
558
- } else if (harnessId === 'gemini') {
559
- latest = '0.3.0';
546
+ latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
560
547
  } else if (harnessId === 'hermes') {
561
- latest = '0.1.0';
548
+ latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
562
549
  } else if (harnessId === 'dsh') {
563
- latest = '1.1.1';
550
+ latest = installedVersion !== 'unknown' ? installedVersion : 'unknown';
564
551
  } else if (harnessId === 'openclaw') {
565
- latest = '2026.4.10';
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: '0%',
597
- weeklyRemainingPct: '0%',
598
- quotaRemaining: 'Not available'
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
- let quotaRemaining = 'Active';
610
- let rolling5hRemainingPct = '100%';
611
- let weeklyRemainingPct = '100%';
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
- rolling5hRemainingPct = '100%';
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
- rolling5hRemainingPct = '95%';
733
- weeklyRemainingPct = '98%';
734
- quotaRemaining = 'DeepSeek reasoning pool active';
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
- source = path.dirname(require.resolve(alias || name));
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
- const declaredTimeout = plugin?.manifest?.cli?.subcommands?.find(item => item.name === operation)?.timeout_ms;
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(path.dirname(pluginsDir),'memory-derived/plugin-replacements',`${id}-${crypto.randomUUID()}`);
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
+ }