Local development

The local loop

sklp dev <stack> runs a stack's local environment from .sklp/<stack>/dev.yaml: databases, migrations, backends and front-ends, started in dependency order and held until you press Ctrl-C. Each service runs in the embedded runtime (runc, no Docker daemon), inside a space built from .sklp/space.yaml, so the toolchain is the same on every machine.

sklp dev                    # list the stacks that have a dev.yaml
sklp dev skalpai            # boot the skalpai stack
sklp dev skalpai --validate # parse and auto-fix the YAML, launch nothing
sklp dev skalpai --validate --check   # report the fixes, write nothing
sklp dev skalpai --only core          # one service plus its transitive deps
FlagEffect
--validateparse and validate the devstack YAML; unambiguous mistakes are fixed in place
--checkwith --validate: show which fixes would apply, write nothing
--only a,blaunch a subset of services and what they depend on
--repo <dir>repository root (default .)
--grace-ms <n>delay between SIGTERM and SIGKILL on shutdown (default 5000)
--no-colorplain log prefixes

The stack name can sit anywhere among the flags. Reference: sklp dev.

The dev.yaml contract

One file per stack, at .sklp/<stack>/dev.yaml. A stack is a directory under .sklp/; the same directory may also hold deploy.yaml and config.yaml. The core of the contract is three keys: run, ready and depends_on.

version: 1
kind: devstack
env_files: [.env]
 
services:
  timescaledb:
    image: timescale/timescaledb:2.17.2-pg16
    port: 5432
    volumes: [sklp-dev-timescaledb-data:/var/lib/postgresql/data]
    ready:
      log: "database system is ready to accept connections"
 
  migrate:
    run: ./apps/migrations/entrypoint.sh up
    oneshot: true
    depends_on: [timescaledb]
 
  core:
    run: go run .
    workdir: apps/core
    port: 4100
    depends_on: [migrate]
    ready:
      http: /health
      timeout: 90s
 
  admin-api:
    run: go run ./cmd/admin-api
    optional: true
KeyMeaning
runshell command; mutually exclusive with image
imageOCI image to run as a container
depends_onservices that must be ready first; cycles and unknown names fail at parse time
readyone probe: http, tcp, log (regex on output) or file, plus timeout
oneshotruns to completion; exit 0 is the ready signal, so it takes no ready
optionaldeclared but skipped at launch; flip it to false to opt in
workdirrepo-root relative working directory
port / portsport the service listens on / host ports to publish
env, env_files, volumes, networkthe service's environment and storage

Each service has its own network namespace, so a dev server must bind 0.0.0.0, not localhost, for its probe and the host to reach it. Services reach each other by name (timescaledb:5432).

The space recipe

.sklp/space.yaml describes the closed environment every space is built from: pinned toolchain versions, apt packages, Go tools, env and the secrets the space may receive. Dev, CI and sklp space up all build from it, which is why "works on my machine" and "works in CI" mean the same thing.

runner:
  toolchain:
    go: "1.26.6"
    node: "24.16.0"
    pnpm: "11.3.0"
  packages: [git, ca-certificates, curl, gcc, libc6-dev]
  go_tools:
    - gotest.tools/gotestsum@v1.13.0
  env:
    GOPRIVATE: github.com/digstack
  secrets: [GITHUB_TOKEN]

Onboarding a repo

sklp init looks at a repository, detects the AI coding assistant you use, and installs skills tailored to the repo so the assistant writes the .sklp/ contracts. Without an assistant (or with --yes) it writes a deterministic baseline. No LLM call and no API key are involved on the sklp side.

sklp init                    # detect the assistant, offer the skills
sklp init --agent claude     # skip detection
sklp init --agent claude --no-run   # install skills, do not hand off
sklp init --yes              # deterministic baseline, no prompt

sklp skills regenerates the skills alone (targets claude, cursor, codex, vibe, zed, cline); --print <skill> shows one on stdout. sklp agents-md writes the repo's deploy facts (what core reconciles, which images roll on publish) into a marked section of AGENTS.md; --check fails when that section is stale, for a git hook.

sklp skills --target claude,cursor --force
sklp agents-md --check

See sklp init, sklp skills and sklp agents-md.

Ephemeral spaces

A space is a closed environment: a rootfs from space.yaml (or a bare OCI image), its own network namespace and named volumes. sklp space opens one for a shell, a test run or a one-off tool, without touching the host.

sklp space up scratch                    # build from .sklp/space.yaml, open a shell
sklp space up pg --image postgres:16     # bare space from an image
sklp space exec scratch go test ./...    # run a command in a live space
sklp space exec scratch -c core          # shell in one container
sklp space ls                            # every space, running or stopped
sklp space get scratch
sklp space stop scratch                  # stop, keep the named volumes
sklp space down scratch                  # stop and delete, volumes included
sklp space purge                         # delete every space with nothing running
sklp space build                         # build the rootfs only

down and purge delete the named volumes with the space, and each run names the volumes it destroyed. Use stop when the data must survive.

Watching what runs

sklp cockpit is a full-screen monitor: the current flow (branch, diff vs main) and the live spaces and services, with logs. It reads first; actions sit behind explicit keys.

sklp logs queries the logs a stack sends to Skalpai, across its projects.

sklp logs --severity ERROR --since 1h
sklp logs -q timeout --since 15m --json
sklp logs --stack <id> --from 2026-10-26T08:00:00Z --to 2026-10-26T09:00:00Z

See sklp cockpit and sklp logs.

Host and macOS

Spaces need a Linux host with a few packages (runc helpers, slirp4netns, user namespaces). sklp host check reports what is missing and exits 1 on a fatal gap; sklp host packages prints the list for your package manager.

sklp host check
sklp host packages        # one package per line, for your package manager

On macOS, sklp runs spaces in a Linux VM (Virtualization.framework). The VM starts on the first command that needs it.

CommandEffect
sklp macos buildbuild the guest kernel and rootfs image
sklp macos upstart the VM now
sklp macos statuswhich artifacts are present, and their versions
sklp macos downstop the VM and every space in it

Cache and cleanup

sklp keeps build caches, runner rootfs trees, OCI images and spaces under ~/.cache/sklp. Inspect it with sklp cache status and free space with sklp clean:

sklp cache status
sklp clean --dry-run   # preview, delete nothing
sklp clean             # dev build cache, CI runner caches, orphan procs and cgroups
sklp clean --images    # also the OCI image cache (re-pulled next run)
sklp clean --deep      # also idle spaces AND their volumes (asks first)
sklp cache gc --max-size 12G   # LRU-cap the CI tool cache

Every scope is machine-global: the caches are shared by all repositories on the host. Running deployments are always kept. ~/.cache/skt (the host toolchain cache) is never touched.

Rules

  • Never delete the cache by hand. No rm -rf under ~/.cache/sklp. Rootless rootfs content belongs to subuid-mapped users, so permission denied there is normal; spaces may hold live overlay mounts; sklp keeps its own bookkeeping. A manual delete leaves stale mounts and desynced state. sklp clean unmounts and accounts for all of it.
  • Ask before deleting. --images, --deep and space down destroy data or force a full rebuild.
  • Stop with Ctrl-C, never pkill. Stop a stack in its own terminal; if it runs in the background, ask whoever started it. A pattern kill also matches its own shell and leaves runc containers and mounts behind. Check what runs by probing it (curl -sf http://127.0.0.1:4100/health), not by reading a kill's exit code.