Commit Graph

56 Commits

Author SHA1 Message Date
CIAgent f281eeaf62 docs(P05): mark REQ-3-006 + phase 5 verified
---ci---
phase: 5
milestone: v0.3
status: verify
requirements: {covered: [REQ-3-006], partial: []}
---/ci---
v0.2.6
2026-09-12 04:51:45 +00:00
CIAgent 22d4fa212c fix(P05): verifier P0s — browser descriptor served, 409 post-finish, 422 empty audio, verdict persisted
Four gaps found by independent verifier probing of the defense endpoints
(all Must-Have-relevant, all trivially fixed):

1. Browser-mode descriptor was dead code: BROWSER_FALLBACK_DESCRIPTOR
   existed but start always returned mode='mock' even with
   AI_VOICE_PROVIDER=browser (Must-Have #6 violated). start now derives
   the descriptor from settings.voice_provider (D-030).
2. answer after finish returned 200 and appended turns to a sealed
   transcript — the store explicitly assigns sequencing to the endpoints
   (defense_store.py: 'turns after finalize are a sequencing bug for the
   endpoints to prevent, task 5-3-01'); the endpoints didn't. Now 409.
3. Zero-byte audio upload crashed the mock provider (MockVoiceFailure ->
   500); a real provider would 500 the same way. Empty upload is a client
   error: 422 before any provider call (provider contract unchanged).
4. Verdict was NOT persisted (Must-Have #1 'verdict + transcript
   persisted'): finish persisted only signals; GET after finish could not
   re-serve the verdict. The verdict now nests in integrity_signals
   (JSON-object dict per the DefenseStore.finalize contract).

3 regression tests added (empty-audio 422, post-finish 409, verdict
retrievable from GET; browser-descriptor test). Suite 386 green; ruff clean.

---ci---
phase: 5
milestone: v0.3
status: verify
requirements:
  covered: [REQ-3-006]
  partial: []
lessons:
  - A descriptor that exists but is never served is indistinguishable from
    dead code until you probe the configured mode end-to-end (factory tests
    proved selection, not service).
  - Store contracts that 'assign' sequencing to callers need an endpoint
    test for the forbidden transition, or the assignment is decorative.
---/ci---
2026-09-12 04:49:46 +00:00
CIAgent 007865a5a1 test(P05): latency instrumentation + conversational-budget docs (Wave 4)
Task 5-4-01: tests/voice/test_latency.py — instrumentation presence + population
(stt_ms/llm_ms/tts_ms in every answer response; latency_ms on every persisted turn);
DEFENSE_TURN_BUDGET_MS=4s named; mock turns within budget. README: voice mock-first
section — real server STT/TTS deferred to v0.4 (CUT-1/G-7), AI_VOICE_PROVIDER modes,
the v0.4 wall-clock acceptance probe (manual, keys in .ciagent/.env.secrets only).

Suite 383 green; ruff clean.

---ci---
phase: 5
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-006], partial: []}
---/ci---
2026-09-12 04:39:29 +00:00
CIAgent 04bdccf189 feat(P05): defense endpoints (Wave 3)
Task 5-3-01: api/defense.py — POST /v1/defense/start (DefenseRecord + first examiner
question persisted + voice descriptor + trace_complete disclosure — the defense does
NOT gate on completeness, the grader does per G-4), POST /{id}/answer ({text} or
multipart audio -> STT via VoiceProvider; follow-up question; per-turn latency
stt_ms/llm_ms/tts_ms), GET /{id}/audio/{turn_id} (streaming TTS WAV), POST /{id}/finish
(DefenseVerdict via D-020 + A-109 integrity signals: long pauses computed from turn
metadata at PAUSE_THRESHOLD_MS=15s), GET /{id} (ordered transcript + signals + status).
Lifespan wires DefenseStore + voice provider + ExaminerAgent. python-multipart dep for
audio parsing.

11 endpoint tests green (full loop mock voice + mock LLM; long-pause signal; 404s; 422
no-body; audio streaming). Suite 378 green; ruff clean.

---ci---
phase: 5
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-006], partial: []}
---/ci---
2026-09-12 04:37:30 +00:00
CIAgent f3071e4b79 feat(P05): Examiner agent — seventh agent (Wave 2)
Task 5-2-01: prompts/examiner.py (Socratic oral-defense examiner; one question per
turn; grounded in TraceDigest + variant statement — never raw trace, never learner id,
D-028 mirror; rubric internals never revealed) + agents/examiner.py — ExaminerAgent
(next_question for the SSE pipeline; final_verdict -> DefenseVerdict via the D-020
defense). BOUNDARY: the examiner is a text agent and imports NO voice/ (STT/TTS belong
to the endpoints; integrity signals computed from turn metadata — A-109). Registry
registers all seven agents centrally (G-4); registry test updated six -> seven.

6 examiner tests (digest-grounded prompt w/o learner id; D-020 retry; 7-agent roster;
boundary import scan). Suite 367 green; ruff clean.

---ci---
phase: 5
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-006], partial: []}
---/ci---
2026-09-12 04:20:43 +00:00
CIAgent 3d72fd28ec feat(P05): voice layer + DefenseStore (Wave 1)
Task 5-1-01: voice/ — VoiceProvider protocol (transcribe/synthesize, D-030 mirroring
LLMProvider), deterministic MockVoiceProvider (scripted STT queue, canned tone-WAV
TTS chunks, failure modes incl. empty audio), browser fallback descriptor (client
native SR/TTS), factory (mock default; browser; openai-audio REJECTED as a v0.4 seam
per CUT-1/G-7), config key AI_VOICE_PROVIDER + .env.example note. voice/ imports no
agents/api (AST-tested).
Task 5-1-03: DefenseStore (4th D-027 store; first FK family) — DefenseRecord +
DefenseTurn (ordered by (defense_id, seq)); start/append_turn/finalize/get/
list_for_learner; PRAGMA foreign_keys=ON for Postgres parity; integrity signals JSON
(A-109); store owns the finished transition.

34 voice tests green; ruff clean.

---ci---
phase: 5
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-006], partial: []}
---/ci---
2026-09-12 04:04:42 +00:00
CIAgent 97893f2386 chore(P04): checkpoint complete — phase 4 shipped as v0.2.5
---ci---
phase: 4
milestone: v0.3
status: complete
requirements: {covered: [REQ-3-005], partial: []}
---/ci---
2026-09-12 03:52:16 +00:00
CIAgent e63b996361 merge(P04): phase/04 variant generation → milestone/v0.3-credential-engines
---ci---
phase: 4
milestone: v0.3
status: ship
---/ci---
2026-09-12 03:51:36 +00:00
CIAgent 0b34255855 docs(P04): mark REQ-3-005 + phase 4 verified (anchors fix applied)
---ci---
phase: 4
milestone: v0.3
status: verify
requirements: {covered: [REQ-3-005], partial: []}
---/ci---
v0.2.5
2026-09-12 03:51:30 +00:00
CIAgent 6ab0ae2c0a fix(P04): ship variant anchors to the grader prompt (MH#4 second clause — verifier P1)
GradingEngine takes an optional VariantStore (constructor DI); when the graded
task_id joins to a stored variant: the template's difficulty anchors render into
the grader user turn ("Expected effort envelope" — same bar for every variant of
the template, a-5) and the variant seed is stamped on the GradeRecord (D-029).
Lifespan reordered: VariantStore builds before the engine and is passed in.
Anchors context carries only template id + anchor numbers — D-028 learner-anonymity
preserved (leak tests keep holding). Plain engine (no store) stays variant-blind;
non-variant tasks grade without the envelope.

3 new tests: variant task -> anchors + seed present in prompt/record;
non-variant task -> no envelope; plain engine -> variant_seed None.
Suite 327 green; ruff clean.

---ci---
phase: 4
milestone: v0.3
status: verify
requirements: {covered: [REQ-3-005], partial: []}
---/ci---
2026-09-12 03:51:25 +00:00
CIAgent 9ff86f9cd0 verify(P04): gaps_found — P0 docstring fixes; anchor->grader shipment is a P1 gap
Trivial P0 fixes (this commit): two docstrings claimed things the wiring
does not do. variants/__init__.py said the package 'never imports agents/'
— false since Wave 2: generator.py holds the module-direct
agents.structured import (the sanctioned D-020 shared defense, same
exception as grading/engine.py); docstring now states the real boundary.
templates.py (header + RubricAnchors) claimed rubric anchors are 'used by
grading context' / 'shipped to the grader' — false in the current wiring:
GradingEngine is variant-blind (variant_seed=None; no variant lookup; no
anchor consumption; render_trace_digest takes only the digest). Docstrings
now tell the truth and name the follow-up.

Verification (four layers, evidence in the phase report):
- Structural: AST boundary audit clean — variants/ has zero api/fastapi
  imports; only sanctioned agents.structured + llm/prompts/config/store.
- Behavioral: variants slice 41/41 green; full suite 324/324 green;
  pnpm typecheck 7/7 green (forced, no cache). Live app probes confirmed
  distinct learners -> distinct statements/seeds/task_ids at the API
  level, cache hit = zero LLM calls, deterministic fallback (calls==2),
  a-5 fairness envelope test green, sha256(template|learner|milestone)
  seed derivation verified byte-exact against the spec formula.
- Security: secrets scan over the P04 diff (b52bef9..4acffac, 17 files
  +2100/-1) clean — no key/token/password assignments, no URLs, no key
  shapes. Prompt-injection surface bounded: the variant prompt carries
  only template skeleton/title/id + seeded slot values — no learner id
  or user-controlled content reaches the LLM. Empty learner_id -> 422.
- Quality: ruff clean; AI_MODEL env override verified live; tests are
  mock-only (MockProvider family, zero network imports).

Must-Haves: 5 of 6 SATISFIED. NOT satisfied: MH#4 half — anchors are
present per template and a-5-testable, but NOT shipped to the grader
prompt context (non-trivial cross-module wiring: engine + prompt
signature + lifespan ordering — grading engine is built before the
variant store exists; reported as P1, not fixed here).

---ci---
phase: 4
milestone: v0.3
status: verify
requirements:
  covered: [REQ-3-005]
  partial: [REQ-3-005]
lessons:
  - Docstrings that describe a must-have's target state ('shipped to the
    grader prompt') read as done in review — verify wiring, not words:
    grep the consumer side (grading/) before believing the producer side.
  - main.py builds GradingEngine before variant_store exists; any P4/P6
    anchor-shipment fix must reorder lifespan construction or inject the
    variant store into the engine after the fact.
  - Params distinctness is parametric (280-64908 combos per template) —
    two learners CAN draw identical params (~15% at 10 learners on the
    tightest template); distinctness is proven via seeds/task_ids/statement
    embeddings, which is what the must-have actually requires.
---/ci---
2026-09-12 03:46:30 +00:00
CIAgent 4acffac71e feat(P04): variants API + TS types (Wave 3)
Task 4-3-01: POST /v1/variants (template_id or competency_id resolution; cache-first
D-029; 404 unknown template/competency; 422 neither), GET /v1/variants/{task_id},
GET /v1/variants?learner_id=. SQLiteVariantStore + VariantGenerator wired into lifespan.
Task 4-3-02: packages/types/variants.ts (TaskVariant, VariantParams) + grading.ts
(RubricScore + GradeRecord, criteria-typed) — field-for-field Python parity,
scores typed as the documented rubric-or-gate union.

11 endpoint tests green (distinct learners -> distinct statements at API level; cache
hit -> zero LLM calls); suite 324 green; typecheck 7/7; ruff clean.

---ci---
phase: 4
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-005], partial: []}
---/ci---
2026-09-12 03:21:11 +00:00
CIAgent 82b9de382a feat(P04): seeded variant generator (Wave 2)
Task 4-2-01: prompts/variant.py (render-only contract — the LLM never invents params;
slots change scenario, never difficulty) + variants/generator.py — seed =
sha256(template|learner|milestone) (D-029), pure-code seeded slot sampling, LLM render
via the D-020 defense with a deterministic skeleton-render fallback (never blocks on the
provider; the seed IS the provenance — no model column needed), cache-first (second call
= stored variant, zero LLM calls), deterministic task_id derivation.

Tests: distinct learners -> distinct statements; same learner -> cached, calls asserted;
params schema-valid; fallback deterministic + bounded retry (calls==2); unknown template
raises; a-5 fairness envelope BINDING — 10 seeded draws per template produce digests
inside the anchor bands (same bar testable); store roundtrip.

313 tests green; ruff clean.

---ci---
phase: 4
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-005], partial: []}
---/ci---
2026-09-12 03:08:45 +00:00
CIAgent 430b4a727d feat(P04): task template library + VariantStore (Wave 1)
Task 4-1-01: variants/templates.py — 3 task templates (llm-judge, guardrail-schema,
rag-chunker) bound to D-021 corpus competency IDs; typed ParameterSlots
(enum/int_range/string-set) with seeded pure-code sampler (random.Random(seed));
RubricAnchors difficulty-normalization envelope; starter-file scaffolds + test command.
Task 4-1-02: VariantStore protocol + SQLiteVariantStore (insert-only first-wins;
unique (learner,template) + unique task_id; WAL; tz-normalized) — the audit trail
for proctoring cross-checks.

22 variant tests green; ruff clean.

---ci---
phase: 4
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-005], partial: []}
---/ci---
2026-09-12 02:57:09 +00:00
CIAgent b52bef93e5 chore(P03): checkpoint complete — phase 3 shipped as v0.2.4
---ci---
phase: 3
milestone: v0.3
status: complete
requirements: {covered: [REQ-3-004], partial: []}
---/ci---
2026-09-12 02:49:33 +00:00
CIAgent e798c52edb merge(P03): phase/03 trace grading → milestone/v0.3-credential-engines
---ci---
phase: 3
milestone: v0.3
status: ship
---/ci---
2026-09-12 02:48:55 +00:00
CIAgent b303a41a45 docs(P03): mark REQ-3-004 + phase 3 verified
---ci---
phase: 3
milestone: v0.3
status: verify
requirements: {covered: [REQ-3-004], partial: []}
---/ci---
v0.2.4
2026-09-12 02:48:49 +00:00
CIAgent 3322c1dab9 chore(P03): checkpoint verify
---ci---
phase: 3
milestone: v0.3
status: verify
---/ci---
2026-09-12 02:48:15 +00:00
CIAgent 0bde9cbf2e verify(P03): passed — process-trace grading engine verified end-to-end on this box
Four layers all PASS:
- Structural: grading/ AST-audited — zero fastapi/api imports; the ONLY
  agents/ dependency is the module-direct agents.structured import
  (sanctioned D-020 shared defense, grep-auditable); no D-020 duplication
  inside grading/ (no extract_json/parse_structured/retry logic — engine
  composes agents/structured.py). api/assessment.py owns all FastAPI
  wiring; engine knows nothing of HTTP (DI via deps.py + main lifespan).
- Behavioral: grading slice 65/65 green; full suite 283/283 green. Digest-
  leak test green (planted SECRET-COMMAND-MARKER-7f3a absent from every
  provider message; digest JSON present; learner/task ids absent too —
  grading is learner-anonymous). G-4 tests assert provider.calls==0 at
  BOTH engine and HTTP level (gapped, INCOMPLETE_FLOODED, empty, both-
  signals cases). D-020 retry recovers a fenced wrong-shape first reply
  (calls==2, validation error fed back); persistent failure raises
  StructuredOutputError -> 502 with NOTHING persisted. Calibration
  ordering contract green (strong>=lazy on process, strong>struggling on
  correctness) + deterministic digest feature separation over the three
  D-021-aligned archetypes. Two composed ad-hoc probes (WS ingest ->
  grade over the real app) additionally proved the cross-surface loop:
  (a) WS flood -> INCOMPLETE_FLOODED flag -> POST grade -> 200
  UNGRADABLE_TRACE_INCOMPLETE, LLM never called, durable gate record
  via GET; (b) WS ingest of a complete iterative trace -> POST grade ->
  200 GRADED with digest computed IN CODE from the ingested events
  (edit_count=2, error_fix_cycles=1, final pass), marker/file-path/ids
  absent from the provider prompt through the full stack.
- Security: secrets scan over the phase diff (2474678..HEAD, 16 files
  +2958/-5) clean — no key/token/password assignments, no env/secret
  files touched (initial sk-regex hits were false positives inside
  'task-calibration'/'task_id'). Prompt-injection surface bounded:
  digest-only prompts; TraceDigest carries fixed numeric fields + small
  histograms, no raw commands/contents/payloads; bounded at 361 bytes
  even at 49,999 events (just under the G-3 cap). No PII reaches the LLM
  at all — stronger than the required learner/task-id-only bound.
- Quality: ruff check . clean; config env-overridable (AI_DB_PATH/
  AI_PROVIDER/A_MODEL override verified live); grading tests are
  mock-only (no network imports; providers are MockProvider family);
  GradeStore shares the D-027 store contract (WAL, tz-normalization,
  detached rows, protocol-wrapped, upsert-latest-wins documented).

Must-Haves 1-6 (PLAN.md Phase 3): all SATISFIED. REQ-3-004 covered.

P1/P2 for final review (no P0s found, no code changes needed):
- P1 (documented PLAN deviation, ratify): PLAN 3-3-01 said 'unknown trace
  -> 404' on POST; implementation returns 200 UNGRADABLE_EMPTY_TRACE for
  a POST of an unknown/empty pair (persisted first-class gate record —
  the engine cannot distinguish absent from empty), reserving 404 for GET
  of a never-graded pair. Reasoned in api/assessment.py + test docstrings.
- P2: RubricScore requires 1-2 strengths/gaps (min_length=1) — a model
  legitimately returning zero gaps burns a D-020 retry; deliberate
  strictness, low impact given the prompt demands both lists.
- P2: calibration is mock-scripted ORDERING through the real pipeline,
  not a live-LLM quality benchmark (honest scope documented in
  test_calibration.py); live-model calibration remains future work.
- P2: compute_digest is not idempotent over duplicate events, but
  unreachable — TraceStore dedups on (learner,task,seq) and the engine
  reads via get_trace.

---ci---
phase: 3
milestone: v0.3
status: verify
requirements:
  covered: [REQ-3-004]
  partial: []
lessons:
  - G-4 must stay gate-FIRST ordering in _grade(): integrity flag checked
    before gaps() because gaps() returns [] for an empty trace — swapping
    the order lets an INCOMPLETE_FLOODED-but-complete-row trace fall
    through to the LLM path.
  - Starlette TestClient WS session: the telemetry ingest protocol has NO
    per-event ack (only gap_warning/event_rejected/close frames) — probing
    the composed loop means send-then-verify-via-store, not read-ack.
  - pnpm is absent from PATH on this box (P01 lesson persists); the
    underlying venv pytest/ruff commands are what scripts/test.sh and
    lint.sh exec — running them directly is equivalent verification.
  - ingest and grading MUST share one TraceIntegrityMap instance via
    app.state; a second map in the engine would silently miss every
    INCOMPLETE_FLOODED mark (probe proved the shared-map loop works).
---/ci---
2026-09-12 02:48:10 +00:00
CIAgent 85028678c7 feat(P03): assessment grade endpoint (Wave 3)
Task 3-3-01: POST /v1/assessment/grade (engine over DI; GRADED -> 200 full record;
UNGRADABLE_* gate outcomes -> 200 gate records with missing_seqs/integrity_flag —
never 5xx, they are valid results; persistent D-020 failure -> 502, nothing persisted)
+ GET /v1/assessment/grade/{learner}/{task} (404 unknown). GradeStore wired into
lifespan alongside TraceStore (single db_path, per-connection WAL). Unknown task POST ->
200 UNGRADABLE_EMPTY_TRACE (engine cannot distinguish absent from empty; gate record
persisted so POST->GET round-trips).

11 endpoint tests green; suite 283 green; ruff clean.

---ci---
phase: 3
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-004], partial: []}
---/ci---
2026-09-12 02:34:47 +00:00
CIAgent 1a46606827 feat(P03): rubric scoring engine + calibration (Wave 2)
Task 3-2-01: prompts/grading.py (grader-v1 rubric, 4 criteria x 0-4 anchors, a-4 churn
advisory) + grading/engine.py — GradingEngine with the BINDING G-4 gate-first ordering
(INCOMPLETE_FLOODED -> gaps -> empty; LLM unreachable for gated traces; first-class
UNGRADABLE_* GradeRecords, model="none" provenance), digest-only prompts (D-028; planted
marker proven absent from all provider messages), D-020 reused via one module-direct
import of agents/structured (grep-auditable). RubricScore validated per-criterion.
Task 3-2-02: corpus/trace_fixtures.py (D-021-aligned archetype IDs) + ordering-contract
calibration test (strong>=lazy on process; strong>struggling on correctness; digest
feature separation asserted deterministically).

54 grading tests green; suite 272 green; ruff clean.

---ci---
phase: 3
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-004], partial: []}
---/ci---
2026-09-12 02:17:06 +00:00
CIAgent 0fccb8d250 feat(P03): trace digest + GradeStore (Wave 1)
Task 3-1-01: grading/features.py — compute_digest (D-028): deterministic features
(test pass/fail + final status w/ run_result exit-code fallback, edit count, error/fix
cycles + mean fix latency, idle gaps, command category histogram, session duration,
first-test-pass offset). TraceDigest pydantic model: bounded, no raw commands/contents/
payloads — raw trace never reaches the LLM (leak test enforces). Tolerates both live
trace mixes (activity+file_diff daemon topology; REPL kinds).
Task 3-1-02: grading/store.py — GradeStore protocol + SQLiteGradeStore (D-027 pattern:
WAL, tz-normalization, detached rows); upsert-latest-wins on regrade (documented
contrast vs TraceStore's append-only dedup).

20 grading tests green; suite 238 green (reconnect-flush flake under load fixed with a
20s deadline); ruff clean.

---ci---
phase: 3
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-004], partial: []}
---/ci---
2026-09-12 01:54:25 +00:00
CIAgent 2474678da6 chore(P02): checkpoint complete — phase 2 shipped as v0.2.3
---ci---
phase: 2
milestone: v0.3
status: complete
---/ci---
2026-09-12 01:41:31 +00:00
CIAgent 3f58fc3454 merge(P02): phase/02 live build telemetry → milestone/v0.3-credential-engines
---ci---
phase: 2
milestone: v0.3
status: ship
---/ci---
2026-09-12 01:41:21 +00:00
CIAgent edf586b03a docs(P02): mark REQ-3-003 + phase 2 complete
---ci---
phase: 2
milestone: v0.3
status: complete
requirements: {covered: [REQ-3-003], partial: []}
---/ci---
v0.2.3
2026-09-12 01:41:13 +00:00
CIAgent 4439486858 chore(P02): checkpoint verify
---ci---
phase: 2
milestone: v0.3
status: verify
---/ci---
2026-09-12 01:41:08 +00:00
CIAgent b49b9189fe fix(P02): destroy kills the inner namespace init, not just the unshare shim
Verifier-found P0: UnshareBackend.destroy reaped agent → inner → helper as
asyncio subprocesses, but the inner entry is the `unshare --fork` PARENT —
its forked child (the `sleep` that is PID 1 of the sandbox pid/mnt/net ns)
reparents to host init and survives, holding the tmpfs + workspace bind for
the sleep duration (3600s). Every tracked-sandbox destroy leaked one
namespace process: ~30 orphaned `sleep 3600` observed after one suite run.
`--kill-child` does not reach the child under this flag combo (verified
empirically: the child still survives parent SIGTERM).

Fix: SIGKILL the ns-init's host pid (already tracked as `inner_pid` for
nsenter) in destroy(), after reaping the agent so it cannot flush into a
dead sandbox. Regression test creates a REAL task sandbox, asserts agent +
ns-init alive, destroys, and asserts both host pids are gone — fails on the
old code, passes with the fix. Full suite 218 green; ruff clean.

---ci---
phase: 2
milestone: v0.3
status: verify
requirements: {covered: [REQ-3-003], partial: []}
---/ci---
2026-09-12 01:39:44 +00:00
CIAgent f75352d0f0 test(P02): dropped-connection durability probe + delivery semantics docs (Wave 4)
Task 2-4-01: tests/telemetry/test_durability.py — real uvicorn ingest + real namespace
sandbox + killable TCP proxy severing the agent's WS mid-stream: every event lands
exactly once, in order, after reconnect (spool fsync + server-side (learner,task,seq)
dedup). Proxy outage is flag-driven with self-closing pumps — external socket surgery
on loop-registered sockets deadlocks the child-watcher (documented in kill()).
README: telemetry delivery semantics (at-least-once delivery, exactly-once storage,
replay path, G-3 flood boundary).

Full suite 217 green; ruff clean.

---ci---
phase: 2
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-003], partial: []}
---/ci---
2026-09-12 01:18:41 +00:00
CIAgent 26b4a5be60 feat(P02): telemetry-wired sandbox spawn (Wave 3)
Task 2-3-01: create(learner_id, task_id) — telemetry sandboxes run a persistent
helper/inner namespace topology (offline inner ns; agent joins mount ns only and stays
online to reach the loopback ingest). Capture agent copied into the workspace (visible
in-ns at the bind), launched via sh -c with in-ns absolute paths (host cwd invalid after
the nsenter mount swap), stdin=DEVNULL daemonizes the agent (lifecycle tied to sandbox:
destroy reaps agent -> inner -> helper). Wire contract: frames strip URL-owned identity
(ingest extra=forbid anti-spoofing); spool keeps full events.

E2E test (real uvicorn on ephemeral port): exec in a live namespace sandbox -> events
arrive at WS ingest -> SQLite, ordered, sandbox-scoped. Pure-shell path (task_id=None)
asserts no capture agent. Full suite 216 green; ruff clean.

---ci---
phase: 2
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-003], partial: []}
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent b6850fc036 feat(P02): in-sandbox capture agent + websocket ingest (Wave 2)
Task 2-2-01: sandbox-agent.py stdlib-only (D-031) — shell capture (command/file_diff/
run_result/test_result/stdin/stdout), per-task monotonic seq, JSONL fsync spool (at-least-once,
D-026), raw-socket RFC6455 client (no websockets in-namespace), exponential-backoff reconnect,
SIGKILL-safe. Loopback tests: ordered emission, spool-on-disconnect, reconnect flush order,
stdlib-only AST scan.

Task 2-2-02: WS /v1/telemetry/ingest (D-026) — query-param identity (extra=forbid frames),
server-side dedup on (learner,task,seq), gap detection + warnings, ping keepalive. G-3 flood
control: overflow or >50000 events/task -> 1008 close + trace marked INCOMPLETE_FLOODED
(terminal, Proctor signal); no silent drop. GET traces + gaps endpoints. TraceIntegrityMap
(process-local, D-019 precedent) exposes is_incomplete for the Phase 3 grader gate (G-4).

214/214 + ruff clean.

---ci---
phase: 2
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-003], partial: []}
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent cfdceac17a feat(P02): telemetry models + TraceStore SQLite + TS types (Wave 1)
Task 2-1-01: TelemetryEvent (SQLModel table, composite PK learner/task/seq) + TraceSpan
derived view; SQLAlchemy @validates enforcement (sqlmodel drops Field constraints on table
models). Task 2-1-02: TraceStore protocol (D-019-mirrored) + SQLiteTraceStore with WAL +
synchronous=NORMAL (a-3); concurrent writer/reader no database-locked; idempotent
dedup-on-retry. Task 2-1-03: TS TelemetryEvent/TraceSpan mirroring Python field-for-field
(snake_case for byte-identical JSON); full typecheck green.

195/195 + typecheck pass; ruff clean.

---ci---
phase: 2
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-003], partial: []}
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent f0df18576e docs(P01): mark REQ-3-001/002 + phase 1 complete
---ci---
phase: 1
milestone: v0.3
status: complete
requirements: {covered: [REQ-3-001, REQ-3-002], partial: []}
---/ci---
v0.2.2
2026-09-12 00:47:45 +00:00
CIAgent 8a3296cd21 chore(P01): checkpoint complete — phase 1 shipped as v0.2.2
---ci---
phase: 1
milestone: v0.3
status: complete
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent df4c115bb7 chore(P01): checkpoint verify
---ci---
phase: 1
milestone: v0.3
status: verify
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent 76f15622d6 verify(P01): passed — sandbox fabric verified end-to-end on this box
Four layers all PASS:
- Structural: sandbox/ AST-audited — zero imports of api/, agents/, llm/;
  manager composed only via api/deps.py DI + main.py lifespan; no docker/
  podman/sudo/containerd in the spawner path (D-024 holds).
- Behavioral: pnpm ai:test = 174/174 green; sandbox+api slice 40/40 green
  with ALL probe-gated tests RAN (zero skips) — uid=0 in-ns, 0 network
  interfaces, workspace-write lands on host, no escape into snapshots,
  /proc-remount blocked, memory/CPU/FSIZE rlimits kill violators,
  wall-clock reaper proven, G-2 size sweep destroys + preserves snapshot,
  orphan startup reaper proven, 503/403/429 paths all live-verified via
  a real uvicorn probe (curl roundtrip: 201→list→snapshot→403→429→204→404).
- Security: no secrets in sandbox/api code or logs (sk-test key in tests is
  an anti-leak assertion fixture); CORS localhost-only, no credentials;
  documented gaps (userns != DAC write-barrier; in-ns children reparent to
  host PID 1 when supervisor is killed; per-sandbox NPROC shared at host
  uid) are present in unshare_backend.py, test_resource_limits.py, README,
  and are NOT relied on — RLIMIT_NPROC is never set, disk cap is the G-2
  sweep not a kernel quota.
- Quality: pnpm ai:lint = ruff clean; pnpm ai:bootstrap idempotent (exit 0);
  all config keys env-overridable (AI_SANDBOX_*); zero cloud calls in
  sandbox/api tests (mock-first verified by grep).

Must-haves 1-8 (PLAN.md Phase 1): all SATISFIED.

---ci---
phase: 1
milestone: v0.3
status: verify
requirements:
  covered: [REQ-3-001, REQ-3-002]
  partial: []
lessons:
  - util-linux 2.38 lacks unshare --bind; the in-namespace mount shim is the
    correct swap — keep the rlimit-on-payload-only ordering (bind, then
    ulimit, then exec) so unshare/mount stay unconstrained.
  - tmp_path under /tmp breaks the backend (in-ns tmpfs shadows host /tmp);
    repo-anchored tests/sandboxes/ is the load-bearing fixture choice.
  - pnpm on this box needs 'corepack enable' before turbo can resolve the
    package-manager binary; bootstrap env setup, not a repo defect.
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent b01de4be7d feat(P01): sandboxes lifecycle API + G-5 abuse control (Wave 3)
Task 1-3-01: /v1/sandboxes endpoints (create/list/get/snapshot/delete) over the manager
singleton via DI; lifespan boots the startup orphan reaper (a-1) + destroys all on shutdown.
Abuse control (G-5): learner allowlist (403 unknown id), per-learner active cap (429),
global create-rate cap (429). CORS gains DELETE. 174 full-suite tests green; manual probe
POST /v1/sandboxes -> 201 verified live; ruff clean.

---ci---
phase: 1
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-001, REQ-3-002], partial: []}
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent d1b933621b feat(P01): sandbox manager + resource-limit enforcement (Wave 2)
Task 1-2-01: SandboxManager — pool-full guard (503/D-032), create/list/get/snapshot/destroy,
reap_expired + workdir-size sweep (G-2 512MB), startup orphan reaper (a-1). In-memory
process-local registry (D-019 precedent). config.py gains the sandbox knobs.

Task 1-2-02: probe-guarded enforcement tests — all RAN green on this box (3.1s):
memory MemoryError kill, CPU SIGKILL at budget, RLIMIT_FSIZE truncation, wall-clock reaper,
NPROC shared-at-host-uid statically asserted (no fork-bomb per G-1). Documented honest
limitation: killing the supervisor does not kill in-namespace children (re-parent to host
PID 1); disclosed for the P7 release note.

155 full-suite tests green; real-UnshareBackend smoke runs on this box; ruff clean.

---ci---
phase: 1
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-001, REQ-3-002], partial: []}
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent 2d9f3201b3 feat(P01): sandbox backend + workdir + deps + config (Wave 1)
Task 1-1-01: SandboxBackend protocol + UnshareBackend (unshare user/mount/pid/net)
+ workdir layout (workspace/ + snapshots/). Isolation probe real on this box:
in-ns uid=0, network isolated (fresh net ns, lo only), writes contained to per-sandbox
bind dir; proc-remount not permitted (documented, not required). Deviations (documented):
util-linux 2.38.1 lacks --bind flag -> bind moved into namespace via sh -c mount shim;
rlimits moved into shim (preexec would kill the pytest interpreter); userns != DAC barrier
(host-uid-owned targets), documented for hardening under D-025.

Task 1-1-02: add sqlmodel, sqlalchemy, websockets, aiofiles (PyPI-verified); config keys
SANDBOX_DIR/MAX_CONCURRENT=5/TIMEOUT_S=900/MAX_WORKDIR_MB=512/DB_PATH (D-024/027/032/G-2).

Task 1-1-03: README sandbox isolation section with real probe transcript + locked
resource-limit mechanism (G-1/G-2); ruff config already correct (no change).

139/139 tests pass; ruff clean. .gitignore: ignore all ai-service sandboxes dirs (runtime+tests).

---ci---
phase: 1
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-001, REQ-3-002], partial: []}
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent 45b2162bec chore(P00): checkpoint complete — phase 0 shipped as v0.2.1
---ci---
phase: 0
milestone: v0.3
status: complete
---/ci---
v0.2.1
2026-09-12 00:47:45 +00:00
CIAgent d8fb56fb56 chore(P00): checkpoint mvp_ux_check 2026-09-12 00:47:45 +00:00
CIAgent a5b8be2e4d decision(P00): mvp/ux gate — verified present
PLAN.md contains all three mandatory sections with substantive content (post-GRILL CUT-2 wording):
- ## User-Facing Surface (learner build + defend flow; /build run/test+output panel, /defend live defense+grading)
- ## Happy Path (9-step end-to-end: variant -> sandbox build -> run/test telemetry -> defense -> grading -> verdict)
- ## UX Acceptance Criteria (10 criteria incl. no-mock-in-learner-path, 503 busy state, error/retry, build+typecheck+tests green)
Gate PASSES — EXECUTE unblocked. KYC deferred (A-110); voice mock+browser (CUT-1); run/test build panel (CUT-2).

---ci---
phase: 0
milestone: v0.3
status: mvp_ux_check
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent 18b9ceda93 chore(P00): checkpoint grill 2026-09-12 00:47:45 +00:00
CIAgent 0131da5b0d docs(P00): grill verdict — GO-WITH-CHANGES, binding decisions + scope cuts applied
G-1 resource-limit claims match mechanism (mem/cpu/fsize/wallclock kernel-enforced;
  per-sandbox pids + hard disk quota documented as accepted v0.3 gap, no cgroup/sudo)
G-2 disk cap via manager workdir-size sweep (AI_SANDBOX_MAX_WORKDIR_MB, default 512MB)
G-3 telemetry flood control: WS 1008 + INCOMPLETE_FLOODED trace (no silent drop-oldest)
G-4 grader refuses gapped/incomplete traces (UNGRADABLE_TRACE_INCOMPLETE verdict)
G-5 no-auth abuse control: per-learner caps + learner allowlist (ships despite KYC defer)
G-6 release note discloses partial resource enforcement (P7 honesty)
CUT-1 real server STT/TTS deferred to v0.4 (voice mock+browser-first)
CUT-2 interactive xterm shell relay deferred to v0.4 (build panel = run/test + output)
Advisories a-1..a-5 applied (startup reaper, RLIMIT_FSIZE, SQLite WAL, digest-gaming
  prompt note, variant fairness envelope)

---ci---
phase: 0
milestone: v0.3
status: grill
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent 717c46aadc chore(P00): checkpoint plan 2026-09-12 00:47:45 +00:00
CIAgent 044e4b8cc8 docs(P00): create phase plans — v0.3 credential engines
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---
2026-09-12 00:47:45 +00:00
CIAgent ec897336a5 chore(P00): checkpoint research 2026-09-12 00:47:45 +00:00
CIAgent 6be95cf2f6 docs(P00): research findings — credential engine architecture + personas
ARCHITECTURE.md:
- D-024 sandbox isolation via unshare Linux user/mount/pid/net namespaces
  (probe-verified: in-ns uid=0, network isolated, no container runtime/sudo on box)
- D-025 sandbox scope = coding IDE only (design/sim deferred to v0.4)
- D-026 telemetry = WebSocket ingest + SQLite ordered event log (seq gap detection)
- D-027 first persistence = SQLite, protocol-wrapped stores (Trace/Grade/Variant/Defense)
- D-028 trace grading = deterministic digest features + LLM rubric (LLM never sees raw trace)
- D-029 variant generation = seeded template instantiation, seed persisted
- D-030 voice = provider-agnostic mock-first STT/TTS + browser fallback (D-014 mirror)
- D-031 extend ai-service (no new apps); D-032 single-box 1-5 concurrent sandboxes
- New engine component tables + v0.3 build order

PERSONAS.md (lead-developer assessment):
- new sandbox-engineer + voice-engineer custom personas (phase-specific)
- backend-engineer territory extended to engine persistence/APIs; ai-engineer re-scoped
  to model-facing grading/variant/voice logic; security-auditor stays inactive (KYC deferred)

A-101 corrected post-probe: bwrap absent -> unshare namespaces (verified)

.gitignore: ignore SQLite data + sandbox runtime dirs

---ci---
phase: 0
milestone: v0.3
status: research
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent da9320bf4e docs(P00): clarify v0.3 ambiguities — auto-resolved (full autonomy)
A-101 bwrap user-namespace sandbox isolation; A-102 coding-IDE-only scope (design/sim deferred);
A-103 xterm.js+WebSocket build UX; A-104 WebSocket+SQLite telemetry transport; A-105 SQLite trace store;
A-106 LLM trace grader w/ code-computed deterministic features; A-107 seeded LLM variant generation;
A-108 provider-agnostic mock-first voice (STT/TTS); A-109 Examiner agent reuses BaseAgent+SSE;
A-110 KYC/age-gating deferred per founder directive; A-111 extend ai-service (no new apps);
A-112 single-box 1-5 concurrent sandbox capacity target.

All resolutions confidence >= 0.60.

---ci---
phase: 0
milestone: v0.3
status: clarify
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent 01286a02cb docs(init): validate specification — milestone v0.3 credential-engines
- config.json: milestone v0.3 credential-engines, branch milestone/v0.3-credential-engines
- PROJECT.md: v0.3 scope sandbox fabric, telemetry, trace grading, variant gen, voice defense;
  REQ-F-017 (identity/age-gating) explicitly deferred per founder directive
- REQUIREMENTS.md: activate REQ-F-007/008/009/010/021 as REQ-3-001..008; REQ-F-017 re-deferred to v0.4+
- ROADMAP.md: v0.3 phase structure P0-P7
- .gitignore: ignore .ciagent/bin/ (local credential helper)
- Clear stale v0.2 CHECKPOINT.json (milestone complete; new run starts at SPECIFY)

---ci---
phase: 0
milestone: v0.3
status: specify
---/ci---
2026-09-12 00:47:45 +00:00
CIAgent c4a083387a chore: forge migration git.cloudinit.dev -> git.coreci.dev
---ci---
phase: 7
milestone: v0.2
status: complete
---/ci---

Forge hard-cutover verified: old host TLS-dead, new host serving
repo with full history; coreci-bot token valid (push+admin);
all 12 releases migrated with IDs intact (#847-#864, v0.0.1-v0.2.2).

- config.json: release.base_url -> https://git.coreci.dev
- PROJECT.md: D-006 decision row records the migration
- origin remote URL updated (git remote set-url)
- local credential helper (.ciagent/bin, gitignored) now matches
  both hosts — new primary + legacy for older checkouts
2026-09-12 00:45:24 +00:00