ADR-0014: Agent-shell dev launch and CDP GUI verification
Status: Active · Date: 2026-07-22
References
- Dev-launch delivery notes
- Agent Debugging guide — how to launch, list targets, and evaluate JS over CDP
- ADR-0012: Vertical right-dock tabs, ADR-0013: Business themes export ui-variables — the shipped-but-unseen UI work this unblocks
Context
A batch of UI work (the vertical tab panel, the ui-variables refactor, the Settings default tab) shipped authored-and-compile-checked but never seen running, because the agent doing the work could not launch the app.
yarn start → tranquil-client/scripts/dev.js spawns Electron inheriting the parent
environment (env: { ...process.env, … }). An agent/IDE shell — specifically the VSCode
extension host the agent runs inside — exports ELECTRON_RUN_AS_NODE=1. Inherited by the
spawn, it makes the Electron binary run as a plain Node interpreter rather than a GUI
app; Node then rejects the Chromium flags and the process exits in ~0.3s:
.../Electron: bad option: --no-sandbox
.../Electron: bad option: --enable-logging The bad option: prefix is Node’s unknown-flag format (not Chromium’s) — the tell that the
binary is running as Node. Confirmed empirically: electron --version reports Node’s v20.16.0 with the variable set, and Electron’s own v30.5.1 under env -u ELECTRON_RUN_AS_NODE.
Decision
Strip ELECTRON_RUN_AS_NODE from the child environment in scripts/dev.js before
spawning Electron, and adopt CDP-driven inspection on localhost:9222 as the standard
live GUI-verification workflow for agent-authored UI changes — connecting with puppeteer-core (browserURL, never newPage()) or a raw-WebSocket Runtime.evaluate helper and driving the app through the live renderer atom global.
Two supporting changes to scripts/dev.js make agent launches safe to run and tear
down: (1) it forwards extra CLI args to Electron, so an agent launches an isolated,
disposable instance with --user-data-dir=<tmp> (own window-state + single-instance
lock, never touching a real session and dodging the single-instance handoff); and (2) it forwards SIGINT/SIGTERM to the Electron child, so stopping the launcher cleanly tears
down the whole app instead of orphaning the GUI.
Full isolation also isolates config and the project, so the app never pollutes the real environment or operates on real source:
ATOM_HOME=<tmp>(seeded from the real one to keep theme/packages) gives the instance its own config/session — earlier launches that used the real~/.tranquiloverwrote the saved window layout, which this prevents.- A dedicated
tranquil-test-suiterepo is opened as the project instead oftranquil-client. Because Electron consumes the app dir.as its first positional and Atom also opens it as a project, the launched window would start with bothtranquil-clientand the test suite as roots. A gated filter insrc/main-process/atom-application.js(openPaths) drops the resource-path (tranquil-client) root before any window opens — zero flash — whenprocess.env.TRANQUIL_VERIFYis set (whichscripts/verify.jssets). Post-loadsetPathswas tried first but left tranquil-client visibly mounted for ~1s during load; the gated filter is the only reliable zero-flash option.
The whole flow is packaged as yarn verify (scripts/verify.js): seed throwaway ATOM_HOME,
launch isolated opening the test suite, wait for CDP, print status, and tear down cleanly on stop.
Options considered
- Strip the variable in
scripts/dev.js(chosen) — one line, fixes every launch context; deleting an unset variable is a no-op in a normal terminal.scripts/dev.jsis Tranquil-owned (upstream Pulsar usesscript/, singular, with nodev.js), so this adds zero upstream merge surface. - Unset it ad-hoc per launch (
env -u ELECTRON_RUN_AS_NODE yarn start) — rejected: relies on every caller remembering the incantation; the fix belongs in the launcher. - Drive verification off screenshots — rejected as the primary signal:
Page.captureScreenshot/page.screenshot()can hang indefinitely on a webview-heavy renderer. CDPevaluateof layout facts (getBoundingClientRect,getComputedStyle) plus a human-in-the-loop for visual judgement is the reliable path.
Consequences
- Live GUI verification is now possible from an agent shell.
yarn startopens real Electron windows; verified end-to-end (Electron 30.5.1, CDP live,atomglobal reachable,tranquil-business-darkactive, owned packages loaded). - One-command verify.
yarn verify(scripts/verify.js, using the already-presentpuppeteer-coreand the already-enabled CDP port fromstart.js) launches the fully isolated instance and holds it open; the workflow is documented in the Agent Debugging guide. - Isolated, safely-disposable instances. Agent launches use
--user-data-dir=<tmp>and are torn down by simply stopping the launcher (signals now propagate to Electron — verified: an 8-process instance reaped completely,:9222released, no orphan). No broadpkillagainst the shared binary path (which would also kill a real session). - A new, gated
src/main-processedit. TheopenPathsfilter is the one core change here (alongside the pre-existingremote-debugging-portline) — a small upstream merge surface, accepted because zero-flash project isolation is impossible from owned/dev code alone. It is inert unlessTRANQUIL_VERIFYis set, so it cannot affect production or a plainyarn start. - Caveats to respect (all in the guide): screenshots can hang on a webview-heavy renderer; use
Node ≥18 for the CDP client; the single-instance handoff still applies to a plain
yarn start(a distinct--user-data-dirsidesteps it). - Re-evaluate if the app moves off the
remote-debugging-portswitch, or if a headless agent environment stops exportingELECTRON_RUN_AS_NODEand a different launch blocker appears.