@rubytech/create-maxy-code 0.1.300 → 0.1.302

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 (219) 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 +93 -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/PLUGIN.md +1 -1
  8. package/payload/platform/plugins/cloudflare/references/manual-setup.md +14 -8
  9. package/payload/platform/plugins/cloudflare/references/reset-guide.md +1 -1
  10. package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +5 -2
  11. package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
  12. package/payload/platform/plugins/docs/references/admin-identity-gate.md +6 -6
  13. package/payload/platform/plugins/docs/references/admin-session.md +15 -15
  14. package/payload/platform/plugins/docs/references/admin-ui.md +27 -18
  15. package/payload/platform/plugins/docs/references/aeo.md +1 -1
  16. package/payload/platform/plugins/docs/references/deployment.md +3 -3
  17. package/payload/platform/plugins/docs/references/internals.md +28 -28
  18. package/payload/platform/plugins/docs/references/platform.md +21 -21
  19. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -3
  20. package/payload/platform/plugins/docs/references/troubleshooting.md +6 -6
  21. package/payload/platform/plugins/docs/references/visitor-graph.md +3 -3
  22. package/payload/platform/plugins/email/PLUGIN.md +2 -2
  23. package/payload/platform/plugins/email/references/email-reference.md +2 -2
  24. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -2
  25. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  26. package/payload/platform/plugins/memory/PLUGIN.md +9 -9
  27. package/payload/platform/plugins/memory/references/graph-primitives.md +2 -2
  28. package/payload/platform/plugins/memory/references/schema-base.md +14 -14
  29. package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +1 -1
  30. package/payload/platform/plugins/memory/references/transcript-formats/otter.md +1 -1
  31. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +6 -6
  32. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +1 -1
  33. package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +8 -8
  34. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +5 -5
  35. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/references/daily-notes.md +1 -1
  36. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -1
  37. package/payload/platform/plugins/slides/PROVENANCE.md +1 -1
  38. package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +1 -1
  39. package/payload/platform/plugins/telegram/PLUGIN.md +1 -1
  40. package/payload/platform/plugins/url-get/PLUGIN.md +2 -2
  41. package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
  42. package/payload/platform/plugins/whatsapp/PLUGIN.md +5 -5
  43. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +5 -5
  44. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +1 -1
  45. package/payload/platform/plugins/workflows/PLUGIN.md +1 -1
  46. package/payload/platform/plugins/x-import/PLUGIN.md +2 -2
  47. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  48. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +125 -0
  49. package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +253 -0
  50. package/payload/platform/scripts/check-no-task-id-leaks.mjs +62 -6
  51. package/payload/platform/scripts/resume-tunnel.sh +89 -5
  52. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +1 -0
  53. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  54. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +23 -0
  55. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  56. package/payload/platform/services/webchat-channel/dist/notification.d.ts +12 -0
  57. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  58. package/payload/platform/services/webchat-channel/dist/notification.js +29 -0
  59. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  60. package/payload/platform/services/webchat-channel/dist/server.d.ts +1 -0
  61. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  62. package/payload/platform/services/webchat-channel/dist/server.js +21 -6
  63. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  64. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +26 -0
  65. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  66. package/payload/platform/services/whatsapp-channel/dist/notification.js +40 -0
  67. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  68. package/payload/platform/services/whatsapp-channel/dist/server.d.ts +4 -0
  69. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  70. package/payload/platform/services/whatsapp-channel/dist/server.js +93 -9
  71. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  72. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +22 -0
  73. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -0
  74. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +157 -0
  75. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -0
  76. package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
  77. package/payload/platform/templates/specialists/agents/librarian.md +2 -2
  78. package/payload/server/public/assets/AdminShell-CKQFuyG1.js +1 -0
  79. package/payload/server/public/assets/{Checkbox-BZ1XbA1t.js → Checkbox-CTB1YICm.js} +1 -1
  80. package/payload/server/public/assets/{Transcript-CaVix6W0.js → Transcript-ClvmtDzk.js} +1 -1
  81. package/payload/server/public/assets/admin-STC3C8ed.js +1 -0
  82. package/payload/server/public/assets/{arc-DxSm8tli.js → arc-BrTRjBIM.js} +1 -1
  83. package/payload/server/public/assets/architecture-YZFGNWBL-DqQB3T6j.js +1 -0
  84. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-7JSAh0vL.js → architectureDiagram-Q4EWVU46-DJ9on7sZ.js} +1 -1
  85. package/payload/server/public/assets/{audio-attachment-mime-CFyc3gpD.js → audio-attachment-mime-B6GLbrSj.js} +3 -3
  86. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DLoVmHKD.js → blockDiagram-DXYQGD6D-CIcyQn0q.js} +1 -1
  87. package/payload/server/public/assets/{browser-ClnGUKp-.js → browser-Df57jxpr.js} +1 -1
  88. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Nv1G0o9P.js → c4Diagram-AHTNJAMY-Dt5dh_GR.js} +1 -1
  89. package/payload/server/public/assets/channel-NZeJdLVK.js +1 -0
  90. package/payload/server/public/assets/chat-B9ZI_e8i.js +1 -0
  91. package/payload/server/public/assets/{chunk-2KRD3SAO-BpqPMhoA.js → chunk-2KRD3SAO-BUq3MqUy.js} +1 -1
  92. package/payload/server/public/assets/{chunk-336JU56O-DgXHlVU_.js → chunk-336JU56O-CUzJH5Qt.js} +2 -2
  93. package/payload/server/public/assets/chunk-426QAEUC-DFKX5l0L.js +1 -0
  94. package/payload/server/public/assets/{chunk-4BX2VUAB-nwyZSZH8.js → chunk-4BX2VUAB-B4JUc3UA.js} +1 -1
  95. package/payload/server/public/assets/{chunk-4TB4RGXK-Tfxz9LHX.js → chunk-4TB4RGXK-HRO9fIIs.js} +1 -1
  96. package/payload/server/public/assets/{chunk-55IACEB6-6FMJQGXG.js → chunk-55IACEB6-MZdrLBuw.js} +1 -1
  97. package/payload/server/public/assets/{chunk-5FUZZQ4R-g2FgZF3D.js → chunk-5FUZZQ4R-XcAN5zlM.js} +1 -1
  98. package/payload/server/public/assets/{chunk-5PVQY5BW-DtLwXsPH.js → chunk-5PVQY5BW-PQ1GEsxo.js} +1 -1
  99. package/payload/server/public/assets/{chunk-67CJDMHE-K91CP5ZU.js → chunk-67CJDMHE-BVvCaqGd.js} +1 -1
  100. package/payload/server/public/assets/{chunk-7N4EOEYR-BGpCQhqK.js → chunk-7N4EOEYR-BlBbZdpM.js} +1 -1
  101. package/payload/server/public/assets/{chunk-AA7GKIK3-jkDbInck.js → chunk-AA7GKIK3-Ct-PgkGa.js} +1 -1
  102. package/payload/server/public/assets/{chunk-BSJP7CBP-mDosdzGs.js → chunk-BSJP7CBP-DZRRfCSz.js} +1 -1
  103. package/payload/server/public/assets/{chunk-CIAEETIT-DhBxewpQ.js → chunk-CIAEETIT-BoGEJiXe.js} +1 -1
  104. package/payload/server/public/assets/{chunk-EDXVE4YY-B7c-9Emg.js → chunk-EDXVE4YY-DiSmN31f.js} +1 -1
  105. package/payload/server/public/assets/{chunk-ENJZ2VHE-Cqhhncox.js → chunk-ENJZ2VHE-uhhokIvs.js} +1 -1
  106. package/payload/server/public/assets/{chunk-FMBD7UC4-BpDq6wj1.js → chunk-FMBD7UC4-CfCx5WI7.js} +1 -1
  107. package/payload/server/public/assets/{chunk-FOC6F5B3-Ds02-ktu.js → chunk-FOC6F5B3-Dj9E9gTu.js} +1 -1
  108. package/payload/server/public/assets/{chunk-ICPOFSXX-BTX5POFr.js → chunk-ICPOFSXX-DcBs_FNy.js} +2 -2
  109. package/payload/server/public/assets/{chunk-K5T4RW27-Du1PXXBU.js → chunk-K5T4RW27-7-VefyJD.js} +1 -1
  110. package/payload/server/public/assets/{chunk-KGLVRYIC-siasOAf8.js → chunk-KGLVRYIC-CqgTEVxm.js} +1 -1
  111. package/payload/server/public/assets/{chunk-LIHQZDEY-xf1rbZtf.js → chunk-LIHQZDEY-C0gzDXha.js} +1 -1
  112. package/payload/server/public/assets/{chunk-ORNJ4GCN-DeTLQ_LD.js → chunk-ORNJ4GCN-JXz23R1H.js} +1 -1
  113. package/payload/server/public/assets/{chunk-OYMX7WX6-DQ7_oVJn.js → chunk-OYMX7WX6-C2snCNq9.js} +1 -1
  114. package/payload/server/public/assets/chunk-QZHKN3VN-BWCCN0AM.js +1 -0
  115. package/payload/server/public/assets/{chunk-U2HBQHQK-CXmFUKgn.js → chunk-U2HBQHQK-CXCU7M-M.js} +1 -1
  116. package/payload/server/public/assets/{chunk-X2U36JSP-Bj8-8Wkz.js → chunk-X2U36JSP-CEdcSb28.js} +1 -1
  117. package/payload/server/public/assets/{chunk-XPW4576I-Con3TXqt.js → chunk-XPW4576I-Cckc75YS.js} +1 -1
  118. package/payload/server/public/assets/{chunk-YZCP3GAM-Dx8BHGWf.js → chunk-YZCP3GAM-4nN8AO7H.js} +1 -1
  119. package/payload/server/public/assets/{chunk-ZZ45TVLE-Dp4Q5Y-f.js → chunk-ZZ45TVLE-DcoBc8-7.js} +1 -1
  120. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DPlCQCX2.js +1 -0
  121. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CqkQq74h.js +1 -0
  122. package/payload/server/public/assets/clone-CSykdD1A.js +1 -0
  123. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-pZiCv69E.js → cose-bilkent-S5V4N54A-BFFro_t9.js} +1 -1
  124. package/payload/server/public/assets/{dagre-CLqhEgbL.js → dagre-CN5to_JM.js} +1 -1
  125. package/payload/server/public/assets/{dagre-KV5264BT-BCxE8iyk.js → dagre-KV5264BT-WYrPwDg0.js} +1 -1
  126. package/payload/server/public/assets/{data-DDjeQQ7e.js → data-BYiwvhSl.js} +1 -1
  127. package/payload/server/public/assets/{diagram-5BDNPKRD-slUEdZzz.js → diagram-5BDNPKRD-Dz2CiboN.js} +1 -1
  128. package/payload/server/public/assets/{diagram-G4DWMVQ6-DPcraMfu.js → diagram-G4DWMVQ6-r8h3Ip9O.js} +1 -1
  129. package/payload/server/public/assets/{diagram-MMDJMWI5-DR6luhGK.js → diagram-MMDJMWI5-CECcpGtr.js} +1 -1
  130. package/payload/server/public/assets/{diagram-TYMM5635-Jap1B4wA.js → diagram-TYMM5635-CHOcAyIP.js} +1 -1
  131. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DJKUs2hO.js → erDiagram-SMLLAGMA-YIqKdPbS.js} +1 -1
  132. package/payload/server/public/assets/{file-download-DBMW0BNz.js → file-download-CU0xHXGF.js} +1 -1
  133. package/payload/server/public/assets/{flatten-Cl1Bc3dR.js → flatten-DhYYI_lo.js} +1 -1
  134. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-C8W-ZZ9W.js → flowDiagram-DWJPFMVM-DaEvQWpj.js} +1 -1
  135. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DYCX4Xu2.js → ganttDiagram-T4ZO3ILL-x1hClEOB.js} +1 -1
  136. package/payload/server/public/assets/gitGraph-7Q5UKJZL-V7Tx7_eY.js +1 -0
  137. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CZu_fpgS.js → gitGraphDiagram-UUTBAWPF-C-sZsh6Z.js} +1 -1
  138. package/payload/server/public/assets/{graph-CR74qec3.js → graph-BqdF9YLX.js} +1 -1
  139. package/payload/server/public/assets/{graph-labels-1Uz0ZBRd.js → graph-labels-Bv6MHqfG.js} +1 -1
  140. package/payload/server/public/assets/{graphlib-Bdp7TA4y.js → graphlib-CNkAqm2w.js} +1 -1
  141. package/payload/server/public/assets/info-OMHHGYJF-D3DqVPGn.js +1 -0
  142. package/payload/server/public/assets/infoDiagram-42DDH7IO-B_O-R0vN.js +2 -0
  143. package/payload/server/public/assets/{isEmpty-D1pGOD1J.js → isEmpty-DGhq_DBP.js} +1 -1
  144. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-B7KfIX6z.js → ishikawaDiagram-UXIWVN3A-wHE-RwzZ.js} +1 -1
  145. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CSDwBfhF.js → journeyDiagram-VCZTEJTY-DQB6iThz.js} +1 -1
  146. package/payload/server/public/assets/jsx-runtime-DuiIXAPx.css +1 -0
  147. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CWzcEKTr.js → kanban-definition-6JOO6SKY-Ck08vXSA.js} +1 -1
  148. package/payload/server/public/assets/{line-BNQSlbYn.js → line-Cdc2FBEU.js} +1 -1
  149. package/payload/server/public/assets/{linear-BN6kGN93.js → linear-DhtUm5H7.js} +1 -1
  150. package/payload/server/public/assets/{mermaid-parser.core-CcFhkElq.js → mermaid-parser.core-CSYulPIL.js} +2 -2
  151. package/payload/server/public/assets/{mermaid.core-BsIeJ7Cc.js → mermaid.core-CP-hOmMh.js} +3 -3
  152. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CQVzj6D_.js → mindmap-definition-QFDTVHPH-GxlkvV1Z.js} +1 -1
  153. package/payload/server/public/assets/operator-lvmlNtX0.js +1 -0
  154. package/payload/server/public/assets/{ordinal-CBZeH4Yp.js → ordinal-CibZU3PM.js} +1 -1
  155. package/payload/server/public/assets/packet-4T2RLAQJ-BY7mgCTQ.js +1 -0
  156. package/payload/server/public/assets/page-lv61ROf4.js +1 -0
  157. package/payload/server/public/assets/pie-ZZUOXDRM-B9f6jN54.js +1 -0
  158. package/payload/server/public/assets/{pieDiagram-DEJITSTG-DnrQ9fyn.js → pieDiagram-DEJITSTG-Di6kWh9c.js} +1 -1
  159. package/payload/server/public/assets/{public-W-HholkU.js → public-B3qyifbI.js} +1 -1
  160. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BQWlCyWi.js → quadrantDiagram-34T5L4WZ-iNHC5bkO.js} +1 -1
  161. package/payload/server/public/assets/radar-PYXPWWZC-BKS8JOUe.js +1 -0
  162. package/payload/server/public/assets/{reduce-PuPlFPRX.js → reduce-Da0RBva0.js} +1 -1
  163. package/payload/server/public/assets/{requirementDiagram-MS252O5E-Du_vZhU1.js → requirementDiagram-MS252O5E-BBlLREXo.js} +1 -1
  164. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-Cf6Z8GPQ.js → sankeyDiagram-XADWPNL6-qyDkP-Ca.js} +1 -1
  165. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-CS8GVol8.js → sequenceDiagram-FGHM5R23-C0MJuS0m.js} +1 -1
  166. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-Bv1NW2F1.js → stateDiagram-FHFEXIEX-aHTrlosa.js} +1 -1
  167. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-V2vo4BjY.js +1 -0
  168. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-ClGYAsr0.js → timeline-definition-GMOUNBTQ-BlRZIxEp.js} +1 -1
  169. package/payload/server/public/assets/treeView-SZITEDCU-bIKzyIdw.js +1 -0
  170. package/payload/server/public/assets/treemap-W4RFUUIX-CQycQ4H9.js +1 -0
  171. package/payload/server/public/assets/{useSelectionMode-geYq13zs.js → useSelectionMode-FzQBlPMx.js} +1 -1
  172. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-C2wTcxQT.js → vennDiagram-DHZGUBPP-s76adysx.js} +1 -1
  173. package/payload/server/public/assets/wardley-RL74JXVD-COr9o-nG.js +1 -0
  174. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-D86-ivEx.js → wardleyDiagram-NUSXRM2D-xC8t2QGW.js} +1 -1
  175. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-Ba5WAN8P.js → xychartDiagram-5P7HB3ND-DRcHg-Wj.js} +1 -1
  176. package/payload/server/public/browser.html +6 -6
  177. package/payload/server/public/chat.html +10 -9
  178. package/payload/server/public/data.html +5 -5
  179. package/payload/server/public/graph.html +8 -8
  180. package/payload/server/public/index.html +9 -10
  181. package/payload/server/public/operator.html +11 -10
  182. package/payload/server/public/public.html +7 -7
  183. package/payload/server/server.js +51 -6
  184. package/payload/server/public/assets/AdminShell-BFdBxg4P.js +0 -1
  185. package/payload/server/public/assets/admin-BK7xIr3o.js +0 -1
  186. package/payload/server/public/assets/architecture-YZFGNWBL-6g9jRVgc.js +0 -1
  187. package/payload/server/public/assets/channel-BgQLJanG.js +0 -1
  188. package/payload/server/public/assets/chat-ZTAhSoJV.js +0 -1
  189. package/payload/server/public/assets/chunk-426QAEUC-Dr_XVuUq.js +0 -1
  190. package/payload/server/public/assets/chunk-QZHKN3VN-DF4o9HRJ.js +0 -1
  191. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DAbPKU6u.js +0 -1
  192. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BKuFQLcv.js +0 -1
  193. package/payload/server/public/assets/clone-DnNHGhNe.js +0 -1
  194. package/payload/server/public/assets/gitGraph-7Q5UKJZL-g7mvu6IY.js +0 -1
  195. package/payload/server/public/assets/info-OMHHGYJF-DZqJuIkB.js +0 -1
  196. package/payload/server/public/assets/infoDiagram-42DDH7IO-cGfGccHz.js +0 -2
  197. package/payload/server/public/assets/jsx-runtime-BqGVUT1h.css +0 -1
  198. package/payload/server/public/assets/operator-QP_z97s8.js +0 -1
  199. package/payload/server/public/assets/packet-4T2RLAQJ-Hn8rlHpy.js +0 -1
  200. package/payload/server/public/assets/page-CLijftr1.js +0 -1
  201. package/payload/server/public/assets/pie-ZZUOXDRM-BPGWVqBm.js +0 -1
  202. package/payload/server/public/assets/radar-PYXPWWZC-DMdLrj3E.js +0 -1
  203. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BGUCxPmp.js +0 -1
  204. package/payload/server/public/assets/treeView-SZITEDCU-CWxofUbI.js +0 -1
  205. package/payload/server/public/assets/treemap-W4RFUUIX-Cfgb_ZG0.js +0 -1
  206. package/payload/server/public/assets/wardley-RL74JXVD-B45olYjj.js +0 -1
  207. /package/payload/server/public/assets/{page-CWMpccrR.css → 500-CWMpccrR.css} +0 -0
  208. /package/payload/server/public/assets/{_baseFor-brRjpUOX.js → _baseFor-BFYpZGkN.js} +0 -0
  209. /package/payload/server/public/assets/{array-BGFCBI0e.js → array-CYkMkqnU.js} +0 -0
  210. /package/payload/server/public/assets/{cytoscape.esm-TvRJ2tGX.js → cytoscape.esm-DqlHsaog.js} +0 -0
  211. /package/payload/server/public/assets/{defaultLocale-CRZydyG6.js → defaultLocale-Beh6XjaL.js} +0 -0
  212. /package/payload/server/public/assets/{dist-Dbh7HrZX.js → dist-t9p-NJH2.js} +0 -0
  213. /package/payload/server/public/assets/{init-B8gtcn7T.js → init-Rr1s_RiX.js} +0 -0
  214. /package/payload/server/public/assets/{jsx-runtime-D3JkFIbi.js → jsx-runtime-Brvwqc85.js} +0 -0
  215. /package/payload/server/public/assets/{katex-RxgSu_dy.js → katex-pVJiI_rc.js} +0 -0
  216. /package/payload/server/public/assets/{path-DZF-JdEe.js → path-BAQ3hXlG.js} +0 -0
  217. /package/payload/server/public/assets/{preload-helper-CQ36nxok.js → preload-helper-B_l5jfgx.js} +0 -0
  218. /package/payload/server/public/assets/{rough.esm-6CnTHTkH.js → rough.esm-nHaDi0Kw.js} +0 -0
  219. /package/payload/server/public/assets/{src-BoaldmnP.js → src-DuPQVw-H.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.
@@ -49,7 +49,7 @@ The agent loads these references on demand via `plugin-read` as the conversation
49
49
 
50
50
  ### Success contract
51
51
 
52
- The setup-done claim only fires when `curl -I https://<hostname>` issued from outside the local network returns `HTTP/2 200` (or `HTTP/1.1 200 OK`) and the response is surfaced verbatim in chat. No state file, no service-active claim, no `cloudflared` exit code substitutes for the live HTTP response. When the curl returns anything else, the agent diagnoses with `cloudflared tunnel info <tunnelId>` and `systemctl --user status ${BRAND}-cloudflared.service`, cites the relevant step in `references/reset-guide.md`, and stops.
52
+ The setup-done claim only fires when `curl -I https://<hostname>` issued from outside the local network returns `HTTP/2 200` (or `HTTP/1.1 200 OK`) and the response is surfaced verbatim in chat. No state file, no service-active claim, no `cloudflared` exit code substitutes for the live HTTP response. When the curl returns anything else, the agent diagnoses with `cloudflared tunnel info <tunnelId>` and `systemctl --user status cloudflared-${BRAND}.service`, cites the relevant step in `references/reset-guide.md`, and stops.
53
53
 
54
54
  ## Identity model
55
55
 
@@ -2,7 +2,7 @@
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
 
@@ -237,7 +237,7 @@ ingress:
237
237
  EOF
238
238
  ```
239
239
 
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. 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.
241
241
 
242
242
  **Success:** Confirm with:
243
243
 
@@ -429,7 +429,7 @@ A non-530 response means the tunnel is live.
429
429
 
430
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.).
431
431
 
432
- **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.
433
433
 
434
434
  **Observability.** A connector death is now a logged, countable event, not an invisible gap:
435
435
 
@@ -440,9 +440,13 @@ journalctl --user -u cloudflared-<brand>.service | grep "Scheduled restart job"
440
440
 
441
441
  # Confirm auto-update is off — cloudflared prints its effective settings at startup:
442
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)
443
447
  ```
444
448
 
445
- `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`.
446
450
 
447
451
  **Durability prerequisites (one-time per device):**
448
452
 
@@ -472,7 +476,7 @@ systemctl --user restart "cloudflared-${BRAND}.service"
472
476
 
473
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.
474
478
 
475
- **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).
476
480
 
477
481
  **Success:**
478
482
 
@@ -565,6 +569,8 @@ systemctl --user restart "cloudflared-${BRAND}.service"
565
569
 
566
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.
567
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
+
568
574
  ### Verification
569
575
 
570
576
  ```
@@ -601,7 +607,7 @@ systemctl --user restart "cloudflared-${BRAND}.service"
601
607
 
602
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.
603
609
 
604
- **The operator hostname is arbitrary** — any FQDN the operator chooses (e.g. `glsops.siteoffice.online`). Task 829 removed the `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).
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).
605
611
 
606
612
  Take the operator FQDN from chat:
607
613
 
@@ -640,7 +646,7 @@ The UI logs `[operator-domains] reloaded <n> operator domain(s): …` within ~2
640
646
 
641
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).
642
648
 
643
- > Installer auto-provisioning of this host is out of scope here (documented manual/scripted add only); Task 825 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.
644
650
 
645
651
  ---
646
652
 
@@ -683,7 +689,7 @@ If `admin.<yourdomain>` is rendering the public-agent UI, the tunnel is fine but
683
689
  - the hostname starts with `public.`, or
684
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).
685
691
 
686
- 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.
687
693
 
688
694
  **Diagnose:**
689
695
 
@@ -41,7 +41,7 @@ rm -rf "${CFG_DIR}"
41
41
  Then stop the brand's cloudflared user service so the now-deleted tunnel's connector exits:
42
42
 
43
43
  ```
44
- systemctl --user stop "${BRAND}-cloudflared.service" 2>/dev/null || true
44
+ systemctl --user stop "cloudflared-${BRAND}.service" 2>/dev/null || true
45
45
  ```
46
46
 
47
47
  ### What it does not do
@@ -30,7 +30,7 @@ Neither the master token nor any per-scope token is ever written into a project
30
30
 
31
31
  Every Cloudflare operation proves itself with a live behavioural signal surfaced verbatim in chat:
32
32
 
33
- - **Tunnel:** `curl -I https://<hostname>` from outside the local network returns `HTTP/2 200` (or `HTTP/1.1 200 OK`). No state file, no `result=ok` line, no "service is active" claim substitutes. If the curl returns anything else, diagnose with `cloudflared tunnel info <tunnelId>` and `systemctl --user status ${BRAND}-cloudflared.service` and recover per `references/manual-setup.md`.
33
+ - **Tunnel:** `curl -I https://<hostname>` from outside the local network returns `HTTP/2 200` (or `HTTP/1.1 200 OK`). No state file, no `result=ok` line, no "service is active" claim substitutes. If the curl returns anything else, diagnose with `cloudflared tunnel info <tunnelId>` and `systemctl --user status cloudflared-${BRAND}.service` and recover per `references/manual-setup.md`.
34
34
  - **Hosting:** the deployed Pages URL returns the new build (`curl -s https://<project>.pages.dev/ | head` shows the new markup).
35
35
  - **D1 capture:** a test POST to the live form appears in `wrangler d1 execute <db> --remote --command "SELECT … WHERE swept = 0"`.
36
36
 
@@ -38,6 +38,8 @@ Every Cloudflare operation proves itself with a live behavioural signal surfaced
38
38
 
39
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
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
+
41
43
  ## Host classes — three peers on one brand UI port
42
44
 
43
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.
@@ -62,7 +64,8 @@ Tunnel mutations the agent performs from `references/manual-setup.md` (each one
62
64
  - `cloudflared tunnel --origincert <cert> route dns <tunnelId> <hostname>` — one call per non-apex hostname.
63
65
  - For a public hostname not starting with `public.`, append it to `~/.${BRAND}/alias-domains.json` so `isPublicHost()` treats it as public.
64
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".
65
- - Install + start the `${BRAND}-cloudflared.service` user unit (`systemctl --user daemon-reload && systemctl --user enable --now ${BRAND}-cloudflared.service`).
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.
68
+ - The `cloudflared-${BRAND}.service` connector is **not** a unit the operator installs or enables. `resume-tunnel.sh` — the brand service's `ExecStartPre` — spawns it as a transient `systemd-run --unit=cloudflared-${BRAND}.service` service every time `${BRAND}.service` starts. So the tunnel comes up with the brand service: there is no separate `daemon-reload` or `enable --now` step for the connector. To pick up a freshly written `config.yml`, restart the brand service via the cgroup-safe arming pattern in `references/manual-setup.md` (never a direct `systemctl --user restart ${BRAND}.service` from the agent's own Bash).
66
69
 
67
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.
68
71
 
@@ -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
 
@@ -44,20 +44,20 @@ either is a regression.
44
44
  |---|---|---|
45
45
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
46
46
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
47
- | `/sidebar-sessions` | Sole data path for the sidebar Sessions list (Tasks 538 + 543). One JSONL on disk equals one row. The row's delete button (Task 543) is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone (Task 543) — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. | `GET /` |
48
- | `/session-delete` | POST `{ sessionId, projectDir }` (Task 543). Best-effort SIGTERM of the live PID (resolved from `sessions/<pid>.json` body match) then unlink the JSONL + `<sid>.meta.json` sidecar. Absent PID file is not an error. Containment: `projectDir` must live under `<CLAUDE_CONFIG_DIR>/projects/`. | `POST /` |
49
- | `/session-rc-spawn` | POST `{ sessionId?, name? }` (Task 543). Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (Task 648, raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. The Sidebar navigates the opened tab to `claude.ai/code/session_<id>` on `outcome=bound`; on `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
50
- | `/claude-sessions` | **Spawn surface only** (Task 500). `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (Task 626 removed the recorder loopback caller; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
47
+ | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. | `GET /` |
48
+ | `/session-delete` | POST `{ sessionId, projectDir }`. Best-effort SIGTERM of the live PID (resolved from `sessions/<pid>.json` body match) then unlink the JSONL + `<sid>.meta.json` sidecar. Absent PID file is not an error. Containment: `projectDir` must live under `<CLAUDE_CONFIG_DIR>/projects/`. | `POST /` |
49
+ | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. The Sidebar navigates the opened tab to `claude.ai/code/session_<id>` on `outcome=bound`; on `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
50
+ | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
51
51
 
52
- Task 500 — **admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. **Task 820** adds the third inception lever, permission mode: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the Task 798 spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
52
+ **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
53
53
 
54
54
  **Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
55
55
 
56
- **Task 785 — `/chat` empty-state greeting panel.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`) above two read-only lists from `GET /api/webchat/greeting` (`server/routes/webchat-greeting.ts`, `requireAdminSession`-gated, `accountId`-scoped): the next 5 upcoming `:Event` rows (`eventStatus IN ['scheduled','due']`, future `COALESCE(nextRun, startDate)`, soonest first) and the outstanding `:Task` rows (`status IN ['pending','active','running']`, 5 shown + `+N more`). Read failure returns HTTP 200 `{ ok: false }` and the page shows the headline alone — the panel never blocks the composer. **Task 792** adds a third section, **Your specialists**: the account's installed roster — every `*.md` under `data/accounts/<accountId>/specialists/agents/`, core and `--`-prefixed premium files alike — shown by frontmatter `name` + `summary` (fallback `description`), empty dir → "No specialists installed". The roster read (`app/lib/claude-agent/specialist-roster.ts`) is isolated from the graph fallback: malformed files are skipped (`op=specialist-skipped file=<name> reason=<…>`), a whole-roster failure degrades to `specialists: []` inside the `ok: true` payload (`op=specialists-read-failed`). Logs: `[webchat:greeting] op=read account=<id8> events=<n> tasks=<n> specialists=<n> ms=<n>` / `op=read-failed reason=…`; client warns `[admin-ui] greeting-unavailable …`. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
56
+ **`/chat` empty-state greeting panel.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`) above two read-only lists from `GET /api/webchat/greeting` (`server/routes/webchat-greeting.ts`, `requireAdminSession`-gated, `accountId`-scoped): the next 5 upcoming `:Event` rows (`eventStatus IN ['scheduled','due']`, future `COALESCE(nextRun, startDate)`, soonest first) and the outstanding `:Task` rows (`status IN ['pending','active','running']`, 5 shown + `+N more`). Read failure returns HTTP 200 `{ ok: false }` and the page shows the headline alone — the panel never blocks the composer. A third section, **Your specialists**, shows the account's installed roster — every `*.md` under `data/accounts/<accountId>/specialists/agents/`, core and `--`-prefixed premium files alike — shown by frontmatter `name` + `summary` (fallback `description`), empty dir → "No specialists installed". The roster read (`app/lib/claude-agent/specialist-roster.ts`) is isolated from the graph fallback: malformed files are skipped (`op=specialist-skipped file=<name> reason=<…>`), a whole-roster failure degrades to `specialists: []` inside the `ok: true` payload (`op=specialists-read-failed`). Logs: `[webchat:greeting] op=read account=<id8> events=<n> tasks=<n> specialists=<n> ms=<n>` / `op=read-failed reason=…`; client warns `[admin-ui] greeting-unavailable …`. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
57
57
 
58
- **Task 799 — `/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble with only the message text (no label, no per-turn time); all agent output renders as plain prose (the reply-document filename line stays, as prose); a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx` unmodified, `app/chat/__tests__/transcript-render.test.tsx` new).
58
+ **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble with only the message text (no label, no per-turn time); all agent output renders as plain prose (the reply-document filename line stays, as prose); a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx` unmodified, `app/chat/__tests__/transcript-render.test.tsx` new).
59
59
 
60
- **Task 624 — maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the bridge (`dispatchOnce` → `managerSpawn`) to the manager `POST /spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
60
+ **maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the bridge (`dispatchOnce` → `managerSpawn`) to the manager `POST /spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
61
61
 
62
62
  ### Graph
63
63
 
@@ -74,13 +74,13 @@ Task 500 — **admin session management moved entirely to claude's own interface
74
74
 
75
75
  | Mount | Purpose |
76
76
  |---|---|
77
- | `/sidebar-artefacts` | Lists the sidebar Artefacts rows: every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'` — all file types, excluding the separate `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates (Task 684). |
77
+ | `/sidebar-artefacts` | Lists the sidebar Artefacts rows: every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'` — all file types, excluding the separate `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. |
78
78
  | `/sidebar-artefact-content` | Reads a single artefact's bytes for the artefact pane. |
79
79
  | `/sidebar-artefact-save` | Persists an artefact edit. |
80
80
  | `/attachment` | Per-attachment binary fetch (images, PDFs, etc.). |
81
81
  | `/files` | File browser CRUD (list, download, upload, delete). Listings put directories first, then files newest-first by `mtime` (name tie-break) so a just-changed file leads the panel. |
82
82
 
83
- **Artefact download resolution (Task 524).** Clicking a sidebar Artefacts row
83
+ **Artefact download resolution.** Clicking a sidebar Artefacts row
84
84
  streams the `KnowledgeDocument`'s real backing file, which can live in one of
85
85
  three on-disk classes: the admin-UI upload store (`<DATA_ROOT>/uploads/<acc>/
86
86
  <attachmentId>/`), the agent-authored output dir (`<DATA_ROOT>/accounts/<acc>/
@@ -106,7 +106,7 @@ snapping back to the data root; the hash never reaches the server and `/data`
106
106
  has no client router, so the channel is isolated. `path=` is cleared at the
107
107
  root for a clean URL.
108
108
 
109
- **`/data` search — locate-in-files link and clear button (Task 791).** Every
109
+ **`/data` search — locate-in-files link and clear button.** Every
110
110
  search hit is a `:FileArtifact` backed by a physical file, so each result card
111
111
  whose `relativePath` is a non-empty string carries a **locate link** (folder
112
112
  icon + containing folder; root-level files show `data`). Clicking it clears the
@@ -125,12 +125,12 @@ button** while it holds text — clicking empties the input, clears results
125
125
 
126
126
  | Mount | Purpose |
127
127
  |---|---|
128
- | `/browser` | `POST /launch` — launches Chromium on the resolved transport (native display or VNC) on demand. Backs the standalone `/browser` operator page (Task 665). Lifeline tag `[admin/browser/launch]`. |
128
+ | `/browser` | `POST /launch` — launches Chromium on the resolved transport (native display or VNC) on demand. Backs the standalone `/browser` operator page. Lifeline tag `[admin/browser/launch]`. |
129
129
 
130
130
  The in-chat / artefact-embedded brand-VNC iframe components and the
131
- `/browser-iframe` event-beacon route were retired in Task 287 and stay
131
+ `/browser-iframe` event-beacon route were retired and stay
132
132
  deleted; the `/device-browser` (navigate-the-device-tab) route was also
133
- removed in Task 287 and is not restored. The standalone `/browser` page is a
133
+ removed and is not restored. The standalone `/browser` page is a
134
134
  sanctioned operator surface peer to `/graph` and `/data`: it embeds the live
135
135
  VNC viewer (`/vnc-viewer.html`, popout `/vnc-popout.html`) and calls
136
136
  `/api/admin/browser/launch`. On darwin (no noVNC asset) it shows a "VNC surface
@@ -229,16 +229,25 @@ documented in
229
229
  [`platform.md`](platform.md) "The Web Interface" — that paragraph is
230
230
  authoritative. This section names the surfaces and what backs each.
231
231
 
232
+ > **Admin variant only.** Every surface below belongs to the `admin` shell
233
+ > variant. The **operator** variant (`operator.<host>`) mounts no sidebar at
234
+ > all: its dashboard collapsed to two surfaces — the persistent admin webchat
235
+ > at `/` and the data browser at `/data` — with a three-item header (Chat,
236
+ > Data, Log out) and a single-column `.platform.platform-operator` grid. The
237
+ > operator never sees the sessions list, the WhatsApp reader, or the nav rows.
238
+ > The operator surface is documented in full in the internal doc
239
+ > `maxy-code/.docs/operator-dashboard.md`.
240
+
232
241
  | Surface | What it does | Backed by |
233
242
  |---|---|---|
234
243
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
235
244
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
236
245
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
237
- | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every resumable row (not `resumable === false` public rows) carries two launch actions (Task 800): the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). | `/claude-sessions/events`, `/claude-sessions` |
238
- | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session** (Task 738). Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title` (Task 747): the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
239
- | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom (Task 747). The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn (Task 746), sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
246
+ | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every resumable row (not `resumable === false` public rows) carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). | `/claude-sessions/events`, `/claude-sessions` |
247
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
248
+ | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
240
249
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
241
- | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree — Task 684) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal (Task 518). | `sidebar-artefacts.ts`, `files.ts` |
250
+ | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal. | `sidebar-artefacts.ts`, `files.ts` |
242
251
  | System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
243
252
  | Footer | Operator avatar, name, role, and the actions popover. | `session.ts` |
244
253
 
@@ -75,7 +75,7 @@ Diagnostic path: `grep -E '^\[aeo-' platform-logs/*.log | grep <urlOrEntityId>`.
75
75
 
76
76
  ## What this plugin does not do
77
77
 
78
- - **No citation monitor — out of scope.** Tracking whether your brand is cited by Claude / ChatGPT / Perplexity / Gemini would require multi-engine answer harvesting that doesn't fit maxy-code's no-API-key architecture. Archived without sprinting (Task 363). Check citation manually when needed.
78
+ - **No citation monitor — out of scope.** Tracking whether your brand is cited by Claude / ChatGPT / Perplexity / Gemini would require multi-engine answer harvesting that doesn't fit maxy-code's no-API-key architecture. Archived without sprinting. Check citation manually when needed.
79
79
  - **No auto-emission on page render.** `aeo-emit-jsonld` is callable on demand. Wiring it into the platform's page-generator render path is per-renderer work, filed as a follow-up.
80
80
  - **No publish-hook regeneration of `llms.txt`.** The tool runs on demand. Hooking it into the publish event is a follow-up.
81
81