The Prime Directive: Infrastructure as Code

Everything BoB manages must be declarative, version-controlled, reproducible, idempotent, and observable — with every piece of state owned by exactly one source of truth.

This is BoB’s Prime Directive. It is the single governing principle behind every design decision in the framework. If a pattern can’t be expressed as code in a repository, it doesn’t belong in BoB.

Core Principles

1. Declarative Manifests Over Imperative Steps

Configuration is declared in structured files (JSON, YAML, Markdown), not performed as a sequence of manual steps. The system reads manifests and converges to the desired state.

Why: Declarative manifests are self-documenting, diffable, and reviewable. Imperative steps are fragile, forgettable, and unauditable.

2. Version Control as Source of Truth

Every configuration file, script, template, and manifest lives in the BoB source repo (BOB_SOURCE, a git repository) and is deployed to ~/.claude/ (BOB_HOME) at runtime. Changes are tracked, reversible, and attributable. No configuration lives only in memory, environment variables, or local state.

Why: If it’s not in git, it doesn’t exist. Version control provides history, rollback, and collaboration.

3. Reproducibility

Given the same repository state, any machine can reach the same working configuration. Clone the repo, run cdi, and the environment is ready. No “works on my machine” exceptions.

Why: Reproducibility enables portability across machines, disaster recovery, and onboarding.

4. Idempotency

Running a setup or provisioning operation multiple times produces the same result as running it once. Scripts check before acting. Manifests describe desired state, not actions to take.

Why: Idempotent operations are safe to retry, safe to automate, and safe to run on schedule.

5. Canonical State Ownership

Every piece of mutable state has exactly one owner — a single file or system that is the authoritative source. Other systems may read that state, but only the owner may write it. Ownership boundaries are explicit and documented.

Why: Ambiguous ownership causes conflicting writes, stale reads, and subtle bugs. When two systems can both mutate the same state, neither can be trusted.

Ownership map:

StateOwnerReaders
Project tooling needsprovisions/<project>.jsonprovision.sh, cdprov, cdb
Dev environment shapedev.json (project root)dev-up, warp-drive
Global settingssettings.jsonClaude Code, hooks
Local overridessettings.local.json (gitignored)Claude Code
Work itemsGitHub Issueswarp-drive, commands, agents
Decision/lesson logGitHub Issues (decision, lesson)reports, retrospectives
Seed dataseed/ directorydev-up, test suites

6. Observability

All state transitions must be visible. When BoB provisions a project, seeds a database, or makes a decision, the outcome is recorded in a durable, inspectable medium — git history, GitHub Issues, or structured command output.

Why: If you can’t prove what happened, you can’t debug what went wrong. Observability turns “it broke” into “this specific transition failed at this point.”

Existing mechanisms: Git commit history (who changed what, when). GitHub Issues with decision and lesson labels (why). Warp-drive session summaries (what happened during autonomous work). cdprov --check output (current compliance state).

Principle: New tooling should produce structured output that answers: what changed, from what state, to what state, and why.

7. Convergence Under Failure

Operations must converge toward the declared state even from inconsistent starting points. Partial failures must not leave the system in an unrecoverable state. When an operation fails partway through, re-running it should make forward progress rather than compounding the damage.

Why: Idempotency guarantees safety on repeat. Convergence guarantees safety on partial failure. Both are needed for operations that can be interrupted (network drops, process kills, human cancellation).

Design rules:

  • Prefer atomic operations (symlink swap) over multi-step sequences
  • When multi-step is unavoidable, make each step independently idempotent
  • Never leave temporary state behind — clean up on both success and failure
  • cdprov --refresh is the reconciliation path: it reads the manifest and converges, regardless of current state

Approved Patterns

These are the established IaC patterns in BoB. New tooling should follow these conventions.

Provision Manifests

Each project declares what it needs from the registry in a JSON manifest.

~/.claude/provisions/bodmail.json
{
  "_meta": {
    "project": "bodmail",
    "path": "/Users/paulirving/Sites/bodmail",
    "stack": ["sveltekit", "cloudflare-workers", "d1"]
  },
  "skills": ["code-expert", "webapp-testing"],
  "commands": ["status"],
  "agents": ["code-reviewer", "lead-dev-architect"]
}

The manifest is the single source of truth for what a project needs. Running cdprov reads it and creates/removes symlinks accordingly.

Dev Environment Manifests

Each project that needs a dev environment declares it in dev.json at the project root.

{
  "server": {
    "command": "npm run dev",
    "port": 5173,
    "health_endpoint": "/api/health"
  },
  "database": {
    "type": "d1",
    "migrations": "drizzle"
  },
  "seed": {
    "directory": "seed",
    "command": "npm run seed"
  },
  "auth": {
    "adapter": "d1",
    "users_file": "seed/users.json"
  }
}

The dev-up script reads this manifest and brings the environment to a fully testable state: start server, run migrations, seed data, provision users, health check.

Seed Data Scripts

Seed data lives in a seed/ directory with idempotent scripts. Data covers all lifecycle states the domain defines — not just fresh records.

seed/
├── users.json          # Test users (admin@test.local / admin123)
├── 01-base-data.sql    # Reference data
├── 02-sample-emails.sql # Entities in various states
└── run.sh              # Idempotent seed runner

Templates

Reusable starting points live in ~/.claude/templates/. They’re copied (not symlinked) to projects, then customized.

~/.claude/templates/
├── dev.json            # Dev environment manifest template
└── seed/
    └── users.json      # Standard test users template

Global Settings

settings.json declares hooks, permissions, and status line configuration. It’s version-controlled and shared across all projects.

Skills, Commands, and Agents

Each is a self-contained markdown file (or directory with supporting files) that follows a consistent structure. Universal items live in ~/.claude/{skills,commands,agents}/. Domain-specific items live in ~/.claude/registry/{skills,commands,agents}/ and are provisioned per-project.

Anti-Patterns

These patterns violate the Prime Directive. Avoid them; refactor them when found.

Manual Configuration

Anti-pattern: Telling someone to “edit this file” or “run these commands” to set up a project.

# BAD: setup instructions in a README
1. Create a .env file with DATABASE_URL=...
2. Run `npm install`
3. Manually create the database
4. Copy the hook files from ~/.claude/hooks/ into .claude/hooks/

Fix: Declare the configuration in a manifest (dev.json, provisions/<project>.json) and let tooling handle it. If steps can’t be automated, they belong in a runbook with a script that does as much as possible.

Undeclared Dependencies

Anti-pattern: A project works because something was manually installed or configured outside its declared manifests.

# BAD: project requires a global npm package nobody documented
# "Oh, you need to `npm install -g wrangler` first"

# BAD: hook references a script that doesn't exist in the repo
"command": "/Users/someone/.claude/hooks/my-custom-hook.sh"

Fix: Every dependency belongs in a manifest. Global tools go in check-deps.sh. Project tools go in package.json. Hook scripts go in ~/.claude/hooks/ (version-controlled).

Imperative-Only Setup

Anti-pattern: Setup is a script that runs a sequence of commands without checking current state first.

# BAD: not idempotent — fails on second run
mkdir seed
cp template.sql seed/
createdb myproject
psql myproject < seed/template.sql

Fix: Check before acting. Use mkdir -p, CREATE TABLE IF NOT EXISTS, upserts instead of inserts. Make every script safe to run twice.

Unversioned State

Anti-pattern: Configuration exists only in local files, environment variables, or cloud dashboards — never committed to git.

# BAD: .env file with secrets is the only place API keys are documented
# BAD: Cloudflare Workers settings configured via dashboard, not wrangler.toml
# BAD: Database schema managed by clicking around in a GUI

Fix: Use .env.example (committed) for structure, secrets management for values. Use wrangler.toml (committed) for Workers config. Use migration files (committed) for schema changes.

Snowflake Configurations

Anti-pattern: Each project has a unique, hand-crafted setup that doesn’t follow any shared conventions.

# BAD: project A uses .env, project B uses config.yaml, project C uses env.sh
# BAD: project A seeds with SQL, project B with a Node script, project C manually

Fix: Follow the established conventions. Use dev.json for dev environment. Use provisions/<project>.json for tooling. Use seed/ for seed data. Templates exist to make this easy.

Orphaned Artifacts

Anti-pattern: Old symlinks, removed hooks, renamed commands, or deleted templates that leave dangling references.

# BAD: symlink points to a file that was deleted months ago
.claude/hooks/old-hook.sh -> ~/.claude/hooks/old-hook.sh (dangling)

Fix: When renaming or removing items, clean up all references. Run make check-symlinks to catch orphans. The provisioning system (cdprov) handles this for manifested items.

Shared Ownership of State

Anti-pattern: Multiple systems can write the same state, or a piece of state has no clear owner.

# BAD: both settings.json and settings.local.json define the same hook
# Which one wins? Depends on merge order — not declared anywhere.

# BAD: a script writes directly to provisions/<project>.json
# AND the user edits it manually AND cdprov also modifies it

Fix: Assign a single owner for each piece of state and document it. Other systems read; only the owner writes. If a piece of state doesn’t have a clear owner, it’s a design smell — resolve it before building on top of it.

Silent Operations

Anti-pattern: A script runs, exits 0, and produces no output about what it did. When something breaks later, there’s no trail to follow.

# BAD: provisioning silently creates symlinks
ln -sf "$source" "$target"
 
# BAD: seed script runs SQL with no indication of what was inserted
sqlite3 "$DB" < seed.sql

Fix: Log what changed. At minimum, report the transition: “linked code-expert ~/.claude/registry/skills/code-expert”, “seeded 12 emails across 4 lifecycle states”. Structured output (JSON, table) is preferred over free-text.

Non-Convergent Failure Modes

Anti-pattern: An operation that, when interrupted and re-run, makes things worse instead of better.

# BAD: appends to a config file without checking if the entry exists
echo "NEW_SETTING=true" >> .env
 
# BAD: creates a resource without checking if it already exists,
# then fails on the duplicate instead of converging
createdb myproject  # fails on second run with "already exists"

Fix: Check current state before acting. Use upserts, CREATE IF NOT EXISTS, mkdir -p, and symlink-swap patterns. Design for the re-run case, not just the first-run case.

Guardrails

Automated enforcement prevents drift from the Prime Directive.

Compliance Check

Run cdprov --check (or provision.sh --check <project-dir>) to audit a project’s IaC compliance. The check detects:

CheckWhat it catches
Undeclared itemsLocal files in .claude/{skills,commands,agents}/ not in the manifest
Modified templatesItems that should be symlinks but are local files (someone copied instead of linking)
Manifest driftRegistry symlinks present that aren’t declared in the manifest
Broken symlinksLinks pointing to deleted or moved targets
Missing registry itemsManifest declares items that don’t exist in the registry

Each warning includes a fix instruction. Exit code equals the number of warnings (0 = fully compliant).

Resolving Warnings

WarningFix
”local file not tracked in manifest”Add the item to the manifest, or delete the local file
”should be a symlink but is a local file”Delete the local file, run cdprov --refresh to restore the symlink
”provisioned but not in manifest”Add to manifest or run cdprov --refresh to remove
”broken symlink”Run cdprov --refresh to repair, or delete manually
”declared in manifest but missing from registry”Add the item to the registry, or remove from manifest

Verification Pipeline

The full CI pipeline (make ci) validates:

  • Dependencies (check-deps): Required tools at correct versions
  • Schemas (check-schemas): All JSON configs valid against schemas
  • Symlinks (check-symlinks): All symlinks across projects resolve
  • Hooks (check-hooks): All declared hook scripts exist and are executable
  • Provisions (check-provisions): Project state matches manifests

Convergence Test

Any provisioning or setup operation should pass the convergence test:

  1. Run from clean state — succeeds
  2. Run again immediately — succeeds with no changes (idempotency)
  3. Manually break something (delete a symlink, corrupt a file) — re-run succeeds and repairs (convergence)

If step 3 fails, the operation is idempotent but not convergent — it needs a reconciliation path.