Files
brain-of-reese/.agents/PLAN.md
T
ducoterra b34b8ca672
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 182_verbatim_show_text
**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**

- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
2026-10-15 08:33:12 -04:00

25 KiB
Raw Blame History

Brain of Reese — Master Plan (minimal)

Status: Minimal re-land (2026-09-10). See §12 for current state. 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.

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).

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 ("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.

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. Tech Stack & Architectural Anchors (LOCKED)

# Component Decision
A1 Runtime Python 3.12+, uv for all package management (API + scripts + tests, one venv)
A2 Web framework FastAPI + Pydantic v2 + Uvicorn (async, SSE-friendly)
A3 Database PostgreSQL 17 + pgvector (local build of official postgres:17, db/Containerfile), cosine (<=>) search; one system for relational + vectors
A4 Orchestration compose.yaml, podman compose up -d (dev: db only; --profile prod adds the app container)
A5 LLM backend OpenAI-compatible self-hosted endpoint https://aipi.reeseapps.com/v1 via the openai async client: turbo (chat, streams reasoning_content thinking), embed (embeddings), lite (one-shot: document summaries, KB overview, folder summaries)
A6 Embedding dim 768 (verified against the live endpoint); chunks.embedding is fixed at table creation — a dim mismatch must fail loudly, never silently re-embed
A7 Retrieval→context Hybrid: cosine top-100 ∪ Postgres FTS top-30 (OR tsquery, ts_rank), fused with RRF (k=60) → parent docs ranked by best fused chunk → full text of top-N=2 documents, never truncated on the retrieval path (owner 2026-08-24: "this should never happen"; the agent read-tool cap is a separate owner-permitted path)
A8 Honesty gate Deflect (LOW mode) only when best cosine < BOR_RELEVANCE_THRESHOLD (0.62) and (zero FTS hits or best cosine < BOR_LEXICAL_SUPPORT_FLOOR (0.35)); an FTS hit flips HIGH only when best_cosine >= lexical_support_floor — the vector signal must corroborate the lexical match (A8 revised 2026-09-14, owner-confirmed, TODO L2a: lexical-only hits without vector support deflect). LOW prompt carries weak-hit titles only + the DEFLECT_MODE marker (the E2E mock keys on its presence) + the plain-text no-tools line
A9 Content scope Default md, markdown, txt, yaml, yml, json, py + Podman quadlet family + j2; BOR_IMPORT_EXTENSIONS may name any well-formed extension or narrow the set; hidden (dot) paths + exclusion list + per-source ignore_paths (raw prefixes, no globs) always apply
A10 State & auth POST /api/chat is stateless (client-provided history only, budget-trimmed — nothing stored per conversation). Auth = single admin, signed bor_session cookie (Starlette SessionMiddleware + itsdangerous; no server-side session store); admin-issued SHA-256-hashed access tokens are the only other identity; only shared chats stay anonymous. Fail-loud at boot while BOR_ADMIN_PASSWORD/BOR_SESSION_SECRET are empty
A11 Frontend Vanilla HTML/CSS/JS in git, no CDN — everything served by FastAPI StaticFiles; system font stack; the navbar views are views of ONE shell document (frontend/index.html + router.js deep-links)
A12 Aux services None (no Valkey/queue/SeaweedFS): sync runs in-process, the login rate limit is in-memory per-process, sessions are the signed cookie. Restart clearing in-memory state is accepted
A13 Migrations Alembic + SQLAlchemy 2.0 (sync) + psycopg 3; every migration ships a tested downgrade (A13 — reversible)
A14 Debugging debugpy imported only when DEBUGPY=1 (app/core/debugging.py); never imported otherwise (unit-tested)
A15 Chat transport SSE from POST /api/chat; event types thinking, delta, tool, retry, done, error, tool_result; no proxy buffering, no client caching on the stream
A16 Testing Per phase: unit + integration (pytest) with >90% coverage on app/ + one dedicated Playwright E2E file, run in isolation (--no-cov); E2E uses a deterministic mock LLM by default (E2E_REAL_LLM=1 opts into live aipi)
A17 Git Conventional Commits, always --no-gpg-sign, one atomic commit per completed phase
A18 Docs push Save-a-answer-as-doc: server-side draft (doc_drafts, long body never in a URL — unguessable uuid4 token is the URL credential) pushed to a generic git remote (BOR_DOCS_REPO, URL or local path; no gh) on a dedicated branch, --ff-only, re-cut per push; no PR tooling; inert (hidden + 409) while the repo is unset
A19 Deploys & caching HTML pages Cache-Control: no-cache (no etag/last-modified); /assets/* versioned (?v=<token>) + immutable, max-age=1y; the token is the deploy (git HEAD short SHA; content-hash fallback) — a deploy is a commit the browser sees without a hard refresh
A20 Security headers Every response: Content-Security-Policy: default-src 'self' (+ the theme's inline-<style> hash on themed pages), frame-ancestors 'none', X-Content-Type-Options: nosniff; login rate-limited 10 attempts / 15 min (in-memory, A12)
A21 LLM call economics ALL LLM calls are free (chat, vision, embed, lite — self-hosted aipi): the sync/import path may burn as many LLM/vision calls as quality requires — e.g. render + describe every sparse page of a pictorial PDF, no page caps, no call budgets (owner 2026-09-30). Latency targets bind the interactive path only (retrieval, inference, tool calling — the inference engine's job, not ours to micro-optimize): never cap, throttle, or skip work to save calls; a slower sync is acceptable, a lower-quality KB is not

UI/theming anchors (B):

# Decision
B1 Theme ui_settings columns: NULL/empty = "use the default" — env value for the 3 strings, built-in palette for colors (colors have no env fallback)
B3 The semantic state families (--ok-*, --err-*, --accent-*) join the storable palette — 17 variables total, all NULL = built-in (the built-in theme stays byte-identical). The 2026-09-09 lock that they are "not identity" is lifted
B4 Byte-identical contract: with no ui_settings row (or a fully default theme) the served HTML is byte-identical to the built-in default — no #bor-theme tag, caching rewrites are rewrite-only
B5 Admin-only nav views (Sources, Git sources, Tuning, History, Tokens, Theme) are hidden from non-admins; a monochrome theme must keep every state text label ("text + color, never color alone")

Key recent revisions (full log in the previous plan revision / phase records): A21 — ALL LLM calls (chat, vision, embed, lite) are free for this project (owner 2026-09-30): import/sync may burn unlimited LLM/vision calls for quality (e.g. every page of a 500+ page pictorial PDF, rendered + described); sync duration is not an optimization target, interactive-path speed is the inference engine's job. A10 — single-admin cookie auth, saved chats + shares + staleness, client history budgets, access tokens. A15 — thinking, tool, unlimited tool rounds under BOR_AGENT_MAX_ROUNDS, retry, harness-aligned ls/read/grep surface. A7 — the retrieval path's never-truncated contract is unchanged; the read-tool cap is the only exception, owner-permitted 2026-09-10.

Phase 182 (verbatim text display, owner chat 2026-10-14) adds show_text(path, offset?, limit?) as the fifth agent tool — offered only when BOR_ANSWER_TEXT=1 (the default), gated exactly like show_image, with its own <tools> clause and its own kill switch — and the additive done.answer_text field above. The new settings are BOR_ANSWER_TEXT (master switch, ON), BOR_SHOW_TEXT_MAX_CHARS (per-block budget, 12 000) and BOR_SHOW_TEXT_MAX_BLOCKS (per-answer budget, 5), all following the agent_max_rounds pattern (0 inert, negative refuses startup). The principle in one sentence: the model points, the server supplies the bytes — a ```bor source/path 12-40 fence or a show_text call names a location (canonical locator source/path or source/path#start-end, 1-based and inclusive), and the displayed text is sliced from documents.content server-side, so what the user copies is the knowledge base, never a re-typed approximation. A1–A6 and A15 are otherwise UNCHANGED: no new SSE event type, no new table or column, no new model, endpoint or transport, no change to retrieval, the honesty gate, the persona text or the embedding dimension.


3. High-Level Architecture

Browser (SPA shell, no CDN)
  │  HTTP pages · SSE chat · JSON API
  ▼
FastAPI app (app/main.py)
  ├── middleware: SessionMiddleware → caching (A19) → SecurityHeaders (A20)
  ├── /api/*  chat, docs, git-sources, steering, sync, chats,
  │           doc-drafts, tokens, ui-settings, auth, health, suggestions
  ├── RAG pipeline (app/rag/)  embed → retrieve → gate → agent → turbo
  └── StaticFiles (frontend/)  — shell routes registered before the mount
  │            SQL (psycopg)                     OpenAI-compat
  ▼            ▼                                 ▼
Postgres 17 + pgvector              aipi.reeseapps.com/v1
(db service, podman compose)        (self-hosted: turbo / embed / lite)

Sources registry (Postgres `git_sources`, admin-managed; DB rows win,
BOR_GIT_SOURCES is the empty-table git-only fallback):
  git repos → clone/pull via scripts/git_sync.py (the ONLY git-invocation
              site; stdlib subprocess)   local dirs → walked directly
  archives  → unpacked under BOR_UPLOAD_DIR (zip-bomb guarded)
Component Lives in
App + middlewares + auth/tokens/rate-limit/theming/caching app/main.py, app/core/
RAG (retriever, prompts, agent, llm, chunker, importer, summarizer, overview, scaffolding, suggestions, …) app/rag/
API routers app/api/
Import / sync tooling scripts/import_docs.py, app/api/sync.py, scripts/git_sync.py, scripts/eval_retrieval.py, scripts/llm_probe.py
Frontend (shell + standalone pages) frontend/ (index.html shell; document.html, shared.html, login.html, doc-edit.html)
Migrations (all reversible) alembic/versions/

4. Chat Turn & SSE Contract (§3/§4 in older comments)

POST /api/chat {message, history?}   (auth: require_user)
  → embed(question)                                   [retried, A15 ext.]
  → cosine top-100 ∪ FTS top-30 → RRF fuse (k=60)     [A7]
  ├─ HIGH (cosine ≥ 0.62 OR fts_hits > 0):
  │    persona + <knowledge_base> + <tuning> + full top-2 <documents>
  │    + <tools> → agent loop (ls/read/grep, plus show_image when
  │    BOR_ANSWER_IMAGES=1 and show_text when BOR_ANSWER_TEXT=1 — the two
  │    "show the user an artifact" tools, gated independently; round-capped
  │    BOR_AGENT_MAX_ROUNDS=10, 0 = tools off) → turbo streamed
  └─ LOW (A8): DEFLECT_MODE prompt (weak titles only, no tools,
       byte-identical direct-stream path) → turbo streamed
  → query_log row + per-turn log line (§9)

SSE frames (data: <json>\n\n): thinking (before first delta) → tool (grounded turns, one per model call) → retry (pre-first-frame restarts) → delta (answer tokens) → done {deflected, sources[], suggestions[], budget_exhausted?, answer_text[]?} (terminal). Phase 182 extends the done frame ADDITIVELY — answer_text is the list of server-resolved verbatim blocks (locator, source, path, start, end, total_lines, text, format, truncated) the answer displayed; there is no eighth event type, the key is OMITTED when no block was displayed, and with BOR_ANSWER_TEXT=0 it is absent on every turn (the frame is then byte-identical to pre-phase). The same array persists on the saved chat record, so restore and the anonymous shared page re-render the panels from the server's bytes. Failure: error (terminal — no done, no query_log row; a pre-stream DB outage is a plain 503 JSON). A tools=None request never emits a tool frame; a round-capped turn whose final reply carries no visible content answers with the budget fallback instead of erroring (phase 160, recorded revision of the phase-71 cap-path terminal, owner-confirmed 2026-10-05). BOR_STREAM_THINKING=0 suppresses thinking frames server-side (chars still counted). LLM retries (A15 extension): retry count and delay are configurable (defaults: 3 retries, 5 s flat delay), only before a request has streamed its first output frame.

Retrieval details (A7/A8) and the persona/<tools> prompt contract: see the previous plan revision §6 or app/rag/retriever.py / app/rag/prompts.py / app/rag/agent.py — the module docstrings carry the full contracts. Persona text changes through the plan, not in code (phase 03 convention); the DEFLECT_MODE marker and the mock-LLM markers (SUMMARY_MODE, KB_OVERVIEW_MODE, …) may not change without updating tests/e2e/mock_llm.py.


5. Data Model & Chunking (§5 in older comments)

Tables (full column detail: app/models.py — it is the living doc; each migration has a tested downgrade):

Table Purpose
documents one row per imported file — full content, (source, path) unique, sha256 content_hash, lite summary (non-markdown)
chunks retrieval units; embedding VECTOR(768); position −1 = the embedded summary chunk; stored tsvector (GIN) for FTS
query_log every question: top score, FTS hits, deflection, sources, latency (threshold-tuning record)
steering_notes owner tuning notes → <tuning> section of every turn (char-budgeted)
kb_overview single row id=1: lite-generated KB outline → <knowledge_base> section (regenerated on KB change)
git_sources source registry: kind git|local, url/path, ignore_paths JSONB
saved_chats owner-saved conversations; messages = the raw bor.chat.v1 JSONB; share_token (NULL = private, uuid4 → /shared/<token>); sources_version (stale when < current)
sources_meta single row id=1: the KB generation counter — bumped once per KB-changing sync
doc_drafts save-as-doc drafts; token (uuid4) is the URL credential; draft → pushed (branch + sha)
api_tokens access tokens; only the SHA-256 of the full bor_… string is stored; revoked_at = dead
ui_settings single row id=1: Theme tab persistence (3 strings + 17 color variables; NULL = default, B1)

Single-row tables use id = 1 (the kb_overview precedent).

Chunking (format-aware, app/rag/chunker.py): markdown on ##/### headings (paragraph sub-split > BOR_CHUNK_TARGET_CHARS=2000, 200 overlap); YAML/JSON on top-level keys; Python on top-level defs via ast; txt on paragraphs; quadlet/j2 plain text. Every format honors the 1200-char hard cap (aipi ~1024-token request limit).

Import workflow (script and UI Sync share the importer): sha256 delta (unchanged files skip), two-phase upsert (one transaction per file), --prune drops deleted/ignored files, non-markdown files get lite summaries (best-effort/fail-soft), and a KB-changing run regenerates the kb_overview + bumps sources_meta.version exactly once. A failed source aborts the run — no partial junk.


6. Retrieval & Persona

(See §4's flow and the anchor rows A7/A8. The locked persona, section order — <relevance> → <knowledge_base> → <tuning> → mode body — and the <tools> teaching live in app/rag/prompts.py; the agent loop, teaching refusals, and scaffolding filter in app/rag/agent.py / app/rag/scaffolding.py. Empty prompt sections omit themselves — a no-notes/no-overview prompt is byte-identical to the pre-steering text.)

Tool surface (harness-aligned): ls (lists indexed docs source: X | path: Y | title: Z; path = a source name — drill-down tree); read (combined source/path including the source name; appends the full document — with a truncation cap); grep (case- insensitive fixed substring, ≤20 source/path:line: text matches, optional one-doc scope; a locator that adds no source/context — never a regex). Rejected calls get deterministic teaching refusals and consume a round; holder.tool_calls counts executed calls only.


7. UI/UX Standards

  • Layout (§7.1): sticky 64px header + <main> + footer; container max-width: 72rem centered; chat is a centered 46rem column (2× = 92rem at ≥1500px desktops — deliberate, not a bug); Sources uses a full-width responsive table — no skinny single-column lists (lists/tables/grids ≥80–90% of container width); document viewer is a near-fullscreen same-page modal; mobile ≤640px: hamburger nav, ≥44px touch targets, safe-area composer.
  • Accessibility (§7.2, WCAG 2.1 AA): semantic landmarks on every view, labeled controls (icon buttons get aria-label), aria-live="polite" stream, role="status"/role="alert", 3px :focus-visible, prefers-reduced-motion respected, text pairs ≥4.5:1 (state is text + color, never color alone).
  • Theming (B1–B5): built-in dark palette in app/core/theming.py::BUILTIN_COLORS (the :root-drift unit test parses built-ins from styles.css — no second copy); the admin Theme tab persists the palette in ui_settings, injected pre-paint as <style id="bor-theme"> with a matching CSP hash (A20); B4 byte-identical when nothing is set.
  • No CDN (§7.3): zero external <script>/<link> (integration test on the index page); markdown rendering is a small local escape-first function; esbuild minify is build-time only.
  • Never-stale feedback (§7.4): every control state — idle / thinking / calling tool / streaming / retrying / done (answer or deflected) / error / KB-offline / stopped — has a defined UI, every failure path re-enables its controls, a 300 s pre-token guard (TURN_TIMEOUT_MS) turns a hung stream into the error state (counts only visible time), and no auto-follow during a turn (viewport moves only on user intent). Pinned by unit tests on the app.js state machine + the story E2E suites.
  • Frontend house rules: app.js and siblings never build HTML strings (createElement + textContent); asset paths carry ?v=<token> (A19).

8. Debugging

DEBUGPY unset/0 → debugpy never imported (unit-tested, A14). DEBUGPY=1 → non-blocking listener on 0.0.0.0:${DEBUGPY_PORT:-5678}; IDE attaches on demand. Wired at app/main.py module import (app/core/debugging.py), so uvicorn app.main:app, python -m scripts.…, and tests all honor it.


9. Observability

  • Logs: single-line timestamp LEVEL logger :: message on stdout, INFO default (BOR_LOG_LEVEL), third-party loggers capped at WARNING.
  • Per-chat-turn line (required — AGENTS.md rule 10):
    question=… embed_ms=… top_score=… fts_hits=… summary_hits=… tuning=N
    kb_chars=N history_msgs=N threshold=… deflected=… sources=…
    thinking_chars=… tool_calls=N rounds=N cap_hit=0|1 total_ms=…
    retries=N scaffold_stripped=N verbatim_blocks=N
    
    (verbatim_blocks= — phase 182 — is the count of verbatim display blocks actually SENT on the done frame, 0 on every turn without them, deflected included.) (sources= = retrieval + agent-read docs, deduped; tool_calls= executed only; a cancelled turn writes neither line nor query_log row.)
  • Importer: per-file added|updated|unchanged|pruned + a greppable cron-safe summary line (import: summary files=… added=… … formats=…).
  • query_log is the durable tuning/gap-finding record.

10. Testing & Quality Gates (A16 — non-negotiable)

Gate Command
Unit + integration uv run pytest
Coverage >90% on app/ uv run pytest --cov=app --cov-report=term-missing
Story E2E, in isolation uv run pytest tests/e2e/test_<story>.py -v --no-cov (DB up: podman compose up -d db)
Lint + types uv run ruff check . && uv run pyright

E2E determinism: tests/e2e/mock_llm.py is a deterministic OpenAI-compatible mock (genuine L2-normalized token-overlap embeddings, so the cosine gate behaves like production); tests/e2e/slow_llm.py is the slow/dead variant; E2E_REAL_LLM=1 opts into live aipi. Story E2E fixtures truncate/re-import per module — suites must run in isolation. Real-model tool-calling verification uses the controlled methodology in TOOL_CALLING_TESTING.md + scripts/agent_realmodel_check.py (fixture KB: tests/fixtures/test_kb.dump.sql; the 1,000-doc live-replica snapshot restores via the restore-test-db skill).


11. Import & Update Workflow

uv run python -m scripts.import_docs                # sync sources + index delta
uv run python -m scripts.import_docs --prune        # also drop deleted/ignored
uv run python -m scripts.import_docs --source ~/X   # extra directories
# or one click: the admin Sources page "Sync sources" (in-process, 409 while running)
uv run python -m scripts.eval_retrieval "<question>"   # rank hybrid results (tuning)
uv run python -m scripts.llm_probe                     # models + embedding dim sanity

The loop for git sources is commit → re-run. Uploads: POST /api/git-sources/upload unpacks + registers (202); the scan is deferred to Sync. Source removal prunes on the next sync.


12. Current State & Roadmap

  • Shipped phases: ls .agents/phases/complete/ — read-only history; each phase's 00_phase.md has the full detail. The shipped- features recap of the previous plan revision and the README cover it.
  • Migrations: ls alembic/versions/ — all reversible.
  • Next up (todo/): Already authored via phase-authoring. Read the directory listing in .agents/phases/todo/ for the ordered task list; each file's 00_phase.md has the full description.
  • Next free phase number: max(completed phase numbers) + 1 — computed from .agents/phases/complete/.
  • Post-v1 hooks (deliberately not built): HNSW index at scale; inotify auto-import; more providers (the OpenAI-compatible client is the seam); multi-user accounts (the require_user split is the seam); index-backed grep.

Per-phase completion (AGENTS.md rules 8/9): unit + integration green, coverage >90%, the phase's Playwright E2E green in isolation, UI verified against §7, one atomic --no-gpg-sign Conventional Commit, phase dir moved to complete/.


13. House Conventions (quick catch-up)

  • Env: every setting is a BOR_-prefixed env var (or gitignored .env; see .env.example); get_settings() is lru_cached. Kill switches follow the agent_max_rounds pattern: 0 disables the feature, a negative value fails startup loudly (validator names the field).
  • Fail loud, never half-configured: missing admin secrets, bad extension lists, dim mismatches, unresolvable docs branches — all refuse to start or refuse the request with a named reason.
  • Shared marker: TRUNCATION_MARKER = "[…truncated…]" (app/rag/retriever.py) is the only overflow marker; char budgets are the pattern for anything sent to lite.
  • Byte-identical contracts are load-bearing: deflected turns (A8), empty prompt sections, the B4 theme no-op, and the rewrite-only caching all have tests that assert byte-identity.
  • git is invoked only in scripts/git_sync.py (A11 — stdlib subprocess); everything else talks Postgres.
  • One story → one phase → one dedicated Playwright file (AGENTS.md rules 4/9); .agents/ is tracked; only .agents/phase-sessions/ and .agents/pipeline.log are gitignored.
  • Full-history references: the previous full plan (all revision notes, complete API table, column-level data model) is in git — git show dac4a3e:.agents/PLAN.md; per-phase decisions are in .agents/phases/complete/*/00_phase.md; owner test methodologies in TOOL_CALLING_TESTING.md; skills for model testing/KB restore in .agents/skills/.