Tutorial: A Paint App

What We Are Building

A guided walk through speccing a small paint program in Psy, one step at a time.

Everything up to here has been explained in pieces. This section does one small thing end to end.

We are going to spec out a paint program. An MS Paint clone — a bitmap, three drawing tools, and the two file operations you cannot do without:

  • Pencil — hard single-pixel line
  • Brush — soft, sized, anti-aliased
  • Eraser — paints the background colour back
  • New Drawing — start again
  • Save Drawing — write a PNG

Not much of a product. Exactly enough to need every definition kind, and small enough that you can hold all of it in your head while we go.

By the end you will have a directory of .psy files that compiles, a set of generated Claude Code artifacts, and an agent that knows what it is building.

What you end up with

paint/
  psy.config.ts
  src/
    concepts/      Paint, StrokeModel
    domains/       Canvas, Tools, Files
    features/      Pencil, Brush, Eraser, NewDrawing, SaveDrawing
    components/    CanvasSurface, ToolPalette, ColorPicker, FileStore
    references/    Voice, Naming
    skills/        CanvasRules, ImplementTool, WriteInterfaceCopy
    agents/        BuildPaint
    index.psy
  .claude/         generated
    agents/build-paint.md
    skills/*/SKILL.md

Sixteen declarations. About three hundred lines of Psy. And this on the other side, one file of nineteen:

---
name: pencil
description: "Feature: Draw a hard single-pixel line."
---

# Pencil

## Behaviour

Dragging draws a one-pixel line in the foreground colour, following the
pointer. The line is hard-edged: no anti-aliasing, no opacity ramp, every
touched pixel is set to exactly the foreground colour.

## Edge Cases

- A click without a drag paints exactly one pixel.
- Dragging outside the canvas and back continues the same stroke; the pixels
  outside are discarded rather than clamped to the edge.

## 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.

Set up the project

mkdir paint && cd paint
psy init . --template morphic --yes
psy init: created 4 files from template `morphic`
  psy.config.ts
  src/skills/ExampleSkill.psy
  src/agents/ExampleAgent.psy
  src/index.psy

Delete the two example declarations — we are writing our own:

rm src/skills/ExampleSkill.psy src/agents/ExampleAgent.psy

And make psy.config.ts this:

import { defineConfig } from "@psy/config";
import { morphic } from "@psy/morphic";
import { claude } from "@psy/adapter-claude";

export default defineConfig({
    source: ["./src"],
    frameworks: [morphic()],
    targets: [claude({ output: ".claude" })],
    artifacts: { manifest: true },
});

Three lines of that matter:

  • frameworks: [morphic()] gives us the eight declaration keywords and the rules that go with them.
  • targets: [claude(...)] turns declarations into Claude Code artifacts.
  • artifacts: { manifest: true } lets psy build clean up after itself when we rename something, which we will. See pruning.

Empty out src/index.psy for now:

// src/index.psy

And check that everything is wired up:

psy check
check: no problems found in 1 file(s)

One file, no problems, nothing declared. That is the starting line.

How this goes

Seven steps, outside in. We describe the program before we describe its parts, and we describe its parts before we describe how to work on it.

  1. The Concept — what a drawing is.
  2. The Domains — the three areas of the problem.
  3. The Features — all five, and what makes each one decided.
  4. The Components — what owns the bitmap, and what must stay true.
  5. The References — the rules that apply everywhere.
  6. Skills And An Agent — wiring knowledge to work.
  7. Build It — and see what an agent actually receives.

Every step shows the .psy you write and the file it produces. Nothing is hand-waved.

Next: The Concept

On this page