Appearance
Dev Environment Lifecycle (IaC)
TL;DR. Declare a project's dev environment in
dev.json;/dev-upbrings it to a fully testable state — server up, migrations run, seed data loaded, test users provisioned, health checked. Warp-drive calls it automatically before the first chunk and between chunks.
Declarative dev environment management that ensures apps are always running, seeded, and testable — both on-demand and during warp-drive autonomous coding.
Overview
Every project declares its dev environment in dev.json at the project root. The dev-up command reads this manifest and orchestrates the full lifecycle: server management, migrations, seed data, and test user provisioning. It's idempotent, composable, and integrates directly into warp-drive.
Quick Start
bash
# 1. Copy the template to your project
cp ~/.claude/templates/dev.json ./dev.json
# 2. Customize for your project (edit server command, port, auth adapter, etc.)
vi dev.json
# 3. Set up seed data
mkdir -p seed/
cp ~/.claude/templates/seed/users.json ./seed/
cp ~/.claude/templates/seed/001-base-data.sql ./seed/
cp ~/.claude/templates/seed/002-sample-entities.sql ./seed/
# Customize the SQL for your schema
# 4. Run it
~/.claude/scripts/dev-lifecycle/dev-up.sh . --verbosedev.json Manifest
json
{
"server": {
"command": "npm run dev", // Command to start the dev server
"port": 5173, // Port the server listens on
"health": "/api/health", // Health check endpoint path
"restart": "on-failure", // Restart strategy: "on-failure" | "always" | "never"
"env": {} // Additional environment variables
},
"migrations": {
"command": "npm run migrate:dev", // Migration command (null to skip)
"auto_run": true // Run migrations automatically
},
"docs": { // Optional docs-site dev server (#732); omit for no docs server
"enabled": false, // Manage the docs server? (defaults true when the block is present)
"engine": null, // "vitepress" | "starlight" | null (resolve from docs-site.json)
"port": null, // null resolves the ledger docs port (base+3)
"health": "/", // Docs health-check path
"restart": "on-failure" // Restart strategy: "on-failure" | "always" | "never"
},
"seed": {
"directory": "seed/", // Path to seed scripts
"runner": "node", // Script runner: "node" | "npx tsx" | custom
"order": "alphabetical", // Execution order: "alphabetical" | "numeric"
"d1_database": "db", // D1 database name for SQL seeds (if applicable)
"auto_generate": true // Auto-generate seed data for uncovered tables during dev-up
},
"auth": {
"adapter": "d1", // Auth adapter: "d1" | "sqlite" | "supabase" | "script" | "custom"
"users_file": "seed/users.json", // Path to test user definitions
"table": "users", // Database table name for users
"db_file": "dev.db", // SQLite database file path (for the "sqlite" adapter)
"provision_command": null // Custom provisioning command (for "custom" adapter)
},
"access": {
"localhost": "http://localhost:5173", // Local access URL
"farm_01": null // Remote dev server URL (if accessible)
}
}The full schema lives at schemas/dev.schema.json and is validated by make check. The block above is the canonical single-environment shape; templates/dev.json is the scaffold dev-up copies into new projects.
Multi-environment profiles & promotion (#447)
Beyond the dev-only fields above, dev.json may declare higher environments and a promotion policy. Both blocks are optional — a project without them is a single-environment (dev-only) project, and existing manifests keep validating unchanged.
json
{
"environments": {
"test": { // also: "prod", or any named env (e.g. "staging")
"url": "https://app-test.example.workers.dev",
"deploy": {
"adapter": "cloudflare", // "cloudflare" | "node" | "drupal" | "script" | "none"
"command": null, // override deploy command (script adapter)
"target": "app-test" // deploy target (e.g. Worker/env name)
},
"db": {
"binding": "DB", // binding name in this environment
"database": "app-db-test", // database identifier
"adapter": "d1" // "d1" | "sqlite" | "supabase" | "postgres" | "none"
},
"env": { "ENVIRONMENT": "test" } // env-specific variables
}
},
"promotion": {
"ceiling": "prod", // how far promotion may go: "pr" | "external" | "test" | "prod" (default "external")
"strategy": "ci" // how promotion is triggered: "manual" | "auto" | "ci" (refined in #448)
}
}environments.{test,prod,…}— per-environment deploy config, public URL, database binding, and env vars. The deploy-adapter interface is expanded in #450; the test-env provisioning + prod→test mirror in #451.promotion— the project's promotion-pipeline policy.ceiling(required when the block is present) caps how far automated promotion may go:pr(stop at a pull request),external(merge to git but no deploy — the default),test/prod(merge and deploy up to that environment). The merge/deploy decision isf(automation_level, ceiling), enforced byscripts/promotion/promotion.js; see automation-behavior.md for the matrix. Defined in #448.
Docs-site dev server (#732)
The optional docs block lets dev-up bring a project's documentation site up alongside the app server. When the block is present and not disabled, dev-up starts /docs-site dev on the project's ledger docs port (base+3), reports its URL, and health-checks it.
- Opt-in. Omitting the block — or setting
enabled: false— means no docs server; the rest of the lifecycle is unchanged.templates/dev.jsonships the block inert (enabled: false) for discoverability. - Port.
port: nullresolves the ledger docs port (base+3), so the docs server never collides with the app (base+0/+1), the dashboard (+2), or another project. A numericportoverrides. - Engine.
engine: nullresolves the engine from the project'sdocs-site.json; starting the server requires that file (run/docs-site initfirst). - Non-fatal. A docs-server start or health failure is surfaced (logged, and reported in the
--checkJSON underdocs) but is never fatal for the app — the app's health verdict and exit code are unaffected. Between warp-drive chunks,dev-up --checkverifies the docs server when declared and flags a failure without blocking the app.
json
{
"docs": {
"enabled": true,
"engine": null, // resolve from docs-site.json
"port": null, // resolve the ledger docs port (base+3)
"health": "/",
"restart": "on-failure"
}
}Dev-up recipes (#449)
dev.json is a committed, vetted file — generated once from a stack-keyed recipe, then owned by the project. dev-up uses the committed file; it does not regenerate it on every run. This replaced the old on-the-fly generation in bin/dev-up so warp-drive gets a stable, project-specific bring-up.
How it works
Recipes live in templates/dev-recipes/ — one JSON file per stack (cloudflare-d1, sveltekit, drupal-ddev, node, …). Each is a dev.json template plus a _recipe metadata block:
json
{
"_recipe": { "name": "cloudflare-d1", "stacks": ["cloudflare-workers"], "priority": 20 },
"server": { "command": "npx wrangler dev", "port": "${PORT}", ... },
"seed": { "d1_database": "${D1_DATABASE}", ... },
...
}The engine (scripts/dev-lifecycle/dev-recipe.js):
- Detects the stack by reusing the fleet readiness audit's detector (
scripts/fleet/readiness.js::detectStack) — there is no second stack detector to keep in sync. - Selects the highest-
priorityrecipe whose_recipe.stacksintersect the detected stack (so a Cloudflare+Node project pickscloudflare-d1, priority 20, overnode, priority 5). - Substitutes placeholders (
${PORT},${D1_DATABASE}) from lightly-detected values, strips_recipe, and writes a schema-validdev.json.
When no recipe matches (e.g. a CLI/tooling repo with no dev server), the engine writes nothing and dev-up skips the dev environment — no spurious dev.json.
Commands
| Command | Effect |
|---|---|
dev-up [dir] | Use the committed dev.json; seed one once from the recipe if missing |
dev-up --regen [dir] | Rewrite dev.json from the stack recipe (overwrites) |
dev-recipe.js detect|render|write [--force]|list [dir] | Inspect/drive recipes directly |
Adding a new stack recipe
Adding a stack is a pure data change — no engine edits:
- Drop a new
templates/dev-recipes/<name>.jsonfile. - Give it a
_recipeblock:name, thestacksit applies to (matchingdetectStacktokens such assveltekit,drupal,go,python), and apriority(higher wins ties; specific stacks should outrank genericnode). - Author the
dev.jsonbody, using${PORT}/${D1_DATABASE}where detection should fill values.
The engine discovers it automatically on the next run; the new recipe's rendered output is validated against dev.schema.json by the test suite.
Port Allocation (#197)
Moved — see Port Allocation.
Scripts
All scripts live in ~/.claude/scripts/dev-lifecycle/ and follow the IaC prime directive: standalone, idempotent, parameterized, with --help.
| Script | Purpose | Usage |
|---|---|---|
dev-up.sh | Full lifecycle orchestrator | dev-up.sh [DIR] [--check|--verbose|--skip-*] |
health-check.sh | Lightweight health probe | health-check.sh [DIR] [--port N --path /path] |
provision-users.sh | Test user provisioning | provision-users.sh [DIR] [--adapter TYPE] |
check-seed-coverage.sh | Seed data coverage check | check-seed-coverage.sh [DIR] |
dev-up.sh
The main orchestrator. Runs the full lifecycle:
- Server: Check health → start if not running → kill & restart if unhealthy
- Migrations: Run migration command if configured and
auto_runis true - Seed data: Execute all scripts in
seed/directory alphabetically - Test users: Provision users via the configured auth adapter
- Report: Output JSON status and access URL
Flags:
--check— Health check only (no start/seed/users). Fast gate for warp-drive.--skip-seed— Skip seed data scripts--skip-users— Skip user provisioning--skip-server— Skip server management (run seed/users only)--verbose— Show detailed step-by-step output
health-check.sh
Lightweight standalone health probe. Returns JSON:
json
{"status": "healthy", "port": 5173, "url": "http://localhost:5173"}provision-users.sh
Reads seed/users.json and provisions users via pluggable adapters:
| Adapter | How it works |
|---|---|
d1 | Generates SQL, executes via wrangler d1 execute --local |
sqlite | Direct sqlite3 INSERT OR REPLACE |
supabase | Delegates to supabase db reset --local |
script | Runs project-local seed/provision-users.sh |
custom | Runs auth.provision_command from dev.json |
check-seed-coverage.sh
Advisory check: warns if migration/schema files changed but no seed files were modified. Used by warp-drive during the coding phase to remind about additive seed data.
Seed Data Convention
See ~/.claude/templates/seed/README.md for full details.
Key rules:
- Files run in alphabetical order. Use numeric prefixes:
001-base.sql,002-entities.sql - Every script MUST be idempotent (INSERT OR REPLACE, UPSERT, etc.)
- Seed entities in ALL lifecycle states your domain defines — not just fresh/active records
- New features add their own seed file in the same commit (e.g.,
010-feature-invoicing.sql)
Standardized Test Users
Defined in seed/users.json. The superuser credentials are consistent across all projects:
| Role | Password | Purpose | |
|---|---|---|---|
| superuser | admin@test.local | admin123 | Full access, consistent across projects |
| editor | editor@test.local | editor123 | Read/write/publish access |
| viewer | viewer@test.local | viewer123 | Read-only access |
| guest | guest@test.local | guest123 | Minimal/no access |
Projects should customize the role-specific users to match their permission model while keeping the superuser unchanged.
Warp-Drive Integration
When a project has dev.json, warp-drive automatically manages the dev environment:
| Phase | Action |
|---|---|
| prerequisites | Full dev-up --verbose — start everything before coding begins |
| coding | check-seed-coverage — advisory reminder if schema changes lack seed data |
| chunk_complete | dev-up --check — fast health gate before next chunk |
| chunk_complete (recovery) | Full dev-up --verbose if health check fails |
Dev health failure is treated as a blockable event. If recovery fails after 2 attempts, warp-drive escalates per its error protocol.
When a docs block is declared, the same dev-up --check gate also verifies the docs server and surfaces its status under docs in the JSON output. A docs-server failure is reported but is not treated as an app health failure — it never blocks the chunk gate on its own.
Projects without dev.json skip all dev lifecycle steps — the integration is opt-in.
Structured checks (checks) (#586)
A project can declare the checks the warp-drive inner loop runs, turning the testing phase's feedback from an LLM-supplied free-text string into a typed contract. Add a checks array to dev.json (or a sibling checks.json):
json
{
"checks": [
{ "name": "unit", "command": "npm test", "kind": "test" },
{ "name": "types", "command": "tsc --noEmit", "kind": "typecheck" },
{ "name": "lint", "command": "npm run lint", "kind": "lint" }
]
}Each check has a name, a command, and a kind (test | typecheck | lint | build | smoke). The runner executes them and parses each result by kind:
bash
node ~/.claude/scripts/warp-drive/run-checks.js --cwd . # exit 0 = all pass, 1 = a check failedtest— failing test names + count (vitest / jest / node:test).typecheck—tscdiagnostics as{file, line, message}.lint— eslint (stylish + compact) diagnostics.build/smoke/ unknown — a raw output tail.
Parsers degrade gracefully — an unrecognised runner yields a tail, never a crash. The runner emits a typed last_check_result (schema: schemas/last-check-result.schema.json) with a stable failure signature; warp-drive persists it to state and the stall guard compares it instead of an LLM string. The smoke kind lets a change-scoped functional assertion (not just "server up") gate a chunk.
Backward compatible: with no checks block the runner reports fallback: true and warp-drive runs the detected test command as before. See the warp-drive testing phase for the loop wiring.
Loop memory — hidden-rules store (#588)
The loop records the quirks it discovers (flaky tests, legacy quirks, constraints, gotchas) in a per-project episodic-memory store so the next run starts smarter — the Reflexion pattern. Unlike journal/lessons/trace-mining (which flow outward to humans and the BoB repo), this store is read back by the same project's loop and is agent-consumed, not filed as issues.
The store lives at .claude/loop-notes.json (schema: schemas/loop-notes.schema.json) and is git-ignored by default — it is machine state. A team that wants shared hidden rules can remove the .gitignore line and commit it.
bash
# record a discovered rule (kind: flaky-test | legacy-quirk | constraint | gotcha)
node ~/.claude/scripts/warp-drive/loop-notes.js add --kind flaky-test \
--summary "test/api.test.js > fetches catalog" --evidence "passed on retry" --source-chunk 2
node ~/.claude/scripts/warp-drive/loop-notes.js list # inspect the store
node ~/.claude/scripts/warp-drive/loop-notes.js flaky # flaky-test identifiersCapture is bounded (capped, oldest evicted) and de-duped by signature (kind + normalized summary; a repeat refreshes last_seen_at and increments count). Two consumers read it back:
- Session start — the
loop-notes-inject.shSessionStart hook injects the notes asadditionalContext, so the next run does not re-learn a known quirk. - Check-runner — a
flaky-testnote (itssummaryis the failing test id) lets the structured check-runner quarantine that test: a failing run whose failures are all known-flaky is retried once and, if it then passes, surfaced as quarantined rather than treated as a hard failure (still surfaced, never silently ignored).
Adopting in a Project
- Copy
~/.claude/templates/dev.jsonto your project root - Customize the server command, port, and health endpoint
- Create
seed/directory with your initial seed scripts - Copy
~/.claude/templates/seed/users.jsonand customize roles - Set the auth adapter in dev.json to match your stack
- Run
dev-up . --verboseto verify
The template at ~/.claude/templates/dev.json has sensible defaults for SvelteKit + Cloudflare Workers projects.
Docs Site Lifecycle (#153)
Moved to how-to — see Docs-Site Lifecycle.