@rubytech/create-maxy-code 0.1.264 → 0.1.266

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/models/dist/index.d.ts +1 -1
  3. package/payload/platform/lib/models/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/models/dist/index.js +5 -2
  5. package/payload/platform/lib/models/dist/index.js.map +1 -1
  6. package/payload/platform/lib/models/src/index.ts +5 -2
  7. package/payload/platform/neo4j/schema.cypher +13 -0
  8. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  9. package/payload/platform/plugins/admin/PLUGIN.md +6 -1
  10. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.d.ts +2 -0
  11. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.d.ts.map +1 -0
  12. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js +106 -0
  13. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js.map +1 -0
  14. package/payload/platform/plugins/admin/mcp/dist/index.js +36 -1
  15. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts +12 -0
  17. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
  18. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +175 -1
  19. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
  20. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +11 -3
  21. package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -2
  22. package/payload/platform/plugins/business-assistant/PLUGIN.md +1 -5
  23. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  24. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +9 -1
  25. package/payload/platform/plugins/memory/references/schema-base.md +1 -1
  26. package/payload/platform/plugins/memory/references/schema-construction.md +1 -1
  27. package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/format-login-start.test.d.ts +2 -0
  28. package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/format-login-start.test.d.ts.map +1 -0
  29. package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/format-login-start.test.js +16 -0
  30. package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/format-login-start.test.js.map +1 -0
  31. package/payload/platform/plugins/whatsapp/mcp/dist/format.d.ts +7 -0
  32. package/payload/platform/plugins/whatsapp/mcp/dist/format.d.ts.map +1 -0
  33. package/payload/platform/plugins/whatsapp/mcp/dist/format.js +21 -0
  34. package/payload/platform/plugins/whatsapp/mcp/dist/format.js.map +1 -0
  35. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +11 -19
  36. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  37. package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +15 -13
  38. package/payload/platform/scripts/check-skill-frontmatter.mjs +8 -5
  39. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  41. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +15 -15
  44. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +10 -0
  46. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  47. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +59 -0
  48. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  49. package/payload/platform/templates/account.json +1 -1
  50. package/payload/platform/templates/agents/admin/IDENTITY.md +3 -3
  51. package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
  52. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  53. package/payload/platform/templates/specialists/agents/research-assistant.md +1 -1
  54. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +1 -1
  55. package/payload/premium-plugins/writer-craft/PLUGIN.md +4 -4
  56. package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
  57. package/payload/premium-plugins/writer-craft/mcp/dist/index.js +44 -9
  58. package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
  59. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts +31 -0
  60. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts.map +1 -1
  61. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js +28 -0
  62. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js.map +1 -1
  63. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts +7 -1
  64. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  65. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +93 -44
  66. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  67. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
  68. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js +1 -0
  69. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
  70. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts +7 -1
  71. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  72. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js +14 -3
  73. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  74. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +22 -8
  75. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  76. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +93 -84
  77. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  78. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts +18 -0
  79. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  80. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js +32 -3
  81. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js.map +1 -1
  82. package/payload/premium-plugins/writer-craft/mcp/scripts/smoke.mjs +35 -2
  83. package/payload/premium-plugins/writer-craft/mcp/src/index.ts +52 -10
  84. package/payload/premium-plugins/writer-craft/mcp/src/lib/voice-corpus.ts +39 -0
  85. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +108 -44
  86. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-ingest-session-text.ts +1 -0
  87. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-record-feedback.ts +24 -4
  88. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +136 -102
  89. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-tag-content.ts +45 -3
  90. package/payload/premium-plugins/writer-craft/skills/voice-mirror/SKILL.md +34 -23
  91. package/payload/server/{adminuser-self-heal-YC47O34W.js → adminuser-self-heal-LC7HZAC6.js} +17 -9
  92. package/payload/server/{chunk-HYQNUVGO.js → chunk-PFF6I7KP.js} +1 -8
  93. package/payload/server/{chunk-ZC63DFWF.js → chunk-SOLVVUST.js} +1 -1
  94. package/payload/server/maxy-edge.js +2 -2
  95. package/payload/server/public/assets/{AdminShell-8JLQM1Mz.js → AdminShell-BcHJy_Y2.js} +1 -1
  96. package/payload/server/public/assets/{Checkbox-CuFJh7lI.js → Checkbox-BY2lndUH.js} +1 -1
  97. package/payload/server/public/assets/{admin-D4nsdmKU.js → admin-C364q_-g.js} +1 -1
  98. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-D-ODjJS5.js → architectureDiagram-Q4EWVU46-B9ik0JyO.js} +1 -1
  99. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DFuVe8f9.js → blockDiagram-DXYQGD6D-C-w-AMRJ.js} +1 -1
  100. package/payload/server/public/assets/{browser-DIKmvDl5.js → browser-D66JrDpx.js} +1 -1
  101. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CeVLfznT.js → c4Diagram-AHTNJAMY-BH-b0NZ1.js} +1 -1
  102. package/payload/server/public/assets/channel-5xoW3jfU.js +1 -0
  103. package/payload/server/public/assets/{chunk-336JU56O-BTaLfR3a.js → chunk-336JU56O-CZHLbmJj.js} +2 -2
  104. package/payload/server/public/assets/{chunk-426QAEUC-BzTbm4pP.js → chunk-426QAEUC-Qx6Ggp9s.js} +1 -1
  105. package/payload/server/public/assets/{chunk-4TB4RGXK-SiUyOXVG.js → chunk-4TB4RGXK-J1Raz9l0.js} +1 -1
  106. package/payload/server/public/assets/{chunk-5FUZZQ4R-BcQ33LnT.js → chunk-5FUZZQ4R-D6TFnJVF.js} +1 -1
  107. package/payload/server/public/assets/{chunk-5PVQY5BW-CT7C1Tik.js → chunk-5PVQY5BW-CC5aEdzK.js} +1 -1
  108. package/payload/server/public/assets/{chunk-EDXVE4YY-CK3uI2u6.js → chunk-EDXVE4YY-Bfbcdcbt.js} +1 -1
  109. package/payload/server/public/assets/{chunk-ENJZ2VHE-Ddp8Otnw.js → chunk-ENJZ2VHE-7EKi84Dw.js} +1 -1
  110. package/payload/server/public/assets/{chunk-ICPOFSXX-DHTxymk_.js → chunk-ICPOFSXX-DIH1FT8n.js} +1 -1
  111. package/payload/server/public/assets/{chunk-OYMX7WX6-Heg3hegz.js → chunk-OYMX7WX6-MgP02n7p.js} +1 -1
  112. package/payload/server/public/assets/{chunk-U2HBQHQK-Cw2PR7aJ.js → chunk-U2HBQHQK-Cwmbq3M7.js} +1 -1
  113. package/payload/server/public/assets/{chunk-X2U36JSP-C7vBCAQQ.js → chunk-X2U36JSP-D65N3MkW.js} +1 -1
  114. package/payload/server/public/assets/{chunk-YZCP3GAM-BS8Mybh1.js → chunk-YZCP3GAM-BkCiRhSG.js} +1 -1
  115. package/payload/server/public/assets/{chunk-ZZ45TVLE-DmnbQp47.js → chunk-ZZ45TVLE-3BQY1FWN.js} +1 -1
  116. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DRSNlIL5.js +1 -0
  117. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CYvRRfMp.js +1 -0
  118. package/payload/server/public/assets/clone-DvQUt_lq.js +1 -0
  119. package/payload/server/public/assets/{dagre-D4i6r8zi.js → dagre-DVhW13RD.js} +1 -1
  120. package/payload/server/public/assets/{dagre-KV5264BT-znWS6Wh2.js → dagre-KV5264BT-CtfjEgDz.js} +1 -1
  121. package/payload/server/public/assets/{data-CHqF4bkS.js → data-uiOcPh8T.js} +1 -1
  122. package/payload/server/public/assets/{diagram-5BDNPKRD-B6tQBN1Y.js → diagram-5BDNPKRD-C8AXEFch.js} +1 -1
  123. package/payload/server/public/assets/{diagram-G4DWMVQ6-t73WpYIY.js → diagram-G4DWMVQ6-B1P8Cwuz.js} +1 -1
  124. package/payload/server/public/assets/{diagram-MMDJMWI5-Cm63UOJm.js → diagram-MMDJMWI5-QBVBnPmO.js} +1 -1
  125. package/payload/server/public/assets/{diagram-TYMM5635-DuTSLjVB.js → diagram-TYMM5635-D2xE8fN8.js} +1 -1
  126. package/payload/server/public/assets/{erDiagram-SMLLAGMA-sbHunitX.js → erDiagram-SMLLAGMA-DMee1Yih.js} +1 -1
  127. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BWOrPuqz.js → flowDiagram-DWJPFMVM-DAd3JjoB.js} +1 -1
  128. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-WaLMGard.js → ganttDiagram-T4ZO3ILL-Bs33CmKo.js} +1 -1
  129. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CsbIgKDn.js → gitGraphDiagram-UUTBAWPF-NXc8aeps.js} +1 -1
  130. package/payload/server/public/assets/{graph-kwyPyf_b.js → graph-BSHyF267.js} +1 -1
  131. package/payload/server/public/assets/{graph-labels-pX3uIQys.js → graph-labels-D5ecmK-Z.js} +1 -1
  132. package/payload/server/public/assets/{graphlib-B8yKFZvc.js → graphlib-DN7sm5nK.js} +1 -1
  133. package/payload/server/public/assets/{infoDiagram-42DDH7IO-Bd55TzsD.js → infoDiagram-42DDH7IO-D2g5STFr.js} +1 -1
  134. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-BFdOsA9f.js → ishikawaDiagram-UXIWVN3A-DBgfV-LB.js} +1 -1
  135. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CMQ5XWO9.js → journeyDiagram-VCZTEJTY-Chh9lW9P.js} +1 -1
  136. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-_RpNNRN7.js → kanban-definition-6JOO6SKY-buI2AXpi.js} +1 -1
  137. package/payload/server/public/assets/{line-SkFxjGJ8.js → line-C8OFYjrW.js} +1 -1
  138. package/payload/server/public/assets/{mermaid-parser.core-Ddok2IAe.js → mermaid-parser.core-BI5Ythbv.js} +1 -1
  139. package/payload/server/public/assets/{mermaid.core-CihAjXBG.js → mermaid.core-C4dyl1Xw.js} +3 -3
  140. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CJsq3hrO.js → mindmap-definition-QFDTVHPH-BbXKSpiv.js} +1 -1
  141. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CSgg8lBB.js → pieDiagram-DEJITSTG-C8-OIoUW.js} +1 -1
  142. package/payload/server/public/assets/{public-BA9P-3ZV.js → public-DGlZwbyw.js} +3 -3
  143. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-TtHxMQtS.js → quadrantDiagram-34T5L4WZ-HOEtpu4n.js} +1 -1
  144. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DdYupKtL.js → requirementDiagram-MS252O5E-DLxjohCO.js} +1 -1
  145. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BVfdyUnO.js → sankeyDiagram-XADWPNL6-E_O_HiFL.js} +1 -1
  146. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-aFLOua6n.js → sequenceDiagram-FGHM5R23-C3-hhAup.js} +1 -1
  147. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-_3Ix8SPZ.js → stateDiagram-FHFEXIEX-BhAmQ0XX.js} +1 -1
  148. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Dhrf9hq_.js +1 -0
  149. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Bs96WqZ7.js → timeline-definition-GMOUNBTQ-DpfZt7xI.js} +1 -1
  150. package/payload/server/public/assets/{useSelectionMode-L9uAHstA.css → useSelectionMode-98jrB9kD.css} +1 -1
  151. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DE_SxsN2.js → vennDiagram-DHZGUBPP-Ck2Ago5q.js} +1 -1
  152. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-DTCdKs6v.js → wardleyDiagram-NUSXRM2D-Dgk8C9kF.js} +1 -1
  153. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CNIrxADs.js → xychartDiagram-5P7HB3ND-BRBSl8ID.js} +1 -1
  154. package/payload/server/public/browser.html +4 -4
  155. package/payload/server/public/data.html +5 -5
  156. package/payload/server/public/graph.html +6 -6
  157. package/payload/server/public/index.html +5 -5
  158. package/payload/server/public/public.html +4 -4
  159. package/payload/server/server.js +607 -5064
  160. package/payload/platform/plugins/business-assistant/references/quote-engine.md +0 -122
  161. package/payload/platform/plugins/business-assistant/references/quote-generation.md +0 -94
  162. package/payload/platform/plugins/business-assistant/references/quoting.md +0 -85
  163. package/payload/platform/plugins/business-assistant/skills/pricing-method/SKILL.md +0 -78
  164. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/learning-from-history.md +0 -51
  165. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/maintenance.md +0 -32
  166. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/manual-definition.md +0 -42
  167. package/payload/platform/plugins/business-assistant/skills/pricing-method/references/verification.md +0 -37
  168. package/payload/server/public/assets/channel-Db8J_1Rl.js +0 -1
  169. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DjquNGFr.js +0 -1
  170. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CcE8sWEP.js +0 -1
  171. package/payload/server/public/assets/clone-xa7JZYoY.js +0 -1
  172. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-eVQQEyxx.js +0 -1
  173. /package/payload/server/public/assets/{useSelectionMode-Da9Glxvu.js → useSelectionMode-80iYuGPa.js} +0 -0
@@ -29,7 +29,7 @@
29
29
  import neo4j from "neo4j-driver";
30
30
  import { getSession } from "../lib/neo4j.js";
31
31
  import { notTrashed } from "../lib/voice-corpus.js";
32
- import { voiceCorpusWhereWithFormat, } from "../lib/voice-corpus.js";
32
+ import { voiceCorpusWhereWithFormat, voiceCorpusWhereWithFormatAndAuthor, ORG_USER_ID, } from "../lib/voice-corpus.js";
33
33
  const DEFAULT_TOKEN_BUDGET_SHORT = 16_000;
34
34
  const DEFAULT_TOKEN_BUDGET_LONG = 96_000;
35
35
  const CHARS_PER_TOKEN = 4;
@@ -52,33 +52,14 @@ export async function voiceRetrieveConditioning(params) {
52
52
  (length === "long" ? DEFAULT_TOKEN_BUDGET_LONG : DEFAULT_TOKEN_BUDGET_SHORT);
53
53
  const charBudget = tokenBudget * CHARS_PER_TOKEN;
54
54
  const format = brief.format;
55
- const corpusWhere = voiceCorpusWhereWithFormat(format);
56
- // getSession() is inside the try so driver-construction / connection-config
57
- // faults (NEO4J_URI unset, password file missing) are caught, logged, and
58
- // returned as the error state rather than escaping the function silently
59
- // (Task 493).
60
- let session;
61
- try {
62
- session = getSession();
63
- // 1. Style card — per-format profile.
64
- const profile = await session.run(`MATCH (a:AdminUser {accountId: $accountId, userId: $userId})
65
- OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile {accountId: $accountId, userId: $userId, format: $format})
66
- RETURN p.styleCard AS styleCard`, { accountId, userId, format });
67
- const styleCard = profile.records[0]?.get("styleCard") ?? null;
68
- // 2. Exemplars — filtered to brief.format. Keyword-aware when topic is set.
69
- //
70
- // Body resolution (Task 471):
71
- // - For :KnowledgeDocument with HAS_SECTION children, concatenate
72
- // child :Section bodies in `position` order. KD.body is null after
73
- // Task 465 server-slicing.
74
- // - Other labels read n.body directly via the COALESCE fallback chain.
75
- //
76
- // The topic filter runs against the computed body, so a topic that
77
- // appears only in n.summary (LLM abstract) no longer matches — by
78
- // design: the corpus surfaces verbatim prose, not the abstract.
79
- const topic = (brief.topic ?? "").trim();
80
- const usesTopic = topic.length > 0;
81
- const bodyComputeBlock = `WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
55
+ const scope = brief.scope ?? "personal";
56
+ const topic = (brief.topic ?? "").trim();
57
+ const usesTopic = topic.length > 0;
58
+ // Body resolution (Task 471): for :KnowledgeDocument with HAS_SECTION
59
+ // children, concatenate child :Section bodies in `position` order (KD.body is
60
+ // null after Task 465 server-slicing); other labels read n.body via the
61
+ // COALESCE chain. The topic filter runs against the computed body.
62
+ const bodyComputeBlock = `WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
82
63
  OPTIONAL MATCH (n)-[:HAS_SECTION]->(s:Section)
83
64
  WHERE ${notTrashed("s")}
84
65
  WITH n, ts, s ORDER BY s.position
@@ -88,64 +69,92 @@ export async function voiceRetrieveConditioning(params) {
88
69
  THEN reduce(acc = '', b IN sectionBodies | acc + b + '\n\n')
89
70
  ELSE coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '')
90
71
  END AS body`;
91
- const cypher = usesTopic
92
- ? `MATCH (n)
93
- WHERE ${corpusWhere}
94
- ${bodyComputeBlock}
95
- WHERE toLower(body) CONTAINS toLower($topic)
96
- RETURN elementId(n) AS id,
97
- labels(n) AS labels,
98
- body,
99
- coalesce(n.subject, n.title, n.name, '') AS source,
100
- ts
101
- ORDER BY ts IS NULL, ts DESC
102
- LIMIT $k`
103
- : `MATCH (n)
104
- WHERE ${corpusWhere}
105
- ${bodyComputeBlock}
106
- RETURN elementId(n) AS id,
107
- labels(n) AS labels,
108
- body,
109
- coalesce(n.subject, n.title, n.name, '') AS source,
110
- ts
111
- ORDER BY ts IS NULL, ts DESC
112
- LIMIT $k`;
113
- const result = await session.run(cypher, {
114
- accountId,
115
- format,
116
- topic,
117
- // neo4j-driver serializes a plain JS number as a Cypher float (15 → 15.0),
118
- // which LIMIT rejects ("not a valid value. Must be a non-negative integer").
119
- // Send it as a Neo4j integer.
120
- k: neo4j.int(k),
121
- });
122
- let charsUsed = 0;
123
- const exemplars = [];
124
- for (const r of result.records) {
125
- const body = r.get("body") ?? "";
126
- if (body.length === 0)
127
- continue;
128
- const labels = r.get("labels") ?? [];
129
- const label = labels.find((l) => ["KnowledgeDocument", "Message", "SocialPost", "Conversation"].includes(l)) ?? labels[0] ?? "Unknown";
130
- const remaining = charBudget - charsUsed;
131
- if (remaining <= 0)
132
- break;
133
- const truncated = body.length > remaining ? body.slice(0, remaining) : body;
134
- exemplars.push({
135
- nodeId: r.get("id"),
136
- label,
137
- body: truncated,
138
- source: (r.get("source") ?? "").slice(0, 200),
72
+ // getSession() is inside the try so driver-construction / connection-config
73
+ // faults (NEO4J_URI unset, password file missing) are caught, logged, and
74
+ // returned as the error state rather than escaping the function silently
75
+ // (Task 493).
76
+ let session;
77
+ try {
78
+ session = getSession();
79
+ // One scoped lookup: the profile style card (keyed on the profile's userId)
80
+ // plus K voice-matched exemplars over the scope's corpus. Org passes the
81
+ // account-wide corpus + `__org__`; personal passes the author-filtered
82
+ // corpus + the operator's userId. Each call gets a fresh char budget.
83
+ const lookup = async (profileUserId, exemplarWhere, exemplarParams) => {
84
+ const profile = await session.run(`OPTIONAL MATCH (p:VoiceProfile {accountId: $accountId, userId: $profileUserId, format: $format})
85
+ RETURN p.styleCard AS styleCard`, { accountId, profileUserId, format });
86
+ const styleCard = profile.records[0]?.get("styleCard") ?? null;
87
+ const cypher = usesTopic
88
+ ? `MATCH (n)
89
+ WHERE ${exemplarWhere}
90
+ ${bodyComputeBlock}
91
+ WHERE toLower(body) CONTAINS toLower($topic)
92
+ RETURN elementId(n) AS id, labels(n) AS labels, body,
93
+ coalesce(n.subject, n.title, n.name, '') AS source, ts
94
+ ORDER BY ts IS NULL, ts DESC
95
+ LIMIT $k`
96
+ : `MATCH (n)
97
+ WHERE ${exemplarWhere}
98
+ ${bodyComputeBlock}
99
+ RETURN elementId(n) AS id, labels(n) AS labels, body,
100
+ coalesce(n.subject, n.title, n.name, '') AS source, ts
101
+ ORDER BY ts IS NULL, ts DESC
102
+ LIMIT $k`;
103
+ const result = await session.run(cypher, {
104
+ ...exemplarParams,
105
+ topic,
106
+ // neo4j-driver serializes a plain JS number as a Cypher float (15
107
+ // 15.0), which LIMIT rejects. Send a Neo4j integer.
108
+ k: neo4j.int(k),
139
109
  });
140
- charsUsed += truncated.length;
110
+ let charsUsed = 0;
111
+ const exemplars = [];
112
+ for (const r of result.records) {
113
+ const body = r.get("body") ?? "";
114
+ if (body.length === 0)
115
+ continue;
116
+ const labels = r.get("labels") ?? [];
117
+ const label = labels.find((l) => ["KnowledgeDocument", "Message", "SocialPost", "Conversation"].includes(l)) ?? labels[0] ?? "Unknown";
118
+ const remaining = charBudget - charsUsed;
119
+ if (remaining <= 0)
120
+ break;
121
+ const truncated = body.length > remaining ? body.slice(0, remaining) : body;
122
+ exemplars.push({
123
+ nodeId: r.get("id"),
124
+ label,
125
+ body: truncated,
126
+ source: (r.get("source") ?? "").slice(0, 200),
127
+ });
128
+ charsUsed += truncated.length;
129
+ }
130
+ return { styleCard, exemplars };
131
+ };
132
+ const hasData = (r) => (r.styleCard !== null && r.styleCard.length > 0) || r.exemplars.length > 0;
133
+ const orgLookup = () => lookup(ORG_USER_ID, voiceCorpusWhereWithFormat(format), { accountId, format });
134
+ let result;
135
+ let status;
136
+ if (scope === "personal") {
137
+ const personal = await lookup(userId, voiceCorpusWhereWithFormatAndAuthor(format), { accountId, format, voiceAuthor: userId });
138
+ if (hasData(personal)) {
139
+ result = personal;
140
+ status = "ok";
141
+ }
142
+ else {
143
+ // No personal profile/corpus yet — borrow the org (house) voice rather
144
+ // than the bare default register (Task 676).
145
+ const org = await orgLookup();
146
+ result = org;
147
+ status = hasData(org) ? "fallback-org" : "no-data";
148
+ }
149
+ }
150
+ else {
151
+ const org = await orgLookup();
152
+ result = org;
153
+ status = hasData(org) ? "ok" : "no-data";
141
154
  }
142
- process.stderr.write(`[voice-retrieve] task=${brief.topic ?? length} format=${format} styleCardBytes=${styleCard?.length ?? 0} exemplarCount=${exemplars.length} tokenBudget=${tokenBudget}\n`);
143
- // An empty-string styleCard with no exemplars is no-data, not ok — a blank
144
- // card conditions nothing, so reporting "ok" would be the same lie this
145
- // discriminator exists to prevent (Task 493).
146
- const hasStyleCard = styleCard !== null && styleCard.length > 0;
147
- const status = hasStyleCard || exemplars.length > 0 ? "ok" : "no-data";
148
- return { styleCard, exemplars, status };
155
+ process.stderr.write(`[voice-retrieve] scope=${scope} format=${format} status=${status} ` +
156
+ `styleCardBytes=${result.styleCard?.length ?? 0} exemplarCount=${result.exemplars.length} tokenBudget=${tokenBudget}\n`);
157
+ return { styleCard: result.styleCard, exemplars: result.exemplars, status };
149
158
  }
150
159
  catch (err) {
151
160
  const message = err instanceof Error ? err.message : String(err);
@@ -1 +1 @@
1
- {"version":3,"file":"voice-retrieve-conditioning.js","sourceRoot":"","sources":["../../src/tools/voice-retrieve-conditioning.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"voice-retrieve-conditioning.js","sourceRoot":"","sources":["../../src/tools/voice-retrieve-conditioning.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,30 @@
1
1
  import { type VoiceFormat } from "../lib/voice-corpus.js";
2
2
  export type AuthorshipMode = "human-only" | "human-led-agent-assisted" | "agent-led-human-reviewed" | "agent-only" | "unknown";
3
3
  export declare const AUTHORSHIP_MODES: ReadonlySet<AuthorshipMode>;
4
+ /**
5
+ * Resolve the corpus author for a tag write (Task 676). An explicit, non-blank
6
+ * `author` wins; otherwise the tagging operator's `userId`. Throws when neither
7
+ * is available (no `author` argument and no `ADMIN_USER_ID` in spawn env), so a
8
+ * tag can never silently land with a null author.
9
+ *
10
+ * This is why single-operator accounts get personal attribution for free: with
11
+ * `author` omitted, the sole operator is the implicit author, so their personal
12
+ * corpus equals the account-wide corpus and prior behaviour is reproduced.
13
+ */
14
+ export declare function resolveVoiceAuthor(author: string | undefined, operatorUserId: string | null): string;
4
15
  export interface VoiceTagContentParams {
5
16
  nodeIds: string[];
6
17
  mode: AuthorshipMode;
7
18
  accountId: string;
8
19
  /** Writing format (required). Editorial classification — operator-supplied. */
9
20
  format: VoiceFormat;
21
+ /**
22
+ * The corpus author (a userId) to stamp on each node (Task 676). Resolve via
23
+ * `resolveVoiceAuthor` before calling — explicit author, else the tagging
24
+ * operator. Personal distillation filters on this; org distillation ignores
25
+ * it (account-wide).
26
+ */
27
+ voiceAuthor: string;
10
28
  }
11
29
  export interface VoiceTagContentResult {
12
30
  updated: number;
@@ -1 +1 @@
1
- {"version":3,"file":"voice-tag-content.d.ts","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"AAeA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,0BAA0B,GAC1B,0BAA0B,GAC1B,YAAY,GACZ,SAAS,CAAC;AAEd,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,cAAc,CAMvD,CAAC;AAEH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAsFhC"}
1
+ {"version":3,"file":"voice-tag-content.d.ts","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"AAeA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,0BAA0B,GAC1B,0BAA0B,GAC1B,YAAY,GACZ,SAAS,CAAC;AAEd,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,cAAc,CAMvD,CAAC;AAEH;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,MAAM,CAOR;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAmGhC"}
@@ -21,8 +21,26 @@ export const AUTHORSHIP_MODES = new Set([
21
21
  "agent-only",
22
22
  "unknown",
23
23
  ]);
24
+ /**
25
+ * Resolve the corpus author for a tag write (Task 676). An explicit, non-blank
26
+ * `author` wins; otherwise the tagging operator's `userId`. Throws when neither
27
+ * is available (no `author` argument and no `ADMIN_USER_ID` in spawn env), so a
28
+ * tag can never silently land with a null author.
29
+ *
30
+ * This is why single-operator accounts get personal attribution for free: with
31
+ * `author` omitted, the sole operator is the implicit author, so their personal
32
+ * corpus equals the account-wide corpus and prior behaviour is reproduced.
33
+ */
34
+ export function resolveVoiceAuthor(author, operatorUserId) {
35
+ const explicit = (author ?? "").trim();
36
+ if (explicit)
37
+ return explicit;
38
+ if (operatorUserId)
39
+ return operatorUserId;
40
+ throw new Error("voice-tag-content: author required — no ADMIN_USER_ID in spawn env and no author given.");
41
+ }
24
42
  export async function voiceTagContent(params) {
25
- const { nodeIds, mode, accountId, format } = params;
43
+ const { nodeIds, mode, accountId, format, voiceAuthor } = params;
26
44
  if (!AUTHORSHIP_MODES.has(mode)) {
27
45
  throw new Error(`voice-tag-content: invalid authorshipMode '${mode}'. Valid values: ${[
28
46
  ...AUTHORSHIP_MODES,
@@ -40,6 +58,12 @@ export async function voiceTagContent(params) {
40
58
  if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
41
59
  return { updated: 0, skipped: 0, skippedReasons: {} };
42
60
  }
61
+ // voiceAuthor is checked after the empty-nodeIds early return: an empty tag is
62
+ // a no-op, so there is nothing to attribute. With nodes to tag, the author
63
+ // must be resolved (index.ts always supplies it via resolveVoiceAuthor).
64
+ if (!voiceAuthor) {
65
+ throw new Error("voice-tag-content: voiceAuthor is required (resolve via resolveVoiceAuthor before calling).");
66
+ }
43
67
  const session = getSession();
44
68
  const now = new Date().toISOString();
45
69
  const skippedReasons = {};
@@ -66,13 +90,15 @@ export async function voiceTagContent(params) {
66
90
  })
67
91
  SET n.authorshipMode = $mode,
68
92
  n.authorshipTaggedAt = $now,
69
- n.format = $format
93
+ n.format = $format,
94
+ n.voiceAuthor = $voiceAuthor
70
95
  RETURN elementId(n) AS id`, {
71
96
  ids: batch,
72
97
  accountId,
73
98
  mode,
74
99
  now,
75
100
  format,
101
+ voiceAuthor,
76
102
  primaryLabels: [...TAGGABLE_PRIMARY_LABELS],
77
103
  });
78
104
  const matched = new Set(result.records.map((r) => r.get("id")));
@@ -88,7 +114,10 @@ export async function voiceTagContent(params) {
88
114
  finally {
89
115
  await session.close();
90
116
  }
91
- process.stderr.write(`[voice-tag] mode=${mode} format=${format} accountId=${accountId} updated=${updated} skipped=${nodeIds.length - updated}\n`);
117
+ // Tagging is always a personal-attribution act: every tagged node carries an
118
+ // author and feeds both that author's personal walk and the account-wide org
119
+ // walk. There is no org-only corpus node, so scope is constant `personal`.
120
+ process.stderr.write(`[voice-tag] mode=${mode} format=${format} scope=personal author=${voiceAuthor} accountId=${accountId} updated=${updated} skipped=${nodeIds.length - updated}\n`);
92
121
  return {
93
122
  updated,
94
123
  skipped: nodeIds.length - updated,
@@ -1 +1 @@
1
- {"version":3,"file":"voice-tag-content.js","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"voice-tag-content.js","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"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"}
@@ -10,12 +10,16 @@
10
10
  * 4. AUTHORSHIP_MODES enum covers the five spec-required values.
11
11
  * 5. TAGGABLE_PRIMARY_LABELS covers the three content labels.
12
12
  * 6. FORMAT_VALUES covers the six writing-format values (Task 462).
13
- * 7. voiceCorpusWhereWithFormat() appends the format predicate.
13
+ * 7. voiceCorpusWhereWithFormat() appends the format predicate;
14
+ * voiceCorpusWhereWithFormatAndAuthor() also appends the author predicate,
15
+ * and ORG_USER_ID / profileUserIdForScope map scope→userId (Task 676).
14
16
  * * 8. voice-retrieve-conditioning discriminates error from no-data (Task 493):
15
17
  * status:"error" on blank identity and on a forced lookup throw, never
16
18
  * collapsing a failed lookup into the no-data payload.
17
19
  * 9. voice-tag-content validates mode and format enums, requires format,
18
- * and returns zero counts on empty nodeIds (with format supplied).
20
+ * returns zero counts on empty nodeIds, and requires voiceAuthor when
21
+ * there are nodes to tag; resolveVoiceAuthor defaults to the operator
22
+ * (Task 676).
19
23
  * 11. voice-ingest-session-text: filterOperatorText() keeps real turns
20
24
  * and excludes slash commands, paste blocks, and system reminders.
21
25
  *
@@ -160,6 +164,17 @@ await check("voiceCorpusWhereWithFormat appends format predicate", () => {
160
164
  assert.ok(result.includes("n.format = $format"));
161
165
  assert.ok(result.includes("email") || result.includes("$format"));
162
166
  });
167
+ await check("voiceCorpusWhereWithFormatAndAuthor appends format + author predicates (Task 676)", () => {
168
+ const result = corpusMod.voiceCorpusWhereWithFormatAndAuthor("article");
169
+ assert.ok(result.includes(corpusMod.VOICE_CORPUS_WHERE));
170
+ assert.ok(result.includes("n.format = $format"));
171
+ assert.ok(result.includes("n.voiceAuthor = $voiceAuthor"));
172
+ });
173
+ await check("ORG_USER_ID sentinel + profileUserIdForScope map scope→userId (Task 676)", () => {
174
+ assert.equal(corpusMod.ORG_USER_ID, "__org__");
175
+ assert.equal(corpusMod.profileUserIdForScope("org", "ua"), "__org__");
176
+ assert.equal(corpusMod.profileUserIdForScope("personal", "ua"), "ua");
177
+ });
163
178
 
164
179
  // ---------------------------------------------------------------------------
165
180
  // 4. deriveFormat is gone (Task 471) — format is operator-supplied editorial.
@@ -290,6 +305,24 @@ await check("voice-tag-content returns zero counts on empty nodeIds (no DB hit)"
290
305
  });
291
306
  assert.deepEqual(result, { updated: 0, skipped: 0, skippedReasons: {} });
292
307
  });
308
+ await check("voice-tag-content requires voiceAuthor when there are nodes to tag (Task 676)", async () => {
309
+ await assert.rejects(
310
+ () =>
311
+ tagMod.voiceTagContent({
312
+ nodeIds: ["x"],
313
+ mode: "human-only",
314
+ accountId: "acc",
315
+ format: "text",
316
+ }),
317
+ /voiceAuthor is required/,
318
+ );
319
+ });
320
+ await check("resolveVoiceAuthor: explicit author wins, omitted defaults to operator, neither throws (Task 676)", () => {
321
+ assert.equal(tagMod.resolveVoiceAuthor("B", "ua"), "B");
322
+ assert.equal(tagMod.resolveVoiceAuthor(undefined, "ua"), "ua");
323
+ assert.equal(tagMod.resolveVoiceAuthor(" ", "ua"), "ua");
324
+ assert.throws(() => tagMod.resolveVoiceAuthor(undefined, null));
325
+ });
293
326
 
294
327
  // ---------------------------------------------------------------------------
295
328
  // 7. voice-ingest-session-text: operator-turn filter (no DB required).
@@ -4,7 +4,8 @@ initStderrTee("writer-craft");
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
- import { voiceTagContent, AUTHORSHIP_MODES, type AuthorshipMode } from "./tools/voice-tag-content.js";
7
+ import { voiceTagContent, resolveVoiceAuthor, AUTHORSHIP_MODES, type AuthorshipMode } from "./tools/voice-tag-content.js";
8
+ import type { VoiceFormat } from "./lib/voice-corpus.js";
8
9
  import { voiceDistilProfile } from "./tools/voice-distil-profile.js";
9
10
  import { voiceRetrieveConditioning } from "./tools/voice-retrieve-conditioning.js";
10
11
  import { voiceRecordFeedback } from "./tools/voice-record-feedback.js";
@@ -53,6 +54,14 @@ const zFormat = z.enum([...FORMAT_VALUES] as [string, ...string[]]).describe(
53
54
  "Writing format: text | email | social-post | article | marketing-copy | note. One :VoiceProfile per (accountId, userId, format).",
54
55
  );
55
56
 
57
+ // Scope enum schema — shared across distil / retrieve / feedback (Task 676).
58
+ const zScope = z
59
+ .enum(["personal", "org"])
60
+ .optional()
61
+ .describe(
62
+ "Voice scope: 'personal' (default) = the operator's own voice, distilled from their author-tagged content and anchored on :AdminUser. 'org' = the account/house voice, distilled from the whole account and anchored on :LocalBusiness under the reserved userId '__org__'.",
63
+ );
64
+
56
65
  server.tool(
57
66
  "voice-tag-content",
58
67
  "Stamp authorshipMode and format on one or many content nodes (:KnowledgeDocument | :Message | :SocialPost) the operator owns. Email threads live as :KnowledgeDocument {source:'email'} since Task 321. Valid modes: human-only, human-led-agent-assisted, agent-led-human-reviewed, agent-only, unknown. Valid formats: text, email, social-post, article, marketing-copy, note. format is editorial — the operator chooses it per node; the tool does not infer it.",
@@ -70,21 +79,48 @@ server.tool(
70
79
  format: zFormat.describe(
71
80
  "Writing format. Required — the operator's editorial classification of the content.",
72
81
  ),
82
+ userId: z
83
+ .string()
84
+ .optional()
85
+ .describe(
86
+ "Operator AdminUser id. Defaults to ADMIN_USER_ID env. Used as the default author when `author` is omitted.",
87
+ ),
88
+ author: z
89
+ .string()
90
+ .optional()
91
+ .describe(
92
+ "voiceAuthor userId to stamp on the nodes. Omit to attribute to the tagging operator — single-operator accounts get personal attribution for free.",
93
+ ),
73
94
  },
74
- async ({ nodeIds, mode, format }) => {
95
+ async ({ nodeIds, mode, format, userId, author }) => {
75
96
  if (!accountId) return refuseNoAccount("voice-tag-content");
97
+ let voiceAuthor: string;
98
+ try {
99
+ voiceAuthor = resolveVoiceAuthor(author, userId ?? resolveUserId());
100
+ } catch (err) {
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text" as const,
105
+ text: err instanceof Error ? err.message : String(err),
106
+ },
107
+ ],
108
+ isError: true,
109
+ };
110
+ }
76
111
  try {
77
112
  const result = await voiceTagContent({
78
113
  nodeIds,
79
114
  mode: mode as AuthorshipMode,
80
115
  accountId,
81
- format: format as import("./lib/voice-corpus.js").VoiceFormat,
116
+ format: format as VoiceFormat,
117
+ voiceAuthor,
82
118
  });
83
119
  return {
84
120
  content: [
85
121
  {
86
122
  type: "text" as const,
87
- text: `voice-tag: mode=${mode} format=${format} updated=${result.updated} skipped=${result.skipped}`,
123
+ text: `voice-tag: mode=${mode} format=${format} author=${voiceAuthor} updated=${result.updated} skipped=${result.skipped}`,
88
124
  },
89
125
  ],
90
126
  };
@@ -104,7 +140,7 @@ server.tool(
104
140
 
105
141
  server.tool(
106
142
  "voice-distil-profile",
107
- "Three-mode tool. mode='sample' (default) walks the operator's full per-format human-only corpus and returns exemplars + recent edit intents for the agent to compose a fresh style card; cadence-guarded (≥20% corpus growth OR ≥30 days). mode='amend' (Task 472) reads only the named documents plus the existing :VoiceProfile.styleCard so the agent can decide whether one or more specific documents move the profile — operator-initiated, no cadence guard. mode='write' persists the YAML style card the agent composed; supply amendedFromNodeIds to attribute the write to specific documents (Task 472, bypasses cadence + corpus walk; existing LEARNED_FROM edges remain via MERGE). Trashed nodes are excluded from every walk. When format is omitted on 'sample', enumerates all formats in the corpus and distils each.",
143
+ "Three-mode tool. mode='sample' (default) walks the operator's full per-format human-only corpus and returns exemplars + recent edit intents for the agent to compose a fresh style card; cadence-guarded (≥20% corpus growth OR ≥30 days). mode='amend' (Task 472) reads only the named documents plus the existing :VoiceProfile.styleCard so the agent can decide whether one or more specific documents move the profile — operator-initiated, no cadence guard. mode='write' persists the YAML style card the agent composed; supply amendedFromNodeIds to attribute the write to specific documents (Task 472, bypasses cadence + corpus walk; existing LEARNED_FROM edges remain via MERGE). Trashed nodes are excluded from every walk. When format is omitted on 'sample', enumerates all formats in the corpus and distils each. scope (Task 676): 'personal' (default) distils the operator's author-tagged content; 'org' distils the whole account into the house voice anchored on the business.",
108
144
  {
109
145
  userId: z
110
146
  .string()
@@ -139,8 +175,9 @@ server.tool(
139
175
  .describe(
140
176
  "Optional for mode='write' after an amend sample. ElementIds the agent decided actually moved the profile. Each becomes a LEARNED_FROM edge from the persisted :VoiceProfile. Setting this routes the write through the amend-write path (no corpus walk, cadence bypassed).",
141
177
  ),
178
+ scope: zScope,
142
179
  },
143
- async ({ userId, format, force, mode, styleCardYaml, nodeIds, amendedFromNodeIds }) => {
180
+ async ({ userId, format, force, mode, styleCardYaml, nodeIds, amendedFromNodeIds, scope }) => {
144
181
  if (!accountId) return refuseNoAccount("voice-distil-profile");
145
182
  const resolvedUserId = userId ?? resolveUserId();
146
183
  if (!resolvedUserId) {
@@ -165,6 +202,7 @@ server.tool(
165
202
  styleCardYaml,
166
203
  nodeIds,
167
204
  amendedFromNodeIds,
205
+ scope: scope as import("./lib/voice-corpus.js").VoiceScope | undefined,
168
206
  });
169
207
 
170
208
  // Summarise single-format or multi-format result.
@@ -207,7 +245,7 @@ server.tool(
207
245
 
208
246
  server.tool(
209
247
  "voice-retrieve-conditioning",
210
- "Return the operator's per-format style card and K voice-matched exemplars for a drafting brief. K=5 short-form (length='short'), K=15 long-form (length='long'). Token-budget bounded. format is required (the writing type: text|email|social-post|article|marketing-copy|note). Returns {styleCard, exemplars, status} where status is one of: 'ok' (profile and/or exemplars returned), 'no-data' (no profile exists and the corpus is empty), 'error' (the lookup failed — carries an error message). Drafting falls back to default register on both 'no-data' and 'error', but only 'no-data' means 'you have no voice profile yet' — never report no-profile on 'error'.",
248
+ "Return the operator's per-format style card and K voice-matched exemplars for a drafting brief. K=5 short-form (length='short'), K=15 long-form (length='long'). Token-budget bounded. format is required (the writing type: text|email|social-post|article|marketing-copy|note). scope (Task 676): 'personal' (default) returns the caller's own voice; 'org' returns the account/house voice. A personal request with no personal profile/corpus falls back to the org profile (status 'fallback-org'). Returns {styleCard, exemplars, status} where status is one of: 'ok', 'fallback-org' (personal asked, org returned), 'no-data' (no profile and empty corpus for the scope), 'error' (lookup failed — carries a message). Drafting falls back to default register on 'no-data' and 'error' and injects the org card on 'fallback-org'; only 'no-data' means 'you have no voice profile yet' — never report no-profile on 'error'.",
211
249
  {
212
250
  userId: z
213
251
  .string()
@@ -223,9 +261,10 @@ server.tool(
223
261
  .optional()
224
262
  .describe("K-selector: short = K=5 (16k tokens, email/post); long = K=15 (96k tokens, brochure/prospectus). Defaults to short."),
225
263
  register: z.string().optional().describe("Optional register hint"),
264
+ scope: zScope,
226
265
  tokenBudget: z.number().int().positive().optional(),
227
266
  },
228
- async ({ userId, format, topic, length, register, tokenBudget }) => {
267
+ async ({ userId, format, topic, length, register, scope, tokenBudget }) => {
229
268
  if (!accountId) return refuseNoAccount("voice-retrieve-conditioning");
230
269
  const resolvedUserId = userId ?? resolveUserId();
231
270
  if (!resolvedUserId) {
@@ -254,6 +293,7 @@ server.tool(
254
293
  topic,
255
294
  length: length as "short" | "long" | undefined,
256
295
  register,
296
+ scope: scope as import("./lib/voice-corpus.js").VoiceScope | undefined,
257
297
  },
258
298
  tokenBudget,
259
299
  });
@@ -287,7 +327,7 @@ server.tool(
287
327
 
288
328
  server.tool(
289
329
  "voice-record-feedback",
290
- "Capture an operator's edit on an agent draft. Writes :VoiceEdit linked to the per-format :VoiceProfile via :FEEDBACK_FOR. The calling agent supplies `intent` (one sentence describing what changed and what voice preference it reveals — Task 424) and `format` (the writing format of the draft).",
330
+ "Capture an operator's edit on an agent draft. Writes :VoiceEdit linked to the per-format :VoiceProfile via :FEEDBACK_FOR. The calling agent supplies `intent` (one sentence describing what changed and what voice preference it reveals — Task 424) and `format` (the writing format of the draft). scope (Task 676): 'org' routes the edit to the account/house profile; 'personal' (default) to the operator's own. The editor is always preserved via the AUTHORED edge.",
291
331
  {
292
332
  userId: z.string().optional(),
293
333
  format: zFormat.describe("Writing format of the draft being edited."),
@@ -299,8 +339,9 @@ server.tool(
299
339
  .describe("One sentence summarising what changed and what voice preference it reveals — produced by the calling agent in-turn from originalText vs editedText (Task 424)."),
300
340
  skill: z.string().optional().describe("Calling skill name, for provenance"),
301
341
  brief: z.string().optional().describe("Drafting brief that produced the original"),
342
+ scope: zScope,
302
343
  },
303
- async ({ userId, format, originalText, editedText, intent, skill, brief }) => {
344
+ async ({ userId, format, originalText, editedText, intent, skill, brief, scope }) => {
304
345
  if (!accountId) return refuseNoAccount("voice-record-feedback");
305
346
  const resolvedUserId = userId ?? resolveUserId();
306
347
  if (!resolvedUserId) {
@@ -322,6 +363,7 @@ server.tool(
322
363
  originalText,
323
364
  editedText,
324
365
  intent,
366
+ scope: scope as import("./lib/voice-corpus.js").VoiceScope | undefined,
325
367
  context: { skill, brief },
326
368
  });
327
369
  return {
@@ -113,3 +113,42 @@ export type VoiceFormat = (typeof FORMAT_VALUES)[number];
113
113
  export function voiceCorpusWhereWithFormat(format: VoiceFormat): string {
114
114
  return `${VOICE_CORPUS_WHERE}\n AND n.format = $format`;
115
115
  }
116
+
117
+ /**
118
+ * Reserved sentinel userId for the per-account org voice profile (Task 676).
119
+ *
120
+ * Keeps the `(accountId, userId, format)` UNIQUE constraint valid with no
121
+ * migration: exactly one org profile per `(account, format)`. A real
122
+ * `:AdminUser` must never hold this userId — `voice-distil-profile` surfaces
123
+ * a Neo4j constraint error (does not swallow it) if one does.
124
+ */
125
+ export const ORG_USER_ID = "__org__";
126
+
127
+ /**
128
+ * Voice profile scope. `personal` = one operator's own voice, keyed
129
+ * `(accountId, userId, format)` and anchored on `:AdminUser`. `org` = the
130
+ * account/house voice, keyed `(accountId, '__org__', format)` and anchored on
131
+ * `:LocalBusiness`.
132
+ */
133
+ export type VoiceScope = "personal" | "org";
134
+
135
+ /**
136
+ * The profile-key userId for a scope: the operator's own id for personal, the
137
+ * reserved sentinel for org. The single place that maps scope → key userId, so
138
+ * distillation, retrieval, and feedback agree on where an org profile lives.
139
+ */
140
+ export function profileUserIdForScope(scope: VoiceScope, userId: string): string {
141
+ return scope === "org" ? ORG_USER_ID : userId;
142
+ }
143
+
144
+ /**
145
+ * `VOICE_CORPUS_WHERE` + format predicate + author predicate. Requires both
146
+ * `$format` and `$voiceAuthor` bound in the calling Cypher parameters.
147
+ *
148
+ * Personal distillation and personal retrieval use this to narrow the
149
+ * account-wide corpus to one author's content; org walks use the unchanged
150
+ * account-wide `voiceCorpusWhereWithFormat`.
151
+ */
152
+ export function voiceCorpusWhereWithFormatAndAuthor(format: VoiceFormat): string {
153
+ return `${VOICE_CORPUS_WHERE}\n AND n.format = $format\n AND n.voiceAuthor = $voiceAuthor`;
154
+ }