nrepl-test was CI's only failure: the server subprocess ran main.janet from source, paying the full compile-mode init, which outran the 5s connect poll on slow runners (locally it always won the race). The test now prefers build/jolt — its ctx is baked at build time, so it accepts in ~20ms and CI builds it anyway — falls back to source, polls up to 60s under a 90s watchdog, and dumps the server's stderr when startup fails so the next CI failure is diagnosable. CLAUDE.md's placeholder sections become real: build/test commands with the run-the-gate-with-a-real-exit-code protocol (a piped gate once shipped masked spec failures), the seed/overlay/tier architecture sketch, and the porting gotchas that have each bitten at least once (leaf verification, stub-breaks- self-recursion, tier macro ordering, ref-get vs get on attached-ops wrappers, :jolt/type map keys, expander-called fns, canonical-port policy) — previously only in local bd memories.
5.7 KiB
Project Instructions for AI Agents
This file provides instructions and context for AI coding agents working on this project.
Beads Issue Tracker
This project uses bd (beads) for issue tracking. Run bd prime to see full workflow context and commands.
Quick Reference
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
Rules
- Use
bdfor ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run
bd primefor detailed command reference and session close protocol - Use
bd rememberfor persistent knowledge — do NOT use MEMORY.md files
Architecture in one line: issues live in a local Dolt DB; sync uses refs/dolt/data on your git remote; .beads/issues.jsonl is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
Session Completion
When ending a work session, you MUST complete ALL steps below. Work is NOT complete until git push succeeds.
MANDATORY WORKFLOW:
- File issues for remaining work - Create issues for anything that needs follow-up
- Run quality gates (if code changed) - Tests, linters, builds
- Update issue status - Close finished work, update in-progress items
- PUSH TO REMOTE - This is MANDATORY:
git pull --rebase git push git status # MUST show "up to date with origin" - Clean up - Clear stashes, prune remote branches
- Verify - All changes committed AND pushed
- Hand off - Provide context for next session
CRITICAL RULES:
- Work is NOT complete until
git pushsucceeds - NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
Build & Test
jpm build # build/jolt + build/jolt-deps (ctx baked at build time)
jpm test # FULL gate — recursive over test/ (spec, unit, integration, bench)
janet test/spec/<f>.janet # one spec file
janet test/integration/conformance-test.janet # 3-mode conformance (interpret/compile/self-host)
janet test/bench/core-bench.janet # bench — compare back-to-back vs main, never absolute
Run the gate with a REAL exit code. jpm test | grep ... reports grep's
exit, not jpm's — this once shipped masked spec failures. Correct form:
jpm test > /tmp/gate.out 2>&1; echo "EXIT: $?"
grep -E "non-zero exit|All tests" /tmp/gate.out
The literal All tests passed. line must be present. CI (.github/workflows/
tests.yml) runs the same gate on every push/PR.
jpm build output goes STALE silently — rm -rf build && jpm clean before
trusting the binary, or test from source (authoritative).
Architecture Overview
Clojure on Janet. A shrinking Janet seed (src/jolt/*.janet: reader, value
layer, vars/ns, evaluator, the self-hosted pipeline's back end) hosts a
Clojure overlay (jolt-core/): the analyzer/IR (jolt-core/jolt/) and
clojure.core in dependency-ordered tiers (jolt-core/clojure/core/NN-*.clj,
loaded in order: 00-syntax, 00-kernel (bootstrap-compiled), 10-seq, 20-coll,
25-sorted, 30-macros, 40-lazy, 50-io). Compile is the default path (analyzer
-> IR -> Janet bytecode, hybrid with interpreter fallback); JOLT_INTERPRET=1
forces the tree-walking interpreter, JOLT_INTERPRET_MACROS=1 additionally
keeps macro expanders interpreted (the pure oracle). api/init-cached serves
a disk-cached ctx image (~5ms vs ~2.4s); the cache key fingerprints sources +
env knobs — add any NEW ctx-shaping env var to image-cache-path in
api.janet or tests will see stale language behavior.
Issue tracking and design notes live in beads (bd prime, bd memories).
Conventions & Patterns
Porting seed fns to the overlay (the jolt-tzo shrink ladder) — traps that have each bitten at least once:
- Verify leaf-ness first: grep ALL
src/jolt/*.janetfor thecore-Xname (defn + core-bindings entry only), and check that tiers loading EARLIER than the target tier don't call it. Nothing the analyzer/ir use may move below the kernel tier. - Delete the seed defn + binding in the same change. A leftover stub breaks direct-linked self-recursion: the overlay fn's recursive call binds to the STUB's root at compile time (line-seq once truncated after one element this way).
- A tier may only use macros from tiers that load before it. Compile mode expands macros at tier LOAD; the interpreter expands lazily — so an if-let (30-macros) inside a 20-coll fn passes every interpreted test and breaks compiled init.
- Never read your own wrapper's fields with
getin attached-ops values (sorted colls):geton the wrapper IS the dispatched lookup and recurses forever. Usejolt.host/ref-get. - Map literals with
:jolt/typeas a key parse as tagged reader forms — don't tag overlay value maps in source. - Expander-called fns live in 00-syntax (empty?/keys/vals): expansion first happens during the kernel-tier compile, before later tiers exist. Early defns and expanders are interpreted during init and recompiled by the staged passes (recompile-defns!/recompile-macros!) once the analyzer is alive.
- Fix latent bugs to match Clojure rather than preserving them, with a regression spec row. Canonical Clojure definitions are preferred verbatim.
- Gate every batch: conformance x3 modes, suite >= baseline (clojure-test-suite-test.janet — raise the baseline when it rises), full jpm test with a real exit code, bench back-to-back vs main.