A protocol for building JavaScript apps that agents can author, verify, and reproduce — plus a tiny reference implementation in a single file.
- Zero dependencies. Zero build step. Native ESM. Copy
lean.jsinto your repo. - Schemas, components, adapters, workflows, verify. Five primitives. Nothing else.
- Workflows + tokens = deterministic, reproducible verification.
- Designed to fit in an agent's context window so the whole framework can be held in working memory while writing code.
Read SPEC.md for the protocol. Read AGENTS.md if you (or your agent) are
writing apps in it. Read lean.js to see the whole implementation in a
sitting.
Run the bundled examples through the verifier:
node bin/verify.mjs
Run one example:
node bin/verify.mjs counter
Run one workflow with custom tokens:
node bin/verify.mjs user-profile mounts-and-shows-name --tokens '{"user":{"seed":42}}'
Run the conformance suite (the contract any implementation must pass):
node bin/verify.mjs --conformance
Generate human-viewable snapshots of every workflow (synthetic data, per-step IR, action transcript — one HTML page per workflow plus an index):
node bin/snapshot.mjs
open snapshots/index.html
Run workflows in a real headless Chrome (no npm; uses your system Chrome/Chromium/Edge/Brave). Catches handler-wiring, event-bubbling, boolean-attribute, and other real-DOM-only bugs:
node tools/dom-verify.mjs # all examples
node tools/dom-verify.mjs triage # one example
node tools/dom-verify.mjs --only-dom # only DOM-marked workflows
See AGENT_LOOP.md for the full iteration pattern an agent (or human) uses
to drive an app from in-progress to ready.
import { string, email, schema, component, synthetic, wire, h,
workflow, mount, action, expect, verify } from './lean.js';
const User = schema({ id: string(), name: string(), email: email() });
const UserCard = component({
name: 'UserCard',
needs: { user: User },
actions: { rename: schema({ name: string() }) },
on: { rename: async ({ payload, state, adapters }) =>
adapters.user.methods.update(state.user.id, { name: payload.name }) },
render: ({ user, dispatch }) =>
h('article', null,
h('h2', null, user.name),
h('button', { onClick: () => dispatch('rename', { name: 'New' }) }, 'rename')),
});
const app = wire({
components: { UserCard },
adapters: { user: synthetic(User) }, // no backend needed
mode: 'synthetic',
});
const wf = workflow('rename-flow', [
mount('UserCard', { user: { adapter: 'user', method: 'get', args: ['u-1'] } }),
action('rename', { name: 'Ada' }),
expect(({ state }) => state.user.name === 'Ada'),
]);
await verify(wf, { user: { seed: 7 } }, { app });
// → { pass: true, reproduce: "node bin/verify.mjs ... --tokens '...'" }Most JS frameworks were designed when humans were the only authors. Agents have different ergonomics:
- They benefit from small, regular surfaces that fit in context.
- They are good at enumerating scenarios and bad at constructing fixtures. So scenarios should be tokens, not code.
- They need to verify without backend access. Schemas + synthetic adapters give that for free.
- They benefit from a zero-build, zero-dep loop where what they read is what runs and dependencies aren't a supply-chain hazard.
LeanJS is one shape that takes those constraints seriously.
SPEC.md the protocol (normative)
AGENTS.md how an agent should write apps in it
AGENT_LOOP.md the iteration pattern for going from in-progress → ready
lean.js single-file reference implementation, zero-dep
bin/verify.mjs CLI: workflows + conformance (no browser, fast)
bin/snapshot.mjs CLI: render workflows as HTML for human inspection
tools/ dev-time verification (uses system Chrome via CDP, no npm)
chrome.mjs locate Chrome/Chromium/Edge/Brave binary
cdp.mjs minimal Chrome DevTools Protocol client
dom-verify.mjs CLI: run workflows in a real headless Chrome
examples/ counter, user-profile, todo-list, triage, github-indexer
conformance/ cases any implementation must pass
examples/github-indexer/ is a realistic frontend app built on the
protocol: a read-only GitHub public repo indexer with three views (new,
popular, search), real fetch-backed adapter, URL routing, loading and
error states. Tests run against a synthetic adapter (zero network); the
browser runs the same component against the real GitHub API:
python3 -m http.server 8000
# open http://localhost:8000/examples/github-indexer/index.html
The bootstrap HTML is the membrane — the only file in the example
that touches window, history, or location. The component is pure.
This pattern is how LeanJS deliberately avoids useEffect-style escape
hatches: browser-event subscriptions (popstate, polling, online/offline,
resize) live in the bootstrap and call app.dispatch(inst, ...) to
drive the framework.
v0.2 — render context gains loading and errors; adapter exceptions
during prop resolution surface via errors rather than halting the
workflow (see SPEC.md § 10.1). The protocol and reference impl are
complete enough that every example workflow, conformance case, and
real-browser DOM test passes. The repo started in 2014 as a data-bind
template library; that codebase has been retired (see git history) in
favor of this schema-first, agent-authorable redesign.