# Phase 15 — Tune How Brain Answers (Steering Notes) **Story:** `.agents/user_stories/steering-notes.md` **Context:** owner report 2026-08-22 — "the answers are off. Add a feature that lets me 'tune' the output if I think an answer isn't quite right. This tuning should be added to the database and read in with the system prompt to steer the replies." ## Goal A first-class **steering** loop: under any answer, "Tune" → short instruction → stored in Postgres (`steering_notes`) → injected into the system prompt of **every** subsequent turn → observable in the reply. Notes are listed and deletable in a "Tuning" panel. ## Implementation steps 1. **Schema** — `alembic/versions/0003_steering_notes.py`: `steering_notes(id UUID PK, note TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`; downgrade drops the table. Model `SteeringNote` in `app/models.py`. 2. **API** — `app/api/steering.py` (stateless, A10): - `GET /api/steering` → `{notes: [{id, note, created_at}]}` newest first. - `POST /api/steering` `{note}` (trimmed, 1–2000 chars; 422 on empty/over-long) → 201 with the created note. - `DELETE /api/steering/{note_id}` → 204; 404 unknown id. Mount in `app/main.py`; schemas in `app/schemas.py`. 3. **Prompt** (`app/rag/prompts.py`): - `build_steering_section(notes: Sequence[str]) -> str` — `""` when empty; else `…` with numbered notes, capped at `steering_max_chars` (new setting, default 8000; `[…truncated…]` marker). **Both** HIGH and DEFLECT prompts carry it (after `…`, before ``/DEFLECT_MODE body). With zero notes the prompt is byte-identical to today. - Preserve the owner's working-tree persona edits (no "you've got this" / no mandated deflection opening — align `tests/unit/test_prompts.py` verbatim check with the current text; record as a PLAN §6 revision). 4. **Chat turn** (`app/api/chat.py`): load notes (created_at asc), pass into `plan_turn` → prompts; per-turn log line gains `tuning=N` (PLAN §9). 5. **E2E mock** (`tests/e2e/mock_llm.py`): when the system prompt contains ``, the composed answer ends with ` (tuning: )` — makes prompt injection observable in the UI deterministically. 6. **UI** (`frontend/index.html`, `app.js`, `styles.css`): - "Tune" button (`.tune-btn`, ghost, ≥44px) in the meta row of every completed brain bubble (deflected included). - Inline `.tune-form`: labeled textarea (maxlength 2000) + Save / Cancel → `POST /api/steering` → success `.tune-saved` (role=status: "Saved — future answers will follow this.") or inline error (role=alert), form kept on failure. - Header `#steering-toggle` "Tuning" + count badge; `#steering-panel` (region) above the messages: notes newest-first, per-note delete (labeled); empty text; count updates on add/delete. - a11y: aria-expanded/controls on the toggle, aria-live announcements for save/delete, focus-visible, Phase-08 tokens (all ≥4.5:1). 7. **Docs:** README "Tuning your answers" section; PLAN §4/§5/§6/§9/§7.5 + roadmap rows 11–15. ## Locked decisions None broken: A10 (stateless endpoints), A13 (alembic), A16 (one E2E suite), A11 (no library). New table + new setting only. ## Testing & Quality - **Unit:** steering section (empty/one/many/budget truncation), prompt placement in both modes, persona-verbatim check aligned to the owner's current persona. - **Integration:** steering CRUD (201/200/204/404/422, ordering, validation); chat turn with a stored note → fake LLM's captured system prompt contains the note (HIGH **and** LOW); log line `tuning=N`. - **Coverage:** `uv run pytest --cov=app --cov-report=term-missing` **>90%**. - **E2E:** `tests/e2e/test_steering.py` per the story mapping. - **Regression:** `test_chat_rag.py`, `test_honest_deflection.py`, `test_retrieval_quality.py` green in isolation (zero-note prompt is byte-identical, so behavior is unchanged until a note exists). ## Commit ```bash git add -A .agents/ alembic/ app/ frontend/ tests/ README.md && git commit --no-gpg-sign -m "feat(rag): steering notes — tune how Brain answers, stored in Postgres and injected into every system prompt" ```