total-recall-brain 3.45.0 → 3.46.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 (362) hide show
  1. package/bin/total-recall.mjs +4 -2
  2. package/frontend/dist/assets/index-BRYE1jmv.css +1 -0
  3. package/frontend/dist/assets/index-D1goIPjC.js +5598 -0
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +5 -4
  6. package/frontend/src/App.tsx +183 -236
  7. package/frontend/src/api/auth-inventory.ts +62 -0
  8. package/frontend/src/api/memory.ts +13 -8
  9. package/frontend/src/api/plugin-credentials.ts +23 -0
  10. package/frontend/src/api/plugins.ts +37 -53
  11. package/frontend/src/api/workspace-drawers.ts +97 -0
  12. package/frontend/src/components/BrainSelector.tsx +9 -3
  13. package/frontend/src/components/plugins/PluginPanel.tsx +83 -42
  14. package/frontend/src/components/plugins/PluginSettings.tsx +10 -20
  15. package/frontend/src/components/plugins/PluginWorkspace.tsx +367 -0
  16. package/frontend/src/components/plugins/ScopedPluginCommands.tsx +142 -0
  17. package/frontend/src/components/plugins/plugin-frame.ts +44 -33
  18. package/frontend/src/components/plugins/scoped-plugin-operations.ts +28 -0
  19. package/frontend/src/components/workspace/DrawerChat.tsx +110 -0
  20. package/frontend/src/components/workspace/DrawerLayout.tsx +196 -0
  21. package/frontend/src/components/workspace/DrawerMarkup.tsx +49 -0
  22. package/frontend/src/components/workspace/PluginChunkLibrary.tsx +92 -0
  23. package/frontend/src/components/workspace/PluginHome.tsx +76 -0
  24. package/frontend/src/components/workspace/WorkspaceDrawers.tsx +96 -0
  25. package/frontend/src/components/workspace/WorkspacePage.tsx +95 -0
  26. package/frontend/src/components/workspace/usePluginDesign.ts +38 -0
  27. package/frontend/src/index.css +409 -136
  28. package/frontend/src/pages/HelpPage.tsx +15 -46
  29. package/frontend/src/pages/MemoryPage.tsx +19 -36
  30. package/frontend/src/pages/SettingsPage.tsx +1 -1
  31. package/metadata.plugin.schema.json +53 -227
  32. package/package.json +1 -1
  33. package/scaffold/.agent/skills/plugin/SKILL.md +13 -11
  34. package/scaffold/.agent/skills/plugin/evals/README.md +14 -21
  35. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +15 -18
  36. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +19 -24
  37. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +12 -20
  38. package/scaffold/.agent/skills/plugin/evals/cases/04-home-collects-credential.mjs +35 -0
  39. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +8 -28
  40. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +85 -47
  41. package/scaffold/.agent/skills/plugin/evals/run.mjs +17 -18
  42. package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +4 -4
  43. package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +2 -2
  44. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +4 -4
  45. package/scaffold/.agent/skills/plugin/references/anti-patterns.md +2 -2
  46. package/scaffold/.agent/skills/plugin/references/generative-home.md +38 -0
  47. package/scaffold/.agent/skills/plugin/references/scaffold.md +5 -5
  48. package/scaffold/.agent/skills/plugin/references/surfaces.md +13 -4
  49. package/scaffold/.agent/skills/plugin/scripts/record.mjs +75 -153
  50. package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +4 -4
  51. package/scaffold/.agent/skills/total-recall/plugins/auth/DESIGN.md +76 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +3 -1
  53. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +20 -15
  54. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
  55. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +2 -1
  56. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +13 -51
  57. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/access-overview.md +17 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/agent-login.md +20 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/baseline-audit.md +22 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-repository-binding.md +17 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-store-maintenance.md +17 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-password.md +18 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-sessions.md +18 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/endpoint-probe.md +23 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/google-sign-in.md +21 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/mesh-machines.md +16 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/passkey-enrollment.md +21 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/registered-brains.md +16 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/scoped-access-keys.md +17 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/secret-inventory.md +25 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/signing-key-rotation.md +22 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +375 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/automations/{ui/DESIGN.md → DESIGN.md} +32 -21
  74. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +17 -1
  75. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +10 -1
  76. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +6 -6
  77. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +4 -3
  78. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +11 -27
  79. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -2
  80. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/control-ledger.md +42 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/local-inventory-triage.md +17 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/pending-control.md +39 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/run-evidence.md +41 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/schedule-vs-run-evidence.md +17 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/source-coverage.md +41 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/{card.js → home.js} +32 -27
  87. package/scaffold/.agent/skills/total-recall/plugins/code-quality/DESIGN.md +53 -0
  88. package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +23 -0
  89. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +4 -2
  90. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +4 -2
  91. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +6 -6
  92. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +2 -1
  93. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +40 -40
  94. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/config.md +15 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/detect.md +15 -0
  96. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/gate.md +15 -0
  97. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/init.md +15 -0
  98. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/report.md +17 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/run.md +17 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/snapshot.md +15 -0
  101. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +55 -23
  102. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/home.mjs +214 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/DESIGN.md +146 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +6 -6
  105. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +2 -1
  106. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +51 -34
  107. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +1 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/create.md +35 -0
  109. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/history.md +35 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/library.md +49 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/list.md +42 -0
  112. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/promote.md +40 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/read.md +25 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/remove.md +26 -0
  115. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/rollback.md +26 -0
  116. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/show.md +53 -0
  117. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/status.md +40 -0
  118. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test-path.md +38 -0
  119. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test.md +46 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/update.md +32 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/home.js +533 -0
  122. package/scaffold/.agent/skills/total-recall/plugins/embeddings/DESIGN.md +110 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +6 -6
  124. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +2 -1
  125. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +13 -10
  126. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/benchmark.md +49 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/coverage.md +53 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/models.md +43 -0
  129. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/pull.md +41 -0
  130. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/status.md +59 -0
  131. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/home.js +353 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/git/DESIGN.md +31 -0
  133. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +25 -1
  134. package/scaffold/.agent/skills/total-recall/plugins/git/cli.mjs +5 -1
  135. package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
  136. package/scaffold/.agent/skills/total-recall/plugins/git/package.json +2 -1
  137. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +17 -27
  138. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -6
  139. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +24 -6
  140. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +33 -7
  141. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +29 -5
  142. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/unmerged.mjs +182 -0
  143. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/change-comparison.md +27 -0
  144. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/commit-identity.md +22 -0
  145. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/create-worktree.md +25 -0
  146. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/registered-worktrees.md +22 -0
  147. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/remove-worktree.md +23 -0
  148. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/repository-ledger.md +25 -0
  149. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/unmerged-work.md +22 -0
  150. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/upstream-divergence.md +17 -0
  151. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-conflict-triage.md +17 -0
  152. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-terminal.md +22 -0
  153. package/scaffold/.agent/skills/total-recall/plugins/git/ui/home.js +273 -0
  154. package/scaffold/.agent/skills/total-recall/plugins/mesh/DESIGN.md +35 -0
  155. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +4 -5
  156. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -3
  157. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +6 -6
  158. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +2 -1
  159. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +37 -39
  160. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +5 -3
  161. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/active-device-dispatch.md +22 -0
  162. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-probe.md +25 -0
  163. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-server.md +22 -0
  164. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/credential-readiness.md +23 -0
  165. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/elected-leader.md +22 -0
  166. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/enrollment-key.md +22 -0
  167. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/file-receipt.md +25 -0
  168. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-execution.md +23 -0
  169. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-presence.md +23 -0
  170. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/interactive-ssh.md +22 -0
  171. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/local-enrollment.md +23 -0
  172. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/mesh-policy.md +23 -0
  173. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/network-atlas.md +25 -0
  174. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-roster.md +26 -0
  175. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-selection-proof.md +17 -0
  176. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/owner-observations.md +23 -0
  177. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/peer-path.md +23 -0
  178. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/pinned-leader.md +24 -0
  179. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/reachability-evidence-ladder.md +17 -0
  180. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/recorded-login.md +23 -0
  181. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/remote-execution.md +22 -0
  182. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/secret-grants.md +26 -0
  183. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/{card.js → home.js} +93 -22
  184. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/DESIGN.md +86 -0
  185. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +19 -0
  186. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +5 -1
  187. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +6 -6
  188. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -1
  189. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +43 -22
  190. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/chat.mjs +80 -0
  191. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/adapter-docs-freshness.md +16 -0
  192. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/chat-handoff.md +26 -0
  193. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/fleet-maintenance.md +21 -0
  194. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/harness-inventory.md +21 -0
  195. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-access.md +18 -0
  196. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-usage-verification.md +15 -0
  197. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/independent-review.md +17 -0
  198. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/managed-execution.md +20 -0
  199. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/official-documentation.md +18 -0
  200. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/quota-recovery.md +21 -0
  201. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/review-routing-decision.md +16 -0
  202. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/route-work.md +18 -0
  203. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/routing-evidence.md +18 -0
  204. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/run-ledger.md +18 -0
  205. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/home.js +198 -0
  206. package/scaffold/.agent/skills/total-recall/plugins/openwiki/DESIGN.md +75 -0
  207. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +2 -0
  208. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +17 -14
  209. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  210. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +2 -1
  211. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +10 -44
  212. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +5 -4
  213. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +3 -4
  214. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +23 -18
  215. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +8 -3
  216. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +6 -3
  217. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +3 -5
  218. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +21 -47
  219. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +17 -6
  220. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/auto-refresh.md +18 -0
  221. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/bootstrap-pages.md +18 -0
  222. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generate-wiki.md +21 -0
  223. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generator-settings.md +18 -0
  224. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/host-probe.md +18 -0
  225. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/ingest-preview.md +18 -0
  226. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-hygiene.md +21 -0
  227. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-topology.md +19 -0
  228. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/passage-search.md +19 -0
  229. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/propagation-plan.md +18 -0
  230. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/provider-credentials.md +18 -0
  231. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/refresh-report.md +18 -0
  232. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/task-evidence.md +22 -0
  233. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/upstream-record.md +18 -0
  234. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/watch-upstream.md +18 -0
  235. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/wiki-atlas.md +19 -0
  236. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +261 -513
  237. package/scaffold/.agent/skills/total-recall/plugins/security/DESIGN.md +84 -0
  238. package/scaffold/.agent/skills/total-recall/plugins/security/README.md +4 -2
  239. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -19
  240. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +6 -6
  241. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +2 -3
  242. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +19 -52
  243. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/device-evidence.md +21 -0
  244. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/evidence-desk.md +18 -0
  245. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/guided-device-review.md +17 -0
  246. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/host-and-network.md +21 -0
  247. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/https-headers.md +17 -0
  248. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/mesh-coverage.md +18 -0
  249. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/private-security-tools.md +16 -0
  250. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/single-origin-header-check.md +17 -0
  251. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/source-review.md +21 -0
  252. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/ssh-permission-plan.md +17 -0
  253. package/scaffold/.agent/skills/total-recall/plugins/security/ui/home.js +283 -0
  254. package/scaffold/.agent/skills/total-recall/plugins/ssss/DESIGN.md +93 -0
  255. package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +2 -2
  256. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -1
  257. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +5 -3
  258. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/conformance.md +26 -0
  259. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/registry.md +37 -0
  260. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/repair.md +42 -0
  261. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/scaffold.md +33 -0
  262. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/status.md +43 -0
  263. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validate.md +34 -0
  264. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validation-diagnostics.md +17 -0
  265. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/workspace-registry-preflight.md +17 -0
  266. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/home.js +348 -0
  267. package/scaffold/.agent/skills/total-recall/references/operations.md +2 -0
  268. package/src/cli/api.mjs +3 -3
  269. package/src/cli/brief.mjs +3 -0
  270. package/src/cli/config.mjs +16 -0
  271. package/src/cli/connect.mjs +2 -7
  272. package/src/cli/plugin/chunks.mjs +145 -0
  273. package/src/cli/plugin/create.mjs +24 -52
  274. package/src/cli/plugin/index.mjs +20 -9
  275. package/src/cli/setup.mjs +2 -9
  276. package/src/cli/start.mjs +1 -0
  277. package/src/cli/startup.mjs +18 -0
  278. package/src/core/app-deploy/design-tokens.mjs +14 -9
  279. package/src/core/app-deploy/ui-elements.mjs +1 -1
  280. package/src/core/brain-registry.mjs +5 -0
  281. package/src/core/brain-updates.mjs +5 -1
  282. package/src/core/daemon-control.mjs +5 -1
  283. package/src/core/dashboard-bundle.mjs +261 -0
  284. package/src/core/ensure-dependencies.mjs +19 -15
  285. package/src/core/memory-brain.mjs +30 -8
  286. package/src/core/plugin-activation.mjs +63 -20
  287. package/src/core/plugin-contracts/{card.mjs → manifest.mjs} +23 -57
  288. package/src/core/plugin-contracts/startup-check.mjs +49 -0
  289. package/src/core/plugin-contracts/ui.mjs +9 -3
  290. package/src/core/plugin-distribution.mjs +6 -6
  291. package/src/core/plugin-lifecycle.mjs +58 -0
  292. package/src/core/plugin-loader.mjs +9 -5
  293. package/src/core/plugin-runner.mjs +21 -10
  294. package/src/core/plugin-startup-checks.mjs +44 -0
  295. package/src/core/plugin-state.mjs +19 -5
  296. package/src/core/plugin-store.mjs +30 -11
  297. package/src/core/project-id.mjs +15 -5
  298. package/src/core/required-plugins.mjs +31 -6
  299. package/src/core/schema.mjs +2 -0
  300. package/src/core/ssss-operation-service.mjs +1 -0
  301. package/src/core/startup-health.mjs +28 -3
  302. package/src/core/testing/plugin-fixture.mjs +22 -21
  303. package/src/core/throttled-fetch.mjs +25 -18
  304. package/src/core/ui-chunk-choice.mjs +77 -0
  305. package/src/core/ui-chunk.registry.json +23 -0
  306. package/src/core/ui-chunks.mjs +434 -0
  307. package/src/core/vault-cache.mjs +58 -59
  308. package/src/core/workspace-compositions.mjs +151 -0
  309. package/src/core/workspace-drawers.mjs +79 -0
  310. package/src/server/api.mjs +4 -27
  311. package/src/server/index.mjs +28 -21
  312. package/src/server/memory-events.mjs +31 -0
  313. package/src/server/plugin-brain-targets.mjs +89 -0
  314. package/src/server/rest.mjs +2 -0
  315. package/src/server/route-inventory.mjs +3 -2
  316. package/src/server/route-manifest.json +52 -4
  317. package/src/server/routes/_shared.mjs +7 -14
  318. package/src/server/routes/brains.mjs +29 -44
  319. package/src/server/routes/dashboard.mjs +26 -0
  320. package/src/server/routes/plugins.mjs +154 -45
  321. package/src/server/routes/ui-chunks.mjs +192 -0
  322. package/frontend/dist/assets/index-DACh1dhx.js +0 -5613
  323. package/frontend/dist/assets/index-DtwsDyFM.css +0 -1
  324. package/frontend/src/components/plugins/card/CardEngine.tsx +0 -108
  325. package/frontend/src/components/plugins/card/CardScenes.tsx +0 -103
  326. package/frontend/src/components/plugins/card/ExecutionPicker.tsx +0 -64
  327. package/frontend/src/components/plugins/card/args.ts +0 -28
  328. package/frontend/src/components/plugins/card/card.css +0 -32
  329. package/frontend/src/components/plugins/card/cli-line.ts +0 -13
  330. package/frontend/src/components/plugins/card/contract.ts +0 -91
  331. package/frontend/src/components/plugins/card/format.ts +0 -49
  332. package/frontend/src/components/plugins/card/runner-context.ts +0 -11
  333. package/frontend/src/components/plugins/card/runner.tsx +0 -43
  334. package/frontend/src/components/plugins/card/theme.ts +0 -35
  335. package/frontend/src/components/plugins/card/use-source.ts +0 -22
  336. package/frontend/src/components/plugins/card/widgets.tsx +0 -94
  337. package/frontend/src/components/plugins/workspace/WorkspaceHost.tsx +0 -23
  338. package/frontend/src/pages/PluginsPage.tsx +0 -1719
  339. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +0 -47
  340. package/scaffold/.agent/skills/plugin/prompts/06-card.md +0 -46
  341. package/scaffold/.agent/skills/plugin/references/card.md +0 -90
  342. package/scaffold/.agent/skills/plugin/subagents/card-designer.md +0 -60
  343. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +0 -67
  344. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +0 -1179
  345. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +0 -32
  346. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +0 -588
  347. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +0 -575
  348. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +0 -32
  349. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +0 -482
  350. package/scaffold/.agent/skills/total-recall/plugins/git/ui/DESIGN.md +0 -100
  351. package/scaffold/.agent/skills/total-recall/plugins/git/ui/card.js +0 -637
  352. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +0 -217
  353. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +0 -32
  354. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +0 -1276
  355. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +0 -45
  356. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +0 -1401
  357. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +0 -4
  358. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +0 -41
  359. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +0 -396
  360. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +0 -98
  361. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/DESIGN.md +0 -15
  362. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/card.js +0 -182
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `startup_check`: a read command a plugin contributes to `startup check` and
3
+ * `brief`, so something the plugin owns that needs attention is visible when a
4
+ * session starts. Core runs it and prints what it says; it never inspects the
5
+ * plugin's domain itself.
6
+ *
7
+ * "startup_check": { "command": "unmerged", "args": ["--json"], "timeout_ms": 15000 }
8
+ *
9
+ * The command prints one JSON object: { ok: boolean, outstanding: integer,
10
+ * summary: string }. `outstanding` above zero means attention is needed and
11
+ * `summary` says what and where, in one line.
12
+ */
13
+ const plain = (v) => v && typeof v === 'object' && !Array.isArray(v);
14
+ const ARG = /^[A-Za-z0-9][A-Za-z0-9._=:\/-]{0,127}$|^--?[a-z][a-z0-9-]{0,63}$/;
15
+ export const STARTUP_CHECK_MAX_TIMEOUT_MS = 30000;
16
+
17
+ /** @returns {string[]} errors */
18
+ export function validateStartupCheck(manifest) {
19
+ const check = manifest?.startup_check;
20
+ if (check === undefined) return [];
21
+ if (!plain(check)) return ["'startup_check' must be an object"];
22
+ const errors = [];
23
+ const subs = Array.isArray(manifest.cli?.subcommands) ? manifest.cli.subcommands : [];
24
+ const sub = subs.find((s) => s?.name === check.command);
25
+ if (typeof check.command !== 'string' || !sub) errors.push("'startup_check.command' must name a declared cli subcommand");
26
+ else if (sub.effect && sub.effect !== 'read') errors.push("'startup_check.command' must be a read command");
27
+ if (check.args !== undefined && (!Array.isArray(check.args) || check.args.length > 8 || check.args.some((a) => typeof a !== 'string' || !ARG.test(a)))) {
28
+ errors.push("'startup_check.args' must be at most 8 plain arguments");
29
+ }
30
+ if (check.timeout_ms !== undefined && (!Number.isInteger(check.timeout_ms) || check.timeout_ms < 1000 || check.timeout_ms > STARTUP_CHECK_MAX_TIMEOUT_MS)) {
31
+ errors.push(`'startup_check.timeout_ms' must be an integer from 1000 to ${STARTUP_CHECK_MAX_TIMEOUT_MS}`);
32
+ }
33
+ return errors;
34
+ }
35
+
36
+ /**
37
+ * Read a startup check's output. Anything other than the declared shape is
38
+ * reported as unavailable, never as clean.
39
+ * @returns {{ status: 'ok'|'attention'|'unavailable', outstanding?: number, summary?: string, reason?: string }}
40
+ */
41
+ export function readStartupCheckOutput(stdout) {
42
+ let body;
43
+ try { body = JSON.parse(String(stdout || '').trim()); } catch { return { status: 'unavailable', reason: 'output is not JSON' }; }
44
+ if (!plain(body) || typeof body.ok !== 'boolean' || !Number.isInteger(body.outstanding) || body.outstanding < 0 || typeof body.summary !== 'string') {
45
+ return { status: 'unavailable', reason: 'output is not { ok, outstanding, summary }' };
46
+ }
47
+ if (!body.ok) return { status: 'unavailable', outstanding: body.outstanding, summary: body.summary.slice(0, 500), reason: 'the check reported a failure' };
48
+ return { status: body.outstanding > 0 ? 'attention' : 'ok', outstanding: body.outstanding, summary: body.summary.slice(0, 500) };
49
+ }
@@ -1,6 +1,6 @@
1
1
  /** Pure plugin UI manifest validation; rendering belongs to capability owners. */
2
2
  import path from 'node:path';
3
- export const UI_ELEMENT_KINDS = ['panel', 'form', 'list', 'detail', 'settings', 'widget', 'card', 'card-face'];
3
+ export const UI_ELEMENT_KINDS = ['panel', 'form', 'list', 'detail', 'settings', 'widget'];
4
4
  const PROP_TYPES = ['string', 'number', 'boolean', 'object', 'array'];
5
5
  const ELEMENT_ID = /^[a-z][a-z0-9-]{0,63}$/;
6
6
  // Custom element names: lowercase, start with a letter, contain a hyphen.
@@ -117,13 +117,13 @@ function normalizeElement(raw, index, errors) {
117
117
  /**
118
118
  * Validates and normalizes a manifest `ui` block.
119
119
  * @param {object} ui
120
- * @returns {{ design_tokens?: string, elements: object[] }}
120
+ * @returns {{ design_tokens?: string, home?: string, elements: object[] }}
121
121
  * @throws {UiSpecError}
122
122
  */
123
123
  export function normalizeUiSpec(ui) {
124
124
  const errors = [];
125
125
  if (!isPlainObject(ui)) throw new UiSpecError(["'ui' must be an object"]);
126
- unknownKeys(ui, ['design_tokens', 'elements'], 'ui', errors);
126
+ unknownKeys(ui, ['design_tokens', 'elements', 'home'], 'ui', errors);
127
127
  const spec = { elements: [] };
128
128
  if (ui.design_tokens !== undefined) {
129
129
  if (!isSafePluginPath(ui.design_tokens, /\.md$/)) {
@@ -142,6 +142,12 @@ export function normalizeUiSpec(ui) {
142
142
  tags.add(el.tag);
143
143
  spec.elements.push(el);
144
144
  });
145
+ if (ui.home !== undefined) {
146
+ if (!spec.elements.some(el => el.id === ui.home && ['panel', 'form', 'list', 'detail', 'widget'].includes(el.kind))) {
147
+ errors.push("'ui.home' must name a declared content element");
148
+ } else spec.home = ui.home;
149
+ if (!spec.design_tokens) errors.push("'ui.home' requires 'ui.design_tokens'");
150
+ }
145
151
  if (errors.length) throw new UiSpecError(errors);
146
152
  return spec;
147
153
  }
@@ -6,8 +6,8 @@ import { installPlugin as installLocal, readPluginRecord, patchPluginRecord, des
6
6
  findInstalled, emit, vaultFor } from './plugin-store.mjs';
7
7
  export * from './plugin-store.mjs';
8
8
 
9
- function describePluginWithShare(plugin) {
10
- const result = describePlugin(plugin);
9
+ function describePluginWithShare(plugin, projectRoot) {
10
+ const result = describePlugin(plugin, projectRoot);
11
11
  result.share_url = result.shared && result.sha256 ? publicPluginShareUrl(plugin.id, result.sha256) : null;
12
12
  return result;
13
13
  }
@@ -50,7 +50,7 @@ export async function setPluginShared(id, shared, options = {}) {
50
50
  if (shared && !plugin.valid) throw new Error(`Plugin '${id}' has an invalid manifest and cannot be shared`);
51
51
  await patchPluginRecord(plugin, { shared: !!shared, public_shared: !!shared });
52
52
  await emit(vaultFor(plugin), id, shared ? 'shared' : 'unshared', { scope: plugin.scope });
53
- return describePluginWithShare(plugin);
53
+ return describePluginWithShare(plugin, options.projectRoot);
54
54
  }
55
55
 
56
56
  export async function setPluginBranding(id, branding, options = {}) {
@@ -68,7 +68,7 @@ export async function setPluginBranding(id, branding, options = {}) {
68
68
  };
69
69
  await patchPluginRecord(plugin, { branding: cleanBranding });
70
70
  await emit(vaultFor(plugin), id, 'branding_updated', { branding: cleanBranding, scope: plugin.scope });
71
- return describePluginWithShare(plugin);
71
+ return describePluginWithShare(plugin, options.projectRoot);
72
72
  }
73
73
 
74
74
  export async function setPluginLocked(id, locked, options = {}) {
@@ -76,7 +76,7 @@ export async function setPluginLocked(id, locked, options = {}) {
76
76
  if (!plugin) throw new Error(`Plugin '${id}' is not installed`);
77
77
  await patchPluginRecord(plugin, { locked: !!locked });
78
78
  await emit(vaultFor(plugin), id, locked ? 'locked' : 'unlocked', { scope: plugin.scope });
79
- return describePluginWithShare(plugin);
79
+ return describePluginWithShare(plugin, options.projectRoot);
80
80
  }
81
81
 
82
82
  export async function deployPluginToStore(id, options = {}) {
@@ -97,7 +97,7 @@ export async function deployPluginToStore(id, options = {}) {
97
97
  scope: plugin.scope,
98
98
  locked: autoLock
99
99
  });
100
- return describePluginWithShare(plugin);
100
+ return describePluginWithShare(plugin, options.projectRoot);
101
101
  }
102
102
 
103
103
  /** What this node offers to mesh peers: valid installed plugins whose record says shared. */
@@ -0,0 +1,58 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { FileLeaseStore } from '@ssss/cli/leases';
5
+ import { globalPluginsDir, projectPluginsDir } from './plugin-loader.mjs';
6
+
7
+ /** Use only real directories below the repository, including any archive parents. */
8
+ function directory(root, names, create = false) {
9
+ let target = root;
10
+ for (const name of names) {
11
+ target = path.join(target, name);
12
+ let stat = fs.lstatSync(target, { throwIfNoEntry: false });
13
+ if (!stat && create) {
14
+ try { fs.mkdirSync(target, { mode: 0o700 }); }
15
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
16
+ stat = fs.lstatSync(target);
17
+ }
18
+ if (!stat) return null;
19
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Plugin lifecycle paths must be real directories.');
20
+ }
21
+ return target;
22
+ }
23
+
24
+ export async function withRepositoryPluginLifecycle(projectRoot, id, change) {
25
+ if (!projectRoot) return change(null);
26
+ if (!/^[a-z][a-z0-9-]{1,63}$/.test(id)) throw new Error('Invalid plugin identity');
27
+ const root = fs.realpathSync(projectRoot);
28
+ const brainPath = path.dirname(projectPluginsDir(root));
29
+ const shared = path.dirname(globalPluginsDir());
30
+ if (brainPath === path.resolve(shared) ||
31
+ (fs.existsSync(brainPath) && fs.existsSync(shared) && fs.realpathSync(brainPath) === fs.realpathSync(shared))) {
32
+ return change(null);
33
+ }
34
+ const brain = directory(root, path.relative(root, brainPath).split(path.sep), true);
35
+ const store = new FileLeaseStore(directory(brain, ['config', 'plugin-activation-leases'], true));
36
+ const lease = await store.acquire({ workspace_id: 'plugin-activation', target: id, principal_id: 'total-recall', operation_id: crypto.randomUUID() }, 120_000);
37
+ try {
38
+ return await change(brain);
39
+ } finally {
40
+ await store.release(lease);
41
+ }
42
+ }
43
+
44
+ /** Move the complete local runtime directory without copying or traversing its contents. */
45
+ export function moveRepositoryPluginState(brain, id, direction) {
46
+ if (!brain) return { ok: true, state: 'none' };
47
+ const live = ['plugin-state'];
48
+ const archive = ['config', 'plugin-state-archive'];
49
+ const restoring = direction === 'restore';
50
+ const source = directory(brain, [...(restoring ? archive : live), id]);
51
+ const destination = restoring ? live : archive;
52
+ const existing = directory(brain, [...destination, id]);
53
+ if (source && existing) throw new Error('Active and archived plugin state both exist. Both copies were preserved.');
54
+ if (!source) return { ok: true, state: !restoring && existing ? 'already-archived' : 'none' };
55
+ const parent = directory(brain, destination, true);
56
+ fs.renameSync(source, path.join(parent, id));
57
+ return { ok: true, state: restoring ? 'restored' : 'archived' };
58
+ }
@@ -5,7 +5,8 @@ import { fileURLToPath } from 'node:url';
5
5
  import { validateAppCliSpec } from './plugin-contracts/app-cli.mjs';
6
6
  import { validateUiSpec } from './plugin-contracts/ui.mjs';
7
7
  import { validateConfigurationSpec } from './plugin-contracts/configuration.mjs';
8
- import { validateCardContract } from './plugin-contracts/card.mjs';
8
+ import { validateManifestContract } from './plugin-contracts/manifest.mjs';
9
+ import { validateStartupCheck } from './plugin-contracts/startup-check.mjs';
9
10
 
10
11
  const ID_PATTERN = /^[a-z][a-z0-9-]{1,63}$/;
11
12
  const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
@@ -24,6 +25,8 @@ export function projectPluginsDir(projectRoot = process.cwd()) {
24
25
  }
25
26
 
26
27
  export function globalPluginsDir() {
28
+ // A staged server can use its matching plugin packages with the same brain and Auth store.
29
+ if (process.env.TR_RUNTIME_PLUGINS_DIR) return path.resolve(process.env.TR_RUNTIME_PLUGINS_DIR);
27
30
  const agentDir = process.env.AGENT_DIR || process.env._TR_TEST_AGENT_DIR || path.join(os.homedir(), '.agent');
28
31
  return path.join(agentDir, 'skills', 'total-recall', 'plugins');
29
32
  }
@@ -94,8 +97,9 @@ export function validatePluginManifest(manifest) {
94
97
  return { valid: false, errors: ['Manifest must be a non-null object'] };
95
98
  }
96
99
  if (manifest.configuration !== undefined) errors.push(...validateConfigurationSpec(manifest.configuration));
97
- const cardContract = validateCardContract(manifest);
98
- errors.push(...cardContract.errors);
100
+ const contract = validateManifestContract(manifest);
101
+ errors.push(...contract.errors);
102
+ errors.push(...validateStartupCheck(manifest));
99
103
 
100
104
  if (!manifest.id || typeof manifest.id !== 'string') {
101
105
  errors.push("Missing required field 'id'");
@@ -332,7 +336,7 @@ export function validatePluginManifest(manifest) {
332
336
  return {
333
337
  valid: errors.length === 0,
334
338
  errors,
335
- warnings: cardContract.warnings
339
+ warnings: contract.warnings
336
340
  };
337
341
  }
338
342
 
@@ -412,7 +416,7 @@ export function readPluginsDir(dir, scope) {
412
416
  /**
413
417
  * Discover installed plugins across project and global directories.
414
418
  * A project plugin shadows a global plugin with the same id.
415
- * @param {string} [projectRoot] - Current project root directory
419
+ * @param {string|null} [projectRoot] - Current project root, or null for only shared and bundled plugins
416
420
  */
417
421
  export function discoverPlugins(projectRoot = process.cwd()) {
418
422
  const seen = new Set();
@@ -9,10 +9,10 @@ import { manifestHelp } from './plugin-help.mjs';
9
9
  import { spawn } from 'node:child_process';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { PACKAGE_ROOT, resolvePluginFile } from './plugin-loader.mjs';
13
- import { readPluginRecord } from './plugin-store.mjs';
12
+ import { PACKAGE_ROOT, projectPluginsDir, resolvePluginFile } from './plugin-loader.mjs';
13
+ import { readPluginRecord, recordPluginsDir } from './plugin-store.mjs';
14
14
  import { pluginExecutionEnabled } from './plugin-state.mjs';
15
- import { setPluginActive } from './plugin-activation.mjs';
15
+ import { activatePluginOnUse, setPluginActive } from './plugin-activation.mjs';
16
16
 
17
17
  const CHILD = path.join(path.dirname(fileURLToPath(import.meta.url)), 'plugin-runner-child.mjs');
18
18
  export const DEFAULT_TIMEOUT_MS = 30_000;
@@ -39,11 +39,12 @@ export function validatePluginSecretInput(plugin, subcommand, input) {
39
39
 
40
40
  /**
41
41
  * @param {object} plugin A discovered plugin ({ id, dir, manifest }).
42
- * @param {{ subcommand?: string, args?: string[], cwd?: string, timeoutMs?: number }} [options]
42
+ * @param {{ subcommand?: string, args?: string[], cwd?: string, projectRoot?: string|null, timeoutMs?: number }} [options]
43
43
  * @returns {Promise<{ ok: boolean, exitCode: number|null, signal: string|null, timedOut: boolean, truncated: boolean, output: string, durationMs: number }>}
44
44
  */
45
45
  export function runPluginCommand(plugin, options = {}) {
46
46
  const { subcommand = '', args = [], cwd = process.cwd(), timeoutMs: requestedTimeoutMs, stdin = 'ignore', stdinText, stdinRequest, composable = false, secretsBrainDir } = options;
47
+ const { projectRoot = cwd } = options;
47
48
  const help = manifestHelp(plugin, { subcommand, args });
48
49
  if (help !== null) return Promise.resolve({ ok: true, exitCode: 0, stdout: help, stderr: '', output: help, durationMs: 0, timedOut: false });
49
50
  const maxOutputBytes = options.maxOutputBytes ?? (stdin === 'inherit' ? CLI_MAX_OUTPUT_BYTES : MAX_OUTPUT_BYTES);
@@ -58,19 +59,26 @@ export function runPluginCommand(plugin, options = {}) {
58
59
  const stdinPayload = stdinText ?? stdinRequest;
59
60
  const operation = subcommand || args[0];
60
61
  if (operation === 'on' || operation === 'off') {
61
- return setPluginActive(plugin.id, operation === 'on', { projectRoot: cwd }).then(result => ({
62
- ok: result.compilation.ok, exitCode: result.compilation.ok ? 0 : 1,
62
+ return setPluginActive(plugin.id, operation === 'on', { projectRoot: projectRoot ?? cwd }).then(result => ({
63
+ ok: result.compilation.ok && result.cleanup?.ok !== false, exitCode: result.compilation.ok && result.cleanup?.ok !== false ? 0 : 1,
63
64
  output: JSON.stringify(result), durationMs: 0, timedOut: false,
64
65
  }));
65
66
  }
66
- if (!pluginExecutionEnabled(plugin, { projectRoot: cwd }) && !['status', 'health', 'jobs', '--help', '-h', 'help'].includes(operation)) {
67
- return Promise.reject(new Error(`Plugin ${plugin.id} is inactive; run total-recall plugin on ${plugin.id}`));
68
- }
67
+ const passive = ['status', 'health', 'jobs', '--help', '-h', 'help'].includes(operation);
69
68
  // Manifests declare this as `timeoutMs`; the runner only read `timeout_ms`, so every declared
70
69
  // timeout was ignored and each command fell back to DEFAULT_TIMEOUT_MS (30s). That killed work
71
70
  // that legitimately takes longer — `mesh nodes` and `tr-transcribe transcribe` (model load) both
72
71
  // died at 30s while their manifests asked for far more. Accept both spellings.
73
72
  const declaration = plugin?.manifest?.cli?.subcommands?.find(item => item.name === operation);
73
+ if (!pluginExecutionEnabled(plugin, { projectRoot }) && !passive) {
74
+ if (options.activateOnUse && declaration && !args.includes('--help') && !args.includes('-h')) {
75
+ return activatePluginOnUse(plugin.id, { projectRoot }).then(result => {
76
+ if (result && !result.compilation.ok) throw new Error('Plugin was activated, but the repository instructions could not be refreshed.');
77
+ return runPluginCommand(plugin, { ...options, activateOnUse: false });
78
+ });
79
+ }
80
+ return Promise.reject(new Error(`Plugin ${plugin.id} is inactive; run total-recall plugin on ${plugin.id}`));
81
+ }
74
82
  const declaredTimeout = declaration?.timeout_ms ?? declaration?.timeoutMs;
75
83
  const timeoutMs = requestedTimeoutMs ?? declaredTimeout ?? DEFAULT_TIMEOUT_MS;
76
84
  if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 600000) return Promise.reject(new Error('Plugin timeout must be 1..600000 ms'));
@@ -85,6 +93,8 @@ export function runPluginCommand(plugin, options = {}) {
85
93
  const safeArgs = (Array.isArray(args) ? args : []).map(String);
86
94
  if (stdinPayload !== undefined && !safeArgs.includes('--stdin')) safeArgs.push('--stdin');
87
95
  const childArgv = [CHILD, handler, command, ...(subcommand ? [String(subcommand)] : []), ...safeArgs];
96
+ // Only an explicitly selected project owns repository state. The cwd fallback above preserves legacy activation.
97
+ const statePluginsDir = options.projectRoot ? projectPluginsDir(projectRoot) : recordPluginsDir(plugin);
88
98
 
89
99
  return new Promise((resolve) => {
90
100
  const started = Date.now();
@@ -101,10 +111,11 @@ export function runPluginCommand(plugin, options = {}) {
101
111
  env: {
102
112
  ...process.env,
103
113
  TR_PACKAGE_ROOT: PACKAGE_ROOT,
114
+ TOTAL_RECALL_BIN: path.join(PACKAGE_ROOT, 'bin', 'total-recall.mjs'),
104
115
  TR_PLUGIN_ID: plugin.id,
105
116
  TR_PLUGIN_DIR: plugin.dir,
106
117
  TR_PLUGIN_CONFIG: JSON.stringify(readPluginRecord(plugin)?.configuration || {}),
107
- ...(plugin.pluginsDir ? { TR_PLUGIN_STATE_DIR: path.join(path.dirname(plugin.pluginsDir), 'plugin-state', plugin.id) } : {}),
118
+ ...(statePluginsDir ? { TR_PLUGIN_STATE_DIR: path.join(path.dirname(statePluginsDir), 'plugin-state', plugin.id) } : {}),
108
119
  ...(secretsBrainDir ? { TR_SECRETS_BRAIN: secretsBrainDir } : {}),
109
120
  TR_PLUGIN_COMMAND_MODE: composable ? 'composable' : 'cli',
110
121
  FORCE_COLOR: '0',
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Run the read commands installed plugins contribute to `startup check` and
3
+ * `brief` (`startup_check` in plugin.json). Core runs them and reports what
4
+ * they say; the domain knowledge stays in the plugin. A check that fails,
5
+ * times out or prints the wrong shape is reported as unavailable, never as
6
+ * clean.
7
+ */
8
+ import { discoverPlugins } from './plugin-loader.mjs';
9
+ import { runPluginCommand } from './plugin-runner.mjs';
10
+ import { pluginExecutionEnabled } from './plugin-state.mjs';
11
+ import { readStartupCheckOutput, STARTUP_CHECK_MAX_TIMEOUT_MS } from './plugin-contracts/startup-check.mjs';
12
+
13
+ const DEFAULT_TIMEOUT_MS = 15000;
14
+
15
+ /**
16
+ * @param {{ cwd?: string, plugins?: object[], run?: Function, enabled?: Function }} [options]
17
+ * @returns {Promise<Array<{ plugin: string, command: string, status: string, outstanding?: number, summary?: string, reason?: string }>>}
18
+ */
19
+ export async function runPluginStartupChecks({ cwd = process.cwd(), plugins, run = runPluginCommand, enabled = pluginExecutionEnabled } = {}) {
20
+ let found;
21
+ try { found = plugins ?? discoverPlugins(cwd); } catch (error) { return [{ plugin: '*', command: 'plugin discovery', status: 'unavailable', reason: error.message }]; }
22
+ const declared = found.filter((plugin) => plugin.valid && plugin.manifest?.startup_check);
23
+ return Promise.all(declared.map(async (plugin) => {
24
+ const check = plugin.manifest.startup_check;
25
+ const command = `${plugin.manifest.cli?.command || plugin.id} ${check.command}`;
26
+ try {
27
+ if (!enabled(plugin, { projectRoot: cwd })) return { plugin: plugin.id, command, status: 'inactive' };
28
+ } catch (error) {
29
+ return { plugin: plugin.id, command, status: 'unavailable', reason: `activation policy unreadable: ${error.message}` };
30
+ }
31
+ const timeoutMs = Math.min(check.timeout_ms ?? DEFAULT_TIMEOUT_MS, STARTUP_CHECK_MAX_TIMEOUT_MS);
32
+ try {
33
+ const result = await run(plugin, { subcommand: check.command, args: check.args || [], cwd, timeoutMs });
34
+ if (result.timedOut) return { plugin: plugin.id, command, status: 'unavailable', reason: `timed out after ${timeoutMs} ms` };
35
+ const read = readStartupCheckOutput(result.stdout);
36
+ if (read.status !== 'unavailable' && result.exitCode !== 0) {
37
+ return { plugin: plugin.id, command, ...read, status: 'unavailable', reason: `exited ${result.exitCode}` };
38
+ }
39
+ return { plugin: plugin.id, command, ...read };
40
+ } catch (error) {
41
+ return { plugin: plugin.id, command, status: 'unavailable', reason: error.message };
42
+ }
43
+ }));
44
+ }
@@ -1,9 +1,15 @@
1
1
  /** Read-only host activation, safe during configuration bootstrap. */
2
2
  import fs from 'node:fs';
3
+ import crypto from 'node:crypto';
3
4
  import path from 'node:path';
4
5
  import matter from './frontmatter.mjs';
5
6
  import { globalPluginsDir, projectPluginsDir, vaultForPluginsDir } from './plugin-loader.mjs';
6
7
 
8
+ /** Bundled fallback packages use the same shared policy as a global install. */
9
+ export function usesSharedPluginPolicy(plugin) {
10
+ return plugin.scope === 'global' || plugin.scope === 'scaffold';
11
+ }
12
+
7
13
  /**
8
14
  * Enablement has two layers. The plugin's own brain holds the shared setting —
9
15
  * the global brain, since a shared plugin lives in one folder per machine. A
@@ -26,14 +32,17 @@ export function pluginActivation(plugin, { projectRoot } = {}) {
26
32
  const record = readActivationRecord(file, plugin.id);
27
33
  // Only a plugin owned by the shared (global) folder has an override to read:
28
34
  // a project's own copy already is that repository's setting.
29
- const override = plugin.scope === 'global' && projectRoot ? projectActivationOverride(plugin.id, projectRoot).record : null;
35
+ const override = usesSharedPluginPolicy(plugin) && projectRoot ? projectActivationOverride(plugin.id, projectRoot).record : null;
30
36
  const applied = override?.runtime_enabled;
31
37
  const source = typeof applied === 'boolean' ? 'project' : 'shared';
32
38
  const shared = record?.runtime_enabled !== false;
33
39
  const enabled = plugin.valid === true && (typeof applied === 'boolean' ? applied : shared);
40
+ const localRecord = usesSharedPluginPolicy(plugin) ? override : record;
41
+ const onUse = !!projectRoot && localRecord?.runtime_on_use === true && localRecord.runtime_enabled === false;
34
42
  const runtime = plugin.manifest?.runtime || {};
35
43
  return {
36
44
  state: enabled ? 'active' : 'inactive', enabled, source,
45
+ ...(onUse ? { mode: 'on-use' } : {}),
37
46
  overridden: source === 'project' && applied !== shared,
38
47
  evidence: 'Host activation policy, not external process or provider health',
39
48
  hasRuntime: Boolean(plugin.manifest?.tasks?.length || Object.keys(runtime).length),
@@ -55,13 +64,13 @@ export function pluginExecutionEnabled(plugin, { projectRoot } = {}) {
55
64
  if (!/^[a-z][a-z0-9-]{1,63}$/.test(plugin.id)) throw new Error('Invalid plugin identity');
56
65
  const file = path.join(vaultForPluginsDir(pluginsDir), 'system', 'plugins', `${plugin.id}.md`);
57
66
  const record = readActivationRecord(file, plugin.id);
58
- const override = plugin.scope === 'global' && projectRoot ? projectActivationOverride(plugin.id, projectRoot).record : null;
67
+ const override = usesSharedPluginPolicy(plugin) && projectRoot ? projectActivationOverride(plugin.id, projectRoot).record : null;
59
68
  const applied = override?.runtime_enabled;
60
69
  return typeof applied === 'boolean' ? applied : record?.runtime_enabled !== false;
61
70
  }
62
71
 
63
72
  /** Pure bootstrap read, avoiding configuration/kernel cycles; malformed existing policy fails closed. */
64
- export function readActivationRecord(file, id) {
73
+ export function readActivationRecord(file, id, { withHash = false } = {}) {
65
74
  if (!file) return null;
66
75
  let stat;
67
76
  try { stat = fs.lstatSync(file); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
@@ -71,7 +80,12 @@ export function readActivationRecord(file, id) {
71
80
  const directory = fs.lstatSync(parent);
72
81
  if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error('Plugin policy cannot follow a linked vault path');
73
82
  }
74
- const record = matter(fs.readFileSync(file, 'utf8')).data;
83
+ const bytes = fs.readFileSync(file);
84
+ const record = matter(bytes.toString('utf8')).data;
75
85
  if (record.type !== 'plugin_record' || record.plugin_id !== id || ['title', 'description', 'timestamp'].some(key => typeof record[key] !== 'string' || !record[key].trim()) || record.runtime_enabled !== undefined && typeof record.runtime_enabled !== 'boolean') throw new Error('Plugin policy is malformed; repair it before execution');
76
- return record;
86
+ if (record.runtime_on_use !== undefined &&
87
+ (typeof record.runtime_on_use !== 'boolean' || (record.runtime_on_use && record.runtime_enabled !== false))) {
88
+ throw new Error('Plugin on-use policy is malformed; repair it before execution');
89
+ }
90
+ return withHash ? { record, hash: 'sha256:' + crypto.createHash('sha256').update(bytes).digest('hex') } : record;
77
91
  }
@@ -31,8 +31,8 @@ import {
31
31
  } from './plugin-bundle.mjs';
32
32
  import { writeVfsDocument, patchVfsDocument, deleteVfsDocument, appendVfsEvent } from './ssss-operation-service.mjs';
33
33
  import { findVfsDocumentByPath } from './vfs-documents.mjs';
34
- import { readActivationRecord, projectActivationOverride } from './plugin-state.mjs';
35
- import { REQUIRED_INSTALLATION_PLUGIN_IDS } from './default-plugins.mjs';
34
+ import { readActivationRecord, projectActivationOverride, pluginActivation } from './plugin-state.mjs';
35
+ import { DEFAULT_PLUGIN_IDS, REQUIRED_INSTALLATION_PLUGIN_IDS } from './default-plugins.mjs';
36
36
 
37
37
  const execFileAsync = promisify(execFile);
38
38
  const ID_PATTERN = /^[a-z][a-z0-9-]{1,63}$/;
@@ -183,7 +183,7 @@ export async function patchPluginRecord(plugin, patches) {
183
183
  * setting so it follows the shared one again. The overlay never installs the
184
184
  * plugin: a repository without it uses the shared folder's copy.
185
185
  */
186
- export async function patchProjectActivationRecord(id, projectRoot, runtimeEnabled, { manifest = null } = {}) {
186
+ export async function patchProjectActivationRecord(id, projectRoot, runtimeEnabled, { manifest = null, onUse = false } = {}) {
187
187
  if (!ID_PATTERN.test(id)) throw new Error('Invalid plugin identity');
188
188
  const pluginsDir = projectPluginsDir(projectRoot);
189
189
  const vaultRoot = vaultForPluginsDir(pluginsDir);
@@ -194,7 +194,7 @@ export async function patchProjectActivationRecord(id, projectRoot, runtimeEnabl
194
194
  if (runtimeEnabled === null) {
195
195
  const existing = readProjectActivationRecord(id, projectRoot);
196
196
  if (!existing) return null;
197
- const { runtime_enabled: _dropped, ...frontmatter } = { ...(existing.frontmatter || {}) };
197
+ const { runtime_enabled: _dropped, runtime_on_use: _onUse, ...frontmatter } = { ...(existing.frontmatter || {}) };
198
198
  await writeVfsDocument(recordPath(id), {
199
199
  ...frontmatter,
200
200
  type: 'plugin_record',
@@ -210,7 +210,7 @@ export async function patchProjectActivationRecord(id, projectRoot, runtimeEnabl
210
210
  if (typeof runtimeEnabled !== 'boolean') throw new Error('Plugin activation requires a boolean or null');
211
211
  const previous = readProjectActivationRecord(id, projectRoot);
212
212
  if (previous) {
213
- return patchVfsDocument(recordPath(id), { runtime_enabled: runtimeEnabled, timestamp: now }, {
213
+ return patchVfsDocument(recordPath(id), { runtime_enabled: runtimeEnabled, runtime_on_use: onUse, timestamp: now }, {
214
214
  actorRole: 'system',
215
215
  intent: `Set repository activation for plugin ${id}`,
216
216
  vaultRoot
@@ -226,6 +226,7 @@ export async function patchProjectActivationRecord(id, projectRoot, runtimeEnabl
226
226
  category: 'plugins',
227
227
  tags: ['plugin', 'activation-override'],
228
228
  runtime_enabled: runtimeEnabled,
229
+ runtime_on_use: onUse,
229
230
  source: { kind: 'shared', ref: globalPluginsDir() }
230
231
  };
231
232
  await writeVfsDocument(recordPath(id), frontmatter, buildPluginDocumentBody(m, id), {
@@ -236,8 +237,19 @@ export async function patchProjectActivationRecord(id, projectRoot, runtimeEnabl
236
237
  return frontmatter;
237
238
  }
238
239
 
240
+ function describeActivation(plugin, projectRoot) {
241
+ const required = REQUIRED_INSTALLATION_PLUGIN_IDS.includes(plugin.id);
242
+ try {
243
+ const { state, enabled, mode } = pluginActivation(plugin, { projectRoot });
244
+ return { state, enabled, required, ...(mode ? { mode } : {}) };
245
+ } catch {
246
+ // An unreadable repository policy is an error, never shared enablement.
247
+ return { state: 'error', enabled: false, required };
248
+ }
249
+ }
250
+
239
251
  /** Facts about one installed plugin, merged with its record. */
240
- export function describePlugin(plugin) {
252
+ export function describePlugin(plugin, projectRoot = process.cwd()) {
241
253
  const record = readPluginRecord(plugin);
242
254
  const m = plugin.manifest || {};
243
255
  let current = null;
@@ -248,6 +260,7 @@ export function describePlugin(plugin) {
248
260
  const taskRuns = record?.task_runs || {};
249
261
  return {
250
262
  id: plugin.id,
263
+ core: DEFAULT_PLUGIN_IDS.includes(plugin.id),
251
264
  name: m.name || plugin.id,
252
265
  version: m.version || '0.0.0',
253
266
  description: m.description || '',
@@ -284,13 +297,18 @@ export function describePlugin(plugin) {
284
297
  ui: m.ui || null,
285
298
  configuration: m.configuration || null,
286
299
  manifest: m,
287
- card_face: record?.card_face || m.card_face || null,
288
- activation: { state: plugin.valid && record?.runtime_enabled !== false ? 'active' : 'inactive', enabled: plugin.valid && record?.runtime_enabled !== false }
300
+ activation: describeActivation(plugin, projectRoot)
289
301
  };
290
302
  }
291
303
 
292
- export function listInstalledPlugins(projectRoot = process.cwd()) {
293
- return discoverPlugins(projectRoot).map(describePlugin);
304
+ export function listInstalledPlugins(projectRoot = process.cwd(), { descriptions } = {}) {
305
+ return discoverPlugins(projectRoot).map(plugin => {
306
+ const description = descriptions?.get(plugin.dir);
307
+ if (description) return { ...description, activation: describeActivation(plugin, projectRoot) };
308
+ const result = describePlugin(plugin, projectRoot);
309
+ descriptions?.set(plugin.dir, result);
310
+ return result;
311
+ });
294
312
  }
295
313
 
296
314
  /** Bundled plugins shipped in the package, flagged with whether they are installed here. */
@@ -344,7 +362,8 @@ export async function recompileAfterPluginMutation(projectRoot = process.cwd(),
344
362
  // read on demand at preflight, so a shared plugin change compiles no other repository
345
363
  // (ON-DEMAND-CONTEXT-01; one install used to rebuild all registered brains).
346
364
  await compileLayer(compileSurface, projectRoot, isGlobal);
347
- if (isGlobal) {
365
+ // A shared change made from the dashboard's global brain has no repository to refresh.
366
+ if (isGlobal && projectRoot) {
348
367
  // The repository the command ran in refreshes its own catalog now, so its plugin library
349
368
  // never lags the change. That compile is cheap: entry files and vault indexes keep their
350
369
  // own triggers, so only the catalog and CLI index move.
@@ -7,6 +7,8 @@ import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { randomUUID } from 'node:crypto';
9
9
 
10
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
11
+
10
12
  function brainJsonPath(brainDir) {
11
13
  return path.join(brainDir, 'config', 'brain.json');
12
14
  }
@@ -35,21 +37,21 @@ export function readProjectId(brainDir) {
35
37
  * Return the project's id, creating a random UUID on first use.
36
38
  * @returns {{ project_id: string, created: boolean }}
37
39
  */
38
- export function ensureProjectId(brainDir) {
40
+ export function ensureProjectId(brainDir, { fallbackId } = {}) {
39
41
  const file = brainJsonPath(brainDir);
40
42
  let cfg;
41
43
  try { cfg = JSON.parse(fs.readFileSync(file, 'utf8')); }
42
44
  catch (err) { if (err.code === 'ENOENT') cfg = {}; else throw new Error('Existing project identity is unreadable or malformed'); }
43
45
  if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) throw new Error('Existing project identity must be an object');
44
46
  if (cfg.project_id) return { project_id: cfg.project_id, created: false };
45
- cfg.project_id = randomUUID();
47
+ cfg.project_id = fallbackId || randomUUID();
46
48
  writeJsonAtomic(file, cfg);
47
49
  return { project_id: cfg.project_id, created: true };
48
50
  }
49
51
 
50
52
  /**
51
- * Give every registered project an id and make the registry agree with each
52
- * project's brain.json (the brain.json id wins).
53
+ * Make the registry agree with each project's brain.json. If the disk identity
54
+ * is missing, preserve a unique valid UUID already published in the registry.
53
55
  * @returns {{ total: number, created: number, synced: number, missing_brain: number }}
54
56
  */
55
57
  export function repairProjectIds(globalBrainDir) {
@@ -57,13 +59,21 @@ export function repairProjectIds(globalBrainDir) {
57
59
  const list = readJson(registryPath, []);
58
60
  const out = { total: 0, created: 0, synced: 0, missing_brain: 0 };
59
61
  if (!Array.isArray(list)) return out;
62
+ const owners = new Map();
63
+ for (const row of list) {
64
+ const onDisk = typeof row?.brainDir === 'string' ? readProjectId(row.brainDir) : null;
65
+ for (const id of new Set([row?.project_id, onDisk])) {
66
+ if (typeof id === 'string' && UUID.test(id)) owners.set(id, (owners.get(id) || 0) + 1);
67
+ }
68
+ }
60
69
  for (const row of list) {
61
70
  out.total++;
62
71
  if (!row?.brainDir || !fs.existsSync(row.brainDir)) {
63
72
  out.missing_brain++;
64
73
  continue;
65
74
  }
66
- const { project_id, created } = ensureProjectId(row.brainDir);
75
+ const fallbackId = owners.get(row.project_id) === 1 ? row.project_id : undefined;
76
+ const { project_id, created } = ensureProjectId(row.brainDir, { fallbackId });
67
77
  if (created) out.created++;
68
78
  if (row.project_id !== project_id) {
69
79
  row.project_id = project_id;