---
phase: 02-gemini-integration-asset-pipeline
plan: 04
type: execute
wave: 2
depends_on: ["02-01", "02-02"]
files_modified:
  - src/companion/companion.ts
  - src/companion/companion.test.ts
autonomous: true
requirements: [COMP-01, COMP-02, COMP-03, COMP-04]

must_haves:
  truths:
    - "getGreeting tries Gemini API first, returns fallback text if API unavailable"
    - "getLetterIntro tries Gemini API first, returns fallback text if API unavailable"
    - "getForestComment tries Gemini API first, returns fallback text if API unavailable"
    - "All returned texts are max 25 words, simple German, no performance praise"
  artifacts:
    - path: "src/companion/companion.ts"
      provides: "Companion text module with API-first + fallback strategy"
      exports: ["getGreeting", "getLetterIntro", "getForestComment"]
    - path: "src/companion/companion.test.ts"
      provides: "Tests for companion text module"
  key_links:
    - from: "src/companion/companion.ts"
      to: "src/api/gemini.ts"
      via: "calls generateText for AI-generated companion dialogue"
      pattern: "generateText"
    - from: "src/companion/companion.ts"
      to: "src/companion/fallbacks.ts"
      via: "imports fallback functions when API returns null"
      pattern: "getRandomFallbackGreeting|getFallbackLetterIntro|getRandomFallbackForestComment"
    - from: "src/companion/companion.ts"
      to: "src/companion/characters.ts"
      via: "looks up companion name and personality by type"
      pattern: "companions"
---

<objective>
Create the companion text module that provides AI-generated or fallback text for greetings, letter introductions, and forest comments.

Purpose: The companion is the emotional core of Zauberwald. This module makes her speak — via AI when possible, via curated fallbacks otherwise.
Output: `src/companion/companion.ts` with 3 exported functions, full test coverage.
</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/02-gemini-integration-asset-pipeline/02-01-SUMMARY.md

@src/companion/characters.ts
@src/companion/fallbacks.ts
@src/api/gemini.ts
@src/api/config.ts

<interfaces>
<!-- From src/api/gemini.ts (Plan 01) -->
```typescript
export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise<string | null>;
export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise<boolean>;
export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise<void>;
```

<!-- From src/api/config.ts (Plan 01) -->
```typescript
export interface GeminiConfig {
  geminiApiKey: string;
  geminiModel: string;
  imageModel: string;
}
export async function loadConfig(): Promise<GeminiConfig | null>;
```

<!-- From src/companion/fallbacks.ts (Plan 02) -->
```typescript
export type TimeOfDay = 'morgen' | 'mittag' | 'abend';
export function getRandomFallbackGreeting(timeOfDay: TimeOfDay): string;
export function getRandomFallbackForestComment(): string;
export function getFallbackLetterIntro(letter: string): LetterIntro | undefined;
```

<!-- From src/companion/characters.ts -->
```typescript
export interface CompanionDefinition {
  type: CompanionType;
  name: string;
  emoji: string;
  personality: string;
}
export const companions: CompanionDefinition[];
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Companion text module with API-first fallback strategy</name>
  <files>src/companion/companion.ts, src/companion/companion.test.ts</files>
  <read_first>
    - src/companion/characters.ts (companion definitions with name, personality)
    - src/companion/fallbacks.ts (fallback functions from Plan 02)
    - src/api/gemini.ts (generateText signature, checkRateLimit, incrementApiCall)
    - src/api/config.ts (loadConfig, GeminiConfig)
    - SPEC.md section 9.2 (exact prompt templates for greeting, letter intro, forest comment)
  </read_first>
  <behavior>
    getGreeting:
    - Test: returns AI text when API succeeds and rate limit not exceeded
    - Test: returns fallback text when loadConfig returns null (no API key)
    - Test: returns fallback text when generateText returns null (API error)
    - Test: returns fallback text when rate limit exceeded
    - Test: passes correct system prompt with companion personality and time of day
    - Test: calls incrementApiCall after successful API call

    getLetterIntro:
    - Test: returns AI text when API succeeds
    - Test: returns fallback LetterIntro text when API unavailable
    - Test: system prompt includes letter, finger description, and companion personality
    - Test: returned text includes the letter being introduced

    getForestComment:
    - Test: returns AI text when API succeeds
    - Test: returns fallback text when API unavailable
    - Test: system prompt includes element description and companion personality
  </behavior>
  <action>
    Create `src/companion/companion.ts` (per D-13):

    ```typescript
    import { companions } from "./characters";
    import type { CompanionType } from "../types";
    import { loadConfig } from "../api/config";
    import { generateText, checkRateLimit, incrementApiCall } from "../api/gemini";
    import {
      type TimeOfDay,
      getRandomFallbackGreeting,
      getFallbackLetterIntro,
      getRandomFallbackForestComment,
    } from "./fallbacks";

    function getTimeOfDay(): TimeOfDay {
      const hour = new Date().getHours();
      if (hour < 12) return "morgen";
      if (hour < 17) return "mittag";
      return "abend";
    }

    function getCompanion(type: CompanionType) {
      return companions.find(c => c.type === type)!;
    }
    ```

    **getGreeting(characterType: CompanionType, db: IDBDatabase): Promise<string>**
    1. Call `loadConfig()` — if null, return `getRandomFallbackGreeting(getTimeOfDay())`
    2. Call `checkRateLimit(db, 'text')` — if false, return fallback
    3. Build system prompt from SPEC.md 9.2 template:
       ```
       Du bist {name}, ein(e) {personality} im Zauberwald.
       Du sprichst mit einem 7-jaehrigen Kind, das Tippen lernt.

       Regeln:
       - Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
       - Einfache Sprache, kurze Woerter
       - Freundlich und warm, nie belehrend
       - Keine Bewertung von Leistung
       - Kein Englisch
       - Beziehe dich auf die Tageszeit: {timeOfDay}
       ```
    4. User prompt: `"Schreib eine Begruessung."`
    5. Call `generateText(userPrompt, systemPrompt, config)`
    6. If result is null, return fallback
    7. Call `incrementApiCall(db, 'text')`
    8. Return result

    **getLetterIntro(characterType: CompanionType, letter: string, fingerDescription: string, db: IDBDatabase): Promise<string>**
    1. Same config/rate-limit pattern
    2. System prompt from SPEC.md 9.2:
       ```
       Du bist {name}, ein(e) {personality} im Zauberwald.
       Du stellst einem 7-jaehrigen Kind den Buchstaben {letter} vor.

       Regeln:
       - Max. 2 Saetze (max. 25 Woerter)
       - Erklaere, welcher Finger den Buchstaben drueckt: {fingerDescription}
       - Verwende eine bildhafte Eselsbruecke, die zum Wald/Natur/Tiere-Thema passt
       - Einfache Sprache
       - Kein Englisch
       ```
    3. User prompt: `"Stelle den Buchstaben vor."`
    4. On failure: return `getFallbackLetterIntro(letter)?.text ?? "Druecke die leuchtende Taste!"`

    **getForestComment(characterType: CompanionType, elementDescription: string, db: IDBDatabase): Promise<string>**
    1. Same config/rate-limit pattern
    2. System prompt from SPEC.md 9.2:
       ```
       Du bist {name}, ein(e) {personality} im Zauberwald.
       Ein Kind hat gerade eine Tippuebung abgeschlossen. Im Wald ist ein neues Wesen erschienen: {elementDescription}.

       Regeln:
       - Max. 2 kurze Saetze (insgesamt max. 25 Woerter)
       - Druecke Staunen und Freude aus
       - Einfache Sprache fuer 7-Jaehrige
       - Kein Lob fuer Leistung, sondern Begeisterung ueber das neue Waldelement
       - Kein Englisch
       ```
    3. User prompt: `"Kommentiere das neue Element."`
    4. On failure: return `getRandomFallbackForestComment()`

    **Tests (src/companion/companion.test.ts):**
    - Mock `fetch` (via vi.spyOn) for generateText calls
    - Mock `loadConfig` to return config or null
    - Use `fake-indexeddb` for rate-limit DB operations
    - Test each function with API success, API failure, no config, rate-limited
    - At least 9 test cases total (3 per function)
  </action>
  <verify>
    <automated>cd /home/dev/workspace/zauberwald && npx vitest run src/companion/companion.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - `grep -c "export async function getGreeting" src/companion/companion.ts` returns 1
    - `grep -c "export async function getLetterIntro" src/companion/companion.ts` returns 1
    - `grep -c "export async function getForestComment" src/companion/companion.ts` returns 1
    - `grep "loadConfig" src/companion/companion.ts` shows config loader is used
    - `grep "checkRateLimit" src/companion/companion.ts` shows rate-limiting is checked
    - `grep "getRandomFallbackGreeting\|getFallbackLetterIntro\|getRandomFallbackForestComment" src/companion/companion.ts` shows fallbacks imported
    - `npx vitest run src/companion/companion.test.ts` passes with 0 failures and >= 9 tests
  </acceptance_criteria>
  <done>Companion module exports getGreeting, getLetterIntro, getForestComment. Each tries Gemini API first (with rate-limit check), falls back to static texts. All tests pass.</done>
</task>

</tasks>

<verification>
- `npx vitest run src/companion/` — all tests pass
- Each function handles: API success, API failure, no config, rate-limited
</verification>

<success_criteria>
- getGreeting returns AI greeting or fallback, respecting rate limits
- getLetterIntro returns AI letter introduction or fallback
- getForestComment returns AI forest comment or fallback
- All texts conform to COMP-04: max 25 words, simple German, no performance praise
- Tests cover all paths (API success, failure, no config, rate-limited)
</success_criteria>

<output>
After completion, create `.planning/phases/02-gemini-integration-asset-pipeline/02-04-SUMMARY.md`
</output>
