The runtime model has seven entities. Use these words consistently in code, comments, error messages, log output, docs, and prose.
- workspace — a persistent grouping and a boundary: one shared
$HOME(~/.devcell/<workspaceName>/), one network, one secrets scope. May host many projects. May be running or stopped. Defaults tomain; override withDEVCELL_WORKSPACE, or inherit fromTMUX_SESSION_NAME. - project — a host directory with code, mounted into a container.
- cell — the engine-agnostic runtime unit for one (workspace, project) pair. Composed from a workspace name + project name + cell ID.
- stack — the image variant a container is built from.
- module — a toggleable Nix capability composed into a stack.
- engine: how a cell is run. One of
docker,tart,winkit(go-winkit, which uses vz on macOS hosts and qemu on Linux hosts). Set with--engineor[cell] engine, or derived from the guest. - guest: the OS image inside a cell. One of
linux,macos,winpe(Windows PE + WSL1),windows-full(full Windows + WSL2, internal only for now). Set with--osor[cell] os. Each engine runs only some guests; the capability table ininternal/engineis the one place that says which.
workspaceName (DEVCELL_WORKSPACE) > projectName (dir basename) > cellId (DEVCELL_CELL_ID) > cellName (project + cellId) > container name (cell-<cellName>-run).
Do not use these as devcell-layer terms:
session— tmux owns this word.bunk— replaced by cell ID (DEVCELL_CELL_ID). The old env varDEVCELL_BUNKis deprecated.
Note: workspace also appears in internal/serve/ for the MS-TSWP RDP protocol, where it is the protocol's own term. WorkspaceResource is still pending a rename to Cell.
- Do NOT create commits automatically. Always ask the user to commit.
- Do NOT push to remote unless the user explicitly asks.
Every behavioral change to cmd/, internal/, or nixhome/modules/llm/*.nix lands with a test that was failing before the change. Write the failing test first, implement the minimum to pass, then refactor.
Applies to: a new flag, env var, TOML key, or CLI subcommand (cmd/*_test.go); a new internal/* function with observable behavior (same package); a new MCP server or docker argv field (internal/engine/docker/*_test.go); a new system-prompt source (internal/cell/*_test.go); a change to another engine's argv, options or capability table (internal/engine/**/*_test.go).
No new test required for: pure refactors, docs, dependency bumps, nix module additions (nixhome now lives in devcell-sh/community-home), or entrypoint shell fragments (covered by test/).
Integration tests in test/ split into short and long buckets by image-build cost: see docs/testing.md.
Never remove or rename a user-facing TOML key, command or flag in one step. See docs/deprecation.md for the full process.
- Nix is owned by the
devcelluser, home at/opt/devcell— stable, never remounted. - The session user is
$HOST_USER, home at/home/$HOST_USER, created at startup by the entrypoint. - Nix profile path is
/opt/devcell/.local/state/nix/profiles/profile— home-manager's native path, updated on everyhome-manager switch. - The entrypoint copies
/opt/devcell/dotfiles to/home/$HOST_USER/withsed "s|/opt/devcell|$HOME|g"to redirect write paths. - Use
ln -sfT(notln -sf) when replacing a symlink-to-directory;-Tprevents creating the link inside the target. ENV USER=devcellis required in the nix stage —nix.shchecks[ -n "$USER" ]and silently no-ops if empty.$HOME/.config/nix/nix.confmust carryexperimental-features = nix-command flakesat BUILD time.
Do NOT use ARG TARGETARCH=amd64 — the docker driver doesn't set it for host-platform builds. Use ARCH=$(uname -m) in RUN steps.
Nix modules (nixhome) now live in the standalone devcell-sh/community-home repo. Edits to .nix files in this repo are limited to flake.nix (the Go package build).
Escaping inside writeShellScriptBin (''...'' strings) is the usual culprit:
${VAR}must be''${VAR}(otherwise Nix interpolates it)''(empty shell string) must be''''$VARwithout braces passes through as-is
The CI Deploy Site workflow compiles all cmd/*.go together with cmd/gendoc.go. Three things must hold or go build exits 1:
- Run
go mod tidy && go build ./...after any dependency or import change, and commit the result. A green local build is NOT enough — CI starts from a clean module cache, so a missinggo.sumentry only surfaces there. - Build-time-only tooling deps must stay anchored in
cmd/tools.go(//go:build tools).cmd/gendoc.gois//go:build ignore, sogo mod tidycan't see itscobra/docimport and would prune the transitive deps. Anchor any other build-ignored tool's deps there too. api/swagger/is gitignored (swagger output) butcmd/serve.goimports it, so any workflow compilingserve.gomust runtask swagger:generatefirst.
After changing go.mod/go.sum, run task nix:sync — it resolves flake.nix's vendorHash and stages it. The pre-commit hook only verifies.
If a build fails with "no space left on device":
- Prune build cache first (safe):
docker buildx prune -af - If still insufficient, ask the user to stop old containers — never stop them yourself. Each pins a ~13 GB untagged image with almost no layer sharing, so 2–3 usually frees ~20 GB. Then
docker image prune.