Build a book from opencode's documentation, using local models, with every factual claim machine-checked against the source.
It is a build system, not a chatbot. Chapters are declarative specs, outputs are fingerprinted, and only stale chapters rebuild — so when upstream docs change, or you tune a prompt, you regenerate exactly what's affected.
python3 -m venv .venv && ./.venv/bin/pip install -e .
./.venv/bin/opencode-book sync # fetch + pin the docs corpus
./.venv/bin/opencode-book status # what needs building
./.venv/bin/opencode-book build 05-agents-and-subagents
./.venv/bin/opencode-book assemble # concatenate into build/manuscript.mdThen publish/ turns those chapters into an EPUB and PDF — see
publish/README.md. That stage is a work in progress.
Working on this project? Read AGENTS.md — the rules there each trace to something that went wrong once.
Editions are cut as GitHub Releases — see documentation/05-releases.md and CHANGELOG.md.
Full write-up in documentation/: the build process, the POC replay walkthrough, known gaps, and next steps.
You need an OpenAI-compatible server running (LM Studio, mlx_lm.server,
Ollama, llama.cpp). Point models.yaml at it. To exercise the pipeline with no
model at all:
python tests/mock_server.py & # returns real quotes + one fabrication
./.venv/bin/opencode-book build 07-skills-and-commands --forceextract claims + verbatim quotes fast model, batched per source
validate literal substring check NO MODEL — pure code
draft prose from validated claims only primary model
verify independent support audit a DIFFERENT model
voice style pass against exemplars voice model
The validate stage is the point of the whole thing. The extractor must
return a verbatim quote for every claim; opencode-book then asserts that quote appears
literally in the cited source file. It's a string comparison, so it cannot
itself hallucinate. Published citation-hallucination rates for LLMs run roughly
11–57%; a substring assertion collapses the fabricated-citation class to about
zero, in microseconds.
Rejections are categorised, because they're different bugs:
rejected [bogus]: quote not found verbatim in skills.mdx#understand-discovery (fabricated or paraphrased)
rejected [c3]: quote not found in agents.mdx#subagents (it appears in agents.mdx#primary-agents -- miscited)
The drafter never sees raw documentation — only validated claims. That's what keeps it from "remembering" a flag that doesn't exist.
Each chapter is a directory with a chapter.yaml:
id: agents
title: Agents and Subagents
target_words: 2600
objectives:
- Distinguish primary agents from subagents in plain language
sources:
- agents.mdx#types
- agents.mdx#primary-agents
- permissions.mdx#agentsSources are doc#anchor refs, not free text. Many docs can feed one chapter;
one doc can feed many chapters. List valid anchors with:
opencode-book anchors --doc agents.mdxA bad ref fails loudly at build time with the list of valid anchors, rather than silently producing a thin chapter.
A chapter's fingerprint hashes four inputs:
chapter spec + prompt templates + source excerpts + model identity
| You change | What rebuilds |
|---|---|
upstream docs (opencode-book sync) |
only chapters citing changed sections |
a file in prompts/ |
every chapter |
a model in models.yaml |
every chapter, and outputs are diffable |
one chapter.yaml |
that chapter |
models.yaml maps roles to models, so swapping is a config edit. Defaults are
tuned for a 64GB M5 Pro (307 GB/s, hard 64GB ceiling):
Current default: one model for all four roles, differing only by
temperature. The differentiated setup (commented in models.yaml) is better on
quality but its three models total ~69 GB against a 64 GB ceiling — they can't
all stay resident, so each role change evicts and reloads from disk. Prove the
pipeline on one model, then differentiate.
| Role | Temp | Why |
|---|---|---|
extract |
0.1 | near-deterministic; the stage depends on copying quotes exactly |
draft |
0.4 | the writer |
verify |
0.0 | auditor — ideally a different model; sharing one is a POC compromise |
voice |
0.7 | room to vary rhythm; first role worth pointing at a hosted API |
Builds are stage-major: every chapter through extract, then every chapter
through draft, and so on. With per-role models that's one model load per stage
instead of one per stage per chapter — the difference between 4 loads and 32
on an 8-chapter run. Use --chapter-major to run one chapter fully through the
pipeline at a time when debugging.
Prefer MoE models on Apple Silicon. Dense models are bandwidth-bound — every parameter is read per token. MoE activates ~3B per token and sidesteps that wall. On Pro-tier chips the effect is large enough that an M4 Pro can beat an M3 Ultra on an 80B MoE despite a third the bandwidth.
Run the bake-off before committing to a model:
opencode-book bakeoff 05-agents-and-subagents --models qwen3-30b-a3b,glm-4.7-flash,gpt-oss-20bThen read all three and pick the voice you'd actually publish. Local models are
strong at extraction and verification and noticeably weaker at warm, book-length
prose — if the drafts read flat, point the voice role at a hosted API (see the
commented block in models.yaml) and leave everything else local.
voice/exemplars.md controls tone more than any prompt does. Replace the
placeholders with 3–5 paragraphs you wrote. Models match demonstrated
rhythm far better than described rhythm — "be conversational" yields the
generic assistant register; your own paragraphs yield your voice.
book.yaml also carries banned_phrases. Add to it every time you catch a tic.
book.yaml sets citation_style:
chapter(default) — one deduplicated source list per chapter, listing each doc, the sections drawn on, and the pinned commit. Prose stays clean.inline— per-claim[^footnote]markers throughout.
This is presentation only. Claim-level verification runs either way, and the
full audit trail (accepted claims, rejections with reasons, verify verdicts)
always lands in build/<chapter>.claims.json.
Right now the corpus is opencode's 36 canonical English docs — about 49,500 words, ~70k tokens. That fits in a single context window, which is why there's no retrieval layer.
Adding third-party articles and general background material does not break this, because of how the stages divide:
extractis the only stage that reads raw sources, and it runs per batch (max_source_wordsinbook.yaml, default 12,000). Batches are independent, so total corpus size is irrelevant — only a single chapter's declared sources matter, and even those get split.draftnever sees raw sources at all. It consumes validated claims, which stay compact no matter how large the corpus grows.
So the scaling order is:
- Cite finer anchors.
article.md#specific-sectioninstead of a whole doc. Free, and improves grounding quality. - Lower
max_source_words. More batches, each with better recall. Local models degrade well before their nominal context limit, so smaller batches often improve extraction. - Only then consider retrieval — and prefer selecting whole sections via the anchor index over embedding chunks, so citations stay exact.
To add non-opencode sources, drop .md/.mdx files into corpus/docs/ with
frontmatter title:, and cite them the same way. Two cautions:
opencode-book synccurrently overwritescorpus/docs/from upstream. Keep added material in a separate directory and extendCorpusto read both, or vendor it after syncing.- Verbatim quoting of third-party copyrighted material is a different legal
question from quoting opencode's MIT-licensed docs. The quotes live in
claims.jsonand the prose paraphrases, which helps — but if you publish, that's worth a real look.
book.yaml global settings, voice bans, citation style
models.yaml role -> model mapping
corpus/lock.json pinned commit + per-file hashes (committed)
corpus/docs/ vendored docs (gitignored, from `opencode-book sync`)
chapters/*/chapter.yaml chapter specs
prompts/ the 10 stage templates — the "skills"
voice/exemplars.md your writing samples
build/ generated chapters + claims audit trails
manuscript/ committed chapters — the release source
publish/ EPUB/PDF conversion (WIP — see publish/README.md)
AGENTS.md working rules (CLAUDE.md symlinks to it)
opencode_book/ the tool
opencode docs -> opencode-book sync pinned corpus
-> opencode-book build grounded chapters (build/*.md)
-> opencode-book assemble single manuscript
-> publish/ EPUB + PDF
Stages 1–3 are working and tested. Stage 4 is documented but unproven against generated chapters.