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| Flag | Effect |
|---|---|
--validate | parse and validate the devstack YAML; unambiguous mistakes are fixed in place |
--check | with --validate: show which fixes would apply, write nothing |
--only a,b | launch 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-color | plain 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| Key | Meaning |
|---|---|
run | shell command; mutually exclusive with image |
image | OCI image to run as a container |
depends_on | services that must be ready first; cycles and unknown names fail at parse time |
ready | one probe: http, tcp, log (regex on output) or file, plus timeout |
oneshot | runs to completion; exit 0 is the ready signal, so it takes no ready |
optional | declared but skipped at launch; flip it to false to opt in |
workdir | repo-root relative working directory |
port / ports | port the service listens on / host ports to publish |
env, env_files, volumes, network | the 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 promptsklp 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 --checkSee 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 onlydown 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:00ZSee 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 managerOn macOS, sklp runs spaces in a Linux VM (Virtualization.framework). The VM starts on the first command that needs it.
| Command | Effect |
|---|---|
sklp macos build | build the guest kernel and rootfs image |
sklp macos up | start the VM now |
sklp macos status | which artifacts are present, and their versions |
sklp macos down | stop 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 cacheEvery 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 -rfunder~/.cache/sklp. Rootless rootfs content belongs to subuid-mapped users, sopermission deniedthere is normal; spaces may hold live overlay mounts; sklp keeps its own bookkeeping. A manual delete leaves stale mounts and desynced state.sklp cleanunmounts and accounts for all of it. - Ask before deleting.
--images,--deepandspace downdestroy 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.