add-service
Skill144 starsChanged 15 months agoMITView on GitHub
Use when adding a new service (a business capability like auth, AI, blog, or finance) to the Aegis Stack framework itself. Covers the plugin-spec registry, migrations, CLI, template, dashboard, test, docs, and i18n files that must change together.
We split this skill into the rules it gives an agent
13 rules. 0 are common, 1 is distinctive, and 12 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 15
Use when the task is "add a new service to Aegis Stack" (a business capability under app/services/<name>/, selected via include_<name> in …
- Very specific: line 19
Do NOT use for adding a component (infrastructure: worker, scheduler, database, redis, ingress, observability) - components live in …
- Very specific: line 48
every path the service owns.
- Very specific: line 243
Write the failing tests first: tests/services/<name>/test_<name>_service.py (template side) for business logic, and …
- Very specific: line 246
Add AnswerKeys.<NAME> / AnswerKeys.SERVICE_<NAME> in aegis/constants.py.
- Very specific: line 248
If the service owns tables, add its ServiceMigrationSpec (name, schema.
- Very specific: line 250
Add the ServiceSpec entry in aegis/core/services.py: identity fields, required_components, pyproject_deps, wiring=PluginWiring(...), …
- Very specific: line 253
Add the include_<name> question (and any config questions) to copier.yml.
- Very specific: line 271
Add the i18n keys service.<name> and projectmap.<name> to en.py with real strings, and stub the same two keys into every other locale for …
- Very specific: line 274
Add the tests/core/test_services.py spec-shape + wiring tests, the tests/cli/conftest.py cache entry, and the STACK_COMBINATIONS / …
- Very specific: line 277
Add the service to SERVICE_INVOCATIONS in tests/cli/test_add_service_importable.py and, if it owns tables, add a migration test in the …
- Very specific: line 283
If any generated-project fix happened in a scratch/prototype project rather than directly in the template, backport it into …
- Distinctive: line 288
Run the gates below and fix anything red.
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Add service
Adds a new service (business logic, e.g. blog, finance, auth) to the framework
so generated projects can select it via aegis init --services or add it
later via aegis add-service. Traced from the two most recent services,
blog and finance.
When to use
Use when the task is "add a new service to Aegis Stack" (a business
capability under app/services/<name>/, selected via include_<name> in
copier.yml).
Do NOT use for adding a component (infrastructure: worker, scheduler,
database, redis, ingress, observability) - components live in
aegis/core/components.py and share the same PluginSpec shape but a
different file footprint. Do NOT use for changing behavior inside an
existing service; that is ordinary feature work, not this skill.
Files that change
Registry (ServiceSpec is a thin alias of PluginSpec pinned to
kind=SERVICE; see aegis/core/plugins/spec.py):
aegis/core/services.py: add aServiceSpecentry to theSERVICESdict. Setcategory(its concern:PartCategory, as Overseer groups the app) and, where the app names it differently,health_name; then runmake parts(orderplaces it in the sidebar), which writes every spec's into the template'sapp/core/parts.py(tests/core/test_parts_file.pyfails while stale). Settype(add a newServiceTypemember only if none fit),description/long_description,required_components,recommended_components,pyproject_deps,template_files(display list),marker_path(the pathaegis updateuses to detect the service on disk), anddocs_path(leave""until the docs page ships - a non-empty value is pinned bytests/core/test_spec_docs_paths.py).aegis/core/services.py:wiring=PluginWiring(...)on the spec - declaresrouters(RouterWiring),dashboard_cards/dashboard_modals(FrontendWidgetWiring), anddeps_providers(SymbolWiring). This metadata mirrors, and must stay in sync with, the hand-written Jinja conditionals in the template files below - it does not currently drive their rendering for in-tree services.aegis/core/services.py:files=FileManifest(primary=[...], extras={...})- every path the service owns.
primaryis the add/init base;compute_file_mapping()derivespost_gen_tasks.get_component_file_mapping()from this, so there is no separate hand-maintained mapping to edit.
- every path the service owns.
aegis/core/migration_generator.py: only if the service owns tables, add aServiceMigrationSpecand reference it from the spec'smigrations=[...]list inservices.py. The spec has no tables in it: a revision's contents are derived from the SQLModel classes by the generated project'sapp/cli/migrate_gen.py(run by init/add inside the project venv), so a column is declared in exactly one place, the model. The spec carries only what models cannot express -service_name,description, the Postgresschemaits tables live in, anddata_sqlfor a row a schema change makes mandatory.MIGRATION_SPECSis derived lazily from every spec's.migrationsviacollect_migrations()- no dict entry to hand-maintain.aegis/constants.py:AnswerKeys- add<NAME> = "include_<name>"andSERVICE_<NAME> = "<name>". Add any sub-flag keys the service needs (seeFINANCE_PLAID,FINANCE_SNAPTRADE,FINANCE_IMPORT).copier.yml(repo root, not undertemplates/): add theinclude_<name>bool question, plus any per-service config questions gated withwhen: "{{ include_<name> }}"(model onfinance_plaid). Extendget_services_needing_migrations(aegis/core/migration_generator.py) if the service owns tables, and the two template mirrors of that rule (pyproject.toml.jinja's alembic dependency,database_init.py.jinja);tests/core/test_alembic_dependency_matches_migrations.pyfails until they agree.aegis/core/<name>_service_parser.py: only if the service takes bracket syntax like<name>[option](model onai_service_parser.py,auth_service_parser.py,insights_service_parser.py; blog and finance need none of this - they have nooptions=on their spec). Wire it intoaegis/core/service_resolver.pyif it introduces a cross-service dependency check (see theinsights[per_user]requiresauth[org]block).aegis/cli/interactive.py:interactive_<name>_service_config()only if the service needs an interactive config prompt duringaegis add-service --interactive(model oninteractive_ai_service_config; not needed for blog/finance).
Generation and update plumbing (thread the new AnswerKeys.<NAME> answer key
through the generate and update flows; a table-owning service that skips these
generates fine but breaks migrations on aegis update):
aegis/core/template_generator.py: add anAnswerKeys.<NAME>: "yes"/"no"entry to the answer dict built fromselected_services(model on the existing per-service entries usingextract_base_service_name).aegis/core/copier_manager.py: addAnswerKeys.<NAME>to the template context booleans, and if the service owns tables include it in theneeds_migration_filesdetection.aegis/core/copier_updater.pyandaegis/commands/update.py: addinclude_<name> = answers.get(AnswerKeys.<NAME>, False)and OR it into the aggregate migration/needs check used during update.aegis/core/manual_updater.py: addAnswerKeys.<NAME>to the answer-keys list it threads through add/remove.
CLI (framework commands, not the generated project's CLI):
aegis/commands/add_service.py: resolves service deps to components, prompts for config, callsManualUpdater.add_component, generates migrations viagenerate_migration/bootstrap_alembic. Only add a service-specific branch here if the service needs bracket-syntax parsing or extra answer keys threaded through (auth, ai, insights do; blog and finance need none).aegis/commands/remove_service.py: callsManualUpdater.remove_component; no per-service branch needed unless the service needs a custom warning (see the auth-specific data-loss warning block).aegis/commands/services.py: theaegis serviceslist command readsSERVICES/ServiceTypeautomatically; no edit needed unless a newServiceTypeheader is required.
Template (aegis/templates/copier-aegis-project/{{ project_slug }}/):
app/services/<name>/: the service package (business logic,deps.pyfor the FastAPI dependency provider referenced bywiring.deps_providers), laid out as the service standard says (docs/services/index.md, Service Structure): its standard files at the top, logic underdomains/, and callers outside it going throughservice.py.app/components/backend/api/<name>/: routes, registered inapp/components/backend/api/routing.py.jinjabehind a full-body{%- if include_<name> %}import +app.include_router(...)block.app/cli/<name>.py.jinja: the project's<name>subcommand. Register it inapp/cli/main.py.jinjawith the existingtry: importlib.import_module(...) / except ImportError: passpattern.- Dashboard card:
app/components/frontend/dashboard/cards/<name>_card.py, exported fromcards/__init__.py.jinjabehind{%- if include_<name> %}, and added to the aggregate{%- if include_auth or ... or include_<name> or _plugins %}gate in that same file (guardsServicesCard's import). - Dashboard modal:
app/components/frontend/dashboard/modals/<name>_modal.py, exported frommodals/__init__.py.jinja, and added to themodal_mapand import block indashboard/cards/card_utils.py.jinja(key convention:"service_<name>", matchingwiring.dashboard_cards[0].modal_id). - The card/modal also need registering in the surrounding frontend wiring, each
behind
{%- if include_<name> %}:dashboard/status_overview.py.jinja(the status grid),app/components/frontend/main.py.jinja(the frontend app that mounts the card),app/services/system/ui.py.jinja(dashboard registry), andapp/components/backend/api/deps.py.jinjaif the service's routes need a shared dependency. Trace howblogappears in each before adding<name>. app/components/backend/startup/component_health.py.jinja: register the service's health check behind{%- if include_<name> %}withregister_service_health_check("<name>", check_<name>_service_health).app/components/backend/startup/database_init.py.jinja(table-owning services only): import the service's SQLModel models behind{%- if include_<name> %}(with# noqa: F401), addinclude_<name>to theneeds_migrationsset expression, and add a"<name>": ("table", "<table>")entry to the existing-table stamp map. Skipping this means the service's tables are never created or stamped.alembic/env.py.jinja(table-owning services only): import the models behind{%- if include_<name> %}so Alembic autogenerate sees them.pyproject.toml.jinja: gate service-specific dependencies behind{%- if include_<name> %}. If the service owns tables, add it to the sharedalembic==1.16.5gate condition (the big{%- if include_auth or ... or include_<name> %}line).docs/services/<name>/under the template tree is a separate, optional convention (ships docs inside the generated project itself); onlycommshas it today. Do not treat it as required.
Framework docs (this repo's own /docs, published via the root mkdocs.yml
- distinct from the template docs above, and what
ServiceSpec.docs_pathpoints at): docs/services/<name>/index.md(+cli.md,api.md,dashboard.md,examples.md,setup.mdas applicable - seedocs/services/blog/anddocs/services/payment/for the current shape).mkdocs.yml(repo root): add aServices: - <Name>:nav block pointing at the new pages (see theblog/payment/insightsentries).aegis/core/services.py: setdocs_path="services/<name>"on the spec once the pages exist (leave""until then, per above).
Tests (generated-project template tests, under
aegis/templates/copier-aegis-project/{{ project_slug }}/):
tests/services/<name>/test_<name>_service.py(ortests/services/test_<name>_service.pyfor a small service): unit tests for the service's business logic (write first, TDD).tests/api/test_<name>_endpoints.py: only if the service exposes routes.- Template-render test (framework side, optional): if the service's template
has non-trivial cross-service Jinja branching (e.g. blog's router renders
differently with auth on/off/rbac), add a
tests/core/test_<name>_template_render.pythat renders the.jinjafile directly viaEnvironment(loader=FileSystemLoader(get_template_path()))and asserts on the rendered source (seetests/core/test_blog_template_render.py).
Tests (this repo's own CLI/core test suite):
tests/core/test_services.py: add a spec-shape test class (name, type, required/recommended components,pyproject_deps) and atest_<name>_service_wiringtest assertingspec.wiring.routers[0].module,.alias, and the dashboardmodal_ids (mirrorstest_blog_service_wiring).tests/cli/test_services_cli.py: extend theaegis serviceslist-command coverage tables with the new service's expected inclusion/exclusion flags.tests/cli/conftest.py: add a<name>_with_database(or similarly named) entry toNAMED_PROJECT_SPECSso downstream tests copy a cached project instead of regenerating one (tests/CLAUDE.mdexplains the cache; skipping this entry costs 10+ minutes of CI time). Also add the new stack totests/cli/test_stack_generation.py'sSTACK_COMBINATIONSand to theeverythingentry inNAMED_PROJECT_SPECS.tests/cli/test_add_service_importable.py: add the service name (with bracket options if it would otherwise prompt interactively) toSERVICE_INVOCATIONS. This is the regen-gap guard: it runsaegis add-service <name>against a cached base project and imports the webserver/CLI/routing entry chains in the generated project's own venv, catching files that render fine atinittime but are missing from the add/remove file mapping.- A migration test in the style of
tests/cli/test_add_service_migrations.pyif the service owns tables (assertsaegis add-service <name>bootstraps alembic and writes the migration file). - Collateral: adding a service to
SERVICESshifts the service list that several existing tests assert against. Expect to updatetests/cli/test_guided.py,tests/cli/test_interactive_project_selection.py,tests/cli/test_interactive_scheduler.py,tests/cli/test_ai_configuration.py, andtests/cli/test_scheduler_persistence.pywhen they fail on counts or expected-option lists. These are not new files; run the gates and fix what goes red.
i18n:
aegis/i18n/locales/en.py: add"service.<name>"(short description, used byaegis services) and"projectmap.<name>"(label used by the project map renderer) with the real English strings.aegis/i18n/locales/{de,es,fr,ja,ko,ru,zh,zh_hant}.py: add the SAME two keys to every other locale, using the English string as a placeholder. This is required for parity:tests/core/test_i18n.pyasserts every locale carries the same keys as English and fails naming the missing key otherwise. Real translation of the placeholders is a separate translator-reviewed pass; do not attempt it here, but the keys must exist now.
Cross-cutting:
- Shared files need no registration. A shared file is any template path
no component/service
FileManifestclaims;aegis add/removefinds it by rendering the tree at the old and new answers and diffing. Add{% if include_<name> %}conditionals torouting.py.jinja,component_health.py.jinja,cards/__init__.py.jinja— or to a brand-new shared file — and it is handled automatically. There is no list to add it to.tests/core/test_shared_scope_completeness.pyfails, naming the file, if a stack-dependent file somehow ends up handled by nothing.
Procedure
- Write the failing tests first:
tests/services/<name>/test_<name>_service.py(template side) for business logic, andtests/api/test_<name>_endpoints.pyif the service exposes routes. Confirm they fail for the right reason. - Add
AnswerKeys.<NAME>/AnswerKeys.SERVICE_<NAME>inaegis/constants.py. - If the service owns tables, add its
ServiceMigrationSpec(name, schema; no table bodies) toaegis/core/migration_generator.py. - Add the
ServiceSpecentry inaegis/core/services.py: identity fields,required_components,pyproject_deps,wiring=PluginWiring(...),migrations=[...], andfiles=FileManifest(primary=[...]). - Add the
include_<name>question (and any config questions) tocopier.yml; table-owning services go inget_services_needing_migrationsand its template mirrors (see above). Thread the newAnswerKeys.<NAME>through the generation/update plumbing:template_generator.py,copier_manager.py,copier_updater.py,manual_updater.py, andcommands/update.py(see Files that change). - Build the template package:
app/services/<name>/, the API router plusrouting.py.jinjaregistration, the CLI module plusmain.py.jinjaregistration, and the health check registration incomponent_health.py.jinja. - Wire the dashboard and frontend: card, modal, both
__init__.py.jinjafiles, themodal_map/import block incard_utils.py.jinja, plusstatus_overview.py.jinja,frontend/main.py.jinja, andsystem/ui.py.jinja. If the service owns tables, also register its models indatabase_init.py.jinja(import +needs_migrations+ stamp map) andalembic/env.py.jinja. - Gate
pyproject.toml.jinjadeps (and the shared alembic condition if the service owns tables). - Add the i18n keys
service.<name>andprojectmap.<name>toen.pywith real strings, and stub the same two keys into every other locale for parity (see i18n above);tests/core/test_i18n.pyfails otherwise. - Add the
tests/core/test_services.pyspec-shape + wiring tests, thetests/cli/conftest.pycache entry, and theSTACK_COMBINATIONS/everythingentries. - Add the service to
SERVICE_INVOCATIONSintests/cli/test_add_service_importable.pyand, if it owns tables, add a migration test in the style oftest_add_service_migrations.py. This verifies the add path on an EXISTING project (aegis add-service <name>against a cached base), not just freshaegis init- the only way to catch files that render at init time but are missing from the FileManifest. - If any generated-project fix happened in a scratch/prototype project
rather than directly in the template, backport it into
aegis/templates/before finishing - templates are the source of truth. - Once the docs page exists, add
docs/services/<name>/pages, wire them into the rootmkdocs.ymlnav, and setdocs_pathon the spec. - Run the gates below and fix anything red.
Gates
make check(lint, typecheck, test) must pass.make test-stacks-quickwhile iterating (fast feedback: runs the full validation phase against a representative subset - base, everything, insights).make test-stacks-fullbefore calling the work done (runstest-stacksgeneration-only pass, the slowtest-stacks-buildvalidation pass, and the kitchen-sinkeverythingstack across every combination).
Pitfalls
- Template changes render from the committed git state unless you pass
aegis init --dev(working tree) or commit first; testing a template edit with plainaegis initon an uncommitted change silently uses stale content. FileManifest.primaryon the spec is the single sourceget_component_file_mapping()derives from (viacompute_file_mapping()). A file present in the template but missing fromprimaryrenders correctly on freshinit(Copier just includes it) but is silently skipped byadd-service/remove-serviceon an existing project - the gap only shows up whentests/cli/test_add_service_importable.pyexercises the add path, which is why that test'sSERVICE_INVOCATIONSlist must include the new service.- A file must be in exactly one place: either the spec's
FileManifest(the service owns it outright) or nowhere (it is shared, and the render-diff engine handles it). Putting an owned file's path in a shared file's conditionals, or vice versa, is the mistake to watch for — a file the manifest claims is invisible to the engine by design, so a{% if include_<name> %}block inside it will never be re-rendered on add/remove of a different component. - The SQLModel classes are the single source of the schema. The generated
project's
tests/test_model_registry.pyreplays the revisions onto a scratch database and asserts they rebuild the models exactly, andtests/cli/test_migrations_match_models.pydoes the same on Postgres. ServiceSpec.wiring(routers, dashboard cards/modals, deps providers) is metadata that currently only mirrors what's hand-wired into the shared Jinja template files - it does not yet drive their rendering for in-tree services. Editing one without the other leaves the spec's self-description (used byaegis services, tests, and future plugin tooling) inconsistent with what the generated project actually does.docs_pathon the spec is pinned bytests/core/test_spec_docs_paths.pyto a real file under this repo's root/docs- setting it before the docs page exists (or pointing it at the template'sdocs/services/<name>/instead of the root one) fails that test.- A table-owning service keeps its models under
app/services/<name>/models(module or package).app/core/model_registry.pyimports that path for alembic, the tests, startup andmigrate-fix; a table defined anywhere else is invisible to all four, andtests/core/test_model_registry_is_the_only_registrar.pyfails on it. - Threading
AnswerKeys.<NAME>intoservices.pyandcopier.ymlis not enough: the generate/update plumbing (template_generator.py,copier_manager.py,copier_updater.py,manual_updater.py,commands/update.py) also reads it, andcopier_manager.pygatesneeds_migration_fileson it, so a table-owning service that skips them breaks migrations onaegis updatewhile passing a freshaegis init. - Adding an i18n key to
en.pyonly failstests/core/test_i18n.py, which requires every locale to carry the same keys; stub the key into all locales now and leave real translation to the separate translator pass. - Full-body
{% if %}gating only inside a shared file; never use Copier conditional filenames (breaks on Windows).
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- jblemee/docker-compose-homelab★ 9same name
- ludeo-labs/cosmos-os★ 3same name
- ficaviolaodorata520/met-museum-mcp-server★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository14 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- tests/CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- llms.txtllms.txt
- add-cli-commandSkill
- add-componentSkill
- create-pluginSkill
- example-skillSkill
- execute-issueSkill
- i18nSkill
- releaseSkill
- squash-branchSkill
- template-devSkill
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
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.

