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
@@ -12,9 +12,14 @@
12
12
  * Exiting an unsupervised server is not a restart, it is an outage, so
13
13
  * supervision is proven (the job's reported PID is ours) rather than assumed.
14
14
  */
15
- import { execFileSync } from 'node:child_process';
15
+ import { execFileSync, spawnSync } from 'node:child_process';
16
16
  import fs from 'node:fs';
17
17
  import path from 'node:path';
18
+ import { processIdentity } from './startup-health.mjs';
19
+ import { runningServerStatus } from './running-server.mjs';
20
+
21
+ const GLOBAL_PACKAGE = 'total-recall-brain';
22
+ const HOLD_LIMIT_MS = 6 * 60 * 60 * 1000;
18
23
 
19
24
  const LAUNCHD_LABELS = ['com.totalrecall.brain', 'com.totalrecall.server'];
20
25
  const SYSTEMD_UNITS = ['total-recall-server', 'total-recall-server.service'];
@@ -47,30 +52,33 @@ function systemdPidFor(unit) {
47
52
 
48
53
  /**
49
54
  * Prove that a supervisor owns THIS process and will restart it.
50
- * @returns {{supervised: boolean, kind: string|null, label: string|null, reason: string}}
55
+ * `scope` says whose supervisor it is: `user` belongs to one login, `system` to the machine.
56
+ * @returns {{supervised: boolean, kind: string|null, label: string|null, scope: string|null, reason: string}}
51
57
  */
52
58
  export function detectSupervisor(pid = process.pid) {
53
59
  // An explicit opt-in for hosts running under a supervisor we cannot query
54
60
  // (docker restart policies, pm2, runit). The operator asserts respawn.
55
61
  if (process.env.TR_SUPERVISED === '1') {
56
- return { supervised: true, kind: 'declared', label: 'TR_SUPERVISED=1', reason: 'declared by operator' };
62
+ return { supervised: true, kind: 'declared', label: 'TR_SUPERVISED=1', scope: null, reason: 'declared by operator' };
57
63
  }
58
64
  if (process.platform === 'darwin') {
59
65
  for (const label of LAUNCHD_LABELS) {
60
66
  const jobPid = launchdPidFor(label);
61
67
  if (jobPid === pid) {
62
- return { supervised: true, kind: 'launchd', label, reason: `launchd job ${label} owns pid ${pid}` };
68
+ // launchctl answers for the caller's domain, so as root this is a system daemon.
69
+ const scope = process.getuid?.() === 0 ? 'system' : 'user';
70
+ return { supervised: true, kind: 'launchd', label, scope, reason: `launchd job ${label} owns pid ${pid}` };
63
71
  }
64
72
  }
65
- return { supervised: false, kind: null, label: null, reason: 'no launchd job reports this pid' };
73
+ return { supervised: false, kind: null, label: null, scope: null, reason: 'no launchd job reports this pid' };
66
74
  }
67
75
  for (const unit of SYSTEMD_UNITS) {
68
76
  const found = systemdPidFor(unit);
69
77
  if (found && found.pid === pid) {
70
- return { supervised: true, kind: 'systemd', label: unit, reason: `systemd ${found.scope} unit ${unit} owns pid ${pid}` };
78
+ return { supervised: true, kind: 'systemd', label: unit, scope: found.scope, reason: `systemd ${found.scope} unit ${unit} owns pid ${pid}` };
71
79
  }
72
80
  }
73
- return { supervised: false, kind: null, label: null, reason: 'no systemd unit reports this pid' };
81
+ return { supervised: false, kind: null, label: null, scope: null, reason: 'no systemd unit reports this pid' };
74
82
  }
75
83
 
76
84
  /**
@@ -128,3 +136,116 @@ export function packageVersionOnDisk(rootDir) {
128
136
  return null;
129
137
  }
130
138
  }
139
+
140
+ /**
141
+ * Can maintenance replace the server on this host, and how?
142
+ *
143
+ * `npm install --global` moves the files under a running server and nothing else
144
+ * moves the process, so a host that serves from that install stays on the release it
145
+ * booted on. Replacing it has a gap, so maintenance does it only where a gap is allowed
146
+ * and a restart is certain to help:
147
+ * - a per-user supervisor owns the process. A LaunchAgent or user unit belongs to one
148
+ * login; a system service is production, and `deploy release` replaces that with no gap.
149
+ * - the process runs the global install's entry, and that install is not a checkout
150
+ * (auto-pull owns a checkout) and is at the release. Otherwise a restart starts the
151
+ * same old code, or code nobody released.
152
+ *
153
+ * @returns {{method: 'restart', kind: string, label: string, pid: number}|{method: 'deploy'|'none', reason: string}}
154
+ */
155
+ export function planServerReplacement({ brainDir, globalRoot, latest, env = process.env, identity = processIdentity, supervisor = detectSupervisor, versionOnDisk = packageVersionOnDisk } = {}) {
156
+ const leave = (reason, method = 'none') => ({ method, reason });
157
+ if (env.TR_SERVER_AUTO_RESTART === '0') return leave('TR_SERVER_AUTO_RESTART=0 leaves this server to the operator.');
158
+ if (!globalRoot) return leave('The global npm root could not be read, so what a restart would start is unknown.');
159
+ const install = path.join(globalRoot, GLOBAL_PACKAGE);
160
+ const observed = identity(path.join(brainDir, 'server.pid'), path.join(install, 'src', 'server', 'index.mjs'));
161
+ if (!observed.pid) return leave('The process serving this brain could not be identified.');
162
+ const owner = supervisor(observed.pid);
163
+ if (owner.supervised && owner.scope === 'system') return leave(`${owner.label} is a system service: replace it with total-recall deploy release.`, 'deploy');
164
+ if (observed.status !== 'running') return leave(`The server does not run from the global install${observed.entry ? ` (it runs ${observed.entry})` : ''}, so a restart would not bring it to the release.`);
165
+ if (fs.existsSync(path.join(install, '.git'))) return leave('The global install is a link to a git checkout, which auto-pull keeps current.');
166
+ const onDisk = versionOnDisk(install);
167
+ if (onDisk !== latest) return leave(`The global install is ${onDisk ?? 'unreadable'} and the release is ${latest}.`);
168
+ if (!owner.supervised || owner.scope !== 'user') return leave(`Nothing maintenance can drive would bring the server back: ${owner.reason}.`);
169
+ return { method: 'restart', kind: owner.kind, label: owner.label, pid: observed.pid };
170
+ }
171
+
172
+ /** Connections open to the server right now, or null when that cannot be read. */
173
+ export function openServerConnections({ pid, port, platform = process.platform, run = spawnSync } = {}) {
174
+ const options = { encoding: 'utf8', timeout: 10000 };
175
+ if (platform === 'linux') {
176
+ const result = run('ss', ['-tnH', 'state', 'established', 'sport', '=', `:${port}`], options);
177
+ if (result.error || result.status !== 0) return null;
178
+ return result.stdout.split('\n').filter((line) => line.trim()).length;
179
+ }
180
+ const result = run('lsof', ['-nP', '-a', '-p', String(pid), '-iTCP', '-sTCP:ESTABLISHED', '-Fn'], options);
181
+ if (result.error) return null;
182
+ // lsof exits 1 when nothing matches, and also on a real failure, which is the one that writes to stderr.
183
+ if (result.status !== 0 && (result.stdout?.trim() || result.stderr?.trim())) return null;
184
+ return (result.stdout || '').split('\n').filter((line) => line.startsWith('n') && line.split('->')[0].endsWith(`:${port}`)).length;
185
+ }
186
+
187
+ /** The command that stops the server and starts it again, or null when the job is not there. */
188
+ function restartCommand(plan, { run, uid }) {
189
+ if (plan.kind === 'systemd') return ['systemctl', ['--user', 'restart', plan.label]];
190
+ // `gui` exists while the user is logged in; a Mac with no login session has only `user`.
191
+ const target = [`gui/${uid}/${plan.label}`, `user/${uid}/${plan.label}`]
192
+ .find((candidate) => run('launchctl', ['print', candidate], { encoding: 'utf8', timeout: 10000 }).status === 0);
193
+ // kickstart -k delivers SIGTERM and waits (measured, launchd does not document it), so the
194
+ // server drains and releases its PID lock, and it needs no KeepAlive to come back.
195
+ return target ? ['launchctl', ['kickstart', '-k', target]] : null;
196
+ }
197
+
198
+ /**
199
+ * Bring a stale server to the release, when `planServerReplacement` allows it.
200
+ *
201
+ * An open connection holds this back so nobody's work is cut off, for as long as
202
+ * `holdLimitMs`: an open dashboard tab must not keep a host on an old release forever.
203
+ * Replaced means /health reports the release, never that the command returned.
204
+ *
205
+ * @returns the server status to report, with `replacement` saying what happened.
206
+ */
207
+ export async function replaceStaleServer({
208
+ brainDir, server, globalRoot, previous, uid = process.getuid?.(), plan = planServerReplacement, connections = openServerConnections,
209
+ run = spawnSync, observe = runningServerStatus, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now,
210
+ idleWaitMs = 30000, healthWaitMs = 120000, pollMs = 2000, holdLimitMs = HOLD_LIMIT_MS,
211
+ } = {}) {
212
+ let replacement;
213
+ try {
214
+ replacement = plan({ brainDir, globalRoot, latest: server.latest });
215
+ if (replacement.method !== 'restart') return { ...server, replacement };
216
+
217
+ const port = new URL(server.url).port;
218
+ const idleBy = now() + idleWaitMs;
219
+ let open = connections({ pid: replacement.pid, port });
220
+ while (open !== 0 && now() < idleBy) {
221
+ await sleep(pollMs);
222
+ open = connections({ pid: replacement.pid, port });
223
+ }
224
+ if (open !== 0) {
225
+ const heldSince = previous?.replacement?.state === 'held' ? previous.replacement.held_since : now();
226
+ if (now() - heldSince < holdLimitMs) {
227
+ const reason = open === null ? 'Open connections could not be read.' : `${open} connection(s) are open on the server.`;
228
+ return { ...server, replacement: { ...replacement, state: 'held', held_since: heldSince, reason } };
229
+ }
230
+ }
231
+
232
+ const command = restartCommand(replacement, { run, uid });
233
+ if (!command) return { ...server, replacement: { ...replacement, state: 'failed', reason: `launchd has no job ${replacement.label} for this user.` } };
234
+ const restarted = run(command[0], command[1], { encoding: 'utf8', timeout: 60000 });
235
+ if (restarted.error || restarted.status !== 0) {
236
+ const reason = `${command[0]} ${command[1].join(' ')} failed: ${restarted.error?.message || restarted.stderr?.trim() || `exit ${restarted.status}`}`;
237
+ return { ...server, replacement: { ...replacement, state: 'failed', reason } };
238
+ }
239
+
240
+ const healthyBy = now() + healthWaitMs;
241
+ let after;
242
+ do {
243
+ await sleep(pollMs);
244
+ after = await observe({ brainDir, latest: server.latest, timeoutMs: 5000 });
245
+ } while (after.status !== 'current' && now() < healthyBy);
246
+ if (after.status === 'current') return { ...after, replacement: { ...replacement, state: 'replaced', from: server.running } };
247
+ return { ...after, replacement: { ...replacement, state: 'failed', reason: `/health did not report ${server.latest} within ${healthWaitMs / 1000} seconds of the restart.` } };
248
+ } catch (error) {
249
+ return { ...server, replacement: { ...replacement, state: 'failed', reason: error.message } };
250
+ }
251
+ }
@@ -0,0 +1,148 @@
1
+ /** Repair this installation's macOS server LaunchAgent. Never manages systemd. */
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { processIdentity } from './startup-health.mjs';
8
+
9
+ const PACKAGE_ROOT = fileURLToPath(new URL('../..', import.meta.url));
10
+ const LABEL = 'com.totalrecall.server';
11
+ const corePathEnv = new Set(['AGENT_DIR', 'TR_SECRETS_BRAIN', 'TR_SERVER_PID_FILE']);
12
+ const inside = (root, value) => value === root || value.startsWith(root + path.sep);
13
+ const real = value => { try { return fs.realpathSync(value); } catch { return path.resolve(value); } };
14
+
15
+ export function stableNodePath(env = process.env) {
16
+ for (const dir of (env.PATH || '').split(path.delimiter)) {
17
+ if (!dir || !path.isAbsolute(dir)) continue;
18
+ const candidate = path.join(dir, 'node');
19
+ if (candidate.split(path.sep).includes('Cellar')) continue;
20
+ try { fs.accessSync(candidate, fs.constants.X_OK); if (fs.statSync(candidate).isFile()) return candidate; } catch {}
21
+ }
22
+ throw new Error('No stable node executable on PATH. Add the node symlink directory to PATH.');
23
+ }
24
+
25
+ function convert(input, format, exec) {
26
+ const result = exec('plutil', ['-convert', format, '-o', '-', '-'], { input, encoding: 'utf8', timeout: 5000 });
27
+ if (result.status !== 0 || result.error) throw new Error(`Cannot convert server plist to ${format}.`);
28
+ return result.stdout;
29
+ }
30
+
31
+ /** Detect foreign installations without naming any operator repository. */
32
+ function checkoutFor(value, oldRoot, packageRoot) {
33
+ if (inside(packageRoot, value) || inside(real(packageRoot), value)) return null;
34
+ if (oldRoot && real(oldRoot) !== real(packageRoot) && inside(oldRoot, value)) return oldRoot;
35
+ for (let dir = value; ; dir = path.dirname(dir)) {
36
+ try {
37
+ if (JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name === 'total-recall-brain'
38
+ && real(dir) !== real(packageRoot)) return dir;
39
+ } catch {}
40
+ if (dir === path.dirname(dir)) return null;
41
+ }
42
+ }
43
+
44
+ export function serverEnvironment(existing, { packageRoot, oldRoot }) {
45
+ const env = {};
46
+ for (const [key, value] of Object.entries(existing || {})) {
47
+ if (typeof value !== 'string') throw new Error(`Server environment ${key} must be a string.`);
48
+ if (key === 'PATH') {
49
+ env[key] = value.split(path.delimiter).map(part => {
50
+ const foreign = path.isAbsolute(part) && checkoutFor(part, oldRoot, packageRoot);
51
+ return foreign ? path.join(packageRoot, path.relative(foreign, part)) : part;
52
+ }).join(path.delimiter);
53
+ continue;
54
+ }
55
+ const foreign = path.isAbsolute(value) && checkoutFor(value, oldRoot, packageRoot);
56
+ if (!foreign) env[key] = value;
57
+ else if (corePathEnv.has(key)) env[key] = path.join(packageRoot, path.relative(foreign, value));
58
+ // Obsolete settings must not retain a path into another installation.
59
+ }
60
+ return env;
61
+ }
62
+
63
+ function publicPlist(plist) {
64
+ const copy = structuredClone(plist);
65
+ for (const key of Object.keys(copy.EnvironmentVariables || {})) {
66
+ if (/password|secret(?!s_brain)|token|api.?key|credential/i.test(key)) copy.EnvironmentVariables[key] = '[redacted]';
67
+ }
68
+ return copy;
69
+ }
70
+
71
+ function plistDiff(before, after) {
72
+ const lines = [];
73
+ const walk = (a, b, key) => {
74
+ if (JSON.stringify(a) === JSON.stringify(b)) return;
75
+ if (a && b && !Array.isArray(a) && !Array.isArray(b) && typeof a === 'object' && typeof b === 'object') {
76
+ for (const part of new Set([...Object.keys(a), ...Object.keys(b)])) walk(a[part], b[part], key ? `${key}.${part}` : part);
77
+ } else {
78
+ if (a !== undefined) lines.push(`- ${key}: ${JSON.stringify(a)}`);
79
+ if (b !== undefined) lines.push(`+ ${key}: ${JSON.stringify(b)}`);
80
+ }
81
+ };
82
+ walk(publicPlist(before), publicPlist(after), '');
83
+ return lines.join('\n') || 'No plist changes.';
84
+ }
85
+
86
+ export async function ensureServerService({ dryRun = false, platform = process.platform, home = os.homedir(),
87
+ packageRoot = PACKAGE_ROOT, env = process.env, exec = spawnSync, identity = processIdentity,
88
+ uid = process.getuid?.(), sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), attempts = 40 } = {}) {
89
+ if (platform !== 'darwin') return { status: 'unchanged', reason: 'server-release owns the server on systemd hosts; no changes made.' };
90
+ packageRoot = path.resolve(packageRoot);
91
+ const plistPath = path.join(home, 'Library/LaunchAgents', `${LABEL}.plist`);
92
+ const oldBytes = fs.existsSync(plistPath) ? fs.readFileSync(plistPath) : null;
93
+ const existing = oldBytes ? JSON.parse(convert(oldBytes, 'json', exec)) : {};
94
+ const template = fs.readFileSync(path.join(packageRoot, 'templates', `${LABEL}.plist`), 'utf8');
95
+ const desired = JSON.parse(convert(template, 'json', exec));
96
+ const substitute = value => typeof value === 'string' ? value.replaceAll('__ROOT__', packageRoot).replaceAll('__HOME__', home)
97
+ : Array.isArray(value) ? value.map(substitute) : value && typeof value === 'object'
98
+ ? Object.fromEntries(Object.entries(value).map(([key, val]) => [key, substitute(val)])) : value;
99
+ const plist = substitute(desired);
100
+ plist.WorkingDirectory = packageRoot;
101
+ plist.ProgramArguments[0] = stableNodePath(env);
102
+ plist.EnvironmentVariables = { ...plist.EnvironmentVariables,
103
+ ...serverEnvironment(existing.EnvironmentVariables, { packageRoot, oldRoot: existing.WorkingDirectory }) };
104
+ const diff = plistDiff(existing, plist);
105
+ if (dryRun) return { status: 'dry_run', plistPath, diff };
106
+
107
+ const xml = convert(JSON.stringify(plist), 'xml1', exec);
108
+ const domain = `gui/${uid}`;
109
+ const service = `${domain}/${LABEL}`;
110
+ const run = args => exec('launchctl', args, { encoding: 'utf8', timeout: 10000 });
111
+ const ok = result => result.status === 0 && !result.error;
112
+ const must = args => { if (!ok(run(args))) throw new Error(`launchctl ${args[0]} failed for ${service}.`); };
113
+ const agentDir = plist.EnvironmentVariables.AGENT_DIR || path.join(home, '.agent');
114
+ const pidFile = plist.EnvironmentVariables.TR_SERVER_PID_FILE || path.join(agentDir, 'skills/total-recall/server.pid');
115
+ const entry = path.join(packageRoot, 'src/server/index.mjs');
116
+ const previous = identity(pidFile, entry);
117
+ if (previous.status === 'unknown') throw new Error('Server process identity is unknown. Nothing changed.');
118
+ const loaded = ok(run(['print', service]));
119
+ if (!loaded && ['running', 'conflict'].includes(previous.status)) throw new Error('A server runs outside this LaunchAgent. Nothing changed.');
120
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
121
+ for (const log of [plist.StandardOutPath, plist.StandardErrorPath]) if (log) fs.mkdirSync(path.dirname(log), { recursive: true });
122
+ const pending = `${plistPath}.${process.pid}.tmp`;
123
+ try {
124
+ fs.writeFileSync(pending, xml, { mode: 0o600 });
125
+ fs.renameSync(pending, plistPath);
126
+ if (loaded) {
127
+ must(['bootout', service]);
128
+ let retired = false;
129
+ for (let i = 0; i < attempts; i++) {
130
+ if (!ok(run(['print', service]))) { retired = true; break; }
131
+ await sleep(250);
132
+ }
133
+ if (!retired) throw new Error('LaunchAgent did not retire.');
134
+ }
135
+ must(['bootstrap', domain, plistPath]);
136
+ for (let i = 0; i < attempts; i++) {
137
+ const observed = identity(pidFile, entry);
138
+ if (observed.status === 'running' && (!loaded || observed.pid !== previous.pid)) return { status: 'ready', plistPath, pid: observed.pid, entry: observed.entry };
139
+ await sleep(250);
140
+ }
141
+ throw new Error('Server process verification failed after launchd reload.');
142
+ } catch (error) {
143
+ // Keep the previous definition available for operator recovery. Never report success.
144
+ if (oldBytes !== null) fs.writeFileSync(plistPath, oldBytes, { mode: 0o600 });
145
+ else fs.rmSync(plistPath, { force: true });
146
+ throw error;
147
+ } finally { fs.rmSync(pending, { force: true }); }
148
+ }
@@ -4,6 +4,7 @@ import os from 'os';
4
4
  import crypto from 'crypto';
5
5
  import { atomicWrite } from './vault.mjs';
6
6
  import { logger } from './logger.mjs';
7
+ import { readLessonEntries } from './lesson-sessions.mjs';
7
8
 
8
9
  /**
9
10
  * Total Recall Session Watcher
@@ -30,6 +31,12 @@ const HOME = os.homedir();
30
31
  * of unified SSSS session entries.
31
32
  */
32
33
  const SOURCES = [
34
+ {
35
+ name: 'openclaw',
36
+ root: path.join(HOME, '.openclaw', 'agents'),
37
+ filter: filename => filename.endsWith('.jsonl'),
38
+ adapter: file => parseClaudeCode(file).map(entry => ({ ...entry, source: 'openclaw' })),
39
+ },
33
40
  {
34
41
  name: 'claude-code',
35
42
  root: path.join(HOME, '.claude', 'projects'),
@@ -248,6 +255,15 @@ export function parseClaudeCode(filePath) {
248
255
  */
249
256
  export function parseCodex(filePath) {
250
257
  const raw = fs.readFileSync(filePath, 'utf8');
258
+ if (/"type"\s*:\s*"(?:response_item|event_msg)"/.test(raw)) {
259
+ let parentId = null;
260
+ return readLessonEntries(filePath).map(message => {
261
+ const entry = createSessionEntry({ ...message, parentId, source: 'codex',
262
+ type: message.role === 'user' ? 'task' : message.role === 'tool' ? 'tool_call' : 'observation' });
263
+ parentId = entry.id;
264
+ return entry;
265
+ });
266
+ }
251
267
  const lines = raw.split('\n').filter(Boolean);
252
268
  const entries = [];
253
269
  let prevId = null;
@@ -661,7 +677,9 @@ export function writeSession(entries, sessionsDir, sourceFile) {
661
677
  // Skip if already ingested and unchanged
662
678
  if (fs.existsSync(sessionFile)) {
663
679
  try {
664
- if (fs.readFileSync(sessionFile, 'utf8') === jsonl) {
680
+ const previous = fs.readFileSync(sessionFile, 'utf8');
681
+ const semantic = rows => JSON.stringify(rows.map(({ role, type, content, source, cwd }) => ({ role, type, content, source, cwd })));
682
+ if (previous === jsonl || semantic(previous.trim().split('\n').filter(Boolean).map(line => JSON.parse(line))) === semantic(entries)) {
665
683
  return null;
666
684
  }
667
685
  } catch { /* ignore */ }
@@ -0,0 +1,16 @@
1
+ import { discoverPlugins } from './plugin-loader.mjs';
2
+ import { runPluginCommand } from './plugin-runner.mjs';
3
+
4
+ /** Skill engineering belongs to skill-manager. Missing operations fail closed. */
5
+ export async function requestSkillGap(topic, evidence, { projectRoot = process.cwd(), discover = discoverPlugins, run = runPluginCommand } = {}) {
6
+ if (typeof topic !== 'string' || !topic.trim() || typeof evidence !== 'string' || !evidence.trim()) {
7
+ throw new Error('Skill gap requires a topic and an evidence reference');
8
+ }
9
+ const plugin = discover(projectRoot).find(item => item.id === 'skill-manager' && item.valid);
10
+ if (!plugin?.manifest.cli?.subcommands?.some(item => item.name === 'gap')) {
11
+ throw new Error('Skill-manager gap is unavailable. Install an owner version that provides: total-recall skill-manager gap "<topic>" --evidence <ref>');
12
+ }
13
+ const result = await run(plugin, { subcommand: 'gap', args: [topic.trim(), '--evidence', evidence], cwd: projectRoot });
14
+ if (!result.ok || result.truncated) throw new Error(`Skill-manager gap failed: ${result.stderr || result.output || result.exitCode}`);
15
+ return { success: true, output: result.stdout || result.output, executor: 'skill-manager' };
16
+ }
@@ -12,7 +12,8 @@ import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import crypto from 'node:crypto';
14
14
  import matter from './frontmatter.mjs';
15
- import { createEngine } from '@ssss/cli/engine';
15
+ import { AsyncLocalStorage } from 'node:async_hooks';
16
+ import { createEngine, resolveContainedPath } from '@ssss/cli/engine';
16
17
  import { createValidator } from '@ssss/cli/validator';
17
18
  import {
18
19
  composeRegistryLayers,
@@ -20,6 +21,7 @@ import {
20
21
  resolvePrimitiveDefinition,
21
22
  } from '@ssss/cli/registry';
22
23
  import { detectDirectWrites } from '@ssss/cli/guard';
24
+ import { checkRuleLength } from './rule-length.mjs';
23
25
  import { validateMemoryNode } from './total-recall-memory-validator.mjs';
24
26
  import {
25
27
  TOTAL_RECALL_HOST_EXTENSION,
@@ -66,6 +68,7 @@ const DIRECT_WRITE_APPROVED = [
66
68
 
67
69
  let cachedEngine = null;
68
70
  let cachedRegistryKey = '';
71
+ const writePolicy = new AsyncLocalStorage();
69
72
 
70
73
  export function getKernelMode() {
71
74
  // Default to package-kernel cutover for core types (memory/workflow/low-risk).
@@ -187,7 +190,27 @@ export function getTotalRecallEngine(options = {}) {
187
190
  const registrySet = createTotalRecallRegistrySet(options);
188
191
  const key = `${[...registrySet.types.keys()].sort().join(',')}|${options.leaseStore || ''}`;
189
192
  if (!cachedEngine || cachedRegistryKey !== key || options.forceNew) {
190
- const validator = createValidator({ registrySet });
193
+ const baseValidator = createValidator({ registrySet });
194
+ const validator = {
195
+ ...baseValidator,
196
+ validateData(data, callOptions) {
197
+ const result = baseValidator.validateData(data, callOptions);
198
+ const context = writePolicy.getStore();
199
+ if (context?.envelope.type !== 'patch') return result;
200
+ // The kernel calls validateData with the merged frontmatter. Check the
201
+ // replacement body against that category and those exception tags.
202
+ // A metadata-only patch keeps the current document body.
203
+ const { envelope, vaultRoot, options } = context;
204
+ const body = envelope.patches.__body__ === undefined
205
+ ? matter(fs.readFileSync(resolveContainedPath(vaultRoot, envelope.path), 'utf8')).content
206
+ : String(envelope.patches.__body__);
207
+ const policy = checkRuleLength(data, body, options);
208
+ context.warnings.push(...policy.warnings);
209
+ return { ...result, valid: result.valid && !policy.errors.length,
210
+ errors: [...result.errors, ...policy.errors],
211
+ field_errors: [...result.field_errors, ...policy.errors.map(issue => ({ field: '__body__', issue }))] };
212
+ },
213
+ };
191
214
  cachedEngine = createEngine({
192
215
  registrySet,
193
216
  validator,
@@ -235,9 +258,8 @@ export function isProtocolPath(vfsPath = '') {
235
258
  }
236
259
 
237
260
  function stringifyDocument(data, body) {
238
- // Prefer gray-matter when available; fall back to simple frontmatter dump.
239
261
  try {
240
- return matter.stringify(body || '', data);
262
+ return matter.stringify({ content: body || '', data });
241
263
  } catch {
242
264
  const yaml = Object.entries(data)
243
265
  .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
@@ -278,6 +300,9 @@ export function prepareEnvelopeForKernel(envelope, options = {}) {
278
300
  return { envelope: env, warnings, errors: [`Content parsing error: ${error.message}`] };
279
301
  }
280
302
  const data = { ...parsed.data };
303
+ const length = checkRuleLength(data, parsed.content, options);
304
+ errors.push(...length.errors);
305
+ warnings.push(...length.warnings);
281
306
 
282
307
  // Package 0.9 universal frontmatter.
283
308
  if (!data.title && data.name) data.title = data.name;
@@ -393,11 +418,13 @@ export async function processViaPackageKernel(envelope, vaultRoot, options = {})
393
418
 
394
419
  const eventsDir = path.join(vaultRoot, '.events');
395
420
  if (!env.dry_run && !fs.existsSync(eventsDir)) fs.mkdirSync(eventsDir, { recursive: true });
396
- const response = await engine.processOperation(env, vaultRoot, {
421
+ const context = { envelope: env, vaultRoot, options, warnings: [] };
422
+ const response = await writePolicy.run(context, () => engine.processOperation(env, vaultRoot, {
397
423
  ...options,
398
424
  vaultRoot,
399
425
  principal: options.principal || mapTrPrincipal(env, options, vaultRoot),
400
- });
426
+ }));
427
+ prepared.warnings.push(...context.warnings);
401
428
 
402
429
  if (prepared.warnings.length && response?.validation) {
403
430
  response.validation.warnings = [
@@ -148,7 +148,12 @@ export async function startupHealth({ cwd = process.cwd(), brainDir, brainUrl =
148
148
  remoteDaemon = { status: body.daemon === 'running' ? 'running' : ['dead', 'stopped', 'not-started'].includes(body.daemon) ? 'stopped' : 'unknown', source: 'remote-health' };
149
149
  if (local) {
150
150
  const own = identity(serverFile, path.join(packageRoot, 'src', 'server', 'index.mjs'));
151
- if (own.status !== 'running') return { status: own.status === 'conflict' ? 'conflict' : 'unknown', reason: 'server-process-unverified' };
151
+ if (own.status !== 'running') {
152
+ const actualRoot = own.entry?.endsWith(path.join('src', 'server', 'index.mjs'))
153
+ ? path.resolve(path.dirname(own.entry), '../..') : null;
154
+ return { status: own.status === 'conflict' ? 'conflict' : 'unknown',
155
+ reason: actualRoot ? `server runs from ${actualRoot}; expected ${packageRoot}; run total-recall server ensure` : 'server-process-unverified' };
156
+ }
152
157
  if (!await listener(own.pid, new URL(brainUrl), { run, cwd })) return { status: 'unknown', reason: 'server-listener-unverified' };
153
158
  }
154
159
  const drift = local && verdict.status === 'ready' ? serverCodeDrift(verdict.version, onDiskVersion()) : null;