Compare commits

..
4 Commits
Author SHA1 Message Date
ducoterra 055c0b5d85 feat(rag): pass chat history with prior thinking to the LLM
Build and Push Containers / build-and-push-app (push) Successful in 1m39s
Build and Push Containers / build-and-push-db (push) Successful in 11s
Phase 74 (TODO.md L4): a follow-up question now reaches the model WITH
the conversation so far — every prior user/brain turn and the prior
thinking blocks on brain turns (preserve-thinking) — while
POST /api/chat stays stateless (A10): the client provides the history
in the request body and the server stores nothing new.

Server (task 01):
- ChatRequest.history: optional list[HistoryTurn] (who: user|brain,
  text, optional thinking) — absent/empty keeps the request
  byte-identical to pre-phase-74 (the two-message [system, user]
  request; the kill-switch semantics are pinned in the integration
  suite).
- app.rag.prompts.history_to_messages: pure mapper — walks the turns
  newest-first against the settings budgets (history_max_turns=40 /
  history_max_chars=24000, BOR_HISTORY_MAX_TURNS /
  BOR_HISTORY_MAX_CHARS); a capped turn is dropped WHOLE (never cut
  mid-answer); the kept window is returned oldest-first; brain turns
  carry their thinking as reasoning_content (A4) only when
  non-empty.
- Both branches feed it: the deflected path splices it between the
  system prompt and the current user message (the phase-71 recovery
  still rebuilds from messages[1:]), the grounded agent receives
  run_agent(..., history=hist); llm.py's message params widen to
  list[dict[str, Any]] (string-only messages stay byte-identical on
  the wire — the SDK passes message dicts through verbatim).
- The per-turn log line (PLAN §9) gains history_msgs=N after
  kb_chars=N.
- Pins: tests/unit/test_history.py (mapper: mapping, reasoning
  gating, both budgets, drop-whole, ordering, empty default),
  tests/unit/test_config.py (the two settings + env overrides),
  tests/unit/test_agent.py (the history splice + the default),
  tests/integration/test_chat_api.py (deflected AND grounded forward
  the history incl. reasoning_content, no-history byte-identity, 422
  pins, the log field).

Client (task 02):
- runTurn — the single funnel for fresh send / phase-49 retry /
  phase-53 stale-regen — sends history = the conversation record
  minus the current question, with thinking only on brain records
  that streamed one (undefined drops the key from the JSON, the
  record's convention); the question is never duplicated into the
  history.

Wire proof (task 03):
- The mock's echo my history marker (HISTORY_TRIGGER) answers with
  the deterministic history echo — history: N prior messages; last
  answer tail: <last 24 chars>; thinking: yes|no — checked BEFORE
  the DEFLECT_MODE branch (like TABLE_TRIGGER), so it fires on both
  turn branches whatever the gate says; the module docstring records
  the user/assistant-only history invariant that keeps every
  existing (tool-result-classified) marker flow unaffected.
- tests/e2e/test_llm_history.py (isolated): a grounded follow-up and
  a deflected follow-up both receive history: 2 prior messages +
  thinking: yes + the byte-exact tail of turn 1's answer (derived
  from the persisted bor.chat.v1 record — the same array the client
  maps into the body); a cold start receives history: 0 prior
  messages / last answer tail: none / thinking: no.
- Regressions green in isolation: chat_rag, chat_history (phase 50),
  agent_document_tools, harness_aligned_tools, stop_generation,
  retry_answer, response_to_docs.
2026-09-05 16:04:40 -04:00
ducoterra a16130c71d fix(chat): keep generating while the tab is hidden
Root cause (task 01): none of C1-C3 - in Chromium 151 (real mode) a
merely-hidden tab neither stops the stream (frames arrive at full rate;
turn completes) nor fires pagehide on tab switch; C1's double-record
path was proven latent via a synthetic pagehide (trigger is
browser-dependent, e.g. Safari) and C2 (the 120s pre-token guard) was
confirmed to fire while hidden.

- C1: the pagehide partial-persist is correlated with the turn's settle
  (leavePartialIndex) - the done/stop settle REPLACES it in place
  (identity-guarded rememberBrainTurn in-place mode), so bor.chat.v1
  and the auto-saved saved_chats row keep exactly ONE brain turn per
  question; a real navigation never runs a settle, so the leave-save
  is unchanged.
- C2: the visibility re-arm gives the still-armed pre-token guard a
  fresh TURN_TIMEOUT_MS when the tab returns to visible - hidden time
  no longer counts toward the 120s guard.
- Phase-48 teardown contract untouched: Stop / tab close / real
  navigation still cancel the fetch and stop the model.
- Unit pins: tests/unit/test_frontend_hidden_tab.py (the app.js
  mechanisms without a browser).
- E2E pins: tests/e2e/test_hidden_tab_stream.py - synthetic pagehide
  mid-stream completes exactly once with one brain turn (localStorage
  + auto-saved row), reload restores one bubble, no-event baseline,
  and the fake-clock pre-token guard re-arm (discriminating: fails
  with the re-arm disabled).
2026-09-05 14:34:33 -04:00
ducoterra 45c3fa2863 chore(agent): phase roadmap from TODO.md (phases 73-75), clear the file
Convert the two unchecked TODO items into executable phases (Protocol B,
appended after the 72 completed phases):

- 73_hidden_tab_stream (TODO L3): a merely-hidden browser tab must never
  stop a generating answer; repro/root-cause decision tree + the pagehide
  partial-correlation fix + the hidden-tab E2E pin.
- 74_llm_chat_history (TODO L4, history): client-provided history in
  POST /api/chat (stateless, A10) mapped through both the deflected and
  grounded agent paths, prior thinking blocks preserved via
  reasoning_content, capped oldest-first; mock echo marker + E2E.
- 75_save_doc_full_session (TODO L4, save-as-doc): the Save-as-doc draft
  body becomes the full session transcript; edit-out happens in the
  existing doc-edit body; multi-turn git-verified E2E.

Owner-confirmed assumptions A1-A7 are recorded as ASSUMPTION lines in the
task files. TODO.md is cleared (items now live in .agents/phases/todo/).
2026-09-05 12:14:21 -04:00
ducoterra dbf2af26c6 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.
2026-09-05 10:57:07 -04:00
1179 changed files with 6564 additions and 692 deletions
@@ -1,11 +0,0 @@
All green (pytest exit 0; summary matches the earlier full run of 295 passed). Phase 18 is complete.
**Task 03 report — `18_follow_bottom_scroll` (docs, PLAN, validation, commit)**
- Created `.agent/user_stories/follow-bottom-scroll.md` (header, narrative, 6 acceptance criteria, UI visualization, verbatim 5-scenario Playwright Mapping Rule — matches sibling format)
- `.agent/PLAN.md`: header revisions line + §7.4 "Scroll (follow-the-bottom, phase 18)" row + §12 row 18, each noting the owner choice (2026-08-23, option 1 — no pill); no anchor text altered; README untouched (per task)
- Gates: `uv run pytest --cov=app --cov-report=term-missing` → 295 passed, **99%** coverage; `uv run pytest tests/e2e/test_follow_bottom_scroll.py -v --no-cov` → 5/5 in isolation; regressions in isolation: thinking_display 5✓, chat_persistence 4✓, loading_feedback 5✓, chat_rag 3✓, suggestion_chips 4✓; `uv run ruff check . && uv run pyright` clean; `rg -c "scrollIntoView" frontend/assets/app.js` → 1
- Commit `bc0158f` `feat(ui): chat auto-scrolls only while pinned to the bottom — …` (conventional, `--no-gpg-sign`); phase dir now under `.agent/phases/complete/`
- Deviations: (1) task's `git add -A .agent/` errors on the ignored pathspec in this git — staged via `git add -f .agent/PLAN.md` (the AGENTS.md rule-8 mechanism); (2) `mv` of the todo dir failed because the harness had already moved tasks 01/02 into `complete/18` — merged remaining `00`/`03` files in and removed the todo dir; (3) header line carries "(Phase 18, owner choice 2026-08-23)" so the owner-choice wording sits on every note, per the phase-17 precedent
Next pending task: `.agent/phases/todo/19_shared_header/` (first task in that phase).
@@ -1 +0,0 @@
bash: .agent/validate.sh: No such file or directory
@@ -1,7 +1,7 @@
# Phase 02 — Story: Import Documents
**Story:** `.agent/user_stories/import-documents.md`
**Context:** `.agent/PLAN.md` §5 (data model), §9 (logging), §11 (import workflow)
**Story:** `.agents/user_stories/import-documents.md`
**Context:** `.agents/PLAN.md` §5 (data model), §9 (logging), §11 (import workflow)
## Goal
The importer (`scripts/import_docs.py`) + `GET /api/docs` + the Sources page
@@ -1,7 +1,7 @@
# Phase 03 — Story: Chat RAG Answer (happy path)
**Story:** `.agent/user_stories/chat-rag-answer.md`
**Context:** `.agent/PLAN.md` §3 (data flow), §4 (SSE contract), §6 (persona), §9 (logging)
**Story:** `.agents/user_stories/chat-rag-answer.md`
**Context:** `.agents/PLAN.md` §3 (data flow), §4 (SSE contract), §6 (persona), §9 (logging)
## Goal
The core product loop: question → embed → cosine top-4 → full top-2
@@ -1,7 +1,7 @@
# Phase 04 — Story: Honest Deflection
**Story:** `.agent/user_stories/honest-deflection.md`
**Context:** `.agent/PLAN.md` §4, §6 (honesty gate), §9
**Story:** `.agents/user_stories/honest-deflection.md`
**Context:** `.agents/PLAN.md` §4, §6 (honesty gate), §9
## Goal
When retrieval finds nothing relevant, Brain says so — plainly, chippily —
@@ -1,7 +1,7 @@
# Phase 05 — Story: Suggestion Chips
**Story:** `.agent/user_stories/suggestion-chips.md`
**Context:** `.agent/PLAN.md` §7 (UI/UX), story file for chip spec
**Story:** `.agents/user_stories/suggestion-chips.md`
**Context:** `.agents/PLAN.md` §7 (UI/UX), story file for chip spec
## Goal
Zero-friction onboarding: 3–4 real example questions on first load,
@@ -1,7 +1,7 @@
# Phase 06 — Story: Loading Feedback & Progress
**Story:** `.agent/user_stories/loading-feedback.md`
**Context:** `.agent/PLAN.md` §7.4 ("never stale" contract), §9
**Story:** `.agents/user_stories/loading-feedback.md`
**Context:** `.agents/PLAN.md` §7.4 ("never stale" contract), §9
## Goal
An unambiguous state machine — `idle → thinking → streaming → done |
@@ -1,7 +1,7 @@
# Phase 07 — Story: Responsive, Polished, Accessible UI
**Story:** `.agent/user_stories/responsive-polish.md`
**Context:** `.agent/PLAN.md` §7 (the whole UI/UX strategy)
**Story:** `.agents/user_stories/responsive-polish.md`
**Context:** `.agents/PLAN.md` §7 (the whole UI/UX strategy)
## Goal
The final visual + accessibility audit pass across chat and Sources. No
@@ -1,7 +1,7 @@
# Phase 08 — Story: Dark Tech Theme
**Story:** `.agent/user_stories/dark-tech-theme.md`
**Context:** `.agent/PLAN.md` §7 (UI/UX strategy), §10 (testing)
**Story:** `.agents/user_stories/dark-tech-theme.md`
**Context:** `.agents/PLAN.md` §7 (UI/UX strategy), §10 (testing)
## Goal
Re-skin the whole UI to a dark, techy, emoji-free look with a subtly
@@ -126,11 +126,11 @@ animate; reduced-motion context → `animation-name: none`; on-topic smoke
- [ ] layout metrics + chat behavior unchanged (smoke E2E)
- [ ] existing story E2E suites still green in isolation
- [ ] unit + integration green, coverage >90%, ruff + pyright green
- [ ] committed (force-add `.agent/PLAN.md` + this phase record — rule 8)
- [ ] committed (force-add `.agents/PLAN.md` + this phase record — rule 8)
## Commit
```bash
git add -A
git add -f .agent/PLAN.md .agent/phases/todo/08_story_dark_tech_theme.md
git add -f .agents/PLAN.md .agents/phases/todo/08_story_dark_tech_theme.md
git commit --no-gpg-sign -m "feat(ui): dark tech theme — emoji-free chrome, subtle animated CSS background, WCAG AA dark palette"
```
@@ -1,7 +1,7 @@
# Phase 09 — Story: Retrieval Quality — Multi-Format Ingestion + Hybrid Search
**Story:** `.agent/user_stories/retrieval-quality.md`
**Context:** `.agent/PLAN.md` §3 (data flow), §5 (data model), §6 (retrieval), §11 (import)
**Story:** `.agents/user_stories/retrieval-quality.md`
**Context:** `.agents/PLAN.md` §3 (data flow), §5 (data model), §6 (retrieval), §11 (import)
## Goal
Fix "RAG retrieval is terrible": ingest the full text-format set (not
@@ -1,7 +1,7 @@
# Phase 10 — Story: Clickable Document Viewer
**Story:** `.agent/user_stories/document-viewer.md`
**Context:** `.agent/PLAN.md` §4 (API), §7 (UI/UX)
**Story:** `.agents/user_stories/document-viewer.md`
**Context:** `.agents/PLAN.md` §4 (API), §7 (UI/UX)
## Goal
Every document Brain cites — and every row in the Sources table — opens
@@ -1,6 +1,6 @@
# Phase 11 — Long Answers (No Truncation)
**Story:** `.agent/user_stories/long-answers.md`
**Story:** `.agents/user_stories/long-answers.md`
**Context:** owner report 2026-08-22 — "responses keep getting cut off.
It should be allowed to respond up to 32768 tokens."
@@ -48,5 +48,5 @@ cap per owner instruction.
## Commit
```bash
git add -A .agent/ app/ tests/ .env.example && git commit --no-gpg-sign -m "fix(rag): lift chat output cap to 32768 tokens — long answers no longer cut off"
git add -A .agents/ app/ tests/ .env.example && git commit --no-gpg-sign -m "fix(rag): lift chat output cap to 32768 tokens — long answers no longer cut off"
```
@@ -1,6 +1,6 @@
# Phase 12 — One Header, Same Size Everywhere
**Story:** `.agent/user_stories/header-consistency.md`
**Story:** `.agents/user_stories/header-consistency.md`
**Context:** owner report 2026-08-22 — "the header changes size between
sources and chat."
@@ -43,5 +43,5 @@ None. A11/Phase-08 tokens untouched (same colors, same hairline).
## Commit
```bash
git add -A .agent/ frontend/assets/styles.css tests/e2e/test_header_consistency.py && git commit --no-gpg-sign -m "fix(ui): uniform header bar height on chat, sources, and the document viewer"
git add -A .agents/ frontend/assets/styles.css tests/e2e/test_header_consistency.py && git commit --no-gpg-sign -m "fix(ui): uniform header bar height on chat, sources, and the document viewer"
```
@@ -1,6 +1,6 @@
# Phase 13 — Document Back Button Returns to Where You Came From
**Story:** `.agent/user_stories/document-back-navigation.md`
**Story:** `.agents/user_stories/document-back-navigation.md`
**Context:** owner report 2026-08-22 — "clicking a document from the
chat tab pulls up the document correct, but the back button goes back to
sources, not the chat."
@@ -47,5 +47,5 @@ None. A10/A11 untouched (no new endpoint, no new asset).
## Commit
```bash
git add -A .agent/ frontend/ tests/e2e/ && git commit --no-gpg-sign -m "fix(ui): document viewer back button returns to the page you came from (chat or sources)"
git add -A .agents/ frontend/ tests/e2e/ && git commit --no-gpg-sign -m "fix(ui): document viewer back button returns to the page you came from (chat or sources)"
```
@@ -1,6 +1,6 @@
# Phase 14 — Chat Survives a Refresh (localStorage)
**Story:** `.agent/user_stories/chat-persistence.md`
**Story:** `.agents/user_stories/chat-persistence.md`
**Context:** owner report 2026-08-22 — "the chat disappears as soon as
the browser refreshes. It should use local storage to track previous
sessions."
@@ -56,5 +56,5 @@ A11 untouched (no library — raw `localStorage` JSON).
## Commit
```bash
git add -A .agent/ frontend/ tests/e2e/test_chat_persistence.py && git commit --no-gpg-sign -m "feat(ui): persist the chat conversation in localStorage — survives refresh and navigation, with a New chat reset"
git add -A .agents/ frontend/ tests/e2e/test_chat_persistence.py && git commit --no-gpg-sign -m "feat(ui): persist the chat conversation in localStorage — survives refresh and navigation, with a New chat reset"
```
@@ -1,6 +1,6 @@
# Phase 15 — Tune How Brain Answers (Steering Notes)
**Story:** `.agent/user_stories/steering-notes.md`
**Story:** `.agents/user_stories/steering-notes.md`
**Context:** owner report 2026-08-22 — "the answers are off. Add a
feature that lets me 'tune' the output if I think an answer isn't quite
right. This tuning should be added to the database and read in with the
@@ -77,5 +77,5 @@ suite), A11 (no library). New table + new setting only.
## Commit
```bash
git add -A .agent/ alembic/ app/ frontend/ tests/ README.md && git commit --no-gpg-sign -m "feat(rag): steering notes — tune how Brain answers, stored in Postgres and injected into every system prompt"
git add -A .agents/ alembic/ app/ frontend/ tests/ README.md && git commit --no-gpg-sign -m "feat(rag): steering notes — tune how Brain answers, stored in Postgres and injected into every system prompt"
```
@@ -1,6 +1,6 @@
# Phase 16 — Admin Sign-In (Single-Admin Auth)
**Story:** `.agent/user_stories/admin-auth.md`
**Story:** `.agents/user_stories/admin-auth.md`
**Context:** owner request 2026-08-22 — "add authentication. Only the admin
user (there will be only one admin user, me) should be able to tune the
outputs and view the entire sources page. Anonymous users should only be
@@ -179,10 +179,10 @@ is the only session state.
- [ ] UI Structure Check (AGENTS.md rule 5): login page — landmarks,
labeled control, contrast ≥4.5:1, focus-visible, `role=alert` error,
centered card in the standard frame, no CDN tags.
- [ ] One `--no-gpg-sign` commit (below); `.agent/phases/todo/16_admin_auth.md`
moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit (below); `.agents/phases/todo/16_admin_auth.md`
moved to `.agents/phases/complete/`.
## Commit
```bash
git add -A .agent/ app/ frontend/ tests/ README.md .env.example && git commit --no-gpg-sign -m "feat(auth): single-admin password login (signed cookie) — gate tuning + Sources catalog, keep chat and document viewer public"
git add -A .agents/ app/ frontend/ tests/ README.md .env.example && git commit --no-gpg-sign -m "feat(auth): single-admin password login (signed cookie) — gate tuning + Sources catalog, keep chat and document viewer public"
```
@@ -1,6 +1,6 @@
# Phase 17 — Model "Thinking" in the Chat UI
**Story:** `.agent/user_stories/thinking-display.md` (created by task 04)
**Story:** `.agents/user_stories/thinking-display.md` (created by task 04)
**Context:** PLAN §3/§4 (SSE chat transport, A15), §6 (locked prompt),
§7.4/§7.5 (feedback contract + component inventory), §9 (per-turn log line);
`app/rag/llm.py` (streaming client), `app/api/chat.py` (SSE mapping),
@@ -219,14 +219,14 @@ Models/turns that emit no reasoning render exactly as before.
all text contrast ≥4.5:1, summary is a real focusable control with
≥44px target, `prefers-reduced-motion` respected, no CDN tags,
chat column still 46rem.
- [ ] `.agent/user_stories/thinking-display.md` exists; PLAN §2/§4/§7.4/
- [ ] `.agents/user_stories/thinking-display.md` exists; PLAN §2/§4/§7.4/
§7.5/§9/§12 carry the revision notes (owner permission
2026-08-23).
- [ ] One `--no-gpg-sign` commit (below);
`.agent/phases/todo/17_thinking_display/` moved to
`.agent/phases/complete/`.
`.agents/phases/todo/17_thinking_display/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -A .agent/ app/ frontend/ tests/ README.md .env.example && git commit --no-gpg-sign -m "feat(chat): stream model thinking over SSE and show it in a collapsible block"
git add -A .agents/ app/ frontend/ tests/ README.md .env.example && git commit --no-gpg-sign -m "feat(chat): stream model thinking over SSE and show it in a collapsible block"
```
@@ -1,6 +1,6 @@
# Task 01 — Backend: stream thinking pieces end-to-end
**Phase:** `17_thinking_display` · **Story:** `.agent/user_stories/thinking-display.md`
**Phase:** `17_thinking_display` · **Story:** `.agents/user_stories/thinking-display.md`
## Objective
The chat pipeline carries the model's reasoning: `LLMClient.chat_stream`
@@ -1,6 +1,6 @@
# Task 02 — Frontend: the collapsible Thinking block
**Phase:** `17_thinking_display` · **Story:** `.agent/user_stories/thinking-display.md`
**Phase:** `17_thinking_display` · **Story:** `.agents/user_stories/thinking-display.md`
## Objective
`index.html` chat shows the model's thinking: a `<details class="thinking">`
@@ -1,6 +1,6 @@
# Task 03 — E2E: mock thinking trigger + the story suite
**Phase:** `17_thinking_display` · **Story:** `.agent/user_stories/thinking-display.md`
**Phase:** `17_thinking_display` · **Story:** `.agents/user_stories/thinking-display.md`
## Objective
Deterministic E2E coverage of the thinking display: the mock LLM gains a
@@ -1,6 +1,6 @@
# Task 04 — Story file, docs, PLAN revisions, the phase commit
**Phase:** `17_thinking_display` · **Story:** `.agent/user_stories/thinking-display.md`
**Phase:** `17_thinking_display` · **Story:** `.agents/user_stories/thinking-display.md`
## Objective
Record the feature where it belongs: the user story file (AGENTS.md rule
@@ -10,7 +10,7 @@ precedent of phase 16), and the single atomic `--no-gpg-sign` commit with
the phase moved to `complete/`.
## Work
1. `.agent/user_stories/thinking-display.md` (new — match the format of
1. `.agents/user_stories/thinking-display.md` (new — match the format of
the sibling stories, e.g. `loading-feedback.md`):
- Header: `**Phase:** 17_thinking_display · **E2E:**
tests/e2e/test_thinking_display.py`.
@@ -54,7 +54,7 @@ the phase moved to `complete/`.
`BOR_STREAM_THINKING=0` kill-switch. No CDN rule, no other edits.
3. `.env.example` — verify the task-01 line (`BOR_STREAM_THINKING=1`)
is present with its comment; add nothing new.
4. `.agent/PLAN.md` revisions — **record owner permission
4. `.agents/PLAN.md` revisions — **record owner permission
(2026-08-23) in each note**, exactly the style phase 16 used for the
A10 revision:
- Header revisions line: append
@@ -92,9 +92,9 @@ the phase moved to `complete/`.
change).
6. Commit + phase move (last step, only when all gates are green):
```bash
git add -A .agent/ app/ frontend/ tests/ README.md .env.example
git add -A .agents/ app/ frontend/ tests/ README.md .env.example
git commit --no-gpg-sign -m "feat(chat): stream model thinking over SSE and show it in a collapsible block"
mv .agent/phases/todo/17_thinking_display .agent/phases/complete/
mv .agents/phases/todo/17_thinking_display .agents/phases/complete/
```
## Testing & Quality
@@ -104,15 +104,15 @@ the phase moved to `complete/`.
tests, and only then commit.
## Completion Criteria
- [ ] `.agent/user_stories/thinking-display.md` exists with all five
- [ ] `.agents/user_stories/thinking-display.md` exists with all five
sections (header, narrative, acceptance, UI visualization,
Playwright Mapping Rule).
- [ ] README "Thinking" section + `.env.example` line present;
`.agent/PLAN.md` carries the §2-revision/§4/§7.4/§7.5/§9/§12 notes
`.agents/PLAN.md` carries the §2-revision/§4/§7.4/§7.5/§9/§12 notes
with the 2026-08-23 owner-permission wording; no anchor text
altered.
- [ ] All gates green (coverage > 90%, story E2E + 4 regressions in
isolation, ruff + pyright clean).
- [ ] Exactly one new commit, conventional, `--no-gpg-sign`;
`.agent/phases/todo/17_thinking_display/` is now under
`.agent/phases/complete/`.
`.agents/phases/todo/17_thinking_display/` is now under
`.agents/phases/complete/`.
@@ -1,6 +1,6 @@
# Phase 18 — Scroll Control: Follow the Bottom (No Yank While Reading)
**Story:** `.agent/user_stories/follow-bottom-scroll.md` (created by task 03)
**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
@@ -149,12 +149,12 @@ conversation still lands on the latest message.
`prefers-reduced-motion` still respected (the `SCROLL` constant
is untouched).
- [ ] PLAN carries the revisions with the 2026-08-23 owner-choice
wording; `.agent/user_stories/follow-bottom-scroll.md` exists.
wording; `.agents/user_stories/follow-bottom-scroll.md` exists.
- [ ] One `--no-gpg-sign` commit (below);
`.agent/phases/todo/18_follow_bottom_scroll/` moved to
`.agent/phases/complete/`.
`.agents/phases/todo/18_follow_bottom_scroll/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -A .agent/ 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"
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"
```
@@ -1,6 +1,6 @@
# Task 01 — app.js: single scroll gate (follow-the-bottom)
**Phase:** `18_follow_bottom_scroll` · **Story:** `.agent/user_stories/follow-bottom-scroll.md`
**Phase:** `18_follow_bottom_scroll` · **Story:** `.agents/user_stories/follow-bottom-scroll.md`
## Objective
All chat-page scrolling goes through one gate — `scrollReveal` — which
@@ -1,6 +1,6 @@
# Task 02 — E2E: the follow-the-bottom story suite
**Phase:** `18_follow_bottom_scroll` · **Story:** `.agent/user_stories/follow-bottom-scroll.md`
**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
@@ -1,6 +1,6 @@
# Task 03 — Story file, PLAN revisions, the phase commit
**Phase:** `18_follow_bottom_scroll` · **Story:** `.agent/user_stories/follow-bottom-scroll.md`
**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
@@ -8,7 +8,7 @@ revisions with the owner-choice noted, and the single atomic
`--no-gpg-sign` commit with the phase moved to `complete/`.
## Work
1. `.agent/user_stories/follow-bottom-scroll.md` (new — match the
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`.
@@ -39,7 +39,7 @@ revisions with the owner-choice noted, and the single atomic
unchanged.
- Playwright Mapping Rule: the five scenarios of
`tests/e2e/test_follow_bottom_scroll.py` verbatim from task 02.
2. `.agent/PLAN.md` revisions — **record the owner choice
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
@@ -67,9 +67,9 @@ revisions with the owner-choice noted, and the single atomic
- `rg -c "scrollIntoView" frontend/assets/app.js` → 1.
5. Commit + phase move (last step, only when all gates are green):
```bash
git add -A .agent/ frontend/ tests/
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 .agent/phases/todo/18_follow_bottom_scroll .agent/phases/complete/
mv .agents/phases/todo/18_follow_bottom_scroll .agents/phases/complete/
```
## Testing & Quality
@@ -78,13 +78,13 @@ revisions with the owner-choice noted, and the single atomic
the owning task's files, re-run that task's tests, then commit.
## Completion Criteria
- [ ] `.agent/user_stories/follow-bottom-scroll.md` exists with all
- [ ] `.agents/user_stories/follow-bottom-scroll.md` exists with all
five sections (header, narrative, acceptance, UI visualization,
Playwright Mapping Rule).
- [ ] `.agent/PLAN.md` carries the header-revision/§7.4/§12 notes with
- [ ] `.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`;
`.agent/phases/todo/18_follow_bottom_scroll/` is now under
`.agent/phases/complete/`.
`.agents/phases/todo/18_follow_bottom_scroll/` is now under
`.agents/phases/complete/`.
@@ -1,6 +1,6 @@
# Phase 19 — Shared Header: auth + New Chat on every page, Sources link admin-only
**Story:** `.agent/user_stories/shared-header.md` (created by task 03)
**Story:** `.agents/user_stories/shared-header.md` (created by task 03)
**Context:** the four page headers (`frontend/index.html`,
`sources.html`, `document.html`, `login.html` — each hand-rolled, which is
exactly why the controls "disappear" between pages), `frontend/assets/
@@ -176,12 +176,12 @@ of the A10 revision are untouched).
focus-visible, no CDN tags, one header bar per page, heights
unchanged.
- [ ] PLAN carries the revisions with the 2026-08-23 owner-permission
wording; `.agent/user_stories/shared-header.md` exists.
wording; `.agents/user_stories/shared-header.md` exists.
- [ ] One `--no-gpg-sign` commit (below);
`.agent/phases/todo/19_shared_header/` moved to
`.agent/phases/complete/`.
`.agents/phases/todo/19_shared_header/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -A .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(ui): shared header — Sign in/Sign out and New Chat on every page; hide the Sources nav link from anonymous users"
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(ui): shared header — Sign in/Sign out and New Chat on every page; hide the Sources nav link from anonymous users"
```
@@ -1,6 +1,6 @@
# Task 01 — header.js shared module + page wiring
**Phase:** `19_shared_header` · **Story:** `.agent/user_stories/shared-header.md`
**Phase:** `19_shared_header` · **Story:** `.agents/user_stories/shared-header.md`
## Objective
One shared header module drives the auth controls and the Sources nav
@@ -1,6 +1,6 @@
# Task 02 — E2E: the shared-header story suite
**Phase:** `19_shared_header` · **Story:** `.agent/user_stories/shared-header.md`
**Phase:** `19_shared_header` · **Story:** `.agents/user_stories/shared-header.md`
## Objective
Dedicated Playwright gate (A16 — one story, one file, isolated): the
@@ -1,6 +1,6 @@
# Task 03 — Story file, PLAN revisions, the phase commit
**Phase:** `19_shared_header` · **Story:** `.agent/user_stories/shared-header.md`
**Phase:** `19_shared_header` · **Story:** `.agents/user_stories/shared-header.md`
## Objective
Record the change: the user story file (AGENTS.md rule 4), the PLAN
@@ -9,7 +9,7 @@ choice — say so explicitly), and the single atomic `--no-gpg-sign`
commit with the phase moved to `complete/`.
## Work
1. `.agent/user_stories/shared-header.md` (new — match the sibling
1. `.agents/user_stories/shared-header.md` (new — match the sibling
story format):
- Header: `**Phase:** 19_shared_header · **E2E:**
tests/e2e/test_shared_header.py`.
@@ -45,7 +45,7 @@ commit with the phase moved to `complete/`.
`.auth-link` mobile icon-only rules; login page boundary.
- Playwright Mapping Rule: the six scenarios of
`tests/e2e/test_shared_header.py` verbatim from task 02.
2. `.agent/PLAN.md` revisions — **owner permission 2026-08-23 (this
2. `.agents/PLAN.md` revisions — **owner permission 2026-08-23 (this
request)** in each note, phase-16-revision style:
- Header revisions line: append
`; shared header (Phase 19)`.
@@ -74,9 +74,9 @@ commit with the phase moved to `complete/`.
- `uv run ruff check . && uv run pyright`.
5. Commit + phase move (last step, only when all gates are green):
```bash
git add -A .agent/ frontend/ tests/
git add -A .agents/ frontend/ tests/
git commit --no-gpg-sign -m "feat(ui): shared header — Sign in/Sign out and New Chat on every page; hide the Sources nav link from anonymous users"
mv .agent/phases/todo/19_shared_header .agent/phases/complete/
mv .agents/phases/todo/19_shared_header .agents/phases/complete/
```
## Testing & Quality
@@ -85,14 +85,14 @@ commit with the phase moved to `complete/`.
task's files, re-run that task's tests, then commit.
## Completion Criteria
- [ ] `.agent/user_stories/shared-header.md` exists with all five
- [ ] `.agents/user_stories/shared-header.md` exists with all five
sections (header, narrative, acceptance, UI visualization,
Playwright Mapping Rule).
- [ ] `.agent/PLAN.md` carries the header-revision/§2-A10-note/§7.1/
- [ ] `.agents/PLAN.md` carries the header-revision/§2-A10-note/§7.1/
§7.5/§12 notes with the 2026-08-23 owner-permission wording; the
A10 decision text itself is unaltered.
- [ ] All gates green (coverage, story E2E + 5 regressions in
isolation, ruff + pyright).
- [ ] Exactly one new commit, conventional, `--no-gpg-sign`;
`.agent/phases/todo/19_shared_header/` is now under
`.agent/phases/complete/`.
`.agents/phases/todo/19_shared_header/` is now under
`.agents/phases/complete/`.
@@ -2,7 +2,7 @@
**Source:** `TODO.md` L3 — *"Clicking "sources" while chat is generating
clears chat and result will never show up"*
**Story:** `.agent/user_stories/sources-midstream.md` (created by task 02)
**Story:** `.agents/user_stories/sources-midstream.md` (created by task 02)
**Context:** `frontend/assets/app.js` — the phase-14 persistence block
(`STORAGE_KEY = "bor.chat.v1"`, `conversation`, `saveConversation`,
`rememberBrainTurn`), the turn state machine (`UI_STATE.thinking` /
@@ -111,12 +111,12 @@ restored if any).
- [ ] UI Structure Check (AGENTS.md rule 5): no new UI surface — the
restored partial renders through the existing bubble/thinking
contract.
- [ ] `.agent/user_stories/sources-midstream.md` exists.
- [ ] `.agents/user_stories/sources-midstream.md` exists.
- [ ] One `--no-gpg-sign` commit (below);
`.agent/phases/todo/20_sources_midstream_bug/` moved to
`.agent/phases/complete/`.
`.agents/phases/todo/20_sources_midstream_bug/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -A .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(chat): keep the in-flight answer when navigating away mid-turn — partial answer restored on return"
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(chat): keep the in-flight answer when navigating away mid-turn — partial answer restored on return"
```
@@ -38,7 +38,7 @@ validation, and the single atomic commit.
4. `test_new_chat_still_clears_conversation` — regression: completed
turn → `/sources.html` → click the sources-page New Chat button →
lands on `/` with the empty state and `bor.chat.v1` removed.
2. `.agent/user_stories/sources-midstream.md` (new) — the short story
2. `.agents/user_stories/sources-midstream.md` (new) — the short story
file matching the repo's story format (goal, the bug report verbatim
from `TODO.md` L3, the owner-confirmed A1 decisions from
`00_phase.md`, the E2E mapping table test-name → scenario).
@@ -55,13 +55,13 @@ validation, and the single atomic commit.
renders through the existing bubble/thinking contract — no new
surface, no new ids, focus/contrast unchanged.
7. Write the phase report
(`.agent/reports/20_sources_midstream_bug/` — what changed, E2E
(`.agents/reports/20_sources_midstream_bug/` — what changed, E2E
results, the manual-smoke note from task 01).
8. Commit (one atomic commit) and move the phase:
```bash
git add -A .agent/ frontend/ tests/
git add -A .agents/ frontend/ tests/
git commit --no-gpg-sign -m "fix(chat): keep the in-flight answer when navigating away mid-turn — partial answer restored on return"
mv .agent/phases/todo/20_sources_midstream_bug .agent/phases/complete/
mv .agents/phases/todo/20_sources_midstream_bug .agents/phases/complete/
```
## Testing & Quality
@@ -2,7 +2,7 @@
**Source:** `TODO.md` L4 — *"Disable scroll in the thinking window.
Users don't need to scroll back through thinking, just see it live."*
**Story:** `.agent/user_stories/thinking-no-scroll.md` (created by task 02)
**Story:** `.agents/user_stories/thinking-no-scroll.md` (created by task 02)
**Context:** `frontend/assets/styles.css` —
`details.thinking .thinking-text` (today: `max-height: 320px;
overflow-y: auto;`, i.e. a user-scrollable 320px window, ~line 444);
@@ -93,12 +93,12 @@ bottom as chunks arrive.
- [ ] UI Structure Check (AGENTS.md rule 5): no new surface; the block
keeps its focus-visible summary, aria contract, and reduced-motion
behavior.
- [ ] `.agent/user_stories/thinking-no-scroll.md` exists.
- [ ] `.agents/user_stories/thinking-no-scroll.md` exists.
- [ ] One `--no-gpg-sign` commit (below);
`.agent/phases/todo/21_thinking_no_scroll/` moved to
`.agent/phases/complete/`.
`.agents/phases/todo/21_thinking_no_scroll/` moved to
`.agents/phases/complete/`.
## Commit
```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"
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(ui): thinking window no longer scrolls — live 320px view pinned to the stream tail"
```
@@ -2,7 +2,7 @@
**Source:** `TODO.md` L5 — *"Fix background animation not working, just
blinking"*
**Story:** `.agent/user_stories/background-animation.md` (created by task 02)
**Story:** `.agents/user_stories/background-animation.md` (created by task 02)
**Context:** `frontend/assets/styles.css` — the phase-08 animated
background block (~lines 60–100): `body::before` (44px drifting grid,
1px lines at ~35% `--line` alpha, radial mask
@@ -52,7 +52,7 @@ Chromium window (dev server, full page, ~15s of observation):
contract.
5. **Apply the fix in `styles.css`** — document the found root cause in
the phase report (screenshot before/after in
`.agent/screenshots/22_background_animation/`).
`.agents/screenshots/22_background_animation/`).
## Dependencies
- `08_story_dark_tech_theme` (complete) — owns the layers, the palette,
@@ -93,7 +93,7 @@ Chromium window (dev server, full page, ~15s of observation):
- [ ] In a real Chromium viewport, the background visibly and smoothly
animates (grid drift + glow breathe) — screenshot before/after in
the phase report; owner's "just blinking" perception gone.
- [ ] Root cause documented in `.agent/reports/22_background_animation/`.
- [ ] Root cause documented in `.agents/reports/22_background_animation/`.
- [ ] `uv run pytest` green; `uv run pytest --cov=app
--cov-report=term-missing` ≥ today's number.
- [ ] `uv run pytest tests/e2e/test_background_animation.py -v --no-cov`
@@ -103,12 +103,12 @@ Chromium window (dev server, full page, ~15s of observation):
- [ ] UI Structure Check (AGENTS.md rule 5): layers stay behind content
(`z-index: -1`, `pointer-events: none`), no text/contrast impact,
no 360px overflow.
- [ ] `.agent/user_stories/background-animation.md` exists.
- [ ] `.agents/user_stories/background-animation.md` exists.
- [ ] One `--no-gpg-sign` commit (below);
`.agent/phases/todo/22_background_animation/` moved to
`.agent/phases/complete/`.
`.agents/phases/todo/22_background_animation/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -A .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(ui): animated background actually animates — grid drift and glow breathe per the phase-08 design"
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(ui): animated background actually animates — grid drift and glow breathe per the phase-08 design"
```
@@ -32,7 +32,7 @@ commit.
5. `test_no_horizontal_overflow_with_layers` (regression, 360px) —
viewport 360px: `document.documentElement.scrollWidth <=
clientWidth` (the phase-07 pin, replicated locally).
2. `.agent/user_stories/background-animation.md` (new) — story file per
2. `.agents/user_stories/background-animation.md` (new) — story file per
the repo format: goal, the bug report verbatim from `TODO.md` L5, the
owner-confirmed A3 decisions + the found root cause (from task 01's
report), E2E mapping table.
@@ -46,13 +46,13 @@ commit.
`uv run ruff check . && uv run pyright` clean.
6. **UI Structure Check** (AGENTS.md rule 5): layers stay behind
content, no text/contrast impact, no overflow at 360px.
7. Finish the phase report (`.agent/reports/22_background_animation/` —
7. Finish the phase report (`.agents/reports/22_background_animation/` —
E2E results + the task-01 screenshots).
8. Commit (one atomic commit) and move the phase:
```bash
git add -A .agent/ frontend/ tests/
git add -A .agents/ frontend/ tests/
git commit --no-gpg-sign -m "fix(ui): animated background actually animates — grid drift and glow breathe per the phase-08 design"
mv .agent/phases/todo/22_background_animation .agent/phases/complete/
mv .agents/phases/todo/22_background_animation .agents/phases/complete/
```
## Testing & Quality
@@ -4,7 +4,7 @@
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:** `.agent/user_stories/background-no-motion.md` (created by task 02)
**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` →
@@ -149,9 +149,9 @@ the block's perf note).
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).
- [ ] `.agent/reports/25_background_no_motion/` documents the two root
- [ ] `.agents/reports/25_background_no_motion/` documents the two root
causes with before/after screenshot pairs
(`.agent/screenshots/25_background_no_motion/`).
(`.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`
@@ -161,20 +161,20 @@ the block's perf note).
- [ ] `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.
- [ ] `.agent/user_stories/background-no-motion.md` exists; the old
- [ ] `.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);
`.agent/phases/todo/25_background_no_motion/` moved to
`.agent/phases/complete/`.
`.agents/phases/todo/25_background_no_motion/` moved to
`.agents/phases/complete/`.
## Commit
```bash
git add -f .agent/phases/complete/25_background_no_motion \
.agent/user_stories/background-no-motion.md \
.agent/user_stories/background-animation.md \
.agent/reports/25_background_no_motion \
.agent/screenshots/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)"
```
@@ -1,6 +1,6 @@
# Task 02 — Story E2E Suite, Regression Adaptations, Story File, Commit
**Phase:** `25_background_no_motion` · **Story:** `.agent/user_stories/background-no-motion.md`
**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,
@@ -79,7 +79,7 @@ commit.
- `test_reduced_motion_honored` — assert **all four** pseudo-layers
(body `::before`/`::after` + documentElement `::before`/`::after`)
report `animationName` `"none"` and keep their images.
4. **`.agent/user_stories/background-no-motion.md`** — the story file, in
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
@@ -90,11 +90,11 @@ commit.
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. **`.agent/user_stories/background-animation.md`** — add a short
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. **`.agent/reports/25_background_no_motion/`** — short report: the two
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`/
@@ -115,17 +115,17 @@ commit.
8. **One atomic commit** — stage **only this phase's files**; the
unrelated dirty `TODO.md` in the worktree must NOT be staged:
```bash
mv .agent/phases/todo/25_background_no_motion .agent/phases/complete/25_background_no_motion
git add -f .agent/phases/complete/25_background_no_motion \
.agent/user_stories/background-no-motion.md \
.agent/user_stories/background-animation.md \
.agent/reports/25_background_no_motion \
.agent/screenshots/25_background_no_motion
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/`;
`.agent/` is gitignored by design, hence `git add -f`.)
`.agents/` is gitignored by design, hence `git add -f`.)
## Testing & Quality
- The new E2E suite is the story gate (A16): green **in isolation**;
@@ -146,4 +146,4 @@ commit.
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 `.agent/phases/complete/`.
directory now lives in `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 27 — Global Tuning Manager
**Source:** `TODO.md L3 — "Add a way to add 'global tuning' without having a chat to reply to. Also previous tunes should be editable."`
**Story:** `.agent/user_stories/global-tuning.md`
**Story:** `.agents/user_stories/global-tuning.md`
**Context:** Phase 15 added steering notes (owner instructions injected into every system prompt as the `<tuning>` section); phase 16 gated the whole `/api/steering` router behind `require_admin`. The current UI only lets the admin **create** a note by pressing "Tune" under a completed chat bubble, and the header "Tuning" panel lists notes newest-first with **delete-only** per note. There is no way to add a note without a chat, and no way to edit an existing one.
## Objective
@@ -33,8 +33,8 @@ Give the admin a **Global Tuning** page (`/tuning.html`) where notes can be crea
- [ ] `uv run pytest tests/e2e/test_global_tuning.py -v --no-cov` green in isolation.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] UI Structure Check (AGENTS.md rule 5): tuning page — landmarks, labeled controls, contrast ≥4.5:1, focus-visible, ≥44px targets, centered column, no CDN.
- [ ] `.agent/user_stories/global-tuning.md` exists.
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agent/phases/todo/27_global_tuning/` moved to `.agent/phases/complete/`.
- [ ] `.agents/user_stories/global-tuning.md` exists.
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agents/phases/todo/27_global_tuning/` moved to `.agents/phases/complete/`.
## Locked decisions
- **A10 untouched** — `PUT /api/steering/{note_id}` is a new stateless route under `/api`; the API stays stateless, admin-only via `require_admin` (the existing gate).
@@ -1,7 +1,7 @@
# Phase 28 — Git-Based Sources
**Source:** `TODO.md L5 — "We shouldn't be hard-coding Homelab and Deployments. Instead, a list of git links should be specified. import_docs should clone or pull to a dedicated repository location and then index all the specified repository code."`
**Story:** `.agent/user_stories/git-sources.md`
**Story:** `.agents/user_stories/git-sources.md`
**Context:** Phase 11 / the README document the import workflow. `scripts/import_docs.py` currently defaults to the hardcoded `DEFAULT_SOURCES = [Path("~/Homelab"), Path("~/Deployments")]` (repeatable via `--source`). The importer (`app/rag/importer.py::import_sources`) already walks any list of local directories — it needs no change to the walking logic, only to receive the resolved local paths.
## Objective
@@ -29,8 +29,8 @@ Replace the hardcoded `~/Homelab` + `~/Deployments` default with a **list of git
- [ ] `.env.example` documents `BOR_GIT_SOURCES` + `BOR_SOURCES_DIR`; README import section rewritten.
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL ≥ pre-change number.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] `.agent/user_stories/git-sources.md` exists.
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agent/phases/todo/28_git_based_sources/` moved to `.agent/phases/complete/`.
- [ ] `.agents/user_stories/git-sources.md` exists.
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agents/phases/todo/28_git_based_sources/` moved to `.agents/phases/complete/`.
## Locked decisions
- **A13 untouched** — no DB migration; the importer's `documents`/`chunks` schema is unchanged (source/path are stored as before, keyed by the repo-relative path).
@@ -1,7 +1,7 @@
# Phase 34 — One Navbar on Every Page
**Source:** `TODO.md` L3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."
**Story:** `.agent/user_stories/nav-consistency.md`
**Story:** `.agents/user_stories/nav-consistency.md`
**Context:** The shared header (phase 19, `frontend/assets/header.js`) + the Tuning nav link (phase 29) already standardize nav + auth on chat / sources / tuning — but `document.html` still uses the separate `.doc-header` variant (back + title + actions, **no nav at all**), `login.html` misses the Tuning link, and two functional controls remain page-scoped: the Tuning steering toggle + panel (chat only, logic in `app.js`) and the Sync sources button (Sources only, logic in `sources.js`). Owner confirmation (2026-08-26): the bar must be identical on **all** pages — nav, Tuning toggle, Sync, New chat, and the auth pair all present everywhere; the locked A10 UI revision stays (admin-only controls hidden for anonymous, active for the admin on every tab).
## Objective
@@ -36,7 +36,7 @@ Make the header bar **identical on all five pages** (chat, sources, document vie
- [ ] Regressions green in isolation: `test_header_consistency.py`, `test_shared_header.py`, `test_document_back_navigation.py`, `test_document_viewer.py`, `test_steering.py`, `test_global_tuning.py`, `test_sync_button.py`, `test_tuning_nav_link.py`, `test_smoke.py`, `test_chat_rag.py`, `test_admin_auth.py`.
- [ ] `uv run ruff check . && uv run pyright` clean (no Python change, but run the gate).
- [ ] UI Structure Check (AGENTS.md rule 5): landmarks / labels / contrast ≥4.5:1 / focus-visible preserved; no CDN (rule 6).
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agent/phases/todo/34_consistent_navbar/` moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agents/phases/todo/34_consistent_navbar/` moved to `.agents/phases/complete/`.
## Locked decisions
- **A10 UI revision preserved** — admin-only controls (Sources / Tuning nav links, Sync button) ship hidden and are revealed only for the signed-in admin; anonymous visitors get the reduced bar, identically on every page (owner confirmation 2026-08-26 — "hidden for anon, visible for admin").
@@ -1,7 +1,7 @@
# Task 01 — Steering toggle + panel move into header.js
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
**Story:** `.agent/user_stories/nav-consistency.md`
**Story:** `.agents/user_stories/nav-consistency.md`
## Objective
Make `frontend/assets/header.js` the owner of the steering toggle + panel behavior (today in `frontend/assets/app.js`), so the toggle can sit in every page's header (task 03) with zero page-script duplication. The chat page's behavior — panel open/close, list, count badge, per-note delete, per-bubble Tune form — must be byte-for-byte the same from the user's perspective.
@@ -1,7 +1,7 @@
# Task 02 — Sync state machine + New chat + sign-in next move into header.js
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
**Story:** `.agent/user_stories/nav-consistency.md`
**Story:** `.agents/user_stories/nav-consistency.md`
## Objective
Make `header.js` the owner of the Sync button state machine (today in `sources.js`), the single New chat binding (today duplicated across `app.js` / `sources.js` / `tuning.js` / `document.js`), and the sign-in `?next=` derivation — so the same markup on any page (task 03) behaves identically.
@@ -1,7 +1,7 @@
# Task 03 — The identical full header on all five pages
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
**Story:** `.agent/user_stories/nav-consistency.md`
**Story:** `.agents/user_stories/nav-consistency.md`
## Objective
Ship the **identical header block** on all five pages — brand, nav [Chat, Sources, Tuning], Tuning toggle, Sync sources, New chat, Sign in / Sign out — and the `#steering-panel` section on every page; the document viewer keeps back + title in a second titlebar row; the login page finally carries the full header.
@@ -1,7 +1,7 @@
# Task 04 — Two-row viewer header styles + sync failed state
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
**Story:** `.agent/user_stories/nav-consistency.md`
**Story:** `.agents/user_stories/nav-consistency.md`
## Objective
Style the document viewer's new two-row header (standard row + titlebar row) so row 1 is visually indistinguishable from the other pages' bars, and give the Sync button a visible failed state on pages that have no error banner (every page except Sources).
@@ -1,7 +1,7 @@
# Task 05 — Story E2E + contract-suite updates + regression pass
**Phase:** `34_consistent_navbar` · **Source:** `TODO.md:3 — "I want the navbar to be consistent between every page. I don't want buttons to pop in and out of existance. Just keep all those buttons active across all tabs."`
**Story:** `.agent/user_stories/nav-consistency.md`
**Story:** `.agents/user_stories/nav-consistency.md`
## Objective
Prove the contract with the story's dedicated Playwright suite — identical visible header control inventory on all five pages for each role — update the two pre-existing contract suites that encoded the old viewer bar, and run the full regression list.
@@ -18,7 +18,7 @@ Prove the contract with the story's dedicated Playwright suite — identical vis
4. **Regression pass — each in isolation** (`uv run pytest tests/e2e/<file>.py -v --no-cov`): `test_header_consistency.py`, `test_shared_header.py`, `test_document_back_navigation.py`, `test_document_viewer.py`, `test_steering.py`, `test_global_tuning.py`, `test_sync_button.py`, `test_tuning_nav_link.py`, `test_smoke.py`, `test_chat_rag.py`, `test_admin_auth.py`. Fix fallout in the suites above where the old contract is encoded; fix app code where behavior genuinely changed.
5. Full gate: `uv run pytest` (unit + integration), `uv run pytest --cov=app --cov-report=term-missing` (TOTAL unchanged, >90%), `uv run ruff check . && uv run pyright`.
6. **UI Structure Check** (AGENTS.md rule 5) on the five headers + the new titlebar: landmarks (`<header>`, `<nav aria-label>`, `<main>`), labels, contrast ≥4.5:1, focus-visible, no CDN (rule 6 — the no-CDN integration test covers it).
7. **Commit** (A17): stage only this phase's files (`frontend/**`, `tests/e2e/**`), message `feat(ui): one consistent navbar on every page (TODO.md L3)`, always `--no-gpg-sign`. Move `.agent/phases/todo/34_consistent_navbar/` to `.agent/phases/complete/`.
7. **Commit** (A17): stage only this phase's files (`frontend/**`, `tests/e2e/**`), message `feat(ui): one consistent navbar on every page (TODO.md L3)`, always `--no-gpg-sign`. Move `.agents/phases/todo/34_consistent_navbar/` to `.agents/phases/complete/`.
## Testing & Quality
- E2E: `tests/e2e/test_nav_consistency.py` green **in isolation** — the story gate (A16: one story, one file).
@@ -30,4 +30,4 @@ Prove the contract with the story's dedicated Playwright suite — identical vis
- [ ] `test_header_consistency.py` + `test_shared_header.py` updated and green; every suite in the task 05 regression list green in isolation.
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged (>90%).
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 37 — Agent document tools (list + read)
**Source:** `TODO.md` L3–L9 — "Still having trouble referencing specific documents. The agent should be able to list the available sources as a tool and the read the ones it thinks are relevant… it will need a basic agent loop. I'm thinking it gets one opportunity to list documents and then one opportunity to add exactly one extra document to its context before being required to answer. These values should be configured by environment variables." (L3; L5–L9 is the failure example: `aws-route53.md` references `example-record-file.json` whose contents are not in context, so the model refuses to guess)
**Story:** `.agent/user_stories/agent-document-tools.md`
**Story:** `.agents/user_stories/agent-document-tools.md`
**Context:** PLAN §3/§4 (chat flow + SSE contract), §6 (locked persona prompt), §9 (per-turn log line); `app/api/chat.py` (`plan_turn` + stream), `app/rag/llm.py` (`StreamPiece`, `chat_stream`), `app/rag/prompts.py`, `app/schemas.py`, `frontend/assets/app.js` (chat feedback state machine + thinking block), `tests/e2e/mock_llm.py`.
## Objective
@@ -38,7 +38,7 @@ Give the chat model two server-side tools on **grounded** turns — `list_docume
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run pytest tests/e2e/test_agent_document_tools.py -v --no-cov` green in isolation; existing chat E2E suites green.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] UI Structure Check (AGENTS.md rule 5) + no CDN (rule 6).
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agents/phases/complete/`.
## Locked decisions
- **A7/A8 honoured** — the retrieval gate is untouched; `read_document` appends the **full** document text (no truncation, A7-revised contract); the deflection path is byte-identical.
@@ -1,7 +1,7 @@
# Task 06 — Mock-LLM tool behavior + story E2E + docs + commit
**Phase:** `37_agent_document_tools` · **Source:** `TODO.md:3–9 — "The agent should be able to list the available sources as a tool and the read the ones it thinks are relevant"` (L5–L9 failure example: `aws-route53.md` references `example-record-file.json` for the exact JSON shape but the file's contents are not in context — "I don't want to invent it!")
**Story:** `.agent/user_stories/agent-document-tools.md`
**Story:** `.agents/user_stories/agent-document-tools.md`
## Objective
A deterministic mock tool-call behavior, the story's isolated Playwright suite, the docs, and the phase commit.
@@ -20,7 +20,7 @@ A deterministic mock tool-call behavior, the story's isolated Playwright suite,
- Deflected question (the mock's unrelated-question path) → no `tool` frames (the grounded-only scope).
4. Docs: `.env.example` (`BOR_AGENT_LIST_CALLS` / `BOR_AGENT_READ_CALLS` — defaults 1/1, "0 disables the tool") and README (chat-behavior section: the two tools, the budgets, the SSE `tool` frame, the "calling tool" UI state).
5. Regression pass: `uv run pytest` (unit + integration), `uv run pytest --cov=app --cov-report=term-missing` (>90%), `uv run pytest tests/e2e/test_agent_document_tools.py -v --no-cov` (in isolation), plus the chat-adjacent E2E suites green: `test_chat_rag.py`, `test_thinking_display.py`, `test_honest_deflection.py`, `test_chat_persistence.py`.
6. Commit — one atomic `--no-gpg-sign` Conventional Commits commit for the whole phase (AGENTS.md rule 8), e.g. `feat(rag): agent document tools — list/read tools with env-tuned budgets, SSE tool events + "calling tool" UI`; include the task-01 probe verdict in the commit message; move the phase directory to `.agent/phases/complete/`.
6. Commit — one atomic `--no-gpg-sign` Conventional Commits commit for the whole phase (AGENTS.md rule 8), e.g. `feat(rag): agent document tools — list/read tools with env-tuned budgets, SSE tool events + "calling tool" UI`; include the task-01 probe verdict in the commit message; move the phase directory to `.agents/phases/complete/`.
## Testing & Quality
- The gates above are this task's quality bar (A16: one story, one isolated E2E file, coverage >90%).
@@ -29,4 +29,4 @@ A deterministic mock tool-call behavior, the story's isolated Playwright suite,
- [ ] The marker flow is deterministic across two consecutive isolated runs.
- [ ] Step-5 suites all green; coverage >90%.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 38 — Local directory sources
**Source:** `TODO.md` L11 — "Also need a way to import from existing directory if it's not a git repo"
**Story:** `.agent/user_stories/local-directory-sources.md`
**Story:** `.agents/user_stories/local-directory-sources.md`
**Context:** `35_git_sources_admin` (todo — runs first: the `git_sources` table, the admin API, the sources page, `effective_git_sources()`), `32_admin_sync_button` (the in-process sync pipeline), `28_git_based_sources` (the `import_docs` resolution order + `clone_or_pull`), `16_admin_auth` (the `require_admin` pattern).
## Objective
@@ -35,7 +35,7 @@ Make an **existing local directory** (not a git repo) a first-class source: the
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run pytest tests/e2e/test_local_directory_sources.py -v --no-cov` green in isolation; the task-05 regression list green.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] UI Structure Check (AGENTS.md rule 5) + no CDN (rule 6).
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agents/phases/complete/`.
## Locked decisions
- **The phase-35 table is extended, not duplicated (owner permission 2026-08-26)** — one `git_sources` table with a `kind` discriminator, one admin page, one Sync button; no `local_sources` table, no second page.
@@ -1,7 +1,7 @@
# Task 05 — Story E2E + docs + commit
**Phase:** `38_local_directory_sources` · **Source:** `TODO.md:11 — "Also need a way to import from existing directory if it's not a git repo"`
**Story:** `.agent/user_stories/local-directory-sources.md`
**Story:** `.agents/user_stories/local-directory-sources.md`
## Objective
The story's isolated Playwright suite (add → validate → sync-import → prune-remove), the README update, and the phase commit.
@@ -16,7 +16,7 @@ The story's isolated Playwright suite (add → validate → sync-import → prun
2. README: the "Sources" section — the two kinds (git = clone/pull mirror; local = direct import of an existing directory), add-time validation, union pruning; note that the DB is the local-source registry (no env var for local paths).
3. `.env.example` — no new variable; extend phase 35's `BOR_GIT_SOURCES` note if needed ("env fallback is git-only — local directories are registered on the admin page").
4. Regression pass: `uv run pytest` + the coverage gate (>90%) + the isolated story E2E + `tests/e2e/test_git_sources_admin.py` (phase 35's suite — its page assertions must survive the new form; if a selector collided, scope the test to the git form and note it in the commit message) + `tests/e2e/test_sync_button.py`.
5. Commit — one atomic `--no-gpg-sign` Conventional Commits commit for the whole phase (AGENTS.md rule 8), e.g. `feat(admin): local directory sources — kind/path on git_sources, combined sync + import, page form + badges`; move the phase directory to `.agent/phases/complete/`.
5. Commit — one atomic `--no-gpg-sign` Conventional Commits commit for the whole phase (AGENTS.md rule 8), e.g. `feat(admin): local directory sources — kind/path on git_sources, combined sync + import, page form + badges`; move the phase directory to `.agents/phases/complete/`.
## Testing & Quality
- The gates above are this task's quality bar (A16: one story, one isolated E2E file, coverage >90%).
@@ -25,4 +25,4 @@ The story's isolated Playwright suite (add → validate → sync-import → prun
- [ ] The story E2E is green in isolation, deterministic across two consecutive runs.
- [ ] 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/`.
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 40 — Tuning toggle anonymous flash
**Source:** `TODO.md` L3 — "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`
**Story:** `.agents/user_stories/tuning-toggle-flash.md`
**Context:** Phase 15/34 shared header (`frontend/assets/header.js` owns `#steering-toggle` / `#steering-panel` on all six pages; anonymous → `remove()` post-whoami). The admin-only **nav links** already ship `hidden` (phase-19 contract) — the flashing control is the **steering toggle button labeled "Tuning"**, which ships visible in all six pages and is removed only after `/api/whoami` resolves.
## Objective
@@ -28,7 +28,7 @@ Kill the anonymous flash: the tuning toggle ships `hidden` in every page's marku
- [ ] Regression E2E suites green in isolation: `test_shared_header.py`, `test_global_tuning.py`, `test_steering.py`, `test_tuning_nav_link.py`, `test_smoke.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] UI Structure Check (AGENTS.md rule 5): no new focus targets; landmarks/contrast unchanged; no CDN.
- [ ] One `--no-gpg-sign` commit; phase dir moved `.agent/phases/todo/` → `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved `.agents/phases/todo/` → `.agents/phases/complete/`.
## Locked decisions
- **A10 untouched** — no API change; the fix is pure UI visibility off the existing `/api/whoami` gate.
@@ -1,7 +1,7 @@
# Task 02 — Flash E2E + regression + commit
**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`
**Story:** `.agents/user_stories/tuning-toggle-flash.md`
## Objective
Prove the flash is gone at the browser level (never visible, not even for a frame) and that the shared-header contract is intact; commit the phase.
@@ -15,7 +15,7 @@ Prove the flash is gone at the browser level (never visible, not even for a fram
- `test_nav_contract_regression` — anonymous: `#nav-sources` / `#nav-git-sources` / `#nav-tuning` stay hidden; admin: revealed.
2. Regression pass (isolation runs, per A16): `test_shared_header.py`, `test_global_tuning.py`, `test_steering.py`, `test_tuning_nav_link.py`, `test_smoke.py` — all green; fix only true regressions.
3. `uv run pytest` (unit+integration) green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged; `uv run ruff check . && uv run pyright` clean.
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `fix(header): ship the tuning toggle hidden — no anonymous flash`, staging this phase's changed files; move `.agent/phases/todo/40_tuning_toggle_flash/` → `.agent/phases/complete/` (force-add per AGENTS.md rule 8 if the history tracks the tree).
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `fix(header): ship the tuning toggle hidden — no anonymous flash`, staging this phase's changed files; move `.agents/phases/todo/40_tuning_toggle_flash/` → `.agents/phases/complete/` (force-add per AGENTS.md rule 8 if the history tracks the tree).
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_tuning_toggle_flash.py -v --no-cov` green in isolation (DB up: `podman compose up -d db`).
@@ -23,4 +23,4 @@ Prove the flash is gone at the browser level (never visible, not even for a fram
## Completion Criteria
- [ ] The story E2E file passes in isolation; the four regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 41 — Sync fails fast + modal when a model is down
**Source:** `TODO.md` L4 — "If the embedding or lite model is not accessible the sync button should fail fast and there should be a modal error popup explaining that the model isn't available."
**Story:** `.agent/user_stories/sync-model-fail-fast.md`
**Story:** `.agents/user_stories/sync-model-fail-fast.md`
**Context:** `app/api/sync.py::_run_sync` (phase 32/35/38) runs source resolution → git clones → `import_sources` (embeds) → overview (lite) — with a dead LLM endpoint the run discovers it only mid-import, after slow clones. The sync state machine is module-owned by `frontend/assets/header.js` (`applySyncFailure` → button title/aria + `.is-error` + `bor:sync-status` event; the Sources page renders `#sync-error-banner`). No dialog component exists yet.
## Objective
@@ -29,7 +29,7 @@ When `embed` or `lite` is unreachable, the sync fails **before any expensive wor
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%.
- [ ] `uv run pytest tests/e2e/test_sync_model_down.py -v --no-cov` green in isolation (DB up).
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **A12 untouched** — still in-process, no queue, no new service; the probe is two cheap model calls.
@@ -1,7 +1,7 @@
# Task 01 — Model probe: fail fast before any clone
**Phase:** `41_sync_fail_fast_models` · **Source:** `TODO.md:4` — "If the embedding or lite model is not accessible the sync button should fail fast and there should be a modal error popup explaining that the model isn't available."
**Story:** `.agent/user_stories/sync-model-fail-fast.md`
**Story:** `.agents/user_stories/sync-model-fail-fast.md`
## Objective
The sync run verifies both models it needs (`embed` + the summary `lite`) **first** — before source resolution, before any `clone_or_pull` — and fails the run with a clear, model-naming error when either is unreachable.
@@ -1,7 +1,7 @@
# Task 02 — Sync error modal (module-owned, every page)
**Phase:** `41_sync_fail_fast_models` · **Source:** `TODO.md:4` — "If the embedding or lite model is not accessible the sync button should fail fast and there should be a modal error popup explaining that the model isn't available."
**Story:** `.agent/user_stories/sync-model-fail-fast.md`
**Story:** `.agents/user_stories/sync-model-fail-fast.md`
## Objective
A readable, accessible modal dialog for sync failures, built by the shared header module (which owns the sync state machine), so every page carrying `#sync-btn` gets it with zero page-markup changes.
@@ -1,7 +1,7 @@
# Task 03 — Model-down E2E + regressions + commit
**Phase:** `41_sync_fail_fast_models` · **Source:** `TODO.md:4` — "If the embedding or lite model is not accessible the sync button should fail fast and there should be a modal error popup explaining that the model isn't available."
**Story:** `.agent/user_stories/sync-model-fail-fast.md`
**Story:** `.agents/user_stories/sync-model-fail-fast.md`
## Objective
Prove the whole story in the browser against a **dead model endpoint** — fast failure, readable modal, dismissal, unchanged secondary surfaces, and an untouched healthy pipeline — then commit the phase.
@@ -16,7 +16,7 @@ Prove the whole story in the browser against a **dead model endpoint** — fast
- Module fixture teardown: terminate both apps (the conftest pattern).
2. Regression pass (isolation runs): `tests/e2e/test_sync_button.py` (phase 32 — must stay green unmodified), `test_git_sources_admin.py`, `test_local_directory_sources.py`.
3. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(sync): fail fast with a modal when a model is unavailable`, staging this phase's files; move `.agent/phases/todo/41_sync_fail_fast_models/` → `.agent/phases/complete/`.
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(sync): fail fast with a modal when a model is unavailable`, staging this phase's files; move `.agents/phases/todo/41_sync_fail_fast_models/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_sync_model_down.py -v --no-cov` green in isolation.
@@ -25,4 +25,4 @@ Prove the whole story in the browser against a **dead model endpoint** — fast
## Completion Criteria
- [ ] The model-down suite passes in isolation: fast fail before clones, modal with model-naming error, all three dismissal paths, secondary surfaces intact, healthy run unaffected.
- [ ] Phase-32/35/38 regression suites green in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 42 — No reply autoscroll
**Source:** `TODO.md` L5 — "Get rid of the chat reply autoscroll, it's breaking things like making it impossible for the user to scroll while a reply generates."
**Story:** `.agent/user_stories/no-reply-autoscroll.md`
**Story:** `.agents/user_stories/no-reply-autoscroll.md`
**Context:** Phase 18 ("follow-the-bottom", owner choice 2026-08-23) added `NEAR_BOTTOM_PX = 200` / `isNearBottom()` / `scrollReveal(wrap, behavior, force)` in `frontend/assets/app.js`: the page auto-scrolls on every `thinking` / `tool` / `delta` frame while the user is within 200px of the bottom. The owner now finds that fighting their own scroll. The gate and the per-frame scrolls are **removed**; scrolling happens only on explicit user intent (submit, restore landing).
## Objective
@@ -27,7 +27,7 @@ The chat page never auto-scrolls during a turn. The viewport moves only when the
- [ ] `uv run pytest tests/e2e/test_no_reply_autoscroll.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_chat_rag.py`, `test_thinking_display.py`, `test_chat_persistence.py`, `test_long_answers.py`, `test_smoke.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner direction (2026-08-27, roadmap A1)** revises the phase-18 owner choice (2026-08-23): follow-the-bottom auto-follow is removed; submit-reveal + restore-landing are kept. Recorded in the story file and the `app.js` docstring (PLAN.md §7.4's Scroll row is a PLAN-side revision to be noted by the owner — this phase does not edit PLAN.md).
@@ -1,7 +1,7 @@
# Task 01 — Remove the auto-follow gate and per-frame scrolls
**Phase:** `42_no_reply_autoscroll` · **Source:** `TODO.md:5` — "Get rid of the chat reply autoscroll, it's breaking things like making it impossible for the user to scroll while a reply generates."
**Story:** `.agent/user_stories/no-reply-autoscroll.md`
**Story:** `.agents/user_stories/no-reply-autoscroll.md`
## Objective
`app.js` scrolls only on explicit user intent: sending a message and the phase-14 restore landing. No scroll happens anywhere in the streaming path.
@@ -1,7 +1,7 @@
# Task 02 — No-autoscroll E2E (replaces phase 18) + regressions + commit
**Phase:** `42_no_reply_autoscroll` · **Source:** `TODO.md:5` — "Get rid of the chat reply autoscroll, it's breaking things like making it impossible for the user to scroll while a reply generates."
**Story:** `.agent/user_stories/no-reply-autoscroll.md`
**Story:** `.agents/user_stories/no-reply-autoscroll.md`
## Objective
Prove the inverse of the phase-18 contract in the browser: no streaming autoscroll, submit-reveal and restore-landing intact — then delete the obsolete phase-18 suite and commit.
@@ -16,7 +16,7 @@ Prove the inverse of the phase-18 contract in the browser: no streaming autoscro
2. **Delete** `tests/e2e/test_follow_bottom_scroll.py` (its behavior is intentionally removed — owner direction 2026-08-27; the unit pin was rewritten in task 01).
3. Regression pass (isolation runs): `test_chat_rag.py`, `test_thinking_display.py`, `test_chat_persistence.py`, `test_long_answers.py`, `test_smoke.py` — all green; fix only true regressions.
4. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged; `uv run ruff check . && uv run pyright` clean.
5. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `fix(chat): stop autoscrolling while a reply streams (owner direction)`, staging this phase's files (including the deleted E2E); move `.agent/phases/todo/42_no_reply_autoscroll/` → `.agent/phases/complete/`.
5. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `fix(chat): stop autoscrolling while a reply streams (owner direction)`, staging this phase's files (including the deleted E2E); move `.agents/phases/todo/42_no_reply_autoscroll/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_no_reply_autoscroll.py -v --no-cov` green in isolation.
@@ -24,4 +24,4 @@ Prove the inverse of the phase-18 contract in the browser: no streaming autoscro
## Completion Criteria
- [ ] The new suite passes in isolation; the phase-18 suite is gone; the five regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 43 — Thinking scroll back (user scroll + generate-time autoscroll)
**Source:** `TODO.md` L7 — "Add scrolling back to the thinking block, but have it autoscroll while thinking content is generating."
**Story:** `.agent/user_stories/thinking-scroll-back.md`
**Story:** `.agents/user_stories/thinking-scroll-back.md`
**Context:** Phase 17 streams reasoning into the collapsible `.thinking` block with a per-chunk bottom-pin (`textEl.scrollTop = textEl.scrollHeight` in the `thinking` SSE handler). Phase 21 (owner choice 2026-08-24) made `.thinking-text` a no-scroll live tail: `overflow-y: hidden` (the JS pin is the sole scroller). The owner now reverses phase 21: the window is user-scrollable again, and the pin becomes **gated** — follow the tail only while the user is pinned near the window's bottom. This is the window-level successor of the phase-18 pattern (the page-level one is removed in phase 42, which runs first and touches the same `thinking` handler line — order matters).
## Objective
@@ -30,7 +30,7 @@ The Thinking block follows its live tail while reasoning is generating **and** t
- [ ] `uv run pytest tests/e2e/test_thinking_scroll.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_thinking_display.py`, `test_chat_persistence.py`, `test_no_reply_autoscroll.py`, `test_smoke.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner direction (2026-08-27, roadmap A2)** reverses the phase-21 owner choice (2026-08-24): the window is user-scrollable again; autoscroll only while pinned near the bottom (32px band). The 320px clip is kept (owner-confirmed).
@@ -1,7 +1,7 @@
# Task 01 — Window user-scrollable again (CSS + unit pin swap)
**Phase:** `43_thinking_scroll_back` · **Source:** `TODO.md:7` — "Add scrolling back to the thinking block, but have it autoscroll while thinking content is generating."
**Story:** `.agent/user_stories/thinking-scroll-back.md`
**Story:** `.agents/user_stories/thinking-scroll-back.md`
## Objective
Restore user scrolling on the Thinking window — `overflow-y: auto`, 320px clip kept, comment updated — and swap the phase-21 unit pins for the new contract so the suite stays green.
@@ -1,7 +1,7 @@
# Task 02 — Gated bottom pin (follow while pinned)
**Phase:** `43_thinking_scroll_back` · **Source:** `TODO.md:7` — "Add scrolling back to the thinking block, but have it autoscroll while thinking content is generating."
**Story:** `.agent/user_stories/thinking-scroll-back.md`
**Story:** `.agents/user_stories/thinking-scroll-back.md`
## Objective
The phase-17 per-chunk pin becomes a **gate**: the window follows the live tail only while the user is near its bottom; a scrolled-up user is never re-pinned; returning to the bottom re-arms the pin automatically.
@@ -1,7 +1,7 @@
# Task 03 — Thinking-scroll E2E (replaces phase 21) + regressions + commit
**Phase:** `43_thinking_scroll_back` · **Source:** `TODO.md:7` — "Add scrolling back to the thinking block, but have it autoscroll while thinking content is generating."
**Story:** `.agent/user_stories/thinking-scroll-back.md`
**Story:** `.agents/user_stories/thinking-scroll-back.md`
## Objective
Prove the full contract in the browser — user scroll restored, follow-while-pinned, pause-on-scroll-up, resume-on-return, CSS contract, and the phase-11/17 regressions — then commit the phase.
@@ -17,7 +17,7 @@ Prove the full contract in the browser — user scroll restored, follow-while-pi
- `test_restored_collapsed_thinking_unaffected` (phase 17) — settled thinking turn reloads collapsed with full text.
2. Regression pass (isolation runs): `test_thinking_display.py`, `test_chat_persistence.py`, `test_no_reply_autoscroll.py` (phase 42 — the cleaned `thinking` handler must not have lost the phase-42 contract), `test_smoke.py`.
3. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged; `uv run ruff check . && uv run pyright` clean.
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(chat): thinking window scrolls again, follows the tail only while pinned`, staging this phase's files **including the two deleted phase-21 test files** (`tests/unit/test_thinking_no_scroll.py`, `tests/e2e/test_thinking_no_scroll.py`) and the new unit + E2E files; move `.agent/phases/todo/43_thinking_scroll_back/` → `.agent/phases/complete/`.
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(chat): thinking window scrolls again, follows the tail only while pinned`, staging this phase's files **including the two deleted phase-21 test files** (`tests/unit/test_thinking_no_scroll.py`, `tests/e2e/test_thinking_no_scroll.py`) and the new unit + E2E files; move `.agents/phases/todo/43_thinking_scroll_back/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_thinking_scroll.py -v --no-cov` green in isolation.
@@ -25,4 +25,4 @@ Prove the full contract in the browser — user scroll restored, follow-while-pi
## Completion Criteria
- [ ] The new suite passes in isolation (all seven tests); the four regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit covering both deleted and both new test files; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit covering both deleted and both new test files; phase dir moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 44 — Markdown tables (chat, viewer, thinking)
**Source:** `TODO.md` L6 — "Certain markdown formatting isn't working - tables for example don't get rendered as tables in the chat response."
**Story:** `.agent/user_stories/markdown-tables.md`
**Story:** `.agents/user_stories/markdown-tables.md`
**Context:** `frontend/assets/markdown.js` is the shared escape-first renderer (no libs, A11): fence protection → escape → inline transforms (`code`, `**bold**`, `*em*`, h1–h3, lists) → paragraph pass → fence restore. It has **no table support** — GFM pipe tables render as one raw `|`-littered paragraph. The renderer serves the chat answer, the document viewer/modal, and the thinking block, so one change covers all three.
## Objective
@@ -31,7 +31,7 @@ GFM pipe tables render as semantic, styled, XSS-safe `<table>` elements everywhe
- [ ] `uv run pytest tests/e2e/test_markdown_tables.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_chat_rag.py`, `test_document_viewer.py`, `test_document_summaries.py`, `test_smoke.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **A11 untouched** — still the local ~90-line renderer, no library, no CDN.
@@ -1,7 +1,7 @@
# Task 01 — Table pass in the shared renderer + styles
**Phase:** `44_markdown_tables` · **Source:** `TODO.md:6` — "Certain markdown formatting isn't working - tables for example don't get rendered as tables in the chat response."
**Story:** `.agent/user_stories/markdown-tables.md`
**Story:** `.agents/user_stories/markdown-tables.md`
## Objective
`renderMarkdown` turns GFM pipe-table blocks into semantic tables (XSS-safe, inline markdown in cells), wrapped in a horizontal-overflow container, styled in the dark-tech palette.
@@ -1,7 +1,7 @@
# Task 02 — Deterministic table answer in the mock
**Phase:** `44_markdown_tables` · **Source:** `TODO.md:6` — "Certain markdown formatting isn't working - tables for example don't get rendered as tables in the chat response."
**Story:** `.agent/user_stories/markdown-tables.md`
**Story:** `.agents/user_stories/markdown-tables.md`
## Objective
The E2E mock serves a byte-stable table answer (plus a deliberately wide table and an XSS cell) on demand, following the existing trigger convention.
@@ -1,7 +1,7 @@
# Task 03 — Tables E2E + regressions + commit
**Phase:** `44_markdown_tables` · **Source:** `TODO.md:6` — "Certain markdown formatting isn't working - tables for example don't get rendered as tables in the chat response."
**Story:** `.agent/user_stories/markdown-tables.md`
**Story:** `.agents/user_stories/markdown-tables.md`
## Objective
Prove the table contract in the browser — chat, overflow, XSS, viewer, and the two "not a table" regressions — then commit the phase.
@@ -17,7 +17,7 @@ Prove the table contract in the browser — chat, overflow, XSS, viewer, and the
- Assert non-deflection (`.is-deflected` absent) in the table tests — the honesty gate interplay is part of the contract.
2. Regression pass (isolation runs): `test_chat_rag.py`, `test_document_viewer.py`, `test_document_summaries.py` (the renderer is shared — summaries render through it too), `test_smoke.py`.
3. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged; `uv run ruff check . && uv run pyright` clean.
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(chat): render markdown tables in answers, viewer, and thinking`, staging this phase's files; move `.agent/phases/todo/44_markdown_tables/` → `.agent/phases/complete/`.
4. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(chat): render markdown tables in answers, viewer, and thinking`, staging this phase's files; move `.agents/phases/todo/44_markdown_tables/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_markdown_tables.py -v --no-cov` green in isolation.
@@ -25,4 +25,4 @@ Prove the table contract in the browser — chat, overflow, XSS, viewer, and the
## Completion Criteria
- [ ] The story E2E suite passes in isolation (all six tests); the four regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 45 — Agent makes as many tool calls as it wants
**Source:** `TODO.md` L8 — "Allow the LLM to make as many tool calls as it wants, remove the restrictions, they're causing problems getting correct answers"
**Story:** `.agent/user_stories/agent-unlimited-tools.md`
**Story:** `.agents/user_stories/agent-unlimited-tools.md`
**Context:** Phase 37 shipped the grounded-turn agent loop (`app/rag/agent.py::run_agent`) with per-turn budgets — `agent_list_calls` / `agent_read_calls` (default 1 each, `BOR_AGENT_LIST_CALLS` / `BOR_AGENT_READ_CALLS`), "budgets-as-kill-switch" locked decision. The exhaustion refusals (`LIST_EXHAUSTED` / `READ_EXHAUSTED`) are where correct multi-document answers die. Owner direction (2026-08-27): remove both budgets; the loop keeps one guard — a configurable **round cap** that also doubles as the no-tools kill switch (`0`).
## Objective
@@ -28,12 +28,12 @@
- [ ] A multi-read turn (list + 2 reads) streams three tool lines, answers non-deflected, and `done.sources` lists the retrieved doc(s) + both reads deduped.
- [ ] `agent_max_rounds=0` → single `tools=None` request (kill switch); at the cap the loop forces a final no-tools answer (log warning kept).
- [ ] `tool` SSE event shape and `tool_calls=N` per-turn log field unchanged.
- [ ] `.agent/PLAN.md` carries the phase-37 revision note (owner permission 2026-08-27, `TODO.md` L8) — the only PLAN edit in this phase.
- [ ] `.agents/PLAN.md` carries the phase-37 revision note (owner permission 2026-08-27, `TODO.md` L8) — the only PLAN edit in this phase.
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%.
- [ ] `uv run pytest tests/e2e/test_agent_unlimited_tools.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_agent_document_tools.py`, `test_chat_rag.py`, `test_smoke.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner-locked revision (2026-08-27, roadmap R2):** the phase-37 "budgets-as-kill-switch" decision is **revised** — both per-tool budgets removed; `BOR_AGENT_MAX_ROUNDS` (default 10) is the only loop guard and the kill switch (`0`). Recorded as a PLAN.md revision note (the established owner-permission pattern, like the A10/A7/A9/A15 notes) — a recorded revision, not a silent deviation (AGENTS.md rule 3).
@@ -1,7 +1,7 @@
# Task 03 — Unlimited-tools E2E + PLAN revision note + commit
**Phase:** `45_agent_unlimited_tools` · **Source:** `TODO.md:8` — "Allow the LLM to make as many tool calls as it wants, remove the restrictions, they're causing problems getting correct answers"
**Story:** `.agent/user_stories/agent-unlimited-tools.md`
**Story:** `.agents/user_stories/agent-unlimited-tools.md`
## Objective
Prove the multi-tool turn end to end, record the phase-37 decision revision in PLAN.md, run the regressions, and commit the phase.
@@ -12,7 +12,7 @@ Prove the multi-tool turn end to end, record the phase-37 decision revision in P
- `test_done_sources_include_reads` — the source chips under the answer list the retrieval doc(s) **plus both** read documents, deduped (the phase-37 `done.sources` extension contract, now with 2 reads);
- `test_relist_allowed` — the listing tool ran without a "No listing budget left" refusal: assert no refusal text anywhere in the bubble/tool lines (the old refusal strings must be gone — grep the app for them is task 01's job; here assert the UI never shows one);
- `test_single_tool_flow_regression` (phase 37) — the original 3-step flow (marker without the multi-read trigger) still answers after exactly one read with its single tool pair (this may be a targeted re-assertion; the full suite `test_agent_document_tools.py` runs in the regression pass).
2. `.agent/PLAN.md` — **the only PLAN edit in this phase** (owner-locked revision, roadmap R2): in the §4 SSE-revision block, after the phase-37 revision note, add a new note in the established style:
2. `.agents/PLAN.md` — **the only PLAN edit in this phase** (owner-locked revision, roadmap R2): in the §4 SSE-revision block, after the phase-37 revision note, add a new note in the established style:
> **SSE revision (phase 45, owner permission 2026-08-27):** the phase-37
> per-turn tool budgets are **removed** (owner: "allow the LLM to make
> as many tool calls as it wants — `TODO.md` L8): `BOR_AGENT_LIST_CALLS`
@@ -27,7 +27,7 @@ Prove the multi-tool turn end to end, record the phase-37 decision revision in P
Touch **nothing else** in PLAN.md (Protocol B: no roadmap-table edit for appended phases).
3. Regression pass (isolation runs): `test_agent_document_tools.py` (phase 37 — must pass **unmodified**), `test_chat_rag.py`, `test_smoke.py`.
4. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
5. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(rag): unbounded agent tool calls behind a round cap (owner revision)`, staging this phase's files **including the force-added `.agent/PLAN.md`** (AGENTS.md rule 8: `git add -f .agent/PLAN.md`) and the phase dir move `.agent/phases/todo/45_agent_unlimited_tools/` → `.agent/phases/complete/`.
5. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(rag): unbounded agent tool calls behind a round cap (owner revision)`, staging this phase's files **including the force-added `.agents/PLAN.md`** (AGENTS.md rule 8: `git add -f .agents/PLAN.md`) and the phase dir move `.agents/phases/todo/45_agent_unlimited_tools/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_agent_unlimited_tools.py -v --no-cov` green in isolation.
@@ -36,4 +36,4 @@ Prove the multi-tool turn end to end, record the phase-37 decision revision in P
## Completion Criteria
- [ ] The multi-read E2E suite passes in isolation; the phase-37 suite passes unmodified in isolation.
- [ ] PLAN.md carries the phase-45 revision note (owner permission 2026-08-27) and nothing else changed.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -1,7 +1,7 @@
# Phase 47 — Import quadlet + jinja files
**Source:** `TODO.md` L10–L11 — "Add '.container', '.network', '.volume' and other quadlet files to the list of allowed/parsed files" / "Add '.j2' jinja files to the list of allowed/parsed files"
**Story:** `.agent/user_stories/quadlet-jinja-import.md`
**Story:** `.agents/user_stories/quadlet-jinja-import.md`
**Context:** A9 (LOCKED, revised 2026-08-21): default import formats `md, markdown, txt, yaml, yml, json, py`; `app/config.py::_ALLOWED_IMPORT_EXTENSIONS` bounds `BOR_IMPORT_EXTENSIONS` (narrow-only); `app/rag/chunker.py::_FORMAT_CHUNKERS` maps suffix → chunker (unknown suffix → `chunk_text` fallback). The owner-locked revision (2026-08-27, roadmap R1): ten new formats join the allowed **and** default set — the full Podman quadlet family (`container, network, volume, image, pod, kube, swap, os, endpoint`) plus `j2` — chunked as plain text.
## Objective
@@ -34,7 +34,7 @@ Quadlet unit files and Jinja templates are indexed like any other A9 format: all
- [ ] `uv run pytest tests/e2e/test_quadlet_jinja_import.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_import_documents.py`, `test_sync_button.py`, `test_git_sources_admin.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner-locked revision (2026-08-27, roadmap R1):** A9's format set is **revised** — ten formats added to the allowed + default set (the full quadlet family + `j2`); plain-text chunking (no TOML/Jinja-aware splitters); recorded as a PLAN.md A9 revision note with owner permission — a recorded revision, not a silent deviation (AGENTS.md rule 3).
@@ -1,7 +1,7 @@
# Task 04 — Quadlet/jinja E2E + docs (README, PLAN A9 revision) + commit
**Phase:** `47_quadlet_jinja_import` · **Source:** `TODO.md:10–11` — "Add '.container', '.network', '.volume' and other quadlet files to the list of allowed/parsed files" / "Add '.j2' jinja files to the list of allowed/parsed files"
**Story:** `.agent/user_stories/quadlet-jinja-import.md`
**Story:** `.agents/user_stories/quadlet-jinja-import.md`
## Objective
Prove the story end to end (import → catalog → Sources table → viewer → FTS retrieval), record the A9 revision in the docs, run the regressions, and commit the phase.
@@ -15,10 +15,10 @@ Prove the story end to end (import → catalog → Sources table → viewer →
- module fixture: truncate `documents`/`chunks`/`query_log` per test module (the house E2E pattern) and re-import — note: this file's re-import changes the KB for the session; it is run in **isolation** (A16), so no cross-suite interference.
2. **Docs:**
- `README.md` — wherever the import format list is documented (the "Import & update" section mirrors PLAN §11 / A9), extend it with the ten new formats (the 2026-08-27 A9 revision, plain-text chunking);
- `.agent/PLAN.md` — **the only PLAN edit in this phase** (owner-locked revision R1): in the §2 anchors table, the A9 row's decision text gains the extension — append to the A9 row (keep the original wording, mark the revision in the row's notes/status or in a revision note under the table, the established style): "**A9 revision (phase 47, owner permission 2026-08-27):** the format set extends with the Podman quadlet family (`container, network, volume, image, pod, kube, swap, os, endpoint`) and `j2` (Jinja templates) — plain-text chunking (`chunk_text`), owner: `TODO.md` L10–L11. The narrow-only `BOR_IMPORT_EXTENSIONS` rule and the hidden-dir/exclusion invariants are unchanged." Update PLAN §5's chunking-policy format line and §11's workflow line to list the extended set (same note style). Touch **nothing else** in PLAN.md.
- `.agents/PLAN.md` — **the only PLAN edit in this phase** (owner-locked revision R1): in the §2 anchors table, the A9 row's decision text gains the extension — append to the A9 row (keep the original wording, mark the revision in the row's notes/status or in a revision note under the table, the established style): "**A9 revision (phase 47, owner permission 2026-08-27):** the format set extends with the Podman quadlet family (`container, network, volume, image, pod, kube, swap, os, endpoint`) and `j2` (Jinja templates) — plain-text chunking (`chunk_text`), owner: `TODO.md` L10–L11. The narrow-only `BOR_IMPORT_EXTENSIONS` rule and the hidden-dir/exclusion invariants are unchanged." Update PLAN §5's chunking-policy format line and §11's workflow line to list the extended set (same note style). Touch **nothing else** in PLAN.md.
3. Regression pass (isolation runs): `test_import_documents.py` (task 02's count-constant update must hold — the tree now has four more files), `test_sync_button.py`, `test_git_sources_admin.py`.
4. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
5. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(import): index quadlet unit files and jinja templates (A9 revision)`, staging this phase's files **including the force-added `.agent/PLAN.md`** (AGENTS.md rule 8) and the phase dir move `.agent/phases/todo/47_quadlet_jinja_import/` → `.agent/phases/complete/`.
5. Commit (Conventional Commits, `--no-gpg-sign`), e.g. `feat(import): index quadlet unit files and jinja templates (A9 revision)`, staging this phase's files **including the force-added `.agents/PLAN.md`** (AGENTS.md rule 8) and the phase dir move `.agents/phases/todo/47_quadlet_jinja_import/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_quadlet_jinja_import.py -v --no-cov` green in isolation.
@@ -27,4 +27,4 @@ Prove the story end to end (import → catalog → Sources table → viewer →
## Completion Criteria
- [ ] The story E2E suite passes in isolation (all four tests); the three regression suites pass in isolation.
- [ ] README + `.env.example` (task 01) + PLAN.md (A9 row + §5 + §11) record the extended set; no other PLAN change.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -2,7 +2,7 @@
**Source:** `TODO.md` L3 — "Need a way to stop or cancel generation of text in the chat"
**Story:** n/a (TODO-derived — owner roadmap confirmation 2026-08-29)
**Context:** A chat turn is `POST /api/chat` → an SSE stream (`thinking` → `tool` → `delta` → `done`/`error`) produced by `app/api/chat.py`'s `stream()` over `app.rag.agent.run_agent` (grounded turns) or `LLMClient.chat_stream` (deflected turns — `app/rag/llm.py`, an `AsyncOpenAI` streaming request). The frontend consumes it in `frontend/assets/app.js` (`handleSend` → `fetch` → `readSSE`) with the never-stale state machine (`setUiState`: idle → thinking → streaming → done|error → idle, PLAN §7.4) and the phase-14 localStorage conversation (`bor.chat.v1`; optional per-record fields like `thinking`/`tools`/`stopped` are the no-version-bump convention).
**Context:** A chat turn is `POST /api/chat` → an SSE stream (`thinking` → `tool` → `delta` → `done`/`error`) produced by `app/api/chat.py`'s `stream()` over `app.rag.agents.run_agent` (grounded turns) or `LLMClient.chat_stream` (deflected turns — `app/rag/llm.py`, an `AsyncOpenAI` streaming request). The frontend consumes it in `frontend/assets/app.js` (`handleSend` → `fetch` → `readSSE`) with the never-stale state machine (`setUiState`: idle → thinking → streaming → done|error → idle, PLAN §7.4) and the phase-14 localStorage conversation (`bor.chat.v1`; optional per-record fields like `thinking`/`tools`/`stopped` are the no-version-bump convention).
## Objective
The user can stop an in-flight answer at any time: while a turn is live the Send button becomes a **Stop** button; stopping keeps the partial answer on screen and in the persisted conversation (marked as stopped), settles the UI to idle with no error banner, and makes the server tear down the model's HTTP stream promptly so the local model stops generating.
@@ -32,7 +32,7 @@ The user can stop an in-flight answer at any time: while a turn is live the Send
- [ ] `uv run pytest tests/e2e/test_stop_generation.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_chat_persistence.py`, `test_loading_feedback.py`, `test_chat_rag.py` (the loading-feedback suite's in-flight button assertions are revised in place to the new contract — see task 02).
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **A10 untouched** — `/api/chat` stays stateless; stopping is a client disconnect, no new endpoint, no server session state.
@@ -42,5 +42,5 @@ The user can stop an in-flight answer at any time: while a turn is live the Send
## Commit
```bash
git add -A .agent/ app/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): stop an in-flight answer — Send becomes Stop, the partial is kept and persisted, the model stream is torn down"
git add -A .agents/ app/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): stop an in-flight answer — Send becomes Stop, the partial is kept and persisted, the model stream is torn down"
```
@@ -14,7 +14,7 @@ Prove the stop contract in the browser (mid-stream stop, pre-token stop, persist
- Determinism notes: the mock streams 12 chars / 0.02 s, so the long answer takes ~8 s — a comfortable stop window; wait for observable states, no fixed sleeps beyond those.
2. Regression pass (isolation runs): `test_chat_persistence.py` (save/restore + New chat — the button and state machine changed), `test_loading_feedback.py` (the revised contract — 120s guard + live regions + typing dots), `test_chat_rag.py` (a normal turn still streams to `done` with sources + Tune).
3. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
4. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agent/phases/todo/48_stop_generation/` → `.agent/phases/complete/`.
4. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agents/phases/todo/48_stop_generation/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_stop_generation.py -v --no-cov` green in isolation.
@@ -22,4 +22,4 @@ Prove the stop contract in the browser (mid-stream stop, pre-token stop, persist
## Completion Criteria
- [ ] All three story tests pass in isolation; the three regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -28,7 +28,7 @@ A **Retry** button on the last brain answer re-asks the preceding question in pl
- [ ] `uv run pytest tests/e2e/test_retry_answer.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_chat_rag.py`, `test_chat_persistence.py`, `test_stop_generation.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner-locked (2026-08-29, roadmap confirmation):** redo-in-place (the old answer is replaced, the question is not duplicated); only the **last** brain bubble is retryable; Retry is available to **all** visitors (chat is public — unlike Tune, which is admin-only); inert while a turn is in flight.
@@ -37,5 +37,5 @@ A **Retry** button on the last brain answer re-asks the preceding question in pl
## Commit
```bash
git add -A .agent/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): retry the last answer — redo-in-place Retry button on the latest brain bubble"
git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): retry the last answer — redo-in-place Retry button on the latest brain bubble"
```
@@ -14,7 +14,7 @@ Prove the redo contract in the browser, run the regressions, and commit the phas
- `test_retry_inert_while_in_flight` — seed one completed turn (so a last bubble exists), then ask a "pretend to think slowly …" question; while in flight (button "Stop"), click the Retry button on the previous last bubble; assert nothing happens: the in-flight turn still completes to its own `done`, and after settle there is exactly one user record for the in-flight question (no second turn started, no bubble duplication).
2. Regression pass (isolation runs): `test_chat_rag.py`, `test_chat_persistence.py` (the record shape is asserted there), `test_stop_generation.py` (phase 48 — the stop finalize path now also calls `markLastRetryable`).
3. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged; `uv run ruff check . && uv run pyright` clean.
4. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agent/phases/todo/49_retry_answer/` → `.agent/phases/complete/`.
4. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agents/phases/todo/49_retry_answer/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_retry_answer.py -v --no-cov` green in isolation.
@@ -22,4 +22,4 @@ Prove the redo contract in the browser, run the regressions, and commit the phas
## Completion Criteria
- [ ] All four story tests pass in isolation; the three regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -35,7 +35,7 @@ The owner can Save the current conversation, see every saved chat on a new **His
- [ ] `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_chat_persistence.py`, `test_nav_consistency.py`, `test_shared_header.py`, `test_admin_auth.py`, `test_cache_busting.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner-locked extension (2026-08-29, recorded per AGENTS.md rule 3):** `saved_chats` in Postgres stores **only** conversations the owner explicitly saves; `/api/chat` stays stateless; phase 14's local persistence is unchanged (the localStorage session keeps working exactly as before — saving is an additional, explicit action).
@@ -44,5 +44,5 @@ The owner can Save the current conversation, see every saved chat on a new **His
## Commit
```bash
git add -A .agent/ app/ alembic/versions/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): save and view chat history — admin-only saved_chats, History page, open-a-chat return"
git add -A .agents/ app/ alembic/versions/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): save and view chat history — admin-only saved_chats, History page, open-a-chat return"
```
@@ -17,7 +17,7 @@ Prove the save → list → open → return → delete loop in the browser (admi
2. `tests/e2e/test_cache_busting.py` — add `/history.html` to the pages that suite walks (the no-cache + `?v=` contract applies to the new page — the minimal diff to its page list).
3. Regression pass (isolation runs): `test_chat_persistence.py` (the boot path gained the `?chat=` branch), `test_nav_consistency.py` + `test_shared_header.py` (a seventh nav link — extend their link enumeration if they assert the exact nav set), `test_admin_auth.py` (the whoami gate unchanged), `test_cache_busting.py` (after the page-list addition).
4. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
5. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agent/phases/todo/50_chat_history/` → `.agent/phases/complete/`.
5. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agents/phases/todo/50_chat_history/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` green in isolation.
@@ -25,4 +25,4 @@ Prove the save → list → open → return → delete loop in the browser (admi
## Completion Criteria
- [ ] All five story tests pass in isolation; the regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -29,7 +29,7 @@ The owner can turn a saved chat into a public link (`/shared/<token>`); anyone w
- [ ] `uv run pytest tests/e2e/test_share_chat.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_chat_history.py`, `test_chat_persistence.py`, `test_smoke.py`, `test_cache_busting.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner-locked (2026-08-29, roadmap confirmation):** share links are public by design (128-bit `uuid4` token; unshare revokes); the shared page renders the full conversation read-only (thinking collapsed) with **zero** interactive controls; source chips are plain text (guests cannot open documents — the documents API is admin-only); clipboard copy with an inline-link fallback (a homelab http origin may not be a secure context).
@@ -38,5 +38,5 @@ The owner can turn a saved chat into a public link (`/shared/<token>`); anyone w
## Commit
```bash
git add -A .agent/ app/ alembic/versions/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): share a chat by link — anonymous read-only /shared/<token> page, share/unshare"
git add -A .agents/ app/ alembic/versions/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): share a chat by link — anonymous read-only /shared/<token> page, share/unshare"
```
@@ -16,7 +16,7 @@ Prove the share → anonymous view → revoke loop in the browser, run the regre
2. `tests/e2e/test_cache_busting.py` — add the shared page to the walk: create + share a chat via the admin API for the test, then assert `GET /shared/<token>` carries `Cache-Control: no-cache` and the served HTML's asset refs are `?v=`-tagged (the task-01 prefix extension) — the minimal addition to the suite's page coverage.
3. Regression pass (isolation runs): `test_chat_history.py` (the schemas + the History table changed), `test_chat_persistence.py` (the chat page's boot + buttons), `test_smoke.py`, `test_cache_busting.py` (after the shared-page addition).
4. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
5. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agent/phases/todo/51_share_chat/` → `.agent/phases/complete/`.
5. Commit (Conventional Commits, `--no-gpg-sign`) — the message from the phase overview's Commit section — staging this phase's files; move `.agents/phases/todo/51_share_chat/` → `.agents/phases/complete/`.
## Testing & Quality
- E2E: `uv run pytest tests/e2e/test_share_chat.py -v --no-cov` green in isolation.
@@ -24,4 +24,4 @@ Prove the share → anonymous view → revoke loop in the browser, run the regre
## Completion Criteria
- [ ] All four story tests pass in isolation; the four regression suites pass in isolation.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -38,7 +38,7 @@ Every sync that actually changes the knowledge base bumps a sources version; sav
- [ ] `uv run pytest tests/e2e/test_stale_saved_chats.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_chat_history.py`, `test_share_chat.py`, `test_sync_button.py`, `test_retry_answer.py`, `test_chat_persistence.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Recorded assumptions (TODO conversion, 2026-08-30 — owner asked for no confirmation):**
@@ -52,5 +52,5 @@ Every sync that actually changes the knowledge base bumps a sources version; sav
## Commit
```bash
git add -A .agent/ app/ alembic/versions/ scripts/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): invalidate saved chats on sources sync — versioned stamps, stale marker, Regenerate against the new index"
git add -A .agents/ app/ alembic/versions/ scripts/ frontend/ tests/ && git commit --no-gpg-sign -m "feat(chat): invalidate saved chats on sources sync — versioned stamps, stale marker, Regenerate against the new index"
```
@@ -2,7 +2,7 @@
**Source:** owner instruction 2026-08-30 (chat) — found live while verifying phase 52: *"add that as phase 54"*. Not a TODO.md item (TODO.md is empty).
**Story:** n/a (bug fix discovered by measurement, not a user story)
**Context:** Phase 33 (`app/core/caching.py`, `.agent/reports/33_cache_busting__*`) ships two layers: `asset_version()` — a per-process token, `git rev-parse --short HEAD` in a git checkout (`functools.cache`, so it flips only on process start) — and `CachingMiddleware` (mounted in `app/main.py::create_app` via `configure_caching`, wrapping the `StaticFiles(directory=static_dir, html=True)` catch-all mounted at `/`). For every known page (`HTML_PAGES` + the dynamic `/shared/<token>` prefix) the middleware drains the body, rewrites each local `assets/…` `href`/`src` to carry `?v=<token>` (`rewrite_asset_refs`), and re-serves it with `Cache-Control: no-cache`. `/assets/*` gets `public, max-age=31536000, immutable` header-only; `/api/*` (incl. the SSE chat stream) passes through byte-identical. `_no_cache_headers()` copies **every** upstream header and overrides only `content-length` (dropped) and `Cache-Control` — so the static file's `etag` and `last-modified` travel with the rewritten body.
**Context:** Phase 33 (`app/core/caching.py`, `.agents/reports/33_cache_busting__*`) ships two layers: `asset_version()` — a per-process token, `git rev-parse --short HEAD` in a git checkout (`functools.cache`, so it flips only on process start) — and `CachingMiddleware` (mounted in `app/main.py::create_app` via `configure_caching`, wrapping the `StaticFiles(directory=static_dir, html=True)` catch-all mounted at `/`). For every known page (`HTML_PAGES` + the dynamic `/shared/<token>` prefix) the middleware drains the body, rewrites each local `assets/…` `href`/`src` to carry `?v=<token>` (`rewrite_asset_refs`), and re-serves it with `Cache-Control: no-cache`. `/assets/*` gets `public, max-age=31536000, immutable` header-only; `/api/*` (incl. the SSE chat stream) passes through byte-identical. `_no_cache_headers()` copies **every** upstream header and overrides only `content-length` (dropped) and `Cache-Control` — so the static file's `etag` and `last-modified` travel with the rewritten body.
**The defect (measured on the running app, 2026-08-30):** the conditional-request validators describe the *static file*, but the *served bytes* are the rewritten body — a body this process built from its own token. A revalidation therefore 304s out of the rewrite and the browser keeps the HTML it already has, whose `?v=` points at the **previous** commit's CSS/JS, which is cached `immutable` for a year:
@@ -48,7 +48,7 @@ HTML pages are revalidated against the bytes that are actually served, never aga
- [ ] Regression E2E suites green in isolation: `test_cache_busting.py`, `test_smoke.py`, `test_shared_header.py`, `test_share_chat.py`, `test_chat_history.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] `README.md` documents the rule (why pages never 304, why assets still may) and `git rev-parse --short HEAD` + hard-reload behaviour is recorded.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **A2/A11 honoured** — the fix stays inside the existing Starlette middleware; no new dependency, no CDN, no new service (A12 unchanged: still no Valkey/CDN to hold asset versions).
@@ -64,5 +64,5 @@ HTML pages are revalidated against the bytes that are actually served, never aga
## Commit
```bash
git add -A .agent/ app/ README.md tests/ && git commit --no-gpg-sign -m "fix(web): never 304 a rewritten page — pages drop conditional validators, assets keep them"
git add -A .agents/ app/ README.md tests/ && git commit --no-gpg-sign -m "fix(web): never 304 a rewritten page — pages drop conditional validators, assets keep them"
```
@@ -14,17 +14,17 @@ One isolated Playwright story suite proving, through a real browser, that the do
- **`test_assets_still_immutable_with_validators`** — `with page.expect_response("**/assets/styles.css**") as info: page.goto(app_url)`; `resp.status == 200`; `resp.headers["cache-control"] == "public, max-age=31536000, immutable"`; `resp.headers` **has** `etag` (the asset keeps its validators — only pages dropped them).
2. `README.md` — extend the deployment/caching documentation with a short **Cache busting** subsection (create it if the README has no caching section; place it near the deployment docs): the `?v=<git short SHA>` token flips on the next process start; `/assets/*` is cached `immutable` for a year under the versioned URL (a conditional `GET` may 304 — the URL already encodes the version); HTML pages are served `no-cache` and **never 304** — because the page body is rewritten per process, upstream validators would describe the file, not the bytes served (phase 54); during local development a `git` commit changes the token on server restart — if a browser still shows an old layout, hard-reload once (the fix guarantees the NEXT navigation is current, it cannot un-pin what a pre-54 deploy already 304'd).
3. Regressions, each in isolation (`uv run pytest tests/e2e/<file> -v --no-cov`, DB up): `test_cache_busting.py` (the phase-33 story suite — the contract it pins must survive intact), `test_smoke.py`, `test_shared_header.py`, `test_share_chat.py` (the dynamic `/shared/<token>` page), `test_chat_history.py` (`/history.html`).
4. One `--no-gpg-sign` commit staging `.agent/ app/ README.md tests/` (message per the phase overview); move `.agent/phases/todo/54_asset_cache_bust_revalidation/` to `.agent/phases/complete/`.
4. One `--no-gpg-sign` commit staging `.agents/ app/ README.md tests/` (message per the phase overview); move `.agents/phases/todo/54_asset_cache_bust_revalidation/` to `.agents/phases/complete/`.
- No `app/` change in this task; if one proves necessary, stop and flag it — the fix must already be complete from task 01.
## Testing & Quality
- E2E (mandatory, A16): `uv run pytest tests/e2e/test_asset_cache_revalidation.py -v --no-cov` green in isolation (DB up).
- The five regression suites above green in isolation.
- Final gate before the move: `bash .agent/validate.sh` (unit + integration, coverage >90%, ruff, pyright).
- Final gate before the move: `bash .agents/validate.sh` (unit + integration, coverage >90%, ruff, pyright).
## Completion Criteria
- [ ] `uv run pytest tests/e2e/test_asset_cache_revalidation.py -v --no-cov` green in isolation (DB up).
- [ ] `test_cache_busting.py`, `test_smoke.py`, `test_shared_header.py`, `test_share_chat.py`, `test_chat_history.py` green in isolation.
- [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] README documents the no-304-pages / immutable-assets rule and the restart/hard-reload behavior.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -36,7 +36,7 @@ Every conversation on the chat page saves itself (no Save button), **any** visit
- [ ] `uv run pytest tests/e2e/test_save_share_ux.py -v --no-cov` green in isolation (DB up).
- [ ] Regression E2E suites green in isolation: `test_share_chat.py`, `test_chat_history.py`, `test_chat_persistence.py`, `test_stale_saved_chats.py`, `test_smoke.py`.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Owner-locked (2026-08-31, roadmap confirmation):**
@@ -50,5 +50,5 @@ Every conversation on the chat page saves itself (no Save button), **any** visit
## Commit
```bash
git add -A app/ frontend/ tests/ && git add -f .agent/phases/todo/55_save_share_ux .agent/phases/complete && git commit --no-gpg-sign -m "feat(chat): save by default + share anonymously — auto-saved chats, guest-facing Share, success toast, action row"
git add -A app/ frontend/ tests/ && git add -f .agents/phases/todo/55_save_share_ux .agents/phases/complete && git commit --no-gpg-sign -m "feat(chat): save by default + share anonymously — auto-saved chats, guest-facing Share, success toast, action row"
```
@@ -14,11 +14,11 @@ One isolated Playwright story suite for the whole phase: auto-save without a but
- **Layout (L6):** desktop viewport (1280×800): `#new-chat-btn` and `#share-chat-btn` bounding boxes on one row (overlapping `y` bands; Share's `x` > New chat's `x` + width; each at intrinsic width, not the full 46rem column). Mobile viewport (390×844, `page.set_viewport_size`): stacked (Share's `y` > New chat's `y` + height). At 360px wide: no horizontal overflow (`document.documentElement.scrollWidth` ≤ 360).
- **Admin still works (A1 sanity):** admin login → `/` → ask a question → the auto-saved row appears for the admin too (same machinery, session or not).
2. Regressions, each in isolation (`uv run pytest tests/e2e/<file> -v --no-cov`, DB up): `test_share_chat.py` (Save-click pins already adapted in task 02), `test_chat_history.py` (auto-save pins adapted in task 02 + the anonymous Share-pill pin from task 03), `test_chat_persistence.py`, `test_stale_saved_chats.py` (phase 53 — the stale Regenerate's auto re-save must survive task 02's helper rename; if it still references the removed Save pill or `saveCurrentChat` by name, adapt those pins to the auto-save contract, edits limited to the rename/removal), `test_smoke.py`.
3. One `--no-gpg-sign` commit staging `app/ frontend/ tests/` + the phase dir force-added (`.agent/` is gitignored by design — AGENTS.md rule 8: `git add -f .agent/…`):
3. One `--no-gpg-sign` commit staging `app/ frontend/ tests/` + the phase dir force-added (`.agents/` is gitignored by design — AGENTS.md rule 8: `git add -f .agents/…`):
```bash
git add -A app/ frontend/ tests/ && git add -f .agent/phases/complete/55_save_share_ux && git commit --no-gpg-sign -m "feat(chat): save by default + share anonymously — auto-saved chats, guest-facing Share, success toast, action row"
git add -A app/ frontend/ tests/ && git add -f .agents/phases/complete/55_save_share_ux && git commit --no-gpg-sign -m "feat(chat): save by default + share anonymously — auto-saved chats, guest-facing Share, success toast, action row"
```
then move `.agent/phases/todo/55_save_share_ux/` → `.agent/phases/complete/55_save_share_ux/` (stage the move with `git add -f` on the new path before committing, so the tracked phase files land under `complete/`).
then move `.agents/phases/todo/55_save_share_ux/` → `.agents/phases/complete/55_save_share_ux/` (stage the move with `git add -f` on the new path before committing, so the tracked phase files land under `complete/`).
## Testing & Quality
- E2E (mandatory, A16): `tests/e2e/test_save_share_ux.py` green in isolation.
@@ -30,4 +30,4 @@ One isolated Playwright story suite for the whole phase: auto-save without a but
- [ ] `test_share_chat.py`, `test_chat_history.py`, `test_chat_persistence.py`, `test_stale_saved_chats.py`, `test_smoke.py` green in isolation.
- [ ] `uv run pytest` green; coverage TOTAL >90%.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/`.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
@@ -28,7 +28,7 @@
- [ ] `uv run pytest` green; coverage TOTAL >90%.
- [ ] `uv run pytest tests/e2e/test_import_extensions_env.py -v --no-cov` green in isolation (DB up).
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agent/phases/complete/` (`.agent/` stays untracked — owner instruction, commit 281f355).
- [ ] One `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/` (`.agents/` stays untracked — owner instruction, commit 281f355).
## Locked decisions
- **Owner-locked (2026-08-31, roadmap confirmation, D1):** `BOR_IMPORT_EXTENSIONS` may **extend** beyond the A9 family — the hard-coded set becomes the default, not the ceiling. This revises the A9 "narrow-only" clause (2026-08-21/27) by owner permission; the A9 list remains the built-in default and the documented example.

Some files were not shown because too many files have changed in this diff Show More