fix(daemon): shipped startup files and resources follow the running install (#261) (#269) (#281)

* fix(daemon): deliver built-in startup files from the running install (#261)

Built-in startup files (CULTURE-default.md, openrig-start.md and the two
onboarding files) are stored per seat as absolute paths inside the install
that created the seat. After an upgrade removes that install (mise and other
versioned managers), every fresh launch rolled back with ENOENT, and restore
blocked on the missing required file. While the old install remained, seats
kept receiving the old version's shipped guidance.

Fresh launch (SeatLifecycleService.readStartupContext) and restore
(pre-validation and replay) now re-anchor a stored file to the running
daemon's assets when it is a recognized built-in: one of the four logical
names, stored at exactly <ownerRoot>/<known relative path>, with ownerRoot a
daemon/assets directory. Custom rig and agent files, including a same-named
CULTURE-default.md, are delivered exactly as stored. Required/appliesOn/
delivery metadata is unchanged, a missing current built-in still fails
honestly, and a same-native resume still replays nothing.

Refs #261

* fix(daemon): project shipped-spec resources from the running install (#261)

Kernel and library agents ship inside the install, so their stored projection
entries (agent guidance, shared runtime settings/MCP fragments) also point
into the install that created the rig. After that install is removed, a fresh
launch still rolled back in projection, before startup-file delivery.

Fresh launch and restore (pre-validation drift checks and replay) now
re-anchor a stored projection entry to the running install's daemon/specs when
both its sourcePath and its resource path lie under the same OpenRig install's
daemon/specs (packaged @openrig/cli/daemon/specs or a packages/daemon/specs
checkout). A resource stored elsewhere, such as the openrig-core plugin under
~/.openrig/plugins, user specs, or a folder merely named daemon/specs, is
projected exactly as stored. Identifiers, category, target, merge behavior and
the existing skill exclusion are unchanged; a resource missing from the
running version keeps the existing failure or warning.

Refs #261

* fix(daemon): own-key built-in lookup; restore-check judges the running paths (#261)

The built-in name lookup used a plain object, so a custom startup file named
constructor, toString or __proto__ matched an inherited key and fresh launch
threw on a valid seat (review finding on be599a64). Use a Map.

RestoreCheckService.checkStartupContext checked and reported the stored
paths directly, so a seat whose old install was removed showed missing
required inputs that replay no longer uses, and named stale paths in its
evidence and remediation. It now applies the same resolvers to the inspected
context and uses the resolved paths in both the predicates and the reported
text. The red/yellow rule, genuinely missing current or custom files, and
missing/malformed/probe-error handling are unchanged. The route's context
reader passes ownerRoot and sourcePath through so the resolvers can apply.

Refs #261

* fix(daemon): shipped-spec startup files follow the running install (#261)

Kernel and library seats also store their rig culture and agent startup files
(culture/CULTURE.md, guidance/role.md, startup/context.md) as absolute paths
inside the install's daemon/specs. With the projection fixed, a kernel created
by a removed install still rolled back on the culture file, and its post-launch
role/context would have followed.

The startup-file resolver now also re-anchors a stored startup file whose
ownerRoot and absolutePath both lie under the same recognized install's
daemon/specs, using the same mapping as projection entries. It applies through
fresh launch (pre- and post-launch delivery), restore replay and
pre-validation, and restore-check. Custom, external and plugin paths, and all
required/applicability/delivery metadata, are unchanged; a file missing from
the running version still fails or warns honestly.

Refs #261

* fix(daemon): re-anchor dev-checkout paths only when missing (#261)

A daemon running from one install remapped every recognized shipped path,
including ones stored from a developer checkout (packages/daemon/...). Those
files can be newer than, or absent from, the running install, so a present
file was remapped to a missing running path and the launch rolled back
(review finding, confirmed by both reviewers).

Packaged installs (@openrig/cli/daemon/...) still re-anchor even while the
stored file exists, so seats never keep an old version's shipped content.
Dev-checkout files re-anchor only when the stored file is missing. Recognition
is limited to those two layouts for built-in assets too. Each consumer passes
its own existence check (restore fsOps, restore-check deps; fresh launch uses
the filesystem).

Refs #261

* fix(daemon): leave legacy persisted startup shapes as stored (#261)

Snapshots and contexts persisted by older versions can lack ownerRoot on
startup files or sourcePath on projection entries. The #261 resolvers called
path.resolve on those fields, which threw, so restore reported restore_error
instead of its real outcome; restore pre-validation walks projection entries
even for an exact resume (CI: restore-honesty-d1-d6 D6a, 4 failures).

Entries without a string root or path are now returned exactly as stored,
which is their pre-#261 behavior.

Refs #261

---------

Co-authored-by: v-openrig-build <[email protected]>
This commit is contained in:
Mike Schwarzandv-openrig-build authored and GitHub committed 2026-10-01 00:06:13 -07:00
1 parent d1ba4ee71e
commit 07dab0761d
9 files changed
+646 -11

No files matched your search

@@ -0,0 +1,128 @@
// Startup files and projection resources that ship with OpenRig are stored per seat as
// absolute paths inside the install that created the seat (rigspec-instantiator resolves
// them from import.meta.dirname, or from a rig spec that lives in the install). After an
// upgrade that removes or moves that install, the stored paths go stale (#261). Delivery
// consumers and restore-check re-anchor them to the RUNNING install, so seats get this
// version's shipped content:
// - the four built-in startup files under daemon/assets (by logical name and known path);
// - startup files and projection resources under an install's daemon/specs (the kernel and
// library rigs and agents), when both their root and their file lie in that same specs root.
// A packaged install (@openrig/cli/daemon/...) is re-anchored even while it still exists, so
// seats never keep an old version's shipped content. A dev checkout (packages/daemon/...) is
// re-anchored only when the stored file is missing: its files may be newer than, or absent
// from, the running install. Custom, user, plugin and other external paths are never rewritten.
import { existsSync } from "node:fs";
import nodePath from "node:path";
/** Logical name -> path relative to the daemon assets root, as rigspec-instantiator produces them. */
const BUILTIN_STARTUP_FILES: ReadonlyMap<string, string> = new Map([
["CULTURE-default.md", "guidance/CULTURE-default.md"],
["openrig-start.md", "guidance/openrig-start.md"],
["openrig-onboarding-01.md", "onboarding/01-world-and-purpose.md"],
["openrig-onboarding-02.md", "onboarding/02-self-and-competent-action.md"],
]);
type Exists = (path: string) => boolean;
type InstallLayout = "packaged" | "dev-checkout";
/** The running daemon's assets root (packages/daemon/assets, or <cli>/daemon/assets when packaged). */
export function runningBuiltinAssetsRoot(): string {
return nodePath.resolve(import.meta.dirname, "../../assets");
}
/** The running daemon's shipped specs root (packages/daemon/specs, or <cli>/daemon/specs when packaged). */
export function runningShippedSpecsRoot(): string {
return nodePath.resolve(import.meta.dirname, "../../specs");
}
/** The layout of the OpenRig install whose daemon/<dir> root is `parts[0..i+1]`, if recognized. */
function layoutAt(parts: string[], i: number): InstallLayout | null {
if (i >= 2 && parts[i - 1] === "cli" && parts[i - 2] === "@openrig") return "packaged";
if (i >= 1 && parts[i - 1] === "packages") return "dev-checkout";
return null;
}
/** The recognized OpenRig install's daemon/specs root containing `p`, with its layout. */
function shippedSpecsRootOf(p: string): { root: string; layout: InstallLayout } | null {
const parts = nodePath.resolve(p).split(nodePath.sep);
for (let i = parts.length - 2; i >= 1; i--) {
if (parts[i] !== "daemon" || parts[i + 1] !== "specs") continue;
const layout = layoutAt(parts, i);
if (layout) return { root: parts.slice(0, i + 2).join(nodePath.sep), layout };
}
return null;
}
/** A stored file from a dev checkout is followed only when it is gone; a packaged one always. */
function shouldReanchor(layout: InstallLayout, storedFile: string, exists: Exists): boolean {
return layout === "packaged" || !exists(storedFile);
}
/** Map a (root, file) pair stored under one install's daemon/specs onto the running specs root,
* preserving both relative paths. Null when the root is not in a recognized install's specs,
* the file lies outside that same specs root, or a dev-checkout file is still present. */
function mapUnderShippedSpecs(root: string, file: string, specsRoot: string, exists: Exists): { root: string; file: string } | null {
const stored = shippedSpecsRootOf(root);
if (!stored) return null;
const resolvedFile = nodePath.resolve(file);
if (!resolvedFile.startsWith(stored.root + nodePath.sep)) return null;
if (!shouldReanchor(stored.layout, file, exists)) return null;
return {
root: nodePath.join(specsRoot, nodePath.relative(stored.root, nodePath.resolve(root))),
file: nodePath.join(specsRoot, nodePath.relative(stored.root, resolvedFile)),
};
}
/**
* Re-anchor one stored startup file to the running install when it ships with OpenRig:
* - a built-in: its logical name is one of the four built-ins, its stored absolutePath is
* exactly <ownerRoot>/<known relative path>, and ownerRoot is a recognized install's
* daemon/assets directory; or
* - a shipped-spec file: its ownerRoot and absolutePath both lie under the same recognized
* install's daemon/specs (for example the kernel rig culture and agent role/startup files).
* Dev-checkout files are re-anchored only when missing. Anything else (custom rig/agent files,
* user content with a matching basename) is returned unchanged. Required, applicability,
* delivery and every other field are preserved.
*/
export function reanchorBuiltinStartupFile<T extends { path: string; absolutePath: string; ownerRoot: string }>(
file: T,
assetsRoot: string = runningBuiltinAssetsRoot(),
specsRoot: string = runningShippedSpecsRoot(),
exists: Exists = existsSync,
): T {
// Persisted contexts from older versions may lack ownerRoot/path; leave them exactly as stored.
if (typeof file.ownerRoot !== "string" || typeof file.absolutePath !== "string") return file;
const relative = typeof file.path === "string" ? BUILTIN_STARTUP_FILES.get(file.path) : undefined;
if (relative) {
const storedRoot = nodePath.resolve(file.ownerRoot);
const parts = storedRoot.split(nodePath.sep);
const i = parts.length - 2;
const layout = parts[i] === "daemon" && parts[i + 1] === "assets" ? layoutAt(parts, i) : null;
if (layout && nodePath.resolve(file.absolutePath) === nodePath.join(storedRoot, relative)) {
return shouldReanchor(layout, file.absolutePath, exists)
? { ...file, absolutePath: nodePath.join(assetsRoot, relative), ownerRoot: assetsRoot }
: file;
}
}
const mapped = mapUnderShippedSpecs(file.ownerRoot, file.absolutePath, specsRoot, exists);
return mapped ? { ...file, ownerRoot: mapped.root, absolutePath: mapped.file } : file;
}
/**
* Re-anchor one stored projection entry to the running install when it is a shipped-spec
* resource: both its sourcePath and its resource absolutePath lie under the same recognized
* OpenRig install's daemon/specs root (dev-checkout resources only when missing). A resource
* stored elsewhere (a plugin under ~/.openrig/plugins, user specs, custom paths) is returned
* unchanged even when its sourcePath is a shipped spec. Identifiers, category, target and
* merge behavior are preserved.
*/
export function reanchorShippedProjectionEntry<T extends { sourcePath: string; absolutePath: string }>(
entry: T,
specsRoot: string = runningShippedSpecsRoot(),
exists: Exists = existsSync,
): T {
// Persisted entries from older versions may lack sourcePath; leave them exactly as stored.
if (typeof entry.sourcePath !== "string" || typeof entry.absolutePath !== "string") return entry;
const mapped = mapUnderShippedSpecs(entry.sourcePath, entry.absolutePath, specsRoot, exists);
return mapped ? { ...entry, sourcePath: mapped.root, absolutePath: mapped.file } : entry;
}
@@ -2,6 +2,7 @@ import { existsSync, accessSync, constants } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join, relative } from "node:path";
import { getCompatibleOpenRigPath } from "../openrig-compat.js";
import { reanchorBuiltinStartupFile, reanchorShippedProjectionEntry } from "./builtin-startup-files.js";
// --- Types ---
@@ -112,12 +113,14 @@ export interface StartupContextResolvedFile {
required: boolean;
path?: string | null;
deliveryHint?: string | null;
ownerRoot?: string | null;
}
export interface StartupContextProjectionEntry {
absolutePath: string;
effectiveId?: string | null;
category?: string | null;
sourcePath?: string | null;
}
export type StartupContextProbeResult =
@@ -851,7 +854,21 @@ export class RestoreCheckService {
break;
}
const startupContext = probe;
// #261: inspect (and report) the paths replay will actually use — recognized built-in startup
// files and shipped-spec projection resources follow the running install.
const startupContext = {
...probe,
resolvedStartupFiles: probe.resolvedStartupFiles.map((file) => (
typeof file.path === "string" && typeof file.ownerRoot === "string"
? reanchorBuiltinStartupFile({ ...file, path: file.path, ownerRoot: file.ownerRoot }, undefined, undefined, this.deps.exists)
: file
)),
projectionEntries: probe.projectionEntries.map((entry) => (
typeof entry.sourcePath === "string"
? reanchorShippedProjectionEntry({ ...entry, sourcePath: entry.sourcePath }, undefined, this.deps.exists)
: entry
)),
};
const missingRequired = startupContext.resolvedStartupFiles.filter((file) => file.required && !this.deps.exists(file.absolutePath));
const missingOptional = startupContext.resolvedStartupFiles.filter((file) => !file.required && !this.deps.exists(file.absolutePath));
const missingProjectionEntries = startupContext.projectionEntries.filter((entry) => !this.deps.exists(entry.absolutePath));
@@ -17,6 +17,7 @@ import type { CodexResumeAdapter } from "../adapters/codex-resume.js";
import type { PiResumeAdapter } from "../adapters/pi-resume.js";
import type { TranscriptStore } from "./transcript-store.js";
import { assessNativeResumeProbe } from "./native-resume-probe.js";
import { reanchorBuiltinStartupFile, reanchorShippedProjectionEntry } from "./builtin-startup-files.js";
import type {
RestoreOutcome,
RestoreRigResult,
@@ -697,7 +698,9 @@ export class RestoreOrchestrator {
else consumesReplay = true;
}
for (const file of consumesReplay ? startupCtx.resolvedStartupFiles ?? [] : []) {
for (const storedFile of consumesReplay ? startupCtx.resolvedStartupFiles ?? [] : []) {
// Validate the file replay will actually deliver (#261: built-ins follow the running install).
const file = reanchorBuiltinStartupFile(storedFile, undefined, undefined, exists);
if (!file.required) {
if (this.pathLike(file.absolutePath) && !exists(file.absolutePath)) {
warnings.push(`Restore pre-validation: optional startup file missing for ${node.logicalId}: ${file.absolutePath}`);
@@ -739,7 +742,8 @@ export class RestoreOrchestrator {
// gate from blocking the attempt entirely. Missing REQUIRED startup
// files and genuinely-fatal blockers (malformed snapshot, missing nodes)
// stay critical above.
for (const entry of startupCtx.projectionEntries ?? []) {
for (const storedEntry of startupCtx.projectionEntries ?? []) {
const entry = reanchorShippedProjectionEntry(storedEntry, undefined, exists);
if (this.pathLike(entry.sourcePath) && !exists(entry.sourcePath)) {
warnings.push(`projection_drift: source root missing for ${node.logicalId}: ${entry.sourcePath} (projection will be skipped at startup; session continuity is unaffected)`);
}
@@ -1305,8 +1309,9 @@ export class RestoreOrchestrator {
if (adapter) {
// Prefilter: check which files/entries still exist
const existsFn = opts.fsOps?.exists ?? (() => true);
const sourceEntries = replayContained ? [] : startupCtx.projectionEntries;
const sourceFiles = replayContained ? [] : startupCtx.resolvedStartupFiles;
const sourceEntries = replayContained ? [] : startupCtx.projectionEntries.map((e) => reanchorShippedProjectionEntry(e, undefined, existsFn));
// #261: recognized built-in startup files follow the running install.
const sourceFiles = replayContained ? [] : startupCtx.resolvedStartupFiles.map((f) => reanchorBuiltinStartupFile(f, undefined, undefined, existsFn));
const sourceActions = replayContained ? [] : startupCtx.startupActions;
const filteredEntries = sourceEntries.filter((e) => {
if (!existsFn(e.absolutePath)) {
@@ -13,6 +13,7 @@ import type { RuntimeAdapter, ResolvedStartupFile } from "./runtime-adapter.js";
import type { ProjectionEntry, ProjectionPlan } from "./projection-planner.js";
import type { StartupAction } from "./types.js";
import { resolveStartupProof } from "./startup-resolver.js";
import { reanchorBuiltinStartupFile, reanchorShippedProjectionEntry } from "./builtin-startup-files.js";
import type { OccupantInvalidator } from "./occupant-invalidator.js";
import { rebindAndVerifyPaneIdentity } from "./seat-attention-reconciler.js";
import { observeSolePane } from "./pane-binding-observation.js";
@@ -847,7 +848,8 @@ export class SeatLifecycleService {
// S04 owns the live ambient skill set. Replaying the older catalog
// selection here could reinstall a skill that work-install removed.
if (raw["category"] === "skill") continue;
entries.push({
// #261: shipped-spec resources follow the running install.
entries.push(reanchorShippedProjectionEntry({
category: raw["category"],
effectiveId: raw["effectiveId"],
sourceSpec: raw["sourceSpec"],
@@ -859,7 +861,7 @@ export class SeatLifecycleService {
...(typeof raw["mergeStrategy"] === "string" ? { mergeStrategy: raw["mergeStrategy"] as ProjectionEntry["mergeStrategy"] } : {}),
...(typeof raw["target"] === "string" ? { target: raw["target"] } : {}),
...(typeof raw["pluginType"] === "string" ? { pluginType: raw["pluginType"] as ProjectionEntry["pluginType"] } : {}),
});
}));
}
const resolvedStartupFiles: ResolvedStartupFile[] = [];
@@ -872,7 +874,8 @@ export class SeatLifecycleService {
|| !isOptionalOneOf(raw["kind"], ["file"] as const)) {
return this.malformedStartupContext(nodeId, "resolved_files_json contains an invalid entry");
}
resolvedStartupFiles.push({
// #261: recognized built-in startup files follow the running install.
resolvedStartupFiles.push(reanchorBuiltinStartupFile({
path: raw["path"],
absolutePath: raw["absolutePath"],
ownerRoot: raw["ownerRoot"],
@@ -880,7 +883,7 @@ export class SeatLifecycleService {
required: raw["required"],
appliesOn: raw["appliesOn"],
...(raw["kind"] === "file" ? { kind: "file" as const } : {}),
});
}));
}
const startupActions: StartupAction[] = [];
@@ -108,6 +108,7 @@ function getStartupContext(db: Database.Database, nodeId: string): StartupContex
required: candidate["required"] !== false,
path: typeof candidate["path"] === "string" ? candidate["path"] : null,
deliveryHint: typeof candidate["deliveryHint"] === "string" ? candidate["deliveryHint"] : null,
ownerRoot: typeof candidate["ownerRoot"] === "string" ? candidate["ownerRoot"] : null,
}];
}),
projectionEntries: projectionEntries.value.flatMap((entry) => {
@@ -118,6 +119,7 @@ function getStartupContext(db: Database.Database, nodeId: string): StartupContex
absolutePath: candidate["absolutePath"].trim(),
effectiveId: typeof candidate["effectiveId"] === "string" ? candidate["effectiveId"] : null,
category: typeof candidate["category"] === "string" ? candidate["category"] : null,
sourcePath: typeof candidate["sourcePath"] === "string" ? candidate["sourcePath"] : null,
}];
}),
};
@@ -0,0 +1,188 @@
// #261: recognized built-in startup files re-anchor to the running install; nothing else moves.
import { describe, it, expect } from "vitest";
import path from "node:path";
import { reanchorBuiltinStartupFile, reanchorShippedProjectionEntry, runningBuiltinAssetsRoot, runningShippedSpecsRoot } from "../src/domain/builtin-startup-files.js";
const RUNNING = "/new/lib/node_modules/@openrig/cli/daemon/assets";
const OLD = "/mise/installs/npm-openrig-cli/0.6.2/node_modules/@openrig/cli/daemon/assets";
const meta = { deliveryHint: "guidance_merge" as const, required: true, appliesOn: ["fresh_start" as const, "restore" as const] };
describe("reanchorBuiltinStartupFile", () => {
it.each([
["CULTURE-default.md", "guidance/CULTURE-default.md"],
["openrig-start.md", "guidance/openrig-start.md"],
["openrig-onboarding-01.md", "onboarding/01-world-and-purpose.md"],
["openrig-onboarding-02.md", "onboarding/02-self-and-competent-action.md"],
])("re-anchors %s from an old packaged install, preserving metadata", (name, rel) => {
const stored = { path: name, absolutePath: `${OLD}/${rel}`, ownerRoot: OLD, ...meta, kind: "file" as const };
expect(reanchorBuiltinStartupFile(stored, RUNNING)).toEqual({ ...stored, absolutePath: `${RUNNING}/${rel}`, ownerRoot: RUNNING });
});
it("re-anchors a built-in stored from a dev checkout (packages/daemon/assets)", () => {
const dev = "/src/openrig/packages/daemon/assets";
const stored = { path: "openrig-start.md", absolutePath: `${dev}/guidance/openrig-start.md`, ownerRoot: dev, ...meta };
expect(reanchorBuiltinStartupFile(stored, RUNNING).absolutePath).toBe(`${RUNNING}/guidance/openrig-start.md`);
});
it("leaves a custom rig file with the same basename unchanged", () => {
const custom = { path: "CULTURE-default.md", absolutePath: "/home/u/rig/CULTURE-default.md", ownerRoot: "/home/u/rig", ...meta };
expect(reanchorBuiltinStartupFile(custom, RUNNING)).toBe(custom);
});
it("leaves a built-in name whose stored path is not the known relative path unchanged", () => {
const odd = { path: "CULTURE-default.md", absolutePath: `${OLD}/custom/CULTURE-default.md`, ownerRoot: OLD, ...meta };
expect(reanchorBuiltinStartupFile(odd, RUNNING)).toBe(odd);
});
it("leaves a known relative path under a non daemon/assets root unchanged", () => {
const root = "/home/u/my-assets";
const user = { path: "CULTURE-default.md", absolutePath: `${root}/guidance/CULTURE-default.md`, ownerRoot: root, ...meta };
expect(reanchorBuiltinStartupFile(user, RUNNING)).toBe(user);
});
it("leaves non-built-in names unchanged even under daemon/assets", () => {
const other = { path: "guidance/CULTURE-default.md", absolutePath: `${OLD}/guidance/CULTURE-default.md`, ownerRoot: OLD, ...meta };
expect(reanchorBuiltinStartupFile(other, RUNNING)).toBe(other);
});
it.each(["constructor", "toString", "__proto__", "hasOwnProperty"])("a custom startup file named %s is returned unchanged (no inherited-key match)", (name) => {
const custom = { path: name, absolutePath: `/home/u/rig/${name}`, ownerRoot: "/home/u/rig", ...meta };
expect(reanchorBuiltinStartupFile(custom, RUNNING)).toBe(custom);
const underAssets = { path: name, absolutePath: `${OLD}/${name}`, ownerRoot: OLD, ...meta };
expect(reanchorBuiltinStartupFile(underAssets, RUNNING)).toBe(underAssets);
});
it("defaults to this daemon's assets root, which holds all four built-ins", () => {
expect(runningBuiltinAssetsRoot()).toBe(path.resolve(import.meta.dirname, "../assets"));
});
});
describe("reanchorShippedProjectionEntry", () => {
const RUN_SPECS = "/new/lib/node_modules/@openrig/cli/daemon/specs";
const OLD_SPECS = "/mise/installs/npm-openrig-cli/0.6.2/node_modules/@openrig/cli/daemon/specs";
const base = { category: "runtime_resource", effectiveId: "shared:claude-default-settings", sourceSpec: "shared",
resourcePath: "runtime/claude-settings.fragment.json", resourceType: "claude_settings_fragment", mergeStrategy: "managed_block", target: ".claude/settings.local.json" };
it("re-anchors a shipped resource (source and resource under an old install's daemon/specs), preserving every other field", () => {
const stored = { ...base, sourcePath: `${OLD_SPECS}/agents/shared`, absolutePath: `${OLD_SPECS}/agents/shared/runtime/claude-settings.fragment.json` };
expect(reanchorShippedProjectionEntry(stored, RUN_SPECS)).toEqual({
...base, sourcePath: `${RUN_SPECS}/agents/shared`, absolutePath: `${RUN_SPECS}/agents/shared/runtime/claude-settings.fragment.json`,
});
});
it("re-anchors kernel agent guidance stored under the rig's own shipped spec directory", () => {
const dir = `${OLD_SPECS}/rigs/launch/kernel/agents/advisor/lead`;
const stored = { ...base, category: "guidance", effectiveId: "role", sourceSpec: "advisor.lead", sourcePath: dir, absolutePath: `${dir}/guidance/role.md` };
expect(reanchorShippedProjectionEntry(stored, RUN_SPECS).absolutePath).toBe(`${RUN_SPECS}/rigs/launch/kernel/agents/advisor/lead/guidance/role.md`);
});
it("re-anchors from a dev checkout (packages/daemon/specs)", () => {
const dev = "/src/openrig/packages/daemon/specs";
const stored = { ...base, sourcePath: `${dev}/agents/shared`, absolutePath: `${dev}/agents/shared/runtime/claude-mcp.fragment.json` };
expect(reanchorShippedProjectionEntry(stored, RUN_SPECS).absolutePath).toBe(`${RUN_SPECS}/agents/shared/runtime/claude-mcp.fragment.json`);
});
it("leaves a plugin projected from ~/.openrig/plugins unchanged even though its sourcePath is a shipped spec", () => {
const plugin = { ...base, category: "plugin", effectiveId: "shared:openrig-core", sourcePath: `${OLD_SPECS}/agents/shared`, absolutePath: "/home/u/.openrig/plugins/openrig-core" };
expect(reanchorShippedProjectionEntry(plugin, RUN_SPECS)).toBe(plugin);
});
it("leaves user spec resources unchanged", () => {
const user = { ...base, sourcePath: "/home/u/rigs/acme/agents/dev", absolutePath: "/home/u/rigs/acme/agents/dev/guidance/role.md" };
expect(reanchorShippedProjectionEntry(user, RUN_SPECS)).toBe(user);
});
it("does not treat a user folder merely named daemon/specs as an OpenRig install", () => {
const lookalike = { ...base, sourcePath: "/home/u/daemon/specs/agents/x", absolutePath: "/home/u/daemon/specs/agents/x/role.md" };
expect(reanchorShippedProjectionEntry(lookalike, RUN_SPECS)).toBe(lookalike);
});
it("defaults to this daemon's shipped specs root", () => {
expect(runningShippedSpecsRoot()).toBe(path.resolve(import.meta.dirname, "../specs"));
});
});
describe("reanchorBuiltinStartupFile — shipped-spec startup files", () => {
const RUN_SPECS = "/new/lib/node_modules/@openrig/cli/daemon/specs";
const OLD_SPECS = "/mise/installs/npm-openrig-cli/0.6.2/node_modules/@openrig/cli/daemon/specs";
const KERNEL = "rigs/launch/kernel";
it("re-anchors the kernel rig culture (pre-launch guidance_merge), preserving metadata", () => {
const stored = { path: "culture/CULTURE.md", absolutePath: `${OLD_SPECS}/${KERNEL}/culture/CULTURE.md`, ownerRoot: `${OLD_SPECS}/${KERNEL}`, ...meta };
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS)).toEqual({
...stored, absolutePath: `${RUN_SPECS}/${KERNEL}/culture/CULTURE.md`, ownerRoot: `${RUN_SPECS}/${KERNEL}`,
});
});
it("re-anchors agent role and startup context (post-launch send_text)", () => {
const agent = `${OLD_SPECS}/${KERNEL}/agents/advisor/lead`;
for (const rel of ["guidance/role.md", "startup/context.md"]) {
const stored = { path: rel, absolutePath: `${agent}/${rel}`, ownerRoot: agent, deliveryHint: "send_text" as const, required: true, appliesOn: ["fresh_start" as const, "restore" as const] };
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS)).toMatchObject({
absolutePath: `${RUN_SPECS}/${KERNEL}/agents/advisor/lead/${rel}`, ownerRoot: `${RUN_SPECS}/${KERNEL}/agents/advisor/lead`, deliveryHint: "send_text",
});
}
});
it("leaves a file outside the shipped specs root unchanged even when its ownerRoot is shipped", () => {
const stored = { path: "notes.md", absolutePath: "/home/u/notes.md", ownerRoot: `${OLD_SPECS}/${KERNEL}`, ...meta };
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS)).toBe(stored);
});
it("leaves user rig culture and look-alike daemon/specs folders unchanged", () => {
const user = { path: "culture/CULTURE.md", absolutePath: "/home/u/rigs/acme/culture/CULTURE.md", ownerRoot: "/home/u/rigs/acme", ...meta };
const lookalike = { path: "culture/CULTURE.md", absolutePath: "/home/u/daemon/specs/r/culture/CULTURE.md", ownerRoot: "/home/u/daemon/specs/r", ...meta };
expect(reanchorBuiltinStartupFile(user, RUNNING, RUN_SPECS)).toBe(user);
expect(reanchorBuiltinStartupFile(lookalike, RUNNING, RUN_SPECS)).toBe(lookalike);
});
});
describe("dev-checkout layouts re-anchor only when the stored file is missing", () => {
const RUN_SPECS = "/new/lib/node_modules/@openrig/cli/daemon/specs";
const DEV = "/src/openrig/packages/daemon";
const present = () => true;
const missing = () => false;
it("built-in from a present dev checkout stays as stored; a missing one re-anchors", () => {
const stored = { path: "openrig-start.md", absolutePath: `${DEV}/assets/guidance/openrig-start.md`, ownerRoot: `${DEV}/assets`, ...meta };
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS, present)).toBe(stored);
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS, missing).absolutePath).toBe(`${RUNNING}/guidance/openrig-start.md`);
});
it("shipped-spec startup file from a present dev checkout stays as stored; a missing one re-anchors", () => {
const stored = { path: "culture/CULTURE.md", absolutePath: `${DEV}/specs/rigs/launch/kernel/culture/CULTURE.md`, ownerRoot: `${DEV}/specs/rigs/launch/kernel`, ...meta };
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS, present)).toBe(stored);
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS, missing).absolutePath).toBe(`${RUN_SPECS}/rigs/launch/kernel/culture/CULTURE.md`);
});
it("projection resource from a present dev checkout stays as stored; a missing one re-anchors", () => {
const entry = { sourcePath: `${DEV}/specs/agents/shared`, absolutePath: `${DEV}/specs/agents/shared/runtime/claude-mcp.fragment.json`, effectiveId: "shared:claude-default-mcp" };
expect(reanchorShippedProjectionEntry(entry, RUN_SPECS, present)).toBe(entry);
expect(reanchorShippedProjectionEntry(entry, RUN_SPECS, missing).absolutePath).toBe(`${RUN_SPECS}/agents/shared/runtime/claude-mcp.fragment.json`);
});
it("packaged installs re-anchor even while the stored file is present", () => {
const stored = { path: "CULTURE-default.md", absolutePath: `${OLD}/guidance/CULTURE-default.md`, ownerRoot: OLD, ...meta };
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS, present).absolutePath).toBe(`${RUNNING}/guidance/CULTURE-default.md`);
});
it("an unrecognized daemon/assets layout is not treated as an OpenRig install", () => {
const other = "/opt/something/daemon/assets";
const stored = { path: "CULTURE-default.md", absolutePath: `${other}/guidance/CULTURE-default.md`, ownerRoot: other, ...meta };
expect(reanchorBuiltinStartupFile(stored, RUNNING, RUN_SPECS, missing)).toBe(stored);
});
});
describe("legacy persisted shapes are returned exactly as stored", () => {
it("startup file without ownerRoot (or path) does not throw and is unchanged", () => {
const noOwner = { path: "required-onboarding.md", absolutePath: "/tmp/never-read/required-onboarding.md", required: true } as unknown as { path: string; absolutePath: string; ownerRoot: string };
expect(reanchorBuiltinStartupFile(noOwner, RUNNING)).toBe(noOwner);
const noPath = { absolutePath: `${OLD}/guidance/CULTURE-default.md`, ownerRoot: OLD } as unknown as { path: string; absolutePath: string; ownerRoot: string };
expect(() => reanchorBuiltinStartupFile(noPath, RUNNING)).not.toThrow();
});
it("projection entry without sourcePath does not throw and is unchanged", () => {
const legacy = { absolutePath: "/tmp/never-read/CLAUDE.md", category: "memory" } as unknown as { sourcePath: string; absolutePath: string };
expect(reanchorShippedProjectionEntry(legacy)).toBe(legacy);
});
});
@@ -117,8 +117,8 @@ function claudeNode(overrides?: Partial<NodeInventoryEntry> & { cwd?: string | n
function startupContextProbe(options?: {
status?: "ok" | "missing" | "malformed" | "probe_error";
evidence?: string;
resolvedStartupFiles?: Array<{ absolutePath: string; required: boolean; path?: string; deliveryHint?: string }>;
projectionEntries?: Array<{ absolutePath: string; effectiveId?: string; category?: string }>;
resolvedStartupFiles?: Array<{ absolutePath: string; required: boolean; path?: string; deliveryHint?: string; ownerRoot?: string }>;
projectionEntries?: Array<{ absolutePath: string; effectiveId?: string; category?: string; sourcePath?: string }>;
runtime?: string;
}) {
if (options?.status && options.status !== "ok") {
@@ -1478,6 +1478,87 @@ describe("RestoreCheckService", () => {
}));
});
// #261: restore-check inspects and REPORTS the paths replay will use. A stale old-install built-in or
// shipped-spec resource is judged at the running install; red/yellow semantics are unchanged.
describe("#261 stale old-install paths are judged at the running install", () => {
const OLD = "/old-openrig/lib/node_modules/@openrig/cli/daemon";
const RUN_ASSETS = path.resolve(import.meta.dirname, "../assets");
const RUN_SPECS = path.resolve(import.meta.dirname, "../specs");
const files = [{ path: "CULTURE-default.md", absolutePath: `${OLD}/assets/guidance/CULTURE-default.md`, ownerRoot: `${OLD}/assets`, required: true }];
const entries = [{ absolutePath: `${OLD}/specs/agents/shared/runtime/claude-settings.fragment.json`, sourcePath: `${OLD}/specs/agents/shared`, effectiveId: "shared:claude-default-settings", category: "runtime_resource" }];
const stoppedNode = () => claudeNode({ sessionStatus: "stopped", startupStatus: "failed", latestError: "seat crashed" });
it("old install removed, current present: green, and evidence names only the running paths", () => {
const service = new RestoreCheckService(mockDeps({
getNodeInventory: () => [stoppedNode()],
getStartupContext: () => startupContextProbe({ resolvedStartupFiles: files, projectionEntries: entries }) as never,
getLatestSnapshot: () => ({ id: "snap-261", kind: "auto-pre-down" }),
exists: (p) => !p.startsWith("/old-openrig"),
}));
const startup = (service.check({ noQueue: true, noHooks: true }) as any).checks.find((c: { check: string }) => c.check === "[email protected]");
expect(startup.status).toBe("green");
expect(startup.evidence).toContain(`${RUN_ASSETS}/guidance/CULTURE-default.md`);
expect(startup.evidence).toContain(`${RUN_SPECS}/agents/shared/runtime/claude-settings.fragment.json`);
expect(startup.evidence).not.toContain("/old-openrig");
});
it("current built-in genuinely missing on a non-ready seat: still red, reporting the running path", () => {
const runningCulture = `${RUN_ASSETS}/guidance/CULTURE-default.md`;
const service = new RestoreCheckService(mockDeps({
getNodeInventory: () => [stoppedNode()],
getStartupContext: () => startupContextProbe({ resolvedStartupFiles: files }) as never,
getLatestSnapshot: () => ({ id: "snap-261", kind: "auto-pre-down" }),
exists: (p) => !p.startsWith("/old-openrig") && p !== runningCulture,
}));
const startup = (service.check({ noQueue: true, noHooks: true }) as any).checks.find((c: { check: string }) => c.check === "[email protected]");
expect(startup.status).toBe("red");
expect(startup.evidence).toContain(runningCulture);
expect(startup.remediation).toContain(runningCulture);
expect(startup.evidence).not.toContain("/old-openrig");
});
it("shipped-spec rig culture: stale old path is judged at the running specs (green when present, red naming it when missing)", () => {
const culture = [{ path: "culture/CULTURE.md", absolutePath: `${OLD}/specs/rigs/launch/kernel/culture/CULTURE.md`, ownerRoot: `${OLD}/specs/rigs/launch/kernel`, required: true }];
const runningCulture = `${RUN_SPECS}/rigs/launch/kernel/culture/CULTURE.md`;
const check = (exists: (p: string) => boolean) => (new RestoreCheckService(mockDeps({
getNodeInventory: () => [stoppedNode()],
getStartupContext: () => startupContextProbe({ resolvedStartupFiles: culture }) as never,
getLatestSnapshot: () => ({ id: "snap-261", kind: "auto-pre-down" }),
exists,
})).check({ noQueue: true, noHooks: true }) as any).checks.find((c: { check: string }) => c.check === "[email protected]");
const present = check((p) => !p.startsWith("/old-openrig"));
expect(present.status).toBe("green");
expect(present.evidence).toContain(runningCulture);
const missing = check((p) => !p.startsWith("/old-openrig") && p !== runningCulture);
expect(missing.status).toBe("red");
expect(missing.evidence).toContain(runningCulture);
expect(missing.evidence).not.toContain("/old-openrig");
});
it("current built-in missing on a running/ready seat stays yellow (red/yellow rule unchanged)", () => {
const runningCulture = `${RUN_ASSETS}/guidance/CULTURE-default.md`;
const service = new RestoreCheckService(mockDeps({
getStartupContext: () => startupContextProbe({ resolvedStartupFiles: files }) as never,
exists: (p) => !p.startsWith("/old-openrig") && p !== runningCulture,
}));
const startup = (service.check({ noQueue: true, noHooks: true }) as any).checks.find((c: { check: string }) => c.check === "[email protected]");
expect(startup.status).toBe("yellow");
});
it("a missing custom file with a built-in basename is reported as stored, still red on a non-ready seat", () => {
const custom = [{ path: "CULTURE-default.md", absolutePath: "/user-rig/CULTURE-default.md", ownerRoot: "/user-rig", required: true }];
const service = new RestoreCheckService(mockDeps({
getNodeInventory: () => [stoppedNode()],
getStartupContext: () => startupContextProbe({ resolvedStartupFiles: custom }) as never,
getLatestSnapshot: () => ({ id: "snap-261", kind: "auto-pre-down" }),
exists: (p) => p !== "/user-rig/CULTURE-default.md",
}));
const startup = (service.check({ noQueue: true, noHooks: true }) as any).checks.find((c: { check: string }) => c.check === "[email protected]");
expect(startup.status).toBe("red");
expect(startup.evidence).toContain("/user-rig/CULTURE-default.md");
});
});
it("running/ready node with missing required startup file is a yellow caveat", () => {
const requiredPath = path.join(os.tmpdir(), "restore-check-startup-required-ready-missing.md");
const service = new RestoreCheckService(mockDeps({
@@ -3737,4 +3737,135 @@ describe("RestoreOrchestrator", () => {
expect(subsetEvents[0]?.result?.warnings?.some((w) => w.includes("FR-5"))).toBe(true);
});
});
// #261: stored built-in startup files follow the running install on restore (pre-validation + replay);
// custom files are delivered exactly as stored; a same-native resume still replays nothing.
describe("#261 built-in startup files follow the running install", () => {
const OLD_ASSETS = "/old-openrig/lib/node_modules/@openrig/cli/daemon/assets";
const RUNNING_ASSETS = path.resolve(import.meta.dirname, "../assets");
const builtin = (name: string, rel: string) => ({
path: name, absolutePath: `${OLD_ASSETS}/${rel}`, ownerRoot: OLD_ASSETS,
deliveryHint: "guidance_merge" as const, required: true, appliesOn: ["fresh_start" as const, "restore" as const],
});
const customSameBasename = {
path: "CULTURE-default.md", absolutePath: "/user-rig/CULTURE-default.md", ownerRoot: "/user-rig",
deliveryHint: "guidance_merge" as const, required: true, appliesOn: ["fresh_start" as const, "restore" as const],
};
const storedFiles = [
builtin("CULTURE-default.md", "guidance/CULTURE-default.md"),
builtin("openrig-start.md", "guidance/openrig-start.md"),
customSameBasename,
];
const notOld = (p: string) => !p.startsWith("/old-openrig");
function seedPodAware(resume: boolean) {
const rig = rigRepo.createRig("test-rig");
db.prepare("INSERT INTO pods (id, rig_id, label) VALUES (?, ?, ?)").run("pod-261", rig.id, "Dev");
const node = rigRepo.addNode(rig.id, "dev.impl", { runtime: "claude-code", podId: "pod-261" });
const session = sessionRegistry.registerSession(node.id, "dev-impl@test-rig");
sessionRegistry.updateStatus(session.id, "running");
if (resume) sessionRegistry.updateResumeToken(session.id, "claude_id", "resume-token-261");
db.prepare("INSERT INTO node_startup_context (node_id, projection_entries_json, resolved_files_json, startup_actions_json, runtime) VALUES (?, ?, ?, ?, ?)")
.run(node.id, "[]", JSON.stringify(storedFiles), "[]", "claude-code");
const snap = snapshotCapture.captureSnapshot(rig.id, "test");
sessionRegistry.updateStatus(session.id, "exited");
db.prepare("DELETE FROM bindings WHERE node_id = ?").run(node.id);
const deliverStartup = vi.fn(async () => ({ delivered: 0, failed: [] }));
const adapter = {
runtime: "claude-code",
listInstalled: vi.fn(async () => []),
project: vi.fn(async () => ({ projected: [], skipped: [], failed: [] })),
deliverStartup,
checkReady: vi.fn(async () => ({ ready: true })),
launchHarness: vi.fn(async () => ({ ok: true as const, resumeToken: "t", resumeType: "claude_id" })),
};
return { snap, deliverStartup, adapter };
}
it("fresh replay: old install removed -> no blocker, built-ins delivered from the running install, custom file unchanged", async () => {
const { snap, deliverStartup, adapter } = seedPodAware(false);
const result = await createOrchestrator().restore(snap.id, {
adapters: { "claude-code": adapter }, fsOps: { exists: notOld }, freshLogicalIds: ["dev.impl"],
});
expect(result.ok).toBe(true);
const delivered = deliverStartup.mock.calls.flatMap((c) => c[0] as Array<{ path: string; absolutePath: string; ownerRoot: string; required: boolean; appliesOn: string[]; deliveryHint: string }>);
const culture = delivered.filter((f) => f.path === "CULTURE-default.md");
expect(culture.map((f) => f.absolutePath).sort()).toEqual([`${RUNNING_ASSETS}/guidance/CULTURE-default.md`, "/user-rig/CULTURE-default.md"].sort());
expect(delivered.find((f) => f.path === "openrig-start.md")).toMatchObject({
absolutePath: `${RUNNING_ASSETS}/guidance/openrig-start.md`, ownerRoot: RUNNING_ASSETS,
required: true, appliesOn: ["fresh_start", "restore"], deliveryHint: "guidance_merge",
});
expect(delivered.some((f) => f.absolutePath.startsWith("/old-openrig"))).toBe(false);
});
it("re-anchors even while the old install still exists (current shipped guidance is selected)", async () => {
const { snap, deliverStartup, adapter } = seedPodAware(false);
const result = await createOrchestrator().restore(snap.id, {
adapters: { "claude-code": adapter }, fsOps: { exists: () => true }, freshLogicalIds: ["dev.impl"],
});
expect(result.ok).toBe(true);
const delivered = deliverStartup.mock.calls.flatMap((c) => c[0] as Array<{ absolutePath: string }>);
expect(delivered.some((f) => f.absolutePath.startsWith("/old-openrig"))).toBe(false);
expect(delivered.some((f) => f.absolutePath === `${RUNNING_ASSETS}/guidance/CULTURE-default.md`)).toBe(true);
});
it("a genuinely missing CURRENT built-in still blocks restore honestly, naming the running path", async () => {
const { snap, adapter } = seedPodAware(false);
const runningCulture = `${RUNNING_ASSETS}/guidance/CULTURE-default.md`;
const result = await createOrchestrator().restore(snap.id, {
adapters: { "claude-code": adapter }, fsOps: { exists: (p) => notOld(p) && p !== runningCulture }, freshLogicalIds: ["dev.impl"],
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.result.blockers?.[0]).toMatchObject({ code: "required_startup_file_missing", path: runningCulture });
});
it("extension: replay projects shipped-spec resources from the running install; plugin entry untouched; no drift warning for the old path", async () => {
const OLD_SPECS = "/old-openrig/lib/node_modules/@openrig/cli/daemon/specs";
const RUNNING_SPECS = path.resolve(import.meta.dirname, "../specs");
const { snap, adapter } = seedPodAware(false);
const entries = [
{ category: "runtime_resource", effectiveId: "shared:claude-default-mcp", sourceSpec: "shared", sourcePath: `${OLD_SPECS}/agents/shared`, resourcePath: "r", absolutePath: `${OLD_SPECS}/agents/shared/runtime/claude-mcp.fragment.json`, resourceType: "claude_mcp_fragment" },
{ category: "plugin", effectiveId: "shared:openrig-core", sourceSpec: "shared", sourcePath: `${OLD_SPECS}/agents/shared`, resourcePath: "p", absolutePath: "/home/u/.openrig/plugins/openrig-core" },
];
const fixed = updateSnapshotData(snap, (data) => { for (const k of Object.keys(data.nodeStartupContext)) data.nodeStartupContext[k].projectionEntries = entries; });
const result = await createOrchestrator().restore(fixed.id, {
adapters: { "claude-code": adapter }, fsOps: { exists: notOld }, freshLogicalIds: ["dev.impl"],
});
expect(result.ok).toBe(true);
const projected = adapter.project.mock.calls.flatMap((c) => ((c[0] as { entries: Array<{ effectiveId: string; absolutePath: string }> }).entries));
expect(projected.find((e) => e.effectiveId === "shared:claude-default-mcp")?.absolutePath).toBe(`${RUNNING_SPECS}/agents/shared/runtime/claude-mcp.fragment.json`);
expect(projected.find((e) => e.effectiveId === "shared:openrig-core")?.absolutePath).toBe("/home/u/.openrig/plugins/openrig-core");
if (result.ok) {
const drift = (result.result.warnings ?? []).filter((w) => w.includes("projection_drift"));
// The re-anchored resource raises no drift; the deliberately untouched plugin entry keeps its honest
// "source root missing" warning for the old shipped sourcePath while its own file still projects.
expect(drift.some((w) => w.includes("claude-mcp.fragment.json"))).toBe(false);
expect(drift.every((w) => w.includes("source root missing") && w.includes("/old-openrig/lib/node_modules/@openrig/cli/daemon/specs/agents/shared"))).toBe(true);
}
});
it("startup extension: replay delivers a shipped-spec rig culture from the running install; custom unchanged", async () => {
const OLD_SPECS = "/old-openrig/lib/node_modules/@openrig/cli/daemon/specs";
const RUNNING_SPECS = path.resolve(import.meta.dirname, "../specs");
const { snap, deliverStartup, adapter } = seedPodAware(false);
const shippedCulture = { path: "culture/CULTURE.md", absolutePath: `${OLD_SPECS}/rigs/launch/kernel/culture/CULTURE.md`, ownerRoot: `${OLD_SPECS}/rigs/launch/kernel`, deliveryHint: "guidance_merge", required: true, appliesOn: ["fresh_start", "restore"] };
const fixed = updateSnapshotData(snap, (data) => { for (const k of Object.keys(data.nodeStartupContext)) data.nodeStartupContext[k].resolvedStartupFiles = [shippedCulture, customSameBasename]; });
const result = await createOrchestrator().restore(fixed.id, {
adapters: { "claude-code": adapter }, fsOps: { exists: notOld }, freshLogicalIds: ["dev.impl"],
});
expect(result.ok).toBe(true);
const delivered = deliverStartup.mock.calls.flatMap((c) => (c[0] as Array<{ absolutePath: string }>).map((f) => f.absolutePath));
expect(delivered.sort()).toEqual([`${RUNNING_SPECS}/rigs/launch/kernel/culture/CULTURE.md`, "/user-rig/CULTURE-default.md"].sort());
});
it("same-native resume: replay stays contained (no startup files delivered), stored built-ins notwithstanding", async () => {
const { snap, deliverStartup, adapter } = seedPodAware(true);
const orch = createOrchestrator({ listProcesses: nativeLineage("claude-code", "resume-token-261") });
const result = await orch.restore(snap.id, { adapters: { "claude-code": adapter }, fsOps: { exists: notOld } });
expect(result.ok).toBe(true);
const delivered = deliverStartup.mock.calls.flatMap((c) => c[0] as unknown[]);
expect(delivered).toEqual([]);
});
});
});
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import path from "node:path";
import type Database from "better-sqlite3";
import { createFullTestDb } from "./helpers/test-app.js";
import { RigRepository } from "../src/domain/rig-repository.js";
@@ -571,4 +572,83 @@ describe("SeatLifecycleService.launchFresh", () => {
expect(sessionRegistry.currentOccupantTenure(seat.node.id)?.kind).toBe("fresh");
expect(db.prepare("SELECT COUNT(*) AS c FROM events WHERE type = 'seat.fresh_launch_failed'").get()).toEqual({ c: 1 });
});
// #261: a fresh launch delivers stored built-in startup files from the RUNNING install;
// a custom rig file with the same basename is delivered exactly as stored.
it("#261 delivers stored built-ins from the running install and leaves custom files untouched", async () => {
const seat = seedSeat({ clean: true });
const oldAssets = "/mise/installs/npm-openrig-cli/0.6.2/node_modules/@openrig/cli/daemon/assets";
const running = path.resolve(import.meta.dirname, "../assets");
const meta = { deliveryHint: "guidance_merge", required: true, appliesOn: ["fresh_start", "restore"] };
const stored = [
{ path: "CULTURE-default.md", absolutePath: `${oldAssets}/guidance/CULTURE-default.md`, ownerRoot: oldAssets, ...meta },
{ path: "openrig-onboarding-01.md", absolutePath: `${oldAssets}/onboarding/01-world-and-purpose.md`, ownerRoot: oldAssets, ...meta },
{ path: "CULTURE-default.md", absolutePath: "/project/CULTURE-default.md", ownerRoot: "/project", ...meta },
];
db.prepare("UPDATE node_startup_context SET resolved_files_json=? WHERE node_id=?").run(JSON.stringify(stored), seat.node.id);
const delivered: Array<{ path: string; absolutePath: string; ownerRoot: string; required: boolean }> = [];
adapter.deliverStartup = async (files) => { delivered.push(...(files as typeof delivered)); return { delivered: files.length, failed: [] }; };
const result = await service.launchFresh({ seatRef: seat.sessionName, fresh: true, reason: "issue 261" });
expect(result.ok).toBe(true);
expect(delivered.map((f) => f.absolutePath).sort()).toEqual([
"/project/CULTURE-default.md",
`${running}/guidance/CULTURE-default.md`,
`${running}/onboarding/01-world-and-purpose.md`,
].sort());
expect(delivered.every((f) => f.required)).toBe(true);
const persisted = JSON.parse((db.prepare("SELECT resolved_files_json AS j FROM node_startup_context WHERE node_id=?").get(seat.node.id) as { j: string }).j) as Array<{ absolutePath: string }>;
expect(persisted.some((f) => f.absolutePath.startsWith(oldAssets))).toBe(false);
});
// #261 extension: shipped-spec projection resources follow the running install; a plugin stored
// outside daemon/specs and user resources are projected exactly as stored.
it("#261 projects shipped-spec resources from the running install and leaves plugin/user entries untouched", async () => {
const seat = seedSeat({ clean: true });
const oldSpecs = "/mise/installs/npm-openrig-cli/0.6.2/node_modules/@openrig/cli/daemon/specs";
const runningSpecs = path.resolve(import.meta.dirname, "../specs");
const entry = (over: Record<string, string>) => ({ category: "runtime_resource", effectiveId: "x", sourceSpec: "shared", resourcePath: "r", ...over });
const stored = [
entry({ effectiveId: "shared:claude-default-settings", sourcePath: `${oldSpecs}/agents/shared`, absolutePath: `${oldSpecs}/agents/shared/runtime/claude-settings.fragment.json`, resourceType: "claude_settings_fragment" }),
entry({ category: "plugin", effectiveId: "shared:openrig-core", sourcePath: `${oldSpecs}/agents/shared`, absolutePath: "/home/u/.openrig/plugins/openrig-core" }),
entry({ category: "guidance", effectiveId: "role", sourceSpec: "dev.impl", sourcePath: "/project/agents/impl", absolutePath: "/project/agents/impl/guidance/role.md" }),
];
db.prepare("UPDATE node_startup_context SET projection_entries_json=? WHERE node_id=?").run(JSON.stringify(stored), seat.node.id);
const result = await service.launchFresh({ seatRef: seat.sessionName, fresh: true, reason: "issue 261 projection" });
expect(result.ok).toBe(true);
const byId = Object.fromEntries((projectedPlan?.entries ?? []).map((e) => [e.effectiveId, e]));
expect(byId["shared:claude-default-settings"]).toMatchObject({
sourcePath: `${runningSpecs}/agents/shared`, absolutePath: `${runningSpecs}/agents/shared/runtime/claude-settings.fragment.json`,
resourceType: "claude_settings_fragment", category: "runtime_resource",
});
expect(byId["shared:openrig-core"]).toMatchObject({ sourcePath: `${oldSpecs}/agents/shared`, absolutePath: "/home/u/.openrig/plugins/openrig-core" });
expect(byId["role"]).toMatchObject({ absolutePath: "/project/agents/impl/guidance/role.md" });
});
// #261 startup extension: shipped-spec startup files (kernel culture, agent role/startup context) follow the running install.
it("#261 delivers shipped-spec startup files (pre- and post-launch) from the running install", async () => {
const seat = seedSeat({ clean: true });
const oldSpecs = "/mise/installs/npm-openrig-cli/0.6.2/node_modules/@openrig/cli/daemon/specs";
const runningSpecs = path.resolve(import.meta.dirname, "../specs");
const kernel = "rigs/launch/kernel";
const agent = `${kernel}/agents/advisor/lead`;
const stored = [
{ path: "culture/CULTURE.md", absolutePath: `${oldSpecs}/${kernel}/culture/CULTURE.md`, ownerRoot: `${oldSpecs}/${kernel}`, deliveryHint: "guidance_merge", required: true, appliesOn: ["fresh_start", "restore"] },
{ path: "guidance/role.md", absolutePath: `${oldSpecs}/${agent}/guidance/role.md`, ownerRoot: `${oldSpecs}/${agent}`, deliveryHint: "send_text", required: true, appliesOn: ["fresh_start", "restore"] },
{ path: "culture/CULTURE.md", absolutePath: "/project/culture/CULTURE.md", ownerRoot: "/project", deliveryHint: "guidance_merge", required: true, appliesOn: ["fresh_start", "restore"] },
];
db.prepare("UPDATE node_startup_context SET resolved_files_json=? WHERE node_id=?").run(JSON.stringify(stored), seat.node.id);
const delivered: string[] = [];
adapter.deliverStartup = async (files) => { delivered.push(...files.map((f) => f.absolutePath)); return { delivered: files.length, failed: [] }; };
const result = await service.launchFresh({ seatRef: seat.sessionName, fresh: true, reason: "issue 261 shipped specs" });
expect(result.ok).toBe(true);
// Pre-launch culture and the post-launch send_text role both arrive at the running install; custom is as stored.
expect(delivered.sort()).toEqual([
`${runningSpecs}/${agent}/guidance/role.md`,
`${runningSpecs}/${kernel}/culture/CULTURE.md`,
"/project/culture/CULTURE.md",
].sort());
const persisted = JSON.parse((db.prepare("SELECT resolved_files_json AS j FROM node_startup_context WHERE node_id=?").get(seat.node.id) as { j: string }).j) as Array<{ path: string; absolutePath: string; deliveryHint: string }>;
expect(persisted.find((f) => f.path === "guidance/role.md")).toMatchObject({ absolutePath: `${runningSpecs}/${agent}/guidance/role.md`, deliveryHint: "send_text" });
expect(persisted.some((f) => f.absolutePath.startsWith(oldSpecs))).toBe(false);
});
});