total-recall-brain 3.41.5 → 3.41.6

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 (131) hide show
  1. package/bin/total-recall.mjs +7 -4
  2. package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
  3. package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/package.json +11 -0
  6. package/frontend/src/App.tsx +47 -122
  7. package/frontend/src/components/plugins/plugin-frame.ts +1 -0
  8. package/frontend/src/index.css +152 -86
  9. package/frontend/src/pages/HelpPage.tsx +13 -10
  10. package/frontend/src/pages/MemoryPage.tsx +28 -30
  11. package/frontend/src/pages/NetworkPage.tsx +3 -3
  12. package/frontend/src/pages/PluginsPage.tsx +6 -5
  13. package/frontend/src/pages/RulesPage.tsx +16 -12
  14. package/package.json +3 -2
  15. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +8 -8
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -0
  17. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +8 -8
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +8 -8
  20. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -0
  21. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +8 -8
  22. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +3 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +8 -8
  24. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +4 -1
  25. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
  26. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
  27. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
  28. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +8 -8
  29. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -1
  30. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
  31. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +9 -3
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +10 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +3 -3
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +42 -190
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +1 -1
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +8 -8
  73. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -0
  74. package/src/cli/agent.mjs +12 -1
  75. package/src/cli/compact.mjs +2 -2
  76. package/src/cli/connect.mjs +56 -1
  77. package/src/cli/context.mjs +10 -7
  78. package/src/cli/doctor.mjs +5 -2
  79. package/src/cli/harness.mjs +62 -50
  80. package/src/cli/memory-entry.mjs +2 -2
  81. package/src/cli/preflight.mjs +152 -16
  82. package/src/cli/remember.mjs +19 -1
  83. package/src/cli/rules.mjs +54 -12
  84. package/src/cli/startup.mjs +1 -1
  85. package/src/cli/status.mjs +12 -2
  86. package/src/core/claude-hooks.mjs +41 -3
  87. package/src/core/cli-semantic-index.mjs +3 -2
  88. package/src/core/conflict-detector.mjs +23 -12
  89. package/src/core/context-compactor.mjs +2 -0
  90. package/src/core/context-policy.mjs +20 -0
  91. package/src/core/duplicate-guard.mjs +350 -0
  92. package/src/core/meta-harness.mjs +31 -11
  93. package/src/core/preflight-gate.mjs +2 -2
  94. package/src/core/preflight-receipt.mjs +28 -0
  95. package/src/core/scaffold-plugins.mjs +7 -0
  96. package/src/core/stdio-flush.mjs +23 -0
  97. package/src/core/surface.mjs +33 -10
  98. package/src/server/routes/plugins.mjs +2 -2
  99. package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
  100. package/frontend/dist/assets/index-WT3gs-x6.js +0 -5631
  101. package/frontend/src/components/DaemonLogsTab.tsx +0 -54
  102. package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
  103. package/frontend/src/components/TaskQueueTab.tsx +0 -193
  104. package/frontend/src/components/VoiceInput.tsx +0 -112
  105. package/frontend/src/pages/ChatPage.tsx +0 -1059
  106. package/frontend/src/pages/CollabPage.tsx +0 -470
  107. package/frontend/src/pages/DesignDocsPage.tsx +0 -621
  108. package/frontend/src/pages/FilesPage.tsx +0 -393
  109. package/frontend/src/pages/HealthPage.tsx +0 -359
  110. package/frontend/src/pages/InboxPage.tsx +0 -83
  111. package/frontend/src/pages/InstructionsPage.tsx +0 -583
  112. package/frontend/src/pages/IntegrationsPage.tsx +0 -761
  113. package/frontend/src/pages/MeshPage.css +0 -58
  114. package/frontend/src/pages/MeshPage.tsx +0 -1219
  115. package/frontend/src/pages/OkfPage.tsx +0 -535
  116. package/frontend/src/pages/SandboxPage.tsx +0 -113
  117. package/frontend/src/pages/SkillsPage.tsx +0 -1994
  118. package/frontend/src/pages/TasksPage.tsx +0 -427
  119. package/frontend/src/pages/UsagePage.tsx +0 -702
  120. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
  121. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
  122. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
  123. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
  124. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
  125. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
  126. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
  127. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
  128. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
  129. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
  130. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
  131. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
@@ -19,6 +19,8 @@ export const DOC_SOURCES = [
19
19
  { id: 'cline', index: 'https://docs.cline.bot/llms.txt', prefix: 'https://docs.cline.bot/', catalog:true },
20
20
  { id: 'openclaw', index: 'https://docs.openclaw.ai/llms.txt', prefix: 'https://docs.openclaw.ai/', catalog:true },
21
21
  { id: 'aider', index: 'https://aider.chat/docs/', prefix: 'https://aider.chat/docs/' },
22
+ { id: 'goose', index: 'https://block.github.io/goose/docs/quickstart', prefix: 'https://block.github.io/goose/docs/' },
23
+ { id: 'openhands', index: 'https://docs.openhands.dev/llms.txt', prefix: 'https://docs.openhands.dev/', catalog:true },
22
24
  { id: 'replit', index: 'https://docs.replit.com/llms.txt', prefix: 'https://docs.replit.com/', catalog:true },
23
25
  { id: 'lovable', index: 'https://docs.lovable.dev/llms.txt', prefix: 'https://docs.lovable.dev/', catalog:true },
24
26
  { id: 'ollama', index: 'https://docs.ollama.com/llms.txt', prefix: 'https://docs.ollama.com/', catalog:true },
@@ -29,7 +31,11 @@ function pageFile(directory,id,file){if(!/^[a-f0-9]{64}-[a-f0-9]{64}\.txt$/.test
29
31
  function atomic(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, value, { mode: 0o600 }); fs.renameSync(temp, file); }
30
32
  export function documentationDirectory() {
31
33
  const brain = process.env.TR_SECRETS_BRAIN || path.join(process.env.AGENT_DIR || path.join(os.homedir(), '.agent'), 'skills/total-recall');
32
- return path.join(brain, 'memory-derived/cli-agents/docs');
34
+ const directory = path.join(brain, 'memory-derived/meta-harness/docs');
35
+ // Keep the downloaded pages when the old directory name is still on disk.
36
+ const legacy = path.join(brain, 'memory-derived/cli-agents/docs');
37
+ if (!fs.existsSync(directory) && fs.existsSync(legacy)) { fs.mkdirSync(path.dirname(directory), { recursive: true }); fs.renameSync(legacy, directory); }
38
+ return directory;
33
39
  }
34
40
  function metadata(directory, id) { try { return JSON.parse(fs.readFileSync(path.join(directory,id,'index.json'),'utf8')); } catch { return null; } }
35
41
  export function documentationStatus({ directory = documentationDirectory(), agent, now = Date.now() } = {}) {
@@ -47,11 +53,11 @@ export function documentationStatus({ directory = documentationDirectory(), agen
47
53
  return { ok:agents.every(row=>row.fresh), directory, agents };
48
54
  }
49
55
  async function download(url, fetchImpl) {
50
- const allowed = new Set(['code.claude.com','claude.com','platform.claude.com','docs.anthropic.com','anthropic.com','developers.openai.com','openai.com','help.openai.com','learn.chatgpt.com','chatgpt.com','antigravity.google','cursor.com','docs.x.ai','dev.meta.ai','docs.devin.ai','docs.windsurf.com','windsurf.com','docs.cline.bot','docs.openclaw.ai','aider.chat','docs.replit.com','docs.lovable.dev','docs.ollama.com','raw.githubusercontent.com','api.github.com']);
56
+ const allowed = new Set(['code.claude.com','claude.com','platform.claude.com','docs.anthropic.com','anthropic.com','developers.openai.com','openai.com','help.openai.com','learn.chatgpt.com','chatgpt.com','antigravity.google','cursor.com','docs.x.ai','dev.meta.ai','docs.devin.ai','docs.windsurf.com','windsurf.com','docs.cline.bot','docs.openclaw.ai','aider.chat','docs.replit.com','docs.lovable.dev','docs.ollama.com','block.github.io','docs.openhands.dev','raw.githubusercontent.com','api.github.com']);
51
57
  let target = new URL(url);
52
58
  for(let redirects=0;redirects<5;redirects++) {
53
59
  if(target.protocol!=='https:' || target.username || target.password || !allowed.has(target.hostname)) throw new Error(`Unapproved official documentation origin: ${target.hostname}`);
54
- const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-CLI-Agents-Docs','Accept':'*/*'}});
60
+ const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-Meta-Harness-Docs','Accept':'*/*'}});
55
61
  if(response.status>=300 && response.status<400){target=new URL(response.headers.get('location'),target);continue;}
56
62
  // Some entries advertise Markdown exports which no longer exist while the
57
63
  // same official HTML document remains available. Fetch that representation.
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
- import { runWorker, runProcess, getActiveAdapters } from './worker.mjs';
4
+ import { runWorker, runProcess, getActiveAdapters, notReady } from './worker.mjs';
5
5
 
6
6
  async function hostModule(name) {
7
7
  const selected = process.env.TR_PACKAGE_ROOT;
@@ -36,7 +36,7 @@ export async function listTargets() {
36
36
 
37
37
  export function workerProgram(request) {
38
38
  const source = fs.readFileSync(new URL('./worker.mjs', import.meta.url), 'utf8');
39
- return source + '\nrunWorker(' + JSON.stringify(request) + ').then(result=>console.log(JSON.stringify(result))).catch(()=>{console.error("Selected agent worker failed");process.exitCode=1;});\n';
39
+ return source + '\nrunWorker(' + JSON.stringify(request) + ').then(result=>console.log(JSON.stringify(result))).catch(error=>{console.error(String(error?.message||"Selected agent worker failed"));process.exitCode=1;});\n';
40
40
  }
41
41
 
42
42
  export async function executeOnNode(node, request, { worker = runWorker, processRunner = runProcess, mesh, access } = {}) {
@@ -49,8 +49,8 @@ export async function executeOnNode(node, request, { worker = runWorker, process
49
49
  const resolved = access.resolveNodeAccess(target);
50
50
  const args = access.buildSshArgs(resolved, { command: 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"; exec node --input-type=module', extraOptions: ['BatchMode=yes', 'ConnectTimeout=10'] });
51
51
  if (!args) throw new Error('Selected mesh node has no verified SSH access');
52
- const result = await processRunner('ssh', args, { input: workerProgram(request), timeoutMs: request.operation === 'inspect' ? 120000 : 330000 });
53
- if (result.reason || result.exitCode !== 0) throw new Error('Selected mesh worker failed; verify node access, workspace and agent installation');
52
+ const result = await processRunner('ssh', args, { input: workerProgram(request), timeoutMs: request.operation === 'inspect' ? 120000 : (request.timeoutMs || 900000) + 30000 });
53
+ if (result.reason || result.exitCode !== 0) throw notReady(`Worker on ${node} failed: ${String(result.stderr || result.reason || 'verify node access, workspace and agent installation').trim().slice(0, 400)}`);
54
54
  try { return JSON.parse(result.stdout); } catch { throw new Error('Selected mesh worker returned invalid data'); }
55
55
  }
56
56
 
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Google Antigravity (agy) PreToolUse & lifecycle hook adapter.
3
+ * Official doc: https://antigravity.google/docs/hooks?tab=cli
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
10
+
11
+ export const agyAdapter = {
12
+ id: 'agy',
13
+ aliases: ['antigravity'],
14
+ level: 'enforced',
15
+ source: {
16
+ url: 'https://antigravity.google/docs/hooks?tab=cli',
17
+ retrieved: '2026-11-04',
18
+ note: 'Antigravity CLI hooks in ~/.gemini/config/hooks.json or .agents/hooks.json'
19
+ },
20
+ readablePaths: [
21
+ /\/\.gemini\/antigravity\/brain\/[^/]+\/\.system_generated\/tasks\/[^/]+\.log$/,
22
+ /\/\.gemini\/antigravity\/brain\/[^/]+\/\.system_generated\/steps\/[^/]+\/output\.txt$/
23
+ ],
24
+ limits: [
25
+ 'Hooks receive camelCase toolCall fields: toolCall.name and toolCall.args',
26
+ 'PreToolUse blocks execution with decision: "deny" and exit code 2'
27
+ ],
28
+
29
+ parse(input = {}) {
30
+ const event = input.event || input.hookEventName || 'PreToolUse';
31
+ const toolCall = input.toolCall || {};
32
+ const name = toolCall.name || input.toolName || input.tool_name || '';
33
+ const args = toolCall.args || input.toolArgs || input.tool_input || {};
34
+ const cwd = input.cwd || input.workspacePaths?.[0] || process.cwd();
35
+
36
+ let tool = 'other';
37
+ let command;
38
+ let filePath;
39
+
40
+ if (name === 'run_command' || name === 'Bash') {
41
+ tool = 'shell';
42
+ command = args.CommandLine || args.command || '';
43
+ } else if (name === 'read_file' || /^(Read|Glob|Grep)$/.test(name)) {
44
+ tool = 'read';
45
+ filePath = args.Path || args.file_path || args.path || '';
46
+ } else if (name === 'write_file' || name === 'edit_file' || /^(Edit|Write)$/.test(name)) {
47
+ tool = 'write';
48
+ filePath = args.Path || args.file_path || args.path || '';
49
+ } else if (name === 'search_web' || /^(WebFetch|WebSearch)$/.test(name)) {
50
+ tool = 'web';
51
+ } else if (/^(ask_question|show_message|manage_task)$/.test(name)) {
52
+ tool = 'meta';
53
+ } else if (/^(invoke_subagent|send_message|manage_subagents)$/.test(name)) {
54
+ tool = 'agent';
55
+ }
56
+
57
+ return {
58
+ event,
59
+ tool,
60
+ command,
61
+ path: filePath,
62
+ cwd,
63
+ raw: input
64
+ };
65
+ },
66
+
67
+ emit(decision, event, cwd = process.cwd()) {
68
+ if (decision?.decision === 'allow') {
69
+ const payload = { decision: 'allow' };
70
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
71
+ }
72
+
73
+ const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
74
+ const payload = {
75
+ decision: 'deny',
76
+ reason,
77
+ fix: decision?.fix || ACK_COMMAND
78
+ };
79
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
80
+ },
81
+
82
+ async run({ stdin, decideWith }) {
83
+ const parsed = this.parse(stdin);
84
+ const decision = decideWith({
85
+ tool: parsed.tool,
86
+ command: parsed.command,
87
+ path: parsed.path,
88
+ cwd: parsed.cwd
89
+ });
90
+ return this.emit(decision, parsed.event, parsed.cwd);
91
+ },
92
+
93
+ install({ home = os.homedir(), dryRun = false, remove = false, off = false } = {}) {
94
+ const isRemove = remove || off;
95
+ const geminiDir = path.join(home, '.gemini', 'config');
96
+ const hooksPath = path.join(geminiDir, 'hooks.json');
97
+ const added = [];
98
+ const removed = [];
99
+
100
+ let config = { hooks: {} };
101
+ if (fs.existsSync(hooksPath)) {
102
+ try {
103
+ config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
104
+ } catch (err) {
105
+ throw new Error(`${hooksPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
106
+ }
107
+ }
108
+ config.hooks = config.hooks || {};
109
+ config.hooks.PreToolUse = config.hooks.PreToolUse || [];
110
+
111
+ if (isRemove) {
112
+ const beforeLen = config.hooks.PreToolUse.length;
113
+ config.hooks.PreToolUse = config.hooks.PreToolUse.filter(e =>
114
+ !String(e.command || '').includes('preflight')
115
+ );
116
+ if (config.hooks.PreToolUse.length < beforeLen) {
117
+ removed.push('managed PreToolUse gate');
118
+ }
119
+ if (!dryRun && fs.existsSync(hooksPath)) {
120
+ fs.writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`);
121
+ }
122
+ return { settings: hooksPath, added: [], removed };
123
+ }
124
+
125
+ const gateCommand = 'total-recall preflight gate --harness agy';
126
+ const hasPreTool = config.hooks.PreToolUse.some(e =>
127
+ String(e.command || '').includes('preflight gate --harness agy')
128
+ );
129
+
130
+ if (!hasPreTool) {
131
+ config.hooks.PreToolUse.push({
132
+ matcher: '*',
133
+ command: gateCommand,
134
+ timeout: 10
135
+ });
136
+ added.push('managed PreToolUse gate');
137
+ }
138
+
139
+ if (!dryRun && added.length) {
140
+ fs.mkdirSync(geminiDir, { recursive: true });
141
+ if (fs.existsSync(hooksPath)) {
142
+ fs.copyFileSync(hooksPath, `${hooksPath}.bak-total-recall`);
143
+ }
144
+ fs.writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`);
145
+ }
146
+
147
+ return { settings: hooksPath, added };
148
+ },
149
+
150
+ verify({ home = os.homedir() } = {}) {
151
+ const hooksPath = path.join(home, '.gemini', 'config', 'hooks.json');
152
+ let installed = false;
153
+ if (fs.existsSync(hooksPath)) {
154
+ try {
155
+ const config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
156
+ const preHooks = config.hooks?.PreToolUse || [];
157
+ installed = preHooks.some(e => String(e.command || '').includes('preflight gate --harness agy'));
158
+ } catch { /* unparseable */ }
159
+ }
160
+
161
+ const syntheticDeniedCall = {
162
+ event: 'PreToolUse',
163
+ toolCall: {
164
+ name: 'run_command',
165
+ args: { CommandLine: 'rm -rf /tmp/foo' }
166
+ },
167
+ cwd: '/nonexistent-directory-without-receipt'
168
+ };
169
+
170
+ const out = this.emit(
171
+ { decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
172
+ 'PreToolUse',
173
+ syntheticDeniedCall.cwd
174
+ );
175
+
176
+ const exercised = out.exitCode === 2 && out.stdout.includes('"decision":"deny"');
177
+
178
+ return {
179
+ installed,
180
+ exercised,
181
+ level: 'enforced',
182
+ native: out.stdout.trim()
183
+ };
184
+ }
185
+ };
186
+
187
+ export default agyAdapter;
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Claude Code PreToolUse & UserPromptSubmit adapter.
3
+ * Official doc: https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
10
+
11
+ export const claudeAdapter = {
12
+ id: 'claude',
13
+ level: 'enforced',
14
+ source: {
15
+ url: 'https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview',
16
+ retrieved: '2026-11-04',
17
+ note: 'Claude Code hook configuration in settings.json with PreToolUse and UserPromptSubmit'
18
+ },
19
+ readablePaths: [
20
+ /\/\.claude\/projects\/[^/]+\/[^/]+\/tool-results\/[^/]+$/
21
+ ],
22
+ limits: [
23
+ 'UserPromptSubmit hook is advisory-only per ENFORCE-R1 and never blocks turn',
24
+ 'PreToolUse blocks tool executions with exit 2 or permissionDecision: "deny"'
25
+ ],
26
+
27
+ /**
28
+ * Parse Claude Code native hook JSON from stdin into normalized shape:
29
+ * { event, tool, command, path, cwd }
30
+ */
31
+ parse(input = {}) {
32
+ const event = input.hook_event_name || (input.tool_name ? 'PreToolUse' : 'UserPromptSubmit');
33
+ const toolName = input.tool_name || '';
34
+ const cwd = input.cwd || process.cwd();
35
+ const toolInput = input.tool_input || {};
36
+
37
+ let tool = 'other';
38
+ let command;
39
+ let filePath;
40
+
41
+ if (toolName === 'Bash') {
42
+ tool = 'shell';
43
+ command = toolInput.command || '';
44
+ } else if (/^(Read|Glob|Grep)$/.test(toolName)) {
45
+ tool = 'read';
46
+ filePath = toolInput.file_path || toolInput.path || toolInput.pattern || '';
47
+ } else if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(toolName)) {
48
+ tool = 'write';
49
+ filePath = toolInput.file_path || toolInput.notebook_path || '';
50
+ } else if (/^(WebFetch|WebSearch)$/.test(toolName)) {
51
+ tool = 'web';
52
+ } else if (/^mcp__ccd_turn__/.test(toolName)) {
53
+ tool = 'ui';
54
+ } else if (/^mcp__/.test(toolName)) {
55
+ tool = 'mcp';
56
+ } else if (toolName === 'Agent') {
57
+ tool = 'agent';
58
+ } else if (/^(ToolSearch|Skill|AskUserQuestion)$/.test(toolName)) {
59
+ tool = 'meta';
60
+ }
61
+
62
+ return {
63
+ event,
64
+ tool,
65
+ command,
66
+ path: filePath,
67
+ cwd,
68
+ raw: input
69
+ };
70
+ },
71
+
72
+ /**
73
+ * Emit Claude Code native response: { stdout, exitCode }
74
+ */
75
+ emit(decision, event, cwd = process.cwd()) {
76
+ if (event === 'UserPromptSubmit') {
77
+ // ENFORCE-R1: Prompt hooks never block
78
+ if (decision?.decision === 'deny') {
79
+ const payload = {
80
+ hookSpecificOutput: {
81
+ hookEventName: 'UserPromptSubmit',
82
+ additionalContext: `TR-PREFLIGHT FAILED for ${cwd}: ${decision.reason || 'check failed'}. First read the full context capsule (never truncate it), then run: ${decision.fix || ACK_COMMAND}`
83
+ }
84
+ };
85
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
86
+ }
87
+ return { stdout: '', exitCode: 0 };
88
+ }
89
+
90
+ // PreToolUse
91
+ if (decision?.decision === 'allow') {
92
+ return { stdout: '', exitCode: 0 };
93
+ }
94
+
95
+ // Denied
96
+ const payload = {
97
+ hookSpecificOutput: {
98
+ hookEventName: 'PreToolUse',
99
+ permissionDecision: 'deny',
100
+ permissionDecisionReason: decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`
101
+ }
102
+ };
103
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
104
+ },
105
+
106
+ /**
107
+ * Run the gate pipeline for Claude Code
108
+ */
109
+ async run({ stdin, decideWith }) {
110
+ const parsed = this.parse(stdin);
111
+ const decision = decideWith({
112
+ tool: parsed.tool,
113
+ command: parsed.command,
114
+ path: parsed.path,
115
+ cwd: parsed.cwd
116
+ });
117
+ return this.emit(decision, parsed.event, parsed.cwd);
118
+ },
119
+
120
+ /**
121
+ * Install Claude Code managed hooks in settings.json, removing the stopgap
122
+ */
123
+ install({ home = os.homedir(), dryRun = false, remove = false, off = false } = {}) {
124
+ const isRemove = remove || off;
125
+ const claudeDir = path.join(home, '.claude');
126
+ const settingsPath = path.join(claudeDir, 'settings.json');
127
+ const stopgapPath = path.join(claudeDir, 'hooks', 'total-recall-preflight.mjs');
128
+ const added = [];
129
+ const removed = [];
130
+
131
+ let settings = {};
132
+ if (fs.existsSync(settingsPath)) {
133
+ try {
134
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8') || '{}');
135
+ } catch (err) {
136
+ throw new Error(`${settingsPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
137
+ }
138
+ }
139
+
140
+ const hooks = settings.hooks || (settings.hooks = {});
141
+
142
+ if (isRemove) {
143
+ for (const event of ['PreToolUse', 'UserPromptSubmit', 'SessionStart', 'PostToolUse']) {
144
+ if (Array.isArray(hooks[event])) {
145
+ const beforeLen = hooks[event].length;
146
+ hooks[event] = hooks[event].filter(entry => {
147
+ const subHooks = entry.hooks || [];
148
+ return !subHooks.some(h => String(h.command || '').includes('preflight') || String(h.command || '').includes('total-recall-preflight'));
149
+ });
150
+ if (hooks[event].length < beforeLen) {
151
+ removed.push(`managed ${event} hook entry`);
152
+ }
153
+ }
154
+ }
155
+ if (!dryRun && (removed.length || fs.existsSync(settingsPath))) {
156
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
157
+ }
158
+ return { settings: settingsPath, added: [], removed };
159
+ }
160
+
161
+ // 1. Remove stopgap references if present
162
+ for (const event of ['PreToolUse', 'UserPromptSubmit', 'SessionStart', 'PostToolUse']) {
163
+ if (Array.isArray(hooks[event])) {
164
+ const beforeLen = hooks[event].length;
165
+ hooks[event] = hooks[event].filter(entry => {
166
+ const subHooks = entry.hooks || [];
167
+ return !subHooks.some(h => String(h.command || '').includes('total-recall-preflight.mjs'));
168
+ });
169
+ if (hooks[event].length < beforeLen) {
170
+ removed.push(`stopgap ${event} hook entry`);
171
+ }
172
+ }
173
+ }
174
+
175
+ // Remove stopgap file on disk if not dryRun
176
+ if (!dryRun && fs.existsSync(stopgapPath)) {
177
+ try {
178
+ fs.unlinkSync(stopgapPath);
179
+ removed.push('total-recall-preflight.mjs file');
180
+ } catch { /* ignore */ }
181
+ }
182
+
183
+ // 2. Add managed preflight gate entries
184
+ const gateCommand = 'total-recall preflight gate --harness claude';
185
+ const remindCommand = 'total-recall preflight remind';
186
+
187
+ // PreToolUse: match all tools
188
+ hooks.PreToolUse = hooks.PreToolUse || [];
189
+ const hasPreTool = hooks.PreToolUse.some(e =>
190
+ (e.hooks || []).some(h => String(h.command || '').includes('preflight gate --harness claude'))
191
+ );
192
+ if (!hasPreTool) {
193
+ hooks.PreToolUse.push({
194
+ matcher: '*',
195
+ hooks: [{
196
+ type: 'command',
197
+ command: gateCommand,
198
+ timeout: 5
199
+ }]
200
+ });
201
+ added.push('managed PreToolUse gate');
202
+ }
203
+
204
+ // UserPromptSubmit: advisory reminder
205
+ hooks.UserPromptSubmit = hooks.UserPromptSubmit || [];
206
+ const hasPrompt = hooks.UserPromptSubmit.some(e =>
207
+ (e.hooks || []).some(h => String(h.command || '').includes('preflight remind'))
208
+ );
209
+ if (!hasPrompt) {
210
+ hooks.UserPromptSubmit.push({
211
+ hooks: [{
212
+ type: 'command',
213
+ command: remindCommand,
214
+ timeout: 5
215
+ }]
216
+ });
217
+ added.push('managed UserPromptSubmit reminder');
218
+ }
219
+
220
+ if (!dryRun && (added.length || removed.length)) {
221
+ fs.mkdirSync(claudeDir, { recursive: true });
222
+ if (fs.existsSync(settingsPath)) {
223
+ fs.copyFileSync(settingsPath, `${settingsPath}.bak-total-recall`);
224
+ }
225
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
226
+ }
227
+
228
+ return { settings: settingsPath, added, removed };
229
+ },
230
+
231
+ /**
232
+ * Verify installed hook with a synthetic denied tool call
233
+ */
234
+ verify({ home = os.homedir() } = {}) {
235
+ const settingsPath = path.join(home, '.claude', 'settings.json');
236
+ if (!fs.existsSync(settingsPath)) {
237
+ return { installed: false, exercised: false, reason: 'settings.json not found' };
238
+ }
239
+ let settings;
240
+ try {
241
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8') || '{}');
242
+ } catch {
243
+ return { installed: false, exercised: false, reason: 'settings.json is unparseable' };
244
+ }
245
+ const preHooks = settings.hooks?.PreToolUse || [];
246
+ const installed = preHooks.some(e =>
247
+ (e.hooks || []).some(h => String(h.command || '').includes('preflight gate --harness claude'))
248
+ );
249
+
250
+ // Exercise with synthetic denied payload
251
+ const syntheticDeniedCall = {
252
+ hook_event_name: 'PreToolUse',
253
+ tool_name: 'Bash',
254
+ tool_input: { command: 'git status' },
255
+ cwd: '/nonexistent-directory-without-receipt'
256
+ };
257
+
258
+ const out = this.emit(
259
+ { decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
260
+ 'PreToolUse',
261
+ syntheticDeniedCall.cwd
262
+ );
263
+
264
+ const exercised = out.exitCode === 2 && out.stdout.includes('"permissionDecision":"deny"');
265
+
266
+ return {
267
+ installed,
268
+ exercised,
269
+ level: 'enforced',
270
+ native: out.stdout.trim()
271
+ };
272
+ }
273
+ };
274
+
275
+ export default claudeAdapter;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Cline Agent SDK & plugin hook adapter.
3
+ * Official doc: https://docs.cline.bot/sdk/plugins.md
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+
9
+ const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
10
+
11
+ export const clineAdapter = {
12
+ id: 'cline',
13
+ level: 'enforced (unverified live)',
14
+ source: {
15
+ url: 'https://docs.cline.bot/sdk/plugins.md',
16
+ retrieved: '2026-11-04',
17
+ note: 'Cline SDK plugin beforeTool hooks; hookErrorMode default is ignore (fail-open)'
18
+ },
19
+ limits: [
20
+ 'Cline defaults hookErrorMode to ignore (fail-open); requires explicit strict config',
21
+ 'Returns { skip: true, reason } or { cancel: true } on denied calls'
22
+ ],
23
+
24
+ parse(input = {}) {
25
+ const event = input.event || 'beforeTool';
26
+ const toolName = input.toolName || input.tool || '';
27
+ const toolInput = input.input || input.params || input.toolInput || {};
28
+ const cwd = input.cwd || process.cwd();
29
+
30
+ let tool = 'other';
31
+ let command;
32
+ let filePath;
33
+
34
+ if (toolName === 'execute_command' || toolName === 'Bash' || toolName === 'shell') {
35
+ tool = 'shell';
36
+ command = toolInput.command || toolInput.cmd || '';
37
+ } else if (toolName === 'write_to_file' || toolName === 'replace_in_file' || toolName === 'Edit' || toolName === 'Write') {
38
+ tool = 'write';
39
+ filePath = toolInput.path || toolInput.file_path || '';
40
+ } else if (toolName === 'read_file' || toolName === 'list_files') {
41
+ tool = 'read';
42
+ filePath = toolInput.path || '';
43
+ }
44
+
45
+ return {
46
+ event,
47
+ tool,
48
+ command,
49
+ path: filePath,
50
+ cwd,
51
+ raw: input
52
+ };
53
+ },
54
+
55
+ emit(decision, event, cwd = process.cwd()) {
56
+ if (decision?.decision === 'allow') {
57
+ const payload = { skip: false, cancel: false };
58
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
59
+ }
60
+
61
+ const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
62
+ const payload = {
63
+ skip: true,
64
+ cancel: true,
65
+ reason,
66
+ errorMessage: reason,
67
+ fix: decision?.fix || ACK_COMMAND
68
+ };
69
+ return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
70
+ },
71
+
72
+ async run({ stdin, decideWith }) {
73
+ const parsed = this.parse(stdin);
74
+ const decision = decideWith({
75
+ tool: parsed.tool,
76
+ command: parsed.command,
77
+ path: parsed.path,
78
+ cwd: parsed.cwd
79
+ });
80
+ return this.emit(decision, parsed.event, parsed.cwd);
81
+ },
82
+
83
+ install({ home = os.homedir(), dryRun = false } = {}) {
84
+ const clineDir = path.join(home, 'Documents', 'Cline', 'Hooks');
85
+ const hooksPath = path.join(clineDir, 'preflight-hook.json');
86
+ const added = [];
87
+
88
+ const hookDef = {
89
+ name: 'total-recall-preflight',
90
+ hookErrorMode: 'block',
91
+ command: 'total-recall preflight gate --harness cline',
92
+ timeout: 5
93
+ };
94
+
95
+ if (!dryRun) {
96
+ fs.mkdirSync(clineDir, { recursive: true });
97
+ fs.writeFileSync(hooksPath, `${JSON.stringify(hookDef, null, 2)}\n`);
98
+ added.push('managed Cline beforeTool hook');
99
+ }
100
+
101
+ return { settings: hooksPath, added };
102
+ },
103
+
104
+ verify({ home = os.homedir() } = {}) {
105
+ const hooksPath = path.join(home, 'Documents', 'Cline', 'Hooks', 'preflight-hook.json');
106
+ const installed = fs.existsSync(hooksPath);
107
+
108
+ const syntheticDeniedCall = {
109
+ event: 'beforeTool',
110
+ toolName: 'execute_command',
111
+ input: { command: 'rm -rf /tmp/foo' },
112
+ cwd: '/nonexistent-directory-without-receipt'
113
+ };
114
+
115
+ const out = this.emit(
116
+ { decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
117
+ 'beforeTool',
118
+ syntheticDeniedCall.cwd
119
+ );
120
+
121
+ const exercised = out.exitCode === 2 && (out.stdout.includes('"skip":true') || out.stdout.includes('"cancel":true'));
122
+
123
+ return {
124
+ installed,
125
+ exercised,
126
+ level: 'enforced (unverified live)',
127
+ native: out.stdout.trim()
128
+ };
129
+ }
130
+ };
131
+
132
+ export default clineAdapter;