By early June I had three desktop apps that all half-did the same job. A launcher on port 7200 that knew how to start my projects. A wallpaper shell that rendered widgets behind the Windows desktop icons. A command deck overlay for quick actions. Each one managed processes its own way, kept its own config, and broke in its own ways. When I finally audited the overlap, about 70 percent of what a proper launcher needed was already half-built inside the shell. It was just scattered.
The note I wrote to myself was blunt: I want a base "like Steam that can handle any type of stuff". Steam does not care whether it is installing a 100 GB shooter or a 40 MB puzzle game. The library treats both the same: install, launch, show it running, close it. I wanted that indifference for my own software. Web apps, Node scripts, Electron windows, scheduled agents, someone else's repo that only starts through docker compose. One host, one list, one way to launch.
Inversion, not accretion
The obvious move was to take the biggest existing app, the wallpaper shell, and bolt the launcher into it. I have watched myself do that before, and it always produces the same thing: a widget app with a launcher stapled on, twice the surface area, none of the structure. So the founding decision went the other way, and I wrote it down as a sentence I could hold future sessions to: the Shell does not become the Engine. The Engine becomes a kernel, and the Shell becomes its first tenant.
Concretely, extraction instead of addition. The process management inside the shell moved into a kernel that knows nothing about wallpapers or widgets. Then the wallpaper, the dock, the deck and every widget re-entered as ordinary tenants of that kernel. The dependency arrow flipped, and it is enforced structurally: nothing under kernel/ imports from surfaces/ or widgets/. If that import direction ever reverses, the whole idea is dead.
One manifest, six blocks
Everything the Engine can run is described by one JSON manifest, a runnable, with six composable blocks: launch, ready, surface, services, capabilities, policy.
launch says how to start it: via process, compose, url, window or agent. ready says how to know it is actually up: an exit code, a port, an http probe, a log pattern, or (later) the app reporting in itself. surface says whether it renders somewhere unusual, like the wallpaper layer. services declares what the host should inject. capabilities are the OS tricks a tenant may request, workerw and mouse-forward being the first two. policy holds restart rules and cron schedules.
The rule with actual teeth: there is no privileged kind. The kernel never branches on what a thing is. A web app is not special, the wallpaper is not special, an agent is not special. Adding a new type of citizen means registering one launch adapter, and the schema does not change. Every time I have been tempted to cheat on this, the cheat would have been the first crack in the base.
Done means deletable
I gave v1 a litmus test before writing any of it: the Engine is done when it boots and launches a web app, a script, Inzo (an external repo that only starts through docker compose), and an agent, with the wallpaper and all widgets deleted.
The deletion clause is the whole test. If the kernel still works with every tenant gone, the inversion is real and the launcher is not secretly welded to the UI. The litmus passed 7/7 on the first full run, then was reframed to 8/8 once the shell port landed. Unit tests stood at 11/11. Runtime dependencies: zod for schema validation and koffi for the Win32 calls. That is the entire list.
The part I still find funny is that v1 shipped the same day the project was founded, 6 June. Not because the Engine is trivial, but because that 70 percent really did exist already, scattered across three apps. Extraction was mostly moving code home.
Steam habits that earned their keep
Some Steam behaviors I copied on instinct mattered more than the architecture.
External adoption. Steam shows a game as running even if you launched it from the desktop. The Engine does the same: it discovers processes it did not start (a poll every 5 seconds) and adopts them into the library view. It cannot own their lifecycle, the pid stays null and stopping is best effort, but the library never lies about what is running.
Boot reconcile. On start the kernel scans for orphans from its previous life and either adopts them or cleans them up. Before this existed, a crash meant zombie processes squatting on ports.
Preflight. Launching a compose citizen first checks Docker Desktop, starts it if needed, and waits up to 180 seconds for it. Ready timeouts got tuned by scar tissue too: the early timeout tore Inzo down mid-boot while it was perfectly healthy, so compose citizens now get 300 seconds when warm and 900 when building images. The sharper version of that lesson, a polling probe that kills the healthy app it is measuring, got its own post.
The first tenant pays rent
The wallpaper survived the inversion and came back as a tenant that requests the workerw capability, the Windows window-layer trick I wrote up in the wallpaper post. Ctrl+Shift+Q opens the Library, a Steam-style client where every runnable shows its state and its logs live.
The tenant model got an unplanned stress test almost immediately. Boot turned slow and the desktop flickered, and the cause was embarrassing: the old standalone shell was still sitting in the Startup folder, so two Electron apps were fighting over the same wallpaper layer, each cold-loading hundreds of megabytes off a SATA drive. The shell had become a pure duplicate of its own successor. Deleting it freed 816 MB and half the boot time. A project is not superseded until the old one stops launching itself.
Schedules, and the missed-run problem
Runnables can carry a cron schedule in policy, and the kernel fires them. The first scheduled agent is a weekly hub-hygiene sweep, Sundays at 10:00, migrated off Windows Task Scheduler so the Engine owns the whole story: the schedule, the run history, and a ledger file per agent that my dashboard reads.
The honest trade-off is that schedules only fire while the Engine is up, and this is a desktop PC that sleeps. So a missed run is neither silently dropped nor silently executed. On the next boot it surfaces as pending, with a toast and a badge, and I choose: run now, snooze to a time I pick, or dismiss. No silent auto-run, because a missed job firing unannounced at boot is exactly how you stop trusting your own automation.
What it refuses to do
The brain service, the hook where my knowledge-graph project Inzo will eventually plug in, ships as a stub until Inzo itself reaches v1. The Engine launches Inzo fine; it does not pretend to talk to a brain that is not finished. External stops stay best effort rather than faking control the Engine does not have. And the schema has survived a month without growing a special case, which I take as the real verdict on the no-privileged-kind rule.
Steam needed years to become boring infrastructure. Mine took a day to build and the rest of the month to earn the boring part, and boring was the goal: the Engine autostarts hidden at boot, becomes the wallpaper, and I have stopped thinking about how anything launches.