DWE — Dev Workspace Engine
DWE is a CLI that orchestrates Docker-based local development environments on top of a project's compose file — configuration layering, lifecycle, validation, declarative commands. It does not replace compose; edit the compose file freely.
This skill is a
navigator with an authoring layer, not a reference. It says
which command to run,
which file to edit and
in what order. Every schema, field and deep behaviour lives in the built-in docs, versioned with the binary:
dwe docs show <topic> --lang en
. Look things up there instead of guessing.
Detecting a DWE project
Walk up from cwd until a directory contains
— that is the root. Every
command resolves the root itself; run it from the root or any descendant. A project with
only plus commented scaffold files is a fresh
— see
references/populate-init-repo.md
.
First step: orient yourself
Once per session, inside the project:
shell
dwe docs llms-txt --lang en
It emits a project-aware index (services, live command list, both builtin registries, template syntax by site, doc topics). Read it before anything else.
If a root exists, read it too. It is the project-specific layer (real services, real command IDs, project rules); this skill is the generic layer. On a project fact the
wins; on a generic rule the skill wins.
Two different
files exist — do not confuse them:
| File | Origin | Edit? |
|---|
| root (next to ) | written once by , then hand-maintained | yes, directly |
| hub (inside a service hub, ) | generated by the render pack (, or a deploy step) | no — edit workspace/templates/ai/<pack>/
, hand off |
Neither has a
beside it: Claude Code reads
natively, and a
in or above the cwd would make it skip every
.
Project anatomy
- 3-layer config, later wins, maps deep-merge: (identity: , , , ) → (tracked: toggles, , , , ) → (gitignored per-dev overrides; tool-written). The root is strict — free-form values live only under ; a bare custom root key is a hard load error. A value may be an marker (committed secret, decrypted at load) → .
- — compose project name, shared volumes, per-subcommand compose args, build prepull. Loaded separately, not in the 3-layer merge; per-key override, per-dev in → .
- Services = folders:
workspace/services/<name>/service.yml
; the folder name is the key (no field). The container itself lives in the compose base or an overlay. Optional per-service / .
- User commands:
workspace/commands/**.yml
; path + filename + key = dot-ID; run with .
- Render packs:
workspace/templates/{config,ide,ai,git,workspace}/
— writes runtime files into the service hub; // write hub dotfiles (devcontainer, hub , git hooks); writes project-root artifacts selected by top-level (explicit render only).
- Pipelines (optional, full-replacement):
workspace/{deploy,lifecycle,reset}.yml
. Absent = built-in default (reported , not an error). Also (first-deploy wizard), (project checks), , / (dashboard, branding), .
- Integration tests:
workspace/tests/<scenario>.yml
(name = basename; deploy step schema).
- Generated, never hand-edited: , (deploy journal, , logs, test runs), , rendered hub files.
Layout reference:
dwe docs show concepts/project-layout --lang en
.
Rules no validator will catch
checks shape, not judgement. These pass validation either way and are what a fresh project gets wrong:
- Two disjoint registries share the word . takes the seven condition predicates ( · · · · · ·
generated-missing <svc> <field>
). A step body and take the step builtins (, , , , …). Neither accepts the other's names. Need a the predicates lack → ; need its inverse as a → . Both registries with kinds and summaries: dwe docs llms-txt --lang en
§ Builtins.
- Mount the whole hub, not just . + +
work_dir_internal: /workspace/src
, as a unit. Pointing at the checkout leaves rendered configs, caches and tooling state outside the container.
- One definition. A deploy step that runs something the project already declares as a command uses + , never a pasted copy — the copies drift, and only the command carries service/workdir/user/env (a shell step runs on the host).
- A entry in does not publish anything — compose binds only when an rule surfaces it (
{name: APP_PORT, from: services.<name>.ports.http}
) and compose interpolates that var. DWE tooling still reads the declaration: preflight checks it and remaps it, so removing it silently drops both. , , , are injected into automatically and may not be redeclared; the compose project name is set through , always lowercased.
Output conventions
- Always for docs (
llms-txt|search|show|list
); translations may lag.
- () for data you parse: , , , , , , , , , , , .
- Read the envelope keys before indexing — a wrong key returns empty with exit 0. is
{project, apps, tools, infra, daemons, deploy, topology, git}
(no top-level ); is ; is ; is with , , optional , (localized — match on ) + (its first line), , ; is {phases[{name, steps[{name, type, cmd, unresolved[], when, check, …}]}]}
.
- Exceptions: and always emit markdown and ignore ( when piping; writes a file). is always YAML. is always dotenv text.
- Scope a doc instead of reading it whole:
dwe docs show <topic>#<anchor>
, , .
- Bare / / / open a TUI on a terminal and fall back to plain output in a pipe. Always call the explicit read forms (, , , ).
When to use what
| Goal | Command / reference |
|---|
| Project overview (start here) | dwe docs llms-txt --lang en
|
| Run a project task (tests, lint, migrate, …) | dwe commands list --output json
→ ; dwe cmd -i <id> --output json
first if unfamiliar — see Running things |
| One-off command in a service container | dwe shell <service> -c '<cmd>'
|
| Inspect state / logs / config | · dwe logs <service> --output json
· dwe validate --output json
|
| Search docs / read a topic | dwe docs search <term> --lang en
· dwe docs show <topic> --lang en
|
| Read vars / set a var | dwe vars get|list|inspect <var> --output json
· handoff dwe vars set <path> <value>
(writes ); secret-like → --generate hex[:N]|base64url[:N]|uuid
|
| Encrypted-secret inventory | dwe secrets status --output json
(read-only, exit 0 even when nothing decrypts; / name the fix) |
| Populate a fresh repo from git URL(s) | references/populate-init-repo.md
|
| Add a service / tool / infra | references/add-service-and-tools.md
|
| Author a command or daemon | references/authoring-commands.md
|
| Render packs / vars / / generated secrets | references/render-and-vars.md
|
| Author a pipeline (deploy / lifecycle / setup / validate / info / styles / docker.yml) | references/pipelines-and-orchestration.md
|
| Integration test / verify a clean deploy | references/integration-tests.md
|
| Snapshot / reset / troubleshoot | references/snapshots-reset-troubleshoot.md
|
| Apply a change | Picking the apply command below — edit yml, then ASK the user to run it |
Running things
Most work in an existing project is running its own tasks. Two commands, not interchangeable:
— a task the project declares. Prefer it: it carries the right service, workdir, user, env and compose flags. Find IDs with
dwe commands list --output json
(entries carry
,
,
; match on
, the description follows the project locale).
dwe cmd -i <id> --output json
shows what it runs, whether it takes
params or
pass-through after
(
dwe cmd site.test -- --run x.test.ts
), and its
flag. A command that rejects extra arguments has no
slot — the error names the one-line fix; adding it beats working around it.
dwe shell <service> -c '<cmd>'
— anything not declared. Legitimate escape hatch; repetition is the signal it wants to be a declared command — say so rather than running it a third time.
- Check the registry before any task a declared command may cover — tests, linters, formatters, builds, codegen, migrations, seeds, cache/token management, package-manager scripts. Once per session, again after changes. Do not run the underlying npm/composer/make/docker command until that check is made.
- Scoped first, full before giving up. A hub may carry a block naming the service's groups (
dwe commands list <group> --output json
). A scoped listing that finds nothing is not proof of absence: project-wide and workflow commands declare no — run one full listing before falling back to . Block counts are declared; a can shorten the live listing.
- Output streams. The run banner goes to stderr (and is not printed with ), so stdout of is the command's own output. also echoes the exact process it spawns (
docker compose … exec|run …
, host ) to stderr.
- Long-running command → ( has no such flag). Without it the child's stdout is a pipe, block-buffers, and looks hung. Leave it off when parsing output (a PTY turns into ).
- Never / instead. resolves the container from the service name and applies its block; is the tell you wanted or .
Picking the apply command
After editing yml, hand the user the command that matches what changed (never run it yourself):
- / a service's / / / service / added a service / →
- or the compose base/overlays →
- toggled a service →
dwe services enable|disable <name> --apply
- template / top-level →
dwe render workspace [pack…]
(explicit; not automatic on deploy)
- only → (or ) — that block is in no config hash, so a plain journal-skips the render (
references/render-and-vars.md
§ 6)
- only icon / host / display strings → (then / if runtime is affected)
- authored a
workspace/tests/<scenario>.yml
→ , then (gated — see below)
- mixed / unsure → (ends in , so it covers a restart)
dwe deploy run --service <name>
requires that service's own
and
skips the final stack up — only for re-running one provisioned service, never for a new one. Never recommend
as a clean install: it only ignores prior state (
still applies). A true clean install is
dwe reset run && dwe deploy run
.
Permission boundary — read freely, never mutate
READ — run without asking (no state change, safe even when they report errors):
- , , (+
validate config|checks|env|tests|…
, ),
- , , , / without
snapshot list|current|inspect
compose argv <subcommand>
(bare, it errors), , docker ps|logs|project-name
,
- , / ,
- , — no plaintext, no key material. and DO print secret material — handoff.
- bare only (no ): prints the resolved , writes nothing. Its unfiltered body is the whole exported secret set — always scope it:
dwe render env | grep -E '^<NAME>='
. Host-only.
docs show|search|list|llms-txt
- (lock-free, no Docker; carries each scenario's ),
Running project tasks — judge the task, not the verb
and
dwe shell <service> -c '…'
are
transports: their risk is whatever they carry.
- Run without asking when the task only reads or verifies in place: linters, type-checks, formatters in check mode, in-container inspection, a test suite that talks to nothing stateful.
- Ask first when it changes project or data state: migrations, seeds, resets, dependency installs, anything writing outside a build cache — and anything you are unsure about.
- A test suite that reaches the project's database or another stateful service is in the second group. Integration suites routinely truncate and re-seed the schema they run against; pointed at the live stack they destroy the developer's working data (observed: one via silently replaced every row of a dev database). Check what the suite connects to; when it needs the stack, the isolated way is a scenario via , gated below.
- shows — a declared one means ask. Being asked to "run the tests" is permission to run the tests, not to run them against the live stack when they write to it.
The gate — cheap AND isolated, or ask
is a full clean Docker deploy of a throwaway copy — mutating and slow, but the only way to prove a deploy pipeline still works. Read
dwe test list --output json
first; each scenario's
decides:
- Hard stops — hand it over, no judgement call: non-empty after dropping entries with (the fix for is routing the port through or an rule ); > 0; > 0 (project-authored code on the host — , the builtin, host commands, shell /; dwe's own subcommands don't count, so the built-in pipeline reports 0).
- Cost — judge it: , , . A build is not an automatic stop — read the Dockerfile; the profile reports whether there is a build, never what it costs (layer-cache warmth is not modelled).
Run unattended only when all hard stops are clear
and you can account for the cost; otherwise — and when unsure — hand over the exact command. Always
(free) first, and propose a run only for substantial changes (new service, reworked pipeline, provisioning/secrets), never after a display-only edit. Details:
references/integration-tests.md
.
MUTATE — never invoke yourself. Prepare the change, then give the user the exact command:
- (safe to re-run: gap-fills; overwrites)
dwe deploy run [--service|--force|--resume]
, / / , (destructive)
dwe services enable|disable <name> --apply
- ,
dwe render config|ide|ai|git|workspace
, dwe render env --out <path>
, dwe render config --harvest
- / ,
dwe deploy state clear|repair
dwe snapshot create|restore|rollback|remove|pack|unpack
, , dwe docs generate|export|cache clear
dwe secrets init|set|get|encrypt|decrypt|rekey|key import|export|remove
— each writes a layer / keyfile or prints secret material. is a human handoff: it opens a hidden prompt; never ask the user for the identity text so you can paste it (it would land in a transcript).
- / when the task they carry mutates
- (without ); when the gate above does not clear it
Pattern:
edit yml → show the diff → give the exact command → wait. Never work around the boundary through
/
/ project scripts — including through
.
Anti-patterns
- Do NOT bypass the lifecycle: never , , or writes — DWE tracks state and holds locks.
- Do NOT hand-edit generated artifacts (, , , rendered hub files incl. the hub ). Edit the source (export rule / var / template / pack) and hand off the render/deploy.
- Do NOT put a free-form key at the config root — it goes under .
- Do NOT "fix" an value or a block by editing yml. The marker means this machine lacks the project's age identity;
dwe secrets status --output json
names the cause and the fix (, run by the user, or ). At a terminal // offer the import themselves. Rewriting the marker as plaintext commits the credential; deleting it breaks everyone else.
- Do NOT assume config shape — verify with
dwe docs show config/<area> --lang en
before editing anything under .
- Do NOT enumerate pending-op consumers from memory after without — run ; its banner is authoritative.
Reference files
Load on demand when the task matches:
references/populate-init-repo.md
— git URL(s) / "set up this project" / fresh 'd repo: interview → init → services → deploy → commands → render/vars → validate → deploy handoff.
references/add-service-and-tools.md
— app/tool/infra service, toggles, compose overlays, .
references/authoring-commands.md
— user commands and daemons: type zoo, params, templating, bridge opt-in.
references/render-and-vars.md
— render packs, generated-secret harvest, the sandbox, .
references/pipelines-and-orchestration.md
— per-service & project , , , , /, .
references/integration-tests.md
— workspace/tests/<scenario>.yml
, port isolation, the debugging loop.
references/snapshots-reset-troubleshoot.md
— triage trio, snapshot workflows, reset.