diff --git a/.agents/PLAN.md b/.agents/PLAN.md new file mode 100644 index 0000000..ec45c4a --- /dev/null +++ b/.agents/PLAN.md @@ -0,0 +1,825 @@ +# Brain of Reese — Master Plan + +> **Status:** v1 complete — phases 01–92 shipped (see §12); +> `.agents/phases/todo/` is empty as of this rebuild (2026-09-10). +> **Rule:** Every agent reads this file first. Decisions marked `LOCKED` in +> the §2 Anchors table are settled — do not re-litigate them in a phase. +> A change to a LOCKED anchor requires explicit owner permission and is +> recorded as a dated revision note under the table — never a silent +> deviation (AGENTS.md rule 3). +> +> **Provenance:** This file restores the master plan that lived at +> `.agent/PLAN.md` until commit 281f355 (removed from VCS by the owner, +> 2026-08-31). It is re-landed at `.agents/PLAN.md` — the path `AGENTS.md` +> and the codebase reference throughout (`PLAN §…` comments) — rebuilt from +> the original text plus every owner-permitted revision recorded in the +> completed phases through 92. +> +> **Revisions (2026-08-21, owner permission):** A7/A8/A9 revised (multi-format +> ingestion, hybrid FTS+vector retrieval, re-tuned honesty gate); dark tech +> theme (Phase 08); clickable document viewer (Phase 10); thinking display +> (Phase 17, owner permission 2026-08-23); follow-the-bottom scroll +> (Phase 18, owner choice 2026-08-23); shared header (Phase 19); +> whole-document context (Phase 24 — A7's 24k context cap removed, +> owner permission 2026-08-24). +> +> **Revisions (2026-08-25 → 2026-09-09, owner permission):** `lite` summary +> model (Phase 30); agent document tools `ls`/`read`/`grep` +> (Phases 37/45/68/70/71/72); single-admin auth + saved chats + shares +> (Phases 16/50/51/53/79); any-extension import (Phase 56); docs push +> (Phase 59); SPA shell (Phase 76); LLM retries (Phase 67); login rate +> limit (Phase 81); security headers (Phase 82); per-source ignore paths +> (Phase 89); admin Theme tab (Phases 91/92). See the revision log under +> §2 and the phase records in `.agents/phases/complete/`. + +--- + +## 1. Mission + +A **knowledge base chatbot** that embeds Reese's homelab documentation — +git repos, local directories, and uploaded archives registered on the admin +Sources page — into a Postgres vector database and lets anyone ask *Reese* +(the bot) questions about them. + +**Product feel:** a chippy, upbeat assistant that is optimistic about the +user's ability and **radically honest** — if retrieval didn't surface +anything relevant it says *"I haven't done anything like that"* and offers +alternatives instead of hallucinating. + +### Delivered (v1, phases 01–92) +- Chat UI — mobile-friendly, WCAG 2.1 AA, no CDN — as a single-page shell + with deep-linkable views: Chat `/`, Sources `/sources.html`, Git sources + `/git-sources.html`, Tuning `/tuning.html`, History `/history.html`, + Tokens `/tokens.html`, Theme `/theme.html`, plus Login, Shared chat, and + Document viewer/edit pages. +- RAG over text knowledge files — the A9 family by default + (`md, markdown, txt, yaml, yml, json, py` + the Podman quadlet family + + `j2`; `BOR_IMPORT_EXTENSIONS` may name **any** well-formed extension, + A9 revised) — from admin-registered **git sources**, **local directory + sources**, and **uploaded archives**. +- Hybrid retrieval (pgvector cosine ∪ Postgres FTS, RRF-fused), an honesty + gate with honest deflection, whole-document context (never truncated), + `lite`-model summaries of non-markdown docs, a `lite`-generated KB + overview, and grounded-turn agent tools (`ls`/`read`/`grep`). +- Self-hosted models via `https://aipi.reeseapps.com/v1` — `turbo` (chat, + with streamed thinking), `embed` (embeddings, **768 dims — verified**), + `lite` (one-shot: document summaries, KB overview). +- Single-admin signed-cookie auth + admin-issued access tokens; saved + chats with anonymous share links and KB-generation staleness; steering + notes; save-answer-as-doc git push; per-source ignore paths; admin Theme + tab; deploy-safe asset caching; security headers; login rate limit. +- Idempotent delta import (`uv run python -m scripts.import_docs`), the + one-click UI **Sync sources** button, and ample server logging + + explicit UI feedback (never a stale submit button). + +### Still out of scope (deliberately) +- Binary / non-text content ingestion. +- Real-time document watching — the import script / Sync button is the + refresh loop (commit → re-run). +- PR tooling for docs push — the owner opens the PR themselves. +- Multi-admin / per-user accounts — one admin + hand-out tokens is the + access model. + +--- + +## 2. Architectural Anchors (LOCKED DECISIONS) + +| # | Component | Decision | Rationale | Status | +|---|-----------|----------|-----------|--------| +| A1 | Runtime | Python 3.12+, `uv` for all package management | Fast, reproducible envs; one language for API + tooling | LOCKED | +| A2 | Web framework | FastAPI + Pydantic v2 + Uvicorn | Async, typed, SSE-friendly for LLM streaming, free OpenAPI docs | LOCKED | +| A3 | Database | **PostgreSQL 17** (`docker.io/postgres:17`, pgvector compiled in via `db/Containerfile`) with **cosine** (`<=>`) search | One system for relational + vectors; pgvector is mature; official base image kept per project standard | LOCKED | +| A4 | Orchestration | `compose.yaml`, started with **`podman compose up -d`** | Matches Reese's toolchain | LOCKED | +| A5 | LLM backend | OpenAI-compatible `https://aipi.reeseapps.com/v1`; models **`turbo`** (chat), **`embed`** (embeddings), **`lite`** (one-shot — A5 extended, phase 30); `openai` async client | Self-hosted, offline from cloud; no new model management | LOCKED (extended phase 30) | +| A6 | Embedding dim | **768** (verified 2026-08-21 against live endpoint via `scripts/llm_probe.py`); configured by `BOR_EMBEDDING_DIM` | User recalled 768 — probe confirmed; dimension is fixed at table creation, so mismatch must fail loudly at import time | LOCKED | +| A7 | Retrieval→context | **Hybrid:** cosine top-100 + Postgres FTS top-30 (OR tsquery, `ts_rank`) fused with **RRF (k=60)** → map to parent documents ranked by best fused chunk score → feed the **full text of top-N=2 documents** (deduped) to the LLM — **never truncated** (A7 revised 2026-08-24; vector window widened 30→100 on 2026-08-22; summary chunks are first-class retrieval targets, phase 30) | Owner permission 2026-08-21: pure-cosine top-4 missed real docs (gitlab case — best chunk ranked 7th behind vendored-cache junk; score compression 0.41–0.84); the lexical signal finds name-your-tool questions; the 100-wide vector window lets double-hit chunks outrank generic templates (measured on the live 2774-chunk KB, 2026-08-22); whole-document context contract preserved — no `[…truncated…]` cap (owner: "this should never happen"; emergency-valve variant rejected) | LOCKED (revised 2026-08-24) | +| A8 | Honesty gate | **Deflection mode** (LLM must NOT pretend to know; offers 2–3 alternative questions about things it DOES have notes on) when best cosine < `BOR_RELEVANCE_THRESHOLD` **and** no candidate chunk FTS-matches the question; threshold re-tuned for the `embed` model's compressed score range (default **0.62**, calibrated via `scripts/eval_retrieval.py`; the E2E mock uses its own 0.30 calibration via the app fixture). Deflected prompt: weak-hit *titles only* (never document content) + the `DEFLECT_MODE` marker + the phase-71 plain-text line "Reply in plain text only — you have no tools in this mode." | Owner permission 2026-08-21: at 0.30 the gate never discriminated (measured corpus range 0.41–0.84); the FTS-OR keeps name-your-tool questions honest-positive; deflection product behavior unchanged. Phase 71 (owner-permitted 2026-09-03): the one added line closes the tool-markup door at the prompt — the `DEFLECT_MODE` marker contract is unchanged | LOCKED (revised 2026-08-21) | +| A9 | Content scope | Text formats **`md, markdown, txt, yaml, yml, json, py`** + the Podman quadlet family (`container, network, volume, image, pod, kube, swap, os, endpoint`) + `j2` (default, `BOR_IMPORT_EXTENSIONS` — may name **any** well-formed extension or narrow to a subset, A9 revised 2026-08-31), **hidden (dot) directories/files skipped**, plus the exclusion list (`node_modules`, `__pycache__`, `.pytest_cache`, `dist`, `build`, …), plus **per-source ignore paths** (phase 89: raw source-relative prefixes, no globs, `git_sources.ignore_paths`) | Owner permission 2026-08-21: real notes live in yaml/py/json/txt too; the dot-dir skip removes the ~470 vendored-cache junk docs (`.esphome/.espressif/**`, …) that outranked real content. Phase 47 (2026-08-27): quadlet + j2 join the default — plain-text chunking. Phase 56 (2026-08-31): any well-formed extension allowed (the A9 family is a default, not a ceiling); a blank list or malformed token fails startup loudly, naming the value | LOCKED (revised 2026-08-31) | +| A10 | Auth | **None in v1**; all endpoints stateless under `/api` | Per user (auth later); statelessness keeps the future migration cheap | LOCKED (revised phase 16 — see revision log) | +| A11 | Frontend | Vanilla HTML/CSS/JS in git; **no CDN** — everything served by FastAPI `StaticFiles`; minified by esbuild in the `Containerfile` build stage; system font stack | No external deps at runtime; tiny, auditable surface; mobile-friendly by construction | LOCKED (extended phase 76 — see revision log) | +| A12 | Aux services | **None** (no Valkey, no SeaweedFS, no queue) — everything in-process: session state is a signed cookie (no store), the Sync button runs the import in-process, the login rate limit is in-memory per-process | No server-side session state; a homelab single-box deploy; add later only if a need appears | LOCKED | +| A13 | Migrations | Alembic + SQLAlchemy 2.0 (sync) + psycopg 3; **every migration ships a tested downgrade** | Standard, reversible, reviewable schema history | LOCKED | +| A14 | Debugging | `debugpy` **only when `DEBUGPY=1`** (env var read directly, not via settings); listen `0.0.0.0:5678` (override `DEBUGPY_PORT`), non-blocking, attach-on-demand; **not imported at all when off** | Zero overhead by default per project standard; attach-on-demand keeps production runs clean | LOCKED | +| A15 | Chat transport | **SSE streaming** from `POST /api/chat` — event types `thinking`, `delta`, `tool`, `retry`, `done`, `error` (§4) | Local LLM latency is 10–30s; live token stream + explicit completion event power the UI's feedback states | LOCKED (extended phases 17/37/45/67 — see revision log) | +| A16 | Testing | Per phase: unit + integration (pytest, **coverage >90%** on `app/`) + **one dedicated Playwright E2E file per user story**, run in isolation; E2E uses a deterministic mock LLM by default (`E2E_REAL_LLM=1` opts into live aipi) | One story, one phase, one E2E gate — the pipeline's core invariant | LOCKED | +| A17 | Git | Conventional Commits, **always `--no-gpg-sign`**, repo-local `commit.gpgsign=false`; one atomic commit per completed phase | Subsequent agents may lack the GPG key | LOCKED | +| A18 | Docs push | Save a chat answer as documentation: server-side draft (`doc_drafts`, the long body never lives in a URL — an unguessable `uuid4` token is the edit-screen URL credential), pushed to a generic git remote (`BOR_DOCS_REPO` — URL or local path; no `gh`, no GitHub assumption) on a dedicated branch (`git push --ff-only`, re-cut from `BOR_DOCS_BASE_BRANCH` per push); **no PR tooling** — the owner opens the PR; the feature is inert (hidden + 409) while `BOR_DOCS_REPO` is empty | Phase 59, owner permission 2026-08-30 (decisions D1–D3): answers should flow back into the docs with a review step, without coupling the app to one forge; the empty-repo kill switch mirrors the git-sources env-fallback pattern | LOCKED (added phase 59) | +| A19 | Deploys & caching | One Starlette middleware: HTML pages ship `Cache-Control: no-cache` (no `etag`/`last-modified`); assets are versioned (`/assets/*?v=`) and ship `Cache-Control: public, max-age=31536000, immutable`; **the token is the deploy** — the short SHA of git `HEAD` (content hash of the `frontend/` tree without a `.git`) | A deploy is a commit — the browser must see it without a hard refresh; no CDN, no new services, no build-step change (phases 33 + 54) | LOCKED (added phase 33, revised phase 54) | +| A20 | Security headers | Every response carries `Content-Security-Policy: default-src 'self'` (the themed HTML pages additionally allow the inline `