@rubytech/create-maxy-code 0.1.83 → 0.1.85

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 (149) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh +142 -24
  3. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +131 -218
  4. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +85 -17
  5. package/payload/platform/plugins/cloudflare/PLUGIN.md +2 -11
  6. package/payload/platform/plugins/cloudflare/scripts/__tests__/setup-tunnel-display-precedence.test.sh +194 -0
  7. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +43 -21
  8. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +29 -10
  9. package/payload/platform/plugins/docs/references/cloudflare.md +10 -9
  10. package/payload/platform/scripts/seed-neo4j.sh +12 -0
  11. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  12. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +13 -18
  13. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  14. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  15. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +3 -1
  16. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +17 -7
  17. package/payload/server/{chunk-TFEIFU7F.js → chunk-BDFOTLPW.js} +1 -1
  18. package/payload/server/{chunk-W33UUYYR.js → chunk-BVDLDM6O.js} +1 -1
  19. package/payload/server/{chunk-VGMVEYX7.js → chunk-JRBCOVA4.js} +64 -5
  20. package/payload/server/{cloudflare-task-tracker-7P355PSD.js → cloudflare-task-tracker-M5ONAGUT.js} +2 -2
  21. package/payload/server/maxy-edge.js +2 -2
  22. package/payload/server/public/assets/Checkbox-B34vplI7.js +1 -0
  23. package/payload/server/public/assets/{admin-BHaOSIuo.js → admin-BkTlPuP0.js} +5 -5
  24. package/payload/server/public/assets/{arc-aUiRP9AS.js → arc-DMDAZHAN.js} +1 -1
  25. package/payload/server/public/assets/architecture-YZFGNWBL-COhEvUpo.js +1 -0
  26. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-B8o-_aT-.js → architectureDiagram-Q4EWVU46-DQMCKDU2.js} +1 -1
  27. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BcprLXNk.js → blockDiagram-DXYQGD6D-CqJNtiQY.js} +1 -1
  28. package/payload/server/public/assets/{brand-kwAWaknq.js → brand-HqTMr1bV.js} +1 -1
  29. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CAkKlTxa.js → c4Diagram-AHTNJAMY-BVAa1YL3.js} +1 -1
  30. package/payload/server/public/assets/channel-CZG4mVkt.js +1 -0
  31. package/payload/server/public/assets/{chunk-2KRD3SAO-BK3470lx.js → chunk-2KRD3SAO-Di4bO8ir.js} +1 -1
  32. package/payload/server/public/assets/chunk-336JU56O-XIsdqKyD.js +2 -0
  33. package/payload/server/public/assets/chunk-426QAEUC-Dw4mcMYK.js +1 -0
  34. package/payload/server/public/assets/{chunk-4BX2VUAB-BOvVdJLf.js → chunk-4BX2VUAB-DyEhFk-Z.js} +1 -1
  35. package/payload/server/public/assets/{chunk-4TB4RGXK-MLx8Jj8R.js → chunk-4TB4RGXK-BvrlG2FS.js} +1 -1
  36. package/payload/server/public/assets/{chunk-55IACEB6-BwZyF7vR.js → chunk-55IACEB6-BRJOZLpU.js} +1 -1
  37. package/payload/server/public/assets/{chunk-5FUZZQ4R-DFWTZhLd.js → chunk-5FUZZQ4R-3-UZ9191.js} +1 -1
  38. package/payload/server/public/assets/{chunk-5PVQY5BW-vTE3gFPo.js → chunk-5PVQY5BW-DOeY60Lp.js} +1 -1
  39. package/payload/server/public/assets/{chunk-67CJDMHE-D5bhMrtY.js → chunk-67CJDMHE-BG6-9r6c.js} +1 -1
  40. package/payload/server/public/assets/{chunk-7N4EOEYR-Si7Lgrwc.js → chunk-7N4EOEYR-BvMbitX7.js} +1 -1
  41. package/payload/server/public/assets/{chunk-AA7GKIK3-DMuHtDqO.js → chunk-AA7GKIK3-CE8mGBD5.js} +1 -1
  42. package/payload/server/public/assets/{chunk-BSJP7CBP-L79XKVcb.js → chunk-BSJP7CBP-DP7LTBll.js} +1 -1
  43. package/payload/server/public/assets/{chunk-CIAEETIT-C0O7Upmg.js → chunk-CIAEETIT-VfnIdN-h.js} +1 -1
  44. package/payload/server/public/assets/{chunk-EDXVE4YY-Bp1I_rQl.js → chunk-EDXVE4YY-BSbGSOxN.js} +1 -1
  45. package/payload/server/public/assets/{chunk-ENJZ2VHE-DsDBZ8WH.js → chunk-ENJZ2VHE-CszgAcWZ.js} +1 -1
  46. package/payload/server/public/assets/{chunk-FMBD7UC4-C_E43NFJ.js → chunk-FMBD7UC4-w-yBZsN2.js} +1 -1
  47. package/payload/server/public/assets/{chunk-FOC6F5B3-D9lWWHAu.js → chunk-FOC6F5B3-f9cFywhd.js} +1 -1
  48. package/payload/server/public/assets/{chunk-ICPOFSXX-BKO6vxzn.js → chunk-ICPOFSXX-rgw1I6pt.js} +2 -2
  49. package/payload/server/public/assets/{chunk-K5T4RW27-DuhsNH4c.js → chunk-K5T4RW27-B_ZUrFUq.js} +1 -1
  50. package/payload/server/public/assets/{chunk-KGLVRYIC-B4-A1Abi.js → chunk-KGLVRYIC-CcWTvRlI.js} +1 -1
  51. package/payload/server/public/assets/{chunk-LIHQZDEY-BxqgHRgT.js → chunk-LIHQZDEY-D-5-peQw.js} +1 -1
  52. package/payload/server/public/assets/{chunk-ORNJ4GCN-DEYQ5WaJ.js → chunk-ORNJ4GCN-B4Z5L25I.js} +1 -1
  53. package/payload/server/public/assets/{chunk-OYMX7WX6-D-ievGGw.js → chunk-OYMX7WX6-DL4v2Nx4.js} +1 -1
  54. package/payload/server/public/assets/chunk-QZHKN3VN-CWh_0JsP.js +1 -0
  55. package/payload/server/public/assets/{chunk-U2HBQHQK-D_5PbJhp.js → chunk-U2HBQHQK-CwMCwyfq.js} +1 -1
  56. package/payload/server/public/assets/{chunk-X2U36JSP-QmmoxQya.js → chunk-X2U36JSP-CvKfDVvq.js} +1 -1
  57. package/payload/server/public/assets/{chunk-XPW4576I-CBfZXZDB.js → chunk-XPW4576I-m1Y_r88I.js} +1 -1
  58. package/payload/server/public/assets/{chunk-YZCP3GAM-BXSWZZag.js → chunk-YZCP3GAM-D7vju2_U.js} +1 -1
  59. package/payload/server/public/assets/{chunk-ZZ45TVLE-oyzperET.js → chunk-ZZ45TVLE-sANOwJKn.js} +1 -1
  60. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BoqQ3pHg.js +1 -0
  61. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-FoKuBK7n.js +1 -0
  62. package/payload/server/public/assets/clone-CT8AM-bU.js +1 -0
  63. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-Br2gjtEO.js → cose-bilkent-S5V4N54A-BhtgY3T7.js} +1 -1
  64. package/payload/server/public/assets/{dagre-65Q3iWcL.js → dagre-CplqWwSs.js} +1 -1
  65. package/payload/server/public/assets/{dagre-KV5264BT-Cyc37LVU.js → dagre-KV5264BT-DNbl5g9o.js} +1 -1
  66. package/payload/server/public/assets/data-BIDM6J5V.js +1 -0
  67. package/payload/server/public/assets/{device-url-actions-BftdR4st.js → device-url-actions-nxh9PmIH.js} +1 -1
  68. package/payload/server/public/assets/{diagram-5BDNPKRD-DKItZE_3.js → diagram-5BDNPKRD-BCdMaYnY.js} +1 -1
  69. package/payload/server/public/assets/{diagram-G4DWMVQ6-DKlYJZby.js → diagram-G4DWMVQ6-8XhDBU0o.js} +1 -1
  70. package/payload/server/public/assets/{diagram-MMDJMWI5-DLqF5x2e.js → diagram-MMDJMWI5-M-RMQgtx.js} +1 -1
  71. package/payload/server/public/assets/{diagram-TYMM5635-Cdufrh7g.js → diagram-TYMM5635-BBHPIBvQ.js} +1 -1
  72. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D6eSXJWj.js → erDiagram-SMLLAGMA-D6jOL-9s.js} +1 -1
  73. package/payload/server/public/assets/{flatten-Bo6YRmWl.js → flatten-BsWEYbBB.js} +1 -1
  74. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BB4luljp.js → flowDiagram-DWJPFMVM-CiyTYRlU.js} +1 -1
  75. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-USBgsV4V.js → ganttDiagram-T4ZO3ILL-DHSIpjfS.js} +1 -1
  76. package/payload/server/public/assets/gitGraph-7Q5UKJZL-DeTNsAO0.js +1 -0
  77. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BqyKsQZ9.js → gitGraphDiagram-UUTBAWPF-CEneVF3o.js} +1 -1
  78. package/payload/server/public/assets/graph-Cm-iWFqy.js +1 -0
  79. package/payload/server/public/assets/graph-labels-CFX6KkCz.js +1 -0
  80. package/payload/server/public/assets/{graphlib-C5MkULnV.js → graphlib-CuF7rLfE.js} +1 -1
  81. package/payload/server/public/assets/info-OMHHGYJF-DJJ9GlS6.js +1 -0
  82. package/payload/server/public/assets/infoDiagram-42DDH7IO-wGl-JoP7.js +2 -0
  83. package/payload/server/public/assets/{isEmpty-D6Kr-M1M.js → isEmpty-BWl67LAZ.js} +1 -1
  84. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CODjk_-5.js → ishikawaDiagram-UXIWVN3A-B3TxFNi_.js} +1 -1
  85. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-RyhxP8aB.js → journeyDiagram-VCZTEJTY-B5n8No7p.js} +1 -1
  86. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-jrm_4IpR.js → kanban-definition-6JOO6SKY-ByGF39_h.js} +1 -1
  87. package/payload/server/public/assets/{line-DsE8JMaX.js → line-Br0FyyFW.js} +1 -1
  88. package/payload/server/public/assets/{linear-DOh_6k2k.js → linear-0O14Y6uf.js} +1 -1
  89. package/payload/server/public/assets/{mermaid-parser.core-F27uElsX.js → mermaid-parser.core-BUcTjodx.js} +2 -2
  90. package/payload/server/public/assets/{mermaid.core-B0PzAHME.js → mermaid.core-BKzdtZgG.js} +3 -3
  91. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-GA-xLC9K.js → mindmap-definition-QFDTVHPH-CobGMF9l.js} +1 -1
  92. package/payload/server/public/assets/{ordinal-BDi6f4xk.js → ordinal-krseTxxN.js} +1 -1
  93. package/payload/server/public/assets/packet-4T2RLAQJ-CGbvGkvF.js +1 -0
  94. package/payload/server/public/assets/{page-vRUDyjk4.js → page-B5NRnW0t.js} +1 -1
  95. package/payload/server/public/assets/{page-CXFoiyRN.js → page-Ba0BAfpu.js} +2 -2
  96. package/payload/server/public/assets/pie-ZZUOXDRM-BZy8rjFn.js +1 -0
  97. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CkP9U2tn.js → pieDiagram-DEJITSTG-DXFmGcNR.js} +1 -1
  98. package/payload/server/public/assets/{public-B_gqH5Cf.js → public-DWfBzf83.js} +5 -5
  99. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-sYIgx5bu.js → quadrantDiagram-34T5L4WZ-BnChStlO.js} +1 -1
  100. package/payload/server/public/assets/radar-PYXPWWZC-DcfWIVXr.js +1 -0
  101. package/payload/server/public/assets/{reduce-CGi9ik8i.js → reduce-tk-xY6Fv.js} +1 -1
  102. package/payload/server/public/assets/{requirementDiagram-MS252O5E-CKtMUXvK.js → requirementDiagram-MS252O5E-DdiBKsyO.js} +1 -1
  103. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CP5qebN_.js → sankeyDiagram-XADWPNL6-16-np_56.js} +1 -1
  104. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-zvkjl2R6.js → sequenceDiagram-FGHM5R23-D67SCRpA.js} +1 -1
  105. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-D-1VwYNL.js → stateDiagram-FHFEXIEX-CvNJX0YC.js} +1 -1
  106. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CFw2U4iL.js +1 -0
  107. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BSd_xlMt.js → timeline-definition-GMOUNBTQ-BJE5YoBa.js} +1 -1
  108. package/payload/server/public/assets/treeView-SZITEDCU-3WugwVdj.js +1 -0
  109. package/payload/server/public/assets/treemap-W4RFUUIX-Cf5mDLlu.js +1 -0
  110. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-D1hxlYfF.js → vennDiagram-DHZGUBPP-DF17ddXt.js} +1 -1
  111. package/payload/server/public/assets/wardley-RL74JXVD-Bv4md4b3.js +1 -0
  112. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-yNOC0LD-.js → wardleyDiagram-NUSXRM2D-xn_2XUOo.js} +1 -1
  113. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-D9D_gyVi.js → xychartDiagram-5P7HB3ND-DVF8zf3f.js} +1 -1
  114. package/payload/server/public/data.html +4 -4
  115. package/payload/server/public/graph.html +5 -4
  116. package/payload/server/public/index.html +7 -6
  117. package/payload/server/public/public.html +4 -3
  118. package/payload/server/server.js +229 -22
  119. package/payload/server/public/assets/architecture-YZFGNWBL--v-pJPNp.js +0 -1
  120. package/payload/server/public/assets/channel-C9mhUrr8.js +0 -1
  121. package/payload/server/public/assets/chunk-336JU56O-DZ53mw2n.js +0 -2
  122. package/payload/server/public/assets/chunk-426QAEUC-3s5PxkHp.js +0 -1
  123. package/payload/server/public/assets/chunk-QZHKN3VN-Bd-GrQM6.js +0 -1
  124. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BXiYcaCJ.js +0 -1
  125. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BRxESuCS.js +0 -1
  126. package/payload/server/public/assets/clone-Bz40VsQ_.js +0 -1
  127. package/payload/server/public/assets/data-DU2ZQbl-.js +0 -1
  128. package/payload/server/public/assets/gitGraph-7Q5UKJZL-CI0s_tqn.js +0 -1
  129. package/payload/server/public/assets/graph-BgLMP7mY.js +0 -1
  130. package/payload/server/public/assets/graph-labels-CUN_9NnB.js +0 -1
  131. package/payload/server/public/assets/info-OMHHGYJF-g3gYW7Qm.js +0 -1
  132. package/payload/server/public/assets/infoDiagram-42DDH7IO-BF1spWbk.js +0 -2
  133. package/payload/server/public/assets/packet-4T2RLAQJ-CT0TB9HI.js +0 -1
  134. package/payload/server/public/assets/pie-ZZUOXDRM-CXLe7TFF.js +0 -1
  135. package/payload/server/public/assets/radar-PYXPWWZC-DnPLBl-D.js +0 -1
  136. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BGmOYY_E.js +0 -1
  137. package/payload/server/public/assets/treeView-SZITEDCU-C3cb7Xwe.js +0 -1
  138. package/payload/server/public/assets/treemap-W4RFUUIX-Dc7G3Bgm.js +0 -1
  139. package/payload/server/public/assets/wardley-RL74JXVD-DtgibWAt.js +0 -1
  140. /package/payload/server/public/assets/{_baseFor-Cam2PbSt.js → _baseFor-BHtDrjIo.js} +0 -0
  141. /package/payload/server/public/assets/{array-DYRGGQae.js → array-DetWRiSa.js} +0 -0
  142. /package/payload/server/public/assets/{cytoscape.esm-nWsJMTNI.js → cytoscape.esm-C9yNhe1u.js} +0 -0
  143. /package/payload/server/public/assets/{defaultLocale-Du1XY3Dp.js → defaultLocale-_WRwicXn.js} +0 -0
  144. /package/payload/server/public/assets/{dist-BzAsli7o.js → dist-Bd4S37oi.js} +0 -0
  145. /package/payload/server/public/assets/{init-B5BXBRcm.js → init-sTEcj9YX.js} +0 -0
  146. /package/payload/server/public/assets/{katex-HOUACuRw.js → katex-s61Rgv6l.js} +0 -0
  147. /package/payload/server/public/assets/{path-CNO468J-.js → path-B0Ik7Tu9.js} +0 -0
  148. /package/payload/server/public/assets/{rough.esm-DRO6hWPh.js → rough.esm-DKRO8IF-.js} +0 -0
  149. /package/payload/server/public/assets/{src-CWiyyVfn.js → src-B6XdH6xq.js} +0 -0
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env bash
2
- # Task 129/131/165/175/177/195 regression test for the Stop hook.
2
+ # Task 129/131/165/175/177/195/214/216 regression test for the Stop hook.
3
3
  #
4
- # Contract (post-195):
4
+ # Dormant since Task 214 — the Stop-hook registration was removed from
5
+ # account `settings.json`; the script + this test remain as preserved
6
+ # infrastructure (see PLUGIN.md). When the script does run (e.g. via this
7
+ # test invoking it directly), the post-Task-214 `database-operator.md`
8
+ # template carries no `{schema}` / `{conversation}` / `{accountId}`
9
+ # placeholders, so the substitution layer is a no-op pass-through and
10
+ # `initialMessage` is the literal post-frontmatter body of the agent file.
11
+ #
12
+ # Contract (post-214):
5
13
  # - Every log emission goes through `POST /api/admin/log-ingest`. Hook
6
14
  # stderr is silent on every path.
7
15
  # - Every gated-off path emits one `trigger-skipped` line carrying a
@@ -11,18 +19,15 @@
11
19
  # `conversationBytes=<n>` and exactly ONE POST to
12
20
  # `/api/admin/claude-sessions`. The body carries the operator
13
21
  # session id as `adminSessionId` and an `initialMessage` whose value
14
- # is the FILLED database-operator paragraph the four-sentence
15
- # template with `{schema}` replaced by `schema-base.md` (plus the
16
- # brand-declared vertical when set) and `{conversation}` replaced by
17
- # a flat `role: text` rendering of the envelope's `turns` array.
18
- # Task 213 strips the prior `[tool: name(args)]` markers they
19
- # taught the recorder to copy the shape into prose. The initialMessage
20
- # is a plain string, not a JSON-stringified envelope; no instruction
21
- # prose, no leading JSON.
22
- # - The hook emits exactly one `[turn-recorder] envelope ...` line and
23
- # one `[turn-recorder] substitution ...` line before `spawn-request`.
24
- # The substitution line carries `schemaBytes`, `conversationBytes`,
25
- # and `bodyBytes` so the per-spawn substitution is auditable.
22
+ # is the literal database-operator agent body (post-frontmatter).
23
+ # No placeholders are substituted because the post-Task-214 template
24
+ # has none. The walker still parses the JSONL, still emits the
25
+ # `envelope turnsCount=N userTurns=U assistantTurns=A toolCallTurns=T`
26
+ # log line, and the schema file is still read and reported in the
27
+ # `substitution schemaBytes=N` line but neither the transcript
28
+ # nor the schema content reaches `initialMessage`.
29
+ # - The hook emits exactly one envelope line and one substitution line
30
+ # before `spawn-request`.
26
31
  # - The hook never writes the legacy `spawn-with-input` / `fired` stderr
27
32
  # lines.
28
33
  #
@@ -223,12 +228,14 @@ else
223
228
  fail "case-5b expected exactly 1 recorder-spawn POST, got $RECORDER_LINES"
224
229
  fi
225
230
 
226
- # 5c. (Task 195) initialMessage is the FILLED four-sentence paragraph.
227
- # The template's `{schema}` is replaced by schema-base.md content;
228
- # `{conversation}` is replaced by `role: text` lines for every turn.
229
- # The string is plain text, not a JSON envelope. The template's
230
- # opening sentence and closing sentence are present verbatim; the
231
- # four operator/assistant texts each appear on their own line.
231
+ # 5c. (Task 216) initialMessage is the literal post-frontmatter body of
232
+ # `database-operator.md`. Task 214 stripped the `{schema}` /
233
+ # `{conversation}` / `{accountId}` placeholders from the template,
234
+ # so the substitution layer in the hook is now a no-op pass-through
235
+ # and the agent body lands in `initialMessage` verbatim. Assertions
236
+ # are property-based they guard the regression class (substitution
237
+ # layer reintroduced, transcript leak, schema content leak, outer
238
+ # spawn-body drift) without coupling to the prompt's exact wording.
232
239
  RECORDER_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
233
240
  BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
234
241
  import sys, json
@@ -241,35 +248,43 @@ try:
241
248
  print("no:initialMessage-not-string:"+repr(type(msg).__name__))
242
249
  sys.exit(0)
243
250
  conds = []
251
+ # Template-header anchor — the post-Task-214 body opens with this
252
+ # sentence. A different opener means the agent file was renamed,
253
+ # rewritten, or its frontmatter parse drifted.
244
254
  conds.append(("starts-with-template",
245
255
  msg.startswith("You are an expert Neo4J graph operator.")))
246
- conds.append(("ends-with-template",
247
- msg.rstrip().endswith("do not emit it.")))
256
+ # Placeholder-reintroduction guards. If any of these reappear, Task
257
+ # 214 was reverted without updating the template.
248
258
  conds.append(("no-placeholder-schema", "{schema}" not in msg))
249
259
  conds.append(("no-placeholder-conversation", "{conversation}" not in msg))
250
260
  conds.append(("no-placeholder-accountid", "{accountId}" not in msg))
251
- # schema-base.md content is interpolated.
252
- conds.append(("contains-schema-base-header", "Schema Reference" in msg))
253
- # Task 199 accountId clarifier sentence + literal value rendered.
254
- conds.append(("has-accountid-clarifier",
255
- "accountId" in msg and "automatically supplied" in msg))
256
- conds.append(("has-accountid-value",
257
- "The current accountId is `" + acct + "`." in msg))
258
- # The conversation lines appear in role: text format.
259
- conds.append(("has-user-1", "\nuser: New Real Agent session" in msg))
260
- conds.append(("has-asst-1",
261
- "\nassistant: Session noted. What would you like to work on?" in msg))
262
- conds.append(("has-user-2", "\nuser: you tell me" in msg))
263
- conds.append(("has-asst-2",
264
- "\nassistant: One question to anchor the next move: what is your name?"
265
- in msg))
261
+ # Transcript-leak guard. The walker still parses the JSONL, but its
262
+ # `turns` array no longer reaches `initialMessage`. Any `\nuser: ` or
263
+ # `\nassistant: ` line means substitution crept back in.
264
+ conds.append(("no-transcript-user", "\nuser: " not in msg))
265
+ conds.append(("no-transcript-asst", "\nassistant: " not in msg))
266
+ # Schema-leak guard. schema-base.md is still read (and its bytes are
267
+ # reported in the substitution log line), but its content is no
268
+ # longer substituted into the body. `Schema Reference` is the
269
+ # opening header of `platform/plugins/memory/references/schema-base.md`
270
+ # (`# Schema Reference — Base`); its appearance in the body means
271
+ # `{schema}` re-entered the template.
272
+ conds.append(("no-schema-content", "Schema Reference" not in msg))
273
+ # AccountId-value leak guard. If `{accountId}` is re-added in
274
+ # isolation (no `{schema}` / `{conversation}` reintroduction), the
275
+ # placeholder guards above pass (because substitution replaces the
276
+ # token cleanly), but the operator accountId still leaks into the
277
+ # prompt. The test fixture passes ACCOUNT_ID="$ACCT_ID" into the
278
+ # hook env, so the literal acct id must NOT appear anywhere in the
279
+ # body.
280
+ conds.append(("no-accountid-value-leak", acct not in msg))
281
+ # Pre-Task-177 envelope-shape guard. `initialMessage` is a plain
282
+ # string, never a JSON envelope.
266
283
  conds.append(("no-leading-json", not msg.lstrip().startswith("{")))
267
284
  conds.append(("no-operatorMessage", "operatorMessage" not in msg))
268
285
  conds.append(("no-assistantReply", "assistantReply" not in msg))
286
+ # Spawn-body outer-shape sanity (unchanged from Task 195/200).
269
287
  conds.append(("outer-specialist", outer.get("specialist") == "database-operator"))
270
- # Task 200 — model field is omitted from the spawn body so the
271
- # database-operator agent frontmatter (claude-sonnet-4-6) is the
272
- # runtime model. The hard-coded haiku override has been removed.
273
288
  conds.append(("outer-no-model-key", "model" not in outer))
274
289
  conds.append(("outer-adminSessionId", outer.get("adminSessionId") == op))
275
290
  conds.append(("outer-channel", outer.get("channel") == "browser"))
@@ -279,9 +294,9 @@ except Exception as e:
279
294
  print("parse-fail:"+str(e))
280
295
  ' "$OP_ID" "$ACCT_ID" 2>/dev/null)
281
296
  if [[ "$BODY_OK" == "yes" ]]; then
282
- pass "case-5c initialMessage is the filled paragraph; schema/accountId/conversation placeholders substituted; accountId value rendered; transcript as role: text lines"
297
+ pass "case-5c initialMessage is the literal template body (no placeholders, no transcript leak, no schema leak); outer spawn shape intact"
283
298
  else
284
- fail "case-5c filled-prompt shape wrong ($BODY_OK)"
299
+ fail "case-5c literal-body shape wrong ($BODY_OK)"
285
300
  fi
286
301
 
287
302
  # 5h. (Task 195 + 199) exactly one `substitution` log-ingest line carrying
@@ -354,17 +369,27 @@ else
354
369
  pass "case-5g no legacy stderr emissions"
355
370
  fi
356
371
 
357
- # --- Case 6 (Task 195): multi-block assistant collapse ----------------
358
- # Two assistant records sharing `message.id`: thinking-only then text.
359
- # The walker collapses both into ONE assistant turn whose text is the
360
- # text-record's text. The filled prompt's conversation portion has
361
- # exactly two transcript lines: `user: think then answer` and
362
- # `assistant: answer is 42`.
372
+ # --- Case 6: multi-block assistant collapse (walker coverage) ---------
373
+ # Two assistant records sharing `message.id`, BOTH carrying text. The
374
+ # walker's collapse branch (turn-completed-graph-write.sh, the
375
+ # `msg_id in msg_id_to_turn_index` branch in the assistant arm) merges
376
+ # them into ONE assistant turn whose text is the concatenation. Without
377
+ # the collapse the walker would emit two assistant turns. Pre-Task-214
378
+ # this was visible in `initialMessage` via a single `\nassistant: ...`
379
+ # line carrying both fragments; post-Task-214 the transcript no longer
380
+ # reaches the body, so coverage moves to the envelope log line —
381
+ # `turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0` proves
382
+ # the two assistant records collapsed to one (otherwise the line would
383
+ # read assistantTurns=2). Fixture must use TEXT-bearing records on both
384
+ # sides; a thinking-only first record would be skipped at line 202 of
385
+ # the walker before the collapse branch ever runs, so the test would
386
+ # only prove the skip path (already covered by case 9) instead of the
387
+ # collapse path.
363
388
  MULTI_TRANSCRIPT=$(mktemp); TMPFILES+=("$MULTI_TRANSCRIPT")
364
389
  {
365
- printf '{"type":"user","timestamp":"2026-05-19T21:30:00.000Z","message":{"role":"user","content":"think then answer"}}\n'
366
- printf '{"type":"assistant","timestamp":"2026-05-19T21:30:01.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"thinking","thinking":"considering..."}]}}\n'
367
- printf '{"type":"assistant","timestamp":"2026-05-19T21:30:02.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"text","text":"answer is 42"}]}}\n'
390
+ printf '{"type":"user","timestamp":"2026-05-19T21:30:00.000Z","message":{"role":"user","content":"split your answer"}}\n'
391
+ printf '{"type":"assistant","timestamp":"2026-05-19T21:30:01.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"text","text":"part one"}]}}\n'
392
+ printf '{"type":"assistant","timestamp":"2026-05-19T21:30:02.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"text","text":" and part two"}]}}\n'
368
393
  } > "$MULTI_TRANSCRIPT"
369
394
  MULTI_ENVELOPE=$(python3 -c '
370
395
  import json, sys
@@ -373,46 +398,24 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
373
398
  : > "$REQ_LOG"
374
399
  run_hook "admin" "" "$MULTI_ENVELOPE"
375
400
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-6 rc=$HOOK_RC stderr=$HOOK_STDERR"
376
- MULTI_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
377
- MULTI_OK=$(printf '%s' "$MULTI_BODY" | python3 -c '
378
- import sys, json
379
- try:
380
- outer = json.load(sys.stdin)
381
- msg = outer.get("initialMessage")
382
- if not isinstance(msg, str):
383
- print("no:initialMessage-not-string")
384
- sys.exit(0)
385
- conds = []
386
- # Filled-prompt sanity.
387
- conds.append(("starts-template", msg.startswith("You are an expert Neo4J graph operator.")))
388
- conds.append(("no-placeholder-conv", "{conversation}" not in msg))
389
- # Conversation lines.
390
- conds.append(("has-user", "\nuser: think then answer" in msg))
391
- conds.append(("has-asst", "\nassistant: answer is 42" in msg))
392
- # Thinking content does NOT bleed in.
393
- conds.append(("no-thinking-text", "considering..." not in msg))
394
- # Exactly two transcript lines (one per turn) — no duplicate assistant.
395
- user_lines = msg.count("\nuser: ")
396
- asst_lines = msg.count("\nassistant: ")
397
- conds.append(("one-user-line", user_lines == 1))
398
- conds.append(("one-asst-line", asst_lines == 1))
399
- failed = [name for name, ok in conds if not ok]
400
- print("yes" if not failed else "no:"+repr(failed))
401
- except Exception as e:
402
- print("parse-fail:"+str(e))
403
- ' 2>/dev/null)
404
- if [[ "$MULTI_OK" == "yes" ]]; then
405
- pass "case-6 multi-block assistant collapses to one transcript line; thinking content does not bleed in"
401
+ echo "case-6 SKIPPED dormant since Task 214; transcript no longer substituted into initialMessage. Walker collapse now asserted via envelope log line below."
402
+ ENV_LINE_6=$(ingest_lines | grep -cE "^envelope sessionId=${OP_ID} turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0$" || true)
403
+ if [[ "$ENV_LINE_6" -eq 1 ]]; then
404
+ pass "case-6 envelope log line counts multi-block collapse: turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0"
406
405
  else
407
- fail "case-6 multi-block collapse wrong ($MULTI_OK): $MULTI_BODY"
406
+ fail "case-6 expected 1 envelope line w/ turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0, got $ENV_LINE_6 (lines: $(ingest_lines))"
408
407
  fi
409
408
 
410
- # --- Case 7 (Task 177 + Task 213): tool-call pairing kept in envelope, -
411
- # stripped from the transcript. Assistant tool_use + user tool_result still
412
- # fold into one `toolCalls` entry on the assistant turn (envelope-level
413
- # observability via `[turn-recorder] envelope toolCallTurns=N`). The
414
- # rendered transcript carries NO `[tool: …]` markers pre-213 they taught
415
- # the recorder to emit `<tool_call>` as text.
409
+ # --- Case 7: tool-call pairing (walker coverage) ----------------------
410
+ # Assistant tool_use + user tool_result fold into one `toolCalls` entry
411
+ # on the assistant turn; the user-record carrying only the tool_result
412
+ # never creates its own user turn. Pre-Task-214 this was visible in
413
+ # `initialMessage` as "exactly one user line, two assistant lines (one
414
+ # empty-bodied)". Post-Task-214 the transcript no longer reaches the
415
+ # body, so the pairing is now only observable through the envelope log
416
+ # line — `turnsCount=3 userTurns=1 assistantTurns=2 toolCallTurns=1`
417
+ # proves the tool_result was absorbed (no extra user turn) and the
418
+ # tool_use was paired on the owning assistant turn (toolCallTurns=1).
416
419
  TOOL_TRANSCRIPT=$(mktemp); TMPFILES+=("$TOOL_TRANSCRIPT")
417
420
  python3 - "$TOOL_TRANSCRIPT" <<'PY'
418
421
  import json, sys
@@ -439,54 +442,24 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
439
442
  : > "$REQ_LOG"
440
443
  run_hook "admin" "" "$TOOL_ENVELOPE"
441
444
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-7 rc=$HOOK_RC stderr=$HOOK_STDERR"
442
- TOOL_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
443
- TOOL_OK=$(printf '%s' "$TOOL_BODY" | python3 -c '
444
- import sys, json
445
- try:
446
- outer = json.load(sys.stdin)
447
- msg = outer.get("initialMessage")
448
- if not isinstance(msg, str):
449
- print("no:initialMessage-not-string")
450
- sys.exit(0)
451
- conds = []
452
- # The user turn carrying only a tool_result becomes no transcript line;
453
- # so the filled prompt should have exactly ONE `user:` line.
454
- conds.append(("one-user-line", msg.count("\nuser: ") == 1))
455
- conds.append(("has-user-prompt", "\nuser: please write a node" in msg))
456
- # Two assistant lines: one carrying the tool_use marker (with empty
457
- # text body), one carrying "done".
458
- conds.append(("two-asst-lines", msg.count("\nassistant: ") == 2))
459
- conds.append(("has-asst-done", "\nassistant: done" in msg))
460
- # Task 213 — no `[tool: …]` markup in the rendered transcript.
461
- conds.append(("no-tool-marker", "[tool:" not in msg))
462
- conds.append(("no-tool-input-leak", "Smalleys" not in msg))
463
- # The owning assistant turn lands as an empty-bodied line: the tool_use
464
- # block contributed no text, and the marker that used to fill it is gone.
465
- conds.append(("has-empty-asst-line", "\nassistant: \n" in msg))
466
- # The tool_result content does not bleed in either.
467
- conds.append(("no-output-leakage", "ok: wrote LocalBusiness" not in msg))
468
- failed = [name for name, ok in conds if not ok]
469
- print("yes" if not failed else "no:"+repr(failed))
470
- except Exception as e:
471
- print("parse-fail:"+str(e))
472
- ' 2>/dev/null)
473
- if [[ "$TOOL_OK" == "yes" ]]; then
474
- pass "case-7 assistant tool_use leaves no [tool: …] markup in transcript; tool_result does not create a separate user line; toolCallTurns count survives via envelope log"
475
- else
476
- fail "case-7 tool pairing wrong ($TOOL_OK): $TOOL_BODY"
477
- fi
445
+ echo "case-7 SKIPPED dormant since Task 214; transcript no longer substituted into initialMessage. Tool-call pairing now asserted via envelope log line below."
478
446
 
479
447
  # Envelope log line for case-7: turnsCount=3, userTurns=1, assistantTurns=2, toolCallTurns=1
480
448
  ENV_LINE_7=$(ingest_lines | grep -cE "^envelope sessionId=${OP_ID} turnsCount=3 userTurns=1 assistantTurns=2 toolCallTurns=1$" || true)
481
449
  if [[ "$ENV_LINE_7" -eq 1 ]]; then
482
- pass "case-7 envelope log line counts tool-call turn correctly"
450
+ pass "case-7 envelope log line counts tool-call turn correctly: turnsCount=3 userTurns=1 assistantTurns=2 toolCallTurns=1"
483
451
  else
484
452
  fail "case-7 expected envelope line w/ turnsCount=3 userTurns=1 assistantTurns=2 toolCallTurns=1, got $ENV_LINE_7 (lines: $(ingest_lines))"
485
453
  fi
486
454
 
487
- # --- Case 8 (Task 177): long-conversation 100 turns ------------------
488
- # Transcript with 100 turns (50 user + 50 assistant, interleaved)
489
- # turns.length === 100. No drop, no truncation, no env-knob override.
455
+ # --- Case 8: long-conversation 100 turns (walker coverage) -----------
456
+ # Transcript with 100 turns (50 user + 50 assistant, interleaved).
457
+ # Pre-Task-214 the body had to contain all 100 transcript lines and
458
+ # exceed 5_000 bytes. Post-Task-214 the transcript no longer reaches
459
+ # the body, so the "no windowing" invariant is asserted via the
460
+ # envelope log line: `turnsCount=100 userTurns=50 assistantTurns=50
461
+ # toolCallTurns=0`. A flat or shrinking turnsCount means windowing or
462
+ # truncation crept back into the walker.
490
463
  LONG_TRANSCRIPT=$(mktemp); TMPFILES+=("$LONG_TRANSCRIPT")
491
464
  python3 - "$LONG_TRANSCRIPT" <<'PY'
492
465
  import json, sys
@@ -505,36 +478,12 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
505
478
  : > "$REQ_LOG"
506
479
  run_hook "admin" "" "$LONG_ENVELOPE"
507
480
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-8 rc=$HOOK_RC stderr=$HOOK_STDERR"
508
- LONG_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
509
- LONG_OK=$(printf '%s' "$LONG_BODY" | python3 -c '
510
- import sys, json
511
- try:
512
- outer = json.load(sys.stdin)
513
- msg = outer.get("initialMessage")
514
- if not isinstance(msg, str):
515
- print("no:initialMessage-not-string")
516
- sys.exit(0)
517
- user_lines = msg.count("\nuser: ")
518
- asst_lines = msg.count("\nassistant: ")
519
- conds = []
520
- # No windowing — every one of the 100 turns appears as a transcript line.
521
- conds.append(("100-user", user_lines == 50))
522
- conds.append(("100-asst", asst_lines == 50))
523
- conds.append(("first-user-0", "\nuser: user msg 0" in msg))
524
- conds.append(("last-user-49", "\nuser: user msg 49" in msg))
525
- conds.append(("last-asst-49", "\nassistant: asst reply 49" in msg))
526
- # The filled prompt is large — schema + 100 transcript lines easily
527
- # exceeds 5_000 bytes even with a tiny schema.
528
- conds.append(("body-large", len(msg.encode("utf-8")) > 5000))
529
- failed = [name for name, ok in conds if not ok]
530
- print("yes" if not failed else "no:"+repr(failed)+"|users="+str(user_lines)+"|assts="+str(asst_lines))
531
- except Exception as e:
532
- print("parse-fail:"+str(e))
533
- ' 2>/dev/null)
534
- if [[ "$LONG_OK" == "yes" ]]; then
535
- pass "case-8 100-turn transcript → 50 user lines + 50 assistant lines in filled prompt; no truncation"
481
+ echo "case-8 SKIPPED dormant since Task 214; transcript no longer substituted into initialMessage. No-windowing invariant now asserted via envelope log line below."
482
+ ENV_LINE_8=$(ingest_lines | grep -cE "^envelope sessionId=${OP_ID} turnsCount=100 userTurns=50 assistantTurns=50 toolCallTurns=0$" || true)
483
+ if [[ "$ENV_LINE_8" -eq 1 ]]; then
484
+ pass "case-8 envelope log line counts 100-turn transcript: turnsCount=100 userTurns=50 assistantTurns=50 toolCallTurns=0 (no windowing)"
536
485
  else
537
- fail "case-8 long-conversation wrong ($LONG_OK)"
486
+ fail "case-8 expected 1 envelope line w/ turnsCount=100 userTurns=50 assistantTurns=50 toolCallTurns=0, got $ENV_LINE_8 (lines: $(ingest_lines))"
538
487
  fi
539
488
 
540
489
  # --- Case 9 (Task 177): empty-skip ------------------------------------
@@ -563,9 +512,15 @@ else
563
512
  pass "case-9 no recorder-spawn POST"
564
513
  fi
565
514
 
566
- # --- Case 10 (Task 177): escaping round-trip --------------------------
567
- # Quotes, backslashes, newlines, non-ASCII all round-trip cleanly through
568
- # the JSON envelope's `turns[*].text` field.
515
+ # --- Case 10: escaping round-trip (walker coverage) -------------------
516
+ # User and assistant text with quotes / backslashes / em-dash / emoji.
517
+ # Pre-Task-214 these had to survive interpolation into the body and
518
+ # appear verbatim. Post-Task-214 the transcript no longer reaches the
519
+ # body, but the walker still parses the JSONL — the envelope log line
520
+ # `turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0` proves
521
+ # the walker did NOT crash on the special-character text. A parser
522
+ # regression would either drop a turn (zero counts) or fail the hook
523
+ # (non-zero rc).
569
524
  ESC_TRANSCRIPT=$(mktemp); TMPFILES+=("$ESC_TRANSCRIPT")
570
525
  python3 - "$ESC_TRANSCRIPT" <<'PY'
571
526
  import json, sys
@@ -586,37 +541,12 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
586
541
  : > "$REQ_LOG"
587
542
  run_hook "admin" "" "$ESC_ENVELOPE"
588
543
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-10 rc=$HOOK_RC stderr=$HOOK_STDERR"
589
- ESC_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
590
- ESC_OK=$(printf '%s' "$ESC_BODY" | python3 -c '
591
- import sys, json
592
- # The user turn text contains literal quotes, a backslash-n (two chars),
593
- # a real newline, an em-dash, and a fox emoji. The assistant text has
594
- # two backslashes and a real newline.
595
- EXPECTED_OP_FRAG_1 = "hello \"world\""
596
- EXPECTED_OP_FRAG_2 = "last — café \U0001F98A"
597
- EXPECTED_ASST_FRAG = "reply with backslash \\\\ and newline"
598
- try:
599
- outer = json.load(sys.stdin)
600
- msg = outer.get("initialMessage")
601
- if not isinstance(msg, str):
602
- print("no:initialMessage-not-string")
603
- sys.exit(0)
604
- conds = []
605
- conds.append(("op-quotes", EXPECTED_OP_FRAG_1 in msg))
606
- conds.append(("op-emoji", EXPECTED_OP_FRAG_2 in msg))
607
- conds.append(("asst-backslash", EXPECTED_ASST_FRAG in msg))
608
- conds.append(("user-prefix", "\nuser: " in msg))
609
- conds.append(("asst-prefix", "\nassistant: " in msg))
610
- conds.append(("template-intact", "You are an expert Neo4J graph operator." in msg))
611
- failed = [name for name, ok in conds if not ok]
612
- print("yes" if not failed else "no:"+repr(failed))
613
- except Exception as e:
614
- print("parse-fail:"+str(e))
615
- ' 2>/dev/null)
616
- if [[ "$ESC_OK" == "yes" ]]; then
617
- pass "case-10 escaping: quotes / backslashes / em-dash / emoji survive interpolation into the filled prompt"
544
+ echo "case-10 SKIPPED dormant since Task 214; transcript no longer substituted into initialMessage. Walker-survives-special-chars now asserted via envelope log line below."
545
+ ENV_LINE_10=$(ingest_lines | grep -cE "^envelope sessionId=${OP_ID} turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0$" || true)
546
+ if [[ "$ENV_LINE_10" -eq 1 ]]; then
547
+ pass "case-10 envelope log line counts special-character transcript: turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0 (walker survived quotes/backslashes/em-dash/emoji)"
618
548
  else
619
- fail "case-10 escaping wrong ($ESC_OK): $ESC_BODY"
549
+ fail "case-10 expected 1 envelope line w/ turnsCount=2 userTurns=1 assistantTurns=1 toolCallTurns=0, got $ENV_LINE_10 (lines: $(ingest_lines))"
620
550
  fi
621
551
 
622
552
  # --- Case 11 (Task 199): missing accountId → accountIdPresent=no, no crash ---
@@ -650,35 +580,18 @@ else
650
580
  fi
651
581
 
652
582
  # 11b. recorder spawn still fires — the hook does NOT loud-fail on absent
653
- # accountId. The filled body still substitutes; the placeholder
654
- # collapses to empty backticks.
655
- NO_ACCT_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
656
- NO_ACCT_OK=$(printf '%s' "$NO_ACCT_BODY" | python3 -c '
657
- import sys, json
658
- try:
659
- outer = json.load(sys.stdin)
660
- msg = outer.get("initialMessage")
661
- if not isinstance(msg, str):
662
- print("no:initialMessage-not-string")
663
- sys.exit(0)
664
- conds = []
665
- conds.append(("starts-template", msg.startswith("You are an expert Neo4J graph operator.")))
666
- conds.append(("no-placeholder-accountid", "{accountId}" not in msg))
667
- # Placeholder collapses to empty backticks.
668
- conds.append(("empty-accountid-rendered", "The current accountId is ``." in msg))
669
- conds.append(("clarifier-still-present",
670
- "never refuse a write because accountId" in msg))
671
- # Transcript still rendered.
672
- conds.append(("has-user-line", "\nuser: " in msg))
673
- failed = [name for name, ok in conds if not ok]
674
- print("yes" if not failed else "no:"+repr(failed))
675
- except Exception as e:
676
- print("parse-fail:"+str(e))
677
- ' 2>/dev/null)
678
- if [[ "$NO_ACCT_OK" == "yes" ]]; then
679
- pass "case-11b empty ACCOUNT_ID → filled body still substitutes; {accountId} → empty backticks; clarifier sentence intact"
583
+ # accountId. Pre-Task-214 the {accountId} placeholder collapsed to
584
+ # empty backticks in the body. Post-Task-214 the template has no
585
+ # {accountId} placeholder, so the body is the literal template
586
+ # regardless of ACCOUNT_ID the regression surface for "hook
587
+ # survives empty ACCOUNT_ID" is now 11a (substitution log line
588
+ # carries accountIdPresent=no) plus the spawn POST landing at all.
589
+ echo "case-11b SKIPPED — dormant since Task 214; {accountId} placeholder removed from template, body is literal regardless of ACCOUNT_ID. Survival on empty accountId is now asserted by 11a (substitution log line) + spawn-POST presence below."
590
+ NO_ACCT_POST=$(grep -cE '^/api/admin/claude-sessions ' "$REQ_LOG" || true)
591
+ if [[ "$NO_ACCT_POST" -eq 1 ]]; then
592
+ pass "case-11b spawn POST fires on empty ACCOUNT_ID (hook does not loud-fail)"
680
593
  else
681
- fail "case-11b filled-body shape wrong on empty accountId ($NO_ACCT_OK): $NO_ACCT_BODY"
594
+ fail "case-11b expected 1 spawn POST on empty ACCOUNT_ID, got $NO_ACCT_POST"
682
595
  fi
683
596
 
684
597
  # --- Summary ------------------------------------------------------------
@@ -21,24 +21,89 @@ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"'
21
21
  # ---------------------------------------------------------------------------
22
22
  if [ "$AGENT_TYPE" = "admin" ]; then
23
23
 
24
- # ── Task 213: memory-write/memory-update revocation ──────────────────────
25
- # The admin agent does not write to the graph; the Stop-hook recorder
26
- # (database-operator specialist) owns these tools. Session d2aaa85e
27
- # 2026-05-21 showed admin emitting `tool_use: memory-update`, filling in
28
- # when the recorder produced no tool_use. `pty-spawner.ts` already passes
29
- # `--disallowed-tools` on the spawn argv; this hook is the observability
30
- # surface — every relapse lands in server.log as `[admin-tool-revoke]`,
31
- # which the `--disallowed-tools` path itself does not produce.
24
+ # ── Task 213 (rev. Task 222): memory-write/memory-update gate ────────────
25
+ # Admin does not write to the graph directly. The contract (Task 214 +
26
+ # admin IDENTITY.md) is that admin delegates a write to `database-operator`
27
+ # via the Task tool; database-operator holds the writers in its agent
28
+ # frontmatter and executes the write inside its own in-window subagent
29
+ # turn.
30
+ #
31
+ # The PreToolUse payload includes `transcript_path`, the JSONL the
32
+ # invoking session writes turns to. Claude Code stores a subagent's
33
+ # turns in a separate file at
34
+ # <project-key>/<sessionId>/subagents/agent-<agentId>.jsonl
35
+ # so the path pattern is the deterministic discriminator between admin
36
+ # direct calls (parent JSONL) and Task subagent calls (subagent JSONL).
37
+ #
38
+ # The hook gates the two memory writers per-tool-use:
39
+ # * Task-subagent caller (path matches subagent pattern)
40
+ # → allow, emit caller=subagent:<subagent_type> source=hook-passthrough result=allowed
41
+ # * admin direct caller (any other path)
42
+ # → block exit 2, emit caller=admin source=hook result=rejected
43
+ # * unparseable payload (no JSON, no transcript_path)
44
+ # → fail-closed: block exit 2, emit caller=unknown source=hook result=rejected
45
+ # The subagent_type enrichment is best-effort: the subagent's own JSONL
46
+ # does not encode its agent name, so the hook reads the parent transcript
47
+ # (sibling `<sessionId>.jsonl` to the subagent dir) and extracts the
48
+ # `subagent_type` from the most recent `Task` tool_use. If that read
49
+ # fails for any reason the caller is logged as bare `subagent` and the
50
+ # call is still allowed.
32
51
  case "$TOOL_NAME" in
33
52
  mcp__plugin_memory_memory__memory-write|mcp__plugin_memory_memory__memory-update)
34
- SESSION_ID=$(printf '%s' "$INPUT" | python3 -c '
35
- import sys, json
53
+ VERDICT=$(printf '%s' "$INPUT" | python3 -c '
54
+ import sys, json, os, re
55
+ tool_name = sys.argv[1]
36
56
  try:
37
- d = json.load(sys.stdin)
38
- print(d.get("session_id", "") or "unknown")
57
+ d = json.load(sys.stdin)
39
58
  except Exception:
40
- print("unknown")
41
- ' 2>/dev/null || echo "unknown")
59
+ print("block|unknown|unknown|" + tool_name)
60
+ sys.exit(0)
61
+ sid = d.get("session_id") or "unknown"
62
+ tp = d.get("transcript_path") or ""
63
+ m = re.search(r"/subagents/agent-[A-Za-z0-9_-]+\.jsonl$", tp)
64
+ if not m:
65
+ caller = "admin" if tp else "unknown"
66
+ print("block|" + sid + "|" + caller + "|" + tool_name)
67
+ sys.exit(0)
68
+ parent = tp[: m.start()] + ".jsonl"
69
+ subagent_type = None
70
+ if os.path.isfile(parent):
71
+ try:
72
+ with open(parent, "r", encoding="utf-8") as f:
73
+ for ln in f:
74
+ ln = ln.strip()
75
+ if not ln:
76
+ continue
77
+ try:
78
+ r = json.loads(ln)
79
+ except Exception:
80
+ continue
81
+ if r.get("type") != "assistant":
82
+ continue
83
+ msg = r.get("message") or {}
84
+ content = msg.get("content")
85
+ if not isinstance(content, list):
86
+ continue
87
+ for b in content:
88
+ if (isinstance(b, dict) and b.get("type") == "tool_use"
89
+ and b.get("name") == "Task"):
90
+ st = (b.get("input") or {}).get("subagent_type")
91
+ if isinstance(st, str) and st:
92
+ subagent_type = st
93
+ except Exception:
94
+ subagent_type = None
95
+ caller = "subagent:" + subagent_type if subagent_type else "subagent"
96
+ print("allow|" + sid + "|" + caller + "|" + tool_name)
97
+ ' "$TOOL_NAME" 2>/dev/null)
98
+ [ -z "$VERDICT" ] && VERDICT="block|unknown|unknown|${TOOL_NAME}"
99
+ DECISION=$(printf '%s' "$VERDICT" | cut -d'|' -f1)
100
+ SESSION_ID=$(printf '%s' "$VERDICT" | cut -d'|' -f2)
101
+ CALLER=$(printf '%s' "$VERDICT" | cut -d'|' -f3)
102
+ if [ "$DECISION" = "allow" ]; then
103
+ LINE_BODY="role=admin sessionId=${SESSION_ID} tool=${TOOL_NAME} caller=${CALLER} source=hook-passthrough result=allowed"
104
+ else
105
+ LINE_BODY="role=admin sessionId=${SESSION_ID} tool=${TOOL_NAME} caller=${CALLER} source=hook result=rejected"
106
+ fi
42
107
  UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
43
108
  if [ -n "$UI_PORT" ]; then
44
109
  curl -sS -o /dev/null -X POST \
@@ -46,11 +111,14 @@ except Exception:
46
111
  --max-time 2 \
47
112
  --data "$(python3 -c '
48
113
  import sys, json
49
- print(json.dumps({"tag":"admin-tool-revoke","level":"info","line":f"role=admin sessionId={sys.argv[1]} tool={sys.argv[2]} result=rejected source=permission-hook"}))
50
- ' "$SESSION_ID" "$TOOL_NAME")" \
114
+ print(json.dumps({"tag":"admin-tool-revoke","level":"info","line":sys.argv[1]}))
115
+ ' "$LINE_BODY")" \
51
116
  "http://127.0.0.1:${UI_PORT}/api/admin/log-ingest" 2>/dev/null || true
52
117
  fi
53
- echo "Blocked: admin agent does not write to the graph. The Stop-hook database-operator recorder owns memory-write and memory-update." >&2
118
+ if [ "$DECISION" = "allow" ]; then
119
+ exit 0
120
+ fi
121
+ echo "Blocked: admin agent does not write to the graph directly. Delegate the write to database-operator via the Task tool; database-operator holds the memory-write and memory-update tools and will execute the write on admin's behalf." >&2
54
122
  exit 2
55
123
  ;;
56
124
  esac