Files
khosra/HARNESS.md
T
Claude Opus 5andbdeshi 751ab9c06f move the demo's coverage test beside the wiring it proves
The test rebuilt the feature list by hand, because a package cannot import a
main, and it drifted three times in one session — the dialect, notation, Compose
— each caught by a failing case rather than by the copy.

The obvious fix was a composition package under internal/ext, and it was the
wrong one: ADR-0069 forbids a feature importing its sibling, so that package
would have failed the gate on its first build and the invariant would have been
weakened one commit after becoming mechanical. Moving the test is smaller and
points the other way — cmd stays the only place a feature is named, which is
what extensions.md asks for, and wire_test.go was already here for the same
reason.

runServe and the test now call one function for the renderer, so there is no
list to keep in step. Four files named a feature before; three do now, all of
them package main, plus a benchmark that deliberately wires one extension to
measure the render path and never claimed to be the shipped list.

The core ceiling paid for it rather than being raised a third time: `given` was
a helper with one caller and is now inlined into the only function that used it.
core 2842/2850.

Latent item cleared — the one that was marked due.
2026-08-01 23:47:55 +06:00

257 lines
19 KiB
Markdown

# 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 (`skills/feature-loop/`), six commands, and `launch.json`.
`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 → Commit → 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.
1. **You:** "Add tag pages."
2. **Agent:** up to three clarifying questions, each with a default. Answer or ignore.
3. **Agent:** a short plan — goal, primitive, success criteria, files, ±LOC, deps, earn-it check,
and what it is deliberately *not* doing.
4. **You:** "go", or edit the plan. "Just do it" skips the gate on small changes.
5. **Agent:** implements, runs `./scripts/verify.sh`, shows real output, updates docs, commits, 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.
- `/audit` every ~5 features — finds abstractions that never earned their keep.
- `/invariants` at 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-docs` after 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 `cmd/khosra` serves it
through the real handler and the real feature list with one case per feature (ADR-0072), 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).
`.claude/launch.json` points the editor's preview at that same `make demo` on `localhost:8080`, so
"look at it" and "test it" are the one site. It is the only dev server this repo has: the engine serves
a site root and there is nothing else to run.
**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 has moved twice, both 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. It went 2800 → 2850 (ADR-0065) for eight lines of theme contract, and ADR-0041's own
test — read a second raise as evidence something belongs in `internal/ext/` — was applied rather than waived:
`feed.go` and `discover.go` are the features that should leave, and they cannot, because an `ext` feature
cannot own a **route** until the extension registry exists. When it does, they leave and this ceiling should
come back down rather than stay as headroom. 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.
**Context is a budget, and three pieces of it are mechanical.** The limit on this project is how much
work fits in a session, so `docs/context-economy.md` holds the reading, searching and reporting
disciplines — read the compressed form first, batch calls, script anything repeatable, never pay twice
for the same bytes (ADR-0053). Three parts are enforced rather than trusted: `docs/surface.md` is
regenerated and staged by the pre-commit hook and independently compared by `verify.sh`, so the map of
the code cannot mislead (261 lines standing for 3,757 of source) and no one has to remember to run it;
`CLAUDE_LOC_MAX` bounds the one file re-sent on every turn;
and `./scripts/verify.sh --quiet` prints a verdict instead of forty-three lines, which is what the
pre-commit hook now runs. The doc leads with a floor rather than the savings, because every cheap
failure mode is also a token saving — not opening the owning doc, guessing a signature, reporting
from a diff — and each of those has already cost this repo a defect.
**The agent commits by itself, and tells you the first time.** Work is committed as part of the loop
rather than on request (ADR-0052) — one commit per revertible unit, never a `push`. In a session where
you have not asked for that, the first commit is preceded by a line saying so and naming the off
switch ("stop committing"), because a commit appearing in your repository unannounced is a surprise
even when it is harmless. The asymmetry is the argument: a commit you did not want is one `git revert`
away, and work that existed only in the working tree when something went wrong is gone. If you want
the tree left dirty, say so and the loop stops at Document.
Authorship records who wrote the bytes: a commit the agent wrote is authored by it, one you both wrote
is yours with the agent as co-author, one you wrote alone names only you (`conventions.md` "Git"). You
remain the committer and the signature in every case — the key says you take responsibility for the
commit, which is not the same claim as having typed it. `git log --author=Claude` is then a truthful
answer to how much of this the agent wrote.
**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.** Raw HTML renders for site-root content, because
that content is the author's own (ADR-0060) — and the *trusted* half of that sentence is the whole claim.
`verify.sh` fails unless `html.WithUnsafe()` appears in exactly one file, `internal/render/render.go`. A
second call site means some other input has been handed the pipeline that trusts its author, which is the
failure ADR-0003 exists to prevent; when comments arrive they get their own goldmark without it. A feature
wanting to emit HTML still renders a theme template instead (ADR-0036).
## Coming back after a long absence
In order, cheapest first:
1. `./scripts/verify.sh` — one command, tells you whether the thing is still coherent and whether
`docs/state.md` has fallen behind the code.
2. `docs/state.md` — what exists, the earn-it counters, the latent list. This is the only doc that
describes the present, and `verify.sh` compares its last commit against the last `.go` one rather than
trusting a sha written by hand (ADR-0057). Its companion `docs/surface.md` is generated:
every declaration and its line, so you can find your way around without opening anything.
3. `git log --oneline` — one revertible unit per commit, each body saying *why* (`conventions.md`). This is the
real map of how the code got here.
4. `/refresh-docs` — reconciles every doc against the actual code and reports drift, which is exactly the
question you have after a year.
5. `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.
6. `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
**Every counter row says what does *not* count**, and `verify.sh` fails on a row that leaves that column
empty (ADR-0070). Four counters had to be re-scoped the first time anything tested them, and each fix was a
sentence about what had been wrongly included — so the sentence is now required up front. A counter that
cannot name an exclusion is measuring a symptom.
**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 `_WARN`s, 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.sh` fails when `CLAUDE.md`, `scripts/` or `.claude/` changes without `HARNESS.md`
changing — except `.claude/settings.json`, which is Claude Code's permission list rather than anything
about khosra. That exemption is one path: `scripts/budgets.env` and every other file under `scripts/`
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.
- **Invariant 7 is mechanical now.** "Every feature is a leaf" means only `cmd/` may import
`internal/ext/…`, and `verify.sh` states that as one rule rather than a list of the pairs that happen to
exist today (ADR-0069) — so a core package added next month cannot quietly import a feature. A feature
importing its sibling fails the same rule.
- `verify.sh` fails when a `.go` file changes without `docs/state.md` changing, warns when `state.md`'s last
commit is older than the last `.go` one — the two together mean the doc ships inside the change, never in a
commit trailing it (ADR-0057) — and fails when `cmd/` or
`internal/` code changes without a `_test.go` changing — behaviour ships with a test. A comment-only or
`gofmt`-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-commit` runs it on every commit; enable once per clone
with `git config core.hooksPath scripts/hooks`. `--no-verify` bypasses it, and the commit body should
say why. The hook also regenerates `docs/surface.md` and stages it, so a generated file is never
something you have to remember — and it refuses a commit with unstaged `.go` changes, because what it
generated describes the working tree rather than what you are committing.
- `docs/decisions.md` registers 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 --list` names every gate that exists. A doc claiming enforcement is checkable
against it in one command, and `/refresh-docs` checks 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.md` rule 8 forbids it.
- `docs/README.md` carries 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.mod` does not exist yet — `go mod init` belongs to the first feature, and the module path is
still unchosen.
- `.claude/settings.json` denies reading `./.env*`, but secrets a parent directory's `.envrc`
exports are in every command's environment regardless. The deny rule is narrower than it looks.