feat(cli): add embeddable node lifecycle
Summary
- publish
agent-relay/node-embeddedwith structuredstartEmbeddedNode,downEmbeddedNode, andstatusEmbeddedNoderesults - reuse the existing
runUpCommand/runDownCommand/runStatusCommandlifecycle implementation throughCoreDependenciesinjection - replace process exit with
BrokerLifecycleExitError, capture lifecycle signal handlers behind an idempotent handle, isolate the host environment, and route CLI/provider output without global console or inherited Python-provider stdio ownership - bump the published CLI, config, and harness-driver packages to 10.3.0 and document the API
Lifecycle ownership
Embedded startup supports foreground mode only. Any truthy background value returns a structured code-2 failure. The value forwarded to runUpCommand is authoritatively set to false after spreading caller options, so stateful JavaScript getters/proxies cannot reach the detached-spawn path. The host forwards its own signals through handle.stop(); Relay does not install SIGINT/SIGTERM listeners on the host.
Exit-site audit
All 14 deps.exit sites were traced on the final lifecycle implementation: 11 in runUpCommand, 0 in runDownCommand, and 3 in runStatusCommand.
- Background-only
runUpCommandexits at current lines 1129, 1133, 1148, 1195, and 1201 are structurally unreachable because embedded startup always forwards a plainbackground: false. Static truthy values and false-then-true accessors were independently probed with zero detached spawns. - Foreground exits at lines 1235 and 1314, plus their shared outer catch at 1366, receive the injected
BrokerLifecycleExitError; host-process survival was independently exercised through both nested chains. - The second-SIGINT exit at line 1336 is unreachable through the public idempotent handle/no-global-listener API, but its captured handler closes over the same injected exit. First-SIGINT and SIGTERM exits at lines 1342 and 1349 are invoked only through the returned handle and convert to structured success.
runStatusCommandexits at lines 1520, 1531, and 1563 all receive the injected dependencies and return structured failures; stopped-after-wait, live-PID/API-unavailable, and invalid-wait paths were exercised.
A static regression test pins the 11 + 0 + 3 count and bans raw process.exit, SIGINT/SIGTERM process.on, direct console.*, and inherited stdio from the embedded lifecycle implementation.
Verification
npm run typechecknpm --prefix packages/cli run lint(passes with pre-existing warnings only)- full Vitest suite: 98 files passed, 1 skipped; 1234 tests passed, 11 skipped
- independent adversarial review: host survival, signal/console/listener isolation, static and stateful background bypasses, env isolation, stderr classification, shutdown failures/state retention, and nested-CWD marker discovery
npm --prefix packages/cli run build- installed-workspace subpath import smoke for
agent-relay/node-embedded - CLI/config package dry-runs at 10.3.0; CLI tarball contains the new JS and declarations
npm ci --dry-run --ignore-scripts
The CodeQL regex annotation is pre-existing at blame commit 83a19438ab and outside this PR's changed hunks.
No publish or deploy performed.