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

🎉 Begin a project. 4edda86 · on 00224b167266e7d496672adfe1e60fcffeab7e63 · k33g · yesterday
spec.yaml · 603 lines · 33.3 KBYAML Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
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
    # <asset>.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<version>/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/<ver>/<triple>/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/<kit-name>.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 <target>` 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-<language>`, 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-<slug>.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.