Skip to content
hekla

Writing heklang with an agent

A language an agent can finish.

Most of what makes generated backend code untrustworthy is not the logic. It is the quiet nondeterminism: a clock read in a fold, an id minted on a retry, a call out from a handler that has to replay. heklang does not have syntax for any of that, so the review that matters is the domain rule rather than the machinery around it.

01Watch one write it

Ask for a rule. Get a program.

Pick a request. Each one is a real project in this repository, and the hekla check line under it is what running the checker against that project actually prints.

This is a recording, not a live model. Nothing here calls an API, and the code below was written once, checked, and committed rather than generated on demand.

You

A shop should never be able to reserve more stock than it has on hand.

  • The rule spans every movement of one sku, so the command folds both event types filtered on it.
  • Folding that slice is what conditions the append, so two concurrent reservations cannot both pass.
  • Running out is a fact about the world, so it is a refusal with a code rather than an invalid.
events/stock.hk
event @stock.received {
  sku: String @max(64),
  quantity: Int,
}

event @stock.reserved {
  reservation_id: Uuid,
  sku: String @max(64),
  quantity: Int,
}
commands/reserve-stock.hk
refusal NotEnoughStock(available: Int) "only {available} left for this sku"

command ReserveStock(reservation_id: Uuid, sku: String, wanted: Int) {
  // Every movement of this sku, which is what the reservation has to be
  // consistent with. Folding it is what conditions the append on it.
  fold on_hand: Int = 0
    on @stock.received(sku) { quantity } => on_hand + quantity
    on @stock.reserved(sku) { quantity } => on_hand - quantity

  if wanted <= 0 {
    return invalid("a reservation needs a positive quantity")
  }
  if wanted > on_hand {
    return reject NotEnoughStock { available: on_hand }
  }

  emit @stock.reserved { reservation_id, sku, quantity: wanted }
}
$ hekla check .ok: no errors, 0 warning(s)
02Why this language in particular

Four properties that happen to suit a model.

None of these were designed for agents. They fall out of a language built so a projector rebuild and an effect replay reproduce exactly what they did the first time, and they happen to be the same properties that make a program mechanically checkable by anything.

The mistakes are unrepresentable

The usual failure modes of generated backend code are a stray clock read, a minted random id, a call out from somewhere that has to be reproducible. None of them parse here. A model cannot introduce nondeterminism into a projector by accident, because the syntax for it does not exist in one.

The checker is a complete oracle

One pass either accepts the program or names every reason it does not, with an extent and a hint per finding. There is no separate type-check to reach and no partial state to reason about, so the loop is write, check, fix, rather than write and hope.

Every rejection has a documented fix

The diagnostic set is closed at forty-four codes, and the skill's reference maps each one to what to do about it. Recovering from a rejection is a lookup rather than an invention, which is exactly the step models are worst at.

It can check its own work

A test is a declaration in the same language, so the agent writes cases beside the code and hekla test runs them against a real log, real read models and a real key store. There is no harness to stand up and nothing to mock.

03Setting it up

Two skills ship with the source.

Both repositories carry a Claude skill: the rules distilled out of the specification, one reference file per declaration kind, a table from every diagnostic code to its fix, and a worked example program. Run this in a project that writes .hk files and the agent working there picks them up.

It takes one path out of each repository's tarball, so nothing is cloned and nothing else lands. Drop hekla from the list if you only want the language.

in your own project
mkdir -p .claude/skills

for repo in heklang hekla; do
  curl -sL "https://git.tqwewe.com/tephra/$repo/archive/main.tar.gz" |
    tar -xz -C .claude/skills --strip-components=3 "$repo/.claude/skills/$repo"
done

.claude/skills/heklang/

SKILL.md, seven reference files, a seven-file example

The language: declarations, folds, sealed content, refusals and tests, plus a table from every diagnostic code to its fix.

.claude/skills/hekla/

SKILL.md, six reference files, a twelve-file example project

Everything the runtime adds around it: the directory convention, the generated HTTP surface, the CLI, and how subject encryption works underneath.

04What it does not fix

Still your judgement.

A checker that accepts a program is saying it is well formed, not that it is right. These are the parts no static pass and no model can decide for you.

A green check is necessary, not sufficient.

What it cannot see is data-dependent. A Money operation that cannot be answered exactly type-checks and then fails at run time naming mul or div, and no static pass can know which values will arrive.

The boundary is a modelling decision.

Which slices a command folds is a judgement about the domain, and a model will pick a plausible one rather than the right one. hek check --boundaries prints what each command guards, transitively, so the choice is reviewable rather than buried.

Which fields are personal is your call.

A field appended without a @subject can never be erased, and nothing warns about it. That is a judgement about meaning, and neither the checker nor a model can make it from a name.

There is no language server yet.

An agent works from hek check output rather than in-editor diagnostics. The tree-sitter grammar is the start of one, and hek fmt - formats a module from stdin, which is what format-on-save wants.