The loop and the tools
Every call returns the whole outcome, errors first. A failure arrives as data to act on rather than prose to parse. deploy is synchronous: it comes back with the result, not an acceptance. There is nothing to poll, no build log to tail and no dashboard an agent has to be told to check.
deploy → read status → crashed? read errors + log_tail → fix → deploy
A deploy that crashes is reported as a crash, never as a success, with the errors in the same response. What it does not do is leave the previous version serving: a redeploy stops the running version before it starts the new one, so a crashed redeploy takes the URL down until a deploy succeeds. That is fine while nobody depends on the app. Once somebody does, run the loop on a fork: fork, deploy to app--f1 until it is healthy and looks right, then promote. A wrong guess then costs a fork, not the URL.
Once it is up, the remaining question is whether it looks right, and screenshot is the only thing that answers it: the rendered page plus the browser's console errors and failed requests. A 200 is not a working page, and that gap is the part an agent otherwise cannot see.
deploy: ship a directory; get a URL, or the errorslogs: stdout and stderr since a timestamp, filterable to stderr alonescreenshot: the rendered page, console errors, failed requestssql: schema, queries and migrations, against the app's SQLite or its external Postgresfork·promote: clone with state, then make it the real oneset_secret: write-only environment secretlist_apps: every app with its URL, state and last deploydestroy: irreversible
The console is built on exactly those endpoints. It has no private API, so there is no second contract to keep honest. Deliberately not tools: sleeping and waking happen on their own, and the durable-storage tier is a plan decision made here rather than by an agent mid-loop.