Skip to content

Dev Environment Lifecycle (IaC)

TL;DR. Declare a project’s dev environment in dev.json; /dev-up brings 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.

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.

Terminal window
# 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 . --verbose
{
"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)

Section titled “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.

{
"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 is f(automation_level, ceiling), enforced by scripts/promotion/promotion.js; see automation-behavior.md for the matrix. Defined in #448.

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.json ships the block inert (enabled: false) for discoverability.
  • Port. port: null resolves 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 numeric port overrides.
  • Engine. engine: null resolves the engine from the project’s docs-site.json; starting the server requires that file (run /docs-site init first).
  • Non-fatal. A docs-server start or health failure is surfaced (logged, and reported in the --check JSON under docs) but is never fatal for the app — the app’s health verdict and exit code are unaffected. Between warp-drive chunks, dev-up --check verifies the docs server when declared and flags a failure without blocking the app.
{
"docs": {
"enabled": true,
"engine": null, // resolve from docs-site.json
"port": null, // resolve the ledger docs port (base+3)
"health": "/",
"restart": "on-failure"
}
}

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.

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:

{
"_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):

  1. 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.
  2. Selects the highest-priority recipe whose _recipe.stacks intersect the detected stack (so a Cloudflare+Node project picks cloudflare-d1, priority 20, over node, priority 5).
  3. Substitutes placeholders (${PORT}, ${D1_DATABASE}) from lightly-detected values, strips _recipe, and writes a schema-valid dev.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.

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 stack is a pure data change — no engine edits:

  1. Drop a new templates/dev-recipes/<name>.json file.
  2. Give it a _recipe block: name, the stacks it applies to (matching detectStack tokens such as sveltekit, drupal, go, python), and a priority (higher wins ties; specific stacks should outrank generic node).
  3. Author the dev.json body, 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.

Moved — see Port Allocation.

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]

The main orchestrator. Runs the full lifecycle:

  1. Server: Check health → start if not running → kill & restart if unhealthy
  2. Migrations: Run migration command if configured and auto_run is true
  3. Seed data: Execute all scripts in seed/ directory alphabetically
  4. Test users: Provision users via the configured auth adapter
  5. 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

Lightweight standalone health probe. Returns JSON:

{"status": "healthy", "port": 5173, "url": "http://localhost:5173"}

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

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.

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)

Defined in seed/users.json. The superuser credentials are consistent across all projects:

Role Email 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.

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.

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):

{
"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:

Terminal window
node ~/.claude/scripts/warp-drive/run-checks.js --cwd . # exit 0 = all pass, 1 = a check failed
  • test — failing test names + count (vitest / jest / node:test).
  • typechecktsc diagnostics 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.

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.

Terminal window
# 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 identifiers

Capture 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.sh SessionStart hook injects the notes as additionalContext, so the next run does not re-learn a known quirk.
  • Check-runner — a flaky-test note (its summary is 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).
  1. Copy ~/.claude/templates/dev.json to your project root
  2. Customize the server command, port, and health endpoint
  3. Create seed/ directory with your initial seed scripts
  4. Copy ~/.claude/templates/seed/users.json and customize roles
  5. Set the auth adapter in dev.json to match your stack
  6. Run dev-up . --verbose to verify

The template at ~/.claude/templates/dev.json has sensible defaults for SvelteKit + Cloudflare Workers projects.

Moved to how-to — see Docs-Site Lifecycle.