@rubytech/create-maxy-code 0.1.158 → 0.1.159

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 (233) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts +1 -1
  3. package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/admin-conversation-purge/dist/index.js +4 -4
  5. package/payload/platform/lib/admin-conversation-purge/dist/index.js.map +1 -1
  6. package/payload/platform/lib/admin-conversation-purge/src/index.ts +5 -5
  7. package/payload/platform/lib/graph-style/dist/index.js +2 -2
  8. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  9. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +1 -1
  10. package/payload/platform/lib/graph-style/src/index.ts +2 -2
  11. package/payload/platform/lib/graph-trash/dist/index.js +1 -1
  12. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  13. package/payload/platform/lib/graph-trash/src/index.ts +1 -1
  14. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +1 -1
  15. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -1
  16. package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts +3 -3
  17. package/payload/platform/lib/graph-write/dist/conversation-provenance.js +5 -5
  18. package/payload/platform/lib/graph-write/dist/conversation-provenance.js.map +1 -1
  19. package/payload/platform/lib/graph-write/dist/index.js +1 -1
  20. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  21. package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +1 -1
  22. package/payload/platform/lib/graph-write/src/conversation-provenance.ts +8 -8
  23. package/payload/platform/lib/graph-write/src/index.ts +1 -1
  24. package/payload/platform/neo4j/schema.cypher +30 -13
  25. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  26. package/payload/platform/plugins/admin/PLUGIN.md +8 -4
  27. package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +1 -1
  28. package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +361 -0
  29. package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +13 -13
  30. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +1 -1
  31. package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +102 -0
  32. package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +106 -0
  33. package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +15 -8
  34. package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +4 -4
  35. package/payload/platform/plugins/admin/mcp/dist/index.js +65 -24
  36. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  37. package/payload/platform/plugins/admin/skills/session-management/SKILL.md +2 -2
  38. package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +5 -5
  39. package/payload/platform/plugins/admin/skills/stream-log-review/references/analysis-patterns.md +6 -6
  40. package/payload/platform/plugins/business-assistant/references/profiling.md +1 -1
  41. package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
  42. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +2 -2
  43. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
  44. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-erase.js +3 -3
  45. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.d.ts +1 -1
  46. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-export.js +2 -2
  47. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts +1 -1
  48. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
  49. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +4 -4
  50. package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
  51. package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js +10 -10
  52. package/payload/platform/plugins/contacts/mcp/dist/tools/group-manage.js.map +1 -1
  53. package/payload/platform/plugins/docs/references/admin-session.md +6 -6
  54. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  55. package/payload/platform/plugins/docs/references/internals.md +5 -5
  56. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -3
  57. package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
  58. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  59. package/payload/platform/plugins/email/PLUGIN.md +15 -2
  60. package/payload/platform/plugins/email/mcp/dist/index.js +53 -0
  61. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  62. package/payload/platform/plugins/email/mcp/dist/lib/embedding.js +2 -2
  63. package/payload/platform/plugins/email/mcp/dist/lib/embedding.js.map +1 -1
  64. package/payload/platform/plugins/email/mcp/dist/lib/graph.js +1 -1
  65. package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
  66. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +1 -1
  67. package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.d.ts +23 -0
  68. package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.d.ts.map +1 -0
  69. package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.js +15 -0
  70. package/payload/platform/plugins/email/mcp/dist/lib/ingest-batch.js.map +1 -0
  71. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts +15 -0
  72. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts.map +1 -0
  73. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js +49 -0
  74. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js.map +1 -0
  75. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts +20 -0
  76. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -0
  77. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +86 -0
  78. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -0
  79. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.d.ts.map +1 -1
  80. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js +32 -52
  81. package/payload/platform/plugins/email/mcp/dist/tools/email-setup.js.map +1 -1
  82. package/payload/platform/plugins/email/references/email-reference.md +19 -83
  83. package/payload/platform/plugins/email/skills/email-ingest/SKILL.md +87 -0
  84. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  85. package/payload/platform/plugins/memory/mcp/dist/index.js +29 -29
  86. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  87. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js +2 -2
  88. package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/x-dm.js.map +1 -1
  89. package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.d.ts +3 -3
  90. package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.d.ts.map +1 -1
  91. package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.js +6 -6
  92. package/payload/platform/plugins/memory/mcp/dist/lib/graph-prune.js.map +1 -1
  93. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +1 -1
  94. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  95. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +2 -2
  96. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +2 -2
  97. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts +1 -1
  98. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-pass.d.ts.map +1 -1
  99. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.js +3 -3
  100. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-memory-expunge-emit.test.js.map +1 -1
  101. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js +2 -2
  102. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js.map +1 -1
  103. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js +8 -8
  104. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js.map +1 -1
  105. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +2 -2
  106. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -1
  107. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.d.ts +1 -1
  108. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.d.ts.map +1 -1
  109. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js +2 -2
  110. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js.map +1 -1
  111. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts +2 -2
  112. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
  113. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +4 -4
  114. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
  115. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +1 -1
  116. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +8 -8
  117. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  118. package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts +1 -1
  119. package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts.map +1 -1
  120. package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js +2 -2
  121. package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js.map +1 -1
  122. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +1 -1
  123. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +1 -1
  124. package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.d.ts +1 -1
  125. package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.d.ts.map +1 -1
  126. package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.js +2 -2
  127. package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.js.map +1 -1
  128. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +2 -2
  129. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  130. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +1 -1
  131. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
  132. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +11 -11
  133. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  134. package/payload/platform/plugins/memory/mcp/scripts/backfill-typed-edges.ts +1 -1
  135. package/payload/platform/plugins/memory/mcp/scripts/graph/fixture.cypher +1 -1
  136. package/payload/platform/plugins/memory/references/graph-primitives.md +1 -1
  137. package/payload/platform/plugins/memory/references/schema-base.md +4 -4
  138. package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +1 -1
  139. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  140. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +2 -2
  141. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  142. package/payload/platform/plugins/work/mcp/dist/index.js +2 -2
  143. package/payload/platform/plugins/work/mcp/dist/index.js.map +1 -1
  144. package/payload/platform/plugins/work/mcp/dist/tools/project-create.js +6 -6
  145. package/payload/platform/plugins/work/mcp/dist/tools/project-create.js.map +1 -1
  146. package/payload/platform/plugins/work/mcp/dist/tools/session-list.d.ts +1 -1
  147. package/payload/platform/plugins/work/mcp/dist/tools/session-list.d.ts.map +1 -1
  148. package/payload/platform/plugins/work/mcp/dist/tools/session-list.js +1 -1
  149. package/payload/platform/plugins/work/mcp/dist/tools/session-list.js.map +1 -1
  150. package/payload/platform/plugins/x-import/references/archive-shape.md +2 -2
  151. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +3 -3
  152. package/payload/platform/scripts/admin-persist-audit.ts +18 -18
  153. package/payload/platform/scripts/identity-forbidden-token-check.mjs +2 -0
  154. package/payload/platform/scripts/seed-neo4j.sh +1 -0
  155. package/payload/platform/services/claude-session-manager/dist/http-server.js +3 -3
  156. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  157. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +3 -3
  158. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +4 -4
  159. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  160. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  161. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +2 -0
  162. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  163. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  164. package/payload/server/{chunk-AXOL2PPG.js → chunk-3TRIXQSJ.js} +101 -101
  165. package/payload/server/maxy-edge.js +1 -1
  166. package/payload/server/public/assets/{admin-3fLFD5h_.js → admin-Dal5FWaD.js} +2 -2
  167. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Breg9-99.js → architectureDiagram-Q4EWVU46-Do2zYONG.js} +1 -1
  168. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DXf1_673.js → blockDiagram-DXYQGD6D-DpIX_bRu.js} +1 -1
  169. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CSZ36W4-.js → c4Diagram-AHTNJAMY-Cz335ZSY.js} +1 -1
  170. package/payload/server/public/assets/channel-PC4_IjI1.js +1 -0
  171. package/payload/server/public/assets/{chunk-336JU56O-DsrLk2eK.js → chunk-336JU56O-Dmknn3Wo.js} +2 -2
  172. package/payload/server/public/assets/{chunk-426QAEUC-JpNfNf81.js → chunk-426QAEUC-ClfGjizN.js} +1 -1
  173. package/payload/server/public/assets/{chunk-4TB4RGXK-DmClEAD0.js → chunk-4TB4RGXK-BPKctJ5c.js} +1 -1
  174. package/payload/server/public/assets/{chunk-5FUZZQ4R-BI9blW3N.js → chunk-5FUZZQ4R-BWDCO9zw.js} +1 -1
  175. package/payload/server/public/assets/{chunk-5PVQY5BW-BbgttEFO.js → chunk-5PVQY5BW-BE34_Db0.js} +1 -1
  176. package/payload/server/public/assets/{chunk-EDXVE4YY-BX_LCFOx.js → chunk-EDXVE4YY-BysA-_6v.js} +1 -1
  177. package/payload/server/public/assets/{chunk-ENJZ2VHE-Bb7EoD0w.js → chunk-ENJZ2VHE-BMVyMsqA.js} +1 -1
  178. package/payload/server/public/assets/{chunk-ICPOFSXX-DH8uQk-s.js → chunk-ICPOFSXX-C5KRdLV4.js} +1 -1
  179. package/payload/server/public/assets/{chunk-OYMX7WX6-FH5kgXsw.js → chunk-OYMX7WX6-DVZD-AVc.js} +1 -1
  180. package/payload/server/public/assets/{chunk-U2HBQHQK-D_ljlkHr.js → chunk-U2HBQHQK-DYmQdx8x.js} +1 -1
  181. package/payload/server/public/assets/{chunk-X2U36JSP-BX3EmZvm.js → chunk-X2U36JSP-DrZenIl3.js} +1 -1
  182. package/payload/server/public/assets/{chunk-YZCP3GAM-Vu7-E93h.js → chunk-YZCP3GAM-CDuGbvMq.js} +1 -1
  183. package/payload/server/public/assets/{chunk-ZZ45TVLE-CwjhNPX5.js → chunk-ZZ45TVLE-COztbiu5.js} +1 -1
  184. package/payload/server/public/assets/classDiagram-6PBFFD2Q-B_pMa-SX.js +1 -0
  185. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BdBAgF20.js +1 -0
  186. package/payload/server/public/assets/clone-CkNt9L9r.js +1 -0
  187. package/payload/server/public/assets/{dagre-Ce6gsj0C.js → dagre-Cv9Ib2OM.js} +1 -1
  188. package/payload/server/public/assets/{dagre-KV5264BT-BaMGpsoy.js → dagre-KV5264BT-CoEd61KZ.js} +1 -1
  189. package/payload/server/public/assets/{data-BHASh0_Y.js → data-DbSORCl4.js} +1 -1
  190. package/payload/server/public/assets/{diagram-5BDNPKRD-CHEE9rjq.js → diagram-5BDNPKRD-WXNzw3uW.js} +1 -1
  191. package/payload/server/public/assets/{diagram-G4DWMVQ6-C98sZLW1.js → diagram-G4DWMVQ6-C6IuQ2VV.js} +1 -1
  192. package/payload/server/public/assets/{diagram-MMDJMWI5-DNvMGsTZ.js → diagram-MMDJMWI5-CdJdir5A.js} +1 -1
  193. package/payload/server/public/assets/{diagram-TYMM5635-Cdte4IBa.js → diagram-TYMM5635-DxWKQH5T.js} +1 -1
  194. package/payload/server/public/assets/{erDiagram-SMLLAGMA-C9VZsWXK.js → erDiagram-SMLLAGMA-C9fvs33K.js} +1 -1
  195. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-tuP5fzzR.js → flowDiagram-DWJPFMVM-BO7Yrbqv.js} +1 -1
  196. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-SJgRPUwx.js → ganttDiagram-T4ZO3ILL-CQJyWBZq.js} +1 -1
  197. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-DU_cI_50.js → gitGraphDiagram-UUTBAWPF-DZfQHrYw.js} +1 -1
  198. package/payload/server/public/assets/{graph-labels-DXlmm1og.js → graph-labels-BBB5jsZk.js} +1 -1
  199. package/payload/server/public/assets/{graph-BhEI09tF.js → graph-ys_joP2o.js} +1 -1
  200. package/payload/server/public/assets/{graphlib-D_nAsvmx.js → graphlib-C40H-Q5E.js} +1 -1
  201. package/payload/server/public/assets/{infoDiagram-42DDH7IO-Cj2Y19m6.js → infoDiagram-42DDH7IO-CakfV5f9.js} +1 -1
  202. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DMc1xq1y.js → ishikawaDiagram-UXIWVN3A-CPuBPyfa.js} +1 -1
  203. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-C4CRAQma.js → journeyDiagram-VCZTEJTY-zpZm2Mmo.js} +1 -1
  204. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CTr4AldU.js → kanban-definition-6JOO6SKY-DfJduQCo.js} +1 -1
  205. package/payload/server/public/assets/{line-Btlm_IAl.js → line-BpYQqO5R.js} +1 -1
  206. package/payload/server/public/assets/{mermaid-parser.core-DXFdj_y1.js → mermaid-parser.core-C5Mwi14D.js} +1 -1
  207. package/payload/server/public/assets/{mermaid.core-DFBhoCr5.js → mermaid.core-D8qBtvvT.js} +3 -3
  208. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-r9gVQybo.js → mindmap-definition-QFDTVHPH-Bli9JhIN.js} +1 -1
  209. package/payload/server/public/assets/{page-CDWu1Aft.js → page--bl5Y7ZI.js} +2 -2
  210. package/payload/server/public/assets/page-CwmZ76U7.js +1 -0
  211. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CHKZjSaH.js → pieDiagram-DEJITSTG-MsLXTSLg.js} +1 -1
  212. package/payload/server/public/assets/{public-DyGWgfaZ.js → public-DltgEfA9.js} +2 -2
  213. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CSGR1HJr.js → quadrantDiagram-34T5L4WZ-De1sh2K0.js} +1 -1
  214. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DZ6hX9D6.js → requirementDiagram-MS252O5E-D5Sfg1t_.js} +1 -1
  215. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DbZBKh0m.js → sankeyDiagram-XADWPNL6-DHA3LIbv.js} +1 -1
  216. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BHHpb8dt.js → sequenceDiagram-FGHM5R23-BE_QlG1B.js} +1 -1
  217. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DQ1jiLB4.js → stateDiagram-FHFEXIEX-NIwp3F4u.js} +1 -1
  218. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-pfk2F6st.js +1 -0
  219. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BlGiEU7p.js → timeline-definition-GMOUNBTQ-CPE9jyDF.js} +1 -1
  220. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DmEH-hsB.js → vennDiagram-DHZGUBPP-CKfoyYmH.js} +1 -1
  221. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C7KJJ40d.js → wardleyDiagram-NUSXRM2D-B9yJek-o.js} +1 -1
  222. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-JDnusCaa.js → xychartDiagram-5P7HB3ND-DuCcSLqX.js} +1 -1
  223. package/payload/server/public/data.html +3 -3
  224. package/payload/server/public/graph.html +3 -3
  225. package/payload/server/public/index.html +4 -4
  226. package/payload/server/public/public.html +1 -1
  227. package/payload/server/server.js +133 -133
  228. package/payload/server/public/assets/channel-BmZnt3PP.js +0 -1
  229. package/payload/server/public/assets/classDiagram-6PBFFD2Q-kmMLFUpV.js +0 -1
  230. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BV7Ae8ct.js +0 -1
  231. package/payload/server/public/assets/clone-B5eqoN9I.js +0 -1
  232. package/payload/server/public/assets/page-cyvshX4Z.js +0 -1
  233. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CByT2RzF.js +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "admin",
3
- "description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform.",
3
+ "description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective), and pre-turn-graph-pass-skip (the deterministic decline sentinel the per-turn graph-pass Stop hook reads via JSONL grep to release the per-turn gate when the operator turn has no new writes; admin calls it instead of dispatching to database-operator) for managing the Maxy platform.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: admin
3
3
  surface: platform
4
- description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform."
4
+ description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective), and pre-turn-graph-pass-skip (the deterministic decline sentinel the per-turn graph-pass Stop hook reads via JSONL grep to release the per-turn gate when the operator turn has no new writes; admin calls it instead of dispatching to database-operator) for managing the Maxy platform."
5
5
  tools:
6
6
  - name: system-status
7
7
  publicAllowlist: false
@@ -75,6 +75,9 @@ tools:
75
75
  - name: session-retrospective-mark-complete
76
76
  publicAllowlist: false
77
77
  adminAllowlist: true
78
+ - name: pre-turn-graph-pass-skip
79
+ publicAllowlist: false
80
+ adminAllowlist: true
78
81
  hidden:
79
82
  - remote-auth-status
80
83
  - remote-auth-set-password
@@ -110,7 +113,7 @@ Tools are available via the `admin` MCP server.
110
113
 
111
114
  `logs-read { type: "agent-stream" }` is the canonical name for the per-conversation tool-use/tool-result archive previously called `system`; both names work and the legacy alias is preserved.
112
115
 
113
- **Stream log is the primary diagnostic surface for an in-progress session.** Every stream log is named by sessionKey and the stream-log file exists from the first token. The single-writer mandate (2026-05-14) mechanically enforces this contract: the single writer module at [`platform/ui/app/lib/claude-agent/stream-log-writer.ts`](../../ui/app/lib/claude-agent/stream-log-writer.ts) owns every open of `claude-agent-stream-*.log`, opens the file lazily on the first SDK token byte (via `streamLog.writeToken`), and buffers pre-token markers in memory while duplicating them to `server.log` via `console.error` so zero-token sessions retain observability. The build gate `platform/ui/scripts/check-stream-log-writer.mjs` rejects any external `appendFileSync`/`createWriteStream` against the `claude-agent-stream-*` pattern outside the writer module, plus any `claude-agent-stream-${conversationId}` literal (the pre-1013 wrong-identifier signature that produced two files for one session on the Beacons Pi 2026-05-14). The first-token invariant is bound by `platform/scripts/__tests__/first-token-creates-stream-log.test.sh`: one operator turn, one token, `claude-agent-stream-<sessionKey>.log` exists and contains the token bytes after the byte returns to the operator. The parent-process console fan-out tee in [`platform/ui/app/lib/claude-agent/logging.ts`](../../ui/app/lib/claude-agent/logging.ts) appends every `[<tag>]`-prefixed `console.error` / `console.log` line to every active session's stream log alongside `server.log`. For diagnosing an in-session issue (WhatsApp inbound, Cloudflare action, persist write, baileys error), call `logs-read { sessionKey: "<…>" }` first — the stream log carries both the agent lifecycle AND the parent-process events that occurred during the session window. The `conversationId` parameter remains a legacy alias resolving to the same sessionKey-named file. `logs-read { type: "server" }` is the cross-session escape hatch for events outside any single session window.
116
+ **Stream log is the primary diagnostic surface for an in-progress session.** Every stream log is named by sessionKey and the stream-log file exists from the first token. The single-writer mandate (2026-05-14) mechanically enforces this contract: the single writer module at [`platform/ui/app/lib/claude-agent/stream-log-writer.ts`](../../ui/app/lib/claude-agent/stream-log-writer.ts) owns every open of `claude-agent-stream-*.log`, opens the file lazily on the first SDK token byte (via `streamLog.writeToken`), and buffers pre-token markers in memory while duplicating them to `server.log` via `console.error` so zero-token sessions retain observability. The build gate `platform/ui/scripts/check-stream-log-writer.mjs` rejects any external `appendFileSync`/`createWriteStream` against the `claude-agent-stream-*` pattern outside the writer module, plus any `claude-agent-stream-${sessionId}` literal (the pre-1013 wrong-identifier signature that produced two files for one session on the Beacons Pi 2026-05-14). The first-token invariant is bound by `platform/scripts/__tests__/first-token-creates-stream-log.test.sh`: one operator turn, one token, `claude-agent-stream-<sessionKey>.log` exists and contains the token bytes after the byte returns to the operator. The parent-process console fan-out tee in [`platform/ui/app/lib/claude-agent/logging.ts`](../../ui/app/lib/claude-agent/logging.ts) appends every `[<tag>]`-prefixed `console.error` / `console.log` line to every active session's stream log alongside `server.log`. For diagnosing an in-session issue (WhatsApp inbound, Cloudflare action, persist write, baileys error), call `logs-read { sessionKey: "<…>" }` first — the stream log carries both the agent lifecycle AND the parent-process events that occurred during the session window. The `sessionId` parameter remains a legacy alias resolving to the same sessionKey-named file. `logs-read { type: "server" }` is the cross-session escape hatch for events outside any single session window.
114
117
 
115
118
  ## Skills
116
119
 
@@ -139,8 +142,9 @@ Tools are available via the `admin` MCP server.
139
142
  - `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
140
143
  - `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
141
144
  - `hooks/session-end-retrospective.sh` — **Stop hook.** Fires on every assistant end_turn but exits 0 with `trigger-skipped reason=<role-not-admin|is-specialist|empty-stdin|missing-transcript|no-intent-match>` on every path EXCEPT when the operator's latest real-user message carries an end-intent token (`/end`, `/archive`, `end session`, `archive this session`, case-insensitive, must match the whole message or a standalone line — not embedded in prose). On that path the hook blocks (exit 2 + structured retrospective instruction on stderr per Stop-hook contract) and emits `[session-retrospective] gate-blocked sessionId=<id> reason=sentinel-absent` until the admin agent calls the `session-retrospective-mark-complete` MCP sentinel tool. Completion is recognised by greping the operator's own JSONL (`transcript_path` on the Stop envelope) for a `tool_use` block whose `name` equals that exact string — never by parsing prose ([[feedback_no_stdout_parsing_for_control_flow]], [[feedback_doctrine_paragraph_is_not_a_gate]]). The sentinel-grep doubles as the re-entry guard: every Stop after the sentinel call sees it and exits 0 with `gate-released sessionId=<id>`. Recursion guard: any session whose `MAXY_SPECIALIST` env is set (specialist subagent dispatched via Task tool) skips the gate. The Sidebar archive button POSTs `/:sessionId/archive` directly to the manager which kills the PTY — no Stop fires after that, so clicking archive is the operator's current escape hatch (opt-out remains deferred per the task spec). All emits go through `POST /api/admin/log-ingest`; the only stderr writer is the gate-blocked path (the instruction block the agent reads). The retrospective itself runs as one or more additional turns inside the operator's existing admin session — Task-tool delegation to `database-operator` is the existing IDENTITY.md "Recording" route, not a parallel admin session ([[feedback_deterministic_means_remove_llm]]).
142
- - `hooks/pre-turn-graph-pass.sh` — **UserPromptSubmit hook.** Tasks 425, 430 supersedes Task 412's Stop-hook registration of the same logic. Task 412 fired on every admin-agent `end_turn`, including the agent's own ack after the dispatch, producing an unbounded loop until context exhaustion. Firing on `UserPromptSubmit` fires once per operator prompt and removes the loop primitive entirely. **Task 430 reshaped the orchestration:** admin is the session-walker. The hook no longer packages the conversation into the specialist's prompt that collapsed the data channel and instruction channel into one context, so operator prose pattern-matching as a writer-binding instruction ("memory-update is denied this session") was treated as binding and the pass self-denied (session `d7070b66-1759-41ca-a771-d7559b8a1967` retrospective wrote 3 graph rows on the same session where the pre-turn pass returned `writes=0`). The Task 425 Task 430 shape change brings the per-turn pass into line with the session-end retrospective, which never had this defect: admin walks, admin dispatches per-write briefs to `database-operator`. The hook now `GET /api/admin/post-turn-context?conversationId&accountId` to fetch every node already written under this conversationId, then emits the documented UserPromptSubmit stdout JSON envelope `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"<directive>"}}` with `exit 0`. The directive body begins with the literal token `[system: pre-turn-graph-pass-hook]` and carries one interpolated `<prior-writes>` block (the skip-list); the IDENTITY.md UserPromptSubmit-directive section routes the agent into scanning the just-submitted operator turn (already in admin's context) and dispatching one `Task → database-operator` per candidate write before composing its reply. Coverage trade: the assistant response to the most recent operator prompt is not covered by a per-turn pass until the next operator turn (or never, if no next turn arrives); end-of-session coverage is provided by `session-end-retrospective.sh` which walks the full transcript at `/end`. Duplicate writes between per-turn dispatches and the admin agent's discretionary inline `database-operator` dispatch are accepted at write time; cleanup is delegated to the dream-cycle / hygiene sweeps (Tasks 410, 411). Early exits: `reason=role-not-admin` (the hook fires only on admin-agent prompt submissions by doctrine), `reason=is-specialist` (`MAXY_SPECIALIST` env-stamped on every subagent PTY blocks the hook from firing inside a specialist's own session), `reason=missing-transcript` (safety exit), `reason=wrong-hook-event event=<…>` (stale Stop-shaped fire), `reason=missing-env env=MAXY_UI_INTERNAL_PORT` (UI port unset). All emits go through `POST /api/admin/log-ingest` under tag `pre-turn-graph-pass`. The canonical fire line is `dispatch sessionId=<id> conversationId=<id> directive-shape=admin-walks priorWritesCount=<n> contextBytes=<n> ms=<n>`; the `directive-shape=admin-walks` field distinguishes the Task 430 shape from any cached device still running the Task 425 shape. Success: one or more `db-op: writes=<n>` lines in the admin session transcript reflecting per-write dispatches admin actually emitted (zero candidate writes ⇒ zero dispatches, no `db-op` line). Failure: a `db-op: error reason=<…>` line, or a turn that named new entities / commitments where admin emitted no dispatch — the latter is the regression signal for self-denial.
143
- - `hooks/turn-completed-graph-write.sh` — **Dormant since Task 214**the Stop-hook registration is no longer written to account settings.json; admin delegates graph writes to `database-operator` via the Task tool. The script, envelope walker, loopback `/api/admin/claude-sessions` 127.0.0.1 bypass, `[turn-recorder]` emitters, recorder-auto-archive subscriber, and `initialMessage` envelope spec are preserved as reusable infrastructure for any future autonomous post-turn flow. Historical contract (still describes the dormant path): Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** `initialMessage` is a JSON-stringified envelope; the schema lives in [`platform/plugins/docs/references/admin-session.md`](../docs/references/admin-session.md) under "`initialMessage` JSON envelope (Task 177)". Top-level keys exactly: `turns`, `conversationId`, `accountId`, `occurredAt`. `turns` is the full conversation transcript, oldest first, newest last, with each entry `{ role: "user"|"assistant", text, ts, toolCalls? }`. No windowing, no truncation. Multi-record assistant messages collapse on `message.id`. `tool_use` and matching `tool_result` blocks attach as a single `toolCalls` entry on the owning assistant turn; the user record carrying only the `tool_result` does not create a separate user turn. No leading instruction prose. `toolCalls[].input` and `toolCalls[].output` are native JSON values, never re-stringified. (Replaces Task 175's `(operatorMessage, assistantReply)` pair, which asserted a temporal Q→A relationship the walker never enforced.) One observability line `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` precedes `spawn-request`. The envelope rides on the `/spawn` body as a trailing positional argv to `claude`, so the database-operator session's JSONL first `role=user` line is the JSON object verbatim. No separate `POST /:id/input` call, no bracketed-paste, no keystroke injection. The recorder-auto-archive subscriber stops the recorder PTY as soon as its JSONL contains an assistant message with `stop_reason === "end_turn"`. **Task 129** every emit goes through `POST /api/admin/log-ingest` so the lines land in `server.log` keyed by the operator session id. The chain is `trigger` `spawn-request` `spawn-success` `input-delivered` `tool-call` × N → `tool-result` × N `write-complete` `auto-archive`; each gated-off path emits one `trigger-skipped reason=<role-not-admin|is-recorder|empty-stdin|missing-transcript|conversation-empty>` line. Failure modes `spawn-failed`, `tool-surface-missing`, `input-failed`, `write-empty`, `auto-archive reason=stale-recorder` each emit one named line; absence is itself a defect.
145
+ - `hooks/per-turn-graph-pass-gate.sh` — **Stop hook.** Task 440. Per-turn blocking gate that closes the gap left when Task 425 moved the per-turn graph pass off Stop onto `UserPromptSubmit`. `pre-turn-graph-pass.sh` delivers the directive (with prior-writes skip-list) as `additionalContext` and exits 0 that channel can't carry a release path because UserPromptSubmit's only blocking shape erases the prompt without feeding stderr to Claude ([[feedback_doctrine_paragraph_is_not_a_gate]]). This Stop hook is the enforcement primitive: walks the operator's JSONL since the latest real-user message, releases on either a `Task` `tool_use` whose `input.subagent_type` is `database-operator` (admin dispatched the writes) or a `tool_use` whose `name` is `mcp__admin__pre-turn-graph-pass-skip` (admin declined via the deterministic skip sentinel). Both releases emit `gate-released sessionId=<id> via=<dispatch|skip-sentinel>` and exit 0. Absent both, exits 2 with a stderr instruction naming the two release paths admin reads the instruction (Stop's exit-2 stderr-to-Claude contract) and acts. Recursion guard: any session with `MAXY_SPECIALIST` set (specialist subagent dispatched via Task) skips. Loop-safety: once either release tool_use sits in the JSONL, every subsequent Stop on the same operator-prompt window sees it and exits 0 the Task 412 loop is avoided because the release decision is JSONL-based, never a re-fire of the directive. Composes with `session-end-retrospective.sh` (the other Stop-array entry): the session-end hook gates only on operator end-intent, this one gates every turn. Early exits: `reason=role-not-admin`, `reason=is-specialist`, `reason=empty-stdin`, `reason=missing-transcript`, `reason=no-real-user` (transcript has no real-user record yet — e.g. SessionStart bootstrap, or only `tool_result`-shaped user records). All emits go through `POST /api/admin/log-ingest` under tag `per-turn-graph-pass-gate`.
146
+ - `hooks/pre-turn-graph-pass.sh` — **UserPromptSubmit hook.** Tasks 425, 430 supersedes Task 412's Stop-hook registration of the same logic. Task 412 fired on every admin-agent `end_turn`, including the agent's own ack after the dispatch, producing an unbounded loop until context exhaustion. Firing on `UserPromptSubmit` fires once per operator prompt and removes the loop primitive entirely. **Task 430 reshaped the orchestration:** admin is the session-walker. The hook no longer packages the conversation into the specialist's prompt that collapsed the data channel and instruction channel into one context, so operator prose pattern-matching as a writer-binding instruction ("memory-update is denied this session") was treated as binding and the pass self-denied (session `d7070b66-1759-41ca-a771-d7559b8a1967` retrospective wrote 3 graph rows on the same session where the pre-turn pass returned `writes=0`). The Task 425 Task 430 shape change brings the per-turn pass into line with the session-end retrospective, which never had this defect: admin walks, admin dispatches per-write briefs to `database-operator`. The hook now `GET /api/admin/post-turn-context?sessionId&accountId` to fetch every node already written under this sessionId, then emits the documented UserPromptSubmit stdout JSON envelope `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"<directive>"}}` with `exit 0`. The directive body begins with the literal token `[system: pre-turn-graph-pass-hook]` and carries one interpolated `<prior-writes>` block (the skip-list); the IDENTITY.md UserPromptSubmit-directive section routes the agent into scanning the just-submitted operator turn (already in admin's context) and dispatching one `Task database-operator` per candidate write before composing its reply. Enforcement is the Stop-array sibling `per-turn-graph-pass-gate.sh` (Task 440): every admin turn end blocks until admin either dispatched the writes (`Task` `tool_use` with `input.subagent_type=database-operator`) or called the skip sentinel (`pre-turn-graph-pass-skip` MCP tool with a `reason` arg) since the latest real-user message. Duplicate writes between per-turn dispatches and the admin agent's discretionary inline `database-operator` dispatch are accepted at write time; cleanup is delegated to the dream-cycle / hygiene sweeps (Tasks 410, 411). Early exits: `reason=role-not-admin` (the hook fires only on admin-agent prompt submissions by doctrine), `reason=is-specialist` (`MAXY_SPECIALIST` env-stamped on every subagent PTY blocks the hook from firing inside a specialist's own session), `reason=missing-transcript` (safety exit), `reason=wrong-hook-event event=<…>` (stale Stop-shaped fire), `reason=missing-env env=MAXY_UI_INTERNAL_PORT` (UI port unset). All emits go through `POST /api/admin/log-ingest` under tag `pre-turn-graph-pass`. The canonical fire line is `dispatch sessionId=<id> sessionId=<id> directive-shape=admin-walks priorWritesCount=<n> contextBytes=<n> ms=<n>`; the `directive-shape=admin-walks` field distinguishes the Task 430 shape from any cached device still running the Task 425 shape. Success: one or more `db-op: writes=<n>` lines in the admin session transcript reflecting per-write dispatches admin actually emitted (zero candidate writes zero dispatches, no `db-op` line). Failure: a `db-op: error reason=<…>` line, or a turn that named new entities / commitments where admin emitted no dispatchthe latter is the regression signal for self-denial.
147
+ - `hooks/turn-completed-graph-write.sh` — **Dormant since Task 214** — the Stop-hook registration is no longer written to account settings.json; admin delegates graph writes to `database-operator` via the Task tool. The script, envelope walker, loopback `/api/admin/claude-sessions` 127.0.0.1 bypass, `[turn-recorder]` emitters, recorder-auto-archive subscriber, and `initialMessage` envelope spec are preserved as reusable infrastructure for any future autonomous post-turn flow. Historical contract (still describes the dormant path): Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** — the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` — no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) — no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** — `initialMessage` is a JSON-stringified envelope; the schema lives in [`platform/plugins/docs/references/admin-session.md`](../docs/references/admin-session.md) under "`initialMessage` JSON envelope (Task 177)". Top-level keys exactly: `turns`, `sessionId`, `accountId`, `occurredAt`. `turns` is the full conversation transcript, oldest first, newest last, with each entry `{ role: "user"|"assistant", text, ts, toolCalls? }`. No windowing, no truncation. Multi-record assistant messages collapse on `message.id`. `tool_use` and matching `tool_result` blocks attach as a single `toolCalls` entry on the owning assistant turn; the user record carrying only the `tool_result` does not create a separate user turn. No leading instruction prose. `toolCalls[].input` and `toolCalls[].output` are native JSON values, never re-stringified. (Replaces Task 175's `(operatorMessage, assistantReply)` pair, which asserted a temporal Q→A relationship the walker never enforced.) One observability line `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` precedes `spawn-request`. The envelope rides on the `/spawn` body as a trailing positional argv to `claude`, so the database-operator session's JSONL first `role=user` line is the JSON object verbatim. No separate `POST /:id/input` call, no bracketed-paste, no keystroke injection. The recorder-auto-archive subscriber stops the recorder PTY as soon as its JSONL contains an assistant message with `stop_reason === "end_turn"`. **Task 129** — every emit goes through `POST /api/admin/log-ingest` so the lines land in `server.log` keyed by the operator session id. The chain is `trigger` → `spawn-request` → `spawn-success` → `input-delivered` → `tool-call` × N → `tool-result` × N → `write-complete` → `auto-archive`; each gated-off path emits one `trigger-skipped reason=<role-not-admin|is-recorder|empty-stdin|missing-transcript|conversation-empty>` line. Failure modes — `spawn-failed`, `tool-surface-missing`, `input-failed`, `write-empty`, `auto-archive reason=stale-recorder` — each emit one named line; absence is itself a defect.
144
148
 
145
149
  ## Session identifiers (Task 135)
146
150
 
@@ -103,7 +103,7 @@ run_case "BYPASS: nested rows[0].archiveType=linkedin + top-level archiveType=wh
103
103
  2
104
104
 
105
105
  run_case "BYPASS: conversation.archiveType=linkedin + top-level archiveType=whatsapp-export → BLOCKED" \
106
- '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"conversation":{"archiveType":"linkedin-connections","conversationId":"x"},"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a","rows":[]}}' \
106
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"conversation":{"archiveType":"linkedin-connections","sessionId":"x"},"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a","rows":[]}}' \
107
107
  2
108
108
 
109
109
  # Plugin-source-edit path block must read tool_input.file_path top-level,
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env bash
2
+ # Task 440 — per-turn-graph-pass-gate Stop hook test suite.
3
+ #
4
+ # Mirrors session-end-retrospective.test.sh's listener-mock pattern. Local
5
+ # Python HTTP server stands in for /api/admin/log-ingest and records every
6
+ # POST so the assertions read every log line the hook emitted.
7
+ #
8
+ # Contract under test:
9
+ # - Role/specialist gates: exit 0, single trigger-skipped log line, no
10
+ # stderr.
11
+ # role-not-admin | is-specialist | empty-stdin | missing-transcript
12
+ # - No real-user record in transcript (e.g. SessionStart bootstrap, or
13
+ # only tool_result-shaped user records): exit 0, single
14
+ # `trigger-skipped reason=no-real-user` line, no stderr.
15
+ # - Block path — admin's turn since the latest real-user message contains
16
+ # no `Task(database-operator)` dispatch and no `pre-turn-graph-pass-skip`
17
+ # tool_use: exit 2, stderr carries the instruction block,
18
+ # `gate-blocked sessionId=<id> reason=sentinel-absent` emitted.
19
+ # - Release via dispatch — a `Task` tool_use with
20
+ # `input.subagent_type=database-operator` appears AFTER the latest
21
+ # real-user message: exit 0, `gate-released sessionId=<id> via=dispatch`.
22
+ # - Release via skip-sentinel — a `tool_use` named
23
+ # `mcp__admin__pre-turn-graph-pass-skip` appears AFTER the latest
24
+ # real-user message: exit 0,
25
+ # `gate-released sessionId=<id> via=skip-sentinel`.
26
+ # - Per-prompt scoping — a dispatch BEFORE the latest real-user message
27
+ # does NOT release the gate.
28
+ # - Re-entry — a second Stop fired after the release tool_use still
29
+ # releases (the JSONL grep is the re-entry guard, no loop).
30
+
31
+ set -u
32
+
33
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/per-turn-graph-pass-gate.sh"
34
+ if [[ ! -x "$HOOK" ]]; then
35
+ echo "FAIL: $HOOK not executable" >&2
36
+ exit 1
37
+ fi
38
+
39
+ OP_ID='aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'
40
+
41
+ TMPFILES=()
42
+ LISTENER_PIDS=()
43
+ cleanup_test_state() {
44
+ for pid in "${LISTENER_PIDS[@]:-}"; do
45
+ if [[ -n "$pid" ]]; then
46
+ kill "$pid" 2>/dev/null || true
47
+ wait "$pid" 2>/dev/null || true
48
+ fi
49
+ done
50
+ for f in "${TMPFILES[@]:-}"; do
51
+ [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
52
+ done
53
+ }
54
+ trap cleanup_test_state EXIT
55
+
56
+ PASS=0
57
+ FAIL=0
58
+ pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
59
+ fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
60
+
61
+ start_listener() {
62
+ REQ_LOG=$(mktemp); TMPFILES+=("$REQ_LOG")
63
+ LISTENER_PORT=$((39800 + RANDOM % 100))
64
+ python3 - "$LISTENER_PORT" "$REQ_LOG" <<'PY' &
65
+ import sys, http.server, json
66
+ port = int(sys.argv[1])
67
+ log_path = sys.argv[2]
68
+ class H(http.server.BaseHTTPRequestHandler):
69
+ def log_message(self, *a, **k): pass
70
+ def do_POST(self):
71
+ n = int(self.headers.get('Content-Length','0') or 0)
72
+ body = self.rfile.read(n).decode('utf-8','replace')
73
+ with open(log_path, 'a', encoding='utf-8') as f:
74
+ f.write(self.path + '\t' + body + '\n')
75
+ self.send_response(200)
76
+ self.send_header('Content-Type','application/json')
77
+ self.end_headers()
78
+ self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
79
+ http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()
80
+ PY
81
+ LISTENER_PIDS+=("$!")
82
+ for _ in $(seq 1 20); do
83
+ if curl -sS --max-time 1 -X POST "http://127.0.0.1:${LISTENER_PORT}/ping" -d '{}' >/dev/null 2>&1; then
84
+ break
85
+ fi
86
+ sleep 0.1
87
+ done
88
+ : > "$REQ_LOG"
89
+ }
90
+
91
+ run_hook() {
92
+ local role="$1"; local specialist="$2"; local stdin_json="$3"
93
+ local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
94
+ local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
95
+ printf '%s' "$stdin_json" | \
96
+ MAXY_SESSION_ROLE="$role" \
97
+ MAXY_SPECIALIST="$specialist" \
98
+ MAXY_UI_INTERNAL_PORT="$LISTENER_PORT" \
99
+ bash "$HOOK" >"$stdout_file" 2>"$stderr_file"
100
+ HOOK_RC=$?
101
+ HOOK_STDERR=$(cat "$stderr_file")
102
+ HOOK_STDOUT=$(cat "$stdout_file")
103
+ sleep 0.1
104
+ }
105
+
106
+ ingest_lines() {
107
+ grep -E '^/api/admin/log-ingest ' "$REQ_LOG" 2>/dev/null | python3 -c '
108
+ import sys, json
109
+ for raw in sys.stdin:
110
+ try:
111
+ _, body = raw.rstrip("\n").split("\t", 1)
112
+ d = json.loads(body)
113
+ if isinstance(d, dict) and isinstance(d.get("line"), str):
114
+ print(d["line"])
115
+ except Exception:
116
+ pass
117
+ ' || true
118
+ }
119
+
120
+ # write_transcript: builds JSONL from positional specs.
121
+ # Spec format: <role>|<text>|<extra>
122
+ # extra "" — plain text record
123
+ # extra "tool_result" — user record carrying only a tool_result block
124
+ # extra "tool_use:<NAME>" — assistant record with a tool_use block (no input)
125
+ # extra "task:<subagent_type>" — assistant record with a Task tool_use whose
126
+ # input.subagent_type equals <subagent_type>
127
+ write_transcript() {
128
+ local out="$1"; shift
129
+ python3 - "$out" "$@" <<'PY'
130
+ import sys, json
131
+ out = sys.argv[1]
132
+ spec = sys.argv[2:]
133
+ with open(out, "w", encoding="utf-8") as f:
134
+ for entry in spec:
135
+ role, text, extra = entry.split("|", 2)
136
+ rec = {"type": role, "timestamp": "2026-05-26T10:00:00.000Z"}
137
+ if role == "user":
138
+ if extra == "tool_result":
139
+ rec["message"] = {
140
+ "role": "user",
141
+ "content": [
142
+ {"type": "tool_result", "tool_use_id": "tu_x", "content": text or "ok"},
143
+ ],
144
+ }
145
+ else:
146
+ rec["message"] = {"role": "user", "content": text}
147
+ else:
148
+ content_blocks = []
149
+ if text:
150
+ content_blocks.append({"type": "text", "text": text})
151
+ if extra.startswith("tool_use:"):
152
+ tool_name = extra.split(":", 1)[1]
153
+ content_blocks.append({
154
+ "type": "tool_use",
155
+ "id": f"toolu_{tool_name}",
156
+ "name": tool_name,
157
+ "input": {},
158
+ })
159
+ elif extra.startswith("task:"):
160
+ subagent = extra.split(":", 1)[1]
161
+ content_blocks.append({
162
+ "type": "tool_use",
163
+ "id": f"toolu_task_{subagent}",
164
+ "name": "Task",
165
+ "input": {"subagent_type": subagent, "description": "test", "prompt": "test"},
166
+ })
167
+ rec["message"] = {
168
+ "id": f"msg_{role}_{hash(text) & 0xffff:04x}",
169
+ "role": "assistant",
170
+ "content": content_blocks,
171
+ }
172
+ f.write(json.dumps(rec) + "\n")
173
+ PY
174
+ }
175
+
176
+ envelope_for() {
177
+ python3 -c '
178
+ import json, sys
179
+ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
180
+ ' "$1" "$2"
181
+ }
182
+
183
+ INSTRUCTION_TOKEN='Before this turn closes, do the per-turn graph pass'
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # Case 1: role != admin → trigger-skipped reason=role-not-admin
187
+ # ---------------------------------------------------------------------------
188
+ start_listener
189
+ TR_BASIC=$(mktemp); TMPFILES+=("$TR_BASIC")
190
+ write_transcript "$TR_BASIC" "user|hello|" "assistant|hi|"
191
+ ENV_BASIC=$(envelope_for "$OP_ID" "$TR_BASIC")
192
+ : > "$REQ_LOG"
193
+ run_hook "public" "" "$ENV_BASIC"
194
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-1 rc=$HOOK_RC"
195
+ [[ -z "$HOOK_STDERR" ]] || fail "case-1 stderr must be empty"
196
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=role-not-admin$"; then
197
+ pass "case-1 role=public → trigger-skipped reason=role-not-admin"
198
+ else
199
+ fail "case-1 expected role-not-admin, got: $(ingest_lines)"
200
+ fi
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Case 2: MAXY_SPECIALIST set → trigger-skipped reason=is-specialist
204
+ # ---------------------------------------------------------------------------
205
+ : > "$REQ_LOG"
206
+ run_hook "admin" "database-operator" "$ENV_BASIC"
207
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-2 rc=$HOOK_RC"
208
+ [[ -z "$HOOK_STDERR" ]] || fail "case-2 stderr must be empty"
209
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=is-specialist specialist=database-operator$"; then
210
+ pass "case-2 specialist=database-operator → trigger-skipped is-specialist"
211
+ else
212
+ fail "case-2 expected is-specialist, got: $(ingest_lines)"
213
+ fi
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # Case 3: empty stdin → trigger-skipped reason=empty-stdin
217
+ # ---------------------------------------------------------------------------
218
+ : > "$REQ_LOG"
219
+ run_hook "admin" "" ""
220
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-3 rc=$HOOK_RC"
221
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=.* reason=empty-stdin$"; then
222
+ pass "case-3 empty stdin → trigger-skipped reason=empty-stdin"
223
+ else
224
+ fail "case-3 expected empty-stdin, got: $(ingest_lines)"
225
+ fi
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Case 4: missing transcript_path → trigger-skipped reason=missing-transcript
229
+ # ---------------------------------------------------------------------------
230
+ BAD_ENV=$(python3 -c 'import json,sys; print(json.dumps({"session_id": sys.argv[1]}))' "$OP_ID")
231
+ : > "$REQ_LOG"
232
+ run_hook "admin" "" "$BAD_ENV"
233
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-4 rc=$HOOK_RC"
234
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=missing-transcript$"; then
235
+ pass "case-4 missing transcript → trigger-skipped reason=missing-transcript"
236
+ else
237
+ fail "case-4 expected missing-transcript, got: $(ingest_lines)"
238
+ fi
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Case 5: no real-user record in transcript → trigger-skipped no-real-user
242
+ # ---------------------------------------------------------------------------
243
+ TR_NO_USER=$(mktemp); TMPFILES+=("$TR_NO_USER")
244
+ # Only a tool_result-shaped user record + an assistant record. The hook should
245
+ # treat this as "no real user yet" and trigger-skip.
246
+ write_transcript "$TR_NO_USER" "user|ok|tool_result" "assistant|booting|"
247
+ ENV_NO_USER=$(envelope_for "$OP_ID" "$TR_NO_USER")
248
+ : > "$REQ_LOG"
249
+ run_hook "admin" "" "$ENV_NO_USER"
250
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-5 rc=$HOOK_RC"
251
+ [[ -z "$HOOK_STDERR" ]] || fail "case-5 stderr must be empty"
252
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=no-real-user$"; then
253
+ pass "case-5 only tool_result-user records → trigger-skipped no-real-user"
254
+ else
255
+ fail "case-5 expected no-real-user, got: $(ingest_lines)"
256
+ fi
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # Case 6: gate-blocked — admin replied to operator without dispatching or
260
+ # calling the skip sentinel.
261
+ # ---------------------------------------------------------------------------
262
+ TR_BLOCKED=$(mktemp); TMPFILES+=("$TR_BLOCKED")
263
+ write_transcript "$TR_BLOCKED" "user|hello|" "assistant|hi there|"
264
+ ENV_BLOCKED=$(envelope_for "$OP_ID" "$TR_BLOCKED")
265
+ : > "$REQ_LOG"
266
+ run_hook "admin" "" "$ENV_BLOCKED"
267
+ [[ "$HOOK_RC" -eq 2 ]] || fail "case-6 expected rc=2, got $HOOK_RC"
268
+ if ! printf '%s' "$HOOK_STDERR" | grep -qF "$INSTRUCTION_TOKEN"; then
269
+ fail "case-6 stderr must contain instruction token; got: $HOOK_STDERR"
270
+ fi
271
+ if ingest_lines | grep -qE "^gate-blocked sessionId=${OP_ID} reason=sentinel-absent$"; then
272
+ pass "case-6 admin replied without dispatch/skip → gate-blocked + stderr instruction + rc=2"
273
+ else
274
+ fail "case-6 expected gate-blocked, got: $(ingest_lines)"
275
+ fi
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # Case 7: release via dispatch — admin issued Task(database-operator) AFTER
279
+ # the latest real-user message.
280
+ # ---------------------------------------------------------------------------
281
+ TR_DISPATCH=$(mktemp); TMPFILES+=("$TR_DISPATCH")
282
+ write_transcript "$TR_DISPATCH" \
283
+ "user|tell me about Alice|" \
284
+ "assistant|dispatching|task:database-operator" \
285
+ "assistant|done|"
286
+ ENV_DISPATCH=$(envelope_for "$OP_ID" "$TR_DISPATCH")
287
+ : > "$REQ_LOG"
288
+ run_hook "admin" "" "$ENV_DISPATCH"
289
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-7 expected rc=0, got $HOOK_RC"
290
+ [[ -z "$HOOK_STDERR" ]] || fail "case-7 stderr must be empty on release"
291
+ if ingest_lines | grep -qE "^gate-released sessionId=${OP_ID} via=dispatch$"; then
292
+ pass "case-7 Task(database-operator) after latest user → gate-released via=dispatch"
293
+ else
294
+ fail "case-7 expected gate-released via=dispatch, got: $(ingest_lines)"
295
+ fi
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # Case 8: release via skip-sentinel — admin called the skip MCP tool AFTER
299
+ # the latest real-user message.
300
+ # ---------------------------------------------------------------------------
301
+ TR_SKIP=$(mktemp); TMPFILES+=("$TR_SKIP")
302
+ write_transcript "$TR_SKIP" \
303
+ "user|thanks|" \
304
+ "assistant|noted|tool_use:mcp__admin__pre-turn-graph-pass-skip" \
305
+ "assistant|you're welcome|"
306
+ ENV_SKIP=$(envelope_for "$OP_ID" "$TR_SKIP")
307
+ : > "$REQ_LOG"
308
+ run_hook "admin" "" "$ENV_SKIP"
309
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-8 expected rc=0, got $HOOK_RC"
310
+ [[ -z "$HOOK_STDERR" ]] || fail "case-8 stderr must be empty on release"
311
+ if ingest_lines | grep -qE "^gate-released sessionId=${OP_ID} via=skip-sentinel$"; then
312
+ pass "case-8 skip-sentinel after latest user → gate-released via=skip-sentinel"
313
+ else
314
+ fail "case-8 expected gate-released via=skip-sentinel, got: $(ingest_lines)"
315
+ fi
316
+
317
+ # ---------------------------------------------------------------------------
318
+ # Case 9: per-prompt scoping — a dispatch BEFORE the latest real-user
319
+ # message does NOT release the current turn's gate.
320
+ # ---------------------------------------------------------------------------
321
+ TR_PRIOR=$(mktemp); TMPFILES+=("$TR_PRIOR")
322
+ write_transcript "$TR_PRIOR" \
323
+ "user|first turn|" \
324
+ "assistant|dispatching|task:database-operator" \
325
+ "assistant|done|" \
326
+ "user|second turn|" \
327
+ "assistant|hello again|"
328
+ ENV_PRIOR=$(envelope_for "$OP_ID" "$TR_PRIOR")
329
+ : > "$REQ_LOG"
330
+ run_hook "admin" "" "$ENV_PRIOR"
331
+ [[ "$HOOK_RC" -eq 2 ]] || fail "case-9 expected rc=2, got $HOOK_RC"
332
+ if ingest_lines | grep -qE "^gate-blocked sessionId=${OP_ID} reason=sentinel-absent$"; then
333
+ pass "case-9 dispatch before latest user does NOT release current turn → gate-blocked"
334
+ else
335
+ fail "case-9 expected gate-blocked, got: $(ingest_lines)"
336
+ fi
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # Case 10: re-entry — a release tool_use earlier in the same prompt window
340
+ # stays released on a second Stop fire (no loop).
341
+ # ---------------------------------------------------------------------------
342
+ TR_REENTRY=$(mktemp); TMPFILES+=("$TR_REENTRY")
343
+ write_transcript "$TR_REENTRY" \
344
+ "user|hi|" \
345
+ "assistant|dispatching|task:database-operator" \
346
+ "assistant|done|" \
347
+ "assistant|wrap-up reply|"
348
+ ENV_REENTRY=$(envelope_for "$OP_ID" "$TR_REENTRY")
349
+ : > "$REQ_LOG"
350
+ run_hook "admin" "" "$ENV_REENTRY"
351
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-10 expected rc=0, got $HOOK_RC"
352
+ if ingest_lines | grep -qE "^gate-released sessionId=${OP_ID} via=dispatch$"; then
353
+ pass "case-10 second Stop after dispatch still releases (no loop)"
354
+ else
355
+ fail "case-10 expected gate-released via=dispatch, got: $(ingest_lines)"
356
+ fi
357
+
358
+ # ---------------------------------------------------------------------------
359
+ echo
360
+ echo "per-turn-graph-pass-gate tests: $PASS passed, $FAIL failed"
361
+ [[ "$FAIL" -eq 0 ]]
@@ -26,13 +26,13 @@
26
26
  # "additionalContext":"..."}}` whose additionalContext begins with
27
27
  # `[system: pre-turn-graph-pass-hook]`, names `database-operator` as
28
28
  # the dispatch target, carries a `<prior-writes>` block with the
29
- # fetched node, interpolates conversationId from CONVERSATION_NODE_ID,
29
+ # fetched node, interpolates sessionId from SESSION_NODE_ID,
30
30
  # and carries NO `<conversation>` block (admin has the turn already).
31
31
  # Dispatch log line carries `directive-shape=admin-walks`.
32
32
  # - Prior-writes fetch fails (context endpoint 500): exit 0, dispatch
33
33
  # still fires, `<prior-writes>` block resolves to `(none)`.
34
- # - CONVERSATION_NODE_ID unset: exit 0, additionalContext names
35
- # `conversationId: <unset>`, and no GET to /api/admin/post-turn-context
34
+ # - SESSION_NODE_ID unset: exit 0, additionalContext names
35
+ # `sessionId: <unset>`, and no GET to /api/admin/post-turn-context
36
36
  # is attempted.
37
37
  # - UserPromptSubmit stdin-shape regression: a synthetic UserPromptSubmit
38
38
  # payload produces the documented stdout envelope; a stale Stop-shaped
@@ -125,7 +125,7 @@ PY
125
125
  }
126
126
 
127
127
  run_hook() {
128
- # Args: role, specialist, ui_port, conversation_node_id, stdin_json
128
+ # Args: role, specialist, ui_port, session_node_id, stdin_json
129
129
  local role="$1"; local specialist="$2"; local ui_port="$3"
130
130
  local conv="$4"; local stdin_json="$5"
131
131
  local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
@@ -137,7 +137,7 @@ run_hook() {
137
137
  env_cmd+=("MAXY_UI_INTERNAL_PORT=${ui_port}")
138
138
  fi
139
139
  if [[ -n "$conv" ]]; then
140
- env_cmd+=("CONVERSATION_NODE_ID=${conv}")
140
+ env_cmd+=("SESSION_NODE_ID=${conv}")
141
141
  fi
142
142
  env_cmd+=("ACCOUNT_ID=acct-test-0001")
143
143
  printf '%s' "$stdin_json" | "${env_cmd[@]}" bash "$HOOK" >"$stdout_file" 2>"$stderr_file"
@@ -328,14 +328,14 @@ fi
328
328
  if ! printf '%s' "$CTX_BODY" | grep -q 'database-operator'; then
329
329
  fail "case-5 additionalContext must name the database-operator dispatch target"
330
330
  fi
331
- if ! printf '%s' "$CTX_BODY" | grep -q "conversationId: ${CONV_ID}"; then
332
- fail "case-5 additionalContext must interpolate conversationId=${CONV_ID}"
331
+ if ! printf '%s' "$CTX_BODY" | grep -q "sessionId: ${CONV_ID}"; then
332
+ fail "case-5 additionalContext must interpolate sessionId=${CONV_ID}"
333
333
  fi
334
334
  if ! printf '%s' "$CTX_BODY" | grep -q 'KnowledgeDocument'; then
335
335
  fail "case-5 additionalContext <prior-writes> must include fetched labels"
336
336
  fi
337
- if ingest_lines | grep -qE "^dispatch sessionId=${OP_ID} conversationId=${CONV_ID} directive-shape=admin-walks "; then
338
- pass "case-5 dispatch path → exit 0 + JSON envelope with prior-writes, conversationId, admin-walks shape"
337
+ if ingest_lines | grep -qE "^dispatch claudeSessionId=${OP_ID} sessionId=${CONV_ID} directive-shape=admin-walks "; then
338
+ pass "case-5 dispatch path → exit 0 + JSON envelope with prior-writes, sessionId, admin-walks shape"
339
339
  else
340
340
  fail "case-5 expected dispatch log line with directive-shape=admin-walks, got: $(ingest_lines)"
341
341
  fi
@@ -356,7 +356,7 @@ else
356
356
  fi
357
357
 
358
358
  # ---------------------------------------------------------------------------
359
- # Case 7: CONVERSATION_NODE_ID unset → conversationId:<unset>, no context GET.
359
+ # Case 7: SESSION_NODE_ID unset → sessionId:<unset>, no context GET.
360
360
  # ---------------------------------------------------------------------------
361
361
  cat > "$CTX_FILE" <<'JSON'
362
362
  {"writes":[]}
@@ -365,12 +365,12 @@ JSON
365
365
  run_hook "admin" "" "$LISTENER_PORT" "" "$ENV_BASIC"
366
366
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-7 expected rc=0, got $HOOK_RC"
367
367
  CTX_BODY=$(additional_context)
368
- if ! printf '%s' "$CTX_BODY" | grep -q 'conversationId: <unset>'; then
369
- fail "case-7 additionalContext must contain 'conversationId: <unset>'"
368
+ if ! printf '%s' "$CTX_BODY" | grep -q 'sessionId: <unset>'; then
369
+ fail "case-7 additionalContext must contain 'sessionId: <unset>'"
370
370
  fi
371
371
  GET_COUNT=$(context_get_count)
372
372
  if [[ "$GET_COUNT" == "0" ]]; then
373
- pass "case-7 CONVERSATION_NODE_ID unset → no GET /api/admin/post-turn-context attempted"
373
+ pass "case-7 SESSION_NODE_ID unset → no GET /api/admin/post-turn-context attempted"
374
374
  else
375
375
  fail "case-7 expected zero context GETs, got: $GET_COUNT"
376
376
  fi
@@ -194,7 +194,7 @@ fi
194
194
  # --- Case 5 (Task 177): ordered-turns happy path -----------------------
195
195
  # Transcript with four turns: user-A, asst-A-reply, user-B, asst-B-reply.
196
196
  # Envelope `turns` array must carry exactly that order, oldest first.
197
- # Envelope shape: { turns, conversationId, accountId, occurredAt } — no
197
+ # Envelope shape: { turns, sessionId, accountId, occurredAt } — no
198
198
  # top-level operatorMessage / assistantReply.
199
199
  ORDERED_TRANSCRIPT=$(mktemp); TMPFILES+=("$ORDERED_TRANSCRIPT")
200
200
  {