Morphic Agent Framework

Specifying Software

How to describe what you are building, in Psy, so an agent can build it correctly instead of plausibly.

An agent with good instructions and no knowledge of your software will produce work that is confident, well-formatted, tested, and wrong.

It will invent an architecture, because it needs one and you did not give it one. It will name things plausibly and inconsistently. It will "fix" a deliberate design decision, because nothing told it the decision was deliberate. And when you explain all of that in review, the explanation goes into the review and evaporates, so the next task starts from zero.

The fix is not a longer prompt. It is a description of the software that lives in the repository, is checked by a compiler, and reaches the agent automatically when it is relevant.

export component LayerStack:
    description: The ordered stack of layers in a document.

    responsibilities:
        - Maintain layer order and z-index
        - Apply blend modes and opacity

    invariants:
        - Order is stable across save and load
        - The renderer never mutates it

Which becomes skills/layer-stack/SKILL.md:

---
name: layer-stack
description: "Component: The ordered stack of layers in a document."
---

# LayerStack

## Responsibilities

- Maintain layer order and z-index
- Apply blend modes and opacity

## Invariants

- Order is stable across save and load
- The renderer never mutates it

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

That is a definition. This chapter is about writing good ones.

A definition is not documentation

The distinction matters, so let me be precise about it.

Documentation explains the code to a person who is going to read the code. A definition states what is true about the software to someone who is about to change it — and is allowed to be true before the code is.

Which gives definitions three properties documentation does not have:

  • They are normative. Where the implementation disagrees with the definition, the definition is right and the implementation is a bug. Every definition inherits a paragraph saying exactly that.
  • They are checked. They are Psy. They have contracts, types, and a compiler that rejects a component with no responsibilities.
  • They are addressable. A definition has a name, so a skill can context it and an agent gets handed it. Documentation has to be found.

Practical consequence: do not describe the code. Describe the software. If a definition would go stale when someone renames a function, it is documentation and you have written the wrong thing.

# No. This is documentation, and it is already wrong.
export component LayerStack:
    responsibilities:
        - Exposes push(), pop(), reorder(from, to) and flatten()

# Yes. This is still true after any refactor that keeps the design.
export component LayerStack:
    responsibilities:
        - Maintain layer order and z-index
        - Apply blend modes and opacity
    invariants:
        - Order is stable across save and load
        - The renderer never mutates it

The five kinds

KeywordDescribesRequires
conceptan idea you have to understand — starting with the program itselfoverview: stringone root, plus one per big idea
domaina logical area, and what belongs to itcovers: string[]three to seven
featurea specific behaviour the product exhibitsbehaviour: stringwhen it was decided, not obvious
componenta reusable unit of implementationresponsibilities: string[]when it has a non-obvious job or an invariant
referencecross-cutting material governing how, not whatguidance: stringwhen you have explained it twice

Each has its own page with the properties worth adding and a set of worked examples. What follows here is how they fit together.

As the package declares them:

export abstract spec Concept extends Definition as concept:
    abstract overview: string

export abstract spec Domain extends Definition as domain:
    abstract covers: string[]

export abstract spec Feature extends Definition as feature:
    abstract behaviour: string

export abstract spec Component extends Definition as component:
    abstract responsibilities: string[]

export abstract spec Reference extends Definition as reference:
    abstract guidance: string

Choosing between them

The pair people get stuck on is domain and component.

A domain is a region of the problem. Rendering. Export. Input. It is where work happens, not a thing you can import.

A component is a thing in the codebase. LayerStack. ExportDialog. DocumentStore. You could point at it.

A domain contains components. If you are unsure which you have, ask whether a new hire would be assigned to it (domain) or told to go and read it (component).

The other three are easier:

  • concept is an idea you have to understand. There is one root concept describing the whole program, plus one for anything bigger than a feature and less specific than a domain — the document model, how undo works, what "eventually consistent" means here. If you would draw it on a whiteboard, it is a concept.
  • feature is user-visible behaviour. What the product does, from outside.
  • reference is everything that governs how work is done regardless of where — voice, naming, visual language, error style. A concept describes how something works; a reference states how work should be done.

A worked example

Say you are building a vector graphics editor. Here is the whole thing, from the top down.

Start with the root concept. It is the thing an agent reads first, and the only place the shape of the program is stated.

use "@psy/morphic"

export concept Editor:
    description: How the editor fits together.

    overview:
        A document is a tree of layers rendered onto a single canvas. Tools
        mutate the document; the renderer never does. Every mutation goes
        through the command stack, which is what makes undo total rather than
        best-effort.

    constraints:
        - The renderer is pure: document in, pixels out
        - No tool touches the canvas directly
        - Every user-visible action is undoable
---
name: editor
description: "Concept: How the editor fits together."
---

# Editor

## Overview

A document is a tree of layers rendered onto a single canvas. Tools mutate the
document; the renderer never does. Every mutation goes through the command
stack, which is what makes undo total rather than best-effort.

## Constraints

- The renderer is pure: document in, pixels out
- No tool touches the canvas directly
- Every user-visible action is undoable

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

Note constraints — not a property Morphic knows about. It became a section anyway. Anything you declare does.

Then a concept for each idea that spans domains. These are the whiteboard ones — bigger than a feature, less specific than a domain, and owned by nobody.

export concept UndoModel:
    description: How undo works, and what makes something undoable.

    overview:
        Every document mutation is a command with a forward and a reverse. Undo
        applies reverses in order. There is no snapshotting.

        A gesture is one command, not many. Dragging a layer produces a single
        Move, coalesced as it happens, because a user thinks of it as one action.

    boundaries:
        - Selection, zoom, tool choice and panel layout are not undoable
        - Everything inside the document always is
---
name: undo-model
description: "Concept: How undo works, and what makes something undoable."
---

# UndoModel

## Overview

Every document mutation is a command with a forward and a reverse. Undo applies
reverses in order. There is no snapshotting.

A gesture is one command, not many. Dragging a layer produces a single Move,
coalesced as it happens, because a user thinks of it as one action.

## Boundaries

- Selection, zoom, tool choice and panel layout are not undoable
- Everything inside the document always is

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

Undo shows up in Tools, in Files, and in the UI. It belongs to none of them, which is exactly why it is a concept rather than a domain.

Then the domains. Three or four. These are the regions work gets assigned to.

export domain Rendering:
    description: Turning a document into pixels.

    covers:
        - The scene graph and its traversal
        - Rasterisation and caching
        - Colour management

export domain Tools:
    description: Everything the user draws and edits with.

    covers:
        - Tool activation and modal state
        - Hit testing and selection
        - The command stack

export domain Files:
    description: Reading and writing documents.

    covers:
        - The on-disk format and its versioning
        - Import and export

Then the components that live in them.

export component LayerStack:
    description: The ordered stack of layers in a document.

    responsibilities:
        - Maintain layer order and z-index
        - Apply blend modes and opacity

    invariants:
        - Order is stable across save and load
        - The renderer never mutates it

export component CommandStack:
    description: The undo/redo history for a document.

    responsibilities:
        - Record every document mutation as a reversible command
        - Coalesce rapid mutations from a single gesture

    invariants:
        - A mutation that bypasses this is a bug, not an optimisation

Then the features, which are what a user would say the product does.

export feature PenTool:
    description: Draw and edit bezier paths.

    behaviour:
        Click places an anchor. Dragging while placing shapes the handles.
        Clicking the first anchor closes the path.

export feature Export:
    description: Write a document to disk.

    behaviour:
        Choose a format, then a path, then write.

    formats:
        - SVG
        - PNG
        - PDF

    constraints:
        Export never mutates the document.

And the references — the cross-cutting rules that apply everywhere.

export reference Voice:
    description: How the product speaks to users.

    guidance:
        Plain and direct. Never cute. Name the object, not the gesture:
        "Delete layer", not "Remove this".

export reference Naming:
    description: Naming conventions across the codebase.

    guidance:
        Types are nouns. Commands are imperative verbs. Anything that mutates
        the document is a Command and ends in one.
psy build
build: 9 artifacts across 1 target (9 updated, 0 unchanged)
  claude: 9 artifacts in .claude
.claude/skills/
  editor/SKILL.md          the concept
  rendering/SKILL.md       the domains
  tools/SKILL.md
  files/SKILL.md
  layer-stack/SKILL.md     the components
  command-stack/SKILL.md
  pen-tool/SKILL.md        the features
  export/SKILL.md
  voice/SKILL.md           the references
  naming/SKILL.md

That is maybe two hundred lines describing an entire application, and it is enough for an agent to make correct decisions in code it has never seen.

The layout

One declaration per file, one folder per kind, re-exported through an index.psy:

src/
  concepts/      Editor, UndoModel, CoordinateSpaces
  domains/       Rendering, Tools, Files
  components/    LayerStack, CommandStack, CanvasSurface
  features/      PenTool, Export, Undo
  references/    Voice, Naming
  skills/        ImplementRendering, ImplementTool, WriteInterfaceCopy
  agents/        BuildEditor
  index.psy

The first five folders say what the program is. The last two say how to work on it. That split is the whole design, and keeping it visible in the directory tree is worth the extra folders.

// src/components/index.psy
export { LayerStack } from "./LayerStack"
export { CommandStack } from "./CommandStack"
export { CanvasSurface } from "./CanvasSurface"

Index modules are the idiomatic entry point — see More Than One File.

How much to write

The honest answer is: less than you think, and about things that will still be true in a year.

Some rules of thumb that have held up:

  • One root concept, describing the whole program. Then one more for every idea that spans domains and needs explaining — the core model, the undo machinery, the coordinate spaces. If onboarding somebody involves drawing it, write it down.
  • A domain per area you would assign work to. If nobody would ever be "working on" it, it is not a domain.
  • A component when its behaviour is not obvious from its name, or when it has an invariant somebody could break. Button needs no definition. CommandStack does.
  • A feature when the behaviour is decided rather than obvious. "Undo" needs one — what coalesces, what is undoable. "Zoom in" probably does not.
  • A reference for every rule you have caught yourself explaining twice in review.

The signal you are writing too much: a definition you would have to update to land an ordinary refactor. The signal you are writing too little: you explain the same thing in review twice in one week. That second one is the better guide — write the definition the moment you find yourself explaining something for the second time.

What every definition has

All five extend a common base that is deliberately not a keyword of its own. There is no generic definition; you pick one of the five.

abstract spec Definition:
    abstract description: string
    abstract when: string?

    authority:
        This describes the product as it is meant to work. Where the
        implementation disagrees, treat this as correct unless the task is
        explicitly to change it.

descriptionstring, required

One line saying what this is. Required, and required to be non-empty — the platform decides whether to load a definition from that line alone, so a bad one means it never gets loaded and the whole exercise was pointless.

whenstring?

When the definition is worth loading. Optional, unlike a skill's, because a definition's description is usually self-describing. Supply it when the name alone would not tell an agent it is relevant:

export component CommandStack:
    description: The undo/redo history for a document.
    when: Use when changing anything that mutates a document.

authority

The most important line in Morphic, and it is a default rather than a rule.

Every definition inherits a statement that it is normative — that where the implementation and the definition disagree, the definition wins unless the task is explicitly to change it.

That single inherited paragraph is most of what separates a definition from a comment. It is what stops an agent reading your architecture description, noticing the code does something else, and deciding the description is out of date.

And it is an ordinary inherited property, so override it when your project means something else:

export abstract spec Feature extends MorphicFeature as feature:
    authority:
        Describes intended behaviour. Where the implementation differs, file a
        bug rather than assuming this is wrong.

The required properties are not special

overview, behaviour, covers, responsibilities and guidance are all ordinary properties. Nothing special-cases them. They render as sections exactly like invariants and constraints did above.

Which means all five kinds render uniformly, and you add whatever a kind needs:

export feature Export:
    description: Write a document to disk.
    behaviour: Choose a format, then a path, then write.

    formats:
        - SVG
        - PNG
        - PDF

    constraints:
        Export never mutates the document.
---
name: export
description: "Feature: Write a document to disk."
---

# Export

## Behaviour

Choose a format, then a path, then write.

## Formats

- SVG
- PNG
- PDF

## Constraints

Export never mutates the document.

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

Both new properties became sections. No adapter change, no vocabulary change, nothing to register anywhere.

Enforcing your own shape

This is where it stops being a documentation format and starts being a specification language.

Extend a kind to add requirements every declaration of that kind must satisfy:

// src/framework.psy
import { Component as MorphicComponent, Domain } from "@psy/morphic"

export abstract spec Component extends MorphicComponent as component:
    abstract owner: string
    abstract domain: Domain

    testing:
        Every component ships with a rendering test and an interaction test.
---
name: layer-stack
description: "Component: The ordered stack of layers in a document."
---

# LayerStack

## Responsibilities

- Maintain layer order and z-index

## Testing

Every component ships with a rendering test and an interaction test.

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

Now every component must declare an owner and a domain, and inherits a Testing section.

src/components/Toolbar.psy:2:1 error PSY3015: `Toolbar` does not define abstract
property `owner`.
    src/framework.psy:5:5: `owner` is required here

A component without an owner is a failed build, not a review comment. A domain that names a feature by mistake is PSY4001.

export component LayerStack:
    description: The ordered stack of layers in a document.
    owner: rendering-team
    domain: Rendering

    responsibilities:
        - Maintain layer order and z-index

Modules then use "./framework" instead of the package.

That is a project-specific contract on your own architecture description, checked by a compiler, in about six lines. It is the reason to write this in Psy rather than in Markdown.

Wiring it to the work

A definition nothing points at will never be loaded. The link is context, and it goes on the skill, the agent or the command:

export skill ImplementRendering:
    description: Implement a change in the renderer.
    when: Use when changing anything under the rendering domain.

    context:
        - Rendering
        - LayerStack
        - Naming

    prompt:
        Keep mutation out of the renderer. If a change seems to require the
        renderer to write to the document, that is a design problem — say so
        rather than working around it.

An agent that picks up ImplementRendering is handed the domain, the component and the naming rules. It does not go looking. It does not guess. And the last sentence of that prompt only works because the definitions are there to make "that is a design problem" a checkable claim rather than an opinion.

---
name: implement-rendering
description: Implement a change in the renderer. Use when changing anything under the rendering domain.
---

# ImplementRendering

Keep mutation out of the renderer. If a change seems to require the renderer to
write to the document, that is a design problem — say so rather than working
around it.

## Context

Consult these definitions of how the product works:

- `rendering` (Rendering)
- `layer-stack` (LayerStack)
- `naming` (Naming)

Both directions are type-checked. A skill in context is an error; a definition in use is an error:

src/skills/Bad.psy:6:11 error PSY6001: `Bad.context` references `RunChecks`,
which is not a definition (concept, domain, feature, component, reference).

You cannot wire it up backwards.

export agent BuildEditor:
    description: Implement changes in the editor.

    use:
        - ImplementRendering
        - ImplementTool
        - RunChecks

    context:
        - Editor

    prompt:
        Implement the requested change, then run the checks.
---
name: build-editor
description: Implement changes in the editor.
---

# BuildEditor

Implement the requested change, then run the checks.

## Skills

Use the following Psy-defined skills when carrying out this work:

- `implement-rendering` (ImplementRendering)
- `implement-tool` (ImplementTool)
- `run-checks` (RunChecks)

## Context

Consult these definitions of how the product works:

- `editor` (Editor)

Note the agent takes only the root concept, Editor, as context. The specific domains and components arrive through whichever skill turns out to be relevant. That keeps the agent's own context small and lets the platform load the rest on demand.

How it reaches the agent

Definitions render as skills — which sounds odd for a minute, until you say it the other way round: progressive disclosure is exactly what reference material wants. Loaded when relevant, not always, and never all at once.

---
name: layer-stack
description: "Component: The ordered stack of layers in a document."
---

# LayerStack

## Responsibilities

- Maintain layer order and z-index

## Invariants

- Order is stable across save and load
- The renderer never mutates it

## Authority

This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.

The kind is prefixed onto the description, so a reader knows what they are looking at without opening the file. Full mapping in What Gets Generated.

Keeping it honest

Three failure modes, all of which I have caused personally.

Definitions that describe the code. They go stale on the next refactor, an agent notices, and now nothing in the directory is trustworthy. Describe intent and invariants. If a rename could falsify it, delete it.

Definitions nobody points at. Written once, never context-ed, never loaded. Dead weight that looks like coverage. If a definition is not named by any skill, either wire it up or delete it — and note that noUnusedSpecs will tell you about the exported ones nothing references.

Definitions as a wish list. A definition is normative, which is a strong claim. Writing one for behaviour you intend to build someday means an agent will treat unbuilt behaviour as the source of truth and "fix" the code to match. If it is not yet true and not being built now, it belongs in an issue.

The mechanical checks help with the rest. psy check catches a definition with no description, a context pointing at a skill, two definitions that slug the same. psy build --check in CI catches a definition edited without regenerating. See What Gets Generated.

Starting on an existing codebase

Do not try to describe the whole thing. It will take a week and be wrong.

  1. Write the root concept. Just the overview — how the pieces fit and why. An afternoon, at most. Resist writing the others yet.
  2. Write a domain for each area you would assign work to. Usually three to five.
  3. Write a reference for every rule you have explained twice in review lately. These pay off immediately and cost almost nothing.
  4. Then stop, and let the rest arrive on demand: the next time you explain something to a reviewer or an agent, write the definition instead of the explanation, and put it in context.

Step 4 is the whole practice. Everything above it is scaffolding to make step 4 cheap.

Next: concept

On this page