total-recall-brain 3.45.0 → 3.46.1

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 (363) hide show
  1. package/bin/total-recall.mjs +7 -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/default-plugin-refresh.mjs +39 -0
  285. package/src/core/ensure-dependencies.mjs +19 -15
  286. package/src/core/memory-brain.mjs +30 -8
  287. package/src/core/plugin-activation.mjs +63 -20
  288. package/src/core/plugin-contracts/{card.mjs → manifest.mjs} +23 -57
  289. package/src/core/plugin-contracts/startup-check.mjs +49 -0
  290. package/src/core/plugin-contracts/ui.mjs +9 -3
  291. package/src/core/plugin-distribution.mjs +6 -6
  292. package/src/core/plugin-lifecycle.mjs +58 -0
  293. package/src/core/plugin-loader.mjs +9 -5
  294. package/src/core/plugin-runner.mjs +21 -10
  295. package/src/core/plugin-startup-checks.mjs +44 -0
  296. package/src/core/plugin-state.mjs +19 -5
  297. package/src/core/plugin-store.mjs +30 -11
  298. package/src/core/project-id.mjs +15 -5
  299. package/src/core/required-plugins.mjs +31 -6
  300. package/src/core/schema.mjs +2 -0
  301. package/src/core/ssss-operation-service.mjs +1 -0
  302. package/src/core/startup-health.mjs +28 -3
  303. package/src/core/testing/plugin-fixture.mjs +22 -21
  304. package/src/core/throttled-fetch.mjs +25 -18
  305. package/src/core/ui-chunk-choice.mjs +77 -0
  306. package/src/core/ui-chunk.registry.json +23 -0
  307. package/src/core/ui-chunks.mjs +434 -0
  308. package/src/core/vault-cache.mjs +58 -59
  309. package/src/core/workspace-compositions.mjs +151 -0
  310. package/src/core/workspace-drawers.mjs +79 -0
  311. package/src/server/api.mjs +4 -27
  312. package/src/server/index.mjs +28 -21
  313. package/src/server/memory-events.mjs +31 -0
  314. package/src/server/plugin-brain-targets.mjs +89 -0
  315. package/src/server/rest.mjs +2 -0
  316. package/src/server/route-inventory.mjs +3 -2
  317. package/src/server/route-manifest.json +52 -4
  318. package/src/server/routes/_shared.mjs +7 -14
  319. package/src/server/routes/brains.mjs +29 -44
  320. package/src/server/routes/dashboard.mjs +26 -0
  321. package/src/server/routes/plugins.mjs +154 -45
  322. package/src/server/routes/ui-chunks.mjs +192 -0
  323. package/frontend/dist/assets/index-DACh1dhx.js +0 -5613
  324. package/frontend/dist/assets/index-DtwsDyFM.css +0 -1
  325. package/frontend/src/components/plugins/card/CardEngine.tsx +0 -108
  326. package/frontend/src/components/plugins/card/CardScenes.tsx +0 -103
  327. package/frontend/src/components/plugins/card/ExecutionPicker.tsx +0 -64
  328. package/frontend/src/components/plugins/card/args.ts +0 -28
  329. package/frontend/src/components/plugins/card/card.css +0 -32
  330. package/frontend/src/components/plugins/card/cli-line.ts +0 -13
  331. package/frontend/src/components/plugins/card/contract.ts +0 -91
  332. package/frontend/src/components/plugins/card/format.ts +0 -49
  333. package/frontend/src/components/plugins/card/runner-context.ts +0 -11
  334. package/frontend/src/components/plugins/card/runner.tsx +0 -43
  335. package/frontend/src/components/plugins/card/theme.ts +0 -35
  336. package/frontend/src/components/plugins/card/use-source.ts +0 -22
  337. package/frontend/src/components/plugins/card/widgets.tsx +0 -94
  338. package/frontend/src/components/plugins/workspace/WorkspaceHost.tsx +0 -23
  339. package/frontend/src/pages/PluginsPage.tsx +0 -1719
  340. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +0 -47
  341. package/scaffold/.agent/skills/plugin/prompts/06-card.md +0 -46
  342. package/scaffold/.agent/skills/plugin/references/card.md +0 -90
  343. package/scaffold/.agent/skills/plugin/subagents/card-designer.md +0 -60
  344. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +0 -67
  345. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +0 -1179
  346. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +0 -32
  347. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +0 -588
  348. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +0 -575
  349. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +0 -32
  350. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +0 -482
  351. package/scaffold/.agent/skills/total-recall/plugins/git/ui/DESIGN.md +0 -100
  352. package/scaffold/.agent/skills/total-recall/plugins/git/ui/card.js +0 -637
  353. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +0 -217
  354. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +0 -32
  355. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +0 -1276
  356. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +0 -45
  357. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +0 -1401
  358. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +0 -4
  359. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +0 -41
  360. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +0 -396
  361. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +0 -98
  362. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/DESIGN.md +0 -15
  363. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/card.js +0 -182
@@ -1,15 +1,4 @@
1
- /**
2
- * Functional card contract. A plugin's card face is its own bespoke Web
3
- * Component, declared as a `ui.elements[]` entry of kind `card`. There is no
4
- * template: no shared layout, no widget vocabulary, no allowlist of shapes.
5
- * Core validates that the face exists and that the commands behind it are
6
- * declared; it never dictates how the face is arranged.
7
- *
8
- * A plugin carries exactly one face and AS MANY secondary screens as it needs,
9
- * each declared `kind: 'panel'`. Screens are the plugin's own design: it names
10
- * them, orders them and decides what is on them. The host imposes no count and
11
- * no screen set, and never derives a screen from the command list.
12
- */
1
+ /** Cross-field validation for plugin manifests and their command contracts. */
13
2
  const plain = (v) => v && typeof v === 'object' && !Array.isArray(v);
14
3
  const COMMAND = /^[a-z][a-z0-9-]{0,63}$/;
15
4
  const ARG_NAME = /^[a-z][a-zA-Z0-9]{0,63}$/;
@@ -19,11 +8,10 @@ export const COMMAND_EFFECTS = ['read', 'write', 'destructive', 'send'];
19
8
  export const OUTPUT_KINDS = ['json', 'text'];
20
9
  export const TIERS = ['core', 'official', 'personal', 'community'];
21
10
  export const LICENSE_CLASSES = ['open', 'closed'];
22
- const STATUS_STATES = ['ok', 'warn', 'error', 'unknown'];
23
- export const CARD_FONTS = ['system', 'inter', 'ibm-plex', 'space-grotesk', 'jetbrains-mono', 'dm-sans', 'outfit', 'fraunces', 'newsreader', 'chivo-mono'];
24
- export const CARD_DENSITIES = ['compact', 'comfortable', 'spacious'];
25
- export const CARD_RADII = ['none', 'sm', 'md', 'lg', 'pill'];
26
- export const CARD_HEADERS = ['plain', 'ledger-rule', 'band', 'tab-strip', 'badge-left'];
11
+ const BRAND_FONTS = ['system', 'inter', 'ibm-plex', 'space-grotesk', 'jetbrains-mono', 'dm-sans', 'outfit', 'fraunces', 'newsreader', 'chivo-mono'];
12
+ const BRAND_DENSITIES = ['compact', 'comfortable', 'spacious'];
13
+ const BRAND_RADII = ['none', 'sm', 'md', 'lg', 'pill'];
14
+ const BRAND_HEADERS = ['plain', 'ledger-rule', 'band', 'tab-strip', 'badge-left'];
27
15
  export const WORKSPACE_PERMISSIONS = ['microphone', 'camera', 'notifications'];
28
16
  const HEX = /^#[0-9a-fA-F]{6}$/;
29
17
 
@@ -41,7 +29,7 @@ function validateArg(arg, where, errors) {
41
29
  if (arg.secret === true && arg.type !== 'string') errors.push(`${where}: a secret stdin argument must be a string`);
42
30
  }
43
31
 
44
- export function validateCardContract(manifest) {
32
+ export function validateManifestContract(manifest) {
45
33
  const errors = [];
46
34
  const warnings = [];
47
35
  const subs = manifest.cli?.subcommands;
@@ -55,6 +43,13 @@ export function validateCardContract(manifest) {
55
43
  else names.add(sub.name);
56
44
  if (sub.effect !== undefined && !COMMAND_EFFECTS.includes(sub.effect)) errors.push(`${where}.effect must be one of: ${COMMAND_EFFECTS.join(', ')}`);
57
45
  if (sub.output !== undefined && !OUTPUT_KINDS.includes(sub.output)) errors.push(`${where}.output must be one of: ${OUTPUT_KINDS.join(', ')}`);
46
+ const terminalFlags = sub.terminal_only_args;
47
+ if (sub.terminal_only !== undefined && typeof sub.terminal_only !== 'boolean') errors.push(`${where}.terminal_only must be a boolean`);
48
+ if (terminalFlags !== undefined && (!Array.isArray(terminalFlags) ||
49
+ terminalFlags.some(flag => typeof flag !== 'string' || !FLAG.test(flag)) ||
50
+ new Set(terminalFlags).size !== terminalFlags.length)) {
51
+ errors.push(`${where}.terminal_only_args must contain unique long flags`);
52
+ }
58
53
  // The runner refuses anything outside this range, so a manifest that
59
54
  // declares it must fail validation rather than every call at run time.
60
55
  if (sub.timeout_ms !== undefined && (!Number.isInteger(sub.timeout_ms) || sub.timeout_ms < 1 || sub.timeout_ms > 600000)) errors.push(`${where}.timeout_ms must be an integer from 1 to 600000`);
@@ -91,7 +86,7 @@ export function validateCardContract(manifest) {
91
86
  if (!LICENSE_CLASSES.includes(manifest.distribution.license)) errors.push(`'distribution.license' must be one of: ${LICENSE_CLASSES.join(', ')}`);
92
87
  }
93
88
  }
94
- validateCardFace(manifest, errors, warnings);
89
+ validatePresentation(manifest, errors, warnings);
95
90
  validateBranding(manifest.branding, errors, warnings);
96
91
  validateWorkspace(manifest.workspace, errors);
97
92
  if (Array.isArray(manifest.secrets)) {
@@ -99,30 +94,15 @@ export function validateCardContract(manifest) {
99
94
  if (!plain(s) || typeof s.key !== 'string' || !/^[A-Z][A-Z0-9_]{0,63}$/.test(s.key)) errors.push(`secrets[${i}].key must be an uppercase identifier`);
100
95
  });
101
96
  }
102
- if (plain(manifest.card_face) && Array.isArray(manifest.card_face.metrics)) {
103
- warnings.push("card_face.metrics are fixed text and cannot reflect real state; declare a 'status' command instead");
104
- }
105
- return { errors, warnings, states: STATUS_STATES };
97
+ return { errors, warnings };
106
98
  }
107
99
 
108
- /**
109
- * Every plugin has a card face: bespoke owner code, declared once in
110
- * ui.elements[]. A plugin without one is incomplete, and a plugin that declares
111
- * a shared template is invalid.
112
- *
113
- * The *scaffold* emitted by `plugin create` deliberately contains no card: a
114
- * face is authored by hand once the plugin's function is understood. A scaffold
115
- * declares `scaffold: true`, so it installs while the author works and reports
116
- * its missing face as a warning rather than an error. Any other plugin without
117
- * a face is an error, because nothing else explains why it is missing.
118
- */
119
- function validateCardFace(manifest, errors, warnings) {
120
- if (manifest.card_layout !== undefined) {
121
- errors.push("'card_layout' is not supported: a card face is bespoke owner code in ui.elements[], never a shared template");
100
+ function validatePresentation(manifest, errors, warnings) {
101
+ for (const field of ['card', 'card_face', 'card_layout']) {
102
+ if (manifest[field] !== undefined) errors.push(`'${field}' is not supported: declare a native ui.home instead`);
122
103
  }
123
104
  const elements = manifest.ui?.elements;
124
- // A brick is distributed as capability code alone. It carries no UI, so it
125
- // has no face; every other plugin still requires exactly one.
105
+ // A brick is distributed as capability code alone and has no UI.
126
106
  if (manifest.brick === true) {
127
107
  validateCapability(manifest, errors);
128
108
  if (manifest.ui !== undefined && Array.isArray(elements) && elements.length > 0) {
@@ -131,23 +111,9 @@ function validateCardFace(manifest, errors, warnings) {
131
111
  return;
132
112
  }
133
113
  if (manifest.capability !== undefined) validateCapability(manifest, errors);
134
- if (!Array.isArray(elements) || elements.length === 0) {
135
- // A scaffold is a freshly created plugin whose face has not been authored
136
- // yet. It must be installable so the author can iterate, but it is not a
137
- // shippable plugin, so the missing face is reported and stays visible.
138
- if (manifest.scaffold === true) {
139
- warnings.push("'ui.elements' declares no card face yet: author ui/card.js and ui/DESIGN.md by hand for this plugin's own function, then declare the face in ui.elements[]");
140
- return;
141
- }
142
- errors.push("'ui.elements' must declare the plugin's card face; every plugin has one, authored by hand");
143
- return;
144
- }
145
- // One face, and as many secondary screens as the plugin needs. Cardinality is
146
- // checked for `card` alone, so panels may be declared in any number and on
147
- // either side of the face.
148
- const faces = elements.filter((element) => plain(element) && element.kind === 'card');
149
- if (faces.length === 0) errors.push("'ui.elements' must declare an element of kind 'card'");
150
- if (faces.length > 1) errors.push("'ui.elements' must declare exactly one card face; further screens use kind 'panel'");
114
+ if (manifest.ui?.home !== undefined) return;
115
+ if (manifest.scaffold === true) warnings.push("Scaffold needs an owner-authored 'ui.home' and DESIGN.md before shipping");
116
+ else errors.push("A plugin must declare a native 'ui.home'");
151
117
  }
152
118
 
153
119
  /**
@@ -188,7 +154,7 @@ function validateBranding(branding, errors, warnings) {
188
154
  if (branding.icon !== undefined) {
189
155
  warnings.push('branding.icon is deprecated: show the real logo with branding.logo instead of an emoji or generic icon');
190
156
  }
191
- const enums = { font: CARD_FONTS, density: CARD_DENSITIES, radius: CARD_RADII, header: CARD_HEADERS };
157
+ const enums = { font: BRAND_FONTS, density: BRAND_DENSITIES, radius: BRAND_RADII, header: BRAND_HEADERS };
192
158
  for (const [key, allowed] of Object.entries(enums)) {
193
159
  const value = branding[key];
194
160
  if (value === undefined || allowed.includes(value)) continue;
@@ -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
  }