schemaVersion: "2" kind: mixin name: dev-toolkit displayName: Development toolkit (Go + Rust + Node + qlty + skills) description: > A general-purpose development toolkit. Installs the qlty 0.639.0 code-quality engine, which analyses whatever languages a project actually uses, plus three pinned language toolchains: Go 1.26.5 with TinyGo 0.41.1 for WebAssembly / WASI builds, Rust 1.97.1 via rustup with the `wasm32-wasip2` target (what Zed compiles extensions to), and Node.js 24.19.0 with npm, pnpm and the `@vscode/vsce` / `ovsx` extension packagers. Ships four Claude Code skills: `quality` (self-configures qlty, measures the project, writes a Markdown report, tracks progression across runs, and drives refactoring until the quality gate passes), `diataxis-doc` (documents a project with the Diátaxis four-quadrant method), `methodical-dev` (step-by-step, approval-gated feature development that orchestrates the other two, and keeps a committed `.memory/` project record), plus a `~/.claude/CLAUDE.md` that makes every session start by reading that record, and `turbo-new-editor` (builds a complete terminal IDE for a new language on top of the `turbo-core` library, inside `methodical-dev`). Only the toolchain binaries are language-specific; the quality, documentation and workflow skills are not. licenses: - MIT # Deliberately no `requires.agent`: the toolchain is agent-agnostic. The skills # under files/home/.claude/skills/ and files/home/.claude/CLAUDE.md are # Claude-Code-specific and are simply inert on other agents, which still get the # quality workflow and the `.memory/` convention through `agentInstructions` below. permissions: network: allow: # ---- install-time, one-shot ------------------------------------------- # Go toolchain tarball. dl.google.com is the canonical host go.dev/dl # redirects to; hitting it directly keeps the allowlist to one entry. - dl.google.com # TinyGo release tarball. github.com 302-redirects binary downloads to # release-assets.githubusercontent.com (covered by the wildcard below). - github.com # Matches exactly one DNS label, so this single entry covers # release-assets. / objects. / raw.githubusercontent.com. - "*.githubusercontent.com" # qlty CLI tarball (and `qlty upgrade`, if ever run by hand). - qlty-releases.s3.amazonaws.com # The C/C++ toolchain comes from Ubuntu's own archives. ports.* serves # arm64, archive.*/security.* serve amd64 — which one is used depends on the # sandbox architecture, so all three are listed. - ports.ubuntu.com - archive.ubuntu.com - security.ubuntu.com # Only reached if the cached apt lists ever go stale and the fallback # `apt-get update` runs: it re-fetches *every* configured source, and the # template configures Docker's as well as Ubuntu's. - download.docker.com # `rustup-init`, plus every toolchain component rustup resolves. One host # serves both the rustup releases and the dist channel, so this covers the # install *and* later `rustup target add` / `rustup component add`. - static.rust-lang.org # The Node.js tarball comes from nodejs.org, already listed under "qlty # runtime" below — qlty provisions its own Node for the JS/TS linters, so the # host is needed either way. # ---- qlty runtime ------------------------------------------------------ # qlty resolves its default plugin source (qltysh/qlty-plugins) and every # linter release through the GitHub API, its own GitHub proxy, and # codeload for repository tarballs. - api.github.com - github-proxy.qlty.sh - codeload.github.com # radarlint-go — enabled by `qlty init` on Go repos; ships from its own # bucket and pulls a Temurin JVM from GitHub releases. - radarlint-releases.s3.amazonaws.com # osv-scanner — also enabled by `qlty init` on Go repos. - api.osv.dev - osv-vulnerabilities.storage.googleapis.com # Language runtimes qlty provisions on demand for non-Go linters. Trim # these if you only ever lint Go — except nodejs.org, which this kit's own # Node install step also needs. - nodejs.org - registry.npmjs.org - pypi.org - files.pythonhosted.org # ---- Go module resolution --------------------------------------------- - proxy.golang.org - sum.golang.org # ---- Cargo dependency resolution -------------------------------------- # Three distinct hosts, all needed: the sparse index cargo reads by # default, the CDN the .crate files come from, and the API `cargo add` / # `cargo search` / `cargo publish` talk to. Git dependencies go through # github.com, already allowed above. - index.crates.io - static.crates.io - crates.io # ---- VS Code extension tooling ---------------------------------------- # `vsce publish` / `vsce ls-publishers`, and the gallery query API that # `@vscode/test-electron` uses to resolve extension dependencies. - marketplace.visualstudio.com # Extension asset (.vsix) download. Each publisher gets its own subdomain, # and the wildcard matches exactly one DNS label, so both forms are needed. - "*.gallery.vsassets.io" - "*.gallerycdn.vsassets.io" # `@vscode/test-electron` asks this for the build to test against, then # downloads the archive from Microsoft's CDN. - update.code.visualstudio.com - vscode.download.prss.microsoft.com # `ovsx publish` — the Open VSX registry, which is where a VS Code # extension has to go to be installable in VSCodium, Cursor or Zed. - open-vsx.org environment: variables: # Pin the toolchain we actually installed. Without this, Go 1.21+ silently # downloads whatever `go`/`toolchain` line go.mod names, which would defeat # the point of pinning 1.26.5 (and needs egress we haven't allowed). If a # go.mod legitimately requires something newer, this surfaces as a clear # error instead of a surprise download. GOTOOLCHAIN: local # TinyGo resolves its root from the executable path, which already works for # the tarball layout. Setting it explicitly wins over any value baked in at # release-build time, so `tinygo` keeps working when invoked via the # /usr/local/bin symlink. TINYGOROOT: /usr/local/tinygo # qlty phones home to cdp.customer.io on every command otherwise. That host # is not allow-listed above, so the calls would fail anyway — this turns # them off at the source instead of relying on the firewall. QLTY_TELEMETRY: "off" # Rust lives under /usr/local rather than in the agent's home, for the same # reason Go and TinyGo do: the install runs once at container creation, and # only paths baked into the image are guaranteed to survive a restart. Both # directories are made world-writable by the install step, so `rustup target # add`, `rustup component add` and `cargo install` all work as the agent # user — this is the layout the official `rust` Docker image uses. RUSTUP_HOME: /usr/local/rustup CARGO_HOME: /usr/local/cargo # Deliberately NOT setting npm's prefix here. The base template already # exports NPM_CONFIG_PREFIX=/usr/local/share/npm-global, and that directory is # agent-owned and already on PATH ahead of /usr/local/bin — so `npm install -g` # works for the agent user with no sudo. Overriding it to /usr/local would # move global installs into a root-owned tree and break exactly that. The # install step below installs its pinned CLIs into the template's prefix and # hands ownership back to the agent. # Package caches. Each of these is repopulated over the network on a cold start, # so persisting them across container restarts turns minutes into a no-op. # Volumes are fixed at container creation: `sbx kit add` warns and skips them, so # use `--kit` at create time to get the caches. volumes: # qlty downloads a JVM (for radarlint), Node, and every linter binary into # ~/.qlty/cache. - path: /home/agent/.qlty # Cargo's downloaded .crate files and its copy of the sparse index. # Deliberately the `registry` subdirectory rather than all of CARGO_HOME: # mounting an empty volume over CARGO_HOME would shadow the rustup shims that # the install step puts in its `bin`. - path: /usr/local/cargo/registry # npm's content-addressable cache (`npm config get cache`). - path: /home/agent/.npm setup: install: # Go 1.26.5, version + digest pinned. The base template already ships a # /usr/bin/go (1.26.0 at time of writing); /usr/local/bin precedes /usr/bin # on PATH, so the symlinks below shadow it without removing anything. # To bump: change GO_VERSION and both SHA256 values (from # https://go.dev/dl/?mode=json). - command: | set -eu GO_VERSION=1.26.5 ARCH=$(dpkg --print-architecture) case "$ARCH" in amd64) SHA256=5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053 ;; arm64) SHA256=fe4789e92b1f33358680864bbe8704289e7bb5fc207d80623c308935bd696d49 ;; *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; esac if [ "$(/usr/local/go/bin/go version 2>/dev/null | awk '{print $3}')" != "go${GO_VERSION}" ]; then URL="https://dl.google.com/go/go${GO_VERSION}.linux-${ARCH}.tar.gz" curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/go.tgz "$URL" echo "${SHA256} /tmp/go.tgz" | sha256sum -c - rm -rf /usr/local/go tar -C /usr/local -xzf /tmp/go.tgz rm -f /tmp/go.tgz fi ln -sf /usr/local/go/bin/go /usr/local/bin/go ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt /usr/local/bin/go version user: "0" description: "Install Go 1.26.5 (version + digest pinned), shadowing the image's Go" # TinyGo 0.41.1. Its supported range is Go 1.19–1.26 and the published # binary is itself built with go1.26.2, so it accepts the Go 1.26.5 above # (TinyGo also refuses a GOROOT newer than its own build toolchain). # Bumping Go to 1.27 would break this pairing — bump TinyGo together with it. - command: | set -eu TINYGO_VERSION=0.41.1 ARCH=$(dpkg --print-architecture) case "$ARCH" in amd64) SHA256=e156d1d93a376eef639a4143d13be07e8c463fb6cf2d7d447698ed4474d23e91 ;; arm64) SHA256=789733bc3b5bace0bd1835a267b3ea267804a7ef1cfe69bc522c295f5226d624 ;; *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; esac if [ "$(/usr/local/tinygo/bin/tinygo version 2>/dev/null | awk '{print $3}')" != "$TINYGO_VERSION" ]; then TARBALL="tinygo${TINYGO_VERSION}.linux-${ARCH}.tar.gz" URL="https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/${TARBALL}" curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/tinygo.tgz "$URL" echo "${SHA256} /tmp/tinygo.tgz" | sha256sum -c - rm -rf /usr/local/tinygo tar -C /usr/local -xzf /tmp/tinygo.tgz rm -f /tmp/tinygo.tgz fi ln -sf /usr/local/tinygo/bin/tinygo /usr/local/bin/tinygo /usr/local/bin/tinygo version user: "0" description: "Install TinyGo 0.41.1 (version + digest pinned)" # qlty 0.639.0, pulled straight from the release bucket rather than through # `curl https://qlty.sh | sh`. The install script resolves "latest" (not # reproducible), POSTs an install event to cdp.customer.io, and rewrites the # user's shell rc files — none of which belongs in a sandbox image. # The asset is .tar.xz and the base image has no `xz`, so fall back to # Python's lzma (stdlib, and python3 is present) instead of pulling xz-utils # over apt. This step deliberately stays apt-free: it runs *before* the # C-toolchain step below, so it cannot assume anything about apt having been # exercised yet, and a self-contained step is one less ordering dependency. # To bump: change QLTY_VERSION and both SHA256 values (published as # .sha256 next to each release asset). - command: | set -eu QLTY_VERSION=0.639.0 ARCH=$(dpkg --print-architecture) case "$ARCH" in amd64) TARGET=x86_64-unknown-linux-gnu SHA256=49f8299d25a1c50cf02af8263483b1a04b8b665d71c5b6512847e08ccb392a90 ;; arm64) TARGET=aarch64-unknown-linux-gnu SHA256=8f2f5cd7eb06c3f1ba106866bbf80cfb563a06d70ee495c14391dced37d05a4e ;; *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; esac if [ "$(/usr/local/bin/qlty --version 2>/dev/null | awk '{print $2}')" != "$QLTY_VERSION" ]; then URL="https://qlty-releases.s3.amazonaws.com/qlty/v${QLTY_VERSION}/qlty-${TARGET}.tar.xz" curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/qlty.tar.xz "$URL" echo "${SHA256} /tmp/qlty.tar.xz" | sha256sum -c - rm -rf "/tmp/qlty-${TARGET}" if command -v xz >/dev/null 2>&1; then tar -C /tmp -xJf /tmp/qlty.tar.xz "qlty-${TARGET}/qlty" else python3 -c 'import lzma,sys; sys.stdout.buffer.write(lzma.open(sys.argv[1],"rb").read())' \ /tmp/qlty.tar.xz | tar -C /tmp -xf - "qlty-${TARGET}/qlty" fi install -m 0755 "/tmp/qlty-${TARGET}/qlty" /usr/local/bin/qlty rm -rf /tmp/qlty.tar.xz "/tmp/qlty-${TARGET}" fi /usr/local/bin/qlty --version user: "0" description: "Install qlty CLI 0.639.0 (version + digest pinned, no install script)" # A C/C++ toolchain. Not optional, and not obvious: the base template ships # `make` but **no `cc`, `gcc`, `clang`, `ld` or libc headers at all**. Without # this step: # - `cargo build` / `cargo test` for the host target fail with # "error: linker `cc` not found" (measured). Cross-compiling to # wasm32-wasip2 still works, because rustc uses its own bundled rust-lld # for wasm targets — so a Zed extension builds but cannot be unit-tested. # - any crate with a `build.rs` that compiles C, and any npm package with a # native addon built through node-gyp, fails the same way. # `apt-get update` is deliberately NOT run: the template ships populated # /var/lib/apt/lists, so a plain install works, and `update` would re-fetch # every configured source (including download.docker.com) and fail if any one # of them is not allow-listed. It is kept only as a fallback for the day those # cached lists go stale. - command: | set -eu if ! command -v cc >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive PKGS="gcc g++ libc6-dev pkg-config" # `-o DPkg::Lock::Timeout` is not optional here. The claude kit registers # a *backgrounded* `apt-get update` as a startup command, so at the moment # this step runs (~11s into the install sequence) that update may still # hold the dpkg frontend lock. Without the timeout apt does not wait — it # gives up at once with exit 100, and the whole kit apply fails. APT="apt-get -o DPkg::Lock::Timeout=300" $APT install -y --no-install-recommends $PKGS \ || { $APT update && $APT install -y --no-install-recommends $PKGS; } fi cc --version | head -1 c++ --version | head -1 user: "0" description: "Install a C/C++ toolchain (gcc, g++, libc6-dev, pkg-config) — required to link host-target Rust builds and native npm addons" # Node.js 24.19.0 — "Krypton", the active LTS line. The base template ships a # /usr/bin/node (22.22.1 at time of writing); as with Go, /usr/local/bin # precedes /usr/bin on PATH, so the symlinks shadow it without removing it. # The .tar.gz is used rather than the (smaller) .tar.xz on purpose: the base # image has no `xz`, and unlike qlty, Node publishes both formats — so this # avoids the python-lzma workaround the qlty step needs. # To bump: change NODE_VERSION and both SHA256 values, from # https://nodejs.org/dist/v/SHASUMS256.txt. - command: | set -eu NODE_VERSION=24.19.0 ARCH=$(dpkg --print-architecture) case "$ARCH" in amd64) NODE_ARCH=x64; SHA256=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 ;; arm64) NODE_ARCH=arm64; SHA256=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f ;; *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; esac if [ "$(/usr/local/node/bin/node --version 2>/dev/null)" != "v${NODE_VERSION}" ]; then TARBALL="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" URL="https://nodejs.org/dist/v${NODE_VERSION}/${TARBALL}" curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/node.tgz "$URL" echo "${SHA256} /tmp/node.tgz" | sha256sum -c - rm -rf /usr/local/node mkdir -p /usr/local/node tar -C /usr/local/node --strip-components=1 -xzf /tmp/node.tgz rm -f /tmp/node.tgz fi for b in node npm npx corepack; do ln -sf "/usr/local/node/bin/$b" "/usr/local/bin/$b" done # Global installs go to the template's own prefix, which is already on # PATH. --prefix is passed explicitly rather than trusting NPM_CONFIG_PREFIX # to be exported into this root-run step. NPM_PREFIX=/usr/local/share/npm-global mkdir -p "$NPM_PREFIX" # Pinned, and deliberately only the tools that belong to the *sandbox* # rather than to a project: the two extension packagers, and pnpm. A # project's TypeScript, esbuild and test runner belong in its own # devDependencies, where the version is committed alongside the code. # # --allow-scripts is required by npm 11: it otherwise skips these two # lifecycle scripts and only warns. keytar builds the native addon vsce # uses to store a publishing PAT, and vsce-sign unpacks the signing # binary — both are needed by `vsce publish`, and both fail silently # (at publish time, not now) if their scripts never run. /usr/local/bin/npm install -g --prefix "$NPM_PREFIX" --no-fund --no-audit \ --allow-scripts=@vscode/vsce-sign,keytar \ @vscode/vsce@3.9.2 \ ovsx@1.1.1 \ pnpm@11.22.0 # Root just wrote into an agent-owned tree; hand it back so the agent can # install or update globals later without sudo. chown -R agent:agent "$NPM_PREFIX" /usr/local/bin/node --version /usr/local/bin/npm --version "$NPM_PREFIX/bin/vsce" --version user: "0" description: "Install Node.js 24.19.0 LTS (digest pinned) + pinned vsce, ovsx, pnpm" # Rust 1.97.1 via a digest-pinned `rustup-init` *binary* — not the # `curl https://sh.rustup.rs | sh` bootstrapper, which resolves "latest" and # rewrites the user's shell rc files. rustup rather than the standalone # tarballs because it is what Zed shells out to when an extension needs a # target that is not installed yet (`rustup target add wasm32-wasip2`). # `--profile minimal` skips the ~500 MB of offline docs; clippy and rustfmt # are added explicitly because qlty's Rust plugins invoke them. # To bump: change RUSTUP_VERSION and both SHA256 values (published at # static.rust-lang.org/rustup/archive///rustup-init.sha256), # and/or RUST_VERSION on its own (rustup resolves that one itself). - command: | set -eu RUSTUP_VERSION=1.29.0 RUST_VERSION=1.97.1 ARCH=$(dpkg --print-architecture) case "$ARCH" in amd64) TRIPLE=x86_64-unknown-linux-gnu SHA256=4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10 ;; arm64) TRIPLE=aarch64-unknown-linux-gnu SHA256=9732d6c5e2a098d3521fca8145d826ae0aaa067ef2385ead08e6feac88fa5792 ;; *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; esac export RUSTUP_HOME=/usr/local/rustup export CARGO_HOME=/usr/local/cargo if [ "$(/usr/local/cargo/bin/rustc --version 2>/dev/null | awk '{print $2}')" != "$RUST_VERSION" ]; then URL="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${TRIPLE}/rustup-init" curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/rustup-init "$URL" echo "${SHA256} /tmp/rustup-init" | sha256sum -c - chmod 0755 /tmp/rustup-init # --no-modify-path: PATH comes from the /usr/local/bin symlinks below, # not from an appended line in a shell rc file. /tmp/rustup-init -y --no-modify-path --profile minimal \ --default-toolchain "$RUST_VERSION" \ -c clippy -c rustfmt \ -t wasm32-wasip2 rm -f /tmp/rustup-init fi for b in rustc cargo rustup rustfmt cargo-fmt cargo-clippy clippy-driver; do ln -sf "/usr/local/cargo/bin/$b" "/usr/local/bin/$b" done # The agent runs as a non-root user, and `rustup target add`, # `rustup component add` and `cargo install` all write inside these two # trees. Same approach as the official `rust` image: make them writable # rather than forcing every Rust command through sudo. chmod -R a+w /usr/local/rustup /usr/local/cargo /usr/local/bin/rustc --version /usr/local/bin/cargo --version /usr/local/bin/rustup target list --installed user: "0" description: "Install Rust 1.97.1 via digest-pinned rustup-init, with clippy, rustfmt and the wasm32-wasip2 target" startup: # Every cache volume mounts with a root-owned root because the runtime formats # block volumes as ext4, which shadows the image's directory ownership. # Without this the agent user cannot populate the caches — and for # /usr/local/cargo/registry the failure is a confusing "permission denied" # from the middle of a `cargo build`, not from anything the agent typed. # Idempotent by construction — startup commands run on every container start. - command: - "sh" - "-c" - | for d in /home/agent/.qlty /home/agent/.npm; do mkdir -p "$d" && chown -R agent:agent "$d" 2>/dev/null || true done # Left group/world-writable rather than chowned, to stay consistent # with the `chmod -R a+w` the Rust install applies to the rest of # CARGO_HOME: root installs into it, the agent writes to it. mkdir -p /usr/local/cargo/registry && chmod -R a+w /usr/local/cargo/registry 2>/dev/null || true user: "0" description: "Re-own the qlty, npm and cargo cache volumes so the agent user can write to them" agentInstructions: # `filename` is deliberately absent: it is ignored (with a warning) for a # mixin, whose content the engine writes to kits-memory/.md. content: | ## Development toolkit (kit `dev-toolkit`) This sandbox has a pinned toolchain installed at container creation: | Tool | Version | Location | Notes | | -------- | ------- | ------------------- | ----- | | qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics, for **any** language the project uses. | | Go | 1.26.5 | `/usr/local/go` | Symlinked to `/usr/local/bin/go`; shadows the image's own `/usr/bin/go`. | | TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` is set; use for `wasm`, `wasip1`, `wasip2` targets. | | Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup. `clippy`, `rustfmt` and the `wasm32-wasip2` target are installed. | | Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the image's own `/usr/bin/node` (22.x). | | npm | 11.17.0 (bundled) | `/usr/local/bin/npm` | Globals go to `/usr/local/share/npm-global`, agent-owned and on PATH. | | pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Also `corepack`, if a project pins a different version via `packageManager`. | | vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions (Marketplace / Open VSX). | | gcc / g++ | Ubuntu's | `/usr/bin` | Installed by this kit — the base image has **no** C compiler at all. | Verify with `qlty --version && go version && tinygo version && rustc --version && node --version`. qlty is **not** language-specific: `qlty init` detects the languages present and enables the matching linters (on a Go repo that is gofmt, golangci-lint, osv-scanner, radarlint-go, ripgrep and trufflehog; on a TypeScript or Rust repo it is an entirely different set). Only the toolchain binaries above are language-specific — the three skills are not. ### Toolchain constraints worth knowing - `GOTOOLCHAIN=local`. Go will **not** auto-download a different toolchain. If a build fails with "go.mod requires go >= X", that is real — either lower the requirement or bump this kit's pinned Go and TinyGo together. - TinyGo 0.41.1 supports Go 1.19 through 1.26 **and** refuses a GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 is inside both bounds. Do not bump Go past 1.26 without bumping TinyGo. - `QLTY_TELEMETRY=off`, and qlty's telemetry host is not allow-listed. Pass `--no-upgrade-check` to qlty commands to skip its update probe as well. - Rust lives under `/usr/local`, not in the home directory, and both `RUSTUP_HOME` and `CARGO_HOME` are world-writable — so `rustup target add`, `rustup component add` and `cargo install` work **without** sudo. Do not re-run `rustup-init`; if a toolchain or target is missing, add it with `rustup`. - `wasm32-wasip2` is the only cross-compilation target preinstalled. Others (`wasm32-unknown-unknown` for wasm-bindgen, say) are one `rustup target add ` away — `static.rust-lang.org` is allow-listed. - Rust needs a C linker for **host** builds, and the base image ships none — this kit installs `gcc`/`g++` for that reason. If you ever see "error: linker `cc` not found", the C toolchain step did not run; that is a kit problem, not a project problem. Note the asymmetry: `cargo build --target wasm32-wasip2` works *without* `cc` (rustc uses its own bundled `rust-lld` for wasm), so a missing compiler breaks `cargo test` while leaving the wasm build green. - Prefer a project `devDependency` plus `npx` over `npm install -g`: the version then lives in the repository instead of in this container. Globals do work without sudo if you need them. ### Building editor extensions Both extension ecosystems are supported, and they use different toolchains: | | VS Code | Zed | | --- | --- | --- | | Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly | | Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` (+ `Cargo.toml`) | | Build | the project's own script — `npm run compile`, usually tsc or esbuild | `cargo build --release --target wasm32-wasip2` | | Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor | | Publish | `vsce publish` (Marketplace), `ovsx publish` (Open VSX) | a PR to `zed-industries/extensions` | Two things that genuinely do not work in this sandbox, so plan around them rather than debugging them: - **No editor runs here.** There is no display, so neither VS Code's F5 "Extension Development Host" nor Zed's *Install Dev Extension* can be used from inside the sandbox. Both are host-side actions on the user's machine. What you *can* do here is compile, unit-test, lint and package. - **`@vscode/test-electron` needs a display.** Its download hosts are allow-listed and `--extensionDevelopmentPath` runs will fetch a build, but launching it needs an X server (`xvfb-run`), which this kit does not install. Write the extension so its logic is testable without the `vscode` module — keep the `vscode` API calls in a thin activation layer and unit-test the rest — and say so plainly rather than reporting an untested integration path as verified. For a Zed extension, `wasm32-wasip2` is the target Zed itself compiles with; building for anything else produces an artifact Zed will refuse. Extensions that ship Tree-sitter **grammars** additionally need the wasi-sdk to compile the parsers — Zed downloads that itself, on the host, so grammar compilation is not reproducible inside this sandbox either. ### Checking code quality Use the **`quality` skill** (`~/.claude/skills/quality/SKILL.md`) rather than invoking qlty ad hoc. It configures qlty on first use, measures the project, writes a timestamped Markdown report under `.quality/`, records a history line so progression across runs is visible, and then drives refactoring until the gate passes. Non-Claude agents: run `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` to get the same report and history, then refactor against its findings. ### Other skills shipped by this kit They live under `~/.claude/skills/` and are picked up automatically: - **`diataxis-doc`** — writes or restructures project documentation with the Diátaxis method (tutorials / how-to / reference / explanation). It asks which language to produce the docs in before writing anything, unless the invoking skill has already fixed it. - **`methodical-dev`** — drives a new feature through gathering requirements, planning, and implementation, stopping for approval after every step. It is the **orchestrator**: it invokes `quality` and `diataxis-doc` as mandatory closing phases, requires unit tests in the same step as the code they cover, and keeps a committed `.memory/` record (summary, append-only history, handoffs) so work can be resumed in a fresh sandbox. Start here for any non-trivial change. - **`turbo-new-editor`** — builds a complete terminal IDE for one language, `turbo-`, on top of the `turbo-core` library: a command, a profile and a syntax scanner, with everything else — windows, menus, dialogs, themes, the LSP client, the terminal emulator — coming from the library. It runs *inside* `methodical-dev` rather than replacing it, and takes the language and the path of a repository the user has already created. ### `.memory/` — the project record (read it first) This kit also installs `~/.claude/CLAUDE.md`, which Claude Code loads at the start of **every** session: it says to read `.memory/` before planning or asking the user anything. On non-Claude agents that file is inert — the convention below applies anyway. `methodical-dev` maintains `.memory/` at the repository root, **committed, not gitignored**: | Path | Contents | | ---- | -------- | | `summary.md` | Living snapshot: architecture, decisions in force, build/test commands. Edited in place. | | `history.md` | Append-only dated log, one entry per completed feature. Never rewritten. | | `handoffs/` | Per-session handoff: state, work in flight, next steps, blockers. | If `.memory/` exists, **read `summary.md` and the latest handoff before starting work** — it records the project's state and any unfinished business, and saves you from re-deriving it or asking the user what is already written down. **Every session also ends by bringing all three files up to date** — no exceptions, including sessions that fixed one line, only read code, or achieved nothing (those last stop the next agent hitting the same wall): - `summary.md` — **edit in place, never regenerate.** Change only what your session establishes or invalidates and leave the rest byte-for-byte alone; rewriting it wholesale from one session's slice destroys accurate content you never looked at. Only from what you verified, never a plausible guess; what you could not check goes under an explicit `## Not yet established` heading. If the project's state did not change, leave the file untouched. - `history.md` — **append exactly one dated entry.** Never rewrite or delete an existing one. - `handoffs/YYYY-MM-DD-.md` — write today's, or update it if it already covers this topic. Never overwrite another session's handoff. Match the length to the work: a one-line fix earns a few lines, not a filled-in template — padding with "N/A" makes the record unreadable. Record what the next person cannot re-derive (where you stopped, what failed, the trap, the pending decision), not a diff summary. If `.memory/` does not exist yet, create the whole skeleton: `README.md`, `summary.md`, `history.md` and `handoffs/`. Keep the boundary clean: `docs/` is for people *using* the project, `.memory/` is for whoever *continues building* it. Two things that make the measurement meaningless — never do either: - Do not add `exclude_patterns`, disable a plugin, or raise a threshold in `.qlty/qlty.toml` to make the gate pass. Fix the code. - Do not delete or skip tests to clear a finding.