refactor(agents): migrate .agent/ planning tree to .agents/

Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
This commit is contained in:
2026-09-05 10:57:07 -04:00
parent 766702c750
commit dbf2af26c6
1118 changed files with 664 additions and 664 deletions
@@ -0,0 +1,48 @@
# Phase 65 — Chat Action Cluster to the Pinned Bottom
**Source:** `TODO.md` L3 — "Move the new chat and share button to the tune/retry/save doc cluster area so it's always at the bottom of the screen and easily accessble. Make the button clusters look better, neater, more aligned"
**Story:** n/a (TODO-derived — owner roadmap confirmation 2026-09-01)
**Context:** Verified layout inventory (audited 2026-09-01, line numbers as of this writing):
- `frontend/index.html` — the `.chat-actions` row (New chat + Share pills, ONE wrapper) sits at the **TOP** of the 46rem chat column: the comment block runs L126–148, the row div is L149, positioned between `#steering-announcer` (L129) and `<section class="messages" id="messages">` (L193). The pinned composer form follows the messages at L225. In any real conversation the row scrolls off-screen the moment the reader is at the bottom — the exact defect this phase removes.
- `frontend/assets/styles.css` — `.chat-actions` (L433: horizontal row, `align-items: center`, `gap: 0.6rem`, intrinsic pill widths); `.new-chat-btn` (L301) / `.share-chat-btn` (L335) — solid brand pill family, `min-height: 44px`, `border-radius: 999px`; `.composer` (L1226) — `position: sticky; bottom: env(safe-area-inset-bottom, 0)`, no z-index (the phase-52 pin, owner-locked 2026-08-30); `.messages` — `flex: 1 1 auto` + `min-height: 200px` (the grow that rests the bottom at the screen on short chats); the per-answer meta cluster: `.msg-meta` (L641), `.tune-btn` (L694), `.retry-btn` (L723), `.save-as-doc-btn` (L756) — ghost pill family, `min-height: 44px`, 14px icons, `margin(-inline-start): auto` pushes Tune/Retry/Save-as-doc to the row's **right edge** ("Save as doc" is the bottom-right action of every completed brain bubble — the app's action corner). The ≤640px block (L3049) flips `.chat-actions` to a full-width vertical stack.
- `frontend/assets/header.js` L336–339 — binds `#new-chat-btn` and dispatches the `bor:new-chat` window event; `frontend/assets/app.js` L1700 listens and resets the conversation (it owns the in-flight-turn guard); `app.js` L246/L2067 binds `#share-chat-btn` to `shareCurrentChat` (the empty-chat no-op writes `Nothing to share yet.` to `#send-status`). All bindings are id-based and **position-independent** — the move is pure HTML/CSS.
- `frontend/assets/app.js` `copyShareLinkWithFallback` (L1424) appends the clipboard-fallback field to the **composer** (not the row) — unaffected by the move.
- Existing suites that constrain the design: `tests/e2e/test_pinned_composer.py` (the `#composer` computed `position` must stay `sticky`; the composer box flush with the viewport bottom at EVERY scroll position inside the sticky range; settles back into flow above the footer at the document bottom; NO z-index on the composer; the empty chat must not be scrollable, `sh <= ch + 1`); `tests/e2e/test_save_share_ux.py::test_action_row_layout` (ONE `.chat-actions`, DOM order New chat → Share, horizontal row at 1280px with intrinsic pill widths, full-width stack at 390px, no horizontal overflow at 360px — all RELATIVE geometry, nothing pins the row's vertical position); `tests/e2e/test_chat_persistence.py` (`#new-chat-btn` ≥44px, click resets the conversation, `New chat started` in `#send-status`).
- `.agents/` is tracked and committed (AGENTS.md rule 8) — the phase commit stages `.agents/` + `frontend/` + `tests/`.
## Objective
The New chat + Share pills live in the **bottom cluster** of the chat column — the same screen zone as the tune/retry/save-doc meta actions — and are **always at the bottom of the screen** (pinned with the composer, visible even while scrolled up reading a long conversation). The two button clusters read as neat, aligned sets: the bottom row hugs the column's right edge, mirroring the right-aligned "Save as doc" corner above it.
## Dependencies
- `64_sync_upload_progress` (todo, preceding — no functional dependency; ordering by number)
## Tasks
1. `01_move_chat_actions_to_bottom.md` — relocate the `.chat-actions` row (buttons + comments byte-identical) to the last child of `.chat-shell`, directly above the composer; in-flow at first.
2. `02_pin_bottom_cluster.md` — wrap row + composer in a sticky `.chat-bottom` unit (A1): the cluster is pinned at the viewport bottom at every scroll position and settles into flow above the footer at the document bottom.
3. `03_align_button_clusters.md` — right-align the bottom row to the column's right edge (A2); verify the five action pills share one geometry (44px targets, 999px radius, global focus-visible).
4. `04_e2e_bottom_chat_actions.md` — the dedicated Playwright suite `tests/e2e/test_bottom_chat_actions.py` (run in isolation): pinned at every scroll position, resting at the screen bottom, settled above the footer, geometry + DOM order, mobile stack, both buttons still work.
5. `05_regressions_and_commit.md` — regression E2E matrix, full `validate.sh`, one atomic commit.
## Testing & Quality
- E2E (mandatory, house rule — one file per phase, run in isolation): `tests/e2e/test_bottom_chat_actions.py` against the shared conftest server (DB up, mock LLM, seeded KB — the `test_pinned_composer.py` patterns for overflow conversations and scroll measurement).
- Frontend-only phase: `app/` is untouched, so the coverage gate stays satisfied at its current level — `validate.sh` (unit + integration >90% + ruff + pyright) must still pass end to end.
- Regression E2E (each in isolation): `test_pinned_composer.py`, `test_save_share_ux.py`, `test_chat_persistence.py`, `test_share_chat.py`, `test_chat_history.py`, `test_smoke.py`.
## Completion Criteria
- [ ] The `.chat-actions` row is the bottom cluster of `.chat-shell` — above the composer, inside the sticky `.chat-bottom` — with DOM order New chat → Share preserved and all ids/aria-labels intact.
- [ ] Over-viewport conversation, scrolled to the top: the row is fully visible, pinned at the viewport bottom above the composer, at every scroll position inside the sticky range; at the document bottom the cluster settles into flow above the footer (no overlap).
- [ ] Empty chat at 1280×800: the row rests at the bottom of the screen, the document is not scrollable.
- [ ] Desktop: the row's right edge aligns with the column's right edge (A2); ≤640px: full-width vertical stack; no horizontal overflow at 360px.
- [ ] `#new-chat-btn` click resets the conversation (`New chat started` in `#send-status`); `#share-chat-btn` on an empty chat is a no-op (`Nothing to share yet.`) — the bindings survived the move.
- [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` green in isolation (DB up); the six regression E2E suites green in isolation.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner-locked (2026-09-01, roadmap confirmation, A1):** *pinned bottom cluster* — a `.chat-bottom` wrapper (`position: sticky; bottom: env(safe-area-inset-bottom, 0)`, NO z-index) groups `.chat-actions` + `#composer` as one sticky unit, so the pills are literally always at the bottom of the screen — the rejected alternative (plain in-flow row) would scroll away while reading up a long conversation. The `#composer`'s own sticky declaration STAYS (redundant inside the wrapper — it cannot shift within its containing block — but `test_pinned_composer.py` pins the computed style `sticky`).
- **Owner-locked (2026-09-01, roadmap confirmation, A2):** *right-aligned bottom row* — the New chat + Share row hugs the column's RIGHT edge on desktop (so the bottom-right reads as one aligned action column with the right-aligned "Save as doc" buttons above); the ≤640px stack stays full-width (alignment is moot when stretched).
## Commit
```bash
git add .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(web): move the chat action cluster to the pinned bottom and align the button sets"
```
@@ -0,0 +1,28 @@
# Task 01 — Move the `.chat-actions` row to the bottom of the chat column
**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "Move the new chat and share button to the tune/retry/save doc cluster area so it's always at the bottom of the screen and easily accessble…"
**Story:** n/a (TODO-derived)
## Objective
The New chat + Share row leaves the TOP of the column (where it scrolls off-screen the moment a conversation grows) and becomes the last child of `.chat-shell`, directly above the pinned composer — the same screen zone as the tune/retry/save-doc meta cluster. This task is the MOVE only (in-flow); the sticky pin lands in task 02 and the alignment in task 03.
## Work
1. `frontend/index.html`:
- Cut the ENTIRE `.chat-actions` block — the comment at L126–148 PLUS the `<div class="chat-actions">…</div>` at L149–187 (both buttons, every line of their comments included) — from between `#steering-announcer` (L129) and `<section class="messages" id="messages">` (L193).
- Paste it as the LAST child of `.chat-shell`: between the messages `</section>` (L207) and the composer comment block (L209), so each comment stays glued to its own element and the form (L225) is untouched. Indent to match the sibling elements (6 spaces); keep the blank-line separation between the four children.
- The two buttons stay BYTE-IDENTICAL: ids (`#new-chat-btn`, `#share-chat-btn`), classes, `aria-label`s, SVGs, the label spans, and the DOM order **New chat → Share** (pinned by `test_save_share_ux.py::test_action_row_layout`).
- Update the row's comment block in place (it moves with the row): keep the Phase 55/14/51 history, and APPEND a Phase 65 note — the row was relocated from the top of the column to the bottom (above the composer) per `TODO.md` L3 (owner confirmation 2026-09-01), so the cluster sits where the tune/retry/save-doc meta actions live; the top of the column is now banner → steering → announcer → messages.
2. No CSS changes in this task: the row's styling is position-independent (`.chat-actions` at `styles.css` L433; the ≤640px stack rule at L3049). As a column child it inherits `.chat-shell`'s `gap: 1rem` above (from `.messages`) and below (to the composer) — acceptable until task 02/03 set the wrapper gap.
3. No JS changes: every binding is id-based and position-independent — `frontend/assets/header.js` L336–339 (`#new-chat-btn` → `bor:new-chat` dispatch), `frontend/assets/app.js` L1700 (the listener), L246 + L2067 (`#share-chat-btn` → `shareCurrentChat`). The clipboard-fallback field already appends to the composer (`app.js` L1424), not the row.
4. Leave UNTOUCHED: `#kb-banner`, `#stale-banner`, `#steering-panel`, `#steering-announcer`, `#messages`, `#composer`, and every comment except the row's own moved block.
## Testing & Quality
- `uv run pytest` green (unit + integration — no Python changed, gate stays green); coverage TOTAL **>90%**; `uv run ruff check . && uv run pyright` clean.
- Highest-risk existing suite for this move: `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (DB up) — it pins the row's geometry (single `.chat-actions`, DOM order, horizontal/stacked, intrinsic widths, 360px overflow) but nothing about its vertical position, so the move must not perturb it.
## Completion Criteria
- [ ] In `frontend/index.html`, the `.chat-actions` div appears AFTER `</section>` (the messages close, L207) and immediately before the composer form — it is the last child of `.chat-shell`; nothing remains at the old position (grep: exactly one `.chat-actions` in the file).
- [ ] `grep -n "new-chat-btn\|share-chat-btn" frontend/index.html` shows both buttons with unchanged ids/classes/aria-labels, New chat before Share.
- [ ] The row's comment block moved with the row and carries the Phase 65 relocation note.
- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
- [ ] `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (DB up).
@@ -0,0 +1,46 @@
# Task 02 — Pin the bottom cluster (`.chat-bottom` sticky unit)
**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "…so it's always at the bottom of the screen and easily accessble."
**Story:** n/a (TODO-derived)
## Objective
The row + composer become ONE pinned unit (locked A1): at every scroll position inside the sticky range the bottom cluster sits flush with the viewport bottom — the pills are literally always at the bottom of the screen, even while the reader is scrolled up through a long conversation — and at the document bottom the cluster settles back into normal flow above the footer (never floating over it).
## Work
1. `frontend/index.html`:
- Wrap the `.chat-actions` div (task 01's position) AND the `<form class="composer">…</form>` (incl. its comment block) in a new `<div class="chat-bottom">` … `</div>` that is the single LAST child of `.chat-shell` (before the shell's closing `</div>`, currently L237). No id (nothing in JS binds it — the bindings are on the inner elements).
- Append a Phase 65 note to the row's comment (A1): the wrapper makes row + composer one sticky unit — `position: sticky; bottom: env(safe-area-inset-bottom, 0)`, NO z-index (the sticky header stays on top — the phase-46 stacking pinned by `test_pinned_composer.py`); the composer's OWN sticky declaration is kept (redundant inside the wrapper — its containing block is the wrapper, so it cannot shift — but `test_pinned_composer.py` asserts the computed style `sticky` on `#composer`).
2. `frontend/assets/styles.css` — new rule directly above the `.composer` rule (L1226):
```css
/* Phase 65 (owner-locked A1, 2026-09-01, TODO.md L3): the pinned
bottom cluster — the .chat-actions row + the #composer form as
ONE sticky unit ... (full house-style rationale: sticky range =
the .chat-shell containing block; the .messages flex-grow still
rests the unit at the screen bottom on short chats; no z-index,
so the phase-12 sticky header (z 20) and the phase-46 dropdown
always stay on top; the composer's own sticky rule stays —
redundant here, pinned by tests/e2e/test_pinned_composer.py). */
.chat-bottom {
display: flex;
flex-direction: column;
gap: 0.5rem;
position: sticky;
bottom: env(safe-area-inset-bottom, 0);
}
```
- `gap: 0.5rem` is the row↔composer spacing INSIDE the wrapper (the wrapper is one column child, so `.chat-shell`'s 1rem gap applies only ABOVE it, from `.messages`).
- The `#composer` rule itself is NOT modified (its `position: sticky; bottom: env(...)` stays — see the note above).
- The ≤640px block needs no change: the wrapper is a column flex box, so the stacked full-width pills (the existing L3049 `.chat-actions` rule) simply make the unit taller on phones.
3. Update the `.composer` rule's phase-52 comment minimally: the pin now belongs to `.chat-bottom`; the composer's own declaration is retained for the computed-style test pin (one line, no deletion of the phase-52 history).
4. Leave UNTOUCHED: `.messages` (`flex: 1 1 auto` + `min-height: 200px` — the grow that absorbs free space on short chats still works: the wrapper is the last child, so the grow pushes the WHOLE unit down), every z-index in the file, and all JS.
## Testing & Quality
- `uv run pytest` green; `uv run ruff check . && uv run pyright` clean; coverage TOTAL **>90%** (gate stays green — no Python changed).
- The load-bearing regression: `uv run pytest tests/e2e/test_pinned_composer.py -v --no-cov` green in isolation (DB up) — it re-verifies the whole pin contract WITH the wrapper: composer box flush with the viewport bottom at every scroll position, settled into flow above the footer at the document bottom, `#composer` computed `position === "sticky"` and `zIndex` in {auto, 0}, under the 64px header on a 375×812 phone, and the empty chat still not scrollable (`sh <= ch + 1` — the wrapper adds no height: the row already existed in the column, task 01).
- `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (row geometry unchanged by the wrapper).
## Completion Criteria
- [ ] `frontend/index.html`: exactly one `.chat-bottom` div, the last child of `.chat-shell`, containing exactly `.chat-actions` then `#composer` (in that order).
- [ ] `frontend/assets/styles.css`: the `.chat-bottom` rule above `.composer` with `position: sticky; bottom: env(safe-area-inset-bottom, 0)` and NO z-index declaration; the `#composer` rule unmodified.
- [ ] Browser check via `test_pinned_composer.py` green in isolation — in particular `test_composer_pinned_at_every_scroll_position` (flush at 0/25/50/75% of the sticky range), `test_empty_chat_composer_sits_at_the_screen_bottom` (no scrollable space), `test_pin_holds_on_mobile_under_the_header` (no header overlap, no z-index).
- [ ] `test_save_share_ux.py` green in isolation.
@@ -0,0 +1,27 @@
# Task 03 — Align the two button clusters (neater, more aligned)
**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "Make the button clusters look better, neater, more aligned"
**Story:** n/a (TODO-derived)
## Objective
The bottom row and the per-answer meta cluster read as ONE tidy, aligned system (locked A2): the New chat + Share row hugs the column's RIGHT edge on desktop — mirroring the right-aligned "Save as doc" action of every brain bubble above it — so the bottom-right of the chat is a single aligned action column. The five action pills share one geometry (≥44px targets, 999px radius, the global focus-visible ring).
## Work
1. `frontend/assets/styles.css` — the `.chat-actions` rule (L433): add `justify-content: flex-end;` so the row hugs the column's right edge on desktop. The pills keep their intrinsic widths (never stretched — `align-items: center` already beats the column default, and `justify-content` only shifts the group).
- The ≤640px block (L3049: `.chat-actions { flex-direction: column; align-items: stretch; gap: 0.5rem; }`) is UNCHANGED — with stretched full-width pills, horizontal alignment is moot (the stacked pills already edge-to-edge the column).
- Update the rule's comment: the row is the bottom cluster's top member (task 01/02), right-aligned per A2, and the vertical row↔composer spacing is the `.chat-bottom` wrapper's 0.5rem gap (task 02).
2. Five-pill geometry pass (VERIFY — fix only if a deviation exists; the expectation is "no declaration changes"):
- `.new-chat-btn` (L301) / `.share-chat-btn` (L335): solid brand family — `min-height: 44px`, `border-radius: 999px`, `border: 0`, 16px icons (hidden on desktop, icon-only ≤640px), `white-space: nowrap`.
- `.tune-btn` (L694) / `.retry-btn` (L723) / `.save-as-doc-btn` (L756): ghost family — `min-height: 44px`, `border-radius: 999px`, `padding: 0.35rem 0.8rem`, 14px icons, `margin-left/auto` right-alignment, `font-size: 0.82rem`.
- If any pill measures <44px tall at 360–1280px (deviation), bring it to the family's `min-height: 44px` here — do not invent new visual languages; the two families (solid = chat-level actions, ghost = per-answer actions) are intentional (phase 55 A2).
3. No HTML changes, no JS changes, no changes to the meta-row layout (`.msg-meta` L641 stays as-is — its internal order, chips-then-actions, is pinned by the phase 49/59 comment history).
4. No new CSS beyond the one declaration + comments.
## Testing & Quality
- `uv run pytest` green; `uv run ruff check . && uv run pyright` clean; coverage TOTAL **>90%** (gate stays green).
- `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation — its desktop assertions (Share right of New chat, both pills < column/2 wide, ONE `.chat-actions`) hold for a right-aligned row; the right-EDGE alignment itself is pinned by this phase's suite in task 04.
## Completion Criteria
- [ ] `.chat-actions` rule carries `justify-content: flex-end;` with the A2 rationale in its comment; the ≤640px stack rule is byte-identical.
- [ ] All five action pills (New chat, Share, Tune, Retry, Save as doc) report ≥44px rendered height at 360px, 390px, and 1280px widths (quick manual/Playwright measurement during the task; the standing pin lands in task 04).
- [ ] `test_save_share_ux.py` green in isolation (DB up).
@@ -0,0 +1,43 @@
# Task 04 — Dedicated E2E: `tests/e2e/test_bottom_chat_actions.py`
**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "…so it's always at the bottom of the screen and easily accessble. Make the button clusters look better, neater, more aligned"
**Story:** n/a (TODO-derived)
## Objective
One dedicated Playwright suite (house rule: one file per phase, run in isolation) pins the NEW contract — the bottom cluster is always at the bottom of the screen, right-aligned, still stacked/clickable — so a future refactor cannot silently move the pills back to the top.
## Work
1. `tests/e2e/test_bottom_chat_actions.py` (NEW — self-contained, house patterns):
- Header docstring: Source `TODO.md` L3, run-in-isolation command `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` (DB up: `podman compose up -d db`), the owner-locked A1/A2 contracts, and the test→contract mapping (house "Playwright Mapping Rule").
- Fixtures/helpers: reuse the conftest `page` (1280×800), `app_url`, `mock_llm`, `db_ready`; copy the house `_reset_db` (TRUNCATE `chunks, documents, query_log, steering_notes, saved_chats`) + threaded `_import_fixtures` KB-seed pattern from `tests/e2e/test_pinned_composer.py` (13 fixture docs) into a `seeded_kb` fixture; `login` from `tests/e2e/auth_helpers.py` is NOT needed (chat is public).
- Test 1 `test_empty_chat_row_rests_at_screen_bottom` (no KB seed needed — static markup):
- fresh `/`: exactly ONE `.chat-bottom`, the row inside it; the document is NOT scrollable (`scrollHeight <= innerHeight + 1` — the wrapper must not invent scrollable space);
- the row's box is in the LOWER part of the viewport (row bottom ≥ 75% of the viewport height) and sits directly ABOVE the composer (row bottom ≤ composer top + 4px, no overlap);
- A2: the row's RIGHT edge aligns with the `.chat-shell` right edge (±2px);
- the composer's bottom is not clipped below the viewport.
- Test 2 `test_bottom_cluster_pinned_at_every_scroll_position` (`seeded_kb`, the `test_pinned_composer.py` overflow pattern — 6 short grounded turns from that file's `SHORT_QUESTIONS`):
- assert the overflow precondition (`scrollHeight > innerHeight + 200`);
- compute the sticky range from the `.chat-bottom`'s CONTAINING BLOCK (`.chat-shell` document-bottom, same math as `test_pinned_composer.py::test_composer_pinned_at_every_scroll_position`) and assert `pin_limit > 200` and `max_scroll > pin_limit`;
- at scroll y ∈ {0, 25%, 50%, 75%, pin_limit−1}: the `.chat-bottom` box is FULLY inside the viewport with its bottom flush with the viewport bottom (±4px) — the row AND the composer are visible at every reading position (this is the TODO's "always at the bottom");
- at the document bottom: the cluster settles into flow — NO overlap with `.app-footer`, the composer is no longer glued to the viewport edge (composer bottom < viewport bottom − 4px), and the row is still directly above the composer.
- Test 3 `test_row_geometry_and_alignment` (no conversation needed for the row; one seeded turn for `.retry-btn`):
- exactly one `.chat-actions` inside `.chat-bottom`; exactly one `#new-chat-btn` + one `#share-chat-btn` in it; DOM order New chat → Share (`compareDocumentPosition` — same check as `test_save_share_ux.py`);
- desktop 1280×800: one horizontal row (overlapping y-bands), Share right of New chat, each pill's width < column width/2 (intrinsic, never stretched), row right edge == `.chat-shell` right edge (±2px, A2);
- mobile 390×844: vertical stack — Share BELOW New chat, both pills the same width, each == the `.chat-bottom` content width (±2px, full-width stretch);
- 360×800: `document.documentElement.scrollWidth <= 360` (no horizontal overflow);
- touch targets: `#new-chat-btn`, `#share-chat-btn` (and, after one seeded turn, the injected `.retry-btn` on the last brain bubble) all render ≥44px tall at BOTH 1280 and 390 widths.
- Test 4 `test_buttons_still_work_from_the_bottom` (`seeded_kb`, one turn so a real conversation exists):
- `#new-chat-btn` click → `.msg` count 0, `#empty-state` visible, `localStorage["bor.chat.v1"]` null, `#send-status` contains "New chat started" (the header.js → `bor:new-chat` → app.js chain survived the move);
- on the now-empty chat, `#share-chat-btn` click → `#send-status` contains "Nothing to share yet.", NO `.toast`, URL unchanged (the empty-conversation no-op guard, `app.js` L1494–1497).
- Determinism: mock-only answers (`MOCK_ANSWER_MARKER`), settled-state assertions (`wait_settled` pattern — `#send-label` back to "Send"), no scroll calls from the app itself (the phase-42 never-auto-scroll contract — the test does all scrolling via `window.scrollTo`).
2. No other file changes in this task.
## Testing & Quality
- `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` green in isolation (DB up) — run it after EACH of the four tests is written, not all at once.
- `uv run pytest` green (unit + integration — no Python app changed); `uv run ruff check . && uv run pyright` clean (the new test file must pass both).
- Coverage TOTAL **>90%** (validate.sh gate — unchanged, no `app/` code).
## Completion Criteria
- [ ] The four tests exist, are named as specified, and are green in isolation (DB up).
- [ ] The suite asserts the A1 pin (wrapper flush at every scroll position), the A2 right-edge alignment, the mobile stack, the 360px overflow bound, the DOM order, and BOTH click-through contracts (New chat reset, Share no-op).
- [ ] Full suite green, lint + types clean, coverage >90%.
@@ -0,0 +1,33 @@
# Task 05 — Regression matrix, full gate, atomic commit
**Phase:** `65_bottom_chat_actions` · **Source:** `TODO.md:3` — "Move the new chat and share button to the tune/retry/save doc cluster area so it's always at the bottom of the screen and easily accessble. Make the button clusters look better, neater, more aligned"
**Story:** n/a (TODO-derived)
## Objective
Prove the move + pin + alignment broke nothing in the adjacent contracts (the composer pin, the phase-55 action row, chat persistence, sharing, history) and land ONE atomic commit.
## Work
1. Regression E2E — each in isolation, DB up (run in this order; stop and fix the FIRST failure, do not batch-patch):
- `uv run pytest tests/e2e/test_pinned_composer.py -v --no-cov` (the composer pin — the most entangled contract with the new wrapper);
- `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` (the phase-55 row geometry + auto-save + share flows);
- `uv run pytest tests/e2e/test_chat_persistence.py -v --no-cov` (the New chat reset contract, incl. the ≥44px pin);
- `uv run pytest tests/e2e/test_share_chat.py -v --no-cov` (the full share lifecycle — the button moved under it);
- `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` (history restore + the Share button on the chat page);
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` (front-door sanity).
2. The dedicated suite one final time: `uv run pytest tests/e2e/test_bottom_chat_actions.py -v --no-cov` green in isolation.
3. Full gate: `uv run pytest` green; coverage TOTAL **>90%**; `uv run ruff check . && uv run pyright` clean (equivalently `.agents/validate.sh` exit 0).
4. One atomic commit (`.agents/` is tracked — AGENTS.md rule 8):
```bash
git add .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(web): move the chat action cluster to the pinned bottom and align the button sets"
```
Commit body: one line per task — the move (task 01), the sticky `.chat-bottom` unit (task 02, A1), the right-edge alignment (task 03, A2), the dedicated suite (task 04), the regression outcome (task 05).
## Testing & Quality
- The six regression suites + the dedicated suite green in isolation (DB up).
- `uv run pytest` green; coverage TOTAL **>90%**; lint + types clean.
## Completion Criteria
- [ ] All six regression suites green in isolation; `test_bottom_chat_actions.py` green in isolation.
- [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] Exactly one new commit on `main` staging `.agents/ frontend/ tests/` only, `--no-gpg-sign`, Conventional Commits message as specified.
- [ ] Phase dir moved to `.agents/phases/complete/65_bottom_chat_actions/` (pipeline step — `.agents/` stays tracked and committed).