@rubytech/create-maxy-code 0.1.181 → 0.1.183
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__/bundler-rewrite-platform-lib.test.js +68 -0
- package/dist/bundler-rewrite-platform-lib.js +29 -0
- package/package.json +1 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts +10 -0
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-search/dist/index.js +57 -30
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/hybrid.test.ts +95 -2
- package/payload/platform/lib/graph-search/src/__tests__/vector-index-coverage.test.ts +198 -0
- package/payload/platform/lib/graph-search/src/index.ts +73 -29
- package/payload/platform/neo4j/schema.cypher +36 -0
- package/payload/platform/plugins/admin/PLUGIN.md +2 -0
- package/payload/platform/plugins/admin/hooks/admin-authoring-observer.sh +155 -0
- package/payload/platform/plugins/admin/skills/access-manager/SKILL.md +7 -5
- package/payload/platform/plugins/admin/skills/access-manager/references/operations.md +27 -55
- package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +178 -0
- package/payload/platform/plugins/admin/skills/specialist-management/SKILL.md +3 -2
- package/payload/platform/plugins/aeo/mcp/dist/index.js +3 -0
- package/payload/platform/plugins/aeo/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/PLUGIN.md +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js +3 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/access-control.md +29 -22
- package/payload/platform/plugins/email/mcp/dist/scripts/send-transactional.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/send-transactional.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/send-transactional.js +56 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/send-transactional.js.map +1 -0
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +5 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +76 -2
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js +30 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts +15 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts +10 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +35 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +61 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +125 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +65 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update-by-name.d.ts +15 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update-by-name.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update-by-name.js +45 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update-by-name.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +24 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +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 +16 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/references/graph-primitives.md +1 -0
- package/payload/platform/plugins/memory/references/schema-base.md +4 -2
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +24 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +3 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/work/PLUGIN.md +1 -0
- package/payload/platform/plugins/work/mcp/dist/index.js +4 -0
- package/payload/platform/plugins/work/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/workflows/PLUGIN.md +1 -0
- package/payload/platform/plugins/workflows/mcp/dist/index.js +3 -0
- package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
- package/payload/platform/scripts/__tests__/agents-md-bootstrap.test.sh +111 -0
- package/payload/platform/scripts/lib/agents-md-bootstrap.sh +49 -0
- package/payload/platform/scripts/setup-account.sh +18 -14
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +8 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- 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 +12 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +32 -4
- 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 +154 -53
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/AGENTS.md +18 -7
- package/payload/platform/templates/agents/admin/IDENTITY.md +2 -2
- package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +1 -1
- package/payload/platform/templates/specialists/agents/citation-auditor.md +1 -1
- package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +1 -1
- package/payload/platform/templates/specialists/agents/content-producer.md +3 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +2 -2
- package/payload/platform/templates/specialists/agents/librarian.md +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/platform/templates/specialists/agents/project-manager.md +1 -1
- package/payload/platform/templates/specialists/agents/public-session-reviewer.md +40 -0
- package/payload/platform/templates/specialists/agents/research-assistant.md +1 -1
- package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts +13 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js +15 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/src/lib/voice-corpus.ts +15 -1
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +1 -1
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +1 -1
- package/payload/server/public/assets/ChatInput-DzacFNMk.css +1 -0
- package/payload/server/public/assets/{Checkbox-YrQovXpN.js → Checkbox-BJKNkUYu.js} +1 -1
- package/payload/server/public/assets/{admin-DH5jp-1u.js → admin-BNy4N2d6.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Do2zYONG.js → architectureDiagram-Q4EWVU46-DZoOzmjE.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DpIX_bRu.js → blockDiagram-DXYQGD6D-DG3yufCR.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Cz335ZSY.js → c4Diagram-AHTNJAMY-BBx5wXuZ.js} +1 -1
- package/payload/server/public/assets/channel-DdtUtt5g.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-Dmknn3Wo.js → chunk-336JU56O-DwP7g5ns.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-ClfGjizN.js → chunk-426QAEUC-DXzGdc22.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BPKctJ5c.js → chunk-4TB4RGXK-1LDRjk0E.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-BWDCO9zw.js → chunk-5FUZZQ4R-BHjWhwhn.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-BE34_Db0.js → chunk-5PVQY5BW-CwPHRVN8.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-BysA-_6v.js → chunk-EDXVE4YY-7443hrYZ.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-BMVyMsqA.js → chunk-ENJZ2VHE-BTnmmnmX.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-C5KRdLV4.js → chunk-ICPOFSXX-CPnMJplL.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DVZD-AVc.js → chunk-OYMX7WX6-DCQ5Q4RS.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-DYmQdx8x.js → chunk-U2HBQHQK-BmfZG8e4.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-DrZenIl3.js → chunk-X2U36JSP-Da2yJL2u.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-CDuGbvMq.js → chunk-YZCP3GAM-BdUmMUJ6.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-COztbiu5.js → chunk-ZZ45TVLE-abkGirrZ.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CKk2DFZe.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E--YvfDQCh.js +1 -0
- package/payload/server/public/assets/clone-Cw6UfPGM.js +1 -0
- package/payload/server/public/assets/{dagre-Cv9Ib2OM.js → dagre-BVVuAEsU.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-CoEd61KZ.js → dagre-KV5264BT-VG2kNj19.js} +1 -1
- package/payload/server/public/assets/data-uBpwaldK.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-WXNzw3uW.js → diagram-5BDNPKRD-DRiVy69K.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-C6IuQ2VV.js → diagram-G4DWMVQ6-DtpiRTYX.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CdJdir5A.js → diagram-MMDJMWI5-Da3t99fY.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-DxWKQH5T.js → diagram-TYMM5635-BzQmLTJQ.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-C9fvs33K.js → erDiagram-SMLLAGMA-D3Ll-GDw.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BO7Yrbqv.js → flowDiagram-DWJPFMVM-D17bje6d.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-CQJyWBZq.js → ganttDiagram-T4ZO3ILL-BjeQ-Bun.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-DZfQHrYw.js → gitGraphDiagram-UUTBAWPF-LtX4pvZS.js} +1 -1
- package/payload/server/public/assets/{graph-labels-BMU8LsOD.js → graph-labels-Cn9jAE6I.js} +1 -1
- package/payload/server/public/assets/graph-tvTPvBaz.js +1 -0
- package/payload/server/public/assets/{graphlib-C40H-Q5E.js → graphlib-CzwsfZOw.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CakfV5f9.js → infoDiagram-42DDH7IO-DOgH4i52.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CPuBPyfa.js → ishikawaDiagram-UXIWVN3A-DYqy5BFj.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-zpZm2Mmo.js → journeyDiagram-VCZTEJTY-CsFpIULJ.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DfJduQCo.js → kanban-definition-6JOO6SKY-5uZuArw7.js} +1 -1
- package/payload/server/public/assets/{lib-Bnh57com.js → lib-p4ylk2XS.js} +1 -1
- package/payload/server/public/assets/{line-BpYQqO5R.js → line-BY8PmCSW.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-C5Mwi14D.js → mermaid-parser.core-CJ_9xqwK.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-D8qBtvvT.js → mermaid.core-CXznBkX7.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-Bli9JhIN.js → mindmap-definition-QFDTVHPH-DZbHBud5.js} +1 -1
- package/payload/server/public/assets/{page-0lzgQWnS.js → page-B3VYaBcf.js} +1 -1
- package/payload/server/public/assets/{page-C29S3cBt.js → page-CfN7nw1J.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-MsLXTSLg.js → pieDiagram-DEJITSTG-QmOi4DRG.js} +1 -1
- package/payload/server/public/assets/public-Bbew78md.js +7 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-De1sh2K0.js → quadrantDiagram-34T5L4WZ-Cpidq6Xj.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-D5Sfg1t_.js → requirementDiagram-MS252O5E-CBqtfbzR.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DHA3LIbv.js → sankeyDiagram-XADWPNL6-4BiY4dV9.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BE_QlG1B.js → sequenceDiagram-FGHM5R23-DVlL-l2u.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-NIwp3F4u.js → stateDiagram-FHFEXIEX-CAm9dW68.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B2EzXi50.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-CPE9jyDF.js → timeline-definition-GMOUNBTQ-CD-tZc4y.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CKfoyYmH.js → vennDiagram-DHZGUBPP-Davcm0TF.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-B9yJek-o.js → wardleyDiagram-NUSXRM2D-40LBYw_Y.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DuCcSLqX.js → xychartDiagram-5P7HB3ND-DEPT-__v.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 +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +1158 -337
- package/payload/premium-plugins/writer-craft/lib/graph-trash/dist/index.d.ts +0 -106
- package/payload/premium-plugins/writer-craft/lib/graph-trash/dist/index.d.ts.map +0 -1
- package/payload/premium-plugins/writer-craft/lib/graph-trash/dist/index.js +0 -340
- package/payload/premium-plugins/writer-craft/lib/graph-trash/dist/index.js.map +0 -1
- package/payload/premium-plugins/writer-craft/lib/graph-trash/package.json +0 -7
- package/payload/server/public/assets/ChatInput-CJ50oqWt.css +0 -1
- package/payload/server/public/assets/channel-PC4_IjI1.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-B_pMa-SX.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BdBAgF20.js +0 -1
- package/payload/server/public/assets/clone-CkNt9L9r.js +0 -1
- package/payload/server/public/assets/data-CtanHhM2.js +0 -1
- package/payload/server/public/assets/graph-6DPD6be1.js +0 -1
- package/payload/server/public/assets/public-DltgEfA9.js +0 -7
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-pfk2F6st.js +0 -1
- /package/payload/server/public/assets/{ChatInput-CJo_77bp.js → ChatInput-CvRaT05l.js} +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Doctrine test — pins the per-identity-label vector-index coverage
|
|
8
|
+
* invariant so future identity labels added to schema-base.md cannot
|
|
9
|
+
* silently drift into BM25-only semantic ranking (Task 483).
|
|
10
|
+
*
|
|
11
|
+
* Pre-Task-483 the :Organization label carried `embedding` from the
|
|
12
|
+
* ingest writer but no vector index, so hybrid() fell through to
|
|
13
|
+
* BM25-only for Organization-only queries (Task 478 made the
|
|
14
|
+
* fallback honest). Without a coverage gate, the next analogous gap
|
|
15
|
+
* — a new identity label written with embeddings but no index —
|
|
16
|
+
* would silently degrade the same way.
|
|
17
|
+
*
|
|
18
|
+
* Doctrine: every identity label declared in schema-base.md's
|
|
19
|
+
* `## Node Types` table is either (a) covered by a
|
|
20
|
+
* `CREATE VECTOR INDEX … FOR (n:<Label>) ON (n.embedding)` declaration
|
|
21
|
+
* in schema.cypher, or (b) explicitly listed in
|
|
22
|
+
* `BM25_ONLY_IDENTITY_LABELS` below with a one-line rationale. A new
|
|
23
|
+
* identity label that satisfies neither condition fails this test
|
|
24
|
+
* and points the author at the decision they have to make.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const SCHEMA_CYPHER_PATH = resolve(__dirname, "../../../../neo4j/schema.cypher");
|
|
29
|
+
const SCHEMA_BASE_PATH = resolve(
|
|
30
|
+
__dirname,
|
|
31
|
+
"../../../../plugins/memory/references/schema-base.md",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Identity labels documented in schema-base.md that legitimately rank
|
|
36
|
+
* via BM25 only — no vector index is declared, and none is intended.
|
|
37
|
+
*
|
|
38
|
+
* The rationale per label is the doctrinal record: it answers the
|
|
39
|
+
* question a future reader will ask ("why doesn't <Label> get
|
|
40
|
+
* semantic ranking?") without forcing them to re-derive it from
|
|
41
|
+
* scratch. Removing a label from this set without adding the
|
|
42
|
+
* corresponding `CREATE VECTOR INDEX` (or vice versa) fails the
|
|
43
|
+
* test below.
|
|
44
|
+
*/
|
|
45
|
+
const BM25_ONLY_IDENTITY_LABELS: ReadonlyMap<string, string> = new Map([
|
|
46
|
+
// Structured business records — looked up by deterministic key
|
|
47
|
+
// (orderNumber / confirmationNumber / address), not by semantic
|
|
48
|
+
// similarity. No `body`-like free-text writer.
|
|
49
|
+
["Order", "structured business record; deterministic key lookup"],
|
|
50
|
+
["Invoice", "structured business record; deterministic key lookup"],
|
|
51
|
+
["PostalAddress", "structured fields only; no free-text writer"],
|
|
52
|
+
["OpeningHoursSpecification", "structured fields only"],
|
|
53
|
+
// Identity nodes accessed via composite key, never semantic search.
|
|
54
|
+
["UserProfile", "always queried by (accountId, userId)"],
|
|
55
|
+
["AccessGrant", "always queried by (contactValue, agentSlug, accountId) or magicToken"],
|
|
56
|
+
// CDM-only labels — declared in schema-base for vertical extension
|
|
57
|
+
// hooks; no writer in the current platform.
|
|
58
|
+
["Lead", "CDM-only declaration; no platform writer"],
|
|
59
|
+
["Case", "CDM-only declaration; no platform writer"],
|
|
60
|
+
// Catalogue / branding nodes — small fixed populations, accessed by
|
|
61
|
+
// accountId + purpose / name, not by semantic similarity.
|
|
62
|
+
["FAQPage", "container node; child :Question nodes carry semantic content"],
|
|
63
|
+
["ImageObject", "binary asset; alt-text not currently indexed"],
|
|
64
|
+
// Legacy / retired writers (kept for read compatibility).
|
|
65
|
+
["CloudflareTunnel", "legacy projection (Task 288); writer retired"],
|
|
66
|
+
["CloudflareHostname", "legacy projection (Task 288); writer retired"],
|
|
67
|
+
["WhatsAppConversation", "legacy; superseded by :ConversationArchive (Task 397)"],
|
|
68
|
+
["WhatsAppMessage", "legacy; superseded by :ConversationArchive (Task 397)"],
|
|
69
|
+
// Authored-content history — looked up by accountId + datePublished
|
|
70
|
+
// / runAt; semantic search lives on the parent or downstream report.
|
|
71
|
+
["SocialPost", "authored-content record; lookup by accountId + datePublished"],
|
|
72
|
+
["AEOAudit", "audit-history record; lookup by accountId + runAt"],
|
|
73
|
+
// Voice-mirror records — keyed by (accountId, adminUserId).
|
|
74
|
+
["VoiceProfile", "always queried by (accountId, adminUserId)"],
|
|
75
|
+
["VoiceEdit", "always queried by parent VoiceProfile"],
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
interface VectorIndexDeclaration {
|
|
79
|
+
name: string;
|
|
80
|
+
label: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extract every `CREATE VECTOR INDEX <name> IF NOT EXISTS FOR (<alias>:<Label>) ON (<alias>.embedding)`
|
|
85
|
+
* declaration from schema.cypher. The alias and label vary across
|
|
86
|
+
* declarations (`p:Person`, `b:LocalBusiness`, `o:Organization` …),
|
|
87
|
+
* so the regex captures both and we keep only the label.
|
|
88
|
+
*
|
|
89
|
+
* Line comments are stripped first so a commented-out historical
|
|
90
|
+
* declaration cannot inject a phantom label.
|
|
91
|
+
*/
|
|
92
|
+
function parseVectorIndexes(cypher: string): VectorIndexDeclaration[] {
|
|
93
|
+
const stripped = cypher.replace(/^\s*\/\/.*$/gm, "");
|
|
94
|
+
const decls: VectorIndexDeclaration[] = [];
|
|
95
|
+
const re = /CREATE\s+VECTOR\s+INDEX\s+(\w+)\s+IF\s+NOT\s+EXISTS\s+FOR\s+\(\w+:(\w+)\)/g;
|
|
96
|
+
for (const m of stripped.matchAll(re)) {
|
|
97
|
+
decls.push({ name: m[1], label: m[2] });
|
|
98
|
+
}
|
|
99
|
+
return decls;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract every documented identity label from schema-base.md's
|
|
104
|
+
* `## Node Types` table — column 2 of each table row, stripped of
|
|
105
|
+
* backticks and any trailing `{property:'value'}` qualifier.
|
|
106
|
+
*
|
|
107
|
+
* Parser scope: the first markdown table that appears under
|
|
108
|
+
* `## Node Types`. Stops at the first non-table line (blank line or
|
|
109
|
+
* a paragraph), so explanatory prose under the table cannot leak
|
|
110
|
+
* pseudo-labels.
|
|
111
|
+
*/
|
|
112
|
+
function parseIdentityLabels(md: string): Set<string> {
|
|
113
|
+
const heading = md.indexOf("## Node Types");
|
|
114
|
+
if (heading < 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"parseIdentityLabels: could not find '## Node Types' heading in schema-base.md. " +
|
|
117
|
+
"Schema-base layout drifted; update the doctrine test.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const after = md.slice(heading);
|
|
121
|
+
const lines = after.split("\n");
|
|
122
|
+
const labels = new Set<string>();
|
|
123
|
+
let inTable = false;
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
if (line.startsWith("|")) {
|
|
126
|
+
// Header row + separator row both contain `|`; skip them.
|
|
127
|
+
if (line.includes("Entity") && line.includes("Neo4j Label")) {
|
|
128
|
+
inTable = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (/^\|\s*-+/.test(line)) continue;
|
|
132
|
+
if (!inTable) continue;
|
|
133
|
+
const cols = line.split("|").map((c) => c.trim());
|
|
134
|
+
// `| Entity | Neo4j Label | ... |` → cols = ["", "Entity", "Neo4j Label", ..., ""]
|
|
135
|
+
const labelCell = cols[2];
|
|
136
|
+
if (!labelCell) continue;
|
|
137
|
+
// Strip backticks and trailing `{...}` property qualifier.
|
|
138
|
+
const cleaned = labelCell.replace(/`/g, "").replace(/\s*\{.*\}\s*$/, "").trim();
|
|
139
|
+
// Defensive: only accept PascalCase identifiers. Anything else
|
|
140
|
+
// (multi-label decoration, prose, etc.) is skipped — the test's
|
|
141
|
+
// job is to gate identity-label coverage, not to police table
|
|
142
|
+
// formatting.
|
|
143
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(cleaned)) labels.add(cleaned);
|
|
144
|
+
} else if (inTable && line.trim().length === 0) {
|
|
145
|
+
// Blank line ends the table.
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return labels;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
describe("vector-index coverage — every identity label is vector-indexed or explicitly BM25-only (Task 483)", () => {
|
|
153
|
+
const cypher = readFileSync(SCHEMA_CYPHER_PATH, "utf-8");
|
|
154
|
+
const schemaBase = readFileSync(SCHEMA_BASE_PATH, "utf-8");
|
|
155
|
+
|
|
156
|
+
const declarations = parseVectorIndexes(cypher);
|
|
157
|
+
const vectorIndexed = new Set(declarations.map((d) => d.label));
|
|
158
|
+
const identityLabels = parseIdentityLabels(schemaBase);
|
|
159
|
+
|
|
160
|
+
it("parsers extracted non-trivial sets — sanity floor", () => {
|
|
161
|
+
expect(declarations.length).toBeGreaterThanOrEqual(15);
|
|
162
|
+
expect(identityLabels.size).toBeGreaterThanOrEqual(20);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("every identity label in schema-base.md is either vector-indexed or in BM25_ONLY_IDENTITY_LABELS", () => {
|
|
166
|
+
const uncovered: string[] = [];
|
|
167
|
+
for (const label of identityLabels) {
|
|
168
|
+
if (vectorIndexed.has(label)) continue;
|
|
169
|
+
if (BM25_ONLY_IDENTITY_LABELS.has(label)) continue;
|
|
170
|
+
uncovered.push(label);
|
|
171
|
+
}
|
|
172
|
+
// Failure here means a new identity label was added to schema-base.md
|
|
173
|
+
// without either declaring a vector index in schema.cypher or adding
|
|
174
|
+
// it to BM25_ONLY_IDENTITY_LABELS with a one-line rationale. Pick
|
|
175
|
+
// one — the silent gap is the failure mode.
|
|
176
|
+
expect(uncovered).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("BM25_ONLY_IDENTITY_LABELS does not overlap with vector-indexed labels", () => {
|
|
180
|
+
const overlap: string[] = [];
|
|
181
|
+
for (const label of BM25_ONLY_IDENTITY_LABELS.keys()) {
|
|
182
|
+
if (vectorIndexed.has(label)) overlap.push(label);
|
|
183
|
+
}
|
|
184
|
+
// A label cannot legitimately appear in both sets: the allowlist
|
|
185
|
+
// documents the *absence* of a vector index. If the index was
|
|
186
|
+
// added later, remove the label from the allowlist.
|
|
187
|
+
expect(overlap).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it(":Organization carries a vector index (Task 483 acceptance)", () => {
|
|
191
|
+
// Pin the load-bearing assertion of this task explicitly so a
|
|
192
|
+
// regression that re-removed the declaration would fail with a
|
|
193
|
+
// pointed message rather than as a generic coverage gap.
|
|
194
|
+
expect(vectorIndexed.has("Organization")).toBe(true);
|
|
195
|
+
const orgDecl = declarations.find((d) => d.label === "Organization");
|
|
196
|
+
expect(orgDecl?.name).toBe("organization_embedding");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -203,6 +203,48 @@ export interface Bm25OnlyParams {
|
|
|
203
203
|
* guard); admin route enforces non-empty `labels` as a precondition.
|
|
204
204
|
*/
|
|
205
205
|
labels?: string[];
|
|
206
|
+
/**
|
|
207
|
+
* Task 485 — per-visitor slice ringfence. When set, the scope filter
|
|
208
|
+
* widens from `(scope IS NULL OR scope IN $allowedScopes)` to
|
|
209
|
+
* `(sliceToken = $sliceToken OR scope IS NULL OR scope IN $allowedScopes)`
|
|
210
|
+
* so a gated public-agent read sees its visitor's user-scoped nodes in
|
|
211
|
+
* addition to public-scope rows. Skipped on every admin and open-mode
|
|
212
|
+
* public call; the env var `ACCESS_SLICE_TOKEN` populates it on the
|
|
213
|
+
* memory MCP server side.
|
|
214
|
+
*/
|
|
215
|
+
sliceToken?: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build the scope-and-slice WHERE fragment + params. Centralises the three
|
|
220
|
+
* cypher composition sites (bm25, vector, prop) so the gated/non-gated
|
|
221
|
+
* behaviour does not drift between them. Returns empty strings when no
|
|
222
|
+
* filter applies.
|
|
223
|
+
*/
|
|
224
|
+
function buildScopeAndSliceClause(
|
|
225
|
+
allowedScopes: readonly string[] | undefined,
|
|
226
|
+
sliceToken: string | undefined,
|
|
227
|
+
alias: string = "node",
|
|
228
|
+
): { clause: string; params: Record<string, unknown> } {
|
|
229
|
+
const hasSlice = typeof sliceToken === "string" && sliceToken.length > 0;
|
|
230
|
+
const hasScopes = Array.isArray(allowedScopes) && allowedScopes.length > 0;
|
|
231
|
+
if (!hasSlice && !hasScopes) return { clause: "", params: {} };
|
|
232
|
+
if (hasSlice && hasScopes) {
|
|
233
|
+
return {
|
|
234
|
+
clause: `AND (${alias}.sliceToken = $sliceToken OR ${alias}.scope IS NULL OR ${alias}.scope IN $allowedScopes)`,
|
|
235
|
+
params: { sliceToken, allowedScopes },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (hasSlice) {
|
|
239
|
+
return {
|
|
240
|
+
clause: `AND (${alias}.sliceToken = $sliceToken OR ${alias}.scope IS NULL)`,
|
|
241
|
+
params: { sliceToken },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
clause: `AND (${alias}.scope IS NULL OR ${alias}.scope IN $allowedScopes)`,
|
|
246
|
+
params: { allowedScopes },
|
|
247
|
+
};
|
|
206
248
|
}
|
|
207
249
|
|
|
208
250
|
export interface HybridParams extends Bm25OnlyParams {
|
|
@@ -388,10 +430,9 @@ export async function bm25Only(
|
|
|
388
430
|
session: Session,
|
|
389
431
|
params: Bm25OnlyParams,
|
|
390
432
|
): Promise<SearchHit[]> {
|
|
391
|
-
const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch, labels } = params;
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
: "";
|
|
433
|
+
const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch, labels, sliceToken } = params;
|
|
434
|
+
const scope = buildScopeAndSliceClause(allowedScopes, sliceToken);
|
|
435
|
+
const scopeClause = scope.clause;
|
|
395
436
|
const agentClause = agentSlug
|
|
396
437
|
? "AND node.agents IS NOT NULL AND $agentSlug IN node.agents"
|
|
397
438
|
: "";
|
|
@@ -420,7 +461,7 @@ export async function bm25Only(
|
|
|
420
461
|
query: escaped,
|
|
421
462
|
accountId,
|
|
422
463
|
limit: int(limit),
|
|
423
|
-
...
|
|
464
|
+
...scope.params,
|
|
424
465
|
...(agentSlug ? { agentSlug } : {}),
|
|
425
466
|
...(labels && labels.length > 0 ? { labels } : {}),
|
|
426
467
|
...(keywordFilter?.params ?? {}),
|
|
@@ -486,6 +527,7 @@ export async function hybrid(
|
|
|
486
527
|
accountId,
|
|
487
528
|
limit,
|
|
488
529
|
allowedScopes,
|
|
530
|
+
sliceToken,
|
|
489
531
|
keywords,
|
|
490
532
|
keywordMatch = "any",
|
|
491
533
|
agentSlug,
|
|
@@ -579,31 +621,35 @@ export async function hybrid(
|
|
|
579
621
|
const labelToIndex = await discoverIndexes(session);
|
|
580
622
|
|
|
581
623
|
// --- Resolve vector indexes from the effective label set ---
|
|
624
|
+
//
|
|
625
|
+
// Task 478: when `effectiveLabels` is set but none resolve to a vector index
|
|
626
|
+
// (e.g. `labels:["Organization"]` — universal BM25 covers it, no vector
|
|
627
|
+
// index declared), proceed with `indexesToQuery=[]`. The vector loop below
|
|
628
|
+
// runs zero iterations and the BM25 half still runs with the label filter,
|
|
629
|
+
// so search returns rows whose existence the rest of the platform asserts.
|
|
630
|
+
// Pre-fix this branch returned an empty result, hiding ingest-created
|
|
631
|
+
// Organization rows that BM25 would have found — the deadlock recorded in
|
|
632
|
+
// .tasks/archive/478-*.md. The labels-without-vector list is emitted as an
|
|
633
|
+
// observability line so a future analogous gap stays visible.
|
|
582
634
|
let indexesToQuery: string[];
|
|
583
635
|
if (effectiveLabels && effectiveLabels.length > 0) {
|
|
584
636
|
indexesToQuery = effectiveLabels
|
|
585
637
|
.map((l) => labelToIndex.get(l))
|
|
586
638
|
.filter((idx): idx is string => idx !== undefined);
|
|
587
639
|
if (indexesToQuery.length === 0) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
suppressed: 0,
|
|
594
|
-
bm25Bypass: 0,
|
|
595
|
-
threshold: params.vectorThreshold ?? null,
|
|
596
|
-
stageCounts: emptyStageCounts,
|
|
597
|
-
};
|
|
640
|
+
const missing = effectiveLabels.filter((l) => !labelToIndex.has(l));
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`[graph-search] labels-without-vector-index=${missing.join(",")} ` +
|
|
643
|
+
`accountId=${(accountId ?? "").slice(0, 8)} — falling through to BM25-only for these labels\n`,
|
|
644
|
+
);
|
|
598
645
|
}
|
|
599
646
|
} else {
|
|
600
647
|
indexesToQuery = [...new Set(labelToIndex.values())];
|
|
601
648
|
}
|
|
602
649
|
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const scopeParams = allowedScopes ? { allowedScopes } : {};
|
|
650
|
+
const hybridScope = buildScopeAndSliceClause(allowedScopes, sliceToken);
|
|
651
|
+
const scopeClause = hybridScope.clause;
|
|
652
|
+
const scopeParams = hybridScope.params;
|
|
607
653
|
const agentClause = agentSlug
|
|
608
654
|
? "AND node.agents IS NOT NULL AND $agentSlug IN node.agents"
|
|
609
655
|
: "";
|
|
@@ -752,6 +798,7 @@ export async function hybrid(
|
|
|
752
798
|
accountId,
|
|
753
799
|
limit,
|
|
754
800
|
allowedScopes,
|
|
801
|
+
sliceToken,
|
|
755
802
|
});
|
|
756
803
|
if (kwHits.length === 0) continue;
|
|
757
804
|
const rawScores = kwHits.map((h) => h.score);
|
|
@@ -761,23 +808,21 @@ export async function hybrid(
|
|
|
761
808
|
}
|
|
762
809
|
}
|
|
763
810
|
|
|
764
|
-
const
|
|
765
|
-
? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)"
|
|
766
|
-
: "";
|
|
811
|
+
const propScope = buildScopeAndSliceClause(allowedScopes, sliceToken);
|
|
767
812
|
const propResult = await session.run(
|
|
768
813
|
`MATCH (node)
|
|
769
814
|
WHERE node.accountId = $accountId
|
|
770
815
|
AND ${notTrashed("node")}
|
|
771
816
|
AND node.keywords IS NOT NULL
|
|
772
817
|
AND ANY(kw IN $kwSubs WHERE ANY(nk IN node.keywords WHERE toLower(nk) = kw))
|
|
773
|
-
${
|
|
818
|
+
${propScope.clause}
|
|
774
819
|
RETURN node, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
775
820
|
LIMIT $limit`,
|
|
776
821
|
{
|
|
777
822
|
accountId,
|
|
778
823
|
kwSubs: keywordSubscriptions,
|
|
779
824
|
limit: int(limit),
|
|
780
|
-
...
|
|
825
|
+
...propScope.params,
|
|
781
826
|
},
|
|
782
827
|
);
|
|
783
828
|
for (const record of propResult.records) {
|
|
@@ -808,6 +853,7 @@ export async function hybrid(
|
|
|
808
853
|
accountId,
|
|
809
854
|
limit,
|
|
810
855
|
allowedScopes,
|
|
856
|
+
sliceToken,
|
|
811
857
|
});
|
|
812
858
|
if (kwHits.length === 0) continue;
|
|
813
859
|
const rawScores = kwHits.map((h) => h.score);
|
|
@@ -817,23 +863,21 @@ export async function hybrid(
|
|
|
817
863
|
}
|
|
818
864
|
}
|
|
819
865
|
|
|
820
|
-
const
|
|
821
|
-
? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)"
|
|
822
|
-
: "";
|
|
866
|
+
const propScope = buildScopeAndSliceClause(allowedScopes, sliceToken);
|
|
823
867
|
const propResult = await session.run(
|
|
824
868
|
`MATCH (node)
|
|
825
869
|
WHERE node.accountId = $accountId
|
|
826
870
|
AND ${notTrashed("node")}
|
|
827
871
|
AND node.keywords IS NOT NULL
|
|
828
872
|
AND ANY(kw IN $kwSubs WHERE ANY(nk IN node.keywords WHERE toLower(nk) = kw))
|
|
829
|
-
${
|
|
873
|
+
${propScope.clause}
|
|
830
874
|
RETURN node, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
831
875
|
LIMIT $limit`,
|
|
832
876
|
{
|
|
833
877
|
accountId,
|
|
834
878
|
kwSubs: keywordSubscriptions,
|
|
835
879
|
limit: int(limit),
|
|
836
|
-
...
|
|
880
|
+
...propScope.params,
|
|
837
881
|
},
|
|
838
882
|
);
|
|
839
883
|
for (const record of propResult.records) {
|
|
@@ -135,6 +135,24 @@ OPTIONS {
|
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
|
|
138
|
+
// :Organization carries `embedding` from the ingest writer
|
|
139
|
+
// (memory-ingest.ts writeRelatedNode). Pre-Task-483 no vector index
|
|
140
|
+
// existed, so hybrid() fell through to BM25-only for Organization-only
|
|
141
|
+
// queries (Task 478 made that fallback honest). The index restores
|
|
142
|
+
// semantic-similarity ranking parity with the other identity labels —
|
|
143
|
+
// a query like "the company that does antimicrobial coatings" surfaces
|
|
144
|
+
// BioSymm via cosine similarity rather than relying on literal BM25
|
|
145
|
+
// token matching. Neo4j populates the index against existing rows on
|
|
146
|
+
// CREATE; no manual reindex required.
|
|
147
|
+
CREATE VECTOR INDEX organization_embedding IF NOT EXISTS
|
|
148
|
+
FOR (o:Organization) ON (o.embedding)
|
|
149
|
+
OPTIONS {
|
|
150
|
+
indexConfig: {
|
|
151
|
+
`vector.dimensions`: 768,
|
|
152
|
+
`vector.similarity_function`: 'cosine'
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
138
156
|
CREATE VECTOR INDEX price_embedding IF NOT EXISTS
|
|
139
157
|
FOR (p:PriceSpecification) ON (p.embedding)
|
|
140
158
|
OPTIONS {
|
|
@@ -878,6 +896,24 @@ FOR (ag:AccessGrant) ON (ag.magicToken);
|
|
|
878
896
|
CREATE INDEX access_grant_status IF NOT EXISTS
|
|
879
897
|
FOR (ag:AccessGrant) ON (ag.status);
|
|
880
898
|
|
|
899
|
+
// Slice token (Task 485) — per-visitor ringfence key for gated public agents.
|
|
900
|
+
// Indexed on AccessGrant for revoke-eviction lookups and on every label that
|
|
901
|
+
// the session-end reviewer writes through (via database-operator), so
|
|
902
|
+
// memory-search can filter `n.sliceToken = $sliceToken OR n.scope = "public"`
|
|
903
|
+
// without a label scan. New labels added to the reviewer's surface gain a
|
|
904
|
+
// twin index here.
|
|
905
|
+
CREATE INDEX access_grant_slice_token IF NOT EXISTS
|
|
906
|
+
FOR (ag:AccessGrant) ON (ag.sliceToken);
|
|
907
|
+
|
|
908
|
+
CREATE INDEX person_slice_token IF NOT EXISTS
|
|
909
|
+
FOR (n:Person) ON (n.sliceToken);
|
|
910
|
+
|
|
911
|
+
CREATE INDEX preference_slice_token IF NOT EXISTS
|
|
912
|
+
FOR (n:Preference) ON (n.sliceToken);
|
|
913
|
+
|
|
914
|
+
CREATE INDEX idea_slice_token IF NOT EXISTS
|
|
915
|
+
FOR (n:Idea) ON (n.sliceToken);
|
|
916
|
+
|
|
881
917
|
// ----------------------------------------------------------
|
|
882
918
|
// Agent node — projection of file-based public agents
|
|
883
919
|
//
|
|
@@ -126,6 +126,7 @@ Tools are available via the `admin` MCP server.
|
|
|
126
126
|
| Manage plugins and settings | User asks to install, enable, disable, or configure plugins, or change account settings | `skills/plugin-management/SKILL.md` |
|
|
127
127
|
| Manage specialists | User asks to install or remove a specialist subagent, or activate/deactivate premium plugin agents | `skills/specialist-management/SKILL.md` |
|
|
128
128
|
| Generate print-quality PDF | User asks to create a PDF document, one-pager, brochure, or any HTML intended for print/download | `skills/a4-print-documents/SKILL.md` |
|
|
129
|
+
| Compose a business document | User asks to write or draft a proposal, report (market / analysis / strategic), brief, memo, white paper, plan (non-fundraise), or case study | `skills/professional-document/SKILL.md` |
|
|
129
130
|
| Plain-English explanation | User asks to explain, define, or pre-empts ("explain in plain English", "what does X mean", "define X", "I don't understand", "what is this"), or admin is about to return a reply containing a term not in the operator's prior turn | `skills/plainly/SKILL.md` |
|
|
130
131
|
| Manage admin users | User asks to add, remove, or list admins on this account, or change an admin PIN | `skills/admin-user-management/SKILL.md` |
|
|
131
132
|
| Session reset / continue / resume | User asks to clear the session, start fresh, continue the last session, or pick up where they left off | `skills/session-management/SKILL.md` |
|
|
@@ -139,6 +140,7 @@ Tools are available via the `admin` MCP server.
|
|
|
139
140
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
140
141
|
- `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
|
|
141
142
|
- `hooks/session-end-retrospective.sh` — **Stop hook.** Fires on every assistant end_turn but exits 0 with `trigger-skipped reason=<role-not-admin|is-specialist|empty-stdin|missing-transcript|no-intent-match>` on every path EXCEPT when the operator's latest real-user message carries an end-intent token (`/end`, `/archive`, `end session`, `archive this session`, case-insensitive, must match the whole message or a standalone line — not embedded in prose). On that path the hook blocks (exit 2 + structured retrospective instruction on stderr per Stop-hook contract) and emits `[session-retrospective] gate-blocked sessionId=<id> reason=sentinel-absent` until the admin agent calls the `session-retrospective-mark-complete` MCP sentinel tool. Completion is recognised by greping the operator's own JSONL (`transcript_path` on the Stop envelope) for a `tool_use` block whose `name` equals that exact string — never by parsing prose ([[feedback_no_stdout_parsing_for_control_flow]], [[feedback_doctrine_paragraph_is_not_a_gate]]). The sentinel-grep doubles as the re-entry guard: every Stop after the sentinel call sees it and exits 0 with `gate-released sessionId=<id>`. Recursion guard: any session whose `MAXY_SPECIALIST` env is set (specialist subagent dispatched via Task tool) skips the gate. The Sidebar archive button POSTs `/:sessionId/archive` directly to the manager which kills the PTY — no Stop fires after that, so clicking archive is the operator's current escape hatch (opt-out remains deferred per the task spec). All emits go through `POST /api/admin/log-ingest`; the only stderr writer is the gate-blocked path (the instruction block the agent reads). The retrospective itself runs as one or more additional turns inside the operator's existing admin session — Task-tool delegation to `database-operator` is the existing IDENTITY.md "Recording" route, not a parallel admin session ([[feedback_deterministic_means_remove_llm]]).
|
|
143
|
+
- `hooks/admin-authoring-observer.sh` — **PostToolUse hook on Write and Edit (Task 486).** Observation only — never blocks; exits 0 on every path. Fires when the admin agent (not a specialist subagent — gated by `MAXY_SPECIALIST` env) writes or edits a file under `<accountDir>/output/`. Walks the session transcript from the latest real-user turn forward to detect any prior `Task` `tool_use` whose `subagent_type` starts with `specialists:`. Emits one stderr line `[admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>`. A `false` value on a long-form prose file is the regression signal Task 486 was designed to make visible — the BioSymm proposal session (admin authored a customer-facing proposal inline despite content-producer being installed) is the failure mode this surfaces mechanically. Mechanical enforcement (refuse the write, force a re-spawn) is deferred per the task spec.
|
|
142
144
|
- `hooks/turn-completed-graph-write.sh` — **Dormant since Task 214** — the Stop-hook registration is no longer written to account settings.json; admin delegates graph writes to `database-operator` via the Task tool. The script, envelope walker, loopback `/api/admin/claude-sessions` 127.0.0.1 bypass, `[turn-recorder]` emitters, recorder-auto-archive subscriber, and `initialMessage` envelope spec are preserved as reusable infrastructure for any future autonomous post-turn flow. Historical contract (still describes the dormant path): Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** — the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` — no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) — no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** — `initialMessage` is a JSON-stringified envelope; the schema lives in [`platform/plugins/docs/references/admin-session.md`](../docs/references/admin-session.md) under "`initialMessage` JSON envelope (Task 177)". Top-level keys exactly: `turns`, `sessionId`, `accountId`, `occurredAt`. `turns` is the full conversation transcript, oldest first, newest last, with each entry `{ role: "user"|"assistant", text, ts, toolCalls? }`. No windowing, no truncation. Multi-record assistant messages collapse on `message.id`. `tool_use` and matching `tool_result` blocks attach as a single `toolCalls` entry on the owning assistant turn; the user record carrying only the `tool_result` does not create a separate user turn. No leading instruction prose. `toolCalls[].input` and `toolCalls[].output` are native JSON values, never re-stringified. (Replaces Task 175's `(operatorMessage, assistantReply)` pair, which asserted a temporal Q→A relationship the walker never enforced.) One observability line `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` precedes `spawn-request`. The envelope rides on the `/spawn` body as a trailing positional argv to `claude`, so the database-operator session's JSONL first `role=user` line is the JSON object verbatim. No separate `POST /:id/input` call, no bracketed-paste, no keystroke injection. The recorder-auto-archive subscriber stops the recorder PTY as soon as its JSONL contains an assistant message with `stop_reason === "end_turn"`. **Task 129** — every emit goes through `POST /api/admin/log-ingest` so the lines land in `server.log` keyed by the operator session id. The chain is `trigger` → `spawn-request` → `spawn-success` → `input-delivered` → `tool-call` × N → `tool-result` × N → `write-complete` → `auto-archive`; each gated-off path emits one `trigger-skipped reason=<role-not-admin|is-recorder|empty-stdin|missing-transcript|conversation-empty>` line. Failure modes — `spawn-failed`, `tool-surface-missing`, `input-failed`, `write-empty`, `auto-archive reason=stale-recorder` — each emit one named line; absence is itself a defect.
|
|
143
145
|
|
|
144
146
|
## Session identifiers (Task 135)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Admin-authoring observer (Task 486).
|
|
3
|
+
#
|
|
4
|
+
# Observation-only PostToolUse hook. When the admin agent (not a specialist
|
|
5
|
+
# subagent) writes or edits a file under <accountDir>/output/, emit one
|
|
6
|
+
# stderr line capturing whether a specialist was dispatched earlier in the
|
|
7
|
+
# current turn. A `priorSpecialistSpawnInTurn=false` value on a long-form
|
|
8
|
+
# prose file is the regression signal Task 486 was designed to make visible
|
|
9
|
+
# without relying on operator complaint — it surfaces the same failure mode
|
|
10
|
+
# the BioSymm proposal session exhibited (admin authored a customer-facing
|
|
11
|
+
# proposal inline despite content-producer being installed).
|
|
12
|
+
#
|
|
13
|
+
# This hook never blocks. Exit code is always 0.
|
|
14
|
+
#
|
|
15
|
+
# Output line shape (on stderr, no stdout):
|
|
16
|
+
# [admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
# Empty stdin (terminal invocation) — silent no-op for safety in tests.
|
|
21
|
+
if [ -t 0 ]; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
|
|
26
|
+
# Only PostToolUse on Write or Edit is observed.
|
|
27
|
+
HOOK_EVENT=$(printf '%s' "$INPUT" | python3 -c '
|
|
28
|
+
import sys, json
|
|
29
|
+
try:
|
|
30
|
+
print(json.load(sys.stdin).get("hook_event_name", "") or "")
|
|
31
|
+
except Exception:
|
|
32
|
+
print("")
|
|
33
|
+
' 2>/dev/null)
|
|
34
|
+
TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c '
|
|
35
|
+
import sys, json
|
|
36
|
+
try:
|
|
37
|
+
print(json.load(sys.stdin).get("tool_name", "") or "")
|
|
38
|
+
except Exception:
|
|
39
|
+
print("")
|
|
40
|
+
' 2>/dev/null)
|
|
41
|
+
if [ "$HOOK_EVENT" != "PostToolUse" ]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ]; then
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Subagent writes are out of scope. pty-spawner stamps MAXY_SPECIALIST on
|
|
49
|
+
# every specialist process; only the admin process sees it empty.
|
|
50
|
+
if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
FILE_PATH=$(printf '%s' "$INPUT" | python3 -c '
|
|
55
|
+
import sys, json
|
|
56
|
+
try:
|
|
57
|
+
d = json.load(sys.stdin)
|
|
58
|
+
print((d.get("tool_input", {}) or {}).get("file_path", "") or "")
|
|
59
|
+
except Exception:
|
|
60
|
+
print("")
|
|
61
|
+
' 2>/dev/null)
|
|
62
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
63
|
+
|
|
64
|
+
# Resolve to absolute. The hook fires in the account dir (PWD), which is the
|
|
65
|
+
# claude project root (each account has its own .git).
|
|
66
|
+
case "$FILE_PATH" in
|
|
67
|
+
/*) ABS="$FILE_PATH" ;;
|
|
68
|
+
*) ABS="$PWD/$FILE_PATH" ;;
|
|
69
|
+
esac
|
|
70
|
+
|
|
71
|
+
# Only files under <accountDir>/output/ are in scope.
|
|
72
|
+
case "$ABS" in
|
|
73
|
+
"$PWD/output/"*) REL="${ABS#$PWD/}" ;;
|
|
74
|
+
*) exit 0 ;;
|
|
75
|
+
esac
|
|
76
|
+
|
|
77
|
+
TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | python3 -c '
|
|
78
|
+
import sys, json
|
|
79
|
+
try:
|
|
80
|
+
print(json.load(sys.stdin).get("transcript_path", "") or "")
|
|
81
|
+
except Exception:
|
|
82
|
+
print("")
|
|
83
|
+
' 2>/dev/null)
|
|
84
|
+
# Test override so the .test.sh suite can pin a fixture transcript.
|
|
85
|
+
if [ -n "${ADMIN_AUTHORING_TRANSCRIPT_PATH:-}" ]; then
|
|
86
|
+
TRANSCRIPT_PATH="$ADMIN_AUTHORING_TRANSCRIPT_PATH"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
90
|
+
echo "[admin-authoring] inline-write path=${REL} priorSpecialistSpawnInTurn=unknown" >&2
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Walk the transcript: from the latest real-user turn forward, find any
|
|
95
|
+
# assistant `tool_use` whose `name == "Task"` and whose
|
|
96
|
+
# `input.subagent_type` starts with `specialists:`.
|
|
97
|
+
PRIOR=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
|
|
98
|
+
import os, json, sys
|
|
99
|
+
|
|
100
|
+
path = os.environ["TRANSCRIPT_PATH"]
|
|
101
|
+
records = []
|
|
102
|
+
try:
|
|
103
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
104
|
+
for line in f:
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if line:
|
|
107
|
+
records.append(json.loads(line))
|
|
108
|
+
except Exception:
|
|
109
|
+
print("unknown"); sys.exit(0)
|
|
110
|
+
|
|
111
|
+
def is_real_user(d):
|
|
112
|
+
if d.get("type") != "user":
|
|
113
|
+
return False
|
|
114
|
+
msg = d.get("message", {}) or {}
|
|
115
|
+
if msg.get("role") != "user":
|
|
116
|
+
return False
|
|
117
|
+
content = msg.get("content")
|
|
118
|
+
if isinstance(content, str):
|
|
119
|
+
return True
|
|
120
|
+
if isinstance(content, list):
|
|
121
|
+
for b in content:
|
|
122
|
+
if isinstance(b, dict) and b.get("type") != "tool_result":
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
last_user = -1
|
|
128
|
+
for i in range(len(records) - 1, -1, -1):
|
|
129
|
+
if is_real_user(records[i]):
|
|
130
|
+
last_user = i
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
for d in records[last_user + 1:]:
|
|
134
|
+
if d.get("type") != "assistant":
|
|
135
|
+
continue
|
|
136
|
+
msg = d.get("message", {}) or {}
|
|
137
|
+
if msg.get("role") != "assistant":
|
|
138
|
+
continue
|
|
139
|
+
for b in (msg.get("content") or []):
|
|
140
|
+
if not isinstance(b, dict):
|
|
141
|
+
continue
|
|
142
|
+
if b.get("type") != "tool_use":
|
|
143
|
+
continue
|
|
144
|
+
if b.get("name") != "Task":
|
|
145
|
+
continue
|
|
146
|
+
sub = (b.get("input") or {}).get("subagent_type")
|
|
147
|
+
if isinstance(sub, str) and sub.startswith("specialists:"):
|
|
148
|
+
print("true"); sys.exit(0)
|
|
149
|
+
|
|
150
|
+
print("false")
|
|
151
|
+
PY
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
echo "[admin-authoring] inline-write path=${REL} priorSpecialistSpawnInTurn=${PRIOR}" >&2
|
|
155
|
+
exit 0
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: access-manager
|
|
3
|
-
description: "Manage per-agent access grants — invite visitors by email
|
|
3
|
+
description: "Manage per-agent access grants for gated public agents — invite visitors by email, list who has access, revoke or extend access, resend invitations, and set agent access modes (open, gated)."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Access Manager
|
|
7
7
|
|
|
8
|
-
Applies when the business owner wants to control who can access a gated
|
|
8
|
+
Applies when the business owner wants to control who can access a gated public agent.
|
|
9
9
|
|
|
10
10
|
## When to Activate
|
|
11
11
|
|
|
12
|
-
- Admin asks to invite someone to a gated agent (by email
|
|
12
|
+
- Admin asks to invite someone to a gated agent (always by email)
|
|
13
13
|
- Admin asks who has access, or to list access grants for an agent
|
|
14
14
|
- Admin asks to revoke or extend someone's access
|
|
15
|
-
- Admin asks to set an agent's access mode (open
|
|
15
|
+
- Admin asks to set an agent's access mode (open or gated)
|
|
16
16
|
- Admin asks to resend an invitation
|
|
17
17
|
|
|
18
18
|
## Constraints
|
|
@@ -20,7 +20,9 @@ Applies when the business owner wants to control who can access a gated or paid
|
|
|
20
20
|
- **Admin-scope only.** Every AccessGrant node lands with `scope: "admin"`. Credential data must never be visible to public agents — the recorder enforces the scope tag on its write turn.
|
|
21
21
|
- **Agent must exist.** Before any grant operation, verify the target agent directory exists under `agents/` and read its `config.json`.
|
|
22
22
|
- **Confirm before mutating.** Present a `confirm` component before grants are created, access is revoked, or access modes change. The recorder writes off the confirmed transcript.
|
|
23
|
-
- **Domain required
|
|
23
|
+
- **Domain required.** Resolve the public hostname via `tunnel-status`. Without a confirmed hostname, magic link URLs cannot be constructed.
|
|
24
|
+
- **`sliceToken` is immutable.** Stamped at create as a UUIDv4; revoke / extend / resend never mutate it. The same visitor across multiple invitations has the same person record but distinct sliceTokens per grant.
|
|
25
|
+
- **Revoke evicts active sessions.** After the cypher revoke succeeds, POST loopback `/api/admin/access-session-evict?grantId=<elementId>` so any in-flight `__access_session` cookie for that grant is dropped from the UI server's session map.
|
|
24
26
|
- **Deterministic grant queries.** AccessGrant nodes are not vector-indexed. Query them via `cypher-shell` (password at `~/.maxy/.neo4j-password`), not via `memory-search`.
|
|
25
27
|
|
|
26
28
|
## Operations
|