How Psy Works

Psy Describes Psy

The self-hosting loop — a language specified in itself, and the agents that maintain it reading that specification.

Time to explain the thing on the front page.

Psy is a language, and it is a language capable of describing itself. Those are two different claims and the second one needs care, so let me kill the obvious misreading first:

The compiler is not written in Psy. The compiler is TypeScript and stays TypeScript. Psy has no functions and no runtime; you could not write a compiler in it if you wanted to, and you should not want to.

What the .psy files are is the authoritative declarative specification from which the implementation is built and maintained.

The loop

Psy language specification (.psy)

Psy compiler

Morphic framework

generated build / review / docs agents

agents read Psy's own .psy specification

implement and maintain Psy

Read that once more and notice it closes. The specification is compiled by the compiler, into agents, which read the specification, to change the compiler.

I did not plan this. I built a language that was meant to be good at specification, and then ran out of excuses not to point it at the hardest specification I had access to, which was its own.

Half one: the language, in Psy

spec/ describes Psy's syntax, semantics, compiler stages, diagnostics and tooling, using a framework defined in spec/framework.psy:

export abstract spec LanguageConcept as concept:
    abstract description: string

    stability: stable
    layer: language

export abstract spec SyntaxRule extends LanguageConcept as syntax-rule:
    abstract syntax: string
    abstract semantics: string

    layer: syntax

export abstract spec SemanticRule extends LanguageConcept as semantic-rule:
    abstract semantics: string

    layer: semantics

export abstract spec CompilerStage extends LanguageConcept as compiler-stage:
    abstract input: string
    abstract output: string
    abstract responsibilities: string[]

Nine keywords in total: concept, syntax-rule, semantic-rule, compiler-stage, data-shape, diagnostic-rule, lint-rule, cli-command and non-goal.

That is an ordinary Psy framework, exactly as chapter 7 described. The core compiler has never heard of a syntax rule.

Rules are data, not prose

export semantic-rule ParentPrecedence:
    description:
        Defines precedence for multiple inheritance.

    semantics:
        In extends A, B, C, A has higher precedence than B and B has
        higher precedence than C.
export semantic-rule ComposedSuper:
    semantics:
        + super composes all inherited values of the current property
        while preserving parent precedence.

Compiled to JSON — which is how the specification is published as machine-readable data:

{
  "ParentPrecedence": {
    "$psy": {
      "name": "ParentPrecedence",
      "module": "spec/syntax/inheritance.psy",
      "keyword": "semantic-rule",
      "bases": ["SemanticRule"]
    },
    "description": "Defines precedence for multiple inheritance.",
    "semantics": "In extends A, B, C, A has higher precedence than B and B has\nhigher precedence than C.",
    "layer": "semantics",
    "stability": "stable"
  }
}

Note layer and stability — neither was written on ParentPrecedence. Both were inherited from SemanticRule and LanguageConcept.

Every rule has a name. Which sounds like a small thing and turns out to be the load-bearing detail — because a name can be referenced, by a test, by a fixture, by a diagnostic, by an agent.

And it uses its own features on itself

export spec CommonDeclaration:
    properties:
        - name
        - sourceRange

export syntax-rule ImportDeclaration extends CommonDeclaration:
    properties:
        + super
        - specifiers
        - source
{
  "ImportDeclaration": {
    "properties": ["name", "sourceRange", "specifiers", "source"]
  }
}

The specification of a language with inheritance and composition, using inheritance and composition, rather than repeating itself. If that had turned out to be awkward, it would have been a real signal about the language. It was not awkward, which is the most useful thing the exercise told me.

spec/
  framework.psy      the language-description vocabulary
  language.psy       the root concept and shared mixins
  architecture.psy   pipeline, packages, layering, bootstrap
  syntax/            modules, declarations, values, strings, comments, lists,
                     objects, references, interpolation, inheritance,
                     composition, types
  compiler/          lexer, parser, ast, semantics, ir, provenance,
                     diagnostics, formatter, linter, cli
  index.psy          re-exports the whole specification

spec is its own repository and verifies itself: npm run verify there runs psy check, psy lint, psy format --check and psy build --check over the specification, with the real compiler. spec/index.psy is where a reader starts — human or otherwise.

Half two: the agents, in Psy

build/framework.psy is the Morphic framework, extended locally to attach a standing contract:

export abstract spec Skill as skill:
    abstract description: string?
    abstract when: string
    abstract prompt: string
    abstract use: Skill[]?

    contract:
        The Psy specification in spec/ is authoritative. Read it before
        changing behaviour, and do not invent behaviour that contradicts it.

export abstract spec Agent as agent:
    abstract description: string?
    abstract prompt: string
    abstract use: Skill[]?
    abstract model: string?
    abstract tools: string[]?

Every skill in the project now carries that paragraph, from one place.

Which every skill in the project then inherits:

## Contract

The Psy specification in spec/ is authoritative. Read it before changing
behaviour, and do not invent behaviour that contradicts it.

Skills use inheritance so instructions are written once:

export skill ImplementCompilerComponent:
    prompt:
        Read the relevant Psy specification before modifying code.

        Preserve compiler stage boundaries.

        Add or update tests for every semantic change.

export skill ImplementLexer extends ImplementCompilerComponent:
    prompt:
        + super

        Focus on tokenization, indentation, source locations,
        interpolation, comments, and lexical errors.

Agents do the same, and the specialised ones compose both the prompt and the skill list:

export agent BuildLexer extends BuildPsy:
    use:
        + super
        - ImplementLexer

    prompt:
        + super

        Focus specifically on lexer behaviour defined by
        spec/compiler/lexer.psy.

.claude/agents/build-lexer.md:

---
name: build-lexer
description: Implement and maintain the Psy lexer.
---

# BuildLexer

Read the relevant Psy specification before modifying code.

Preserve compiler stage boundaries.

Focus specifically on lexer behaviour defined by spec/compiler/lexer.psy.

## Skills

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

- `read-psy-spec` (ReadPsySpec)
- `run-tests` (RunTests)
- `implement-lexer` (ImplementLexer)

## Contract

The Psy specification in spec/ is authoritative. Read it before changing
behaviour, and do not invent behaviour that contradicts it.

Both prompts composed, both skill lists composed, and the contract inherited from the framework — none of it written in BuildLexer.psy.

The full set is BuildPsy, BuildLexer, BuildParser, BuildSemantics, BuildFormatter, BuildLinter, BuildCLI, ReviewPsy, DocumentPsy and IdentifySpecGaps.

Generating them

psy build

compiles build/**/*.psy into real Claude Code artifacts:

.claude/
  agents/build-psy.md
  agents/build-lexer.md
  ...
  skills/read-psy-spec/SKILL.md
  ...

each carrying a header naming the source it came from:

<!-- Generated by Psy from build/agents/BuildLexer.psy. Do not edit by hand;
     edit the .psy source and run `psy build`. -->

And nothing declared in Psy is dropped — description becomes frontmatter, prompt becomes the body, use becomes a skills list, and every other declared string or string-list property becomes its own section. Which is why ReviewPsy's responsibilities and method, and the contract every skill inherits from the framework, all show up in the generated file without anyone teaching the adapter about them.

Generation is deterministic. psy build --check fails if anything is stale.

The authority relationship

spec/**/*.psy

ReadPsySpec

BuildPsy

TypeScript implementation

BuildPsy is not a generic coding agent that happens to work on this repository. Its prompt says so, and ReadPsySpec encodes how to read a specification that is spread across forty files:

export skill ReadPsySpec:
    prompt:
        Read the Psy specification from spec/index.psy.

        Follow imports and references recursively.

        Treat the Psy specification as the authoritative description
        of expected language and compiler behaviour.

        Read root-level specification files first.

        Then recurse into subdirectories.

        Files within the same directory should be interpreted as locally
        related context.

        When ambiguity remains, prefer the nearest relevant specification
        context while respecting more explicit rules elsewhere.

That skill is the hinge of the whole thing. It is what turns "there is a specification in the repository" into "the specification is what gets consulted".

The bootstrap problem

There is an obvious one:

Psy must compile .psy files
before Psy-generated agents can exist.

Solved the boring way, explicitly. The initial TypeScript compiler is the bootstrap implementation, and the milestones were:

  1. implement a minimal lexer, parser and IR
  2. compile simple Psy
  3. compile framework keywords
  4. compile spec/
  5. compile build/
  6. generate BuildPsy, ReviewPsy and DocumentPsy
  7. use those definitions as the project's maintained workflow

After step 4 the language specification is dogfooded. After step 5 the agent definitions are. The finished implementation supports the complete v1 language, not merely the subset the bootstrap needed — that distinction is the one that usually gets fudged, so it is written down.

Does it hold?

npm run verify checks all of it:

  • the compiler builds and type-checks
  • the test suite passes
  • psy check succeeds over spec/ and build/
  • psy lint reports nothing
  • psy format --check reports every file canonical
  • psy build --check reports no stale artifacts
  • the documentation site builds

Plus tests that assert the loop specifically, rather than just the parts: that BuildPsy uses ReadPsySpec and names spec as authoritative; that specialised agents inherit it; that skill inheritance composes prompts rather than duplicating them; and that the formatter is a fixed point over every .psy file in the repository.

There is also a test asserting that every rule name a fixture claims to validate actually exists in spec/, and that every diagnostic code an invalid fixture expects has a diagnostic-rule describing it. That keeps the trace from specification rule → test → implementation honest, which is the part that would otherwise rot first.

Why this is not a stunt

Because the layering never breaks, and it would break here first if it were going to.

Psy                     a general declarative language
psy-spec framework      a language-description vocabulary
Morphic framework       a skill and agent vocabulary
adapters                concrete platform output

Nothing in a lower layer depends on a higher one. The core compiler has never heard of a skill, an agent, or a syntax rule. Every one of those vocabularies is an ordinary framework built the way chapter 7 tells you to build yours.

Which is the actual claim I want to make. Not "look, it describes itself" — that is a party trick. The claim is: a language good enough to specify itself, using nothing but the extension mechanism it offers everyone else, is a language that will be good enough to specify whatever you point it at.

That is the test. It passed. Go point it at something.


The Language Reference has every construct, the grammar, the diagnostic codes, and an argument for each thing v1 deliberately does not do. The Reference behind it has the config fields, the compiler types, installation in full, editor support and the example projects.

On this page