Pharo under agentic control — live-image TDD via MCP. A coding agent develops, tests, visualizes, and debugs directly inside a running Smalltalk object memory.
<img width="986" height="875" alt="image" src="https://github.com/user-attachments/assets/a0fe3dbc-2a05-407a-9675-74626ad1acb1" />
# Reverse Smalltalk — Agentic Control PoC
A proof of concept for putting Smalltalk under agentic control: a live Pharo 13
image, running headless, that a coding agent can inspect, modify, and execute
directly — the original Smalltalk vision of an integrated live environment,
with an LLM agent in the loop instead of (or alongside) a human at the browser.
## Architecture
```
Agent (Vibe / Claude / any MCP client)
│ MCP stdio, 22 tools
▼
smalltalk-interop-mcp-server (Python, via uvx)
│ HTTP (localhost:8086)
▼
SIS — PharoSmalltalkInteropServer (in-image, Teapot + NeoJSON)
│
▼
Live Pharo 13 image (headless)
```
And the other direction, entirely inside the image:
```
AgentSession (a Smalltalk object in the image) --Zinc--> Mistral API
^-- tool calls executed as native messages --v
Live Pharo object memory
```
The agent's unit of change is the *method*, not the file: it evaluates
Smalltalk code, reads and writes class/method sources, searches
implementors/references, imports/exports Tonel packages, and runs test
suites — all against the running image.
## What's here
| Path | Purpose |
|---|---|
| `Pharo.image` / `Pharo.changes` | The live image (SIS loaded, auto-starts on boot) |
| `pharo`, `pharo-ui`, `pharo-vm/` | Pharo 13 launcher scripts and VM |
| `keepalive.st` | Startup script that keeps the headless image alive |
| `start-pharo-server.cmd` | Double-click launcher for the headless server |
| `start-pharo-desktop.cmd` | Double-click launcher for the Pharo desktop (GUI) |
| `restart-server.cmd` | Health check + restart: starts the headless server if it is down, never a second VM |
| `snapshot.cmd` / `restore.cmd` | Whole-world undo: copy/restore the image files |
| `demo/` | The agent's actual eval payloads + one-command replay script |
| `gen_gosper.py` | RLE verifier + eval-payload generator for the Gosper gun |
| `gen_fractals.py` | Composes the diamond + fractal replay payloads from `src/` |
| `gen_agent.py` | Generator for the AgentSession build payloads (`demo/agent-*.json`) |
| `src/` | Durable source of truth: Tonel export of live packages |
| `AGENTS.md` | Operating rules for AI agents working in this directory |
| `MANUAL.md` | Plain user's guide: setup, desktop tour, troubleshooting |
| `DESKTOP.md` | Learning note: the agentic process of building a custom desktop (the method; the private art is not published) |
| `TODO.md` | Roadmap / next steps |
## Starting the server
Double-click `start-pharo-server.cmd`, or from Git Bash:
```
./pharo --headless Pharo.image st keepalive.st
```
SIS auto-starts on image boot and listens on **port 8086**
(`PHARO_SIS_PORT` to change). Stop with Ctrl+C.
Persistence caveat: the image on disk only changes when it is *saved*
(`Smalltalk snapshot: true andQuit: false`). Code compiled into a running
image is lost if the VM dies unsaved — which is why the demo exports
Tonel sources to `src/` and saves the image at the end.
## The Pharo desktop
`start-pharo-desktop.cmd` opens the same image with the full Pharo GUI —
browser, playground, inspector, the original live environment. SIS still
serves on 8086, so an agent session and a human share the same live
world: the agent can compile methods (they appear in the browser),
evaluate code, open windows on your screen, and verify its own UI work
via `/read-screen`.
One hard rule: **never run two VMs on the same image files** — stop the
headless server (close its console or `taskkill /IM PharoConsole.exe`)
before opening the desktop, and vice versa.
Verify it is up:
```
curl -X POST http://localhost:8086/eval/ \
-H "Content-Type: application/json" \
-d '{"code":"3+4"}'
```
## Connecting an agent
Register the MCP server with your client. Vibe / Claude Code / Cursor
`mcp.json` entry:
```json
{
"mcpServers": {
"smalltalk-interop": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/mumez/smalltalk-interop-mcp-server.git",
"smalltalk-interop-mcp-server"
],
"env": { "SIS_PORT": "8086" }
}
}
}
```
Exposed tools: `eval`, `get_class_source`, `get_method_source`,
`get_class_comment`, `search_classes_like`, `search_methods_like`,
`search_implementors`, `search_references`, `list_packages`, `list_classes`,
`export_package`, `import_package` (Tonel), `run_package_test`,
`run_class_test`, `install_project`, `read_screen`, `get_settings`,
`apply_settings`, and more.
## The MVP: an agent develops Conway's Game of Life, live
A coding agent built a package with a test suite — no files, no build
system, no IDE. Every change was compiled into the running image through
the SIS API, test-first. The exact payloads it sent are in `demo/`, and the
whole sequence replays with one command:
```
python demo/replay_demo.py
```
What happens (actual output, abridged):
```
== Create GameOfLife class (empty shell, no behaviour yet)
-> {"result": "created", "success": true}
== Compile storage: initialize/withAll:/liveCells/isAliveAt:
-> {"result": "storageCompiled", "success": true}
== Create GameOfLifeTest (TestCase subclass)
-> {"result": "testClassCreated", "success": true}
== Compile 5 tests describing the rules of the game
-> {"result": "testsCompiled", "success": true}
== Run tests BEFORE implementation (expect RED)
-> "5 ran, 0 passed, 0 failures, 5 errors"
== Compile the evolution rules: neighbours + nextGeneration
-> {"result": "evolutionCompiled", "success": true}
== Run tests AFTER implementation (expect GREEN)
-> "5 ran, 5 passed, 0 failures, 0 errors"
== Export durable sources (Tonel) into src/
-> GameOfLife exported, GameOfLife-Tests exported
```
The red run fails because `nextGeneration` does not exist yet — the tests
describe the rules of the game before any implementation exists. Then the
agent compiles the evolution rules into the live classes, re-runs, and the
suite goes green. Full transcript: `demo/replay-output.txt`. Durable
sources land in `src/` as plain Tonel files, reviewable in git.
The package also has a graphical view, added live by an agent while the
desktop was open: `(GameOfLife new withAll: { 1@1. 2@2. 0@3. 1@3. 2@3 })
openEvolutionView` opens an animated Roassal grid — watch a glider crawl
while SIS's screen-structure endpoint (`/read-screen`) lets the agent
verify what it drew. Human and agent share the same live world.
The showpiece is Bill Gosper's 1970 glider gun — the first known pattern
with unbounded growth, firing a glider every 30 generations, forever:
```smalltalk
GameOfLife gosperGun openGunView
```
It was added the disciplined way: the canonical RLE was fetched from a
GitHub mirror (LifeWiki is Cloudflare-walled), verified by local
simulation *before* touching the image (36 cells, one extra glider per
30-generation cycle), then driven test-first into the live system —
red, implement, green (7/7). The replay demo builds it too, so a fresh install gets the complete package. `gen_gosper.py` holds the verifier and
payload generator; `demo/gosper-*.json` are the exact evals used.
Its companion is the classic puffer train - a B-heptomino kept alive by
two LWSS escorts, lurching across the grid at c/2 while laying a dirty
debris trail (`GameOfLife pufferTrain openPufferView`). Same
discipline: the canonical RLE (Golly's pattern collection, from Alan
Hensel's lifebc) was verified by local simulation before touching the
image - the population must grow and the engine must advance exactly
c/2 - then driven test-first into the live system (red, implement,
green, 8/8). `gen_puffer.py` holds the verifier and payload generator;
`demo/puffer-*.json` are the exact evals used; the replay demo builds
it into fresh images too.
The image also carries a small fractal gallery in its own `Fractals`
package (tests in `Fractals-Tests`) — generator programs,
not data: `SierpinskiCarpet level: n` (ternary-digit exclusion rule,
8^n cells), `DragonCurve order: n` (the Heighway dragon by
deterministic doubling), and `TwinDragon order: n` (two dragons tail
to tail — the plane-tiling Davis-Knuth twindragon). Each was built
test-first and cross-verified against a Python reference point for
point, and each opens a full-screen view: `openCarpetView`,
`openDragonView`, `openTwinDragonView`.
## BoingBall: the classic Amiga demo, live on your desktop
The `BoingBall` package is the 1983 Amiga boing ball demo rebuilt as
a live Morphic animation: a checkered sphere, rendered pixel-by-pixel
from a lit unit sphere and rotated through 24 cached phases, bouncing
across the world with its shadow - and *boinging* on every bounce.
It was developed live by an agent while the user watched the desktop,
and it doubles as a live-systems architecture experiment:
- **Variants are two-line subclasses.** All state is per-class (class
instance variables, not shared class variables), each variant
overrides only hooks - `primaryColor`, `startPosition:`,
`startVelocity`, `bouncePitch` - so three balls fly simultaneously
with independent form caches and processes:
```smalltalk
BoingBall start. "red, 180 Hz, enters upper-left, moving right"
BlueBoingBall start. "blue, 260 Hz, enters lower-right, moving left"
GreenBoingBall start. "green, 320 Hz, drops from top-center"
```
- **The sound is synthesized in the image.** `BoingSound` builds the
descending boooing - a sine sweeping down one octave with a decay
envelope - into an in-memory WAV and plays it through
`PlaySound(SND_ASYNC|SND_MEMORY)` via ThreadedFFI (`winmm.dll`).
No sound files, no external tooling. Per-ball and global debounces
keep overlapping bounces from rattling into machine-gun fire.
- **It survives live surgery.** Slots were added to the running
class (Shift migrates live instances) and methods were recompiled
while the balls were animating - the loops pick up new compiled
methods o