Vertical-slice plan: 6 execution phases, 20 waves, 39 tasks. P1 sandbox fabric (unshare namespaces, D-024/032) · P2 telemetry (WS+SQLite, D-026/027) P3 trace grading (digest+LLM, D-028) · P4 variants (seeded templates, D-029) P5 voice defense (VoiceProvider, Examiner, D-030) · P6 re-grounding + learner integration. MVP/UX sections included (User-Facing Surface, Happy Path, UX Acceptance Criteria). Must-haves per phase; KYC deferred (A-110); mock-first voice (no blocking key). ---ci--- phase: 0 milestone: v0.3 status: plan ---/ci---
53 KiB
Nextcraft v0.3 — PLAN.md
Overview
This plan covers execution phases 1-6 of milestone v0.3 (Credential Engines): the real engines that replace v0.2's mock inputs — a namespace-isolated sandbox fabric, live build telemetry over WebSocket + SQLite, a process-trace grading engine, seeded per-learner variant task generation, and an oral/voice defense with a seventh Examiner agent — plus re-grounding the Lab/Assessor/Proctor agents onto real engine inputs and wiring the v0.1 learner surfaces to the real build/defense/grading paths. 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 3.11.2 via python3 -m venv (no uv, no system pip); pnpm 12.3.4 via corepack; turborepo; ai-service port 8420; default model gemma4:31b (config via AI_TUTOR_MODEL); ollama-cloud base https://ollama.com/v1 (OpenAI-compatible, Bearer auth); 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 LLM and mock voice providers and never call cloud or voice APIs. New ai-service deps this milestone: sqlmodel, sqlalchemy, websockets, aiofiles (all PyPI-verified; added in Task 1-1-02). New web deps: @xterm/xterm, @xterm/addon-fit (Task 6-1-01). SQLite data (apps/ai-service/ai_service/data/) and sandbox dirs (apps/ai-service/sandboxes/) are already gitignored. KYC/age-gating is deferred per founder directive (A-110) — no identity work appears anywhere in this plan; age-gating stays the v0.1 visual flow mockup.
| Phase | Name | Requirements | Waves | Personas |
|---|---|---|---|---|
| 1 | Sandbox fabric | REQ-3-001, 002 | 3 | sandbox-engineer, backend-engineer, ai-engineer (W1 lint only) |
| 2 | Live build telemetry | REQ-3-003 | 4 | sandbox-engineer, backend-engineer, data-engineer |
| 3 | Process-trace grading engine | REQ-3-004 | 3 | ai-engineer, backend-engineer |
| 4 | Variant task generation | REQ-3-005 | 3 | ai-engineer, backend-engineer, data-engineer |
| 5 | Oral / voice defense | REQ-3-006 | 4 | voice-engineer, ai-engineer, backend-engineer |
| 6 | Agent re-grounding + learner surface integration | REQ-3-007, 008 | 5 | ai-engineer, frontend-engineer, design-system-engineer, data-engineer, backend-engineer, lead-developer |
Phase 1: Sandbox Fabric
Requirements: REQ-3-001, REQ-3-002
Goal: SandboxBackend protocol + unshare-based namespace spawner (D-024) + lifecycle manager with concurrency guard (D-032) + per-sandbox workdir; isolation and resources probe-verified on this box; /v1/sandboxes API live; deps + gitignore landed
Wave 1: Foundations (parallel — no shared files)
Task 1-1-01: SandboxBackend protocol + unshare spawner + probe test
- Persona: sandbox-engineer — REQ: REQ-3-001, REQ-3-002
- Files:
apps/ai-service/ai_service/sandbox/__init__.py,apps/ai-service/ai_service/sandbox/backend.py,apps/ai-service/ai_service/sandbox/workdir.py,apps/ai-service/ai_service/sandbox/unshare_backend.py,apps/ai-service/tests/sandbox/__init__.py,apps/ai-service/tests/sandbox/test_isolation.py - Action:
backend.py:SandboxBackendprotocol +SandboxSpec(sandbox_id, learner_id, workdir, resource limits) +SandboxHandle(id, pid, workdir, created_at);spawn(spec),exec(handle, cmd),snapshot(handle) -> Path,destroy(handle).workdir.py: per-sandbox layout underapps/ai-service/sandboxes/<id>/(workspace/ writable, snapshot() = recursive copy tosnapshots/<ts>/) — no symlinks as the snapshot mechanism.unshare_backend.py: subprocess spawner —unshare --user --map-root-user --mount --pid --fork --netwith the per-sandbox dir bind-mounted (--bind <dir> /work) andchdir /work(D-024); pipes for stdout/stderr; async wrappers.test_isolation.py— re-verify box isolation properties (runs on this box, guarded by probe skip): (a)id -uinside namespace prints0; (b)ip linkinside namespace shows 0 usable interfaces (loopback-only/no carrier) — network isolated; (c) file written to/work/inside.txtlands atsandboxes/<id>/workspace/inside.txton the host; (d) attempt to write outside the mount (e.g. host tmp path via bind) does not escape the per-sandbox dir; (e)/procvisibility degraded (proc-remount not permitted per A-101 — assert the probe documents this, not that it fails). - Verify:
pnpm ai:test—tests/sandbox/test_isolation.pygreen on this box (probe-gated: skips with an explicit reason if userns unavailable);lintclean
Task 1-1-02: v0.3 dependencies + gitignore + config additions
- Persona: backend-engineer — REQ: REQ-3-001
- Files:
apps/ai-service/pyproject.toml(update),apps/ai-service/ai_service/config.py(update),apps/ai-service/.env.example(update),apps/ai-service/scripts/bootstrap.sh(update if needed), rootpackage.json(no change),turbo.json(no change) - Action: Add pinned deps:
sqlmodel,sqlalchemy,websockets,aiofilesto pyproject.config.pyadditions (env_prefixAI_):AI_SANDBOX_DIR(defaultapps/ai-service/sandboxes),AI_SANDBOX_MAX_CONCURRENT(default 5, D-032),AI_SANDBOX_CPU_LIMIT(default 1 core / cpu.max),AI_SANDBOX_MEM_LIMIT_MB(default 512),AI_SANDBOX_PIDS_LIMIT(default 256),AI_SANDBOX_TIMEOUT_S(default 1800),AI_DB_PATH(defaultai_service/data/nextcraft.db),AI_VOICE_BASE_URL/AI_VOICE_API_KEY/AI_VOICE_STT_MODEL/AI_VOICE_TTS_MODEL(all optional, default empty — mock-first, D-030; documented in.env.exampleand README). Confirm.gitignorealready coversai_service/data/+sandboxes/(it does — v0.3 block present). Re-run bootstrap idempotently to install new deps. - Verify:
pnpm ai:bootstrapre-installs cleanly (no-op venv, new wheels land);python -c "import sqlmodel, sqlalchemy, websockets, aiofiles"succeeds in the venv; settings parse with new keys unset
Task 1-1-03: Resource-limit probe documentation + harness ruff pass
- Persona: ai-engineer — REQ: REQ-3-001
- Files:
apps/ai-service/README.md(update: sandbox section + probe transcript),apps/ai-service/pyproject.toml(no change),apps/ai-service/tests/sandbox/test_isolation.py(no change) - Action: Record the A-101 probe transcript in README (verbatim commands + observed output from this box:
unshare --user --map-root-user --mount --pid --fork --net id -u→0;ip link→ loopback only; write containment). Document the v0.3 resource-limit mechanism choice: cgroup-v2 delegation via per-sandbox scope files is not available on this box without sudo → enforcement = subprocess-level (ulimit-equivalent viapreexec_fn: RLIMIT_AS for memory, RLIMIT_CPU for CPU-seconds, RLIMIT_NPROC for pids) + hard wall-clock timeout kill in the manager. This is the locked v0.3 mechanism (D-024 + no-sudo constraint). Run ruff over the new tree; fix all findings. - Verify:
pnpm ai:lintexits 0; README shows the probe transcript and the rlimit mechanism note
Wave 2: Lifecycle manager (depends on Wave 1)
Task 1-2-01: Sandbox manager + concurrency guard + snapshots
- Persona: sandbox-engineer — REQ: REQ-3-001, REQ-3-002
- Files:
apps/ai-service/ai_service/sandbox/manager.py,apps/ai-service/tests/sandbox/test_manager.py - Action:
SandboxManager:create(learner_id) -> SandboxHandle(guard: active count ≥AI_SANDBOX_MAX_CONCURRENT→ raisePoolFullError→ API maps to 503, D-032; no queue);list() -> list[SandboxHandle];get(id);snapshot(id) -> Path(delegates to workdir);destroy(id)(kill process tree, keep or purge workdir per flag);reap_expired()background hook forAI_SANDBOX_TIMEOUT_S. Enforce rlimits per spawner (Task 1-1-03) at exec time. Handle registry persisted in-memory (v0.3, single process; not a store — see D-019 precedent) with a clear note that handles are process-local. Narrow typed interface only — manager never imports api/ (boundary rule). - Verify:
pnpm ai:test— test_manager covers create/list/destroy/snapshot, 6th create raises PoolFullError (503 path), destroy kills the namespace process (pid gone), snapshot dir exists with workspace contents, timeout reaper removes a stale handle
Task 1-2-02: Resource-limit enforcement test
- Persona: sandbox-engineer — REQ: REQ-3-002
- Files:
apps/ai-service/tests/sandbox/test_resource_limits.py - Action: Concrete enforcement probes (guarded like isolation tests): (a) spawn a process that allocates >
RLIMIT_AS→ assert it dies with MemoryError/killed within a bound; (b) spawn a CPU spinner pastRLIMIT_CPU→ assert SIGXCPU/kill; (c) fork-bomb attempt beyondRLIMIT_NPROC→ assert fork failures; (d) wall-clock: spawnsleep 9999with a small manager timeout → reaper destroys it. Assert limits are observable (handle reports its limit set). - Verify:
pnpm ai:test— test_resource_limits green; limits proven enforced and observable
Wave 3: API exposure (depends on Wave 2)
Task 1-3-01: Sandboxes API module
- Persona: backend-engineer — REQ: REQ-3-001, REQ-3-002
- Files:
apps/ai-service/ai_service/api/sandboxes.py,apps/ai-service/ai_service/main.py(update: include router + lifespan manager),apps/ai-service/ai_service/api/deps.py(update),apps/ai-service/tests/api/test_sandboxes.py - Action: DI exposes a singleton
SandboxManager. Endpoints:POST /v1/sandboxes {learner_id}→ 201 handle (503 when pool full);GET /v1/sandboxes→ list;GET /v1/sandboxes/{id}→ handle;POST /v1/sandboxes/{id}/snapshot→ snapshot path;DELETE /v1/sandboxes/{id}→ 204. All behind localhost CORS (A-008). Lifespan creates/destroys the manager; on shutdown destroys any live sandboxes (no orphans). - Verify:
pnpm ai:test— test_sandboxes green (create→list→snapshot→delete roundtrip via TestClient; 6th create → 503; delete of unknown id → 404); manual probe:curl -X POST localhost:8420/v1/sandboxes -d '{"learner_id":"l1"}'returns a handle id
Must-Haves (Phase 1)
- Isolation probe test green on this box: in-namespace uid=0, network isolated (0 usable interfaces), host writes confined to the per-sandbox bind dir (A-101 re-verified as an automated test, not just research notes)
- Resource limits enforced + observable: memory/CPU/pids rlimits kill violating processes; wall-clock reaper destroys stale sandboxes (test_resource_limits green)
- No cross-tenant access: sandbox A cannot read sandbox B's workdir (isolation test asserts containment)
- Lifecycle API works end-to-end: create/list/snapshot/destroy via TestClient; pool full → 503 (D-032, no queue)
- Snapshot produces a restorable directory copy under the sandbox's own snapshots/ dir
pnpm ai:testandpnpm ai:lintgreen; new deps (sqlmodel,sqlalchemy,websockets,aiofiles) installed via idempotent bootstrap- Boundary rules hold:
sandbox/imports nothing fromapi/oragents/; onlyapi/sandboxes.pycomposes the manager via DI - No docker/podman/sudo anywhere in the spawner path (D-024);
SandboxBackendprotocol is the only coupling to the spawner (containerd swap possible later)
Phase 2: Live Build Telemetry
Requirements: REQ-3-003
Goal: First real persistence (D-027: SQLite via SQLModel) with a TraceStore protocol; TelemetryEvent model with per-(learner,task) monotonic seq; WebSocket ingest endpoint (D-026) with gap detection; stdlib-only in-sandbox capture agent streams real sandbox activity into ai-service; trace retrievable by learner+task
Wave 1: Models + stores + TS types (parallel — no shared files)
Task 2-1-01: Telemetry event models
- Persona: backend-engineer — REQ: REQ-3-003
- Files:
apps/ai-service/ai_service/telemetry/__init__.py,apps/ai-service/ai_service/telemetry/models.py,apps/ai-service/tests/telemetry/__init__.py,apps/ai-service/tests/telemetry/test_models.py - Action: Pydantic/SQLModel
TelemetryEvent:learner_id,task_id,seq(int, monotonic per (learner,task)),kind(command|file_diff|run_result|test_result|activity|stdin|stdout),payload(JSON),ts(datetime, monotonic-envelope),sandbox_id.TraceSpanderived view (ordered events for one (learner,task)). Validation: seq ≥ 0, kind enum, non-empty ids. - Verify:
pnpm ai:test— test_models green (validation rules enforced, JSON payload roundtrip)
Task 2-1-02: TraceStore protocol + SQLite implementation
- Persona: backend-engineer — REQ: REQ-3-003
- Files:
apps/ai-service/ai_service/telemetry/store.py,apps/ai-service/tests/telemetry/test_store.py,apps/ai-service/ai_service/data/.gitkeep - Action:
TraceStoreprotocol (D-027, Postgres-migration-ready):append(event) -> None(idempotent on (learner,task,seq) — at-least-once dedup),get_trace(learner_id, task_id) -> list[TelemetryEvent](ordered by seq),gaps(learner_id, task_id) -> list[int](missing seqs),latest_seq(learner_id, task_id) -> int,list_tasks(learner_id) -> list[str],close().SQLiteTraceStore(SQLModel): singletelemetry_eventtable, composite PK ((learner_id, task_id, seq)), indexes on (learner_id, task_id). Engine creation fromAI_DB_PATH;SQLModel.metadata.create_allat app lifespan. - Verify:
pnpm ai:test— test_store green (append/ordered-get/dedup-on-retry/gap detection/latest_seq; tmp-path SQLite per test)
Task 2-1-03: TS types for telemetry/traces
- Persona: data-engineer — REQ: REQ-3-003
- Files:
packages/types/telemetry.ts(new),packages/types/index.ts(update) - Action: TS
TelemetryEvent,TraceSpan,TelemetryKindmirroring the Python model field-for-field (cross-referencing header, same string enums). Consumed by Phase 6 web surfaces; no runtime code. - Verify:
pnpm typecheckpasses; TS type keys match Python model keys exactly
Wave 2: Capture agent + ingest (depends on Wave 1)
Task 2-2-01: In-sandbox capture agent (stdlib-only)
- Persona: sandbox-engineer — REQ: REQ-3-003
- Files:
apps/ai-service/scripts/sandbox-agent.py,apps/ai-service/tests/sandbox/test_sandbox_agent.py - Action: Tiny standalone process (D-031, stdlib only — no deps shipped into the namespace): wraps a shell inside the sandbox; captures commands, file diffs (mtime/content polling of
workspace/at 250ms), run/test results, activity; assigns per-(learner,task)seq; buffers to a local spool file on disconnect (at-least-once, D-026); reconnects with exponential backoff and flushes spool in order; small WebSocket client implemented over rawsocket(RFC6455 client handshake + frames — stdlib only, nowebsocketsin-namespace). Configured via env baked at spawn (NC_LEARNER_ID,NC_TASK_ID,NC_INGEST_URL). - Verify:
pnpm ai:test— unit tests with a loopback fake WS server: ordered seq emission, spool-on-disconnect, reconnect flush preserves order (no loss, dupes deduped server-side), no third-party imports in the file (asserted by AST scan)
Task 2-2-02: WebSocket ingest endpoint
- Persona: backend-engineer — REQ: REQ-3-003
- Files:
apps/ai-service/ai_service/telemetry/ingest.py,apps/ai-service/ai_service/api/sandboxes.py(update: register WS route),apps/ai-service/ai_service/main.py(update),apps/ai-service/tests/api/test_telemetry_ingest.py - Action:
WS /v1/telemetry/ingest(D-026): accepts connections carrying learner_id/task_id/sandbox_id; validates + appends events toTraceStore(idempotent — server-side dedup on (learner,task,seq)); emits gap warnings when seq skips (logged + surfaced in a per-connection status); backpressure = bounded inbound queue with drop-oldest only on unbounded growth (documented); ping/pong keepalive.GET /v1/telemetry/traces/{learner_id}/{task_id}returns the ordered trace;GET /v1/telemetry/gaps/{learner_id}/{task_id}returns missing seqs. - Verify:
pnpm ai:test— test_telemetry_ingest green (TestClient websocket: connect → send 3 events → trace retrievable ordered; resend event 2 → deduped; skip seq 5 → gap reported; unknown sandbox tolerated in v0.3 no-auth mode)
Wave 3: Sandbox telemetry wiring (depends on Wave 2)
Task 2-3-01: Spawn sandboxes with the capture agent
- Persona: sandbox-engineer — REQ: REQ-3-003
- Files:
apps/ai-service/ai_service/sandbox/manager.py(update),apps/ai-service/ai_service/sandbox/unshare_backend.py(update),apps/ai-service/tests/sandbox/test_telemetry_wiring.py - Action:
create()gains optionaltask_id; when set the spawner copiesscripts/sandbox-agent.pyinto the sandbox workdir, injectsNC_*env, and launches the agent as a child of the namespace process (agent lifecycle tied to sandbox lifecycle; destroy kills the agent). No capture when task_id absent (pure shell sandbox). - Verify:
pnpm ai:test— end-to-end on this box: create sandbox with task_id → run 2 commands via exec → events arrive at the ingest endpoint and land in SQLite in order
Wave 4: Reliability probe (depends on Wave 3)
Task 2-4-01: Dropped-connection durability probe
- Persona: backend-engineer — REQ: REQ-3-003
- Files:
apps/ai-service/tests/telemetry/test_durability.py - Action: Integration probe: run a capture agent against ingest, kill the WS connection mid-stream (simulate network failure), keep generating events, reconnect, assert the SQLite trace contains every event exactly once in order (spool + dedup). Document at-least-once semantics + replay path in README.
- Verify:
pnpm ai:test— test_durability green; README documents semantics
Must-Haves (Phase 2)
- Real telemetry from a live sandbox arrives at ai-service: shell commands, file diffs, run/test results appear as ordered events in SQLite (end-to-end, no mocks)
- Per-(learner,task) monotonic
seq; gap detection reports missing seqs; replay yields the complete ordered trace - At-least-once proven: transient disconnect + reconnect loses no events; duplicates deduped server-side (durability probe green)
- Capture agent is stdlib-only (AST-verified) and its lifecycle is tied to the sandbox (destroy kills it)
- Trace retrievable by learner+task via
GET /v1/telemetry/traces/...; unknown trace → 404 TraceStoreprotocol respected: no api/ code touches SQLite directly;telemetry/never importsagents/(D-027)pnpm ai:testgreen;packages/typestelemetry TS types compile (pnpm typecheck)
Phase 3: Process-Trace Grading Engine
Requirements: REQ-3-004
Goal: Deterministic feature computation over traces (D-028) → compact digest → LLM rubric scoring via existing D-020 JSON defense → validated structured scores stored in GradeStore; LLM never sees the raw trace; engine calibrated against v0.2 mock corpora so process quality separates paste-and-run from iterative debugging
Wave 1: Features + grades store (parallel — no shared files)
Task 3-1-01: Deterministic trace digest (features)
- Persona: ai-engineer — REQ: REQ-3-004
- Files:
apps/ai-service/ai_service/grading/__init__.py,apps/ai-service/ai_service/grading/features.py,apps/ai-service/tests/grading/__init__.py,apps/ai-service/tests/grading/test_features.py - Action: Pure compute module (D-028):
compute_digest(trace: list[TelemetryEvent]) -> TraceDigest. Deterministic features: test pass/fail counts + final status; edit count; error/fix cycle count + mean fix latency; idle gaps (>Ns, count + total); command category histogram (build/test/file/nav/debug/other); session duration; first-test-pass offset.TraceDigestpydantic model — compact (bounded size, no raw commands), LLM-safe. - Verify:
pnpm ai:test— test_features green over synthetic traces: paste-and-run trace (0 error/fix cycles, single test pass at end) vs iterative trace (many cycles) produce observably different digests
Task 3-1-02: GradeStore protocol + SQLite implementation
- Persona: backend-engineer — REQ: REQ-3-004
- Files:
apps/ai-service/ai_service/grading/store.py,apps/ai-service/tests/grading/test_store.py - Action:
GradeStoreprotocol (D-027):save(grade),get(learner_id, task_id),list_for_learner(learner_id),close(). SQLModelGradeRecord: learner_id, task_id, variant_seed (null until P4), digest (JSON), scores (JSON), verdict, model, created_at. PK (learner_id, task_id). Postgres-migration-ready. - Verify:
pnpm ai:test— test_store green (save/get/list roundtrip, overwrite-on-regrade documented)
Wave 2: Grading engine + calibration (depends on Wave 1)
Task 3-2-01: Rubric scoring engine
- Persona: ai-engineer — REQ: REQ-3-004
- Files:
apps/ai-service/ai_service/grading/engine.py,apps/ai-service/ai_service/prompts/grading.py(new),apps/ai-service/tests/grading/test_engine.py - Action:
GradingEngine.grade(learner_id, task_id) -> GradeRecord: load trace viaTraceStore→compute_digest→ render rubric prompt (prompts/grading.py: criteria + level anchors for process quality, correctness, debugging discipline, test usage) → LLM structured output through the existing D-020 4-layer defense (agents/structured.pyreused — engine composes it, never duplicates it) → validateRubricScoremodel (per-criterion 0-4 + strengths + gaps + verdict) → persist viaGradeStore. Grading depends onllm/+telemetry/+prompts/only (boundary). Mock provider scripts deterministic rubric JSON for tests. - Verify:
pnpm ai:test— test_engine green (mock provider: digest-only prompt asserted — raw trace string absent from prompt; validated scores returned; malformed JSON exercises D-020 retry; unknown trace → error)
Task 3-2-02: Calibration against v0.2 mock corpora
- Persona: ai-engineer — REQ: REQ-3-004
- Files:
apps/ai-service/ai_service/corpus/trace_fixtures.py(new),apps/ai-service/tests/grading/test_calibration.py - Action: Synthetic trace fixtures aligned with v0.2
corpus/telemetry.py+corpus/artifacts.pyscenario IDs (strong/lazy/struggling builder archetypes). Assert grading separates them: strong archetype scores ≥ lazy archetype on process-quality criterion (mock provider maps digest shape → scripted scores; test asserts the ordering contract + that fixture IDs align with existing corpus IDs, D-021). - Verify:
pnpm ai:test— test_calibration enforces the ordering contract
Wave 3: Grading endpoint (depends on Wave 2)
Task 3-3-01: Assessment grade endpoint (real traces)
- Persona: backend-engineer — REQ: REQ-3-004
- Files:
apps/ai-service/ai_service/api/assessment.py(update),apps/ai-service/tests/api/test_grading.py - Action:
POST /v1/assessment/grade {learner_id, task_id}→GradingEngine→ validatedRubricScoreJSON;GET /v1/assessment/grade/{learner_id}/{task_id}→ stored grade; unknown trace → 404. Composed via DI (api/ owns wiring; engine knows nothing of FastAPI). - Verify:
pnpm ai:test— test_grading green (grade roundtrip via TestClient with mock provider; 404 on unknown; GET after POST returns same scores)
Must-Haves (Phase 3)
- Engine emits structured rubric-aligned scores from a real process trace (not pre-baked input) — TestClient roundtrip green
- Deterministic features computed in code (test pass/fail, edit count, error/fix cycles, idle gaps, command categories); LLM receives the digest only — test asserts the raw trace never reaches the prompt (D-028)
- Scores distinguish process quality: iterative-debugging archetype out-scores paste-and-run on the process criterion (calibration contract test)
- Grades persisted + retrievable by learner+task via GradeStore (SQLite, protocol-wrapped, D-027)
- Boundary rules hold:
grading/imports noapi//agents/internals except the shared D-020 structured defense;pnpm ai:test+pnpm ai:lintgreen
Phase 4: Variant Task Generation
Requirements: REQ-3-005 Goal: Template library with typed parameter slots (D-029) + seeded LLM instantiation + per-learner variant registry (SQLite) with difficulty-normalization anchors; two learners on the same competency get provably distinct, reproducible, auditable tasks
Wave 1: Templates + store (parallel — no shared files)
Task 4-1-01: Task template library
- Persona: ai-engineer — REQ: REQ-3-005
- Files:
apps/ai-service/ai_service/variants/__init__.py,apps/ai-service/ai_service/variants/templates.py,apps/ai-service/tests/variants/__init__.py,apps/ai-service/tests/variants/test_templates.py - Action: ≥3 initial task templates bound to existing competency IDs (D-021 alignment).
TaskTemplate: id, competency_id, statement skeleton with{slot}placeholders,ParameterSlot[](name, type: enum/int-range/string-set, allowed values),rubric anchors(difficulty normalization: expected feature envelope — e.g. expected edit-count band — used by grading context), starter-file scaffolds served to the sandbox. Seeded slot sampler is pure code (random.Random(seed)), fully reproducible. - Verify:
pnpm ai:test— test_templates green (slot validation: bad value rejected; seeded sampling reproducible across runs; all templates bind to real competency IDs)
Task 4-1-02: VariantStore protocol + SQLite implementation
- Persona: backend-engineer — REQ: REQ-3-005
- Files:
apps/ai-service/ai_service/variants/store.py,apps/ai-service/tests/variants/test_store.py - Action:
VariantStoreprotocol (D-027):save(variant),get(learner_id, template_id_or_task_id),list_for_learner(learner_id),list_by_template(template_id),close(). SQLModelVariantRecord: learner_id, task_id (the grading/telemetry task key), template_id, seed, params (JSON), statement (rendered), created_at. Unique (learner_id, template_id). - Verify:
pnpm ai:test— test_store green (roundtrip, unique constraint, audit listing)
Wave 2: Generator (depends on Wave 1)
Task 4-2-01: Seeded LLM variant generator
- Persona: ai-engineer — REQ: REQ-3-005
- Files:
apps/ai-service/ai_service/variants/generator.py,apps/ai-service/ai_service/prompts/variant.py(new),apps/ai-service/tests/variants/test_generator.py - Action:
VariantGenerator.generate(learner_id, template_id) -> VariantRecord: derive seed (sha256(template_id|learner_id|milestone)— reproducible, D-029); sample typed slots in code; render a fill prompt (statement skeleton + concrete slot values) → LLM via D-020 structured defense → unique task statement + starter files → validate → persist (seed + params + statement) viaVariantStore. Cache: existing (learner,template) returns the stored variant (no duplicate work). Mock provider scripts deterministic statements per seed for tests. - Verify:
pnpm ai:test— test_generator green: two different learner_ids → distinct statements for the same template; same learner twice → identical stored variant (reproducible); params JSON contains only schema-valid slot values
Wave 3: Variant endpoint + TS types (depends on Wave 2)
Task 4-3-01: Variant task endpoint
- Persona: backend-engineer — REQ: REQ-3-005
- Files:
apps/ai-service/ai_service/api/variants.py(new),apps/ai-service/ai_service/main.py(update),apps/ai-service/tests/api/test_variants.py - Action:
POST /v1/variants {learner_id, template_id or competency_id}→ generated (or cached) variant: statement, starter files, task_id, seed;GET /v1/variants/{task_id}→ stored variant;GET /v1/variants?learner_id=→ learner's variants. DI wiring in api/ only. - Verify:
pnpm ai:test— test_variants green (generate→get roundtrip; cache hit on regenerate; distinct learners → distinct statements asserted at the API layer)
Task 4-3-02: TS types for variants (+ grades)
- Persona: data-engineer — REQ: REQ-3-005
- Files:
packages/types/variants.ts(new),packages/types/grading.ts(new),packages/types/index.ts(update) - Action: TS
TaskVariant,VariantParams,RubricScore,GradeRecordmatching Python models (cross-referencing headers; same field names). Consumed by Phase 6 surfaces. - Verify:
pnpm typecheckpasses
Must-Haves (Phase 4)
- Two learners requesting the same competency receive provably distinct task variants (API-level test)
- Seed derivation reproducible: same (template, learner) → same variant, served from cache without a second LLM call (D-029)
- Variant seed + typed params persisted + auditable (VariantStore listing; proctoring cross-check path exists)
- Difficulty normalization anchors present per template and shipped to the grader prompt context
- Starter-file scaffolds defined per template (P6 wires them into the sandbox workdir)
pnpm ai:test+pnpm typecheckgreen;variants/imports no api/ (boundary)
Phase 5: Oral / Voice Defense
Requirements: REQ-3-006
Goal: VoiceProvider protocol with mock-first STT/TTS (D-030) + seventh Examiner agent streaming over existing SSE + HTTP audio endpoints + DefenseStore persisting transcript + integrity signals — nothing blocks on a real voice key (mock + browser fallback are first-class)
Wave 1: Voice provider layer (parallel — no shared files)
Task 5-1-01: VoiceProvider protocol + mock provider
- Persona: voice-engineer — REQ: REQ-3-006
- Files:
apps/ai-service/ai_service/voice/__init__.py,apps/ai-service/ai_service/voice/base.py,apps/ai-service/ai_service/voice/mock.py,apps/ai-service/tests/voice/__init__.py,apps/ai-service/tests/voice/test_mock.py - Action:
VoiceProviderprotocol mirroringLLMProvider(D-030):transcribe(audio: bytes, fmt) -> TranscriptSegment+synthesize(text, voice) -> AsyncIterator[bytes](audio chunks).MockVoiceProvider: deterministic canned transcript (scripted per test), canned 1kHz-tone WAV bytes; scripted failure modes (provider down, empty audio).voice/never importsagents/orapi/. - Verify:
pnpm ai:test— test_mock green (deterministic: two identical transcribe calls → identical segments; synthesize yields non-empty bytes; failure modes trigger)
Task 5-1-02: OpenAI-compatible audio provider + browser fallback descriptor + factory
- Persona: voice-engineer — REQ: REQ-3-006
- Files:
apps/ai-service/ai_service/voice/openai_audio.py,apps/ai-service/ai_service/voice/browser.py,apps/ai-service/ai_service/voice/factory.py,apps/ai-service/tests/voice/test_openai_audio.py,apps/ai-service/tests/voice/test_factory.py - Action:
OpenAIAudioProvider: STT = POST multipart to{AI_VOICE_BASE_URL}/audio/transcriptions(Whisper-compatible, BearerAI_VOICE_API_KEY); TTS = POST to/audio/speechstreaming mp3/pcm chunks; reuses lifespan httpx client.browser.py: fallback descriptor only (sr_available: true, endpoint hints) consumed by the web client to select browser-nativeSpeechRecognition/speechSynthesiswhen no server key configured.factory.py:AI_VOICE_PROVIDER=openai-audio | browser | mock(default: mock when no key — mock-first, nothing blocks on a real voice key). - Verify:
pnpm ai:test— httpx MockTransport tests: transcription multipart parse, TTS byte stream, 4xx/5xx error mapping, factory selects mock with empty key; zero network calls
Task 5-1-03: DefenseStore protocol + SQLite implementation
- Persona: backend-engineer — REQ: REQ-3-006
- Files:
apps/ai-service/ai_service/voice/defense_store.py,apps/ai-service/tests/voice/test_defense_store.py - Action:
DefenseStoreprotocol (D-027):start(defense),append_turn(defense_id, turn),finalize(defense_id, integrity_signals),get(defense_id),list_for_learner(learner_id),close(). SQLModelDefenseRecord(id, learner_id, task_id, status, created/finished_at) +DefenseTurn(defense_id FK, turn seq, role examiner|learner, text, ts, latency_ms) + integrity signals JSON on the record (long pauses, off-scope cadence markers — A-109). - Verify:
pnpm ai:test— test_defense_store green (start→append turns→finalize→get roundtrip; ordered turns by seq)
Wave 2: Examiner agent (depends on Wave 1)
Task 5-2-01: Examiner agent (seventh agent)
- Persona: ai-engineer — REQ: REQ-3-006
- Files:
apps/ai-service/ai_service/agents/examiner.py,apps/ai-service/ai_service/prompts/examiner.py(new),apps/ai-service/ai_service/agents/registry.py(update: central registration, G-4 pattern),apps/ai-service/tests/agents/test_examiner.py - Action:
ExaminerAgent(BaseAgent)(D-030/A-109): builds questions from learner transcript + trace digest + (P4) variant statement; probes understanding + challenges process choices ("why did you choose X at step N?"); streams questions over the existing SSE pipeline;structuredverdict mode returns verdict + per-answer integrity signal list (long pause flags, off-scope answers) computed from turn metadata; calls voice only through theVoiceProviderprotocol (PERSONAS conflict rule — never concrete providers). Session-scoped history reused from v0.2. - Verify:
pnpm ai:test— test_examiner green (question stream references trace-digest facts; verdict structured output validates via D-020 defense; registry resolves all seven agents; mock-VoiceProvider wiring through protocol only — asserted by import scan in test)
Wave 3: Defense endpoints (depends on Wave 2)
Task 5-3-01: Defense session + audio endpoints
- Persona: backend-engineer — REQ: REQ-3-006
- Files:
apps/ai-service/ai_service/api/defense.py(new),apps/ai-service/ai_service/main.py(update),apps/ai-service/tests/api/test_defense.py - Action:
POST /v1/defense/start {learner_id, task_id}→ creates DefenseRecord + streams the first examiner question (SSE, agent=examiner);POST /v1/defense/{id}/answer(multipart audio from browser MediaRecorder, or{text}for typed fallback) → STT via VoiceProvider → append learner turn → stream examiner follow-up (SSE) → TTS audio chunks overGET /v1/defense/{id}/audio/{turn_id};POST /v1/defense/{id}/finish→ verdict + integrity signals persisted;GET /v1/defense/{id}→ full transcript + signals. Browser-fallback mode: when provider=browser, start returns the fallback descriptor instead of server audio. - Verify:
pnpm ai:test— test_defense green (full loop with mock voice + mock LLM: start → answer(text) → answer(audio bytes) → finish → transcript retrievable with per-turn latency; unknown id → 404;GETsignals present after finish)
Wave 4: Turn-latency probe (depends on Wave 3)
Task 5-4-01: Latency budget probe + README budget doc
- Persona: voice-engineer — REQ: REQ-3-006
- Files:
apps/ai-service/tests/voice/test_latency.py,apps/ai-service/README.md(update: latency budget section + voice config docs) - Action: Instrument per-turn latency (STT ms + LLM TTFT ms + TTS ms) recorded on each DefenseTurn. Deterministic timing probe over mock providers: total turn budget < target (documented; mock runs are near-instant, so the test asserts instrumentation presence + budget field populated rather than wall-clock). README documents the conversational-budget target and how to run a manual live probe with a real voice endpoint (
AI_VOICE_PROVIDER=openai-audio+ key in.ciagent/.env.secrets— explicitly optional, never in tests/commits). - Verify:
pnpm ai:test— test_latency green (latency_ms fields populated on every turn; budget constant defined); README documents the manual live probe as out-of-band
Must-Haves (Phase 5)
- Spoken defense runs end-to-end over HTTP with mock providers: start → answer (audio + typed fallback) → examiner follow-up streams → verdict + transcript persisted (automated)
- Examiner is the seventh registered agent; streams over the existing SSE envelope (meta agent=examiner)
- Transcript + integrity signals persisted via DefenseStore and retrievable; per-turn latency instrumented (A-109)
VoiceProviderprotocol respected: examiner + api touch voice only via the protocol; mock-first — no task requires a real voice key to pass- Browser-native SR/TTS fallback descriptor returned when no server key configured (mock/no-key path first-class, D-030)
- Optional voice config documented:
AI_VOICE_BASE_URL/AI_VOICE_API_KEY/ models in.env.example+ README; keys only in gitignored.ciagent/.env.secrets; tests never call a voice API - Boundary rules hold:
voice/imports noagents//api/;pnpm ai:test+pnpm ai:lintgreen - Manual live-voice turn-latency probe documented in README (out-of-band, optional)
Phase 6: Agent Re-grounding + Learner Surface Integration
Requirements: REQ-3-007, REQ-3-008
Goal: Lab/Assessor/Proctor consume real engine inputs (live telemetry, grading output, defense signals) with no mock fallback in the learner path; the v0.1 sandbox + assessment mockups become real — in-browser xterm.js build/run, live telemetry panel, live voice/typed defense, live grading; pnpm build + pnpm typecheck + full pnpm ai:test green
Note: E2E verification runs against the real engines over HTTP with AI_PROVIDER=mock + AI_VOICE_PROVIDER=mock permitted (G-2 precedent) — the requirement is real engine plumbing (sandbox/telemetry/grading/defense over real endpoints, no corpus mocks in the learner path); a cloud outage must not block P6.
Wave 1: Agent re-grounding (parallel — no shared files)
Task 6-1-01: Lab agent on live telemetry
- Persona: ai-engineer — REQ: REQ-3-007
- Files:
apps/ai-service/ai_service/agents/lab.py(update),apps/ai-service/ai_service/prompts/lab.py(update: render real trace digest),apps/ai-service/tests/agents/test_lab_live.py(new) - Action: Lab consumes a live trace digest (grading/features
compute_digestoverTraceStoreevents) instead ofcorpus/telemetry.py. build_messages renders digest facts (recent commands, failing tests, idle). Mock-provider scripts assert digest-derived content. v0.2 corpus path removed from the agent (dormant corpus retained until Task 6-1-04 check). - Verify:
pnpm ai:test— test_lab_live green: feedback references events actually present in a seeded SQLite trace (not corpus fixtures); nocorpus.telemetryimport inagents/lab.py(AST-asserted)
Task 6-1-02: Assessor agent on grading output
- Persona: ai-engineer — REQ: REQ-3-007
- Files:
apps/ai-service/ai_service/agents/assessor.py(update),apps/ai-service/ai_service/prompts/assessor.py(update),apps/ai-service/tests/agents/test_assessor_live.py(new) - Action: Assessor consumes
GradeStoreoutput (validatedRubricScore+ digest) for learner+task instead of pre-baked artifacts/transcripts; renders strengths/gaps/verdict with rubric-anchored coaching framing. Structured output unchanged (D-020). - Verify:
pnpm ai:test— test_assessor_live green: given a real stored grade, Assessor output reflects its scores; corpus artifact path gone from the agent (AST-asserted)
Task 6-1-03: Proctor agent on telemetry + defense signals
- Persona: ai-engineer — REQ: REQ-3-007
- Files:
apps/ai-service/ai_service/agents/proctor.py(update),apps/ai-service/ai_service/prompts/proctor.py(update),apps/ai-service/tests/agents/test_proctor_live.py(new) - Action: Proctor consumes real integrity inputs: idle gaps + command cadence from the trace digest + defense integrity signals from
DefenseStore→ classified signals + coaching interventions (supportive tone retained). Cross-checks variant seed params (P4) for off-template work. - Verify:
pnpm ai:test— test_proctor_live green: signals derived from seeded real trace + defense records; corpus proctor scenarios no longer imported (AST-asserted)
Task 6-1-04: Corpus dormancy + mockup removal verification
- Persona: lead-developer — REQ: REQ-3-007
- Files:
apps/ai-service/ai_service/corpus/telemetry.py,apps/ai-service/ai_service/corpus/artifacts.py,apps/ai-service/ai_service/corpus/(README note),apps/ai-service/tests/test_corpus_dormancy.py(new) - Action: Verify no production code path imports
corpus/telemetry.pyorcorpus/artifacts.pyanymore (test scans imports acrossagents/,api/, engines). Retain files as Phase-3 calibration history with a header note marking them dormant — v0.2 mocks, not used at runtime; learner-context corpus stays (agents still need learner context). Disposes v0.2's G-5-class dead-code risk deliberately. - Verify:
pnpm ai:test— test_corpus_dormancy green (zero runtime importers); suite otherwise unchanged
Wave 2: Client plumbing + design primitives (parallel — no shared files)
Task 6-2-01: Terminal + sandbox engine client
- Persona: frontend-engineer — REQ: REQ-3-008
- Files:
apps/web/package.json(update:@xterm/xterm,@xterm/addon-fit),pnpm-lock.yaml(update),apps/web/hooks/use-sandbox-session.ts(new),apps/web/lib/engine-client.ts(new),apps/web/.env.example(update) - Action:
engine-client.ts: typed fetch/WS client for/v1/sandboxes(create/destroy — learner_id from the v0.3 mock session constant),/v1/variants,/v1/assessment/grade,/v1/telemetry/traces,/v1/defense/*; baseNEXT_PUBLIC_AI_SERVICE_URL.use-sandbox-session.ts: create sandbox+variant on task open → wire a WebSocket terminal channel (xterm attach: keystrokes → exec stdin in sandbox, stdout → terminal render; implemented as a WS relay endpoint consumed from api/sandboxes — reused ingest WS pattern) → destroy on unmount (idempotent cleanup, AbortController pattern). 503 pool-full → user-facing "environment busy, retry" state (D-032 surfaced honestly). - Verify:
pnpm install && pnpm typecheckpass; hook unmount destroys the sandbox (manual probe:curl localhost:8420/v1/sandboxesshows count drop after navigation)
Task 6-2-02: New design primitives
- Persona: design-system-engineer — REQ: REQ-3-008
- Files:
packages/ui/src/primitives/terminal-frame.tsx(new),packages/ui/src/primitives/mic-control.tsx(new),packages/ui/src/primitives/grade-badge.tsx(new),packages/ui/src/primitives/telemetry-status.tsx(new),packages/ui/src/primitives/transcript-viewer.tsx(new),packages/ui/src/primitives/index.ts(update),packages/ui/src/index.ts(update) - Action: Token-driven primitives: TerminalFrame (chrome around xterm with connection status), MicControl (record/stop with consent state + no-mic fallback state, MediaRecorder permission UX), GradeBadge (verdict rendering), TelemetryStatus (live event pulse / disconnected indicator), TranscriptViewer (examiner/learner turn list). Dark mode + WCAG AA; stories for each.
- Verify: primitives import from
@nextcraft/ui; Storybook stories render dark + light;pnpm build(ui package) passes
Task 6-2-03: Defense TS types
- Persona: data-engineer — REQ: REQ-3-008
- Files:
packages/types/defense.ts(new),packages/types/index.ts(update) - Action: TS
DefenseSession,DefenseTurn,IntegritySignal,Verdictmirroring P5 Python models (cross-referencing header). - Verify:
pnpm typecheckpasses
Wave 3: Real build surface (depends on Wave 2)
Task 6-3-01: Sandbox mockup → real in-browser IDE
- Persona: frontend-engineer — REQ: REQ-3-008
- Files:
apps/web/app/(learner)/build/[competencyId]/page.tsx(rewrite),apps/web/components/learner/sandbox-terminal.tsx(new),apps/web/components/learner/file-tree.tsx(new),apps/web/components/learner/run-controls.tsx(new),apps/web/components/learner/lab-feedback-panel.tsx(update: live trace) - Action: Replace the mockup with the real build environment (A-103): file tree (HTTP CRUD into the sandbox workdir via
/v1/sandboxes/{id}/filesroutes added to api/sandboxes — read/write/list workspace files; reused by run/test), syntax-highlight editor (existing), xterm.js terminal onuse-sandbox-session, Run/Test buttons (exec in sandbox via relay; results stream to terminal), starter files from the P4 variant scaffold. Lab panel now postslearner_id+task_id→ streams Lab feedback over the live trace (no scenario IDs). Telemetry sidebar shows live TelemetryStatus. Pool-full 503 → busy state with retry. - Verify: with ai-service up: open
/build/comp-01→ variant statement + starter files load → typeecho helloin terminal → real output renders; Run executes tests in-sandbox; Lab panel streams digest-derived feedback; telemetry status shows live events. Manual probe documented;pnpm typecheckgreen
Wave 4: Live defense + grading surfaces (depends on Waves 2-3)
Task 6-4-01: Assessment mockup → live defense + live grading
- Persona: frontend-engineer — REQ: REQ-3-008
- Files:
apps/web/app/(learner)/defend/[competencyId]/page.tsx(rewrite),apps/web/components/learner/defense-session.tsx(new),apps/web/components/learner/assessor-results-panel.tsx(update),apps/web/components/learner/proctor-banner.tsx(update),apps/web/components/learner/oral-defense-interface.tsx(rewrite or remove) - Action: Real assessment flow: Start Defense → POST
/v1/defense/start→ examiner question streams → learner answers via MicControl (MediaRecorder webm/opus → multipart POST) with typed fallback when mic denied orprovider=browser(nativeSpeechRecognition/speechSynthesispath per fallback descriptor) → follow-ups stream → Finish → verdict + integrity signals panel (TranscriptViewer, GradeBadge) + Grade My Work → POST/v1/assessment/grade→ structured rubric bars render. Proctor banner reads signals for task_id. Loading + error states throughout; no mock defense data remains in the learner path. - Verify: with ai-service up: full defense loop runs in-browser (typed fallback acceptable in CI-less manual probe; mic path exercised manually with permission granted); grade panel renders real rubric scores;
pnpm typecheckgreen
Wave 5: End-to-end verification (depends on Waves 3-4)
Task 6-5-01: Full learner-path E2E probe + green builds
- Persona: backend-engineer — REQ: REQ-3-007, REQ-3-008
- Files:
apps/ai-service/tests/api/test_e2e_credential_flow.py(new),apps/ai-service/README.md(update: E2E probe doc) - Action: Endpoint-level E2E (mock LLM/voice providers, real engines): create variant → create sandbox with task_id → ingest trace events via real sandbox exec → POST grade → start defense (typed answers) → finish → assert: trace persisted, grade stored + digest-linked, defense transcript + signals stored, Proctor/Assessor endpoints serve them. No corpus fixture anywhere in the flow (AST-asserted). Run
pnpm build+pnpm typecheck+ fullpnpm ai:testat repo root; fix all failures before phase ship. - Verify:
pnpm ai:testgreen incl. test_e2e_credential_flow;pnpm build+pnpm typecheckgreen; README E2E probe section documents the manual browser pass
Must-Haves (Phase 6)
- Lab/Assessor/Proctor operate on real inputs with no mock fallback in the learner path (AST-verified: no corpus telemetry/artifact/proctor imports in production paths)
- Learner builds in-browser for real: xterm.js terminal executes in a namespace sandbox; file tree CRUD + Run/Test work; starter files come from the learner's variant scaffold
- Live telemetry: build activity streams to ai-service and the sidebar shows live status (TelemetryStatus); trace persisted in SQLite
- Live defense: start → answer (mic or typed fallback) → examiner follow-ups → finish → transcript + integrity signals + verdict rendered; browser-native path works with no server voice key
- Live grading: grade request returns structured rubric scores computed from the real trace digest; grade panel renders them
- 503 pool-full surfaced honestly in UI; navigating away destroys the sandbox (no leaked sandboxes —
GET /v1/sandboxesmanual probe) - Examiner remains protocol-clean (voice only via
VoiceProvider); module boundary rules hold across all new code pnpm buildandpnpm typecheckpass; fullpnpm ai:testgreen; no cloud/voice calls in any automated test- Release-note input (for P7): v0.3 ships real engines; identity/age-gating (KYC) remains deferred — age-gating is still a visual mockup (A-110); sandbox scope is coding-IDE only (design tool/simulation deferred to v0.4, D-025)
Phase 7: Final Review + Ship (no planned tasks)
Orchestrated by the SHIP stage, not this plan: multi-persona code review (correctness, testing, module boundaries, secrets hygiene — keys absent from code/logs/commits/errors, localhost-only CORS, no PII in prompts), project health audit (reconstruction, .ciagent/ discipline, branch/commit hygiene), then merge milestone → main, tag the final v0.2.x patch, create the Gitea release, mark all 8 v0.3 requirements complete.
Release-note honesty: the release note must state (a) Lab/Assessor/Proctor now run on real engine inputs (v0.2 mock-input caveat retired), (b) sandbox scope = coding IDE only — design tool + simulation environments deferred to v0.4 (D-025), (c) identity/age-gating (KYC) deferred per founder directive — age-gating remains the v0.1 visual flow mockup (A-110), (d) voice runs mock-first with browser-native fallback — a server STT/TTS key is optional and configured only via .ciagent/.env.secrets.
Secrets-hygiene checklist (P7): .ciagent/.env.secrets gitignored and never committed; AI_VOICE_API_KEY / AI_TUTOR_API_KEY referenced only via env; SQLite DBs + sandbox dirs gitignored; no keys in logs, error messages, or test fixtures.
Disposal checks (G-5 class): v0.2 dormant corpus files carry the dormant-header note (Task 6-1-04); any now-unused mock-data exports for the old sandbox/defense mockups (e.g. aiTutorResponses-class leftovers) must be removed or deprecated by review.
User-Facing Surface
The primary user-facing surface is the learner build + defend flow at http://localhost:3000, backed by the real engines in ai-service at http://localhost:8420:
/dashboard— AI tutor chat (Coach/Tutor, streaming) + Mentor panel (unchanged from v0.2)/learn/[competencyId]— byte viewer with streaming Tutor explanations (unchanged)/build/[competencyId]— real in-browser IDE: xterm.js terminal into a namespace sandbox, file tree, Run/Test, live telemetry status, live Lab feedback, per-learner variant task statement/defend/[competencyId]— live oral defense + live grading: Examiner voice/typed dialogue, transcript + integrity signals, real rubric scores from the process trace
The marketplace, employer, and admin surfaces are unchanged from v0.1/v0.2.
Happy Path
- Learner opens
/build/comp-01→ a per-learner variant statement and starter files load; a namespace sandbox is created for the session - Learner edits files in the tree and types real shell commands in the xterm.js terminal → output renders live; the telemetry sidebar pulses as events stream to ai-service and persist in SQLite
- Run/Test executes in-sandbox; results stream into the terminal; the Lab panel streams feedback derived from the live trace digest (real commands, real failures)
- Learner opens
/defend/comp-01→ Start Defense: the Examiner streams an opening question ("Walk me through your build — why did you structure it this way?") - Learner answers by voice (mic consent → MediaRecorder → STT) or typed fallback → examiner follow-ups probe the trace ("You hit three test failures before passing — what changed?"); TTS plays examiner audio (or browser speech in fallback)
- Learner finishes the defense → transcript + integrity signals appear; verdict renders in a GradeBadge
- Learner clicks Grade My Work → the grading engine computes the digest from the real trace, rubric-scores it, and the panel renders per-criterion bars + strengths/gaps/verdict
- Proctor banner shows integrity signals from the live trace + defense in coaching tone; Mentor panel on
/dashboardcan narrate the real outcome - Navigating away destroys the sandbox (pool slot freed); killing ai-service shows inline error + retry states on every panel, with no crashes
UX Acceptance Criteria
- Terminal output is visibly live (per keystroke/command round-trip), not a replay animation
- The learner path contains no mock engine data — scenarios, canned artifacts, and scripted defense transcripts from v0.2 are gone from runtime
- Variant statements visibly differ between two learner sessions on the same competency
- Mic permission flow is graceful: consent prompt, recording indicator, no-mic/typed fallback, and browser-native speech path when no server voice key is configured
- Defense transcript renders turn-by-turn with latency shown; integrity signals render in coaching (supportive) tone
- Grade results render as structured per-criterion bars with verdict, from the real trace — not from final-output-only heuristics
- Pool-full (503) shows an honest "environment busy — retry" state; navigation/unmount destroys sandboxes with no leaks
- When ai-service is unreachable: inline error + retry on every panel — no crashes, no console errors, no blank UI
- All new UI uses design tokens, supports dark mode, meets WCAG AA contrast, responsive at 375px / 768px / 1280px
pnpm buildandpnpm typecheckpass with zero errors;pnpm ai:testgreen cloud-free and voice-key-free