@rubytech/create-maxy-code 0.1.167 → 0.1.170

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 (136) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -1
  3. package/payload/platform/plugins/.claude-plugin/marketplace.json +0 -5
  4. package/payload/platform/plugins/docs/references/platform.md +1 -1
  5. package/payload/premium-plugins/.claude-plugin/marketplace.json +10 -0
  6. package/payload/premium-plugins/teaching/.claude-plugin/plugin.json +8 -0
  7. package/payload/premium-plugins/teaching/PLUGIN.md +58 -0
  8. package/payload/premium-plugins/teaching/skills/interactive-tutor/SKILL.md +59 -0
  9. package/payload/premium-plugins/teaching/skills/interactive-tutor/references/assessment.md +70 -0
  10. package/payload/premium-plugins/teaching/skills/interactive-tutor/references/classroom-conduct.md +43 -0
  11. package/payload/premium-plugins/teaching/skills/interactive-tutor/references/teaching-modes.md +83 -0
  12. package/payload/premium-plugins/teaching/skills/lesson-planner/SKILL.md +48 -0
  13. package/payload/premium-plugins/teaching/skills/lesson-planner/references/context-gathering.md +41 -0
  14. package/payload/premium-plugins/teaching/skills/lesson-planner/references/plan-structure.md +94 -0
  15. package/payload/premium-plugins/teaching/skills/study-pack-builder/SKILL.md +52 -0
  16. package/payload/premium-plugins/teaching/skills/study-pack-builder/references/disaggregation.md +49 -0
  17. package/payload/premium-plugins/teaching/skills/study-pack-builder/references/materials.md +116 -0
  18. package/payload/premium-plugins/writer-craft/.claude-plugin/plugin.json +17 -0
  19. package/payload/premium-plugins/writer-craft/PLUGIN.md +130 -0
  20. package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +96 -0
  21. package/payload/premium-plugins/writer-craft/mcp/package.json +19 -0
  22. package/payload/premium-plugins/writer-craft/mcp/scripts/smoke.mjs +152 -0
  23. package/payload/premium-plugins/writer-craft/mcp/src/index.ts +289 -0
  24. package/payload/premium-plugins/writer-craft/mcp/src/lib/neo4j.ts +56 -0
  25. package/payload/premium-plugins/writer-craft/mcp/src/lib/voice-corpus.ts +54 -0
  26. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +303 -0
  27. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-record-feedback.ts +114 -0
  28. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +145 -0
  29. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-tag-content.ts +117 -0
  30. package/payload/premium-plugins/writer-craft/mcp/tsconfig.json +8 -0
  31. package/payload/premium-plugins/writer-craft/skills/citation-style/SKILL.md +94 -0
  32. package/payload/premium-plugins/writer-craft/skills/citation-style/references/book-and-chapter-models.md +77 -0
  33. package/payload/premium-plugins/writer-craft/skills/citation-style/references/citation-rules.md +103 -0
  34. package/payload/premium-plugins/writer-craft/skills/citation-style/references/journal-article-models.md +74 -0
  35. package/payload/premium-plugins/writer-craft/skills/citation-style/references/other-source-models.md +146 -0
  36. package/payload/premium-plugins/writer-craft/skills/citation-style/references/reference-list-rules.md +70 -0
  37. package/payload/premium-plugins/writer-craft/skills/editorial-practice/SKILL.md +108 -0
  38. package/payload/premium-plugins/writer-craft/skills/editorial-practice/references/copyediting.md +73 -0
  39. package/payload/premium-plugins/writer-craft/skills/editorial-practice/references/developmental-editing.md +85 -0
  40. package/payload/premium-plugins/writer-craft/skills/editorial-practice/references/genre-specific-editing.md +78 -0
  41. package/payload/premium-plugins/writer-craft/skills/editorial-practice/references/line-editing.md +55 -0
  42. package/payload/premium-plugins/writer-craft/skills/editorial-practice/references/self-editing.md +89 -0
  43. package/payload/premium-plugins/writer-craft/skills/persuasive-storytelling/SKILL.md +114 -0
  44. package/payload/premium-plugins/writer-craft/skills/persuasive-storytelling/references/audience-analysis.md +73 -0
  45. package/payload/premium-plugins/writer-craft/skills/persuasive-storytelling/references/crafting-persuasive-story.md +76 -0
  46. package/payload/premium-plugins/writer-craft/skills/persuasive-storytelling/references/persuasion-case-studies.md +67 -0
  47. package/payload/premium-plugins/writer-craft/skills/persuasive-storytelling/references/transformation-framework.md +86 -0
  48. package/payload/premium-plugins/writer-craft/skills/point-of-view/SKILL.md +97 -0
  49. package/payload/premium-plugins/writer-craft/skills/point-of-view/references/indirect-narration.md +72 -0
  50. package/payload/premium-plugins/writer-craft/skills/point-of-view/references/pov-types-and-voice.md +91 -0
  51. package/payload/premium-plugins/writer-craft/skills/point-of-view/references/protagonist-filter.md +71 -0
  52. package/payload/premium-plugins/writer-craft/skills/point-of-view/references/tense-and-person.md +85 -0
  53. package/payload/premium-plugins/writer-craft/skills/prose-craft/SKILL.md +100 -0
  54. package/payload/premium-plugins/writer-craft/skills/prose-craft/references/punctuation-and-grammar.md +72 -0
  55. package/payload/premium-plugins/writer-craft/skills/prose-craft/references/repetition.md +71 -0
  56. package/payload/premium-plugins/writer-craft/skills/prose-craft/references/sound-and-rhythm.md +64 -0
  57. package/payload/premium-plugins/writer-craft/skills/prose-craft/references/word-economy.md +93 -0
  58. package/payload/premium-plugins/writer-craft/skills/reader-engagement/SKILL.md +100 -0
  59. package/payload/premium-plugins/writer-craft/skills/reader-engagement/references/cause-effect-setup-payoff.md +79 -0
  60. package/payload/premium-plugins/writer-craft/skills/reader-engagement/references/conflict-escalation.md +81 -0
  61. package/payload/premium-plugins/writer-craft/skills/reader-engagement/references/hooking-readers.md +67 -0
  62. package/payload/premium-plugins/writer-craft/skills/reader-engagement/references/neurochemistry-of-engagement.md +94 -0
  63. package/payload/premium-plugins/writer-craft/skills/review-manuscript/SKILL.md +111 -0
  64. package/payload/premium-plugins/writer-craft/skills/review-manuscript/references/review-manuscript-checklist.md +119 -0
  65. package/payload/premium-plugins/writer-craft/skills/review-prose/SKILL.md +99 -0
  66. package/payload/premium-plugins/writer-craft/skills/review-prose/references/prose-review-checklist.md +112 -0
  67. package/payload/premium-plugins/writer-craft/skills/review-scene/SKILL.md +99 -0
  68. package/payload/premium-plugins/writer-craft/skills/review-scene/references/scene-analysis-framework.md +95 -0
  69. package/payload/premium-plugins/writer-craft/skills/story-architecture/SKILL.md +106 -0
  70. package/payload/premium-plugins/writer-craft/skills/story-architecture/references/blueprinting-and-scene-cards.md +118 -0
  71. package/payload/premium-plugins/writer-craft/skills/story-architecture/references/inner-issue-and-protagonist-goal.md +66 -0
  72. package/payload/premium-plugins/writer-craft/skills/story-architecture/references/misbelief-desire-worldview.md +87 -0
  73. package/payload/premium-plugins/writer-craft/skills/story-architecture/references/origin-scenes-and-escalation.md +82 -0
  74. package/payload/premium-plugins/writer-craft/skills/story-blueprint/SKILL.md +133 -0
  75. package/payload/premium-plugins/writer-craft/skills/story-blueprint/references/blueprinting-exercises.md +118 -0
  76. package/payload/premium-plugins/writer-craft/skills/story-blueprint/references/blueprinting-process.md +128 -0
  77. package/payload/premium-plugins/writer-craft/skills/voice-mirror/SKILL.md +166 -0
  78. package/payload/server/{chunk-3TRIXQSJ.js → chunk-L2YK2VK3.js} +17 -29
  79. package/payload/server/maxy-edge.js +1 -1
  80. package/payload/server/server.js +1 -1
  81. package/payload/platform/plugins/venture-studio/.claude-plugin/plugin.json +0 -8
  82. package/payload/platform/plugins/venture-studio/PLUGIN.md +0 -119
  83. package/payload/platform/plugins/venture-studio/bin/scaffold.sh +0 -116
  84. package/payload/platform/plugins/venture-studio/skills/brand-pack/SKILL.md +0 -256
  85. package/payload/platform/plugins/venture-studio/skills/brand-pack/references/color-psychology.md +0 -118
  86. package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +0 -376
  87. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +0 -64
  88. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/compliance-research-checklist.md +0 -53
  89. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +0 -88
  90. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +0 -39
  91. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/design-tokens-application.md +0 -79
  92. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/html-pdf-pipeline.md +0 -236
  93. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/internal-workings-scrub.md +0 -33
  94. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/termsheet-template.md +0 -88
  95. package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/index.html +0 -1565
  96. package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/render-pdf.mjs +0 -91
  97. package/payload/platform/plugins/venture-studio/skills/investor-data-room/templates/prospectus/term_sheet.html +0 -715
  98. package/payload/platform/plugins/venture-studio/skills/office-hours/SKILL.md +0 -587
  99. package/payload/platform/plugins/venture-studio/skills/prototype-host/SKILL.md +0 -179
  100. package/payload/platform/plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +0 -81
  101. package/payload/platform/plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +0 -60
  102. package/payload/platform/plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +0 -104
  103. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/SKILL.md +0 -336
  104. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/aarrr-metrics.md +0 -275
  105. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/assumption-testing.md +0 -93
  106. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/boolean-search.md +0 -308
  107. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/build-measure-learn.md +0 -262
  108. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/business-model-canvas.md +0 -171
  109. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/commitment-signals.md +0 -246
  110. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/design-thinking.md +0 -183
  111. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/earlyvangelist.md +0 -190
  112. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/first-principles.md +0 -58
  113. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/fishbone.md +0 -114
  114. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/five-whys.md +0 -43
  115. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/ice-scoring.md +0 -237
  116. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/innovation-accounting.md +0 -290
  117. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/jtbd.md +0 -105
  118. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/landing-page.md +0 -361
  119. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/market-type.md +0 -167
  120. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/mom-test.md +0 -193
  121. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/mvp-types.md +0 -200
  122. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/og-images.md +0 -239
  123. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pareto.md +0 -103
  124. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/persona-development.md +0 -291
  125. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pivot-types.md +0 -225
  126. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/positioning-statement.md +0 -179
  127. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/prd.md +0 -363
  128. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/pre-mortem.md +0 -74
  129. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/problem-validation.md +0 -253
  130. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/product-market-fit.md +0 -256
  131. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/research-synthesis.md +0 -276
  132. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/three-engines-of-growth.md +0 -248
  133. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/validation-tests.md +0 -89
  134. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/value-proposition-canvas.md +0 -121
  135. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/win-loss-analysis.md +0 -242
  136. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/references/workflow-mapping.md +0 -271
@@ -0,0 +1,303 @@
1
+ /**
2
+ * voice-distil-profile — walk the operator's `human-only` corpus and
3
+ * persist a `:VoiceProfile` style card.
4
+ *
5
+ * Cadence guard: re-runs on ≥20% corpus growth since last profile, OR
6
+ * ≥30 days since `generatedAt`. `force: true` overrides both.
7
+ *
8
+ * Body-text sampling caps at ~120k chars (rough proxy for tokens at
9
+ * 4 chars/token = 30k tokens) — well inside Haiku's 200k context window
10
+ * with headroom for the system prompt + tool schema.
11
+ */
12
+ import neo4j from "neo4j-driver";
13
+ import { getSession } from "../lib/neo4j.js";
14
+ import { VOICE_CORPUS_WHERE } from "../lib/voice-corpus.js";
15
+
16
+ // Task 424 — the Haiku style-card composition moved into the calling agent's
17
+ // turn. The agent reads the corpus exemplars and feedback intents (this tool
18
+ // returns them when called with `mode='sample'`), composes the style-card YAML,
19
+ // then calls this tool again with `mode='write'` + `styleCardYaml` set.
20
+
21
+ /**
22
+ * Coerce a neo4j cypher RETURN value to a plain JS number.
23
+ *
24
+ * Neo4j-driver represents INTs as `Integer` objects (high/low 32-bit
25
+ * pair to safely carry int64). `Number()` *usually* works via valueOf()
26
+ * but the safe path is `Integer#toNumber()`. Returns null when the
27
+ * value is null/undefined so cadence-guard arithmetic stays explicit
28
+ * about the "no prior profile" case.
29
+ */
30
+ function toJsNumber(value: unknown): number | null {
31
+ if (value === null || value === undefined) return null;
32
+ if (typeof value === "number") return value;
33
+ if (neo4j.isInt(value)) return (value as { toNumber: () => number }).toNumber();
34
+ if (typeof value === "string") {
35
+ const parsed = Number(value);
36
+ return Number.isFinite(parsed) ? parsed : null;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ const SAMPLE_CHAR_CAP = 120_000;
42
+ const RE_RUN_GROWTH_FRACTION = 0.2;
43
+ const RE_RUN_AGE_DAYS = 30;
44
+
45
+ export interface VoiceDistilProfileParams {
46
+ accountId: string;
47
+ adminUserId: string;
48
+ force?: boolean;
49
+ /**
50
+ * Operation mode (Task 424):
51
+ * - `'sample'` (default) — read the corpus + feedback intents and return
52
+ * them. No write. The calling agent composes the style-card YAML
53
+ * in-turn from the returned exemplars.
54
+ * - `'write'` — persist the style card the agent produced. Requires
55
+ * `styleCardYaml`.
56
+ */
57
+ mode?: "sample" | "write";
58
+ /** Required when `mode='write'`. The style-card YAML produced by the agent. */
59
+ styleCardYaml?: string;
60
+ }
61
+
62
+ export interface VoiceCorpusExemplar {
63
+ id: string;
64
+ label: string;
65
+ body: string;
66
+ }
67
+
68
+ export interface VoiceDistilProfileResult {
69
+ profileId: string | null;
70
+ corpusSize: number;
71
+ generatedAt: string | null;
72
+ feedbackEntries: number;
73
+ skipped: boolean;
74
+ skipReason?: "below-threshold" | "recent" | "empty-corpus" | "missing-style-card";
75
+ errorReason?: string;
76
+ /** Present when `mode='sample'`: corpus exemplars for the agent to read. */
77
+ exemplars?: VoiceCorpusExemplar[];
78
+ /** Present when `mode='sample'`: operator-edit intents to fold into the style card. */
79
+ feedbackIntents?: string[];
80
+ }
81
+
82
+ // The agent produces a YAML style card with at minimum these keys:
83
+ // sentenceLengthDistribution, register, favoured, avoided, taboos,
84
+ // openingPattern, closingPattern, exemplarSentences
85
+ // Exemplar sentences should be quoted verbatim from the returned corpus.
86
+
87
+ export async function voiceDistilProfile(
88
+ params: VoiceDistilProfileParams,
89
+ ): Promise<VoiceDistilProfileResult> {
90
+ const { accountId, adminUserId, force = false } = params;
91
+ if (!accountId || !adminUserId) {
92
+ throw new Error("voice-distil-profile: accountId and adminUserId required");
93
+ }
94
+ const session = getSession();
95
+ const now = new Date();
96
+ const nowIso = now.toISOString();
97
+
98
+ try {
99
+ // 1. Read existing profile (if any) for cadence-guard comparison.
100
+ const existing = await session.run(
101
+ `MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
102
+ OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
103
+ RETURN elementId(p) AS profileId, p.corpusSize AS corpusSize,
104
+ p.generatedAt AS generatedAt`,
105
+ { accountId, adminUserId },
106
+ );
107
+ const existingRow = existing.records[0];
108
+ const prevProfileId = (existingRow?.get("profileId") as string | null) ?? null;
109
+ const prevCorpusSize = toJsNumber(existingRow?.get("corpusSize"));
110
+ const prevGeneratedAt = (existingRow?.get("generatedAt") as string | null) ?? null;
111
+
112
+ // 2. Count corpus. Filter matches human-only + human-led-agent-assisted
113
+ // across the four primary content labels plus conversation-document
114
+ // :Section chunks (chat archives — children of a :KnowledgeDocument
115
+ // with `conversationIdentity` set). See VOICE_CORPUS_WHERE above.
116
+ const countResult = await session.run(
117
+ `MATCH (n)
118
+ WHERE ${VOICE_CORPUS_WHERE}
119
+ RETURN count(n) AS c`,
120
+ { accountId },
121
+ );
122
+ const corpusSize = toJsNumber(countResult.records[0]?.get("c")) ?? 0;
123
+
124
+ if (corpusSize === 0) {
125
+ process.stderr.write(
126
+ `[voice-distil] adminUser=${adminUserId} skip reason=empty-corpus\n`,
127
+ );
128
+ return {
129
+ profileId: prevProfileId,
130
+ corpusSize: 0,
131
+ generatedAt: prevGeneratedAt,
132
+ feedbackEntries: 0,
133
+ skipped: true,
134
+ skipReason: "empty-corpus",
135
+ };
136
+ }
137
+
138
+ // 3. Cadence guard.
139
+ if (!force && prevProfileId && prevCorpusSize !== null && prevGeneratedAt) {
140
+ const ageMs = now.getTime() - new Date(prevGeneratedAt).getTime();
141
+ const ageDays = ageMs / (24 * 60 * 60 * 1000);
142
+ // prevCorpusSize is non-null here (the guard above checks). When it's
143
+ // zero we treat growth as infinite — first real corpus always triggers
144
+ // a re-run.
145
+ const growth =
146
+ prevCorpusSize > 0
147
+ ? (corpusSize - prevCorpusSize) / prevCorpusSize
148
+ : 1;
149
+ const ageTrigger = ageDays >= RE_RUN_AGE_DAYS;
150
+ const growthTrigger = growth >= RE_RUN_GROWTH_FRACTION;
151
+ if (!ageTrigger && !growthTrigger) {
152
+ const reason = ageDays < 1 ? "recent" : "below-threshold";
153
+ process.stderr.write(
154
+ `[voice-distil] adminUser=${adminUserId} skip reason=${reason} corpusSize=${corpusSize} prevCorpus=${prevCorpusSize} ageDays=${ageDays.toFixed(1)}\n`,
155
+ );
156
+ return {
157
+ profileId: prevProfileId,
158
+ corpusSize,
159
+ generatedAt: prevGeneratedAt,
160
+ feedbackEntries: 0,
161
+ skipped: true,
162
+ skipReason: reason,
163
+ };
164
+ }
165
+ }
166
+
167
+ // 4. Sample corpus bodies up to the char cap. Pick recent first.
168
+ // Timestamp coalesces include `firstMessageAt` for conversation-document
169
+ // :Section chunks so chat archives sort by when the conversation
170
+ // actually happened.
171
+ const sampled = await session.run(
172
+ `MATCH (n)
173
+ WHERE ${VOICE_CORPUS_WHERE}
174
+ WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
175
+ OPTIONAL MATCH (parent:KnowledgeDocument)-[:HAS_SECTION]->(n)
176
+ WHERE 'Section' IN labels(n) AND parent.conversationIdentity IS NOT NULL
177
+ RETURN elementId(n) AS id,
178
+ labels(n) AS labels,
179
+ coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '') AS body,
180
+ parent.conversationIdentity IS NOT NULL AS isConversationChunk,
181
+ ts
182
+ ORDER BY ts DESC NULLS LAST
183
+ LIMIT 500`,
184
+ { accountId },
185
+ );
186
+ const samples: { id: string; label: string; body: string }[] = [];
187
+ let charBudget = SAMPLE_CHAR_CAP;
188
+ for (const r of sampled.records) {
189
+ const body = (r.get("body") as string | null) ?? "";
190
+ const labels = (r.get("labels") as string[]) ?? [];
191
+ // Conversation-document sections carry `:Section` only after the Task
192
+ // 324 collapse; surface "Conversation" via the parent's
193
+ // `conversationIdentity` so the LLM exemplar header still says "this
194
+ // is chat" rather than the bare base label.
195
+ const isConversationChunk = Boolean(r.get("isConversationChunk"));
196
+ const label = isConversationChunk
197
+ ? "Conversation"
198
+ : (labels.find((l) =>
199
+ ["KnowledgeDocument", "Message", "SocialPost"].includes(l),
200
+ ) ?? labels[0] ?? "Unknown");
201
+ if (body.length === 0) continue;
202
+ const trimmed = body.length > charBudget ? body.slice(0, charBudget) : body;
203
+ samples.push({ id: r.get("id") as string, label, body: trimmed });
204
+ charBudget -= trimmed.length;
205
+ if (charBudget <= 0) break;
206
+ }
207
+
208
+ // 5. Pull feedback intents from existing :VoiceEdit nodes for this operator.
209
+ const feedback = await session.run(
210
+ `MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
211
+ MATCH (e:VoiceEdit {adminUserId: $adminUserId, accountId: $accountId})
212
+ RETURN e.intent AS intent ORDER BY e.occurredAt DESC LIMIT 50`,
213
+ { accountId, adminUserId },
214
+ );
215
+ const feedbackIntents = feedback.records
216
+ .map((r) => r.get("intent") as string | null)
217
+ .filter((x): x is string => typeof x === "string" && x.length > 0);
218
+
219
+ // 6. Mode branch (Task 424).
220
+ // - `sample` (default): return the exemplars + feedback intents to the
221
+ // calling agent. The agent composes the YAML style card in its own
222
+ // turn and re-invokes with mode='write'.
223
+ // - `write`: persist the agent-produced style card.
224
+ const mode = params.mode ?? "sample";
225
+ if (mode === "sample") {
226
+ process.stderr.write(
227
+ `[voice-distil] adminUser=${adminUserId} mode=sample corpusSize=${corpusSize} ` +
228
+ `exemplars=${samples.length} feedbackEntries=${feedbackIntents.length}\n`,
229
+ );
230
+ return {
231
+ profileId: prevProfileId,
232
+ corpusSize,
233
+ generatedAt: prevGeneratedAt,
234
+ feedbackEntries: feedbackIntents.length,
235
+ skipped: true,
236
+ skipReason: "missing-style-card",
237
+ exemplars: samples,
238
+ feedbackIntents,
239
+ };
240
+ }
241
+
242
+ const styleCardYaml = (params.styleCardYaml ?? "").trim();
243
+ if (!styleCardYaml) {
244
+ throw new Error(
245
+ "voice-distil-profile: mode='write' requires styleCardYaml (the YAML you composed from the sample-mode response).",
246
+ );
247
+ }
248
+
249
+ // 7. Write the :VoiceProfile node + LEARNED_FROM edges to every sampled node.
250
+ //
251
+ // The :AdminUser must already exist (onboarding/auth is the only
252
+ // legitimate origin for :AdminUser). MATCH not MERGE: a typo in
253
+ // adminUserId, an env mismatch, or a dropped credential surfaces
254
+ // here as a typed error rather than silently minting an orphan
255
+ // AdminUser that bypasses the platform's label-origin protection.
256
+ const writeResult = await session.run(
257
+ `MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
258
+ MERGE (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile {accountId: $accountId, adminUserId: $adminUserId})
259
+ ON CREATE SET p.createdAt = $now
260
+ SET p.styleCard = $styleCard,
261
+ p.generatedAt = $now,
262
+ p.corpusSize = $corpusSize,
263
+ p.feedbackEntries = $feedbackEntries,
264
+ p.updatedAt = $now
265
+ WITH p
266
+ UNWIND $sampleIds AS sid
267
+ MATCH (s) WHERE elementId(s) = sid
268
+ MERGE (p)-[:LEARNED_FROM]->(s)
269
+ RETURN elementId(p) AS profileId`,
270
+ {
271
+ accountId,
272
+ adminUserId,
273
+ styleCard: styleCardYaml,
274
+ now: nowIso,
275
+ corpusSize,
276
+ feedbackEntries: feedbackIntents.length,
277
+ sampleIds: samples.map((s) => s.id),
278
+ },
279
+ );
280
+ // MATCH-on-:AdminUser failure yields zero records. Throw the typed
281
+ // error here rather than returning a half-baked result.
282
+ if (writeResult.records.length === 0) {
283
+ throw new Error(
284
+ `voice-mirror: :AdminUser {accountId='${accountId}', adminUserId='${adminUserId}'} not found. Onboarding must promote the operator to :AdminUser before distillation.`,
285
+ );
286
+ }
287
+ const profileId = writeResult.records[0].get("profileId") as string;
288
+
289
+ process.stderr.write(
290
+ `[voice-distil] adminUser=${adminUserId} mode=write corpusSize=${corpusSize} generatedAt=${nowIso} feedbackEntries=${feedbackIntents.length}\n`,
291
+ );
292
+
293
+ return {
294
+ profileId,
295
+ corpusSize,
296
+ generatedAt: nowIso,
297
+ feedbackEntries: feedbackIntents.length,
298
+ skipped: false,
299
+ };
300
+ } finally {
301
+ await session.close();
302
+ }
303
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * voice-record-feedback — capture an operator's edit on an agent draft
3
+ * as feedback for the next distillation.
4
+ *
5
+ * Task 424: the Haiku diff-intent summarisation moved into the calling
6
+ * agent's turn. The agent compares `originalText` to `editedText` itself
7
+ * and passes the resulting `intent` string as a required parameter. The
8
+ * tool now does the graph writes only.
9
+ *
10
+ * Side effects:
11
+ * 1. Write `(:VoiceEdit)-[:FEEDBACK_FOR]->(:VoiceProfile)` if the profile
12
+ * exists; otherwise write the :VoiceEdit alone so the next distillation
13
+ * back-fills the edge.
14
+ */
15
+ import { randomUUID } from "node:crypto";
16
+ import { getSession } from "../lib/neo4j.js";
17
+
18
+ export interface VoiceRecordFeedbackParams {
19
+ accountId: string;
20
+ adminUserId: string;
21
+ originalText: string;
22
+ editedText: string;
23
+ /**
24
+ * One sentence describing what changed and what voice preference it reveals.
25
+ * The calling agent summarises this from `originalText` vs `editedText` in
26
+ * its own turn before invoking the tool (Task 424). Examples: "shorter and
27
+ * dropped the apology", "switched to first person", "removed corporate
28
+ * sign-off", "added a specific date".
29
+ */
30
+ intent: string;
31
+ context?: {
32
+ skill?: string;
33
+ brief?: string;
34
+ };
35
+ }
36
+
37
+ export interface VoiceRecordFeedbackResult {
38
+ editId: string;
39
+ intent: string;
40
+ occurredAt: string;
41
+ hadProfile: boolean;
42
+ }
43
+
44
+ export async function voiceRecordFeedback(
45
+ params: VoiceRecordFeedbackParams,
46
+ ): Promise<VoiceRecordFeedbackResult> {
47
+ const { accountId, adminUserId, originalText, editedText, intent: rawIntent, context } = params;
48
+ if (!accountId || !adminUserId) {
49
+ throw new Error("voice-record-feedback: accountId and adminUserId required");
50
+ }
51
+ if (!originalText || !editedText) {
52
+ throw new Error("voice-record-feedback: originalText and editedText required");
53
+ }
54
+ const intent = (rawIntent ?? "").trim();
55
+ if (!intent) {
56
+ throw new Error(
57
+ "voice-record-feedback: intent is required — summarise what the operator changed in-turn before calling this tool (Task 424).",
58
+ );
59
+ }
60
+
61
+ const editId = randomUUID();
62
+ const occurredAt = new Date().toISOString();
63
+ const cappedIntent = intent.slice(0, 500);
64
+
65
+ const session = getSession();
66
+ try {
67
+ const result = await session.run(
68
+ `MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
69
+ CREATE (e:VoiceEdit {
70
+ editId: $editId,
71
+ accountId: $accountId,
72
+ adminUserId: $adminUserId,
73
+ originalText: $originalText,
74
+ editedText: $editedText,
75
+ intent: $intent,
76
+ occurredAt: $occurredAt,
77
+ skill: $skill,
78
+ brief: $brief
79
+ })
80
+ CREATE (a)-[:AUTHORED]->(e)
81
+ WITH e
82
+ OPTIONAL MATCH (:AdminUser {accountId: $accountId, adminUserId: $adminUserId})-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
83
+ FOREACH (_ IN CASE WHEN p IS NULL THEN [] ELSE [1] END |
84
+ MERGE (e)-[:FEEDBACK_FOR]->(p)
85
+ )
86
+ RETURN elementId(e) AS editId, (p IS NOT NULL) AS hadProfile`,
87
+ {
88
+ accountId,
89
+ adminUserId,
90
+ editId,
91
+ originalText,
92
+ editedText,
93
+ intent: cappedIntent,
94
+ occurredAt,
95
+ skill: context?.skill ?? null,
96
+ brief: context?.brief ?? null,
97
+ },
98
+ );
99
+ if (result.records.length === 0) {
100
+ throw new Error(
101
+ `voice-mirror: :AdminUser {accountId='${accountId}', adminUserId='${adminUserId}'} not found. Onboarding must promote the operator to :AdminUser before feedback can be recorded.`,
102
+ );
103
+ }
104
+ const hadProfile = result.records[0].get("hadProfile") === true;
105
+
106
+ process.stderr.write(
107
+ `[voice-record-feedback] adminUser=${adminUserId} intent="${cappedIntent.replace(/"/g, "'")}" diffBytes=${editedText.length - originalText.length}\n`,
108
+ );
109
+
110
+ return { editId, intent: cappedIntent, occurredAt, hadProfile };
111
+ } finally {
112
+ await session.close();
113
+ }
114
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * voice-retrieve-conditioning — return the operator's style card plus K
3
+ * voice-matched exemplars for a drafting brief.
4
+ *
5
+ * Retrieval order (best to worst):
6
+ * 1. If brief.topic is provided AND keyword search on the human-only corpus
7
+ * returns ≥K matches → use those (ordered by recency).
8
+ * 2. Otherwise, return the K most-recent human-only nodes.
9
+ *
10
+ * Embeddings are not used in v1: the corpus is small (single operator's
11
+ * personal writing — typically <500 docs), and recency-with-keyword-filter
12
+ * is both faster and more predictable than vector ANN at that scale.
13
+ * Vector-search is a Task 365-follow-up if the corpus grows beyond the
14
+ * 500-doc breakeven point.
15
+ *
16
+ * Graceful degradation: missing profile, empty corpus, and DB errors all
17
+ * return `{styleCard: null, exemplars: []}` rather than throwing — the
18
+ * calling drafting skill falls back to its existing default register.
19
+ */
20
+ import { getSession } from "../lib/neo4j.js";
21
+ import { VOICE_CORPUS_WHERE } from "../lib/voice-corpus.js";
22
+
23
+ const DEFAULT_TOKEN_BUDGET_SHORT = 16_000;
24
+ const DEFAULT_TOKEN_BUDGET_LONG = 96_000;
25
+ const CHARS_PER_TOKEN = 4;
26
+
27
+ export interface VoiceRetrieveConditioningParams {
28
+ accountId: string;
29
+ adminUserId: string;
30
+ brief: {
31
+ topic?: string;
32
+ format: "short" | "long";
33
+ register?: string;
34
+ };
35
+ tokenBudget?: number;
36
+ }
37
+
38
+ export interface VoiceExemplar {
39
+ nodeId: string;
40
+ label: string;
41
+ body: string;
42
+ source: string;
43
+ }
44
+
45
+ export interface VoiceRetrieveConditioningResult {
46
+ styleCard: string | null;
47
+ exemplars: VoiceExemplar[];
48
+ }
49
+
50
+ const EMPTY: VoiceRetrieveConditioningResult = { styleCard: null, exemplars: [] };
51
+
52
+ export async function voiceRetrieveConditioning(
53
+ params: VoiceRetrieveConditioningParams,
54
+ ): Promise<VoiceRetrieveConditioningResult> {
55
+ const { accountId, adminUserId, brief } = params;
56
+ if (!accountId || !adminUserId) return EMPTY;
57
+
58
+ const k = brief.format === "long" ? 15 : 5;
59
+ const tokenBudget =
60
+ params.tokenBudget ??
61
+ (brief.format === "long" ? DEFAULT_TOKEN_BUDGET_LONG : DEFAULT_TOKEN_BUDGET_SHORT);
62
+ const charBudget = tokenBudget * CHARS_PER_TOKEN;
63
+
64
+ const session = getSession();
65
+ try {
66
+ // 1. Style card.
67
+ const profile = await session.run(
68
+ `MATCH (a:AdminUser {accountId: $accountId, adminUserId: $adminUserId})
69
+ OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
70
+ RETURN p.styleCard AS styleCard`,
71
+ { accountId, adminUserId },
72
+ );
73
+ const styleCard = (profile.records[0]?.get("styleCard") as string | null) ?? null;
74
+
75
+ // 2. Exemplars. Keyword-aware filter when brief.topic is set.
76
+ const topic = (brief.topic ?? "").trim();
77
+ const usesTopic = topic.length > 0;
78
+
79
+ const cypher = usesTopic
80
+ ? `MATCH (n)
81
+ WHERE ${VOICE_CORPUS_WHERE}
82
+ AND toLower(coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '')) CONTAINS toLower($topic)
83
+ WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
84
+ RETURN elementId(n) AS id,
85
+ labels(n) AS labels,
86
+ coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '') AS body,
87
+ coalesce(n.subject, n.title, n.name, '') AS source,
88
+ ts
89
+ ORDER BY ts DESC NULLS LAST
90
+ LIMIT $k`
91
+ : `MATCH (n)
92
+ WHERE ${VOICE_CORPUS_WHERE}
93
+ WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
94
+ RETURN elementId(n) AS id,
95
+ labels(n) AS labels,
96
+ coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '') AS body,
97
+ coalesce(n.subject, n.title, n.name, '') AS source,
98
+ ts
99
+ ORDER BY ts DESC NULLS LAST
100
+ LIMIT $k`;
101
+
102
+ const result = await session.run(cypher, {
103
+ accountId,
104
+ topic,
105
+ k,
106
+ });
107
+
108
+ let charsUsed = 0;
109
+ const exemplars: VoiceExemplar[] = [];
110
+ for (const r of result.records) {
111
+ const body = (r.get("body") as string | null) ?? "";
112
+ if (body.length === 0) continue;
113
+ const labels = (r.get("labels") as string[]) ?? [];
114
+ const label =
115
+ labels.find((l) =>
116
+ ["KnowledgeDocument", "Message", "SocialPost", "Conversation"].includes(l),
117
+ ) ?? labels[0] ?? "Unknown";
118
+ const remaining = charBudget - charsUsed;
119
+ if (remaining <= 0) break;
120
+ const truncated = body.length > remaining ? body.slice(0, remaining) : body;
121
+ exemplars.push({
122
+ nodeId: r.get("id") as string,
123
+ label,
124
+ body: truncated,
125
+ source: ((r.get("source") as string | null) ?? "").slice(0, 200),
126
+ });
127
+ charsUsed += truncated.length;
128
+ }
129
+
130
+ process.stderr.write(
131
+ `[voice-retrieve] task=${brief.topic ?? brief.format} styleCardBytes=${
132
+ styleCard?.length ?? 0
133
+ } exemplarCount=${exemplars.length} tokenBudget=${tokenBudget}\n`,
134
+ );
135
+
136
+ return { styleCard, exemplars };
137
+ } catch (err) {
138
+ process.stderr.write(
139
+ `[voice-retrieve] error: ${err instanceof Error ? err.message : String(err)}\n`,
140
+ );
141
+ return EMPTY;
142
+ } finally {
143
+ await session.close();
144
+ }
145
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * voice-tag-content — set `authorshipMode` on one or many content nodes.
3
+ *
4
+ * The tool routes through neo4j directly with an account-scoped MERGE so
5
+ * the operator can only mutate their own nodes. We do not go through
6
+ * `memory-update` because that tool re-embeds and re-validates on every
7
+ * call — overkill for a single-property authorship stamp.
8
+ */
9
+ import { getSession } from "../lib/neo4j.js";
10
+ import { TAGGABLE_PRIMARY_LABELS } from "../lib/voice-corpus.js";
11
+
12
+ export type AuthorshipMode =
13
+ | "human-only"
14
+ | "human-led-agent-assisted"
15
+ | "agent-led-human-reviewed"
16
+ | "agent-only"
17
+ | "unknown";
18
+
19
+ export const AUTHORSHIP_MODES: ReadonlySet<AuthorshipMode> = new Set([
20
+ "human-only",
21
+ "human-led-agent-assisted",
22
+ "agent-led-human-reviewed",
23
+ "agent-only",
24
+ "unknown",
25
+ ]);
26
+
27
+ export interface VoiceTagContentParams {
28
+ nodeIds: string[];
29
+ mode: AuthorshipMode;
30
+ accountId: string;
31
+ }
32
+
33
+ export interface VoiceTagContentResult {
34
+ updated: number;
35
+ skipped: number;
36
+ skippedReasons: Record<string, number>;
37
+ }
38
+
39
+ export async function voiceTagContent(
40
+ params: VoiceTagContentParams,
41
+ ): Promise<VoiceTagContentResult> {
42
+ const { nodeIds, mode, accountId } = params;
43
+
44
+ if (!AUTHORSHIP_MODES.has(mode)) {
45
+ throw new Error(
46
+ `voice-tag-content: invalid authorshipMode '${mode}'. Valid values: ${[
47
+ ...AUTHORSHIP_MODES,
48
+ ].join(", ")}`,
49
+ );
50
+ }
51
+ if (!accountId) {
52
+ throw new Error("voice-tag-content: accountId is required");
53
+ }
54
+ if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
55
+ return { updated: 0, skipped: 0, skippedReasons: {} };
56
+ }
57
+
58
+ const session = getSession();
59
+ const now = new Date().toISOString();
60
+ const skippedReasons: Record<string, number> = {};
61
+ let updated = 0;
62
+
63
+ try {
64
+ // Bulk update in batches of 50 to keep transaction latency bounded.
65
+ const BATCH = 50;
66
+ for (let i = 0; i < nodeIds.length; i += BATCH) {
67
+ const batch = nodeIds.slice(i, i + BATCH);
68
+ const result = await session.run(
69
+ // Tag any of the four primary content labels, or a conversation-
70
+ // document :Section chunk (parent :KnowledgeDocument carries
71
+ // `conversationIdentity`). The EXISTS sub-pattern guards against
72
+ // accidentally tagging document-path :Section rows AND parent
73
+ // `:Conversation` / `:AdminConversation` nodes (which are not
74
+ // body-bearing content).
75
+ `UNWIND $ids AS id
76
+ MATCH (n) WHERE elementId(n) = id AND n.accountId = $accountId
77
+ WITH n
78
+ WHERE any(lbl IN labels(n) WHERE lbl IN $primaryLabels)
79
+ OR ('Section' IN labels(n) AND EXISTS {
80
+ MATCH (parent:KnowledgeDocument)-[:HAS_SECTION]->(n)
81
+ WHERE parent.conversationIdentity IS NOT NULL
82
+ })
83
+ SET n.authorshipMode = $mode,
84
+ n.authorshipTaggedAt = $now
85
+ RETURN elementId(n) AS id`,
86
+ {
87
+ ids: batch,
88
+ accountId,
89
+ mode,
90
+ now,
91
+ primaryLabels: [...TAGGABLE_PRIMARY_LABELS],
92
+ },
93
+ );
94
+ const matched = new Set(result.records.map((r) => r.get("id") as string));
95
+ updated += matched.size;
96
+ for (const id of batch) {
97
+ if (matched.has(id)) continue;
98
+ const reason = "not-found-or-not-content-label";
99
+ skippedReasons[reason] = (skippedReasons[reason] ?? 0) + 1;
100
+ }
101
+ }
102
+ } finally {
103
+ await session.close();
104
+ }
105
+
106
+ process.stderr.write(
107
+ `[voice-tag] mode=${mode} accountId=${accountId} updated=${updated} skipped=${
108
+ nodeIds.length - updated
109
+ }\n`,
110
+ );
111
+
112
+ return {
113
+ updated,
114
+ skipped: nodeIds.length - updated,
115
+ skippedReasons,
116
+ };
117
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../platform/tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }