- Build ambitious outcomes behind small typed interfaces; give behavior one owner and generate or parity-test projections instead of duplicating policy.
- Work autonomously within approved scope. Pause for destructive or production effects, exceptional spend, material ambiguity, or new authority.
- Treat stop, wait, stand down, pivot, and steer as control events.
- Use the smallest owning product-path check. Add a falsifier for contested, load-bearing, or potentially vacuous claims; record controls, recovery, and blind spots.
- Evidence follows source/artifact identity. Reuse proof when relevant code, build inputs, and dependencies are unchanged. Repeat affected checks for relevant changes, failures, deployment, or packaging differences. Do not rebuild or recapture solely for main.
- Ship means owning-main integration with terminal merge and applicable release/deploy checks. Confirm landed content and result; an open PR is incomplete.
- Use
shipwhere deployed; otherwise use protected PRs: queue when required, or squash with--match-head-commitafter required reviews/checks. Never--adminor bypass protections.
Title every pull request [Area] Sentence case, for example
[Game] End the round when the candle burns out early. Common areas here are
Game, Prompts, CI, and Docs. See
CONTRIBUTING.md for the verification commands.
20EQ is one Harn package: a terminal chat game whose deterministic turn policy
lives in lib/game.harn and whose character prompt and terminal loop live in
20eq.harn.
- The entry point is
pub fn main(harness: Harness)in20eq.harn. It must staypub, because[exports].maininharn.tomldocuments that symbol. - Route every effect through
harness.*. Pass the narrowest handle a helper needs:print_candleandheadertakeHarnessStdio,hex_optionstakesHarnessEnv. - Model routing has one owner,
hex_options. It readsTWENTY_EQ_MODELthroughmodel_from_envand returns a{provider, model}record. Do not pass aprovider:modelstring to an LLM call, and do not hard-code a route at a call site. - The private notebook boundary has one owner too:
SECRET_OPENandSECRET_CLOSEinlib/game.harn. The streaming call hides it live through theprivateoption, andstrip_secretcleans a non-streamed reply. Any new model call must go through one of those. - Keep
tests/game_test.harnfree of real model calls and wall-clock reads. Useharness.llm.mock_enqueuefor a reply under test.
Run what CI runs, in this order:
harn fmt --check 20eq.harn lib/game.harn tests/game_test.harn
harn check --strict-types 20eq.harn lib/game.harn tests/game_test.harn
harn lint --strict 20eq.harn lib/game.harn tests/game_test.harn
harn test tests/ --parallel
harn package checkharn test accepts one positional target. Chain separate invocations with
&& rather than passing two paths.
For a change to the terminal loop, also diff the deterministic paths against
main before and after, since none of them need a model:
printf '/quit\n' | harn run 20eq.harn
printf '' | harn run 20eq.harn
printf '\n\n/quit\n' | harn run 20eq.harnBumping the Harn toolchain means editing .harn-version, the harn range in
harn.toml, and the pinned version in the README install command together.