Imported from zoolutions/dash (
AGENTS.md). Install upstream withnpx skills add zoolutions/dash. Copyright stays with the author.
dash
Project instructions for every agent: Claude Code (CLAUDE.md imports this file), Grok, Cursor,
Copilot, Codex. Claude-only extras (rules, skills, commands) live under .claude/.
dash (zoolutions/dash) — deploy web apps anywhere. Began as a fork of basecamp/kamal; made a clean break in 2026-08 (issue #115) and now moves independently — no upstream remote, no sync, no contributions back. Published on rubygems.org as dash; the executable is dash and the Ruby namespace is Dash:: (stage 2, issue #117). The server run directory is .dash/ as of stage 3b; the remaining on-server artifacts (kamal-proxy container, kamal network, KAMAL_* env vars) keep their names until stage 3c ships (see Staged rename below).
Tech Stack
- Ruby: 3.2–4.0 (CI matrix), Thor CLI, SSHKit + net-ssh, Zeitwerk
- Gem:
dash, built fromdash.gemspec - Proxy: ghcr.io/zoolutions/dash-proxy (sibling repo
../kamal-proxy→zoolutions/dash-proxy) - Testing: minitest + mocha; integration tests run real deploys in Docker
- Linting: rubocop-rails-omakase
Critical Rules
Never Do
- NO pushing directly to
main— everything lands via PR (ruleset-enforced; admin bypass is for migrations, not routine) - NO upstream syncs — the fork network is left; basecamp code arrives only by deliberate cherry-pick, never via an
upstreamremote - NO
-suffixversions likev1.0.0-rc1for the proxy —Gem::Versionparses-as a prerelease, which sorts OLDER than the base and hard-failsdash proxy boot - NO gem release before the proxy image exists — the tag named by
Dash::Configuration::Proxy::Run::MINIMUM_VERSIONmust be pullable fromghcr.io/zoolutions/dash-proxyfirst (rake releasegates on this) - NO
git push --tags— single-tag pushes only;rake releasecreates the gem tag viagh release create - NO rebasing published branches — merge forward; history is shared
- NO renaming the remaining server artifacts yet — the
kamal-proxycontainer name, thekamaldocker network, thekamal-proxy-config/kamal-loadbalancer-configvolumes,KAMAL_*env vars, and the image title label wait for stage 3c and a coordinated dash-proxy release. The run directory (.dash/) and config-digest label landed in 3b.
Always Do
- Branch features off
main, PR back intomain - Interpolate
MINIMUM_VERSIONin test expectations — never hardcode proxy versions - Run unit tests + rubocop before pushing;
bin/testbefore merging
Commands
bin/test # Full suite (integration needs Docker + published proxy image)
bundle exec ruby -Itest -e 'Dir["test/**/*_test.rb"].grep_v(/integration/).each { |f| require File.expand_path(f) }' # Unit tests only
bundle exec rubocop --parallel # Lint
bin/release [patch|minor|major|X.Y.Z] # Release: previews the bump + changelog, confirms, then runs rake release (`list`, `--dry-run`, `--force`)
rake release[3.2.0] # Release (low-level): version bump + tag v3.2.0 + GitHub release; CI trusted-publishes to RubyGems (Sigstore)
rake verify # Build the gem and list its contents
bin/sync-proxy-flags # Refresh the proxy flag manifest when MINIMUM_VERSION moves
Command output is condensed by rtk (PreToolUse hook). .rtk/filters.toml covers this repo's
bundle exec ruby -Itest unit-test run and rake verify (both shell out to a second Ruby
process that duplicate-loads rdoc/Gem::Platform, which is pure noise); every edit to it needs
rtk trust --yes + rtk verify. Write commands in hook-rewritable shapes: no for/subshell
wrappers, no | head on rtk-handled commands, bundle exec rubocop not bin/rubocop.
Architecture
Layer 5: bin/dash (entry point -> Dash::Cli::Main)
Layer 4: Dash::Cli::* lib/dash/cli (Thor commands, hooks)
Layer 3: Dash::Commander lib/dash/commander.rb (DASH singleton, target resolution)
Layer 2: Dash::Commands::* lib/dash/commands (docker command builders)
Layer 1: Dash::Configuration lib/dash/configuration (deploy.yml -> objects, validation)
Layer 0: SSHKit (remote execution)
The mental model
mainis the branch; a dash release ismainplus a published dash-proxy image whose tag equalsMINIMUM_VERSION. Proxy image first, gem second — always.
Release flow
- If the proxy changed or
MINIMUM_VERSIONmust move: in../kamal-proxy,script/release-dash v1.0.0.X→ CI publishesghcr.io/zoolutions/dash-proxy:v1.0.0.X(multi-arch, must be PUBLIC); setMINIMUM_VERSIONhere and runbin/sync-proxy-flags. bin/test(full suite).bin/release [patch|minor|major|X.Y.Z]— computes the next version fromlib/dash/version.rb, shows the commits since the last tag, requires a clean, up-to-datemain, and asks for confirmation before runningrake release[X.Y.Z], which gates on the proxy image, bumpslib/dash/version.rb+ theGemfile.lockpin, commits, pushesmain, creates thevX.Y.ZGitHub release. Therelease.ymlworkflow then tests, builds, Sigstore-signs, and trusted-publishes to RubyGems (environmentrubygems).
Gem tags are plain vX.Y.Z (own semver, 3.x line). Historical dash-v* tags are frozen. Proxy tags stay v<base>.<n> (or plain semver).
Proxy image contract
- dash reads the running proxy version FROM THE IMAGE TAG (
docker inspect kamal-proxy --format '{{.Config.Image}}') and compares it withGem::Version(Dash::Utils.older_version?). Only the tag is compared, so oldkamal-proxy-image containers upgrade cleanly todash-proxyimages. - The image must carry the label
org.opencontainers.image.title=kamal-proxy—dash proxy removeprunes by it (label rename waits for the bridge). - Defaults live in
lib/dash/configuration/proxy/run.rb(MINIMUM_VERSION, repositoryghcr.io/zoolutions/dash-proxy) andlib/dash/configuration/proxy/boot.rb(legacy boot path). - The old
ghcr.io/zoolutions/kamal-proxypackage stays published — gem versions < 3.2.0 pull it.
Staged rename (issue #115 stages 2–3)
| Stage | Scope | Status |
|---|---|---|
| 1 | CLI executable + user-facing text + docs (dash only) |
DONE (3.2.0) |
| 2 | Ruby namespace Kamal:: → Dash::, lib/kamal → lib/dash |
DONE (#117) |
| 3a | Local project directory .kamal/ → .dash/, dual-emitted DASH_*/KAMAL_* env vars |
DONE (#125) |
| 3b | Server run directory .kamal/ → .dash/ (self-migrating mv), config-digest label org.kamal.* → org.dash.* (read-both), local registry container |
DONE (#123) |
| 3c | kamal-proxy container + network + volume names, org.opencontainers.image.title — needs a coordinated dash-proxy release |
follow-up issue |
| 3d | Drop the read-both fallbacks (.kamal/ project dir, legacy digest label) |
5.0 |
Testing
- Unit: everything under
test/excepttest/integration— genuinely no Docker needed.test_helper.rbpinsDash::Utils.docker_archand stubsDash::Docker.included_files, so the suite is green whether or not a daemon is running. The old "two builder tests fail on Apple Silicon" caveat is gone — they were reading the local daemon's architecture. - Integration: real deploys against Docker-in-Docker VMs; pulls
ghcr.io/zoolutions/dash-proxy:$MINIMUM_VERSION— the tag must be published or the suite fails. - CI: rubocop + actionlint/zizmor + Ruby 3.2–4.0 matrix on
main. - Multi-host fixtures with a >1-host primary role need
loadbalancer: falseunderproxy:— the loadbalancer auto-activates and the dind harness can't support it.
Slash Commands
| Command | Purpose |
|---|---|
/lfg |
Full autonomous workflow: branch off main → understand → plan → TDD → verify → PR into main |
/plan |
Read-only planning → GitHub issue or docs/plans/ markdown (execute with /lfg) |
/architect |
Coordinate multi-layer work across the Thor CLI → Commander → Commands → Configuration cake |
/tdd |
Enforce RED → GREEN → REFACTOR with Minitest + Mocha |
/security |
Audit SSH command construction, secret handling, shell escaping, error-page paths |
/perf |
Baseline vs main in a worktree — command construction only (dash has no bench suite) |
/review-pr |
Review a PR for pattern + project-constraint compliance |
/github-review-pr |
Full PR pass: fix CI failures, then process review comments |
/github-review-failures |
Diagnose + fix CI failures until green |
/github-review-comments |
Process unresolved PR review comments |
/finish-prs |
Drive a set of open PRs to merge-ready, one at a time |
/debug-flaky |
Root-cause an intermittent test failure — evidence → repro → stress-proofed fix; never skip/retry |
Commands pin a model tier via frontmatter aliases (sonnet implementation, opus orchestration/security/review, fable read-only planning) so they track the latest model per tier.
More Documentation
docs/— the documentation site: a self-contained docs-kit Rails app (own bundle, RSpec, CI jobdocs-ci.yml), deployed to https://dash.zoolutions.llc bydeploy-docs.ymlon each release. The Configuration pages are GENERATED fromlib/dash/configuration/docs/*.yml(parsed bydocs/app/models/config_doc.rb); a new doc YAML failsdocs/spec/config_docs_spec.rbuntil registered indocs/app/models/doc.rb.ROADMAP.md— evidence-linked improvement roadmap.claude/rules/— coding-style, git-workflow, testing, agents, performance, striving-for-excellence, upstream-sync (historical).claude/commands/— the slash commands above- Proxy repo:
../kamal-proxy/CLAUDE.md— cross-repo release ordering - Upstream kamal docs (shared basics): https://kamal-deploy.org