@rubytech/create-realagent-code 0.1.255 → 0.1.256
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/dist/__tests__/plugin-install.test.js +58 -40
- package/dist/index.js +77 -26
- package/dist/lib/plugin-install.js +31 -29
- package/package.json +1 -1
- package/payload/platform/config/brand-registry.json +8 -0
- package/payload/platform/config/brand.json +2 -2
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +12 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +2 -0
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +2 -0
- package/payload/platform/neo4j/schema.cypher +126 -0
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +3 -6
- package/payload/platform/plugins/admin/mcp/dist/index.js +0 -60
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/insight/SKILL.md +24 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +63 -10
- package/payload/platform/plugins/docs/PLUGIN.md +1 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +18 -5
- package/payload/platform/plugins/docs/references/memory-guide.md +4 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/slides.md +31 -0
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-typed-edge-pass.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +10 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-construction.md +72 -0
- package/payload/platform/plugins/slides/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/slides/LICENSE +21 -0
- package/payload/platform/plugins/slides/PLUGIN.md +18 -0
- package/payload/platform/plugins/slides/PROVENANCE.md +40 -0
- package/payload/platform/plugins/slides/commands/add-slide.md +29 -0
- package/payload/platform/plugins/slides/commands/slides-claus.md +39 -0
- package/payload/platform/plugins/slides/commands/slides-new-component.md +39 -0
- package/payload/platform/plugins/slides/commands/slides-outline.md +43 -0
- package/payload/platform/plugins/slides/commands/slides-review.md +52 -0
- package/payload/platform/plugins/slides/commands/slides-theme.md +64 -0
- package/payload/platform/plugins/slides/commands/slides.md +59 -0
- package/payload/platform/plugins/slides/skills/deck-system/REFERENCE.md +581 -0
- package/payload/platform/plugins/slides/skills/deck-system/SKILL.md +607 -0
- package/payload/platform/plugins/slides/skills/deck-system/STORYTELLING-board.md +426 -0
- package/payload/platform/plugins/slides/skills/deck-system/STORYTELLING-claus.md +185 -0
- package/payload/platform/plugins/slides/skills/deck-system/STORYTELLING-mbb.md +450 -0
- package/payload/platform/plugins/slides/skills/deck-system/STORYTELLING-product-launch.md +579 -0
- package/payload/platform/plugins/slides/skills/deck-system/STORYTELLING-sales.md +464 -0
- package/payload/platform/plugins/slides/skills/deck-system/STORYTELLING-sequoia.md +489 -0
- package/payload/platform/plugins/slides/skills/deck-system/STORYTELLING.md +273 -0
- package/payload/platform/plugins/slides/skills/deck-system/deck-craft.html +1371 -0
- package/payload/platform/plugins/slides/skills/deck-system/deck-solid.html +1667 -0
- package/payload/platform/plugins/slides/skills/deck-system/deck.html +1359 -0
- package/payload/platform/plugins/url-get/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/url-get/PLUGIN.md +26 -21
- package/payload/platform/plugins/url-get/mcp/dist/index.js +3 -3
- package/payload/platform/plugins/url-get/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts +1 -2
- package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts.map +1 -1
- package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js +20 -40
- package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js.map +1 -1
- package/payload/platform/scripts/setup-account.sh +1 -10
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +32 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +2 -2
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
- package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +1 -1
- package/payload/server/public/assets/AdminShell-T-YknnBn.js +1 -0
- package/payload/server/public/assets/Checkbox-DmDxpqVv.js +1 -0
- package/payload/server/public/assets/admin-COUV-jgt.js +1 -0
- package/payload/server/public/assets/{arc-aUiRP9AS.js → arc-B2CweJq3.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-Dnn6Hc65.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-c09loTER.js → architectureDiagram-Q4EWVU46-DP2o-MFV.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Cjdeyoq1.js → blockDiagram-DXYQGD6D-DO4mcYDJ.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-NY6Wlzo2.js → c4Diagram-AHTNJAMY-Sy1giHbj.js} +1 -1
- package/payload/server/public/assets/channel-CEpR_0rE.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-BK3470lx.js → chunk-2KRD3SAO-CKsCYCsN.js} +1 -1
- package/payload/server/public/assets/chunk-336JU56O-C0-P-aUF.js +2 -0
- package/payload/server/public/assets/chunk-426QAEUC-DFjEt3Zb.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-BOvVdJLf.js → chunk-4BX2VUAB-B8bqAmBa.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BXpto3yW.js → chunk-4TB4RGXK-D1k0VSlW.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-BwZyF7vR.js → chunk-55IACEB6-B-p_QNqz.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-C403gCUk.js → chunk-5FUZZQ4R-D6U6tV_j.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-CjVzXQEp.js → chunk-5PVQY5BW-CYK76xfs.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-D5bhMrtY.js → chunk-67CJDMHE-BC9js-lf.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-Si7Lgrwc.js → chunk-7N4EOEYR-4j2OqKkv.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-DMuHtDqO.js → chunk-AA7GKIK3-Coen-fXN.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-L79XKVcb.js → chunk-BSJP7CBP-CAiOBvec.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-C0O7Upmg.js → chunk-CIAEETIT-AJzzpZVb.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-DJcJAsAg.js → chunk-EDXVE4YY-BL4BKozX.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-CFDNvYu1.js → chunk-ENJZ2VHE-mhAFG8UD.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-C_E43NFJ.js → chunk-FMBD7UC4-H231gZA_.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-D9lWWHAu.js → chunk-FOC6F5B3-Cl3ZZjYG.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-ecLOxGhL.js → chunk-ICPOFSXX-DOEzvzJa.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-DuhsNH4c.js → chunk-K5T4RW27-C_ipbUDD.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-B4-A1Abi.js → chunk-KGLVRYIC-CTsDNSCU.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-BxqgHRgT.js → chunk-LIHQZDEY-DvSXhkGf.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-DEYQ5WaJ.js → chunk-ORNJ4GCN-p574NOI7.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-B7MW66KB.js → chunk-OYMX7WX6-BlEgFM6U.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-DpF06ZZQ.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-BMawmsyk.js → chunk-U2HBQHQK-B2bDK0jv.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CT6g7pno.js → chunk-X2U36JSP-D69BxKFw.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-CBfZXZDB.js → chunk-XPW4576I-Dm-PcyUi.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-xeAluiAH.js → chunk-YZCP3GAM-Be8RnXgx.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-BRN9qUC5.js → chunk-ZZ45TVLE-Ck8PCTa4.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CYbXvKLI.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DEyHzRhq.js +1 -0
- package/payload/server/public/assets/clone-y8gexbBy.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-Br2gjtEO.js → cose-bilkent-S5V4N54A-CmkW2Eaj.js} +1 -1
- package/payload/server/public/assets/{dagre-DTjePoco.js → dagre-Dqp-ns8F.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-DHBkRke4.js → dagre-KV5264BT-ZgWWXPLc.js} +1 -1
- package/payload/server/public/assets/data-gy6QH9c1.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-BIq1-idL.js → diagram-5BDNPKRD-CTX5-ScM.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-BsIUDzV6.js → diagram-G4DWMVQ6-BovIsO6H.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CgHSri2i.js → diagram-MMDJMWI5-DcETsQy-.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-Ce2Wh9ZX.js → diagram-TYMM5635-yyq6peoZ.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-BU0Kh6OQ.js → erDiagram-SMLLAGMA-CiNToftB.js} +1 -1
- package/payload/server/public/assets/{flatten-Bo6YRmWl.js → flatten-BtFI066E.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-B0N06MF7.js → flowDiagram-DWJPFMVM-Xnl3SpIM.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BVbx4ARZ.js → ganttDiagram-T4ZO3ILL-C1iyWe0f.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-CNs-LD5i.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-C-xRJ94t.js → gitGraphDiagram-UUTBAWPF-D97pbMQb.js} +1 -1
- package/payload/server/public/assets/graph-labels-cZu4pK16.js +1 -0
- package/payload/server/public/assets/{graph-g48ZcA5M.js → graph-qz5tFKqU.js} +3 -3
- package/payload/server/public/assets/{graphlib-YmNcoMjY.js → graphlib-Lq8ijgON.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-DsTNigSS.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-C_OarRTA.js +2 -0
- package/payload/server/public/assets/{isEmpty-D6Kr-M1M.js → isEmpty-D6QovjYR.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DTrq54yC.js → ishikawaDiagram-UXIWVN3A-B8XBdjJn.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-OZZZMrFX.js → journeyDiagram-VCZTEJTY-CZYbiOaQ.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY--w-IP9pN.js → kanban-definition-6JOO6SKY-B1PybFoh.js} +1 -1
- package/payload/server/public/assets/{line-Ckeulv5T.js → line-D-tw3hHp.js} +1 -1
- package/payload/server/public/assets/{linear-DOh_6k2k.js → linear-BHhXD3cd.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-CVRAxYRD.js → mermaid-parser.core-C9RAnysF.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-B-mE18I1.js → mermaid.core-B532LT1r.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-Bm8mDicL.js → mindmap-definition-QFDTVHPH-DGlgeeTV.js} +1 -1
- package/payload/server/public/assets/{ordinal-BDi6f4xk.js → ordinal-Bl-aM5b9.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-DGES22b-.js +1 -0
- package/payload/server/public/assets/pie-ZZUOXDRM-ChKeDbzt.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BCmRLgGO.js → pieDiagram-DEJITSTG-DV9FIWko.js} +1 -1
- package/payload/server/public/assets/{public-DknO-g9S.js → public-Bu2_Xi0a.js} +5 -5
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CniTIUTm.js → quadrantDiagram-34T5L4WZ-Betwya4l.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-FGG5Fs7N.js +1 -0
- package/payload/server/public/assets/{reduce-CGi9ik8i.js → reduce-BD4xUd2c.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-CoxBSj9M.js → requirementDiagram-MS252O5E-Cq3vODdg.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BjS-4jzq.js → sankeyDiagram-XADWPNL6-x8krXWcS.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-B9jVOnPR.js → sequenceDiagram-FGHM5R23-i-_uH-Yl.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BvOQPzP8.js → stateDiagram-FHFEXIEX-il4KqSgI.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B6zNJ6Tv.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-CdfgWLo1.js → timeline-definition-GMOUNBTQ-DATdZkA5.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-VAQQdbtf.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-DKchO3zI.js +1 -0
- package/payload/server/public/assets/useSelectionMode-A5KItZ2T.js +13 -0
- package/payload/server/public/assets/{brand-D0gNihp7.css → useSelectionMode-C-Ojh7W9.css} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-JCgpIbj-.js → vennDiagram-DHZGUBPP-BJh9tJTt.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-CBGtx0bS.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-Dei3VqHo.js → wardleyDiagram-NUSXRM2D-EMN1Hdfg.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DUtIyoIb.js → xychartDiagram-5P7HB3ND-DbUWXa7T.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +5 -6
- package/payload/server/public/public.html +4 -5
- package/payload/server/server.js +53 -12
- package/payload/platform/plugins/admin/hooks/__tests__/insight.test.sh +0 -395
- package/payload/platform/plugins/admin/hooks/insight.sh +0 -219
- package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +0 -239
- package/payload/server/public/assets/AdminShell-892Jy_rs.js +0 -1
- package/payload/server/public/assets/Checkbox-Bc2QzX9b.js +0 -1
- package/payload/server/public/assets/admin-D3K13ndi.js +0 -1
- package/payload/server/public/assets/architecture-YZFGNWBL--v-pJPNp.js +0 -1
- package/payload/server/public/assets/brand-CcN3dELF.js +0 -9
- package/payload/server/public/assets/channel-B1IT7to2.js +0 -1
- package/payload/server/public/assets/chunk-336JU56O-CdKRCIeE.js +0 -2
- package/payload/server/public/assets/chunk-426QAEUC-BybuQ3Ve.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-Bd-GrQM6.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-rjCize6i.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BORWOUt0.js +0 -1
- package/payload/server/public/assets/clone-Csqv5U6T.js +0 -1
- package/payload/server/public/assets/data-Br-pdljK.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-CI0s_tqn.js +0 -1
- package/payload/server/public/assets/graph-labels-BYH-IPCb.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-g3gYW7Qm.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-Di6oPQ_-.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-v4ND10uR.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/useSelectionMode-DwsyptOw.js +0 -5
- package/payload/server/public/assets/wardley-RL74JXVD-DtgibWAt.js +0 -1
- /package/payload/server/public/assets/{_baseFor-Cam2PbSt.js → _baseFor-Cs8Y-rGh.js} +0 -0
- /package/payload/server/public/assets/{array-DYRGGQae.js → array-iHZP4KWJ.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-nWsJMTNI.js → cytoscape.esm-BR2GOQ8_.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-Du1XY3Dp.js → defaultLocale-B9aLeOTg.js} +0 -0
- /package/payload/server/public/assets/{dist-BzAsli7o.js → dist-DB-VPj_8.js} +0 -0
- /package/payload/server/public/assets/{init-B5BXBRcm.js → init-BNFRgqHM.js} +0 -0
- /package/payload/server/public/assets/{katex-HOUACuRw.js → katex-B-EfS3nw.js} +0 -0
- /package/payload/server/public/assets/{path-CNO468J-.js → path-DmWWdwp7.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-DRO6hWPh.js → rough.esm-Ci7Kjt46.js} +0 -0
- /package/payload/server/public/assets/{src-CWiyyVfn.js → src-C1jfwBq0.js} +0 -0
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Task 634 — on-demand `/insight` UserPromptSubmit hook test suite.
|
|
3
|
-
#
|
|
4
|
-
# Listener-mock pattern: a local Python HTTP server stands in for
|
|
5
|
-
# /api/admin/log-ingest and records every POST so the assertions can read
|
|
6
|
-
# every log line the hook emitted.
|
|
7
|
-
#
|
|
8
|
-
# Contract under test (run-and-continue, no Stop gate):
|
|
9
|
-
# - Event is UserPromptSubmit. Trigger detection reads the `prompt` field
|
|
10
|
-
# of the stdin envelope, NOT the transcript. The token `/insight` is
|
|
11
|
-
# matched case-insensitively, as the whole message (after strip) OR as a
|
|
12
|
-
# standalone line — never embedded in prose.
|
|
13
|
-
# - On match: the four-pass instruction is printed to STDOUT (UserPromptSubmit
|
|
14
|
-
# stdout is injected as turn context), `trigger sessionId=<id> token=/insight`
|
|
15
|
-
# is emitted via log-ingest, exit 0. No stderr instruction, no exit 2.
|
|
16
|
-
# - Every gated-off / non-match path emits one `trigger-skipped reason=<r>`
|
|
17
|
-
# line and exits 0:
|
|
18
|
-
# role-not-admin | is-specialist | empty-stdin | missing-transcript |
|
|
19
|
-
# no-insight-token
|
|
20
|
-
# - Standing reconciliation: on every invocation the hook scans the
|
|
21
|
-
# transcript (`transcript_path`) for the latest real-user turn whose text
|
|
22
|
-
# matches `/insight`; if no `session-retrospective-mark-complete` tool_use
|
|
23
|
-
# appears after that turn, it emits
|
|
24
|
-
# `prior-incomplete sessionId=<id> triggered-turn=<idx>`. A sentinel after
|
|
25
|
-
# the latest `/insight` turn suppresses the emit.
|
|
26
|
-
# - All log emissions go through POST /api/admin/log-ingest. The only stdout
|
|
27
|
-
# writer is the instruction block on the trigger path.
|
|
28
|
-
|
|
29
|
-
set -u
|
|
30
|
-
|
|
31
|
-
HOOK="$(cd "$(dirname "$0")/.." && pwd)/insight.sh"
|
|
32
|
-
if [[ ! -x "$HOOK" ]]; then
|
|
33
|
-
echo "FAIL: $HOOK not executable" >&2
|
|
34
|
-
exit 1
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
OP_ID='aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'
|
|
38
|
-
|
|
39
|
-
TMPFILES=()
|
|
40
|
-
LISTENER_PIDS=()
|
|
41
|
-
cleanup_test_state() {
|
|
42
|
-
for pid in "${LISTENER_PIDS[@]:-}"; do
|
|
43
|
-
if [[ -n "$pid" ]]; then
|
|
44
|
-
kill "$pid" 2>/dev/null || true
|
|
45
|
-
wait "$pid" 2>/dev/null || true
|
|
46
|
-
fi
|
|
47
|
-
done
|
|
48
|
-
for f in "${TMPFILES[@]:-}"; do
|
|
49
|
-
[[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
|
|
50
|
-
done
|
|
51
|
-
}
|
|
52
|
-
trap cleanup_test_state EXIT
|
|
53
|
-
|
|
54
|
-
PASS=0
|
|
55
|
-
FAIL=0
|
|
56
|
-
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
57
|
-
fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
|
58
|
-
|
|
59
|
-
start_listener() {
|
|
60
|
-
REQ_LOG=$(mktemp); TMPFILES+=("$REQ_LOG")
|
|
61
|
-
LISTENER_PORT=$((39500 + RANDOM % 100))
|
|
62
|
-
python3 - "$LISTENER_PORT" "$REQ_LOG" <<'PY' &
|
|
63
|
-
import sys, http.server, json
|
|
64
|
-
port = int(sys.argv[1])
|
|
65
|
-
log_path = sys.argv[2]
|
|
66
|
-
class H(http.server.BaseHTTPRequestHandler):
|
|
67
|
-
def log_message(self, *a, **k): pass
|
|
68
|
-
def do_POST(self):
|
|
69
|
-
n = int(self.headers.get('Content-Length','0') or 0)
|
|
70
|
-
body = self.rfile.read(n).decode('utf-8','replace')
|
|
71
|
-
with open(log_path, 'a', encoding='utf-8') as f:
|
|
72
|
-
f.write(self.path + '\t' + body + '\n')
|
|
73
|
-
self.send_response(200)
|
|
74
|
-
self.send_header('Content-Type','application/json')
|
|
75
|
-
self.end_headers()
|
|
76
|
-
self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
|
|
77
|
-
http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()
|
|
78
|
-
PY
|
|
79
|
-
LISTENER_PIDS+=("$!")
|
|
80
|
-
for _ in $(seq 1 20); do
|
|
81
|
-
if curl -sS --max-time 1 -X POST "http://127.0.0.1:${LISTENER_PORT}/ping" -d '{}' >/dev/null 2>&1; then
|
|
82
|
-
break
|
|
83
|
-
fi
|
|
84
|
-
sleep 0.1
|
|
85
|
-
done
|
|
86
|
-
: > "$REQ_LOG"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
run_hook() {
|
|
90
|
-
local role="$1"; local specialist="$2"; local stdin_json="$3"
|
|
91
|
-
local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
|
|
92
|
-
local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
|
|
93
|
-
printf '%s' "$stdin_json" | \
|
|
94
|
-
MAXY_SESSION_ROLE="$role" \
|
|
95
|
-
MAXY_SPECIALIST="$specialist" \
|
|
96
|
-
MAXY_UI_INTERNAL_PORT="$LISTENER_PORT" \
|
|
97
|
-
bash "$HOOK" >"$stdout_file" 2>"$stderr_file"
|
|
98
|
-
HOOK_RC=$?
|
|
99
|
-
HOOK_STDERR=$(cat "$stderr_file")
|
|
100
|
-
HOOK_STDOUT=$(cat "$stdout_file")
|
|
101
|
-
sleep 0.1
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
ingest_lines() {
|
|
105
|
-
grep -E '^/api/admin/log-ingest ' "$REQ_LOG" 2>/dev/null | python3 -c '
|
|
106
|
-
import sys, json
|
|
107
|
-
for raw in sys.stdin:
|
|
108
|
-
try:
|
|
109
|
-
_, body = raw.rstrip("\n").split("\t", 1)
|
|
110
|
-
d = json.loads(body)
|
|
111
|
-
if isinstance(d, dict) and isinstance(d.get("line"), str):
|
|
112
|
-
print(d["line"])
|
|
113
|
-
except Exception:
|
|
114
|
-
pass
|
|
115
|
-
' || true
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
# Build a JSONL transcript from positional triples. Each triple is
|
|
119
|
-
# role|text|extra where extra is one of:
|
|
120
|
-
# "" → plain user/assistant text record
|
|
121
|
-
# "tool_use:<NAME>" → assistant record carrying a tool_use block
|
|
122
|
-
# "tool_result" → user record carrying only a tool_result block
|
|
123
|
-
write_transcript() {
|
|
124
|
-
local out="$1"; shift
|
|
125
|
-
: > "$out"
|
|
126
|
-
python3 - "$out" "$@" <<'PY'
|
|
127
|
-
import sys, json
|
|
128
|
-
out = sys.argv[1]
|
|
129
|
-
spec = sys.argv[2:]
|
|
130
|
-
with open(out, "w", encoding="utf-8") as f:
|
|
131
|
-
for entry in spec:
|
|
132
|
-
role, text, extra = entry.split("|", 2)
|
|
133
|
-
rec = {"type": role, "timestamp": "2026-06-05T10:00:00.000Z"}
|
|
134
|
-
if role == "user":
|
|
135
|
-
if extra == "tool_result":
|
|
136
|
-
rec["message"] = {
|
|
137
|
-
"role": "user",
|
|
138
|
-
"content": [
|
|
139
|
-
{"type": "tool_result",
|
|
140
|
-
"tool_use_id": "tu_x",
|
|
141
|
-
"content": text or "ok"},
|
|
142
|
-
],
|
|
143
|
-
}
|
|
144
|
-
else:
|
|
145
|
-
rec["message"] = {"role": "user", "content": text}
|
|
146
|
-
else:
|
|
147
|
-
content_blocks = []
|
|
148
|
-
if text:
|
|
149
|
-
content_blocks.append({"type": "text", "text": text})
|
|
150
|
-
if extra.startswith("tool_use:"):
|
|
151
|
-
tool_name = extra.split(":", 1)[1]
|
|
152
|
-
content_blocks.append({
|
|
153
|
-
"type": "tool_use",
|
|
154
|
-
"id": f"toolu_{tool_name}",
|
|
155
|
-
"name": tool_name,
|
|
156
|
-
"input": {},
|
|
157
|
-
})
|
|
158
|
-
rec["message"] = {
|
|
159
|
-
"id": f"msg_{role}_{hash(text) & 0xffff:04x}",
|
|
160
|
-
"role": "assistant",
|
|
161
|
-
"content": content_blocks,
|
|
162
|
-
}
|
|
163
|
-
f.write(json.dumps(rec) + "\n")
|
|
164
|
-
PY
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
# UserPromptSubmit envelope: session_id, transcript_path, prompt, hook_event_name.
|
|
168
|
-
envelope_for() {
|
|
169
|
-
python3 -c '
|
|
170
|
-
import json, sys
|
|
171
|
-
print(json.dumps({
|
|
172
|
-
"session_id": sys.argv[1],
|
|
173
|
-
"transcript_path": sys.argv[2],
|
|
174
|
-
"prompt": sys.argv[3],
|
|
175
|
-
"hook_event_name": "UserPromptSubmit",
|
|
176
|
-
}))
|
|
177
|
-
' "$1" "$2" "$3"
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
# A transcript with no prior /insight turn — used whenever the test is about
|
|
181
|
-
# trigger detection and reconciliation must stay silent.
|
|
182
|
-
NEUTRAL_TRANSCRIPT=$(mktemp); TMPFILES+=("$NEUTRAL_TRANSCRIPT")
|
|
183
|
-
|
|
184
|
-
# ---------------------------------------------------------------------------
|
|
185
|
-
# Case 1: role != admin → trigger-skipped reason=role-not-admin
|
|
186
|
-
# ---------------------------------------------------------------------------
|
|
187
|
-
start_listener
|
|
188
|
-
write_transcript "$NEUTRAL_TRANSCRIPT" "user|hello|" "assistant|hi|"
|
|
189
|
-
ENV_INSIGHT=$(envelope_for "$OP_ID" "$NEUTRAL_TRANSCRIPT" "/insight")
|
|
190
|
-
: > "$REQ_LOG"
|
|
191
|
-
run_hook "public" "" "$ENV_INSIGHT"
|
|
192
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-1 rc=$HOOK_RC"
|
|
193
|
-
[[ -z "$HOOK_STDOUT" ]] || fail "case-1 stdout must be empty, got: $HOOK_STDOUT"
|
|
194
|
-
if ingest_lines | grep -qE '^trigger-skipped sessionId=.* reason=role-not-admin$'; then
|
|
195
|
-
pass "case-1 role=public → trigger-skipped reason=role-not-admin"
|
|
196
|
-
else
|
|
197
|
-
fail "case-1 expected role-not-admin, got: $(ingest_lines)"
|
|
198
|
-
fi
|
|
199
|
-
|
|
200
|
-
# ---------------------------------------------------------------------------
|
|
201
|
-
# Case 2: MAXY_SPECIALIST set → trigger-skipped is-specialist
|
|
202
|
-
# ---------------------------------------------------------------------------
|
|
203
|
-
: > "$REQ_LOG"
|
|
204
|
-
run_hook "admin" "database-operator" "$ENV_INSIGHT"
|
|
205
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-2 rc=$HOOK_RC"
|
|
206
|
-
[[ -z "$HOOK_STDOUT" ]] || fail "case-2 stdout must be empty, got: $HOOK_STDOUT"
|
|
207
|
-
if ingest_lines | grep -qE '^trigger-skipped sessionId=.* reason=is-specialist specialist=database-operator$'; then
|
|
208
|
-
pass "case-2 specialist=database-operator → trigger-skipped is-specialist"
|
|
209
|
-
else
|
|
210
|
-
fail "case-2 expected is-specialist, got: $(ingest_lines)"
|
|
211
|
-
fi
|
|
212
|
-
|
|
213
|
-
# ---------------------------------------------------------------------------
|
|
214
|
-
# Case 3: empty stdin → trigger-skipped reason=empty-stdin
|
|
215
|
-
# ---------------------------------------------------------------------------
|
|
216
|
-
: > "$REQ_LOG"
|
|
217
|
-
run_hook "admin" "" ""
|
|
218
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-3 rc=$HOOK_RC"
|
|
219
|
-
if ingest_lines | grep -qE '^trigger-skipped sessionId=.* reason=empty-stdin$'; then
|
|
220
|
-
pass "case-3 empty stdin → trigger-skipped reason=empty-stdin"
|
|
221
|
-
else
|
|
222
|
-
fail "case-3 expected empty-stdin, got: $(ingest_lines)"
|
|
223
|
-
fi
|
|
224
|
-
|
|
225
|
-
# ---------------------------------------------------------------------------
|
|
226
|
-
# Case 4: missing transcript_path → trigger-skipped missing-transcript
|
|
227
|
-
# ---------------------------------------------------------------------------
|
|
228
|
-
: > "$REQ_LOG"
|
|
229
|
-
BAD_ENV=$(python3 -c 'import json,sys; print(json.dumps({"session_id": sys.argv[1], "prompt": "/insight"}))' "$OP_ID")
|
|
230
|
-
run_hook "admin" "" "$BAD_ENV"
|
|
231
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-4 rc=$HOOK_RC"
|
|
232
|
-
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=missing-transcript$"; then
|
|
233
|
-
pass "case-4 missing transcript → trigger-skipped reason=missing-transcript"
|
|
234
|
-
else
|
|
235
|
-
fail "case-4 expected missing-transcript, got: $(ingest_lines)"
|
|
236
|
-
fi
|
|
237
|
-
|
|
238
|
-
# ---------------------------------------------------------------------------
|
|
239
|
-
# Case 5: prompt has no /insight token → trigger-skipped reason=no-insight-token
|
|
240
|
-
# ---------------------------------------------------------------------------
|
|
241
|
-
ENV_BORING=$(envelope_for "$OP_ID" "$NEUTRAL_TRANSCRIPT" "what's the weather today")
|
|
242
|
-
: > "$REQ_LOG"
|
|
243
|
-
run_hook "admin" "" "$ENV_BORING"
|
|
244
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-5 rc=$HOOK_RC"
|
|
245
|
-
[[ -z "$HOOK_STDOUT" ]] || fail "case-5 stdout must be empty"
|
|
246
|
-
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=no-insight-token$"; then
|
|
247
|
-
pass "case-5 no token → trigger-skipped reason=no-insight-token"
|
|
248
|
-
else
|
|
249
|
-
fail "case-5 expected no-insight-token, got: $(ingest_lines)"
|
|
250
|
-
fi
|
|
251
|
-
|
|
252
|
-
# ---------------------------------------------------------------------------
|
|
253
|
-
# Case 6: prompt is exactly /insight → instruction on stdout, trigger logged
|
|
254
|
-
# ---------------------------------------------------------------------------
|
|
255
|
-
: > "$REQ_LOG"
|
|
256
|
-
run_hook "admin" "" "$ENV_INSIGHT"
|
|
257
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-6 expected rc=0, got $HOOK_RC"
|
|
258
|
-
[[ -z "$HOOK_STDERR" ]] || fail "case-6 stderr must be empty (no exit-2 instruction path), got: $HOOK_STDERR"
|
|
259
|
-
if ! printf '%s' "$HOOK_STDOUT" | grep -q "session-retrospective-mark-complete"; then
|
|
260
|
-
fail "case-6 instruction (stdout) missing sentinel tool name"
|
|
261
|
-
fi
|
|
262
|
-
if ! printf '%s' "$HOOK_STDOUT" | grep -q "database-operator"; then
|
|
263
|
-
fail "case-6 instruction (stdout) missing database-operator reference"
|
|
264
|
-
fi
|
|
265
|
-
if ! printf '%s' "$HOOK_STDOUT" | grep -qi "keep going\|continue"; then
|
|
266
|
-
fail "case-6 instruction (stdout) missing run-and-continue wording"
|
|
267
|
-
fi
|
|
268
|
-
if ingest_lines | grep -qE "^trigger sessionId=${OP_ID} token=/insight$"; then
|
|
269
|
-
pass "case-6 /insight whole-message → instruction on stdout + trigger logged"
|
|
270
|
-
else
|
|
271
|
-
fail "case-6 expected trigger line, got: $(ingest_lines)"
|
|
272
|
-
fi
|
|
273
|
-
|
|
274
|
-
# ---------------------------------------------------------------------------
|
|
275
|
-
# Case 7: /insight as a standalone line among other lines → triggers
|
|
276
|
-
# ---------------------------------------------------------------------------
|
|
277
|
-
ENV_LINE=$(envelope_for "$OP_ID" "$NEUTRAL_TRANSCRIPT" $'here is some context\n/insight\nthanks')
|
|
278
|
-
: > "$REQ_LOG"
|
|
279
|
-
run_hook "admin" "" "$ENV_LINE"
|
|
280
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-7 rc=$HOOK_RC"
|
|
281
|
-
if ingest_lines | grep -qE "^trigger sessionId=${OP_ID} token=/insight$"; then
|
|
282
|
-
pass "case-7 /insight standalone line → triggers"
|
|
283
|
-
else
|
|
284
|
-
fail "case-7 expected trigger, got: $(ingest_lines)"
|
|
285
|
-
fi
|
|
286
|
-
|
|
287
|
-
# ---------------------------------------------------------------------------
|
|
288
|
-
# Case 8: case-insensitive — /INSIGHT triggers, logged as /insight
|
|
289
|
-
# ---------------------------------------------------------------------------
|
|
290
|
-
ENV_CASE=$(envelope_for "$OP_ID" "$NEUTRAL_TRANSCRIPT" "/INSIGHT")
|
|
291
|
-
: > "$REQ_LOG"
|
|
292
|
-
run_hook "admin" "" "$ENV_CASE"
|
|
293
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-8 rc=$HOOK_RC"
|
|
294
|
-
if ingest_lines | grep -qE "^trigger sessionId=${OP_ID} token=/insight$"; then
|
|
295
|
-
pass "case-8 case-insensitive: /INSIGHT normalises to /insight"
|
|
296
|
-
else
|
|
297
|
-
fail "case-8 expected token=/insight, got: $(ingest_lines)"
|
|
298
|
-
fi
|
|
299
|
-
|
|
300
|
-
# ---------------------------------------------------------------------------
|
|
301
|
-
# Case 9: embedded prose does NOT trigger
|
|
302
|
-
# ---------------------------------------------------------------------------
|
|
303
|
-
ENV_PROSE=$(envelope_for "$OP_ID" "$NEUTRAL_TRANSCRIPT" "can you give me some /insight into the numbers")
|
|
304
|
-
: > "$REQ_LOG"
|
|
305
|
-
run_hook "admin" "" "$ENV_PROSE"
|
|
306
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-9 rc=$HOOK_RC"
|
|
307
|
-
[[ -z "$HOOK_STDOUT" ]] || fail "case-9 stdout must be empty (prose must not trigger)"
|
|
308
|
-
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=no-insight-token$"; then
|
|
309
|
-
pass "case-9 embedded '/insight' prose does not trigger"
|
|
310
|
-
else
|
|
311
|
-
fail "case-9 expected no-insight-token, got: $(ingest_lines)"
|
|
312
|
-
fi
|
|
313
|
-
|
|
314
|
-
# ---------------------------------------------------------------------------
|
|
315
|
-
# Case 10: reconciliation — prior /insight turn with NO sentinel after it →
|
|
316
|
-
# prior-incomplete emitted (and current non-token prompt skips)
|
|
317
|
-
# ---------------------------------------------------------------------------
|
|
318
|
-
T_INCOMPLETE=$(mktemp); TMPFILES+=("$T_INCOMPLETE")
|
|
319
|
-
write_transcript "$T_INCOMPLETE" \
|
|
320
|
-
"user|hi|" \
|
|
321
|
-
"assistant|hello|" \
|
|
322
|
-
"user|/insight|" \
|
|
323
|
-
"assistant|here is a prose summary|"
|
|
324
|
-
ENV_RECON=$(envelope_for "$OP_ID" "$T_INCOMPLETE" "what's next")
|
|
325
|
-
: > "$REQ_LOG"
|
|
326
|
-
run_hook "admin" "" "$ENV_RECON"
|
|
327
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-10 rc=$HOOK_RC"
|
|
328
|
-
if ! ingest_lines | grep -qE "^prior-incomplete sessionId=${OP_ID} triggered-turn=[0-9]+$"; then
|
|
329
|
-
fail "case-10 expected prior-incomplete, got: $(ingest_lines)"
|
|
330
|
-
elif ! ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=no-insight-token$"; then
|
|
331
|
-
fail "case-10 expected current prompt skip, got: $(ingest_lines)"
|
|
332
|
-
else
|
|
333
|
-
pass "case-10 prior /insight without sentinel → prior-incomplete"
|
|
334
|
-
fi
|
|
335
|
-
|
|
336
|
-
# ---------------------------------------------------------------------------
|
|
337
|
-
# Case 11: reconciliation negative — prior /insight turn WITH a sentinel
|
|
338
|
-
# tool_use after it → no prior-incomplete
|
|
339
|
-
# ---------------------------------------------------------------------------
|
|
340
|
-
T_COMPLETE=$(mktemp); TMPFILES+=("$T_COMPLETE")
|
|
341
|
-
write_transcript "$T_COMPLETE" \
|
|
342
|
-
"user|hi|" \
|
|
343
|
-
"user|/insight|" \
|
|
344
|
-
"assistant|running passes|" \
|
|
345
|
-
"assistant||tool_use:mcp__admin__session-retrospective-mark-complete"
|
|
346
|
-
ENV_DONE=$(envelope_for "$OP_ID" "$T_COMPLETE" "what's next")
|
|
347
|
-
: > "$REQ_LOG"
|
|
348
|
-
run_hook "admin" "" "$ENV_DONE"
|
|
349
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-11 rc=$HOOK_RC"
|
|
350
|
-
if ingest_lines | grep -qE "^prior-incomplete"; then
|
|
351
|
-
fail "case-11 sentinel present must suppress prior-incomplete, got: $(ingest_lines)"
|
|
352
|
-
else
|
|
353
|
-
pass "case-11 prior /insight with sentinel → no prior-incomplete"
|
|
354
|
-
fi
|
|
355
|
-
|
|
356
|
-
# ---------------------------------------------------------------------------
|
|
357
|
-
# Case 13: the just-submitted /insight turn is already in the transcript with
|
|
358
|
-
# no assistant response yet → must NOT self-report prior-incomplete.
|
|
359
|
-
# (Guards against the false positive if Claude Code writes the
|
|
360
|
-
# current prompt to the transcript before UserPromptSubmit fires.)
|
|
361
|
-
# ---------------------------------------------------------------------------
|
|
362
|
-
T_CURRENT=$(mktemp); TMPFILES+=("$T_CURRENT")
|
|
363
|
-
write_transcript "$T_CURRENT" \
|
|
364
|
-
"user|hi|" \
|
|
365
|
-
"assistant|hello|" \
|
|
366
|
-
"user|/insight|"
|
|
367
|
-
ENV_CURRENT=$(envelope_for "$OP_ID" "$T_CURRENT" "/insight")
|
|
368
|
-
: > "$REQ_LOG"
|
|
369
|
-
run_hook "admin" "" "$ENV_CURRENT"
|
|
370
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-13 rc=$HOOK_RC"
|
|
371
|
-
if ingest_lines | grep -qE "^prior-incomplete"; then
|
|
372
|
-
fail "case-13 current /insight turn must not self-report prior-incomplete, got: $(ingest_lines)"
|
|
373
|
-
elif ingest_lines | grep -qE "^trigger sessionId=${OP_ID} token=/insight$"; then
|
|
374
|
-
pass "case-13 current /insight (in transcript, no response yet) → trigger only, no prior-incomplete"
|
|
375
|
-
else
|
|
376
|
-
fail "case-13 expected trigger without prior-incomplete, got: $(ingest_lines)"
|
|
377
|
-
fi
|
|
378
|
-
|
|
379
|
-
# ---------------------------------------------------------------------------
|
|
380
|
-
# Case 12: no path returns exit 2 — /insight must not block the turn
|
|
381
|
-
# ---------------------------------------------------------------------------
|
|
382
|
-
: > "$REQ_LOG"
|
|
383
|
-
run_hook "admin" "" "$ENV_INSIGHT"
|
|
384
|
-
if [[ "$HOOK_RC" -eq 2 ]]; then
|
|
385
|
-
fail "case-12 hook must never exit 2 (run-and-continue), got rc=$HOOK_RC"
|
|
386
|
-
else
|
|
387
|
-
pass "case-12 trigger path exits 0, never 2"
|
|
388
|
-
fi
|
|
389
|
-
|
|
390
|
-
# ---------------------------------------------------------------------------
|
|
391
|
-
# Summary
|
|
392
|
-
# ---------------------------------------------------------------------------
|
|
393
|
-
echo
|
|
394
|
-
echo "insight tests: $PASS passed, $FAIL failed"
|
|
395
|
-
[[ "$FAIL" -eq 0 ]]
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# UserPromptSubmit hook — on-demand `/insight`. When the operator types the
|
|
3
|
-
# literal token `/insight` as a standalone message (or a standalone line), the
|
|
4
|
-
# admin agent runs a four-pass review (technical learnings, operator-
|
|
5
|
-
# relationship updates, graph-completeness sweep, typed-edge auto-extraction)
|
|
6
|
-
# over the session so far and calls the deterministic
|
|
7
|
-
# `session-retrospective-mark-complete` MCP sentinel — then the session
|
|
8
|
-
# CONTINUES. Nothing is blocked, terminated, reset, or cleared. Task 634
|
|
9
|
-
# replaced the Task 282 session-end Stop gate with this trigger; the four
|
|
10
|
-
# passes and the sentinel call are byte-for-byte the same process — only the
|
|
11
|
-
# trigger and timing changed.
|
|
12
|
-
#
|
|
13
|
-
# Trigger detection. The token is read from the `prompt` field of the
|
|
14
|
-
# UserPromptSubmit stdin envelope, case-insensitively, matching the whole
|
|
15
|
-
# message (after .strip()) OR a standalone line within it — never embedded in
|
|
16
|
-
# prose. This is the same whole-message / standalone-line discipline the
|
|
17
|
-
# session-end gate applied to its end-intent tokens; it keeps "give me some
|
|
18
|
-
# /insight into the numbers" from false-triggering.
|
|
19
|
-
#
|
|
20
|
-
# Injection. On a match the four-pass instruction is printed to STDOUT.
|
|
21
|
-
# UserPromptSubmit stdout is appended to the turn as additional context, so the
|
|
22
|
-
# agent receives the instruction alongside the operator's `/insight` message.
|
|
23
|
-
# Structured logs go through POST /api/admin/log-ingest (never stdout), so the
|
|
24
|
-
# injected context stays clean. There is no exit-2 path and no stderr
|
|
25
|
-
# instruction — UserPromptSubmit cannot and must not block the turn.
|
|
26
|
-
#
|
|
27
|
-
# Standing reconciliation (no-event failure detector). The run-and-continue
|
|
28
|
-
# model removes the Stop gate that previously forced completion, so the
|
|
29
|
-
# dominant failure mode is "triggered but never completed" — the agent injects
|
|
30
|
-
# the instruction, returns prose, and never calls the sentinel. Because this
|
|
31
|
-
# hook fires on every prompt, it scans the transcript (`transcript_path`) each
|
|
32
|
-
# invocation for the latest real-user turn whose text matches `/insight`; when
|
|
33
|
-
# no `session-retrospective-mark-complete` tool_use appears after that turn it
|
|
34
|
-
# emits `[insight] prior-incomplete sessionId=<id> triggered-turn=<idx>`. This
|
|
35
|
-
# surfaces an unfinished pass on the next prompt without waiting for session
|
|
36
|
-
# end and without reproducing the failure.
|
|
37
|
-
# [[feedback_no_stdout_parsing_for_control_flow]] — the trigger is a literal
|
|
38
|
-
# token match, never prose parsing.
|
|
39
|
-
#
|
|
40
|
-
# Gating (common guards live in `lib/admin-graph-pass-common.sh`):
|
|
41
|
-
# - role-not-admin / is-specialist / empty-stdin / missing-transcript:
|
|
42
|
-
# handled by the shared helper, emit `trigger-skipped` and exit 0.
|
|
43
|
-
# - no-insight-token: the prompt carries no standalone `/insight` token,
|
|
44
|
-
# emit `trigger-skipped` and exit 0.
|
|
45
|
-
#
|
|
46
|
-
# Input: Claude Code's UserPromptSubmit stdin shape
|
|
47
|
-
# { "session_id": "<id>", "transcript_path": "<jsonl path>",
|
|
48
|
-
# "prompt": "<operator message>", "hook_event_name": "UserPromptSubmit" }
|
|
49
|
-
|
|
50
|
-
set -uo pipefail
|
|
51
|
-
|
|
52
|
-
AGP_LOG_TAG="insight"
|
|
53
|
-
AGP_LIB="$(dirname "$0")/lib/admin-graph-pass-common.sh"
|
|
54
|
-
if [ ! -f "$AGP_LIB" ]; then
|
|
55
|
-
echo "[${AGP_LOG_TAG}] trigger-skipped sessionId=unknown reason=missing-helper path=${AGP_LIB}" >&2
|
|
56
|
-
exit 0
|
|
57
|
-
fi
|
|
58
|
-
# shellcheck source=lib/admin-graph-pass-common.sh
|
|
59
|
-
source "$AGP_LIB"
|
|
60
|
-
|
|
61
|
-
agp_setup_logging
|
|
62
|
-
agp_read_stdin
|
|
63
|
-
agp_parse_stdin
|
|
64
|
-
agp_guard_role_specialist
|
|
65
|
-
agp_guard_transcript require-stdin
|
|
66
|
-
|
|
67
|
-
# Extract the operator's raw prompt from the envelope. Empty when absent.
|
|
68
|
-
PROMPT=$(printf '%s' "$INPUT" | python3 -c '
|
|
69
|
-
import sys, json
|
|
70
|
-
try:
|
|
71
|
-
d = json.load(sys.stdin)
|
|
72
|
-
p = d.get("prompt", "") or ""
|
|
73
|
-
sys.stdout.write(p if isinstance(p, str) else "")
|
|
74
|
-
except Exception:
|
|
75
|
-
pass
|
|
76
|
-
' 2>/dev/null)
|
|
77
|
-
|
|
78
|
-
# Single Python pass reports two facts on one line:
|
|
79
|
-
# 1. Whether PROMPT carries a standalone `/insight` token → token or empty.
|
|
80
|
-
# 2. Reconciliation: the index of the latest real-user transcript turn whose
|
|
81
|
-
# text matches `/insight` that has NO subsequent
|
|
82
|
-
# `session-retrospective-mark-complete` tool_use → index or empty.
|
|
83
|
-
# Output: <trigger-token-or-empty>\t<prior-incomplete-idx-or-empty>
|
|
84
|
-
INSPECTION=$(PROMPT="$PROMPT" TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
|
|
85
|
-
import os, json
|
|
86
|
-
|
|
87
|
-
SENTINEL_TOOL = "mcp__admin__session-retrospective-mark-complete"
|
|
88
|
-
TOKEN = "/insight"
|
|
89
|
-
|
|
90
|
-
def text_has_standalone_token(text):
|
|
91
|
-
# Case-insensitive: token is the whole message (after strip) OR a
|
|
92
|
-
# standalone line within it. Never matches the token embedded in prose.
|
|
93
|
-
if not text:
|
|
94
|
-
return False
|
|
95
|
-
haystacks = {text.strip().lower()}
|
|
96
|
-
for line in text.splitlines():
|
|
97
|
-
s = line.strip().lower()
|
|
98
|
-
if s:
|
|
99
|
-
haystacks.add(s)
|
|
100
|
-
return TOKEN in haystacks
|
|
101
|
-
|
|
102
|
-
def is_real_user(rec):
|
|
103
|
-
if not isinstance(rec, dict) or rec.get("type") != "user":
|
|
104
|
-
return False
|
|
105
|
-
msg = rec.get("message")
|
|
106
|
-
if not isinstance(msg, dict) or msg.get("role") != "user":
|
|
107
|
-
return False
|
|
108
|
-
content = msg.get("content")
|
|
109
|
-
if isinstance(content, str):
|
|
110
|
-
return True
|
|
111
|
-
if isinstance(content, list):
|
|
112
|
-
for b in content:
|
|
113
|
-
if isinstance(b, dict) and b.get("type") != "tool_result":
|
|
114
|
-
return True
|
|
115
|
-
return False
|
|
116
|
-
return True
|
|
117
|
-
|
|
118
|
-
def extract_user_text(rec):
|
|
119
|
-
msg = rec.get("message", {}) or {}
|
|
120
|
-
content = msg.get("content")
|
|
121
|
-
if isinstance(content, str):
|
|
122
|
-
return content
|
|
123
|
-
if isinstance(content, list):
|
|
124
|
-
out = []
|
|
125
|
-
for b in content:
|
|
126
|
-
if isinstance(b, dict) and b.get("type") == "text":
|
|
127
|
-
t = b.get("text")
|
|
128
|
-
if isinstance(t, str):
|
|
129
|
-
out.append(t)
|
|
130
|
-
return "".join(out)
|
|
131
|
-
return ""
|
|
132
|
-
|
|
133
|
-
def is_assistant(rec):
|
|
134
|
-
return (isinstance(rec, dict) and rec.get("type") == "assistant"
|
|
135
|
-
and isinstance(rec.get("message"), dict)
|
|
136
|
-
and rec["message"].get("role") == "assistant")
|
|
137
|
-
|
|
138
|
-
def assistant_tool_use_names(rec):
|
|
139
|
-
if not is_assistant(rec):
|
|
140
|
-
return []
|
|
141
|
-
content = rec["message"].get("content")
|
|
142
|
-
if not isinstance(content, list):
|
|
143
|
-
return []
|
|
144
|
-
return [b.get("name") for b in content
|
|
145
|
-
if isinstance(b, dict) and b.get("type") == "tool_use"
|
|
146
|
-
and isinstance(b.get("name"), str)]
|
|
147
|
-
|
|
148
|
-
# Trigger — read from the prompt field, not the transcript.
|
|
149
|
-
trigger = TOKEN if text_has_standalone_token(os.environ.get("PROMPT", "")) else ""
|
|
150
|
-
|
|
151
|
-
# Reconciliation — scan the transcript for the latest real-user `/insight`
|
|
152
|
-
# turn and check whether a sentinel tool_use follows it.
|
|
153
|
-
records = []
|
|
154
|
-
try:
|
|
155
|
-
with open(os.environ["TRANSCRIPT_PATH"], "r", encoding="utf-8") as f:
|
|
156
|
-
for raw in f:
|
|
157
|
-
raw = raw.strip()
|
|
158
|
-
if not raw:
|
|
159
|
-
continue
|
|
160
|
-
try:
|
|
161
|
-
records.append(json.loads(raw))
|
|
162
|
-
except Exception:
|
|
163
|
-
continue
|
|
164
|
-
except Exception:
|
|
165
|
-
records = []
|
|
166
|
-
|
|
167
|
-
latest_insight_idx = -1
|
|
168
|
-
for i in range(len(records) - 1, -1, -1):
|
|
169
|
-
if is_real_user(records[i]) and text_has_standalone_token(extract_user_text(records[i])):
|
|
170
|
-
latest_insight_idx = i
|
|
171
|
-
break
|
|
172
|
-
|
|
173
|
-
prior_incomplete = ""
|
|
174
|
-
if latest_insight_idx != -1:
|
|
175
|
-
after = records[latest_insight_idx + 1:]
|
|
176
|
-
# "Triggered but never completed" means the agent RESPONDED to the
|
|
177
|
-
# `/insight` turn (≥1 assistant record after it) yet never called the
|
|
178
|
-
# sentinel. Requiring an assistant record excludes the just-submitted
|
|
179
|
-
# `/insight` turn — which has no response yet — so a genuine trigger
|
|
180
|
-
# never self-reports as incomplete, regardless of whether Claude Code
|
|
181
|
-
# has already written the current prompt to the transcript.
|
|
182
|
-
assistant_after = any(is_assistant(r) for r in after)
|
|
183
|
-
sentinel_after = any(SENTINEL_TOOL in assistant_tool_use_names(r) for r in after)
|
|
184
|
-
if assistant_after and not sentinel_after:
|
|
185
|
-
prior_incomplete = str(latest_insight_idx)
|
|
186
|
-
|
|
187
|
-
print(f"{trigger}\t{prior_incomplete}")
|
|
188
|
-
PY
|
|
189
|
-
)
|
|
190
|
-
TRIGGER_TOKEN="${INSPECTION%% *}"
|
|
191
|
-
PRIOR_INCOMPLETE_IDX="${INSPECTION#* }"
|
|
192
|
-
|
|
193
|
-
# Standing reconciliation emit — independent of the current trigger.
|
|
194
|
-
if [ -n "$PRIOR_INCOMPLETE_IDX" ]; then
|
|
195
|
-
agp_emit_log "prior-incomplete sessionId=${SESSION_ID} triggered-turn=${PRIOR_INCOMPLETE_IDX}"
|
|
196
|
-
fi
|
|
197
|
-
|
|
198
|
-
if [ -z "$TRIGGER_TOKEN" ]; then
|
|
199
|
-
agp_emit_log "trigger-skipped sessionId=${SESSION_ID} reason=no-insight-token"
|
|
200
|
-
exit 0
|
|
201
|
-
fi
|
|
202
|
-
|
|
203
|
-
agp_emit_log "trigger sessionId=${SESSION_ID} token=${TRIGGER_TOKEN}"
|
|
204
|
-
|
|
205
|
-
cat <<'INSTRUCTION'
|
|
206
|
-
The operator requested an insight pass — run the four passes now over the session so far, then keep going in this same session. Do NOT call `session-reset`, do NOT clear the context, do NOT end the session: the conversation continues afterward and the operator (or a parallel session) keeps working against the same live state. Use the tools you already have.
|
|
207
|
-
|
|
208
|
-
Pass 1 — Technical learnings. Walk this session for operator corrections and self-detected mistakes. For each one, delegate one graph write to `database-operator` via the Task tool, persisting a Learning or Correction node per the schema. Count what you write.
|
|
209
|
-
|
|
210
|
-
Pass 2 — Operator-relationship updates. Walk this session for tonal signals, rejected phrasings, and working-style preferences. For each one worth carrying forward, Write the observation into `SOUL.md` at the account-scoped path. Count what you write.
|
|
211
|
-
|
|
212
|
-
Pass 3 — Graph completeness sweep. Walk this session for any node, edge, or commitment that was discussed but not written to the graph in-flight. For each missing write, delegate to `database-operator` via the Task tool. Count what you write.
|
|
213
|
-
|
|
214
|
-
Pass 4 — Typed-edge auto-extraction. Delegate to `database-operator` via the Task tool with a prompt naming the typed-edge pass and the session-start timestamp. The specialist calls `memory-typed-edge-pass` with that timestamp and returns the counters. Capture `nodesProcessed` and the accepted count from its reply.
|
|
215
|
-
|
|
216
|
-
Then call `session-retrospective-mark-complete` with all five counts (learningsCount, preferencesCount, graphWriteCount, typedEdgesCount, nodesProcessed) and reply with one short paragraph in your house voice summarising what you wrote — then carry on with the operator's work.
|
|
217
|
-
INSTRUCTION
|
|
218
|
-
|
|
219
|
-
exit 0
|