Formats
The wire contracts in @spunto/build — written by one program, read by another, and changed only through a migration.
Most of @spunto/build is ordinary code. If a function gets better, callers upgrade and nothing else
has to happen. Some of it is not code at all. It is a format: bytes that one program writes and a
different program reads back, often in a different language, in a different process, minutes or
months later.
- A shell script running inside a
docker buildprints::spunto:step:start:feature:node, and a TypeScript tracker in the control plane turns it into a lit-up block in the UI. - A setup script writes
~/.mp-status.jsoninside a container, and a control planecats it every 1.5 s to decide whether the worker is ready. - A worker created last month is called
mp-worker-<id>, and today's code has to find it under that name. - A JSON file exported from Lite sits on someone's disk until they import it into Cloud.
The writer and the reader never ship together. A container keeps the name it was created with. A
build log stored last week keeps the markers it was written with. A project file keeps the kind it
was exported with. So changing a value in a format is a migration, not an edit. Each format gets
its own page in this tab, with its exact bytes, so that reviewers can tell when a diff is one.
The tests in the package pin these formats to their exact strings, and they are meant to be annoying to change. If you find yourself updating one, you are changing a contract with things that already exist. Plan for both the old bytes and the new ones.
The formats
| Format | Defined in | Written by | Read by |
|---|---|---|---|
Build log markers ::spunto:step:start:<id> / end:<id> | ./steps | The generated image build script (echo), and agent-side code next to the build (stepMarker) | BuildStepTracker on the live log, renderStoredBuildLog on replay |
Extension failures [build] EXTENSION FAILED: <id> and /etc/mp-extension-failures | ./steps, ./naming | An image build script that cannot install an extension (not yet emitted by the package's own buildImageScript, see the page) | parseFailedExtensions on the stored build log (Spunto Lite's project page) |
Project spec kind: "spunto/project" | ./spec | buildProjectSpec (Export) | parseProjectSpec (Import), in either product |
Worker setup status ~/.mp-status.json | ./types, ./naming | The generated setup and start scripts, inside the container | The control plane, polling the file (or receiving the same SetupStatus from an orchestrating agent) |
Names and paths mp-worker-<id>, mp-proj-<slug>:v<n>, /opt/mp-extensions… | ./naming | Whatever creates the container, volume, network, image or file | Whatever looks it up later: Docker clients, teardown, disk accounting, the UI |
Skills layout ~/.spunto/skills/<id>/, .links, [skills] | ./script (buildSkillsScript) | The skills script, at setup and before each task | The same script on its next run, the harness (through symlinks), and humans reading the log |
Agent events TASK_EVENT_TYPES | ./agent-stream | parseStreamLine adapters, or a harness speaking jsonl natively | Whatever stores and renders a task's timeline |
What is not a format
The generators in ./script produce scripts, but the script text itself is not a contract. Nobody
parses it back. It can be refactored freely, as long as it keeps emitting the formats above. When
an image recipe change must reach images that were already built, IMAGE_RECIPE_VERSION is bumped,
and both products compare it at spawn and rebuild stale images.
The same applies to ./extensions and ./terminal. They are protocol clients (Open VSX, a
VS Code gallery, dtach), not protocols of their own, with one exception: ./terminal builds shell
commands whose layout (/tmp/spunto-term) is shared between the commands it builds. That layout is
documented on the terminal module page.