diff --git a/.agent/phases/complete/21_thinking_no_scroll/02_e2e_story_suite_commit.md b/.agent/phases/complete/21_thinking_no_scroll/02_e2e_story_suite_commit.md deleted file mode 100644 index e92d2c7..0000000 --- a/.agent/phases/complete/21_thinking_no_scroll/02_e2e_story_suite_commit.md +++ /dev/null @@ -1,70 +0,0 @@ -# Task 02 — E2E story suite, story file, validation, commit - -**Phase:** `21_thinking_no_scroll` · **Source:** `TODO.md` L4 - -## Objective -The story gate: `tests/e2e/test_thinking_no_scroll.py` proves the window -can't be user-scrolled but always tracks the live tail, plus regressions, -story file, final validation, and the single atomic commit. - -## Work -1. `tests/e2e/test_thinking_no_scroll.py` (new — reuse - `test_thinking_display.py`'s mock-LLM streaming scaffolding; the mock - must stream a **long** thinking body, in many chunks, so - `.thinking-text` overflow exceeds its 320px box). Tests: - 1. `test_thinking_window_not_user_scrollable` — open the block, wait - until `scrollHeight > clientHeight`; focus `.thinking-text` - (`el.focus()`), dispatch mouse wheel over it - (`page.mouse.wheel(0, -200)` after moving the mouse over the - element) and press `Home`/`ArrowUp`: `scrollTop` must not decrease - (assert `scrollTop` unchanged within 1px between actions). - 2. `test_thinking_window_tracks_live_tail` — while chunks stream, - after the 2nd-to-last and last chunk: - `scrollTop === scrollHeight` (within 1px) — the visible window is - the live tail; the **last** chunk's text is within the visible - rectangle (its offsetTop + scrollTop geometry check, or - `elementFromPoint` at the box's bottom). - 3. `test_thinking_window_css_contract` — computed style of - `.thinking-text`: `overflow-y === "hidden"`, - `max-height === "320px"`. - 4. `test_answer_bubble_still_scrollable` (regression, phase 11) — a - long answer (use the long-answer mock from - `test_long_answers.py`): the answer bubble is still - user-scrollable (scrollTop moves on wheel) and - `overflow-y` is not `hidden` there. - 5. `test_restored_collapsed_thinking_unaffected` (regression, - phase 17) — a turn with stored `thinking`, reload: the collapsed - Thinking block renders with its text (existing pin from - `test_thinking_display.py` — replicate, don't duplicate the file). -2. `.agent/user_stories/thinking-no-scroll.md` (new) — story file per - the repo format: goal, the bug report verbatim from `TODO.md` L4, the - owner-confirmed A2 decisions from `00_phase.md`, E2E mapping table. -3. Run the suite **in isolation** (prereq `podman compose up -d db`): - `uv run pytest tests/e2e/test_thinking_no_scroll.py -v --no-cov`. -4. Regressions, in isolation, one command each: - - `uv run pytest tests/e2e/test_thinking_display.py -v --no-cov` - - `uv run pytest tests/e2e/test_long_answers.py -v --no-cov` -5. Final validation: `uv run pytest` green; `uv run pytest --cov=app - --cov-report=term-missing` ≥ today's number (>90% gate); - `uv run ruff check . && uv run pyright` clean. -6. **UI Structure Check** (AGENTS.md rule 5): no new surface; the block - keeps its summary chevron, focus-visible ring, aria-live/label - contract, and the reduced-motion stillness (styles.css ~line 686). -7. Write the phase report (`.agent/reports/21_thinking_no_scroll/`). -8. Commit (one atomic commit) and move the phase: - ```bash - git add -A .agent/ frontend/ tests/ - git commit --no-gpg-sign -m "fix(ui): thinking window no longer scrolls — live 320px view pinned to the stream tail" - mv .agent/phases/todo/21_thinking_no_scroll .agent/phases/complete/ - ``` - -## Testing & Quality -- Story suite green **in isolation**; both regression suites green in - isolation; full unit+integration suite green; `app/` coverage at or - above today's number (>90%); ruff + pyright clean. - -## Completion Criteria -- [ ] `test_thinking_no_scroll.py` 5/5 in isolation. -- [ ] Regressions (thinking display, long answers) green in isolation. -- [ ] Story file + phase report exist. -- [ ] One `--no-gpg-sign` commit; phase directory in `complete/`. diff --git a/.agent/phases/complete/22_background_animation/01_fix_background_animation.md b/.agent/phases/complete/22_background_animation/01_fix_background_animation.md deleted file mode 100644 index 22b9e41..0000000 --- a/.agent/phases/complete/22_background_animation/01_fix_background_animation.md +++ /dev/null @@ -1,60 +0,0 @@ -# Task 01 — Diagnose and fix the animated background (styles.css) - -**Phase:** `22_background_animation` · **Source:** `TODO.md` L5 — -*"Fix background animation not working, just blinking"* - -## Objective -Find why the phase-08 animated background reads as "just blinking" and -fix `styles.css` so the grid drift and the glow breathe are both visibly -and smoothly alive, per the phase-08 design comments (pure CSS, zero JS). - -## Work -1. **Reproduce:** `uv run uvicorn app.main:app --reload` (db up), open `/` - in a visible Chromium window (Playwright or the interactive browser), - observe ≥15s. Note exactly what moves and what doesn't. -2. **Diagnose per the `00_phase.md` checklist** (per-layer visibility - toggles, `background-position` samples on `body::before`, mask - inspection, occlusion check against `html`/`body` rules, glow - opacity-swing perception). Record findings + before-screenshot in - `.agent/reports/22_background_animation/` and - `.agent/screenshots/22_background_animation/`. - **ASSUMPTION (to verify, not assume):** the likely culprits, in - order — (a) the masked grid drift is too faint/slow to perceive, - (b) only the glow opacity swing is visible and it reads as a blink, - (c) a later rule occludes the `z-index: -1` layers. Confirm which - one actually fires before touching CSS; the fix must match the found - cause. -3. **Fix in `frontend/assets/styles.css`** (smallest change that makes - the design read): - - grid: raise line alpha and/or the mask's visible radius and/or the - drift speed as needed for a clearly visible, seamless drift - (drift delta must still equal one 44px cell for a seamless loop — - if the speed changes, keep `background-position` 0→44px and only - move the duration); - - glow: if the breathe reads as a blink, narrow the opacity delta - (e.g. 0.8↔1) and/or lengthen the period — it must read as - breathing, not pulsing; - - keep: both layers `position: fixed; inset: 0; z-index: -1; - pointer-events: none`; no `filter: blur`; no JS; palette/contrast - untouched. -4. **After-screenshot** (same viewport, two frames a few seconds apart - showing motion) into the same screenshots dir. -5. `tests/unit/test_background_animation.py` (new — repo source-pin - pattern): pin the **final** `styles.css` values — both - `@keyframes` present, `body::before` → `bg-grid-drift linear - infinite`, `body::after` → `bg-glow-breathe`, both layers - `fixed`/`z-index: -1`/`pointer-events: none`, `html` keeps - `background: var(--bg)`, `body` keeps `background: transparent`. -6. Manual re-verify: the "just blinking" perception is gone — smooth - drift + gentle breathe, no jank, no static frame. - -## Testing & Quality -- `uv run pytest tests/unit/test_background_animation.py -v` green. -- `uv run ruff check . && uv run pyright` clean. - -## Completion Criteria -- [ ] Root cause documented (with before/after screenshots) in the - phase report dir. -- [ ] Both layers visibly animate as the phase-08 design describes; - pure CSS, zero JS, no blur. -- [ ] Unit pins green against the final values; lint/types clean. diff --git a/.agent/phases/complete/23_containerfile_build/00_phase.md b/.agent/phases/complete/23_containerfile_build/00_phase.md deleted file mode 100644 index 3160eec..0000000 --- a/.agent/phases/complete/23_containerfile_build/00_phase.md +++ /dev/null @@ -1,120 +0,0 @@ -# Phase 23 — Containerfile: Build the Whole App Image Again - -**Source:** `TODO.md` L6 — *"Fix Containerfile build not working"* -**Story:** `.agent/user_stories/containerfile-build.md` (created by task 02) -**Context:** `Containerfile` (3 stages: node:22-alpine + esbuild -0.25.5 frontend bundle → uv/python deps → slim runtime serving -`/app/static`); `frontend/` (4 pages: `index.html`, `sources.html`, -`document.html`, `login.html`; assets: `styles.css`, `markdown.js` -(classic script), `header.js`/`app.js`/`sources.js`/`document.js`/ -`login.js` (ES modules)); `scripts/entrypoint.sh`. - -## Verified diagnosis (2026-08-24, this conversion — not a guess) -1. **Root cause of the build failure:** phase 19 switched the page - scripts to `import … from "/assets/header.js"` (an absolute URL). - esbuild resolves that as the *filesystem* path `/assets/header.js` - and the stage-1 bundle dies: - `✘ [ERROR] Could not resolve "/assets/header.js"` - (reproduced with esbuild **0.25.5**, the exact pinned version, on a - copy of `frontend/`). -2. **Secondary gap (image would be broken even if it built):** stage 1 - bundles only `app.js` + `sources.js` and copies only `index.html` + - `sources.html`. Missing from the image: `document.html` + - `login.html` (phases 10/16), `document.js` + `login.js`, and - `markdown.js` (classic script loaded by `index.html` + - `document.html`). -3. **Verified fix:** with relative imports (`from "./header.js"`) all - four page scripts bundle cleanly with esbuild 0.25.5. -4. **Latent double-evaluation trap:** all four HTML pages also load - ``; - - `frontend/sources.html` (~line 125), `frontend/document.html` - (~line 80), `frontend/login.html` (~line 67) — same tag. - - Update the surrounding HTML comments that describe the - header-before-page-script load order (e.g. index.html ~lines - 115–119): the order is now guaranteed by the page script's own - `import` (hoisted, evaluated before the page script body calls - `initSharedHeader()`). -4. **`Containerfile` stage 1** — cover the whole app (keep the pinned - `esbuild@0.25.5` and the existing flags): - ```dockerfile - RUN mkdir -p /out/assets \ - && esbuild ./assets/app.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/app.js \ - && esbuild ./assets/sources.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/sources.js \ - && esbuild ./assets/document.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/document.js \ - && esbuild ./assets/login.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/login.js \ - && esbuild ./assets/markdown.js --minify --outfile=/out/assets/markdown.js \ - && esbuild ./assets/styles.css --minify --outfile=/out/assets/styles.css \ - && cp ./index.html ./sources.html ./document.html ./login.html /out/ - ``` - (`markdown.js` is a classic script — minify only, **no** `--bundle`; - it exposes globals used by the pages.) -5. **Verify locally (no podman):** with esbuild 0.25.5, all four module - bundles + the markdown minify succeed on the real `frontend/` (not a - copy — the copy was only for the diagnosis). -6. **`podman build -f Containerfile .`** → green. -7. **Image smoke test** (results + log excerpt into the report dir): - - throwaway Postgres 17 + pgvector (`podman compose up -d db` and - point the container at it, or a one-off container with the same - env as `compose.yaml`); - - run the built image (migrations run via the entrypoint); - - `GET /`, `/sources.html`, `/document.html`, `/login.html` → 200; - - `GET /assets/app.js` → 200, minified (single-line-ish), and - contains the header code (e.g. the `clearChatStorage` function - body); `GET /assets/markdown.js`, `/styles.css`, the other three - page modules → 200; - - No CDN rule: none of the four served pages contain an `http(s)://` - `src`/`href` asset reference. - - Teardown the throwaway containers when done. -8. **Dev-server regression check** (the tag removal touches dev page - load — confirm boot order still holds): `uv run uvicorn - app.main:app --reload`, load all four pages, check the sign-out - binding exists exactly once (DevTools: no duplicate listener — one - `POST /api/logout` per click) and `initSharedHeader()` ran. (The - isolated E2E regressions run in task 02.) - -## Testing & Quality -- Steps 5–8 above; `uv run ruff check . && uv run pyright` clean - (no Python changes, but keep the gate green). - -## Completion Criteria -- [ ] The recorded build failure is fixed at the root cause (relative - imports) — not masked by an alias/patch. -- [ ] All four direct `header.js` tags removed + comments updated; the - page scripts' `import "./header.js"` is the only header load. -- [ ] Stage 1 produces: 4 HTML pages, 4 bundled modules, minified - `markdown.js`, minified `styles.css`. -- [ ] `podman build` green; image smoke all-200 + No CDN + single - header evaluation; dev-server boot unchanged (step 8). -- [ ] Log/screenshot evidence in `.agent/reports/23_containerfile_build/`. diff --git a/.agent/phases/complete/23_containerfile_build/02_integration_test_commit.md b/.agent/phases/complete/23_containerfile_build/02_integration_test_commit.md deleted file mode 100644 index 812a8a8..0000000 --- a/.agent/phases/complete/23_containerfile_build/02_integration_test_commit.md +++ /dev/null @@ -1,70 +0,0 @@ -# Task 02 — Integration coverage test, story file, validation, commit - -**Phase:** `23_containerfile_build` · **Source:** `TODO.md` L6 - -## Objective -Pin the stage-1 asset coverage so it can't silently rot again (a new -page/script/asset without a matching Containerfile line fails CI), plus -regressions, story file, final validation, and the single atomic commit. - -## Work -1. `tests/integration/test_containerfile_assets.py` (new — **hermetic**: - parses `Containerfile` + `frontend/` as text, no podman, no network). - Tests: - 1. `test_every_html_page_is_copied_into_stage1` — for each - `frontend/*.html` in the repo, a stage-1 line copies it into - `/out` (regex over the `cp` line; the set must be exactly the - four current pages — a new page added to `frontend/` fails this). - 2. `test_every_local_asset_reference_is_produced` — collect every - local `src=`/`href=` under `assets/` or `/assets/` from the four - HTML files; each basename must be produced by a stage-1 line - (an `esbuild … --outfile=/out/assets/` or a `cp` of it). - (This is what catches a missing `markdown.js`-style gap.) - 3. `test_page_module_scripts_are_bundled` — the set of `type="module"` - page scripts referenced by the HTML (basenames) equals the set of - scripts esbuild bundles in stage 1 (`app.js`, `sources.js`, - `document.js`, `login.js`). - 4. `test_header_module_is_imported_not_directly_loaded` — no HTML - file contains a `` alongside the existing `app.js` module script). `index.html` already loads `markdown.js` as a classic script (needed by `renderDocument`). -4. `frontend/sources.html` — load `document-modal.js` as a module (it needs `document.js`'s `renderDocument`, so both `document.js` and `document-modal.js` must be module scripts; `markdown.js` classic script stays). The Sources page currently loads `sources.js` as a module; add the modal module script next to it. -5. Verify the no-CDN integration test (`tests/integration/test_api.py::test_index_html_served_locally`) still passes — the new module scripts are same-origin `` (no leading slash!), and `` **before** the page's module script on every page (classic script → runs at parse time; the module scripts execute later). No other template changes — the walker + attribute pass is the single mechanism; do **not** add `data-brand` markers. The phase-33 `?v=` rewriting picks the new ref up automatically (`app/core/caching.py` matches any `src="…assets/…"`). -3. `frontend/assets/app.js` — replace the "Brain of Reese" literals with `window.BOR_BRAND` reads: the `UI_STATE` labels (~L107: "… is thinking" / "… is answering"), `TYPING_LABEL` (~L112), the elapsed-hint aria-label (~L540). Pattern: `const brand = () => window.BOR_BRAND || "Brain of Reese";` + template strings. (Mid-turn staleness: a label set before the fetch lands keeps the old name for that turn — accepted, see the phase's locked decisions.) -4. `frontend/assets/document.js` — the page titles (L147/152: `${doc.title} · Brain of Reese` / `"Document not found · …"`) → use the same `window.BOR_BRAND` read (document.js is a module — `window.BOR_BRAND` is set by then). -5. `Containerfile` — add the esbuild line for the new file next to the others (L17–23 pattern, classic script like `markdown.js`): `esbuild ./assets/brand.js --minify --outfile=/out/assets/brand.js`. -6. Verify the no-op property: with the default settings the rendered DOM text is byte-identical to pre-phase on all five pages (the replace is a no-op for the default name) — the existing suites' title/label assertions are the guard; if any assert a string this task moved onto `window.BOR_BRAND`, the default path must render the identical bytes. - -## Testing & Quality -- No Python logic — the story E2E (task 03) is the gate; the existing suites (which assert the default "Brain of Reese" titles/labels against the shared conftest server) must stay green **unchanged**. -- No CDN (rule 6): no new external tags. UI Structure Check (rule 5): no landmark/contrast change — the brand text keeps its existing classes and styling (the `innerHTML` rewrite only re-emits the same structure with the new name). - -## Completion Criteria -- [ ] `BOR_APP_NAME` unset → all five pages render exactly as today (existing suites green). -- [ ] `BOR_APP_NAME="Brain of Testy"` → title/header/greeting/labels/aria all carry the new name (asserted by the story E2E, task 03). -- [ ] The Containerfile build includes brand.js; the asset ref is versioned like its siblings (phase 33). -- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean. diff --git a/.agent/phases/complete/39_configurable_brand/03_e2e_docs_commit.md b/.agent/phases/complete/39_configurable_brand/03_e2e_docs_commit.md deleted file mode 100644 index 25f7e3e..0000000 --- a/.agent/phases/complete/39_configurable_brand/03_e2e_docs_commit.md +++ /dev/null @@ -1,27 +0,0 @@ -# Task 03 — Story E2E + docs + commit - -**Phase:** `39_configurable_brand` · **Source:** `TODO.md:12 — "Also need a way to customize the name for 'Brain of'. Should be an env var."` -**Story:** `.agent/user_stories/configurable-brand.md` - -## Objective -The story's isolated Playwright suite against an app instance booted with the overridden name, the env docs, and the phase commit. - -## Work -1. `tests/e2e/test_configurable_brand.py` (the story gate — one story, one file, run in isolation): - - A **second app instance** — the shared `app_server` conftest fixture keeps the default name (the other suites' title/label assertions depend on it). Copy the conftest `app_server` env block (same DB, the mock-LLM base URL, `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET`, `BOR_STATIC_DIR`, `BOR_RELEVANCE_THRESHOLD`) with two changes: `BOR_APP_NAME="Brain of Testy"` and a distinct port (`APP_PORT + 1` per the conftest convention). A session-scoped fixture **inside the test file**, started after `mock_llm` is available. - - Assertions (custom instance): index `document.title` == `"Brain of Testy"`; the `.brand-text` `innerHTML` == `Brain of Testy`; the empty-state h1 text == `"Hey! I'm Brain of Testy."`; the `#messages` `aria-label` == `"Conversation with Brain of Testy"`; the sources page title `"Sources · Brain of Testy"`; the login page title `"Sign in · Brain of Testy"`; one chat turn with a pre-token window (the `think out loud` marker) → the button label shows `"Brain of Testy is thinking"`. - - Default-name assertion (cheap regression in the same file): the shared conftest server's index title still == `"Brain of Reese"`. - - The chat-turn assertion works on the default (possibly empty) KB — a deflected answer is fine; the label assertion is pre-token, so no DB seeding is required. -2. `.env.example` — document `BOR_APP_NAME` in the App section (the display name on all pages; default "Brain of Reese"). -3. README — the configuration section: `BOR_APP_NAME` (what it affects: titles, the header brand, the status labels, the aria text; the default; the bold-split rendering rule: names starting "Brain of " bold the remainder, any other name renders in normal weight). -4. Regression pass: `uv run pytest` + the coverage gate (>90%) + the isolated story E2E + the suites that assert brand strings (`test_smoke.py`, `test_shared_header.py`, `test_header_consistency.py`, `test_chat_persistence.py`) green. -5. Commit — one atomic `--no-gpg-sign` Conventional Commits commit for the whole phase (AGENTS.md rule 8), e.g. `feat(brand): configurable app name — BOR_APP_NAME drives /api/config + the frontend brand layer`; move the phase directory to `.agent/phases/complete/`. - -## Testing & Quality -- The gates above are this task's quality bar (A16: one story, one isolated E2E file, coverage >90%). - -## Completion Criteria -- [ ] The story E2E is green in isolation, deterministic across two consecutive runs (custom-name instance + the default-name assertion). -- [ ] The step-4 regression list green; coverage >90%. -- [ ] `uv run ruff check . && uv run pyright` clean. -- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`. diff --git a/.agent/phases/complete/40_tuning_toggle_flash/01_toggle_ships_hidden.md b/.agent/phases/complete/40_tuning_toggle_flash/01_toggle_ships_hidden.md deleted file mode 100644 index 2b1fdc7..0000000 --- a/.agent/phases/complete/40_tuning_toggle_flash/01_toggle_ships_hidden.md +++ /dev/null @@ -1,26 +0,0 @@ -# Task 01 — Toggle ships hidden, admin-only reveal - -**Phase:** `40_tuning_toggle_flash` · **Source:** `TODO.md:3` — "Loading the page briefly shows the 'Tuning' button in the header even when the user isn't authenticated. Only show that if the user is authenticated." -**Story:** `.agent/user_stories/tuning-toggle-flash.md` - -## Objective -Ship `#steering-toggle` `hidden` in all six pages and unhide it in `header.js` only when whoami says admin — zero flash for anonymous, identical admin UX. - -## Work -1. `frontend/index.html`, `frontend/sources.html`, `frontend/document.html`, `frontend/git-sources.html`, `frontend/login.html`, `frontend/tuning.html` — add the `hidden` attribute to the existing `#steering-toggle` ` - ``` - - give the existing nav the id: `