diff --git a/.agents/PLAN.md b/.agents/PLAN.md index ec45c4a..18da1fd 100644 --- a/.agents/PLAN.md +++ b/.agents/PLAN.md @@ -1,825 +1,372 @@ -# Brain of Reese — Master Plan +# Brain of Reese — Master Plan (minimal) -> **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). +> **Status:** Minimal re-land (2026-09-10). Phases 01–92 shipped; +> `todo/` holds 93–95 (see §12). Section numbers and anchor IDs match the +> previous full plan (in git history, commit `dac4a3e`) so the codebase's +> `PLAN §…` / anchor comments stay valid — consult that revision or the +> phase records in `.agents/phases/complete/` for full detail and the +> complete owner-permission revision log. > -> **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. +> **Rule:** Every agent reads this file first. `LOCKED` anchors in §2 are +> settled — a change requires explicit owner permission, recorded as a +> dated revision note under the table. Never a silent deviation +> (AGENTS.md rule 3). > -> **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/`. +> **This is deliberately minimal:** it captures the tech stack, the +> architecture an agent needs to work, and the locked decisions. It does +> NOT recap the shipped work — the git log, the README, and the completed +> phase directories are the history. --- ## 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. +A **knowledge-base chatbot** ("Brain of Reese") over the owner's homelab +documentation — git repos, local directories, and uploaded archives +registered on the admin Sources page — embedded into Postgres + pgvector. +Product feel: chippy, upbeat, and **radically honest** — if retrieval +surfaces nothing relevant it says *"I haven't done anything like that"* +and offers alternatives instead of hallucinating. Self-hosted LLMs only. -**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. +**Deliberately out of scope:** binary/non-text ingestion; real-time file +watching (the import script / Sync button is the refresh loop); PR +tooling for docs push (the owner opens the PR); multi-admin/per-user +accounts (one admin + hand-out tokens is the model). --- -## 2. Architectural Anchors (LOCKED DECISIONS) +## 2. Tech Stack & Architectural Anchors (LOCKED) -| # | 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 `