ilo: A Programming Language for AI Agents, Not Humans

Open Source Workflow Automation for ilo-lang

Here we will look at four bits of automation that sit around ilo-lang. None of them are clever in isolation. Together they’re what lets me push a tag and walk away.

  • scripts/bump-version.sh for the lockstep version bump across Cargo, AGENTS.md, and the Claude Code plugin manifests
  • The release workflow that publishes binaries, WASM, npm, and crates.io off a single v* tag
  • The site at ilo-lang.ai, built into an nginx image and redeployed by Dokploy on every push
  • A sync-next-from-main action that keeps the next-version integration branch current as patches land on main

The version-bump script

The ilo version lives in four files: Cargo.toml, AGENTS.md, .claude-plugin/plugin.json, and .claude-plugin/marketplace.json. Miss one and a release will tag with mismatched metadata. scripts/bump-version.sh does all four in one pass.

scripts/bump-version.sh 0.12.1

It validates the argument as X.Y.Z, refuses to run outside the repo root, then uses awk to rewrite the version field in each file: the first version = "..." line in Cargo.toml, the Current version: **X.Y.Z** line in AGENTS.md, and the "version" field in both plugin manifests. The JSON files get a line-based replacement so the script doesn’t reflow the file or escape unicode. Last step:

cargo update -p ilo --precise 0.12.1

to refresh Cargo.lock, falling back to cargo check if that fails.

The output is one line: bumped to 0.12.1. I run git diff to eyeball, commit, and tag.

The release pipeline

.github/workflows/release.yml triggers on any tag matching v*. It runs six jobs.

The first builds release binaries on a matrix of five targets: x86_64 and ARM64 Linux, x86_64 and ARM64 macOS, and x86_64 Windows. ARM64 Linux uses cross for cross-compilation; everything else compiles natively. Each renames the binary to ilo-<target> and uploads it as an artifact.

The second builds the WASM artifact:

cargo build --release --target wasm32-wasip1 --no-default-features

The third collects all artifacts, generates checksums-sha256.txt, and creates the GitHub release with softprops/action-gh-release@v2 using generate_release_notes: true. The changelog comes out of GitHub’s auto-generated notes from the commit history between tags (keepachangelog.com is the manual alternative if you want more control), which is why I keep commits small and one-thing-per-commit.

The fourth is publish-crates. cargo publish runs with CARGO_REGISTRY_TOKEN (publishing to crates.io requires a token from your account settings), and the step swallows an “already exists” error so re-running a tag is idempotent.

The fifth is publish-npm. It sparse-checks out the npm/ directory, copies the top-level README in, pulls the WASM artifact from the WASM job, stamps the version from the tag with npm version, and publishes to npm with NODE_AUTH_TOKEN. Same idempotency trick: a “previously published” error is treated as success.

A sixth job, publish-pi, does the same for the pi/ package and syncs skills/ilo/SKILL.md into it first so the published package carries the canonical skill content.

The site redeploy

ilo-lang.ai is an Astro site. The repo at ilo-lang/site has a two-stage Dockerfile: node:22-alpine builds the static site with npm run build, then nginx:alpine serves the output from /usr/share/nginx/html with a tuned nginx.conf (relative redirects so Cloudflare doesn’t have to rewrite the scheme, /sitemap.xml 301’d to /sitemap-index.xml, immutable cache headers on hashed assets).

Dokploy on the Hetzner box watches the repo. Push to main, Dokploy detects the commit, rebuilds the image, and rolls the container. Cloudflare sits in front for TLS and caching. There’s no GitHub Action involved on the site side.

This works because the build is fully self-contained in the Dockerfile. There’s no vercel.json, no platform-specific config to drift. The same image runs locally if I docker build it.

main to next sync

ilo-lang has two long-running branches. main is 0.12.x, where patches land. next is the 0.13.0 integration branch, where the tree-walker removal and the larger refactors live. Without intervention, every 0.12.x patch that lands on main makes next more divergent, and the eventual merge gets uglier.

The new .github/workflows/sync-next.yml runs on every push to main. It checks out next, attempts git merge --ff-only origin/main, and pushes if the fast-forward succeeds. If next has diverged enough that a fast-forward is impossible, the action opens a sync PR against next with the diff for manual conflict resolution.

The effect is that next is never more than one fast-forward behind main. Patches stop accumulating as a debt against the integration branch. If a real conflict shows up, I see a PR in my queue with the conflict spelled out, instead of finding it weeks later when I try to cut 0.13.0.