Files
praxis/.ciagent/PLAN.md
T
Praxis CI 48cbd4a2b3 docs(P00): complete pre-execution phase
---ci---
phase: 0
milestone: v0.1
status: complete
requirements:
  covered: [REQ-VOICE-01, REQ-VOICE-02, REQ-VOICE-03, REQ-VOICE-04, REQ-SCEN-01, REQ-STATE-01, REQ-LLM-01, REQ-LLM-02, REQ-DEBRIEF-01, REQ-ORCH-01, REQ-ORCH-02, REQ-SCEN-FMT-01, REQ-NFR-LAT-01, REQ-NFR-SAFE-01, REQ-NFR-COST-01]
  partial: []
---/ci---
2026-08-01 12:49:34 +00:00

30 KiB

Praxis — Phase 1 Plan (Minimal Viable Voice Loop)

Milestone: v0.1 (foundation) Phase: 1 — Minimal Viable Voice Loop Branch: phase/01-minimal-voice-loop (created at EXECUTE) Status: plan Source artifacts: PROJECT.md (D-001..D-020), REQUIREMENTS.md, ARCHITECTURE.md, RESEARCH.md (R1-R10), PERSONAS.md, ROADMAP.md


1. Phase 1 Summary

Goal

A single learner can open the React web client, speak to an AI tutor playing a Customer Service role-play scenario ("angry customer requesting refund on damaged product", one branch point: escalate vs accept), hear the tutor respond with <600ms end-to-end latency target, receive a single end-of-session text+voice coaching debrief, and have the session logged to SQLite learner state.

Scope (in)

  • Streaming voice loop: Deepgram Nova-3 ASR → Ollama Cloud LLM (gemma4:cloud) → Cartesia/Piper TTS, orchestrated by Pipecat with Silero VAD
  • One branching Customer Service scenario (refund, one branch point, failure_mode field present)
  • Interruptibility (abort-and-yield per D-008)
  • Pluggable guardrail layer with Customer Service ruleset
  • Single-learner SQLite session log + per-session cost logging
  • End-of-session text+voice coaching debrief (deepseek-v4-flash:cloud, no-think mode)
  • React + WebRTC client via Pipecat client SDK
  • R1-R4 latency spike (the single biggest v0.1 technical risk — RESEARCH.md directive)

Scope (out — deferred per PROJECT.md)

  • Mastery scoring, competency rubrics, credentials
  • Multi-language (Canadian English only)
  • Employer dashboard, Live Assist, WhatsApp/USSD
  • Multi-learner / auth / multi-tenant
  • Active failure-injection provocation (hook present, not provoked — D-009)
  • Multiple personas / voice switching (one voice — D-006)

Risks addressed in this plan

# Risk (from RESEARCH.md) How this plan addresses it
R1 Deepgram first-partial latency from Canada unmeasured SLICE-01 day-1 probe; SLICE-02 integrated measurement
R2 Cartesia first-audio latency unmeasured SLICE-01 probe; SLICE-02 integrated measurement
R3 Ollama Cloud gemma4:cloud first-token latency unmeasured SLICE-01 probe; SLICE-02 integrated measurement
R4 All-cloud three-hop path likely ~670ms (over 600ms) SLICE-01 measures the integrated path; TTS behind interface from SLICE-02; Piper pre-staged as mitigation if R4 confirms. SLICE-01 is the wave-1 go/no-go gate.
R6 Pipecat + Ollama direct-API integration depth unverified SLICE-02 task verifies Pipecat Ollama service accepts custom host + bearer; thin adapter if not
R7 Scenario branch detection (learner signal classification) SLICE-03: LLM-as-judge (deepseek-v4-flash:cloud no-think) at session end, offline from voice loop

Success criteria (Phase 1 exit)

  1. A learner can complete a full session: open client → hear disclaimer → speak to AI customer → AI responds <600ms (target; logged even if exceeded) → reach a branch outcome → receive text+voice debrief → session logged to SQLite.
  2. R1-R4 latency report exists with measured (not vendor-claimed) per-segment and end-to-end numbers; a documented TTS decision (Cartesia vs Piper) justified by data.
  3. All 15 P1 REQ-IDs verified as covered (see §5 coverage matrix).
  4. Per-session cost is logged (token counts + segment latencies + derived cost).
  5. Guardrail layer is pluggable (interface + one Customer Service ruleset implementation) and enforces the v0.1 ruleset (disclaimer, no legal/financial/medical advice, stay-in-role).
  6. Scenario is YAML → Pydantic → Pipecat Flows with failure_mode field present.

2. Vertical Slices

Slices are ordered into 3 waves. Each slice delivers end-to-end value (a demoable behavior), not a horizontal layer. Wave N+1 depends on Wave N output.

SLICE-01 — Component & Integrated Latency Spike (R1-R4)

Wave: 1 REQ-IDs covered: REQ-VOICE-03, REQ-NFR-LAT-01, REQ-LLM-01 (probe), REQ-LLM-02 (probe) Personas: lead-developer, backend-engineer Dependencies: none (first slice) Demoable outcome: A latency report (docs/latency-report.md or reports/latency-spike.md) with measured per-segment and end-to-end numbers, plus a recorded go/no-go decision on TTS (Cartesia cloud vs Piper self-hosted pre-stage). Running make latency-spike (or python scripts/latency_spike.py) reproduces the measurements.

Rationale: RESEARCH.md is explicit: "This is the single biggest v0.1 technical risk and must be spiked in Phase 1 week 1." The all-cloud three-hop path likely lands ~670ms. We measure before building the full loop so SLICE-02 can wire the correct TTS from the start.

Tasks:

Task ID Description Verification
TASK-01-01 Create repo skeleton: server/, client/, scenarios/, db/, guardrails/, llm/, asr/, tts/, scripts/, tests/ dirs; pyproject.toml (server) with pipecat, deepgram, cartesia, piper-tts, ollama, pydantic, aiosqlite deps; .env.example documenting DEEPGRAM_API_KEY, CARTESIA_API_KEY, OLLAMA_API_KEY, PIPECAT_* scopes. python -c "import pipecat" succeeds; dir structure matches PERSONAS.md territory.
TASK-01-02 R1 probe: scripts/probe_deepgram.py — streaming WebSocket to Deepgram Nova-3, send a sample audio file (or synthesized PCM), measure first-partial-transcript latency from a Canada-region endpoint over 20 iterations; log min/median/p95. Running the script prints a latency table; results recorded in latency report.
TASK-01-03 R2 probe: scripts/probe_cartesia.py — WebSocket to Cartesia Sonic, send a sample text chunk, measure first-audio-byte latency over 20 iterations; log min/median/p95. Running the script prints a latency table; results recorded.
TASK-01-04 R3 probe: scripts/probe_ollama.py — direct API call to https://ollama.com/api/chat with OLLAMA_API_KEY bearer, model gemma4:cloud, stream=True, measure time-to-first-token over 20 iterations; also probe deepseek-v4-flash:cloud no-think mode TTFT. Log min/median/p95 + any throttle events (R5). Running the script prints TTFT tables for both models; results recorded.
TASK-01-05 R4 probe: scripts/probe_e2e.py — integrated three-hop: feed a sample ASR transcript → Ollama gemma4:cloud streaming → Cartesia TTS streaming; measure end-to-end (transcript-in → first-audio-out). Run 10 iterations. Also measure the same path with Piper self-hosted (if Piper can be stood up locally in this task; otherwise note as pending and pre-stage in SLICE-02). Running the script prints the integrated e2e latency; recorded in report.
TASK-01-06 Write docs/latency-report.md: per-segment measured latencies (R1-R4), integrated e2e, comparison vs the 600ms budget, and a TTS decision (Cartesia cloud vs Piper pre-stage) with rationale. If e2e >600ms with Cartesia, document Piper as the production v0.1 TTS and note pre-staging work for SLICE-02. Report file exists with measured numbers (not vendor claims) and a decision block.

Must-have verification criteria:

  • scripts/probe_deepgram.py, probe_cartesia.py, probe_ollama.py, probe_e2e.py all run and produce measured latency output.
  • docs/latency-report.md contains real measured numbers for R1, R2, R3, R4 (not vendor claims).
  • Report contains an explicit TTS decision (Cartesia vs Piper) justified by the R4 integrated measurement.
  • If R4 integrated path >600ms, Piper pre-staging is documented as a SLICE-02 task.

SLICE-02 — Thin Vertical Voice Loop (Walking Skeleton)

Wave: 1 REQ-IDs covered: REQ-VOICE-01, REQ-VOICE-02, REQ-VOICE-03, REQ-VOICE-04, REQ-ORCH-01, REQ-LLM-01, REQ-NFR-LAT-01 Personas: lead-developer, backend-engineer, frontend-engineer Dependencies: SLICE-01 (uses the TTS decision; latency budget confirmed feasible) Demoable outcome: A learner opens a minimal React page, clicks "Start", speaks one utterance, and hears the AI reply over WebRTC — end-to-end voice loop works, latency is displayed. Quality may be poor (hardcoded single-turn scenario, no branching, stub guardrail). This is the walking skeleton that makes latency measurable on the real integrated path.

Rationale: The first integrated slice must be minimal but complete (client → server → ASR → LLM → TTS → client) so we measure real latency, not probe latency. All swappable services (TTS D-014, LLM D-020, guardrail D-019) sit behind interfaces from this first slice so later swaps don't touch the pipeline.

Tasks:

Task ID Description Verification
TASK-02-01 Define service interfaces in server/services/: TTSProvider (async synthesize(text) -> audio_stream, voice_id), LLMProvider (async chat(messages, stream=True) -> token_stream, model), Guardrail (async check(text, context) -> verdict). ABCs/Protocols with type annotations. python -c "from server.services import TTSProvider, LLMProvider, Guardrail" succeeds; interfaces are abstract.
TASK-02-02 Implement CartesiaTTS and PiperTTS adapters behind TTSProvider. Pre-stage Piper self-hosted on the pilot server per SLICE-01 decision (install piper-tts, download one voice model). TTS selection via env var `PRAXIS_TTS=cartesia piper`.
TASK-02-03 Implement OllamaCloudLLM adapter behind LLMProvider — direct API to https://ollama.com/api/chat with bearer auth, stream=True, model param. Verify Pipecat's Ollama LLM service accepts custom host + bearer (R6); if not, wrap with this thin adapter so Pipecat consumes it as a generic LLM service. Adapter unit-tested with a mocked HTTP streaming response; a real call to gemma4:cloud returns a first token (confirms R6).
TASK-02-04 Build Pipecat server pipeline in server/pipeline.py: Silero VAD → Deepgram Nova-3 STT (streaming) → OllamaCloudLLM (gemma4:cloud) → selected TTSProvider → WebRTC output. Wire interruptibility: learner VAD during TTS aborts TTS + yields floor (D-008, Pipecat built-in). Hardcoded single-turn system prompt (no YAML scenario yet). python -m server starts the Pipecat pipeline; a WebSocket/WebRTC connection is accepted; logs show VAD → STT → LLM → TTS frame flow.
TASK-02-05 Build minimal React client in client/ (Vite + React + Pipecat client SDK): one page with "Start session" button, mic permission, WebRTC connect, audio playback, live transcript display (optional), and a latency readout. No branching UI, no debrief. npm run dev serves the client; clicking Start connects WebRTC; speaking produces an AI audio reply in the browser.
TASK-02-06 Add an end-to-end latency probe to the pipeline: timestamp at final-transcript-ready, LLM-first-token, TTS-first-audio, client-playback-start; log to console and surface the ASR→TTS-first-audio number to the client for display. The client displays a latency number after the first turn; logged numbers match probe_e2e.py within tolerance.
TASK-02-07 Stub guardrail: NoOpGuardrail implementing Guardrail (always returns allow) so the pipeline has the pluggable hook in place. Real ruleset comes in SLICE-03. Pipeline calls guardrail.check() on each turn; swapping to a real impl requires no pipeline change.

Must-have verification criteria:

  • A learner can click Start, speak one utterance, and hear the AI reply in the browser.
  • End-to-end latency (transcript-ready → first-audio) is measured and displayed.
  • TTS is selected via env var; both Cartesia and Piper adapters exist behind the TTSProvider interface.
  • LLM is behind LLMProvider; gemma4:cloud returns tokens via direct API (R6 resolved).
  • Interruptibility works: speaking during AI TTS cuts the AI off (manual test).
  • Guardrail slot exists and is swappable without touching the pipeline.

SLICE-03 — Branching Scenario + Guardrails + Interruptibility

Wave: 2 REQ-IDs covered: REQ-SCEN-01, REQ-SCEN-FMT-01, REQ-ORCH-02, REQ-VOICE-04, REQ-NFR-SAFE-01 Personas: lead-developer, backend-engineer, data-engineer Dependencies: SLICE-02 (voice loop + interfaces exist) Demoable outcome: The AI plays the "angry customer refund" scenario with a real branch point — the learner's approach either resolves (accept) or escalates — and the session-start disclaimer plays. Guardrails enforce the Customer Service ruleset. The scenario is defined in YAML, loaded via Pydantic, and drives Pipecat Flows.

Tasks:

Task ID Description Verification
TASK-03-01 Define Pydantic scenario schema in server/scenarios/schema.py: Scenario (id, path, market, language, title, difficulty, failure_mode, persona, setup, success_criteria, common_mistakes, branches[], debrief) matching the RESEARCH.md example. Branch has id, trigger.learner_signals, outcome, failure_mode (optional), debrief_focus. Validate at load time. Unit tests: a valid YAML parses; an invalid YAML raises a typed Pydantic error.
TASK-03-02 Author scenarios/customer_service_refund_ca_v01.yaml per D-010 and the RESEARCH.md example: "Angry customer requesting refund on damaged product", one branch point (accept_resolution vs escalate), failure_mode: escalates_unresolved present, success criteria, common mistakes, debrief config (model deepseek-v4-flash:cloud, mode no_think). python -c "from server.scenarios.loader import load; load('customer_service_refund_ca_v01')" returns a valid Scenario object with both branches.
TASK-03-03 Integrate Pipecat Flows: map the scenario branches to a Flows state machine. The system prompt is built from setup.system_prompt; opening line from setup.opening_line is the first TTS utterance. Branch transition logic is driven by learner-signal classification (TASK-03-06). Pipeline runs the scenario: AI speaks the opening line, then converses; reaching a branch transitions to the branch outcome.
TASK-03-04 Implement CustomerServiceGuardrail behind the Guardrail interface (D-019): system-prompt constraints (no legal/financial/medical advice, no real-company impersonation, stay-in-role, concise-for-voice), debrief output filter (block recommendations that learner advise legal action), session-start disclaimer audio ("This is an AI practice session for training purposes. It is not a real conversation and no real company is involved."). Wire into pipeline replacing NoOpGuardrail. Unit tests: guardrail flags a "sue them" recommendation; allows a normal coaching line; disclaimer text is defined. Pipeline plays disclaimer as first audio.
TASK-03-05 Verify interruptibility on branching turns: learner can cut the AI mid-utterance during any turn (including the opening line and post-branch turns); AI aborts TTS and yields (D-008). Manual + automated test. Manual test: speaking during AI speech cuts it off; a test script confirms TTS abort event fires on VAD during TTS.
TASK-03-06 Implement branch classifier (R7): at session end (or turn boundary), call deepseek-v4-flash:cloud in no-think mode as LLM-as-judge to classify learner signals into accept_resolution or escalate based on the turn transcripts + the scenario's learner_signals definitions. Offline from the voice loop (not on the latency-critical path). A scripted transcript classified as "empathy + concrete_resolution" → accept; "defensive + policy_first" → escalate.
TASK-03-07 Replace the hardcoded system prompt from SLICE-02 with the scenario-driven prompt from the loaded YAML. The pipeline now starts a session by loading a named scenario. Starting a session with scenario cs_refund_ca_v01 plays the correct opening line and uses the scenario's system prompt.

Must-have verification criteria:

  • Scenario is YAML → Pydantic → Pipecat Flows; failure_mode field is present.
  • One branch point (accept vs escalate) is reachable and changes the session outcome.
  • Session-start disclaimer audio plays as the first AI utterance.
  • CustomerServiceGuardrail is plugged into the Guardrail interface (no pipeline change) and enforces the ruleset (unit-tested).
  • Interruptibility works on all turns (manual + automated).
  • Branch classifier runs offline (not on the voice latency path) and correctly classifies two scripted transcripts.

SLICE-04 — Learner State + Cost Logging

Wave: 2 REQ-IDs covered: REQ-STATE-01, REQ-NFR-COST-01 Personas: lead-developer, backend-engineer, data-engineer Dependencies: SLICE-02 (loop produces turns to log), SLICE-03 (scenario produces branch outcome to log) Demoable outcome: After a session, praxis.db contains the session row with branch path and outcome, all turns with ASR/TTS text and per-turn latency, and a derived cost row. sqlite3 praxis.db "SELECT * FROM sessions" shows the last session.

Tasks:

Task ID Description Verification
TASK-04-01 Create SQLite schema in db/schema.sql + migrations (db/migrations/0001_init.sql): learner(id, display_name, created_at) with one hardcoded row (learner-1, "Alex"); sessions(id, learner_id, scenario_id, started_at, ended_at, branch_path_json, outcome, cost_estimated_cents); turns(id, session_id, seq, role, asr_text, tts_text, latency_ms, created_at); progress(learner_id, scenario_id, attempts, last_outcome, updated_at). Use aiosqlite for async access. Migration runs; sqlite3 praxis.db ".schema" shows all 4 tables; the hardcoded learner row exists.
TASK-04-02 Implement db/store.py async access layer: start_session(learner_id, scenario_id), log_turn(session_id, seq, role, asr_text, tts_text, latency_ms), end_session(session_id, branch_path, outcome, cost_cents), update_progress(learner_id, scenario_id, outcome). Type-annotated, returns typed objects. Unit tests with a temp DB: start session → log 3 turns → end session → query returns the full session with turns.
TASK-04-03 Wire the store into the Pipecat pipeline: on session start (create row), per turn (log turn with latency), on branch decision (update branch_path), on session end (set outcome + update progress). No auth — learner_id is the hardcoded learner-1. After a manual session, SELECT * FROM sessions and SELECT * FROM turns show the session and its turns.
TASK-04-04 Implement cost logging (REQ-NFR-COST-01, D-012): per session, count LLM input/output tokens (gemma4 + deepseek-v4-flash), Deepgram audio minutes, Cartesia/Piper characters; derive an estimated cost in cents using a cost_rates.yaml config (no enforced ceiling). Store in sessions.cost_estimated_cents. After a session, SELECT cost_estimated_cents FROM sessions returns a non-null number; a cost_breakdown is logged (token counts, minutes, chars).

Must-have verification criteria:

  • SQLite praxis.db exists with learner, sessions, turns, progress tables.
  • One hardcoded learner row exists (no auth).
  • A completed session produces a sessions row + turns rows + a progress update.
  • cost_estimated_cents is non-null for a completed session and backed by a logged breakdown.

SLICE-05 — Coaching Debrief + Full Client UX

Wave: 3 REQ-IDs covered: REQ-DEBRIEF-01, REQ-LLM-02, REQ-NFR-SAFE-01 (debrief filter) Personas: lead-developer, backend-engineer, frontend-engineer Dependencies: SLICE-03 (branch outcome + scenario debrief config), SLICE-04 (session logged with turns) Demoable outcome: At session end, the learner sees a text coaching debrief and hears a voice version, both generated from their actual turns + branch outcome + the scenario's debrief_focus. The React client shows a polished session flow: start → live turn indicators → interrupt feedback → end debrief view (text + audio playback + latency summary).

Tasks:

Task ID Description Verification
TASK-05-01 Implement debrief generation in server/debrief.py: on session end, load the session turns + branch outcome + scenario debrief.debrief_focus, call deepseek-v4-flash:cloud in no-think mode (per D-020 / scenario config) with the debrief prompt template. Produce a concise text summary (what you did well / what to improve / one next step). A scripted session (turns + outcome=escalate) produces a debrief text that references the learner's actual turns and the escalates_unresolved focus.
TASK-05-02 Route the debrief text through CustomerServiceGuardrail output filter (block legal-action recommendations, keep focus on learner performance). Unit test: a debrief containing "tell the customer to sue" is filtered/blocked; a normal coaching debrief passes.
TASK-05-03 Synthesize the debrief as voice via the TTSProvider (same voice as the role-play per D-006) and stream to the client over the existing WebRTC connection. At session end, the client receives and plays the debrief audio; the same TTSProvider interface is reused (no new TTS path).
TASK-05-04 Build the full React client session UX: (a) start screen with scenario title + disclaimer acknowledgement, (b) live session view with turn indicators (learner/AI), interrupt feedback (visual on AI-yield), live latency readout, (c) end-of-session debrief view with debrief text + audio replay + latency/cost summary. Replace the SLICE-02 minimal page. A full session flows through all three views; the debrief view shows text + an audio playback control + a latency summary.
TASK-05-05 Wire debrief persistence: store the debrief text + the branch outcome in the session row (extend sessions with debrief_text column via migration 0002_debrief.sql). After a session, SELECT debrief_text FROM sessions WHERE id=? returns the generated debrief.
TASK-05-06 End-to-end verification script (scripts/e2e_smoke.py or tests/test_e2e.py): start session → simulate 2-3 turns → trigger a branch → end session → assert debrief generated, session + turns + cost logged in SQLite, latency < budget (or logged if exceeded). Running the script passes; it asserts DB rows, debrief non-empty, cost non-null.

Must-have verification criteria:

  • At session end, a text coaching debrief is generated referencing the learner's actual turns and branch outcome.
  • The debrief is spoken in the same voice as the role-play (D-006) via the TTSProvider interface.
  • Debrief text passes the guardrail output filter.
  • React client shows a complete session flow: start → live → debrief views.
  • deepseek-v4-flash:cloud no-think mode is used for the debrief (REQ-LLM-02).
  • End-to-end smoke test passes (session → turns → branch → debrief → DB logged).

3. Wave Ordering

Wave 1 (foundation + risk spike — must pass before Wave 2)
├── SLICE-01  Latency spike (R1-R4)           [lead-developer, backend-engineer]
└── SLICE-02  Thin vertical voice loop         [lead-developer, backend-engineer, frontend-engineer]
   ↑ depends on SLICE-01 TTS decision

Wave 2 (scenario + state — builds on verified loop)
├── SLICE-03  Branching scenario + guardrails  [lead-developer, backend-engineer, data-engineer]
└── SLICE-04  Learner state + cost logging     [lead-developer, backend-engineer, data-engineer]
   ↑ SLICE-03 and SLICE-04 can run in parallel after Wave 1;
     SLICE-04 wiring benefits from SLICE-03 branch outcome but schema is independent

Wave 3 (debrief + UX — completes the daily loop)
└── SLICE-05  Coaching debrief + full client   [lead-developer, backend-engineer, frontend-engineer]
   ↑ depends on SLICE-03 (branch outcome + debrief config) and SLICE-04 (session turns logged)

Wave 1 gate: SLICE-01 produces the latency report + TTS decision. If R4 confirms e2e >600ms with Cartesia, Piper pre-staging becomes a SLICE-02 task before the loop is wired. Wave 2 does not start until the walking skeleton (SLICE-02) demonstrates a working end-to-end voice turn with measured latency.

Wave 2 parallelism: SLICE-03 (scenario + guardrails) and SLICE-04 (SQLite state) are largely independent — the schema is authored from REQUIREMENTS, not from scenario runtime. They can proceed in parallel; SLICE-04's pipeline wiring consumes SLICE-03's branch outcome, so the final wiring task in SLICE-04 depends on SLICE-03's branch classifier. In practice, start both, merge the wiring last.

Wave 3 gate: SLICE-05 requires both SLICE-03 (branch outcome + debrief config) and SLICE-04 (logged turns) to be verified.


4. Phase 1 Exit Criteria

All must be true for Phase 1 to ship:

  1. Full session works end-to-end: A learner opens the React client, hears the disclaimer, speaks to the AI customer (refund scenario), the AI responds, the conversation reaches a branch outcome (accept or escalate), the learner receives a text+voice coaching debrief, and the session is logged to praxis.db.
  2. Latency is measured, not assumed: docs/latency-report.md exists with real R1-R4 numbers. End-to-end latency is logged per session (even if >600ms — the target, with Piper mitigation if needed).
  3. TTS is behind an interface and swappable: PRAXIS_TTS=cartesia|piper selects the provider with no pipeline change (D-014).
  4. LLM is behind an interface and swappable: LLMProvider wraps Ollama Cloud direct API; gemma4:cloud (role-play) and deepseek-v4-flash:cloud no-think (debrief) both callable (D-020, REQ-LLM-01, REQ-LLM-02).
  5. Guardrail layer is pluggable: Guardrail interface + CustomerServiceGuardrail implementation; disclaimer plays; ruleset unit-tested (D-019, REQ-NFR-SAFE-01).
  6. Scenario is YAML → Pydantic → Pipecat Flows: customer_service_refund_ca_v01.yaml loads, validates, drives the branching runtime, and carries the failure_mode field (D-018, REQ-SCEN-FMT-01, REQ-SCEN-01).
  7. Interruptibility works: Learner speech cuts AI TTS mid-utterance; AI yields (D-008, REQ-VOICE-04).
  8. Learner state persists: SQLite has session + turns + progress + cost; single hardcoded learner, no auth (D-007, REQ-STATE-01).
  9. Cost is logged per session: cost_estimated_cents non-null with a logged breakdown (REQ-NFR-COST-01, D-012 — no enforced ceiling).
  10. End-to-end smoke test passes: tests/test_e2e.py (or scripts/e2e_smoke.py) verifies the full loop including DB assertions.

5. REQ Coverage Matrix

Every P1 must/principle REQ-ID mapped to at least one slice.

REQ-ID Priority Slice(s) Covered by task(s)
REQ-VOICE-01 must SLICE-02 TASK-02-04 (Deepgram Nova-3 streaming ASR in pipeline)
REQ-VOICE-02 must SLICE-02 TASK-02-02, TASK-02-04 (TTS behind interface, one voice, Cartesia/Piper)
REQ-VOICE-03 must SLICE-01, SLICE-02 TASK-01-05, TASK-02-06 (measured e2e latency)
REQ-VOICE-04 must SLICE-02, SLICE-03 TASK-02-04, TASK-03-05 (interruptibility, abort-and-yield)
REQ-SCEN-01 must SLICE-03 TASK-03-02, TASK-03-03 (refund scenario, one branch, failure_mode)
REQ-STATE-01 must SLICE-04 TASK-04-01..04-03 (SQLite, single learner, session log)
REQ-LLM-01 must SLICE-01, SLICE-02 TASK-01-04, TASK-02-03 (gemma4:cloud direct API callable)
REQ-LLM-02 must SLICE-01, SLICE-05 TASK-01-04, TASK-05-01 (deepseek-v4-flash:cloud no-think for debrief)
REQ-DEBRIEF-01 must SLICE-05 TASK-05-01..05-03 (end-of-session text+voice summary)
REQ-ORCH-01 must SLICE-02 TASK-02-04 (Pipecat + Silero VAD + interruptibility)
REQ-ORCH-02 must SLICE-03 TASK-03-04 (pluggable guardrail + Customer Service ruleset)
REQ-SCEN-FMT-01 must SLICE-03 TASK-03-01, TASK-03-02 (YAML DSL → Pydantic → Pipecat Flows)
REQ-NFR-LAT-01 must SLICE-01, SLICE-02 TASK-01-05, TASK-02-06 (<600ms measured + logged)
REQ-NFR-SAFE-01 must (baseline) SLICE-03, SLICE-05 TASK-03-04, TASK-05-02 (guardrails + disclaimer + debrief filter)
REQ-NFR-COST-01 must (logging) SLICE-04 TASK-04-04 (per-session cost logged, no enforced ceiling)

Coverage: 15/15 P1 REQ-IDs mapped. No P1 REQ is uncovered.


Planning Decisions

ID Decision Rationale Confidence Alternatives
D-P1-01 5 slices across 3 waves Wave 1 = risk spike + walking skeleton (2 slices); Wave 2 = scenario + state (2 slices, parallelizable); Wave 3 = debrief + UX (1 slice). Balances risk-front-loading with vertical-slice discipline. 0.85 4 slices (merge state into scenario), 6 slices (split client UX from debrief)
D-P1-02 SLICE-01 is a standalone probe slice before SLICE-02 RESEARCH.md mandates R1-R4 be spiked in week 1. Standalone probes are cheaper/faster than building the full loop first, and the TTS decision (R4) informs SLICE-02 wiring. 0.90 Fold probes into SLICE-02 (delays the go/no-go; risks building on the wrong TTS)
D-P1-03 SLICE-02 is a thin walking skeleton (hardcoded single-turn, no branching) Measures integrated latency on the real path before investing in scenario runtime. Quality is deliberately poor; completeness over polish. 0.85 Build the full branching loop directly (couples latency validation to scenario complexity)
D-P1-04 SLICE-03 and SLICE-04 run in parallel in Wave 2 The SQLite schema is authored from REQUIREMENTS, not from scenario runtime; only the final wiring task depends on the branch classifier. Parallelism shortens Wave 2. 0.75 Strict sequence (slower, no benefit)
D-P1-05 Branch classifier (R7) uses LLM-as-judge offline at session end Keeps the latency-critical voice loop free of a second LLM call. deepseek-v4-flash:cloud no-think is cheap and fast enough for a one-shot end-of-session classification. 0.80 Rule-based classifier (brittle), inline per-turn classifier (adds latency)
D-P1-06 Debrief reuses the same TTSProvider (one voice, D-006) D-006 mandates one voice persona for both role-play and mentor. No second TTS config; the debrief is just another TTS utterance via the same interface. 0.90 Separate mentor voice (violates D-006, adds config risk)

End of Phase 1 plan. Next step: orchestrator reviews, optionally grills (GRILL stage), then proceeds to EXECUTE on branch phase/01-minimal-voice-loop.