Docs

Agent-Shell Dev Launch — Delivery Notes

Delivery summary for making yarn start launch 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.

PieceState
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:

commandreported versionmeaning
electron --version (agent shell)v20.16.0Node’s version → running as Node
env -u ELECTRON_RUN_AS_NODE electron --versionv30.5.1Electron’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

  1. pgrep confirmed no stale instance and :9222 was unbound.
  2. yarn start (background) → a live Electron process that did not exit in 0.3s, with the Chromium flags accepted.
  3. curl http://localhost:9222/json/versionElectron/30.5.1, User-Agent: … Tranquil/2.4.0.
  4. Connected with puppeteer-core (browserURL: 'http://localhost:9222', not browserWSEndpoint; never browser.newPage() — it throws on Electron) and evaluated the live renderer atom global: version 2.4.0, active theme tranquil-business-dark, all owned packages active, panes restored across multiple windows.

The drive/inspect harness needs no repo codepuppeteer-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 throwaway ATOM_HOME=<tmp> seeded from ~/.tranquil (own config/session; theme + packages preserved). Earlier launches that used the real ~/.tranquil overwrote its saved window layout — the throwaway home prevents that.
  • Opens tranquil-test-suite, not tranquil-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.js sets TRANQUIL_VERIFY=1, which activates a gated filter in src/main-process/atom-application.js (openPaths) that drops the resource-path (tranquil-client) root before any window opens — zero flash. (Post-load setPaths was 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 plain yarn start still opens tranquil-client.)
  • scripts/dev.js propagates SIGINT/SIGTERM to the Electron child (and verify.js does the same) → stopping the launcher tears down the whole app. Previously cleanup() just process.exit(0)‘d the Node wrapper and orphaned the GUI, forcing the broad pkill.

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.captureScreenshot can block forever on a webview-heavy renderer — prefer layout facts (getBoundingClientRect, getComputedStyle) via evaluate, and lean on the human for visual judgement.
  • Node ≥18 for the CDP client (puppeteer connect needs global fetch).

Follow-up

The ADR-0012 / ADR-0013 UI backlog can now get its pending live GUI pass through this workflow.