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 PsyRead 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 specificationspec 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 buildcompiles 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 implementationBuildPsy 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:
- implement a minimal lexer, parser and IR
- compile simple Psy
- compile framework keywords
- compile
spec/ - compile
build/ - generate
BuildPsy,ReviewPsyandDocumentPsy - 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 checksucceeds overspec/andbuild/psy lintreports nothingpsy format --checkreports every file canonicalpsy build --checkreports 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 outputNothing 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.