The human asked for a demo extensive enough to review by hand, tracked as files, and kept current the way docs are. Generated filler cannot be reviewed — you cannot read a Go function and see what a reader sees — so the generator is deleted and `examples/demo-site/` is an ordinary site: 33 Markdown files, six pictures, site.yaml, a template override and static files. My reason for generating it was ADR-0011, and I had that rule wrong: it puts the *author's site root* outside this repository, not fixtures. conventions.md already keeps golden files in testdata/, and this is the same category one size up. Keeping both a generator and files would have been two sources of truth, so one had to go. Kept true by two gates rather than by good intentions. A table-driven test in internal/web serves the directory through the real handler with one case per feature — thirty-five of them, each naming what it proves — and verify.sh runs `khosra check` over it and fails on anything fatal. Adding a feature now means adding it to the demo and adding its case, and the build says so if you don't. Extensive on purpose: eleven dated posts so a section actually paginates, a four-chapter series so first/last are not the neighbours, a gallery with three JPEGs and an SVG so both the resampled and the untouched paths show, a Bengali-only bundle, a titleless status, a draft, a future date, an include, a nested extras tree, and a typography page that demonstrates what the engine will not do. Two expectations of mine were wrong and the demo corrected them: the site's own list template overrides *tag* listings too, so there are no group headings there — which turns out to be the better demonstration of ADR-0046, since the theme picking the flat shape is exactly the point. And template literal text is not escaped, so an apostrophe stays an apostrophe.
15 KiB
The khosra harness — human's guide
Scaffolding that makes an agent build khosra the way you want: minimally, surgically, questions
before code, docs that stay true.
README.md is a two-line signpost — human here, agent to CLAUDE.md. CLAUDE.md is the
constitution (always loaded). docs/ is what the agent needs to build the
engine — anything in it may be pulled into context on demand. ideas/ and reference/ sit outside
docs/ deliberately: storage, opened only when you name a file, swept by nothing.
.claude/ holds the feature-loop skill and six commands. scripts/ holds the gate.
How you use it
You ask for a feature. The skill fires by itself and runs
Clarify → Plan → Implement → Verify → Document → Report.
This runs for casual requests too — the skill fires on "the default-language files do not need the
.en part" exactly as it does on "add tag pages", with a one-line plan instead of a full one. A short
sentence is not a small change: that one renames content on disk, makes a suffix optional in code, and
contradicts every doc calling it required. The agent walks every surface — code, fixture sites under
testdata/, harness docs — and, because it cannot reach your site root, writes down the migration you
need to run there rather than pretending to have done it.
- You: "Add tag pages."
- Agent: up to three clarifying questions, each with a default. Answer or ignore.
- Agent: a short plan — goal, primitive, success criteria, files, ±LOC, deps, earn-it check, and what it is deliberately not doing.
- You: "go", or edit the plan. "Just do it" skips the gate on small changes.
- Agent: implements, runs
./scripts/verify.sh, shows real output, updates docs, reports.
Occasional maintenance, by you:
- "Park this" → the agent writes
ideas/<slug>.md, resumable cold, and indexes it. Name the file later to pick the thread up; it reads these only when named. /auditevery ~5 features — finds abstractions that never earned their keep./invariantsat arc boundaries, before a freeze, before the first deploy — checks the nine architecture invariants against the code, which no grep can do.
Conflicts come back to you. If a request contradicts a recorded decision — an ADR, an invariant, the permalink shape, the untrusted boundary, a hard budget — the agent stops, quotes the line, and gives you two paths: comply, or change the decision on purpose. It resolves only what is genuinely unambiguous, such as a doc that has fallen behind the code. Repeating the request is a valid answer and is taken as your decision; what it will not do is quietly reverse a choice you made earlier, or split the difference into a compromise nobody picked.
/refresh-docsafter a burst of work — reconciles docs with reality, reports drift./verify,/adr,/leaf <topic>as needed.
Three surfaces, one of them yours to edit here. Engine source lives in this repo; content lives in
the site root (ADR-0011); the theme is a third surface with its own owner (ADR-0023). A request that spans
engine and theme produces a contract extension in docs/theme-contract.md plus a note of what the theme
must do — not the theme. verify.sh fails if the embedded reference theme changes without the contract doc
changing, because in practice those two drift together — and it fails on a <script> tag in that theme,
because a reference theme that grows taste stops being a reference (ADR-0026).
The demo is gated like the docs. examples/demo-site/ is a real site, tracked as files so you can read it,
edit it and serve it with make demo. It stays true by two gates: a coverage test in internal/web serves it
through the real handler with one case per feature, and verify.sh runs khosra check over it. A feature added
without a case there is a feature the demo does not show, and the build says so (ADR-0051).
The agent names the layer as well as the primitive. Content on disk, engine, theme, browser — and it builds
at the outermost layer that can do the job (CLAUDE.md §2, docs/architecture.md). This exists because a whole
feature was built at the wrong one: widow prevention as a Markdown transform that inserted a non-breaking space
into an author's prose. It worked and it had tests; text-wrap: pretty does it better with no bytes in the
content, so the feature was deleted (ADR-0045). If a feature only rearranges how something looks, expect the
agent to push back toward CSS or a template.
The core ceiling moved once, on purpose. CORE_LOC_MAX went 2000 → 2800 (ADR-0041) because the original
figure was costed before any code existed and never budgeted what cmd/ would hold — check, new, -dev,
change detection. Treat a second raise as evidence that something belongs in internal/ext/ instead: the
two ceilings exist so that "core stops growing, ext rises" is observable, and that stops being true the moment
leaves are allowed into core.
What is staged has to build, not just the working tree. Every other gate looks at your files; this one
looks at the commit you are about to make, by building a throwaway checkout of the index. It exists because
staging a subset — a changed signature without its caller — produced a commit that did not compile while
verify.sh reported green, and a broken commit is something git bisect trips over for as long as the repo
lives. It only runs when something is staged.
The injection boundary is a gate now, not a memory. goldmark drops raw HTML from authored Markdown by
default, and that default was the only thing standing between a Markdown file and script injection. A
feature wanting to emit HTML renders a theme template instead (ADR-0036), so nothing here needs raw HTML
enabled — and verify.sh fails on WithUnsafe appearing in any non-comment line under cmd/ or
internal/. Anything that genuinely needs it must argue with a failing gate first, which is the point.
Coming back after a long absence
In order, cheapest first:
./scripts/verify.sh— one command, tells you whether the thing is still coherent and whetherdocs/state.mdhas fallen behind the code.docs/state.md— what exists, the earn-it counters, the latent list, the commit it was last verified against. This is the only doc that describes the present.git log --oneline— one feature per commit, each body saying why (conventions.md). This is the real map of how the code got here./refresh-docs— reconciles every doc against the actual code and reports drift, which is exactly the question you have after a year.docs/decisions.md— the ADR log, when you hit something and think "why on earth is it like this". Each entry names the observation that would overturn it, so you can tell a stale decision from a deliberate one.docs/toolchain.md— if something is broken rather than merely unfamiliar. It records what this was built against and which agent-tooling contracts it assumes, so a tooling change is diagnosable instead of looking like a harness bug.
Then docs/README.md for whichever topic you are actually here for.
Why the pieces exist
Counters in docs/state.md turn "no abstraction before its second use" into arithmetic. Every
threshold lives in that one table and nowhere else. The agent cannot argue a pipeline into existence
one transform early — it writes the next one inline and lets the count force the extraction. Most
load-bearing mechanism here, and the one with least machine enforcement, which is why verify.sh
fails any .go change that does not touch state.md. That does not prove the counters are right;
it makes forgetting them impossible, which is the real failure mode.
The dependency allowlist names modules that are permitted, not required. The gate also fails an
untidy go.mod, because go get marks a module indirect until something imports it — and an indirect
entry is not checked against the allowlist, so an unapproved dependency could sit there unnoticed. # starts a comment anywhere on a line, including after an entry — the gate strips those, so an entry may carry the reason it exists. Being listed is permission;
DEPS_MAX counts what go.mod actually pulls in. scripts/allowed-deps.txt is the list.
Budgets in scripts/budgets.env. Two hard LOC ceilings, core and extensions, plus a dependency
cap. Exceeding one fails verify.sh, so raising it is a deliberate act with an ADR attached rather
than a drift. The split makes "the core stops growing after Arc 2" measurable: post-freeze the core
figure holds and only ext rises. File and function length are _WARNs, not ceilings — the cheapest
way to satisfy a hard per-file limit is sharding a coherent file into a _helpers.go, which is
worse code with a greener gate.
The latent list absorbs the urge to refactor mid-feature: a line in state.md instead of a
drive-by fix. You decide when latent items become features — but an arc cannot close with an
untriaged one, so the list drains instead of becoming a graveyard.
This repo is engine source only. The site root — content/, static/, optional templates/ —
lives in its own repository and is passed to the binary with -site (ADR-0011), and validating content
is the engine's own job rather than the gate's. The gate skips ideas/, reference/ and the uncommitted
.scratch/, so exploratory code parked there never has to compile.
The architecture gate. verify.sh enforces the layering in conventions.md from go list
output: content imports no sibling, render imports neither web nor ext, web imports no ext, nothing
imports cmd. One convenient sibling import is what turns a layered engine into a ball of mud, and
it always looks locally reasonable — so it is a hard failure, not a review note.
Feature locality is enforced, not hoped for. A feature is one directory under internal/ext/<name>/
plus one line in cmd/khosra/wire.go. verify.sh fails on a feature importing a sibling and on a
feature package without a doc.go — the first because sibling imports make an agent's read set compound,
the second because a four-line doc.go turns orientation into a fifteen-line read (ADR-0027).
Documentation is gated too. Every package carries a package comment, and a decision cited in code
(// Path shape: ADR-0008.) must name an ADR that exists. Doc comments on exported identifiers are
warned rather than failed — presence is checkable, usefulness is not, and a hard gate there would buy
// Load loads. The point
is a codebase still navigable by go doc and git log alone, years from now, with no agent available —
verify.sh enforces presence, and only you can enforce that the comment says something.
The style floor fails, it does not warn. Forbidden package names, init(), importing log
instead of log/slog, panic() outside cmd/, and time.Now() outside a clock.go are stated
absolutely in conventions.md, so they exit non-zero. That last one exists because a Stage that reads
the clock without an expiry would serve staleness invisibly. A rule enforced as a suggestion teaches the agent to read every rule as one.
A green run has no warnings once code exists. An advisory that fires on correct code gets fixed —
either the check or the code — because a warning nobody can act on trains you to skim the ones you can.
Two of these have already been narrowed after firing on code the harness itself mandates.
Softer signals stay advisory: fmt.Errorf without %w, nesting past 4, exported-and-referenced-once,
and any in an exported signature — only exported, because ADR-0002 mandates an open page object,
so unexported code reading frontmatter takes any legitimately and forever.
STATUS markers in docs/architecture.md give the target shape and what is legal today, so
the agent can read the endgame without building toward it.
Six primitives, and everything reduces to one. Effect (ADR-0012) covers work off the request
path — derivatives, indexes, feed files, outbound syndication — on content change, on a schedule, or on
demand. The test that keeps it honest: if the thing is computable at render time from the page plus the
clock, it is a Stage, not an Effect. An "old article" banner is a Stage; it needs no job and can never
be stale.
The harness maintains itself
The harness is code and drifts like code. Same rule as the engine: a change ships with the docs that describe it, in the same change.
verify.shfails whenCLAUDE.md,scripts/or.claude/changes withoutHARNESS.mdchanging — except.claude/settings.json, which is Claude Code's permission list rather than anything about khosra. That exemption is one path:scripts/budgets.envand every other file underscripts/and.claude/stays gated, and khosra's own settings are not exempt from anything. This file is the current description of the machine, not a snapshot of its design.verify.shfails when a.gofile changes withoutdocs/state.mdchanging, and whencmd/orinternal/code changes without a_test.gochanging — behaviour ships with a test. A comment-only orgofmt-only diff is exempt: it ships no behaviour, and failing it would only teach you--no-verify.- The gate is not optional.
scripts/hooks/pre-commitruns it on every commit; enable once per clone withgit config core.hooksPath scripts/hooks.--no-verifybypasses it, and the commit body should say why. docs/decisions.mdregisters every ADR number ever used, entries and withdrawals alike, so a citation can resolve to a decision or to a deferral but never to nothing../scripts/verify.sh --listnames every gate that exists. A doc claiming enforcement is checkable against it in one command, and/refresh-docschecks it in both directions — a claimed gate that is missing, and a real gate nothing explains. Asserting a mechanism before it exists is the drift that reads as enforcement and is decoration;CLAUDE.mdrule 8 forbids it.docs/README.mdcarries three tables: topic → doc to read before asserting a rule, change → doc to update after making one, and an authority table naming the one home of every value. A new mechanism adds a row; nothing outside a value's home may restate it. A number written twice eventually disagrees with itself.
Open
- No ADR gate blocks Arc 1. One question remains in
docs/state.md: the language suffix on the first content file. go.moddoes not exist yet —go mod initbelongs to the first feature, and the module path is still unchosen..claude/settings.jsondenies reading./.env*, but secrets a parent directory's.envrcexports are in every command's environment regardless. The deny rule is narrower than it looks.