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,160 @@
# Phase 18 — Scroll Control: Follow the Bottom (No Yank While Reading)
**Story:** `.agents/user_stories/follow-bottom-scroll.md` (created by task 03)
**Context:** `frontend/assets/app.js` — every `scrollIntoView` call site
(`addMessage`, `addTyping`, the streaming `delta` branch, and the
phase-17 `thinking` branch), the `SCROLL`/`reducedMotion` constants, and
the phase-14 restore path; PLAN §7.4 ("never stale" feedback contract).
## Objective
The chat must stop yanking the viewport. Today `app.js` calls
`scrollIntoView` on **every message added, on the typing indicator, and
on every streaming delta** — so a user who scrolls up to read earlier
messages (or the top of a long thinking block) is dragged back to the
bottom token by token. This phase implements the owner-chosen
**follow-the-bottom** contract (owner choice 2026-08-23, option 1 of the
two presented — no "↓ new content" pill): the page auto-scrolls *only
while the user is already pinned at the bottom*; submitting a question
still reveals the user's own message; once the user scrolls up, nothing
auto-scrolls for the rest of the turn (thinking or answer); a restored
conversation still lands on the latest message.
## Dependencies
- `17_thinking_display` (**todo — must complete first**): its task 02
adds the `thinking` branch that also scrolls per chunk; this phase
gates that call site too. The pipeline runs phases in numeric order, so
17 lands before 18 by construction.
- `14_chat_persistence` (complete) — the restore path keeps its
one-shot "land on the latest message" behavior (now via forced
reveals).
- `06_story_loading_feedback` (complete) — the state machine and the
`SCROLL` smooth/auto constant are reused, not changed.
- `08_story_dark_tech_theme` (complete) — no new UI surface, so no new
tokens.
## Design
- **Scroller:** the document itself (there is no inner scroll container —
`body` is `min-height: 100dvh` and the page scrolls on the window).
All measurements go through `window.scrollY` /
`document.documentElement.scrollHeight` / `window.innerHeight`.
- **New constants/helpers in `frontend/assets/app.js`:**
- `export const NEAR_BOTTOM_PX = 200;` — the "pinned to the bottom"
band (exported + unit-pinned, same pattern as `TURN_TIMEOUT_MS`).
200px ≈ the composer zone (the textarea auto-grows to 192px plus
the button row), so "the composer is fully in view" counts as
pinned — exactly where the user sits when they submit. Scrolling
up into the conversation (≫200px from the bottom) leaves the band.
- `function isNearBottom()` —
`document.documentElement.scrollHeight - window.scrollY -
window.innerHeight <= NEAR_BOTTOM_PX`.
- `function scrollReveal(wrap, behavior = SCROLL, force = false)` —
the **single** scroll call site:
`if (force || isNearBottom()) wrap.scrollIntoView({ behavior,
block: "end" });` — `force` is used only by the phase-14 restore
landing (one-shot, load-time).
The existing `SCROLL` constant (smooth, or `auto` under
`prefers-reduced-motion` — "calm, don't remove") still controls the
*feel* of a follow scroll; reduced-motion handling is untouched.
- **Call-site wiring (all in `app.js`):**
- `addMessage(who, html, scrollBehavior = SCROLL, force = false)` —
body ends with `scrollReveal(wrap, scrollBehavior, force)` (replaces
the unconditional `wrap.scrollIntoView(…)`).
- **Submit** (`handleSend`): the user's own message is revealed
through the **same gate** (no force): `addMessage("user",
renderMarkdown(text))`. In real use the user submits from the
composer — i.e. they are pinned (within the 200px band) — so the
message appears in view, per the owner's option-1 contract ("on
send you still see your message and the answer appear"); a submit
that happens with the viewport away from the bottom (only reachable
artificially) does **not** yank it.
- **Typing indicator** (`addTyping`): `scrollReveal(wrap)` — right
after submit the user is pinned (visible); if they scroll up during
pre-token "Thinking…", the indicator no longer drags them down.
- **Streaming `delta` branch:** replace
`wrap.scrollIntoView({ behavior: SCROLL, block: "end" })` with
`scrollReveal(wrap)` — follows only while pinned.
- **Phase-17 `thinking` branch:** same replacement
(`scrollReveal(wrap)`); the block's internal
`.thinking-text` bottom-pinning (scrollTop of the block's own
overflow) stays as-is — that is the user's element, not the page.
- **Restore (phase 14):** both restore call sites become
`addMessage(…, "auto", true)` — one-shot, non-smooth, lands on the
last restored message exactly as today (the only remaining `force`
users; a load-time landing, not continuous auto-scroll —
intentional, pinned by E2E test 5).
- **Fallbacks** (empty-answer bubble, `done`-without-wrap "…"
placeholder): `scrollReveal` with no force — appears if pinned,
never yanks.
- `startNewChat`: no change (the document shrinks; the browser clamps
the stale scrollTop).
- **After this phase, `scrollIntoView` appears exactly once in
`app.js`** (inside `scrollReveal`) — the regression pin.
- **Non-goals:** no "↓ new content" pill (owner opted against extras,
2026-08-23 — a follow-up phase if ever wanted); no scroll-position
persistence across reloads; no changes to other pages (Sources,
document viewer, login have no vertical auto-scroll); no virtualized
message list; no new DOM nodes, no CSS changes, no backend changes.
## Tasks
1. `01_follow_bottom_scroll.md` — `app.js`: `NEAR_BOTTOM_PX`,
`isNearBottom`, `scrollReveal`, all call sites rewired; new
source-level unit pins.
2. `02_e2e_story_suite.md` — `tests/e2e/test_follow_bottom_scroll.py`
(5 scenarios, isolated run) + regression suites (incl. phase 17's
`test_thinking_display.py` and phase 14's `test_chat_persistence.py`).
3. `03_docs_plan_commit.md` — user story file, PLAN revisions
(owner-permission noted), final validation, the single atomic commit,
phase move to `complete/`.
## Locked decisions
- **No anchor changes.** A11 (vanilla JS, no CDN) — pure `app.js`
logic. A15/A16 — one new story E2E suite + unit pins, no API change.
A10/A12/A13 — untouched. This is a UI behavior change only.
## Testing & Quality
- **Unit:** `tests/unit/test_frontend_scroll.py` (new) — source-level
pins: `export const NEAR_BOTTOM_PX = 200`; `function isNearBottom`;
`function scrollReveal`; `scrollIntoView` occurs **exactly once** in
`app.js` and only inside `scrollReveal`; the submit call is the plain
default (gated, no force); restore call sites pass
`("auto", true)`.
- **Integration:** none (no `app/` code changes) —
`uv run pytest --cov=app` must stay ≥ today's number.
- **Coverage:** frontend-only phase; the `app/` >90% gate is unaffected
but re-run to prove it.
- **E2E:** `tests/e2e/test_follow_bottom_scroll.py` — five scenarios
(see task 02), green **in isolation**
(`uv run pytest tests/e2e/test_follow_bottom_scroll.py -v --no-cov`,
prereq `podman compose up -d db`).
- **Lint/types:** `uv run ruff check . && uv run pyright` clean.
## Completion Criteria
- [ ] `uv run pytest` green; `uv run pytest --cov=app
--cov-report=term-missing` ≥ today's coverage (frontend-only
phase — expect no change).
- [ ] `uv run pytest tests/e2e/test_follow_bottom_scroll.py -v --no-cov`
green in isolation (5/5).
- [ ] Regression suites green **in isolation** (one command each):
`test_thinking_display.py` (phase 17 — the thinking scroll it
added is now gated), `test_chat_persistence.py` (restore landing),
`test_loading_feedback.py`, `test_chat_rag.py`,
`test_suggestion_chips.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] Manual check (dev server): submit a question → own message +
answer follow into view; scroll up mid-stream (or mid-thinking
with a real aipi turn) → viewport holds still until the turn
ends; "New chat" and reload behave as before.
- [ ] UI Structure Check (AGENTS.md rule 5): no new UI surface —
verify no CSS/HTML template change was needed;
`prefers-reduced-motion` still respected (the `SCROLL` constant
is untouched).
- [ ] PLAN carries the revisions with the 2026-08-23 owner-choice
wording; `.agents/user_stories/follow-bottom-scroll.md` exists.
- [ ] One `--no-gpg-sign` commit (below);
`.agents/phases/todo/18_follow_bottom_scroll/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(ui): chat auto-scrolls only while pinned to the bottom — submitting reveals your message, scrolling up holds the viewport"
```
@@ -0,0 +1,106 @@
# Task 01 — app.js: single scroll gate (follow-the-bottom)
**Phase:** `18_follow_bottom_scroll` · **Story:** `.agents/user_stories/follow-bottom-scroll.md`
## Objective
All chat-page scrolling goes through one gate — `scrollReveal` — which
only fires when the user is pinned to the bottom (the 200px composer-zone
band, `NEAR_BOTTOM_PX`) or when a call site forces it (restore only).
The per-delta / per-thinking-chunk / typing-indicator scrolls that yank
the viewport disappear.
## Work
1. `frontend/assets/app.js`
- Next to the `SCROLL` constant, add:
```js
/* Follow-the-bottom scroll contract (phase 18, owner choice
* 2026-08-23): the page auto-scrolls only while the user is pinned
* at the bottom — the 200px band covers the composer zone (the
* textarea auto-grows to 192px + the button row), i.e. "the
* composer is in view". Exported so the band is unit-pinned (same
* pattern as TURN_TIMEOUT_MS). */
export const NEAR_BOTTOM_PX = 200;
function isNearBottom() {
const doc = document.documentElement;
return doc.scrollHeight - window.scrollY - window.innerHeight <= NEAR_BOTTOM_PX;
}
/* The ONE scroll call site in this file. `force` is used only by
* the phase-14 restore landing (one-shot, load-time). */
function scrollReveal(wrap, behavior = SCROLL, force = false) {
if (force || isNearBottom()) {
wrap.scrollIntoView({ behavior, block: "end" });
}
}
```
- `addMessage(who, html, scrollBehavior = SCROLL, force = false)` —
replace the trailing `wrap.scrollIntoView({ behavior: scrollBehavior,
block: "end" });` with `scrollReveal(wrap, scrollBehavior, force);`
(update the function's comment: scrolling is now conditional).
- `addTyping()` — replace its
`wrap.scrollIntoView({ behavior: SCROLL, block: "end" });` with
`scrollReveal(wrap);`.
- `handleSend` — the user-message call stays
`addMessage("user", renderMarkdown(text));` (default: gated, no
force). The user submits from the composer, so they are pinned and
the message reveals; a submit with the viewport away from the
bottom does not yank it.
- Streaming `delta` branch — replace
`wrap.scrollIntoView({ behavior: SCROLL, block: "end" });` with
`scrollReveal(wrap);`.
- Phase-17 `thinking` branch — replace its
`wrap.scrollIntoView({ behavior: SCROLL, block: "end" });` with
`scrollReveal(wrap);` (keep the `.thinking-text` internal
`scrollTop = scrollHeight` bottom-pinning — that scrolls the
block's own overflow, not the page). Phase 17 is complete by the
time this task runs (pipeline numeric order), so the branch exists.
- `renderStoredMessage` (both user and brain branches) — the restore
calls become `addMessage("user", renderMarkdown(m.text), "auto",
true)` / `addMessage("brain", renderMarkdown(m.text), "auto",
true)` (one-shot, non-smooth landing on the last restored message —
phase-14 behavior preserved).
- `startNewChat`, fallback/placeholder `addMessage` calls: no change
(they now go through the gated default — no force).
- Header doc comment: add a short "Scroll (phase 18)" paragraph
describing the follow-the-bottom contract and naming
`NEAR_BOTTOM_PX` / `scrollReveal` as the single gate.
2. `tests/unit/test_frontend_scroll.py` (new file — same source-level
style as `test_frontend_feedback.py`, reading
`frontend/assets/app.js`):
- `test_near_bottom_constant_exported_at_200px` — regex
`export\s+const\s+NEAR_BOTTOM_PX\s*=\s*200\s*;`.
- `test_is_near_bottom_uses_document_scroller` — `isNearBottom`
defined; references `documentElement.scrollHeight`,
`window.scrollY`, `window.innerHeight`, `NEAR_BOTTOM_PX`.
- `test_single_scroll_gate` — `function scrollReveal` exists; its
body guards with `force || isNearBottom()`;
`app.js.count("scrollIntoView") == 1` (exactly one occurrence,
inside `scrollReveal`); `block: "end"` still used.
- `test_submit_reveal_is_gated` — the send handler's user-message
call is the plain default `addMessage("user", renderMarkdown(text))`
(no force argument — the gate decides, and it does in real use
because the composer being visible means pinned).
- `test_restore_force_landing` — both restore call sites pass
`("auto", true)`.
- `test_streaming_scrolls_only_through_gate` — the delta branch and
the thinking branch contain `scrollReveal(wrap)` and **no**
raw `scrollIntoView` (covered by the count==1 test, but pin the
two call-site markers explicitly for locality).
## Testing & Quality
- No `app/` code changes — `uv run pytest --cov=app
--cov-report=term-missing` must report the same coverage as before the
task (gate stays green).
- `uv run pytest tests/unit/test_frontend_scroll.py -v --no-cov` green.
## Completion Criteria
- [ ] `rg -c "scrollIntoView" frontend/assets/app.js` reports exactly 1.
- [ ] `uv run pytest tests/unit/test_frontend_scroll.py -v --no-cov`
green; `uv run pytest` fully green.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] Manual smoke (dev server, real or mock LLM): submit → own message
+ typing + answer follow into view while at the bottom; scroll to
the top mid-stream → the viewport does not move for the rest of
the turn; reload → lands on the latest message.
- [ ] No changes to `index.html`, `styles.css`, or any other page.
@@ -0,0 +1,106 @@
# Task 02 — E2E: the follow-the-bottom story suite
**Phase:** `18_follow_bottom_scroll` · **Story:** `.agents/user_stories/follow-bottom-scroll.md`
## Objective
Dedicated Playwright gate for the scroll contract (A16 — one story, one
file, run in isolation): submit reveals the user's message, streaming
follows while pinned at the bottom, and — the heart of the story — the
viewport **holds still** while the user is scrolled up, whether the
stream is thinking (phase 17) or answering; restore still lands on the
latest message.
## Work
1. `tests/e2e/test_follow_bottom_scroll.py` (new)
- Header comment: story, prereq (`podman compose up -d db`), and the
measurement convention — the scroller is the **document**
(no inner scroll container): read
`{ y: window.scrollY, sh: document.documentElement.scrollHeight,
ch: window.innerHeight }` via `page.evaluate`; "near bottom" =
`sh - y - ch <= 200` (mirrors `NEAR_BOTTOM_PX`); scroll to the top
with `page.evaluate("() => window.scrollTo(0, 0)")`.
- Real-user flow note: the user submits from the composer (pinned at
the bottom — normal `fill` + `Enter`), and only **after** the
stream starts do they scroll up to read. The no-yank scenarios
follow exactly that sequence, so no off-screen input manipulation
is needed.
- Fixtures mirroring `tests/e2e/test_chat_persistence.py`:
`seeded_kb` (truncate, import `tests/fixtures/docs` through the
real `LLMClient` + `import_sources`, assert `summary.added == 8`,
truncate in teardown); reuse the conftest `db_ready` skip.
- Helpers:
- `LONG_QUESTION = "write a long answer about my kubernetes cluster"`
(mock long-answer trigger ≈ 900 words ≈ 8s of streaming — a wide,
deterministic window to scroll away in).
- `THINK_LONG_QUESTION = "think out loud — write a long answer about my kubernetes cluster"`
(phase-17 thinking prefix + long answer; both mock triggers fire
independently).
- `scroll_state(page)` → the evaluate above; `near_bottom(state)`
helper with the 200px band.
- `wait_settled(page)` — `expect(page.locator("#send-btn")).to_be_enabled(timeout=30_000)`.
- **The five scenarios** (also the story's Playwright Mapping Rule):
1. `test_submit_reveals_new_message` — fresh chat page (pinned at
the bottom by default); submit `LONG_QUESTION`; once settled,
the last `.msg.brain` is inside the viewport (bounding box) and
`near_bottom(scroll_state(page))`.
2. `test_stream_follows_while_pinned_at_bottom` — fresh page;
submit `LONG_QUESTION`; ~2s into the stream (poll until the
last brain bubble's text length > 200), assert
`near_bottom(scroll_state(page))` — the follow behavior is
alive, not accidentally removed; at settle, still near bottom.
3. `test_no_yank_while_scrolled_up_during_answer_stream` — submit
`LONG_QUESTION` (fresh page, pinned — normal flow; once
settled, the document overflows the 800px viewport — assert
`sh > ch`); submit a second `LONG_QUESTION`; wait until the
new brain bubble's text length > 200 (streaming has started);
`window.scrollTo(0, 0)` (the user goes up to read); wait until
the bubble's text length > 600 (the stream kept running while
the viewport was at the top); assert
`scroll_state(page)["y"] <= 5` (viewport held); wait for
settle; assert `y <= 5` again (no scroll happened for the rest
of the turn — the answer finished off-screen below, by
design).
4. `test_no_yank_while_scrolled_up_during_thinking` — one settled
turn first (overflow exists); submit `THINK_LONG_QUESTION`
(normal flow, pinned); wait for `details.thinking` in the last
`.msg.brain` to attach (thinking is streaming — phase-17
behavior) and is open; `window.scrollTo(0, 0)`; wait until the
answer `.bubble` text is non-empty (the whole thinking stream
plus the answer's start happened at the top); assert
`y <= 5`; wait for settle; assert `y <= 5` and that the
thinking text contains `Step 2: Check my notes` and the bubble
is filled (all present but off-screen — the point of the
story).
5. `test_restore_lands_on_latest_message` — two settled turns
(user + brain × 2, overflow); `page.reload()`; after restore,
the last `.msg.brain` is inside the viewport and
`near_bottom(scroll_state(page))` (phase-14 one-shot landing
preserved — pinned so a future "remove all scrolling" change
fails loudly instead of silently).
- Determinism note (comment in the file): the mock paces SSE frames
at 0.02s; the long answer (~900 words) streams for several seconds,
so "mid-stream" assertions land comfortably inside the window on
headless Chromium; every "held still" assertion compares against
the exact `scrollTo(0, 0)` position (tolerance 5px for rounding).
2. Regression pass — each **in isolation** (A16):
`test_thinking_display.py` (phase 17 — its thinking scroll is now
gated; its assertions attach/visible from a pinned-at-bottom fresh
page, so they must still pass), `test_chat_persistence.py`
(restore behavior), `test_loading_feedback.py` (pre-token/streaming
feedback), `test_chat_rag.py` (core turn), `test_suggestion_chips.py`
(chip row is horizontal — unaffected, cheap to include).
## Testing & Quality
- This suite is the story's gate:
`uv run pytest tests/e2e/test_follow_bottom_scroll.py -v --no-cov`
green in isolation.
- No `app/` or mock changes in this task. If a scenario exposes a real
bug, fix it in `frontend/assets/app.js` (the owning file) and re-run
task 01's unit pins + this suite.
## Completion Criteria
- [ ] `uv run pytest tests/e2e/test_follow_bottom_scroll.py -v --no-cov`
green in isolation (5/5).
- [ ] All five regression suites above green, one command each.
- [ ] `uv run pytest` (unit + integration) still green;
`uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,90 @@
# Task 03 — Story file, PLAN revisions, the phase commit
**Phase:** `18_follow_bottom_scroll` · **Story:** `.agents/user_stories/follow-bottom-scroll.md`
## Objective
Record the change: the user story file (AGENTS.md rule 4), the PLAN
revisions with the owner-choice noted, and the single atomic
`--no-gpg-sign` commit with the phase moved to `complete/`.
## Work
1. `.agents/user_stories/follow-bottom-scroll.md` (new — match the
format of the sibling stories, e.g. `loading-feedback.md`):
- Header: `**Phase:** 18_follow_bottom_scroll · **E2E:**
tests/e2e/test_follow_bottom_scroll.py`.
- Narrative: as a user, Brain's answers (and thinking) stream for
10–30s. I want to scroll up and read without the page dragging me
back to the bottom token by token — but when I submit, I should
still see my message and the reply appear.
- Acceptance criteria:
1. Submitting a question always reveals the user's own message
(explicit action, unconditional).
2. While the user is pinned to the bottom (within 200px of it —
the composer zone), the typing indicator, thinking chunks, and
answer deltas follow into view (smooth, or instant under
`prefers-reduced-motion`).
3. Once the user scrolls up (more than 200px from the bottom),
nothing auto-scrolls for the rest of the turn — thinking or
answer; the viewport position is unchanged at turn end.
4. The thinking block's *internal* text still bottom-pins itself
while open (that is the block's own overflow, not the page).
5. Restoring a stored conversation still lands on the latest
message (one-shot, non-smooth).
6. No new UI surface (no "new content" pill — owner chose the
minimal contract, 2026-08-23); no other page is affected.
- UI Visualization & Structure: the scroller is the document (no
inner overflow container); `NEAR_BOTTOM_PX = 200` (exported,
unit-pinned); `scrollReveal` is the single `scrollIntoView` call
site; the `SCROLL` smooth/auto constant (reduced-motion) is reused
unchanged.
- Playwright Mapping Rule: the five scenarios of
`tests/e2e/test_follow_bottom_scroll.py` verbatim from task 02.
2. `.agents/PLAN.md` revisions — **record the owner choice
(2026-08-23, "option 1: follow-the-bottom, no pill")** in each note,
style per the phase-16 A10-revision precedent:
- Header revisions line: append
`; follow-the-bottom scroll (Phase 18)`.
- **§7.4 table:** new row — **Scroll (follow-the-bottom, phase
18)**: the page auto-scrolls only while the user is pinned to the
bottom (≤200px band, `NEAR_BOTTOM_PX` — the composer zone; submit
reveals the user's message through the same gate, which holds in
real use); scrolling up holds the viewport for the rest of the
turn (thinking and answer alike); restore lands one-shot on the
latest message.
- **§12 roadmap:** new row 18 — `18_follow_bottom_scroll` /
`follow-bottom-scroll.md` / `test_follow_bottom_scroll.py`.
- Do not alter any locked anchor or renumber anything. (No §9/§4/§7.5
changes — no log, API, or new component surface.)
3. `README.md` — no change (no operator-facing behavior; the README
does not document chat scroll behavior today).
4. Final validation pass (all gates, AGENTS.md rules 5 + 9):
- `uv run pytest --cov=app --cov-report=term-missing` (≥ today's
coverage),
- `uv run pytest tests/e2e/test_follow_bottom_scroll.py -v --no-cov`
in isolation, plus the five regression suites from task 02 (one
command each, in isolation),
- `uv run ruff check . && uv run pyright`,
- `rg -c "scrollIntoView" frontend/assets/app.js` → 1.
5. Commit + phase move (last step, only when all gates are green):
```bash
git add -A .agents/ frontend/ tests/
git commit --no-gpg-sign -m "feat(ui): chat auto-scrolls only while pinned to the bottom — submitting reveals your message, scrolling up holds the viewport"
mv .agents/phases/todo/18_follow_bottom_scroll .agents/phases/complete/
```
## Testing & Quality
- No new logic — this is the record-keeping + validation pass; the
gates above are the phase's final proof. If validation fails, fix in
the owning task's files, re-run that task's tests, then commit.
## Completion Criteria
- [ ] `.agents/user_stories/follow-bottom-scroll.md` exists with all
five sections (header, narrative, acceptance, UI visualization,
Playwright Mapping Rule).
- [ ] `.agents/PLAN.md` carries the header-revision/§7.4/§12 notes with
the 2026-08-23 owner-choice wording; no anchor text altered.
- [ ] All gates green (coverage, story E2E + 5 regressions in
isolation, ruff + pyright, single `scrollIntoView`).
- [ ] Exactly one new commit, conventional, `--no-gpg-sign`;
`.agents/phases/todo/18_follow_bottom_scroll/` is now under
`.agents/phases/complete/`.