forked from bots-garden/ori
| 🎉 Begin a project. | 1 | # `dev-toolkit` kit |
| 2 | ||
| 3 | A Docker Sandboxes **v2 mixin kit** that provisions a general-purpose development toolkit: | |
| 4 | a code-quality engine, pinned Go / Rust / Node toolchains, and three Claude Code skills | |
| 5 | covering code quality, documentation, and a controlled feature-development workflow. | |
| 6 | ||
| 7 | | Tool | Version | Installed at | Notes | | |
| 8 | | --- | --- | --- | --- | | |
| 9 | | qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics engine, for any language. | | |
| 10 | | Go | 1.26.5 | `/usr/local/go` | Symlinked into `/usr/local/bin`, shadowing the template's own `/usr/bin/go`. | | |
| 11 | | TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` set; for `wasm`, `wasip1`, `wasip2` targets. | | |
| 12 | | Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup, `minimal` profile + `clippy` + `rustfmt`, with the `wasm32-wasip2` target. | | |
| 13 | | Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the template's own `/usr/bin/node` (22.x). | | |
| 14 | | pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Plus bundled `corepack`, for projects that pin a version via `packageManager`. | | |
| 15 | | vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions to the Marketplace / Open VSX. | | |
| 16 | | gcc, g++ | Ubuntu's | `/usr/bin` | The base template has **no** C compiler; Rust and node-gyp both need one. | | |
| 17 | ||
| 18 | | Skill | Purpose | | |
| 19 | | --- | --- | | |
| 20 | | `quality` | Configures qlty, measures the project, writes a report, tracks progression, refactors until the gate passes. | | |
| 21 | | `diataxis-doc` | Writes or restructures documentation with the Diátaxis four-quadrant method. | | |
| 22 | | `methodical-dev` | Orchestrator. Drives a feature through requirements → plan → implementation → quality gate → bilingual docs → project memory, stopping for approval at every step. Invokes the other two. | | |
| 23 | ||
| 24 | ||
| 25 | Plus a `~/.claude/CLAUDE.md`, loaded at the start of every session, which brackets the work at | |
| 26 | both ends: **read** the project's committed `.memory/` record — summary, history, latest | |
| 27 | handoff — before planning or asking the user anything, and **update all three** before handing | |
| 28 | control back, unconditionally, however small the session. `summary.md` is edited in place and | |
| 29 | never regenerated from one session's partial view; `history.md` gets exactly one appended | |
| 30 | entry; `handoffs/` gets today's file. `methodical-dev` (Phase 7) holds the full templates — | |
| 31 | `CLAUDE.md` is what makes the same discipline apply to ad-hoc work that never invokes the skill. | |
| 32 | ||
| 33 | **Only the toolchain binaries are language-specific.** qlty is not a Go tool: `qlty init` | |
| 34 | detects the languages present and enables the matching linters, and all three skills are | |
| 35 | language-agnostic. Drop the toolchain install steps from `spec.yaml` and the kit is a pure | |
| 36 | quality/docs/workflow mixin for any stack. The install steps are independent of each other, | |
| 37 | so a Go-only or Rust-only sandbox is a matter of deleting the ones you do not want — except | |
| 38 | the C-toolchain step, which Rust and native npm addons both depend on. | |
| 39 | ||
| 40 | Project-specific skills do **not** belong here. `builtin-parity`, which describes the | |
| 41 | GoloScript repository in particular, lives in the sibling [`golo-toolkit`](../golo-toolkit/) | |
| 42 | kit and is composed in only where it applies. | |
| 43 | ||
| 44 | ## Layout | |
| 45 | ||
| 46 | ``` | |
| 47 | kits/dev-toolkit/ | |
| 48 | ├── spec.yaml # schemaVersion "2", kind: mixin | |
| 49 | ├── README.md | |
| 50 | └── files/home/.claude/ | |
| 51 | ├── CLAUDE.md # loaded every session: read .memory/ first | |
| 52 | └── skills/ | |
| 53 | ├── quality/ | |
| 54 | │ ├── SKILL.md # the workflow the agent follows | |
| 55 | │ └── scripts/quality_report.py # measurement, report, history, gate verdict | |
| 56 | ├── diataxis-doc/SKILL.md | |
| 57 | └── methodical-dev/SKILL.md | |
| 58 | ``` | |
| 59 | ||
| 60 | `files/home/**` is copied into the agent's home at container start, so the skills land | |
| 61 | under `~/.claude/skills/` and Claude Code picks them up as personal skills, and `CLAUDE.md` | |
| 62 | lands at `~/.claude/CLAUDE.md` — the user-level memory file, prepended to **every** Claude | |
| 63 | Code session in the sandbox regardless of which project is open. | |
| 64 | ||
| 65 | **Why both a `CLAUDE.md` and a skill say to read `.memory/`.** `methodical-dev`'s Phase 0 | |
| 66 | only runs when that skill is invoked. `~/.claude/CLAUDE.md` is loaded unconditionally, so an | |
| 67 | ad-hoc request ("fix this bug") also starts by reading the project record instead of | |
| 68 | re-deriving state that is already written down. Keep the file short — it costs tokens on | |
| 69 | every single session; the detail belongs in the skills and in the project's own | |
| 70 | `.memory/README.md`. | |
| 71 | ||
| 72 | Everything in that tree is packed into the kit and shipped into the sandbox, so keep it to | |
| 73 | the skills themselves — a stray `__pycache__/` would be shipped alongside them. The | |
| 74 | repository `.gitignore` guards against that. | |
| 75 | ||
| 76 | ## Usage | |
| 77 | ||
| 78 | ```bash | |
| 79 | # create the sandbox with the kit — `--kit` at create time, not `sbx kit add`, | |
| 80 | # so the cache volumes are attached (see Known limitations) | |
| 81 | sbx run claude --kit ./kits/dev-toolkit | |
| 82 | ``` | |
| 83 | ||
| 84 | Then, in the agent session: *"run a quality report"*, *"document this project with | |
| 85 | Diátaxis"*, or *"let's build this feature methodically"*. | |
| 86 | ||
| 87 | ### Editor extensions | |
| 88 | ||
| 89 | The two ecosystems use different toolchains, and both are provisioned: | |
| 90 | ||
| 91 | | | VS Code | Zed | | |
| 92 | | --- | --- | --- | | |
| 93 | | Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly | | |
| 94 | | Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` + `Cargo.toml` | | |
| 95 | | Build | the project's own script (tsc, esbuild) | `cargo build --release --target wasm32-wasip2` | | |
| 96 | | Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor | | |
| 97 | | Publish | `vsce publish`, `ovsx publish` | a PR to `zed-industries/extensions` | | |
| 98 | ||
| 99 | `wasm32-wasip2` is preinstalled because that is the target Zed compiles extensions with; | |
| 100 | building for anything else produces an artifact it refuses. Both flows were verified inside | |
| 101 | the sandbox end to end — `npm install` → `tsc` → `vsce package` → `.vsix`, and | |
| 102 | `zed_extension_api` → `cargo build --target wasm32-wasip2` → a WASM **component** (layer | |
| 103 | `0100`, not a core module). | |
| 104 | ||
| 105 | What does **not** work here, by construction: no editor runs in the sandbox, so VS Code's F5 | |
| 106 | Extension Development Host and Zed's *Install Dev Extension* are host-side actions, and | |
| 107 | `@vscode/test-electron` needs an X server this kit does not install. Compile, unit-test, lint | |
| 108 | and package in the sandbox; drive the editor on the host. | |
| 109 | ||
| 110 | ## Design notes | |
| 111 | ||
| 112 | **Why a mixin and not a `kind: sandbox` kit.** The kit adds tooling to whatever agent you | |
| 113 | are already using rather than defining its own image, so it composes: `--kit` it onto | |
| 114 | `claude`, `codex`, `shell`, or anything else. Exactly one `sandbox` kit is allowed per | |
| 115 | composition; mixins stack freely. | |
| 116 | ||
| 117 | **Why pinned tarballs instead of the official installers.** `curl https://qlty.sh | sh` | |
| 118 | resolves `latest` (so two sandboxes built a day apart differ), POSTs an install event to | |
| 119 | `cdp.customer.io`, and rewrites the user's shell rc files. Every artifact here is pinned | |
| 120 | by version **and** SHA256, verified with `sha256sum -c` before it is unpacked — the same | |
| 121 | pattern the official `trivy`, `vale`, and `mise` kits use. qlty's own published | |
| 122 | `.sha256` files match the digests in `spec.yaml`. | |
| 123 | ||
| 124 | **Why Python unpacks the qlty archive.** qlty ships only `.tar.xz` and the sandbox | |
| 125 | template has no `xz` binary. Installing `xz-utils` would mean running `apt-get update`, | |
| 126 | which re-fetches *every* configured apt source — including the template's | |
| 127 | `download.docker.com` — and fails if any one of them is not allow-listed. `python3` is | |
| 128 | present, and `lzma` is in its standard library, so the archive is decompressed with no | |
| 129 | extra egress at all. | |
| 130 | ||
| 131 | **Toolchain pairing is load-bearing.** TinyGo 0.41.1 accepts Go 1.19–1.26 *and* refuses a | |
| 132 | GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 satisfies both | |
| 133 | bounds. Bumping Go to 1.27 breaks TinyGo — bump the two together. `GOTOOLCHAIN=local` | |
| 134 | prevents Go from silently downloading a different toolchain behind your back. | |
| 135 | ||
| 136 | **Why a C compiler is a hard requirement, not a nicety.** The base template ships `make` | |
| 137 | but no `cc`, `gcc`, `clang`, `ld` or libc headers. That produces a genuinely misleading | |
| 138 | failure: `cargo build --target wasm32-wasip2` **succeeds** without a C compiler, because | |
| 139 | rustc links wasm targets with its own bundled `rust-lld` — while `cargo test`, which builds | |
| 140 | for the host, dies with ``error: linker `cc` not found``. A Zed extension therefore appears | |
| 141 | to build fine and cannot be unit-tested, which is the worst possible shape for a missing | |
| 142 | dependency. The same gap breaks any crate with a C-compiling `build.rs`, and any npm package | |
| 143 | with a native addon built through node-gyp. `apt-get update` is deliberately *not* run: the | |
| 144 | template ships populated `/var/lib/apt/lists`, so a plain `apt-get install` works, whereas | |
| 145 | `update` re-fetches every configured source (Docker's included) and fails if one is not | |
| 146 | allow-listed. It stays only as a fallback for the day those lists go stale. | |
| 147 | ||
| 148 | **Why rustup rather than the standalone Rust tarballs.** A pinned `rustup-init` *binary* | |
| 149 | (digest-verified, not the `curl https://sh.rustup.rs | sh` bootstrapper, which resolves | |
| 150 | `latest` and edits shell rc files) buys a working `rustup target add` — which matters because | |
| 151 | Zed itself shells out to exactly that when an extension needs a target that is not installed. | |
| 152 | Rust lives under `/usr/local` rather than in the home directory, for the same reason Go does: | |
| 153 | the install runs once at container creation, and only paths baked into the image survive a | |
| 154 | restart. Both `RUSTUP_HOME` and `CARGO_HOME` are left world-writable — the layout the | |
| 155 | official `rust` Docker image uses — so `rustup target add`, `rustup component add` and | |
| 156 | `cargo install` work as the agent user without sudo. | |
| 157 | ||
| 158 | **Why the kit does *not* set npm's prefix.** The obvious move — `npm_config_prefix=/usr/local`, | |
| 159 | so global installs land in `/usr/local/bin` — is wrong here. The base template already exports | |
| 160 | `NPM_CONFIG_PREFIX=/usr/local/share/npm-global`, and that directory is **agent-owned and | |
| 161 | already on PATH ahead of `/usr/local/bin`**. Overriding it would move global installs into a | |
| 162 | root-owned tree and break `npm install -g` for the agent user, which currently works with no | |
| 163 | sudo. The install step passes `--prefix` explicitly (rather than trusting the variable to be | |
| 164 | exported into a root-run step) and then `chown`s the tree back to the agent. | |
| 165 | ||
| 166 | **`--allow-scripts` is not optional for vsce.** npm 11 skips unrecognised lifecycle scripts | |
| 167 | and merely warns. Two of vsce's dependencies need theirs: `keytar` builds the native addon | |
| 168 | that stores a publishing PAT, and `@vscode/vsce-sign` unpacks the signing binary. Skipped, | |
| 169 | they fail at `vsce publish` time — long after the install that caused it. | |
| 170 | ||
| 171 | **The cache volumes.** Three, all of them things that are otherwise re-downloaded on a cold | |
| 172 | start: `~/.qlty` (qlty provisions a JVM for `radarlint-go`, Node, and each linter binary on | |
| 173 | demand), `/usr/local/cargo/registry` (Cargo's `.crate` files and sparse index) and `~/.npm`. | |
| 174 | The Cargo one is deliberately the `registry` subdirectory rather than all of `CARGO_HOME`: | |
| 175 | mounting an empty volume over `CARGO_HOME` would shadow the rustup shims in its `bin`. Volumes | |
| 176 | are fixed at container creation, so `sbx kit add` warns and skips them — use `--kit` at create | |
| 177 | time to get the caches. | |
| 178 | ||
| 179 | ## Maintenance | |
| 180 | ||
| 181 | **Bumping a version.** Change the version *and* both per-arch `SHA256` values in the | |
| 182 | matching `setup.install` entry in `spec.yaml`. Sources: | |
| 183 | ||
| 184 | - Go — <https://go.dev/dl/?mode=json> (`files[].sha256`) | |
| 185 | - TinyGo — compute from the release tarball; the project publishes no checksums file | |
| 186 | - qlty — the `<asset>.tar.xz.sha256` file next to each release asset | |
| 187 | - Node — `https://nodejs.org/dist/v<version>/SHASUMS256.txt`. Pick from the **active LTS** | |
| 188 | line; `https://nodejs.org/dist/index.json` marks it with a non-`false` `lts` field. Note the | |
| 189 | `.tar.gz` digest, not `.tar.xz`: the template has no `xz`, and unlike qlty, Node publishes | |
| 190 | both — which is why this step needs no python-lzma workaround | |
| 191 | - rustup — `https://static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256`, | |
| 192 | with the current version at `https://static.rust-lang.org/rustup/release-stable.toml` | |
| 193 | - Rust itself — no digest to update; rustup resolves and verifies the toolchain. The current | |
| 194 | stable version is the `[pkg.rust] version` in | |
| 195 | `https://static.rust-lang.org/dist/channel-rust-stable.toml` | |
| 196 | - vsce / ovsx / pnpm — `npm view <pkg> version` | |
| 197 | ||
| 198 | **Before bumping the Zed target.** `wasm32-wasip2` is not a preference — it is read from Zed's | |
| 199 | own extension builder (`RUST_TARGET` in `crates/extension/src/extension_builder.rs`). Confirm | |
| 200 | there before changing it; an extension built for another target is one Zed refuses to load. | |
| 201 | ||
| 202 | **Validating a change.** | |
| 203 | ||
| 204 | ```bash | |
| 205 | sbx kit validate ./kits/dev-toolkit | |
| 206 | sbx kit inspect ./kits/dev-toolkit --json | jq '.warnings' # expect null or [] | |
| 207 | ``` | |
| 208 | ||
| 209 | Empty warnings is the green light — a non-empty list means a v1 surface crept in that | |
| 210 | will stop loading at the spec's Phase 6 cutover. | |
| 211 | ||
| 212 | `Install commands completed` only means the commands exited `0`. Verify the outcome: | |
| 213 | ||
| 214 | ```bash | |
| 215 | sbx exec <sandbox> -- sh -lc ' | |
| 216 | go version && tinygo version && qlty --version && | |
| 217 | rustc --version && cargo --version && rustup target list --installed && | |
| 218 | node --version && npm --version && pnpm --version && vsce --version && ovsx --version && | |
| 219 | cc --version | head -1' | |
| 220 | ``` | |
| 221 | ||
| 222 | And verify the thing versions cannot tell you — that host-target Rust actually **links**, | |
| 223 | which is what the C-toolchain step exists for: | |
| 224 | ||
| 225 | ```bash | |
| 226 | sbx exec <sandbox> -- sh -lc ' | |
| 227 | cd "$(mktemp -d)" && cargo init --lib -q . && cargo test 2>&1 | tail -3' | |
| 228 | ``` | |
| 229 | ||
| 230 | **Extending the network allowlist.** `permissions.network.allow` covers the install downloads, | |
| 231 | qlty's plugin/linter resolution, the Go module proxy, Cargo's three hosts (the sparse index | |
| 232 | `index.crates.io`, the CDN `static.crates.io`, and the API `crates.io` — all three are needed), | |
| 233 | Ubuntu's archives for the C toolchain, and the VS Code Marketplace / Open VSX publishing paths. | |
| 234 | A linter for another language will need its registry added. Find out what is missing rather | |
| 235 | than guessing: | |
| 236 | ||
| 237 | ```bash | |
| 238 | sbx policy log <sandbox> | |
| 239 | ``` | |
| 240 | ||
| 241 | That prints what the proxy actually allowed and blocked. Add blocked hosts one at a time. | |
| 242 | ||
| 243 | ## Known limitations | |
| 244 | ||
| 245 | - **`sbx kit add` does not write mixin context.** The engine gates the kit-memory write on | |
| 246 | the artifact's own AI-profile filename, which mixins deliberately do not carry (`agentInstructions.filename` is ignored for a mixin), so adding this | |
| 247 | kit at runtime silently skips `kits-memory/dev-toolkit.md`. Create the sandbox with | |
| 248 | `--kit` instead. The same applies to the `~/.qlty` volume. | |
| 249 | - **The skills and `CLAUDE.md` are Claude Code-specific.** The toolchain is not. The kit sets | |
| 250 | no `requires.agent`, so it works with any agent; on non-Claude agents those files are inert | |
| 251 | and `agentInstructions` carries the same instructions — the `.memory/`-first rule and the | |
| 252 | `quality` script invocation. | |
| 253 | - **Only `linux/amd64` and `linux/arm64`** are supported. Other architectures fail the | |
| 254 | install with an explicit error rather than installing something wrong. | |
| 255 | - **No editor, and no display.** Nothing in this kit can run VS Code or Zed. The | |
| 256 | Extension Development Host (F5), Zed's *Install Dev Extension*, and any | |
| 257 | `@vscode/test-electron` run all need a GUI session on the user's machine; there is no X | |
| 258 | server here and the kit does not install `xvfb`. The sandbox covers compile, unit-test, lint | |
| 259 | and package — write extensions so their logic is testable without the `vscode` module and | |
| 260 | the split costs you nothing. | |
| 261 | - **Tree-sitter grammars in Zed extensions are not reproducible here.** Compiling a parser | |
| 262 | needs the wasi-sdk, which Zed downloads itself, on the host. The Rust half of such an | |
| 263 | extension builds fine; the grammar half does not happen in this sandbox. | |
| 264 | - **The C toolchain is the one unpinned install.** Go, TinyGo, qlty, Node and rustup are all | |
| 265 | version-and-digest pinned; `gcc` comes from Ubuntu's archives at whatever version they serve | |
| 266 | (15.2.0 at time of writing). Pinning it would mean pinning a `.deb` set and its transitive | |
| 267 | closure, which is a much larger commitment than it is worth for a linker driver — but it does | |
| 268 | mean two sandboxes built months apart can differ here. |