ADR-0013: Taskfile Contract v2 — Prescriptive Naming and Recipes
Decisions to extend the Taskfile contract with prescriptive task naming, CI namespace, developer lifecycle, environment handling, and CodeArtifact recipes.
Status: Accepted
Date: 2026-03-14
Deciders: Engineering Leadership
Supersedes: Extends ADR-0003: Taskfile as Contract (still valid, not replaced)
Context
ADR-0003 established Taskfile as the mandatory operational contract. It defined the principle ("Taskfile is the single interface") and listed suggested namespaces, but intentionally left task naming loose to encourage adoption.
After 12+ months of adoption across 15+ repositories, we observed:
Naming inconsistency:
- Flat task names (
install,test,lint,ci) coexist with namespaced ones (lint:check,test:unit) — sometimes within the same repo - Synonyms for the same operation:
sandbox:startvssandbox:up,validate:allvsci: - No shared vocabulary for task name segments — developers have no framework to decide how to name a new task
CI/validation confusion:
- 7 repos use bare
ci:as a flat rollup (lint + test + typecheck) - 3 repos use
validate:allfor the same purpose - Neither is decomposed — developers can't run just the lint step of CI locally
- No repo uses
ci:*for CI-specific operations (its intended purpose per the original pattern)
No standard developer onboarding path:
- Repos use
install,setup,bootstrap,deps:install, or nothing for first-use setup - No "clone → one command → working" pattern exists
Environment variable handling:
- Some repos hardcode
AWS_PROFILE: ontopix-devwith no override mechanism - Inconsistent use of
.envvs.envrcvs inline values - No documented precedence chain
CodeArtifact login — three incompatible approaches:
- Method A:
eval $(task deps:login:npm)— outputs token to stdout, no system pollution - Method B:
aws codeartifact login --tool npm— writes to~/.npmrc, pollutes system config - Method C: Token written to
.envfile — persists across tasks but mutates a file
Infrastructure formatting:
- Some repos use
terraform fmt -recursive, some don't — leading to inconsistent CI results
Decisions
Decision 1: Task Name Terminology
Task names use colons to separate up to 3 hierarchical segments:
namespace:action → lint:check, test:unit, sandbox:start
namespace:action:target → deploy:lambda:mcp-erp, deps:login:npm
| Segment | Role | Examples |
|---|---|---|
| namespace | Concern area | lint, test, ci, sandbox, infra, deploy, release, deps, dev |
| action | The operation | check, fix, start, plan, install, login |
| target (optional) | Specific scope | npm, mcp-erp, terraform |
Rules:
- Maximum 3 segments (typically 2, 4 allowed in rare cases)
- Hyphens within segments for multi-word names (
agent-support, notagentSupport) - No abbreviations (
integration, notint) - Flat tasks (no namespace) reserved for meta/utility:
default,help,clean
Rationale: This formalizes what ~80% of repos already do organically. The remaining repos using flat names (audit-utils, audit-service, schemas-sdk) are the exception, not the norm.
Decision 2: ci:* as CI Namespace
ci:* is the namespace for tasks that mirror CI pipeline steps. A developer runs them locally to verify what CI will run.
ci:lint → Same lint checks CI runs
ci:test → Same tests CI runs
ci:build → Same build CI runs (if applicable)
ci:all → Full CI pipeline: ci:lint + ci:test + ci:build
Rule: If task ci:all passes locally, CI must pass. No surprises.
What ci:* is NOT for: CI-environment-specific setup (credential injection, cache config, runner provisioning). Those belong in GitHub Actions workflow YAML.
Rationale: 7 repos already use ci: for this purpose, but as a flat rollup without decomposition. Decomposing into ci:lint, ci:test, ci:build maps 1:1 to CI workflow steps, letting developers run the full pipeline or just one slice. The existing validate:all tasks that serve as CI rollups migrate to ci:all.
Decision 3: validate:* Retained for Domain Validation
validate:* is for domain-specific conformance checks — not CI rollup.
Examples: validate:structure (required files), validate:schemas (data conformance), validate:config (configuration files).
Some validate:* tasks are naturally called by ci:lint or ci:all (e.g., validate:structure). The task lives in validate:, the CI step references it.
Rationale: 10+ repos already use validate:structure. The namespace has a clear purpose distinct from CI rollup.
Decision 4: dev:setup as Clone-to-Working Entry Point
dev:setup → Full first-use setup. Idempotent.
Calls dev:install, copies .env.example → .env if missing,
runs validate:structure, prints next-steps.
dev:install → Install language dependencies (uv sync / pnpm install).
dev:run → Start the application locally.
Rationale: The "I just cloned this repo, now what?" question has no standard answer today. Repos use install, setup, bootstrap, or nothing. dev:setup is the single entry point. It's idempotent — safe to re-run. dev:install stays useful standalone for "just update my deps after a pull."
Decision 5: sandbox:start / sandbox:stop as Canonical Names
Not up/down. This is what the original pattern prescribed and what the sandbox-environments pattern uses.
Rationale: start/stop is tool-agnostic. up/down is Docker Compose jargon. Repos using sandbox:up/down (notably maxcolchon) should migrate when next touched.
Decision 6: infra:fmt Must Use -recursive
infra:fmt MUST use terraform fmt -check -recursive.
Rationale: Without -recursive, subdirectories (modules, environments) are silently skipped. This has caused inconsistent CI results where local checks pass but CI fails, or vice versa.
Decision 7: CodeArtifact Login — Method A with Optional .env Persistence
Recommended: eval $(task deps:login:npm) — outputs an export statement, no system pollution.
Optional complement: A deps:login:npm:env variant that writes the token to .env for persistence across task invocations without re-eval.
Forbidden: aws codeartifact login --tool npm/pip — this writes to ~/.npmrc or ~/.config/pip/pip.conf, polluting the developer's system-wide configuration. The pollution persists after the session ends and can cause conflicts with other projects.
Rationale: Method A keeps credentials in the shell session (ephemeral, clean). The optional .env persistence solves the practical problem of tokens not propagating across task subprocess boundaries. Method B was used in some repos but is explicitly forbidden going forward due to system-wide side effects.
Decision 8: .env as Standard Override Mechanism
Precedence chain: Taskfile defaults → .env file → shell environment (last wins).
- Every repo MUST use
dotenv: ['.env']in Taskfile - Every repo MUST ship
.env.examplewith all supported variables and comments .envMUST be in.gitignore.envrcis an optional developer convenience (direnv), not a requirement
Never hardcode values that differ per developer or environment at the top-level env: block. Use vars: with default instead:
# ❌ Hardcoded
env:
AWS_PROFILE: ontopix-dev
# ✅ Overridable
vars:
AWS_PROFILE: '{{.AWS_PROFILE | default "ontopix-dev"}}'
Rationale: Without a standard override mechanism, developers either hardcode values (breaking other developers' setups) or each repo invents its own convention. .env is universally understood and supported by Taskfile natively.
Decision 9: Flat Task Names Reserved for Meta/Utility
Only meta/utility tasks may be flat (no namespace): default, help, clean, clean:deep.
All operational tasks MUST be namespaced.
Rationale: Flat names like install, test, build are ambiguous — install what? test what? Namespacing (deps:install, test:unit, build:dist) makes intent explicit and enables task --list to group related operations.
Decision 10: release:* as Library/Artifact Release Namespace
release:* is the namespace for publishing libraries and packages to artifact repositories (CodeArtifact, npm, PyPI).
release:publish → Publish built artifacts to registry
release:version → Sync VERSION to package manifests (package.json, pyproject.toml, etc.)
release:all → Full release pipeline: release:version + build:dist + release:publish
release:* mirrors deploy:* symmetry: deploy puts services in production, release puts libraries in artifact registries.
release:all chains build:dist — artifacts are always rebuilt from the current version before publishing. Individual tasks (release:publish, release:version) remain independently callable for partial workflows.
Not in scope: The versioning protocol itself (VERSION file as source of truth, sync mechanics, validation of version consistency across monorepo packages) is a cross-cutting concern for a separate pattern. This decision only prescribes the task namespace and composition.
Rationale: Library repos (schemas-sdk, stylebook, audit-utils, and any future shared library) follow a bump → build → publish cycle that had no prescribed namespace. release:* fills this gap without overloading deploy:* (which targets running services, not package registries).
Consequences
Positive
- Predictable naming — any developer can guess task names in an unfamiliar repo
- Local CI parity —
task ci:alleliminates "works on my machine" surprises - One-command onboarding —
task dev:setupfrom clone to working state - Clean credentials — no system-wide config pollution from private registries
- Override chain — developers customize behavior without modifying committed files
Negative
- Migration effort — existing repos need task renames (see explainer for migration guide)
- Breaking change for
validate:allusers — repos callingtask validate:allas CI rollup must rename toci:all
Mitigations
- Migration happens incrementally — when a repo is next touched for meaningful work, not as a big-bang
- New repos MUST follow the pattern from day one
- Migration guide and compliance checklist provided as explainer
References
- ADR-0003: Taskfile as Contract — original decision (still valid)
- Taskfile Contract Pattern — prescriptive content
- Taskfile Migration Guide — migration guide and compliance checklist
- Sandbox Environments Pattern — sandbox task naming
ADR-0012: Workflow Architecture for Multi-Environment Deploys
Extend the GitHub Actions Workflows pattern to cover authentication, multi-environment architecture, and topic ownership across CI/CD patterns.
Taskfile Migration Guide
Step-by-step guide to bring any repository into compliance with the Taskfile contract v2, plus a compliance checklist.