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,180 @@
# Phase 25 — Background: No Motion, Only Fading Light
**Source:** Owner report (2026-08-25, chat): the background "jitters down
and to the right every second and it slowly blinks brighter and darker. It
should be smooth, fluxuating, dimming and brightening, but not moving.
Different bright spots should slowly fade in and out."
**Story:** `.agents/user_stories/background-no-motion.md` (created by task 02)
**Context:** `frontend/assets/styles.css` — the background block:
`body::before` (44px grid, 1px lines at 60% `--line` alpha, widened radial
mask, `animation: bg-grid-drift 60s linear infinite` → `0 0` →
`44px 44px` ≈ 0.73px/s down-right) and `body::after` (indigo + cyan radial
glows, `animation: bg-glow-breathe 14s ease-in-out infinite alternate` →
opacity 0.85↔1 + scale 1↔1.05). Both are
`position: fixed; inset: 0; z-index: -1; pointer-events: none`; `<html>`
owns the `var(--bg)` canvas, `<body>` stays transparent (no-occlusion
contract). Pins to adapt: `tests/unit/test_background_animation.py`,
`tests/e2e/test_background_animation.py`,
`tests/e2e/test_dark_tech_theme.py` (`test_animated_background` pins
60s/14s; `test_reduced_motion_honored` pins the two body pseudo-layers).
## Objective
Stop the background from moving entirely, and replace the uniform
whole-layer "blink" with **different bright spots that slowly fade in and
out**: the grid becomes a static texture, and three soft glow spots
(phase-08 colors/positions, plus a third spot) each run their own
slow, **opacity-only** fade cycle at a different period, so the
background's brightness fluxuates smoothly and irregularly — no blink, no
jitter, no motion.
## Root cause (found from the code, 2026-08-25)
1. **"Jitters down and to the right every second"** = `bg-grid-drift`:
44px/60s (≈0.73px/s) in the diagonal `44px 44px` direction (exactly
down-right). A 1px grid line translated sub-pixel-by-sub-pixel is
rasterized with per-frame stepping/shimmer — perceived as a once-per-
second jitter, not smooth drift. Phase 22 made the drift *visible*;
that is precisely why it now reads as jitter.
2. **"Slowly blinks brighter and darker"** = `bg-glow-breathe`: a uniform
whole-layer opacity swing 0.85↔1 over 14s (alternate) plus
`scale(1)↔scale(1.05)` — the entire background pulses in unison (the
scale adds a faint zoom). One synchronized pulse reads as a blink; the
owner wants independent spots instead.
Phase 22 served the phase-08 design intent (grid drift + whole-layer
breathe). The owner now supersedes that design intent — this is an
owner revision of a **design comment**, not of any LOCKED anchor: A1–A17
are untouched, and the phase-08 anchors this phase must still honor are
pure CSS / zero JS / no CDN / no new assets / no `filter: blur` (A11 +
the block's perf note).
## Owner direction (2026-08-25, verbatim)
> "It should be smooth, fluxuating, dimming and brightening, but not
> moving. Different bright spots should slowly fade in and out."
1. **No movement** — no grid drift, no `scale`/`transform`, no
`background-position` animation, anywhere in the background.
2. **Fluxuating brightness** — overall page brightness varies smoothly and
irregularly (not one synchronized pulse).
3. **Different bright spots** — multiple glow spots, each fading in and
out on its own slow cycle.
4. **The static grid stays** — the owner rejected the grid's *motion*, not
the grid; it remains as a still texture. (If the owner later wants the
grid gone, that is a follow-up, not this phase.)
## Design (pure CSS, zero JS — A11 anchor)
- `body::before` — grid: **remove** the `animation`; **delete**
`@keyframes bg-grid-drift`. Keep the 44px cells, 60% `--line` alpha
lines, and widened radial mask (the static texture).
- Three glow-spot layers, one soft radial gradient each, **opacity-only**
keyframes (`0%,100%` low → `50%` 1, `ease-in-out`, `infinite`), with
different durations + negative delays so the cycles are out of phase
(periods 26/34/42s → LCM 4641s, the composite pattern effectively never
repeats within a viewing session):
| layer | spot (gradient) | keyframes | cycle |
|---|---|---|---|
| `body::after` | indigo `rgb(109 120 242 / 0.14)`, circle 56rem at 12% 8% (phase-08) | `bg-glow-a` | 26s, low opacity 0.25 |
| `html::before` | cyan `rgb(34 211 238 / 0.10)`, circle 60rem at 88% 92% (phase-08) | `bg-glow-b` | 34s, delay −12s, low 0.20 |
| `html::after` | indigo `rgb(109 120 242 / 0.09)`, circle 52rem at 14% 86% | `bg-glow-c` | 42s, delay −23s, low 0.15 |
- `html::before` / `html::after` join `body::after` as background layers:
`<html>` is the root stacking context — its `z-index: -1`
pseudo-elements paint **above** the `var(--bg)` canvas and **below**
the transparent, non-stacking `<body>`'s content, so the no-occlusion
contract holds unchanged (verify in the E2E, not just assume).
- All four layers keep: `content: ""; position: fixed; inset: 0;
z-index: -1; pointer-events: none;`
- `prefers-reduced-motion: reduce` stills **all four** layers
(`animation: none`).
- No `filter` (phase-08 no-blur perf anchor), no JS, no new assets; the
WCAG palette and every text contrast pair are untouched (the layers
carry no text). Opacity-only keyframes stay compositor-friendly.
## Dependencies
- `08_story_dark_tech_theme` (complete) — the layers, the palette, the
pure-CSS / no-blur / no-CDN anchors, and the 60s/14s pins in
`tests/e2e/test_dark_tech_theme.py` this phase adapts.
- `22_background_animation` (complete) — the current implementation and
the unit/E2E pins this phase supersedes.
- `07_story_responsive_polish` (complete) — the 360px overflow pin (the
layers are `fixed; inset: 0` — they must add no width).
## Tasks
1. `01_still_background_css.md` — the CSS redesign (static grid + three
opacity-only glow fades) + unit source pins (new
`tests/unit/test_background_no_motion.py`; the phase-22 pins in
`tests/unit/test_background_animation.py` adapted).
2. `02_e2e_story_suite_commit.md` — `tests/e2e/test_background_no_motion.py`
(the story gate, isolated), the regression suites adapted
(`test_background_animation.py`, `test_dark_tech_theme.py`), the story
file, the phase report + screenshots, final validation, the single
atomic commit, phase move to `complete/`.
## Locked decisions
- **A11 untouched** — vanilla HTML/CSS/JS in git, no CDN, zero JS, system
fonts; the whole change is CSS. **No `filter`/`blur`** (phase-08 perf
anchor). No new assets. **No anchor changed** — the superseded spec is
the phase-08 *design intent* (grid drift + whole-layer breathe), not a
LOCKED decision; the owner's 2026-08-25 direction is recorded above as
the revision. **A16 honored** — one new story E2E suite + adapted
regressions. **A17 honored** — one atomic `--no-gpg-sign` commit.
## Testing & Quality
- **Unit (source-level, repo source-pin pattern):**
- New `tests/unit/test_background_no_motion.py` — pins the full new
contract (task 01, step 6).
- Adapted `tests/unit/test_background_animation.py` — the phase-22
drift/breathe pins flip to the new contract; the generic
layer-plumbing and no-blur/no-JS sections stay (task 01, step 7).
- **Integration:** none (no `app/` changes).
- **Coverage:** frontend-only; the >90% `app/` gate is unaffected (TOTAL
must stay ≥ the pre-change number).
- **E2E:** `tests/e2e/test_background_no_motion.py` (task 02), green
**in isolation** (prereq `podman compose up -d db`); regressions green
in isolation after adaptation: `test_background_animation.py`,
`test_dark_tech_theme.py`, `test_responsive_polish.py`.
- **Lint/types:** `uv run ruff check . && uv run pyright` clean.
## Completion Criteria
- [ ] No movement: the grid is static (`bg-grid-drift` gone, no
`animation` on `body::before`) and **no `bg-*` keyframe animates
anything but `opacity`** — audited in a real Chromium via
`document.styleSheets` (E2E test 3).
- [ ] Three distinct bright spots (`body::after`, `html::before`,
`html::after`) run distinct slow opacity fades (26s/34s/42s,
out of phase); the timelines advance; the layer opacity and a
clipped screenshot of the glow region measurably change within a
few seconds (E2E tests 2, 4, 5).
- [ ] Contracts hold: all four layers `fixed; inset: 0; z-index: -1;
pointer-events: none`; `<html>` keeps the `var(--bg)` canvas and
`<body>` stays transparent (no occlusion); `prefers-reduced-motion`
stills all four; no horizontal overflow at 360px (E2E tests 6–8).
- [ ] `.agents/reports/25_background_no_motion/` documents the two root
causes with before/after screenshot pairs
(`.agents/screenshots/25_background_no_motion/`).
- [ ] `uv run pytest` green; `uv run pytest --cov=app
--cov-report=term-missing` TOTAL ≥ pre-change number (gate >90%).
- [ ] `uv run pytest tests/e2e/test_background_no_motion.py -v --no-cov`
green in isolation; `test_background_animation.py`,
`test_dark_tech_theme.py`, `test_responsive_polish.py` green in
isolation after adaptation.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] UI Structure Check (AGENTS.md rule 5): layers stay behind content,
no text/contrast impact, no 360px overflow.
- [ ] `.agents/user_stories/background-no-motion.md` exists; the old
`background-animation.md` story carries a supersession note.
- [ ] One `--no-gpg-sign` commit staging **only this phase's files** (the
unrelated dirty `TODO.md` must NOT be staged);
`.agents/phases/todo/25_background_no_motion/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -f .agents/phases/complete/25_background_no_motion \
.agents/user_stories/background-no-motion.md \
.agents/user_stories/background-animation.md \
.agents/reports/25_background_no_motion \
.agents/screenshots/25_background_no_motion
git add frontend/assets/styles.css tests/unit tests/e2e
git commit --no-gpg-sign -m "fix(ui): background no longer moves — static grid, three glow spots fading in and out on their own slow cycles (owner 2026-08-25)"
```
@@ -0,0 +1,149 @@
# Task 02 — Story E2E Suite, Regression Adaptations, Story File, Commit
**Phase:** `25_background_no_motion` · **Story:** `.agents/user_stories/background-no-motion.md`
## Objective
Prove the new behavior in a real Chromium viewport (no movement,
independent slow fades, all layer contracts), adapt the two
phase-08/22 E2E suites to the new contract, write the story file and the
phase report, run the final validation, and land the single atomic
commit.
## Work
1. **New `tests/e2e/test_background_no_motion.py`** — the story gate,
run in isolation (fixtures as in `tests/e2e/test_background_animation.py`:
`page`, `app_url`, `db_ready`; keep that file's Chromium notes:
pseudo-element CSS animations are enumerated by
`document.getAnimations()`, and the html pseudo-layers' computed
styles come from
`getComputedStyle(document.documentElement, "::before"/"::after")`).
Tests (8):
1. `test_grid_layer_is_static` — computed `animationName` of
`body::before` is `"none"`; no `bg-grid-drift` entry in
`document.getAnimations()`; the grid `backgroundImage` is still
present (the static texture survives).
2. `test_three_glow_layers_run_distinct_fades` — `body::after` →
`bg-glow-a` (26s), `documentElement::before` → `bg-glow-b` (34s),
`documentElement::after` → `bg-glow-c` (42s); each
`ease-in-out` + `infinite`, with a matching
`playState === "running"` entry in the document animation list;
the three durations are pairwise distinct.
3. `test_no_motion_properties_in_background_keyframes` — walk
`document.styleSheets`; for every `CSSRule.KEYFRAMES_RULE` whose
name starts with `bg-`, collect the declared property names of
every keyframe frame (iterate `frame.style`); the set across all
frames is exactly `{"opacity"}` — the deterministic no-movement
proof (no `transform`/`background-position` anywhere).
4. `test_glow_timelines_advance` — poll until all three timelines
report `currentTime > 0` (the phase-22 pattern: headless Chromium
starts the document timeline ~1s after load), sample all three,
wait ~500ms, each advanced ≥ 200ms.
5. `test_background_light_actually_changes` — (a) read the computed
`opacity` of `body::after` (or `documentElement::before`) at t0 and
poll up to ~8s until |Δ| ≥ 0.05 (a real fade, not a frozen frame);
(b) take two clipped screenshots ~4s apart of the bottom-left glow
region (the `html::after` spot at 14%/86%, e.g. clip
`{"x": 0, "y": 500, "width": 500, "height": 300}`) and assert the
bytes differ — the light visibly changes while nothing moves
(a fresh `/` page has no other animation, so the diff is the
background's).
6. `test_background_layers_contracts` — all four pseudo-elements
(`body::before`, `body::after`, `documentElement::before`,
`documentElement::after`): `position: fixed`, `z-index: -1`,
`pointer-events: none`, top/right/bottom/left all `0px`;
`documentElement` computed background is `rgb(10, 14, 23)`
(`var(--bg)` — the canvas stays on `html`); `document.body`
computed background is `rgba(0, 0, 0, 0)` (no occlusion).
7. `test_reduced_motion_stills_all_layers` — `reduced_motion="reduce"`
context: all four pseudo-layers report computed `animationName`
`"none"` and still carry a `backgroundImage` (static background
remains visible).
8. `test_no_horizontal_overflow_with_layers` — 360px viewport:
`documentElement.scrollWidth <= clientWidth` (the phase-07 pin).
2. **Adapt `tests/e2e/test_background_animation.py`** (the phase-22 story
suite — now a regression): replace `test_grid_layer_animation_running`,
`test_glow_layer_animation_running`, `test_animations_advance` with the
new-contract equivalents (grid static; `body::after` runs
`bg-glow-a` running; the glow timelines advance — delegate to the
same JS-report pattern); keep and extend
`test_background_layers_contracts` to the two new html pseudo-layers;
keep `test_no_horizontal_overflow_with_layers`; rewrite the module
docstring (phase 25 supersedes the phase-22 pins; pointer to
`background-no-motion.md`).
3. **Adapt `tests/e2e/test_dark_tech_theme.py`:**
- `test_animated_background` — the 60s/14s recipe is gone; new
contract: `body::before` `animationName == "none"` with its grid
image present (static texture); the three glow layers run
`bg-glow-a/b/c` at 26s/34s/42s (read the html pseudo-layers off
`documentElement`); update the docstring/AC text accordingly.
- `test_reduced_motion_honored` — assert **all four** pseudo-layers
(body `::before`/`::after` + documentElement `::before`/`::after`)
report `animationName` `"none"` and keep their images.
4. **`.agents/user_stories/background-no-motion.md`** — the story file, in
the repo's story format (model it on `background-animation.md`):
verbatim owner report (2026-08-25); Given/When/Then narrative; root
causes (sub-pixel 0.73px/s grid drift = once-per-second down-right
jitter; whole-layer 0.85↔1 + scale breathe = uniform blink); the fix
table (phase-22 → phase-25: grid static; three spots, opacity-only,
26/34/42s, delays 0/−12s/−23s, lows 0.25/0.20/0.15); acceptance
criteria = the 8 E2E tests + unit pins + regressions; UI
Visualization & Structure (the four layers, stacking/no-occlusion,
opacity-only motion, reduced-motion); Playwright Mapping Rule
(test → `tests/e2e/test_background_no_motion.py`).
5. **`.agents/user_stories/background-animation.md`** — add a short
supersession note at the top (the motion design is superseded by the
owner direction 2026-08-25 — see `background-no-motion.md`); do not
rewrite the phase-22 history.
6. **`.agents/reports/25_background_no_motion/`** — short report: the two
root causes (with the phase-22 measurements as context), the design
change, and the screenshot pairs: the task-01 `before.png`/
`before_4s.png` (jitter + uniform pulse) plus a new `after.png`/
`after_4s.png` pair captured the same way (the after pair must show a
brightness change with no positional shift of the grid).
7. **Final validation** (in order; DB up: `podman compose up -d db`):
- `uv run pytest` (unit + integration)
- `uv run pytest --cov=app --cov-report=term-missing` — TOTAL ≥
pre-change, app/ gate >90%
- `uv run pytest tests/e2e/test_background_no_motion.py -v --no-cov`
(the story gate, **in isolation**)
- `uv run pytest tests/e2e/test_background_animation.py -v --no-cov`
(in isolation, adapted)
- `uv run pytest tests/e2e/test_dark_tech_theme.py -v --no-cov` and
`uv run pytest tests/e2e/test_responsive_polish.py -v --no-cov`
(in isolation, regressions)
- `uv run ruff check . && uv run pyright`
8. **One atomic commit** — stage **only this phase's files**; the
unrelated dirty `TODO.md` in the worktree must NOT be staged:
```bash
mv .agents/phases/todo/25_background_no_motion .agents/phases/complete/25_background_no_motion
git add -f .agents/phases/complete/25_background_no_motion \
.agents/user_stories/background-no-motion.md \
.agents/user_stories/background-animation.md \
.agents/reports/25_background_no_motion \
.agents/screenshots/25_background_no_motion
git add frontend/assets/styles.css tests/unit tests/e2e
git commit --no-gpg-sign -m "fix(ui): background no longer moves — static grid, three glow spots fading in and out on their own slow cycles (owner 2026-08-25)"
```
(Move the directory first so the committed copy lives in `complete/`;
`.agents/` is gitignored by design, hence `git add -f`.)
## Testing & Quality
- The new E2E suite is the story gate (A16): green **in isolation**;
the adapted regression suites green in isolation; `test_responsive_
polish.py` green in isolation.
- `app/` coverage >90% and TOTAL ≥ pre-change (`app/` is untouched).
- ruff + pyright clean.
## Completion Criteria
- [ ] All 8 tests in `tests/e2e/test_background_no_motion.py` pass in
isolation.
- [ ] `test_background_animation.py` and `test_dark_tech_theme.py`
adapted and green in isolation; `test_responsive_polish.py` green
in isolation.
- [ ] `uv run pytest` green; `uv run pytest --cov=app` TOTAL ≥
pre-change; `uv run ruff check . && uv run pyright` clean.
- [ ] Story file exists; the old story carries the supersession note;
the report + four screenshots exist.
- [ ] Exactly one `--no-gpg-sign` commit, staging only this phase's
files (`git status` shows no staged `TODO.md`); the phase
directory now lives in `.agents/phases/complete/`.