---
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"'
---

<objective>
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.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/36-voice-pipeline-foundation/36-RESEARCH.md

<interfaces>
<!-- Current state of files being modified -->

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<typeof createMessageSchema>;
```

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),
});
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Extend shared validators and types with voiceMode field</name>
  <files>
    packages/shared/src/validators/chat.ts
    packages/shared/src/types/chat.ts
    server/src/__tests__/36-voice-schema.test.ts
  </files>
  <read_first>
    packages/shared/src/validators/chat.ts
    packages/shared/src/types/chat.ts
  </read_first>
  <behavior>
    - 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)
  </behavior>
  <action>
    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;
         ```
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>createMessageSchema accepts optional voiceMode field with enum validation. ChatMessage interface includes voiceMode. VOICE_MODES constant and VoiceMode type are exported for reuse.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Extend nexus-settings schema with voiceMode and telegramToken</name>
  <files>
    server/src/services/nexus-settings.ts
    server/src/__tests__/36-voice-schema.test.ts
  </files>
  <read_first>
    server/src/services/nexus-settings.ts
    server/src/__tests__/36-voice-schema.test.ts
  </read_first>
  <behavior>
    - 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
  </behavior>
  <action>
    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<typeof nexusSettingsSchema>`) auto-updates
       - Update the fallback return in `get()` catch block from `{ mode: "both", voiceEnabled: false }` to `nexusSettingsSchema.parse({})` so it uses Zod defaults consistently
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm --filter @paperclipai/server test --run src/__tests__/36-voice-schema.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>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.</done>
</task>

</tasks>

<verification>
- `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
</verification>

<success_criteria>
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.
</success_criteria>

<output>
After completion, create `.planning/phases/36-voice-pipeline-foundation/36-02-SUMMARY.md`
</output>
