“Remando en dulce de leche” — Dicho popular
dulce-de-leche (ddl)
One command to install, configure, and update every charly-vibes tool.
dulce-de-leche (ddl) is a thin orchestrator that bootstraps the entire
charly-vibes tool ecosystem on any platform
— macOS, Linux, or Windows. ddl itself needs nothing but the binary; a few
tools have prerequisites (npm for incitaciones, and a Rust toolchain for
tools without published release binaries) — ddl tells you exactly what is
missing and how to install it. See Troubleshooting.
The problem
The charly-vibes ecosystem has 8 active Rust CLI tools (wai, dont, ah, pretender, testaruda, vampiro, specodelic, turu/whisper), plus bd and openspec (workflow/issue tooling) and incitaciones (npm) — 12 managed tools in total (funkalization, the visual semantic design workspace, joined as the newest experimental extension). Each tool has:
- Its own config file or directory
- Its own init command
- Its own installation path (cargo install, brew install, scoop install)
Result: a new user runs 5+ install commands and 5+ init commands before seeing value. Configs are scattered across the repo root with no standard convention.
The solution
ddl provides a single binary that:
- Installs all tools from prebuilt GitHub release binaries —
cargo installas fallback when no release binary exists, npm for incitaciones - Configures them under a single
.ddl/directory — no root pollution - Migrates existing configs into
.ddl/(via symlinks in Phase 1) - Reports status across the whole toolset in one command
- Upgrades everything to compatible versions in one step
It is not a package manager, a reimplementation of any tool, or a CLI
launcher that replaces wai/dont/ah. Each tool keeps its own identity
and CLI grammar. ddl is just the orchestrator — the “brew bundle” for
charly-vibes.
Status
Shipped — ddl is published on crates.io and GitHub releases, and the full toolset boots on macOS, Linux, and Windows (verified end-to-end by the 3-platform smoke gate). Current version: see Release Status (generated from Cargo.toml at build time). See Implementation Status for current state.
License
Apache 2.0 — see LICENSE.
Release Status
Generated artifact (docs.yml, from Cargo.toml + git ref). Do not hand-edit — the repo source files remain authoritative.
- Version: v0.9.0
- Built from: main @ 1005d8b
Generated artifact (docs.yml, from docs/ecosystem-map.md). Do not hand-edit — edit that source file instead.
charly-vibes Tool Ecosystem
What ddl manages, and how each tool fits in.
For a machine-generated intent → tool table, see the capability matrix (regenerated from ddl’s registry; drift-checked in CI).
Every repo in the fleet carries the charly-vibes + dulce-de-leche GitHub
topics — filter the whole set with
topic:charly-vibes topic:dulce-de-leche.
Responsibility map
ddl installs, configures, and updates everything below; each tool owns one
slice of the agent-development loop. Dashed borders mark install-only tools —
ddl installs them, but their state lives outside .ddl/.
Every card below is a link — click a tool to open its documentation (in-org tools open their mdBook; external ones open their project page).
Inclusion criteria
ddl manages a tool when all of the following hold:
- It is an installable CLI — users run it in a terminal (not a desktop app, library, static site, or content repo).
- It has (or is about to get) a distribution channel in the charly-vibes tap/bucket.
- It has a config convention ddl can manage (or is explicitly install-only, like bd, openspec, and incitaciones).
Active Tools
Rust CLI tools
| Tool | Binary | Crates.io | Genesis consumer? | Purpose |
|---|---|---|---|---|
| wai | wai | wai-cli | ✓ | Workflow manager for AI development. PARA-based artifact organization, phase tracking, handoffs. The matrix subcommand is the standing deliberation surface from Design in Practice — problem, criteria, approaches, and the judgment at each intersection. |
| dont | dont, dt | dont-cli | ✓ | Epistemic discipline state machine. Claims, evidence, vocabulary tracking. Cozo DB backend. |
| espectacular | ah | espectacular | ✓ | Behavioral verification. Spec-test correspondence enforcement. |
| pretender | pretender | pretender | ✓ | Multi-language structural code quality. Tree-sitter-based complexity, duplication, mutation testing. |
| testaruda | testaruda, testaruda-adapter-rust, testaruda-adapter-python | testaruda | ✓ | Test selection engine. Ascent Datalog + provenance semiring. SQLite store. |
| vampiro | vampiro | vampiro | ✓ | Cross-language composition checking at call, module, effect, law, retry, resource, and trust boundaries. Crate workspace with per-language tracers. |
| wild | wild | wild-vibes | — | Machine-checked compatibility and composition of software components, modelled as a category of contracts. wild extract / wild check. |
| whisper | turu (aliases: turututu, whisper) | whisper-vibes | ✓ | Deterministic knowledge workspace management. Repo-local .whisper/ + global ~/.whisper/ routing. Install-only under ddl — its state is home-dir global, not repo-local legacy config. |
| specodelic | specodelic, spk | specodelic | ✓ | Markdown spec format (Intent / Constraints / Model / Properties) with the CLI that lints, compiles, verifies, and refactors it. Repo-local .specs/ corpus. |
| funkalization | funkalization | funkalization | — | Visual semantic design workspace. Versioned design semantics with typed design edits, generated obligations, scoped evidence, and loss-reporting adapters toward Specodelic and OpenSpec. Planned repo-local .funkalization/ workspace. |
| bajan | bajan | bajan | ✓ | Spec-driven knowledge-graph pipeline CLI — ingest sources, extract claims, query the graph. SQLite-backed triple store, provenance as graph structure. |
| gently | gently | not yet published | — | Rust port of Perl’s Graph::Easy: build graphs, lay them out deterministically, render as ASCII art, boxart, HTML, or Graphviz DOT — plus parse Graph::Easy and DOT input. |
Non-Rust tools
| Tool | Language | In homebrew? | Notes |
|---|---|---|---|
| beads | Go | ✓ (beads, homebrew-core + gastownhall/beads tap) | Issue tracker with first-class dependency support (bd binary). Release assets are named beads_* but ship the bd binary. No cargo fallback — the beads crate on crates.io is an unrelated package. Install-only — manages repo-local .beads/ itself. |
| openspec | TypeScript/npm | homebrew-core (openspec) | Spec-driven development workflow for AI coding agents. npm package @fission-ai/openspec; GitHub releases at fission-ai/openspec. Install-only — manages the repo-local openspec/ directory itself. |
| incitaciones | TypeScript/npm | npm (incitaciones) | Prompt/skill collection for CLI LLM tools. Install-only — skills land in ~/.agents/skills/ or .agents/skills/; ddl init checks for global skills and prompts if missing. |
Config file locations
Before ddl:
| Tool | Config location | Format | Notes |
|---|---|---|---|
| wai | .wai/ | TOML + directory | PARA structure |
| dont | .dont/ | Cozo DB + TOML | Heavyweight store |
| espectacular | .espectacular/ | JSON + TOML | Spec traces |
| pretender | .pretender.toml | TOML | Single file |
| testaruda | .testaruda/ | SQLite + TOML | Dependency graph store |
| vampiro | .vampiro/ | TOML | Composition config (config.toml) |
| specodelic | .specs/ | Markdown corpus | Spec files |
| funkalization | .funkalization/ | JSON workspace (planned) | Design workspace |
After ddl migration (Phase 1):
| Tool | Config location | Backwards compat |
|---|---|---|
| wai | .ddl/wai/ → .wai/ | ✓ symlink |
| dont | .ddl/dont/ → .dont/ | ✓ symlink |
| espectacular | .ddl/ah/ → .espectacular/ | ✓ symlink |
| pretender | .ddl/pretender.toml → .pretender.toml | ✓ symlink |
| testaruda | .ddl/testaruda/ → .testaruda/ | ✓ symlink |
| vampiro | .ddl/vampiro/ → .vampiro/ | ✓ symlink |
| specodelic | .ddl/specodelic/ → .specs/ | ✓ symlink |
| funkalization | .ddl/funkalization/ → .funkalization/ | ✓ symlink |
bd, openspec, incitaciones, and turu are install-only — their state lives outside .ddl/ (bd manages repo-local .beads/ itself; openspec manages openspec/ itself), so there is nothing to migrate.
Shared infrastructure (genesis-vibes)
Genesis-consuming tools (wai, dont, espectacular, pretender, testaruda,
vampiro, bajan) depend on genesis-vibes; wild and gently do not yet. Genesis
provides:
| Module | Used by | Purpose |
|---|---|---|
envelope | All | Structured JSON CLI output |
suggestions | wai, dont | Typo correction (DidYouMean) |
managed_block | wai, dont, ah, testaruda, vampiro | <!-- BLOCK:START --> injection, with opt-in provenance footer (<!-- provenance: generator=… version=… source=… sha=… --> inside the end marker; hash covers only the content, so drift is machine-checkable) |
config | All | ConfigFile trait, ConfigRegistry |
guide | All | CLI dispatch, error handling, verbosity |
status | All | StatusContributor trait, health aggregation |
doctor | All | DoctorRunner with auto-fix |
scaffold | All | init command standardization |
suite_linter | testaruda | Cross-tool config linting |
fixture | All | Test helpers |
git | rollout in progress | Read-only git plumbing: repo-root walks, changed-file enumeration, tracked/ignored checks, content hashes — one canonical subprocess-only substrate. |
git_hooks | wai, espectacular, pretender | Git-hook mechanics: install/uninstall with ownership markers, core.hooksPath resolution, lefthook managed-block injection. Tool-specific gates stay in the tools. |
Distribution channels
| Channel | Tools | Maintainer |
|---|---|---|
| crates.io | All released Rust tools (wild publishes as wild-vibes; gently not yet published) | Individual repos |
| Homebrew | wai, ah, dont, pretender, testaruda, vampiro, turu, dulce-de-leche — dont formula still a placeholder stub; bd and openspec ship in homebrew-core | homebrew-charly tap |
| Scoop | wai, ah, dont, pretender, testaruda, vampiro, turu, dulce-de-leche | scoop-charly bucket |
| GitHub Releases | All Rust tools; bd (gastownhall/beads); openspec (fission-ai/openspec) | Individual repos |
| npm | incitaciones; openspec (@fission-ai/openspec) | Individual repos |
Homebrew formulas (current state)
From homebrew-charly/:
| Formula | Version | Has real SHA? | Notes |
|---|---|---|---|
wai.rb | 2026.8.5 | ✓ | Real release |
testaruda.rb | 0.4.0 | ✓ | Real release (engine + rust/python adapters) |
vampiro.rb | 0.4.0 | ✓ | Real release |
turu.rb | 0.3.0 | ✓ | Real release (auto-updated by whisper’s release workflow) |
ah.rb | 0.5.0 | ✓ | Real release (assets under espectacular releases) |
pretender.rb | 0.5.0 | ✓ | Real release |
dulce-de-leche.rb | 0.3.0 | ✓ | Real release |
dont.rb | 0.0.0 | ✗ placeholder | Blocked: no GitHub release for dont yet (tag v0.3.0 exists locally) |
Scoop mirrors the same versions for Windows. All formula hashes were computed from downloaded release assets and cross-verified against each release’s published checksums.txt.
Only dont remains blocked on an upstream release. ddl automatically falls back to cargo install for tools whose release binaries are not yet published (except bd, which has no crates.io package — the beads crate is unrelated), so ddl init works either way. (Since DDL-ei3, brew/scoop are not part of ddl’s install decisions — the tap and bucket below serve manual installs.)
Installation
Install ddl on your platform. All platforms have a prebuilt binary
option (no toolchain needed); package managers and cargo are alternatives.
| Platform | Fastest path | Alternatives |
|---|---|---|
| macOS | Homebrew (below) or the curl one-liner | cargo install dulce-de-leche |
| Linux | curl one-liner (below) | Homebrew, cargo install dulce-de-leche |
| Windows | Scoop (below) | cargo install dulce-de-leche |
macOS & Linux (curl — prebuilt binary, resolves the latest release)
V=$(basename "$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
https://github.com/charly-vibes/dulce-de-leche/releases/latest)" | sed 's/^v//')
TGT="$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/^x86_64$/amd64/; s/^aarch64$/arm64/')"
curl -fsSL "https://github.com/charly-vibes/dulce-de-leche/releases/download/v${V}/ddl_${V}_${TGT}.tar.gz" | tar xz
chmod +x ddl && sudo mv ddl /usr/local/bin/
macOS (Homebrew)
brew tap charly-vibes/charly
brew install dulce-de-leche
Windows (Scoop)
scoop bucket add charly https://github.com/charly-vibes/scoop-charly.git
scoop install dulce-de-leche
Any platform (Cargo)
cargo install dulce-de-leche
Verify installation
ddl --version
Next steps
Run ddl init to bootstrap the entire charly-vibes toolset — see the
Quick Start guide. If the install fails, see
Troubleshooting.
Quick Start
Bootstrap the toolset
# Install ddl itself (see Installation)
brew tap charly-vibes/charly
brew install dulce-de-leche
# One command to install & configure everything
ddl init
What just happened?
✓ detected platform (macOS arm64 / Linux x86_64 / Windows)
✓ installed 10 managed tools — prebuilt release binaries (incitaciones via npm)
✓ created .ddl/ with configs for the config-managed tools
✓ ran each tool's init (wai init, dont prime, ah init, pretender init, …)
✓ checked for incitaciones skills and offered a global install
✓ created .gitignore entries for .ddl/ data files
Four tools are install-only — they don’t keep config under .ddl/:
bd, openspec, incitaciones, and turu.
Check the ecosystem
ddl status
Shows for each tool:
- ✓ installed, configured, healthy
- ✗ not installed (suggests:
ddl install wai) - ⚠ version outdated (suggests:
ddl upgrade)
Install a single tool
ddl install wai
Update everything
ddl upgrade
Migrate existing configs
If you already have tools configured with their legacy config directories
(.wai/, .dont/, etc.), migrate them under .ddl/:
ddl migrate
See what’s active
ddl scope
Shows which .ddl/ is active (walks up from CWD like git).
Where to go next
- Every command and flag: Commands
- Something not working: Troubleshooting
- What the tools do: Ecosystem Development
Shows which .ddl/ is active (walks up from CWD like git).
Commands
ddl init
Bootstrap the charly-vibes toolset.
ddl init
# Interactive mode — prompts for tool selection, confirmation
ddl init --yes
# Non-interactive mode — installs all tools, no prompts
ddl init --tools wai,dont
# Selective install — only the specified tools
ddl init --no-install
# Configure only — skip installation, just set up .ddl/
ddl init --gates
# Wire the end state users actually ask for: lefthook hard gates
# (pre-commit: ah check, pretender gate, spk lint; pre-push: ah check),
# .gitignore entries for tool data dirs (.testaruda/, .pretender/), and
# beads no-db stamping. Also orders tool inits so openspec runs before ah.
# Idempotent — safe to re-run. Follow with `lefthook install`.
ddl init --yes --gates
# Full bootstrap with gates wiring — the one-shot setup
Init cascade semantics
Tool inits run in a prerequisite order (openspec before ah), with the
.ddl/bin directory on the PATH so freshly downloaded binaries can be
initialized, and tools without an init command are skipped silently.
- A failing tool init fails the run with a resume summary listing the
exact command to finish manually (e.g.
dont init) or the selective re-run (ddl init --tools dont). No more exit-0 half-configured repos. - Successful inits are recorded in
.ddl/manifest.json; re-runs skip them (idempotent-until-green) instead of failing on tools that refuse re-initialization. - Non-interactive mode (
--yes/--json) passes prompt-free flags to tools with wizards (e.g.pretender init --non-interactive).
ddl install <tool>
Install a single tool by name.
ddl install wai
ddl install dont
ddl install ah
ddl install pretender
ddl install testaruda
ddl install vampiro
ddl install bd
ddl install openspec
ddl install incitaciones
ddl install turu
ddl install specodelic
ddl install, ddl init --tools, and ddl upgrade <tool> accept both the
binary name and the crate name, case-insensitive.
Canonical names and aliases
Binary names and crate names don’t always match — and agents repeatedly
cargo install the crate expecting the binary, or gh repo view the
binary name expecting the repo. The table below lists only the tools whose
names mismatch (the rest install by their own name):
| binary | crate | repository |
|---|---|---|
wai | wai-cli | charly-vibes/wai |
dont | dont-cli | charly-vibes/dont |
ah | espectacular | charly-vibes/espectacular |
turu | whisper-vibes | charly-vibes/whisper |
wild | wild-vibes | charly-vibes/wild |
openspec | @fission-ai/openspec (npm) | fission-ai/openspec |
ddl (this tool) | dulce-de-leche | charly-vibes/dulce-de-leche |
So ddl install espectacular installs ah. Typing ddl, dulce, or
dulce-de-leche into ddl install / --tools produces an error naming
the canonical repository — ddl manages every tool except itself.
ddl status
Show cross-tool health overview.
ddl status
ddl status --json # machine-readable output
ddl doctor
Run detailed diagnostics across all tools.
ddl doctor
ddl doctor --fix # attempt auto-fix
ddl doctor --json # machine-readable output
ddl version
Show versions of ddl and all managed tools.
ddl version
ddl version --check # check latest available versions (requires network)
ddl upgrade
Update all tools to latest compatible versions.
ddl upgrade
ddl upgrade wai # upgrade a single tool
ddl migrate
Move existing configs under .ddl/ (phase-1 symlink farm). To unify
disparate per-tool config dirs into one managed place, this is the path:
run ddl migrate at the repo root; each tool’s legacy config dir
(.wai/, .testaruda/, …) is moved under .ddl/<tool>/ and replaced by
a symlink.
ddl migrate
ddl migrate --undo # restore previous layout
Safety semantics (gh#30):
- Git-tracked configs are skipped with guidance. Git does not follow
symlinks, so replacing a tracked dir would make every tracked file
vanish from other clones.
ddl migratedetects tracked paths (git ls-files) and leaves them in place, telling you how to untrack first if you really want them under.ddl/. --undois non-destructive. Each legacy path is restored as a copy of the current.ddl/<tool>/contents; the.ddl/backing store is kept, so the undo can never lose tool state and a re-migrate is lossless. The manifest’smigration_stateis reset tonone.
ddl scope
Show which .ddl/ is active.
ddl scope
ddl feedback <kind>
File a feedback issue against ddl’s own repository
(charly-vibes/dulce-de-leche) via gh. The repository URL is compiled
into the binary, so this works from any directory — no repo spelunking.
Note: the binary is
ddl, but the crate isdulce-de-leche(cargo install dulce-de-leche). Looking forcharly-vibes/ddlon GitHub returns 404 — the canonical repository ischarly-vibes/dulce-de-leche.
ddl feedback bug # kind: bug|feature|question|chore
ddl feedback bug --dry-run # print issue body + gh command, don't submit
ddl feedback bug --from-last-error # prefill body from the last failed ddl command
echo "what happened" | ddl feedback bug # pipe body content via stdin
ddl feedback bug --title "custom title" # override the derived title
The issue body is pre-filled with environment context automatically: ddl
version, OS/arch, shell, gh version, current git remote/branch/dirty
state, repo tooling state, and a repro hash. Without network access, the
body is saved to a local file so nothing is lost.
Global flags
| Flag | Description |
|---|---|
-v, -vv, -vvv | Enable verbose output (progressive detail) |
-q, --quiet | Suppress output except errors |
-y, --yes | Non-interactive mode |
--json | Output as JSON for machine parsing |
--human | Force human-readable output in non-TTY contexts |
--verbose | Alias for -v |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (any failure — see stderr for details) |
Failures are not distinguished by exit code; the human-readable error and
its remediation hint are printed to stderr. Scripts should parse --json
output instead of exit codes.
Troubleshooting
ddl init fails with “Prerequisite missing”
ddl checks for prerequisites before installing. If a requirement is missing, install it and try again:
- Binary download: needs
curlorwget(pre-installed on most systems) - Cargo install: needs
rustc+cargo— install via rustup - incitaciones (npm): needs
npm— install via nodejs.org
Homebrew and Scoop are not used by ddl for installs — the tap and bucket exist only for manual installs of the tools themselves.
ddl install fails with “Tool not found”
Check available tools:
wai— Workflow managerdont— Epistemic disciplineah— Behavioral specification testingpretender— Code qualitytestaruda— Test selectionvampiro— Composition checkingspecodelic— Markdown spec format + linter (binary:spk)bd— Issue tracking (beads)openspec— Spec-driven developmentincitaciones— Prompts and skills for CLI LLM tools (npm)turu— Knowledge workspace management (whisper-vibes)
(ddl itself is not installable via ddl install — it manages every tool
except itself.)
ddl status shows no tools
Run ddl init first to install tools. If you already have tools installed
manually, run ddl init --no-install to create the manifest.
Binary download returns 404
The binary for your platform may not be published yet. Try:
cargo install dulce-de-leche
Network is unavailable
ddl works offline for already-installed tools:
ddl statusworks offlineddl versionworks offline (without--check)ddl upgraderequires network access
cargo install bd installs the wrong crate
The bd name on crates.io belongs to an unrelated crate (“big data tool
kit”); beads itself is also taken. Beads does not publish to crates.io —
never cargo install bd. Install bd through ddl install bd, which
pulls the pinned release binary from the gastownhall GitHub releases.
Deeper postmortems of incidents that cost fleet debugging time (CI install failures, migration symlink bugs) live in the Fleet incident records — maintained under Contributing.
Architecture
Overview
ddl is a thin orchestrator that delegates to existing tools and package managers. It does not reimplement package resolution, tool commands, or database management.
Key design decisions
Subprocess protocol over shared library linking
ddl communicates with each tool via subprocess calls (wai status --json,
dont status --json). This decouples release cycles — a genesis breaking
change doesn’t require coordinated releases of ddl + all 9 other tools.
Fallback installation chain
Binary download → cargo install (only when a release binary is unavailable and cargo is present; npm for incitaciones). Homebrew and Scoop are no longer part of ddl’s install decisions — the tap and bucket remain published for manual installs only.
Binary downloads are the most reliable path (no dependencies). Cargo install is the fallback when no release binary is available and cargo is installed. Brew/scoop are not used by ddl for installs (see DDL-ei3).
Phase 1: Symlink farm
Before deep genesis integration, ddl uses a symlink farm: each tool’s
legacy config directory stays at its old path but becomes a symlink into
.ddl/ (the backing store):
.wai/→.ddl/wai/(symlink).dont/→.ddl/dont/(symlink)- etc.
No tool patches needed. Zero risk — symlinks are transparent to file reads.
Phase 2: Native config proxy
Each tool learns to check .ddl/<tool>/ first via genesis. Requires
coordinated releases of each tool.
Directory layout
.ddl/
manifest.json # {"ddl_version": "0.3.0", "migration_state": "...", "tools": {...}}
config.toml # ddl's own config
compatibility-cache.json # cached compatibility matrix
wai/ # wai config files
dont/ # dont config files
ah/ # ah config files
pretender.toml # pretender config file
testaruda/ # testaruda config files
vampiro/ # vampiro config files
specodelic/ # specodelic config files
Legacy directories become symlinks pointing to .ddl/:
.wai/ -> .ddl/wai/ # symlink to .ddl/
.dont/ -> .ddl/dont/ # symlink to .ddl/
.espectacular/ -> .ddl/ah/ # symlink to .ddl/
.pretender.toml -> .ddl/pretender.toml # symlink to .ddl/
.testaruda/ -> .ddl/testaruda/ # symlink to .ddl/
.vampiro/ -> .ddl/vampiro/ # symlink to .ddl/
.specs/ -> .ddl/specodelic/ # symlink to .ddl/
Data flow for ddl status
ddl status
→ reads config from .ddl/config.toml
→ for each registered tool:
→ loads tool config from .ddl/<tool>/
→ runs tool's status command (subprocess)
→ parses output
→ collects result
→ renders aggregate status report
Platform strategy
| Platform | Primary installer | Fallback |
|---|---|---|
| macOS (ARM) | Binary download | Cargo install |
| macOS (Intel) | Binary download | Cargo install |
| Linux (ARM) | Binary download | Cargo install |
| Linux (Intel) | Binary download | Cargo install |
| Windows | Binary download | Cargo install |
incitaciones always installs via npm. Tools without published release
binaries require cargo on the machine.
See also
Implementation Status
Current state: shipped — all planned phases are implemented and published; macOS, Linux, and Windows are verified end-to-end by the 3-platform smoke gate. Current version: see the generated Release Status page (from Cargo.toml — never hand-typed).
Phases
| Phase | Description | Status |
|---|---|---|
| Core CLI | Cargo crate, CLI structure, error types | ✅ Done |
| Platform detection | OS, arch, package manager detection | ✅ Done |
| Installation chain | Binary-first, cargo fallback, npm for incitaciones | ✅ Done |
| Init command | Interactive and non-interactive bootstrap | ✅ Done |
| .ddl/ directory | Manifest, symlink farm, gitignore | ✅ Done |
| Status command | Cross-tool health overview | ✅ Done |
| Doctor command | Diagnostics and auto-fix | ✅ Done |
| Version management | Manifest, version, upgrade | ✅ Done |
| Cross-platform release | GitHub Actions, binary builds | ✅ Done |
| 3-platform smoke gate | End-to-end verification of release binaries | ✅ Done |
| Homebrew formula | Formula in homebrew-charly tap (manual installs) | ✅ Done |
| Scoop manifest | Manifest in scoop-charly bucket (manual installs) | ✅ Done |
| Documentation | mdBook, README, help text | ✅ Done |
Notes
- Since v0.5.0 (DDL-ei3), brew/scoop are not part of ddl’s install decisions — installs are binary-first with cargo fallback; the tap and bucket serve manual installs.
- Upstream coverage gaps: dont has a placeholder tap formula, so it
requires a Rust toolchain on machines without release binaries.
bdhas no cargo fallback at all (thebeadscrate on crates.io is unrelated) — it installs strictly from release binaries.
Legend
- ✅ Done — completed and tested
- 🔄 In progress — actively being worked on
- ⬜ Not started — design complete, not implemented
Glossary
Terms used throughout this book that are not part of ddl’s own CLI surface.
| Term | Meaning |
|---|---|
| Managed tool | A tool ddl can install, configure, and upgrade — listed by ddl catalog and in Ecosystem Development. |
| Install-only tool | A managed tool whose state lives outside .ddl/ (nothing to migrate): bd, openspec, incitaciones, turu. |
.ddl/ directory | The single backing store for tool configs and ddl’s manifest (manifest.json, config.toml) at a repository root. |
| Symlink farm | Phase-1 migration layout: a tool’s legacy config path (.wai/, .dont/, …) becomes a symlink pointing into .ddl/<tool>/, which holds the real files. |
| Root pollution | Scattering per-tool config directories across a repository root instead of consolidating them under .ddl/. |
| Init cascade | The ordered run of each tool’s own init command during ddl init (e.g. openspec before ah), with resume-until-green semantics. |
| Gates | ddl init --gates wiring: lefthook pre-commit/pre-push checks, .gitignore entries for tool data dirs, and beads no-db stamping. |
| Hard gates | Lefthook hooks that fail the commit/push (not merely warn) when a check fails. |
| Smoke gate | CI end-to-end verification that release binaries install and run on all three platforms (macOS, Linux, Windows). |
| Dogfooding | ddl (and the charly-vibes fleet) running its own conventions and tools on itself; enforced by conformance tests. |
| Genesis | Shared infrastructure crate (genesis-vibes/genesis) used by ecosystem tools for CLI scaffolding, completions, and version management. |
| Binary name / crate name | The executable you run vs. the package you install (ah vs. espectacular, turu vs. whisper-vibes). See the alias table in Commands. |
Development
Prerequisites
Setup
just setup
Common commands
| Command | Description |
|---|---|
just | Build and test |
just build | Build debug binary |
just build-release | Build release binary |
just test | Run all tests |
just lint | Run clippy linter |
just fmt | Format code |
just ci | Full CI pipeline (fmt + lint + test + build) |
just run <args> | Run ddl with arguments |
just docs | Build documentation locally |
just docs-serve | Live preview docs at localhost:3000 |
CI/CD
CI runs on GitHub Actions with the same just ci command used locally.
- CI: runs on every push/PR to main
- Release: triggered by git tags
v*— builds cross-platform binaries, publishes to GitHub Releases, crates.io, Homebrew, and Scoop - Docs: builds and deploys mdBook to GitHub Pages on pushes to
docs/
Release process
# 1. Update version in Cargo.toml
# 2. Update CHANGELOG.md (move Unreleased entries under the new version)
# 3. If managed tool pins changed, update versions.ddl.toml (§4 standard)
# 4. Commit and push
git add Cargo.toml CHANGELOG.md versions.ddl.toml
git commit -m "Release v0.8.1"
git push
# 5. Tag and push
git tag v0.8.1
git push --tags
# 6. GitHub Actions handles the rest:
# - Builds binaries for all platforms
# - Creates GitHub Release
# - Publishes to crates.io
# - Updates Homebrew formula
# - Updates Scoop manifest
# - Regenerates docs/src/release.md from Cargo.toml and deploys the book
The release page (docs/src/release.md) is generated at docs build time and
must never be hand-edited (it is gitignored).
Repository structure
.github/workflows/
ci.yml # CI workflow
release.yml # Cross-platform release workflow
docs.yml # Documentation deployment
docs/
book.toml # mdBook configuration
src/ # Documentation source files
scripts/
update-homebrew.py # Homebrew formula updater
update-scoop.py # Scoop manifest updater
src/
main.rs # CLI entry point
lib.rs # Library root
cli.rs # Command structure (clap derive)
error.rs # Error types
manifest.rs # Manifest management
platform.rs # Platform detection
Postmortems of incidents that shaped the current design: Fleet Incident Records.
Ecosystem Development
How to set up and work on the charly-vibes tool ecosystem as a whole. For
hacking on ddl itself, see Development. The conventions
referenced here are normative in
docs/standardization.md.
The map
Nine in-org tool repos, one meta-tool (ddl, this repo), and three external consumers:
| Repo | Binary | What it is | Category |
|---|---|---|---|
| wai | wai | Workflow management for agent sessions (PARA tracking, session close/next) | Core |
| testaruda | testaruda | Language-agnostic test selection engine | Core |
| dont | dont | Forces agents to ground claims before asserting them | Recommended |
| pretender | pretender | Code quality CLI (complexity, duplication, mutation) | Recommended |
| vampiro | vampiro | Composition-analysis CLI | Extension |
| specodelic | specodelic / spk | Markdown spec format (Intent/Constraints/Model/Properties) + linter/compiler | Extension |
| incitaciones | — | Skills package (npm; the one non-Rust tool) | Extension |
| espectacular | ah | Behavioral verification layer | Recommended |
| whisper | turu (aliases: whisper, turututu) | Deterministic knowledge workspace management | Recommended |
| dulce-de-leche | ddl | This orchestrator | — |
External (install-only, not developed here): bd (beads, issue tracking),
openspec (spec-driven proposals).
Every tool’s why and maturity lives in its README top block and
docs/src/status.md — that placement is the standard (§5), not a convention.
Workspace layout
Sibling checkouts matter: ddl’s conformance drift test
(tests/standard_conformance.rs) discovers them as ../<repo>/ relative to
this repo, and cross-repo work (registry updates, version bumps) assumes the
flat layout:
charly/
├── dulce-de-leche/ # this repo
├── wai/
├── testaruda/
├── dont/ pretender/ vampiro/ specodelic/ incitaciones/ espectacular/
└── homebrew-charly/ scoop-charly/ # taps, updated by release workflows
If you keep repos elsewhere, symlink them into the same parent directory — the drift test skips absent siblings (fleet conformance is a local/ddl-side check; each repo’s own CI only validates itself).
One-time setup
# 1. Rust + just + mdBook
rustup component add rustfmt clippy
brew install just mdbook # or cargo install just / mdbook
# 2. Ecosystem tools used by the repos' own CI (dogfooding)
cargo install wai testaruda pretender dont bd openspec
# 3. Clone the siblings you'll touch
for r in wai testaruda dont pretender vampiro specodelic espectacular incitaciones whisper; do
git clone "git@github.com:charly-vibes/$r.git" "../$r" # from ddl/
done
# 4. Per-repo bootstrap + verify
cd ../wai && just setup && just ci # repeat per repo
Day-to-day
- Task tracking:
bdper repo (issues live in each repo’s.beads/issues.jsonl, committed to git). Cross-repo epics live in ddl. - Session lifecycle:
wai close/wai nextper the workspace AGENTS.md. - Design changes: openspec proposal in the affected repo before code.
- Full check before pushing:
just ci— same command CI runs. There is exactly one entry point per repo; if a check isn’t injust ci, it isn’t a gate.
Conformance
cargo test -p dulce-de-leche --test standard_conformance checks every
present sibling (and ddl itself) against the mechanically-checkable standard
invariants: the three workflow files (§1), the binary release matrix,
root book.toml with the charly theme fields (§2/§3), README
motivation+status block (§5), and the pinned-versions file (§4). Red means
that repo needs a rollout fix — file it under the standardization epic
(DDL-u8x) and turn one assertion green per commit.
Releases
Every repo releases the same way (§1):
git tag v0.X.Y && git push --tags
CI then builds all five targets (linux/darwin amd64+arm64 tar.gz, windows
amd64 zip), publishes a GitHub Release with checksums.txt, publishes to
crates.io (tag must match Cargo.toml), and updates the homebrew tap + scoop
bucket. Caveat: tap/scoop steps skip silently if the TAP_GITHUB_TOKEN
secret is absent — re-run the publish job after adding it.
incitaciones is the exception: npm via its npm-publish.yml (the release
slot under that name).
Adding a new tool
Follow the onboarding checklist in
docs/standardization.md §7:
three workflow files, root book.toml + vendored theme, five book pages,
llms.txt, README why/status block, versions.ddl.toml, a row in ddl’s
registry (src/platform.rs, enforced by tests/tool_registry_drift.rs),
and a dogfood-matrix entry. Document any deviation in the tool’s
docs/src/status.md.
Fleet Incident Records
For maintainers and contributors — symptom → cause → fix records from incidents that actually cost fleet debugging time. User-facing symptoms are covered in Troubleshooting.
Beads release tarball clobbers repo checkouts
| Symptom | Conformance tests report README/LICENSE/CHANGELOG drift in repos that were not touched — across several repos at once, right after adding a beads install step to CI |
| Cause | tar -xzf bd.tgz run in the repo root: the beads release archive ships its own README, LICENSE, CHANGELOG, which overwrote the checkout’s files. Root-caused with a temporary eprintln probe pushed to CI |
| Fix | Extract into a temp dir and copy only the bd binary member |
- run: |
dir=$(mktemp -d)
curl -fsSL <release-asset-url> | tar -xzf - -C "$dir" bd
install -m 0755 "$dir/bd" /usr/local/bin/bd
CI jobs cancelled with no failure — cold cargo install batches
| Symptom | CI runs “cancelled” sporadically, no test failure, no log error — first seen in dont |
| Cause | A batch of pinned cargo installs on a cold cache exceeds the default 10-minute job timeout; GitHub marks the run cancelled, which reads like a flake |
| Fix | Bump the install step to 30 minutes fleet-wide (§4 dogfood matrix); keep installs pinned so cache-cold batches are predictable |
cargo install bd installs the wrong crate
| Symptom | Install of beads “succeeds” but the bd binary is not beads |
| Cause | The bd name on crates.io belongs to an unrelated crate (“big data tool kit”); beads itself is also taken. Beads does not publish to crates.io |
| Fix | Install from the gastownhall GitHub release asset only, pinned to a version (§4 dogfood matrix). Never cargo install bd |
Migration symlink incidents
| Symptom | ddl migrate --undo silently finds nothing to undo; re-running ddl migrate fails with Not a directory; single-file configs (e.g. .pretender.toml) became directory symlinks |
| Cause | Symlink targets persisted as relative paths while later discovery compared absolute paths (unnormalized), so detection saw nothing; migrate_tool() had no already-migrated guard; single-file configs were linked as directories |
| Fix | Normalize symlink targets before comparing (migrated_tools()); guard with is_symlink(legacy_path) and skip; separate single-file link creation/restoration (shipped as DDL-0yp, DDL-el9, DDL-cbp, DDL-40e) |
Bootstrap
Purpose
Define how ddl installs, initializes, and configures the charly-vibes toolset across different platforms. This is the core value proposition of ddl: one binary, one command, any platform.
Problem Statement
Installing the charly-vibes toolset currently requires: (1) knowing which tools exist, (2) installing each via a platform-specific package manager, (3) running each tool’s init command. This friction increases with each new tool and varies by platform. A single bootstrap command eliminates this friction.
Design Rationale
Platform Strategy
ddl detects the OS and architecture at runtime and selects the best installation method for each tool. The fallback chain is: binary download → cargo install → brew/scoop install.
Supported platforms: macos-arm64, macos-amd64, linux-arm64, linux-amd64, windows-amd64. On these platforms, ddl guarantees binary availability as the primary path — no prerequisites beyond the binary itself. Unsupported platforms (32-bit, FreeBSD) must use cargo install.
Tool Name Mapping
Each managed tool has different names across distribution channels. ddl maintains this mapping:
| Tool (binary name) | Cargo crate / npm package | Homebrew formula | GitHub repo |
|---|---|---|---|
wai | wai-cli | wai.rb | charly-vibes/wai |
dont | dont-cli | dont.rb | charly-vibes/dont |
ah | espectacular | ah.rb | charly-vibes/espectacular |
pretender | pretender | pretender.rb | charly-vibes/pretender |
testaruda | testaruda | testaruda.rb | charly-vibes/testaruda |
vampiro | vampiro | vampiro.rb | charly-vibes/vampiro |
turu | whisper-vibes | turu.rb | charly-vibes/whisper |
specodelic | specodelic | — (crate only) | charly-vibes/specodelic |
bd | — (Go tool) | — (homebrew-core + tap beads) | gastownhall/beads |
openspec | — (npm @fission-ai/openspec) | — | fission-ai/openspec |
incitaciones | — (npm incitaciones) | — | charly-vibes/incitaciones |
For cargo install, use the crate name (e.g., cargo install espectacular). For brew install, use the formula name minus .rb (e.g., brew install ah). The install policy (DDL-ei3) is prebuilt binary first, cargo as fallback — brew/scoop formulas remain published for manual installs only.
Prerequisites
Each installation method has prerequisites:
| Method | Prerequisites | Supported platforms |
|---|---|---|
| Binary download | curl or wget, internet access | All (preferred) |
| Cargo install | rustc + cargo | All (fallback) |
| Brew install | brew | macOS, Linux (with brew) |
| Scoop install | scoop | Windows |
The binary download path is the only truly “no prerequisites” path (aside from curl/wget, which are pre-installed on essentially all systems). On supported platforms, ddl distributes binaries for every release, so binary download is always available.
First-Run Flow
ddl init is the primary entry point. It presents an interactive checklist of available tools, installs the selected ones, runs their init commands, and creates the .ddl/ directory. In CI environments, ddl init --yes runs non-interactively with all tools.
Placeholder Detection
Some Homebrew formulas (currently only dont) have placeholder versions (0.0.0 with fake SHA256). ddl MUST detect these and skip brew install with a clear message, falling back to cargo install or binary download.
Network Failure Handling
If a tool installation fails (network error, 404, timeout), ddl records the tool’s status as "failed" in .ddl/manifest.json and continues with remaining tools. A subsequent ddl init or ddl install retries failed tools. This prevents half-installed states from blocking the user.
Scope
Non-Goals
- Installing tools outside the charly-vibes ecosystem
- Resolving version conflicts between tools (each tool manages its own dependencies)
- Installing the Rust toolchain (cargo install is a fallback, not a primary path — ddl does not install rustc)
Requirements
Requirement: Platform Detection
The CLI SHALL detect the operating system and CPU architecture at runtime.
On every supported platform, the preferred installation method is binary download; cargo install is the fallback when a release binary is unavailable and cargo is on PATH. No package manager (brew, scoop) participates in method selection.
Scenario: macOS (ARM)
- WHEN ddl runs on macOS with Apple Silicon
- THEN the system detects
macos-arm64as the platform - AND prefers binary download (primary path) or cargo install (fallback)
Scenario: macOS (Intel)
- WHEN ddl runs on macOS with Intel processor
- THEN the system detects
macos-amd64as the platform - AND prefers binary download (primary path) or cargo install (fallback)
Scenario: Linux (ARM)
- WHEN ddl runs on Linux with ARM64 processor
- THEN the system detects
linux-arm64as the platform - AND prefers binary download (primary path) or cargo install (fallback)
Scenario: Linux (Intel)
- WHEN ddl runs on Linux with AMD64/Intel processor
- THEN the system detects
linux-amd64as the platform - AND prefers binary download (primary path) or cargo install (fallback)
Scenario: Windows
- WHEN ddl runs on Windows
- THEN the system detects
windows-amd64as the platform - AND prefers binary download (primary path) or cargo install (fallback)
- AND binary downloads use
.exeextension and are placed in%LOCALAPPDATA%\ddl\bin\
Scenario: Unsupported platform
- WHEN ddl runs on an unsupported platform (e.g., 32-bit, FreeBSD)
- THEN the system displays a diagnostic error listing supported platforms
- AND exits with code 2
Requirement: Binary Distribution
The CLI SHALL be distributed as a pre-compiled static binary for each supported platform.
Scenario: Release assets
- WHEN a new version of ddl is released
- THEN the release includes binaries for: macos-arm64, macos-amd64, linux-arm64, linux-amd64, windows-amd64
- AND each binary is statically linked (no runtime dependencies)
Scenario: Binary download
- WHEN user downloads a ddl binary
- THEN the binary runs without any installation step (no package manager, no toolchain)
- AND the binary is self-contained (includes all logic)
Requirement: Init Command
The CLI SHALL provide ddl init as the primary bootstrap command.
Scenario: Interactive init (default)
- WHEN user runs
ddl initwithout flags - THEN the system detects the platform
- AND checks prerequisites (curl/wget for binary download; cargo for the fallback path)
- AND presents an interactive checklist of available tools with descriptions
- AND prompts the user to select which tools to install (default: all)
- AND installs each selected tool via binary download, falling back to cargo install when no release binary is available
- AND runs each installed tool’s init command (e.g.,
wai init,dont prime) - AND creates
.ddl/directory structure - AND writes
.ddl/manifest.jsonwith installed versions (status:"installed"or"failed") - AND optionally adds
.gitignoreentries for.ddl/data files
Scenario: Non-interactive init (CI)
- WHEN user runs
ddl init --yes - THEN the system proceeds without any prompts
- AND installs all available tools
- AND fails with a clear error if all tools fail; partial failure reports which succeeded
Scenario: Selective install
- WHEN user runs
ddl init --tools wai,dont - THEN the system only installs and configures the specified tools (wai and dont)
- AND skips all other tools
Scenario: Already initialized
- WHEN user runs
ddl initin a directory that already has.ddl/ - THEN the system checks
.ddl/manifest.jsonfor installed tools - AND prompts to install any missing tools
- AND retries any tools with
"status": "failed"in the manifest - AND skips tools that are already installed and configured
Scenario: Binary-first install with cargo fallback
- WHEN ddl needs to install a tool on any platform
- THEN it downloads the release binary from the tool’s GitHub releases
- AND when the release has no binary for the current platform (404 or asset missing)
- AND cargo is available on PATH
- THEN it falls back to
cargo install <crate>and recordssource: "cargo"in the manifest
Scenario: No release binary and no cargo
- WHEN a tool has no release binary for the current platform
- AND cargo is not installed
- THEN ddl reports an error naming both remedies: installing Rust (https://rustup.rs) to enable cargo install, or downloading the binary manually from the tool’s GitHub releases
- AND records
"status": "failed"for that tool
Scenario: Prerequisites check
- WHEN user runs
ddl init - THEN the system checks which prerequisites are needed based on the installation plan
- AND reports which prerequisites are missing with install guidance
- AND does not proceed with installation until prerequisites are met (unless
--yesis set) - AND prerequisite checks include:
curlorwget(for binary download) andcargo(for the fallback path)
Scenario: Network failure during install
- WHEN a tool installation fails due to network error, 404, or timeout
- THEN ddl records
"status": "failed"for that tool in the manifest - AND reports the failure with the error message
- AND continues with remaining tools
- AND exits with code 1 (partial failure)
- AND a subsequent
ddl initorddl installretries failed tools
Scenario: Binary download returns 404
- WHEN a binary download URL returns 404 (release not published yet)
- AND cargo is available on PATH
- THEN ddl falls back to
cargo install <crate> - AND reports: “⚠
binary not yet available for this platform — using cargo install instead”
Scenario: Binary download returns 404 without cargo
- WHEN a binary download URL returns 404 (release not published yet)
- AND cargo is not available on PATH
- THEN ddl does NOT attempt any further install method
- AND reports: “⚠
binary not yet available for this platform, and cargo is not installed. Install Rust (https://rustup.rs) or download the binary manually from /releases.” - AND exits with code 1
Requirement: Install Command
The CLI SHALL provide ddl install <tool> to install a single tool.
Scenario: Install known tool
- WHEN user runs
ddl install wai - THEN the system detects the platform
- AND looks up the tool in the name mapping table (crate name)
- AND installs via binary download, falling back to cargo install when no release binary is available
- AND updates
.ddl/manifest.json
Scenario: Install unknown tool
- WHEN user runs
ddl install unknown-tool - THEN the system displays a “did you mean” suggestion if a similar tool name exists
- AND exits with code 2
Scenario: Reinstall already installed tool
- WHEN user runs
ddl install waiand wai is already installed - THEN the system checks the version
- AND if up to date, reports “wai is already up to date”
- AND if outdated, suggests
ddl upgradeor offers to reinstall
Scenario: Windows binary placement
- WHEN user runs
ddl install waion Windows - THEN the release binary (
.exe) is downloaded from GitHub releases - AND placed in
%LOCALAPPDATA%\ddl\bin\ - AND suggests adding that path to
%PATH%if not already present
Requirement: Init Subcommand
The CLI SHALL run each managed tool’s init command after installation.
Scenario: wai init
- WHEN ddl installs wai via
ddl initorddl install wai - THEN it runs
wai initafter installation - AND reports success or failure
Scenario: dont prime
- WHEN ddl installs dont via
ddl initorddl install dont - THEN it runs
dont prime --plainafter installation - AND reports success or failure
Scenario: ah init
- WHEN ddl installs ah via
ddl initorddl install ah - THEN it runs
ah initafter installation - AND reports success or failure
Scenario: Tool init fails
- WHEN a tool’s init command fails
- THEN ddl reports the failure with the tool’s error output
- AND continues with remaining tools
- AND exits with code 1 (partial failure)
CLI Core
Purpose
Define the core command structure, global flags, and output conventions for the ddl CLI. All dulce-de-leche commands follow these patterns.
Problem Statement
For ddl to be a reliable bootstrap and orchestration tool, it requires a stable and predictable command interface. Without a consistent command structure, users would face a steep learning curve and inconsistent interactions when managing the charly-vibes toolset.
Design Rationale
Command Structure: Single-Level Nouns
ddl uses a flat noun-based command structure (e.g., ddl init, ddl status, ddl upgrade) rather than a verb-noun hierarchy. This is intentional: ddl is a thin orchestrator with a small number of commands that each do one thing well. The flat structure keeps the CLI discoverable and fast.
Output Format
All commands output human-readable text by default. Commands that return structured data support --json for machine parsing. Error output follows the self-healing pattern: describe the problem, suggest a fix, and provide the exact command to run.
Scope
Non-Goals
- Subcommand nesting (no
ddl tool install wai— useddl install wai) - Interactive mode beyond confirmation prompts (use
--yesfor non-interactive) - Pager integration (output is designed for terminal and CI)
Requirements
Requirement: Command Structure
The CLI SHALL provide a flat set of top-level commands, each performing a single orchestration function.
Scenario: List available commands
- WHEN user runs
ddl --helporddl -h - THEN the system displays a list of all available commands with brief descriptions
- AND shows global flags
Scenario: Version display
- WHEN user runs
ddl --versionorddl -V - THEN the system displays the ddl version number
Scenario: Command not found
- WHEN user runs
ddl <unknown-command> - THEN the system displays a “did you mean” suggestion if a similar command exists
- AND exits with a non-zero code
Requirement: Global Flags
The CLI SHALL support global flags that work with all commands.
Scenario: Verbose output
- WHEN user passes
-vor--verbose - THEN output includes additional context and installation details
Scenario: Quiet mode
- WHEN user passes
-qor--quiet - THEN only errors are shown
Scenario: Non-interactive mode
- WHEN user passes
--yesor-y - THEN the system proceeds with default choices for all confirmations
Scenario: JSON output
- WHEN user passes
--json - THEN the system outputs JSON for machine parsing
- AND the JSON envelope follows the genesis-vibes envelope format:
{"ok": true, "data": {...}, "warnings": [], "hints": []}
Scenario: NO_COLOR support
- WHEN the
NO_COLORenvironment variable is set - THEN the system disables colored output
Requirement: Exit Codes
The CLI SHALL use consistent exit codes across all commands.
Scenario: Success
- WHEN a command completes successfully
- THEN the system exits with code 0
Scenario: Partial failure
- WHEN a command partially completes (e.g., some tools installed, some failed)
- THEN the system exits with code 1
- AND reports which tools succeeded and which failed
Scenario: Unrecoverable error
- WHEN a command encounters an unrecoverable error (e.g., invalid arguments, missing platform)
- THEN the system exits with code 2
- AND displays a diagnostic error with a suggested fix
Requirement: Help Text
The CLI SHALL provide informative help text for every command.
Scenario: Command help
- WHEN user runs
ddl <command> --help - THEN the system displays the command’s usage, description, and flags
Scenario: Help text includes examples
- WHEN user runs
ddl <command> --help - THEN the help text includes 1-2 usage examples
.ddl/ Directory
Purpose
Define the structure, conventions, and lifecycle of the .ddl/ directory — the single configuration directory for all charly-vibes tools managed by ddl.
Problem Statement
Each charly-vibes tool has its own config directory at the repo root (.wai/, .dont/, .espectacular/, .testaruda/, .pretender.toml). This creates root pollution and makes it unclear which files belong to which tool. The .ddl/ directory consolidates all tool configs under one roof, with a clear convention for what is committed (config) and what is ignored (data).
Design Rationale
Phase 1: Symlink Farm
Phase 1 uses symlinks: .ddl/wai/ -> .wai/, .ddl/dont/ -> .dont/, etc. The original tool directories remain at the root as symlinks pointing into .ddl/. This requires zero tool patches — each tool reads from its original path, unaware that it’s a symlink.
Limitation: The root is not truly clean until Phase 2 (native config proxy), but the .ddl/ directory provides a single place to look for all tool configs.
Phase 2: Native Config Proxy (future)
Each tool (via genesis) learns to check .ddl/<tool>/ before its legacy location. At that point, the legacy directories can be removed entirely.
File Ownership
.ddl/manifest.json: Machine-written, machine-read. Committed to repo for reproducibility..ddl/config.toml: ddl’s own config. Human-editable. Committed..ddl/<tool>/config.toml: Per-tool config (symlinked to original location in Phase 1). Committed..ddl/<tool>/*.db: Tool data stores (Cozo, SQLite). Gitignored.
Discovery Rules
ddl discovers .ddl/ by walking up from CWD (same as git). The nearest ancestor .ddl/ wins. This allows nested projects (monorepo) to have their own .ddl/.
Scope
Non-Goals
- Merging tool databases (each tool keeps its own Cozo/SQLite store)
- Providing a UI for browsing
.ddl/contents - Auto-migration from legacy configs (Phase 1 symlinks are created by
ddl migrate, not automatically)
Requirements
Requirement: Directory Layout
The .ddl/ directory SHALL follow a standard layout.
Scenario: Standard layout after init
- WHEN user runs
ddl initorddl init --yes - THEN the system creates
.ddl/with the following structure:.ddl/manifest.json— version manifest.ddl/config.toml— ddl’s own config.ddl/wai/— wai config directory.ddl/dont/— dont config directory.ddl/ah/— espectacular config directory.ddl/pretender.toml— pretender config file.ddl/testaruda/— testaruda config directory.ddl/vampiro/— vampiro config directory.ddl/specodelic/— specodelic config directory
Scenario: Per-tool subdirectory
- WHEN a tool is installed via
ddl install <tool> - THEN the system creates the corresponding subdirectory under
.ddl/<tool>/ - AND the subdirectory is a symlink to the tool’s legacy config directory in Phase 1
Requirement: Phase 1 Symlink Convention
The CLI SHALL use symlinks to preserve backward compatibility with existing tool config paths.
Scenario: Symlink creation
- WHEN
ddl migrateruns - THEN the system moves
.wai/contents to.ddl/wai/ - AND creates a symlink
.wai/->.ddl/wai/ - AND repeats for each installed tool
Scenario: Symlink on Windows
- WHEN ddl runs on Windows
- THEN the system creates directory junctions instead of symlinks (symlinks require admin on Windows)
- AND documents this limitation in
ddl doctor
Scenario: Broken symlink detection
- WHEN user runs
ddl doctorand a symlink target is missing - THEN the system reports the broken symlink
- AND suggests
ddl migrate --undoto restore the original layout
Requirement: Gitignore Convention
The CLI SHALL manage .gitignore entries for .ddl/ data files.
Scenario: Gitignore creation
- WHEN user runs
ddl init - THEN the system prompts: “Add
.gitignoreentries for.ddl/data files? [Y/n]” - AND if confirmed, adds the following entries to
.gitignore:# dulce-de-leche — data files (do not commit) .ddl/**/*.db .ddl/**/store/ .ddl/install-log.json .ddl/doctor-cache.json
Scenario: Gitignore skip
- WHEN user runs
ddl init --yes - THEN the system does NOT modify
.gitignore - AND documents in the output: “Skipped .gitignore — run
ddl initinteractively to configure”
Requirement: Manifest File
The CLI SHALL maintain a .ddl/manifest.json file tracking installed tool versions.
Scenario: Manifest structure
- WHEN
.ddl/manifest.jsonis written - THEN it contains:
ddl_version: the version of ddl that wrote this manifestmigration_state:"phase1"or"phase2"or"none"tools: an object with one entry per tool, each containing:installed: version string (e.g.,"2026.5.1")source: installation source (e.g.,"brew","cargo","binary")status:"installed","pending", or"failed"(for retry support)compatible: version constraint string (e.g.,">=2026.3.0")
Scenario: Manifest creation
- WHEN user runs
ddl initorddl install <tool> - THEN the system creates or updates
.ddl/manifest.json
Scenario: Manifest read
- WHEN user runs
ddl statusorddl doctor - THEN the system reads
.ddl/manifest.jsonto determine which tools are installed - AND compares installed versions against the compatibility matrix
Scenario: Manifest missing
- WHEN user runs
ddl statusand.ddl/manifest.jsondoes not exist - THEN the system scans for tool binaries in PATH
- AND warns that the manifest is missing
- AND suggests
ddl initto create it
Scenario: Per-tool status tracking
- WHEN a tool installation fails
- THEN the manifest records
"status": "failed"for that tool - AND a subsequent
ddl initorddl installretries failed tools
Requirement: File Locking
The CLI SHALL use file locking when writing to .ddl/manifest.json to prevent corruption from concurrent processes.
Scenario: Concurrent write
- WHEN two ddl processes attempt to write to manifest.json simultaneously
- THEN the second process waits for the first to release the lock
- AND both writes complete without corruption
Scenario: Lock contention
- WHEN a ddl process cannot acquire the lock within a timeout
- THEN it reports a warning: “Another ddl process is running. Retrying…”
- AND retries up to 3 times before failing
Health
Purpose
Define how ddl aggregates and reports the health status of all managed charly-vibes tools. This includes ddl status (quick overview) and ddl doctor (detailed diagnostics).
Problem Statement
Each charly-vibes tool has its own status and diagnostic commands (wai status, wai doctor, dont doctor, ah doctor, pretender check, testaruda doctor). To check the health of the entire toolset, a user must run 6+ commands. ddl provides a single command that aggregates all tool health into one view.
Design Rationale
Subprocess Communication
ddl communicates with each tool via subprocess calls, not shared library linking. This decouples ddl’s release cycle from each tool’s release cycle. ddl calls wai status --json, dont status --json, etc. and parses the JSON output. If a tool does not support --json, ddl falls back to parsing the human-readable output (degraded, but functional).
Status vs Doctor
ddl status: quick check — is the tool installed? is it configured? is it working? (<500ms)ddl doctor: detailed diagnostics — full health check with fix suggestions (<2s)
Discovery Mode
When no .ddl/manifest.json exists, ddl scans PATH for known tool binaries. This allows ddl status to work even before ddl init is run (e.g., on a machine where tools were installed manually).
Scope
Non-Goals
- Modifying tool state (read-only operations)
- Running tool-specific commands (e.g.,
ddl dont conclude— usedont concludedirectly) - Diagnosing tool-specific issues beyond what the tool reports
Requirements
Requirement: Status Command
The CLI SHALL provide ddl status to show a quick overview of all managed tools.
Scenario: All tools healthy
- WHEN user runs
ddl statusand all tools are installed, configured, and healthy - THEN the system displays a summary with each tool’s status
- AND uses ✓ for healthy, ⚠ for warning, ✗ for failed
- AND shows tool versions
- AND exits with code 0
Scenario: Some tools not installed
- WHEN user runs
ddl statusand some tools are not installed - THEN the system displays ✗ for missing tools
- AND suggests
ddl install <tool>for each missing tool - AND exits with code 1
Scenario: Some tools unhealthy
- WHEN user runs
ddl statusand some tools are installed but unhealthy - THEN the system displays ⚠ for each unhealthy tool
- AND suggests
ddl doctorfor detailed diagnostics - AND exits with code 1
Scenario: Status as JSON
- WHEN user runs
ddl status --json - THEN the system outputs JSON following the format defined in
cli-core/spec.md(Requirement: JSON Output) - AND the data field contains each tool’s name, version, status, and message
Scenario: Status reads from subprocess
- WHEN user runs
ddl status - THEN ddl calls each installed tool’s status command via subprocess
- AND parses the JSON output (preferred) or human output (fallback)
- AND includes the tool’s own diagnostic output in the ddl report
Scenario: Tool subprocess fails
- WHEN ddl calls a tool’s status command and the subprocess fails
- THEN ddl reports the tool as “unreachable”
- AND includes the subprocess error output
- AND continues checking remaining tools
- AND exits with code 1
Scenario: No tools installed
- WHEN user runs
ddl statusand no tools are installed - THEN the system displays “No charly-vibes tools installed”
- AND suggests
ddl init - AND exits with code 1
Scenario: Discovery mode (no manifest)
- WHEN user runs
ddl statusand.ddl/manifest.jsondoes not exist - THEN the system scans PATH for known tool binaries (wai, dont, ah, pretender, testaruda, vampiro, turu, specodelic, bd, openspec, incitaciones)
- AND reports each found tool as “detected (PATH)”
- AND suggests
ddl initto create a manifest - AND exits with code 0 if all tools found, code 1 if some missing
Requirement: Doctor Command
The CLI SHALL provide ddl doctor to run detailed diagnostics on all managed tools.
Scenario: Full diagnostics
- WHEN user runs
ddl doctor - THEN the system runs each tool’s doctor command via subprocess
- AND aggregates the results into a single report
- AND shows pass/warn/fail status for each tool
- AND includes fix suggestions from each tool’s doctor output
- AND exits with code 0 if all pass, code 1 if any fail
Scenario: Doctor as JSON
- WHEN user runs
ddl doctor --json - THEN the system outputs JSON following the format defined in
cli-core/spec.md - AND groups results by tool with pass/warn/fail counts
Scenario: Doctor fix
- WHEN user runs
ddl doctor --fix - THEN the system runs each tool’s doctor with auto-fix enabled
- AND reports which fixes were applied and which failed
- AND exits with code 0 if all fixes applied, code 1 if any failed
Scenario: Tool doctor not available
- WHEN ddl runs doctor for a tool that doesn’t have a doctor command
- THEN ddl reports “doctor not available” for that tool
- AND performs a basic check (is the binary in PATH? is the config directory valid?)
- AND continues with remaining tools
- AND does not change the exit code (basic checks are not failures)
Version Management
Purpose
Define how ddl tracks, pins, and updates tool versions across environments. This ensures reproducibility: the same ddl init produces the same toolset on any machine.
Problem Statement
Without version pinning, ddl init on two different days could install different versions of tools. This creates “works on my machine” problems for the toolchain itself. A manifest file ensures that all environments get the same tool versions.
Design Rationale
Manifest Format
The manifest (.ddl/manifest.json) is the source of truth for which tools are installed, at what versions, and via which source. It is written by ddl init and ddl install, and read by ddl status, ddl doctor, and ddl upgrade. The manifest is committed to the repository (config changes are reproducible) while data files are gitignored.
Version Compatibility Matrix
ddl uses a compatibility matrix to prevent installing tool versions with incompatible config conventions. The matrix is fetched dynamically from https://charly-vibes.github.io/ddl/compatibility.json at runtime. This ensures the matrix is always fresh without requiring a ddl release. An embedded fallback matrix is used when offline.
Online vs Offline Mode
- Online:
ddl version --checkqueries the latest available versions of all tools and the compatibility matrix - Offline:
ddl version(without--check) shows installed versions from the manifest only, using the embedded fallback matrix for compatibility checks ddl upgraderequires network access (must query latest versions)ddl statusworks offline (reads from manifest)
Binary-Downloaded Tool Upgrades
When a tool was installed via binary download (the preferred path), ddl upgrade re-downloads the binary from the tool’s GitHub releases. The upgrade fetches the latest release tag, downloads the binary for the current platform, and replaces the existing binary. This is the same mechanism as the initial binary download.
Scope
Non-Goals
- Semantic version constraint solving (dependencies between tool versions)
- Rolling back tool versions (use the platform’s package manager for this)
- Managing tool updates outside ddl (if a user runs
brew upgrade wai, ddl detects the version change on next status check)
Requirements
Requirement: Manifest File
The CLI SHALL maintain a .ddl/manifest.json file tracking installed tool versions.
Scenario: Manifest creation
- WHEN user runs
ddl initorddl install <tool> - THEN the system creates or updates
.ddl/manifest.json - AND records the ddl version, each tool’s installed version, installation source, and installation status
Scenario: Manifest structure
- WHEN
.ddl/manifest.jsonis written - THEN it contains:
ddl_version: the version of ddl that wrote this manifestmigration_state: the migration phase (e.g.,"phase1","none")tools: an object with one entry per tool, each containinginstalled(version string),source(e.g.,"brew","cargo","binary"),status("installed","pending","failed"), andcompatible(version constraint string)
Scenario: Manifest read
- WHEN user runs
ddl statusorddl doctor - THEN the system reads
.ddl/manifest.jsonto determine which tools are installed - AND compares installed versions against the compatibility matrix
Scenario: Manifest missing
- WHEN user runs
ddl statusand.ddl/manifest.jsondoes not exist - THEN the system scans for tool binaries in PATH
- AND warns that the manifest is missing
- AND suggests
ddl initto create it
Requirement: Version Command
The CLI SHALL provide ddl version to show the versions of ddl and all managed tools.
Scenario: Offline version display
- WHEN user runs
ddl version(without--check) - THEN the system displays the ddl version
- AND displays each managed tool’s installed version from the manifest
- AND does NOT require network access
- AND exits with code 0
Scenario: Online version check
- WHEN user runs
ddl version --check - THEN the system queries the latest available versions of all tools
- AND marks outdated tools with “update available” and the new version
- AND requires network access
- AND exits with code 0 if all up to date, code 1 if any outdated
Scenario: Version as JSON
- WHEN user runs
ddl version --json - THEN the system outputs JSON following the format defined in
cli-core/spec.md - AND includes ddl version and managed tool versions
- AND works offline (reads from manifest)
Scenario: Version check fails (no network)
- WHEN user runs
ddl version --checkand network is unavailable - THEN the system displays installed versions from manifest
- AND reports: “⚠ Network unavailable — showing cached versions”
- AND exits with code 0
Requirement: Upgrade Command
The CLI SHALL provide ddl upgrade to update all managed tools to their latest compatible versions.
Scenario: Upgrade all tools
- WHEN user runs
ddl upgrade - THEN the system reads
.ddl/manifest.json - AND fetches the latest compatibility matrix
- AND for each tool, calls the appropriate upgrade method:
- brew-installed:
brew upgrade <formula> - cargo-installed:
cargo install <crate> - binary-downloaded: re-downloads binary from GitHub releases
- scoop-installed:
scoop update <app>
- brew-installed:
- AND verifies the new version
- AND updates
.ddl/manifest.json - AND reports which tools were upgraded and which were already up to date
Scenario: Upgrade single tool
- WHEN user runs
ddl upgrade wai - THEN the system upgrades only the specified tool using the appropriate upgrade method
Scenario: Upgrade binary-downloaded tool
- WHEN user runs
ddl upgradeand a tool was installed via binary download - THEN ddl fetches the latest release tag from the tool’s GitHub repo
- AND downloads the binary for the current platform
- AND replaces the existing binary
- AND updates
.ddl/manifest.json
Scenario: No updates available
- WHEN user runs
ddl upgradeand all tools are at their latest versions - THEN the system reports “All tools are up to date”
- AND exits with code 0
Scenario: Upgrade fails
- WHEN an upgrade command fails for a tool
- THEN ddl reports the failure with the error output
- AND continues upgrading remaining tools
- AND exits with code 1 (partial failure)
Scenario: Upgrade requires network
- WHEN user runs
ddl upgradeand network is unavailable - THEN the system reports “⚠ Network unavailable — cannot upgrade”
- AND suggests
ddl versionto view installed versions - AND exits with code 1
Requirement: Compatibility Matrix
The CLI SHALL use a compatibility matrix to prevent installing incompatible tool versions.
Scenario: Dynamic matrix fetch
- WHEN user runs
ddl install,ddl upgrade, orddl version --check - THEN the system fetches the compatibility matrix from
https://charly-vibes.github.io/ddl/compatibility.json - AND caches it in
.ddl/compatibility-cache.jsonfor offline use - AND falls back to the embedded matrix if the fetch fails
Scenario: Compatible version
- WHEN user runs
ddl install waiand the latest wai version is within the compatible range - THEN installation proceeds normally
Scenario: Incompatible version
- WHEN user runs
ddl install waiand the latest wai version is outside the compatible range - THEN the system refuses to install
- AND displays a message: “wai version X.Y.Z is not compatible with this version of ddl. Please upgrade ddl first.”
- AND suggests
ddl upgrade(to upgrade ddl itself)
Scenario: Embedded fallback matrix
- WHEN the dynamic matrix fetch fails (no network, server down)
- THEN the system uses the embedded fallback matrix shipped with the ddl binary
- AND reports: “⚠ Using cached compatibility matrix — some versions may be outdated”
- AND proceeds with installation
Scenario: Compatibility matrix structure
- WHEN the compatibility matrix is fetched or read from cache
- THEN it contains version constraints for each managed tool (e.g.,
"wai": ">=2026.3.0") - AND is updated independently of ddl releases (with each tool release)