Agent-Shell Dev Launch — Delivery Notes
Delivery summary for making
yarn startlaunch Tranquil from an agent shell so UI work can be GUI-verified live. For the decision record read ADR-0014; for the launch/inspect workflow read the Agent Debugging guide. This note says what changed and how it was proven.
Status
Implemented and verified in-app.
| Piece | State |
|---|---|
Strip ELECTRON_RUN_AS_NODE from the child env in tranquil-client/scripts/dev.js | ✅ done |
Forward extra CLI args + propagate SIGINT/SIGTERM to Electron in scripts/dev.js | ✅ done |
| “Launch a dev instance” section added to the Agent Debugging guide | ✅ done |
| ADR-0014 | ✅ written |
| End-to-end launch + CDP verification + clean teardown | ✅ passed |
The symptom
yarn start from the agent shell exited in ~0.3s, printing:
.../Electron.app/Contents/MacOS/Electron: bad option: --no-sandbox
.../Electron.app/Contents/MacOS/Electron: bad option: --enable-logging No window ever opened, so a whole batch of shipped-but-unseen UI work (ADR-0012, ADR-0013, the Settings default tab) could never be looked at.
Root cause (proven, not guessed)
The bad option: prefix is Node’s unknown-flag format, not Chromium’s — so the Electron
binary was running as a plain Node interpreter and choking on the Chromium flags. That happens when ELECTRON_RUN_AS_NODE=1 is present, which the VSCode extension host (the shell the agent runs
inside) exports. scripts/dev.js spawned Electron with env: { ...process.env, … }, inheriting it.
Confirmed by asking the same binary its version with and without the variable:
| command | reported version | meaning |
|---|---|---|
electron --version (agent shell) | v20.16.0 | Node’s version → running as Node |
env -u ELECTRON_RUN_AS_NODE electron --version | v30.5.1 | Electron’s own version → running as Electron |
The fix
One file — tranquil-client/scripts/dev.js. Build the child env explicitly
and delete the variable before spawning:
const childEnv = { ...process.env };
// An agent/IDE shell (e.g. the VSCode extension host) may export
// ELECTRON_RUN_AS_NODE=1; inherited by the spawn it makes Electron run as Node.
delete childEnv.ELECTRON_RUN_AS_NODE;
childEnv.NODE_PATH = path.join(__dirname, '..', 'node_modules');
childEnv.ATOM_RESOURCE_PATH = path.join(__dirname, '..'); Deleting an unset variable is a no-op, so a normal terminal is unaffected.
No upstream merge surface. scripts/dev.js (plural scripts/) is Tranquil-owned — it doesn’t
exist on master or in upstream Pulsar, which keeps its build scripts in script/ (singular) with
no dev.js. An upstream/master merge only touches script/…, a different directory, so this adds
zero conflict surface and honours the “never modify core Pulsar” rule. (src/buffered-node-process.js still sets ELECTRON_RUN_AS_NODE=1 on its own child env to run helper Node processes — correct and
untouched.)
How it was verified
pgrepconfirmed no stale instance and:9222was unbound.yarn start(background) → a live Electron process that did not exit in 0.3s, with the Chromium flags accepted.curl http://localhost:9222/json/version→Electron/30.5.1,User-Agent: … Tranquil/2.4.0.- Connected with
puppeteer-core(browserURL: 'http://localhost:9222', notbrowserWSEndpoint; neverbrowser.newPage()— it throws on Electron) and evaluated the live rendereratomglobal: version2.4.0, active themetranquil-business-dark, all owned packages active, panes restored across multiple windows.
The drive/inspect harness needs no repo code — puppeteer-core is already a dependency and CDP is
already enabled (start.js appends remote-debugging-port 9222). The workflow
lives in the Agent Debugging guide, not in the app.
Isolated launch & clean teardown — yarn verify
The whole isolated launch is packaged as yarn verify (scripts/verify.js). It isolates three
things so the app never pollutes the real environment or touches real source, then prints status and
holds the app open:
--user-data-dir=<tmp>(own window-state + single-instance lock, dodging the handoff) and a throwawayATOM_HOME=<tmp>seeded from~/.tranquil(own config/session; theme + packages preserved). Earlier launches that used the real~/.tranquiloverwrote its saved window layout — the throwaway home prevents that.- Opens
tranquil-test-suite, nottranquil-client. Electron forces.(the app dir) as its first positional and Atom otherwise opens it as a project too, so the window would start with both roots.verify.jssetsTRANQUIL_VERIFY=1, which activates a gated filter insrc/main-process/atom-application.js(openPaths) that drops the resource-path (tranquil-client) root before any window opens — zero flash. (Post-loadsetPathswas tried first but left tranquil-client mounted for ~1s during load; the gated filter is the only reliable zero-flash fix. It’s inert without the env var, so a plainyarn startstill opens tranquil-client.) scripts/dev.jspropagatesSIGINT/SIGTERMto the Electron child (andverify.jsdoes the same) → stopping the launcher tears down the whole app. Previouslycleanup()justprocess.exit(0)‘d the Node wrapper and orphaned the GUI, forcing the broadpkill.
Verified end-to-end: yarn verify → roots = [tranquil-test-suite] only (no flash), theme tranquil-business-dark preserved, real ~/.tranquil untouched; a plain yarn start still opens tranquil-client (filter inert); then an 8-process instance stopped via the launcher’s SIGTERM left zero orphans and released :9222. If a fallback kill is ever required, match the unique dir
(pkill -f 'user-data-dir=.*tranquil-verify'), never the shared binary path.
Caveats (all in the guide)
- Screenshots can hang.
page.screenshot()/Page.captureScreenshotcan block forever on a webview-heavy renderer — prefer layout facts (getBoundingClientRect,getComputedStyle) viaevaluate, and lean on the human for visual judgement. - Node ≥18 for the CDP client (puppeteer
connectneeds globalfetch).
Follow-up
The ADR-0012 / ADR-0013 UI backlog can now get its pending live GUI pass through this workflow.