nandi/oripublic Fork 0
4166f8fba899b222fab287c398dcab9bf6f6e5c9
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

spec.yaml · 603 lines · 33.3 KBYAML Blame HistoryRaw
🎉 Begin a project. 4edda86 k33g yesterday1schemaVersion: "2"
2kind: mixin
3name: dev-toolkit
4displayName: Development toolkit (Go + Rust + Node + qlty + skills)
5description: >
6 A general-purpose development toolkit. Installs the qlty 0.639.0 code-quality
7 engine, which analyses whatever languages a project actually uses, plus three
8 pinned language toolchains: Go 1.26.5 with TinyGo 0.41.1 for WebAssembly / WASI
9 builds, Rust 1.97.1 via rustup with the `wasm32-wasip2` target (what Zed
10 compiles extensions to), and Node.js 24.19.0 with npm, pnpm and the
11 `@vscode/vsce` / `ovsx` extension packagers. Ships four Claude Code skills:
12 `quality` (self-configures qlty, measures the project, writes a Markdown report,
13 tracks progression across runs, and drives refactoring until the quality gate
14 passes), `diataxis-doc` (documents a project with the Diátaxis four-quadrant
15 method), `methodical-dev` (step-by-step, approval-gated feature development
16 that orchestrates the other two, and keeps a committed `.memory/` project
17 record), plus a `~/.claude/CLAUDE.md` that makes every session start by reading
18 that record, and `turbo-new-editor` (builds a complete terminal IDE for a new
19 language on top of the `turbo-core` library, inside `methodical-dev`). Only the
20 toolchain binaries are language-specific; the quality, documentation and
21 workflow skills are not.
22licenses:
23 - MIT
24
25# Deliberately no `requires.agent`: the toolchain is agent-agnostic. The skills
26# under files/home/.claude/skills/ and files/home/.claude/CLAUDE.md are
27# Claude-Code-specific and are simply inert on other agents, which still get the
28# quality workflow and the `.memory/` convention through `agentInstructions` below.
29
30permissions:
31 network:
32 allow:
33 # ---- install-time, one-shot -------------------------------------------
34 # Go toolchain tarball. dl.google.com is the canonical host go.dev/dl
35 # redirects to; hitting it directly keeps the allowlist to one entry.
36 - dl.google.com
37 # TinyGo release tarball. github.com 302-redirects binary downloads to
38 # release-assets.githubusercontent.com (covered by the wildcard below).
39 - github.com
40 # Matches exactly one DNS label, so this single entry covers
41 # release-assets. / objects. / raw.githubusercontent.com.
42 - "*.githubusercontent.com"
43 # qlty CLI tarball (and `qlty upgrade`, if ever run by hand).
44 - qlty-releases.s3.amazonaws.com
45 # The C/C++ toolchain comes from Ubuntu's own archives. ports.* serves
46 # arm64, archive.*/security.* serve amd64 — which one is used depends on the
47 # sandbox architecture, so all three are listed.
48 - ports.ubuntu.com
49 - archive.ubuntu.com
50 - security.ubuntu.com
51 # Only reached if the cached apt lists ever go stale and the fallback
52 # `apt-get update` runs: it re-fetches *every* configured source, and the
53 # template configures Docker's as well as Ubuntu's.
54 - download.docker.com
55 # `rustup-init`, plus every toolchain component rustup resolves. One host
56 # serves both the rustup releases and the dist channel, so this covers the
57 # install *and* later `rustup target add` / `rustup component add`.
58 - static.rust-lang.org
59 # The Node.js tarball comes from nodejs.org, already listed under "qlty
60 # runtime" below — qlty provisions its own Node for the JS/TS linters, so the
61 # host is needed either way.
62
63 # ---- qlty runtime ------------------------------------------------------
64 # qlty resolves its default plugin source (qltysh/qlty-plugins) and every
65 # linter release through the GitHub API, its own GitHub proxy, and
66 # codeload for repository tarballs.
67 - api.github.com
68 - github-proxy.qlty.sh
69 - codeload.github.com
70 # radarlint-go — enabled by `qlty init` on Go repos; ships from its own
71 # bucket and pulls a Temurin JVM from GitHub releases.
72 - radarlint-releases.s3.amazonaws.com
73 # osv-scanner — also enabled by `qlty init` on Go repos.
74 - api.osv.dev
75 - osv-vulnerabilities.storage.googleapis.com
76 # Language runtimes qlty provisions on demand for non-Go linters. Trim
77 # these if you only ever lint Go — except nodejs.org, which this kit's own
78 # Node install step also needs.
79 - nodejs.org
80 - registry.npmjs.org
81 - pypi.org
82 - files.pythonhosted.org
83
84 # ---- Go module resolution ---------------------------------------------
85 - proxy.golang.org
86 - sum.golang.org
87
88 # ---- Cargo dependency resolution --------------------------------------
89 # Three distinct hosts, all needed: the sparse index cargo reads by
90 # default, the CDN the .crate files come from, and the API `cargo add` /
91 # `cargo search` / `cargo publish` talk to. Git dependencies go through
92 # github.com, already allowed above.
93 - index.crates.io
94 - static.crates.io
95 - crates.io
96
97 # ---- VS Code extension tooling ----------------------------------------
98 # `vsce publish` / `vsce ls-publishers`, and the gallery query API that
99 # `@vscode/test-electron` uses to resolve extension dependencies.
100 - marketplace.visualstudio.com
101 # Extension asset (.vsix) download. Each publisher gets its own subdomain,
102 # and the wildcard matches exactly one DNS label, so both forms are needed.
103 - "*.gallery.vsassets.io"
104 - "*.gallerycdn.vsassets.io"
105 # `@vscode/test-electron` asks this for the build to test against, then
106 # downloads the archive from Microsoft's CDN.
107 - update.code.visualstudio.com
108 - vscode.download.prss.microsoft.com
109 # `ovsx publish` — the Open VSX registry, which is where a VS Code
110 # extension has to go to be installable in VSCodium, Cursor or Zed.
111 - open-vsx.org
112
113environment:
114 variables:
115 # Pin the toolchain we actually installed. Without this, Go 1.21+ silently
116 # downloads whatever `go`/`toolchain` line go.mod names, which would defeat
117 # the point of pinning 1.26.5 (and needs egress we haven't allowed). If a
118 # go.mod legitimately requires something newer, this surfaces as a clear
119 # error instead of a surprise download.
120 GOTOOLCHAIN: local
121 # TinyGo resolves its root from the executable path, which already works for
122 # the tarball layout. Setting it explicitly wins over any value baked in at
123 # release-build time, so `tinygo` keeps working when invoked via the
124 # /usr/local/bin symlink.
125 TINYGOROOT: /usr/local/tinygo
126 # qlty phones home to cdp.customer.io on every command otherwise. That host
127 # is not allow-listed above, so the calls would fail anyway — this turns
128 # them off at the source instead of relying on the firewall.
129 QLTY_TELEMETRY: "off"
130 # Rust lives under /usr/local rather than in the agent's home, for the same
131 # reason Go and TinyGo do: the install runs once at container creation, and
132 # only paths baked into the image are guaranteed to survive a restart. Both
133 # directories are made world-writable by the install step, so `rustup target
134 # add`, `rustup component add` and `cargo install` all work as the agent
135 # user — this is the layout the official `rust` Docker image uses.
136 RUSTUP_HOME: /usr/local/rustup
137 CARGO_HOME: /usr/local/cargo
138 # Deliberately NOT setting npm's prefix here. The base template already
139 # exports NPM_CONFIG_PREFIX=/usr/local/share/npm-global, and that directory is
140 # agent-owned and already on PATH ahead of /usr/local/bin — so `npm install -g`
141 # works for the agent user with no sudo. Overriding it to /usr/local would
142 # move global installs into a root-owned tree and break exactly that. The
143 # install step below installs its pinned CLIs into the template's prefix and
144 # hands ownership back to the agent.
145
146# Package caches. Each of these is repopulated over the network on a cold start,
147# so persisting them across container restarts turns minutes into a no-op.
148# Volumes are fixed at container creation: `sbx kit add` warns and skips them, so
149# use `--kit` at create time to get the caches.
150volumes:
151 # qlty downloads a JVM (for radarlint), Node, and every linter binary into
152 # ~/.qlty/cache.
153 - path: /home/agent/.qlty
154 # Cargo's downloaded .crate files and its copy of the sparse index.
155 # Deliberately the `registry` subdirectory rather than all of CARGO_HOME:
156 # mounting an empty volume over CARGO_HOME would shadow the rustup shims that
157 # the install step puts in its `bin`.
158 - path: /usr/local/cargo/registry
159 # npm's content-addressable cache (`npm config get cache`).
160 - path: /home/agent/.npm
161
162setup:
163 install:
164 # Go 1.26.5, version + digest pinned. The base template already ships a
165 # /usr/bin/go (1.26.0 at time of writing); /usr/local/bin precedes /usr/bin
166 # on PATH, so the symlinks below shadow it without removing anything.
167 # To bump: change GO_VERSION and both SHA256 values (from
168 # https://go.dev/dl/?mode=json).
169 - command: |
170 set -eu
171 GO_VERSION=1.26.5
172 ARCH=$(dpkg --print-architecture)
173 case "$ARCH" in
174 amd64) SHA256=5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053 ;;
175 arm64) SHA256=fe4789e92b1f33358680864bbe8704289e7bb5fc207d80623c308935bd696d49 ;;
176 *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;;
177 esac
178 if [ "$(/usr/local/go/bin/go version 2>/dev/null | awk '{print $3}')" != "go${GO_VERSION}" ]; then
179 URL="https://dl.google.com/go/go${GO_VERSION}.linux-${ARCH}.tar.gz"
180 curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/go.tgz "$URL"
181 echo "${SHA256} /tmp/go.tgz" | sha256sum -c -
182 rm -rf /usr/local/go
183 tar -C /usr/local -xzf /tmp/go.tgz
184 rm -f /tmp/go.tgz
185 fi
186 ln -sf /usr/local/go/bin/go /usr/local/bin/go
187 ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
188 /usr/local/bin/go version
189 user: "0"
190 description: "Install Go 1.26.5 (version + digest pinned), shadowing the image's Go"
191
192 # TinyGo 0.41.1. Its supported range is Go 1.19–1.26 and the published
193 # binary is itself built with go1.26.2, so it accepts the Go 1.26.5 above
194 # (TinyGo also refuses a GOROOT newer than its own build toolchain).
195 # Bumping Go to 1.27 would break this pairing — bump TinyGo together with it.
196 - command: |
197 set -eu
198 TINYGO_VERSION=0.41.1
199 ARCH=$(dpkg --print-architecture)
200 case "$ARCH" in
201 amd64) SHA256=e156d1d93a376eef639a4143d13be07e8c463fb6cf2d7d447698ed4474d23e91 ;;
202 arm64) SHA256=789733bc3b5bace0bd1835a267b3ea267804a7ef1cfe69bc522c295f5226d624 ;;
203 *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;;
204 esac
205 if [ "$(/usr/local/tinygo/bin/tinygo version 2>/dev/null | awk '{print $3}')" != "$TINYGO_VERSION" ]; then
206 TARBALL="tinygo${TINYGO_VERSION}.linux-${ARCH}.tar.gz"
207 URL="https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/${TARBALL}"
208 curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/tinygo.tgz "$URL"
209 echo "${SHA256} /tmp/tinygo.tgz" | sha256sum -c -
210 rm -rf /usr/local/tinygo
211 tar -C /usr/local -xzf /tmp/tinygo.tgz
212 rm -f /tmp/tinygo.tgz
213 fi
214 ln -sf /usr/local/tinygo/bin/tinygo /usr/local/bin/tinygo
215 /usr/local/bin/tinygo version
216 user: "0"
217 description: "Install TinyGo 0.41.1 (version + digest pinned)"
218
219 # qlty 0.639.0, pulled straight from the release bucket rather than through
220 # `curl https://qlty.sh | sh`. The install script resolves "latest" (not
221 # reproducible), POSTs an install event to cdp.customer.io, and rewrites the
222 # user's shell rc files — none of which belongs in a sandbox image.
223 # The asset is .tar.xz and the base image has no `xz`, so fall back to
224 # Python's lzma (stdlib, and python3 is present) instead of pulling xz-utils
225 # over apt. This step deliberately stays apt-free: it runs *before* the
226 # C-toolchain step below, so it cannot assume anything about apt having been
227 # exercised yet, and a self-contained step is one less ordering dependency.
228 # To bump: change QLTY_VERSION and both SHA256 values (published as
229 # <asset>.sha256 next to each release asset).
230 - command: |
231 set -eu
232 QLTY_VERSION=0.639.0
233 ARCH=$(dpkg --print-architecture)
234 case "$ARCH" in
235 amd64)
236 TARGET=x86_64-unknown-linux-gnu
237 SHA256=49f8299d25a1c50cf02af8263483b1a04b8b665d71c5b6512847e08ccb392a90
238 ;;
239 arm64)
240 TARGET=aarch64-unknown-linux-gnu
241 SHA256=8f2f5cd7eb06c3f1ba106866bbf80cfb563a06d70ee495c14391dced37d05a4e
242 ;;
243 *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;;
244 esac
245 if [ "$(/usr/local/bin/qlty --version 2>/dev/null | awk '{print $2}')" != "$QLTY_VERSION" ]; then
246 URL="https://qlty-releases.s3.amazonaws.com/qlty/v${QLTY_VERSION}/qlty-${TARGET}.tar.xz"
247 curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/qlty.tar.xz "$URL"
248 echo "${SHA256} /tmp/qlty.tar.xz" | sha256sum -c -
249 rm -rf "/tmp/qlty-${TARGET}"
250 if command -v xz >/dev/null 2>&1; then
251 tar -C /tmp -xJf /tmp/qlty.tar.xz "qlty-${TARGET}/qlty"
252 else
253 python3 -c 'import lzma,sys; sys.stdout.buffer.write(lzma.open(sys.argv[1],"rb").read())' \
254 /tmp/qlty.tar.xz | tar -C /tmp -xf - "qlty-${TARGET}/qlty"
255 fi
256 install -m 0755 "/tmp/qlty-${TARGET}/qlty" /usr/local/bin/qlty
257 rm -rf /tmp/qlty.tar.xz "/tmp/qlty-${TARGET}"
258 fi
259 /usr/local/bin/qlty --version
260 user: "0"
261 description: "Install qlty CLI 0.639.0 (version + digest pinned, no install script)"
262
263 # A C/C++ toolchain. Not optional, and not obvious: the base template ships
264 # `make` but **no `cc`, `gcc`, `clang`, `ld` or libc headers at all**. Without
265 # this step:
266 # - `cargo build` / `cargo test` for the host target fail with
267 # "error: linker `cc` not found" (measured). Cross-compiling to
268 # wasm32-wasip2 still works, because rustc uses its own bundled rust-lld
269 # for wasm targets — so a Zed extension builds but cannot be unit-tested.
270 # - any crate with a `build.rs` that compiles C, and any npm package with a
271 # native addon built through node-gyp, fails the same way.
272 # `apt-get update` is deliberately NOT run: the template ships populated
273 # /var/lib/apt/lists, so a plain install works, and `update` would re-fetch
274 # every configured source (including download.docker.com) and fail if any one
275 # of them is not allow-listed. It is kept only as a fallback for the day those
276 # cached lists go stale.
277 - command: |
278 set -eu
279 if ! command -v cc >/dev/null 2>&1; then
280 export DEBIAN_FRONTEND=noninteractive
281 PKGS="gcc g++ libc6-dev pkg-config"
282 # `-o DPkg::Lock::Timeout` is not optional here. The claude kit registers
283 # a *backgrounded* `apt-get update` as a startup command, so at the moment
284 # this step runs (~11s into the install sequence) that update may still
285 # hold the dpkg frontend lock. Without the timeout apt does not wait — it
286 # gives up at once with exit 100, and the whole kit apply fails.
287 APT="apt-get -o DPkg::Lock::Timeout=300"
288 $APT install -y --no-install-recommends $PKGS \
289 || { $APT update && $APT install -y --no-install-recommends $PKGS; }
290 fi
291 cc --version | head -1
292 c++ --version | head -1
293 user: "0"
294 description: "Install a C/C++ toolchain (gcc, g++, libc6-dev, pkg-config) — required to link host-target Rust builds and native npm addons"
295
296 # Node.js 24.19.0 — "Krypton", the active LTS line. The base template ships a
297 # /usr/bin/node (22.22.1 at time of writing); as with Go, /usr/local/bin
298 # precedes /usr/bin on PATH, so the symlinks shadow it without removing it.
299 # The .tar.gz is used rather than the (smaller) .tar.xz on purpose: the base
300 # image has no `xz`, and unlike qlty, Node publishes both formats — so this
301 # avoids the python-lzma workaround the qlty step needs.
302 # To bump: change NODE_VERSION and both SHA256 values, from
303 # https://nodejs.org/dist/v<version>/SHASUMS256.txt.
304 - command: |
305 set -eu
306 NODE_VERSION=24.19.0
307 ARCH=$(dpkg --print-architecture)
308 case "$ARCH" in
309 amd64) NODE_ARCH=x64; SHA256=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 ;;
310 arm64) NODE_ARCH=arm64; SHA256=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f ;;
311 *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;;
312 esac
313 if [ "$(/usr/local/node/bin/node --version 2>/dev/null)" != "v${NODE_VERSION}" ]; then
314 TARBALL="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
315 URL="https://nodejs.org/dist/v${NODE_VERSION}/${TARBALL}"
316 curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/node.tgz "$URL"
317 echo "${SHA256} /tmp/node.tgz" | sha256sum -c -
318 rm -rf /usr/local/node
319 mkdir -p /usr/local/node
320 tar -C /usr/local/node --strip-components=1 -xzf /tmp/node.tgz
321 rm -f /tmp/node.tgz
322 fi
323 for b in node npm npx corepack; do
324 ln -sf "/usr/local/node/bin/$b" "/usr/local/bin/$b"
325 done
326 # Global installs go to the template's own prefix, which is already on
327 # PATH. --prefix is passed explicitly rather than trusting NPM_CONFIG_PREFIX
328 # to be exported into this root-run step.
329 NPM_PREFIX=/usr/local/share/npm-global
330 mkdir -p "$NPM_PREFIX"
331 # Pinned, and deliberately only the tools that belong to the *sandbox*
332 # rather than to a project: the two extension packagers, and pnpm. A
333 # project's TypeScript, esbuild and test runner belong in its own
334 # devDependencies, where the version is committed alongside the code.
335 #
336 # --allow-scripts is required by npm 11: it otherwise skips these two
337 # lifecycle scripts and only warns. keytar builds the native addon vsce
338 # uses to store a publishing PAT, and vsce-sign unpacks the signing
339 # binary — both are needed by `vsce publish`, and both fail silently
340 # (at publish time, not now) if their scripts never run.
341 /usr/local/bin/npm install -g --prefix "$NPM_PREFIX" --no-fund --no-audit \
342 --allow-scripts=@vscode/vsce-sign,keytar \
343 @vscode/vsce@3.9.2 \
344 ovsx@1.1.1 \
345 pnpm@11.22.0
346 # Root just wrote into an agent-owned tree; hand it back so the agent can
347 # install or update globals later without sudo.
348 chown -R agent:agent "$NPM_PREFIX"
349 /usr/local/bin/node --version
350 /usr/local/bin/npm --version
351 "$NPM_PREFIX/bin/vsce" --version
352 user: "0"
353 description: "Install Node.js 24.19.0 LTS (digest pinned) + pinned vsce, ovsx, pnpm"
354
355 # Rust 1.97.1 via a digest-pinned `rustup-init` *binary* — not the
356 # `curl https://sh.rustup.rs | sh` bootstrapper, which resolves "latest" and
357 # rewrites the user's shell rc files. rustup rather than the standalone
358 # tarballs because it is what Zed shells out to when an extension needs a
359 # target that is not installed yet (`rustup target add wasm32-wasip2`).
360 # `--profile minimal` skips the ~500 MB of offline docs; clippy and rustfmt
361 # are added explicitly because qlty's Rust plugins invoke them.
362 # To bump: change RUSTUP_VERSION and both SHA256 values (published at
363 # static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256),
364 # and/or RUST_VERSION on its own (rustup resolves that one itself).
365 - command: |
366 set -eu
367 RUSTUP_VERSION=1.29.0
368 RUST_VERSION=1.97.1
369 ARCH=$(dpkg --print-architecture)
370 case "$ARCH" in
371 amd64)
372 TRIPLE=x86_64-unknown-linux-gnu
373 SHA256=4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10
374 ;;
375 arm64)
376 TRIPLE=aarch64-unknown-linux-gnu
377 SHA256=9732d6c5e2a098d3521fca8145d826ae0aaa067ef2385ead08e6feac88fa5792
378 ;;
379 *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;;
380 esac
381 export RUSTUP_HOME=/usr/local/rustup
382 export CARGO_HOME=/usr/local/cargo
383 if [ "$(/usr/local/cargo/bin/rustc --version 2>/dev/null | awk '{print $2}')" != "$RUST_VERSION" ]; then
384 URL="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${TRIPLE}/rustup-init"
385 curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/rustup-init "$URL"
386 echo "${SHA256} /tmp/rustup-init" | sha256sum -c -
387 chmod 0755 /tmp/rustup-init
388 # --no-modify-path: PATH comes from the /usr/local/bin symlinks below,
389 # not from an appended line in a shell rc file.
390 /tmp/rustup-init -y --no-modify-path --profile minimal \
391 --default-toolchain "$RUST_VERSION" \
392 -c clippy -c rustfmt \
393 -t wasm32-wasip2
394 rm -f /tmp/rustup-init
395 fi
396 for b in rustc cargo rustup rustfmt cargo-fmt cargo-clippy clippy-driver; do
397 ln -sf "/usr/local/cargo/bin/$b" "/usr/local/bin/$b"
398 done
399 # The agent runs as a non-root user, and `rustup target add`,
400 # `rustup component add` and `cargo install` all write inside these two
401 # trees. Same approach as the official `rust` image: make them writable
402 # rather than forcing every Rust command through sudo.
403 chmod -R a+w /usr/local/rustup /usr/local/cargo
404 /usr/local/bin/rustc --version
405 /usr/local/bin/cargo --version
406 /usr/local/bin/rustup target list --installed
407 user: "0"
408 description: "Install Rust 1.97.1 via digest-pinned rustup-init, with clippy, rustfmt and the wasm32-wasip2 target"
409
410 startup:
411 # Every cache volume mounts with a root-owned root because the runtime formats
412 # block volumes as ext4, which shadows the image's directory ownership.
413 # Without this the agent user cannot populate the caches — and for
414 # /usr/local/cargo/registry the failure is a confusing "permission denied"
415 # from the middle of a `cargo build`, not from anything the agent typed.
416 # Idempotent by construction — startup commands run on every container start.
417 - command:
418 - "sh"
419 - "-c"
420 - |
421 for d in /home/agent/.qlty /home/agent/.npm; do
422 mkdir -p "$d" && chown -R agent:agent "$d" 2>/dev/null || true
423 done
424 # Left group/world-writable rather than chowned, to stay consistent
425 # with the `chmod -R a+w` the Rust install applies to the rest of
426 # CARGO_HOME: root installs into it, the agent writes to it.
427 mkdir -p /usr/local/cargo/registry && chmod -R a+w /usr/local/cargo/registry 2>/dev/null || true
428 user: "0"
429 description: "Re-own the qlty, npm and cargo cache volumes so the agent user can write to them"
430
431agentInstructions:
432 # `filename` is deliberately absent: it is ignored (with a warning) for a
433 # mixin, whose content the engine writes to kits-memory/<kit-name>.md.
434 content: |
435 ## Development toolkit (kit `dev-toolkit`)
436
437 This sandbox has a pinned toolchain installed at container creation:
438
439 | Tool | Version | Location | Notes |
440 | -------- | ------- | ------------------- | ----- |
441 | qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics, for **any** language the project uses. |
442 | Go | 1.26.5 | `/usr/local/go` | Symlinked to `/usr/local/bin/go`; shadows the image's own `/usr/bin/go`. |
443 | TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` is set; use for `wasm`, `wasip1`, `wasip2` targets. |
444 | Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup. `clippy`, `rustfmt` and the `wasm32-wasip2` target are installed. |
445 | Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the image's own `/usr/bin/node` (22.x). |
446 | npm | 11.17.0 (bundled) | `/usr/local/bin/npm` | Globals go to `/usr/local/share/npm-global`, agent-owned and on PATH. |
447 | pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Also `corepack`, if a project pins a different version via `packageManager`. |
448 | vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions (Marketplace / Open VSX). |
449 | gcc / g++ | Ubuntu's | `/usr/bin` | Installed by this kit — the base image has **no** C compiler at all. |
450
451 Verify with
452 `qlty --version && go version && tinygo version && rustc --version && node --version`.
453
454 qlty is **not** language-specific: `qlty init` detects the languages present
455 and enables the matching linters (on a Go repo that is gofmt, golangci-lint,
456 osv-scanner, radarlint-go, ripgrep and trufflehog; on a TypeScript or Rust repo
457 it is an entirely different set). Only the toolchain binaries above are
458 language-specific — the three skills are not.
459
460 ### Toolchain constraints worth knowing
461
462 - `GOTOOLCHAIN=local`. Go will **not** auto-download a different toolchain.
463 If a build fails with "go.mod requires go >= X", that is real — either lower
464 the requirement or bump this kit's pinned Go and TinyGo together.
465 - TinyGo 0.41.1 supports Go 1.19 through 1.26 **and** refuses a GOROOT newer
466 than the Go it was itself built with (1.26.2). Go 1.26.5 is inside both
467 bounds. Do not bump Go past 1.26 without bumping TinyGo.
468 - `QLTY_TELEMETRY=off`, and qlty's telemetry host is not allow-listed. Pass
469 `--no-upgrade-check` to qlty commands to skip its update probe as well.
470 - Rust lives under `/usr/local`, not in the home directory, and both
471 `RUSTUP_HOME` and `CARGO_HOME` are world-writable — so `rustup target add`,
472 `rustup component add` and `cargo install` work **without** sudo. Do not
473 re-run `rustup-init`; if a toolchain or target is missing, add it with
474 `rustup`.
475 - `wasm32-wasip2` is the only cross-compilation target preinstalled. Others
476 (`wasm32-unknown-unknown` for wasm-bindgen, say) are one
477 `rustup target add <target>` away — `static.rust-lang.org` is allow-listed.
478 - Rust needs a C linker for **host** builds, and the base image ships none —
479 this kit installs `gcc`/`g++` for that reason. If you ever see
480 "error: linker `cc` not found", the C toolchain step did not run; that is a
481 kit problem, not a project problem. Note the asymmetry:
482 `cargo build --target wasm32-wasip2` works *without* `cc` (rustc uses its own
483 bundled `rust-lld` for wasm), so a missing compiler breaks `cargo test` while
484 leaving the wasm build green.
485 - Prefer a project `devDependency` plus `npx` over `npm install -g`: the
486 version then lives in the repository instead of in this container. Globals do
487 work without sudo if you need them.
488
489 ### Building editor extensions
490
491 Both extension ecosystems are supported, and they use different toolchains:
492
493 | | VS Code | Zed |
494 | --- | --- | --- |
495 | Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly |
496 | Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` (+ `Cargo.toml`) |
497 | Build | the project's own script — `npm run compile`, usually tsc or esbuild | `cargo build --release --target wasm32-wasip2` |
498 | Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor |
499 | Publish | `vsce publish` (Marketplace), `ovsx publish` (Open VSX) | a PR to `zed-industries/extensions` |
500
501 Two things that genuinely do not work in this sandbox, so plan around them
502 rather than debugging them:
503
504 - **No editor runs here.** There is no display, so neither VS Code's F5
505 "Extension Development Host" nor Zed's *Install Dev Extension* can be used
506 from inside the sandbox. Both are host-side actions on the user's machine.
507 What you *can* do here is compile, unit-test, lint and package.
508 - **`@vscode/test-electron` needs a display.** Its download hosts are
509 allow-listed and `--extensionDevelopmentPath` runs will fetch a build, but
510 launching it needs an X server (`xvfb-run`), which this kit does not install.
511 Write the extension so its logic is testable without the `vscode` module —
512 keep the `vscode` API calls in a thin activation layer and unit-test the rest
513 — and say so plainly rather than reporting an untested integration path as
514 verified.
515
516 For a Zed extension, `wasm32-wasip2` is the target Zed itself compiles with;
517 building for anything else produces an artifact Zed will refuse. Extensions
518 that ship Tree-sitter **grammars** additionally need the wasi-sdk to compile
519 the parsers — Zed downloads that itself, on the host, so grammar compilation
520 is not reproducible inside this sandbox either.
521
522 ### Checking code quality
523
524 Use the **`quality` skill** (`~/.claude/skills/quality/SKILL.md`) rather than
525 invoking qlty ad hoc. It configures qlty on first use, measures the project,
526 writes a timestamped Markdown report under `.quality/`, records a history line
527 so progression across runs is visible, and then drives refactoring until the
528 gate passes.
529
530 Non-Claude agents: run
531 `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .`
532 to get the same report and history, then refactor against its findings.
533
534 ### Other skills shipped by this kit
535
536 They live under `~/.claude/skills/` and are picked up automatically:
537
538 - **`diataxis-doc`** — writes or restructures project documentation with the
539 Diátaxis method (tutorials / how-to / reference / explanation). It asks which
540 language to produce the docs in before writing anything, unless the invoking
541 skill has already fixed it.
542 - **`methodical-dev`** — drives a new feature through gathering requirements,
543 planning, and implementation, stopping for approval after every step. It is the
544 **orchestrator**: it invokes `quality` and `diataxis-doc` as mandatory closing
545 phases, requires unit tests in the same step as the code they cover, and keeps a
546 committed `.memory/` record (summary, append-only history, handoffs) so work can
547 be resumed in a fresh sandbox. Start here for any non-trivial change.
548 - **`turbo-new-editor`** — builds a complete terminal IDE for one language,
549 `turbo-<language>`, on top of the `turbo-core` library: a command, a profile
550 and a syntax scanner, with everything else — windows, menus, dialogs, themes,
551 the LSP client, the terminal emulator — coming from the library. It runs
552 *inside* `methodical-dev` rather than replacing it, and takes the language and
553 the path of a repository the user has already created.
554
555 ### `.memory/` — the project record (read it first)
556
557 This kit also installs `~/.claude/CLAUDE.md`, which Claude Code loads at the start
558 of **every** session: it says to read `.memory/` before planning or asking the user
559 anything. On non-Claude agents that file is inert — the convention below applies
560 anyway.
561
562 `methodical-dev` maintains `.memory/` at the repository root, **committed, not
563 gitignored**:
564
565 | Path | Contents |
566 | ---- | -------- |
567 | `summary.md` | Living snapshot: architecture, decisions in force, build/test commands. Edited in place. |
568 | `history.md` | Append-only dated log, one entry per completed feature. Never rewritten. |
569 | `handoffs/` | Per-session handoff: state, work in flight, next steps, blockers. |
570
571 If `.memory/` exists, **read `summary.md` and the latest handoff before starting
572 work** — it records the project's state and any unfinished business, and saves you
573 from re-deriving it or asking the user what is already written down.
574
575 **Every session also ends by bringing all three files up to date** — no exceptions,
576 including sessions that fixed one line, only read code, or achieved nothing (those
577 last stop the next agent hitting the same wall):
578
579 - `summary.md` — **edit in place, never regenerate.** Change only what your session
580 establishes or invalidates and leave the rest byte-for-byte alone; rewriting it
581 wholesale from one session's slice destroys accurate content you never looked at.
582 Only from what you verified, never a plausible guess; what you could not check goes
583 under an explicit `## Not yet established` heading. If the project's state did not
584 change, leave the file untouched.
585 - `history.md` — **append exactly one dated entry.** Never rewrite or delete an
586 existing one.
587 - `handoffs/YYYY-MM-DD-<slug>.md` — write today's, or update it if it already covers
588 this topic. Never overwrite another session's handoff.
589
590 Match the length to the work: a one-line fix earns a few lines, not a filled-in
591 template — padding with "N/A" makes the record unreadable. Record what the next
592 person cannot re-derive (where you stopped, what failed, the trap, the pending
593 decision), not a diff summary. If `.memory/` does not exist yet, create the whole
594 skeleton: `README.md`, `summary.md`, `history.md` and `handoffs/`.
595
596 Keep the boundary clean: `docs/` is for people *using* the project, `.memory/` is
597 for whoever *continues building* it.
598
599 Two things that make the measurement meaningless — never do either:
600
601 - Do not add `exclude_patterns`, disable a plugin, or raise a threshold in
602 `.qlty/qlty.toml` to make the gate pass. Fix the code.
603 - Do not delete or skip tests to clear a finding.