Docs

ADR-0014: Agent-shell dev launch and CDP GUI verification

Status: Active · Date: 2026-07-22

References

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 starttranquil-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 ~/.tranquil overwrote the saved window layout, which this prevents.
  • A dedicated tranquil-test-suite repo is opened as the project instead of tranquil-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 both tranquil-client and the test suite as roots. A gated filter in src/main-process/atom-application.js (openPaths) drops the resource-path (tranquil-client) root before any window opens — zero flash — when process.env.TRANQUIL_VERIFY is set (which scripts/verify.js sets). Post-load setPaths was 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.js is Tranquil-owned (upstream Pulsar uses script/, singular, with no dev.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. CDP evaluate of 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 start opens real Electron windows; verified end-to-end (Electron 30.5.1, CDP live, atom global reachable, tranquil-business-dark active, owned packages loaded).
  • One-command verify. yarn verify (scripts/verify.js, using the already-present puppeteer-core and the already-enabled CDP port from start.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, :9222 released, no orphan). No broad pkill against the shared binary path (which would also kill a real session).
  • A new, gated src/main-process edit. The openPaths filter is the one core change here (alongside the pre-existing remote-debugging-port line) — a small upstream merge surface, accepted because zero-flash project isolation is impossible from owned/dev code alone. It is inert unless TRANQUIL_VERIFY is set, so it cannot affect production or a plain yarn 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-dir sidesteps it).
  • Re-evaluate if the app moves off the remote-debugging-port switch, or if a headless agent environment stops exporting ELECTRON_RUN_AS_NODE and a different launch blocker appears.