Skip to content

ADR-0050 Architecture decision record

What code names, config declares; what the CLI names, discovery finds ​

A Machine names things outside itself by string, and until now the first sign of a wrong string was an invoke-time failure mid-run. ADR-0049 moved Agents and Sandbox Images INTO the Machine — actor slots and a workspace() option — so those names are typed by xstate's own src typing and by the Machine's own parts. ADR-0051 moved the last one, the Repo, the same way: a slot on the workspace(), bound by url. What this decision fixes is the rule that says where a definition lives, and the gate that enforces it.

Decision ​

  • One rule decides where a definition lives. A thing that code refers to by name and that a Machine cannot carry is declared in jr2.config.ts, so the type system sees the name. A thing that only the CLI or HTTP API refers to by name is discovered from files. Workflows are the latter — jr2 run and POST /workflows/:name/runs name them, a nested Machine is reached by import (ADR-0049) — so workflows/ stays filename-discovered. Agents, images, and Repos are neither: they ride the Machine (ADR-0049, ADR-0051), and the agents/ folder, config.agents, config.images, and config.repos all retire. One thing code names and a Machine cannot carry is a secret's value: an Effect names it with secret("name"), and the Instance declares it in the top-level secrets list (ADR-0068). The name is a string, not a type: the walk after the typecheck refuses an Effect that names a secret the list does not hold, before anything is built. jr2.config.ts holds reach and credentials (harness, secrets, git.credentials, registry).
  • The slot key types a Repo. A workspace() declares its Repos as slots — repos: { target: open, docs: { url, ref } } — and the keys are a phantom on the wrapper type, read through pool()'s worker and workspace()'s body as the image is (ADR-0049). A customize(machine, { repos: { <slot>: url } }) of a slot the Machine does not declare is a compile error, never a silent widening to string, and the body's workspace.repos.<slot> handle is typed by the same keys. There is no Register, no RepoName, and no repoNames(): the url a Machine writes is the thing, not a reference into someone else's file (ADR-0051). A door that offers a menu of Repos is the Instance's own z.enum of urls.
  • jr2 up typechecks the Instance before it builds anything, and refuses on errors, naming them. The scaffold's tsc --noEmit and pinned compiler already exist (ADR-0043's rule for the checker); this makes them a gate instead of a script the user may run. A wrong slot name, a customize() of an Agent or a Repo Slot the Machine does not carry: all stop here. One Repo check is converge-time and not compile-time: a registered Machine with an open slot nobody bound. A workflows/ export has no type to hang it on, so the walk that follows the typecheck refuses it, before it builds anything, naming the Machine, the slot, and the customize line that fixes it (ADR-0051).
  • jr2 check runs the gate and the walk with no cluster. The typecheck and the walk are one function of the CLI, checkInstance. jr2 up and jr2 check both call it, so the two give the same refusals with the same text. The walk loads each registered module. Thus jr2 check also gives each refusal that a module throws when it loads. These are refusals of createMachine, defineEvent and workspace(), and npm run typecheck cannot give them. jr2 check reads no kube context. Thus an Assistant that may not run jr2 up can run it before it gives a change to the user. It runs in a kit checkout and in an installed Instance. The name is the verb that a user types for "is this folder correct". The glossary's check of a Check pod is a different noun in a different place: one model call in the provider preflight. Thus the name stays, and the glossary entry tells the two apart.

Considered options ​

  • A generated declaration (jr2-env.d.ts written by init/up from directory discovery), to keep agents/ and images/ as folders and still type their names. Rejected: a generated-file class jr2 does not have, an editor staleness window, and — decisive — ADR-0049 removed the need by making the names part of the Machine.

  • Agents and images as config entries (agents: { scribe }, images: { default: "./images/default" }), the shape this ADR first took. Rejected the same day: it typed the names but kept them Instance-scoped, so a packaged Machine still depended on someone else's config holding the right strings, and one run could not hold two Machines each carrying a coder.

  • A Repo catalog in config, typed back through a Register interface (repos: [{ name, url }], a RepoName derived from typeof config, z.enum(repoNames(config)) on a door), the shape this ADR took for the one dependency it left in config. Rejected in ADR-0051 by the argument above: a packaged Machine still could not say which repository it works on, and a consumer could bind one only by holding the right NAME. The slot key is the type now.

  • A flag on jr2 up (jr2 up --check, or --dry-run) in place of a verb. Rejected: up reads the kube context first and its flags are converge flags; a flag that stops it after the walk still asks an Assistant to run the converging verb, which a permission rule that forbids jr2 up also forbids. A verb is the line a permission rule can allow.

  • A copy of the walk in jr2 check. Rejected: two walks drift, and then the check passes a folder that jr2 up refuses — the failure that jr2 check exists to remove.

  • Stage the Assembly in jr2 check too. Rejected: staging runs a frozen install from the lockfile (ADR-0043), which needs the registry and takes minutes, and its refusals are about the install, not the code an Assistant writes.

Consequences ​

  • Only Machines the Instance's program typechecks get the compile-time guarantee — those under workflows/ and what they import. A packaged Machine compiled elsewhere carries its own parts, so its internal names are checked where it was built; the Repo it works on is an open slot the consumer binds with customize, typed by the package's own slot keys, or a per-run slot typed by the consumer's mapper over the door.
  • Runtime validation stays: an open slot nobody bound is a jr2 up walk refusal, and the attach still refuses a per-run url no git.credentials entry matches (ADR-0051). The type error is the earlier check, not the only one.

The docs of Kit version 0.5.1. Each page is also at its .md URL.