Making Pocket Pi an Agent-Native App Development Environment
An ESP32-S3 cannot host a conventional JavaScript toolchain. PocketPi becomes a development environment by closing a smaller, stricter loop around the App boundary on the board.
Siwei "Jerry" Yuan7 min read
The ESP32-S3 used for this project has a 240 MHz CPU and 8 MB of PSRAM. It cannot run Node.js, npm, a TypeScript compiler, an unrestricted shell and an IDE in the way a Mac or Windows PC can.
Instead of reproducing that desktop environment, we started from a first-principles question:
Given PocketJS as a small but complete JavaScript runtime substrate, what is the minimal closed loop required for an Agent to develop software?
The answer is four responsibilities:
inspect the source and contract that define an App;
modify an isolated candidate without mutating a live App;
validate the candidate against scratch Data;
commit it as one coherent new App version.
A compiler, a POSIX shell and an IDE are possible ways to implement those responsibilities. They are not the definition of a development environment. For PocketPi, the rest of the architecture exists to make these four steps complete on the device itself.
The model backend may run elsewhere. Here, on-device means the App source, workspace Tools, durable state, validation and activation all live on the target that runs the App.
The development loop works because each of its four steps maps back to one of those architecture choices.
The four responsibilities stay distinct. Runtime rehearsal guides iteration before submission; human review gates activation.
Inspect: make the running App legible
An Agent cannot evolve software it can only observe as pixels. The first requirement is therefore not an editing Tool. It is a filesystem model that makes App source visible without dissolving the boundaries between Apps.
/workspace/
├── system/app/ resident Pi Agent System App
└── apps/
├── demo/
│ ├── release/ source currently running
│ ├── data/ Demo-owned SQLite and files
│ └── tmp/ disposable Demo files
└── research/
├── release/ separate App source
└── data/ separate App-owned state
Ordinary Apps are isolated Guests. An ordinary View or Action Guest receives only the database and filesystem surface owned by that App. It cannot walk into a peer App or the native host. Source and durable Data are also separate, so replacing source does not grant a new release ownership of another App's state.
Pi Agent sits one level above those ordinary Guests. It is the firmware-embedded, resident System App, and the native host gives its workspace Tools a bounded view of/workspace. That is why Pi Agent can use read, find, grep and ls across ordinary App source while each ordinary App remains confined to its own surfaces. Path resolution rejects escapes beyond the workspace, and native credentials remain outside the source tree.
The resident context also exposes a compact installed-App capability catalog and a version-bound authoring contract under .system/authoring. An installed definition is already legible in apps/<id>/release; Pi Agent creates or checks out a candidate only when it intends to author an App.
Resident AgentVisible workspaceHuman update gate
Three 480 × 800 frames from the real ESP32 product-contract simulator. In the third, Pi Agent has submitted a complete update, but activation still waits for human confirmation on the device.
Modify: edit a bounded candidate
Reading live source is safe; editing it in place is not. For an installed App,app.checkout({ id }) copies the complete release/ source once into an isolated checkout/. A new App begins directly in that same candidate location. Reopening either candidate preserves work already in progress.
Checkout copies source only. The live SQLite database, temporary files and native credentials stay where they are. This creates the smallest useful write boundary: Pi Agent can change the complete App definition without mutating the running App or cloning its live state.
The candidate is small enough to understand as a directory, not as a build graph:
apps/<id>/checkout/
├── app.json identity, version, capabilities, Tools, Schedules
├── schema.sql initial SQLite shape for a new installation
├── migrations/N.sql forward Data changes for an existing installation
├── actions.js actor-neutral behavior and SQLite writes
├── view.js human UI assembled with the shared View SDK
└── assets/*.json manifest-declared static resources
The existing bounded write and exact-replacement edit Tools do the actual modification. Pi Agent sets or advances the App version in app.json, changes behavior in actions.js, and changes presentation in view.js. If the durable data shape changes, it also advances schemaVersion and adds the next migration. If only values or behavior change, the schema version stays put.
In the physical demo, those edits were concrete. The Action changed what a button writes to SQLite, while the View changed the badge from blue to green for the new value. The same candidate could also have included a migration if the SQLite shape had changed.
Both entrypoints are raw JavaScript. actions.js uses the shared Framework to define Actions and Data access. view.js assembles View.Screen, View.Row, View.Text, View.Badge and View.ActionButton from the shared View SDK. Layout, semantic colors, typography, touch targets and viewport behavior live in that SDK, so Pi Agent describes the UI with a bounded vocabulary instead of reimplementing a renderer for each App.
There is no on-device TypeScript transform, npm installation or cross-module compile. The files Pi Agent edits are already the files PocketJS will evaluate. Pi Agent validates that candidate with app.validate before submitting it.
Validate: review and rehearse the candidate
A candidate is not valid merely because its JavaScript parses. The requested change must make sense to the human who owns the device, and the implementation must work against the state it will own on the device.
app.validate({ path }) verifies the manifest, source and schema-update contract, then prepares scratch Data. It initializes a new schema or copies the installed SQLite database and rehearses its migrations before loading the candidate Actions and View. Scratch state is always removed; the live release and Data remain untouched.
Validation returns real runtime errors and screenText: a bounded textual view of the first rendered frame, including layout bounds and clipped or offscreen text. Pi Agent is expected to inspect that result and refine the candidate iteratively until validation succeeds.
Once validation succeeds, app.submit({ path }) repeats only the startup subset without building screenText, stages the complete candidate and opens the physical review UI. The human answers the product question—whether this is the App they want—before PocketPi is allowed to activate it.
Commit: activate one coherent App version
Once the human has approved the request and the rehearsal has succeeded, commit activates the whole App. It does not copy individual edited files over a running directory.
For an update, PocketPi applies the rehearsed migrations to the live SQLite database in one transaction and preserves the old source temporarily. A new App initializes its owned Data from schema.sql. Both paths activate the complete candidate at the one canonical release/ path rather than copying individual files.
Atomicity follows the actual ownership boundaries. SQLite migration is atomic in a database transaction. Source activation uses complete-directory renames rather than partial file writes. The .update directory is a crash-recovery record, so a reboot can finish an interrupted activation instead of inventing a mixed release.
This source switch can become executable immediately because PocketJS is one substrate designed to host multiple isolated Guests. The old cached Guests are discarded. A fresh Action Guest evaluates the platform-owned System Framework and then the new raw actions.js; a fresh View Guest evaluates the shared View SDK and then the new raw view.js. No module graph is compiled and no Firmware is rebuilt.
Finally, PocketPi publishes the App's Tool routes and Schedules and removes any temporary old source. Data is initialized, preserved or migrated; Actions and View become directly executable in fresh runtime Guests. That is one coherent transition of Data + Actions + View rather than a set of unrelated patches.
One committed App transition on ESP32-S3: the existing SQLite value survives, the new Action changes subsequent writes and the new View presents the result differently. Data, Actions and View advanced together without rebuilding Firmware.
The development environment is the closed loop
PocketPi is not a general-purpose development machine. It cannot build an arbitrary npm project, provide Node compatibility or let an ordinary App rewrite Firmware and native security boundaries.
What it can do is more precise: Pi Agent can inspect, modify, validate and commit the complete source boundary of an admitted App on the device, whether creating it or evolving an installed version. That is possible because earlier architecture decisions line up with the four responsibilities:
workspace ownership and App isolation make source inspectable;
bounded file and lifecycle Tools make a candidate editable;
App-owned SQLite, schema and migrations make candidates testable without live mutation;
raw JavaScript, the View SDK and one PocketJS substrate make a coherent version directly executable.
The result is not a small microcontroller pretending to be a Mac. It is an ESP32 that understands enough about its own App model to close a real software development loop on itself.