nandi/oripublic Fork 0
f5c963af3c1597c0274ce4a99705dbd92a7d1cba
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

README.md · 268 lines · 16.0 KBmarkdown Blame HistoryRaw
🎉 Begin a project. 4edda86 k33g 2d ago1# `dev-toolkit` kit
2
3A Docker Sandboxes **v2 mixin kit** that provisions a general-purpose development toolkit:
4a code-quality engine, pinned Go / Rust / Node toolchains, and three Claude Code skills
5covering 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
25Plus a `~/.claude/CLAUDE.md`, loaded at the start of every session, which brackets the work at
26both ends: **read** the project's committed `.memory/` record — summary, history, latest
27handoff — before planning or asking the user anything, and **update all three** before handing
28control back, unconditionally, however small the session. `summary.md` is edited in place and
29never regenerated from one session's partial view; `history.md` gets exactly one appended
30entry; `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`
34detects the languages present and enables the matching linters, and all three skills are
35language-agnostic. Drop the toolchain install steps from `spec.yaml` and the kit is a pure
36quality/docs/workflow mixin for any stack. The install steps are independent of each other,
37so a Go-only or Rust-only sandbox is a matter of deleting the ones you do not want — except
38the C-toolchain step, which Rust and native npm addons both depend on.
39
40Project-specific skills do **not** belong here. `builtin-parity`, which describes the
41GoloScript repository in particular, lives in the sibling [`golo-toolkit`](../golo-toolkit/)
42kit and is composed in only where it applies.
43
44## Layout
45
46```
47kits/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
61under `~/.claude/skills/` and Claude Code picks them up as personal skills, and `CLAUDE.md`
62lands at `~/.claude/CLAUDE.md` — the user-level memory file, prepended to **every** Claude
63Code 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
66only runs when that skill is invoked. `~/.claude/CLAUDE.md` is loaded unconditionally, so an
67ad-hoc request ("fix this bug") also starts by reading the project record instead of
68re-deriving state that is already written down. Keep the file short — it costs tokens on
69every single session; the detail belongs in the skills and in the project's own
70`.memory/README.md`.
71
72Everything in that tree is packed into the kit and shipped into the sandbox, so keep it to
73the skills themselves — a stray `__pycache__/` would be shipped alongside them. The
74repository `.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)
81sbx run claude --kit ./kits/dev-toolkit
82```
83
84Then, in the agent session: *"run a quality report"*, *"document this project with
85Diátaxis"*, or *"let's build this feature methodically"*.
86
87### Editor extensions
88
89The 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;
100building for anything else produces an artifact it refuses. Both flows were verified inside
101the 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
105What does **not** work here, by construction: no editor runs in the sandbox, so VS Code's F5
106Extension 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
108and 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
113are 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
115composition; mixins stack freely.
116
117**Why pinned tarballs instead of the official installers.** `curl https://qlty.sh | sh`
118resolves `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
120by version **and** SHA256, verified with `sha256sum -c` before it is unpacked — the same
121pattern 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
125template has no `xz` binary. Installing `xz-utils` would mean running `apt-get update`,
126which 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
128present, and `lzma` is in its standard library, so the archive is decompressed with no
129extra egress at all.
130
131**Toolchain pairing is load-bearing.** TinyGo 0.41.1 accepts Go 1.19–1.26 *and* refuses a
132GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 satisfies both
133bounds. Bumping Go to 1.27 breaks TinyGo — bump the two together. `GOTOOLCHAIN=local`
134prevents 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`
137but no `cc`, `gcc`, `clang`, `ld` or libc headers. That produces a genuinely misleading
138failure: `cargo build --target wasm32-wasip2` **succeeds** without a C compiler, because
139rustc links wasm targets with its own bundled `rust-lld` — while `cargo test`, which builds
140for the host, dies with ``error: linker `cc` not found``. A Zed extension therefore appears
141to build fine and cannot be unit-tested, which is the worst possible shape for a missing
142dependency. The same gap breaks any crate with a C-compiling `build.rs`, and any npm package
143with a native addon built through node-gyp. `apt-get update` is deliberately *not* run: the
144template 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
146allow-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
151Zed itself shells out to exactly that when an extension needs a target that is not installed.
152Rust lives under `/usr/local` rather than in the home directory, for the same reason Go does:
153the install runs once at container creation, and only paths baked into the image survive a
154restart. Both `RUSTUP_HOME` and `CARGO_HOME` are left world-writable — the layout the
155official `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`,
159so 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
161already on PATH ahead of `/usr/local/bin`**. Overriding it would move global installs into a
162root-owned tree and break `npm install -g` for the agent user, which currently works with no
163sudo. The install step passes `--prefix` explicitly (rather than trusting the variable to be
164exported 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
167and merely warns. Two of vsce's dependencies need theirs: `keytar` builds the native addon
168that stores a publishing PAT, and `@vscode/vsce-sign` unpacks the signing binary. Skipped,
169they 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
172start: `~/.qlty` (qlty provisions a JVM for `radarlint-go`, Node, and each linter binary on
173demand), `/usr/local/cargo/registry` (Cargo's `.crate` files and sparse index) and `~/.npm`.
174The Cargo one is deliberately the `registry` subdirectory rather than all of `CARGO_HOME`:
175mounting an empty volume over `CARGO_HOME` would shadow the rustup shims in its `bin`. Volumes
176are fixed at container creation, so `sbx kit add` warns and skips them — use `--kit` at create
177time to get the caches.
178
179## Maintenance
180
181**Bumping a version.** Change the version *and* both per-arch `SHA256` values in the
182matching `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
199own extension builder (`RUST_TARGET` in `crates/extension/src/extension_builder.rs`). Confirm
200there before changing it; an extension built for another target is one Zed refuses to load.
201
202**Validating a change.**
203
204```bash
205sbx kit validate ./kits/dev-toolkit
206sbx kit inspect ./kits/dev-toolkit --json | jq '.warnings' # expect null or []
207```
208
209Empty warnings is the green light — a non-empty list means a v1 surface crept in that
210will 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
215sbx 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
222And verify the thing versions cannot tell you — that host-target Rust actually **links**,
223which is what the C-toolchain step exists for:
224
225```bash
226sbx 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,
231qlty'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),
233Ubuntu's archives for the C toolchain, and the VS Code Marketplace / Open VSX publishing paths.
234A linter for another language will need its registry added. Find out what is missing rather
235than guessing:
236
237```bash
238sbx policy log <sandbox>
239```
240
241That 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.