Aspect

Agents that build software from a formal specification and validate that it achieves the stated goals.

View the Project on GitHub eideroliveira/aspect

Architecture

Why separate agents

One model asked to “write this module and its tests” grades its own homework: the tests it writes mirror the code it wrote, and passing them proves little. Aspect splits the work by what each agent is allowed to know and do:

The pipeline enforces these boundaries mechanically, not just by prompt: each agent’s file list is filtered to its module directory and file kind before anything reaches disk (agents.keepModuleFiles), and the workspace refuses paths that escape the root or touch go.mod.

Data flow

spec.Load ─▶ spec.Validate ─▶ plan.Build ─▶ pipeline.Run
                                              │
                        for each module in dependency order:
                                              │
        ┌──── Coder.Generate(spec, deps) ──────┤
        │            │                        │
        │     ws.WriteFiles                   │
        │            │                        │
        │     Tester.Generate(spec, code) ────┤
        │            │                        │
        │     ws.WriteFiles                   │
        │            │                        │
        │  ┌──▶ ws.Test ── fail ──▶ Coder.Generate(feedback) ─┐
        │  │         │                                        │
        │  └─────────┼────────────────────────────────────────┘  (≤ max-repairs)
        │          pass
        │            │
        │     Validator.Judge(spec, code, tests, result, coverage)
        │            │
        └──── ModuleReport
                                              │
                                        Report.Write ─▶ report.json, REPORT.md

Modules are generated in topological order so each Coder call sees the real source of its dependencies. Ties are broken alphabetically to keep runs comparable.

Within a tier, the plan is cut into waves: a module joins the first wave after all its dependencies’ waves, so modules in one wave are independent. With -parallel N, up to N modules of a wave run at once. Only the model calls overlap; manifest syncs and toolchain runs in a workspace are serialised behind a lock, because two go mod tidy or swift build invocations in one tree corrupt each other. The Swift manifest declares only targets whose directories exist, so a module still being written cannot break another module’s build. The report keeps plan order.

Verification methods

A goal declares how it can be checked, and the Validator applies a different standard to each:

verify Achieved when
test a test genuinely exercising the goal exists and the real run passed
invariant a property-style test exists and the Validator’s reading of the code finds no path (errors, concurrency, overflow) that breaks it
review the Validator can cite code that supports the claim; no test can prove “the API is idiomatic”

A failed test run caps every test and invariant goal below achieved, whatever the model would otherwise say; the pipeline states the real outcome in the Validator’s prompt and the report shows the last run.

The system pass

Module verdicts answer “does this module do its part?”. Once every tier is built, a separate System Validator answers “do the parts add up?”. It sees the whole spec, every module’s test outcome and verdict, and the implementation of every module that provides or consumes a surface (other modules are included while a size budget allows). It returns one verdict per system goal considering all accountable modules together, an intent judgement for the system as a whole, and an integration finding per provider/consumer pairing: routes, methods, shapes, errors and auth compared on both sides, across tiers and languages.

A goal’s final status is the weakest of the module roll-up and the system verdict, so a system that composes badly cannot hide behind modules that each pass.

Silence never means success

Two rules make the report conservative:

  1. reconcileGoals guarantees one verdict per accountable goal. A goal the Validator forgot becomes unverifiable with zero confidence.
  2. summarise rolls a goal up across modules as its weakest verdict, and a module that errored out marks every goal it owns unverifiable.

Model usage

Tiers

A multi-tier spec is a set of single-tier specs joined by interfaces. The pipeline builds tiers in dependency order, each in its own workspace with its own language profile, manifest and import allowlist. Modules see the whole system in outline (every tier, every interface with its provider) but only their own tier’s stack, and the full contract of the surfaces they implement or consume. A consumed surface is rendered with a note that it is served elsewhere: the module must implement a client against the contract, with the base address from configuration.

Single-tier specs are handled through the same code path: the spec exposes an implicit tier with an empty name, so nothing branches on the spec’s shape. Ownership rules become tier-aware (entities belong to the database’s tier, surfaces to the provider’s tier, depends_on never crosses tiers), and the topology field is validated against the shape it claims.

Language profiles

internal/lang is the only place that knows a language’s layout, manifest, toolchain commands and idioms. A profile supplies:

The agents and the pipeline never branch on the language name.

Stack, database and interfaces in prompts

The system-level context every agent sees carries the stack for the target language, the database in outline (engine, test strategy, entity names and intents) and the interfaces in outline. The module section then carries the full definitions of the entities the module owns and the surfaces it implements, plus the guidance of the frameworks those surfaces use. Keeping detail with the owner keeps prompts small and makes drift visible: a module that touches an entity it does not own is contradicting what it was shown.

Drift

aspect drift runs the judging half of the pipeline over code that already exists, generated or not. For every tier it opens the workspace, reads the tree, and for every planned module finds its implementation and tests by the profile’s layout, runs the real toolchain, and asks the Validator for a verdict with the same prompt the build used. The System Validator then judges the whole. Three things fall out that a build cannot tell you:

Every file goes to exactly one module: the one whose directory holds it, the deepest when several do. A module placed by path (what aspect import writes) owns its one package directory, so modules/api never takes modules/apikeys/ or modules/api/v2/, and the root module owns only the root’s files. -repo DIR points drift at a repository instead of aspect run output; it then writes nothing there (no manifest, no go mod tidy), and a symlinked -out entry is followed.

-no-llm keeps the deterministic part only, which is enough for a CI gate on presence and tests.

Importing an existing system

analyze.Go ─▶ inventory (deterministic)
                 │
                 ├─▶ Describer × packages ─▶ fragments (cached as JSON)
                 │
                 └─▶ Synthesizer (fragments + summary [+ target]) ─▶ synthesis
                                                                       │
                          importer.Assemble (deterministic) ◀──────────┘
                                   │
                          spec.Validate ─▶ aspect.yaml + warnings

Two modes share the same agents:

Assembly’s last pass moves any surface a module “implements” but does not provide into consumes, so a model that blurs the line still yields a valid spec.

The inventory is the only language-specific part of the importer. Adding a source language means adding an analyser that produces the same Package shape.

Trust boundaries

Generated code is executed (go test -race) on the machine running Aspect. Treat aspect run like running any code you have not read: use a throwaway directory or container for specs you do not control. The workspace bounds each run with a timeout but does not sandbox it.