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
@@ -47,6 +47,11 @@ export function detectProjectBrain(startDir = process.cwd()) {
47
47
  // sibling in src/cli/agent-dir.mjs checked both, so the two could disagree
48
48
  // about whether a project brain exists at all. It is now the single
49
49
  // implementation and the sibling delegates here.
50
+ // The Total Recall package's own `scaffold/` is the template every install copies. It carries
51
+ // a SKILL.md, so the walk took it for a project: a context call or `project-id` from inside
52
+ // it wrote brain.json and derived ledgers into the template, which then ships (2026-11-09).
53
+ // Skip it: the project that contains the package (its checkout or the consumer) is the brain.
54
+ if (isPackageScaffold(dir)) { dir = path.dirname(dir); continue; }
50
55
  for (const name of ['.agent', '.agents']) {
51
56
  const candidate = path.join(dir, name, 'skills', 'total-recall');
52
57
  if (fs.existsSync(path.join(candidate, 'SKILL.md')) || fs.existsSync(path.join(candidate, 'memory-vault'))) {
@@ -131,3 +136,13 @@ function pluginTargets(pluginsRoot) {
131
136
  }
132
137
  return targets;
133
138
  }
139
+
140
+ /** True for the Total Recall package's install template: `<package>/scaffold`, in a checkout or in node_modules. */
141
+ export function isPackageScaffold(dir) {
142
+ if (path.basename(dir) !== 'scaffold') return false;
143
+ try {
144
+ return JSON.parse(fs.readFileSync(path.join(path.dirname(dir), 'package.json'), 'utf8')).name === 'total-recall-brain';
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
@@ -19,6 +19,13 @@ const sections = ['dependencies', 'devDependencies', 'optionalDependencies', 'pe
19
19
  const ignored = new Set(['node_modules', 'vendor', 'dist', 'build', 'coverage', 'venv',
20
20
  'test', 'tests', '__tests__', 'fixtures', 'tmp', 'scratch', 'scaffold', 'memory-derived']);
21
21
  const canonical = key => key === '@ssss/cli' ? '@gregiteen/ssss-cli' : key;
22
+ // Plugins also force the engine alias through `overrides`. Those pins move with the dependency,
23
+ // or a consumer ends up asking for two versions of the same package.
24
+ const overrideKeys = pkg => Object.entries(pkg.overrides || {})
25
+ .filter(([key, spec]) => typeof spec === 'string' && BRAIN_PACKAGES.includes(canonical(key)) && canonical(key) !== pkg.name)
26
+ .map(([key]) => key);
27
+ const overridesLag = (pkg, versions) => overrideKeys(pkg)
28
+ .some(key => versions[canonical(key)] && pkg.overrides[key] !== targetSpec(key, versions[canonical(key)]));
22
29
  const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
23
30
  const isLink = file => { try { return fs.lstatSync(file).isSymbolicLink(); } catch { return false; } };
24
31
  export const brainUpdateLockPath = owner => path.join(os.tmpdir(),
@@ -109,7 +116,11 @@ export function discoverBrainConsumers(roots, { onError, installedPlugins = new
109
116
  .filter(([key]) => BRAIN_PACKAGES.includes(canonical(key)) && canonical(key) !== pkg.name)
110
117
  .map(([key, declared]) => ({ section, key, package: canonical(key), declared,
111
118
  ...installedBrainPackage(real, key) })));
112
- if (deps.length) consumers.push({ root: real, file, pkg, deps, runtimeOnly });
119
+ // A source checkout of a brain package is not a consumer: its pins move only
120
+ // with its own releases (updateSourceBrainPackage). Rewriting them left the
121
+ // cloud server's checkout dirty, and the release runner refuses a dirty one.
122
+ const brainSource = BRAIN_PACKAGES.includes(pkg.name);
123
+ if (deps.length && !brainSource) consumers.push({ root: real, file, pkg, deps, runtimeOnly });
113
124
  }
114
125
  for (const entry of fs.readdirSync(real, { withFileTypes: true })) {
115
126
  if (!entry.isDirectory() || ignored.has(entry.name)) continue;
@@ -184,13 +195,18 @@ function targetSpec(key, version) {
184
195
  return key === '@ssss/cli' ? `npm:@gregiteen/ssss-cli@${version}` : version;
185
196
  }
186
197
 
187
- /** Dry by default. Resolve releases once; install independently; verify actual resolution. */
188
198
  /**
189
- * A checkout that keeps no copy of a dev dependency runs under the shared install, so it has no
190
- * old version to replace. Its pin stays current, and its dev tree is its owner's to install: one
191
- * plugin's tree is about 460 MB, so restoring them all automatically could fill a disk.
199
+ * Automatic maintenance keeps each checkout's tree as found. A dependency with no installed
200
+ * copy, in a checkout (and owner) with no node_modules at all, gets its pin updated and nothing
201
+ * more: the tree was reclaimed for disk space or never installed, and restoring it is the
202
+ * owner's call. A dev dependency absent from a shared install is the same case. Without this,
203
+ * every hourly run reinstalled full trees into idle checkouts (~10 GB on 2026-11-07, again on
204
+ * 2026-11-09 until the disk filled). `installMissing: true` (CLI --install-missing) opts in.
192
205
  */
193
206
  const absentDevDep = (dep) => dep.section === 'devDependencies' && !dep.file;
207
+ const noInstallTree = (...roots) => roots.every(root => !fs.existsSync(path.join(root, 'node_modules')));
208
+ const keepAsFound = (dep, installMissing, ...roots) =>
209
+ !installMissing && !dep.file && (absentDevDep(dep) || noInstallTree(...roots));
194
210
 
195
211
  /**
196
212
  * The daemon runs with NODE_ENV=production, and under that npm and pnpm remove dev dependencies
@@ -203,6 +219,7 @@ export function installEnv(env = process.env) {
203
219
 
204
220
  export async function updateAllBrains(opts = {}) {
205
221
  const dryRun = opts.dryRun !== false;
222
+ const installMissing = opts.installMissing === true;
206
223
  const exec = opts.exec || spawnSync;
207
224
  const roots = listUpdateRoots(opts).map(row => row.root);
208
225
  const errors = [];
@@ -261,14 +278,18 @@ export async function updateAllBrains(opts = {}) {
261
278
  row.status = 'failed'; row.error = 'Newer local/preview package; refusing an implicit downgrade'; continue;
262
279
  }
263
280
  const shared = isLink(path.join(owner.owner, 'node_modules')) || isLink(path.join(consumer.root, 'node_modules'));
264
- const needsChange = shared || consumer.deps.some(dep => {
281
+ const needsChange = shared || (!consumer.runtimeOnly && overridesLag(consumer.pkg, versions)) || consumer.deps.some(dep => {
265
282
  const pinLags = !consumer.runtimeOnly && dep.declared !== targetSpec(dep.key, versions[dep.package]);
266
- if (absentDevDep(dep)) return pinLags;
283
+ if (keepAsFound(dep, installMissing, consumer.root, owner.owner)) return pinLags;
267
284
  return pinLags || dep.version !== versions[dep.package]
268
285
  || !dep.file?.startsWith(path.join(owner.owner, 'node_modules') + path.sep);
269
286
  });
270
- if (!needsChange) continue;
271
- row.status = dryRun ? 'would_update' : 'pending';
287
+ if (!needsChange) {
288
+ // Kept as found: report the missing tree, install nothing.
289
+ if (consumer.deps.some(dep => !dep.version)) row.status = 'not_installed';
290
+ continue;
291
+ }
292
+ row.status = dryRun ? (consumer.deps.some(dep => !dep.version) ? 'not_installed' : 'would_update') : 'pending';
272
293
  if (!groups.has(owner.owner)) groups.set(owner.owner, { ...owner, consumers: [] });
273
294
  groups.get(owner.owner).consumers.push({ consumer, row });
274
295
  }
@@ -283,7 +304,8 @@ export async function updateAllBrains(opts = {}) {
283
304
  const runtimeOnly = group.consumers.every(({ consumer }) => consumer.runtimeOnly);
284
305
  const unpinned = runtimeOnly && group.manager === 'npm';
285
306
  const needsInstall = group.consumers.some(({ consumer }) =>
286
- isLink(path.join(consumer.root, 'node_modules')) || consumer.deps.some(dep => !absentDevDep(dep)
307
+ isLink(path.join(consumer.root, 'node_modules')) || consumer.deps.some(dep =>
308
+ !keepAsFound(dep, installMissing, consumer.root, group.owner)
287
309
  && (dep.version !== versions[dep.package] || !dep.file?.startsWith(path.join(group.owner, 'node_modules') + path.sep))));
288
310
  try {
289
311
  lockFd = acquireBrainUpdateLock(lock, opts.lockWaitMs === undefined ? {} : { waitMs: opts.lockWaitMs });
@@ -296,6 +318,9 @@ export async function updateAllBrains(opts = {}) {
296
318
  saved.set(consumer.file, text);
297
319
  const pkg = read(consumer.file);
298
320
  for (const dep of consumer.deps) pkg[dep.section][dep.key] = targetSpec(dep.key, versions[dep.package]);
321
+ for (const key of overrideKeys(pkg)) {
322
+ if (versions[canonical(key)]) pkg.overrides[key] = targetSpec(key, versions[canonical(key)]);
323
+ }
299
324
  const indent = /\n([ \t]+)"/.exec(text)?.[1] || 2;
300
325
  fs.writeFileSync(consumer.file, JSON.stringify(pkg, null, indent) + '\n');
301
326
  }
@@ -312,7 +337,10 @@ export async function updateAllBrains(opts = {}) {
312
337
  if (runtimeOnly) args.push(group.manager === 'pnpm' ? '--prod' : '--omit=dev');
313
338
  if (unpinned) args.push('--no-save', ...new Set(group.consumers.flatMap(({ consumer }) =>
314
339
  consumer.deps.map(dep => `${dep.key}@${targetSpec(dep.key, versions[dep.package])}`))));
315
- else if (!needsInstall) args.push(group.manager === 'pnpm' ? '--lockfile-only' : '--package-lock-only');
340
+ // A lockfile-only update builds no tree, so it runs no lifecycle scripts: npm still
341
+ // ran a checkout's own postinstall here, which fails in a repository that was never
342
+ // installed (a postinstall calling a tool that is not yet installed, 2026-11-09).
343
+ else if (!needsInstall) args.push(group.manager === 'pnpm' ? '--lockfile-only' : '--package-lock-only', '--ignore-scripts');
316
344
  const install = exec(group.manager, args, { cwd: group.owner, env: installEnv(), encoding: 'utf8',
317
345
  timeout: 600000, maxBuffer: 8 * 1024 * 1024 });
318
346
  if (install.status !== 0) throw new Error(install.error?.message || install.stderr?.trim().slice(-1500) || `Install exited ${install.status}`);
@@ -328,7 +356,7 @@ export async function updateAllBrains(opts = {}) {
328
356
  const placed = (file) => Boolean(file) && allowedRoots.some((root) => file.startsWith(root));
329
357
  const bad = consumer.deps.find(dep => {
330
358
  const actual = installedBrainPackage(consumer.root, dep.key);
331
- if (absentDevDep(dep) && !actual.file) return false;
359
+ if (keepAsFound(dep, installMissing, consumer.root, group.owner) && !actual.file) return false;
332
360
  return actual.version !== versions[dep.package] || !placed(actual.file);
333
361
  });
334
362
  // A link is only a fault when it escapes every legitimate root; a self-contained one is fine.
@@ -358,5 +386,7 @@ export async function updateAllBrains(opts = {}) {
358
386
  updated: results.filter(row => row.status === 'updated').length,
359
387
  shared_install: results.filter(row => row.status === 'shared_install').length,
360
388
  would_update: results.filter(row => row.status === 'would_update').length,
389
+ not_installed: results.filter(row => row.status === 'not_installed').length,
390
+ up_to_date: results.filter(row => row.status === 'up_to_date').length,
361
391
  failed: results.filter(row => row.status === 'failed').length, results };
362
392
  }
@@ -12,6 +12,7 @@ import fs from 'node:fs';
12
12
  import os from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { installLessonHooks } from './lesson-hooks.mjs';
15
16
 
16
17
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
17
18
  export const GATE_TEMPLATE = path.join(PACKAGE_ROOT, 'templates', 'claude-hooks', 'total-recall-gate.mjs');
@@ -104,5 +105,6 @@ export function installClaudeCodeHooks(opts = {}) {
104
105
  if (fs.existsSync(settingsPath)) fs.copyFileSync(settingsPath, `${settingsPath}.bak-total-recall`);
105
106
  fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
106
107
  }
107
- return { gate: hookPath, settings: settingsPath, added, removed };
108
+ const lessons = installLessonHooks({ harness: 'claude', settings: settingsPath, status: 'installed' }, { dryRun: opts.dryRun });
109
+ return { gate: hookPath, settings: settingsPath, added, removed, lessons };
108
110
  }
@@ -0,0 +1,15 @@
1
+ import { generateMemoryCleanupProposals, dedupeProposals, evaluateProposalGate } from './optimizer.mjs';
2
+ import { writeNode } from './vault.mjs';
3
+
4
+ /** Compaction uses the same gate, identity and review path as dream cleanup. */
5
+ export async function fileCleanupProposals(vaultDir, { generate = generateMemoryCleanupProposals,
6
+ dedupe = dedupeProposals, gate = evaluateProposalGate, write = writeNode } = {}) {
7
+ const { kept, suppressed } = dedupe(await generate(vaultDir), vaultDir);
8
+ let persisted = 0;
9
+ for (const proposal of kept) {
10
+ await gate(proposal, null, vaultDir);
11
+ await write(proposal, vaultDir);
12
+ persisted++;
13
+ }
14
+ return { success: true, persisted, suppressed, output: `Compaction filed ${persisted} cleanup proposals`, executor: 'compaction' };
15
+ }
@@ -5,10 +5,12 @@ export const CLI_COMMANDS = {
5
5
  setup: 'setup.mjs',
6
6
  start: 'start.mjs',
7
7
  startup: 'startup.mjs',
8
+ server: 'server.mjs',
8
9
  deploy: 'deploy.mjs',
9
10
  doctor: 'doctor.mjs',
10
11
  backup: 'backup.mjs',
11
12
  dream: 'dream.mjs',
13
+ lessons: 'lessons.mjs',
12
14
  lint: 'lint.mjs',
13
15
  daemon: 'daemon.mjs',
14
16
  restore: 'restore.mjs',
@@ -21,7 +21,10 @@ export function cliInputFingerprint(root) {
21
21
 
22
22
  export async function maintainCliSurfaces({brainDir,brains,force=false,run=runBounded,fingerprint=cliInputFingerprint}={}) {
23
23
  const results=[];
24
- for(const brain of brains || listLocalBrains({globalBrainDir:brainDir})) {
24
+ // By default only the global brain: entry files are static and each repository reads its
25
+ // rules and commands on demand, so the daemon never fans a refresh out across registered
26
+ // repositories (ON-DEMAND-CONTEXT-01). An explicit brain list is still honoured.
27
+ for(const brain of brains || listLocalBrains({globalBrainDir:brainDir}).filter(item=>item.kind==='global')) {
25
28
  if(!brain.exists || brain.enabled===false)continue;
26
29
  const root=brain.root || path.dirname(path.dirname(brain.brainDir));
27
30
  const file=path.join(brain.brainDir,'memory-derived/cli-maintenance.json');
@@ -22,6 +22,21 @@ import { logger } from './logger.mjs';
22
22
  import { atomicWrite } from './vault.mjs';
23
23
  import { globalAgentDir } from './local-config.mjs';
24
24
 
25
+ /** Usage text for declared plugin arguments: positional as <label>, flags as [--flag <label>] or [--flag]. */
26
+ export function formatSubcommandArgs(args) {
27
+ if (typeof args === 'string') return args ? ` ${args}` : '';
28
+ if (!Array.isArray(args)) return '';
29
+ const parts = [];
30
+ for (const arg of args) {
31
+ if (!arg || typeof arg !== 'object') continue;
32
+ const label = String(arg.name || arg.label || 'value').replace(/\s+/g, '-');
33
+ if (arg.positional || !arg.flag) parts.push(arg.required ? `<${label}>` : `[${label}]`);
34
+ else if (arg.type === 'boolean') parts.push(arg.required ? arg.flag : `[${arg.flag}]`);
35
+ else parts.push(arg.required ? `${arg.flag} <${label}>` : `[${arg.flag} <${label}>]`);
36
+ }
37
+ return parts.length ? ` ${parts.join(' ')}` : '';
38
+ }
39
+
25
40
  /** Canonical descriptions and usage signatures for built-in core commands. */
26
41
  const CORE_METADATA = {
27
42
  app: {
@@ -49,6 +64,11 @@ const CORE_METADATA = {
49
64
  usage: 'total-recall startup [check|ensure]',
50
65
  effect: 'write'
51
66
  },
67
+ server: {
68
+ description: 'Repair and verify the macOS server LaunchAgent for this package.',
69
+ usage: 'total-recall server ensure [--dry-run]',
70
+ effect: 'write'
71
+ },
52
72
  deploy: {
53
73
  description: 'Deploy host runtime layers, daemon services, or launch browser setup.',
54
74
  usage: 'total-recall deploy [--ui]',
@@ -66,7 +86,7 @@ const CORE_METADATA = {
66
86
  },
67
87
  dream: {
68
88
  description: 'Trigger memory consolidation sleep cycle.',
69
- usage: 'total-recall dream',
89
+ usage: 'total-recall dream [--all-brains] [--dry-run] [--json]',
70
90
  effect: 'write'
71
91
  },
72
92
  lint: {
@@ -320,7 +340,7 @@ const CORE_METADATA = {
320
340
  effect: 'read'
321
341
  },
322
342
  harness: {
323
- description: 'Meta-harness orchestration, task dispatch, and council consensus.',
343
+ description: 'Metaharness orchestration, task dispatch, and council consensus.',
324
344
  usage: 'total-recall harness [dispatch|council|usage]',
325
345
  effect: 'write'
326
346
  },
@@ -431,7 +451,7 @@ export function harvestAllCliVerbs({ projectRoot = process.cwd(), skillsDir = nu
431
451
  if (seenIds.has(subId)) continue;
432
452
  seenIds.add(subId);
433
453
 
434
- const argsStr = sub.args ? ` ${sub.args}` : '';
454
+ const argsStr = formatSubcommandArgs(sub.args);
435
455
  verbs.push({
436
456
  id: subId,
437
457
  name: sub.name,
@@ -62,6 +62,9 @@ export function compactRules(items, { budget, expand = [], header, complete = fa
62
62
  if (unknown.length) throw new Error(`Cannot expand rules not in this capsule: ${unknown.join(', ')}`);
63
63
  const duplicateOf = findDuplicates(ranked.filter(item => !expand.includes(item.slug)));
64
64
  const tiers = new Map(ranked.map(item => [item.slug, 'omitted']));
65
+ const isEvidence = item => item.taskEvidence && !item.required;
66
+ const supplement = supplemental.filter(item => item.text && !isEvidence(item)).map(item => item.text.trim());
67
+ const evidence = supplemental.filter(item => item.text && isEvidence(item)).map(item => item.text.trim()).join('\n\n---\n\n');
65
68
 
66
69
  const render = () => {
67
70
  const of = tier => ranked.filter(item => tiers.get(item.slug) === tier);
@@ -71,10 +74,11 @@ export function compactRules(items, { budget, expand = [], header, complete = fa
71
74
  const blocks = [
72
75
  ...full.map(item => item.text.trim()),
73
76
  ...summary.map(item => `[${item.slug}] ${summarizeRule(item.body)}`),
74
- ...supplemental.filter(item => item.text).map(item => item.text.trim()),
77
+ ...supplement,
75
78
  ];
76
79
  if (reference.length) blocks.push(`Referenced: ${reference.map(item => item.slug).join(', ')}`);
77
80
  if (duplicate.length) blocks.push(`Duplicates: ${duplicate.map(item => `${item.slug} (of ${duplicateOf.get(item.slug)})`).join(', ')}`);
81
+ if (evidence) blocks.push(evidence);
78
82
  const body = blocks.join('\n\n---\n\n');
79
83
  // The token count sits inside the text it counts, so iterate to a stable value.
80
84
  let tokens = estimateTokens(body);
@@ -142,5 +146,6 @@ export function compactRules(items, { budget, expand = [], header, complete = fa
142
146
  for (const slug of duplicateOf.keys()) tryTier(ranked.find(item => item.slug === slug), 'duplicate');
143
147
 
144
148
  const { context, counts, tokens } = render();
145
- return { context, counts, tokens, tiers: Object.fromEntries(tiers) };
149
+ return { context, counts, tokens, tiers: Object.fromEntries(tiers),
150
+ ...(evidence && { evidenceStart: context.length - evidence.length }) };
146
151
  }
@@ -1,6 +1,9 @@
1
1
  /** Local task capsules: explicit required rules, ranked supporting knowledge, complete-render accounting. */
2
2
 
3
+ import path from 'node:path';
3
4
  import { getNodes } from './vault-cache.mjs';
5
+ import { recordAccess } from './access-ledger.mjs';
6
+ import { brainDir as globalBrainDir } from './local-config.mjs';
4
7
  import crypto from 'node:crypto';
5
8
  import { selectRules, assembleContext, curatedRules, ruleFingerprint, RULE_CATEGORIES } from './context-policy.mjs';
6
9
  import { retrieveTaskKnowledge, summarizeRetrieval } from './task-context.mjs';
@@ -109,7 +112,10 @@ export async function compileContext({
109
112
  }
110
113
 
111
114
  const cliContributions = cliVerbContribution ? [cliVerbContribution] : [];
112
- const supporting = [...knowledge.contributions, ...cliContributions];
115
+ const evidence = knowledge.summary.selected
116
+ ? knowledge.contributions.map(item => ({ ...item, taskEvidence: true }))
117
+ : knowledge.contributions;
118
+ const supporting = [...evidence, ...cliContributions];
113
119
  const result = assembleContext([...required, ...supporting, ...contributions], { total: b.total });
114
120
  const evidenceMissing = supporting.some(item => !result.stats.contributions.some(placed => placed.id === item.id));
115
121
  // Compaction may reclaim rule explanation space; never claim readiness after losing selected task evidence.
@@ -139,9 +145,26 @@ export async function compileContext({
139
145
  result.stats.slots = { invariants: { nodes: rules.filter(n => n.category === 'invariants').length,
140
146
  tokens: result.stats.required_tokens } };
141
147
  result.stats.slots_filled = result.stats.contributions.length;
148
+ // DECAY-01: a capsule is a real use of the rules and memories it carries.
149
+ recordCapsuleAccess(derivedDir, rules, knowledge.contributions);
142
150
  return result;
143
151
  }
144
152
 
153
+ /**
154
+ * Record each rule and memory a capsule carries in the ledger of the brain that owns it, so
155
+ * decay in the global brain sees global rules used by every project.
156
+ */
157
+ export function recordCapsuleAccess(derivedDir, rules = [], contributions = [], globalDerivedDir = path.join(globalBrainDir, 'memory-derived')) {
158
+ const owned = { project: [], global: [] };
159
+ for (const node of rules) (node._layer === 'global' ? owned.global : owned.project).push(node.slug);
160
+ for (const { id } of contributions || []) {
161
+ const match = /^vault:(global|project):(.+)$/.exec(String(id || ''));
162
+ if (match) owned[match[1]].push(match[2]);
163
+ }
164
+ if (derivedDir) recordAccess(derivedDir, owned.project);
165
+ recordAccess(derivedDir && path.resolve(derivedDir) === path.resolve(globalDerivedDir) ? derivedDir : globalDerivedDir, owned.global);
166
+ }
167
+
145
168
  /**
146
169
  * Second line of every text capsule, so output that is truncated or piped through
147
170
  * `head` still carries the rule (the rule once sat at line 544 of the output and
@@ -163,6 +186,8 @@ export function capsuleResponse(result, { total = 4000, debug = false, format =
163
186
  retrieval: summarizeRetrieval(result.stats.retrieval),
164
187
  };
165
188
  const response = { ready: result.ready, context: result.context, stats };
189
+ // This offset refers to the decoded context string, not the escaped JSON bytes.
190
+ if (result.evidenceStart !== undefined) response.evidence_start = result.evidenceStart;
166
191
  stats.budget = total;
167
192
  // Counts affect serialization themselves. Iterate to a stable representation.
168
193
  for (let i = 0; i < 20; i++) {
@@ -100,14 +100,18 @@ export function assembleContext(contributions, { total = 4000 } = {}) {
100
100
  if (estimateTokens(render([...selected, item])) <= total) selected.push(item);
101
101
  else excluded.push(item.id);
102
102
  }
103
- const context = render(selected);
103
+ const isEvidence = item => item.taskEvidence && !item.required;
104
+ const evidence = selected.filter(isEvidence);
105
+ const ordered = [...selected.filter(item => !isEvidence(item)), ...evidence];
106
+ const context = render(ordered);
107
+ const evidenceText = render(evidence);
104
108
  const totalTokens = estimateTokens(context);
105
- return { context, ready: requiredTokens <= total, stats: {
109
+ return { context, ...(evidenceText && { evidenceStart: context.length - evidenceText.length }), ready: requiredTokens <= total, stats: {
106
110
  token_measurement: 'estimated_chars_divided_by_four', total_tokens: totalTokens,
107
111
  budget_used: totalTokens, budget_remaining: Math.max(0, total - totalTokens),
108
112
  required_tokens: requiredTokens, overflow_tokens: Math.max(0, requiredTokens - total),
109
113
  required_ids: required.map(c => c.id), excluded_ids: excluded,
110
- contributions: selected.map(c => ({ id: c.id, required: Boolean(c.required), chars: c.text.length, estimated_tokens: estimateTokens(c.text) })),
114
+ contributions: ordered.map(c => ({ id: c.id, required: Boolean(c.required), chars: c.text.length, estimated_tokens: estimateTokens(c.text) })),
111
115
  version: crypto.createHash('sha256').update(JSON.stringify({ total, contributions })).digest('hex'),
112
116
  } };
113
117
  }
@@ -37,6 +37,28 @@ export function getDaemonStatus() {
37
37
  return status === 'not-started' ? 'not_started' : status === 'stopped' ? 'dead' : status;
38
38
  }
39
39
 
40
+ /**
41
+ * Remove the lock of a daemon that is gone, for a server told to run none.
42
+ *
43
+ * startDaemon is the only other thing that recovers this lock, and such a server never
44
+ * calls it. A daemon that went down with the machine left a lock nothing would ever
45
+ * clear, so the server reported `dead` and `degraded` for good. Only a lock whose process
46
+ * is verified gone is removed: a live process or one that cannot be identified keeps it.
47
+ * @returns {boolean} True if a lock was removed.
48
+ */
49
+ export function clearStaleDaemonLock({ observe = getDaemonObservation } = {}) {
50
+ try {
51
+ const before = fs.readFileSync(PID_FILE, 'utf8');
52
+ if (observe().status !== 'stopped') return false;
53
+ if (fs.readFileSync(PID_FILE, 'utf8') !== before) return false;
54
+ fs.unlinkSync(PID_FILE);
55
+ logger.info('daemon-control', `Removed the lock of daemon PID ${before.trim()}, which is gone. This server runs no daemon.`);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
40
62
  /**
41
63
  * Detaches the daemon script in the background and writes its PID to file.
42
64
  * @returns {number} The PID of the newly spawned daemon.
@@ -4,8 +4,8 @@ import { loadPluginModule as __loadOwnedPlugin } from './plugin-runtime.mjs';
4
4
  *
5
5
  * Durable worker over the open task envelope queue.
6
6
  * - Dispatches via executor registry (fail-loud on unknown)
7
- * - Does NOT invent idle work unless TR_IDLE_TASKS=1
8
- * - Periodically runs dream (system memory consolidation)
7
+ * - Automations owns scheduled maintenance; empty queues stay empty
8
+ * - Executes dream tasks scheduled by the automations plugin
9
9
  * - Ingests sessions on boot; recompiles surfaces every N ticks
10
10
  * - Auto-syncs all .md files from registered repos into Total Recall
11
11
  *
@@ -17,7 +17,7 @@ import { DAEMON_PID_FILE } from './daemon-control.mjs';
17
17
  import fs from 'fs';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { readProcessCommand, entryPathHint, shouldHonorPidLock } from './pid-lock.mjs';
20
- import { createScheduler, updateTaskStatus, persistTaskToDisk } from './scheduler.mjs';
20
+ import { createScheduler, updateTaskStatus } from './scheduler.mjs';
21
21
  import { scanAndIngest } from './session-watcher.mjs';
22
22
  import { syncAllRepos } from './repo-sync.mjs';
23
23
  import { refreshProviderUsage } from './usage-fetcher.mjs';
@@ -26,7 +26,7 @@ import { updateQueueItem } from './research-queue.mjs';
26
26
  import { agentDir, brainDir, detectProjectBrain } from './config.mjs';
27
27
  import { runCrons } from './crons.mjs';
28
28
  import { dispatchTask as registryDispatch } from './task-executors.mjs';
29
- import { buildTaskEnvelope, publishTaskSchedulerProtocol } from './task-envelope.mjs';
29
+ import { publishTaskSchedulerProtocol } from './task-envelope.mjs';
30
30
 
31
31
  const AGENT_DIR = agentDir;
32
32
  const BRAIN_DIR = brainDir;
@@ -61,8 +61,16 @@ const RECOMPILE_EVERY_N_TASKS = 20;
61
61
  const TASK_SLEEP_MS = 10000;
62
62
  const EMPTY_SLEEP_MS = 30000;
63
63
 
64
- // Run a system dream every N empty ticks (memory consolidation without busy-work)
65
- const DREAM_EVERY_N_EMPTY = 20;
64
+ /** An empty queue only sleeps. Plugin services own recurring task creation. */
65
+ export async function nextDaemonTask(scheduler, { localOnly = false, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
66
+ const next = scheduler.next({ localOnly });
67
+ if (!next.task) {
68
+ const sleepMs = localOnly ? TASK_SLEEP_MS : EMPTY_SLEEP_MS;
69
+ logger.info({ subsystem: 'daemon-loop', message: `Queue empty — sleeping ${sleepMs}ms` });
70
+ await sleep(sleepMs);
71
+ }
72
+ return next;
73
+ }
66
74
 
67
75
  function executorContext(runtimeConfig) {
68
76
  return {
@@ -242,6 +250,7 @@ let running = true;
242
250
  let vaultWatcher = null;
243
251
  let shuttingDown = false;
244
252
  let schedulerProtocolActive = false;
253
+ let lastLessonScan = 0;
245
254
 
246
255
  // Upper bound on a graceful stop. A loop iteration can be mid-task, so the
247
256
  // signal cannot exit instantly — but it must exit *eventually*.
@@ -259,7 +268,7 @@ import { releaseLease } from './leader-election.mjs';
259
268
  * daemon.log across three separate SIGTERMs. The fallback timer bounds that, so
260
269
  * even a wedged iteration cannot keep the daemon alive indefinitely.
261
270
  */
262
- let stopPluginTasks = null;
271
+ let stopPluginServices = null;
263
272
  let stopCliMaintenance = null;
264
273
  let stopAutomaticUpdates = null;
265
274
 
@@ -269,7 +278,7 @@ function shutdown(signal) {
269
278
  logger.info({ subsystem: 'daemon-loop', message: `${signal} received — shutting down gracefully` });
270
279
  running = false;
271
280
  if (vaultWatcher) vaultWatcher.stop();
272
- if (stopPluginTasks) stopPluginTasks();
281
+ if (stopPluginServices) stopPluginServices();
273
282
  stopCliMaintenance?.();
274
283
  stopAutomaticUpdates?.();
275
284
 
@@ -451,15 +460,16 @@ async function main() {
451
460
  }
452
461
  }
453
462
 
454
- let emptyTicks = 0;
455
463
 
456
- // Plugin tasks are per node (a plugin installed here runs here), so they are
457
- // scheduled on their own timer rather than inside the leader-only task loop.
464
+ // Per-node services owned by plugins (scheduled plugin tasks belong to
465
+ // automations) start here; core runs whatever active plugins declare.
458
466
  try {
459
- const { startPluginTaskScheduler } = await import('./plugin-tasks.mjs');
460
- stopPluginTasks = startPluginTaskScheduler();
467
+ const { startPluginDaemonServices } = await import('./plugin-runtime.mjs');
468
+ const services = await startPluginDaemonServices();
469
+ stopPluginServices = services.stop;
470
+ logger.info('daemon-loop', `Plugin daemon services started: ${services.started.join(', ') || 'none'}${services.failed.length ? `; failed: ${services.failed.join(', ')}` : ''}`);
461
471
  } catch (err) {
462
- logger.warn('daemon-loop', `Plugin task scheduler failed to start: ${err.message}`);
472
+ logger.warn('daemon-loop', `Plugin daemon services failed to start: ${err.message}`);
463
473
  }
464
474
 
465
475
  while (running) {
@@ -560,14 +570,20 @@ async function main() {
560
570
  }
561
571
  }
562
572
 
563
- if (!await isLeader()) {
564
- await new Promise(r => setTimeout(r, TASK_SLEEP_MS));
565
- taskCount++;
566
- continue;
573
+ const localOnly = !await isLeader();
574
+
575
+ // Local transcripts stay on their node. Followers also queue and run reviews.
576
+ if (Date.now() - lastLessonScan >= 60_000) {
577
+ lastLessonScan = Date.now();
578
+ try {
579
+ scanAndIngest(SESSIONS_DIR);
580
+ const { enqueueSessionReviews } = await import('./lessons.mjs');
581
+ enqueueSessionReviews({ brainDir: BRAIN_DIR });
582
+ } catch (error) { logger.warn('daemon-loop', `Local lesson ingest failed: ${error.message}`); }
567
583
  }
568
584
 
569
585
  // Periodically recompile surfaces (LEADER ONLY)
570
- if (taskCount > 0 && taskCount % RECOMPILE_EVERY_N_TASKS === 0) {
586
+ if (!localOnly && taskCount > 0 && taskCount % RECOMPILE_EVERY_N_TASKS === 0) {
571
587
  logger.info({ subsystem: 'daemon-loop', message: 'Running scheduled surface recompile...' });
572
588
  try {
573
589
  const { compileSurface } = await import('./surface.mjs');
@@ -581,16 +597,6 @@ async function main() {
581
597
  logger.info({ subsystem: 'daemon-loop', message: `Surface recompile error: ${err.message}` });
582
598
  }
583
599
 
584
- try {
585
- const { compactAppendLogs } = await import('./append-log.mjs');
586
- const compactResult = compactAppendLogs();
587
- if (compactResult.logs_compacted > 0) {
588
- logger.info({ subsystem: 'daemon-loop', message: `Compacted ${compactResult.logs_compacted} append logs (${compactResult.total_entries} entries)` });
589
- }
590
- } catch (err) {
591
- logger.info({ subsystem: 'daemon-loop', message: `Append log compaction error: ${err.message}` });
592
- }
593
-
594
600
  try {
595
601
  await runCrons({ vaultDir: VAULT_DIR, skillsDir: SKILLS_DIR, brainDir: BRAIN_DIR });
596
602
  } catch (err) {
@@ -627,42 +633,13 @@ async function main() {
627
633
  queueDir: QUEUE_DIR,
628
634
  vaultDir: VAULT_DIR,
629
635
  sessionsDir: SESSIONS_DIR,
636
+ localOnly,
630
637
  });
631
638
 
632
- let { task, source } = scheduler.next();
633
-
634
- // Empty queue: optional system dream, then sleep (no busy-work invention)
639
+ const { task, source } = await nextDaemonTask(scheduler, { localOnly });
635
640
  if (!task) {
636
- emptyTicks++;
637
- if (emptyTicks % DREAM_EVERY_N_EMPTY === 0) {
638
- const dreamEnv = buildTaskEnvelope({
639
- intent: 'Scheduled system dream cycle',
640
- slug: `dream-system-${Date.now().toString(36)}`,
641
- kind: 'system',
642
- executor: 'dream',
643
- category: 'memory-maintenance',
644
- priority: 70,
645
- system: true,
646
- origin: { agent: 'daemon', created_by: 'daemon-system' },
647
- capabilities: ['vault:read', 'vault:write'],
648
- });
649
- persistTaskToDisk(dreamEnv, QUEUE_DIR);
650
- task = dreamEnv;
651
- source = 'system';
652
- logger.info({
653
- subsystem: 'daemon-loop',
654
- message: `Empty queue → enqueue system dream (${emptyTicks} empty ticks)`,
655
- });
656
- } else {
657
- logger.info({
658
- subsystem: 'daemon-loop',
659
- message: `Queue empty (tick ${emptyTicks}) — sleeping ${EMPTY_SLEEP_MS}ms`,
660
- });
661
- await new Promise((r) => setTimeout(r, EMPTY_SLEEP_MS));
662
- continue;
663
- }
664
- } else {
665
- emptyTicks = 0;
641
+ if (localOnly) taskCount++;
642
+ continue;
666
643
  }
667
644
 
668
645
  taskCount++;
@@ -1,7 +1,7 @@
1
1
  /** Default runtime foundation and routine agent-development tools. */
2
2
  export const DEFAULT_PLUGIN_IDS = Object.freeze([
3
3
  'auth', 'ssss', 'mesh', 'openwiki', 'composable-cli', 'meta-harness',
4
- 'code-quality', 'security', 'embeddings', 'git-sentinel',
4
+ 'code-quality', 'security', 'embeddings', 'git', 'automations',
5
5
  ]);
6
6
 
7
7
  /** Required defaults that own additional installation payloads. */