Login
Timeline
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

50 most recent check-ins

2026-11-10
20:55
README: open with what crystal-robots is for, as the program's Big Picture states it

one-place asked (crystal-robots forum post 5452a8352f) for README.md to open with the purpose the maintainer confirmed on the program's Big Picture page, in the shape BQ2's and Glint's READMEs now use: what it is and why it's here, what it's made of, how it's shaped, what success looks like (each item with its status, linked), and the four virtues. Written to OCX's docs/writing.md: each term defined where it is first used, with no glossary.

Checked against the code: the multipass parser and -t, the checker, the interpreter and WebAssembly back ends with their shared cycle costs, native builds of the examples in ci.cr, the wasmer specs that compare the two back ends, the site's workbench and front page, and the empty riscv_emitter.cr and dot_emitter.cr, which is why that milestone says not yet. Every site link answers 200.

Cold read: two fresh agent sessions with no context explained it back. The first was unsure whether robots run two ways or three, guessed at about fifteen terms, and found the author's motive sentence too abstract; the second's guesses (OCX, workbench used twice, ROBOT, prelude, dot, the story) and its hardest sentence (the parser's passes) are fixed too. The sections after the opening are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KKGmgTtUcBKzLx1WD7 Leaf check-in: 3b18ffade0 user: x86-1-crystal-robots tags: dev, main, trunk

2026-11-09
10:59
Tournament share link: no localhost when the server configures no base URL

one-place asked (crystal-robots forum post 5d88ca5247) to move what isn't blocked: under OCX's app page the tournament's share link read http://localhost/..., since OCX passes no FOSSIL_URL and the Host header is deliberately not trusted.

- src/web/cgi.cr: request_origin is nil with no FOSSIL_URL. The link page then shows the link alone, with its full path as text (/crystal-robots/ext/ocx2-cgi/app/crystal-robots/tournament?...). The browser makes it absolute from the address it is really on, so copying the link works. With FOSSIL_URL it is unchanged: the absolute URL as text and as link. - spec: the hostile-Host spec and the preview-path spec now expect the path link and no localhost; a new spec covers the app page.

Checked: crystal run ci.cr all green; format check clean. Not checked yet: the live page, which needs a new stage run, import and accept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KKGmgTtUcBKzLx1WD7 Leaf check-in: 66bc8fd2b2 user: x86-1-crystal-robots tags: ocx-app

2026-11-06
06:31
Serve crystal-robots as an OCX app, and build it for OCX's page deploy

The maintainer chose crystal-robots as the next app for OCX's page deploy (request: crystal-robots forum post c1af3faa97; requirements: ocx-console post 5ffe05c34b). OCX's page runs the accepted release at /crystal-robots/ext/ocx2-cgi/app/crystal-robots with only the request's fields: FOSSIL_URI and PATH_INFO, no SCRIPT_NAME, GATEWAY_INTERFACE or PATH.

- src/cli.cr: the binary is the web app when FOSSIL_REPOSITORY or REQUEST_METHOD is set, not only GATEWAY_INTERFACE. With an empty environment it is the command line, which now says "usage: crystal-robots", as OCX's import smoke looks for. - src/web/cgi.cr: under OCX's app page, links, form actions and redirects come from FOSSIL_URI and the path after /app/NAME (with or without /ext/ocx2-cgi before it). Fossil's sign-in g= is relative to the repository. fossil is found as ocx-console finds it (the nearest fossil ancestor), since there is no PATH. The 403 page's Log in link now carries the repository's address, which raw HTML needs (a bug under Fossil's /ext too). Fossil's own /ext serves as before. - ci.cr: "-- programs" and "-- build OUT ARCH", as ocx-console's: the whole gate, then the static crystal-robots (API docs embedded), smoked with an empty environment, with gate.txt and release.txt. - tools/browser-rootfs: copied from ocx-console (MIT, check-in 170ebb1105), for make-static, the Alpine static libraries the build links. - patterns/stage.md: the stage pattern, ocx-console's for crystal-robots.

Checked: crystal run ci.cr -- build OUT x86_64 is green (209 examples, 0 failures, 35 pending: node, wasmer and browser) and writes a 4 MB static crystal-robots that prints its usage under env -i. Run as OCX runs an app (env -i, only OCX's request fields, cwd the release directory, against a real clone), signed out and signed in: overview, version, battle, tournament, wiki-robots.json (fossil sql with no PATH) and the API docs all answer, every link under /ext/ocx2-cgi/app/crystal-robots, and sign-in links carry g= relative to the repository.

Known: a tournament's share link still falls back to http://localhost, since OCX passes no FOSSIL_URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KKGmgTtUcBKzLx1WD7 check-in: 29fc8f597a user: x86-1-crystal-robots tags: ocx-app

06:14
Create new branch named "ocx-app" check-in: ed6fef2447 user: x86-1-crystal-robots tags: ocx-app
2026-11-03
03:37
Merge blog-diffs: diff views for all 61 blog commits, pointed at the public main line

The maintainer confirmed that Pages publishes from dev, which is where trunk lands, so the blog's diff views go to trunk now.

This brings in the generator (scripts/build_blog_diffs.cr), the viewer (site/blog/diff-view.js), the Reading Crystal and Reading JavaScript primers, the glossary, command notes, and one diff view per commit section. Every commit link, git checkout command and diff view names a commit on GitHub main, the rebuilt public line ending at c0ecb30. scripts/resolve_blog_links.cr now also re-points checkout commands and plain commit links.

Checked: crystal run ci.cr is all green, and crystal tool format --check is clean. Not run on this machine, which has no node or browser: blog_diffs_check.mjs and ci.cr --with-wasm32. They last passed at 62d76d1c43. Since then the viewer's code and styles are unchanged; only the views' data, the page's links and notes.yml changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KKGmgTtUcBKzLx1WD7 check-in: 048d0ace53 user: x86-1-crystal-robots tags: dev, main, trunk

01:06
Blog: point every commit link and diff view at the public main line

GitHub main is now the rebuilt public line (tip c0ecb30, Nov 3), so the blog no longer needs the source map. - scripts/resolve_blog_links.cr against origin/main re-pointed the 37 commits after B.8. All 56 git checkout commands and 61 commit links now name commits on main. - Every diff view was regenerated from origin/main with no --map, and site/blog/diffs/sources.map is deleted. - U.3's second excerpt is now spec/robots_spec.cr, the spec the public U.3 adds, in place of the wiki publishing spec, which isn't on main. - The other JSON changes reflect the public line: commit hashes, and line counts where the web app and badge are held back until the appendix.

Checked: crystal run ci.cr green (203 examples, 0 failures, 35 pending). Not run here: the headless browser check of the views, since this machine has no node or browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KLe3do6AYo9XfMcJE2 Leaf check-in: fd6308a5f9 user: x86-1-crystal-robots tags: blog-diffs

2026-11-02
05:18
resolve_blog_links: also re-point checkout commands and plain commit links

The script only updated links with data-subject, so after a rebuilt main line the blog's git checkout commands and See the whole change links would name commits that do not exist. It now carries each renamed commit through the rest of the page.

Checked against the rebuilt main line, on trunk's and this branch's index.html: all 56 checkout commands and 61 commit links resolve; format check clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KLe3do6AYo9XfMcJE2 check-in: 0547dc2b5d user: x86-1-crystal-robots tags: blog-diffs

2026-10-31
12:01
Blog diff views for chapters 6-9 (W, catch-up, browser battles, front door, compiled robots: 24 commits)

Codex task 0e62c438fc: notes and excerpts for W.0-W.6 and every later commit section, a Reading JavaScript part of the primer, glossary entries for the browser chapters, command notes for node, wasmer and opening a page, and summary-only support in the generator and viewer for the catch-up commit.

My changes on top: the ES-modules commit now leads with its actual change, the one-line site/package.json, not only the test comment; the manual commit shows the manual's first robot and its walk-through, not only the manual's spec; the catch-up note says why it has no excerpt and points to the intermission.

All 61 commit sections now have a diff view. Checked: crystal run ci.cr -- --with-wasm32, 204 examples, 0 failures, 18 pending (wasmer-gated); blog_diffs_check.mjs passes on all views at 360px; format check clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KNtdXfvRHqbY5mTH9b check-in: 62d76d1c43 user: crystal-robots-session tags: blog-diffs

11:41
Blog diff views for chapters 2-5 (Phase B, C, T, U: 21 commits)

Codex task 132c9f35ac: notes, excerpts and command notes for B.1-B.8, C.1-C.6, T.1-T.2 and U.1-U.6 to chapter 1's standard; glossary entries for cycle, battlefield physics, channel, tournament pool, bracket and CI; the generator takes mapped commits whose subject differs and diffs each against its first parent; the browser check covers every view on the page.

My changes on top: the generator's private-file filter was too broad. It hid all of src/web/ and spec/web/, which emptied U.4 of its subject, the robot API reference (src/web/robot_api.cr, kept on the public main line and the source of the site's cheat sheet). It now hides only the Fossil CGI files, and U.4 shows the reference table, its honesty test and the interpreter's builtin list. I checked the C.1, C.5, T.2, B.5 and T.1 notes against the code.

Checked: crystal run ci.cr -- --with-wasm32, 204 examples, 0 failures, 18 pending (wasmer-gated); blog_diffs_check.mjs passes on all 37 views; format check clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KNtdXfvRHqbY5mTH9b check-in: 6eb0c2f749 user: crystal-robots-session tags: blog-diffs

11:21
Blog diffs: source map from blog subjects to equivalent commits

Until the public main line is on GitHub, the 36 blog commits after B.8 are mapped to their equivalents on GitHub's tutorial and dev branches. The catch-up has no single equivalent and gets a summary only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KNtdXfvRHqbY5mTH9b check-in: d28ce17c64 user: crystal-robots-session tags: blog-diffs

2026-10-30
13:13
Blog diff views, second pass on chapter 1: real notes, better excerpts, phone layout

Codex task 9dcb82e442: notes.yml rewritten for first-time programmers (2-4 sentences per excerpt, line notes on the lines a beginner would stumble on, terms linked to the primer or a new 'Words you'll see' glossary); excerpts now show the code that carries each idea, picked by anchor and 'through' anchor; notes sit outside the code box and code wraps under 600px; single number column for new code; files list behind a disclosure without manifest.uuid/shard.lock; command notes explain only what's new in each Try it.

My changes on top: notes render `code` spans (they showed literal backticks); the file header and line notes no longer stop at the 72ch paragraph cap; A.11 says the compiler (not the runner) sets main aside; A.13 says the robot is compiled to WebAssembly and run, not run through the emitter.

Checked: crystal run ci.cr -- --with-wasm32, 204 examples, 0 failures, 18 pending (wasmer-gated); blog specs 3/0/0; blog_diffs_check.mjs passes; generator rerun against origin/rewrite-from-initial-commit; format check clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KNtdXfvRHqbY5mTH9b check-in: a2bc6b7e9a user: crystal-robots-session tags: blog-diffs

12:57
Blog diff views, first pass (chapter 1): generator, viewer, Reading Crystal primer, command notes

Codex task e68de3e356. scripts/build_blog_diffs.cr turns site/blog/diffs/notes.yml (hand-picked excerpts and notes, matched to commits by subject) into one JSON per commit section; site/blog/diff-view.js renders them lazily as one-column diffs. Adds a Reading Crystal primer and plain notes on each command in Following along and chapter 1.

On a branch, not trunk: the notes and excerpt choices need a second pass before the public site gets them.

Checked: crystal run ci.cr -- --with-wasm32, 204 examples, 0 failures, 18 pending (wasmer-gated); blog_diffs_check.mjs passes at 360px; format check clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUH8KNtdXfvRHqbY5mTH9b check-in: 4d95992877 user: crystal-robots-session tags: blog-diffs

2026-10-26
15:15
Blog: building crystal-robots, one commit at a time

site/blog/ is the long-form story of the project, in the style of Colin Eberhardt's chasm post. It walks the git main line from Initial commit through compiled robots fighting in the browser, one section per commit: why the change was made, what it does, a short code excerpt where it helps, and 'Try it' / 'What you'll see' with the commands and the real output recorded in that commit's message. There are nine chapters: the compiler to WebAssembly, the language real robots use, the battlefield, the tournament, the pieces in between, into the browser, an intermission where the tutorial line meets the shipping code, battles in the browser, the front door, and the robots starting to fight as WebAssembly. Bugs and dead ends are told as they happened. It links 61 commits.

Each commit link carries data-subject with the commit's exact subject. scripts/resolve_blog_links.cr re-points every link, and its short-hash label, at a branch's current SHAs if the main line is ever re-committed: crystal run scripts/resolve_blog_links.cr -- origin/main.

Every page's navigation gets a Blog link, and the landing page gets a sixth card, 'How it was built'.

Checked: all 61 links resolve against the main line; the resolver changes nothing when the SHAs already match, and repairs a deliberately corrupted link and label exactly; the HTML is well-formed and renders at 360 and 1280 in light and dark; crystal run ci.cr -- --with-wasm32 is all green, 201 examples, 0 failures, 18 pending (wasmer-gated); crystal tool format --check is clean. I checked the CROBOTS tagline against the original manual and fixed it to read 'a programming game, for programmers (or aspiring programmers.)'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: 332afb5929 user: run-3b21425d tags: dev, main, trunk

14:42
Pages: a favicon, an SVG icon and an iOS home-screen icon

The site had no icon, so tabs and bookmarks showed a blank page. site/favicon.svg is the source: a blue robot (ROBOT_COLORS #4C97FF, from svg_replay.cr) on the dark field color, with its turret aimed and an orange missile (#FF8C1A) in flight, drawn bold enough to read at 16 px. site/favicon.ico packs 16, 32 and 48 px PNG renders of it, made with headless Chromium on a transparent background, and site/apple-touch-icon.png is a 180 px square render (iOS rounds the corners itself). Because the site is served from /crystal-robots/, a browser's automatic /favicon.ico request would miss it, so every page links all three with relative paths.

Checked: the ICO holds three valid PNG images (file(1)); a static server returns all three with the right content types; an 8x preview reads clearly at 16 px on light and dark tab bars; crystal run ci.cr -- --with-wasm32 is all green, 201 examples, 0 failures, 18 pending.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: aa43fc190e user: run-3b21425d tags: dev, main, trunk

04:56
Plain language for first-time programmers, a CROBOTS-style manual, and fixes for the failing Pages deploy

The public site is for first-time programmers, so every page now leads with the game and explains the machinery last, using Tom Poindexter's CROBOTS manual as the model for tone and level. Jargon is gone from visitor-facing text, or explained once where it's the right name. Some examples: whole numbers, not integers; changeable, not mutable; small pieces, not glyphs or tokens; code map for the syntax tree.

The manual is restructured along CROBOTS lines. It covers what the game is, then the battlefield with light/dark inline SVG diagrams (field, directions, scanner wedge), then writing a first robot and more instructions, every command with plain explanations and examples (still rendered from cheat-sheet.json), tips and tactics drawn from the bundled robots, how it works inside, running it yourself, and references. spec/manual_spec.cr still checks every complete example.

Rough edges fixed: robot labels stay inside the field near walls (in battle-playback.js; svg_replay.cr's output is unchanged); the tournament shows its champion once and collapses repeated refights into one line per pairing, which can be expanded; the 360px puts console no longer cuts its first line; the workbench runs a robot against target in a real short battle instead of the all-zeros preview host, keeping the plain run for programs without a robot loop; page claims match compiled battles; README.md gets a 'Following along' section (main's commit history and the blog) and points public links at the github.io site.

Changes I made to Codex's version: - site/friendly-errors.js was a list of word-level regex swaps applied to any message ('frames' became 'replay pictures', 'module' became 'program'). That would garble messages it didn't know. It now translates each known reader and checker message whole, keeping the visitor's names: 'drive needs 2 numbers, but 1 was given'; 'The robot doesn't know the name rnge'; an unreadable 'Cannot reduce ...' becomes advice to look for a missing end, a parenthesis or a typo. Anything unrecognised is shown unchanged. - The engine labels 'fast browser version'/'basic version' hid the point of the site. They are now 'compiled to WebAssembly' and 'step-by-step mode', and 'turns of work' is now 'turns'.

CI fixes: - spec/wasm_view_spec.cr ran node unconditionally and crashed on runners without Node (reported by the maintainer). It now reports pending, as the other node specs do. spec/editor_gutter_spec.cr now also requires node, not just a browser. With node hidden from PATH, crystal spec gives 200 examples, 0 failures, 0 errors, 34 pending. - The GitHub Pages build for 15e97eec91 failed in spec/landing_spec.cr after its check had passed: removing Chrome's temp profile hit ENOTEMPTY while Chrome's helper processes were still writing. Both headless checks now retry the removal longer and log a leftover temp dir instead of failing.

Code by Codex (task 8c159cbaf2), apart from the changes above. Verified here: crystal run ci.cr -- --with-wasm32, 201 examples, 0 failures, 18 pending (wasmer-gated); crystal run ci.cr -- --with-wasmer, 200 examples, 0 failures, 13 pending (wasm32-gated); crystal tool format --check clean. I reviewed the manual, tournament, challenge and workbench screenshots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: 6eff61fadb user: run-3b21425d tags: dev, main, trunk

04:27
docs/TUTORIAL.md: fix two mistyped check-in hashes

C.5 is d70d3ea82a (was d70d3ea832) and W.4 is 4b179e19b7 (was 4b179e1971). Both checked against the repository with fossil info. Found while replaying this line onto the public git main line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV Leaf check-in: b7df81b6d8 user: run-3b21425d tags: tutorial

04:22
Compiled robots fight on the public site (slice R2)

Every battle, tournament fight and challenge check on jadonk.github.io/crystal-robots now runs each robot as its own resumable WebAssembly module, compiled in the browser by crystal-robots.wasm, instead of through the interpreter. The headline 'Robots you program, compiled to WebAssembly, fighting in your browser' is now literally true.

src/browser.cr adds crd_resumable, which parses, checks and emits resumable WASM. A source the resumable emitter can't handle comes back as data (WASM_Emitter.resumable_problem), so nothing raises on wasm32. In compiled mode, battle_run and tournament_run give each robot an external stepper that calls the crd_host import module (robot_new, robot_step, robot_error). JS keeps one WebAssembly.Instance per battlefield slot, and its env.* imports call the new crd_host_* exports (scan, cannon, drive, damage, speed, loc_x, loc_y, sleep, rand, puts, sqrt, sin, cos, tan, atan), each going to that slot's RobotHost. A robot that can't be compiled falls back to the interpreter on its own. Reports name each robot's engine, and ?engine=interpreter gives the old path for comparison.

site/battle-worker.js and site/battle-client.js run every battle in a module Worker, where robot modules are compiled ahead of time and instantiated synchronously from their Module when the field asks. The landing page, editor Fight, workbench, challenge checks and tournament all use it. The legend shows 'running as WebAssembly' or 'running in the interpreter' per robot. Landing layer 4 now decodes the resumable module, which is what fights (wasm-view.js handles memory, loads and stores, globals and br_table), and layer 5 names the engine.

spec/wasm32_compiled_spec.cr (node, the real shim and worker code paths): 60 seeded battles across all bundled robots in 2- and 4-robot fields, plus a full 8-robot tournament, give byte-for-byte identical reports and frame buffers compiled and interpreted. It also covers per-robot fallback and a timing budget. The headless landing, tournament and challenge checks run compiled and assert the WebAssembly marker.

Code by Codex (task c2c2a07389). I changed one thing. Codex linked crystal-robots.wasm with --allow-undefined, which would turn any unresolved symbol in the whole build into a silent import. LibCRDHost is now declared with @[Link(wasm_import_module: "crd_host")], ci.cr drops the flag, and the shim passes the host as crd_host. The module's imports are now exactly crd_host.robot_new/robot_step/robot_error plus the WASI set. Verified here: crystal spec -Dcrd_wasm32 spec/wasm32_compiled_spec.cr, 1 example, 0 failures, compiled 131 ms vs interpreted 203 ms for a 4-robot 60,000-cycle battle; crystal run ci.cr -- --with-wasm32, 201 examples, 0 failures, 18 pending (wasmer-gated); crystal run ci.cr -- --with-wasmer, 200 examples, 0 failures, 13 pending (wasm32-gated). I reviewed the 1280 light screenshot of the legend markers and layer 4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: 15e97eec91 user: run-3b21425d tags: dev, main, trunk

03:56
Compiled robots that pause at every cycle: a resumable WASM emitter (slice R1)

Plain WebAssembly cannot suspend in the middle of a function, so until now the emitter's env.tick(n) could only count cycles, and a compiled robot could not take turns on the battlefield. Battles ran robots through the stepwise interpreter instead. This makes the breakpoints real, with no browser-specific features (no JSPI, threads, SharedArrayBuffer or Asyncify): the output is plain WebAssembly 1.0 that wasmer and every browser run.

src/compiler/wasm_resumable.cr: WASM_Emitter's new resumable mode (bin/crystal-robots -c --resumable). The module exports step(), which runs exactly one charged cycle-unit, at the same points and in the same order as Battle::Robot's stepwise interpreter, then returns 0 (paused), 1 (main completed) or 2 (runtime error, named by the exported error_code). Nothing stays on the WASM operand stack across a pause. Each function becomes numbered blocks dispatched on a saved program counter, and every activation's program counter, locals and temporaries live in a 2,048-byte frame in the module's own linear memory. Seven pages hold MAX_CALL_DEPTH + 1 frames, and exceeding the depth is the interpreter's runtime error, not a trap. Globals and top-level constants survive main restarts; division and modulo by zero give 0. The plain emitter's output is byte-for-byte unchanged.

Battle::Robot accepts an external stepper. Robot#step! calls it and then applies the interpreter path's restart and error bookkeeping, so Field and run_stepwise are untouched.

spec/wasm_resumable_spec.cr (under -Dwasmer): every bundled robot (examples and robots/*.md, all 11 compile), 3 seeds each in 2- and 4-robot fields, fights identically compiled and interpreted, the same frames, cycles, restarts, puts output and winner. Focused cases: a pause before a builtin inside a nested call resumes correctly; state matches the interpreter after every charged unit; globals and constants persist across restarts; deep recursion becomes a runtime error; a loop with no builtin calls still pauses; division by zero. Resumable modules are about 10 to 15 times larger than plain ones (sniper: 1,174 to 16,792 bytes).

Code by Codex (task 71ac8d8fa3). The patch again deleted the versioned shard.lock, a side effect of Codex's environment, and I restored it. Verified here: the memory sizing above; crystal spec -Dwasmer spec/wasm_resumable_spec.cr, 8 examples, 0 failures in 15.4 s; crystal run ci.cr -- --with-wasmer, 200 examples, 0 failures (the 13 pending are wasm32-only); crystal run ci.cr -- --with-wasm32, 200 examples, 0 failures, 18 pending (wasmer-gated). The browser does not use this yet; that is slice R2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: e4d4ec3eac user: run-3b21425d tags: dev, main, trunk

03:13
Replays show what each robot prints with puts (slice 3b)

Before this, a robot's puts lines in a battle went into Robot#output, where only the private CGI showed them. The public site's battle report left them out, and they had no time stamps, so a replay could not have shown them at the right moment anyway.

Robot now keeps output_cycles alongside output, stamped from Field#current_cycle by both engines (the fiber run and run_stepwise). Robot#output's type and contents are unchanged, so the CGI is untouched. Browser.battle_run reports each robot's output as [{cycle, text}]. site/battle-playback.js adds a console under the arena: each robot's lines in its own color, showing only lines at or before the playback position, so pausing, scrubbing and speed changes stay in sync. A printed line also flashes briefly as a label beside the robot. The console stays hidden when nothing was printed. Every page using mountBattlePlayback gets it (landing, tournament, challenge, editor, workbench). The challenge's Look hint now suggests puts with a two-line example.

Specs: cycle stamps increase and match between the two engines; wasm32 battle parity covers the new field; the headless landing check scrubs a printing robot to the end (lines shown) and back to the start (none).

Code by Codex (task 11e2ee7cfd); reviewed the engine diff and the 360px dark screenshot. crystal run ci.cr -- --with-wasm32 is all green: 192 examples, 0 failures, 10 pending (wasmer-only).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: 3cf2846d87 user: run-3b21425d tags: dev, main, trunk

02:44
Pages: a tournament page and a seven-level 'bring your own robot' challenge (slice 3)

site/tournament/: pick 3 to 8 entrants from the bundled robots, plus 'Your robot' saved by the challenge or editor. Set a seed (?seed= and ?names= make the lineup shareable) and run the whole tournament in a module Web Worker through tournament(), so the page stays responsive. It shows pool fights and standings, the bracket as an HTML/CSS diagram (left to right on wide screens, stacked on narrow ones), the champion, and any rejected robots with their errors. Clicking any fight replays it with battle() using the recorded entrants and seed, and shows the replay's winner next to the recorded one.

site/challenge/: 'Can your robot win the tournament?' Seven levels: Move, Look, Hit, Win, Moving target (beat rabbit in 2 of 3), Fight back (beat counter in 2 of 3), and The tournament. Each level has a goal, a hint you can reveal, the code carried over from the last level, and a check that runs real battles in the browser (site/challenge/checks.js). The check says in one sentence why it passed or failed and shows the battle that decided it. Progress and the robot are kept in localStorage (site/robot-store.js; every access is guarded). The editing surface (gutter, live check, cheat sheet) is shared with the editor through site/editor/shared.js instead of being copied.

More opponents: scripts/build_robots_json.cr adds the five robots in robots/*.md (hunter, circler, dodger, wallhugger, turret) to site/robots.json, each with its origin and a one-line description. Navigation on every page now includes Challenge and Tournament, and the landing page and manual link to both. In dark mode site.css overrides the battlefield fill, stroke and labels. svg_replay.cr's output, which the private CGI shares, is unchanged.

Specs: the new robots.json entries; the challenge checks against real battle reports, with pass and fail cases for levels 1 to 4; a headless Chromium run of a 4-robot tournament at ?seed=1 that replays a fight and matches its recorded winner, then passes challenge level 1, with no console errors and no overflow at 360px.

Code by Codex (task 82b5569aa7). The patch also deleted the versioned shard.lock, a build side effect unrelated to this slice; I restored it. I reviewed Codex's screenshots of both pages at 1280 and 360, light and dark. Polish items for the next pass: label clipping at the field edge, a duplicated Champion heading, the noisy refight list, and jargon in check messages. crystal run ci.cr -- --with-wasm32 is all green: 190 examples, 0 failures, 10 pending (wasmer-only).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: bd48bef449 user: run-3b21425d tags: dev, main, trunk

02:29
Pages: a landing page that opens on a live battle and peels back to the WebAssembly bytes (slice 2)

The front door of jadonk.github.io/crystal-robots. site/index.html is now one headline and an arena: as soon as crystal-robots.wasm loads, sniper, rook, rabbit and counter fight, auto-playing through mountBattlePlayback. A legend shows each robot's live damage. Rematch draws a new seed, and ?seed=N makes any battle shareable and reproducible. With prefers-reduced-motion it waits for Play.

Below the arena, 'Peel back the layers' opens one robot as five layers: (1) its source, with an editor link (site/editor now accepts ?robot=<example>); (2) the multipass glyph derivation, stepped pass by pass down to the program glyph; (3) the syntax tree, from a new non-raising crd_ast export (parse errors come back as data); (4) the WebAssembly module crd_run emits, decoded by site/wasm-view.js into sections and instruction mnemonics beside their bytes; (5) what the engine did with it in this battle. The old compiler and battle form move unchanged to site/workbench/. battle-playback.js gains autoplay/onFrame/onEnd options, and ci.cr's format check now covers ci.cr itself.

Specs: crd_ast shape and parse-error-as-data (native) and parity (wasm32); wasm-view.js decodes a natively compiled counter.cr with no unknown opcodes; a headless Chromium check loads /?seed=1, opens all five layers, finds no console errors and no horizontal overflow at 360px.

Code by Codex (task 5877b4a4fb). I rewrote Browser.ast_node to stream through one JSON::Builder instead of serializing and re-parsing every subtree. I checked screenshots at 1280 and 360 wide, light and dark. In dark mode the arena keeps the light field from svg_replay.cr, which the CGI shares; a CSS override is queued for the polish pass. crystal run ci.cr -- --with-wasm32 is all green: 187 examples, 0 failures, 10 pending (wasmer-only).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: 079367188a user: run-3b21425d tags: dev, main, trunk

02:12
Browser: run a whole tournament inside crystal-robots.wasm (slice 1)

The public site needs a tournament with no server. src/browser.cr adds Browser.tournament_run and the crd_tournament_alloc/run/result_ptr/result_len exports, the same buffer pattern as crd_battle_*. A request is {robots: [{name, source}], seed, limit}. Every mistake a robot author can make comes back as data: 3 to 8 robots, unique non-empty names, and each source parsed with the non-raising try_parse and then checked. A robot that fails is listed under rejected with its error and left out. If fewer than 3 valid robots remain, the call returns an error instead of calling Tournament.run, which raises (and on wasm32 any raise traps the module). Fights run on Battle::Field#run_stepwise, the same engine and winner rule as battle_run. The report gives pools (fights with entrants, seed and winner, then standings), bracket rounds with best-of-3 games, the champion and fight_count. Each recorded seed replays that exact fight through crd_battle_run. site/wasi-shim.js gains tournament(), mirroring battle().

spec/browser_tournament_spec.cr: shape, champion, replaying a pool fight and a bracket game through battle_run gives the recorded winners, and the error paths (2 robots, a duplicate name, a parse error that leaves 2, a rejected robot left out of a valid field). spec/wasm32_spec.cr: the wasm32 build, driven through node and the real shim, returns a report identical to the native one.

Code by Codex (task a481746be4). I removed its 120-line hand-written JSON syntax validator and the matching spec. battle_run's documented convention is to trap only on a request that isn't JSON at all, which would be the page's own bug, since the page builds requests with JSON.stringify. Robot-level validation stays. crystal run ci.cr -- --with-wasm32 is all green: 182 examples, 0 failures, 10 pending (wasmer-only). crystal-robots.wasm is 1,076,221 bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: b4023896bf user: run-3b21425d tags: dev, main, trunk

02:02
Pages: a public robot manual and the shared site stylesheet (slice 4)

First piece of making jadonk.github.io/crystal-robots the project's front door. site/site.css is the shared look for every page that follows: color tokens on :root with a prefers-color-scheme dark override, an accent palette from the battlefield's ROBOT_COLORS, a .site-nav header, and phone-width layout. site/manual/ is one long page with a sticky table of contents. It covers what crystal-robots is, a first robot explained line by line, the language as the parser and checker accept it, the robot API rendered at runtime from cheat-sheet.json, the battlefield rules (each number annotated with the field.cr or interpreter.cr constant it comes from), how the pipeline and the browser build work, running it locally, and references (CROBOTS, chasm and the Scott Logic article, Crystal, WebAssembly, WASI, wasmer). spec/manual_spec.cr parses and checks every complete robot example on the page, so the manual cannot drift from the language.

Code by Codex (task da124118c8); reviewed here. The operator and compound-assignment claims match the parser's tables, and the CROBOTS link answers 200. crystal run ci.cr -- --with-wasm32 is all green: 176 examples, 0 failures, 10 pending. Codex loaded /manual/ in headless Chromium: all 15 API entries in 5 groups rendered, no console errors, no overflow at narrow width. Nothing links to the Fossil CGI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: 398b5b4cc0 user: run-3b21425d tags: dev, main, trunk

2026-10-24
09:54
Pages deploy: run the headless editor gutter spec with the runner's Google Chrome

After the ubuntu-latest image change, the Pages build fails in crystal run ci.cr -- --with-wasm32. spec/editor_gutter_spec.cr finds a chromium/chromium-browser binary on the runner, likely the snap launcher, and spec/support/editor_gutter_check.mjs gives up with 'timed out waiting for chromium devtools port' after 10 s (GitHub runs 39883705060 and 39924280682, log from the maintainer). The last green deploy (2026-09-21) predates the image change.

- spec/editor_gutter_spec.cr: a non-empty CHROMIUM_BIN picks the browser; otherwise try chromium, chromium-browser, google-chrome, google-chrome-stable. If none is found, the spec stays pending. - spec/support/editor_gutter_check.mjs: wait 30 s for the DevTools port, and put the binary path and the tail of the browser's stderr in the timeout and early-exit errors, so the next CI failure says why. - .github/workflows/pages.yml: a step before ci.cr sets CHROMIUM_BIN to the runner's preinstalled google-chrome (a deb) and echoes the choice. spec/workflow_spec.cr follows the new step order.

Code by Codex; reviewed and re-verified here: spec/editor_gutter_spec.cr and spec/workflow_spec.cr pass (5 examples, 0 failures); CHROMIUM_BIN=/nonexistent gives pending; a stub browser that never opens the port now fails with the binary and stderr in the message; crystal tool format --check is clean. The runner itself is untested until the mirror's deploy runs. Merge word: the maintainer, in this terminal (run 3c42ee4b9a).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: 380e64fae5 user: run-3b21425d tags: dev, main, trunk

2026-10-23
08:12
Pages deploy: mark site/*.js as ES modules so the playback spec runs on older Node

The Pages workflow run for 2df9681956 (GitHub dev f914ff4bfc, run 39883705060) failed in 'crystal run ci.cr -- --with-wasm32'. spec/battle_playback_spec.cr has node import site/battle-playback.js, and site/ had no package.json. Node 22.7+ and 20.19+ detect ES module syntax on their own, but Node 20.18 and earlier load the file as CommonJS: 'SyntaxError: Named export decodeFrames not found'. That reproduces here with Node 20.18.1 and is the likely runner failure (the job log needs GitHub auth). site/package.json {"type": "module"} declares what every site/*.js already is (all are loaded with <script type=module>), so any Node version imports them as ESM. It is also published to Pages as a 20-byte file, which is harmless.

Code by Codex (site/package.json plus a one-line comment in spec/support/battle_playback_check.mjs); reviewed and re-verified. crystal spec spec/battle_playback_spec.cr passes on Node 20.18.1, where it failed before, and on Node 24.14.1. crystal run ci.cr -- --with-wasm32 is all green (172 examples, 0 failures, 10 pending). Merge word: the maintainer, in this terminal (run 3c42ee4b9a).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0191EoCKd5Q7AW4HDeagE4oV check-in: bc53667345 user: run-3b21425d tags: dev, main, trunk

01:16
Merge Pages battle click-scale fix to trunk: branch fix-pages-battle-click-scale, tip 24927d8398. site/battle-playback.js decodeFrames divides robot and missile x/y by CLICK (10 clicks per meter), so the Pages battle and the editor's Fight draw on the 0..1000 m field instead of ~10x off it. Adds spec/battle_playback_spec.cr and spec/support/battle_playback_check.mjs, which decode a real seeded battle's frames with node. The branch's parent is trunk 676111c7ab, so this merged tree is the one verified. Verified in a fresh checkout before this merge: crystal run ci.cr is all green (172 examples, 0 failures, 17 pending); --with-wasm32 is all green (172 examples, 0 failures, 10 pending). In headless Chromium on the built site, counter vs rabbit with seed 1 put the robots at translate(743.2 467.6) and (828.7 799.8). With trunk's decoder the same battle put them at (7432 -4324) and (8287 -1002), off the field. The maintainer (stone) gave the merge word in thread 546a05a4dd, post 0b19affc3a. Source-only merge; Pages redeploy not done here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E9aCHmmUumfaVzNEMvQSqa check-in: 2df9681956 user: run-24e9eebd tags: dev, main, trunk

2026-10-22
22:51
Pages battle: draw robots and missiles in meters, not clicks

Regression from a83cd0a90f (2a43275f00), traced by trunk-ops in thread 0a47a782f3: Browser.encode_frames sends x/y in CROBOTS clicks (10 per meter), but site/battle-playback.js drew them straight into the 0..1000 viewBox, so every robot and missile landed ~10x off the field on the Pages battle and the editor's Fight. decodeFrames now divides by CLICK once, so robots, missiles and trails all get meters; the existing 1000 - y flips are unchanged and now match svg_replay.cr. The wire format stays in clicks (sub-meter precision for interpolation).

spec/battle_playback_spec.cr runs a real seeded battle through CrystalRobots::Browser and has node decode the frame log with the real battle-playback.js (spec/support/battle_playback_check.mjs), asserting every active robot and present missile is within 0..1000 and that a robot coordinate exceeds 100. Fails without the fix (robot 0 x=8413).

Code by Codex; reviewed and re-verified: crystal run ci.cr all green, 172 examples, 0 failures, 17 pending (one more than trunk).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GpM8XdpXoDeyTSRrjmPp2e Leaf check-in: 24927d8398 user: run-1ab99c01 tags: fix-pages-battle-click-scale

2026-09-21
02:51
Merge badge-interface resume/ERROR pinning spec to trunk: branch agent/20260921T021147Z-badge-interface-spec-that-resume-on-an-e, tip 41aebac7efe2, ticket b6ce2a4ff4. Pins the resume/ERROR finding from ticket 27a70ff3ed's review as documented, intended behavior: adds spec/badge_spec.cr's "resume on an errored robot leaves it ERROR and status.json says so" (a real parse-error robot, resume applied, confirms it stays inactive with state ERROR and a populated error_text), a matching comment on Session's class doc and the resume case in src/badge/session.cr, and the same sentence appended to the --badge CLI help text in src/crystal-robots.cr. Also confirms select_robot is not a recovery path either (a no-op in Session#apply, unchanged), so ERROR is structurally terminal for the rest of a --badge match with no in-vocabulary recovery command. No behavior change -- every changed line in session.cr and crystal-robots.cr is a comment or help-text string.

Verified in an isolated clone before this merge: diff read directly confirms no logic lines touched, only comments/help text and the one new spec. crystal run ci.cr: 171/0/17pending. --with-wasm32: 171/0/10pending. --with-wasmer (clean environment): 171/0/7pending -- all one example more than trunk's prior 170, matching the new spec. Cross-checked against badge-launcher's amended wire-contract text (branch d1776fe1fe there, not yet on badge-launcher trunk): word-for-word match on resume clearing ESTOP only, select_robot not clearing ERROR, and ERROR being terminal for the rest of the match.

Merge word: fable, under the maintainer's delegation, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035. Source-only merge, no rebuild or redeploy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> check-in: 676111c7ab user: agent-trunk-ops tags: dev, main, trunk

02:14
Badge: spec + document that resume on an errored robot leaves ERROR

Robot#error has no public setter and a failed robot's fiber has already unwound for good, so Session#apply's resume (which only clears estop and never touches a robot in ERROR) cannot revive it. This is the intended engine behavior per the badge-launcher contract amendment already filed; add a pinning spec plus matching comments in the resume case and the --badge help text so the contract can cite this file.

Confirmed no command is a recovery path: select_robot, like ping, is purely informational in Session#apply and never touches robot/field state, so ERROR is structurally terminal for the rest of the match.

No behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Leaf check-in: 41aebac7ef user: agent-claude tags: agent/20260921T021147Z-badge-interface-spec-that-resume-on-an-e

2026-09-20
23:31
Merge badge status/command interface to trunk: branch agent/20260920T221804Z-badge-status-command-interface-for-badge, tip 86549040cdd1, ticket 27a70ff3ed. Implements the crystal-robots side of badge-launcher's status/command wire contract (docs/crystal-robots-wire-contract.md, badge-launcher trunk f21722a59a, ticket 500ed41616): a new CrystalRobots::Badge module with StatusWriter (writes status.json atomically via write-then-rename, seq bumped every write) and CommandReader (seq-idempotent, six-command vocabulary: estop, resume, select_robot, move, calibrate, ping), wired to a live Battle::Field by Badge::Session. Session ticks from a new optional on_update hook added to Field#run (nil by default, every existing call site unaffected). A new opt-in `crystal-robots --badge robot1.cr robot2.cr` CLI mode runs one live match through it instead of the normal match summary.

Verified in an isolated clone before this merge: status.json/command.json field names and types checked directly against the contract doc read from badge-launcher trunk -- all match; fixtures are byte-identical to the contract's own JSON examples; six-command vocabulary exact. crystal run ci.cr: 170/0/17pending. --with-wasm32: 170/0/10pending. --with-wasmer (clean environment): 170/0/7pending. Manually ran a real --badge match and injected a live estop command mid-match; the targeted robot's status.json entry flipped to ESTOP within one status cycle while the other kept running -- the ticket's acceptance criterion, confirmed for real.

One contract-conformance finding from that review: resume does not clear a robot's ERROR state (Robot#error has no public setter; ERROR is structurally terminal in this engine), diverging from the wire contract doc's literal "clears ESTOP/ERROR" text. Maintainer's call (fable, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035): a contract defect, not a code defect -- badge-launcher's contract text is being corrected to say resume clears ESTOP only, ERROR persists until select_robot, with a pinning spec landing as a separate unit; both filed on the badge-launcher side.

Merge word: fable, under the maintainer's delegation, same thread. Source-only merge, no rebuild or redeploy -- this CLI mode is not part of the served CGI app. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> check-in: 403785bd7d user: agent-trunk-ops tags: dev, main, trunk

22:37
Implement the crystal-robots side of the badge-launcher status/command wire contract (ticket 27a70ff3ed)

Adds CrystalRobots::Badge, matching badge-launcher's docs/crystal-robots-wire-contract.md (badge-launcher tip f21722a59a, ticket 500ed41616) field for field: StatusWriter writes status.json (schema_version, generated_at_ms, seq, mode, a robots array, event_text, banner_text) atomically via write-then-rename; CommandReader reads command.json and is idempotent on an unchanged seq, recognizing exactly the six-command vocabulary (estop, resume, select_robot, move, calibrate, ping). Both paths honor CRYSTAL_ROBOTS_STATE_DIR, CRYSTAL_ROBOTS_STATUS_PATH and CRYSTAL_ROBOTS_COMMAND_PATH, defaulting to /tmp/crystal-robots, per the contract.

Badge::Session wires this to a live Battle::Field: publishes robot state (position, damage-derived battery_pct, active/error-derived state) each tick and applies commands by mutating the field's already-public Robot properties -- estop/resume toggle active, which the fiber-based engine already treats as pause/resume; move nudges position; calibrate clears motion state. It ticks from a new optional on_update hook in Field#run (called once per motion update, nil by default so every existing call site is unchanged) rather than a concurrently spawned fiber -- confirmed by hand that a sleep-based ticker fiber run alongside Field#run's tight robot-fiber handoffs can be starved for an entire match, since the scheduler always has a ready fiber and no reason to service a timer. Ticking from inside the loop that's already running avoids that race entirely; wall-clock rate limiting inside the tick still gives the contract's 5 Hz file-write cadence.

crystal-robots --badge robot1.cr robot2.cr runs one live match through Session. Manually verified end to end outside the spec suite: started a --badge match, wrote a command.json with an estop from another shell mid-match, and watched status.json's targeted robot flip to ESTOP and the match end (its own two-robot-minimum rule, unchanged).

spec/badge_spec.cr round-trips both files against fixtures copied verbatim from the contract doc (spec/fixtures/badge_status_example.json, spec/fixtures/badge_command_example.json): StatusWriter/CommandReader shape and idempotency, and Session applying estop/resume/move against a real Battle::Field. Full suite unaffected: 170 examples, 0 failures, 17 pending (same pending set as before, all optional wasmer/wasm32 toolchains).

Does not change the contract; badge-launcher's lib/crystal_robots_client.py is the reference the field names and paths here were checked against.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com Leaf check-in: 86549040cd user: agent-claude tags: agent/20260920T221804Z-badge-status-command-interface-for-badge

16:57
docs: TUTORIAL.md, the as-built commit-by-commit index for the blog

Adds docs/TUTORIAL.md: the ordered list of all 53 tutorial commits (A.1 through W.6), one line each, grouped by the phase letters the check-in messages themselves used (A compiler/emitter, B grammar+checker parity, C battlefield, D CGI web layer, T tournament, U remaining trunk parity, W the wasm32 browser compiler and its Pages hosting) -- not the older A-E lettering docs/PLAN.md was written with before any of this existed. Also a blog-post outline per phase: a suggested title and beat list (design motivation, the commits in order, a closing demo) for each of the seven posts the maintainer will write from this branch.

Proposes 'tutorial' as the stable branch name for the blog to cite, per the maintainer's direction on ticket 7b6bf1909f (2026-09-20: this stays a permanent parallel history, not merged to trunk).

Re-verified every one of the 53 commits before writing this down, not from memory of their own check-in messages: each hash checked out in a scratch checkout separate from this session's own working tree (which never left this branch), rebuilt with 'crystal build --no-codegen' and run through plain 'crystal spec'. All 53 build clean and pass, unchanged, today -- zero failures, zero skips.

docs/PLAN.md's stale 'Phase A done, B-E next' status line is replaced with a pointer to TUTORIAL.md as the authoritative index; PLAN.md itself is kept as the original design-intent document it was written as.

crystal tool format --check src spec ci.cr: clean (docs-only change, no source or spec touched).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> check-in: 8a1d72bb6f user: agent-claude tags: tutorial

13:39
Merge battle playback to trunk: branch session/e0dba693a07ad705b2df3701d63683faa9099ab25ac913a11c8b4c9bec613bc5, tip 2a43275f00cf, ticket a83cd0a90f. Field now records a frame for every cannon shot, hit, kill or missile impact regardless of the fixed downsample interval (@event_pending, set by cannon/scan/move_robots/move_missiles in src/battle/field.cr, read by both the fiber-based run and run_stepwise), so a long or heavily-downsampled match no longer silently skips a combat event on any path -- the served /battle page and the CLI keep their existing, smaller frame budgets (ANIM_FRAMES=400, Field's own default 200), but now with the same event guarantee: same physics, same standings, more frames, never a skipped event. crd_battle_run no longer builds a SMIL svg_animation for the in-page battle (that many keyframes would itself be the slow rendering this fixes): it returns a static SVG skeleton (Battle.svg_skeleton, stable ids, no animate elements) plus the frame log as a compact binary buffer (Browser.encode_frames, layout documented in src/browser.cr). site/battle-playback.js decodes that buffer and drives the skeleton in requestAnimationFrame at 60fps with play/pause, a speed control and a scrubber; site/battle.js and site/editor/editor.js both switched from inserting result.svg to mountBattlePlayback -- one renderer for the skeleton, the old in-page SMIL path is gone. svg_animation itself is byte-identical and still renders the served /battle page's own replay.

Verified in an isolated clone before this merge, after merging current trunk (the gutter fix) into the branch tip -- clean auto-merge, no conflicts: event-frame guarantee confirmed in both engines; crd_battle_run's report carries no per-frame JSON; served-page/CLI budgets confirmed untouched; site/battle.js and editor.js confirmed off the old innerHTML=result.svg path; site/battle-playback.js confirmed same-origin module, no eval/Function/document.write/CDN; .github/workflows/pages.yml untouched. crystal run ci.cr: 158/0/17pending. --with-wasm32: 158/0/10pending.

First gate held RED on an explicit but stricter reading of "svg_animation output unchanged": empirically ran Battle::Field.new(...).run -> svg_animation on the same seed/robots/budget before and after and found frame_count and the animate keyframes do differ (97 vs 187 frames for an identical 14,385-cycle match, same winner) because @event_pending is shared by Field#run, not scoped to the browser-only path. Maintainer's call (fable, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035): accepted as intended, not a regression -- the served page catching cannon fire/hits it previously could skip between downsampled frames is the point of this ticket, not a side effect. Criterion revised to "same physics, same standings, more frames, never a skipped event", which holds.

Merge word: fable, under the maintainer's delegation, same thread. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> check-in: 5c6b901912 user: agent-trunk-ops tags: dev, main, trunk

12:33
Merge editor line-number gutter fix to trunk: branch agent/20260920T114433Z-editor-line-number-gutter-drifts-from-th, tip 3ea79717dbb2, ticket 7a7758839e. Fixes the gutter drifting out of sync with the textarea on scroll and on soft-wrapped lines (reported on iPhone Safari): the gutter
    had no explicit height so it never actually clipped/scrolled (scrollTop sync was a no-op), and the textarea's soft-wrap put one gutter number out of sync per wrapped line. Gutter now gets height/box-sizing matching the textarea's 22rem plus a ResizeObserver to keep it matched on manual resize; textarea gets wrap="off"/white-space:pre/overflow-x:auto so one logical line is one visual row; scrollTop sync consolidated into one function called on both scroll and input. Adds spec/editor_gutter_spec.cr driving spec/support/editor_gutter_check.mjs, a headless real-Chromium DevTools-protocol check (no npm dependency), pending on a machine without chromium. Verified in an isolated clone before this merge: diff scope limited to site/editor/editor.js, site/editor/index.html plus the two new spec files, .github/workflows/pages.yml untouched. crystal run ci.cr: 150/0/17pending. --with-wasm32: 150/0/10pending -- new headless gutter check ran for real (not pending) in both modes on this host. Merge word: fable, under the maintainer's delegation, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
check-in: 4ed7e60dc6 user: agent-trunk-ops tags: dev, main, trunk
12:05
Phase 6: battle playback at real frame rate, event-guaranteed frames (ticket a83cd0a90f)

Field now records a frame for every motion update that fires a cannon, lands a hit, deals damage, or kills a robot, regardless of the fixed downsample interval a frame budget still sets for quiet stretches -- @event_pending, set by cannon/scan/move_robots/move_missiles (src/battle/field.cr), read by both the fiber-based run and the non-fiber run_stepwise (src/battle/step_robot.cr), so native and wasm32 get the guarantee identically.

Browser.battle_run (src/browser.cr) accepts a request max_frames (200/20,000/60,000 min/default/max, clamped so a page-supplied 0 can't divide by zero on a target where raise traps the whole module); the served /battle page and bin/crystal-robots keep their existing, much smaller budgets. Since building tens of thousands of SMIL keyframes would itself be the slow rendering this fixes, crd_battle_run no longer renders svg_animation for the in-page battle: it returns a static SVG skeleton (Battle.svg_skeleton, src/battle/svg_replay.cr, same shapes, stable ids, no animate elements) plus the frame log packed as a compact binary buffer (Browser.encode_frames, layout documented in src/browser.cr). svg_animation itself is unchanged and still renders the served page.

site/battle-playback.js (new) decodes that buffer and drives the skeleton's ids in requestAnimationFrame at 60fps -- position/heading interpolate linearly between bracketing frames (shortest-path for heading), everything else steps discretely -- with play/pause, a speed control and a scrubber. site/battle.js and site/editor/editor.js both switched from inserting result.svg to mountBattlePlayback.

spec/battle_spec.cr proves every cannon impact and a fatal blow each get a guaranteed frame under both engines at max_frames: 2; new spec/browser_spec.cr (native, no wasm32 build needed) proves frame count scales with the requested budget, the binary header agrees with the JSON report, a non-positive max_frames is clamped instead of crashing, and the guarantee holds through the JSON/binary bridge; spec/wasm32_spec.cr's differential case compares frame_count, robot_count and skeleton against a native run_stepwise run.

crystal run ci.cr and -- --with-wasm32 both green (157 examples, 0 failures, 0/10 pending respectively). Verified in headless Chromium on this host (raw CDP, --no-sandbox): a full 500,010-cycle rabbit-vs-rabbit match records 33,336 frames (vs. the served page's 400-frame cap) and plays back at a steady 60.00 fps (239 samples, mean 16.67ms, max 16.80ms, zero dropped frames, no console errors); a large backward scrub costs one hitch frame for the trail rebuild, then returns to steady 60fps.

docs/PLAN.md records this Phase 6 unit; the wiki-page robot-skins part of the ticket stays open, out of scope here.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com Leaf check-in: 2a43275f00 user: agent-claude tags: session/e0dba693a07ad705b2df3701d63683faa9099ab25ac913a11c8b4c9bec613bc5

11:54
Fix site/editor line-number gutter drift on scroll and wrapped lines

Two bugs, both in site/editor: (1) the gutter

    had no explicit height, so flexbox's default `align-items: stretch` sized it to fit every line number rather than to the textarea's fixed 22rem box -- it never actually became a scrollable, clipped element, so setting its scrollTop on the textarea's scroll event was a no-op and the gutter just showed 1, 2, 3... regardless of scroll position; (2) the textarea soft-wrapped long lines while the gutter renders one row per logical line, so a wrapped line put every gutter number below it out of sync by a row.

    Fix: give the gutter an explicit height/box-sizing matching the textarea (and a ResizeObserver to keep it matching if the user drags the resize handle), disable textarea soft-wrap (white-space: pre, overflow-x: auto, wrap="off") so one logical line is one visual row, and sync the gutter's scrollTop on every scroll and input event.

    Added spec/editor_gutter_spec.cr + spec/support/editor_gutter_check.mjs, a headless check driving a real Chromium over the DevTools protocol (no npm dependency) to verify no line ever soft-wraps and the gutter's scrollTop tracks the textarea's; it reports pending on a machine without a chromium/chromium-browser binary. Also hand-verified at a 400px viewport with examples/counter.cr scrolled to the bottom.

    Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com

Leaf check-in: 3ea79717db user: agent-claude tags: agent/20260920T114433Z-editor-line-number-gutter-drifts-from-th
11:26
Create new branch named "session/e0dba693a07ad705b2df3701d63683faa9099ab25ac913a11c8b4c9bec613bc5" check-in: 7fbe4f56cc user: agent-claude tags: session/e0dba693a07ad705b2df3701d63683faa9099ab25ac913a11c8b4c9bec613bc5
07:19
Merge Phase 6 (browser robot editor) to trunk: branch session/dfc2ad0e45a51e1095e8a17033e675a52f76007ee293abe87eade79320184808, tip 0b7c45258fb6, ticket db7474e899. Adds site/editor (index.html + editor.js, plain textarea, no framework/CDN) forking a bundled example or saved wiki robot into the buffer, live-checked on a 500ms debounce against a new non-raising crd_check/Browser.check export (src/browser.cr, reusing Parser.try_parse from Phase 5b-2) that reports a parse error or checker problems as message/line/col instead of trapping. Fight feeds the buffer into the existing crd_battle_run and renders the same SVG replay site/battle.js already draws. RobotAPI.cheat_sheet_json exports the served panel's own GROUPS/ENTRIES as JSON -- one Crystal source of truth for both surfaces -- written to site/cheat-sheet.json by scripts/build_cheat_sheet.cr, a new ci.cr --with-wasm32 step. A new /wiki-robots.json route lets the editor's picker offer saved robots once reachable from the same origin, via an explicit ?wiki=<base> query parameter the page never guesses (no CORS, no credentialed cross-origin fetch). docs/PLAN.md lists every default as a named switch waiting on the unanswered "Robot Builder ideas" wiki page.

Verified in an isolated clone before this merge: branch parent chain descended directly from current trunk, already up to date; .github/workflows/pages.yml confirmed byte-identical to trunk's copy; /wiki-robots.json's guard confirmed identical to /wiki/<name>'s immediately above it in cgi.cr; editor.js's only cross-origin fetch confirmed gated behind an explicit ?wiki= parameter, every other fetch same-origin. crystal run ci.cr: 149/0/17pending. --with-wasm32: 149/0/10pending, site/cheat-sheet.json built (15 entries/5 groups), its byte-identity spec and the edited-example crd_check spec both ran for real. Merge word: fable, under the maintainer's delegation, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com check-in: ad0933bd6c user: agent-trunk-ops tags: dev, main, trunk

06:26
Phase 6: browser robot editor (fork, live checks, one-page cheat-sheet) - ticket db7474e899

site/editor (index.html + editor.js): a plain-textarea editor page, no framework/CDN, same CSP as the rest of site/. Forks any bundled example or saved wiki robot into the buffer (copy-and-change, the plan's default), checks it live on a 500ms debounce (also the plan's default) against a new non-raising crd_check/Browser.check export (src/browser.cr, reusing Parser.try_parse from Phase 5b-2) that reports a parse error or checker problems as message/line/col instead of trapping the module, rendered beside a line-number gutter in the same plain wording derivation_section already uses server-side. Fight feeds the buffer into the existing crd_battle_run and renders the same SVG replay site/battle.js already draws.

RobotAPI.cheat_sheet_json (src/web/robot_api.cr) exports the served panel's own GROUPS/ENTRIES as JSON -- one Crystal source of truth for both surfaces -- written to site/cheat-sheet.json by scripts/build_cheat_sheet.cr, a new ci.cr --with-wasm32 step alongside site/robots.json. A new wiki-robots.json route (src/web/cgi.cr, gated the same way as the per-robot wiki view route) lets the editor's picker offer saved robots once it is reachable from the same origin as this server, via an explicit wiki query parameter naming that origin, which the page never guesses (no CORS, no credentialed cross-origin fetch); the overview links to the GitHub Pages editor without one today, since the same-origin mount is a deploy topology decision left to trunk-ops.

Every default is a named, one-line switch (docs/PLAN.md, Phase 6), waiting on the still-unanswered "Robot Builder ideas" wiki page.

Proof: spec/robot_api_spec.cr checks cheat_sheet_json structurally against RobotAPI::GROUPS/ENTRIES and Interpreter.builtin_names; spec/wasm32_spec.cr adds a byte-identical check of the generated site/cheat-sheet.json against cheat_sheet_json computed fresh, and a case editing examples/sniper.cr's source and checking it clean through crd_check, proving an edited example round-trips through the module; spec/web_cgi_spec.cr covers the new route's listing and its login/wiki-read gate. crystal run ci.cr: 149 examples, 0 failures, 17 pending. crystal run ci.cr -- --with-wasm32: 149 examples, 0 failures, 10 pending (wasmer-gated only). crystal tool format --check clean. .github/workflows/pages.yml untouched.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com Leaf check-in: 0b7c45258f user: agent-claude tags: session/dfc2ad0e45a51e1095e8a17033e675a52f76007ee293abe87eade79320184808

05:51
Create new branch named "session/dfc2ad0e45a51e1095e8a17033e675a52f76007ee293abe87eade79320184808" check-in: 140365b47e user: agent-claude tags: session/dfc2ad0e45a51e1095e8a17033e675a52f76007ee293abe87eade79320184808
04:59
Merge Phase 5b-2 (battle entirely in the browser) to trunk: branch session/fff08bef5384779ab2aee07ba2252e7487117c854bb7fa9f9c471ea1514b5993, tip db6d9d5c8b35, ticket 4e79e50f1f. Moves src/web/cgi.cr's svg_animation into src/battle/svg_replay.cr as Battle.svg_animation, cgi.cr delegates to it (one renderer, not two). Adds crd_battle_run/crd_battle_result_* exports in src/browser.cr: one JSON battle request in, standings plus the same SVG replay string out. site/battle.js is thin glue (no canvas/JS renderer of its own) that collects picks/seed/limit/cps and inserts the returned SVG. site/robots.json (bundled example robots) is built by scripts/build_robots_json.cr, wired as a step inside ci.cr under --with-wasm32. spec/wasm32_spec.cr adds a differential case comparing a seeded battle's standings, cycle count and rendered SVG text against the native run, plus a case confirming a parse-error robot is isolated rather than trapping the match.

Verified in an isolated clone before this merge: diffed the moved svg_animation body against its pre-move form directly and confirmed it is a pure relocation (namespace-prefix drops plus one harmless rename, no behavior change); site/battle.js and site/app.js confirmed free of canvas/getContext; .github/workflows/pages.yml's diff against the branch's own merge base is comment-only, confirmed by spec/workflow_spec.cr's pinned 5-step job still passing; crystal run ci.cr 140/0/15pending, --with-wasm32 140/0/10pending (both battle differential specs ran for real). Since this branch's merge base was an older trunk than current (the libyaml-dev CI fix landed since), also simulated merging current trunk into the branch tip in the isolated clone first: clean auto-merge, no conflict markers, both ci.cr modes re-verified green in that merged state. This merge into the primary checkout produced the identical MERGE/UPDATE/ADDED file set as that simulation, with no conflict markers. Merge word: fable, under the maintainer's delegation, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com check-in: 0c32c81f82 user: agent-trunk-ops tags: dev, main, trunk

04:12
Merge trunk (ci.cr port, tip 1aa8b275441a): scripts/ci.sh and install_wasm32_wasi_libs.sh are gone, CI is `crystal run ci.cr -- [flags]`, spec/workflow_spec.cr pins the Pages workflow's exact step list.

Rebased this branch's Phase 5b-2 robots.json build step onto ci.cr instead of the deleted script: it is now a step inside ci.cr under --with-wasm32 (crystal run scripts/build_robots_json.cr, right after building site/crystal-robots.wasm), not a separate workflow step -- keeps spec/workflow_spec.cr's pinned 5-step build job (checkout, install-crystal, apt lld, ci.cr, upload-pages-artifact) satisfied with no addition to .github/workflows/pages.yml. Updated ci.cr's own header and scripts/build_robots_json.cr's header comment to match. docs/PLAN.md and .github/workflows/pages.yml comments reconciled to describe both the ci.cr port and Phase 5b-2 under the merged history.

Verified in the foreground: `crystal run ci.cr` -- 140 examples, 0 failures, 15 pending, all green; `crystal run ci.cr -- --with-wasm32` -- 140 examples, 0 failures, 10 pending, all green, site/robots.json built (5 robots: counter, rabbit, rook, sniper, target) alongside site/crystal-robots.wasm; `crystal tool format --check` clean. Leaf check-in: db6d9d5c8b user: agent-claude tags: session/fff08bef5384779ab2aee07ba2252e7487117c854bb7fa9f9c471ea1514b5993

04:01
Merge CI runners need libyaml to trunk: branch agent/20260920T031218Z-ci-runners-need-libyaml-dev-now-that-the, tip f92c278ca214, ticket d34d44b6d3. spec/workflow_spec.cr requires "yaml", so crystal spec now links libyaml. Installs yaml-dev on the GitLab apk line, libyaml-dev on the Pages apt step, and adds an explicit link check in ci.cr (a one-line 'require "yaml"' build) before the spec step, failing with the correct package name per platform the same way the existing node/wasm-ld checks do. spec/workflow_spec.cr's pinned apt-get assertion updated to match.

Verified in an isolated clone before this merge: diff limited to .github/workflows/pages.yml, .gitlab-ci.yml, ci.cr and spec/workflow_spec.cr; plain crystal run ci.cr, 138 examples/0 failures/13 pending; --with-wasmer from a clean environment (no ambient ~/.wasmer or WASMER_DIR), 138/0/3 pending, confirming the prior WASMER_DIR fix still holds under this change. Merge word: fable, under the maintainer's delegation, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com check-in: f75a1fc50c user: agent-trunk-ops tags: dev, main, trunk

03:17
ci: install libyaml-dev/yaml-dev in CI and check it links before spec

spec/workflow_spec.cr requires "yaml", so crystal spec now links libyaml. Install the dev package on both CI runners and add an explicit link check in ci.cr (build a one-line 'require "yaml"' program) before the spec step, failing with the package name for each platform, the way the node and wasm-ld checks already do. Update workflow_spec.cr's pinned apt-get command to match the new package.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Leaf check-in: f92c278ca2 user: agent-claude tags: agent/20260920T031218Z-ci-runners-need-libyaml-dev-now-that-the

02:48
Merge ci.cr is the CI to trunk: branch agent/20260920T014102Z-ci-cr-is-the-ci-port-scripts-ci-sh-to-cr, tip b22855c957a7, ticket feebeb1531. ci.cr no longer forwards to scripts/ci.sh; it is the CI itself, a direct Crystal port of every step (shards install, bootstrap build, build-docs, manifest.uuid check, release build, optional wasmer/wasm32 setup, crystal spec, format check, example builds, CLI smoke), each a Process.run step failing fast with its name and exit status. scripts/ci.sh and scripts/install_wasm32_wasi_libs.sh are deleted; the wasm32-libs installer is ported inline as HTTP::Client code, while wasmer's installer stays a shelled-out script by documented decision. Invocation everywhere is now `crystal run ci.cr -- [flags]`: ci.cr's own header, README, docs/PLAN.md, .gitlab-ci.yml, and .github/workflows/pages.yml. spec/workflow_spec.cr pins the Pages workflow's trigger branches, job order and exact build steps via Crystal's stdlib YAML. Fixes the WASMER_DIR regression from the prior RED gate on 10a50523d0e7 (ENV["WASMER_DIR"] is now set globally beside ENV["PATH"], not only in the installer subprocess's own env), confirmed by re-running --with-wasmer from a genuinely clean environment. Verified in an isolated clone before this merge, all three modes from a clean $HOME with no ambient ~/.wasmer or WASMER_DIR: plain crystal run ci.cr, 138 examples/0 failures/13 pending; --with-wasm32, 138/0/10 pending; --with-wasmer, 138/0/3 pending -- the exact case that failed RED last time, now confirmed fixed. crystal tool format --check clean. Merge word: fable, under the maintainer's delegation, thread 0a47a782f315c77588716fbdb34822ea8b66b3f5c87d354275743e47f7245035.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com check-in: 1aa8b27544 user: agent-trunk-ops tags: dev, main, trunk

01:52
ci.cr is the CI: port scripts/ci.sh to Crystal, delete the shell script

ci.cr no longer forwards to scripts/ci.sh; it is the CI itself. Every step (shards install, bootstrap build, build-docs, manifest.uuid check, release build, optional wasmer/wasm32 setup, crystal spec, format check, example builds, CLI smoke) runs as plain Crystal, Process.run per step, failing fast with the step name and exit status. scripts/ci.sh is deleted. scripts/install_wasm32_wasi_libs.sh is ported inline as HTTP::Client code (download, verify the extracted layout, install under the checkout) and deleted too. wasmer's installer stays a shelled-out script: it does platform/arch detection and checksum verification for an opt-in flag, and porting it would be out of proportion to what it buys -- the decision and reasoning are recorded in ci.cr's header.

Fix on top of the prior attempt (gate feedback on 10a50523d0e7): the --with-wasmer branch set the WASMER_DIR environment variable only in the installer subprocess's env, not in ci.cr's own process environment, so crystal spec -Dwasmer failed at link time with undefined wasm_* references from a clean environment (no ~/.wasmer, no ambient WASMER_DIR). The env variable is now set globally beside the existing PATH update.

Invocation everywhere is now crystal run ci.cr -- with flags after the double dash: ci.cr's own header, README, docs/PLAN.md (Phase 7 and a new status entry), and .github/workflows/pages.yml, which runs crystal run ci.cr -- --with-wasm32. .gitlab-ci.yml, spec/spec_helper.cr and src/crystal-robots.cr's comments are updated the same way; nothing in the tree still runs ci.sh.

Folds in 41dc8cbf88: spec/workflow_spec.cr parses .github/workflows/pages.yml with Crystal's stdlib YAML and pins the trigger branches, the build-then-deploy job order, the build step order (checkout, install-crystal pinned to 1.18.2, the lld apt step, the exact ci.cr command, upload-pages-artifact) and the deploy job's actions/deploy-pages step under the github-pages environment.

Verified in this checkout, each from a clean environment (no ~/.wasmer, no ambient WASMER_DIR, no local wasmer or wasm32-wasi-libs directories): plain crystal run ci.cr, exit 0, 138 examples, 0 failures, 0 errors, 13 pending; with --with-wasm32, exit 0, 138 examples, 0 failures, 0 errors, 10 pending; with --with-wasmer, exit 0, 138 examples, 0 failures, 0 errors, 3 pending. crystal tool format --check clean across src, spec, scripts and ci.cr.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com Leaf check-in: b22855c957 user: agent-claude tags: agent/20260920T014102Z-ci-cr-is-the-ci-port-scripts-ci-sh-to-cr

00:41
Phase 5b-2: battle entirely in the browser, no server

src/battle/step_robot.cr gives Battle::Robot a second, non-fiber execution path (start_stepwise/step!): the interpreter's tree-walk rewritten in continuation-passing style, pausing after exactly one charged CROBOTS cycle -- the same granularity field.cr's fiber/Channel gives the native path -- without spawn, Fiber, or ever calling raise. Field#run_stepwise drives it with the same physics run already has. Needed because wasm32-unknown-wasi has no working exceptions at all (any raise traps the module, even one rescued in the same function, Phase 5b-1's finding) and spawn/Channel are untrusted once _start has returned control to the host. Compiler::Parser gains non-raising try_parse/try_lex (return the Error instead of raising it) so a bad robot's parse failure is data, not a trap; parse/lex are now thin wrappers over them, unchanged for every existing caller.

src/browser.cr exports crd_battle_alloc/crd_battle_run/ crd_battle_result_*: one JSON request (2 to 4 {name, source} robots, a seed, a cycle limit, a replay cps), one JSON report (standings plus an SVG replay string).

Maintainer correction mid-unit: no canvas/JS renderer. Moved src/web/cgi.cr's svg_animation (the served /battle page's SMIL SVG) to src/battle/svg_replay.cr as Battle.svg_animation, a plain function of a finished Field; cgi.cr now delegates to it (ROBOT_COLORS/ANIM_CPS point at the shared constants). crd_battle_run renders with the same function and returns the SVG string; the per-cycle frame log stays inside the call. site/battle.js (new, no framework, no CDN) only collects robot picks/seed/limit/cps and inserts the returned SVG. site/robots.json (bundled examples, from examples/*.cr) is generated by scripts/build_robots_json.cr, wired into .github/workflows/pages.yml directly rather than scripts/ci.sh, which is being replaced by ci.cr on a parallel branch (ticket feebeb1531).

spec/wasm32_spec.cr compares a seeded battle's standings, cycle count and rendered SVG text against Field#run_stepwise + svg_animation run natively -- the same source compiled for both targets, the same cross-compilation-fidelity check Phase 5b-1 already uses for crd_run/ crd_interpret -- and confirms a robot with a parse error is reported inactive with its error, isolated, not a trap.

Verified: crystal run ci.cr and crystal run ci.cr -- --with-wasm32 both green (136 examples, 0 failures; 15 vs 10 pending). Manually confirmed a native run and the same request through node + site/wasi-shim.js produce byte-identical JSON reports; a bad robot source alongside a good one reports {active:false,error:...} for the bad one and a winner among the rest, no trap; the page loads and a battle renders correctly (headless Chrome screenshot, no console errors).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> check-in: c64fdae4fd user: agent-claude tags: session/fff08bef5384779ab2aee07ba2252e7487117c854bb7fa9f9c471ea1514b5993

00:05
ci.cr is the CI: port scripts/ci.sh to Crystal, delete the shell script

ci.cr no longer forwards to scripts/ci.sh; it is the CI itself. Every step (shards install, bootstrap build, build-docs, manifest.uuid check, release build, optional wasmer/wasm32 setup, crystal spec, format check, example builds, CLI smoke) runs as plain Crystal, Process.run per step, failing fast with the step name and exit status. scripts/ci.sh is deleted.

scripts/install_wasm32_wasi_libs.sh is ported inline as HTTP::Client code (download, verify the extracted layout, install under the checkout) and deleted too. wasmer's installer stays a shelled-out script: it does platform/arch detection and checksum verification for an opt-in flag, and porting it would be out of proportion to what it buys -- the decision and reasoning are recorded in ci.cr's header.

Invocation everywhere is now "crystal run ci.cr -- ", plus flags: ci.cr's own header, README, docs/PLAN.md (Phase 7 and a new status entry), and .github/workflows/pages.yml, which runs crystal run ci.cr -- --with-wasm32. .gitlab-ci.yml, spec/spec_helper.cr and src/crystal-robots.cr's comments are updated the same way; nothing in the tree still says ci.sh.

Folds in 41dc8cbf88: spec/workflow_spec.cr parses .github/workflows/pages.yml with Crystal's stdlib YAML and pins the trigger branches, the build-then-deploy job order, the build step order (checkout, install-crystal pinned to 1.18.2, the lld apt step, the exact ci.cr command, upload-pages-artifact) and the deploy job's actions/deploy-pages step under the github-pages environment.

Verified in this checkout: plain crystal run ci.cr and crystal run ci.cr -- --with-wasm32 both green, 138 examples/0 failures/0 errors (13 pending plain, 10 pending with wasm32 since those 3 specs run for real); crystal tool format --check clean.

Co-Authored-By: Claude Sonnet 5, noreply@anthropic.com Leaf check-in: 10a50523d0 user: agent-claude tags: agent/20260919T234834Z-ci-cr-is-the-ci-port-scripts-ci-sh-to-cr

00:02
Phase W.6: ci.cr replaces ci.sh, per maintainer direction

Maintainer direction, 2026-09-19: scripts/ci.sh should not exist; ci.cr is the CI, written in Crystal, invoked as `crystal run ci.cr -- [flags]`. Adds ci.cr at the repo root, a direct Crystal port of every step scripts/ci.sh ran (shards install with its silent-then-loud fallback, a bootstrap build, build-docs before the final build, the manifest.uuid check and its git-HEAD fallback, the final build, the optional wasmer 4.4.0 and wasm32-wasi-libs installs, crystal spec with the right -D flags, format --check, the CLI smoke test), then deletes scripts/ci.sh outright -- this replaces it, not alongside it. install_wasmer.sh and install_wasm32_wasi_libs.sh stay shell scripts under scripts/: one is a vendored third-party installer, the other a plain tarball fetch with nothing Crystal-specific about it, so ci.cr still shells out to both, and to rm/git/sh themselves, the same way it always shelled out to shards and crystal.

A real bug found while verifying this against the actual wasmer install path: the first draft passed WASMER_DIR to the installer's own subprocess environment only, the way PROFILE/TMPDIR/WASMER_INSTALL_LOG are; wasmer installed to the real $HOME/.wasmer instead of this checkout's ./.wasmer, and the following `wasmer --version` step failed outright (No such file or directory) because nothing exported WASMER_DIR into this process's own environment for later steps -- scripts/ci.sh had exported it into the whole script via `export`, a step this port had dropped. Fixed by setting ENV["WASMER_DIR"] directly (Crystal child processes inherit the parent's ENV plus whatever a call's own `env:` hash adds, confirmed directly, not assumed), the same way ENV["PATH"] already needed to be for wasmer's own bin directory. The wasmer-crystal shard needs WASMER_DIR at `crystal spec -Dwasmer` link time too, later in the same run, so this was a real functional gap, not just a style choice.

Also updates .github/workflows/pages.yml's step from `scripts/ci.sh --with-wasm32` to `crystal run ci.cr -- --with-wasm32`, and spec/pages_workflow_spec.cr's step-order assertion to match -- the YAML-pinning spec itself was not redone here, only the one line that had to change regardless once ci.sh stopped existing. Every other scripts/ci.sh mention in comments (spec/spec_helper.cr, spec/web/docs_spec.cr, docs/PLAN.md) is updated to say ci.cr instead.

Verified for real, twice, from a fully clean tree (bin/, docs-api/, .shards/, lib/, .wasmer/, .wasm32-wasi-libs/, site/crystal-robots.wasm, shard.lock and even $HOME/.wasmer removed first): plain `crystal run ci.cr --` built docs, built the binary, ran 242 examples with 0 failures (45 pending, all wasmer/wasm32-gated), format-checked clean, smoke-tested the CLI, and printed "ci.cr: all green"; after fixing the WASMER_DIR bug, `crystal run ci.cr -- --with-wasmer --with-wasm32` installed both wasmer and the wasm32-wasi-libs sysroot from nothing and ran all 242 examples for real, 0 pending, printing "ci.cr: all green (with wasmer) (with wasm32)". A bogus flag still prints "unknown option: --bogus-flag" and exits 2. shard.lock reverted afterward, same as ci.sh always needed.

crystal spec (plain, no flags, current tree): 239 examples, 0 failures, 46 pending (unchanged group). crystal tool format --check src spec ci.cr: clean. check-in: 60538ebc82 user: agent-claude tags: tutorial

2026-09-19
23:44
Phase W.5: pin the Pages workflow with Crystal's own YAML, correcting W.4

W.4's commit message said "no YAML library available in this environment" and fell back to a text-regex check. That was wrong: Crystal's stdlib has one (require "yaml", backed by libyaml, which is installed here) and YAML.parse works directly in this sandbox -- confirmed by running it, not by re-reading the earlier claim. Recorded here so the history says what is actually true rather than repeating the mistake.

Adds spec/pages_workflow_spec.cr, parsing .github/workflows/pages.yml with YAML.parse and pinning: the trigger is exactly a push to main or dev, plus a manual workflow_dispatch; the build job's five steps run in order (checkout, install Crystal, apt-get lld, ci.sh --with-wasm32, upload-pages-artifact); the installed Crystal version matches what this environment itself runs (1.18.2); deploy needs build. A real, documented gotcha surfaced while writing it: YAML 1.1 parses a bare `on:` key as the boolean true, not the string "on", so the trigger lives at workflow[true], not workflow["on"] -- confirmed directly against this file, not assumed from memory of other YAML parsers.

Per the coordinator's standing instruction from here on: this check uses only Crystal's own standard library, no Python or Node -- Node stays in the toolbox solely where ci.sh itself already needs it (the wasm32 specs driving the module through site/wasi-shim.js).

crystal spec: 239 examples, 0 failures, 46 pending (41 wasmer + 4 wasm32 + 1 docs placeholder, unchanged from the last report). crystal tool format --check src spec scripts: clean. check-in: 38321a2764 user: agent-claude tags: tutorial