Groundwork for step 7, and a data-integrity fix in its own right. Two kinds keep a queryable mirror in `notes.data` derived from their body: snippets and, since milestone 385, lessons. Every read prefers the mirror — deliberately, because parsing markdown to answer what an index can answer is how a hot path rots. So a write that moves the body must move the mirror. #3128 found that hole for snippets and plugged it with a hard-coded `if note.note_type == SNIPPET_NOTE_TYPE`. The plug was correct and did not generalise: lessons arrived with the same design and none of the protection, which is precisely the "don't add a fourth instance" defect #3734 was told to avoid. The cost is higher for a lesson. A stale snippet mirror reports the wrong path. A stale lesson mirror reports the wrong TRIGGER, and the trigger is the whole retrieval story — the lesson goes on firing for the situation it used to name while displaying the one it now names. Silent, and confident. So `update_note` now dispatches through `_mirror_recomposers()`, a note_type -> recomposer table. A kind with a derived mirror is covered by registering it, not by someone remembering to widen an if. `lessons.recompose_data` is the lesson's entry. It recovers the subject with `embeddings.untrigger_title` — new, and deliberately placed beside the join it inverts rather than in the caller that wanted it, because a separator spelled in two files is a separator that will one day be changed in one of them (#3207). `TRIGGER_SEP` is now the one spelling, and `parse_snippet_fields` uses it too; it had the third copy inline. The two inverses stay distinct on purpose: a snippet partitions at the first separator (its name is a symbol), a lesson strips an exact known suffix (its subject may legitimately contain a dash). Different algorithms, one constant, so they cannot disagree about where the seam is. Provenance is DROPPED when the body drops it, which is the opposite call from a snippet's `verification` — that is carried because it was never in the body to delete. The body is the authority; carrying a value the reader just removed is the failure the recompose exists to prevent. Tests: test_snippet_mirror_generic_door.py becomes test_derived_mirror_generic_door.py, since the concern is now plural. The registry property is asserted directly (every kind with a mirror is in the table; the dispatch names no kind inline), plus the lesson cases and the join/inverse round-trip. `fake_lesson` moves to tests/helpers.py — it existed in test_lesson_surfacing.py and a second copy was about to be written — and gains the explicit `None`s `fake_snippet` carries, because update_note reads `verify_with` and a MagicMock is truthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
Fabled Scribe
A self-hosted work system-of-record for software projects, built to be driven by Claude Code. Notes, tasks, issues, projects, milestones, rules, and stored processes — reachable from Claude via a built-in MCP endpoint and a bundled Claude Code plugin, with a clean web UI for humans. No in-app LLM; Claude is the sole assistant.
Features
Notes and tasks with a Markdown editor, sub-tasks, milestones, issues, and kanban project workspaces. Stored processes, an engineering rulebook system (with an inception step that decides what each project inherits), and semantic search with proactive knowledge-injection into Claude's context. A knowledge graph, per-user/group sharing, and a built-in MCP server (/mcp) plus a bundled Claude Code plugin so Claude can record and recall your work directly.
Quick Start
Prerequisites: Docker and Docker Compose. No GPU or local model needed — Claude is the sole assistant, reached over MCP.
Download docker-compose.quickstart.yml from this repo, then:
# Optional but recommended — set a secret key
export SECRET_KEY=your-random-secret-here
docker compose -f docker-compose.quickstart.yml up -d
Open http://localhost:5000. The first user to register becomes admin. To connect Claude, create an API key under Settings → API Keys and install the Claude Code plugin — see API Keys & MCP.
Development: To build from source, see Development.
Documentation
| Doc | Contents |
|---|---|
| Architecture | Stack, design decisions, data models, key services |
| Configuration | Environment variables, Docker Compose, production setup, security |
| Features | Detailed feature breakdown and keyboard shortcuts |
| Development | Dev workflow, CI/CD, migrations, release process |
| API Keys & MCP | API key management and Fable MCP install guide |
| SSO / OAuth | OIDC setup for Authentik, Keycloak, and other providers |
| API Reference | All REST API endpoints |
License
This project is privately maintained.