Files
nextcraft/.ciagent/PLAN.md
T
CIAgent 88a1dab810 docs(milestone): complete v0.2-ai-tutor-architecture
---ci---
phase: 7
milestone: v0.2
status: complete
requirements:
  covered: [REQ-2-001, REQ-2-002, REQ-2-003, REQ-2-004, REQ-2-005, REQ-2-006, REQ-2-007, REQ-2-008, REQ-2-009, REQ-2-010, REQ-2-011, REQ-2-012]
  partial: []
---/ci---

Milestone v0.2 (ai-tutor-architecture) merged to main.

Escalation record (audit remediation, durable): P1 executor
delegation failed twice (empty subagent results, zero files
created); auto-resolved at full autonomy to inline execution with
identical plan fidelity (commit 3271373, reflog-only after phase
branch squash-delete).
2026-09-11 17:34:50 +00:00

422 lines
35 KiB
Markdown

# Nextcraft v0.2 — PLAN.md
## Overview
This plan covers execution phases 1-6 of milestone v0.2 (AI Tutor Architecture): the six AI tutor agents (Coach, Tutor, Lab, Assessor, Proctor, Mentor) as real LLM-backed services in a new `apps/ai-service` Python FastAPI application, wired into the existing v0.1 learner surface with streaming responses. Phases are strictly sequential (P1→P6); within each phase, Wave 1 tasks are parallelizable (no cross-file dependencies) and later waves depend on earlier ones.
**Environment facts (apply throughout):** Python via `python3 -m venv` (no system pip, no uv); pnpm 12.3.4 via corepack; ai-service port **8420**; default model `gemma4:31b` (config via `AI_TUTOR_MODEL`); ollama-cloud base `https://ollama.com/v1` (OpenAI-compatible, Bearer auth); API keys live only in gitignored `.ciagent/.env.secrets`, exported by `scripts/dev.sh` — never in code, commits, or logs; all automated tests use the deterministic mock provider and **never call the cloud**.
| Phase | Name | Requirements | Waves | Personas |
|-------|------|-------------|-------|----------|
| 1 | AI service scaffolding | REQ-2-001, 002, 003 | 3 | backend-engineer, ai-engineer |
| 2 | Agent framework | REQ-2-004 | 2 | ai-engineer, backend-engineer |
| 3 | Coach + Tutor agents | REQ-2-005, 006 | 3 | ai-engineer, backend-engineer |
| 4 | Lab + Assessor agents | REQ-2-007, 008 | 4 | ai-engineer, backend-engineer, data-engineer |
| 5 | Proctor + Mentor agents | REQ-2-009, 010 | 3 | ai-engineer, backend-engineer |
| 6 | Learner surface integration | REQ-2-011, 012 | 3 | frontend-engineer, design-system-engineer |
---
## Phase 1: AI Service Scaffolding
**Requirements:** REQ-2-001, REQ-2-002, REQ-2-003
**Goal:** apps/ai-service runs under uvicorn, /health responds, provider-agnostic LLM layer with ollama-cloud/local/mock providers, SSE chat streaming verified, pytest suite green with mock provider, turbo integration wired
### Wave 1: Service shell + LLM core (parallel — no shared files)
#### Task 1-1-01: FastAPI app scaffolding
- **Persona:** backend-engineer — **REQ:** REQ-2-001
- **Files:** `apps/ai-service/pyproject.toml`, `apps/ai-service/ai_service/main.py`, `apps/ai-service/ai_service/config.py`, `apps/ai-service/ai_service/__init__.py`, `apps/ai-service/tests/conftest.py`, `apps/ai-service/tests/test_health.py`, `apps/ai-service/.env.example`, `apps/ai-service/README.md`
- **Action:** pydantic-settings `Settings` (env_prefix `AI_`, env_file, `SecretStr` key, port 8420, provider select, `AI_TUTOR_MODEL` default `gemma4:31b`). FastAPI app factory in `main.py` with lifespan stub (httpx client pool comes in Wave 2), CORS localhost-only (A-008), `GET /health`. pyproject with pinned deps (fastapi, uvicorn, pydantic, pydantic-settings, httpx, sse-starlette, pytest, pytest-asyncio) and dev extra. conftest: settings override + TestClient fixture. `.env.example` documents all `AI_*` vars; README documents venv setup and dev workflow.
- **Verify:** `scripts/bootstrap.sh && scripts/test.sh` — test_health passes; `curl localhost:8420/health` returns 200
#### Task 1-1-02: LLM types, protocol, mock provider
- **Persona:** ai-engineer — **REQ:** REQ-2-002
- **Files:** `apps/ai-service/ai_service/llm/types.py`, `apps/ai-service/ai_service/llm/base.py`, `apps/ai-service/ai_service/llm/mock.py`, `apps/ai-service/ai_service/llm/__init__.py`
- **Action:** `types.py`: pydantic `Message` (role/content), `ChatDelta` (OpenAI-compatible chunk shape). `base.py`: `LLMProvider` protocol — async `stream_chat(messages, model, response_format=None) -> AsyncIterator[ChatDelta]`; the provider is a dumb pipe, no envelope logic (D-016 keeps envelope in API layer). `mock.py`: deterministic scripted provider (hash-seeded token streams, scripted failure modes: connect error, mid-stream error, malformed JSON) for tests and CI.
- **Verify:** mock provider importable and deterministic; two identical calls yield identical streams
#### Task 1-1-03: Monorepo integration (shim + turbo + scripts)
- **Persona:** backend-engineer — **REQ:** REQ-2-001
- **Files:** `apps/ai-service/package.json`, `apps/ai-service/scripts/bootstrap.sh`, `apps/ai-service/scripts/dev.sh`, `apps/ai-service/scripts/test.sh`, `turbo.json` (update), `package.json` (root, update)
- **Action:** zero-dependency shim `package.json` in apps/ai-service with `dev`/`test`/`bootstrap` script entries. Turbo passthrough tasks `ai#dev`, `ai#test`, `ai#bootstrap` (`cache: false`, `outputs: []`). Root scripts `ai:dev`, `ai:test`, `ai:bootstrap`. `bootstrap.sh`: idempotent `python3 -m venv .venv` + pip install. `dev.sh`: exports keys from `.ciagent/.env.secrets` → uvicorn on 8420. `test.sh`: pytest via venv.
- **Verify:** `corepack pnpm install && pnpm ai:bootstrap && pnpm ai:test` runs pytest through turbo; re-running bootstrap is a no-op
#### Task 1-1-04: Python lint (ruff, check-only)
- **Persona:** backend-engineer — **REQ:** REQ-2-001
- **Files:** `apps/ai-service/pyproject.toml` (update: `[tool.ruff]` config + `ruff` in dev extra), `apps/ai-service/scripts/lint.sh`, `package.json` (root, update), `turbo.json` (update)
- **Action:** Add `ruff` (check-only, no formatter) to the dev extra; `[tool.ruff]` with line-length 100, target py311. `scripts/lint.sh`: `.venv/bin/ruff check .` with repo-root path resolution via script-relative dirname (not CWD). Root script `ai:lint`, turbo passthrough `ai#lint` (`cache: false`, `outputs: []`). Run over the entire ai-service tree; fix all findings before phase ship (G-3).
- **Verify:** `pnpm ai:lint` exits 0 on the Phase 1 codebase
### Wave 2: Real providers + SSE endpoint (depends on Wave 1)
#### Task 1-2-01: OpenAI-compatible provider + factory
- **Persona:** ai-engineer — **REQ:** REQ-2-002
- **Files:** `apps/ai-service/ai_service/llm/openai_compat.py`, `apps/ai-service/ai_service/llm/factory.py`
- **Action:** `openai_compat.py`: single `OpenAICompatProvider` for ollama-cloud (`https://ollama.com/v1`, Bearer) and local endpoints (base URL from settings); raw httpx against `/v1/chat/completions` with `stream: true`, byte-identical delta passthrough, tolerant of ollama-cloud quirks. Uses the lifespan-managed `httpx.AsyncClient` (10s connect / 300s read, D-017) — no openai SDK. `factory.py`: select provider from settings (`ollama-cloud` | `local` | `mock`).
- **Verify:** provider constructs from settings for all 3 names; manual probe against ollama-cloud streams tokens (documented in README, not a test)
#### Task 1-2-02: Lifespan wiring + SSE chat endpoint
- **Persona:** backend-engineer — **REQ:** REQ-2-003
- **Files:** `apps/ai-service/ai_service/main.py` (update), `apps/ai-service/ai_service/api/deps.py`, `apps/ai-service/ai_service/api/chat.py`, `apps/ai-service/ai_service/api/__init__.py`
- **Action:** Lifespan creates the shared `httpx.AsyncClient` and provider factory; deps.py provides provider via DI. `POST /v1/chat/stream` in chat.py implements the D-016 envelope: `meta` event (agent/session/model) flushed before first token → raw OpenAI chunks passed through as `data: {json}``done` event → `error` event before `[DONE]` on mid-stream failure; pre-first-byte failures return proper HTTP status codes. Headers `Cache-Control: no-cache`, `X-Accel-Buffering: no`; sse-starlette ping keep-alive.
- **Verify:** `curl -N -X POST localhost:8420/v1/chat/stream` with mock provider shows meta event, token deltas, done, `[DONE]`
### Wave 3: Provider + endpoint test suites (depends on Wave 2)
#### Task 1-3-01: LLM provider tests (byte-exact, no cloud)
- **Persona:** ai-engineer — **REQ:** REQ-2-002
- **Files:** `apps/ai-service/tests/llm/test_openai_compat.py`, `apps/ai-service/tests/llm/test_mock.py`, `apps/ai-service/tests/llm/__init__.py`
- **Action:** httpx `MockTransport` tests parsing byte-exact fixture streams (happy path, empty delta, `[DONE]`, malformed line, mid-stream disconnect). Mock provider tests: determinism, scripted failure modes, response_format echo.
- **Verify:** `pnpm ai:test` — llm suite green; zero network calls in tests
#### Task 1-3-02: SSE stream endpoint tests
- **Persona:** backend-engineer — **REQ:** REQ-2-003
- **Files:** `apps/ai-service/tests/api/test_chat_stream.py`, `apps/ai-service/tests/api/__init__.py`
- **Action:** TestClient `client.stream()` tests: meta-first ordering, delta passthrough, done + `[DONE]` sentinel, mid-stream error event, pre-first-byte failure → HTTP status, required headers. pytest-asyncio auto mode (D-023).
- **Verify:** `pnpm ai:test` — api suite green
### Must-Haves (Phase 1)
- [ ] `scripts/bootstrap.sh` is idempotent; creates venv + installs deps without system pip
- [ ] `pnpm ai:dev` starts uvicorn; `curl localhost:8420/health` returns 200
- [ ] `pnpm ai:lint` exits 0 (ruff check over the ai-service tree) (G-3)
- [ ] `pnpm ai:test` runs the full pytest suite via turbo and passes (mock provider only — no network)
- [ ] SSE stream delivers tokens: meta event, incremental deltas, done, `[DONE]` observed via `curl -N`
- [ ] Mid-stream failure emits `error` event before `[DONE]`; pre-first-byte failure returns HTTP error status
- [ ] Provider factory resolves ollama-cloud / local / mock from settings; manual ollama-cloud probe documented in README
- [ ] `llm/` imports nothing from `agents/` or `api/` (boundary rule holds)
---
## Phase 2: Agent Framework
**Requirements:** REQ-2-004
**Goal:** Shared framework all six agents use: BaseAgent contract, session store, prompt library, registry, structured outputs — all tested against the mock provider
### Wave 1: Framework primitives (parallel — no shared files)
#### Task 2-1-01: BaseAgent ABC
- **Persona:** ai-engineer — **REQ:** REQ-2-004
- **Files:** `apps/ai-service/ai_service/agents/base.py`, `apps/ai-service/tests/agents/test_base.py`, `apps/ai-service/ai_service/agents/__init__.py`, `apps/ai-service/tests/agents/__init__.py`
- **Action:** `BaseAgent` ABC (D-018): `name`, `system_prompt`, `build_messages(history, learner_context)`, `stream_reply(...) -> AsyncIterator[ChatDelta]` (delegates to provider), `structured_reply(...)` (delegates to structured module, landed Wave 2). Subclass contract tested with a stub agent + mock provider.
- **Verify:** `pnpm ai:test` — test_base green
#### Task 2-1-02: SessionStore protocol + in-memory implementation
- **Persona:** ai-engineer — **REQ:** REQ-2-004
- **Files:** `apps/ai-service/ai_service/agents/session.py`, `apps/ai-service/tests/test_session.py`
- **Action:** `SessionStore` protocol + `InMemorySessionStore` (D-019): asyncio.Lock-guarded dict, agent-scoped session keys, 20-message rolling window, 500-cap LRU eviction. Protocol shape is DB-migration-ready (A-003).
- **Verify:** test_session covers create/append/window-trim/LRU-eviction/agent scoping
#### Task 2-1-03: Prompt library scaffolding
- **Persona:** ai-engineer — **REQ:** REQ-2-004
- **Files:** `apps/ai-service/ai_service/prompts/coach.py`, `apps/ai-service/ai_service/prompts/tutor.py`, `apps/ai-service/ai_service/prompts/lab.py`, `apps/ai-service/ai_service/prompts/assessor.py`, `apps/ai-service/ai_service/prompts/proctor.py`, `apps/ai-service/ai_service/prompts/mentor.py`, `apps/ai-service/ai_service/prompts/__init__.py`
- **Action:** Per-agent module with a versioned `SYSTEM_PROMPT` constant + `render_context(learner_context) -> dict` using `str.format_map` for learner-context injection (D-018: prompts are code, versioned in git). Initial drafts for all six; final personas land in Phases 3-5.
- **Verify:** all six prompt modules import; render_context fills placeholders without KeyError
#### Task 2-1-04: Learner context corpus
- **Persona:** ai-engineer — **REQ:** REQ-2-004
- **Files:** `apps/ai-service/ai_service/corpus/learner_context.py`, `apps/ai-service/ai_service/corpus/__init__.py`
- **Action:** Pydantic-typed learner context (active stack, competencies, progress, recent artifacts) mirroring TS `packages/mock-data` IDs per D-021 convention (cross-referencing header comment, identical `comp-*`/`stack-*` ID strings).
- **Verify:** context renders into prompt placeholders; IDs match packages/mock-data strings
### Wave 2: Composition layers (depends on Wave 1)
#### Task 2-2-01: Structured output defense
- **Persona:** ai-engineer — **REQ:** REQ-2-004
- **Files:** `apps/ai-service/ai_service/agents/structured.py`, `apps/ai-service/tests/test_structured.py`
- **Action:** 4-layer defense (D-020): (1) `response_format` request with auto-degrade on provider 400; (2) prompt-embedded JSON schema; (3) parse: strip code fences → first balanced JSON object; (4) single bounded retry with validation-error feedback. Returns pydantic-validated model or raises `StructuredOutputError`.
- **Verify:** test_structured covers fenced/unfenced/invalid JSON, retry path, degrade path — all against mock provider
#### Task 2-2-02: Agent registry
- **Persona:** ai-engineer — **REQ:** REQ-2-004
- **Files:** `apps/ai-service/ai_service/agents/registry.py`, `apps/ai-service/tests/test_registry.py`
- **Action:** Explicit registry: name → agent factory map with `register(name, factory)` / `get(name)`; raises on unknown agent. Agents are registered in their own phases (P3-P5).
- **Verify:** test_registry: register/get round-trip, unknown-agent error, duplicate registration error
#### Task 2-2-03: Session + agent DI wiring into API layer
- **Persona:** backend-engineer — **REQ:** REQ-2-004
- **Files:** `apps/ai-service/ai_service/api/deps.py` (update), `apps/ai-service/ai_service/api/chat.py` (update)
- **Action:** deps.py exposes SessionStore and provider singletons via DI. chat.py persists turn history through the session store (agent-scoped) and includes session ID in the meta event. API composes agents via DI — agents never import api/.
- **Verify:** chat request appends to and replays windowed history; `pnpm ai:test` green
### Must-Haves (Phase 2)
- [ ] BaseAgent unit tests pass (stub agent streams via mock provider)
- [ ] Session store tested: create/append, 20-message window trim, 500-cap LRU eviction, agent-scoped keys
- [ ] Structured output parsing tested against mock provider: fence-strip, first-balanced-object, invalid JSON, one bounded retry, response_format auto-degrade
- [ ] Registry tested: register/get/unknown/duplicate
- [ ] Six prompt modules render learner context without errors
- [ ] Module boundaries hold: `agents/` never imports `api/`; `llm/` never imports `agents/` or `api/`
---
## Phase 3: Coach + Tutor Agents
**Requirements:** REQ-2-005, REQ-2-006
**Goal:** Both learner-facing conversational agents fully implemented with distinct personas, registered, routed through the chat streaming endpoint
### Wave 1: Agent implementations (parallel — no shared files)
#### Task 3-1-01: Coach agent
- **Persona:** ai-engineer — **REQ:** REQ-2-005
- **Files:** `apps/ai-service/ai_service/prompts/coach.py` (finalize), `apps/ai-service/ai_service/agents/coach.py`, `apps/ai-service/tests/test_coach.py`
- **Action:** Final Coach persona: pacing guidance, motivation, retrieval practice prompts; system prompt injects learner context (active stack, progress). `CoachAgent(BaseAgent)` streams replies. Mock provider scripts a distinct coach-voice response for tests.
- **Verify:** test_coach: build_messages includes system prompt + windowed history; stream_reply yields deltas; on-persona content asserted against mock script
#### Task 3-1-02: Tutor agent
- **Persona:** ai-engineer — **REQ:** REQ-2-006
- **Files:** `apps/ai-service/ai_service/prompts/tutor.py` (finalize), `apps/ai-service/ai_service/agents/tutor.py`, `apps/ai-service/tests/test_tutor.py`
- **Action:** Final Tutor persona: concept delivery, Socratic questioning, worked examples. `TutorAgent(BaseAgent)` streams replies; mock scripts a distinct tutor-voice response.
- **Verify:** test_tutor mirrors test_coach; Coach and Tutor mock outputs are observably distinct
### Wave 2: Registration + persona verification (depends on Wave 1)
#### Task 3-2-01: Register Coach + Tutor; document cloud probe
- **Persona:** ai-engineer — **REQ:** REQ-2-005, REQ-2-006
- **Files:** `apps/ai-service/ai_service/agents/registry.py` (update), `apps/ai-service/tests/test_registry.py` (update), `apps/ai-service/README.md` (update)
- **Action:** Register both agents in the explicit registry. Extend test_registry to assert both resolve. Document the manual ollama-cloud persona probe in README (curl commands with `AI_PROVIDER=ollama-cloud`): Coach and Tutor produce distinct on-persona responses; tests remain cloud-free.
- **Verify:** `pnpm ai:test` green; manual probe against ollama-cloud shows distinct personas (documented, not automated)
### Wave 3: Chat endpoint agent routing (depends on Wave 2)
#### Task 3-3-01: Agent routing on /v1/chat/stream
- **Persona:** backend-engineer — **REQ:** REQ-2-005, REQ-2-006
- **Files:** `apps/ai-service/ai_service/api/chat.py` (update), `apps/ai-service/ai_service/api/deps.py` (update), `apps/ai-service/tests/api/test_chat_stream.py` (update)
- **Action:** Chat request gains `agent` field (validated against the registry; unknown agent → 422). Endpoint resolves the agent via DI, persists to the agent-scoped session, meta event carries the agent name. No autonomous routing in v0.2 (A-007).
- **Verify:** TestClient tests: `agent=coach` and `agent=tutor` route correctly, session scoped per agent, unknown agent rejected
### Must-Haves (Phase 3)
- [ ] Both agents produce distinct, on-persona responses (mock-asserted; manual ollama-cloud probe documented in README)
- [ ] Agent routing tested: coach/tutor resolve via registry; unknown agent returns 422
- [ ] Both agents exposed end-to-end via `POST /v1/chat/stream` with agent-scoped session history
- [ ] `pnpm ai:test` green; no cloud calls in tests
---
## Phase 4: Lab + Assessor Agents
**Requirements:** REQ-2-007, REQ-2-008
**Goal:** Lab consumes simulated sandbox telemetry and streams in-flow feedback; Assessor applies rubrics to pre-baked artifacts and returns structured scores — both over mock engine inputs
### Wave 1: Mock engine inputs (parallel — no shared files)
#### Task 4-1-01: Simulated sandbox telemetry corpus
- **Persona:** ai-engineer — **REQ:** REQ-2-007
- **Files:** `apps/ai-service/ai_service/corpus/telemetry.py`
- **Action:** Pydantic-typed Lab telemetry scenarios: scripted build-session event streams (keystrokes, commits, test runs, errors, idle gaps) keyed by scenario ID, aligned with packages/mock-data IDs (D-021).
- **Verify:** scenarios import, validate, and are addressable by ID
#### Task 4-1-02: Pre-baked artifacts + rubrics corpus
- **Persona:** ai-engineer — **REQ:** REQ-2-008
- **Files:** `apps/ai-service/ai_service/corpus/artifacts.py`
- **Action:** Pre-baked artifacts (code, design, simulation), assessment rubrics (criteria, levels, weights), and defense transcripts keyed by ID — the Assessor's mock inputs (real engines are v0.3+).
- **Verify:** rubric/artifact/transcript fixtures validate; IDs align with TS mock data
#### Task 4-1-03: TS mock-data alignment for engine inputs
- **Persona:** data-engineer — **REQ:** REQ-2-012
- **Files:** `packages/mock-data/ai-scenarios.ts` (new), `packages/mock-data/index.ts` (update)
- **Action:** Export scenario/artifact ID constants + display metadata used by the Phase 6 learner panels, mirroring `ai_service/corpus/` IDs exactly (D-021). Data-engineer owns the TS side; headers cross-reference the Python corpus.
- **Verify:** `pnpm typecheck` passes; IDs string-equal to corpus IDs
### Wave 2: Agent implementations (depends on Wave 1)
#### Task 4-2-01: Lab agent
- **Persona:** ai-engineer — **REQ:** REQ-2-007
- **Files:** `apps/ai-service/ai_service/prompts/lab.py` (finalize), `apps/ai-service/ai_service/agents/lab.py`, `apps/ai-service/tests/test_lab.py`
- **Action:** `LabAgent(BaseAgent)` consumes a telemetry scenario, builds messages summarizing the event stream, streams concrete in-flow feedback (what happened, what to adjust, next step). No session chat — scenario-driven.
- **Verify:** test_lab: given a mock scenario, feedback references scenario events (mock-scripted assertions)
#### Task 4-2-02: Assessor agent
- **Persona:** ai-engineer — **REQ:** REQ-2-008
- **Files:** `apps/ai-service/ai_service/prompts/assessor.py` (finalize), `apps/ai-service/ai_service/agents/assessor.py`, `apps/ai-service/tests/test_assessor.py`
- **Action:** `AssessorAgent(BaseAgent)` applies a rubric to an artifact + defense transcript via `structured_reply`, returning a pydantic-validated rubric score model (per-criterion scores, strengths, gaps, verdict).
- **Verify:** test_assessor: structured output validates against the rubric model; failure modes exercise the 4-layer defense
### Wave 3: Registration (depends on Wave 2)
#### Task 4-3-01: Register Lab + Assessor
- **Persona:** ai-engineer — **REQ:** REQ-2-007, REQ-2-008
- **Files:** `apps/ai-service/ai_service/agents/registry.py` (update), `apps/ai-service/tests/test_registry.py` (update)
- **Action:** Register both agents; extend registry tests.
- **Verify:** registry resolves coach/tutor/lab/assessor; `pnpm ai:test` green
### Wave 4: Endpoints (depends on Wave 3)
#### Task 4-4-01: Lab feedback endpoint
- **Persona:** backend-engineer — **REQ:** REQ-2-007
- **Files:** `apps/ai-service/ai_service/api/lab.py`, `apps/ai-service/tests/api/test_lab.py`
- **Action:** `POST /v1/lab/feedback` with scenario ID → resolves corpus scenario + Lab agent → SSE stream using the D-016 envelope (meta names agent=lab). Unknown scenario → 404.
- **Verify:** TestClient streams meta + deltas + done + `[DONE]`; unknown scenario 404
#### Task 4-4-02: Assessment evaluate endpoint
- **Persona:** backend-engineer — **REQ:** REQ-2-008
- **Files:** `apps/ai-service/ai_service/api/assessment.py`, `apps/ai-service/tests/api/test_assessment.py`
- **Action:** `POST /v1/assessment/evaluate` with artifact ID → resolves corpus artifact/rubric/transcript + Assessor agent → JSON response (validated rubric score model). Unknown artifact → 404.
- **Verify:** TestClient returns validated rubric JSON; unknown artifact 404; `pnpm ai:test` green
### Must-Haves (Phase 4)
- [ ] Lab produces scenario-relevant in-flow feedback for mock telemetry scenarios (mock provider, tested)
- [ ] Assessor returns structured rubric scores (pydantic-validated JSON) for pre-baked artifacts/transcripts
- [ ] `POST /v1/lab/feedback` streams (meta → deltas → done → `[DONE]`); `POST /v1/assessment/evaluate` returns validated JSON
- [ ] Unknown scenario/artifact IDs return 404
- [ ] Corpus IDs align with packages/mock-data (D-021); `pnpm ai:test` and `pnpm typecheck` green
---
## Phase 5: Proctor + Mentor Agents
**Requirements:** REQ-2-009, REQ-2-010
**Goal:** Proctor classifies integrity signals with coaching interventions from mock telemetry; Mentor generates long-horizon career narrative; both exposed via endpoints
### Wave 1: Proctor scenarios + Mentor agent (parallel — no shared files)
#### Task 5-1-01: Proctor telemetry scenarios
- **Persona:** ai-engineer — **REQ:** REQ-2-009
- **Files:** `apps/ai-service/ai_service/corpus/telemetry.py` (update)
- **Action:** Add proctor scenarios: tab switches, idle time, paste events, focus loss — scripted integrity-relevant event sets keyed by scenario ID.
- **Verify:** proctor scenarios validate; distinguishable from lab scenarios by type
#### Task 5-1-02: Mentor agent (+ registration)
- **Persona:** ai-engineer — **REQ:** REQ-2-010
- **Files:** `apps/ai-service/ai_service/prompts/mentor.py` (finalize), `apps/ai-service/ai_service/agents/mentor.py`, `apps/ai-service/tests/test_mentor.py`, `apps/ai-service/ai_service/agents/registry.py` (update)
- **Action:** `MentorAgent(BaseAgent)`: long-horizon career narrative — trajectory story, competency-stack progression guidance, market positioning — streaming, session-backed. Registered centrally in `registry.py` (single registration pattern, G-4).
- **Verify:** test_mentor: narrative references learner context (mock-scripted); registry resolves mentor
### Wave 2: Proctor agent + Mentor endpoint (depends on Wave 1)
#### Task 5-2-01: Proctor agent (+ registration)
- **Persona:** ai-engineer — **REQ:** REQ-2-009
- **Files:** `apps/ai-service/ai_service/prompts/proctor.py` (finalize), `apps/ai-service/ai_service/agents/proctor.py`, `apps/ai-service/tests/test_proctor.py`, `apps/ai-service/ai_service/agents/registry.py` (update)
- **Action:** `ProctorAgent(BaseAgent)`: consumes proctor scenario → `structured_reply` returns pydantic-validated signal classification (severity, signal type) + recommended coaching intervention (supportive, not punitive). Registered centrally in `registry.py` (single registration pattern, G-4).
- **Verify:** test_proctor: classified signals + interventions validate for each mock scenario; registry resolves all six agents
#### Task 5-2-02: Mentor narrative endpoint
- **Persona:** backend-engineer — **REQ:** REQ-2-010
- **Files:** `apps/ai-service/ai_service/api/mentor.py`, `apps/ai-service/tests/api/test_mentor.py`
- **Action:** `POST /v1/mentor/narrative` → Mentor agent → SSE stream with D-016 envelope, session-backed.
- **Verify:** TestClient streams meta (agent=mentor) → deltas → done → `[DONE]`
### Wave 3: Proctor endpoint (depends on Wave 2)
#### Task 5-3-01: Proctor signals endpoint
- **Persona:** backend-engineer — **REQ:** REQ-2-009
- **Files:** `apps/ai-service/ai_service/api/proctor.py`, `apps/ai-service/tests/api/test_proctor.py`
- **Action:** `POST /v1/proctor/signals` with scenario ID → resolves corpus scenario + Proctor agent → JSON response (validated signals + interventions). Unknown scenario → 404.
- **Verify:** TestClient returns classified signals JSON; `pnpm ai:test` green — full suite (all six agents registered)
### Must-Haves (Phase 5)
- [ ] Proctor produces classified signals with recommended coaching interventions for each mock scenario (structured JSON, validated)
- [ ] Mentor produces coherent long-horizon career narrative (streaming, session-backed)
- [ ] `POST /v1/proctor/signals` returns validated JSON; `POST /v1/mentor/narrative` streams
- [ ] Registry resolves all six agents; full ai-service test suite green, cloud-free
---
## Phase 6: Learner Surface Integration
**Requirements:** REQ-2-011, REQ-2-012
**Goal:** v0.1 learner surfaces wired to the real ai-service: streaming chat with agent switcher, Lab/Assessor/Proctor/Mentor outputs surfaced, error/loading states, build + typecheck green
**Note (G-2):** End-to-end verification may run with `AI_PROVIDER=mock` as a fallback — the requirement is the real ai-service over HTTP (not canned client-side responses); provider choice is service-internal. This prevents an ollama-cloud outage from blocking P6 verification. Cloud persona probes remain separate (Task 3-2-01).
### Wave 1: Client plumbing + primitives (parallel — no shared files)
#### Task 6-1-01: useChatStream hook
- **Persona:** frontend-engineer — **REQ:** REQ-2-011
- **Files:** `apps/web/hooks/use-chat-stream.ts`, `apps/web/.env.example` (update)
- **Action:** `useChatStream(agent)` hook: `fetch` POST to `${NEXT_PUBLIC_AI_SERVICE_URL}/v1/chat/stream` (default `http://localhost:8420`, A-002 — no API-route proxy); consumes `ReadableStream` with byte buffering, frame split on `\n\n`, joined `data:` lines; **ignores frames containing no `data:` lines (sse-starlette `: ping` keep-alive comment frames)** — TestClient streams are too short to surface pings, but real cloud delta gaps emit them (G-1); handles meta / delta / done / error events and `[DONE]` sentinel; idempotent `AbortController.abort()` in effect cleanup; exposes `{messages, isStreaming, error, send, retry, abort}`. `.env.example` gains `NEXT_PUBLIC_AI_SERVICE_URL`.
- **Verify:** hook unit-tested or exercised via the chat UI; unmount mid-stream aborts cleanly (no state updates after unmount)
#### Task 6-1-02: Agent switcher + streaming primitives
- **Persona:** design-system-engineer — **REQ:** REQ-2-011
- **Files:** `packages/ui/src/primitives/agent-switcher.tsx`, `packages/ui/src/primitives/stream-status.tsx`, `packages/ui/src/primitives/toast.tsx`, `packages/ui/src/primitives/index.ts` (update), `packages/ui/src/index.ts` (update)
- **Action:** Token-driven primitives: AgentSwitcher (segmented coach/tutor control with active state), StreamStatus (idle/streaming/error indicator), Toast with error variant. Dark mode + WCAG AA contrast; exported from `@nextcraft/ui`.
- **Verify:** primitives import from `@nextcraft/ui`; storybook stories render (dark + light)
### Wave 2: Chat rewrite + Byte viewer panel (depends on Wave 1)
#### Task 6-2-01: Real streaming learner chat
- **Persona:** frontend-engineer — **REQ:** REQ-2-011
- **Files:** `apps/web/components/learner/ai-tutor-chat.tsx` (rewrite), `apps/web/components/learner/agent-switcher.tsx` (composition wrapper)
- **Action:** Replace the canned `aiTutorResponses` behavior with `useChatStream`: agent switcher (Coach/Tutor per A-007), token-by-token rendering, streaming cursor + loading state, error state with retry button when ai-service is down (A-010), suggested-action chips from the meta event. Seed welcome message stays static.
- **Verify:** with ai-service running, messages stream visibly token-by-token; with ai-service stopped, error state + retry appears (no crash, no console errors)
#### Task 6-2-02: Byte viewer Tutor panel
- **Persona:** frontend-engineer — **REQ:** REQ-2-011 (agent routing: byte viewer always uses Tutor, A-007)
- **Files:** `apps/web/app/(learner)/learn/[competencyId]/page.tsx` (update), `apps/web/components/learner/byte-tutor-panel.tsx` (new)
- **Action:** Byte viewer gains a Tutor explanation panel (byte viewer always uses Tutor, A-007): "Explain this byte" streams a Socratic concept walkthrough for the current competency via useChatStream (agent fixed to tutor).
- **Verify:** on a byte page, the panel streams a Tutor explanation; error state when service down
### Wave 3: Lab / Assessment / Mentor panels (depends on Wave 1 hook; parallel — no shared files)
#### Task 6-3-01: Sandbox Lab feedback panel
- **Persona:** frontend-engineer — **REQ:** REQ-2-012
- **Files:** `apps/web/app/(learner)/build/[competencyId]/page.tsx` (update), `apps/web/components/learner/lab-feedback-panel.tsx` (new)
- **Action:** Sandbox telemetry sidebar gains a Lab feedback panel: posts the scenario ID (from `packages/mock-data/ai-scenarios`) to `/v1/lab/feedback`, streams in-flow feedback into the panel; loading + error states.
- **Verify:** sandbox page streams Lab feedback for the mock scenario; error state when service down
#### Task 6-3-02: Assessment Assessor + Proctor surfaces
- **Persona:** frontend-engineer — **REQ:** REQ-2-012
- **Files:** `apps/web/app/(learner)/defend/[competencyId]/page.tsx` (update), `apps/web/components/learner/assessor-results-panel.tsx` (new), `apps/web/components/learner/proctor-banner.tsx` (new)
- **Action:** Assessment mockup: AI reviewer panel calls `/v1/assessment/evaluate` with the artifact ID and renders the structured rubric scores (per-criterion bars, strengths, gaps, verdict); a Proctor integrity banner surfaces `/v1/proctor/signals` classifications with coaching tone. Loading skeletons + error states.
- **Verify:** defend page renders real Assessor rubric output + Proctor banner; error states when service down
#### Task 6-3-03: Dashboard Mentor panel
- **Persona:** frontend-engineer — **REQ:** REQ-2-012
- **Files:** `apps/web/app/(learner)/dashboard/page.tsx` (update), `apps/web/components/learner/mentor-panel.tsx` (new)
- **Action:** Learner dashboard gains a Mentor panel: streams career narrative from `/v1/mentor/narrative` (learner progress context), with regenerate button, loading + error states. Sits alongside the existing AI tutor chat.
- **Verify:** dashboard shows streaming Mentor narrative; error state when service down
### Must-Haves (Phase 6)
- [ ] With ai-service running: learner chat at http://localhost:3000/dashboard streams real responses token-by-token (mock provider fallback allowed per G-2 — service over HTTP is the requirement)
- [ ] Agent switcher flips Coach ↔ Tutor and the response persona changes accordingly
- [ ] Hook tolerates keep-alive comment frames (`: ping`, no data lines) during live streams (G-1)
- [ ] With ai-service stopped: all chat/panels show error states with retry — no crashes, no unhandled promise rejections, no console errors
- [ ] Byte viewer, sandbox, and assessment mockups surface Tutor/Lab/Assessor/Proctor outputs; dashboard shows Mentor narrative
- [ ] Unmounting/navigating mid-stream aborts cleanly (no post-unmount state updates)
- [ ] `pnpm build` and `pnpm typecheck` pass; `pnpm ai:test` still green
---
## Phase 7: Final Review + Ship (no planned tasks)
Orchestrated by the SHIP stage, not this plan: multi-persona code review (correctness, testing, secrets hygiene — key absent from code/logs/commits/errors, localhost-only CORS, no PII in prompts), project health audit (reconstruction test, .ciagent/ discipline, branch/commit hygiene), then merge milestone → main, tag v0.2.0, create Gitea release, mark all 12 v0.2 requirements complete.
**Release-note honesty (G-5):** the v0.2.0 release note must explicitly state that Lab/Assessor/Proctor operate on mock engine inputs (real engines are v0.3+), per D-015.
**Dead-code disposal (G-5):** review must dispose of `aiTutorResponses` (packages/mock-data/ai-tutor-responses.ts) — its only consumer is rewritten in Task 6-2-01; remove the export or mark it deprecated.
---
## User-Facing Surface
The primary user-facing surface is the **learner dashboard and learning flow** at `http://localhost:3000`, backed by the real ai-service at `http://localhost:8420`:
- `/dashboard` — AI tutor chat (Coach/Tutor switcher, streaming) + Mentor career-narrative panel
- `/learn/[competencyId]` — byte viewer with streaming Tutor explanations
- `/build/[competencyId]` — sandbox with Lab in-flow feedback panel (mock telemetry)
- `/defend/[competencyId]` — assessment with live Assessor rubric scores + Proctor integrity banner
The marketplace, employer, and admin surfaces are unchanged from v0.1.
## Happy Path
1. Learner opens `/dashboard` → chat shows welcome message; meta event confirms coach/model in the stream
2. Learner types "I'm stuck on multi-agent communication" → reply streams token-by-token with pacing guidance + a retrieval-practice prompt
3. Learner switches to **Tutor** → asks the same question → gets a Socratic concept walkthrough instead
4. Learner opens a byte tutorial → Tutor panel streams an explanation of the current competency
5. Learner opens the build sandbox → Lab panel streams feedback on the simulated telemetry scenario
6. Learner opens the defense mockup → Assessor panel shows structured rubric scores; Proctor banner shows integrity signals in coaching tone
7. Back on `/dashboard`, the Mentor panel streams a career narrative tied to the learner's progress
8. Learner kills ai-service (or it crashes) → next message shows an inline error state with **Retry**; restarting the service and retrying resumes streaming
## UX Acceptance Criteria
1. Streaming is visibly incremental — tokens appear as they arrive, not as one blob
2. Agent switcher shows the active agent (Coach/Tutor) and the response persona visibly changes
3. Loading state during connection (streaming cursor / skeleton) before first token
4. When ai-service is unreachable: inline error state + retry action on every chat/panel — no crashes, no console errors, no blank UI
5. `[DONE]` reliably ends the stream (input re-enables, no stuck "typing" state)
6. Navigating away mid-stream aborts cleanly — no leaked requests or post-unmount updates
7. All new UI uses design tokens, supports dark mode, meets WCAG AA contrast
8. Responsive at 375px, 768px, 1280px
9. No hardcoded model names or URLs in UI code — all via `NEXT_PUBLIC_AI_SERVICE_URL` and server meta events
10. `pnpm build` and `pnpm typecheck` pass with zero errors