feat(slice-22): --openrig-home flag for multi-instance daemon state

release-0.3.1 Slice 22 founder-walk-vm-populated-env Phase B — adds
`--openrig-home <path>` to the CLI lifecycle so the operator can run
two daemons in one Tart preview VM with fully isolated state per OQ-4
(a) (separate OPENRIG_HOME dirs per daemon).

Existing daemon multi-instance support was env-driven (OPENRIG_PORT,
OPENRIG_HOST, OPENRIG_DB, OPENRIG_HOME) and the CLI already had
--port/--host/--db flags. This commit adds the matching
--openrig-home flag end-to-end + tests, mirroring the
user-explicit-vs-default pattern from the auth-bearer-tailscale-trust
slice (host flag): omit OPENRIG_HOME from the spawn env when the opt
is undefined (so the shell-set value passes through baseEnv); set it
explicitly when the operator opts in.

Audits BOTH startDaemon callers per banked
feedback_lifecycle_exporter_audit_all_callers (the lesson from the
auth-bearer slice's two-round forward-fix dance):
- packages/cli/src/commands/daemon.ts — `rig daemon start
  --openrig-home <path>`
- packages/cli/src/commands/up.ts — `rig up <source>
  --openrig-home <path>`
Both threaded through StartOptions → buildDaemonEnv → spawn env.

Tests (TDD red→green with distinct-value discriminators per banked
feedback_poc_regression_must_discriminate):
- openrigHome undefined → OPENRIG_HOME omitted from spawn env
- openrigHome defined → OPENRIG_HOME set to value
- baseEnv has OPENRIG_HOME, opt undefined → shell value preserved
- baseEnv has OPENRIG_HOME=/shell/blank, opt=/opt/populated → opt wins
- daemon-lifecycle.test.ts: 56/56 green (was 52; +4 new)
- Full CLI+daemon sweep (env-vars cleaned): 4270/4272 (one
  environmental pre-existing fail unchanged from prior slices)
- CLI tsc clean

No new SC-29 EXCEPTIONS: OPENRIG_HOME pre-existed; this commit is
purely additive CLI ergonomics on an existing env contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-11 04:37:32 -07:00
1 parent 3c75cb2282
commit 3d9a6f6190
4 files changed
+91 -2

No files matched your search

+10 -1
View File
@@ -77,6 +77,11 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
.option("--port <port>", "Port to listen on")
.option("--host <host>", "Host to bind on")
.option("--db <path>", "Database path")
// Slice 22 founder-walk-vm-populated-env — operator-explicit
// OPENRIG_HOME directory. Enables running two daemons in the same
// VM with fully isolated state (e.g., blank-slate vs populated
// preview environments).
.option("--openrig-home <path>", "OPENRIG_HOME directory for daemon state (transcripts, plugins, specs, etc.)")
// V0.3.1 slice 05 kernel-rig-as-default — skip the kernel auto-boot
// path. Used by test fixtures, headless CI, and operators who want
// a no-kernel daemon for ad-hoc topology work. The daemon proceeds
@@ -91,7 +96,7 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
// weaker "daemon-ready". Default 60s; override with --wait-for-kernel-ms.
.option("--wait-for-kernel", "After daemon binds, also wait for kernel-agent readiness (default timeout 60s)")
.option("--wait-for-kernel-ms <ms>", "Override --wait-for-kernel timeout in milliseconds")
.action(async (opts: { port?: string; host?: string; db?: string; kernel?: boolean; waitForKernel?: boolean; waitForKernelMs?: string }) => {
.action(async (opts: { port?: string; host?: string; db?: string; openrigHome?: string; kernel?: boolean; waitForKernel?: boolean; waitForKernelMs?: string }) => {
try {
const { ConfigStore } = await import("../config-store.js");
const { SystemPreflight } = await import("../system-preflight.js");
@@ -147,6 +152,10 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
// OPENRIG_NO_KERNEL env var so the daemon's kernel-boot
// check in startup.ts honors the flag.
skipKernelBoot: skipKernel,
// Slice 22 founder-walk-vm-populated-env — operator-explicit
// OPENRIG_HOME directory (omit when undefined so the daemon
// falls through to its built-in default ~/.openrig).
openrigHome: opts.openrigHome,
},
getDeps(),
);
+8 -1
View File
@@ -31,7 +31,10 @@ Examples:
.option("--target <root>", "Target root directory for package installation (.rigbundle only; does not change agent cwd)")
.option("--existing", "Treat <source> as an existing rig name; bypass library-spec name resolution")
.option("--json", "JSON output for agents")
.action(async (source: string, opts: { plan?: boolean; yes?: boolean; cwd?: string; target?: string; existing?: boolean; json?: boolean }) => {
// Slice 22 founder-walk-vm-populated-env — operator-explicit
// OPENRIG_HOME for the auto-started daemon's state directory.
.option("--openrig-home <path>", "OPENRIG_HOME directory for the auto-started daemon (state, transcripts, plugins, specs)")
.action(async (source: string, opts: { plan?: boolean; yes?: boolean; cwd?: string; target?: string; existing?: boolean; json?: boolean; openrigHome?: string }) => {
const deps = getDepsF();
// Run preflight before auto-start
@@ -96,6 +99,10 @@ Examples:
transcriptsLines: resolvedConfig?.transcripts.lines,
transcriptsPollIntervalSeconds: resolvedConfig?.transcripts.pollIntervalSeconds,
workspaceRoot: resolvedConfig?.workspace.root,
// Slice 22 founder-walk-vm-populated-env — operator-explicit
// OPENRIG_HOME (omit when undefined so the daemon's default
// homedir-relative ~/.openrig still applies).
openrigHome: opts.openrigHome,
}, deps.lifecycleDeps);
status = await getDaemonStatus(deps.lifecycleDeps);
} catch (err) {
+19
View File
@@ -44,6 +44,10 @@ export interface StartOptions {
// Exposed at the CLI as `rig daemon start --no-kernel`; projected
// into the daemon process env as OPENRIG_NO_KERNEL.
skipKernelBoot?: boolean;
// Slice 22 founder-walk-vm-populated-env — operator-explicit
// OPENRIG_HOME directory for the daemon child process. Enables two
// daemons in one VM to have fully isolated state (OQ-4 (a)).
openrigHome?: string;
}
export interface LifecycleDeps {
@@ -269,6 +273,17 @@ export function buildDaemonEnv(
// V0.3.1 slice 05 — projected via OPENRIG_NO_KERNEL env var so
// the daemon's startup.ts kernel-boot path honors the flag.
skipKernelBoot?: boolean;
/**
* Slice 22 founder-walk-vm-populated-env: operator-explicit
* OPENRIG_HOME directory. When defined, sets OPENRIG_HOME in the
* child process so the daemon writes its state (transcripts,
* plugins, context-packs, agent-images, specs) under the chosen
* directory. When undefined, the env-passthrough loop preserves
* whatever OPENRIG_HOME the operator's shell already had (their
* shell-level opt-in still wins). Enables two daemons in the same
* VM to have fully isolated state per OQ-4 (a).
*/
openrigHome?: string;
},
): Record<string, string> {
const env: Record<string, string> = {};
@@ -297,6 +312,9 @@ export function buildDaemonEnv(
if (opts.transcriptsPollIntervalSeconds !== undefined) {
env["OPENRIG_TRANSCRIPTS_POLL_INTERVAL_SECONDS"] = String(opts.transcriptsPollIntervalSeconds);
}
if (opts.openrigHome !== undefined) {
env["OPENRIG_HOME"] = opts.openrigHome;
}
if (opts.skipKernelBoot) {
env["OPENRIG_NO_KERNEL"] = "1";
}
@@ -361,6 +379,7 @@ export async function startDaemon(opts: StartOptions, deps: LifecycleDeps): Prom
transcriptsLines: opts.transcriptsLines,
transcriptsPollIntervalSeconds: opts.transcriptsPollIntervalSeconds,
skipKernelBoot: opts.skipKernelBoot,
openrigHome: opts.openrigHome,
}),
stdio: ["ignore", logFd, logFd],
detached: true,
@@ -981,6 +981,60 @@ describe("buildDaemonEnv", () => {
});
});
// Slice 22 founder-walk-vm-populated-env: --openrig-home / OPENRIG_HOME
// flag flows through buildDaemonEnv so two daemons in one VM can have
// fully isolated state directories (OQ-4 resolved to (a) fully isolated).
describe("slice 22: openrigHome (OPENRIG_HOME) env projection", () => {
it("omits OPENRIG_HOME when openrigHome opt is undefined (shell-env passthrough wins)", () => {
const baseEnv: Record<string, string> = { HOME: "/Users/tester", PATH: "/usr/bin" };
const result = buildDaemonEnv(baseEnv, {
port: 7433,
db: "/tmp/test.db",
});
expect(result["OPENRIG_HOME"]).toBeUndefined();
});
it("sets OPENRIG_HOME when openrigHome opt is defined (operator opt-in)", () => {
const baseEnv: Record<string, string> = { HOME: "/Users/tester", PATH: "/usr/bin" };
const result = buildDaemonEnv(baseEnv, {
port: 7433,
db: "/tmp/test.db",
openrigHome: "/Users/example/.openrig-blank",
});
expect(result["OPENRIG_HOME"]).toBe("/Users/example/.openrig-blank");
});
it("preserves shell-set OPENRIG_HOME through baseEnv when opt is undefined", () => {
const baseEnv: Record<string, string> = {
HOME: "/Users/tester",
PATH: "/usr/bin",
OPENRIG_HOME: "/Users/example/.openrig-shell",
};
const result = buildDaemonEnv(baseEnv, {
port: 7433,
db: "/tmp/test.db",
});
expect(result["OPENRIG_HOME"]).toBe("/Users/example/.openrig-shell");
});
it("opt-set OPENRIG_HOME overrides shell-set baseEnv value (distinct-value discriminator)", () => {
// Per banked feedback_poc_regression_must_discriminate — distinct
// values so we can prove the override branch fired and didn't pass
// vacuously through baseEnv-passthrough.
const baseEnv: Record<string, string> = {
HOME: "/Users/tester",
PATH: "/usr/bin",
OPENRIG_HOME: "/Users/example/.openrig-shell-blank",
};
const result = buildDaemonEnv(baseEnv, {
port: 7433,
db: "/tmp/test.db",
openrigHome: "/Users/example/.openrig-opt-populated",
});
expect(result["OPENRIG_HOME"]).toBe("/Users/example/.openrig-opt-populated");
});
});
describe("V1 pre-release CLI/daemon Item 1 — transcript rotation tunables projection", () => {
it("projects transcriptsLines + transcriptsPollIntervalSeconds into OPENRIG_* env vars when provided", () => {
const baseEnv: Record<string, string> = { HOME: "/Users/tester", PATH: "/usr/bin" };