@rubytech/create-maxy-code 0.1.82 → 0.1.84
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.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh +142 -24
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +131 -218
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +85 -17
- package/payload/platform/plugins/cloudflare/PLUGIN.md +2 -11
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +9 -7
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +20 -10
- package/payload/platform/plugins/docs/references/cloudflare.md +10 -9
- package/payload/platform/scripts/seed-neo4j.sh +20 -14
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts +6 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +21 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +47 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +265 -44
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +8 -10
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +3 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +18 -8
- package/payload/server/{chunk-TFEIFU7F.js → chunk-BDFOTLPW.js} +1 -1
- package/payload/server/{chunk-W33UUYYR.js → chunk-BVDLDM6O.js} +1 -1
- package/payload/server/{chunk-VGMVEYX7.js → chunk-JRBCOVA4.js} +64 -5
- package/payload/server/{cloudflare-task-tracker-7P355PSD.js → cloudflare-task-tracker-M5ONAGUT.js} +2 -2
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/Checkbox-B34vplI7.js +1 -0
- package/payload/server/public/assets/{admin-Dl273yTS.js → admin-BkTlPuP0.js} +5 -5
- package/payload/server/public/assets/{arc-aUiRP9AS.js → arc-DMDAZHAN.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-COhEvUpo.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Iyf6H93s.js → architectureDiagram-Q4EWVU46-DQMCKDU2.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-e7l5Mhun.js → blockDiagram-DXYQGD6D-CqJNtiQY.js} +1 -1
- package/payload/server/public/assets/{brand-CTMGYFDd.css → brand-C_iHnsbT.css} +1 -1
- package/payload/server/public/assets/{brand-dPgPNs7n.js → brand-HqTMr1bV.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-bDICnX2_.js → c4Diagram-AHTNJAMY-BVAa1YL3.js} +1 -1
- package/payload/server/public/assets/channel-CZG4mVkt.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-BK3470lx.js → chunk-2KRD3SAO-Di4bO8ir.js} +1 -1
- package/payload/server/public/assets/chunk-336JU56O-XIsdqKyD.js +2 -0
- package/payload/server/public/assets/chunk-426QAEUC-Dw4mcMYK.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-BOvVdJLf.js → chunk-4BX2VUAB-DyEhFk-Z.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-CzvvHuMJ.js → chunk-4TB4RGXK-BvrlG2FS.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-BwZyF7vR.js → chunk-55IACEB6-BRJOZLpU.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-DxqHPDro.js → chunk-5FUZZQ4R-3-UZ9191.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-BA6nMu9F.js → chunk-5PVQY5BW-DOeY60Lp.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-D5bhMrtY.js → chunk-67CJDMHE-BG6-9r6c.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-Si7Lgrwc.js → chunk-7N4EOEYR-BvMbitX7.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-DMuHtDqO.js → chunk-AA7GKIK3-CE8mGBD5.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-L79XKVcb.js → chunk-BSJP7CBP-DP7LTBll.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-C0O7Upmg.js → chunk-CIAEETIT-VfnIdN-h.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-gUSTktie.js → chunk-EDXVE4YY-BSbGSOxN.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-C-wW17kd.js → chunk-ENJZ2VHE-CszgAcWZ.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-C_E43NFJ.js → chunk-FMBD7UC4-w-yBZsN2.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-D9lWWHAu.js → chunk-FOC6F5B3-f9cFywhd.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-zLc9NsO5.js → chunk-ICPOFSXX-rgw1I6pt.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-DuhsNH4c.js → chunk-K5T4RW27-B_ZUrFUq.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-B4-A1Abi.js → chunk-KGLVRYIC-CcWTvRlI.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-BxqgHRgT.js → chunk-LIHQZDEY-D-5-peQw.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-DEYQ5WaJ.js → chunk-ORNJ4GCN-B4Z5L25I.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-Bt9RJ_rb.js → chunk-OYMX7WX6-DL4v2Nx4.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-CWh_0JsP.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-BAM6MO6g.js → chunk-U2HBQHQK-CwMCwyfq.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-DbOwgg1R.js → chunk-X2U36JSP-CvKfDVvq.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-CBfZXZDB.js → chunk-XPW4576I-m1Y_r88I.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-BXUfYLeM.js → chunk-YZCP3GAM-D7vju2_U.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-BBIHnMjI.js → chunk-ZZ45TVLE-sANOwJKn.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BoqQ3pHg.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-FoKuBK7n.js +1 -0
- package/payload/server/public/assets/clone-CT8AM-bU.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-Br2gjtEO.js → cose-bilkent-S5V4N54A-BhtgY3T7.js} +1 -1
- package/payload/server/public/assets/{dagre-Bv8aHCaJ.js → dagre-CplqWwSs.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-BZ0q7dDA.js → dagre-KV5264BT-DNbl5g9o.js} +1 -1
- package/payload/server/public/assets/data-BIDM6J5V.js +1 -0
- package/payload/server/public/assets/{device-url-actions-CK6x8-FQ.js → device-url-actions-nxh9PmIH.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-Bbzy2KoW.js → diagram-5BDNPKRD-BCdMaYnY.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-CadVHWrV.js → diagram-G4DWMVQ6-8XhDBU0o.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-DwJ6fVMv.js → diagram-MMDJMWI5-M-RMQgtx.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-Bh8d1vg-.js → diagram-TYMM5635-BBHPIBvQ.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-B1oTY2ob.js → erDiagram-SMLLAGMA-D6jOL-9s.js} +1 -1
- package/payload/server/public/assets/{flatten-Bo6YRmWl.js → flatten-BsWEYbBB.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BZU76iLs.js → flowDiagram-DWJPFMVM-CiyTYRlU.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-PjhJx62i.js → ganttDiagram-T4ZO3ILL-DHSIpjfS.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-DeTNsAO0.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-DLrxzJYi.js → gitGraphDiagram-UUTBAWPF-CEneVF3o.js} +1 -1
- package/payload/server/public/assets/graph-Cm-iWFqy.js +1 -0
- package/payload/server/public/assets/graph-labels-CFX6KkCz.js +1 -0
- package/payload/server/public/assets/{graphlib-D2ExKXiT.js → graphlib-CuF7rLfE.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-DJJ9GlS6.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-wGl-JoP7.js +2 -0
- package/payload/server/public/assets/{isEmpty-D6Kr-M1M.js → isEmpty-BWl67LAZ.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-Ed8ANWEq.js → ishikawaDiagram-UXIWVN3A-B3TxFNi_.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DF1gG775.js → journeyDiagram-VCZTEJTY-B5n8No7p.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-D8VcaKAp.js → kanban-definition-6JOO6SKY-ByGF39_h.js} +1 -1
- package/payload/server/public/assets/{line-CgMZIaVM.js → line-Br0FyyFW.js} +1 -1
- package/payload/server/public/assets/{linear-DOh_6k2k.js → linear-0O14Y6uf.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-ez9lChuL.js → mermaid-parser.core-BUcTjodx.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-BiVW_Lka.js → mermaid.core-BKzdtZgG.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-C-BlLBbo.js → mindmap-definition-QFDTVHPH-CobGMF9l.js} +1 -1
- package/payload/server/public/assets/{ordinal-BDi6f4xk.js → ordinal-krseTxxN.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-CGbvGkvF.js +1 -0
- package/payload/server/public/assets/{page-BQCQwUG5.js → page-B5NRnW0t.js} +1 -1
- package/payload/server/public/assets/{page-DStDaJgc.js → page-Ba0BAfpu.js} +2 -2
- package/payload/server/public/assets/pie-ZZUOXDRM-BZy8rjFn.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BK0-LxVl.js → pieDiagram-DEJITSTG-DXFmGcNR.js} +1 -1
- package/payload/server/public/assets/{public-DMk9cZKl.js → public-DWfBzf83.js} +5 -5
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-vKAQHu-t.js → quadrantDiagram-34T5L4WZ-BnChStlO.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-DcfWIVXr.js +1 -0
- package/payload/server/public/assets/{reduce-CGi9ik8i.js → reduce-tk-xY6Fv.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-Df9sXR35.js → requirementDiagram-MS252O5E-DdiBKsyO.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DG6xirA4.js → sankeyDiagram-XADWPNL6-16-np_56.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-B1a38KHX.js → sequenceDiagram-FGHM5R23-D67SCRpA.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-CRiIDR4O.js → stateDiagram-FHFEXIEX-CvNJX0YC.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CFw2U4iL.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-CkBOE7Y8.js → timeline-definition-GMOUNBTQ-BJE5YoBa.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-3WugwVdj.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-Cf5mDLlu.js +1 -0
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CwKBydYc.js → vennDiagram-DHZGUBPP-DF17ddXt.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-Bv4md4b3.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BWXYfzIr.js → wardleyDiagram-NUSXRM2D-xn_2XUOo.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-D-4v5Dy_.js → xychartDiagram-5P7HB3ND-DVF8zf3f.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -5
- package/payload/server/public/index.html +8 -7
- package/payload/server/public/public.html +5 -4
- package/payload/server/server.js +229 -22
- package/payload/server/public/assets/architecture-YZFGNWBL--v-pJPNp.js +0 -1
- package/payload/server/public/assets/channel-CkgyfA91.js +0 -1
- package/payload/server/public/assets/chunk-336JU56O-CbnUj19H.js +0 -2
- package/payload/server/public/assets/chunk-426QAEUC-DxGLqjw5.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-Bd-GrQM6.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-PyvJekA9.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-6hFNhiTb.js +0 -1
- package/payload/server/public/assets/clone-CFIJrRLl.js +0 -1
- package/payload/server/public/assets/data-BgADjmk1.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-CI0s_tqn.js +0 -1
- package/payload/server/public/assets/graph-B1_erzSo.js +0 -1
- package/payload/server/public/assets/graph-labels-CpFvcba2.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-g3gYW7Qm.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-Cn0r9nCK.js +0 -2
- package/payload/server/public/assets/packet-4T2RLAQJ-CT0TB9HI.js +0 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-CXLe7TFF.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-DnPLBl-D.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-C8cVmi_o.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-C3cb7Xwe.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-Dc7G3Bgm.js +0 -1
- package/payload/server/public/assets/wardley-RL74JXVD-DtgibWAt.js +0 -1
- /package/payload/server/public/assets/{_baseFor-Cam2PbSt.js → _baseFor-BHtDrjIo.js} +0 -0
- /package/payload/server/public/assets/{array-DYRGGQae.js → array-DetWRiSa.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-nWsJMTNI.js → cytoscape.esm-C9yNhe1u.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-Du1XY3Dp.js → defaultLocale-_WRwicXn.js} +0 -0
- /package/payload/server/public/assets/{dist-BzAsli7o.js → dist-Bd4S37oi.js} +0 -0
- /package/payload/server/public/assets/{init-B5BXBRcm.js → init-sTEcj9YX.js} +0 -0
- /package/payload/server/public/assets/{katex-HOUACuRw.js → katex-s61Rgv6l.js} +0 -0
- /package/payload/server/public/assets/{path-CNO468J-.js → path-B0Ik7Tu9.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-DRO6hWPh.js → rough.esm-DKRO8IF-.js} +0 -0
- /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
|
-
#
|
|
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
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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
|
|
227
|
-
#
|
|
228
|
-
# `{conversation}`
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
#
|
|
252
|
-
|
|
253
|
-
#
|
|
254
|
-
conds.append(("
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
conds.append(("
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
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
|
|
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
|
|
358
|
-
# Two assistant records sharing `message.id
|
|
359
|
-
#
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
#
|
|
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":"
|
|
366
|
-
printf '{"type":"assistant","timestamp":"2026-05-19T21:30:01.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"
|
|
367
|
-
printf '{"type":"assistant","timestamp":"2026-05-19T21:30:02.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"text","text":"
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
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
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
#
|
|
415
|
-
# the
|
|
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
|
-
|
|
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
|
|
488
|
-
# Transcript with 100 turns (50 user + 50 assistant, interleaved)
|
|
489
|
-
#
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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
|
|
567
|
-
#
|
|
568
|
-
#
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
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.
|
|
654
|
-
#
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
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
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
print(d.get("session_id", "") or "unknown")
|
|
57
|
+
d = json.load(sys.stdin)
|
|
39
58
|
except Exception:
|
|
40
|
-
|
|
41
|
-
|
|
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":
|
|
50
|
-
' "$
|
|
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
|
-
|
|
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
|