Naming is free before a module is published, a URL is shared, or a binary is deployed; every day it waits costs more. Swept every form: module path, binary, cmd/ directory, KHOSRA_SITE, the feature-loop skill directory, and the prose in earlier ADRs — which describe this project under its old name, not a different project. Recorded as ADR-0030. go mod init lands here rather than with the first feature because the module path is what the rename is about. x/text and yaml.v3 are required but not yet imported, so both are indirect and no direct dependency is claimed yet.
169 lines
11 KiB
Markdown
169 lines
11 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 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.
|
|
|
|
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, 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).
|
|
|
|
## 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, the commit it was last verified
|
|
against. This is the only doc that describes the present.
|
|
3. `git log --oneline` — one feature 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
|
|
|
|
**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. 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/` and `reference/` as well, so
|
|
exploratory scratch 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.
|
|
Softer signals — `fmt.Errorf` without `%w`, `interface{}`, nesting past 4, exported-and-referenced-
|
|
once — stay advisory.
|
|
|
|
**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. This file is the current description of the machine, not a snapshot of its design.
|
|
- `verify.sh` fails when a `.go` file changes without `docs/state.md` changing, and 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.
|
|
- `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.
|