Decision record: ADR-001
Status: Accepted
The Focus AI
2026-07-23
Standards Dockerfile redesign
This is a decision record: what was decided, what else was considered, and what would change the answer. It binds nothing outside this repository (STD-006 §3.5). No standard has been written for this area yet, so nothing here binds
anything.
A decision is never edited to reverse itself. It is superseded by a new record that names it, so the change of mind stays visible.
Problem Statement
A developer who clones the Focus.AI standards repo needs a reproducible, containerized environment to run standards tooling and scaffold new projects. The existing Dockerfile pre-bakes too much — it pre-populates /app with a template mise.toml, installs all tools at build time, and configures fnox and pi extensions inside the image. This creates three problems:
- Stale tooling. When the standards mise.toml changes (new tools, version bumps), the image must be rebuilt. The user can't just
git pullandmise install— they need a full Docker rebuild. - Blurred boundaries. The image conflates the standards environment (
/standards) with the user's workspace (/app). The user can't tell what came from the image vs. what setup-project.md would scaffold. - Unnecessary weight. fnox, secrets infrastructure, zsh, and systemd linger are baked into an image that's meant to be a lightweight dev sandbox.
Solution
A minimal Dockerfile with a single responsibility: make /standards fully operational after mise install. Everything else — project scaffolding, secrets, skills, pi extensions for the user's project — is deferred to the setup-project.md prompt run at runtime inside /app.
The image is lean: system packages, mise, shell activation, tmux config, git identity, and the standards repo. That's it. The user mounts their host's ~/.pi directory for LLM API keys, starts the container, runs mise install inside /standards, and they're ready to scaffold.
User Stories
- As a developer new to Focus.AI standards, I want to
docker buildanddocker runa single image and land in a shell where mise is active and/standardsis present, so that I can start working immediately without installing anything on my host machine.
- As a developer, I want
mise installinside/standardsto fetch the exact versions of Node, pi, fnox, firecrawl-cli, and any other tools declared in the standards mise.toml, so that my environment always matches what the team expects.
- As a developer, I want pi to have access to my LLM API keys without baking them into the image or passing them as
-eflags, so that my keys stay on my host machine and never appear in shell history ordocker inspect.
- As a developer, I want
/appto be a completely empty directory at container start, so that I can scaffold any project into it without colliding with pre-existing config files or tools.
- As a developer, I want to run
pi -p prompts/setup-project.mdtargeting/appand have it scaffold a fully working project from scratch, so that I can go from zero tomise devin a single prompt.
- As a developer, I want the Dockerfile to respect the "never use
mise -g" constraint, so that all tooling is project-scoped and the standards mise.toml is the single source of truth for what's installed.
- As a developer, I want shell activation (mise) to work whether I run
docker run ... bashordocker run ..., so that I don't have to remember which invocation pattern activates mise.
- As a developer, I want tmux to be configured with mouse support and extended keys, so that pi's tmux-based subagents work correctly with mouse scrolling and multi-line input.
- As a developer, I want git to be pre-configured with a default identity, so that pi can perform git operations (commits, branches) without prompting me for name and email.
- As a developer, I want the GitHub CLI available in the container, so that I can authenticate with GitHub and push scaffolded projects without leaving the container.
- As a developer, I want pi extensions (pi-tmux, pi-powerline-footer) to be auto-installed from the standards
.pi/settings.jsonon first use, so that I don't need to runpi installcommands manually.
- As a developer, I want the container to run as a non-root
agentuser, so that file permissions on mounted volumes and git operations use a predictable identity.
- As a developer, I want to
git pullinside/standardswhen the standards repo updates and runmise installagain, so that my tooling stays current without rebuilding the Docker image.
- As a developer, I want the Dockerfile's top comment block to document the exact
docker build,docker run, and first-steps commands, so that I can copy-paste them without reading source code.
- As a developer, I want pi sessions to be ephemeral (lost when the container exits), so that I start fresh each time and don't accumulate stale session state across container runs.
- As a CI pipeline, I want to build the standards image and run the existing test suite against it, so that every change to the Dockerfile or standards repo is validated automatically.
- As a developer, I want the image to exclude zsh, fnox, 1Password CLI, systemd linger, and
/apppre-population, so that the image is small, focused, and fast to build.
- As a developer, I want
mise trustto already be done for/standardsat build time, so that my firstmise installdoesn't present an interactive trust prompt.
- As a developer, I want build-time dependencies (curl, build-essential, git, tmux, ca-certificates, unzip) to be installed in an early layer that caches well, so that iterative Dockerfile changes don't re-download system packages.
- As a developer, I want the
prompts/PRM-001-generate-dockerfile.mdprompt to produce a Dockerfile consistent with all these decisions, so that the generated artifact matches the documented design.
Implementation Decisions
Boundary: build-time vs. runtime
The Dockerfile bakes only what's needed to make mise operational inside /standards. mise install is a runtime step — the user runs it. This keeps the image small, the tooling always current, and the Dockerfile simple.
Boundary: /standards vs. /app
/standardsis the standards repo. It contains mise.toml, pi config, prompts, best-practices, reports, skills-lock.json. It is fully self-contained.mise installmakes all tools available; pi uses.pi/settings.jsonfor extensions and npm routing./appis a blank directory. Nothing is created there at build time — no mise.toml, no .gitignore, no config files. It is the sandbox wheresetup-project.mdscaffolds a new project at runtime.
These two directories are independent domains. The standards environment is ready the moment the container starts; the user's project is built on demand.
User identity
A single non-root user named agent (home: /home/agent). The Dockerfile switches to agent once the user is created and never switches back to root. File ownership is handled via COPY --chown=agent:agent rather than post-hoc chown in a root RUN block.
mise installation
Mise is installed by curl https://mise.run | sh as the agent user. The binary lands at /home/agent/.local/bin/mise. /home/agent/.local/bin is added to PATH via ENV. Mise is never installed globally (mise -g).
mise trust
mise trust /standards runs at build time (after the standards COPY) so the user's first mise install does not trigger an interactive trust prompt.
mise install
Not run at build time. The user runs cd /standards && mise install as their first step inside the container. This fetches Node, pi, fnox, firecrawl-cli, and any other tools declared in the standards mise.toml.
pi and LLM keys
Pi does not have baked-in API keys. The user mounts their host's ~/.pi directory into the container at /home/agent/.pi. Pi finds its user-level config (including API keys) there, while also reading project-level config from /standards/.pi/settings.json.
pi extensions
Pi extensions are declared in /standards/.pi/settings.json (under packages). Pi auto-installs missing packages on startup. The Dockerfile does not run pi install commands.
Shell activation
Mise activation is written to both .bashrc and .bash_profile so that mise tools are available whether the user runs docker run ... bash (non-login shell, reads .bashrc) or docker run ... (login shell via CMD, reads .bash_profile).
Bash is the only shell. Zsh is not installed or configured.
System packages
Only these packages are installed: curl, ca-certificates, git, tmux, unzip, build-essential. GitHub CLI (gh) is installed separately via its own apt repository.
Not installed: zsh, fnox, 1Password CLI, systemd, or any other tool not required for the base image to function.
tmux configuration
A minimal ~/.tmux.conf is baked with mouse mode and extended keys enabled. This is required by the pi-tmux extension for subagent pane interaction.
Git configuration
Default identity is set: TheFocus.AI / agent@thefocus.ai. This allows pi and the user to perform git operations without identity prompts.
Layer ordering
Layers are ordered from least-frequently-changing to most-frequently-changing to maximize Docker build cache reuse:
- System packages (rarely changes)
- Create
agentuser and directories/standards,/app(rarely changes) - Switch to
USER agent(permanent) - Install mise (rarely changes)
- Shell activation, tmux config, git config (rarely changes)
- Copy standards repo +
mise trust(cache-busted when any source file in the repo changes)
CMD
CMD ["/bin/bash", "-l"] — starts a login shell with mise activated. Working directory is the filesystem root; the user navigates to /standards or /app as needed.
PRM-001-generate-dockerfile.md prompt
The prompt that guides pi to produce this Dockerfile enumerates every constraint explicitly. Pi does not need to discover them by reading the entire repo — the prompt is the specification. The prompt is the source of truth; the generated Dockerfile is the derived artifact.
Runtime contract
The Dockerfile's top comment block documents:
- Build command:
docker build -t focus-ai-standards-agent . - Run command:
docker run -it --rm -v ~/.pi:/home/agent/.pi focus-ai-standards-agent - First steps:
cd /standards && mise installthenpi -p prompts/setup-project.md - Key mount explanation: host
~/.piprovides LLM API keys - Session ephemerality: sessions are lost when the container exits
Testing Decisions
What makes a good test
Tests validate external, observable behavior of the built image — not implementation details of the Dockerfile. Tests check: what user is logged in, what directories exist, what tools are on PATH, what shell activation lines exist, what repo files are present, and what anti-patterns are absent.
Tested modules
The existing integration test suite (test-dockerfile.sh) validates the container image as a black box. It covers:
- Identity and filesystem (whoami, /standards, /app, permissions)
- Tool availability (mise, fnox, node, git, tmux, gh, curl, bash)
- Mise-managed tool detection
- Shell activation lines in
.bashrc - Pi binary presence and version
- Standards repo file and directory presence
- Anti-pattern checks (no
npm install -g, nopip installin Dockerfile) - .gitignore coverage for generated paths
Prior art
The existing test-dockerfile.sh follows this black-box pattern. The redesigned Dockerfile should pass the same suite with adjustments: fnox is not expected on PATH (it comes from mise install at runtime, not baked), and /app contains no .gitignore (it's empty). The test suite will be updated to reflect these new expectations.
Out of Scope
- fnox and secrets infrastructure. The image does not include fnox, 1Password CLI, or any secrets management. Secrets are configured by
setup-project.mdfor the user's project, not for the standards container. - Skills installation. Skills declared in
skills-lock.jsonare not installed at build time. They're part of the standards repo as reference; the user's project installs its own skills viasetup-project.md. /apppre-population. No mise.toml, .gitignore, pi config, or any other file is created in/appby the Dockerfile.- zsh support. Bash is the only shell. Zsh is neither installed nor configured.
- Systemd / linger. No
loginctl enable-linger, no systemd integration. - Multi-architecture builds. Only
linux/amd64is targeted. No cross-platform manifest lists. - CI/CD pipeline for the image itself. Publishing to a registry is out of scope. The Dockerfile is a reference artifact built locally.
Further Notes
The prompts/PRM-001-generate-dockerfile.md prompt and the Dockerfile have a producer/consumer relationship: the prompt is the specification, the Dockerfile is the generated output. When standards evolve (new tools, new best-practices, new extensions), the prompt should be updated first, then the Dockerfile regenerated.
The two prompts in this repo serve different audiences:
prompts/PRM-001-generate-dockerfile.md— consumed by pi to produce the standards container imagesetup-project.md— consumed by pi at runtime inside the container to scaffold the user's actual project into/app