--- phase: 36-voice-pipeline-foundation plan: 02 type: execute wave: 1 depends_on: [] files_modified: - packages/shared/src/validators/chat.ts - packages/shared/src/types/chat.ts - server/src/services/nexus-settings.ts - server/src/__tests__/36-voice-schema.test.ts autonomous: true requirements: - VPIPE-05 must_haves: truths: - "createMessageSchema accepts voiceMode field with values text, voice_input, or full_voice" - "createMessageSchema strips unknown fields but preserves voiceMode when present" - "ChatMessage interface includes optional voiceMode field" - "nexus-settings schema accepts voiceMode and telegramToken fields" - "Existing nexus-settings.json files without voiceMode parse without error (defaults to text)" artifacts: - path: "packages/shared/src/validators/chat.ts" provides: "voiceMode field on createMessageSchema" contains: "voiceMode" - path: "packages/shared/src/types/chat.ts" provides: "voiceMode on ChatMessage interface" contains: "voiceMode" - path: "server/src/services/nexus-settings.ts" provides: "voiceMode and telegramToken in settings schema" contains: "voiceMode" - path: "server/src/__tests__/36-voice-schema.test.ts" provides: "Schema validation tests for voiceMode" min_lines: 40 key_links: - from: "packages/shared/src/validators/chat.ts" to: "server/src/routes/chat.ts" via: "createMessageSchema.parse(req.body) preserves voiceMode" pattern: "createMessageSchema" - from: "server/src/services/nexus-settings.ts" to: "nexus-settings.json" via: "Zod .default() handles missing voiceMode in existing files" pattern: 'voiceMode.*default.*"text"' --- Extend shared type definitions and settings schema with voiceMode support so the voice flag can propagate through the entire message pipeline. Purpose: VPIPE-05 requires the voiceMode flag to survive from client request through Express route to message persistence. This plan adds the schema and type foundations that Plan 03 wires together. Output: Updated `createMessageSchema`, `ChatMessage` interface, and `nexus-settings` schema with voiceMode and telegramToken fields, plus tests. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/36-voice-pipeline-foundation/36-RESEARCH.md From packages/shared/src/validators/chat.ts: ```typescript export const createMessageSchema = z.object({ role: z.enum(["user", "assistant", "system"]), content: z.string().min(1).max(100_000), agentId: z.string().uuid().optional(), messageType: z.string().optional(), }); export type CreateMessage = z.infer; ``` From packages/shared/src/types/chat.ts: ```typescript export interface ChatMessage { id: string; conversationId: string; role: "user" | "assistant" | "system"; content: string; agentId: string | null; messageType: string | null; createdAt: string; updatedAt: string | null; files?: ChatFile[]; } ``` From server/src/services/nexus-settings.ts: ```typescript export const NEXUS_MODES = ["personal_ai", "project_builder", "both"] as const; const nexusSettingsSchema = z.object({ mode: z.enum(NEXUS_MODES).default("both"), voiceEnabled: z.boolean().default(false), }); ``` Task 1: Extend shared validators and types with voiceMode field packages/shared/src/validators/chat.ts packages/shared/src/types/chat.ts server/src/__tests__/36-voice-schema.test.ts packages/shared/src/validators/chat.ts packages/shared/src/types/chat.ts - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "full_voice" }) succeeds and output.voiceMode equals "full_voice" - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "voice_input" }) succeeds - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "text" }) succeeds - createMessageSchema.parse({ role: "user", content: "hi" }) succeeds and output.voiceMode is undefined - createMessageSchema.parse({ role: "user", content: "hi", voiceMode: "invalid" }) throws ZodError - Existing createMessageSchema behavior unchanged (role, content, agentId, messageType all work as before) 1. Create `server/src/__tests__/36-voice-schema.test.ts` (RED): - Import `createMessageSchema` from `@paperclipai/shared/validators/chat` - Test: parse with voiceMode "full_voice" returns voiceMode "full_voice" - Test: parse with voiceMode "voice_input" returns voiceMode "voice_input" - Test: parse with voiceMode "text" returns voiceMode "text" - Test: parse without voiceMode returns undefined for voiceMode - Test: parse with voiceMode "invalid" throws - Test: existing fields (role, content, agentId, messageType) still parse correctly 2. Modify `packages/shared/src/validators/chat.ts` (GREEN): - Add to createMessageSchema object: `voiceMode: z.enum(["text", "voice_input", "full_voice"]).optional(),` - Add after existing exports: ```typescript export const VOICE_MODES = ["text", "voice_input", "full_voice"] as const; export type VoiceMode = (typeof VOICE_MODES)[number]; ``` 3. Modify `packages/shared/src/types/chat.ts` (GREEN): - Add to ChatMessage interface after `messageType: string | null;`: ```typescript voiceMode?: "text" | "voice_input" | "full_voice" | null; ``` cd /opt/nexus && pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts - packages/shared/src/validators/chat.ts contains `voiceMode: z.enum(["text", "voice_input", "full_voice"]).optional()` - packages/shared/src/validators/chat.ts contains `export const VOICE_MODES` - packages/shared/src/validators/chat.ts contains `export type VoiceMode` - packages/shared/src/types/chat.ts contains `voiceMode?:` in ChatMessage interface - server/src/__tests__/36-voice-schema.test.ts exits 0 createMessageSchema accepts optional voiceMode field with enum validation. ChatMessage interface includes voiceMode. VOICE_MODES constant and VoiceMode type are exported for reuse. Task 2: Extend nexus-settings schema with voiceMode and telegramToken server/src/services/nexus-settings.ts server/src/__tests__/36-voice-schema.test.ts server/src/services/nexus-settings.ts server/src/__tests__/36-voice-schema.test.ts - nexusSettingsSchema parses { mode: "both" } and output.voiceMode equals "text" (default) - nexusSettingsSchema parses { mode: "both", voiceMode: "full_voice" } and output.voiceMode equals "full_voice" - nexusSettingsSchema parses { mode: "both", telegramToken: "123:ABC" } and output.telegramToken equals "123:ABC" - nexusSettingsSchema parses { mode: "both" } and output.telegramToken is undefined - Existing settings without voiceMode/telegramToken parse without error 1. Add tests to `server/src/__tests__/36-voice-schema.test.ts` (RED): - Import `nexusSettingsService` from `../services/nexus-settings.js` - Note: since nexusSettingsService reads from disk, test the schema directly instead. Import the schema or test via the service's `get()` method with a mocked file system. - Alternative approach: directly import and test the Zod schema. If the schema is not exported, add a named export `nexusSettingsSchema` for testing, or test through the service. - Test: settings without voiceMode defaults to "text" - Test: settings with voiceMode "full_voice" preserves the value - Test: settings with telegramToken preserves the value - Test: settings without telegramToken has telegramToken as undefined 2. Modify `server/src/services/nexus-settings.ts` (GREEN): - Add after existing NEXUS_MODES: ```typescript export const VOICE_MODES = ["text", "voice_input", "full_voice"] as const; export type VoiceMode = (typeof VOICE_MODES)[number]; ``` - Extend nexusSettingsSchema: ```typescript const nexusSettingsSchema = z.object({ mode: z.enum(NEXUS_MODES).default("both"), voiceEnabled: z.boolean().default(false), voiceMode: z.enum(VOICE_MODES).default("text"), telegramToken: z.string().optional(), piperBinaryPath: z.string().optional(), whisperBinaryPath: z.string().optional(), }); ``` - Export the schema for testing: `export { nexusSettingsSchema };` - The `NexusSettings` type (already `z.infer`) auto-updates - Update the fallback return in `get()` catch block from `{ mode: "both", voiceEnabled: false }` to `nexusSettingsSchema.parse({})` so it uses Zod defaults consistently cd /opt/nexus && pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts - server/src/services/nexus-settings.ts contains `voiceMode: z.enum(VOICE_MODES).default("text")` - server/src/services/nexus-settings.ts contains `telegramToken: z.string().optional()` - server/src/services/nexus-settings.ts contains `piperBinaryPath: z.string().optional()` - server/src/services/nexus-settings.ts contains `whisperBinaryPath: z.string().optional()` - server/src/services/nexus-settings.ts contains `export const VOICE_MODES` - server/src/__tests__/36-voice-schema.test.ts exits 0 nexus-settings schema accepts voiceMode (defaulting to "text"), telegramToken, piperBinaryPath, and whisperBinaryPath. Existing nexus-settings.json files without these fields parse without error due to Zod defaults. - `pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts` exits 0 - `grep "voiceMode" packages/shared/src/validators/chat.ts` shows the field - `grep "voiceMode" packages/shared/src/types/chat.ts` shows the field - `grep "voiceMode" server/src/services/nexus-settings.ts` shows the field - `grep "telegramToken" server/src/services/nexus-settings.ts` shows the field The voiceMode field is accepted by createMessageSchema (shared package), present on the ChatMessage type, and configurable in nexus-settings with a safe "text" default. telegramToken is ready for Phase 38. All schema tests pass. After completion, create `.planning/phases/36-voice-pipeline-foundation/36-02-SUMMARY.md`