total-recall-brain 3.43.3 → 3.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/bin/total-recall.mjs +6 -4
  2. package/docs/ARCHITECTURE.md +10 -7
  3. package/docs/reference/cli-reference.md +16 -9
  4. package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
  5. package/frontend/dist/index.html +1 -1
  6. package/frontend/src/App.tsx +2 -2
  7. package/frontend/src/pages/PluginsPage.tsx +1 -1
  8. package/package.json +3 -3
  9. package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
  10. package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
  11. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
  12. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
  13. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
  14. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
  15. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
  16. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
  17. package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
  18. package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
  19. package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
  20. package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
  21. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
  22. package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
  23. package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  28. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
  31. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
  33. package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
  34. package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
  37. package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
  38. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
  39. package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
  40. package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
  44. package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
  49. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
  50. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
  51. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +15 -14
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
  54. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  55. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +15 -14
  56. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
  57. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
  58. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
  59. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
  60. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
  61. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
  63. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +17 -16
  64. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
  65. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
  77. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
  78. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
  79. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
  80. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
  87. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +15 -14
  88. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
  89. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  90. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
  91. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
  92. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
  93. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +160 -29
  94. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
  95. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
  96. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
  97. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
  98. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
  100. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +101 -16
  101. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +54 -21
  103. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
  105. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
  106. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +64 -15
  107. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
  109. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +6 -12
  112. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
  115. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
  116. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
  117. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
  118. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
  119. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +28 -12
  121. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
  122. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
  124. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
  125. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
  126. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
  129. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
  130. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
  131. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
  133. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +597 -70
  134. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
  135. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
  136. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
  137. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
  138. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +15 -14
  139. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
  140. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
  141. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
  142. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
  143. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +15 -14
  144. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
  145. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  146. package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
  147. package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
  148. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
  149. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
  150. package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
  151. package/src/cli/agent.mjs +1 -1
  152. package/src/cli/command.mjs +41 -2
  153. package/src/cli/connect.mjs +2 -29
  154. package/src/cli/context.mjs +1 -0
  155. package/src/cli/dream.mjs +93 -69
  156. package/src/cli/harness.mjs +4 -4
  157. package/src/cli/help.mjs +1 -1
  158. package/src/cli/ingest-okf.mjs +4 -4
  159. package/src/cli/init.mjs +0 -25
  160. package/src/cli/lessons.mjs +51 -0
  161. package/src/cli/memory-plugin.mjs +1 -1
  162. package/src/cli/plugin/copies.mjs +64 -14
  163. package/src/cli/plugin/create.mjs +6 -7
  164. package/src/cli/plugin/index.mjs +11 -7
  165. package/src/cli/plugin/install.mjs +10 -15
  166. package/src/cli/plugin/remove.mjs +1 -1
  167. package/src/cli/plugin/search.mjs +2 -2
  168. package/src/cli/preflight.mjs +19 -6
  169. package/src/cli/proposals.mjs +1 -1
  170. package/src/cli/recall.mjs +66 -23
  171. package/src/cli/release.mjs +4 -1
  172. package/src/cli/remember.mjs +3 -14
  173. package/src/cli/server.mjs +18 -0
  174. package/src/cli/startup.mjs +1 -0
  175. package/src/cli/task.mjs +9 -2
  176. package/src/cli/update.mjs +10 -3
  177. package/src/core/access-ledger.mjs +78 -0
  178. package/src/core/agent-manager.mjs +2 -2
  179. package/src/core/automatic-brain-updates.mjs +11 -6
  180. package/src/core/brain-locate.mjs +15 -0
  181. package/src/core/brain-updates.mjs +42 -12
  182. package/src/core/claude-hooks.mjs +3 -1
  183. package/src/core/cleanup-task.mjs +15 -0
  184. package/src/core/cli-commands.mjs +2 -0
  185. package/src/core/cli-maintenance.mjs +4 -1
  186. package/src/core/cli-semantic-index.mjs +23 -3
  187. package/src/core/context-compactor.mjs +7 -2
  188. package/src/core/context-compiler.mjs +26 -1
  189. package/src/core/context-policy.mjs +7 -3
  190. package/src/core/daemon-control.mjs +22 -0
  191. package/src/core/daemon-loop.mjs +39 -62
  192. package/src/core/default-plugins.mjs +1 -1
  193. package/src/core/dream.mjs +33 -105
  194. package/src/core/ensure-dependencies.mjs +148 -0
  195. package/src/core/fast-recall.mjs +2 -2
  196. package/src/core/field-evaluation.mjs +23 -0
  197. package/src/core/gate-mode.mjs +1 -1
  198. package/src/core/global-brain-updates.mjs +1 -1
  199. package/src/core/harness-hooks.mjs +14 -2
  200. package/src/core/lesson-hooks.mjs +74 -0
  201. package/src/core/lesson-memory.mjs +24 -0
  202. package/src/core/lesson-sessions.mjs +52 -0
  203. package/src/core/lessons.mjs +96 -0
  204. package/src/core/mesh-brain-updates.mjs +2 -1
  205. package/src/core/meta-harness.mjs +2 -2
  206. package/src/core/optimizer.mjs +66 -165
  207. package/src/core/plugin-contracts/card.mjs +3 -0
  208. package/src/core/plugin-loader.mjs +12 -3
  209. package/src/core/plugin-runtime.mjs +40 -1
  210. package/src/core/plugin-store.mjs +35 -23
  211. package/src/core/post-mortem.mjs +52 -36
  212. package/src/core/preflight-gate.mjs +23 -2
  213. package/src/core/proposal-applier.mjs +31 -2
  214. package/src/core/repo-sync.mjs +4 -6
  215. package/src/core/required-plugins.mjs +26 -4
  216. package/src/core/rule-length.mjs +23 -0
  217. package/src/core/runtime.mjs +1 -1
  218. package/src/core/scheduler.mjs +35 -43
  219. package/src/core/schema.mjs +1 -2
  220. package/src/core/search.mjs +4 -3
  221. package/src/core/server-restart.mjs +128 -7
  222. package/src/core/server-service.mjs +148 -0
  223. package/src/core/session-watcher.mjs +19 -1
  224. package/src/core/skill-gap.mjs +16 -0
  225. package/src/core/ssss-kernel-bridge.mjs +33 -6
  226. package/src/core/startup-health.mjs +6 -1
  227. package/src/core/surface.mjs +68 -57
  228. package/src/core/task-context.mjs +9 -8
  229. package/src/core/task-executors.mjs +54 -106
  230. package/src/core/validated-write.mjs +5 -0
  231. package/src/core/vault-files.mjs +8 -2
  232. package/src/core/vault.mjs +1 -1
  233. package/src/core/vector-field.mjs +134 -192
  234. package/src/server/index.mjs +3 -1
  235. package/src/server/invoke-core-child.mjs +24 -0
  236. package/src/server/invoke-core-operations.mjs +102 -0
  237. package/src/server/route-manifest.json +0 -56
  238. package/src/server/routes/brains.mjs +0 -18
  239. package/src/server/routes/context.mjs +0 -41
  240. package/src/server/routes/invoke.mjs +26 -1
  241. package/src/server/routes/ssss.mjs +0 -169
  242. package/src/server/routes/system.mjs +0 -15
  243. package/src/server/routes/tasks.mjs +0 -26
  244. package/src/server/routes/vault.mjs +2 -23
  245. package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
  246. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
  247. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
  248. package/src/core/append-log.mjs +0 -131
  249. package/src/core/plugin-tasks.mjs +0 -173
  250. package/templates/onboarding-interview.md +0 -82
  251. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
  252. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { atomicWrite } from './vault.mjs';
5
+ import { addTask, getTask, resolveQueueDir } from './task-envelope.mjs';
6
+ import { readLessonEntries, countLessonTurns, selectLessonTurns } from './lesson-sessions.mjs';
7
+ import { acquireBrainUpdateLock } from './brain-updates.mjs';
8
+
9
+ export const DEFAULT_LESSON_INTERVAL = 10;
10
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
11
+
12
+ /** A local hook only queues work. It never calls a model or elects a leader. */
13
+ export function tickLessons({ brainDir, sessionId, transcriptPath, event = 'Stop', interval,
14
+ turnId, turnCount, harness = 'unknown', hold } = {}) {
15
+ if (!brainDir || !sessionId) throw new Error('Lesson review requires a brain and session id');
16
+ const sessionsDir = path.join(brainDir, 'sessions');
17
+ const configPath = path.join(brainDir, 'config', 'lessons.json');
18
+ if (fs.existsSync(configPath)) {
19
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
20
+ if (config.enabled === false) return { tasks: [], skipped: 'disabled' };
21
+ interval ??= config.interval;
22
+ if (hold === undefined) hold = config.holdWrites === true;
23
+ }
24
+ interval ??= DEFAULT_LESSON_INTERVAL;
25
+ if (!Number.isInteger(interval) || interval < 1) throw new Error('Lesson interval must be a positive integer');
26
+ hold ??= false;
27
+ const root = path.join(brainDir, 'lesson-reviews');
28
+ fs.mkdirSync(root, { recursive: true });
29
+ // Match writeSession's identity so native hooks and later ingest share a cursor.
30
+ const localTranscript = transcriptPath && path.dirname(path.resolve(transcriptPath)) === path.resolve(sessionsDir);
31
+ const key = transcriptPath ? (localTranscript ? path.basename(transcriptPath, '.jsonl') : hash(path.resolve(transcriptPath))) : hash(`${harness}:${sessionId}`);
32
+ const lock = path.join(root, `${key}.lock`);
33
+ const lockFd = acquireBrainUpdateLock(lock, { waitMs: 0 });
34
+ try {
35
+ const stateFile = path.join(root, `${key}.json`);
36
+ const state = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : { turns: 0, queuedThrough: 0 };
37
+ let entries;
38
+ const savedTranscript = path.join(sessionsDir, `${key}.jsonl`);
39
+ if (!transcriptPath && fs.existsSync(savedTranscript)) transcriptPath = savedTranscript;
40
+ if (transcriptPath) {
41
+ entries = readLessonEntries(transcriptPath);
42
+ turnCount = countLessonTurns(entries);
43
+ if (!localTranscript) {
44
+ fs.mkdirSync(sessionsDir, { recursive: true });
45
+ atomicWrite(path.join(sessionsDir, `${key}.jsonl`), entries.map(entry => JSON.stringify(entry)).join('\n') + '\n');
46
+ }
47
+ }
48
+ if (turnCount !== undefined) {
49
+ if (!Number.isInteger(turnCount) || turnCount < state.turns) throw new Error('Lesson transcript turn count went backwards');
50
+ state.turns = turnCount;
51
+ } else if (event === 'UserPromptSubmit' && (!turnId || turnId !== state.lastTurnId)) {
52
+ state.turns++;
53
+ state.lastTurnId = turnId;
54
+ }
55
+ const final = event === 'SessionEnd' || event === 'session-end';
56
+ const tasks = [];
57
+ // Prompt hooks count only. Stop has the completed response and tool results.
58
+ if (event !== 'UserPromptSubmit' && entries) {
59
+ while (state.turns - state.queuedThrough >= interval || (final && state.turns > state.queuedThrough)) {
60
+ const fromTurn = state.queuedThrough + 1;
61
+ const toTurn = Math.min(state.turns, state.queuedThrough + interval);
62
+ const slug = `lesson-review-${key}-${fromTurn}-${toTurn}`;
63
+ const queueDir = resolveQueueDir(brainDir);
64
+ if (!getTask(queueDir, slug) && !fs.existsSync(path.join(root, `${slug}.done`))) {
65
+ const snapshot = path.join(root, `${slug}.jsonl`);
66
+ atomicWrite(snapshot, selectLessonTurns(entries, { fromTurn, toTurn }).map(entry => JSON.stringify(entry)).join('\n') + '\n');
67
+ addTask({ slug, intent: `Review session ${sessionId}, turns ${fromTurn}–${toTurn}`,
68
+ kind: 'memory', executor: 'lesson-review', category: 'lesson-review', priority: 40,
69
+ origin: { agent: harness, session_id: sessionId },
70
+ payload: { sessionPath: snapshot, fromTurn, toTurn, hold, sessionId },
71
+ }, queueDir);
72
+ }
73
+ tasks.push(slug);
74
+ state.queuedThrough = toTurn;
75
+ }
76
+ }
77
+ atomicWrite(stateFile, JSON.stringify(state));
78
+ return { turns: state.turns, queuedThrough: state.queuedThrough, tasks };
79
+ } finally {
80
+ // Remove only the inode this process acquired. A dead hook's lock is reclaimed
81
+ // by the shared lock owner on the next tick.
82
+ try { if (fs.statSync(lock).ino === fs.fstatSync(lockFd).ino) fs.unlinkSync(lock); }
83
+ finally { fs.closeSync(lockFd); }
84
+ }
85
+ }
86
+
87
+ /** Session-ingest fallback for harnesses without a lifecycle hook. Every node calls this. */
88
+ export function enqueueSessionReviews({ brainDir, now = Date.now(), quietMs = 5 * 60_000, interval, hold } = {}) {
89
+ const dir = path.join(brainDir, 'sessions');
90
+ if (!fs.existsSync(dir)) return [];
91
+ return fs.readdirSync(dir).filter(name => name.endsWith('.jsonl')).map(name => {
92
+ const transcriptPath = path.join(dir, name);
93
+ return tickLessons({ brainDir, sessionId: path.basename(name, '.jsonl'), transcriptPath, interval, hold,
94
+ event: now - fs.statSync(transcriptPath).mtimeMs >= quietMs ? 'SessionEnd' : 'Stop', harness: 'session-ingest' });
95
+ });
96
+ }
@@ -30,7 +30,8 @@ export async function updateMeshBrainPackages({version,versions,listNodes,exec,n
30
30
  let report,serving;try{const status=JSON.parse(observed.stdout);report=status.last;serving=status.server;}catch{}
31
31
  const localFailed=report?(report.local_failed ?? Math.max(0,(report.failed || 0)-(report.mesh?.failed || 0))):null;
32
32
  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']);
33
- // Maintenance cannot fix a lagging server, so this is reported and not dispatched again.
33
+ // Maintenance could not fix this server, or it would have reported a local failure above and
34
+ // been dispatched again, so this is reported. One that lags since the last run waits for that run's report to age.
34
35
  if(current && serving?.status==='stale'){row.status='failed';row.report=report;row.server=serving;row.error=serving.reason;continue;}
35
36
  if(current){row.status='verified';row.report=report;continue;}
36
37
  // npm exec pins the verified release so an old installed CLI can bootstrap
@@ -1,11 +1,11 @@
1
- /** Compatibility facade only. Adapter contracts, auth, usage and execution belong to Meta Harness. */
1
+ /** Compatibility facade only. Adapter contracts, auth, usage and execution belong to Metaharness. */
2
2
  import { loadPluginModule } from './plugin-runtime.mjs';
3
3
 
4
4
  let owner, ownerError;
5
5
  try { owner = await loadPluginModule('meta-harness', 'harness-compat'); }
6
6
  catch (error) { ownerError = error; }
7
7
  function implementation() {
8
- if (!owner) throw new Error(`Meta Harness is unavailable: ${ownerError?.message || 'install the plugin'}`);
8
+ if (!owner) throw new Error(`Metaharness is unavailable: ${ownerError?.message || 'install the plugin'}`);
9
9
  return owner;
10
10
  }
11
11
 
@@ -4,8 +4,8 @@ import crypto from 'crypto';
4
4
  import matter from './frontmatter.mjs';
5
5
  import { writeNode } from './vault.mjs';
6
6
  import { getNodes } from './vault-cache.mjs';
7
+ import { readAccess, lastUsedMs, activeDaysSince } from './access-ledger.mjs';
7
8
  import { logger } from './logger.mjs';
8
- import { brainDir } from './config.mjs';
9
9
 
10
10
  /**
11
11
  * Total Recall Optimizer
@@ -166,129 +166,6 @@ export async function generateMemoryCleanupProposals(vaultDir) {
166
166
  return proposals;
167
167
  }
168
168
 
169
- /**
170
- * Scans skill execution logs (if available) and proposes improvements.
171
- */
172
- /**
173
- * Scan skill files for quality issues and propose improvements.
174
- * Looks at skill files that haven't been updated in >30 days and
175
- * checks for common issues: missing examples, vague instructions, outdated commands.
176
- */
177
- export async function generateSkillImprovementProposals(vaultDir) {
178
- const agentDir = path.join(path.dirname(vaultDir));
179
- const skillsDir = path.join(agentDir, 'skills');
180
- const proposals = [];
181
-
182
- if (!fs.existsSync(skillsDir)) return proposals;
183
-
184
- const now = Date.now();
185
- const staleCutoff = 30 * 24 * 60 * 60 * 1000; // 30 days
186
-
187
- try {
188
- const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
189
- .filter(d => d.isDirectory())
190
- .map(d => path.join(skillsDir, d.name, 'SKILL.md'))
191
- .filter(p => fs.existsSync(p));
192
-
193
- for (const skillFile of skillDirs) {
194
- const stat = fs.statSync(skillFile);
195
- const ageMs = now - stat.mtimeMs;
196
- if (ageMs < staleCutoff) continue;
197
-
198
- const raw = fs.readFileSync(skillFile, 'utf8');
199
- const hasExamples = /##\s*example/i.test(raw);
200
- const hasCaution = /caution|warning|never|must not/i.test(raw);
201
- const lineCount = raw.split('\n').length;
202
-
203
- const issues = [];
204
- if (!hasExamples) issues.push('No examples section');
205
- if (!hasCaution) issues.push('No caution/constraint section');
206
- if (lineCount < 20) issues.push('Very short — may lack detail');
207
-
208
- if (issues.length > 0) {
209
- const skillName = path.basename(path.dirname(skillFile));
210
- proposals.push(createProposal(
211
- 'skill-improvement',
212
- `Improve skill: ${skillName} (${issues.join(', ')})`,
213
- skillFile,
214
- `Skill file is ${Math.floor(ageMs / 86400000)} days old. Issues: ${issues.join('; ')}`,
215
- ));
216
- }
217
- }
218
- } catch (err) {
219
- logger.debug('optimizer: generateSkillImprovementProposals failed to read skillsDir', { err: err.message });
220
- }
221
-
222
- return proposals;
223
- }
224
-
225
- /**
226
- * Scan the task queue for stuck or stalled workflows.
227
- * Tasks that have been 'in-progress' for >1 hour are considered stalled.
228
- */
229
- export async function generateWorkflowRepairProposals(vaultDir) {
230
- const queueDir = path.join(brainDir, 'scheduler', 'queue');
231
- const proposals = [];
232
-
233
- if (!fs.existsSync(queueDir)) return proposals;
234
-
235
- const now = Date.now();
236
- const stalledCutoff = 60 * 60 * 1000; // 1 hour
237
-
238
- try {
239
- const files = fs.readdirSync(queueDir).filter(f => f.endsWith('.md'));
240
- for (const file of files) {
241
- const raw = fs.readFileSync(path.join(queueDir, file), 'utf8');
242
- const { data } = matter(raw);
243
- if (data.status !== 'in-progress') continue;
244
-
245
- const updatedAt = new Date(data.updated_at || data.created || 0).getTime();
246
- if (now - updatedAt < stalledCutoff) continue;
247
-
248
- proposals.push(createProposal(
249
- 'workflow-repair',
250
- `Stalled task: ${data.slug || file}`,
251
- path.join(queueDir, file),
252
- `Task has been in-progress for ${Math.floor((now - updatedAt) / 60000)} minutes. May need retry or cancellation.`,
253
- ));
254
- }
255
- } catch (err) {
256
- logger.debug('optimizer: generateWorkflowRepairProposals failed to read queueDir', { err: err.message });
257
- }
258
-
259
- return proposals;
260
- }
261
-
262
- /**
263
- * Analyze the vault's model usage patterns and propose routing improvements.
264
- * Flags cases where high-priority tasks were sent to the local LLM
265
- * that should have used a frontier model.
266
- */
267
- export async function generateModelRoutingProposals(vaultDir) {
268
- const proposals = [];
269
- const nodes = getNodes(vaultDir);
270
-
271
- // Find high-importance facts with low confidence — likely needed frontier model
272
- const underconfident = nodes.filter(n =>
273
- n.status === 'active' &&
274
- n.category === 'facts' &&
275
- n.importance >= 4 &&
276
- n.confidence < 0.6 &&
277
- n.source?.agent === 'fact-seeker',
278
- );
279
-
280
- for (const node of underconfident.slice(0, 5)) {
281
- proposals.push(createProposal(
282
- 'model-routing',
283
- `Re-research with frontier model: "${node.title}"`,
284
- node.slug,
285
- `High-importance fact (importance: ${node.importance}) has low confidence (${node.confidence}). Originally researched by local model. Frontier model may produce better synthesis.`,
286
- ));
287
- }
288
-
289
- return proposals;
290
- }
291
-
292
169
  /**
293
170
  * Nodes that are important but have gone untouched — computed on demand.
294
171
  *
@@ -310,21 +187,6 @@ export function findStaleNodes(vaultDir, { days = 30, minImportance = 4 } = {})
310
187
  .sort((a, b) => new Date(a.last_accessed) - new Date(b.last_accessed));
311
188
  }
312
189
 
313
- /**
314
- * @deprecated Wrote tickets nothing reads. Retained because the dream cycle's
315
- * master switch (ENABLE_STALE_KNOWLEDGE_REFRESH, off) still references it.
316
- * Stale nodes are surfaced to a human via findStaleNodes instead; nothing
317
- * re-researches them automatically (RESEARCH_SYSTEM2).
318
- */
319
- export async function generateStaleKnowledgeRefreshProposals(vaultDir) {
320
- return findStaleNodes(vaultDir, { days: 30, minImportance: 4 }).map(node => createProposal(
321
- 'stale-knowledge-refresh',
322
- `Refresh stale high-importance memory: ${node.title}`,
323
- node.slug,
324
- 'Node has not been accessed in over 30 days but maintains high importance. Requires verification against current state.',
325
- ));
326
- }
327
-
328
190
  /**
329
191
  * Local Eval Gate for Proposal Promotion
330
192
  * Uses the local runtime to evaluate the validity of a proposal.
@@ -379,6 +241,14 @@ export async function evaluateProposalGate(proposal, runtimeConfig, vaultDir = n
379
241
  return stamp('accepted');
380
242
  }
381
243
 
244
+ // smart-decay is verified mechanically against live state: the memory still exists, is
245
+ // active and unprotected, and the access ledger shows no use since it was judged and for
246
+ // the full decay age. Only then may it reach `accepted` and auto-apply (DECAY-01).
247
+ if (proposal.category === 'smart-decay') {
248
+ const reason = verifySmartDecay(proposal, vaultDir);
249
+ return reason ? stamp('rejected', { rejection_reason: reason }) : stamp('accepted');
250
+ }
251
+
382
252
  // stale-knowledge-refresh no longer produces tickets at all — the work is
383
253
  // handed to the research queue, which can actually perform it. If one is
384
254
  // somehow generated, it is not something to auto-accept.
@@ -422,41 +292,67 @@ Output ONLY valid JSON.`;
422
292
  *
423
293
  * @param {string} vaultDir
424
294
  * @param {object} runtimeConfig
425
- * @param {number} [ageThresholdDays=60] Minimum age before considering decay
295
+ * @param {number} [ageThresholdDays=60] Active days of the brain without a use before considering decay
426
296
  * @returns {object[]} decay proposals
427
297
  */
428
- export async function runSmartDecay(vaultDir, runtimeConfig, ageThresholdDays = 60) {
429
- const { callLocalRuntime } = await import('./runtime.mjs');
298
+ /** Active days (days the brain was used), never calendar days: an idle brain never decays. */
299
+ export const DECAY_AGE_DAYS = 60;
300
+
301
+ /**
302
+ * Why a smart-decay proposal may not be applied, or null when live state supports it.
303
+ * Shared by the gate and the applier so both check the same facts.
304
+ */
305
+ export function verifySmartDecay(proposal, vaultDir, { now = Date.now(), ageThresholdDays = DECAY_AGE_DAYS } = {}) {
306
+ if (!vaultDir) return 'No vault to verify against.';
307
+ const slug = String(proposal.target_path || '').trim();
308
+ const node = getNodes(vaultDir).find(n => n.slug === slug);
309
+ if (!node) return `Memory ${slug} no longer exists.`;
310
+ if (node.status !== 'active') return `Memory ${slug} is already ${node.status}.`;
311
+ if (isDecayProtected(node)) return `Memory ${slug} is protected from decay.`;
312
+ const ledger = readAccess(path.join(path.dirname(vaultDir), 'memory-derived'));
313
+ const used = lastUsedMs(node, ledger);
314
+ if (proposal.decay_last_used_at && used > new Date(proposal.decay_last_used_at).getTime()) return `Memory ${slug} was used after it was judged.`;
315
+ if (activeDaysSince(ledger, used) < ageThresholdDays) return `Memory ${slug} was used within the brain's last ${ageThresholdDays} active days.`;
316
+ return null;
317
+ }
318
+
319
+ /** Memories decay never touches: rules, absolute-priority and importance-5 memories. */
320
+ export function isDecayProtected(node) {
321
+ return node.priority === 'absolute' || node.importance >= 5
322
+ || ['invariants', 'preferences', 'anti-patterns'].includes(node.category);
323
+ }
324
+
325
+ /** Active, unprotected memories unused for at least `ageThresholdDays` of the brain's active days. */
326
+ export function decayCandidates(nodes, ledger, { now = Date.now(), ageThresholdDays = DECAY_AGE_DAYS } = {}) {
327
+ return nodes
328
+ .filter(n => n.status === 'active' && n.type !== 'proposal' && !isDecayProtected(n))
329
+ .filter(n => activeDaysSince(ledger, lastUsedMs(n, ledger)) >= ageThresholdDays)
330
+ .sort((a, b) => lastUsedMs(a, ledger) - lastUsedMs(b, ledger));
331
+ }
332
+
333
+ export async function runSmartDecay(vaultDir, runtimeConfig, ageThresholdDays = DECAY_AGE_DAYS, { derivedDir = path.join(path.dirname(vaultDir), 'memory-derived'), call } = {}) {
334
+ const callLocalRuntime = call || (await import('./runtime.mjs')).callLocalRuntime;
430
335
  const nodes = getNodes(vaultDir);
431
336
  const now = Date.now();
432
- const ageThresholdMs = ageThresholdDays * 24 * 60 * 60 * 1000;
337
+ const ledger = readAccess(derivedDir);
433
338
  const proposals = [];
434
339
 
435
- // Candidates: active, not absolute priority, not accessed recently
436
- const candidates = nodes.filter(n => {
437
- if (n.status !== 'active') return false;
438
- if (n.priority === 'absolute') return false;
439
- if (n.importance >= 5) return false;
440
- const lastAccessed = new Date(n.last_accessed || n.updated || 0).getTime();
441
- return (now - lastAccessed) > ageThresholdMs;
442
- });
443
-
444
- // Process up to 5 candidates per run to avoid excessive LLM calls
445
- const batch = candidates.slice(0, 5);
340
+ // Oldest unused first, up to 5 per run to bound model calls.
341
+ const batch = decayCandidates(nodes, ledger, { now, ageThresholdDays }).slice(0, 5);
446
342
 
447
343
  for (const node of batch) {
448
- const ageDays = Math.floor((now - new Date(node.last_accessed || node.updated || 0).getTime()) / (1000 * 60 * 60 * 24));
344
+ const ageDays = activeDaysSince(ledger, lastUsedMs(node, ledger));
449
345
 
450
346
  const prompt = [
451
347
  `## Memory Node: ${node.slug}`,
452
348
  `Title: "${node.title}"`,
453
- `Category: ${node.category} | Importance: ${node.importance} | Age: ${ageDays} days`,
349
+ `Category: ${node.category} | Importance: ${node.importance} | Unused for ${ageDays} active days of its brain`,
454
350
  `Tags: ${(node.tags || []).join(', ')}`,
455
351
  '',
456
352
  '## Body',
457
353
  (node.body || '').slice(0, 800),
458
354
  '',
459
- `This node has not been accessed in ${ageDays} days. Should it be retained, archived, or decayed?`,
355
+ `This node has not been used on the last ${ageDays} days its brain was active. Should it be retained, archived, or decayed?`,
460
356
  ].join('\n');
461
357
 
462
358
  try {
@@ -468,15 +364,20 @@ export async function runSmartDecay(vaultDir, runtimeConfig, ageThresholdDays =
468
364
  const verdict = result.verdict || 'RETAINED';
469
365
 
470
366
  if (verdict !== 'RETAINED') {
471
- proposals.push(createProposal(
472
- 'smart-decay',
473
- `Smart Decay [${verdict}]: "${node.title}"`,
474
- node.slug,
475
- `Age: ${ageDays} days. Reasoning: ${result.reasoning || 'No reasoning provided.'} Confidence adjustment: ${result.confidence_adjustment || 0}`,
476
- ));
367
+ proposals.push({
368
+ ...createProposal(
369
+ 'smart-decay',
370
+ `Smart Decay [${verdict}]: "${node.title}"`,
371
+ node.slug,
372
+ `Unused for ${ageDays} active days. Reasoning: ${result.reasoning || 'No reasoning provided.'} Confidence adjustment: ${result.confidence_adjustment || 0}`,
373
+ ),
374
+ // The gate re-checks that the memory has not been used since this judgement.
375
+ decay_verdict: verdict,
376
+ decay_last_used_at: new Date(lastUsedMs(node, ledger)).toISOString(),
377
+ });
477
378
  }
478
- } catch {
479
- // Skip failed evaluations silently
379
+ } catch (err) {
380
+ logger.warn('optimizer', `Smart decay could not evaluate ${node.slug}: ${err.message}`);
480
381
  }
481
382
  }
482
383
 
@@ -55,6 +55,9 @@ export function validateCardContract(manifest) {
55
55
  else names.add(sub.name);
56
56
  if (sub.effect !== undefined && !COMMAND_EFFECTS.includes(sub.effect)) errors.push(`${where}.effect must be one of: ${COMMAND_EFFECTS.join(', ')}`);
57
57
  if (sub.output !== undefined && !OUTPUT_KINDS.includes(sub.output)) errors.push(`${where}.output must be one of: ${OUTPUT_KINDS.join(', ')}`);
58
+ // The runner refuses anything outside this range, so a manifest that
59
+ // declares it must fail validation rather than every call at run time.
60
+ if (sub.timeout_ms !== undefined && (!Number.isInteger(sub.timeout_ms) || sub.timeout_ms < 1 || sub.timeout_ms > 600000)) errors.push(`${where}.timeout_ms must be an integer from 1 to 600000`);
58
61
  if (sub.args !== undefined) {
59
62
  if (!Array.isArray(sub.args)) errors.push(`${where}.args must be an array`);
60
63
  else {
@@ -48,7 +48,7 @@ export function resolveProjectRoot(projectRoot, vaultDir) {
48
48
  return fs.existsSync(path.join(parent, '.agent')) ? parent : (projectRoot || process.cwd());
49
49
  }
50
50
 
51
- /** True when a 5-field cron expression is syntactically plausible. Semantics are checked by plugin-tasks. */
51
+ /** True when a 5-field cron expression is syntactically plausible. The scheduling owner checks semantics. */
52
52
  export function isCronExpression(expr) {
53
53
  if (typeof expr !== 'string') return false;
54
54
  const fields = expr.trim().split(/\s+/);
@@ -277,6 +277,13 @@ export function validatePluginManifest(manifest) {
277
277
  if (!/^[a-z][a-z0-9-]*$/.test(name) || !isSafeRelativePath(file)) errors.push(`Invalid runtime module ${name}`);
278
278
  }
279
279
  if (runtime.routers !== undefined && (!Array.isArray(runtime.routers) || runtime.routers.some(file => !isSafeRelativePath(file)))) errors.push('runtime.routers must contain safe relative paths');
280
+ // Daemon services name declared runtime modules; the daemon starts each one.
281
+ if (runtime.daemon !== undefined) {
282
+ if (!Array.isArray(runtime.daemon)) errors.push('runtime.daemon must be an array of runtime module names');
283
+ else for (const name of runtime.daemon) {
284
+ if (typeof name !== 'string' || !Object.hasOwn(runtime.modules || {}, name)) errors.push(`runtime.daemon names undeclared module ${name}`);
285
+ }
286
+ }
280
287
  }
281
288
  }
282
289
 
@@ -411,9 +418,11 @@ export function discoverPlugins(projectRoot = process.cwd()) {
411
418
  const seen = new Set();
412
419
  const plugins = [];
413
420
  const global = globalPluginsDir();
414
- const project = projectPluginsDir(projectRoot);
421
+ // Nothing is installed per repository: every plugin lives in the shared
422
+ // folder, and a repository only switches it on or off. A leftover repository
423
+ // plugin folder is never read (`plugin copies` reports and removes it).
424
+ // `projectRoot` stays in the signature for callers; discovery no longer depends on it.
415
425
  const sources = [
416
- ...(path.resolve(project) === path.resolve(global) ? [] : [[project, 'project']]),
417
426
  [global, 'global'],
418
427
  [path.join(PACKAGE_ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'plugins'), 'scaffold']
419
428
  ];
@@ -1,7 +1,45 @@
1
1
  /** Generic in-process extension loading. Domain behavior stays with its owner. */
2
+ import os from 'node:os';
2
3
  import { pathToFileURL } from 'node:url';
3
4
  import { discoverPlugins, resolvePluginFile, PACKAGE_ROOT } from './plugin-loader.mjs';
4
5
  import { pluginActivation } from './plugin-state.mjs';
6
+ import { logger } from './logger.mjs';
7
+
8
+ /**
9
+ * Start every daemon service an active plugin declares in `runtime.daemon`.
10
+ * Each named runtime module exports `start({ projectRoot })` and may return a
11
+ * stop function. Services run against the global brain (home), never the
12
+ * directory the daemon happens to start in. One failing service is logged and
13
+ * never stops the others.
14
+ * @returns {Promise<{ started: string[], failed: string[], stop: Function }>}
15
+ */
16
+ export async function startPluginDaemonServices({ projectRoot = os.homedir(), load = loadPluginModule } = {}) {
17
+ const stops = [];
18
+ const started = [];
19
+ const failed = [];
20
+ for (const plugin of discoverPlugins(projectRoot).filter(item => item.valid)) {
21
+ const names = plugin.manifest.runtime?.daemon || [];
22
+ if (!names.length || !pluginActivation(plugin, { projectRoot }).enabled) continue;
23
+ for (const name of names) {
24
+ const label = `${plugin.id}:${name}`;
25
+ try {
26
+ const module = await load(plugin.id, name, { projectRoot, activeRequired: true });
27
+ if (typeof module.start !== 'function') throw new Error('daemon module must export start()');
28
+ const stop = await module.start({ projectRoot });
29
+ if (typeof stop === 'function') stops.push(stop);
30
+ started.push(label);
31
+ } catch (err) {
32
+ failed.push(label);
33
+ logger.warn('plugin-runtime', `Daemon service ${label} failed to start: ${err.message}`);
34
+ }
35
+ }
36
+ }
37
+ return {
38
+ started,
39
+ failed,
40
+ stop: () => { for (const stop of stops) { try { stop(); } catch { /* already stopped */ } } },
41
+ };
42
+ }
5
43
 
6
44
  export async function loadPluginModule(id, name, { activeRequired = false, requireValid = true, projectRoot = process.cwd() } = {}) {
7
45
  const plugin = discoverPlugins(projectRoot).find(item => item.id === id);
@@ -18,7 +56,8 @@ export async function loadPluginModule(id, name, { activeRequired = false, requi
18
56
  }
19
57
  process.env.TR_PACKAGE_ROOT = PACKAGE_ROOT;
20
58
  const file = resolvePluginFile(plugin.dir, relative);
21
- return import(process.platform === 'win32' ? pathToFileURL(file).href : file);
59
+ // A file URL is unambiguous on every platform and under every module loader.
60
+ return import(pathToFileURL(file).href);
22
61
  }
23
62
 
24
63
  export async function loadPluginRouters() {
@@ -340,6 +340,31 @@ export function isGitSource(source) {
340
340
  export async function recompileAfterPluginMutation(projectRoot = process.cwd(), isGlobal = false, dependencies = {}) {
341
341
  try {
342
342
  const { compileSurface } = dependencies.compileSurface ? dependencies : await import('./surface.mjs');
343
+ // Only this layer's catalog changes. Entry files are static and rules and commands are
344
+ // read on demand at preflight, so a shared plugin change compiles no other repository
345
+ // (ON-DEMAND-CONTEXT-01; one install used to rebuild all registered brains).
346
+ await compileLayer(compileSurface, projectRoot, isGlobal);
347
+ if (isGlobal) {
348
+ // The repository the command ran in refreshes its own catalog now, so its plugin library
349
+ // never lags the change. That compile is cheap: entry files and vault indexes keep their
350
+ // own triggers, so only the catalog and CLI index move.
351
+ const agentDir = path.dirname(path.dirname(path.dirname(globalPluginsDir())));
352
+ const projectBrain = path.dirname(projectPluginsDir(projectRoot));
353
+ if (path.resolve(projectRoot, '.agent') !== path.resolve(agentDir) && fs.existsSync(projectBrain)) {
354
+ await compileLayer(compileSurface, projectRoot, false);
355
+ }
356
+ }
357
+ return { ok: true, inventoryUpdated: true };
358
+ } catch (err) {
359
+ try {
360
+ const { logger } = await import('./logger.mjs');
361
+ logger.warn('plugin-store', `Automatic compilation after plugin change failed: ${err.message}`);
362
+ } catch {}
363
+ return { ok: false, error: err.message };
364
+ }
365
+ }
366
+
367
+ async function compileLayer(compileSurface, projectRoot, isGlobal) {
343
368
  // Match the actual destination chosen by install/remove, not an ambient brain override.
344
369
  const pluginsDir = isGlobal ? globalPluginsDir() : projectPluginsDir(projectRoot);
345
370
  const brainDir = path.dirname(pluginsDir);
@@ -359,19 +384,6 @@ export async function recompileAfterPluginMutation(projectRoot = process.cwd(),
359
384
  const instructionsFile = path.join(surfaceDir, 'INSTRUCTIONS.md');
360
385
  const result = await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile, force: true, semantic: false });
361
386
  if (result.skipped && result.reason === 'stale-surface-code') throw new Error('Instruction compiler is stale; restart the host or run total-recall compile');
362
- if (isGlobal) {
363
- const { compileRegisteredProjects } = dependencies.compileRegisteredProjects ? dependencies : await import('../cli/rebuild.mjs');
364
- const failed = await compileRegisteredProjects();
365
- if (failed) throw new Error(`${failed} registered project surface compilation(s) failed`);
366
- }
367
- return { ok: true, inventoryUpdated: true };
368
- } catch (err) {
369
- try {
370
- const { logger } = await import('./logger.mjs');
371
- logger.warn('plugin-store', `Automatic compilation after plugin change failed: ${err.message}`);
372
- } catch {}
373
- return { ok: false, error: err.message };
374
- }
375
387
  }
376
388
 
377
389
  /**
@@ -485,12 +497,16 @@ export function pruneReplacementArchives(archiveDir, { id, keep = 1, apply = tru
485
497
  * @param {{ projectRoot?: string, global?: boolean, link?: boolean, peerDeps?: object, publicDeps?: object }} [options]
486
498
  */
487
499
  export async function installPlugin(source, options = {}) {
488
- const { projectRoot = process.cwd(), global: isGlobal = false, link = false, resolveBundle } = options;
500
+ const { projectRoot = process.cwd(), link = false, resolveBundle } = options;
489
501
  if (!source || typeof source !== 'string') throw new Error('Missing plugin source');
490
502
  source = source.trim();
503
+ // Nothing is installed per repository: every install goes to the shared
504
+ // folder, and a repository switches the plugin on or off with `plugin on|off`.
505
+ if (options.project === true) throw new Error("Plugins are installed only in the shared folder; a repository switches them with 'total-recall plugin on|off <id>'");
506
+ const isGlobal = true;
491
507
 
492
- const scope = isGlobal ? 'global' : 'project';
493
- const pluginsDir = isGlobal ? globalPluginsDir() : projectPluginsDir(projectRoot);
508
+ const scope = 'global';
509
+ const pluginsDir = globalPluginsDir();
494
510
 
495
511
  let manifest;
496
512
  let dest;
@@ -610,13 +626,9 @@ export async function installPlugin(source, options = {}) {
610
626
  }
611
627
  }
612
628
 
613
- export function findInstalled(id, { projectRoot = process.cwd(), global: isGlobal } = {}) {
614
- const all = isGlobal === true ? [] : discoverPlugins(projectRoot);
615
- if (isGlobal === true) {
616
- // discoverPlugins() hides a global plugin shadowed by a project one.
617
- return readPluginsDir(globalPluginsDir(), 'global').find((p) => p.id === id) || null;
618
- }
619
- return all.find((p) => p.id === id) || null;
629
+ export function findInstalled(id) {
630
+ // Every install lives in the shared folder.
631
+ return readPluginsDir(globalPluginsDir(), 'global').find((p) => p.id === id) || null;
620
632
  }
621
633
 
622
634
  export async function uninstallPlugin(id, options = {}) {