This file provides guidance to AI agents when working with code in this repository.
This repo is Amber Smalltalk — an implementation of the Smalltalk language that runs
on top of the JS runtime. It is a public open-source project (MIT), primary remote at
lolg.it/amber/amber, released to npm as @ambers/lang (with @ambers/sdk and
@ambers/cli on their own release tracks). Companion repos (Helios IDE, contrib
libraries) are checked out as sibling directories and npm-linked in — see
CONTRIBUTING.md for the full clone setup. CLAUDE.md imports this file in full, so
every session has all of it.
The file holds three kinds of text, in this order: house rules — how an agent works here, and how code, tests and history are written; conventions of this code — what the next change must look like; and a map of what the repository is. The last section says where a change to each kind goes.
lang/base/ — the hand-written JS kernel (boot, brikz DI, fundamentals / language /
runtime layers, parser.js generated from parser.pegjs)lang/src/ — the Smalltalk image: .st sources and their compiled .js
counterparts, both committedcli/ — the amber CLI tool (own release track; cli/dist/amber-cli.js is a build
product)sdk/ — development kit: grunt tasks, amberc compiler driver, test runnergrunt-init-project/ — project scaffolding templateindex.html, Gruntfile.js, config.js — development files, not part of the release;
config.js is generated (grunt devel), never edited by handANALYSIS-2026-08.md — measured architecture analysis (boot cost anatomy, sail
interpreter evaluation, modernization roadmap); read it before structural workANALYSIS-2026-09-honest-checkpoint.md — how the stated environment floor was made
true (ES2020, Node 12), what each gate needed, and what the checkpoint left for laterModes. A session is in one of two modes, named by a code the user gives. Without a code it is CAGT.
Scope. The requested change is the deliverable, neither narrowed nor widened. Code in scope gets no well-intentioned fix and no unsolicited improvement, however small: the codebase is human-reviewed, and the reviewer must not have to separate the change from the tidying. What is noticed on the way is surfaced after the work, separately, in chat. One fix to earlier code is licensed unasked: the preparation the change needs (Preparatory refactoring).
Ask, do not decide, when:
Working on X commit).Everywhere else, make the routine call, say so in the commit message, and go on. The answer to a question asked is written down here, so the next occurrence need not ask.
Report matter-of-factly. Prepare, check, commit, say what happened in a line or two. Verification results belong in the reply; ceremony, repeated hedging and apology do not. A mistake is corrected and reported once.
Git. All local git work is the agent's, none of the remote: never push, pull or fetch —
publishing to lolg.it is the user's, and what is there is final (Commits and history).
The working tree is shared with the user and with other sessions, whose staged and
unstaged work may sit beside yours at any time:
git add -A or git add ., and commit with the paths named
(git commit <paths>) when anything else is staged; re-read git status --short and
git log --oneline -3 immediately before committing, and git show --stat straight
after, saying plainly if anything foreign got in;git add, never spliced in as
hashed blobs; an edit keeps the file's line endings.Before committing, the diff is held against this list:
grunt jshint is clean for the hand-written JS, and
npm test passes;Recompile.
commit (Generated artifacts); an API created, deleted or moved is in
lang/API-CHANGES.txt under the version being worked on, in the same commit;git show --numstat with
git show -w --numstat, and explain or drop every whitespace-only hunk;Co-Authored-By: trailer.The rules are XP's: the simplest thing that could possibly work; nothing speculative or "nice to have later"; nothing unused left behind; no premature abstraction. Good naming and human readability are first-class concerns, and code generated by AI meets the same bar as hand-written code.
Code reads as intent, not implementation — it is written for humans, and only
incidentally for the machine. Every expression states the claim its author is making, not
the mechanics that happen to compute it. In the kernel JS, a guard asking "is there a
function yet?" is method.fn == null (the codebase's deliberate eqnull idiom, covering
both null and undefined), not !method.fn — the coercion is truthy in exactly the same
cases, and says nothing. In Smalltalk, an emptiness claim is aCollection isEmpty, not
aCollection size = 0.
Two languages live here, each with its own idiom, and neither is to be dragged toward the other:
.st) follows classic Smalltalk style — small methods,
intention-revealing selectors, the collection protocol (collect:, select:,
inject:into:) instead of index loops. Kent Beck's Smalltalk Best Practice Patterns
is the house reference, literally.lang/base) is deliberately conservative ES5 syntax (strict mode,
var, function) over an asserted ES6 runtime (kernel-checks.js). Match it; do not
modernize syntax piecemeal — a syntax-level raise is its own concern, decided
deliberately, not smuggled in with a fix.No comment on a part. A method or a statement carries no comment; a name that seems to
need one gets a better name, and in Smalltalk a method comment says what the method is for,
not how it does it. The reason for a thing lives in the commit message, in CHANGELOG, or
in the analysis documents — those are where a reader is meant to reach.
Reuse the established way. Where the codebase already has a way to do something, the
next occurrence uses that way exactly — not a near-twin that reads a little better in
isolation. The kernel's eqnull guard is how absence is spelled in JS here, and stays so
in every new guard; collect: and select: are how the .st sources iterate. This is the
same discipline as not dragging one language toward the other, applied within a language.
A spelling is held against the written rules before it is reused, never against taste:
A near-twin costs more than the duplication it looks like: a reader must diff the two to learn that the difference means nothing, and the extraction that would collapse them stops being obvious. Consistency is not tidiness; it is what keeps a pattern refactorable.
Preparatory refactoring. When a requested change lands in code whose current shape would
make the diff messy, follow Kent Beck: make the change easy (warning: this may be hard), then
make the easy change. The preparation is its own commit ahead of the change, its message
saying plainly what it puts right or clears out of the way, and it prepares no more ground
than the change needs. Its unit is a pattern, never a site: a deviant site is conformed to
the established shape first, and a pattern is changed at every occurrence or not at all;
reshaping it at the one site the change lands in is forbidden, however much cleaner the
diff would be. Over identical occurrences the preparation is trivial and needs no ask; over
nearly identical ones it is always an ask, because making them identical first is the
decision. A pattern's extent is its language: what the .st sources settle says nothing
about the kernel, and the other way round.
Names. Selectors and functions are named for what they claim, and siblings are named symmetrically, each for what distinguishes it: when a second variant joins a first whose names were general only because it was alone, the first is renamed to what it specifically is everywhere it is spoken, in a preparation commit ahead of the feature, and a name that was specific and now serves both becomes general in the feature commit.
Shape. A value computed once over a variable reassigned; symmetry over ifs, flags and
counters. Never "declare an empty collection, fill it in a loop, then use it": a
collection-style transformer — .map()/.filter()/.reduce() in JS,
collect:/select:/inject:into: in Smalltalk — reads as a data pipeline and leaves no
mutable intermediate to misuse.
Tests are SUnit: test... methods on *Test classes in the *-Tests packages
(Kernel-Tests, Compiler-Tests, SUnit-Tests, Platform-DOM-Tests). Method names read
as claims — testInvalidAssignment, testDyadicValue — one behaviour per method. A
feature or fix lands together with the unit test that pins it (CONTRIBUTING.md says so
too; reviews go a lot faster with tests present).
Held to a stricter standard than the code they test, not a looser one. A defect in production is caught by a test; a defect in a test is caught by nobody, and quietly licenses the defect beneath it. "It is only a test" inverts the priority exactly.
Written from the intent, never from the implementation. Test-first is best, because then there is no implementation to consult. A test arriving over code already written imitates that condition: recover the intent — from what the callers do with the result and what a different result would cost them, from the selector's promise, from the commit that introduced it, from what Pharo, the reference implementation, does — state the test from it, and only then read the code to see whether the two agree. A test taken from the code agrees with whatever is there, including whatever is wrong, and breaks when the answer is reworded rather than changed. Where the intent cannot be recovered, the test states what is defensible and the rest is written down rather than asserted.
A test pins what the code promises, caught at the boundary — inputs, outputs, emitted messages, calls leaving the system. What can be read there but was never promised is implementation, and is not asserted: a collection's order unless something orders it, a value that happens to hold today. An assertion states its claim, as any expression does; before one is copied from a neighbouring test, say what it claims. A test that breaks under a refactoring that preserves behaviour is a defect, and is reworked across its pattern before another test copies it.
Three reasons to exist, in Kent Beck's order: a test drives the design, documents the contract, catches a regression. A test written late cannot drive, so the first reason becomes an audit: a contract that is hard to state is a design that is hard to use, and the difficulty surfaces while the test is being written. What the audit finds goes to a suggestion afterwards, never into a silent workaround inside the test, which pins the awkwardness in place as though it were the contract. Where documenting the contract and catching a regression pull apart, the documentation wins: a behaviour true today but never decided is written down, not asserted, because a reader takes every assertion for a promise somebody made.
Read where it stands. The three parts — what is arranged, the one thing done, what is then true — are separated by a blank line and by nothing else, and none of them hides inside a method named for something else. What every case of a kind has in common may go to a helper on the test class; readability outranks factoring, and a helper that exists only to be shared by two other helpers is inlined into both.
History here is curated literature: an authored artifact, written for future readers
with the same care as the code, reading as if development had happened in the ideal order —
even though it didn't. The final tree is the truth that matters; the sequence of commits is
exposition, and exposition is owed to the reader (the argument against the squash school and
the record school is at blog.herby.sk/post/commits — a school articulated back in Amber's
own active days, though this repo's past was not written under it and is not being
rewritten to pretend it was). Two values govern every decision: minimal cognitive load for
a reader replaying history, and maximum git blame utility — blame on any line lands on
the commit that meaningfully introduced it, with a message explaining why it exists.
CONTRIBUTING.md's "split your fix into small Git commits" is this rule in older
words.) Cosmetics — a stray blank line removed, a ragged paragraph reflowed, untouched
code reindented — never ride in a content commit; wanted at all, they are a Cosmetic.
commit of their own. Text the change displaces is the change's, but only as far as the
displacement reaches. Regenerated output is its own Recompile. commit (Generated
artifacts).master is untouchable. Amber has external forks, contributors, and a long past
that was never written under this school; the moment a commit is on lolg.it master,
it is published text, final — never rebased, never rewritten. All curation happens
before the push. The past reads as it was written; only the text from here on is
authored under these rules.Fix
SUnit running., Correct #{} as JavaScriptSource. Fix #1262.). No marker and no
ceremony: it is a complete concern like any other, and mentioning the pattern or the
commit it corrects is a courtesy to the reader, not a notation to be satisfied. What it
must not be is noise — a bare fix typo at the tip earns nothing and costs a line of
blame; while the work is still a draft, fold such a thing into the commit it belongs to
instead.The notation of the literature — Amber's long-standing habits, plus the school's marks:
Attach context to a javascript exception., Fix SUnit running., Cosmetic.lang/ work; component work is scoped: cli:, sdk: / @ambers/sdk:,
CI:; a document by its file name: AGENTS.md:, CHANGELOG:Recompile. — a commit containing only regenerated artifacts (Generated artifacts);
Recompile (while version set to X) during the release ritualFixes #1260.CHANGELOG for X. (or just
Release date.) → Release version X → Recompile (while version set to X) → merge
tag to stable → Working on X+1 (driven by release.sh / setversion.sh; steps in
RELEASING.md)Working on X also opens a breaking change: before the first commit that breaks
something 0.x still carried, a commit of that shape bumps the worked-on version to the
next minor (lang/package.json, Kernel-Infrastructure.st version, its recompiled
.js), so that API-CHANGES.txt entries land under the version that ships them> — body contains an important standalone addendum... — subject is a sentence fragment; body opens with ... completing itCo-Authored-By: <model name> <noreply@anthropic.com> — required trailer on every
AI-assisted commit, naming the acting model (e.g. Claude Fable 5) at its maker's
domain| artifact | source | regenerate with |
|---|---|---|
lang/src/*.js |
lang/src/*.st |
grunt amberc:lang / amberc:tests |
lang/base/parser.js |
lang/base/parser.pegjs |
cd lang && npm run build:parser |
config.js |
package metadata | grunt devel |
cli/dist/amber-cli.js |
cli/src + lang |
npm run build:cli — at release only |
Never hand-edit a generated file. The .st file is the source of truth for lang/src;
change it (directly or through the IDE, which writes both), then recompile. Regenerated
output goes into its own dedicated commit — the long-standing Recompile. convention — so
semantic diffs stay readable and blame on .st lines lands on meaning, not on compiler
churn. The CLI bundle is the exception: it is the published artifact, rebuilt only by
the release ritual's Recompile (while version set to X), and deliberately stale in
between — do not commit a rebuild of it during development (building it to verify is
fine; discard the result). If you created / deleted / moved API, update
lang/API-CHANGES.txt under the version being worked on, in the same commit as the
change.
Setup (npm installs in root, lang/, sdk/; sibling clones for Helios etc.) is in
CONTRIBUTING.md. Day-to-day:
grunt # full build: parser (pegjs) + amberc:lang + tests build + cli + dev
grunt amberc:lang # recompile lang/src .st sources to .js
grunt amberc:tests # recompile the *-Tests packages
grunt devel # regenerate config.js (amdconfig)
grunt test # build test_runner.js and run the SUnit suite (npm run test:sunit)
npm test # test:sunit + cli e2e tests
cd lang && npm run build:parser # regenerate lang/base/parser.js from parser.pegjs
./local-amberc <args> # amberc against this checkout's lang/ instead of the installed one
grunt jshint # lint the hand-written JS: kernel, sdk, amberc-cli, Gruntfile, scaffold template
Node: 12 or newer, as every engines field says — the ES2020 floor that
lang/base/kernel-checks.js probes for and names on failure. Development happens on
current LTS; the build toolchain (grunt) needs a newer Node than Amber itself, so the
floor is verified by running the built test runner, not by building on it.
The short map (the long, measured one is ANALYSIS-2026-08.md):
lang/base/boot.js wires three brikz layers — fundamentals (selectors,
packages, method model) → language (classes, metaclasses, traits) → runtime (method
installation, DNU, contexts), the last loaded lazily at initialize(). brikz.js is
the micro-DI; cross-layer coupling goes through the emit event table.deploy.js (11 kernel packages) ⊂ devel.js (+ SUnit, tests, DOM) ⊂
lang.js (+ the Compiler-* reflection chain). index.html shows the intended
progressive-load choreography.$core.addPackage / addClass / addMethod
with method factories (instantiateFn); //>>excludeStart("ide"/"ctx"/"imports")
pragmas mark strippable metadata, consumed by the r.js optimizer at bundle time.$recv/asReceiver wraps foreign
values.lang/base/kernel-interpreter.js is an unwired experimental sketch (the "sail"
interpreter) — see the analysis document before touching it..st/.js dual-source model dates here.globalThis migration, deprecation removals, targeted fixes with tests.Treat old wiki links and the social-media steps in RELEASING.md as period artifacts;
verify before relying on them.
Three Drone pipelines in .drone.yml on the self-hosted ci.lolg.it (alive; probe it
from this machine — tool egress cannot reach it), each gated by a changed-paths check:
lang (npm installs + test:sunit on node:lts, then the built test runner executed
on node:12, the declared floor), built-cli (build cli/dist + e2e), and
installable-cli (install @ambers/cli from npm + e2e against this checkout's lang).
Releases follow RELEASING.md: green tests → CHANGELOG / API-CHANGES.txt current →
./release.sh (asks release + next version) → merge the tag into stable. The CHANGELOG
entry for the upcoming release is written as the work lands, under a ?? Mon YYYY -
Release X header (as 017e724d did for 0.30.0); the ritual fills in the date, or writes
the whole entry if nothing did (CHANGELOG for X.).
Three kinds of text, and three homes for a change to it:
AGENTS.md not added. A change that falsifies many gets an AGENTS.md: commit of its
own.AGENTS.md: commit when it is settled, the argument
in the body.AGENTS.md: commit. A rule given in conversation — a
correction, a confirmed way of doing something — is written here in the same session, so
that the next agent, and the next person driving one, has it without being told. It enters
in the shape of its section: one sentence that states the rule, a clause at most for why,
folded into the sentence that already covers the case when there is one — never a
paragraph appended where the day's argument happened to end. What jshint or a test can
carry goes there instead.