@rubytech/create-sitedesk-code 0.1.509 → 0.1.511

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 (272) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +555 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +394 -0
  4. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +232 -0
  5. package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +169 -0
  6. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  8. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +10 -0
  9. package/payload/platform/plugins/business-assistant/PLUGIN.md +1 -0
  10. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -0
  11. package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +21 -0
  12. package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
  13. package/payload/platform/plugins/scheduling/PLUGIN.md +1 -0
  14. package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +112 -4
  15. package/payload/platform/scripts/lib/account-schema-owned-dirs.py +61 -32
  16. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +17 -30
  18. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/config.d.ts +50 -4
  20. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/config.js +58 -15
  22. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  23. package/payload/server/maxy-edge.js +20 -10
  24. package/payload/server/public/assets/AdminLoginScreens-CWMpccrR.css.br +0 -0
  25. package/payload/server/public/assets/AdminLoginScreens-CWMpccrR.css.gz +0 -0
  26. package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js +1 -0
  27. package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.br +0 -0
  28. package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.gz +0 -0
  29. package/payload/server/public/assets/AdminShell-BMPEWNZk.js.br +0 -0
  30. package/payload/server/public/assets/AdminShell-BMPEWNZk.js.gz +0 -0
  31. package/payload/server/public/assets/_baseFor-Be3EKK3K.js.br +0 -0
  32. package/payload/server/public/assets/_baseFor-Be3EKK3K.js.gz +0 -0
  33. package/payload/server/public/assets/activity-ByWYF5dC.js.br +0 -0
  34. package/payload/server/public/assets/activity-ByWYF5dC.js.gz +0 -0
  35. package/payload/server/public/assets/admin-Do53K90C.js +1 -0
  36. package/payload/server/public/assets/admin-Do53K90C.js.br +0 -0
  37. package/payload/server/public/assets/admin-Do53K90C.js.gz +0 -0
  38. package/payload/server/public/assets/agents-C0rTfrYd.js.br +0 -0
  39. package/payload/server/public/assets/agents-C0rTfrYd.js.gz +0 -0
  40. package/payload/server/public/assets/arc-DaI5MLTq.js.br +0 -0
  41. package/payload/server/public/assets/arc-DaI5MLTq.js.gz +0 -0
  42. package/payload/server/public/assets/architecture-YZFGNWBL-DWZIX5aV.js.br +0 -0
  43. package/payload/server/public/assets/architectureDiagram-Q4EWVU46-CBGVUEN9.js.br +0 -0
  44. package/payload/server/public/assets/architectureDiagram-Q4EWVU46-CBGVUEN9.js.gz +0 -0
  45. package/payload/server/public/assets/array-X0JlPOfd.js.br +0 -0
  46. package/payload/server/public/assets/blockDiagram-DXYQGD6D-DS6dS4GZ.js.br +0 -0
  47. package/payload/server/public/assets/blockDiagram-DXYQGD6D-DS6dS4GZ.js.gz +0 -0
  48. package/payload/server/public/assets/browser-4tTaem8N.js.br +0 -0
  49. package/payload/server/public/assets/browser-4tTaem8N.js.gz +0 -0
  50. package/payload/server/public/assets/c4Diagram-AHTNJAMY-hBSBUZPt.js.br +0 -0
  51. package/payload/server/public/assets/c4Diagram-AHTNJAMY-hBSBUZPt.js.gz +0 -0
  52. package/payload/server/public/assets/calendar-BEjnkKG1.js.br +0 -0
  53. package/payload/server/public/assets/calendar-BEjnkKG1.js.gz +0 -0
  54. package/payload/server/public/assets/channel-BEokjAeE.js.br +0 -0
  55. package/payload/server/public/assets/chat-DBgzjDIE.js.br +0 -0
  56. package/payload/server/public/assets/chat-DBgzjDIE.js.gz +0 -0
  57. package/payload/server/public/assets/chevron-right-CQfPGsFb.js.br +0 -0
  58. package/payload/server/public/assets/chunk-2KRD3SAO-B6koUoB7.js.br +0 -0
  59. package/payload/server/public/assets/chunk-2KRD3SAO-B6koUoB7.js.gz +0 -0
  60. package/payload/server/public/assets/chunk-336JU56O-BF6BRh8u.js.br +0 -0
  61. package/payload/server/public/assets/chunk-336JU56O-BF6BRh8u.js.gz +0 -0
  62. package/payload/server/public/assets/chunk-426QAEUC-pbQ4PWBU.js.br +0 -0
  63. package/payload/server/public/assets/chunk-426QAEUC-pbQ4PWBU.js.gz +0 -0
  64. package/payload/server/public/assets/chunk-4BX2VUAB-7sO95xR2.js.br +0 -0
  65. package/payload/server/public/assets/chunk-4BX2VUAB-7sO95xR2.js.gz +0 -0
  66. package/payload/server/public/assets/chunk-4TB4RGXK-GCTndJYx.js.br +0 -0
  67. package/payload/server/public/assets/chunk-4TB4RGXK-GCTndJYx.js.gz +0 -0
  68. package/payload/server/public/assets/chunk-55IACEB6-ggIJf_TC.js.br +0 -0
  69. package/payload/server/public/assets/chunk-55IACEB6-ggIJf_TC.js.gz +0 -0
  70. package/payload/server/public/assets/chunk-5FUZZQ4R-DeGmkimt.js.br +0 -0
  71. package/payload/server/public/assets/chunk-5FUZZQ4R-DeGmkimt.js.gz +0 -0
  72. package/payload/server/public/assets/chunk-5PVQY5BW-aFIQKk_U.js.br +0 -0
  73. package/payload/server/public/assets/chunk-5PVQY5BW-aFIQKk_U.js.gz +0 -0
  74. package/payload/server/public/assets/chunk-67CJDMHE-LS8rUbii.js.br +2 -0
  75. package/payload/server/public/assets/chunk-67CJDMHE-LS8rUbii.js.gz +0 -0
  76. package/payload/server/public/assets/chunk-7N4EOEYR-JKOVngS0.js.br +2 -0
  77. package/payload/server/public/assets/chunk-7N4EOEYR-JKOVngS0.js.gz +0 -0
  78. package/payload/server/public/assets/chunk-AA7GKIK3-CoXnTVK6.js.br +0 -0
  79. package/payload/server/public/assets/chunk-AA7GKIK3-CoXnTVK6.js.gz +0 -0
  80. package/payload/server/public/assets/chunk-BSJP7CBP-CcsqObFg.js.br +0 -0
  81. package/payload/server/public/assets/chunk-BSJP7CBP-CcsqObFg.js.gz +0 -0
  82. package/payload/server/public/assets/chunk-CIAEETIT-CpsybR7q.js.br +0 -0
  83. package/payload/server/public/assets/chunk-CIAEETIT-CpsybR7q.js.gz +0 -0
  84. package/payload/server/public/assets/chunk-CltuBf4Z.js.br +0 -0
  85. package/payload/server/public/assets/chunk-CltuBf4Z.js.gz +0 -0
  86. package/payload/server/public/assets/chunk-EDXVE4YY-BZ2I9PSq.js.br +0 -0
  87. package/payload/server/public/assets/chunk-EDXVE4YY-BZ2I9PSq.js.gz +0 -0
  88. package/payload/server/public/assets/chunk-ENJZ2VHE-DbTwt6OZ.js.br +0 -0
  89. package/payload/server/public/assets/chunk-ENJZ2VHE-DbTwt6OZ.js.gz +0 -0
  90. package/payload/server/public/assets/chunk-FMBD7UC4-D27VnSCa.js.br +0 -0
  91. package/payload/server/public/assets/chunk-FMBD7UC4-D27VnSCa.js.gz +0 -0
  92. package/payload/server/public/assets/chunk-FOC6F5B3-C0tU2UFl.js.br +0 -0
  93. package/payload/server/public/assets/chunk-FOC6F5B3-C0tU2UFl.js.gz +0 -0
  94. package/payload/server/public/assets/chunk-ICPOFSXX-UPPu2OIq.js.br +0 -0
  95. package/payload/server/public/assets/chunk-ICPOFSXX-UPPu2OIq.js.gz +0 -0
  96. package/payload/server/public/assets/chunk-K5T4RW27-DIKxqyYw.js.br +0 -0
  97. package/payload/server/public/assets/chunk-K5T4RW27-DIKxqyYw.js.gz +0 -0
  98. package/payload/server/public/assets/chunk-KGLVRYIC-CiL4YVLo.js.br +0 -0
  99. package/payload/server/public/assets/chunk-KGLVRYIC-CiL4YVLo.js.gz +0 -0
  100. package/payload/server/public/assets/chunk-LIHQZDEY-ejKezVlD.js.br +0 -0
  101. package/payload/server/public/assets/chunk-LIHQZDEY-ejKezVlD.js.gz +0 -0
  102. package/payload/server/public/assets/chunk-ORNJ4GCN-Cc0vqc-D.js.br +0 -0
  103. package/payload/server/public/assets/chunk-ORNJ4GCN-Cc0vqc-D.js.gz +0 -0
  104. package/payload/server/public/assets/chunk-OYMX7WX6-Drxn1qG0.js.br +0 -0
  105. package/payload/server/public/assets/chunk-OYMX7WX6-Drxn1qG0.js.gz +0 -0
  106. package/payload/server/public/assets/chunk-QZHKN3VN-Dhlh7Pkk.js.br +0 -0
  107. package/payload/server/public/assets/chunk-QZHKN3VN-Dhlh7Pkk.js.gz +0 -0
  108. package/payload/server/public/assets/chunk-U2HBQHQK-DnbQIkRP.js.br +0 -0
  109. package/payload/server/public/assets/chunk-U2HBQHQK-DnbQIkRP.js.gz +0 -0
  110. package/payload/server/public/assets/chunk-X2U36JSP-CauTSVWg.js.br +0 -0
  111. package/payload/server/public/assets/chunk-X2U36JSP-CauTSVWg.js.gz +0 -0
  112. package/payload/server/public/assets/chunk-XPW4576I-DZqqB9Zi.js.br +0 -0
  113. package/payload/server/public/assets/chunk-XPW4576I-DZqqB9Zi.js.gz +0 -0
  114. package/payload/server/public/assets/chunk-YZCP3GAM-BGHo4Li-.js.br +0 -0
  115. package/payload/server/public/assets/chunk-YZCP3GAM-BGHo4Li-.js.gz +0 -0
  116. package/payload/server/public/assets/chunk-ZZ45TVLE-DWGJKPd7.js.br +0 -0
  117. package/payload/server/public/assets/chunk-ZZ45TVLE-DWGJKPd7.js.gz +0 -0
  118. package/payload/server/public/assets/classDiagram-6PBFFD2Q-D2z_ZV1x.js.br +0 -0
  119. package/payload/server/public/assets/classDiagram-6PBFFD2Q-D2z_ZV1x.js.gz +0 -0
  120. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BJwJoNEK.js.br +0 -0
  121. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BJwJoNEK.js.gz +0 -0
  122. package/payload/server/public/assets/clock-Dn6FHB51.js.br +0 -0
  123. package/payload/server/public/assets/clock-Dn6FHB51.js.gz +0 -0
  124. package/payload/server/public/assets/clone-Bn7XXVhr.js.br +0 -0
  125. package/payload/server/public/assets/cose-bilkent-S5V4N54A-D-95BzXL.js.br +0 -0
  126. package/payload/server/public/assets/cose-bilkent-S5V4N54A-D-95BzXL.js.gz +0 -0
  127. package/payload/server/public/assets/cytoscape.esm-CI2IWMLB.js.br +0 -0
  128. package/payload/server/public/assets/cytoscape.esm-CI2IWMLB.js.gz +0 -0
  129. package/payload/server/public/assets/dagre-BZYNBHvT.js.br +0 -0
  130. package/payload/server/public/assets/dagre-BZYNBHvT.js.gz +0 -0
  131. package/payload/server/public/assets/dagre-KV5264BT-DyU4HQLQ.js.br +0 -0
  132. package/payload/server/public/assets/dagre-KV5264BT-DyU4HQLQ.js.gz +0 -0
  133. package/payload/server/public/assets/data-D_e6Vdfd.js.br +0 -0
  134. package/payload/server/public/assets/data-D_e6Vdfd.js.gz +0 -0
  135. package/payload/server/public/assets/defaultLocale-B6RGN4id.js.br +0 -0
  136. package/payload/server/public/assets/defaultLocale-B6RGN4id.js.gz +0 -0
  137. package/payload/server/public/assets/diagram-5BDNPKRD-CCttJ9XA.js.br +0 -0
  138. package/payload/server/public/assets/diagram-5BDNPKRD-CCttJ9XA.js.gz +0 -0
  139. package/payload/server/public/assets/diagram-G4DWMVQ6-DvkpgSLO.js.br +0 -0
  140. package/payload/server/public/assets/diagram-G4DWMVQ6-DvkpgSLO.js.gz +0 -0
  141. package/payload/server/public/assets/diagram-MMDJMWI5-DHmW5Y9k.js.br +0 -0
  142. package/payload/server/public/assets/diagram-MMDJMWI5-DHmW5Y9k.js.gz +0 -0
  143. package/payload/server/public/assets/diagram-TYMM5635-C6T4hwzO.js.br +0 -0
  144. package/payload/server/public/assets/diagram-TYMM5635-C6T4hwzO.js.gz +0 -0
  145. package/payload/server/public/assets/dist-QvbmK1IP.js.br +0 -0
  146. package/payload/server/public/assets/dist-QvbmK1IP.js.gz +0 -0
  147. package/payload/server/public/assets/erDiagram-SMLLAGMA-5v_o7s3T.js.br +0 -0
  148. package/payload/server/public/assets/erDiagram-SMLLAGMA-5v_o7s3T.js.gz +0 -0
  149. package/payload/server/public/assets/file-text-DMKw3nIk.js.br +0 -0
  150. package/payload/server/public/assets/file-text-DMKw3nIk.js.gz +0 -0
  151. package/payload/server/public/assets/flatten-BtIc9jX7.js.br +0 -0
  152. package/payload/server/public/assets/flatten-BtIc9jX7.js.gz +0 -0
  153. package/payload/server/public/assets/flowDiagram-DWJPFMVM-3UhCDsjt.js.br +0 -0
  154. package/payload/server/public/assets/flowDiagram-DWJPFMVM-3UhCDsjt.js.gz +0 -0
  155. package/payload/server/public/assets/ganttDiagram-T4ZO3ILL-F2zgHIIQ.js.br +0 -0
  156. package/payload/server/public/assets/ganttDiagram-T4ZO3ILL-F2zgHIIQ.js.gz +0 -0
  157. package/payload/server/public/assets/gitGraph-7Q5UKJZL-C-FvNTHZ.js.br +0 -0
  158. package/payload/server/public/assets/gitGraphDiagram-UUTBAWPF-oQc1N3dT.js.br +0 -0
  159. package/payload/server/public/assets/gitGraphDiagram-UUTBAWPF-oQc1N3dT.js.gz +0 -0
  160. package/payload/server/public/assets/graph-C3vW8lvw.js.br +0 -0
  161. package/payload/server/public/assets/graph-C3vW8lvw.js.gz +0 -0
  162. package/payload/server/public/assets/graph-labels-BZgd0L6n.js.br +0 -0
  163. package/payload/server/public/assets/graph-labels-BZgd0L6n.js.gz +0 -0
  164. package/payload/server/public/assets/graphlib-DY4-AT2Q.js.br +0 -0
  165. package/payload/server/public/assets/graphlib-DY4-AT2Q.js.gz +0 -0
  166. package/payload/server/public/assets/info-OMHHGYJF-BjLPgHAC.js.br +0 -0
  167. package/payload/server/public/assets/infoDiagram-42DDH7IO-CXNKLTui.js.br +0 -0
  168. package/payload/server/public/assets/infoDiagram-42DDH7IO-CXNKLTui.js.gz +0 -0
  169. package/payload/server/public/assets/init-BmUWJJHz.js.br +0 -0
  170. package/payload/server/public/assets/init-BmUWJJHz.js.gz +0 -0
  171. package/payload/server/public/assets/isEmpty-Bu4fZ8Kf.js.br +0 -0
  172. package/payload/server/public/assets/isEmpty-Bu4fZ8Kf.js.gz +0 -0
  173. package/payload/server/public/assets/ishikawaDiagram-UXIWVN3A-wJhSOkuw.js.br +0 -0
  174. package/payload/server/public/assets/ishikawaDiagram-UXIWVN3A-wJhSOkuw.js.gz +0 -0
  175. package/payload/server/public/assets/journeyDiagram-VCZTEJTY-DgadC6Pz.js.br +0 -0
  176. package/payload/server/public/assets/journeyDiagram-VCZTEJTY-DgadC6Pz.js.gz +0 -0
  177. package/payload/server/public/assets/kanban-definition-6JOO6SKY-Bs4nQqXI.js.br +0 -0
  178. package/payload/server/public/assets/kanban-definition-6JOO6SKY-Bs4nQqXI.js.gz +0 -0
  179. package/payload/server/public/assets/katex-BjZuzSQn.js.br +0 -0
  180. package/payload/server/public/assets/katex-BjZuzSQn.js.gz +0 -0
  181. package/payload/server/public/assets/line-Bnmxy1hO.js.br +0 -0
  182. package/payload/server/public/assets/line-Bnmxy1hO.js.gz +0 -0
  183. package/payload/server/public/assets/linear-RvwZxFzh.js.br +0 -0
  184. package/payload/server/public/assets/linear-RvwZxFzh.js.gz +0 -0
  185. package/payload/server/public/assets/maximize-2-la3IBA3M.js.br +0 -0
  186. package/payload/server/public/assets/maximize-2-la3IBA3M.js.gz +0 -0
  187. package/payload/server/public/assets/mermaid-parser.core-DeNV8RtA.js.br +0 -0
  188. package/payload/server/public/assets/mermaid-parser.core-DeNV8RtA.js.gz +0 -0
  189. package/payload/server/public/assets/mermaid.core-BU9IiSCP.js.br +0 -0
  190. package/payload/server/public/assets/mermaid.core-BU9IiSCP.js.gz +0 -0
  191. package/payload/server/public/assets/mindmap-definition-QFDTVHPH-D1PMUxxm.js.br +0 -0
  192. package/payload/server/public/assets/mindmap-definition-QFDTVHPH-D1PMUxxm.js.gz +0 -0
  193. package/payload/server/public/assets/{operator-RRRdqoRQ.js → operator-D9NgVL9_.js} +1 -1
  194. package/payload/server/public/assets/operator-D9NgVL9_.js.br +0 -0
  195. package/payload/server/public/assets/operator-D9NgVL9_.js.gz +0 -0
  196. package/payload/server/public/assets/ordinal-C2TEWKNu.js.br +0 -0
  197. package/payload/server/public/assets/ordinal-C2TEWKNu.js.gz +0 -0
  198. package/payload/server/public/assets/packet-4T2RLAQJ-BBAZASfb.js.br +0 -0
  199. package/payload/server/public/assets/page-BWFHRIAH.js.br +0 -0
  200. package/payload/server/public/assets/page-BWFHRIAH.js.gz +0 -0
  201. package/payload/server/public/assets/page-DC11gesX.js.br +0 -0
  202. package/payload/server/public/assets/page-DC11gesX.js.gz +0 -0
  203. package/payload/server/public/assets/path-CoPyR7c2.js.br +0 -0
  204. package/payload/server/public/assets/path-CoPyR7c2.js.gz +0 -0
  205. package/payload/server/public/assets/pdf-render-C257cidb.js.br +0 -0
  206. package/payload/server/public/assets/pdf-render-C257cidb.js.gz +0 -0
  207. package/payload/server/public/assets/pdf.worker.min-yatZIOMy.mjs.br +0 -0
  208. package/payload/server/public/assets/pdf.worker.min-yatZIOMy.mjs.gz +0 -0
  209. package/payload/server/public/assets/pie-ZZUOXDRM-CCJUnMeG.js.br +0 -0
  210. package/payload/server/public/assets/pieDiagram-DEJITSTG-BzdNzoKA.js.br +0 -0
  211. package/payload/server/public/assets/pieDiagram-DEJITSTG-BzdNzoKA.js.gz +0 -0
  212. package/payload/server/public/assets/preload-helper-BQrMkyGX.js.br +0 -0
  213. package/payload/server/public/assets/preload-helper-BQrMkyGX.js.gz +0 -0
  214. package/payload/server/public/assets/public-DQbbLHQN.js.br +0 -0
  215. package/payload/server/public/assets/public-DQbbLHQN.js.gz +0 -0
  216. package/payload/server/public/assets/quadrantDiagram-34T5L4WZ-oFr1MJRO.js.br +0 -0
  217. package/payload/server/public/assets/quadrantDiagram-34T5L4WZ-oFr1MJRO.js.gz +0 -0
  218. package/payload/server/public/assets/radar-PYXPWWZC-BsRU8Aep.js.br +0 -0
  219. package/payload/server/public/assets/reduce-BGpN5lgn.js.br +0 -0
  220. package/payload/server/public/assets/reduce-BGpN5lgn.js.gz +0 -0
  221. package/payload/server/public/assets/requirementDiagram-MS252O5E-C9QBQxDh.js.br +0 -0
  222. package/payload/server/public/assets/requirementDiagram-MS252O5E-C9QBQxDh.js.gz +0 -0
  223. package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.br +0 -0
  224. package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.gz +0 -0
  225. package/payload/server/public/assets/rough.esm-jSt7UMyi.js.br +0 -0
  226. package/payload/server/public/assets/rough.esm-jSt7UMyi.js.gz +0 -0
  227. package/payload/server/public/assets/routines-asrBGZUR.js.br +0 -0
  228. package/payload/server/public/assets/routines-asrBGZUR.js.gz +0 -0
  229. package/payload/server/public/assets/sankeyDiagram-XADWPNL6-c_y3bvVv.js.br +0 -0
  230. package/payload/server/public/assets/sankeyDiagram-XADWPNL6-c_y3bvVv.js.gz +0 -0
  231. package/payload/server/public/assets/sequenceDiagram-FGHM5R23-YwlCBPHE.js.br +0 -0
  232. package/payload/server/public/assets/sequenceDiagram-FGHM5R23-YwlCBPHE.js.gz +0 -0
  233. package/payload/server/public/assets/skills-D4ECwoxa.js.br +0 -0
  234. package/payload/server/public/assets/skills-D4ECwoxa.js.gz +0 -0
  235. package/payload/server/public/assets/src-C2WwTWSY.js.br +0 -0
  236. package/payload/server/public/assets/src-C2WwTWSY.js.gz +0 -0
  237. package/payload/server/public/assets/stateDiagram-FHFEXIEX-BFE3e-FM.js.br +0 -0
  238. package/payload/server/public/assets/stateDiagram-FHFEXIEX-BFE3e-FM.js.gz +0 -0
  239. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CpjB4ykI.js.br +4 -0
  240. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CpjB4ykI.js.gz +0 -0
  241. package/payload/server/public/assets/tasks-U3QdbUsO.js.br +0 -0
  242. package/payload/server/public/assets/tasks-U3QdbUsO.js.gz +0 -0
  243. package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.br +0 -0
  244. package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.gz +0 -0
  245. package/payload/server/public/assets/timeline-definition-GMOUNBTQ-azqhmRwZ.js.br +0 -0
  246. package/payload/server/public/assets/timeline-definition-GMOUNBTQ-azqhmRwZ.js.gz +0 -0
  247. package/payload/server/public/assets/treeView-SZITEDCU-BDzhKA6v.js.br +0 -0
  248. package/payload/server/public/assets/treemap-W4RFUUIX-epHpdeWa.js.br +0 -0
  249. package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.br +0 -0
  250. package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.gz +0 -0
  251. package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.br +0 -0
  252. package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.gz +0 -0
  253. package/payload/server/public/assets/useSubAccountSwitcher-CJKjEPNN.js.br +0 -0
  254. package/payload/server/public/assets/useSubAccountSwitcher-CJKjEPNN.js.gz +0 -0
  255. package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.br +0 -0
  256. package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.gz +0 -0
  257. package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.br +0 -0
  258. package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.gz +0 -0
  259. package/payload/server/public/assets/vennDiagram-DHZGUBPP-Da2RfFs8.js.br +0 -0
  260. package/payload/server/public/assets/vennDiagram-DHZGUBPP-Da2RfFs8.js.gz +0 -0
  261. package/payload/server/public/assets/wardley-RL74JXVD-DNSZCrxV.js.br +0 -0
  262. package/payload/server/public/assets/wardleyDiagram-NUSXRM2D-HdrdBYES.js.br +0 -0
  263. package/payload/server/public/assets/wardleyDiagram-NUSXRM2D-HdrdBYES.js.gz +0 -0
  264. package/payload/server/public/assets/wrench-Cpee3C3J.js.br +2 -0
  265. package/payload/server/public/assets/wrench-Cpee3C3J.js.gz +0 -0
  266. package/payload/server/public/assets/xychartDiagram-5P7HB3ND-DMk6rTcQ.js.br +0 -0
  267. package/payload/server/public/assets/xychartDiagram-5P7HB3ND-DMk6rTcQ.js.gz +0 -0
  268. package/payload/server/public/index.html +2 -2
  269. package/payload/server/public/operator.html +2 -2
  270. package/payload/server/server.js +524 -503
  271. package/payload/server/public/assets/AdminLoginScreens-8Jk7hnWH.js +0 -1
  272. package/payload/server/public/assets/admin-CnUWbKcx.js +0 -1
@@ -0,0 +1,169 @@
1
+ # Task 2014 design — cut the jsdom the `platform/ui` suite does not use
2
+
3
+ Date: 2026-07-26
4
+ Task: `.tasks/pending/2014-platform-ui-suite-is-nondeterministic-under-parallel-load.md`
5
+ Supersedes brief: 1765, folded into 2014 on 2026-07-26
6
+
7
+ ## The measurement this design rests on
8
+
9
+ All figures from the dev Mac, Node 22.22.0, vitest 4.1.2, worktree
10
+ `task-2014-ui-suite-nondeterminism`, 2026-07-26.
11
+
12
+ ### Most apparent worktree flakiness is missing build output, not flakiness
13
+
14
+ `platform/node_modules` and `platform/lib/*/dist` are gitignored build output. A
15
+ fresh worktree has neither. Four full-suite runs, each after adding one more
16
+ piece of provisioning:
17
+
18
+ | Run | Worktree state | Failed | Duration |
19
+ |---|---|---|---|
20
+ | 1 | `platform/ui` deps only | 63 files / 32 tests | 435s |
21
+ | 2 | + 3 libs built | 5 files / 8 tests | 180s |
22
+ | 3 | + `platform` deps | 5 files / 3 tests | 111s |
23
+ | 4 | + all 24 libs built | 0 | 143s |
24
+
25
+ Runs 1 and 2 failed on `Cannot find module .../dist/index.js` and
26
+ `Failed to resolve import "neo4j-driver"`. Those are absent artifacts, not
27
+ timing. Only run 3's three failures are the defect this task describes, and all
28
+ three are in the brief's population: both `/login/start` sub-account cases in
29
+ `whatsapp-self-or-house-scope.test.ts` at 5013ms and 515ms, and
30
+ `whatsapp-conversation-graph-state-projection.test.ts` "sub-account all-senders
31
+ mode returns only its bound thread" at 5017ms. 5000ms is the vitest default
32
+ `testTimeout`.
33
+
34
+ ### Environment setup costs six times what the tests cost
35
+
36
+ Run 4, fully green:
37
+
38
+ ```
39
+ tests 267s
40
+ environment 1571s
41
+ import 257s
42
+ setup 196s
43
+ transform 42s
44
+ ```
45
+
46
+ `vitest.config.ts` sets `environment: 'jsdom'` globally. 530 test files pay it.
47
+
48
+ ### Roughly half the suite boots a DOM it never touches
49
+
50
+ 266 `.test.ts` files do not declare `@vitest-environment node`. Both runs
51
+ back to back, same machine load:
52
+
53
+ | | Duration | environment CPU |
54
+ |---|---|---|
55
+ | as-is (jsdom) | 103.0s | 1317.6s |
56
+ | `--environment node` | 30.9s | 0.074s |
57
+
58
+ Three consecutive node-environment passes over those 266 files agreed on 17
59
+ `.ts` files that genuinely need a DOM. A file that appeared in only one pass
60
+ (`route-cache-watchers.test.ts`) is the known flake, not a DOM dependency. A
61
+ `.tsx` file in the failure list arrived because vitest CLI arguments are
62
+ substring filters rather than exact paths.
63
+
64
+ So the suite's problem is not 4757 tests. The tests are 267s of CPU. It is that
65
+ about half the files run a browser DOM environment for nothing, and that is the
66
+ CPU starving the workers the named tests race against.
67
+
68
+ ### Machine context
69
+
70
+ The dev Mac ran these measurements at load average 100 to 350 with 60 `claude`
71
+ processes across six peer worktree sessions. Self-contention from 20 vitest
72
+ forks is not the only contender for CPU, and any verification has to record the
73
+ load it ran under or a red run cannot be attributed.
74
+
75
+ ## Design
76
+
77
+ ### 1. Route the test environment by file extension
78
+
79
+ `platform/ui/vitest.config.ts` replaces its single global `environment: 'jsdom'`
80
+ with two projects:
81
+
82
+ - `dom`: `**/*.{test,spec}.?(c|m)[jt]sx`, environment `jsdom`.
83
+ - `node`: `**/*.{test,spec}.?(c|m)[jt]s` plus `../scripts/__tests__/**`,
84
+ environment `node`.
85
+
86
+ The existing `resolve.dedupe`, `setupFiles`, `globals`, and `exclude` settings
87
+ carry into both projects unchanged.
88
+
89
+ The rule is structural, not a maintained list: a test that renders JSX gets a
90
+ DOM, and anything else opts in explicitly. No `.tsx` file is edited.
91
+
92
+ ### 2. Seventeen `.ts` files opt back into jsdom
93
+
94
+ Each gets `// @vitest-environment jsdom` with a short reason. Verified on vitest
95
+ 4.1.2 that a per-file docblock overrides a project's `environment` setting.
96
+
97
+ ```
98
+ app/__tests__/spinning-class-global.test.ts
99
+ app/__tests__/wa-viewer-width.test.ts
100
+ app/graph/__tests__/side-panel-helpers.test.ts
101
+ app/hooks/__tests__/useCopyFeedback.test.ts
102
+ app/hooks/__tests__/useMediaQuery.test.ts
103
+ app/hooks/__tests__/useVoiceRecorder.deviceId.test.ts
104
+ app/hooks/__tests__/useVoiceRecorder.mic-result.test.ts
105
+ app/lib/__tests__/admin-auth-login-remote-auth.test.ts
106
+ app/lib/__tests__/switch-account-scope.test.ts
107
+ app/lib/__tests__/use-admin-auth-claude-connected.test.ts
108
+ app/lib/__tests__/use-admin-auth-heartbeat-remote-auth.test.ts
109
+ app/lib/__tests__/useAdminFetch.test.ts
110
+ app/lib/__tests__/useSubAccountSwitcher.test.ts
111
+ app/public/__tests__/extract-seed-question.test.ts
112
+ app/whatsapp/__tests__/useTranscriptStream-activity.test.ts
113
+ app/whatsapp/__tests__/useTranscriptStream-endpoint.test.ts
114
+ app/whatsapp/__tests__/useTranscriptStream.test.ts
115
+ ```
116
+
117
+ A `.ts` test that needs a DOM and lacks the docblock fails loudly on
118
+ `document is not defined`. It does not pass silently.
119
+
120
+ ### 3. A prebuild gate so the waste cannot creep back
121
+
122
+ New `platform/ui/scripts/check-jsdom-budget.mjs`, joining the 24 checks already
123
+ in the `prebuild` script. It counts the files that will boot jsdom, which is
124
+ every `.test.tsx`/`.spec.tsx` plus every `.ts` test declaring the jsdom
125
+ docblock, and fails the build when that count exceeds a ceiling written in the
126
+ script. Adding a jsdom file becomes a deliberate one-line bump with a reason,
127
+ not an accident. The failure message names the files above the ceiling and the
128
+ CPU cost per jsdom file measured here, so a reader knows what the number buys.
129
+
130
+ ### 4. The 141 existing `@vitest-environment node` docblocks stay
131
+
132
+ They remain accurate and are now redundant. Removing them is 141 files of churn
133
+ for no behaviour change.
134
+
135
+ ## What this does not claim
136
+
137
+ Removing the environment waste reduces the contention the named tests race
138
+ against. It is not proven, before measurement, that the ten named tests stop
139
+ failing. If group A's long-press tests still race React Testing Library's
140
+ 1000ms `findBy` default once contention drops, that is a second and separate
141
+ lever, and it gets measured and brought back rather than stacked on top as a
142
+ speculative second fix.
143
+
144
+ ## Verification
145
+
146
+ **Group A and the suite.** Ten consecutive `npm test` runs in `platform/ui` on
147
+ the merged branch, `uptime` captured before and after each run, exit code and
148
+ any `×` lines collected to one file. Success is ten `exit=0` and zero `×`
149
+ matches across the collection, reported alongside the load each ran under. A red
150
+ run with its load recorded is attributable; a red run without is not.
151
+
152
+ **Group B.** Ten runs do not test a one-in-twenty failure rate. Instead measure
153
+ the wall-clock cost of the four group B seed calls under full-suite parallel
154
+ load, before and after the change, and state the headroom inside the 5000ms
155
+ default. The measurement is the evidence, not the run count.
156
+
157
+ **The jsdom cut itself.** The suite must still report 530 files and 4757 tests.
158
+ A file silently dropping out of the run because a glob missed it would look like
159
+ a pass.
160
+
161
+ ## Out of scope
162
+
163
+ - The `platform/lib` build artifacts that made runs 1 to 3 look flaky. That is
164
+ worktree provisioning friction and gets its own task file.
165
+ - A blanket `testTimeout` bump, excluded by the brief.
166
+ - Deleting, skipping, or loosening the four group B tests, excluded by the
167
+ brief: they are the only place the cross-account 403 ordering is asserted.
168
+ - Removing the 141 redundant node docblocks.
169
+ - Any product change to the whatsapp routes, the data browser, or graph-write.
@@ -2,6 +2,7 @@
2
2
  name: admin
3
3
  surface: platform
4
4
  description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), admin-hostname (the admin-host twin of public-hostname — resolves the host that serves /calendar and the rest of the admin shell behind sign-in from the same ingress + alias + operator files, so agents point the operator at the admin /calendar URL instead of the public host that 404s it), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, the multi-account managed-service lifecycle tools (account_list, account_create, account_delete, account_purge, account_channel_routing, account_channel_routing_set — one Rubytech operator hosting N client accounts on one house install; create/delete/purge are operator-only and purge is two-step with a typed confirmation; account_create requires the client business owner's real name (ownerName), refusing a blank or the placeholder "Owner" and seeding it onto the sub-account's owner AdminUser and owner Person, and requires the owner's IANA timezone (timezone) — refusing a blank or a non-IANA value — seeding it onto the owner UserProfile marked owner-set so scheduling reads the owner's real zone from the start rather than the box's server default, while a managing house admin (its userId) is optional — when supplied it is validated as a live house admin and recorded as the sub-account's managingAdminUserId, and when omitted the sub-account is unmanaged and its passive-intake tasks route to the install's primary admin; account_reassign_admin repoints a client sub-account's managing admin to another live house admin (the repair path for a dangling managingAdminUserId, or to record one on an unmanaged account); account_roster is the canonical per-account identity view — one row per sub-account giving business name, role, the managing house admin resolved to a name with status (assigned/absent/dangling), the business owner from the graph with status (named/nameless), and the WhatsApp account-manager binding if any, each field carrying what it means and does not (managing admin is the responsible house team member not the owner; account manager is WhatsApp routing only with no admin rights), so the agent never stitches admin-list + account.json + whatsapp-config; account_set_owner_name names (or corrects) a client sub-account's business owner by writing the owned Person — the canonical name source — refusing a blank or the placeholder "Owner", the guarded repair for a nameless owner (the standing identity-audit op=nameless-owned-person detector); account_channel_routing_set repoints the install's single WhatsApp/Telegram inbound stream to a different sub-account), admin-add (refuses on a client sub-account, where admin access is already install-wide so a per-operator seat grants nothing — the sub-account's one identity, its business owner, is set at account creation; the house account, where operators hold PIN logins and admins[] rows, is unchanged), admin-remove (refuses to remove a house admin who still manages one or more client sub-accounts, naming them so the operator reassigns each with account_reassign_admin first — never silently orphans them), admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, cpu-triage-start and cpu-triage-read (the two-call device CPU diagnostic — start spawns a detached sampling run across at least one full 120s reconcile cycle and returns a run handle immediately, read returns running/complete/failed with the per-core distribution; no admin tool blocks through a sampling window, and no spot reading is obtainable), plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), skill-search (free-text intent → skill resolver: ranks every plugin skill's SKILL.md name+description against a query and returns the top owners with their canonical skills/<slug>/ path, so intent→skill mapping survives the available-skills menu leaving context after a compaction), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the SiteDesk platform."
5
+ account-owned-files: [{"file": "webchat-channel-bindings.json", "description": "Routes this account's inbound admin webchat messages. Written by the session manager's webchat-channel-store."}, {"file": "wa-channel-bindings.json", "description": "Routes this account's inbound WhatsApp messages. Written by the session manager's wa-channel-store, which runs on every brand whether or not the whatsapp plugin ships."}, {"file": "telegram-channel-bindings.json", "description": "Routes this account's inbound Telegram messages. Written by the session manager's telegram-channel-store, which runs on every brand whether or not the telegram plugin ships."}, {"file": "canonical-webchat-session.json", "description": "Pins the account's canonical webchat session. Written by the admin UI server."}, {"file": "session-titles.json", "description": "Operator renames of sessions. Written by the session manager's title stores."}, {"file": "agents-disabled.json", "description": "Names the agents the operator switched off. Written by the admin agents route; provision reads it to re-apply the disables and never creates it."}]
5
6
  tools:
6
7
  - name: system-status
7
8
  publicAllowlist: false
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what SiteDesk ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:aabbb6dbe5d9f4fe8ff6f7b3e2f06a46bd1c5281ea46b820829ab2b3bc93a0c0
4
+ content-hash: sha256:86e768fb27dbcb38fc304ace12dbca3462943fcb58435cea6925ebf2469a0647
5
5
  brand: sitedesk-code
6
6
  product-name: SiteDesk
7
7
  ---
@@ -5451,7 +5451,7 @@ If the initial Cloudflare login fails during setup, SiteDesk will fall back to a
5451
5451
 
5452
5452
  `maxy-edge.service` (always-on front door) classifies upstream errors and serves a brand-aware response. There are two distinct user-visible shapes; the right one depends on what failed.
5453
5453
 
5454
- **Branded holding page (brand logo + "Starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. The page renders the brand logo (inlined as a base64 data URI at edge boot from `<install>/server/public/brand/<assets.logo>`) and the brand display/body fonts (loaded from fonts.googleapis.com) — both paths bypass the unavailable upstream so the page never makes a same-origin asset fetch. When `brand.logoContainsName` is true the logo replaces the productName text; otherwise the page falls back to "SiteDesk is starting". No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds. Boot-time confirmation that the logo resolved: `[edge] brand=<name> holding-logo=inlined assets-dir=<path>` — `holding-logo=missing` means the logo file wasn't found at `assets-dir`, the page degrades to text-only.
5454
+ **Branded holding page (brand logo + "Starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. The page renders the brand logo (inlined as a base64 data URI at edge boot from `<install>/server/public/brand/<assets.logo>`) and the brand display/body fonts (loaded from fonts.googleapis.com) — both paths bypass the unavailable upstream so the page never makes a same-origin asset fetch. When `brand.logoContainsName` is true the logo replaces the productName text; otherwise the page falls back to "SiteDesk is starting". No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] ts=<ISO-8601> upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds. `ts=` is what lets a run of these be read as an outage with a start, an end and a duration, and cross-read against `journalctl --user -u <brand>-code.service`; `path=` carries the endpoint only, never the query string, because this line fires on paths whose query can hold a download signature, an OAuth code, or a customer phone number. Boot-time confirmation that the logo resolved: `[edge] brand=<name> holding-logo=inlined assets-dir=<path>` — `holding-logo=missing` means the logo file wasn't found at `assets-dir`, the page degrades to text-only.
5455
5455
 
5456
5456
  **Branded plain-text 502 ("Bad Gateway (SiteDesk unavailable)") — real upstream failure, not cold-start.** Any error class other than `ECONNREFUSED` (timeouts, resets, host-unreachable) returns the existing 502 path. The diagnostic line carries `err-class=other`. Read the log with `tail -200 ~/.maxy/logs/edge.log | rg 'err-class=other'` and check `~/.maxy/logs/server.log` for upstream stack traces — the upstream itself is the source.
5457
5457
 
@@ -9,6 +9,16 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-07-26 (0.1.511)
13
+
14
+ - Fixed a dashboard bug where the Host CPU card could sit on a loading skeleton for a full minute after its figure had already arrived, even though the sidebar was showing the same number.
15
+
16
+ ## 2026-07-26 (0.1.510)
17
+
18
+ - The admin screens load faster: page assets are now sent pre-compressed, and the sessions list reads from disk without holding up everything else on the server.
19
+ - Fixed several admin screens that could answer for the wrong account when an install has more than one, including the calendar booking link, the logs search, and the chat settings.
20
+ - When an account folder cannot be found, the error now says which condition it actually hit instead of blaming a missing file.
21
+
12
22
  ## 2026-07-26 (0.1.509)
13
23
 
14
24
  - The Agents page now names the tool that set an agent's risk level, and shows it by its short name instead of the full internal path.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: business-assistant
3
3
  description: "Business assistant for small businesses. Handles customer enquiries, appointment booking, quote formatting, invoice generation, and daily briefings. Responds instantly, triages by urgency, and never lets a message fall through the cracks."
4
+ account-owned-dirs: [{"dir": "e-sign", "description": "One folder per signed document, each holding that document's render-once base.pdf, its base.sha256, and the per-signer stamped copies. Written by the e-sign skill; the layout is skill-owned and recreated on the next document."}]
4
5
  metadata: {"platform":{"always":true,"embed":["admin"],"pluginKey":"business-assistant","optional":true,"recommended":true}}
5
6
  ---
6
7
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: cloudflare
3
3
  description: Cloudflare operations — tunnel setup/reset, DNS, Pages hosting, D1 data capture, and dashboard guidance. Zero agent-facing MCP tools; every operation is the agent invoking `cloudflared` or `wrangler` directly via Bash, calling the Cloudflare API with a reused per-scope narrow token, or quoting a dashboard click-path the operator performs themselves.
4
+ account-owned-files: [{"file": "data-portal.json", "description": "The account's data-portal folder index, written by bin/portal-index-push.mjs on every push."}]
4
5
  tools: []
5
6
  mcp-manifest: skip
6
7
  ---
@@ -68,6 +68,27 @@ describe('resolveExposedDirs', () => {
68
68
  }
69
69
  })
70
70
 
71
+ it('does not read a declared file as a plugin-owned dir', () => {
72
+ // Task 1902 — the plugin-owned region also carries file lines now. This is
73
+ // the client-facing copy of the parser, kept separately from
74
+ // platform/lib/account-schema-regions, so it needs its own case: a file must
75
+ // never enter pluginOwned, and asking to expose one must not expose it. The
76
+ // trailing slash inside the backticks is the discriminator.
77
+ const withFile = [
78
+ '<!-- plugin-owned-dirs:start -->',
79
+ '## Plugin-owned top-level entries',
80
+ '',
81
+ '- `sites/` — Owned by the cloudflare plugin.',
82
+ '- `data-portal.json` — the account data-portal index.',
83
+ '<!-- plugin-owned-dirs:end -->',
84
+ ].join('\n')
85
+ const r = resolveExposedDirs(schema({ ontology: ONTOLOGY, pluginOwned: withFile }), [
86
+ 'data-portal.json',
87
+ ])
88
+ expect(r.pluginOwned).toEqual(['sites'])
89
+ expect(r.exposed).not.toContain('data-portal.json')
90
+ })
91
+
71
92
  it('reports a plugin-owned collision and keeps the dir out of exposed', () => {
72
93
  const collide = ONTOLOGY.replace('`quotes/`', '`sites/`').replace('Quotation', 'Site')
73
94
  const r = resolveExposedDirs(schema({ ontology: collide, pluginOwned: PLUGIN_OWNED }))
@@ -283,7 +283,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
283
283
 
284
284
  `maxy-edge.service` (always-on front door) classifies upstream errors and serves a brand-aware response. There are two distinct user-visible shapes; the right one depends on what failed.
285
285
 
286
- **Branded holding page (brand logo + "Starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. The page renders the brand logo (inlined as a base64 data URI at edge boot from `<install>/server/public/brand/<assets.logo>`) and the brand display/body fonts (loaded from fonts.googleapis.com) — both paths bypass the unavailable upstream so the page never makes a same-origin asset fetch. When `brand.logoContainsName` is true the logo replaces the productName text; otherwise the page falls back to "{{productName}} is starting". No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds. Boot-time confirmation that the logo resolved: `[edge] brand=<name> holding-logo=inlined assets-dir=<path>` — `holding-logo=missing` means the logo file wasn't found at `assets-dir`, the page degrades to text-only.
286
+ **Branded holding page (brand logo + "Starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. The page renders the brand logo (inlined as a base64 data URI at edge boot from `<install>/server/public/brand/<assets.logo>`) and the brand display/body fonts (loaded from fonts.googleapis.com) — both paths bypass the unavailable upstream so the page never makes a same-origin asset fetch. When `brand.logoContainsName` is true the logo replaces the productName text; otherwise the page falls back to "{{productName}} is starting". No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] ts=<ISO-8601> upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds. `ts=` is what lets a run of these be read as an outage with a start, an end and a duration, and cross-read against `journalctl --user -u <brand>-code.service`; `path=` carries the endpoint only, never the query string, because this line fires on paths whose query can hold a download signature, an OAuth code, or a customer phone number. Boot-time confirmation that the logo resolved: `[edge] brand=<name> holding-logo=inlined assets-dir=<path>` — `holding-logo=missing` means the logo file wasn't found at `assets-dir`, the page degrades to text-only.
287
287
 
288
288
  **Branded plain-text 502 ("Bad Gateway ({{productName}} unavailable)") — real upstream failure, not cold-start.** Any error class other than `ECONNREFUSED` (timeouts, resets, host-unreachable) returns the existing 502 path. The diagnostic line carries `err-class=other`. Read the log with `tail -200 ~/.maxy/logs/edge.log | rg 'err-class=other'` and check `~/.maxy/logs/server.log` for upstream stack traces — the upstream itself is the source.
289
289
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: scheduling
3
3
  description: "Calendar and scheduling — create events, manage appointments, set up recurring triggers."
4
+ account-owned-files: [{"file": "calendar-availability.json", "description": "The booking page's availability config, written by the publish-availability script and read by the live booking page."}]
4
5
  tools:
5
6
  - name: schedule-event
6
7
  publicAllowlist: false
@@ -10,8 +10,10 @@ REAL_PROJECT_DIR="$(cd "$PLAT_SCRIPTS/.." && pwd)"
10
10
  REAL_TEMPLATE="$REAL_PROJECT_DIR/templates/account-schema/SCHEMA.md"
11
11
  REAL_PREMIUM="$(cd "$REAL_PROJECT_DIR/.." && pwd)/premium-plugins"
12
12
  PASS=0; FAIL=0; FAILED=()
13
- assert_grep() { if printf '%s' "$2" | grep -qF "$1"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected to find [$1]"); fi; }
14
- assert_nogrep() { if printf '%s' "$2" | grep -qF "$1"; then FAIL=$((FAIL+1)); FAILED+=("$3: expected NOT to find [$1]"); else PASS=$((PASS+1)); fi; }
13
+ # `--` so a pattern that starts with a hyphen (a region line: "- `x.json` ")
14
+ # is read as the pattern, not as a grep option.
15
+ assert_grep() { if printf '%s' "$2" | grep -qF -- "$1"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected to find [$1]"); fi; }
16
+ assert_nogrep() { if printf '%s' "$2" | grep -qF -- "$1"; then FAIL=$((FAIL+1)); FAILED+=("$3: expected NOT to find [$1]"); else PASS=$((PASS+1)); fi; }
15
17
  assert_eq() { if [ "$1" = "$2" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected [$2] got [$1]"); fi; }
16
18
  assert_dir() { if [ -d "$1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2: expected directory [$1]"); fi; }
17
19
  assert_nodir() { if [ -d "$1" ]; then FAIL=$((FAIL+1)); FAILED+=("$2: expected NO directory [$1]"); else PASS=$((PASS+1)); fi; }
@@ -57,7 +59,7 @@ assert_grep "op=seed-owned-dirs" "$OUT" "sd-seed-log"
57
59
  assert_grep "added=quoting" "$OUT" "sd-added-quoting"
58
60
  ALLOWED="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_SD/SCHEMA.md")"
59
61
  assert_grep "quoting" "$ALLOWED" "sd-allowlist-has-quoting"
60
- assert_grep "Plugin-owned top-level workspaces" "$(cat "$A_SD/SCHEMA.md")" "sd-descriptive-section"
62
+ assert_grep "Plugin-owned top-level entries" "$(cat "$A_SD/SCHEMA.md")" "sd-descriptive-section"
61
63
  assert_grep "plugin-owned-dirs:start" "$(cat "$A_SD/SCHEMA.md")" "sd-marker"
62
64
 
63
65
  # --- idempotency: second merge is byte-identical ---
@@ -73,7 +75,7 @@ export PROJECT_DIR="$T_MX/platform"
73
75
  merge_owned_dirs_into_schema "$A_MX" >/dev/null
74
76
  ALLOWED_MX="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_MX/SCHEMA.md")"
75
77
  assert_nogrep "quoting" "$ALLOWED_MX" "mx-no-quoting"
76
- assert_nogrep "Plugin-owned top-level workspaces" "$(cat "$A_MX/SCHEMA.md")" "mx-no-section"
78
+ assert_nogrep "Plugin-owned top-level entries" "$(cat "$A_MX/SCHEMA.md")" "mx-no-section"
77
79
 
78
80
  # --- platform plugins declare pages (storage-broker) + archive (memory) ---
79
81
  # Task 1891: these two plugin-owned root dirs must land in the allowed block so
@@ -287,5 +289,111 @@ B_P="$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')"
287
289
  merge_owned_dirs_into_schema "$A_P" >/dev/null
288
290
  assert_eq "$(shasum "$A_P/SCHEMA.md" | awk '{print $1}')" "$B_P" "prune-idempotent"
289
291
 
292
+ # --- account-owned-files: a declared file joins the same allowed block ---
293
+ # The block is a flat list of names and already carries SCHEMA.md and
294
+ # account.json, so a file needs no second surface. A second plugin declares a
295
+ # malformed value: its key is skipped and the valid declaration still merges.
296
+ TF="$(mktemp -d)"
297
+ mkdir -p "$TF/platform/plugins/filer" "$TF/platform/plugins/broken" \
298
+ "$TF/platform/config" "$TF/platform/templates/account-schema"
299
+ cp "$REAL_TEMPLATE" "$TF/platform/templates/account-schema/SCHEMA.md"
300
+ printf '{"installDir":"files-brand","shipsPremiumBundles":[]}\n' > "$TF/platform/config/brand.json"
301
+ printf -- '---\nname: filer\naccount-owned-files: [{"file": "x.json", "description": "Filer index."}]\n---\n' \
302
+ > "$TF/platform/plugins/filer/PLUGIN.md"
303
+ printf -- '---\nname: broken\naccount-owned-files: [{"file": "y.json"\n---\n' \
304
+ > "$TF/platform/plugins/broken/PLUGIN.md"
305
+ A_TF="$(mktemp -d)"; cp "$TF/platform/templates/account-schema/SCHEMA.md" "$A_TF/SCHEMA.md"
306
+ export PROJECT_DIR="$TF/platform"
307
+ OUT_TF="$(merge_owned_dirs_into_schema "$A_TF")"
308
+ ALLOWED_TF="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_TF/SCHEMA.md")"
309
+ assert_grep "x.json" "$ALLOWED_TF" "files-allowlist-has-x"
310
+ assert_nogrep "y.json" "$ALLOWED_TF" "files-malformed-skipped"
311
+ assert_grep "added=x.json" "$OUT_TF" "files-added-logged"
312
+ # A file renders without the trailing slash a dir gets; three parsers read that
313
+ # slash to tell a bucket from anything else.
314
+ assert_grep '- `x.json` — Filer index.' "$(cat "$A_TF/SCHEMA.md")" "files-region-no-slash"
315
+ assert_nogrep '- `x.json/`' "$(cat "$A_TF/SCHEMA.md")" "files-region-not-a-dir"
316
+ RC_TF="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_TF")"
317
+ assert_grep "ownedFile=x.json present=true" "$RC_TF" "files-reconcile-line"
318
+ B_TF="$(shasum "$A_TF/SCHEMA.md" | awk '{print $1}')"
319
+ merge_owned_dirs_into_schema "$A_TF" >/dev/null
320
+ assert_eq "$(shasum "$A_TF/SCHEMA.md" | awk '{print $1}')" "$B_TF" "files-idempotent"
321
+
322
+ # A dir declaration keeps its slash and its ownedDir= reconcile line.
323
+ TD="$(mktemp -d)"
324
+ mkdir -p "$TD/platform/plugins/dirred" "$TD/platform/config" "$TD/platform/templates/account-schema"
325
+ cp "$REAL_TEMPLATE" "$TD/platform/templates/account-schema/SCHEMA.md"
326
+ printf '{"installDir":"dirs-brand","shipsPremiumBundles":[]}\n' > "$TD/platform/config/brand.json"
327
+ printf -- '---\nname: dirred\naccount-owned-dirs: [{"dir": "widgets", "description": "Widget workspace."}]\n---\n' \
328
+ > "$TD/platform/plugins/dirred/PLUGIN.md"
329
+ A_TD="$(mktemp -d)"; cp "$TD/platform/templates/account-schema/SCHEMA.md" "$A_TD/SCHEMA.md"
330
+ export PROJECT_DIR="$TD/platform"
331
+ merge_owned_dirs_into_schema "$A_TD" >/dev/null
332
+ assert_grep '- `widgets/` — Widget workspace.' "$(cat "$A_TD/SCHEMA.md")" "dirs-region-keeps-slash"
333
+ RC_TD="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_TD")"
334
+ assert_grep "ownedDir=widgets present=true" "$RC_TD" "dirs-reconcile-line"
335
+
336
+ # --- a malformed ELEMENT skips its whole key, not just that element ---
337
+ # json.loads succeeds and the first entry is well-formed, so an append-as-you-go
338
+ # parse would merge a.json and silently drop c.json. A half-merged declaration is
339
+ # worse than none: the dropped name reports as a stray forever with no line
340
+ # anywhere saying its declaration failed.
341
+ TP="$(mktemp -d)"
342
+ mkdir -p "$TP/platform/plugins/partial" "$TP/platform/plugins/good" \
343
+ "$TP/platform/config" "$TP/platform/templates/account-schema"
344
+ cp "$REAL_TEMPLATE" "$TP/platform/templates/account-schema/SCHEMA.md"
345
+ printf '{"installDir":"partial-brand","shipsPremiumBundles":[]}\n' > "$TP/platform/config/brand.json"
346
+ printf -- '---\nname: partial\naccount-owned-files: [{"file": "a.json"}, "b.json", {"file": "c.json"}]\n---\n' \
347
+ > "$TP/platform/plugins/partial/PLUGIN.md"
348
+ printf -- '---\nname: good\naccount-owned-files: [{"file": "d.json", "description": "Good."}]\n---\n' \
349
+ > "$TP/platform/plugins/good/PLUGIN.md"
350
+ A_TP="$(mktemp -d)"; cp "$TP/platform/templates/account-schema/SCHEMA.md" "$A_TP/SCHEMA.md"
351
+ export PROJECT_DIR="$TP/platform"
352
+ merge_owned_dirs_into_schema "$A_TP" >/dev/null
353
+ ALLOWED_TP="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_TP/SCHEMA.md")"
354
+ assert_nogrep "a.json" "$ALLOWED_TP" "partial-first-entry-skipped"
355
+ assert_nogrep "c.json" "$ALLOWED_TP" "partial-later-entry-skipped"
356
+ assert_grep "d.json" "$ALLOWED_TP" "partial-sibling-plugin-still-merges"
357
+
358
+ # --- a name carrying a path separator is refused ---
359
+ # The trailing slash inside the backticks is what tells a bucket from a file in
360
+ # the descriptive region, and the fence entry must match a readdir name. A
361
+ # declared "foo/" would render as a bucket line and never match anything.
362
+ TS_="$(mktemp -d)"
363
+ mkdir -p "$TS_/platform/plugins/slashed" "$TS_/platform/config" "$TS_/platform/templates/account-schema"
364
+ cp "$REAL_TEMPLATE" "$TS_/platform/templates/account-schema/SCHEMA.md"
365
+ printf '{"installDir":"slash-brand","shipsPremiumBundles":[]}\n' > "$TS_/platform/config/brand.json"
366
+ printf -- '---\nname: slashed\naccount-owned-files: [{"file": "foo/", "description": "Bad."}]\naccount-owned-dirs: [{"dir": "bar/baz", "description": "Bad."}]\n---\n' \
367
+ > "$TS_/platform/plugins/slashed/PLUGIN.md"
368
+ A_TS="$(mktemp -d)"; cp "$TS_/platform/templates/account-schema/SCHEMA.md" "$A_TS/SCHEMA.md"
369
+ export PROJECT_DIR="$TS_/platform"
370
+ merge_owned_dirs_into_schema "$A_TS" >/dev/null
371
+ assert_nogrep "foo/" "$(cat "$A_TS/SCHEMA.md")" "slash-file-refused"
372
+ assert_nogrep "bar/baz" "$(cat "$A_TS/SCHEMA.md")" "slash-dir-refused"
373
+
374
+ # --- shipped declarations reach an account's allowed block ---
375
+ # Symlink the real plugin dirs so the case exercises the shipped PLUGIN.md
376
+ # frontmatter, the same way the Task 1891 pages/archive case does.
377
+ T_SHIP="$(make_tree '[]')"
378
+ for _p in business-assistant cloudflare scheduling admin; do
379
+ ln -s "$REAL_PROJECT_DIR/plugins/$_p" "$T_SHIP/platform/plugins/$_p"
380
+ done
381
+ A_SHIP="$(make_account "$T_SHIP")"
382
+ export PROJECT_DIR="$T_SHIP/platform"
383
+ merge_owned_dirs_into_schema "$A_SHIP" >/dev/null
384
+ ALLOWED_SHIP="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_SHIP/SCHEMA.md")"
385
+ # The four channel/session files are written by the session manager and the UI
386
+ # server on every brand, so they are declared on admin, which no brand excludes.
387
+ # realagent-code and property-administrators exclude the telegram plugin from
388
+ # their payload (brands/*/brand.json) while still constructing the telegram store
389
+ # (services/claude-session-manager/src/index.ts), so a declaration on the telegram
390
+ # plugin would leave those two brands reporting a permanent false stray. This
391
+ # tree deliberately omits whatsapp and telegram to prove the reach.
392
+ for _n in e-sign data-portal.json calendar-availability.json wa-channel-bindings.json \
393
+ telegram-channel-bindings.json webchat-channel-bindings.json \
394
+ canonical-webchat-session.json session-titles.json agents-disabled.json; do
395
+ assert_grep "$_n" "$ALLOWED_SHIP" "ship-allow-$_n"
396
+ done
397
+
290
398
  echo "PASS=$PASS FAIL=$FAIL"
291
399
  if [ "$FAIL" -ne 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
@@ -1,16 +1,23 @@
1
1
  #!/usr/bin/env python3
2
- """Resolve plugin-declared account-owned top-level dirs for the brand and merge
3
- them into an account's SCHEMA.md.
2
+ """Resolve plugin-declared account-owned top-level entries for the brand and
3
+ merge them into an account's SCHEMA.md.
4
4
 
5
- A plugin declares owned top-level account dirs in its PLUGIN.md frontmatter:
5
+ A plugin declares owned top-level account entries in its PLUGIN.md frontmatter,
6
+ by kind:
6
7
 
7
- account-owned-dirs: [{"dir": "quoting", "description": "..."}]
8
+ account-owned-dirs: [{"dir": "quoting", "description": "..."}]
9
+ account-owned-files: [{"file": "data-portal.json", "description": "..."}]
8
10
 
9
- The owned-dir set for a brand is the union declared by every platform plugin
11
+ The owned-entry set for a brand is the union declared by every platform plugin
10
12
  ($PROJECT_DIR/plugins/*/PLUGIN.md) and every premium sub-plugin of a bundle the
11
13
  brand ships ($PREMIUM_ROOT/<bundle>/plugins/*/PLUGIN.md, gated by brand.json
12
14
  shipsPremiumBundles). Reads PROJECT_DIR from the environment.
13
15
 
16
+ A declaration reaches an account only on a brand whose payload carries the
17
+ declaring plugin (brand.json plugins.excluded drops the directory entirely), so
18
+ an entry written by platform code that runs on every brand belongs on a plugin
19
+ no brand excludes, not on the plugin its feature is named after.
20
+
14
21
  The account SCHEMA.md also gains one domain-entity bucket per top-level
15
22
  operator-entry node type of the brand's business ontology. That set is derived
16
23
  from the brand vertical's schema reference (brand.json#vertical ->
@@ -102,7 +109,17 @@ def _read_vertical(project_dir):
102
109
 
103
110
 
104
111
  def _parse_plugin_md(path):
105
- """Return (name, [ {dir, description} ]) from a PLUGIN.md, or (None, [])."""
112
+ """Return (name, [ {name, kind, description} ]) from a PLUGIN.md, or (None, []).
113
+ Two frontmatter keys, one list: `account-owned-dirs` yields kind='dir',
114
+ `account-owned-files` yields kind='file'. A key whose value is not a JSON
115
+ array of objects is skipped WHOLE: entries are staged and committed only once
116
+ every element parses, so a bad element cannot half-merge a declaration and
117
+ leave the rest reporting as strays. The other key and every other plugin
118
+ still merge, because one hand-edited manifest must never empty the set. A
119
+ name carrying a path separator is dropped: a fence entry has to match one
120
+ readdir name, and the region tells a bucket from a file by the trailing slash
121
+ it adds itself, so a declared `foo/` would render as a bucket that matches
122
+ nothing."""
106
123
  try:
107
124
  with open(path) as f:
108
125
  text = f.read()
@@ -117,16 +134,21 @@ def _parse_plugin_md(path):
117
134
  if nm:
118
135
  name = nm.group(1).strip().strip('"').strip("'")
119
136
  owned = []
120
- om = re.search(r"^account-owned-dirs:\s*(.+)$", fm, re.MULTILINE)
121
- if om:
137
+ for key, field, kind in (("account-owned-dirs", "dir", "dir"),
138
+ ("account-owned-files", "file", "file")):
139
+ om = re.search(r"^" + key + r":\s*(.+)$", fm, re.MULTILINE)
140
+ if not om:
141
+ continue
142
+ staged = []
122
143
  try:
123
- raw = json.loads(om.group(1).strip())
124
- for e in raw:
125
- d = (e.get("dir") or "").strip()
126
- if d:
127
- owned.append({"dir": d, "description": (e.get("description") or "").strip()})
144
+ for e in json.loads(om.group(1).strip()):
145
+ n = (e.get(field) or "").strip()
146
+ if n and "/" not in n:
147
+ staged.append({"name": n, "kind": kind,
148
+ "description": (e.get("description") or "").strip()})
128
149
  except Exception:
129
- pass
150
+ continue
151
+ owned.extend(staged)
130
152
  return name, owned
131
153
 
132
154
 
@@ -158,18 +180,19 @@ def _plugin_md_paths(project_dir):
158
180
 
159
181
 
160
182
  def resolve(project_dir):
161
- """Union of owned-dir declarations. First declarer of a dir wins its owner
162
- label + description."""
183
+ """Union of owned-entry declarations. First declarer of a name wins its
184
+ owner label + description, whichever kind it declared."""
163
185
  seen = {}
164
186
  order = []
165
187
  for path in _plugin_md_paths(project_dir):
166
188
  name, owned = _parse_plugin_md(path)
167
189
  for e in owned:
168
- if e["dir"] in seen:
190
+ if e["name"] in seen:
169
191
  continue
170
- seen[e["dir"]] = {"dir": e["dir"], "description": e["description"], "plugin": name or "unknown"}
171
- order.append(e["dir"])
172
- return [seen[d] for d in order]
192
+ seen[e["name"]] = {"name": e["name"], "kind": e["kind"],
193
+ "description": e["description"], "plugin": name or "unknown"}
194
+ order.append(e["name"])
195
+ return [seen[n] for n in order]
173
196
 
174
197
 
175
198
  def _top_level_labels(schema_path):
@@ -304,7 +327,7 @@ def merge(project_dir, account_dir):
304
327
  # entity (non-blank Owner) nests under its owner and is not a target,
305
328
  # so a pre-fix account's empty owned bucket prunes while a populated one
306
329
  # is kept-and-logged like any other stale bucket.
307
- target = FIXED_SEED | {e["dir"] for e in owned} | {b["dir"] for b in domain if _is_root(b["owner"])}
330
+ target = FIXED_SEED | {e["name"] for e in owned} | {b["dir"] for b in domain if _is_root(b["owner"])}
308
331
  # Reconcile existing entries: keep in-target; keep populated strays
309
332
  # (logged); drop empty/absent strays (logged).
310
333
  new_entries = []
@@ -319,9 +342,9 @@ def merge(project_dir, account_dir):
319
342
  # Append newly-projected owned then domain dirs not already present.
320
343
  insert = []
321
344
  for e in owned:
322
- if e["dir"] not in present and e["dir"] not in insert:
323
- insert.append(e["dir"])
324
- added.append(e["dir"])
345
+ if e["name"] not in present and e["name"] not in insert:
346
+ insert.append(e["name"])
347
+ added.append(e["name"])
325
348
  for b in domain:
326
349
  if not _is_root(b["owner"]):
327
350
  continue # contained entity: no root bucket, nests under its owner
@@ -336,16 +359,21 @@ def merge(project_dir, account_dir):
336
359
  text = _strip_region(text, MARK_START, MARK_END)
337
360
  text = _strip_region(text, ONT_START, ONT_END)
338
361
  if owned:
339
- region = [MARK_START, "## Plugin-owned top-level workspaces", "",
340
- "A plugin that owns a top-level workspace declares it. The internal",
341
- "structure is owned by that plugin's tools and recreated on the next",
342
- "write. Never reorganise it.", ""]
362
+ region = [MARK_START, "## Plugin-owned top-level entries", "",
363
+ "A plugin declares the account-root entries its feature owns. A",
364
+ "directory's internal structure belongs to that plugin's tools and is",
365
+ "recreated on the next write, so never reorganise it; a declared file",
366
+ "is written whole by its owner. Neither is operator data.", ""]
343
367
  for e in owned:
344
368
  desc = e["description"] or f"Owned by the {e['plugin']} plugin."
345
- region.append(f"- `{e['dir']}/` {desc}")
369
+ # The trailing slash marks a bucket. Three readers anchor on it to
370
+ # tell a directory from a declared file (account-schema-regions,
371
+ # schema-exposed-dirs.mjs), so a file must never gain one.
372
+ head = f"{e['name']}/" if e["kind"] == "dir" else e["name"]
373
+ region.append(f"- `{head}` — {desc}")
346
374
  region.append(MARK_END)
347
375
  text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
348
- owned_dirs = {e["dir"] for e in owned}
376
+ owned_dirs = {e["name"] for e in owned if e["kind"] == "dir"}
349
377
  # A dir already claimed by a plugin (described in the plugin-owned region)
350
378
  # is not also described here, so a name collision cannot double-describe.
351
379
  described = [b for b in domain if b["dir"] not in owned_dirs]
@@ -387,8 +415,9 @@ def reconcile(project_dir, account_dir):
387
415
  entries, _s, _e = _allowed_block(f.read())
388
416
  present = set(entries or [])
389
417
  for e in owned:
390
- is_present = "true" if e["dir"] in present else "false"
391
- print(f"brand={brand} plugin={e['plugin']} ownedDir={e['dir']} present={is_present}")
418
+ is_present = "true" if e["name"] in present else "false"
419
+ label = "ownedDir" if e["kind"] == "dir" else "ownedFile"
420
+ print(f"brand={brand} plugin={e['plugin']} {label}={e['name']} present={is_present}")
392
421
  vertical = _read_vertical(project_dir)
393
422
  domain = resolve_domain_buckets(project_dir)
394
423
  if vertical and not domain:
@@ -1 +1 @@
1
- {"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD;;;;;0CAKsC;IACtC,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,eAAe,EAClB,WAAW,SAAI,EACf,aAAa,SAAI,EACjB,UAAU,SAAI,GACb,MAAM,CAGR;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA+ED;;;8EAG8E;AAC9E,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAoCpE;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AA0DD;;;;;;;;;;;;;iCAaiC;AACjC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CA8DtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
1
+ {"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA8EA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD;;;;;0CAKsC;IACtC,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,eAAe,EAClB,WAAW,SAAI,EACf,aAAa,SAAI,EACjB,UAAU,SAAI,GACb,MAAM,CAGR;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA+ED;;;8EAG8E;AAC9E,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAoCpE;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AA0DD;;;;;;;;;;;;;iCAaiC;AACjC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CA8DtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}