diff --git a/docs/handoff/wave-1-evidence-spine-briefs-2026-06-12.md b/docs/handoff/wave-1-evidence-spine-briefs-2026-06-12.md new file mode 100644 index 00000000..6b1e1315 --- /dev/null +++ b/docs/handoff/wave-1-evidence-spine-briefs-2026-06-12.md @@ -0,0 +1,890 @@ +# Wave 1 Evidence Spine — Dispatch Briefs + +Date: 2026-06-12 +Plan: `docs/workbench/wave-1-evidence-spine.md` +Merge target: `main` + +## Dispatch DAG + +``` +Wave A (parallel — no dependencies between them): + Brief 1: Backend Journal + Trace API [GPT5.5] feat/wb-journal + Brief 2: Evidence Primitives [Claude] feat/wb-primitives + Brief 3: Mutation Doctrine Docs [GPT5.5] feat/wb-mutation-docs + +Wave B (after Brief 2 merges): + Brief 4: Evidence Context + Inspector + [Claude] feat/wb-evidence-ui + Command Registry + +Wave C (after Briefs 1 + 4 merge): + Brief 5: Trace Route [GPT5.5] feat/wb-trace-route +``` + +Merge order: `3 → (1 ∥ 2) → 4 → 5` +Brief 3 can merge anytime (docs-only). Briefs 1 and 2 are parallel and +independent. Brief 4 requires Brief 2 on main. Brief 5 requires Briefs 1 +and 4 on main. + +--- + +## Brief 1 — Backend Journal + Trace API + +**Agent:** GPT5.5-Thinking +**Scope:** Python backend only. No frontend, no React, no TypeScript. + +### Worktree setup + +```bash +cd /Users/kaizenpro/Projects/core +git fetch origin +git worktree add ../core-wb-journal origin/main -b feat/wb-journal +cd ../core-wb-journal +``` + +### Brief + +You are building the Workbench Turn Evidence Journal — a local, append-only +JSONL record of the `ChatTurnResult` envelope already returned by +`/chat/turn`. This is a read model, not runtime memory, not teaching memory, +and not a cognitive runtime fork. + +**Why this exists:** The workbench backend currently does NOT attach a +telemetry sink to chat turns. `WorkbenchApi()` constructs with no sink, +`_run_chat_turn()` creates a bare `ChatRuntime()`, and +`serialize_turn_event` redacts content by default. The journal records the +exact evidence the operator already saw, so the Trace route (Brief 5) can +display it honestly. + +**Read before writing code:** +- `docs/workbench/wave-1-evidence-spine.md` — section 1D (your spec) +- `docs/workbench/api-contract-v1.md` — existing contract, especially line 231 + (three surfaces must stay separate) +- `workbench/api.py` — current `_run_chat_turn()` implementation +- `workbench/schemas.py` — existing schema patterns +- `workbench/server.py` — request routing +- `workbench/readers.py` — existing reader patterns + +**Deliverables:** + +1. `workbench/journal.py` + - `TurnJournalEntry` frozen dataclass: `turn_id` (int, sequential), + `timestamp` (ISO-8601 UTC), `trace_hash`, `prompt`, `surface`, + `articulation_surface`, `walk_surface`, `grounding_source`, + `epistemic_state`, `normative_clearance`, `verdicts` (identity/safety/ + ethics), `refusal_emitted`, `hedge_injected`, `proposal_candidates`, + `turn_cost_ms`, `checkpoint_emitted`, `journal_digest` (SHA-256) + - `TurnJournalSummary` frozen dataclass: `turn_id`, `timestamp`, + `prompt_excerpt` (first 120 chars), `surface_excerpt` (first 120 chars), + `trace_hash`, `grounding_source` + - `TurnJournal` class: `__init__(journal_dir: Path)`, `append(entry)`, + `list_summaries(limit, offset)`, `get_entry(turn_id)`, + `next_turn_id() -> int` + - Journal path: `workbench_data/turn_journal.jsonl` + - JSONL is pure — every line valid JSON. No text headers. + - `journal_digest` = SHA-256 of the canonical JSON serialization of the + entry (excluding the digest field itself) + - Append-only: no update, no delete, no truncation + +2. `workbench_data/README.md` + - Content-bearing warning: journal entries contain user prompts and engine + surfaces. This file stores evidence already returned to the local + operator. Not teaching memory. Not runtime memory. + +3. Wire journal into `workbench/api.py` + - `WorkbenchApi.__init__` creates a `TurnJournal` instance + - After a successful `/chat/turn` response, append the result to the + journal before returning the HTTP response + - `turn_id` assigned by `TurnJournal.next_turn_id()` + - Add `turn_id` to the existing `ChatTurnResult` response so the frontend + can reference it + +4. New API routes in `workbench/api.py` + - `GET /trace/turns?limit=50&offset=0` — returns journal summaries in + stable sequential order, inside `{ ok, generated_at, data: { items } }` + envelope + - `GET /trace/{turn_id}` — returns full journal entry for a turn, or 404 + for unknown turn_id. Never return synthetic data. + - Update route dispatch in `do_GET` / `do_POST` handlers + +5. Schema additions in `workbench/schemas.py` + - `TurnJournalEntrySchema` and `TurnJournalSummarySchema` for API + serialization (following existing patterns in the file) + +6. Tests in `tests/test_workbench_journal.py` + - Append-only: entries never modified or deleted + - Stable ordering: sequential by turn_id + - Prompt/content size limits: max 4096 chars prompt (matching existing + `/chat/turn` validation) + - Path confinement: journal writes only to `workbench_data/` + - No journal writes to `teaching/`, `packs/`, `language_packs/data/` + - No NEW writes to `engine_state/` beyond existing chat checkpoint behavior + (ADR-0146/0150) + - Journal digest is deterministic for identical content + - Round-trip: `/chat/turn` response fields == `/trace/{turn_id}` fields + - Unknown turn_id returns 404 + - Pagination: limit/offset produce correct slices + - Empty journal returns empty items list, not error + +**Constraints:** +- Use Python stdlib only (no FastAPI, no pydantic, no new dependencies) +- Follow existing `workbench/` code patterns (frozen dataclasses, readers) +- All three surfaces stored as separate fields — never merge them +- `workbench_data/` should be gitignored (add to `.gitignore` if not present) +- Run `uv run python -m pytest tests/test_workbench_journal.py -q` green +- Run `uv run python -m pytest tests/test_workbench_api.py -q` still green + (existing tests must not break) +- Run `core test --suite smoke -q` green + +### Work completed when + +- [ ] `workbench/journal.py` exists with `TurnJournal`, `TurnJournalEntry`, + `TurnJournalSummary` +- [ ] `/chat/turn` appends to journal and returns `turn_id` +- [ ] `GET /trace/turns` returns paginated summaries from real journal +- [ ] `GET /trace/{turn_id}` returns full entry or 404 +- [ ] `workbench_data/README.md` documents content-bearing nature +- [ ] `workbench_data/` is gitignored +- [ ] All tests green: journal tests + existing workbench tests + smoke + +### Update the plan + +In `docs/workbench/wave-1-evidence-spine.md`, check off every box under +section 1D (Backend, New API endpoints, Tests). Do NOT check off section 1E +(Trace Route frontend) — that is Brief 5. + +### PR and merge + +```bash +cd ../core-wb-journal +git add workbench/journal.py workbench_data/README.md tests/test_workbench_journal.py +git add workbench/api.py workbench/schemas.py workbench/server.py +git add .gitignore docs/workbench/wave-1-evidence-spine.md +# Include any other changed files +git commit -m "feat(workbench): turn evidence journal + trace API (Wave 1D) + +Append-only JSONL journal records the exact ChatTurnResult envelope +returned by /chat/turn with stable turn_id, trace_hash, all three +surfaces, verdicts, and deterministic journal_digest. + +GET /trace/turns and GET /trace/{turn_id} serve journal evidence +for the Trace route frontend (Brief 5). + +Read model only — no teaching/pack/engine_state mutation." +``` + +Push and open PR against `main`: +```bash +git push -u origin feat/wb-journal +gh pr create --title "feat(workbench): turn evidence journal + trace API (Wave 1D)" \ + --body "$(cat <<'EOF' +## Summary +- Adds `workbench/journal.py` — append-only JSONL turn evidence journal +- Wires journal into `/chat/turn` response path +- Adds `GET /trace/turns` and `GET /trace/{turn_id}` API endpoints +- Part of Wave 1 evidence spine (`docs/workbench/wave-1-evidence-spine.md`) + +## Test plan +- [ ] `uv run python -m pytest tests/test_workbench_journal.py -q` +- [ ] `uv run python -m pytest tests/test_workbench_api.py -q` +- [ ] `core test --suite smoke -q` +- [ ] Manual: `core workbench api`, POST a chat turn, GET /trace/turns +EOF +)" +``` + +### Worktree cleanup + +After merge: +```bash +cd /Users/kaizenpro/Projects/core +git worktree remove ../core-wb-journal +git branch -d feat/wb-journal +``` + +--- + +## Brief 2 — Evidence Primitives + +**Agent:** Claude (Opus 4.6) +**Scope:** React/TypeScript frontend only. No Python, no backend changes. + +### Worktree setup + +```bash +cd /Users/kaizenpro/Projects/core +git fetch origin +git worktree add ../core-wb-primitives origin/main -b feat/wb-primitives +cd ../core-wb-primitives +``` + +### Brief + +Build the six evidence primitives that Wave 1 needs. These are shared +design-system components used by the Trace route, Inspector, Command +Registry, and future Wave 2 routes. Build them with the same care as +building an oscilloscope panel — precise, token-driven, keyboard-navigable, +dark, calm. + +**Read before writing code:** +- `docs/workbench/wave-1-evidence-spine.md` — section 1C +- `docs/workbench/design-system.md` — existing design system baseline +- `workbench-ui/src/design/tokens/tokens.css` — actual token names +- `workbench-ui/src/design/tokens/tokens.ts` — TypeScript token mirror +- `workbench-ui/src/design/components/` — existing component patterns + (badges, primitives, states, StableJsonViewer) +- `workbench-ui/src/preview/PreviewPage.tsx` — preview page structure + +**Deliverables — six components under +`workbench-ui/src/design/components/`:** + +1. `SplitPane/SplitPane.tsx` + - Resizable horizontal or vertical split via drag handle + - Props: `direction: 'horizontal' | 'vertical'`, `defaultSplit: number` + (percentage), `minSize: number` (px), `children: [ReactNode, ReactNode]` + - Drag handle uses `cursor: col-resize` or `row-resize` + - Persists split position in localStorage (keyed by caller-provided id) + - Respects `prefers-reduced-motion` for any resize animation + +2. `TabBar/TabBar.tsx` + - Accessible tab bar with ARIA `role="tablist"`, `role="tab"`, + `role="tabpanel"` attributes + - Do NOT add `@radix-ui/react-tabs` as a dependency — implement with + native ARIA semantics + - Props: `tabs: Array<{ id: string, label: string }>`, + `activeTab: string`, `onTabChange: (id: string) => void`, + `children: ReactNode` (renders the active panel) + - Keyboard: `ArrowLeft`/`ArrowRight` moves focus between tabs, + `Enter`/`Space` activates, `Home`/`End` jumps to first/last + - Active tab gets `aria-selected="true"` + visual indicator via + `--color-border-accent` + +3. `MetadataTable/MetadataTable.tsx` + - Key-value pair display for structured metadata + - Props: `rows: Array<{ key: string, value: ReactNode, copyable?: boolean }>` + - Keys rendered in `--font-size-xs` with `--color-text-secondary` + - Values rendered in `--font-size-sm` with `--color-text-primary` + - Copyable values show a copy icon on hover, click copies to clipboard + - Monospace font (`--font-mono`) for hash/digest/numeric values + - Stable row ordering (render in array order, no sorting) + +4. `DigestBadge/DigestBadge.tsx` + - Copyable hash/digest display with truncation and verification indicator + - Props: `digest: string`, `algorithm?: string` (default 'sha256'), + `verified?: boolean | null`, `truncate?: number` (default 16) + - Display: `sha256:abc123de...` with monospace font + - Click copies full digest to clipboard, shows brief "Copied" feedback + - Verified indicator: green dot (true), red dot (false), gray dot (null) + - `aria-label` includes full digest for screen readers + +5. `Timestamp/Timestamp.tsx` + - Relative + absolute time display + - Props: `iso: string` (ISO-8601 UTC), `format?: 'relative' | 'absolute' + | 'both'` (default 'both') + - Relative: "2m ago", "3h ago", "yesterday" (updates on 60s interval) + - Absolute: formatted in PST/PDT (America/Los_Angeles) per user preference + - Hover tooltip shows the other format (if relative shown, tooltip shows + absolute, and vice versa) + - `