| name | template-dev |
| description | Use when developing or modifying Copier templates in aegis/templates, or backporting a change from a generated project (e.g. a prototype scratch project) back into the templates. Covers the two development workflows, the three rendering modes that resolve where Copier reads template content from, and the template-specific gotchas that silently break generation or update. |
Template dev
Develops and backports changes to the Copier templates that every generated
Aegis Stack project is rendered from. This is the general template-editing
workflow: how to iterate, how rendering picks up (or ignores) your edits, and
the traps that only show up on aegis update, not on a fresh aegis init.
When to use
Use when the task touches template content directly (docker-compose.yml.jinja,
Makefile.jinja, any file under app/, pyproject.toml.jinja, copier.yml,
etc.) or when a fix was made inside a generated project and needs to be
carried back into the templates.
Do NOT use this skill for adding a brand-new component or service (a
ComponentSpec/ServiceSpec entry, its FileManifest, and its
generation/update plumbing) - use the add-component or add-service skill
instead; they cover the plugin-spec registry work this skill assumes is
already done. Do NOT use for ordinary feature work inside the CLI tool itself
(aegis/commands/, aegis/core/ logic unrelated to rendering) unless that
work is specifically about how templates render or update.
Files that change
Source-of-truth law: templates ship, generated projects are prototypes. The
templates live under:
aegis/templates/copier-aegis-project/{{ project_slug }}/: every file a
generated project contains, one-to-one. A plain file with no Jinja logic
ships as-is (e.g. app/components/worker/registry.py); a file with
conditional content ships with a .jinja suffix (e.g.
app/components/worker/events.py.jinja).
copier.yml (repo root, not under templates/): the question set Copier
prompts for or reads answers from.
aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja:
the answers file template; see Pitfalls for which fields belong here.
aegis/core/copier_manager.py: renders a new project (aegis init),
including the dev-mode/git-mode source selection and the post-generation
answers backfill.
aegis/core/copier_updater.py: resolves the template source and version
ref for aegis update.
aegis/core/manual_updater.py: adds/removes a single component or service
on an existing project without a full Copier update.
Makefile (repo root): the test-template* and test-stacks* targets used
to validate template changes.
Backport target mapping: any file you edit inside a generated project at
<project>/<path> has its template source at
aegis/templates/copier-aegis-project/{{ project_slug }}/<path> (or
<path>.jinja if that file needs Jinja conditionals). Backport with:
cp my-app/app/components/worker/registry.py \
"aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py"
Quote the destination path; the directory name contains the literal {{ project_slug }}
placeholder, which a shell would otherwise try to expand.
Procedure
Pick one of two workflows, then apply the rendering mode that matches it.
Template-first (the change is already known):
- Edit the file(s) directly under
aegis/templates/copier-aegis-project/{{ project_slug }}/....
- Generate a test project with
aegis init test-project --dev (working
tree, no commit needed) or make test-template-quick/make test-template
(committed state, see rendering modes below).
- Verify the generated project behaves as expected.
- Clean up with
make clean-test-projects.
Prototype-first (exploratory, the fix is easier to find by iterating
directly on generated code):
- Generate a test project (
aegis init <name> --dev, or any
make test-template* target).
- Iterate directly in the generated project until it works.
- Backport immediately, in the same session, using the
cp pattern above
for every file you touched. Do not defer this; changes left only in a
generated project are lost on the next aegis init.
- Regenerate a fresh project from the templates to confirm the backport is
correct, then clean up.
Rendering modes (this resolves which template content Copier actually reads):
- Default (installed package, or local git repo without
--dev): Copier
renders the COMMITTED git state, either via a GitHub URL (pip/uvx install)
or a git+file:// URL pointing at the local repo (aegis/core/copier_manager.py,
the is_git_repo(template_root) branch). Uncommitted template edits are
invisible even though the files look changed locally.
aegis init --dev: renders the WORKING TREE. aegis/core/copier_manager.py's
dev_mode branch copies copier.yml and
aegis/templates/copier-aegis-project from the working tree into a
temporary directory and generates from that plain path, so uncommitted
edits show up immediately. Use this for local iteration; no commit
required. Projects generated this way have no _commit pin and cannot run
aegis update later.
- External project update:
aegis update -y -p <project> -t <aegis-stack repo> --to-version HEAD.
-p/--project-path points at the target project, -t/--template-path
points Copier at a local aegis-stack checkout instead of the installed
package, and --to-version HEAD resolves to the latest commit on the
current branch instead of a version tag. This still reads the COMMITTED
state of that checkout, not its working tree, so template changes must be
committed first.
Gates
make test-template after any template edit (generates a project and runs
its full validation, including make check inside the generated project).
- Whichever narrower target matches the touched component or service, for
faster iteration:
make test-template-quick (no validation),
make test-template-with-components, make test-template-auth,
make test-template-worker, make test-template-database,
make test-template-full, make test-template-ai,
make test-template-ai-memory, make test-template-ai-sqlite.
make test-stacks-quick while iterating on a cross-cutting template change
(fast feedback against a representative subset: base, everything, insights).
make test-stacks-full before calling multi-component template work done
(generation-only pass, the slow build/validation pass, and the kitchen-sink
everything stack).
make check for any non-template Python change made along the way (for
example, editing copier_manager.py or manual_updater.py).
make clean-test-projects to remove generated test project directories
once verification is done.
Pitfalls
- Gate conditional content with a full-body
{% if %}...{% endif %} wrap
only. Never use Copier conditional filenames; that convention broke on
Windows and is banned outright.
- Carry no inline comments in
docker-compose.yml.jinja or its .dev/.prod
variants. Which components a given project selects varies per stack, so a
comment written for one combination misleads for another; state conditions
through the Jinja gate itself.
- Every new
copier.yml question needs a matching line in
.copier-answers.yml.jinja, except fields gated by a when: clause.
Copier itself drops when-gated answers from the rendered answers file
(observed for worker_backend, scheduler_backend, include_oauth), so
the actual persistence path for those is copier_manager.py's
post-generation backfill loop, which patches any copier_data key Copier
dropped directly into the written .copier-answers.yml. Adding a
when-gated field to the .jinja list anyway is harmless but is not what
makes it survive.
copier is pinned below 9.15 in pyproject.toml (9.15+ relocates
.copier-answers.yml out of the generated project directory, breaking
aegis update). A dependabot ignore rule blocks upgrade PRs past that
ceiling; do not bump the pin even if a bot proposes it.
uv.lock hides fresh-install dependency drift. Validate a template
dependency change (new pyproject.toml.jinja gates, new packages) with
uvx or a fresh uv sync inside a freshly generated project, not the
locally locked dev environment, since the lock file can mask a resolution
that only fails on a clean install.