agentleFS
lbednerlbedner/aegis-stack/.claude/skills/template-dev/SKILL.md

template-dev

Skill144 starsChanged 15 months agoMITView on GitHub

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.

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 3 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use when the task touches template content directly (docker-compose.yml.jinja, Makefile.jinja, any file under app/, pyproject.toml.jinja, …

    line 15
  2. Very specific:

    Do NOT use this skill for adding a brand-new component or service (a ComponentSpec/ServiceSpec entry, its FileManifest, and its …

    line 20
  3. Distinctive:

    Every file in the tree is governed by exactly one mechanism, and which one is derived, not declared — there is no registration list.

    line 71
  4. Very specific:

    No annotation means the default: overwrite while pristine (with a .backup), 3-way merge once the user has edited it.

    line 100
  5. Distinctive:

    Verify the generated project behaves as expected.

    line 129
  6. Very specific:

    Default (installed package, or local git repo without --dev): Copier renders the COMMITTED git state, either via a GitHub URL (pip/uvx …

    line 146
  7. Distinctive:

    Start with the fast ones. These run in under a second against rendered output and catch the two mistakes template edits actually make:

    line 168
  8. Very specific:

    Run those *before* generating a project.

    line 178
  9. Very specific:

    Every new copier.yml question needs a matching line in .copier-answers.yml.jinja, except fields gated by a when: clause.

    line 210

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.
  • aegis/core/render_diff.py: the engine manual_updater uses to decide what a shared file needs on add/remove. Reads the file's policy annotation; you rarely edit this, but its module docstring is the reference for the decision table below.
  • 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.

Who owns a template file

Every file in the tree is governed by exactly one mechanism, and which one is derived, not declared — there is no registration list. Adding a new template file requires no bookkeeping anywhere.

  • Component/service-owned — the path appears in some spec's FileManifest (aegis/core/components.py / services.py). Its existence is decided by manifest membership: aegis add copies it, aegis remove deletes it. Most of a component's own files.
  • Shared — nothing claims it. On add/remove the render-diff engine renders the tree at the old answers and the new answers and diffs; a file whose output changed gets written. This is where {% if include_x %} conditionals belong.

Consequence worth internalising: a {% if include_worker %} block inside a file the auth manifest owns will never re-render when worker is added, because owned files are invisible to the engine by design. Cross-cutting conditionals go in shared files.

Per-file policy annotations

A shared file can override the default handling with a comment on its very first line, read and stripped before rendering:

{#- aegis: user-owned -#}        create once, then never touch (README, docs/)
{#- aegis: warn-if-diverged -#}  overwrite while pristine, else preserve + report
{#- aegis: no-backup -#}         overwrite without writing a .backup

No annotation means the default: overwrite while pristine (with a .backup), 3-way merge once the user has edited it.

The -# / #- trim markers are required, not stylistic. aegis init and aegis update render through Copier, which knows nothing about these annotations; without the trim markers the comment leaves a blank line at the top of every generated file. A recognised word missing its markers raises rather than silently degrading.

Whole-file gates leave a stub

{%- if include_x -%}...{%- endif -%} wrapping an entire file still emits a newline when the gate is off, so Copier writes a 1-byte file. That stub is what the template currently produces, so it counts as pristine and gets populated when the gate flips on. Don't "tidy" it by making the engine treat existing-but-empty as user content — that is the exact bug that once left docker-compose.prod.yml empty forever after aegis add ingress.

Procedure

Pick one of two workflows, then apply the rendering mode that matches it.

Template-first (the change is already known):

  1. Edit the file(s) directly under aegis/templates/copier-aegis-project/{{ project_slug }}/....
  2. 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).
  3. Verify the generated project behaves as expected.
  4. Clean up with make clean-test-projects.

Prototype-first (exploratory, the fix is easier to find by iterating directly on generated code):

  1. Generate a test project (aegis init <name> --dev, or any make test-template* target).
  2. Iterate directly in the generated project until it works.
  3. 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.
  4. 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

Start with the fast ones. These run in under a second against rendered output and catch the two mistakes template edits actually make:

  • uv run pytest tests/core/test_template_tree_hygiene.py — every template parses, and no file is shadowed by a .jinja twin.
  • uv run pytest tests/core/test_shared_scope_completeness.py tests/core/test_render_diff_transition_coverage.py — a new stack-dependent file is handled by something, and every add/remove transition writes the files it needs.

Run those before generating a project; a template that fails to parse wastes a 40-second aegis init to tell you so.

Then the generation 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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.