Instructions for an agent working on opencode-book — a build system that generates a how-to book about opencode from official documentation and third-party sources, with every factual claim machine-checked against its source.
Read documentation/ for how the system works. This file is about how to work
on it.
Every rule here traces back to something that actually went wrong during this project. When something new goes wrong, add a rule rather than remembering it. A rule you cannot trace to a real failure is probably noise — delete it.
These are the invariants. Breaking one produces a book that looks fine and is quietly wrong, which is worse than an obvious failure.
- The drafter never sees raw sources. It receives validated claims only. This is what stops it "remembering" a flag that does not exist.
- Validation is code, never a model.
opencode_book/validate.pyasserts each quote is a literal substring of its cited source. Do not add fuzzy matching, edit distance, or "close enough" thresholds. The value of the check is that it cannot itself hallucinate. - Never weaken a check to make a run pass. If validation rejects most claims, the extraction is wrong. Fix the prompt, not the validator.
- Every factual sentence traces to a claim. Connective tissue, motivation, and analogy are free. Product facts are not.
- Never invent to fill a gap. If the sources do not cover a step, write around the gap and flag it. A chapter with an honest hole beats a chapter with a fabricated flag.
Most errors in this project came from asserting something plausible without checking. The pattern repeated often enough to warrant its own section.
| Rule | What went wrong |
|---|---|
| Verify a negative result with a second method before relying on it | A Unicode box-drawing scan returned empty, so the corpus was declared "pure ASCII". It wasn't — 16 files used box-drawing characters, and the first diagram pass silently mangled them |
| Do not trust your own verification regex | A grep '^## User' check appeared to find leaked content; the matches were inside fenced tool output. Verify the verifier before acting on it |
| Check for primary sources on disk before reconstructing from memory | A session transcript was about to be rebuilt from recollection. The real JSONL was sitting in ~/.claude/projects/ |
| Run the diagnostic, don't estimate from it | Two chapters were reported as under-sourced. Running the preflight showed six of eight exceeded their targets |
| Verify version and identity claims against primary sources | SEO "best local LLM 2026" articles confidently named Qwen3.6, Gemma 4, and GLM 5.2. None appear in HuggingFace download data. Use the HF API or the official repo, never a listicle |
| Re-check upstream identity periodically | opencode moved from sst/opencode to anomalyco/opencode. URLs rot |
When you state a fact in a commit message, a doc, or to the user, you should be able to name the command that established it.
- Sync with
opencode-book sync. It pins a commit intocorpus/lock.json; that pin is what makes citations permanent. - Only top-level
.mdxfiles are canonical English. Anything one level deeper is a locale translation. There are 17 locales — including them inflates the corpus ~17× for nothing. - Never hand-edit
corpus/docs/. It is regenerated and gitignored. - There is deliberately no retrieval layer. The corpus is ~70k tokens and fits in context. Do not add a vector store — it is the most failure-prone component in a typical doc pipeline and this corpus does not need it.
The book is expected to draw on articles and general background material, not just opencode's docs. Rules for that:
- Put them in
corpus/external/, nevercorpus/docs/.opencode-book syncoverwritescorpus/docs/wholesale — anything you add there is destroyed on the next sync. - Require frontmatter:
title:,source_url:,retrieved:, andlicense:(orlicense: unknown). Withoutsource_urlthe citation cannot resolve and the material is unusable. - Snapshot, don't hotlink. Store the text locally so quotes stay verifiable when the page changes or disappears.
- Keep third-party quotes short. Verbatim quotes live in
claims.jsonfor verification; the prose should paraphrase and attribute. Quoting opencode's MIT-licensed docs at length is fine. Quoting someone's blog at length is a different question, and this repo is public. - Record the license per source.
license: unknownis a valid value and a signal to paraphrase rather than quote. - Never mix an unlicensed source into a chapter silently. If a chapter
depends on material you cannot license, say so in the chapter's
notes:.
| Thing | License | Implication |
|---|---|---|
opencode source and docs (anomalyco/opencode) |
MIT | Safe to quote and build on, with attribution |
| This repo's code | MIT (LICENSE) |
Yours |
| The Deep Dive into OpenCode community book | none — all rights reserved | Do not redistribute its text. It was the model for the publish pipeline, not a source |
| Third-party articles | varies | Check per source; default to paraphrase |
A software license does not extend to a book written about that software. These are separate works with separate rights holders.
- Sources are
doc#anchorrefs, never free text. Find them withopencode-book anchors --doc agents.mdx. - Verify every ref resolves before spending model time. A bad ref fails the build; catching it earlier is cheaper.
- Treat
target_wordsas a ceiling, not a quota. A target far above the available source words guarantees padding or invention. Ratios up to ~1.5× are healthy — good prose expands on sources with explanation. Beyond that, either widensourcesor lower the target.
# preflight: catch targets that exceed their sources
./.venv/bin/python -c "
import yaml
from pathlib import Path
from opencode_book.corpus import Corpus
c = Corpus(Path('corpus'))
for d in sorted(Path('chapters').iterdir()):
s = yaml.safe_load((d/'chapter.yaml').read_text())
w = sum(len(c.resolve(r).full_text.split()) for r in s.get('sources') or [])
t = s.get('target_words', 0)
if t > w * 1.5: print(f'{d.name}: target {t} vs source {w} — will pad or invent')
"- Capture fenced code blocks as claims. The first extract pass on chapter 05 returned 26 claims and zero code examples, leaving the drafter unable to write a single config snippet without inventing JSON structure. For a how-to book that is fatal, and it is invisible until you notice nothing is runnable.
- A claim must not assert more than its quote establishes. Validation checks
that a quote exists, not that the claim follows from it. Two claims passed
validation with real quotes attached while their text overreached:
- "file edits and bash default to
ask" — the quote said "all of the following are set toask:" without naming the items - "placed globally or per project" — the quote stopped before the paths If the quote refers to a list by reference, quote the list too or narrow the claim.
- "file edits and bash default to
- Quotes must be character-exact. Em-dashes, curly apostrophes and inline backticks in opencode's docs are the usual failure points. Do not normalise them.
- Prefer fewer solid claims to more shaky ones.
verifyshould run on a different model thandraft. A model is a poor auditor of its own output. When they share a model — the current POC posture — treat verify results as weaker evidence than the logs suggest.- The verify stage is the only real entailment check in the system. Do not disable it to speed up a run.
- Strongest verification available for a how-to book about a CLI is execution: extract every command and config block and run it in a container. Textual grounding proves the book matches the docs; execution proves it matches reality, and catches docs that are themselves stale.
Target machine: M5 Pro, 64 GB unified memory, 307 GB/s.
- Sum the resident set against the ceiling before proposing a model lineup. A three-model config was specified totalling ~69 GB against a 64 GB limit — they could never all stay resident, and every role change evicted weights to disk.
- Prefer MoE over dense. Dense models are bandwidth-bound (every parameter read per token); MoE activates ~3B per token and sidesteps that wall. The effect is large enough on Pro-tier silicon that an M4 Pro can beat an M3 Ultra on an 80B MoE despite a third the bandwidth.
- Builds are stage-major: every chapter through
extract, then every chapter throughdraft. Chapter-major ordering makes each role change a model change, turning 4 model loads into 4×chapters. Use--chapter-majoronly to debug a single chapter. - Roles are indirection: stages ask for a role, never a model name. Keep it that way so swapping stays a config edit.
- The voice stage is the weakest link for local models and the first role worth pointing at a hosted API.
- The audience is working developers. Approachable and conversational — a colleague explaining something at a whiteboard, not a spec.
- Voice is controlled by exemplars, not adjectives. "Be conversational"
produces the generic assistant register.
voice/exemplars.mdmust contain paragraphs the author actually wrote. voice/exemplars.mdcurrently holds placeholders. Any output generated against them inherits a borrowed voice. Replace before generating anything intended to be kept.- Add to
banned_phrasesinbook.yamlwhenever a tic surfaces. Cheaper than editing prose by hand. - The voice pass may not change facts, touch code blocks, or add citations.
- Default is
citation_style: chapter— one deduplicated source list per chapter. This is presentation only. Claim-level verification runs regardless, and the audit trail always lands inbuild/<chapter>.claims.json. - Do not "simplify" by removing claim-level grounding. The reader-facing format and the verification mechanism are independent; conflating them was an early over-engineering mistake.
- Every citation resolves to both a live URL and a pinned commit blob.
publish/ is work in progress and has never been run against generated
chapters. Read publish/README.md before touching it — its seven recorded
gotchas are confirmed findings that will recur on a fresh machine:
Debian's epubcheck package ships a broken launcher; @page paged-media CSS
fails epubcheck and must live in style-print.css; --include-before-body
multiplies across every chapter of an EPUB; an image-only section emits an empty
<h1> that fails nav validation; a title: in metadata emits a stray header
ahead of the PDF cover; print pages are narrower than Kindle reflow width; and
pandoc resolves relative image paths against its working directory, not each
file's location.
General rules learned here:
- Use absolute paths for generated assets. Relative paths computed per-file break the moment files from several directories are concatenated.
- Match thresholds to the output medium. A code-width threshold safe for Kindle reflow wrapped mid-diagram in a 6.5in PDF.
- Consider pagination when rasterizing. A 116-line tree became a 4581px-tall image that could not paginate; it was better left as text.
- Verify where output actually landed. A hardcoded path once wrote the build into the repo root instead of the build directory.
Small things that cost real time:
- Check whether a path is a symlink before redirecting into it.
> /usr/bin/epubcheckfollowed the symlink and destroyed the jar it pointed at. Remove the link first. - Restart stateful test harnesses between runs.
tools/replay/server.pycounts verify calls; a stale server made the revision loop appear not to fire. - Use
KeyErroronly for lookups. Its__str__returnsrepr(arg), which escapes newlines and mangles multi-line "did you mean" messages. Define a dedicated exception instead. argparsetreats a value beginning with-as a flag. Use--opt=value.- setuptools auto-discovery claims content directories as packages. Keep
packages = ["opencode-book"]pinned inpyproject.toml.
- Mark uncertainty explicitly. When something is unproven, say so where it
cannot be missed — at the top of the file, not in a footnote.
publish/is labelled WIP for exactly this reason. - Distinguish "documented" from "verified". State plainly which stages have actually run and which have not.
- Prefer the simpler thing the user asked for. Chapter-level source lists were requested over inline footnotes; the inline machinery was more than the job needed.
- Report failures with their output. If a check fails, show it. If a step was skipped, say so.
- Surface findings that contradict earlier statements, including your own. Correcting "two chapters are thin" to "six of eight exceed their targets" was more valuable than the original claim.
- Do not spawn subagents for work that can be done inline.
- Never add
Co-Authored-By, "Generated with", or any model attribution to commit messages or PR bodies. Describe the change only. This is about log signal-to-noise, not about hiding tooling — the agents files stay in-repo. (Subject matter is fine: "using local models" is a description of the project, not an attribution.) - Commit messages explain why, not just what. Record the failure a fix addresses.
- Inspect a remote before any force push. The remote here already held a
LICENSE; force-pushing would have destroyed it. Rebasing preserved it. - Review what a broad
git addstaged before committing, and scan for secrets before pushing — this repo is public. - Keep out of version control: session transcripts (
sessions/), vendored docs (corpus/docs/), build outputs (build/), publishing assets (publish/assets/), and harness debug dumps.
Check documentation/03-gaps-and-mitigations.md for the ranked list. The three
that matter most:
- No local model has ever run. Every execution so far has been mock or replay. Whether a local model can produce publishable prose is the central untested assumption.
voice/exemplars.mdis placeholder text.build/mixes one real chapter with seven mock placeholders, andopencode-book statuscalls them allok— a valid fingerprint says nothing about which backend produced it. Clear the directory when switching backends.
The goal is that someone can move between Claude Code, opencode, and other harnesses without rewriting configuration. Most of that is now settled by open standards — do not invent local conventions where one already exists.
| Layer | Standard | Governance |
|---|---|---|
| Instructions | AGENTS.md — https://agents.md | Linux Foundation / Agentic AI Foundation |
| Skills | Agent Skills (SKILL.md) — https://agentskills.io |
Anthropic-originated, open spec |
| Packaging | Agent Plugins 1.0 — https://github.com/agentplugins/agent-plugins-spec | AAIF (Amazon, Cursor, Microsoft, OpenAI, Vercel) |
| Tools | MCP | AAIF |
| Subagents | none | — |
Agent Skills standardizes the skill format, not the discovery location. The spec defines the folder layout and frontmatter; it says nothing about where clients look. That is why opencode searches six paths and Claude Code searches one, and why a neutral directory plus a symlink is a real fix rather than a workaround. Expect the same split in any future "standard": check whether it covers format, location, or both.
SKILL.md frontmatter, per the spec:
| Field | Required | Notes |
|---|---|---|
name |
yes | ≤64 chars, lowercase/digits/hyphens, must match the parent directory name |
description |
yes | ≤1024 chars; state what it does and when to use it |
license |
no | name or bundled file reference |
compatibility |
no | ≤500 chars; environment requirements |
metadata |
no | arbitrary string map |
allowed-tools |
no | space-separated; experimental, support varies |
Keep SKILL.md under ~500 lines and push detail into references/ — agents load
skills progressively (name + description at startup, body on activation,
resources on demand), so a bloated SKILL.md costs context on every task.
Both tools use markdown with YAML frontmatter, but the schemas differ:
| Claude Code | opencode | |
|---|---|---|
| Fields | name, description, tools, model, permissionMode, maxTurns, skills |
description, mode, model, permission, temperature |
Agent Plugins 1.0 standardizes exactly two component types — Agent Skills and MCP servers — and deliberately leaves subagents out. Treat that as the ecosystem's current answer, not an oversight to route around.
Rule: write subagent definitions to the intersection. description and
model mean the same thing in both. Stick to those and symlink one directory to
the other. Accept that per-tool tuning (tools vs permission) does not
transfer, and do not build a translation layer for a format that is still
moving.
- Portable is not verified. A skill that loads everywhere does not behave identically everywhere. Test against each harness you claim to support.
- Agent Plugins 1.0 has no security model — no permissions, sandboxing, signature checks, or secrets handling. All listed as future work. Fine for distributing your own work; a real consideration before installing others'.
- Re-check this section periodically. Every standard in the table postdates mid-2025, and two of them shipped within the last nine months.
Instruction files, skills, and agent definitions live in three different
places. Conflating them is an easy mistake — an AGENTS.md tucked inside a
folder is read by nothing.
Root-level, and the single source of truth:
AGENTS.md <- the real file, edit this (opencode)
CLAUDE.md -> AGENTS.md (Claude Code)
opencode finds AGENTS.md by walking up from the working directory to the git
worktree. Claude Code reads CLAUDE.md. One symlink keeps them identical.
opencode searches six locations, one of which is tool-neutral:
.opencode/skills/<name>/SKILL.md opencode-specific
.claude/skills/<name>/SKILL.md Claude-compatible
.agents/skills/<name>/SKILL.md agent-compatible <- prefer this
(plus ~/.config/opencode/, ~/.claude/ and ~/.agents/ globals.)
Put skills in .agents/skills/<name>/SKILL.md — note the leading dot.
opencode reads that natively, so no symlink is needed for it. Claude Code only
looks in .claude/skills/, so that path symlinks back:
.agents/skills/ <- the real directory
.claude/skills -> ../.agents/skills
A third location again, and the one case with no .agents/ equivalent:
| Tool | Project | Global |
|---|---|---|
| Claude Code | .claude/agents/ |
~/.claude/agents/ |
| opencode | .opencode/agents/ |
~/.config/opencode/agents/ |
Both take markdown with YAML frontmatter, and both let the filename or a name
field become the agent name — but the frontmatter schemas differ (Claude Code
uses name/description/tools/model; opencode uses
description/mode/model/permission). They are not blindly
interchangeable, so symlink one directory to the other only if the definitions
you write stay within the fields both understand.
Claude Code resolves conflicts by precedence: managed settings, then --agents
CLI flag, then .claude/agents/, then ~/.claude/agents/, then plugin
directories. Nested project dirs are allowed and the definition closest to the
working directory wins.
None exist yet. prompts/ holds the pipeline's stage templates, which are not
agent definitions — they are inputs to opencode-book, not files any tool discovers.