@rubytech/create-maxy-code 0.1.299 → 0.1.301

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 (213) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/PLUGIN.md +10 -10
  3. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +84 -84
  4. package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +4 -4
  5. package/payload/platform/plugins/aeo/PLUGIN.md +1 -1
  6. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +4 -4
  7. package/payload/platform/plugins/cloudflare/references/manual-setup.md +43 -22
  8. package/payload/platform/plugins/cloudflare/references/reset-guide.md +19 -1
  9. package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +19 -3
  10. package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
  11. package/payload/platform/plugins/docs/references/admin-identity-gate.md +6 -6
  12. package/payload/platform/plugins/docs/references/admin-session.md +15 -15
  13. package/payload/platform/plugins/docs/references/admin-ui.md +18 -18
  14. package/payload/platform/plugins/docs/references/aeo.md +1 -1
  15. package/payload/platform/plugins/docs/references/deployment.md +3 -3
  16. package/payload/platform/plugins/docs/references/internals.md +28 -28
  17. package/payload/platform/plugins/docs/references/platform.md +21 -21
  18. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -3
  19. package/payload/platform/plugins/docs/references/troubleshooting.md +6 -6
  20. package/payload/platform/plugins/docs/references/visitor-graph.md +3 -3
  21. package/payload/platform/plugins/email/PLUGIN.md +2 -2
  22. package/payload/platform/plugins/email/references/email-reference.md +2 -2
  23. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -2
  24. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  25. package/payload/platform/plugins/memory/PLUGIN.md +9 -9
  26. package/payload/platform/plugins/memory/references/graph-primitives.md +2 -2
  27. package/payload/platform/plugins/memory/references/schema-base.md +14 -14
  28. package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +1 -1
  29. package/payload/platform/plugins/memory/references/transcript-formats/otter.md +1 -1
  30. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +6 -6
  31. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +1 -1
  32. package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +8 -8
  33. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +5 -5
  34. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/daily-notes.md +1 -1
  35. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -1
  36. package/payload/platform/plugins/slides/PROVENANCE.md +1 -1
  37. package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +1 -1
  38. package/payload/platform/plugins/telegram/PLUGIN.md +1 -1
  39. package/payload/platform/plugins/url-get/PLUGIN.md +2 -2
  40. package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
  41. package/payload/platform/plugins/whatsapp/PLUGIN.md +5 -5
  42. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +5 -5
  43. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +1 -1
  44. package/payload/platform/plugins/workflows/PLUGIN.md +1 -1
  45. package/payload/platform/plugins/x-import/PLUGIN.md +2 -2
  46. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  47. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +125 -0
  48. package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +253 -0
  49. package/payload/platform/scripts/check-no-esm-require.mjs +4 -0
  50. package/payload/platform/scripts/check-no-task-id-leaks.mjs +54 -6
  51. package/payload/platform/scripts/resume-tunnel.sh +89 -5
  52. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +14 -0
  53. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  54. package/payload/platform/services/whatsapp-channel/dist/notification.js +11 -0
  55. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  56. package/payload/platform/services/whatsapp-channel/dist/server.d.ts +4 -0
  57. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  58. package/payload/platform/services/whatsapp-channel/dist/server.js +54 -3
  59. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  60. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +18 -0
  61. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -0
  62. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +137 -0
  63. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -0
  64. package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
  65. package/payload/platform/templates/specialists/agents/librarian.md +2 -2
  66. package/payload/server/{chunk-7H4KMTMC.js → chunk-JHSF5VPE.js} +171 -95
  67. package/payload/server/maxy-edge.js +19 -1
  68. package/payload/server/public/assets/AdminShell-BJrxifYa.js +1 -0
  69. package/payload/server/public/assets/{Checkbox-DHnspkuk.js → Checkbox-C7bBRzLP.js} +1 -1
  70. package/payload/server/public/assets/{Transcript-9FlneAzM.js → Transcript-bGy_Gx99.js} +1 -1
  71. package/payload/server/public/assets/admin-jYKA7jR5.js +1 -0
  72. package/payload/server/public/assets/{arc-CPOhuw1V.js → arc-DxSm8tli.js} +1 -1
  73. package/payload/server/public/assets/architecture-YZFGNWBL-6g9jRVgc.js +1 -0
  74. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BaR37f-j.js → architectureDiagram-Q4EWVU46-7JSAh0vL.js} +1 -1
  75. package/payload/server/public/assets/audio-attachment-mime-DL5R1M2A.js +30 -0
  76. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Du7NMQfQ.js → blockDiagram-DXYQGD6D-DLoVmHKD.js} +1 -1
  77. package/payload/server/public/assets/{browser--bCCzvRh.js → browser-7fyPCFWB.js} +1 -1
  78. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-F3GnFLbs.js → c4Diagram-AHTNJAMY-Nv1G0o9P.js} +1 -1
  79. package/payload/server/public/assets/channel-BgQLJanG.js +1 -0
  80. package/payload/server/public/assets/chat-UF9ATDwe.js +1 -0
  81. package/payload/server/public/assets/{chunk-2KRD3SAO-SUv9UlAA.js → chunk-2KRD3SAO-BpqPMhoA.js} +1 -1
  82. package/payload/server/public/assets/chunk-336JU56O-DgXHlVU_.js +2 -0
  83. package/payload/server/public/assets/chunk-426QAEUC-Dr_XVuUq.js +1 -0
  84. package/payload/server/public/assets/{chunk-4BX2VUAB-D5iKMyHO.js → chunk-4BX2VUAB-nwyZSZH8.js} +1 -1
  85. package/payload/server/public/assets/{chunk-4TB4RGXK-BahbQ7-Z.js → chunk-4TB4RGXK-Tfxz9LHX.js} +1 -1
  86. package/payload/server/public/assets/{chunk-55IACEB6-DfF9myIx.js → chunk-55IACEB6-6FMJQGXG.js} +1 -1
  87. package/payload/server/public/assets/{chunk-5FUZZQ4R-jF6a9UVe.js → chunk-5FUZZQ4R-g2FgZF3D.js} +1 -1
  88. package/payload/server/public/assets/{chunk-5PVQY5BW-BOCI-roy.js → chunk-5PVQY5BW-DtLwXsPH.js} +1 -1
  89. package/payload/server/public/assets/{chunk-67CJDMHE-E5wGK0dP.js → chunk-67CJDMHE-K91CP5ZU.js} +1 -1
  90. package/payload/server/public/assets/{chunk-7N4EOEYR-CtT0GsUT.js → chunk-7N4EOEYR-BGpCQhqK.js} +1 -1
  91. package/payload/server/public/assets/{chunk-AA7GKIK3-CGUj8eo-.js → chunk-AA7GKIK3-jkDbInck.js} +1 -1
  92. package/payload/server/public/assets/{chunk-BSJP7CBP-OPAIjvSk.js → chunk-BSJP7CBP-mDosdzGs.js} +1 -1
  93. package/payload/server/public/assets/{chunk-CIAEETIT-S_D8FhIT.js → chunk-CIAEETIT-DhBxewpQ.js} +1 -1
  94. package/payload/server/public/assets/{chunk-EDXVE4YY-CkD4AyZi.js → chunk-EDXVE4YY-B7c-9Emg.js} +1 -1
  95. package/payload/server/public/assets/{chunk-ENJZ2VHE-CmoK-c5f.js → chunk-ENJZ2VHE-Cqhhncox.js} +1 -1
  96. package/payload/server/public/assets/{chunk-FMBD7UC4-DcyogdNf.js → chunk-FMBD7UC4-BpDq6wj1.js} +1 -1
  97. package/payload/server/public/assets/{chunk-FOC6F5B3-8iNobZdv.js → chunk-FOC6F5B3-Ds02-ktu.js} +1 -1
  98. package/payload/server/public/assets/{chunk-ICPOFSXX-DeGZE9pw.js → chunk-ICPOFSXX-BTX5POFr.js} +2 -2
  99. package/payload/server/public/assets/{chunk-K5T4RW27-CkoKFdgU.js → chunk-K5T4RW27-Du1PXXBU.js} +1 -1
  100. package/payload/server/public/assets/{chunk-KGLVRYIC-DyNP1efP.js → chunk-KGLVRYIC-siasOAf8.js} +1 -1
  101. package/payload/server/public/assets/{chunk-LIHQZDEY-WZPpo5Dd.js → chunk-LIHQZDEY-xf1rbZtf.js} +1 -1
  102. package/payload/server/public/assets/{chunk-ORNJ4GCN-Cr7E0-Df.js → chunk-ORNJ4GCN-DeTLQ_LD.js} +1 -1
  103. package/payload/server/public/assets/{chunk-OYMX7WX6-BPjxo1Oc.js → chunk-OYMX7WX6-DQ7_oVJn.js} +1 -1
  104. package/payload/server/public/assets/chunk-QZHKN3VN-DF4o9HRJ.js +1 -0
  105. package/payload/server/public/assets/{chunk-U2HBQHQK-CuDS7yVX.js → chunk-U2HBQHQK-CXmFUKgn.js} +1 -1
  106. package/payload/server/public/assets/{chunk-X2U36JSP-DEkde3xe.js → chunk-X2U36JSP-Bj8-8Wkz.js} +1 -1
  107. package/payload/server/public/assets/{chunk-XPW4576I-BwRCuKyu.js → chunk-XPW4576I-Con3TXqt.js} +1 -1
  108. package/payload/server/public/assets/{chunk-YZCP3GAM-DD9nUp-w.js → chunk-YZCP3GAM-Dx8BHGWf.js} +1 -1
  109. package/payload/server/public/assets/{chunk-ZZ45TVLE-BDH9b11G.js → chunk-ZZ45TVLE-Dp4Q5Y-f.js} +1 -1
  110. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DAbPKU6u.js +1 -0
  111. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BKuFQLcv.js +1 -0
  112. package/payload/server/public/assets/clone-DnNHGhNe.js +1 -0
  113. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-CfeophtW.js → cose-bilkent-S5V4N54A-pZiCv69E.js} +1 -1
  114. package/payload/server/public/assets/{dagre-CVxEmku2.js → dagre-CLqhEgbL.js} +1 -1
  115. package/payload/server/public/assets/{dagre-KV5264BT-TOuVLdjq.js → dagre-KV5264BT-BCxE8iyk.js} +1 -1
  116. package/payload/server/public/assets/{data-B8Waqy4Q.js → data-CIJnkfgX.js} +1 -1
  117. package/payload/server/public/assets/{diagram-5BDNPKRD-BJQnfJjx.js → diagram-5BDNPKRD-slUEdZzz.js} +1 -1
  118. package/payload/server/public/assets/{diagram-G4DWMVQ6-CqK94J29.js → diagram-G4DWMVQ6-DPcraMfu.js} +1 -1
  119. package/payload/server/public/assets/{diagram-MMDJMWI5-BCnATv2X.js → diagram-MMDJMWI5-DR6luhGK.js} +1 -1
  120. package/payload/server/public/assets/{diagram-TYMM5635-Cl3BUrVs.js → diagram-TYMM5635-Jap1B4wA.js} +1 -1
  121. package/payload/server/public/assets/{erDiagram-SMLLAGMA--Uw0kvib.js → erDiagram-SMLLAGMA-DJKUs2hO.js} +1 -1
  122. package/payload/server/public/assets/{file-download-Pk6cNtuB.js → file-download-gth6Gpmn.js} +1 -1
  123. package/payload/server/public/assets/{flatten-DtM0XAMt.js → flatten-Cl1Bc3dR.js} +1 -1
  124. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-B_VGZIbJ.js → flowDiagram-DWJPFMVM-C8W-ZZ9W.js} +1 -1
  125. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-D-vdf1dC.js → ganttDiagram-T4ZO3ILL-DYCX4Xu2.js} +1 -1
  126. package/payload/server/public/assets/gitGraph-7Q5UKJZL-g7mvu6IY.js +1 -0
  127. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CpyAlvu3.js → gitGraphDiagram-UUTBAWPF-CZu_fpgS.js} +1 -1
  128. package/payload/server/public/assets/{graph-D-VQpkaS.js → graph-DHnB3BHK.js} +1 -1
  129. package/payload/server/public/assets/{graph-labels-cJvlyVxD.js → graph-labels-huk-fKcY.js} +1 -1
  130. package/payload/server/public/assets/{graphlib-DaV4XyBp.js → graphlib-Bdp7TA4y.js} +1 -1
  131. package/payload/server/public/assets/info-OMHHGYJF-DZqJuIkB.js +1 -0
  132. package/payload/server/public/assets/infoDiagram-42DDH7IO-cGfGccHz.js +2 -0
  133. package/payload/server/public/assets/{isEmpty-D__iH1WQ.js → isEmpty-D1pGOD1J.js} +1 -1
  134. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-_4NvfwwU.js → ishikawaDiagram-UXIWVN3A-B7KfIX6z.js} +1 -1
  135. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-PJQ-tPWv.js → journeyDiagram-VCZTEJTY-CSDwBfhF.js} +1 -1
  136. package/payload/server/public/assets/jsx-runtime-DZddXDvJ.css +1 -0
  137. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-asCNlyH4.js → kanban-definition-6JOO6SKY-CWzcEKTr.js} +1 -1
  138. package/payload/server/public/assets/{line-BSXr2IPU.js → line-BNQSlbYn.js} +1 -1
  139. package/payload/server/public/assets/{linear-CSAKvvko.js → linear-BN6kGN93.js} +1 -1
  140. package/payload/server/public/assets/mermaid-parser.core-CcFhkElq.js +4 -0
  141. package/payload/server/public/assets/mermaid.core-BsIeJ7Cc.js +11 -0
  142. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-IxwnMT2Q.js → mindmap-definition-QFDTVHPH-CQVzj6D_.js} +1 -1
  143. package/payload/server/public/assets/operator-D3QAzlPs.js +1 -0
  144. package/payload/server/public/assets/{ordinal-BRQ5CzZY.js → ordinal-CBZeH4Yp.js} +1 -1
  145. package/payload/server/public/assets/packet-4T2RLAQJ-Hn8rlHpy.js +1 -0
  146. package/payload/server/public/assets/page-86edaOS1.js +1 -0
  147. package/payload/server/public/assets/pie-ZZUOXDRM-BPGWVqBm.js +1 -0
  148. package/payload/server/public/assets/{pieDiagram-DEJITSTG-DFqSnPPB.js → pieDiagram-DEJITSTG-DnrQ9fyn.js} +1 -1
  149. package/payload/server/public/assets/preload-helper-CQ36nxok.js +1 -0
  150. package/payload/server/public/assets/public-B4LqL6lA.js +6 -0
  151. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-1AB3hlEb.js → quadrantDiagram-34T5L4WZ-BQWlCyWi.js} +1 -1
  152. package/payload/server/public/assets/radar-PYXPWWZC-DMdLrj3E.js +1 -0
  153. package/payload/server/public/assets/{reduce-CkcXOmmJ.js → reduce-PuPlFPRX.js} +1 -1
  154. package/payload/server/public/assets/{requirementDiagram-MS252O5E-C2Zpzq3l.js → requirementDiagram-MS252O5E-Du_vZhU1.js} +1 -1
  155. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DkmhPhwS.js → sankeyDiagram-XADWPNL6-Cf6Z8GPQ.js} +1 -1
  156. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-1hQLpeZ1.js → sequenceDiagram-FGHM5R23-CS8GVol8.js} +1 -1
  157. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-CBHIwm3f.js → stateDiagram-FHFEXIEX-Bv1NW2F1.js} +1 -1
  158. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BGUCxPmp.js +1 -0
  159. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-CLRWWipW.js → timeline-definition-GMOUNBTQ-ClGYAsr0.js} +1 -1
  160. package/payload/server/public/assets/treeView-SZITEDCU-CWxofUbI.js +1 -0
  161. package/payload/server/public/assets/treemap-W4RFUUIX-Cfgb_ZG0.js +1 -0
  162. package/payload/server/public/assets/{useSelectionMode-6fff2UIV.js → useSelectionMode-lrH0qwAR.js} +1 -1
  163. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-ByFoQMek.js → vennDiagram-DHZGUBPP-C2wTcxQT.js} +1 -1
  164. package/payload/server/public/assets/wardley-RL74JXVD-B45olYjj.js +1 -0
  165. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-DIkXRSha.js → wardleyDiagram-NUSXRM2D-D86-ivEx.js} +1 -1
  166. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BNdEcN73.js → xychartDiagram-5P7HB3ND-Ba5WAN8P.js} +1 -1
  167. package/payload/server/public/browser.html +6 -6
  168. package/payload/server/public/chat.html +9 -8
  169. package/payload/server/public/data.html +5 -5
  170. package/payload/server/public/graph.html +8 -8
  171. package/payload/server/public/index.html +9 -9
  172. package/payload/server/public/operator.html +9 -9
  173. package/payload/server/public/public.html +7 -6
  174. package/payload/server/server.js +164 -124
  175. package/payload/server/public/assets/AdminShell-Eo1RXZxq.js +0 -1
  176. package/payload/server/public/assets/admin-ORJGvOqo.js +0 -1
  177. package/payload/server/public/assets/architecture-YZFGNWBL-CQHotPO2.js +0 -1
  178. package/payload/server/public/assets/audio-attachment-mime-CFRERhV0.js +0 -1
  179. package/payload/server/public/assets/channel-DfO_IgSU.js +0 -1
  180. package/payload/server/public/assets/chat-DIrzZjdO.js +0 -1
  181. package/payload/server/public/assets/chunk-336JU56O-DHUrM3QG.js +0 -2
  182. package/payload/server/public/assets/chunk-426QAEUC-CBo-Lx7X.js +0 -1
  183. package/payload/server/public/assets/chunk-QZHKN3VN-CNlL2tiW.js +0 -1
  184. package/payload/server/public/assets/classDiagram-6PBFFD2Q-Bo-iLGpk.js +0 -1
  185. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Bz7DNpvQ.js +0 -1
  186. package/payload/server/public/assets/clone-Bh_bM_44.js +0 -1
  187. package/payload/server/public/assets/gitGraph-7Q5UKJZL-Ds2hoRX4.js +0 -1
  188. package/payload/server/public/assets/info-OMHHGYJF-BuX4gDTt.js +0 -1
  189. package/payload/server/public/assets/infoDiagram-42DDH7IO-pcFw8-8F.js +0 -2
  190. package/payload/server/public/assets/jsx-runtime-BjFkEGXb.css +0 -1
  191. package/payload/server/public/assets/mermaid-parser.core-DFxv-h16.js +0 -4
  192. package/payload/server/public/assets/mermaid.core-Be34BFtA.js +0 -11
  193. package/payload/server/public/assets/operator-DyLRFmNE.js +0 -1
  194. package/payload/server/public/assets/packet-4T2RLAQJ-9qQ2zAJ3.js +0 -1
  195. package/payload/server/public/assets/page-CFgywgGE.js +0 -1
  196. package/payload/server/public/assets/pie-ZZUOXDRM-aDbmozld.js +0 -1
  197. package/payload/server/public/assets/public-_pD6yEQ6.js +0 -35
  198. package/payload/server/public/assets/radar-PYXPWWZC-CEm-iLey.js +0 -1
  199. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DHGLsXgi.js +0 -1
  200. package/payload/server/public/assets/treeView-SZITEDCU-DYa2gPJa.js +0 -1
  201. package/payload/server/public/assets/treemap-W4RFUUIX-D816ED1y.js +0 -1
  202. package/payload/server/public/assets/wardley-RL74JXVD-BniDWDlr.js +0 -1
  203. /package/payload/server/public/assets/{_baseFor-SRtUHe7G.js → _baseFor-brRjpUOX.js} +0 -0
  204. /package/payload/server/public/assets/{array-BpsnM-Py.js → array-BGFCBI0e.js} +0 -0
  205. /package/payload/server/public/assets/{cytoscape.esm-D4Rl1H5h.js → cytoscape.esm-TvRJ2tGX.js} +0 -0
  206. /package/payload/server/public/assets/{defaultLocale-C7sQPl-O.js → defaultLocale-CRZydyG6.js} +0 -0
  207. /package/payload/server/public/assets/{dist-BXJR8AkG.js → dist-Dbh7HrZX.js} +0 -0
  208. /package/payload/server/public/assets/{init-BydxaDEV.js → init-B8gtcn7T.js} +0 -0
  209. /package/payload/server/public/assets/{jsx-runtime-B54kfC3e.js → jsx-runtime-CMu8uN0-.js} +0 -0
  210. /package/payload/server/public/assets/{katex-B0fVeDx4.js → katex-RxgSu_dy.js} +0 -0
  211. /package/payload/server/public/assets/{path-jlWYQ2i9.js → path-DZF-JdEe.js} +0 -0
  212. /package/payload/server/public/assets/{rough.esm-c4PR5shF.js → rough.esm-6CnTHTkH.js} +0 -0
  213. /package/payload/server/public/assets/{src-CPkr6SrY.js → src-BoaldmnP.js} +0 -0
@@ -49,7 +49,7 @@ Each agent lives at `{accountDir}/agents/{slug}/`:
49
49
 
50
50
  ### Toolless by construction
51
51
 
52
- The public agent has no tools (Task 615). It cannot search the graph, read files, run commands, or load skills mid-conversation. Everything it knows is the KNOWLEDGE.md and SOUL assembled into its system prompt at spawn time. There is no operator toggle for this — it is the fixed posture for every public agent on every channel. If the agent needs to surface graph content created after deployment, refresh its KNOWLEDGE.md (see Update knowledge below); the agent itself never reaches the live graph.
52
+ The public agent has no tools. It cannot search the graph, read files, run commands, or load skills mid-conversation. Everything it knows is the KNOWLEDGE.md and SOUL assembled into its system prompt at spawn time. There is no operator toggle for this — it is the fixed posture for every public agent on every channel. If the agent needs to surface graph content created after deployment, refresh its KNOWLEDGE.md (see Update knowledge below); the agent itself never reaches the live graph.
53
53
 
54
54
  ### knowledgeKeywords
55
55
 
@@ -99,7 +99,7 @@ SOUL.md answers "what does this agent feel like to talk to, and what is it here
99
99
 
100
100
  KNOWLEDGE.md is the public agent's baked, static knowledge surface — the facts the agent should always have on hand without a tool call. It is assembled into the system prompt at invocation time.
101
101
 
102
- Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. The public agent is toolless (Task 615): KNOWLEDGE.md is its **only** knowledge source. There is no graph access mid-conversation. Every gap in KNOWLEDGE.md is a question the agent cannot answer, so the KNOWLEDGE.md you build below is the whole of what the agent will know — populate it thoroughly.
102
+ Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. The public agent is toolless: KNOWLEDGE.md is its **only** knowledge source. There is no graph access mid-conversation. Every gap in KNOWLEDGE.md is a question the agent cannot answer, so the KNOWLEDGE.md you build below is the whole of what the agent will know — populate it thoroughly.
103
103
 
104
104
  The `memory-search` calls described below are **your** admin-time tool for populating KNOWLEDGE.md, not something the public agent ever runs. All `memory-search` queries for knowledge population use admin permissions — do not pass `agentSlug`. The `agentSlug` parameter is a runtime enforcement mechanism for the public agent's MCP server, not an admin-time discovery tool. At creation time, the agent may have zero tagged nodes — searching with `agentSlug` would return nothing.
105
105
 
@@ -155,7 +155,7 @@ After creation, no template metadata persists in the agent's files. The resultin
155
155
  1. **Pre-creation check** — verify the target directory (`agents/{slug}/`) does not exist or is empty before proceeding. If remnant files exist from a prior incomplete deletion, flag the issue to the user: show what files remain and offer to clean them up before starting fresh. Do not silently inherit stale files — a leftover `config.json` from a previous agent would bypass the creation gate.
156
156
  2. Ask for the agent's name (becomes the slug) and role description
157
157
  3. **Knowledge discovery** — search the graph with admin permissions (no `agentSlug` — the agent doesn't exist yet) to find available knowledge. Present available content grouped by category for the user to select.
158
- 4. **Install `IDENTITY.md`** — copy the Rubytech-controlled template verbatim to `agents/{slug}/IDENTITY.md`. It is fixed (the toolless public directive — Task 615) and is never drafted, edited, or presented for review.
158
+ 4. **Install `IDENTITY.md`** — copy the Rubytech-controlled template verbatim to `agents/{slug}/IDENTITY.md`. It is fixed (the toolless public directive) and is never drafted, edited, or presented for review.
159
159
  5. Present `SOUL.md` via document-editor with `filePath: "agents/{slug}/SOUL.md"` (draft personality and the operator-defined role from user conversation — tone plus the role the operator describes for the agent). Write on approval. Must be non-empty; an empty SOUL is refused at spawn.
160
160
  6. Present agent configuration via a single `form` component with these fields:
161
161
  - `knowledgeKeywords` (`text`): label "Knowledge keywords (max 5, comma-separated)", placeholder with examples relevant to the agent's role
@@ -249,7 +249,7 @@ Invoke the agent with `[New session. Greet the visitor.]` and stream the respons
249
249
 
250
250
  ## Runtime Surface
251
251
 
252
- Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. They are toolless by construction (Task 615): no filesystem access, no MCP tools, no subagents, no `memory-search`. Every public spawn resolves an empty `--allowed-tools`, runs in `dontAsk`, and carries a per-spawn `permissions.deny` covering every native, harness, and memory-MCP tool on every channel. Everything the agent needs to say comes from its directory files (IDENTITY.md, SOUL.md, KNOWLEDGE.md) assembled into the system prompt at invocation time — nothing else is reachable.
252
+ Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. They are toolless by construction: no filesystem access, no MCP tools, no subagents, no `memory-search`. Every public spawn resolves an empty `--allowed-tools`, runs in `dontAsk`, and carries a per-spawn `permissions.deny` covering every native, harness, and memory-MCP tool on every channel. Everything the agent needs to say comes from its directory files (IDENTITY.md, SOUL.md, KNOWLEDGE.md) assembled into the system prompt at invocation time — nothing else is reachable.
253
253
 
254
254
  ## URL Routing
255
255
 
@@ -68,7 +68,7 @@ The audit nodes carry `accountId`, `target` (URL or `(inline html)`), `score`, `
68
68
 
69
69
  ## What this plugin does not do
70
70
 
71
- - No LLM API calls. No citation monitor — out of scope. Multi-engine answer harvesting doesn't fit maxy-code's no-API-key architecture, and the monitor was archived without sprinting (Task 363). The audit plus the structured-answer template are the surface; check citation manually when needed.
71
+ - No LLM API calls. No citation monitor — out of scope. Multi-engine answer harvesting doesn't fit maxy-code's no-API-key architecture, and the monitor was archived without sprinting. The audit plus the structured-answer template are the surface; check citation manually when needed.
72
72
  - No automatic emission. `aeo-emit-jsonld` returns a script block; wiring it into the platform's page render path is per-renderer work, also filed as a follow-up.
73
73
  - No prose authoring. AEO audits structure; it does not rewrite content. Writer-craft owns voice.
74
74
 
@@ -18,7 +18,7 @@ It composes two patterns you must read alongside it:
18
18
  - **`cloudflare/references/d1-data-capture.md`** — the form → Pages Function → D1 → sweep mechanics, and the token-scope breakage (§ 0). Everything about minting the Pages-+-D1 token, `wrangler.toml`, and `wrangler d1 execute --remote` lives there; this skill does not repeat it. Note its rule that the D1 **query** endpoint rejects a D1-Read token — every `wrangler d1 execute` here uses a **D1-Edit** token, reads included.
19
19
  - **`business-assistant/references/invoicing.md`** — PDF generation via `browser-navigate` + `browser-pdf-save` (its step 4). The **deploy-time base render** (§ 5) uses that exact pattern. Dispatch (§ 6) does **not** — it stamps the persisted base with a PDF library, no browser and no network.
20
20
 
21
- Auth for every `wrangler`/API call is the **reused per-scope narrow token** of `cloudflare/references/api.md` (one deterministic token per scope — minted once if absent, persisted to the secrets file, then loaded and reused; Task 732), routed by the master's type (`cfat_…` account-scoped vs `cfut_…` user-scoped) by prefix per that reference. Deploy mechanics are `cloudflare/references/hosting-sites.md`.
21
+ Auth for every `wrangler`/API call is the **reused per-scope narrow token** of `cloudflare/references/api.md` (one deterministic token per scope — minted once if absent, persisted to the secrets file, then loaded and reused), routed by the master's type (`cfat_…` account-scoped vs `cfut_…` user-scoped) by prefix per that reference. Deploy mechanics are `cloudflare/references/hosting-sites.md`.
22
22
 
23
23
  ---
24
24
 
@@ -51,7 +51,7 @@ Do **not** deploy until all three pass. Each failure has one exact remediation;
51
51
  On `missing`, stop:
52
52
  > "The account directory isn't set, so I can't attach the signed PDF or persist the base render. `ACCOUNT_DIR` is normally set at spawn; if it's missing, this session was started without an account context — restart the account's session so the spawn seam sets it, then ask me again."
53
53
 
54
- (`ACCOUNT_DIR` is the spawn-seam guarantee of Task 714; this pre-flight only asserts it, it does not set it.)
54
+ (`ACCOUNT_DIR` is a spawn-seam guarantee; this pre-flight only asserts it, it does not set it.)
55
55
 
56
56
  ---
57
57
 
@@ -491,7 +491,7 @@ Dispatch runs **only** inside a live operator session on the account's own devic
491
491
  - once to the **business owner's configured address** (read from configured business identity — never hard-code a recipient in this workflow);
492
492
  - once to the **signer's captured `email`** — this copy is the **statutory record provided to the signer**, the evidentiary copy the legal basis (intro) relies on.
493
493
 
494
- **Owner-copy visibility.** The owner copy only *arrives* if the agent's mailbox actually receives mail addressed to the business address. A recipient-filter that drops mail to the business address (the bug fixed in Task 700 / `rubytech-shared-catchall-mailbox`) makes the owner copy vanish while the signer copy arrives — check that filter first if the owner reports a missing confirmation.
494
+ **Owner-copy visibility.** The owner copy only *arrives* if the agent's mailbox actually receives mail addressed to the business address. A recipient-filter that drops mail to the business address (the bug fixed in `rubytech-shared-catchall-mailbox`) makes the owner copy vanish while the signer copy arrives — check that filter first if the owner reports a missing confirmation.
495
495
 
496
496
  - **Mark this row — and only this row — swept, once both sends return a verified result**, keyed by its `id`:
497
497
 
@@ -658,7 +658,7 @@ grep -q "__cf_access_message" "$DIST/index.html" \
658
658
 
659
659
  Two distinct regressions of the § 1a neutral portal, both caught by the § 8.10 grep of the local output dir:
660
660
 
661
- - **Document-specific root (single-document regression).** The root redirects to or embeds **one** document's slug/path/content (the Task 720 design this supersedes), so it is wrong for every other document on the host and breaks silently the moment a second document is added — no event, until a signer for document B logs out and lands on document A. Signature: any `location.replace(`/meta-refresh or document path in the root. The fix is the neutral portal — no document-specific path, no document content.
661
+ - **Document-specific root (single-document regression).** The root redirects to or embeds **one** document's slug/path/content (the earlier design this supersedes), so it is wrong for every other document on the host and breaks silently the moment a second document is added — no event, until a signer for document B logs out and lands on document A. Signature: any `location.replace(`/meta-refresh or document path in the root. The fix is the neutral portal — no document-specific path, no document content.
662
662
  - **Sticky-param interstitial.** The root **branches on `__cf_access_message=logged_out`** — showing a "signed out" interstitial when the param is present. Because Cloudflare preserves that param through the **entire re-login redirect**, a re-authenticated signer is landed back on `/` with the **stale** param and shown a logged-out screen *while logged in*. Signature: any reference to `__cf_access_message` in the root. The fix is the neutral portal — it makes no auth-state claim, so it is correct in every state.
663
663
 
664
664
  The neutral portal must therefore contain **no** document-specific path **and no** auth-state claim; it shows the same content unconditionally.
@@ -2,18 +2,19 @@
2
2
 
3
3
  Step-by-step recovery path for when the Maxy automation fails or is unavailable. Every command below is isolated in its own code block — nothing to parse from prose. Every path is brand-scoped so multiple brands can coexist on the same Linux user.
4
4
 
5
- Prerequisites: `cloudflared` installed; the brand's VNC running on its own X display + rfbport + websockify + CDP port (per `brand.json`'s `vncDisplay`/`rfbPort`/`websockifyPort`/`cdpPort` fields — Tasks 553 + 924; defaults Maxy=`:99`/`5900`/`6080`/`9222`, Real Agent=`:100`/`5901`/`6081`/`9223`, Maxy-2=`:101`/`5902`/`6082`/`9224`, Maxy-3=`:102`/`5903`/`6083`/`9225`, Maxy-4=`:103`/`5904`/`6084`/`9226`); SSH access to the device. The defaults table is historical — runtime readers (`paths.ts`, admin/MCP, `vnc.sh`, `test-laptop-vnc-boot.sh`) loud-fail with `reason=cdp-port-unresolved` if any of `rfbPort`/`websockifyPort`/`cdpPort` is missing from the live `brand.json`; the values above show what each brand's `brand.json` contains, not what the readers fall back to.
5
+ Prerequisites: `cloudflared` installed; the brand's VNC running on its own X display + rfbport + websockify + CDP port (per `brand.json`'s `vncDisplay`/`rfbPort`/`websockifyPort`/`cdpPort` fields — defaults Maxy=`:99`/`5900`/`6080`/`9222`, Real Agent=`:100`/`5901`/`6081`/`9223`, Maxy-2=`:101`/`5902`/`6082`/`9224`, Maxy-3=`:102`/`5903`/`6083`/`9225`, Maxy-4=`:103`/`5904`/`6084`/`9226`); SSH access to the device. The defaults table is historical — runtime readers (`paths.ts`, admin/MCP, `vnc.sh`, `test-laptop-vnc-boot.sh`) loud-fail with `reason=cdp-port-unresolved` if any of `rfbPort`/`websockifyPort`/`cdpPort` is missing from the live `brand.json`; the values above show what each brand's `brand.json` contains, not what the readers fall back to.
6
6
 
7
7
  ---
8
8
 
9
- ## Why two hostnames (admin and public)
9
+ ## Why several hostnames (admin, public, operator)
10
10
 
11
- Each Maxy installation exposes two distinct surfaces from the same device and the same local port. The platform UI distinguishes them by the inbound `Host:` header.
11
+ Each Maxy installation exposes distinct surfaces from the same device and the same local port. The platform UI distinguishes them by the inbound `Host:` header.
12
12
 
13
13
  - **admin hostname** — the operator's private chat + admin panel. PIN-protected. Only the owner uses it.
14
14
  - **public hostname** — the public-facing agent (customer chat, marketing agent, WhatsApp webhook target). Reachable by anyone on the internet.
15
+ - **operator hostname** (optional) — a trimmed admin dashboard (sessions, chat, data — no graph, no browser). Admin-gated like the admin host, recognised by membership in `operator-domains.json`. See § "Operator dashboard ingress".
15
16
 
16
- Two tunnel hostnames, one connector, one local service. The tunnel's `ingress:` block routes each hostname to `http://localhost:${PORT}`; the platform UI handles the rest.
17
+ Several tunnel hostnames, one connector, one local service. The tunnel's `ingress:` block routes each hostname to `http://localhost:${PORT}`; the platform UI classifies the host (operator → public → admin) and serves the matching surface.
17
18
 
18
19
  ---
19
20
 
@@ -236,7 +237,7 @@ ingress:
236
237
  EOF
237
238
  ```
238
239
 
239
- **Why:** Tells cloudflared at startup which tunnel to run, where its credentials are, and how to route each hostname to the brand's local service port. The trailing `http_status:404` line is the mandatory catch-all. `no-autoupdate: true` stops the connector downloading a new binary and replacing itself (which exits the running process); the cloudflared version is installer-owned and bumped deliberately. This is belt-and-braces with the `--no-autoupdate` flag `resume-tunnel.sh` already passes on the spawn argv — the flag covers every existing install on its next service restart, this line covers a connector started any other way from a freshly-provisioned config. Task 757.
240
+ **Why:** Tells cloudflared at startup which tunnel to run, where its credentials are, and how to route each hostname to the brand's local service port. The trailing `http_status:404` line is the mandatory catch-all. `no-autoupdate: true` stops the connector downloading a new binary and replacing itself (which exits the running process); the cloudflared version is installer-owned and bumped deliberately. This is belt-and-braces with the `--no-autoupdate` flag `resume-tunnel.sh` already passes on the spawn argv — the flag covers every existing install on its next service restart, this line covers a connector started any other way from a freshly-provisioned config.
240
241
 
241
242
  **Success:** Confirm with:
242
243
 
@@ -428,7 +429,7 @@ A non-530 response means the tunnel is live.
428
429
 
429
430
  The correct production pattern is already in place on every Maxy device: the brand's platform UI itself is a **user-space systemd service** at `~/.config/systemd/user/<brand>.service`, and that service spawns the cloudflared connector as an `ExecStartPre=` via `platform/scripts/resume-tunnel.sh`. The connector runs as the Linux user (not root), its config is read from `${CFG_DIR}/config.yml` (brand-scoped, never `/etc`), and multiple brands coexist because each has its own unit file (`maxy.service`, `realagent.service`, etc.).
430
431
 
431
- **The connector is a supervised, self-healing unit (Task 602 + Task 757).** `resume-tunnel.sh` does not run cloudflared as a child of the brand service. It spawns it into its own transient systemd **service** — `cloudflared-<brand>.service` under `cloudflared.slice` — with `Restart=always`. Two consequences: a brand-service restart no longer reaps the connector (Task 602's cgroup decoupling), and systemd resurrects the connector on **any** exit — crash, OOM, manual kill (Task 757). The connector also runs with `--no-autoupdate`, so it never downloads a new binary and replaces itself; the version is installer-owned (pinned in the installer, bumped deliberately). `StartLimitIntervalSec=300`/`StartLimitBurst=5` mean a genuinely broken binary surfaces as a `failed` unit instead of looping forever.
432
+ **The connector is a supervised, self-healing unit.** `resume-tunnel.sh` does not run cloudflared as a child of the brand service. It spawns it into its own transient systemd **service** — `cloudflared-<brand>.service` under `cloudflared.slice` — with `Restart=always`. Two consequences: a brand-service restart no longer reaps the connector (the cgroup decoupling), and systemd resurrects the connector on **any** exit — crash, OOM, manual kill. The connector also runs with `--no-autoupdate`, so it never downloads a new binary and replaces itself; the version is installer-owned (pinned in the installer, bumped deliberately). `StartLimitIntervalSec=300`/`StartLimitBurst=5` mean a genuinely broken binary surfaces as a `failed` unit instead of looping forever.
432
433
 
433
434
  **Observability.** A connector death is now a logged, countable event, not an invisible gap:
434
435
 
@@ -439,9 +440,13 @@ journalctl --user -u cloudflared-<brand>.service | grep "Scheduled restart job"
439
440
 
440
441
  # Confirm auto-update is off — cloudflared prints its effective settings at startup:
441
442
  grep "no-autoupdate:true" "${CFG_DIR}/logs/cloudflared.log"
443
+
444
+ # Single-connector census — resume-tunnel logs a reap on every brand-service start:
445
+ grep "\[tunnel-supervise\] op=reap" "${CFG_DIR}/logs/cloudflared.log"
446
+ # (each completed start ends in "found=<n> killed=<m> survivor=<pid>"; healthy steady state is found=1)
442
447
  ```
443
448
 
444
- `no-autoupdate:true` in cloudflared's startup `Settings:` line is the per-box rollout acceptance signal. A unit that has tripped its start limit shows `Active: failed` under `systemctl --user status cloudflared-<brand>.service` — investigate the binary rather than restarting blindly.
449
+ `no-autoupdate:true` in cloudflared's startup `Settings:` line is the per-box rollout acceptance signal. A unit that has tripped its start limit shows `Active: failed` under `systemctl --user status cloudflared-<brand>.service` — investigate the binary rather than restarting blindly. A reap census whose final line shows `found>1` after a completed start, or any per-kill `target=<pid> tunnel=<id>` line whose tunnel id is not this brand's, is the failure signature (a stray survived, or a co-resident brand's connector was matched); `ps -eo pid,etime,args | grep '[c]loudflared.*tunnel run'` should then show exactly one connector, carrying `--no-autoupdate`.
445
450
 
446
451
  **Durability prerequisites (one-time per device):**
447
452
 
@@ -471,7 +476,7 @@ systemctl --user restart "cloudflared-${BRAND}.service"
471
476
 
472
477
  With `Restart=always` this is a clean reload: systemd starts a fresh connector under the same unit, reading the edited `config.yml`. The brief ~1s blip is the same one every cloudflared upgrade incurs.
473
478
 
474
- **Why not the brand service?** Since Task 602 decoupled the connector into its own unit (and Task 757 made it a supervised `Restart=always` service), `systemctl --user restart "${BRAND}.service"` no longer reaps the connector. Its `ExecStartPre=resume-tunnel.sh` runs, sees the connector already `is-active`, and skips by design (`resume-tunnel.sh` line 65) so the brand-service restart succeeds while the old connector keeps serving the old ingress. That is a silent no-op, not an error. Restarting the brand service only spawns a connector when none is running (the first-provision case in Step 6).
479
+ **Why not the brand service?** Since the connector was decoupled into its own unit (and made a supervised `Restart=always` service), a `systemctl --user restart "${BRAND}.service"` does not replace the running connector: its `ExecStartPre=resume-tunnel.sh` sees the supervised connector already `is-active` and does not respawn it, so the edited `config.yml` is not picked up by a brand-service restart. Restart the connector unit directly (above) to apply a config change. What the brand-service restart *does* do now is **reap strays**: before the skip, `resume-tunnel.sh` terminates every bare or stale `cloudflared … tunnel run` bound to this brand's cert/config/tunnel-id, leaving exactly the one supervised connector. So the two operations are distinct — restart the connector unit to load new ingress; restart the brand service to clear a duplicate connector. Restarting the brand service still only *spawns* a connector when none is running (the first-provision case in Step 6).
475
480
 
476
481
  **Success:**
477
482
 
@@ -564,6 +569,8 @@ systemctl --user restart "cloudflared-${BRAND}.service"
564
569
 
565
570
  Cloudflared does not watch `config.yml` for changes (the brand service unit launches it without `--config-watch`). Restart is the deterministic way to pick up the new rule; the brief ~1s connector blip is the same one every cloudflared upgrade incurs.
566
571
 
572
+ **Single-connector guarantee.** Restarting `cloudflared-${BRAND}.service` replaces the *supervised* connector in place. It does **not** touch a bare `cloudflared … tunnel run` started outside systemd (a manual run, a stale process serving old ingress) — so a bare stray keeps answering with its own `http_status:404` catch-all and Cloudflare can route requests to it, the exact intermittent-404 failure this guarantee closes. If `ps -eo pid,args | grep '[c]loudflared.*tunnel run'` shows more than one connector for this brand, restart the brand service (`systemctl --user restart "${BRAND}.service"`, with the `systemd-run` indirection from Step 6 when you are the admin agent): its `ExecStartPre=resume-tunnel.sh` reaps every stray bound to this brand's cert/config/tunnel-id and leaves exactly one. The reap is brand-scoped — a co-resident brand's connector (different cert path, different tunnel id) is never touched. Never start a second `cloudflared … tunnel run` by hand to apply a change; that stacks the duplicate this guarantee exists to prevent.
573
+
567
574
  ### Verification
568
575
 
569
576
  ```
@@ -596,36 +603,50 @@ systemctl --user restart "cloudflared-${BRAND}.service"
596
603
 
597
604
  ---
598
605
 
599
- ## Operator dashboard ingress (`operator.<host>`)
606
+ ## Operator dashboard ingress (operator host)
607
+
608
+ The operator dashboard is a trimmed admin surface (sessions, chat, WhatsApp reader, data — no graph, no browser). It routes to the **same brand UI port** as the admin host (`${MAXY_UI_PORT}`) and is gated by the **same admin gate** (Cloudflare Access / remote-auth + PIN + Claude OAuth) — it is not a public host. A request to the operator host that is unauthenticated hits the identical login flow as the bare admin host; the UI server then classifies the host and serves the operator shell.
600
609
 
601
- Task 822 adds a trimmed operator dashboard served on `operator.<BRAND_HOSTNAME>`. Unlike a prototype surface, it routes to the **same brand UI port** as the admin host (`${MAXY_UI_PORT}`) and is gated by the **same admin gate** (Cloudflare Access / remote-auth + PIN + Claude OAuth)it is not a public host. A request to `operator.<host>` that is unauthenticated hits the identical login flow as the bare admin host; the UI server classifies the host and serves the operator shell (sessions, chat, WhatsApp reader, data — no graph, no browser).
610
+ **The operator hostname is arbitrary** any FQDN the operator chooses (e.g. `glsops.siteoffice.online`). There is no `operator.` prefix rule: the UI classifies a host as operator **only** when the host is listed in `~/.${BRAND}/operator-domains.json`. Provisioning therefore has two scripted parts the tunnel ingress (so the host reaches the brand UI) and the operator-domains.json write (so the UI serves the operator shell instead of the full admin shell).
602
611
 
603
- Add it with the same Multi-ingress flow above, using `SURFACE=operator` and `PORT=${MAXY_UI_PORT}` (the brand UI port, not a separate dev server):
612
+ Take the operator FQDN from chat:
604
613
 
605
614
  ```
606
- SURFACE="operator"
607
- PORT="${MAXY_UI_PORT}" # the brand UI port — same service as admin/public
608
- HOSTNAME="operator.${BRAND_HOSTNAME}"
615
+ OPERATOR_HOSTNAME="glsops.siteoffice.online" # the operator's chosen FQDN
616
+ PORT="${MAXY_UI_PORT}" # the brand UI port — same service as admin/public
609
617
  ```
610
618
 
611
- Then run the awk append (step 1), validate (step 2), create the DNS row (step 3), and reload the connector (step 4) exactly as documented above. After the edit the ingress block carries the operator row before the catch-all:
619
+ ### 1. Ingress + DNS
620
+
621
+ Run the Multi-ingress flow above with `HOSTNAME="${OPERATOR_HOSTNAME}"` and `PORT="${MAXY_UI_PORT}"`: the awk append (step 1), validate (step 2), create the DNS row (step 3), reload the connector (step 4). After the edit the ingress block carries the operator row before the catch-all (hostnames are illustrative — yours are whatever the operator registered):
612
622
 
613
623
  ```yaml
614
624
  ingress:
615
- - hostname: admin.<BRAND_HOSTNAME>
625
+ - hostname: glsmith.siteoffice.online # admin (primary)
616
626
  service: http://localhost:<MAXY_UI_PORT>
617
- - hostname: public.<BRAND_HOSTNAME>
627
+ - hostname: gls.siteoffice.online # public (also in alias-domains.json)
618
628
  service: http://localhost:<MAXY_UI_PORT>
619
- - hostname: operator.<BRAND_HOSTNAME>
629
+ - hostname: glsops.siteoffice.online # operator (also in operator-domains.json)
620
630
  service: http://localhost:<MAXY_UI_PORT>
621
631
  - service: http_status:404
622
632
  ```
623
633
 
624
- When a provisioning script adds this row it logs `[tunnel-install] operator-ingress added host=operator.<BRAND_HOSTNAME> :${MAXY_UI_PORT}`; the absence of an `operator.` ingress on a brand that should have one is the failure signature.
634
+ ### 2. Classify the host as operator
635
+
636
+ The ingress only gets the request to the brand UI; without this write the UI serves the **full admin shell** on the operator host. Append the hostname to `operator-domains.json` at the **brand root** (not `${CFG_DIR}` — the platform UI watches the brand root and hot-reloads within ~2 s, no restart):
637
+
638
+ ```
639
+ F="${HOME}/.${BRAND}/operator-domains.json"
640
+ [ -f "$F" ] || echo '[]' > "$F"
641
+ jq --arg H "${OPERATOR_HOSTNAME}" 'if index($H) then . else . + [$H] end' "$F" > /tmp/op.json && mv /tmp/op.json "$F"
642
+ cat "$F"
643
+ ```
644
+
645
+ The UI logs `[operator-domains] reloaded <n> operator domain(s): …` within ~2 s of the write. A configured brand whose boot/reload log shows `0 operator domain(s)` while an operator host is expected means the file was written to the wrong path (e.g. under `cloudflared/`).
625
646
 
626
- **Verification.** `curl -I https://operator.<BRAND_HOSTNAME>` returns the admin login (not a 404, not the public agent page). After login the dashboard shows the sessions list and WhatsApp reader, the header menu carries only Dashboard / Chat / Data, and `https://operator.<BRAND_HOSTNAME>/graph` returns 404.
647
+ **Verification.** `curl -I https://${OPERATOR_HOSTNAME}` returns the admin login (not a 404, not the public agent page). After login the dashboard shows the sessions list and WhatsApp reader, the header menu carries only Dashboard / Chat / Data, and `https://${OPERATOR_HOSTNAME}/graph` returns 404. The standing serve signal is `[host-class] host=${OPERATOR_HOSTNAME} class=operator served=operator.html`; `class=admin served=index.html` for a configured operator host is the full-shell leak signature (the operator-domains.json write was missed or written to the wrong path).
627
648
 
628
- > Installer auto-provisioning of this row is out of scope for Task 822 (documented manual/scripted add only); a follow-up task owns wiring it into the installer.
649
+ > Installer auto-provisioning of this host is out of scope here (documented manual/scripted add only); wiring it into the installer is tracked separately.
629
650
 
630
651
  ---
631
652
 
@@ -668,7 +689,7 @@ If `admin.<yourdomain>` is rendering the public-agent UI, the tunnel is fine but
668
689
  - the hostname starts with `public.`, or
669
690
  - the hostname appears in `${HOME}/.${BRAND}/alias-domains.json` (note: brand root, **not** `${CFG_DIR}` — `alias-domains.json` lives one level above `cloudflared/`, because that's where the platform UI watches).
670
691
 
671
- Pre-Task-548 sessions populated this file via the now-deleted `tunnel-add-hostname` MCP tool, which wrote every routed hostname — including `admin.*` — into the alias set. The pollution persists across installs.
692
+ Earlier sessions populated this file via the now-deleted `tunnel-add-hostname` MCP tool, which wrote every routed hostname — including `admin.*` — into the alias set. The pollution persists across installs.
672
693
 
673
694
  **Diagnose:**
674
695
 
@@ -14,7 +14,7 @@ Ask three questions in order. The first `yes` determines the action.
14
14
  2. **Is `config.yml` referencing a tunnel UUID that no longer exists on the account, or a hostname that the account no longer owns?**
15
15
  → Full reset. The tunnel the config names is unreachable; recovering per-step is slower than starting clean.
16
16
 
17
- 3. **Is the only problem a stray CNAME in the dashboard, a rogue token-mode connector process, or an out-of-date `alias-domains.json` entry?**
17
+ 3. **Is the only problem a stray CNAME in the dashboard, a rogue token-mode connector process, or an out-of-date `alias-domains.json` / `operator-domains.json` entry?**
18
18
  → Patch (see § Patching below). Reset would destroy correct local state alongside the bad record.
19
19
 
20
20
  When no question reaches `yes`, the state is probably recoverable per-step via `references/manual-setup.md`. Re-run the relevant manual setup steps before reaching for reset.
@@ -102,6 +102,24 @@ jq --arg H "admin.<yourdomain>" 'map(select(. != $H))' "${HOME}/.${BRAND}/alias-
102
102
 
103
103
  Replace `<yourdomain>` with the actual domain. The platform UI watches the file and reloads without a restart.
104
104
 
105
+ ### Remove a rogue entry from `operator-domains.json`
106
+
107
+ `operator-domains.json` controls which hostnames the platform UI classifies as **operator** (serving the trimmed operator dashboard — sessions, chat, data, no graph/browser) rather than admin or public. It lives at `${HOME}/.${BRAND}/operator-domains.json` — the brand root, **not** `${CFG_DIR}` — for the same reason as `alias-domains.json`: the platform UI watches the brand root.
108
+
109
+ A host wrongly listed here serves the operator shell instead of the full admin shell (or instead of the public agent, since operator wins over public). Remove it manually:
110
+
111
+ ```
112
+ cat "${HOME}/.${BRAND}/operator-domains.json"
113
+ ```
114
+
115
+ If the wrong host is listed, strip it:
116
+
117
+ ```
118
+ jq --arg H "<host-to-remove>" 'map(select(. != $H))' "${HOME}/.${BRAND}/operator-domains.json" > /tmp/op.json && mv /tmp/op.json "${HOME}/.${BRAND}/operator-domains.json"
119
+ ```
120
+
121
+ The platform UI watches the file and reloads without a restart; the next request to the host routes to its corrected surface.
122
+
105
123
  ### Fix a single wrong DNS record without tearing everything down
106
124
 
107
125
  If a hostname's CNAME is pointing at the wrong tunnel (e.g. after an account switch left old records), `cloudflared` can overwrite it once the correct cert is in place:
@@ -36,7 +36,21 @@ Every Cloudflare operation proves itself with a live behavioural signal surfaced
36
36
 
37
37
  ## Inputs (tunnel)
38
38
 
39
- Four inputs come from the operator in chat: the admin FQDN, optional public FQDN, optional apex FQDN, and the admin password. `BRAND` is derived from disk — never hardcoded — with `BRAND=$(jq -r .hostname ~/.<configDir>/brand.json)`. The same install can host different brands on different ports; `brand.json.hostname` is the only authoritative source.
39
+ Five inputs come from the operator in chat: the admin FQDN, optional public FQDN, optional operator-dashboard FQDN, optional apex FQDN, and the admin password. `BRAND` is derived from disk — never hardcoded — with `BRAND=$(jq -r .hostname ~/.<configDir>/brand.json)`. The same install can host different brands on different ports; `brand.json.hostname` is the only authoritative source.
40
+
41
+ The operator always names the hostnames; the agent never invents one. When the operator asks for a suggestion, propose a **sibling** of the existing admin/public hosts — a new label at the same parent domain, not a label nested under another host. E.g. admin `glsmith.siteoffice.online` + public `gls.siteoffice.online` → suggest `glsops.siteoffice.online` (a peer under `siteoffice.online`), never `ops.glsmith.siteoffice.online` or `desk.gls.siteoffice.online` (nested under the admin/public host). Any hostname the operator picks is valid — this only shapes the default suggestion.
42
+
43
+ ## Host classes — three peers on one brand UI port
44
+
45
+ A brand serves three peer host classes, all routed to the **same brand UI port** (`${MAXY_UI_PORT}`); the platform UI classifies each request by its `Host` header. The hostnames are arbitrary — there is no required `admin.`/`public.`/`operator.` prefix.
46
+
47
+ | Class | How the UI recognises it | What it serves |
48
+ |---|---|---|
49
+ | **admin** (default) | the bare primary host — in neither domains file | full admin shell (chat, data, graph, browser) — admin-gated |
50
+ | **public** | host starts with `public.` **or** is listed in `~/.${BRAND}/alias-domains.json` | the public agent page only — no admin surfaces |
51
+ | **operator** | host is listed in `~/.${BRAND}/operator-domains.json` | the trimmed operator dashboard (sessions, chat, data — no graph, no browser) — admin-gated |
52
+
53
+ Precedence is operator → public → admin: a host listed in `operator-domains.json` is operator even if it also matches a public rule. Both domains files live at the **brand root** `~/.${BRAND}/` (not under `cloudflared/`) because the platform UI watches the brand root and hot-reloads within ~2 s. Provisioning admin needs no domains-file write; public writes `alias-domains.json`; operator writes `operator-domains.json`. See `references/manual-setup.md` § "Operator dashboard ingress".
40
54
 
41
55
  ## Execution
42
56
 
@@ -48,7 +62,9 @@ Tunnel mutations the agent performs from `references/manual-setup.md` (each one
48
62
  - `cloudflared tunnel --origincert <cert> create <name>` — produces `<tunnelId>.json` credentials.
49
63
  - Write `~/.${BRAND}/cloudflared/config.yml` (ingress block per the runbook).
50
64
  - `cloudflared tunnel --origincert <cert> route dns <tunnelId> <hostname>` — one call per non-apex hostname.
51
- - For non-admin hostnames not starting with `public.`, append to `~/.${BRAND}/alias-domains.json` so `isPublicHost()` treats them as public.
65
+ - For a public hostname not starting with `public.`, append it to `~/.${BRAND}/alias-domains.json` so `isPublicHost()` treats it as public.
66
+ - For an operator-dashboard hostname, append it to `~/.${BRAND}/operator-domains.json` so the UI serves the trimmed operator shell (admin-gated). See `references/manual-setup.md` § "Operator dashboard ingress".
67
+ - To apply an ingress change (a new operator or public host), relay a **reload to the supervised connector** — `systemctl --user restart "cloudflared-${BRAND}.service"` — which replaces the connector in place reading the edited `config.yml`. **Never** launch a bare `cloudflared … tunnel run` to pick up a change: that stacks a second connector on the tunnel (one on stale config), Cloudflare then routes some requests to the stale one, and the new host serves an intermittent 404. `resume-tunnel.sh` reaps such strays on the next brand-service start, but the correct provisioning path is the supervised reload, never a raw spawn.
52
68
  - Install + start the `${BRAND}-cloudflared.service` user unit (`systemctl --user daemon-reload && systemctl --user enable --now ${BRAND}-cloudflared.service`).
53
69
 
54
70
  After the service is up, the agent runs `curl -I https://<admin-hostname>` and pastes the response. The setup-done claim only fires when a `200` line appears in that response.
@@ -67,7 +83,7 @@ When the operator's request touches Cloudflare, the agent's permitted actions ar
67
83
 
68
84
  - Invoke `cloudflared` via Bash, following `references/manual-setup.md`.
69
85
  - Invoke `wrangler` and the Cloudflare API via Bash, following `references/api.md`, `references/hosting-sites.md`, and `references/d1-data-capture.md`, using the reused per-scope narrow token (minted once if absent, then reused).
70
- - Write `config.yml` and `alias-domains.json` per the runbook.
86
+ - Write `config.yml`, `alias-domains.json`, and `operator-domains.json` per the runbook.
71
87
  - Install and start the brand's cloudflared user service.
72
88
  - Quote `references/manual-setup.md`, `references/reset-guide.md`, or `references/dashboard-guide.md` verbatim when a dashboard click-path is the right tool.
73
89
  - Verify reachability via `curl -I https://<hostname>` and surface the response.
@@ -57,6 +57,6 @@ Tools are available via the `contacts` MCP server.
57
57
 
58
58
  `contact-create` writes a `:Person` node, which is one of the action-provenance-gated labels in `writeNodeWithEdges`. The write should carry an inbound `:PRODUCED` edge from `:Task`, `:Conversation`, or `:Message`. There is no agent-visible field for this — the admin server stamps `SESSION_NODE_ID` (the `sessionId` UUID of the active `:AdminConversation`) at MCP spawn time, and the wrapper auto-injects the `:PRODUCED` edge via `injectConversationProvenance`. The helper MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is enforced inside the natural key, not as a separate gate, so a cross-account env-stamp returns zero rows and never injects.
59
59
 
60
- The agent passes whichever organic relationships are appropriate (`PARTICIPATES_IN` to a Conversation, `WORKS_FOR` to an Organization, `KNOWN_VIA` to a referral source) — the provenance edge is composed automatically and is not an agent concern. When the env-stamp resolution fails (loud `[mcp:contacts] [provenance-missing] reason=<...>`), the downstream gate emits a `[graph-write] warn reason=missing-provenance labels=<csv> agent=<agentLabel>` line and the write proceeds (Task 580 relaxed this from a hard reject). The warn carries the labels and agent so operators can quantify how often the gap fires; the upstream `[provenance-missing]` line names the resolution failure that caused it.
60
+ The agent passes whichever organic relationships are appropriate (`PARTICIPATES_IN` to a Conversation, `WORKS_FOR` to an Organization, `KNOWN_VIA` to a referral source) — the provenance edge is composed automatically and is not an agent concern. When the env-stamp resolution fails (loud `[mcp:contacts] [provenance-missing] reason=<...>`), the downstream gate emits a `[graph-write] warn reason=missing-provenance labels=<csv> agent=<agentLabel>` line and the write proceeds (this was relaxed from a hard reject). The warn carries the labels and agent so operators can quantify how often the gap fires; the upstream `[provenance-missing]` line names the resolution failure that caused it.
61
61
 
62
62
  See [.docs/neo4j.md § Process provenance doctrine](../../../.docs/neo4j.md) for the doctrine and observability surface.
@@ -4,8 +4,8 @@ Admin identity is carried by the surface a session arrives on. It is never gated
4
4
  by a per-session step, and nothing a non-interactive channel cannot satisfy ever
5
5
  blocks a tool.
6
6
 
7
- The PIN identity gate (Task 619) — a universal PreToolUse hook that denied every
8
- tool until the operator submitted a PIN — was retired in Task 681. It deadlocked
7
+ The PIN identity gate — a universal PreToolUse hook that denied every
8
+ tool until the operator submitted a PIN — was retired. It deadlocked
9
9
  non-interactive surfaces: a WhatsApp channel session can never enter a PIN, so the
10
10
  channel reply tool was permanently unreachable. The hook, its `admin-identity-authenticate`
11
11
  submit tool, the `/api/admin/identity/validate` loopback endpoint, and the standing
@@ -34,14 +34,14 @@ sessions skip it.
34
34
  On a channel (WhatsApp today), the inbound sender phone is bound to a person's
35
35
  `userId` via `users.json` (`UserEntry.phone`), confirmed against the account's
36
36
  `admins[]`. The channel admin session carries that `userId` — graph writes and
37
- the `${USER_ID}` MCP env attribute to the actual sender (Task 682). An unbound
37
+ the `${USER_ID}` MCP env attribute to the actual sender. An unbound
38
38
  sender (the owner's self-phone, or an `adminPhones` entry with no `users.json`
39
39
  phone) falls back to the boot-time owner `userId`.
40
40
 
41
41
  A WhatsApp admin whose inbound arrives as an unresolved LID (a cold mapping
42
42
  cache) resolves to their own `userId` via a runtime LID cache
43
43
  (`whatsapp-lid-map.json`), populated the first time their LID resolves to their
44
- phone (Task 686). A LID that has never resolved has no cache entry and still
44
+ phone. A LID that has never resolved has no cache entry and still
45
45
  falls back to the owner `userId` — visible as the `op=admin-identity …
46
46
  userId=owner-fallback` log line.
47
47
 
@@ -49,9 +49,9 @@ On loopback (claude.ai/code), multiple admins resolve to the right *profile*
49
49
  conversationally but share the owner's `userId` at the env/MCP layer.
50
50
  Deterministic per-admin `userId` on loopback is deliberately out of scope:
51
51
  making it deterministic again means a per-session identity step, which is exactly
52
- the deadlock Task 681 removed.
52
+ the deadlock the retirement removed.
53
53
 
54
- ## Boot-time binding drift gate (Task 687)
54
+ ## Boot-time binding drift gate
55
55
 
56
56
  The channel-admin binding is reassembled at read time from three independently
57
57
  operator-maintained sources: `adminPhones` (role allowlist), `admins[]`
@@ -83,22 +83,22 @@ The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/
83
83
 
84
84
  **Body schema.** `{channel?, permissionMode?, initialMessage?, specialist?, model?}`, cookie-auth only (`hidden: false`). `permissionMode` accepts one of five values matching Claude Code's CLI: `'default'`, `'acceptEdits'`, `'plan'`, `'auto'`, `'bypassPermissions'`. The wrapper rejects nothing structurally — string fields are coerced or defaulted (`channel` → `'browser'`, `permissionMode` → undefined, `initialMessage` → null if empty/whitespace).
85
85
 
86
- **Auth surface.** One: `cookie` — `requireAdminSession` validates `session_key` and resolves `senderId` from the in-memory session store. Task 626 removed the turn-recorder loopback bypass; there is no loopback caller and no `adminSessionId` body field.
86
+ **Auth surface.** One: `cookie` — `requireAdminSession` validates `session_key` and resolves `senderId` from the in-memory session store. The turn-recorder loopback bypass was removed; there is no loopback caller and no `adminSessionId` body field.
87
87
 
88
- **Forwarded endpoint.** One upstream call per spawn-with-message: `POST {managerBase}/public-spawn` (enriched body, with `initialMessage` inlined when set). The manager appends `initialMessage` as the trailing positional argv to `claude` so the CLI processes it as the session's first user turn at PTY startup — the JSONL first `role=user` line equals `initialMessage` verbatim. No follow-up `/<sessionId>/input` call, no bracketed-paste. The HTTP response streams the spawn upstream body straight through. (Task 153.)
88
+ **Forwarded endpoint.** One upstream call per spawn-with-message: `POST {managerBase}/public-spawn` (enriched body, with `initialMessage` inlined when set). The manager appends `initialMessage` as the trailing positional argv to `claude` so the CLI processes it as the session's first user turn at PTY startup — the JSONL first `role=user` line equals `initialMessage` verbatim. No follow-up `/<sessionId>/input` call, no bracketed-paste. The HTTP response streams the spawn upstream body straight through.
89
89
 
90
- **Caller.** `Sidebar.tsx`'s "+ New session" click opens the `NewSessionModal` (Task 223) — no POST fires on click. Modal submit POSTs `{channel:'browser', permissionMode, model, initialMessage}` where `initialMessage` is the operator's typed text verbatim; `permissionMode` and `model` are per-session overrides local to the modal. The sidebar's own mode trigger persists across refresh, new tab, and new device under `(accountId, userId)` via `/api/admin/session-defaults` (Task 239) — a single `:SpawnPreference` node MERGEd on the composite key with `permissionMode` and `model`. The on-the-wire signal that the contract held is the `[claude-session-manager:wrapper] spawn-request-in surface=cookie` log line followed by `forward-spawn-done`; it always carries `initialMessage=yes`, which a regressed client gate would flip to `no`.
90
+ **Caller.** `Sidebar.tsx`'s "+ New session" click opens the `NewSessionModal` — no POST fires on click. Modal submit POSTs `{channel:'browser', permissionMode, model, initialMessage}` where `initialMessage` is the operator's typed text verbatim; `permissionMode` and `model` are per-session overrides local to the modal. The sidebar's own mode trigger persists across refresh, new tab, and new device under `(accountId, userId)` via `/api/admin/session-defaults` — a single `:SpawnPreference` node MERGEd on the composite key with `permissionMode` and `model`. The on-the-wire signal that the contract held is the `[claude-session-manager:wrapper] spawn-request-in surface=cookie` log line followed by `forward-spawn-done`; it always carries `initialMessage=yes`, which a regressed client gate would flip to `no`.
91
91
 
92
- ## Session identifiers (Task 135)
92
+ ## Session identifiers
93
93
 
94
- The metadata pane surfaces two distinct identifier values. Task 141 collapsed the three-id model that Task 135 introduced back to Task 080's single-identity contract: claude's bridge suffix and the JSONL basename UUID are two phases of one claude-side identity, not two operator-visible identifiers. The manager resolver accepts either phase on any route. The wire emits one canonical `sessionId` — whichever phase is current — and the pane shows one row.
94
+ The metadata pane surfaces two distinct identifier values. The three-id model was collapsed back to a single-identity contract: claude's bridge suffix and the JSONL basename UUID are two phases of one claude-side identity, not two operator-visible identifiers. The manager resolver accepts either phase on any route. The wire emits one canonical `sessionId` — whichever phase is current — and the pane shows one row.
95
95
 
96
96
  | Operator label | What it is | Manager wire field | Log key |
97
97
  |---|---|---|---|
98
- | `sessionId` | Claude's session. Two phases: bridge suffix (`session_xxx`, the URL segment in `claude.ai/code/<session_xxx>`, set when claude prints the `/remote-control` URL) and JSONL basename UUID (claude's intrinsic id on disk, bound when the first turn flushes the JSONL). Both phases coexist on a live row after URL capture; the manager wire emits the bridge form when set, falling back to the JSONL basename in the pre-URL-capture window. The resolver routes either phase to the same row, so callers never need to choose. | `sessionId` (collapsed from the earlier three-field surface (see Task 141) `sessionId` + `claudeSessionId` + `bridgeSessionId`) | `sessionId=` |
98
+ | `sessionId` | Claude's session. Two phases: bridge suffix (`session_xxx`, the URL segment in `claude.ai/code/<session_xxx>`, set when claude prints the `/remote-control` URL) and JSONL basename UUID (claude's intrinsic id on disk, bound when the first turn flushes the JSONL). Both phases coexist on a live row after URL capture; the manager wire emits the bridge form when set, falling back to the JSONL basename in the pre-URL-capture window. The resolver routes either phase to the same row, so callers never need to choose. | `sessionId` (collapsed from the earlier three-field surface `sessionId` + `claudeSessionId` + `bridgeSessionId`) | `sessionId=` |
99
99
  | `accountId` | Account / workspace UUID. For the admin channel, the value the manager carries as `senderId` IS the accountId; for non-admin channels the same wire field carries a phone / email / chat-id (the channel-level sender), so its log key on those paths is still `senderId=`. | `senderId` (channel-agnostic) | `accountId=` in admin-flow emit sites; `senderId=` in channel-bridge sites |
100
100
 
101
- ## Memory MCP write-path outcome lines (Task 166)
101
+ ## Memory MCP write-path outcome lines
102
102
 
103
103
  Every write-path tool in the memory plugin MCP emits one structured line per invocation to `server.log` via the loopback `/api/admin/log-ingest` route. The operator can answer "did this write succeed?" from a single greppable surface — no need to cross-reference the per-process `mcp-memory-stderr-<date>.log`.
104
104
 
@@ -111,7 +111,7 @@ Line shapes:
111
111
 
112
112
  Enumerated `reason` slugs (failure path): `invalid-input | schema-unknown-label | schema-property-naming | account-isolation | producedby-task-not-found | constraint-violation | conflicting-element-id | invalid-target-node-id | gate-blocked | neo4j-driver-error`. Free-form error text remains in the `detail` field. New slugs are added only by amending the helper's union type; nothing silently falls through to a generic `unknown`.
113
113
 
114
- Wired tools: `memory-write`, `memory-update`, `memory-delete`, `memory-restore`, `memory-empty-trash`, `memory-archive-write`, `memory-ingest`, `memory-ingest-extract`, `memory-ingest-web`, `memory-reindex`, `memory-edit-attachment`, `memory-rename-attachment`, `profile-update`, `profile-delete`, `graph-prune-denylist-add`, `graph-prune-denylist-remove`, `conversation-archive-derive-insights`, `conversation-archive-enrich-rejection`, `conversation-memory-expunge`. Read-path tools (`memory-search`, etc.) are intentionally out of scope — reads do not change state and their failures already reach the caller. (Task 424 removed `memory-rank` and `memory-classify`.)
114
+ Wired tools: `memory-write`, `memory-update`, `memory-delete`, `memory-restore`, `memory-empty-trash`, `memory-archive-write`, `memory-ingest`, `memory-ingest-extract`, `memory-ingest-web`, `memory-reindex`, `memory-edit-attachment`, `memory-rename-attachment`, `profile-update`, `profile-delete`, `graph-prune-denylist-add`, `graph-prune-denylist-remove`, `conversation-archive-derive-insights`, `conversation-archive-enrich-rejection`, `conversation-memory-expunge`. Read-path tools (`memory-search`, etc.) are intentionally out of scope — reads do not change state and their failures already reach the caller. (`memory-rank` and `memory-classify` were removed.)
115
115
 
116
116
  Diagnostic grep:
117
117
 
@@ -130,9 +130,9 @@ grep '\[mcp:memory\] log-ingest-failed' ~/.${brand}/logs/mcp-memory-stderr-*.log
130
130
 
131
131
  The existing `mcp-memory-stderr-<date>.log` capture is unchanged on error paths — the new structured line is additive, not a relocation. Crash-mode debugging continues to read the stderr log; routine "did it land?" questions read `server.log`.
132
132
 
133
- **Test contract (Task 168).** Each of the 20 wired tools carries a `*-emit.test.ts` fixture under `platform/plugins/memory/mcp/src/tools/__tests__/` that mocks `fetch` and asserts the wire body. A refactor that drops `withWriteEvent` from any tool fails the matching tool's tests with a clear `expected "vi.fn()" to be called 1 times, but got 0 times` error, naming the tool. A shared helper at `__tests__/_helpers/emit-capture.ts` standardises the fetch capture + Neo4j Session stub so each tool test stays a few lines of mocks + one happy + one error assertion. Drift between the helper's body shape and the route's validator is pinned by an end-to-end test at `platform/ui/server/routes/admin/__tests__/log-ingest-mcp-end-to-end.test.ts`, which routes the helper's POST through the real Hono `log-ingest` route with a loopback `remoteAddress` shim.
133
+ **Test contract.** Each of the 20 wired tools carries a `*-emit.test.ts` fixture under `platform/plugins/memory/mcp/src/tools/__tests__/` that mocks `fetch` and asserts the wire body. A refactor that drops `withWriteEvent` from any tool fails the matching tool's tests with a clear `expected "vi.fn()" to be called 1 times, but got 0 times` error, naming the tool. A shared helper at `__tests__/_helpers/emit-capture.ts` standardises the fetch capture + Neo4j Session stub so each tool test stays a few lines of mocks + one happy + one error assertion. Drift between the helper's body shape and the route's validator is pinned by an end-to-end test at `platform/ui/server/routes/admin/__tests__/log-ingest-mcp-end-to-end.test.ts`, which routes the helper's POST through the real Hono `log-ingest` route with a loopback `remoteAddress` shim.
134
134
 
135
- ## Diagnosing missing tools in production (Task 183)
135
+ ## Diagnosing missing tools in production
136
136
 
137
137
  When a database-operator spawn fails to call any of its 11 tools, the question is which of two layers dropped them:
138
138
 
@@ -147,13 +147,13 @@ Three log-line shapes, all keyed on the same database-operator `sessionId`, dist
147
147
  [recorder-turn] sessionId=<id> turnIndex=<n> stopReason=<r> textBytes=<n> text=<full json-escaped> toolUseNames=<csv|—> containsToolCallText=true|false
148
148
  ```
149
149
 
150
- - `[pty-spawn-tool-inventory]` (Task 178) measures the **framework** layer — the argv built and the MCP servers' published tools. Argv-tools count must be 11; mcp-listed-tools must be ≥ 11.
151
- - `[recorder-session-init]` (Task 183 — emitted by the JSONL tail in `claude-session-manager`) measures the **CLI/SDK** layer — what the SDK actually exposed to the model. The `source=` field names the data path; `toolsArgvMissingFromExposed` is the Outcome A fingerprint:
150
+ - `[pty-spawn-tool-inventory]` measures the **framework** layer — the argv built and the MCP servers' published tools. Argv-tools count must be 11; mcp-listed-tools must be ≥ 11.
151
+ - `[recorder-session-init]` (emitted by the JSONL tail in `claude-session-manager`) measures the **CLI/SDK** layer — what the SDK actually exposed to the model. The `source=` field names the data path; `toolsArgvMissingFromExposed` is the Outcome A fingerprint:
152
152
  - `source=system.init` / `command_permissions` / `deferred_tools_delta` ⇒ the SDK wrote an init record to the JSONL and the tail matched one of the three known shapes. The bridge-attached `--remote-control` mode writes `attachment.deferred_tools_delta`; the headless SDK init record (`system.subtype=init`) is what `claude -p --output-format=stream-json` modes write.
153
- - `source=headless-argv-fallback` ⇒ Task 185 finding (2026-05-20): headless `claude --verbose` PTY mode (which headless database-operator spawns use) does **not** write any tool-list record to the JSONL — none of the three probe shapes ever appears. The tail falls back to the spawner's `--allowed-tools` argv as the exposed set, independently verified by `[pty-spawn-tool-inventory]`'s `exposed=` field. Treat this source identically to a positive JSONL match: the argv set is authoritative for headless spawns.
153
+ - `source=headless-argv-fallback` ⇒ a finding (2026-05-20): headless `claude --verbose` PTY mode (which headless database-operator spawns use) does **not** write any tool-list record to the JSONL — none of the three probe shapes ever appears. The tail falls back to the spawner's `--allowed-tools` argv as the exposed set, independently verified by `[pty-spawn-tool-inventory]`'s `exposed=` field. Treat this source identically to a positive JSONL match: the argv set is authoritative for headless spawns.
154
154
  - `toolsArgvMissingFromExposed=—` ⇒ all argv tools made it through; Outcome A is ruled out.
155
155
  - Any non-`—` value enumerates the names the SDK dropped and is the diagnosis. Only meaningful when `source` is JSONL-derived; under `headless-argv-fallback` the exposed set equals argv by construction, so this field is always `—`.
156
- - `[recorder-turn]` measures the **model** layer — Haiku's verbatim words (Task 213: full `text=`, not the prior 240-char `textHead`) and the tool names it actually called (`toolUseNames`). `toolUseNames=—` paired with `toolsArgvMissingFromExposed=—` is the Outcome B signature: tools present, model abstained. `containsToolCallText=true` flags any assistant text that contains `<tool_call>`, `<function_calls>`, or `</invoke>` — the recorder model copying the tool-call shape into prose (the regression Task 213 closed).
156
+ - `[recorder-turn]` measures the **model** layer — Haiku's verbatim words (full `text=`, not the prior 240-char `textHead`) and the tool names it actually called (`toolUseNames`). `toolUseNames=—` paired with `toolsArgvMissingFromExposed=—` is the Outcome B signature: tools present, model abstained. `containsToolCallText=true` flags any assistant text that contains `<tool_call>`, `<function_calls>`, or `</invoke>` — the recorder model copying the tool-call shape into prose (the regression that was closed).
157
157
 
158
158
  The three lines are emitted into `server.log` via the platform UI's `/api/admin/log-ingest` loopback route, so a single grep against `server.log` answers the question for any past, present, or future recorder spawn:
159
159
 
@@ -166,7 +166,7 @@ grep -E "\[(pty-spawn-tool-inventory|recorder-session-init|recorder-turn|mcp:(me
166
166
 
167
167
  1. The single `[pty-spawn-tool-inventory]` line proves what the framework provisioned. If `argv-tools < 11`, the bug is upstream of this evidence chain — the agent file or the spawn site.
168
168
  2. The single `[recorder-session-init]` line proves what the model received. If `toolsArgvMissingFromExposed` is non-`—`, the named tools were dropped by the CLI/SDK; that is Outcome A and the fix lives in the `--mcp-config` resolution or the CLI/SDK handshake.
169
- 3. Each `[recorder-turn]` line records one assistant turn. If `toolUseNames=—` across every turn AND `toolsArgvMissingFromExposed=—`, the model could call the tools and chose not to; `text=` carries the model's verbatim reasoning (Task 213 dropped the 240-char head cap so the whole turn is observable from server.log) and is the next surface to read. A `containsToolCallText=true` row is the recorder-relapse signature: the model emitted tool-call shape as prose instead of a real `tool_use` block.
169
+ 3. Each `[recorder-turn]` line records one assistant turn. If `toolUseNames=—` across every turn AND `toolsArgvMissingFromExposed=—`, the model could call the tools and chose not to; `text=` carries the model's verbatim reasoning (the 240-char head cap was dropped so the whole turn is observable from server.log) and is the next surface to read. A `containsToolCallText=true` row is the recorder-relapse signature: the model emitted tool-call shape as prose instead of a real `tool_use` block.
170
170
 
171
171
  **Loopback gate.** The `/api/admin/log-ingest` route is loopback-only on both the socket address and (when present) the `X-Forwarded-For` header. A LAN client whose request reaches the route via a proxy still gets a 403 because XFF tokens are required to be loopback when XFF is set. Without this gate the evidence chain would be worthless — any LAN client could forge `[recorder-session-init]` lines under a spoofed sessionId.
172
172