Aspire 13.3 ships aspire init with most of its detection-and-scaffolding code stripped out. What used to be deterministic file generation is now a handover: init drops a minimal skeleton, then a one-time skill called aspireify drives a coding agent through the wiring that needs to actually read your repo. I ran that handover six times across three Claude models and two un-aspirified eval apps to see where the skill earns its keep, and where it still leans on the model.
The before picture
If you ran aspire init on a non-trivial repo in 13.2 it would try to detect what was there: languages, frameworks, project layout, and emit scaffolded files based on what it guessed. That worked on greenfield demos.
On real monorepos, it usually emitted things you spent the next fifteen minutes deleting or fixing: tsconfig conflicts with the existing ESLint config, Dockerfiles that broke yarn workspaces, endpoint declarations that pointed at the wrong service. The detection heuristics were guessing, and the guessing was wrong often enough that you stopped trusting the output.
The motivation for change is in issue #15878, opened by Damian Edwards in March. Its core argument:
“A better approach would be to optimize
aspire initfor setting up the repo so that a coding agent … can successfully add Aspire orchestration.”
The proposed reframing breaks init into four jobs: install the SDK and dependencies, provide rich context for an agent, set up the development loop, and emit only minimal correct scaffolding. The implementation landed at the end of April as PR #15918 by Maddy Montaquila, who did the actual work of replacing the detection code with a skill. It removes about 700 lines of detection code from aspire init and adds a new agent skill alongside the CLI bundle.
What init drops now
In 13.3, bare aspire init --language typescript on an un-aspirified polyglot repo produces this:
.modules/ aspire.ts + base.ts + transport.ts (SDK shims)
apphost.ts createBuilder() + builder.build().run(), nothing else
apphost.run.json launch profile with random dashboard ports
aspire.config.json AppHost path + language + profiles
eslint.config.mjs Aspire's eslint config for the AppHost
package.json type: module + minimal deps
tsconfig.apphost.json tsconfig narrowed to apphost.ts
The apphost.ts is the literal “Add your resources here, for example…” stub. Run aspire start on it and the dashboard comes up with zero resources. That’s by design. The C# path is similar. A *.AppHost/ directory drops with the aspire-apphost template, a working Program.cs skeleton, and Properties/launchSettings.json. Nothing else is touched, and your .slnx is not modified.
The actual wiring is now the skill’s job: which container, which connection-string env var, which WaitFor, which parameter is a secret.
The skill
Aspireify is a one-time setup skill that ships with the CLI as embedded resources. When aspire agent init runs, the skill is written into .claude/skills/aspireify/, .agents/skills/aspireify/, or wherever your coding agent reads skills from. The agent then loads it and follows it like any other markdown skill file: scan your repo, present what it found, ask where the tradeoffs are, wire the AppHost, validate aspire start, and hand off to the evergreen aspire skill for everything that comes next.
The shape is worth describing. It’s a focused SKILL.md plus five reference documents loaded on demand:
apphost-wiring.md:WithReferencevsWithEnvironment, endpoints, ports,WaitFor/WaitForCompletion, persistent containersdocker-compose.md: parsing existing compose files into AppHost resourcesfull-solution-apphosts.md: solution-aware.slnx/.slninit, mixed-SDK guidancejavascript-apps.md:AddViteAppvsAddNodeAppvsAddJavaScriptApp, monorepo workspace handlingopentelemetry.md: ServiceDefaults placement, Python and Go OTel setup
The split matters. Your agent doesn’t load all five at once. It scans the repo, sees a docker-compose.yml, loads that reference. Sees a .slnx with mixed SDKs, loads that one. Skips the JavaScript document if there’s no package.json. The skill is engineered for a constrained context window.
Two terms come up in the principles below that are worth knowing if you haven’t met them. WithReference is the Aspire-idiomatic way to wire a service to a managed resource; it gets you connection strings, service discovery, and health checks for free. WithEnvironment just sets a literal environment variable on the resource. The skill is opinionated about when each is the right choice.
After aspire start reports every resource Healthy, the skill considers itself done. It does not self-delete: SKILL.md is explicit that the directory should stay in place until you remove it, on the theory that the same playbook stays useful next time you add a service or restructure the AppHost. The evergreen aspire skill is the one that handles ongoing AppHost work (start, stop, describe, logs, add integrations).
The principles the skill bakes in
The principles read like opinions, not rules, and that’s the point. They push the agent toward choices the Aspire team wanted to bias for. Four worth knowing:
Adapt the AppHost to the app, not the other way around. The skill is explicit about it. The user’s services already work, and the goal is to model them in Aspire without breaking anything. If your Python service reads DATABASE_URL, the skill prefers WithEnvironment("DATABASE_URL", db.Resource.ConnectionStringExpression) over asking you to rewrite your config code. Aspire’s connection-string injection still works through a string env var. You just lose service discovery and auto-retry.
Surface tradeoffs, don’t decide silently. Where a small code change unlocks a better integration, the skill tells the agent to ask. Example from the document, paraphrased: “Your API reads DATABASE_URL. I can map that with WithEnvironment (no code change) or you can switch to reading ConnectionStrings:mydb which unlocks WithReference and automatic service discovery. Which do you prefer?” Five categories of tradeoff are documented this way: connection-string naming, port-binding preservation, OTel exporter setup, .env migration, and User Secrets migration.
Prefer typed integrations over raw containers. A typed integration is a hosting method that knows about the resource it models, including its connection-string shape, health-check probes, and image defaults. AddPostgres() is typed; AddContainer("postgres:16") is the same image without any of that. Three tiers: first-party Aspire.Hosting.* (AddPostgres, AddRedis, AddProject<>) → Community Toolkit CommunityToolkit.Aspire.Hosting.* (less-common databases, message brokers, third-party services) → raw AddContainer/AddExecutable/AddDockerfile as the escape hatch. The skill tells the agent to discover the right tier via aspire docs search and aspire add, then fall back only if a typed integration isn’t available.
Optimise for local dev, not deployment. ContainerLifetime.Persistent for databases. WithDataVolume() for things you want to survive restarts. Cookie isolation with *.dev.localhost subdomains. No production health-check probes, no scaling config, no cloud resource definitions. Those belong elsewhere.
The hard rules
These are short and they bite. The skill states each in imperative form with a sentence or two of why:
| Rule | Why |
|---|---|
Don’t install the aspire workload |
Obsolete. The CLI handles SDK resolution and restoration. Installing it causes version conflicts. |
Don’t modify the root global.json |
The repo’s SDK pin is intentional. If the AppHost needs a newer SDK, add a nested global.json inside the AppHost directory only. |
Don’t change existing service <TargetFramework> |
The AppHost on net10.0 can orchestrate services on older TFMs. Only the AppHost itself needs the supported TFM. |
| Always use endpoint references for cross-service URLs | Hardcoded URLs break the moment Aspire assigns different ports. Pass service.GetEndpoint("http") (or EndpointProperty.Url) instead. |
| Prefer HTTPS endpoints by default | WithHttpsEndpoint() over WithHttpEndpoint() unless an integration genuinely doesn’t support TLS. |
These show up as the first thing the agent is told before it does any work. They’re the rules the Aspire team didn’t trust the agent to derive on its own.
Which agent to point at the skill
Before pointing your own coding agent at your own repo, the practical question is which one. The skill prose says “a coding agent” without specifying a tier. Is it worth burning the most expensive model’s tokens to drive aspireify, or does a cheaper model do the job? I ran the same handover six times, three Claude models against two of Microsoft’s own eval apps, to find out.
The eval apps both ship in the Aspire repo as blind benchmarks for the skill, alongside a public EVAL-RUBRIC.md of expected outcomes that I deliberately didn’t let any of the agents read. The polyglot stack is Python FastAPI, Go HTTP, C# minimal API, React + Vite, with Redis. The traditional .NET LOB app is ASP.NET API + Blazor admin + EF migrations + Vue frontend + Postgres + Redis. Each agent got the same prompt: “Add Aspire orchestration to this repo. Follow the aspireify skill as written. Default to the zero-code-change path on any tradeoff the skill says to surface.”
After harvesting transcripts and grading against the rubric:
| App | Opus 4.7 | Sonnet 4.6 | Haiku 4.5 |
|---|---|---|---|
| polyglot | 5/5 resources Healthy 25.6 min, 219 calls |
5/5 resources Healthy 14 min, 163 calls |
4/5 (frontend skipped) 12 min, 133 calls |
| dotnet-traditional | all Healthy 8.6 min, 85 calls |
all Healthy 11 min, 120 calls |
all Healthy 10 min, 119 calls |
Sonnet 4.6 is the sweet spot. Faster than Opus, used fewer tool calls, and on the polyglot run it was the only solution that surfaced the uvicorn-PORT issue and pinned the port deliberately (the cracks section below has the details). Opus works fine, but you’re paying for thoroughness that doesn’t change the outcome on this kind of work: same final AppHost shape, almost twice the wall-clock and tool budget.
Haiku finishes most runs and produces something that boots, but the output needs careful review for the specific corners the dashboard won’t catch. It missed the pip-install workaround on the polyglot Python service, and on dotnet-traditional it skipped both WaitForCompletion(migrations) and WithoutHttpsCertificate() on Redis. If you’re using Haiku for cost reasons, plan to spend the saved tokens on an apphost.ts diff review.
Token spend roughly tracks tool calls. Sonnet used about 75% of Opus’s budget on polyglot and parity on the dotnet path. For a single repo the cost difference is small either way: pick on quality, not on tokens.
What all three models honoured
Across six runs, zero hard-rule violations. No workload installs. No root global.json edits. No <TargetFramework> changes. No hardcoded URLs in cross-service WithEnvironment second arguments. The skill’s hard-rule list is short and stated forcefully, and every model internalised it.
That sounds like a low bar until you remember what untrained LLM authoring does to a .NET project. The default failure modes are the ones that turn a working repo into a broken one: “let me install the workload to fix that build error”, “let me bump the global.json to net10”, “let me hardcode the URL because the endpoint ref looks complicated”. The skill prevents them. None of the three models tried to be helpful in those specific destructive ways.
Honourable mention to the smaller commit-hygiene observations: the more thorough the model, the cleaner the git history. Opus on the polyglot run committed five times with conventional messages walking each step, including:
- the initial TS AppHost skeleton wiring polyglot services
- fixing the python venv path, http endpoint for the csharp project, and full container image names
- enabling dev.localhost subdomains for cookie isolation
Haiku committed less, with messages that read more like a summary than a trail. Neither is the skill’s fault; it doesn’t tell agents to commit per logical step. Maybe it should.
Where the skill leans on the model
The structural rules held up well across the eval. Two places, though, where the skill says what to do but the agent has to know enough about the underlying stack to do it right.
The library you wire might not honour the env var the skill mandates. The skill recommends withHttpEndpoint({ env: "PORT" }) for any service that reads a port from env. That presumes the service actually reads PORT. Plenty of runtimes do (Node apps that follow the Express/PaaS convention, Go services written that way, anything deployed to Heroku-style platforms before), but plenty don’t. uvicorn is one of those. It defaults to port 8000 and ignores PORT unless you pass --port on the command line.
Opus and Haiku both followed the skill literally and wrote:
.addExecutable("api-weather", "./.venv/bin/python", "./api-weather",
["-m", "uvicorn", "main:app", "--host", "0.0.0.0"])
.withHttpEndpoint({ env: "PORT" })
Both runs came up green in the dashboard. But the wiring on paper didn’t match what the service was doing: the PORT env Aspire was injecting was dead, uvicorn was binding to 8000 regardless, and the Aspire-managed endpoint pointed at a port the service wasn’t listening on. The dashboard’s executable status doesn’t catch that mismatch.
Sonnet caught it. Sonnet’s apphost.ts:
.addExecutable("api-weather", "python3", "./api-weather",
["-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"])
.withHttpEndpoint({ targetPort: 8001 })
The source comment alongside that wiring captured the reasoning. uvicorn defaults to port 8000, so the endpoint was fixed to 8001 (per the README) to avoid conflicts and keep the zero-code-change principle. Sonnet then flagged the tradeoff explicitly: switching main.py to read PORT from env would let Aspire manage the port dynamically, but that’s a code change in the service, so it defaulted to zero-code-change. Of the paths available without touching service code, this is the safest one.
A fair objection here: 8001 is hardcoded too. The distinction is what the skill’s “no hardcoded URLs” rule is actually protecting against, namely cross-service routing that breaks the moment Aspire reassigns ports. service.GetEndpoint() survives reassignment; a string literal doesn’t. A pinned targetPort on the resource itself is a different category: it’s where Aspire’s port management negotiates with what the service can actually do. The skill is opinionated about avoiding the first. The second is the cost of services that don’t read PORT from env.
That’s the kind of judgment the skill asks for and can’t check. The skill doesn’t list which libraries honour PORT and which don’t, because there are too many. The agent’s knowledge of the Python ecosystem is doing the work that the skill prose can’t. With a model that knows enough Python, the comment writes itself. Without it, the only way to catch the mismatch is by reading what the service actually binds to.
Dashboard “Healthy” doesn’t catch every missed rule. Two specific rules the skill warns about, where the dashboard won’t catch a skip:
AddRedis()registers a TLS certificate callback by default. Clients usingStackExchange.Redisor any plain-TCP Redis library handshake-mismatch. The fix is.WithoutHttpsCertificate()on the Redis resource. Miss the rule and the container starts fine, the dashboard shows green, but the API can’t actually talk to Redis.- One-shot workers (EF Core migration runners, seed-data initialisers) should run with
WaitForCompletion(migrations)on every dependent project, notWaitFor. If you useWaitForinstead, the dependent starts as soon as the migration process is running, not as soon as it has finished. The race is invisible on an empty Postgres because migrations on no tables finish in milliseconds. On a real schema it surfaces as intermittent “relation does not exist” errors at startup.
Both rules are in apphost-wiring.md, which the skill loads when the agent encounters a Redis or worker resource. They’re still rules the agent has to apply, not rules the AppHost framework enforces.
What this means for your dev loop
The split between deterministic init and skill-driven wiring is the right shape. Init does what can be done correctly without reading your code; the skill does the work that needed to read your code anyway and was previously being faked by detection heuristics. The cost of a wrong guess in 13.2 was you fixing the scaffold. The cost of a wrong guess in 13.3 is the agent asking you a question or showing you a diff before applying it. Different failure mode, much lower friction, provided you read the diff.
Some practical advice from the six runs:
- Read the AppHost the agent wrote. Open the actual
apphost.ts(orAppHost.cs). Grep for hardcoded strings inWithEnvironmentsecond arguments. Verify the secrets you expected to be parameters are parameters. - Take the experiential rules seriously.
WithoutHttpsCertificate(),WaitForCompletion(), port handling per library: these are the ones the dashboard won’t catch. If you only review the structural shape (services modeled? references wired? endpoints declared?), you’ll miss them.
Outcome
The skill can still fail. By missing an experiential rule. By following the prose literally where ecosystem knowledge would have helped. By claiming compliance on a rule it visibly violated. But each of those is a failure mode you can catch by reading a diff, which is what you wanted from aspire init in the first place.
Worth running on your own repo. Sonnet 4.6 finished the polyglot eval in 14 minutes including container startup. Even on a codebase you already aspirified by hand, the diff is informative. It surfaces the choices you didn’t realise you were making, and the rules the skill is opinionated about that you may have skipped.