mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
feat: initialize canonical OpenRig instance layout
This commit is contained in:
1 parent
5820f741bf
commit
f9f7e0f00e
40 files changed
+880
-324
No files matched your search
@@ -67,9 +67,10 @@ in-memory cache at daemon scope.
|
||||
`ContextPackLibraryService.scan()` walks roots, parses each
|
||||
`manifest.yaml`, replaces the in-memory index; collisions are
|
||||
**last-wins** in discovery order (workspace > user_file > builtin).
|
||||
Startup wires three roots: builtin (`../context-packs`, first), user-file
|
||||
(`~/.openrig/context-packs`), and workspace-local
|
||||
`<workspaceRoot>/.openrig/context-packs` when present + distinct. A pack is
|
||||
Startup wires the builtin root (`../context-packs`, first), the configured
|
||||
user-file root (`context.root`, default `$OPENRIG_HOME/context`), its
|
||||
`system/` child as the canonical System World root, and the workspace-local
|
||||
`<workspaceRoot>/.openrig/context-packs` root when present + distinct. A pack is
|
||||
addressed by its path-like **ref** (e.g. `packs/compaction-restore`), which is
|
||||
its unique identity; the opaque entry id is `context-pack:<ref>` (a UI routing
|
||||
key). Colon-id `context-pack:<name>:<version>` addressing and the `/library/:id`
|
||||
@@ -79,8 +80,8 @@ ordinary files only and rejects context-pack expansion; compose a durable ref
|
||||
first, then use a dedicated delivery verb when delivery is intended.
|
||||
|
||||
> Source: `domain/context-packs/context-pack-library-service.ts:59-94`
|
||||
> (scan; last-wins L79-81), `:31-33` (id); `startup.ts:481-502` (3-root
|
||||
> IIFE; builtin `unshift` L497, workspace L491-493) @HEAD.
|
||||
> (scan; last-wins L79-81), `:31-33` (id); `startup.ts`
|
||||
> (`contextPackLibrary` construction) @HEAD.
|
||||
|
||||
The parser is pure (no fs; caller passes raw YAML). It rejects malformed
|
||||
YAML, missing `name`/`version`, non-array `files`, per-file path traversal
|
||||
|
||||
@@ -229,7 +229,9 @@ discovery and exposes empty `install.context` / `install.skills` selectors.
|
||||
The ignore file excludes `exhaust/` and local `.openrig/` projection state
|
||||
while keeping authored project context versionable.
|
||||
|
||||
The initializer is additive. It never deletes or overwrites an existing file;
|
||||
The workspace owner is called by the shared instance initializer used by CLI
|
||||
daemon start and direct daemon first start. The initializer is additive. It
|
||||
never deletes or overwrites an existing file;
|
||||
the retained `--force` spelling is a compatibility no-op for overwrite
|
||||
behavior. CLI and daemon scaffolds are byte-parity pinned. Mission and slice
|
||||
content is created explicitly through `rig scope`, not seeded by workspace
|
||||
@@ -239,6 +241,8 @@ and the retired `artifacts/`, `evidence/`, `progress/`, `field-notes/`,
|
||||
|
||||
> Source: `domain/workspace/default-workspace-scaffold.ts`
|
||||
> (`workspaceScaffoldDirs`, `workspaceScaffoldFiles`); CLI
|
||||
> `domain/instance-initialization.ts`; daemon `index.ts`; CLI
|
||||
> `packages/cli/src/daemon-lifecycle.ts` and
|
||||
> `packages/cli/src/commands/config-init-workspace.ts` (`runInitWorkspace`);
|
||||
> parity: `packages/daemon/test/getting-started-narrative-parity.test.ts`
|
||||
> @HEAD.
|
||||
|
||||
@@ -116,7 +116,11 @@ Subcommands:
|
||||
- `logs [--follow]`
|
||||
|
||||
Notes:
|
||||
- `start` launches the daemon process and accepts runtime overrides for port, host, and DB path.
|
||||
- `start` additively reconciles the canonical instance layout before launching
|
||||
the daemon process, then accepts runtime overrides for port, host, and DB
|
||||
path. Direct daemon first start uses the same initializer. Existing files are
|
||||
preserved and wrong-type managed paths are refused before any write. See
|
||||
`docs/reference/instance-layout.md`.
|
||||
- `logs` reads daemon log output and can follow it.
|
||||
- **Deploy identity (v0.4.4, OPR.0.4.4.11 FR-6/7)**: a PACKAGED build (built via `scripts/build-package.sh`) is stamped with `{semver, commit, dirty, builtAt}`; the daemon's `/healthz` payload carries the four stamp fields additively and `rig --version` renders `<semver> (<commit8>[, dirty])`. A source/dev run has NO stamp and adds NOTHING (never an invented SHA) — `/healthz` keeps its legacy body and `--version` prints the plain semver. This is the 30-second stale-deploy diagnostic: an unstamped or old-commit `/healthz` on a long-running host means you are looking at an older deployed build, not the source tree. Source: `packages/{daemon,cli}/src/build-info.ts` (`stampFields`), `packages/daemon/src/server.ts` (`/healthz`), `packages/cli/src/version.ts`.
|
||||
|
||||
@@ -154,6 +158,10 @@ Supported keys:
|
||||
- `transcripts.enabled`
|
||||
- `transcripts.path`
|
||||
- `workspace.root` (and other workspace-rooted paths used by `init-workspace`)
|
||||
- `context.root` (default `$OPENRIG_HOME/context`; env
|
||||
`OPENRIG_CONTEXT_ROOT`) — the single writable addressable context library.
|
||||
The removed `context.packs_root`, config field `context.packsRoot`, and env
|
||||
`OPENRIG_CONTEXT_PACKS_ROOT` are refused with replacement guidance.
|
||||
- `skills.root` (default `$OPENRIG_HOME/skills`; env `OPENRIG_SKILLS_ROOT`) — the
|
||||
one authoritative Git-versioned managed skill catalog. An override replaces
|
||||
the default; it does not add an overlay root.
|
||||
|
||||
@@ -16,7 +16,8 @@ is no magic one-command onboarding; each step is a real, inspectable verb.
|
||||
the kernel (see "Kernel framing" below).
|
||||
2. **`rig up <rig-spec>`** - launch a rig. This **auto-starts the daemon** if it
|
||||
is not already running (the four daemon-dependent verbs below do not auto-start
|
||||
it - they tell you to run `rig up` or `rig daemon start`).
|
||||
it - they tell you to run `rig up` or `rig daemon start`). Daemon start first
|
||||
reconciles the additive [OpenRig instance layout](instance-layout.md).
|
||||
3. **`rig status`** - see what is currently true: daemon up/down + port, kernel
|
||||
readiness (distinct from daemon health), and your effective workspace root
|
||||
(default vs override).
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# OpenRig Instance Layout
|
||||
|
||||
An OpenRig instance keeps its managed state under one configured
|
||||
`$OPENRIG_HOME`. `rig daemon start` and direct daemon first start reconcile the
|
||||
same additive layout before the database is opened or the listener binds.
|
||||
|
||||
```text
|
||||
$OPENRIG_HOME/
|
||||
config.json # typed instance settings; created as an empty object
|
||||
state/ # runtime-owned durable state
|
||||
context/ # addressable context library (`context.root`)
|
||||
system/ # canonical System World library root
|
||||
skills/ # managed skill catalog (`skills.root`)
|
||||
workspace/ # project work tree (`workspace.root`)
|
||||
SPEC.md # project intent
|
||||
project.yaml # project context and skill selection
|
||||
workspace.yaml # project-location catalog
|
||||
.gitignore
|
||||
missions/
|
||||
exhaust/
|
||||
specs/ # canonical instance spec library
|
||||
topology/ # instance, rig, pod, and seat continuity tree
|
||||
plugins/ # installed OpenRig plugins
|
||||
run/ # process coordination files
|
||||
logs/ # daemon and operation logs
|
||||
transcripts/ # durable per-seat terminal transcripts
|
||||
backups/ # operator-created recovery artifacts
|
||||
secrets/ # local connector and host secrets
|
||||
```
|
||||
|
||||
The initializer creates only missing managed entries. It never overwrites an
|
||||
existing file, and it checks every managed path before the first write. A path
|
||||
with the wrong type is reported by its exact location; unrelated user-owned
|
||||
content is preserved. A second run against an already-converged instance writes
|
||||
nothing.
|
||||
|
||||
The workspace subtree is owned by the [Project Workspace
|
||||
Contract](project-workspace.md). The instance initializer calls that owner
|
||||
rather than carrying another copy of its file bytes. `skills/` and `topology/`
|
||||
are created as empty roots; their respective catalog and topology workflows own
|
||||
their contents.
|
||||
|
||||
## Context library setting
|
||||
|
||||
The addressable context library has one typed setting and one environment
|
||||
override:
|
||||
|
||||
| Surface | Value |
|
||||
| --- | --- |
|
||||
| Config key | `context.root` |
|
||||
| Environment | `OPENRIG_CONTEXT_ROOT` |
|
||||
| Default | `$OPENRIG_HOME/context` |
|
||||
| Resolved property | `contextRoot` |
|
||||
|
||||
The removed `context.packs_root`, `context.packsRoot`, and
|
||||
`OPENRIG_CONTEXT_PACKS_ROOT` spellings are refused with guidance to use
|
||||
`context.root`; they are not compatibility aliases. Bundle installation and
|
||||
`rig context add` both resolve the same configured landing root.
|
||||
|
||||
## Existing spec libraries
|
||||
|
||||
Creating `$OPENRIG_HOME/specs` does not migrate existing launch-era specs.
|
||||
Upgraded installations may still have a separate legacy specs library that the
|
||||
runtime reads for compatibility. Treat the two-home state as an explicit
|
||||
limitation: use the live spec-library commands to determine where a spec is
|
||||
served from, and do not infer convergence merely because the canonical
|
||||
directory exists.
|
||||
@@ -4,6 +4,9 @@ OpenRig's Project TUI is a file-backed view over a workspace root. The folder
|
||||
shape is intentionally simple so humans and agents can create or repair it
|
||||
without daemon-internal knowledge.
|
||||
|
||||
This subtree is part of the wider [OpenRig Instance Layout](instance-layout.md).
|
||||
The instance initializer delegates these exact workspace bytes to this owner.
|
||||
|
||||
## Default Shape
|
||||
|
||||
`rig config init-workspace` creates the default workspace at
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// Canonical project-workspace scaffold used by `rig config init-workspace`.
|
||||
//
|
||||
// Keep this byte-compatible with
|
||||
// packages/daemon/src/domain/workspace/default-workspace-scaffold.ts. The two
|
||||
// packages intentionally do not depend on one another, and the daemon parity
|
||||
// test pins their shared output.
|
||||
// Public adapter for the S01 project-workspace owner used by S05 instance
|
||||
// initialization. The bytes and behavior live in @openrig/daemon.
|
||||
|
||||
import { Command } from "commander";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
ensureDefaultWorkspace,
|
||||
workspaceScaffoldDirs,
|
||||
workspaceScaffoldFiles,
|
||||
type InitWorkspaceResult,
|
||||
} from "@openrig/daemon/instance-initialization";
|
||||
import { ConfigStore } from "../config-store.js";
|
||||
|
||||
export { workspaceScaffoldDirs, workspaceScaffoldFiles };
|
||||
export type { InitWorkspaceResult };
|
||||
|
||||
export interface InitWorkspaceOpts {
|
||||
root?: string;
|
||||
/** Deprecated compatibility input. Existing files are always preserved. */
|
||||
@@ -18,63 +21,6 @@ export interface InitWorkspaceOpts {
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export interface InitWorkspaceResult {
|
||||
root: string;
|
||||
rootCreated: boolean;
|
||||
subdirs: Array<{ name: string; path: string; created: boolean }>;
|
||||
files: Array<{ relPath: string; absPath: string; created: boolean; skipped: "exists" | null }>;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
const WORKSPACE_DIRS = ["missions", "exhaust"] as const;
|
||||
|
||||
const PROJECT_SPEC = `---
|
||||
intent: Organize this project's durable work as missions and slices, then move it through queue-backed agent collaboration.
|
||||
---
|
||||
|
||||
# Project
|
||||
|
||||
This is the project-level context for the default OpenRig workspace. It gives
|
||||
agents a stable project intent before they descend into the active mission and
|
||||
slice. Edit it to describe what your project is for and who benefits from it.
|
||||
`;
|
||||
|
||||
const PROJECT_MANIFEST = `schema: openrig.project/v0alpha1
|
||||
kind: project
|
||||
install:
|
||||
intent: SPEC.md
|
||||
context: []
|
||||
skills: []
|
||||
missions:
|
||||
root: missions
|
||||
# Add ordered project Markdown addresses and stable catalog skill IDs above.
|
||||
# Skill source and the System World remain outside the project workspace.
|
||||
`;
|
||||
|
||||
const WORKSPACE_CATALOG = `schema: openrig.workspace/v0alpha1
|
||||
projects:
|
||||
- id: default
|
||||
root: .
|
||||
`;
|
||||
|
||||
const WORKSPACE_GITIGNORE = `# OpenRig disposable work and local runtime projection state.
|
||||
/exhaust/
|
||||
/.openrig/
|
||||
`;
|
||||
|
||||
export function workspaceScaffoldDirs(): string[] {
|
||||
return [...WORKSPACE_DIRS];
|
||||
}
|
||||
|
||||
export function workspaceScaffoldFiles(): Array<{ relPath: string; content: string }> {
|
||||
return [
|
||||
{ relPath: "SPEC.md", content: PROJECT_SPEC },
|
||||
{ relPath: "project.yaml", content: PROJECT_MANIFEST },
|
||||
{ relPath: "workspace.yaml", content: WORKSPACE_CATALOG },
|
||||
{ relPath: ".gitignore", content: WORKSPACE_GITIGNORE },
|
||||
];
|
||||
}
|
||||
|
||||
export function initWorkspaceCommand(configPath?: string): Command {
|
||||
const cmd = new Command("init-workspace")
|
||||
.description("Scaffold a repo-ready project workspace")
|
||||
@@ -110,37 +56,12 @@ export function initWorkspaceCommand(configPath?: string): Command {
|
||||
export function runInitWorkspace(opts: InitWorkspaceOpts & { configPath?: string }): InitWorkspaceResult {
|
||||
const store = new ConfigStore(opts.configPath);
|
||||
const root = opts.root ?? (store.get("workspace.root") as string);
|
||||
const dryRun = !!opts.dryRun;
|
||||
const scaffoldFiles = workspaceScaffoldFiles();
|
||||
const scaffoldDirs = workspaceScaffoldDirs();
|
||||
const rootExists = existsSync(root);
|
||||
const result: InitWorkspaceResult = {
|
||||
root,
|
||||
rootCreated: !rootExists,
|
||||
subdirs: [],
|
||||
files: [],
|
||||
dryRun,
|
||||
};
|
||||
|
||||
if (!rootExists && !dryRun) mkdirSync(root, { recursive: true });
|
||||
|
||||
for (const sub of scaffoldDirs) {
|
||||
const subPath = join(root, sub);
|
||||
const subExists = existsSync(subPath);
|
||||
if (!subExists && !dryRun) mkdirSync(subPath, { recursive: true });
|
||||
result.subdirs.push({ name: sub, path: subPath, created: !subExists });
|
||||
const result = ensureDefaultWorkspace({ root, dryRun: !!opts.dryRun });
|
||||
if (!result.ok) {
|
||||
const detail = result.conflicts
|
||||
.map((conflict) => `${conflict.path}: expected ${conflict.expected}, found ${conflict.actual}`)
|
||||
.join("; ");
|
||||
throw new Error(`Workspace initialization blocked: ${detail}`);
|
||||
}
|
||||
|
||||
for (const file of scaffoldFiles) {
|
||||
const absPath = join(root, file.relPath);
|
||||
const exists = existsSync(absPath);
|
||||
if (exists) {
|
||||
result.files.push({ relPath: file.relPath, absPath, created: false, skipped: "exists" });
|
||||
continue;
|
||||
}
|
||||
if (!dryRun) writeFileSync(absPath, file.content, "utf-8");
|
||||
result.files.push({ relPath: file.relPath, absPath, created: true, skipped: null });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ Keys:
|
||||
workspace.* root, slices_root, steering_path, specs_root,
|
||||
projects_root, catalog_path, operator_seat_name
|
||||
topology.root the topology tree root (instance altitude at its top; default $OPENRIG_HOME/topology)
|
||||
context.packs_root context-pack library landing zone for 'rig context add' (default $OPENRIG_HOME/context-packs)
|
||||
context.root addressable context library for 'rig context add' (default $OPENRIG_HOME/context)
|
||||
skills.root versioned skill catalog (default $OPENRIG_HOME/skills)
|
||||
onboarding.default_pack.enabled deliver the two-part fresh-seat mental-model pack (default on)
|
||||
files.allowlist name:/abs/path,name:/abs/path
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
// Each delegates to /api/context-packs/library/* against the daemon.
|
||||
// The `add` verb installs a pack from a directory at
|
||||
// ~/.openrig/context-packs/<name>/ — host-symlink-free contract,
|
||||
// $OPENRIG_HOME/context/<name>/ — host-symlink-free contract,
|
||||
// matches `rig specs add` shape (regular files only; no symlinks).
|
||||
|
||||
import { Command } from "commander";
|
||||
@@ -571,7 +571,7 @@ Examples:
|
||||
return;
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
console.log("No context packs in library. Author one at ~/.openrig/context-packs/<name>/ then run: rig context sync");
|
||||
console.log("No context packs in library. Author one under `rig config get context.root`, then run: rig context sync");
|
||||
return;
|
||||
}
|
||||
for (const e of entries) {
|
||||
@@ -883,14 +883,14 @@ Examples:
|
||||
|
||||
cmd.command("add")
|
||||
.argument("<source>", "Local directory OR http(s):// URL of a context pack (manifest.yaml + files)")
|
||||
.description("Install a context pack from a local directory or a URL into the context-packs landing zone")
|
||||
.description("Install a context pack from a local directory or a URL into the configured context library")
|
||||
.option("--name <name>", "Override the install name (defaults to the manifest name / source basename)")
|
||||
.option("--json", "JSON output")
|
||||
.action(async (source: string, opts: { name?: string; json?: boolean }) => {
|
||||
try {
|
||||
// OPR.0.5.3.7 R4 — config-resolved landing zone (env > config > $OPENRIG_HOME/context-packs),
|
||||
// OPR.0.5.9.5 Wave B — config-resolved context library,
|
||||
// never a hardcoded ~/.openrig literal; the daemon resolves the same key.
|
||||
const targetRoot = new ConfigStore().resolve().context.packsRoot;
|
||||
const targetRoot = new ConfigStore().resolve().context.root;
|
||||
let targetDir: string;
|
||||
if (isHttpUrl(source)) {
|
||||
// R4 — URL install: fetch → validate → atomic stage+rename (no partial pack).
|
||||
|
||||
@@ -66,6 +66,16 @@ export function realDeps(): LifecycleDeps {
|
||||
removeFile: (p) => { try { fs.unlinkSync(p); } catch { /* ignore */ } },
|
||||
exists: (p) => fs.existsSync(p),
|
||||
mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
|
||||
pathKind: (p) => {
|
||||
try {
|
||||
const value = fs.lstatSync(p);
|
||||
if (value.isDirectory()) return "directory";
|
||||
if (value.isFile()) return "file";
|
||||
return "other";
|
||||
} catch {
|
||||
return "missing";
|
||||
}
|
||||
},
|
||||
openForAppend: (p) => fs.openSync(p, "a"),
|
||||
isProcessAlive,
|
||||
// RULING 1ae863d2 — sibling-home scan (home-resolution honesty).
|
||||
@@ -152,6 +162,9 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
|
||||
transcriptsEnabled: config.transcripts.enabled,
|
||||
transcriptsPath: config.transcripts.path,
|
||||
workspaceRoot: config.workspace.root,
|
||||
contextRoot: config.context.root,
|
||||
skillsRoot: config.skills.root,
|
||||
topologyRoot: config.topology.root,
|
||||
// V1 pre-release CLI/daemon Item 1 — project the
|
||||
// ConfigStore-resolved rotation tunables into the daemon
|
||||
// process env so file-stored values
|
||||
|
||||
@@ -215,6 +215,9 @@ Examples:
|
||||
transcriptsLines: resolvedConfig.transcripts.lines,
|
||||
transcriptsPollIntervalSeconds: resolvedConfig.transcripts.pollIntervalSeconds,
|
||||
workspaceRoot: resolvedConfig.workspace.root,
|
||||
contextRoot: resolvedConfig.context.root,
|
||||
skillsRoot: resolvedConfig.skills.root,
|
||||
topologyRoot: resolvedConfig.topology.root,
|
||||
}, deps.lifecycleDeps);
|
||||
status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,7 +4,8 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { parse as parseYamlDoc } from "yaml";
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient, DaemonConnectionError } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl, startDaemon, type LifecycleDeps , daemonStatusGuard} from "../daemon-lifecycle.js";
|
||||
import { getDaemonStatus, getDaemonUrl, startDaemon, type LifecycleDeps, daemonStatusGuard } from "../daemon-lifecycle.js";
|
||||
import type { RiggedConfig } from "../config-store.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
|
||||
@@ -133,12 +134,7 @@ Examples:
|
||||
// Run preflight before auto-start
|
||||
let status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
if (status.state !== "running") {
|
||||
let resolvedConfig: {
|
||||
daemon: { port: number; host: string };
|
||||
db: { path: string };
|
||||
transcripts: { enabled: boolean; path: string; lines: number; pollIntervalSeconds: number };
|
||||
workspace: { root: string };
|
||||
} | null = null;
|
||||
let resolvedConfig: RiggedConfig | null = null;
|
||||
// bug-fix slice auth-bearer-tailscale-trust: track whether
|
||||
// daemon.host was operator-explicit (env or config file) vs
|
||||
// default-fallback. The daemon's multi-bind path (loopback +
|
||||
@@ -199,6 +195,9 @@ Examples:
|
||||
transcriptsLines: resolvedConfig?.transcripts.lines,
|
||||
transcriptsPollIntervalSeconds: resolvedConfig?.transcripts.pollIntervalSeconds,
|
||||
workspaceRoot: resolvedConfig?.workspace.root,
|
||||
contextRoot: resolvedConfig?.context.root,
|
||||
skillsRoot: resolvedConfig?.skills.root,
|
||||
topologyRoot: resolvedConfig?.topology.root,
|
||||
}, deps.lifecycleDeps);
|
||||
status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
} catch (err) {
|
||||
|
||||
@@ -46,11 +46,10 @@ export interface RiggedConfig {
|
||||
topology: {
|
||||
root: string;
|
||||
};
|
||||
// OPR.0.5.3.7 R4 — the context-pack library landing zone (`rig context add`
|
||||
// installs here). Config-resolved so a shipped verb runs unmodified on a
|
||||
// stranger machine — never a hardcoded ~/.openrig literal. Twin of the daemon.
|
||||
// OPR.0.5.9.5 Wave B — the addressable context library root (`rig context add`
|
||||
// installs here). The removed packsRoot shape is refused, never bridged.
|
||||
context: {
|
||||
packsRoot: string;
|
||||
root: string;
|
||||
};
|
||||
skills: {
|
||||
root: string;
|
||||
@@ -231,8 +230,8 @@ const DEFAULTS = {
|
||||
},
|
||||
// OPR.0.5.3.6 D1 — derived under the OpenRig home, never a shared-docs literal.
|
||||
topology: { root: getDefaultOpenRigPath("topology") },
|
||||
// OPR.0.5.3.7 R4 — derived under the OpenRig home, never a shared-docs literal.
|
||||
context: { packsRoot: getDefaultOpenRigPath("context-packs") },
|
||||
// OPR.0.5.9.5 Wave B — canonical addressable context library.
|
||||
context: { root: getDefaultOpenRigPath("context") },
|
||||
skills: { root: getDefaultOpenRigPath("skills") },
|
||||
onboarding: { defaultPack: { enabled: true } },
|
||||
files: { allowlist: "" },
|
||||
@@ -362,8 +361,8 @@ export const VALID_KEYS = [
|
||||
// shared-docs/rigs location stays readable via the daemon's
|
||||
// resolveLegacyTopologyRigsRoot fallback (advisory-emitting).
|
||||
"topology.root",
|
||||
// OPR.0.5.3.7 R4 — context-pack landing zone; lockstep with the daemon twin.
|
||||
"context.packs_root",
|
||||
// OPR.0.5.9.5 Wave B — canonical context library; old key is refused.
|
||||
"context.root",
|
||||
"skills.root",
|
||||
"onboarding.default_pack.enabled",
|
||||
"files.allowlist",
|
||||
@@ -454,8 +453,7 @@ export const ENV_MAP: Record<ValidKey, { primary: string; legacy?: string }> = {
|
||||
"workspace.projects_root": { primary: "OPENRIG_WORKSPACE_PROJECTS_ROOT" },
|
||||
"workspace.catalog_path": { primary: "OPENRIG_WORKSPACE_CATALOG_PATH" },
|
||||
"topology.root": { primary: "OPENRIG_TOPOLOGY_ROOT" },
|
||||
// OPR.0.5.3.7 R4 — new key, OPENRIG_* only (no RIGGED_* legacy).
|
||||
"context.packs_root": { primary: "OPENRIG_CONTEXT_PACKS_ROOT" },
|
||||
"context.root": { primary: "OPENRIG_CONTEXT_ROOT" },
|
||||
"skills.root": { primary: "OPENRIG_SKILLS_ROOT" },
|
||||
"onboarding.default_pack.enabled": { primary: "OPENRIG_ONBOARDING_DEFAULT_PACK_ENABLED" },
|
||||
// UEP env-var graduation: existing OPENRIG_FILES_ALLOWLIST /
|
||||
@@ -534,7 +532,7 @@ const KEY_TO_PATH: Record<ValidKey, string[]> = {
|
||||
"workspace.projects_root": ["workspace", "projectsRoot"],
|
||||
"workspace.catalog_path": ["workspace", "catalogPath"],
|
||||
"topology.root": ["topology", "root"],
|
||||
"context.packs_root": ["context", "packsRoot"],
|
||||
"context.root": ["context", "root"],
|
||||
"skills.root": ["skills", "root"],
|
||||
"onboarding.default_pack.enabled": ["onboarding", "defaultPack", "enabled"],
|
||||
"files.allowlist": ["files", "allowlist"],
|
||||
@@ -586,6 +584,28 @@ function isValidKey(key: string): key is ValidKey {
|
||||
return (VALID_KEYS as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
const REMOVED_CONTEXT_KEY = "context.packs_root";
|
||||
const REMOVED_CONTEXT_ENV = "OPENRIG_CONTEXT_PACKS_ROOT";
|
||||
|
||||
export function removedContextSettingMessage(key: string): string | null {
|
||||
return key === REMOVED_CONTEXT_KEY
|
||||
? 'Config key "context.packs_root" was removed; use "context.root".'
|
||||
: null;
|
||||
}
|
||||
|
||||
function assertNoRemovedContextSetting(fileConfig: Record<string, unknown>): void {
|
||||
if (process.env[REMOVED_CONTEXT_ENV]?.trim()) {
|
||||
throw new Error(
|
||||
"OPENRIG_CONTEXT_PACKS_ROOT was removed; use OPENRIG_CONTEXT_ROOT (config key context.root).",
|
||||
);
|
||||
}
|
||||
if (getNestedValue(fileConfig, ["context", "packsRoot"]) !== undefined) {
|
||||
throw new Error(
|
||||
'Config file contains removed key "context.packs_root" (context.packsRoot); replace it with "context.root" (context.root).',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── OPR.0.4.4.15 (G15-P1) — TWIN of the daemon settings-store's ONE
|
||||
// registered dynamic key class: `feed.subscriptions.<hostId>.enabled`
|
||||
// (boolean; v1 per-host key set CLOSED to {enabled}; no env mapping).
|
||||
@@ -926,7 +946,7 @@ export class ConfigStore {
|
||||
root: v("topology.root") as string,
|
||||
},
|
||||
context: {
|
||||
packsRoot: v("context.packs_root") as string,
|
||||
root: v("context.root") as string,
|
||||
},
|
||||
skills: {
|
||||
root: v("skills.root") as string,
|
||||
@@ -1023,6 +1043,8 @@ export class ConfigStore {
|
||||
* default).
|
||||
*/
|
||||
resolveWithSource(key: string): ResolvedSetting {
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) throw new Error(removedMessage);
|
||||
// OPR.0.4.4.15: dynamic class resolves file-or-default (no env in v1).
|
||||
const feedHost = parseFeedHostSubscriptionKey(key);
|
||||
if (feedHost) {
|
||||
@@ -1096,6 +1118,8 @@ export class ConfigStore {
|
||||
}
|
||||
|
||||
set(key: string, value: string): void {
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) throw new Error(removedMessage);
|
||||
// OPR.0.4.4.15: the registered dynamic class is accepted; every other
|
||||
// unknown key keeps the reject-loud behavior below byte-for-byte.
|
||||
const feedHost = parseFeedHostSubscriptionKey(key);
|
||||
@@ -1132,6 +1156,8 @@ export class ConfigStore {
|
||||
try { unlinkSync(this.configPath); } catch { /* missing is fine */ }
|
||||
return;
|
||||
}
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) throw new Error(removedMessage);
|
||||
// OPR.0.4.4.15: dynamic-class reset removes the whole host node.
|
||||
const feedHost = parseFeedHostSubscriptionKey(key);
|
||||
if (feedHost) {
|
||||
@@ -1158,15 +1184,19 @@ export class ConfigStore {
|
||||
}
|
||||
|
||||
private readConfigFile(): Record<string, unknown> {
|
||||
if (!existsSync(this.configPath)) return {};
|
||||
const raw = readFileSync(this.configPath, "utf-8");
|
||||
try {
|
||||
return JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Config file at ${this.configPath} is malformed. Fix the JSON or reset with: rig config reset`
|
||||
);
|
||||
let parsed: Record<string, unknown> = {};
|
||||
if (existsSync(this.configPath)) {
|
||||
const raw = readFileSync(this.configPath, "utf-8");
|
||||
try {
|
||||
parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Config file at ${this.configPath} is malformed. Fix the JSON or reset with: rig config reset`
|
||||
);
|
||||
}
|
||||
}
|
||||
assertNoRemovedContextSetting(parsed);
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,14 @@ import { existsSync } from "node:fs";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { ConfigStore } from "./config-store.js";
|
||||
import { OPENRIG_HOME, LEGACY_RIGGED_HOME, readOpenRigEnv } from "./openrig-compat.js";
|
||||
import { workspaceScaffoldDirs, workspaceScaffoldFiles } from "./commands/config-init-workspace.js";
|
||||
import {
|
||||
ensureDefaultWorkspace,
|
||||
ensureOpenRigInstance,
|
||||
formatInstanceInitializationConflicts,
|
||||
type InitializationFsOps,
|
||||
type InitWorkspaceResult,
|
||||
type ManagedPathKind,
|
||||
} from "@openrig/daemon/instance-initialization";
|
||||
|
||||
export interface DaemonState {
|
||||
pid: number;
|
||||
@@ -139,6 +146,9 @@ export interface StartOptions {
|
||||
transcriptsPath?: string;
|
||||
/** When set, daemon startup idempotently ensures the default workspace exists. */
|
||||
workspaceRoot?: string;
|
||||
contextRoot?: string;
|
||||
skillsRoot?: string;
|
||||
topologyRoot?: string;
|
||||
// V1 pre-release CLI/daemon Item 1 — capture-pane rotation tunables.
|
||||
// Threaded through to the daemon process env so the rotation hook
|
||||
// picks up file-stored ConfigStore values, not just shell env.
|
||||
@@ -167,6 +177,9 @@ export interface LifecycleDeps {
|
||||
removeFile: (path: string) => void;
|
||||
exists: (path: string) => boolean;
|
||||
mkdirp: (path: string) => void;
|
||||
/** Exact production path typing for additive initialization. Tests that do
|
||||
* not exercise collisions may omit it and use the legacy exists seam. */
|
||||
pathKind?: (path: string) => ManagedPathKind;
|
||||
openForAppend: (path: string) => number;
|
||||
isProcessAlive: (pid: number) => boolean;
|
||||
// OPR.0.4.2.1 — optional injectable delay for the status-probe bounded settle/retry.
|
||||
@@ -215,43 +228,23 @@ const STATUS_PROBE_RETRY_DELAY_MS = 200;
|
||||
// restores the timeout the removed HEALTHZ_PROBE_TIMEOUT_MS supplied to them.
|
||||
const HEALTHZ_PROBE_TIMEOUT_MS = 250;
|
||||
|
||||
export interface WorkspaceScaffoldResult {
|
||||
root: string;
|
||||
rootCreated: boolean;
|
||||
subdirs: Array<{ name: string; path: string; created: boolean }>;
|
||||
files: Array<{ relPath: string; absPath: string; created: boolean; skipped: "exists" | null }>;
|
||||
export type WorkspaceScaffoldResult = InitWorkspaceResult;
|
||||
|
||||
const INITIALIZATION_FILE_NAMES = new Set(["config.json", "SPEC.md", "project.yaml", "workspace.yaml", ".gitignore"]);
|
||||
|
||||
function lifecycleInitializationFs(deps: LifecycleDeps): InitializationFsOps {
|
||||
return {
|
||||
pathKind: deps.pathKind ?? ((candidate) => {
|
||||
if (!deps.exists(candidate)) return "missing";
|
||||
return INITIALIZATION_FILE_NAMES.has(path.basename(candidate)) ? "file" : "directory";
|
||||
}),
|
||||
mkdirp: deps.mkdirp,
|
||||
writeFile: deps.writeFile,
|
||||
};
|
||||
}
|
||||
|
||||
export function ensureWorkspaceScaffold(root: string, deps: LifecycleDeps): WorkspaceScaffoldResult {
|
||||
const rootExists = deps.exists(root);
|
||||
if (!rootExists) deps.mkdirp(root);
|
||||
|
||||
const result: WorkspaceScaffoldResult = {
|
||||
root,
|
||||
rootCreated: !rootExists,
|
||||
subdirs: [],
|
||||
files: [],
|
||||
};
|
||||
|
||||
for (const sub of workspaceScaffoldDirs()) {
|
||||
const subPath = path.join(root, sub);
|
||||
const existed = deps.exists(subPath);
|
||||
if (!existed) deps.mkdirp(subPath);
|
||||
result.subdirs.push({ name: sub, path: subPath, created: !existed });
|
||||
}
|
||||
|
||||
for (const file of workspaceScaffoldFiles()) {
|
||||
const absPath = path.join(root, file.relPath);
|
||||
const existed = deps.exists(absPath);
|
||||
if (existed) {
|
||||
result.files.push({ relPath: file.relPath, absPath, created: false, skipped: "exists" });
|
||||
continue;
|
||||
}
|
||||
deps.writeFile(absPath, file.content);
|
||||
result.files.push({ relPath: file.relPath, absPath, created: true, skipped: null });
|
||||
}
|
||||
|
||||
return result;
|
||||
return ensureDefaultWorkspace({ root, fs: lifecycleInitializationFs(deps) });
|
||||
}
|
||||
|
||||
class HealthProbeTimeoutError extends Error {
|
||||
@@ -611,9 +604,16 @@ export async function startDaemon(opts: StartOptions, deps: LifecycleDeps): Prom
|
||||
}
|
||||
const daemonEntry = path.join(getDaemonPath(), "dist/index.js");
|
||||
|
||||
deps.mkdirp(OPENRIG_DIR);
|
||||
if (opts.workspaceRoot) {
|
||||
ensureWorkspaceScaffold(opts.workspaceRoot, deps);
|
||||
const initialization = ensureOpenRigInstance({
|
||||
home: OPENRIG_DIR,
|
||||
workspaceRoot: opts.workspaceRoot,
|
||||
contextRoot: opts.contextRoot,
|
||||
skillsRoot: opts.skillsRoot,
|
||||
topologyRoot: opts.topologyRoot,
|
||||
fs: lifecycleInitializationFs(deps),
|
||||
});
|
||||
if (!initialization.ok) {
|
||||
throw new Error(`OpenRig instance initialization blocked: ${formatInstanceInitializationConflicts(initialization)}`);
|
||||
}
|
||||
|
||||
const logFd = deps.openForAppend(LOG_FILE);
|
||||
|
||||
@@ -51,6 +51,7 @@ function clearEnv(): () => void {
|
||||
"OPENRIG_WORKSPACE_STEERING_PATH", "OPENRIG_WORKSPACE_FIELD_NOTES_ROOT",
|
||||
"OPENRIG_WORKSPACE_SPECS_ROOT", "OPENRIG_DOGFOOD_EVIDENCE_ROOT",
|
||||
"OPENRIG_WORKSPACE_PROJECTS_ROOT", "OPENRIG_WORKSPACE_CATALOG_PATH",
|
||||
"OPENRIG_CONTEXT_ROOT", "OPENRIG_CONTEXT_PACKS_ROOT",
|
||||
"OPENRIG_FILES_ALLOWLIST", "OPENRIG_PROGRESS_SCAN_ROOTS",
|
||||
"OPENRIG_UI_PREVIEW_REFRESH_INTERVAL_SECONDS",
|
||||
"OPENRIG_UI_PREVIEW_MAX_PINS", "OPENRIG_UI_PREVIEW_DEFAULT_LINES",
|
||||
@@ -121,7 +122,7 @@ describe("ConfigStore — extended namespaces (User Settings v0)", () => {
|
||||
"workspace.catalog_path",
|
||||
// OPR.0.5.3.6 D1 — the topology tree root (instance at its top).
|
||||
"topology.root",
|
||||
"context.packs_root",
|
||||
"context.root",
|
||||
"skills.root",
|
||||
"onboarding.default_pack.enabled",
|
||||
"files.allowlist", "progress.scan_roots",
|
||||
@@ -180,6 +181,29 @@ describe("ConfigStore — extended namespaces (User Settings v0)", () => {
|
||||
expect([...VALID_KEYS]).toEqual(expected);
|
||||
});
|
||||
|
||||
it("resolves context.root from the canonical default and OPENRIG_CONTEXT_ROOT", () => {
|
||||
const store = new ConfigStore(configPath);
|
||||
expect(store.resolveWithSource("context.root")).toMatchObject({
|
||||
value: join(HOISTED_HOME, "context"),
|
||||
source: "default",
|
||||
});
|
||||
|
||||
process.env["OPENRIG_CONTEXT_ROOT"] = join(tmpDir, "context-library");
|
||||
expect(store.resolve().context.root).toBe(join(tmpDir, "context-library"));
|
||||
});
|
||||
|
||||
it("refuses every removed context.packs_root input with context.root guidance", () => {
|
||||
const store = new ConfigStore(configPath);
|
||||
expect(() => store.get("context.packs_root")).toThrow(/removed.*context\.root/i);
|
||||
|
||||
writeFileSync(configPath, JSON.stringify({ context: { packsRoot: "/legacy" } }));
|
||||
expect(() => store.resolve()).toThrow(/context\.packs_root.*context\.root/i);
|
||||
|
||||
writeFileSync(configPath, "{}\n");
|
||||
process.env["OPENRIG_CONTEXT_PACKS_ROOT"] = "/legacy-env";
|
||||
expect(() => store.resolve()).toThrow(/OPENRIG_CONTEXT_PACKS_ROOT.*OPENRIG_CONTEXT_ROOT/i);
|
||||
});
|
||||
|
||||
it("W2c idle-gate-qitem cadence defaults to scan=60 and active-wake=900; B6 gate defaults off/empty", () => {
|
||||
const config = new ConfigStore(configPath).resolve();
|
||||
const idleGate = (config.policies as typeof config.policies & {
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("ATOM 2 — `context add` write boundary rejects unsafe install refs BE
|
||||
const tmpHomes: string[] = [];
|
||||
const tmpSrcs: string[] = [];
|
||||
const origHome = process.env["OPENRIG_HOME"];
|
||||
const origPacksRoot = process.env["OPENRIG_CONTEXT_PACKS_ROOT"];
|
||||
const origContextRoot = process.env["OPENRIG_CONTEXT_ROOT"];
|
||||
|
||||
afterEach(() => {
|
||||
for (const d of [...tmpHomes, ...tmpSrcs]) rmSync(d, { recursive: true, force: true });
|
||||
@@ -66,15 +66,15 @@ describe("ATOM 2 — `context add` write boundary rejects unsafe install refs BE
|
||||
tmpSrcs.length = 0;
|
||||
if (origHome === undefined) delete process.env["OPENRIG_HOME"];
|
||||
else process.env["OPENRIG_HOME"] = origHome;
|
||||
if (origPacksRoot === undefined) delete process.env["OPENRIG_CONTEXT_PACKS_ROOT"];
|
||||
else process.env["OPENRIG_CONTEXT_PACKS_ROOT"] = origPacksRoot;
|
||||
if (origContextRoot === undefined) delete process.env["OPENRIG_CONTEXT_ROOT"];
|
||||
else process.env["OPENRIG_CONTEXT_ROOT"] = origContextRoot;
|
||||
});
|
||||
|
||||
function isolatedHome(): string {
|
||||
const home = mkdtempSync(join(tmpdir(), "ctx-add-home-"));
|
||||
tmpHomes.push(home);
|
||||
process.env["OPENRIG_HOME"] = home;
|
||||
process.env["OPENRIG_CONTEXT_PACKS_ROOT"] = join(home, "context-packs");
|
||||
process.env["OPENRIG_CONTEXT_ROOT"] = join(home, "context");
|
||||
return home;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("ATOM 2 — `context add` write boundary rejects unsafe install refs BE
|
||||
expect(errLogs.join("\n")).toMatch(/unsafe/);
|
||||
// the REAL pin: rejection happened BEFORE any fs mutation — the target
|
||||
// store root was never created, nothing was copied anywhere under HOME
|
||||
expect(existsSync(join(home, "context-packs")), "no store root created").toBe(false);
|
||||
expect(existsSync(join(home, "context")), "no store root created").toBe(false);
|
||||
expect(readdirSync(home), "isolated HOME untouched").toEqual([]);
|
||||
});
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("ATOM 2 — `context add` write boundary rejects unsafe install refs BE
|
||||
} catch { /* daemon sync may fail against the mock-less port — the WRITE has already happened */ }
|
||||
console.error = origErr;
|
||||
console.log = origLog;
|
||||
expect(existsSync(join(home, "context-packs", "packs", "compaction-restore", "manifest.yaml"))).toBe(true);
|
||||
expect(existsSync(join(home, "context", "packs", "compaction-restore", "manifest.yaml"))).toBe(true);
|
||||
});
|
||||
|
||||
// Slice-03 lineage repair (R2 terminal HIGH-1): a lexically-safe path-like ref
|
||||
@@ -134,7 +134,7 @@ describe("ATOM 2 — `context add` write boundary rejects unsafe install refs BE
|
||||
tmpSrcs.push(src);
|
||||
const outside = mkdtempSync(join(tmpdir(), "ctx-add-outside-"));
|
||||
tmpSrcs.push(outside);
|
||||
const storeRoot = join(home, "context-packs");
|
||||
const storeRoot = join(home, "context");
|
||||
mkdirSync(storeRoot, { recursive: true });
|
||||
symlinkSync(outside, join(storeRoot, "linked")); // attacker-planted namespace segment
|
||||
let failed = false;
|
||||
@@ -159,7 +159,7 @@ describe("ATOM 2 — `context add` write boundary rejects unsafe install refs BE
|
||||
const home = isolatedHome();
|
||||
const src = validSourcePack();
|
||||
tmpSrcs.push(src);
|
||||
const storeRoot = join(home, "context-packs");
|
||||
const storeRoot = join(home, "context");
|
||||
const outside = join(home, "outside-missing");
|
||||
const leaf = join(storeRoot, "dangle");
|
||||
mkdirSync(storeRoot, { recursive: true });
|
||||
@@ -201,6 +201,6 @@ describe("ATOM 2 — `context add` write boundary rejects unsafe install refs BE
|
||||
process.exitCode = origExit;
|
||||
expect(failed, "add must fail on an unsafe version").toBe(true);
|
||||
expect(errLogs.join("\n")).toMatch(/version/i);
|
||||
expect(existsSync(join(home, "context-packs")), "no store root created — rejected before write").toBe(false);
|
||||
expect(existsSync(join(home, "context")), "no store root created — rejected before write").toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -72,7 +72,7 @@ const FIXTURE_PACK = {
|
||||
purpose: "Smoke test pack",
|
||||
taxonomy: "mission",
|
||||
sourceType: "user_file" as const,
|
||||
sourcePath: "/home/op/.openrig/context-packs/smoke",
|
||||
sourcePath: "/home/op/.openrig/context/smoke",
|
||||
relativePath: "packs/smoke",
|
||||
updatedAt: "2026-05-04T00:00:00Z",
|
||||
manifestEstimatedTokens: null,
|
||||
@@ -506,25 +506,25 @@ files:
|
||||
return { baseUrl: `http://127.0.0.1:${port2}/`, close: () => srv.close() };
|
||||
}
|
||||
|
||||
async function withPacksRoot(fn: (root: string) => Promise<void>): Promise<void> {
|
||||
const root = mkdtempSync(join(tmpdir(), "openrig-r4-packsroot-"));
|
||||
const saved = process.env["OPENRIG_CONTEXT_PACKS_ROOT"];
|
||||
process.env["OPENRIG_CONTEXT_PACKS_ROOT"] = root;
|
||||
async function withContextRoot(fn: (root: string) => Promise<void>): Promise<void> {
|
||||
const root = mkdtempSync(join(tmpdir(), "openrig-r4-context-root-"));
|
||||
const saved = process.env["OPENRIG_CONTEXT_ROOT"];
|
||||
process.env["OPENRIG_CONTEXT_ROOT"] = root;
|
||||
try {
|
||||
await fn(root);
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env["OPENRIG_CONTEXT_PACKS_ROOT"];
|
||||
else process.env["OPENRIG_CONTEXT_PACKS_ROOT"] = saved;
|
||||
if (saved === undefined) delete process.env["OPENRIG_CONTEXT_ROOT"];
|
||||
else process.env["OPENRIG_CONTEXT_ROOT"] = saved;
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const R4_MANIFEST = "name: url-pack\nversion: 1.0.0\ntaxonomy: world\nfiles:\n - path: SKILL.md\n role: instruction\n";
|
||||
|
||||
it("add <url>: installs into the OPENRIG_CONTEXT_PACKS_ROOT landing zone (config-resolved, no hardcoded path)", async () => {
|
||||
it("add <url>: installs into the OPENRIG_CONTEXT_ROOT landing zone (config-resolved, no hardcoded path)", async () => {
|
||||
const pack = await startPackServer({ "manifest.yaml": R4_MANIFEST, "SKILL.md": "# hello\nbody\n" });
|
||||
try {
|
||||
await withPacksRoot(async (root) => {
|
||||
await withContextRoot(async (root) => {
|
||||
const { exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "context", "add", `${pack.baseUrl}manifest.yaml`]);
|
||||
});
|
||||
@@ -545,7 +545,7 @@ files:
|
||||
{ "redirect-manifest": "/pack/manifest.yaml" },
|
||||
);
|
||||
try {
|
||||
await withPacksRoot(async (root) => {
|
||||
await withContextRoot(async (root) => {
|
||||
const { exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "context", "add", `${pack.baseUrl}redirect-manifest`, "--name", "redirected-pack"]);
|
||||
});
|
||||
@@ -563,7 +563,7 @@ files:
|
||||
const absManifest = "name: evil-pack\nversion: 1.0.0\ntaxonomy: world\nfiles:\n - path: http://127.0.0.1:1/secret.md\n role: instruction\n";
|
||||
const pack = await startPackServer({ "manifest.yaml": absManifest });
|
||||
try {
|
||||
await withPacksRoot(async (root) => {
|
||||
await withContextRoot(async (root) => {
|
||||
const { errLogs, exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "context", "add", `${pack.baseUrl}manifest.yaml`]);
|
||||
});
|
||||
@@ -577,7 +577,7 @@ files:
|
||||
});
|
||||
|
||||
it("add <url>: unreachable URL fails loud with the reason and leaves NO partial pack", async () => {
|
||||
await withPacksRoot(async (root) => {
|
||||
await withContextRoot(async (root) => {
|
||||
const { errLogs, exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "context", "add", "http://127.0.0.1:1/manifest.yaml"]);
|
||||
});
|
||||
@@ -590,7 +590,7 @@ files:
|
||||
it("add <url>: malformed manifest fails loud with the reason and leaves NO partial pack", async () => {
|
||||
const pack = await startPackServer({ "manifest.yaml": "name: bad\nversion: 1.0.0\ntaxonomy: world\nfiles:\n - path: ../escape.md\n role: x\n" });
|
||||
try {
|
||||
await withPacksRoot(async (root) => {
|
||||
await withContextRoot(async (root) => {
|
||||
const { errLogs, exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "context", "add", `${pack.baseUrl}manifest.yaml`]);
|
||||
});
|
||||
@@ -607,7 +607,7 @@ files:
|
||||
// manifest declares SKILL.md but the server does not serve it
|
||||
const pack = await startPackServer({ "manifest.yaml": R4_MANIFEST });
|
||||
try {
|
||||
await withPacksRoot(async (root) => {
|
||||
await withContextRoot(async (root) => {
|
||||
const { errLogs, exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "context", "add", `${pack.baseUrl}manifest.yaml`]);
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("Daemon Lifecycle", () => {
|
||||
expect(opts.detached).toBe(true);
|
||||
});
|
||||
|
||||
it("start: idempotently scaffolds configured workspace before spawning", async () => {
|
||||
it("start: initializes the canonical instance and configured workspace before spawning", async () => {
|
||||
const created = new Set<string>();
|
||||
const written = new Map<string, string>();
|
||||
const deps = startableDeps({
|
||||
@@ -99,8 +99,32 @@ describe("Daemon Lifecycle", () => {
|
||||
mkdirp: vi.fn((p: string) => { created.add(p); }),
|
||||
writeFile: vi.fn((p: string, content: string) => { written.set(p, content); }),
|
||||
});
|
||||
await startDaemon({ port: 7433, db: "openrig.sqlite", workspaceRoot: "/tmp/openrig-workspace" }, deps);
|
||||
await startDaemon({
|
||||
port: 7433,
|
||||
db: "openrig.sqlite",
|
||||
workspaceRoot: "/tmp/openrig-workspace",
|
||||
contextRoot: "/tmp/openrig-context",
|
||||
skillsRoot: "/tmp/openrig-skills",
|
||||
topologyRoot: "/tmp/openrig-topology",
|
||||
}, deps);
|
||||
|
||||
for (const root of [
|
||||
path.join(OPENRIG_DIR, "state"),
|
||||
"/tmp/openrig-context",
|
||||
path.join("/tmp/openrig-context", "system"),
|
||||
"/tmp/openrig-skills",
|
||||
path.join(OPENRIG_DIR, "specs"),
|
||||
"/tmp/openrig-topology",
|
||||
path.join(OPENRIG_DIR, "plugins"),
|
||||
path.join(OPENRIG_DIR, "run"),
|
||||
path.join(OPENRIG_DIR, "logs"),
|
||||
path.join(OPENRIG_DIR, "transcripts"),
|
||||
path.join(OPENRIG_DIR, "backups"),
|
||||
path.join(OPENRIG_DIR, "secrets"),
|
||||
]) {
|
||||
expect(created.has(root), root).toBe(true);
|
||||
}
|
||||
expect(written.get(path.join(OPENRIG_DIR, "config.json"))).toBe("{}\n");
|
||||
expect(created.has("/tmp/openrig-workspace")).toBe(true);
|
||||
expect(created.has(path.join("/tmp/openrig-workspace", "missions"))).toBe(true);
|
||||
expect(created.has(path.join("/tmp/openrig-workspace", "exhaust"))).toBe(true);
|
||||
@@ -110,6 +134,20 @@ describe("Daemon Lifecycle", () => {
|
||||
expect(written.get(path.join("/tmp/openrig-workspace", ".gitignore"))).toContain("/exhaust/");
|
||||
});
|
||||
|
||||
it("start: refuses an exact user-owned type conflict before any write or spawn", async () => {
|
||||
const conflict = "/tmp/openrig-context";
|
||||
const deps = startableDeps({
|
||||
pathKind: vi.fn((candidate: string) => candidate === conflict ? "file" : "missing"),
|
||||
});
|
||||
|
||||
await expect(startDaemon({ contextRoot: conflict }, deps)).rejects.toThrow(
|
||||
`${conflict}: expected directory, found file`,
|
||||
);
|
||||
expect(deps.mkdirp).not.toHaveBeenCalled();
|
||||
expect(deps.writeFile).not.toHaveBeenCalled();
|
||||
expect(deps.spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Test 3: start waits for healthz, writes daemon.json with pid+port+db+startedAt
|
||||
it("start: writes daemon.json with pid, port, db, startedAt after healthz", async () => {
|
||||
const deps = startableDeps();
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
"@openrig/daemon/spec-conformance": [
|
||||
"../daemon/dist/spec-conformance-surface.d.ts"
|
||||
],
|
||||
"@openrig/daemon/gateway-protocol": ["../daemon/dist/gateway-protocol-surface.d.ts"]
|
||||
"@openrig/daemon/gateway-protocol": ["../daemon/dist/gateway-protocol-surface.d.ts"],
|
||||
"@openrig/daemon/instance-initialization": [
|
||||
"../daemon/dist/instance-initialization-surface.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- world-claim: world-example-purpose -->
|
||||
A world pack situates an agent: it describes where the agent is, what exists there, and how to act.
|
||||
<!-- world-claim: world-example-install -->
|
||||
Use `rig context add <pack-directory>` to install a world pack, confirm its ref with `rig context list`, and derive the configured store with `rig config get context.packs_root` only when a path is needed.
|
||||
Use `rig context add <pack-directory>` to install a world pack, confirm its ref with `rig context list`, and derive the configured store with `rig config get context.root` only when a path is needed.
|
||||
|
||||
<!-- world-claim: world-example-authoring -->
|
||||
Copy this pack, update its manifest, and replace each prompt below with your world's facts.
|
||||
|
||||
@@ -124,7 +124,7 @@ claims:
|
||||
kind: judgment
|
||||
flagged: "Authored purpose and taste; no live scan can prove it."
|
||||
- id: world-example-install
|
||||
statement: "Use `rig context add <pack-directory>` to install a world pack, confirm its ref with `rig context list`, and derive the configured store with `rig config get context.packs_root` only when a path is needed."
|
||||
statement: "Use `rig context add <pack-directory>` to install a world pack, confirm its ref with `rig context list`, and derive the configured store with `rig config get context.root` only when a path is needed."
|
||||
kind: operational
|
||||
check: world-example-install
|
||||
- id: world-example-authoring
|
||||
|
||||
@@ -368,19 +368,19 @@ else
|
||||
fail private-ref-boundary 'the world-public or local-world namespace projection is false'
|
||||
fi
|
||||
|
||||
configured_packs_root=$(rig config get context.packs_root 2>/dev/null)
|
||||
configured_packs_status=$?
|
||||
configured_context_root=$(rig config get context.root 2>/dev/null)
|
||||
configured_context_status=$?
|
||||
example_install_ok=1
|
||||
[ "$configured_packs_status" -eq 0 ] || example_install_ok=0
|
||||
[ -n "$configured_packs_root" ] || example_install_ok=0
|
||||
if [ -n "${OPENRIG_CONTEXT_PACKS_ROOT:-}" ] && [ "$configured_packs_root" != "$OPENRIG_CONTEXT_PACKS_ROOT" ]; then
|
||||
[ "$configured_context_status" -eq 0 ] || example_install_ok=0
|
||||
[ -n "$configured_context_root" ] || example_install_ok=0
|
||||
if [ -n "${OPENRIG_CONTEXT_ROOT:-}" ] && [ "$configured_context_root" != "$OPENRIG_CONTEXT_ROOT" ]; then
|
||||
example_install_ok=0
|
||||
fi
|
||||
rig context add --help >/dev/null 2>&1 || example_install_ok=0
|
||||
rig context list --help >/dev/null 2>&1 || example_install_ok=0
|
||||
|
||||
probe_ref="world-public-verify-$$"
|
||||
probe_target="$configured_packs_root/$probe_ref"
|
||||
probe_target="$configured_context_root/$probe_ref"
|
||||
probe_installed=0
|
||||
cleanup_probe() {
|
||||
[ "$probe_installed" -eq 1 ] || return 0
|
||||
@@ -517,7 +517,7 @@ if [ "$example_regions_ok" -eq 1 ] &&
|
||||
grep -Fq 'rig context get world-example' "$example/your-world.md" &&
|
||||
grep -Fq 'rig context add <pack-directory>' "$example/your-world.md" &&
|
||||
grep -Fq 'rig context list' "$example/your-world.md" &&
|
||||
grep -Fq 'rig config get context.packs_root' "$example/your-world.md" &&
|
||||
grep -Fq 'rig config get context.root' "$example/your-world.md" &&
|
||||
grep -Fq "Copy this pack, update its manifest, and replace each prompt below with your world's facts." "$example/your-world.md" &&
|
||||
grep -Fq 'Run `rig context get world-example`, copy this pack, and make the project a book world by describing the writer, manuscript, sources, editorial rules, decisions, current draft state, and next useful actions in a few coherent files.' "$example/your-world.md" &&
|
||||
grep -Fq "Name the world and the agent's place in it." "$example/your-world.md" &&
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
"./skill-loadout": {
|
||||
"types": "./dist/skill-loadout-surface.d.ts",
|
||||
"import": "./dist/skill-loadout-surface.js"
|
||||
},
|
||||
"./instance-initialization": {
|
||||
"types": "./dist/instance-initialization-surface.d.ts",
|
||||
"import": "./dist/instance-initialization-surface.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -60,9 +60,9 @@ export interface RouteContextPacksInput {
|
||||
/** Relative paths to context-pack manifest.yaml files declared in the
|
||||
* bundle manifest's context_packs[] block. */
|
||||
declaredContextPacks: string[];
|
||||
/** Absolute path to the operator context-packs library (per
|
||||
* context-pack-types.ts:9-10: typically `<openrigHome>/context-packs`
|
||||
* or a workspace-local `.openrig/context-packs`). Caller resolves. */
|
||||
/** Absolute path to the operator context library (per
|
||||
* context-pack-types.ts:9-10: typically `<context.root>`; the caller may
|
||||
* also route to a workspace-local `.openrig/context-packs`). */
|
||||
targetContextPacksDir: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// reviewable, sendable. Parallel to skills + workflow_specs in shape.
|
||||
//
|
||||
// MVP single-host context: storage filesystem-canonical at
|
||||
// ~/.openrig/context-packs/<name>/ (host) + workspace-local
|
||||
// <context.root>/<ref>/ (host; default $OPENRIG_HOME/context) + workspace-local
|
||||
// .openrig/context-packs/<name>/. NO new SQLite tables; library cache
|
||||
// is in-memory at the daemon scope.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Rig Context / Composable Context Injection v0 (PL-014) — manifest
|
||||
// parser.
|
||||
//
|
||||
// Parses ~/.openrig/context-packs/<name>/manifest.yaml into a typed
|
||||
// Parses <context.root>/<ref>/manifest.yaml into a typed
|
||||
// shape with structured-error rejects on malformed input. Pure
|
||||
// (no fs touches in the parser itself; caller hands in raw YAML).
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
ensureDefaultWorkspace,
|
||||
nodeInitializationFs,
|
||||
type InitializationConflict,
|
||||
type InitializationFsOps,
|
||||
} from "./workspace/default-workspace-scaffold.js";
|
||||
|
||||
export interface OpenRigInstanceInitializationOptions {
|
||||
home: string;
|
||||
workspaceRoot?: string;
|
||||
contextRoot?: string;
|
||||
skillsRoot?: string;
|
||||
topologyRoot?: string;
|
||||
dryRun?: boolean;
|
||||
fs?: InitializationFsOps;
|
||||
}
|
||||
|
||||
export interface OpenRigInstanceInitializationResult {
|
||||
ok: boolean;
|
||||
home: string;
|
||||
createdPaths: string[];
|
||||
conflicts: InitializationConflict[];
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
export function openRigContextLibraryRoots(contextRoot: string): [string, string] {
|
||||
return [contextRoot, join(contextRoot, "system")];
|
||||
}
|
||||
|
||||
/** Reconcile one canonical OpenRig instance layout. This is the shared owner
|
||||
* for CLI setup and daemon first-start. It owns empty roots and config.json;
|
||||
* S01 owns workspace contents, S04 owns skill contents, and topology owns its
|
||||
* contents. */
|
||||
export function ensureOpenRigInstance(
|
||||
options: OpenRigInstanceInitializationOptions,
|
||||
): OpenRigInstanceInitializationResult {
|
||||
const fs = options.fs ?? nodeInitializationFs();
|
||||
const dryRun = options.dryRun ?? false;
|
||||
const workspaceRoot = options.workspaceRoot ?? join(options.home, "workspace");
|
||||
const contextRoot = options.contextRoot ?? join(options.home, "context");
|
||||
const skillsRoot = options.skillsRoot ?? join(options.home, "skills");
|
||||
const topologyRoot = options.topologyRoot ?? join(options.home, "topology");
|
||||
const [contextLibraryRoot, systemContextRoot] = openRigContextLibraryRoots(contextRoot);
|
||||
const directories = [...new Set([
|
||||
options.home,
|
||||
join(options.home, "state"),
|
||||
contextLibraryRoot,
|
||||
systemContextRoot,
|
||||
skillsRoot,
|
||||
join(options.home, "specs"),
|
||||
topologyRoot,
|
||||
join(options.home, "plugins"),
|
||||
join(options.home, "run"),
|
||||
join(options.home, "logs"),
|
||||
join(options.home, "transcripts"),
|
||||
join(options.home, "backups"),
|
||||
join(options.home, "secrets"),
|
||||
])];
|
||||
const configPath = join(options.home, "config.json");
|
||||
const conflicts: InitializationConflict[] = [];
|
||||
|
||||
for (const path of directories) {
|
||||
const actual = fs.pathKind(path);
|
||||
if (actual !== "missing" && actual !== "directory") {
|
||||
conflicts.push({ path, expected: "directory", actual });
|
||||
}
|
||||
}
|
||||
const configKind = fs.pathKind(configPath);
|
||||
if (configKind !== "missing" && configKind !== "file") {
|
||||
conflicts.push({ path: configPath, expected: "file", actual: configKind });
|
||||
}
|
||||
|
||||
const workspacePlan = ensureDefaultWorkspace({ root: workspaceRoot, dryRun: true, fs });
|
||||
conflicts.push(...workspacePlan.conflicts);
|
||||
if (conflicts.length > 0) {
|
||||
return { ok: false, home: options.home, createdPaths: [], conflicts, dryRun };
|
||||
}
|
||||
|
||||
const missingDirectories = directories.filter((path) => fs.pathKind(path) === "missing");
|
||||
const createdPaths = [
|
||||
...missingDirectories,
|
||||
...(configKind === "missing" ? [configPath] : []),
|
||||
...(workspacePlan.rootCreated ? [workspaceRoot] : []),
|
||||
...workspacePlan.subdirs.filter((entry) => entry.created).map((entry) => entry.path),
|
||||
...workspacePlan.files.filter((entry) => entry.created).map((entry) => entry.absPath),
|
||||
];
|
||||
|
||||
if (!dryRun) {
|
||||
for (const path of missingDirectories) fs.mkdirp(path);
|
||||
if (configKind === "missing") fs.writeFile(configPath, "{}\n");
|
||||
ensureDefaultWorkspace({ root: workspaceRoot, fs });
|
||||
}
|
||||
|
||||
return { ok: true, home: options.home, createdPaths, conflicts: [], dryRun };
|
||||
}
|
||||
|
||||
export function formatInstanceInitializationConflicts(
|
||||
result: Pick<OpenRigInstanceInitializationResult, "conflicts">,
|
||||
): string {
|
||||
return result.conflicts
|
||||
.map((conflict) => `${conflict.path}: expected ${conflict.expected}, found ${conflict.actual}`)
|
||||
.join("; ");
|
||||
}
|
||||
@@ -37,11 +37,10 @@ const DEFAULT_TOPOLOGY_ROOT = path.join(
|
||||
"topology",
|
||||
);
|
||||
|
||||
// OPR.0.5.3.7 R4 — the context-pack library landing zone's derived default,
|
||||
// under $OPENRIG_HOME, matching the CLI twin's getDefaultOpenRigPath("context-packs").
|
||||
const DEFAULT_CONTEXT_PACKS_ROOT = path.join(
|
||||
// OPR.0.5.9.5 Wave B — canonical addressable context library.
|
||||
const DEFAULT_CONTEXT_ROOT = path.join(
|
||||
process.env["OPENRIG_HOME"] || process.env["RIGGED_HOME"] || path.join(os.homedir(), ".openrig"),
|
||||
"context-packs",
|
||||
"context",
|
||||
);
|
||||
|
||||
const DEFAULT_SKILLS_ROOT = path.join(
|
||||
@@ -102,8 +101,8 @@ export const SETTINGS_VALID_KEYS = [
|
||||
// for that literal, so walkers carry none. Lockstep with the CLI
|
||||
// config-store twin (each side's parity test pins its own list).
|
||||
"topology.root",
|
||||
// OPR.0.5.3.7 R4 — context-pack landing zone; lockstep with the CLI twin.
|
||||
"context.packs_root",
|
||||
// OPR.0.5.9.5 Wave B — canonical context library; old key is refused.
|
||||
"context.root",
|
||||
"skills.root",
|
||||
"onboarding.default_pack.enabled",
|
||||
"files.allowlist",
|
||||
@@ -228,8 +227,7 @@ const ENV_MAP: Record<SettingsValidKey, { primary: string; legacy?: string }> =
|
||||
"workspace.projects_root": { primary: "OPENRIG_WORKSPACE_PROJECTS_ROOT" },
|
||||
"workspace.catalog_path": { primary: "OPENRIG_WORKSPACE_CATALOG_PATH" },
|
||||
"topology.root": { primary: "OPENRIG_TOPOLOGY_ROOT" },
|
||||
// OPR.0.5.3.7 R4 — new key, OPENRIG_* only (no RIGGED_* legacy).
|
||||
"context.packs_root": { primary: "OPENRIG_CONTEXT_PACKS_ROOT" },
|
||||
"context.root": { primary: "OPENRIG_CONTEXT_ROOT" },
|
||||
"skills.root": { primary: "OPENRIG_SKILLS_ROOT" },
|
||||
"onboarding.default_pack.enabled": { primary: "OPENRIG_ONBOARDING_DEFAULT_PACK_ENABLED" },
|
||||
"files.allowlist": { primary: "OPENRIG_FILES_ALLOWLIST" },
|
||||
@@ -306,7 +304,7 @@ const KEY_TO_PATH: Record<SettingsValidKey, string[]> = {
|
||||
"workspace.projects_root": ["workspace", "projectsRoot"],
|
||||
"workspace.catalog_path": ["workspace", "catalogPath"],
|
||||
"topology.root": ["topology", "root"],
|
||||
"context.packs_root": ["context", "packsRoot"],
|
||||
"context.root": ["context", "root"],
|
||||
"skills.root": ["skills", "root"],
|
||||
"onboarding.default_pack.enabled": ["onboarding", "defaultPack", "enabled"],
|
||||
"files.allowlist": ["files", "allowlist"],
|
||||
@@ -371,6 +369,28 @@ export function isSettingsValidKey(key: string): key is SettingsValidKey {
|
||||
return (SETTINGS_VALID_KEYS as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
const REMOVED_CONTEXT_KEY = "context.packs_root";
|
||||
const REMOVED_CONTEXT_ENV = "OPENRIG_CONTEXT_PACKS_ROOT";
|
||||
|
||||
export function removedContextSettingMessage(key: string): string | null {
|
||||
return key === REMOVED_CONTEXT_KEY
|
||||
? 'Config key "context.packs_root" was removed; use "context.root".'
|
||||
: null;
|
||||
}
|
||||
|
||||
function assertNoRemovedContextSetting(fileConfig: Record<string, unknown>): void {
|
||||
if (process.env[REMOVED_CONTEXT_ENV]?.trim()) {
|
||||
throw new Error(
|
||||
"OPENRIG_CONTEXT_PACKS_ROOT was removed; use OPENRIG_CONTEXT_ROOT (config key context.root).",
|
||||
);
|
||||
}
|
||||
if (getNestedValue(fileConfig, ["context", "packsRoot"]) !== undefined) {
|
||||
throw new Error(
|
||||
'Config file contains removed key "context.packs_root" (context.packsRoot); replace it with "context.root" (context.root).',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── OPR.0.4.4.15 (guard G15-P1 fold, arch-endorsed) ─────────────────────────
|
||||
// ONE registered dynamic key CLASS — NOT a general dynamic-key mechanism:
|
||||
// `feed.subscriptions.<hostId>.enabled` (boolean; the v1 per-host key set is
|
||||
@@ -539,7 +559,7 @@ function getDefaultValue(key: SettingsValidKey, workspaceRoot: string): string |
|
||||
case "workspace.root": return DEFAULT_WORKSPACE_ROOT;
|
||||
// OPR.0.5.3.6 D1 — derived under $OPENRIG_HOME, never a shared-docs literal.
|
||||
case "topology.root": return DEFAULT_TOPOLOGY_ROOT;
|
||||
case "context.packs_root": return DEFAULT_CONTEXT_PACKS_ROOT;
|
||||
case "context.root": return DEFAULT_CONTEXT_ROOT;
|
||||
case "skills.root": return DEFAULT_SKILLS_ROOT;
|
||||
case "onboarding.default_pack.enabled": return true;
|
||||
// Preview Terminal v0 (PL-018) defaults — match cli/src/config-store.ts.
|
||||
@@ -772,6 +792,8 @@ export interface ClaudeCompactionPolicy {
|
||||
|
||||
export interface ResolvedConfig {
|
||||
skillsRoot: string;
|
||||
contextRoot: string;
|
||||
topologyRoot: string;
|
||||
workspaceRoot: string;
|
||||
workspaceSlicesRoot: string;
|
||||
workspaceSteeringPath: string;
|
||||
@@ -802,6 +824,7 @@ export class SettingsStore {
|
||||
|
||||
resolveOne(key: SettingsValidKey, fileConfig?: Record<string, unknown>, workspaceRoot?: string): ResolvedSetting {
|
||||
const fc = fileConfig ?? this.readConfigFile();
|
||||
assertNoRemovedContextSetting(fc);
|
||||
const wr = workspaceRoot ?? this.resolveWorkspaceRootRaw(fc);
|
||||
const defaultValue = getDefaultValue(key, wr);
|
||||
// Slice 27 BLOCKING-FIX-2 — env override is validated; on invalid
|
||||
@@ -857,6 +880,8 @@ export class SettingsStore {
|
||||
const wr = this.resolveWorkspaceRootRaw(fc);
|
||||
return {
|
||||
skillsRoot: this.resolveOne("skills.root", fc, wr).value as string,
|
||||
contextRoot: this.resolveOne("context.root", fc, wr).value as string,
|
||||
topologyRoot: this.resolveOne("topology.root", fc, wr).value as string,
|
||||
workspaceRoot: wr,
|
||||
workspaceSlicesRoot: this.resolveOne("workspace.slices_root", fc, wr).value as string,
|
||||
workspaceSteeringPath: this.resolveOne("workspace.steering_path", fc, wr).value as string,
|
||||
@@ -916,6 +941,8 @@ export class SettingsStore {
|
||||
}
|
||||
|
||||
set(key: string, value: string): void {
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) throw new Error(removedMessage);
|
||||
// OPR.0.4.4.15: the ONE registered dynamic class is accepted here;
|
||||
// every OTHER unknown key keeps the reject-loud behavior below
|
||||
// byte-for-byte.
|
||||
@@ -986,6 +1013,8 @@ export class SettingsStore {
|
||||
try { unlinkSync(this.configPath); } catch { /* missing is fine */ }
|
||||
return;
|
||||
}
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) throw new Error(removedMessage);
|
||||
// OPR.0.4.4.15: dynamic-class reset removes the whole host node
|
||||
// (unsubscribe leaves no residue).
|
||||
const feedHost = parseFeedHostSubscriptionKey(key);
|
||||
@@ -1019,15 +1048,19 @@ export class SettingsStore {
|
||||
}
|
||||
|
||||
private readConfigFile(): Record<string, unknown> {
|
||||
if (!existsSync(this.configPath)) return {};
|
||||
const raw = readFileSync(this.configPath, "utf-8");
|
||||
try {
|
||||
return JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Config file at ${this.configPath} is malformed. Fix the JSON or reset with: rig config reset`,
|
||||
);
|
||||
let parsed: Record<string, unknown> = {};
|
||||
if (existsSync(this.configPath)) {
|
||||
const raw = readFileSync(this.configPath, "utf-8");
|
||||
try {
|
||||
parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Config file at ${this.configPath} is malformed. Fix the JSON or reset with: rig config reset`,
|
||||
);
|
||||
}
|
||||
}
|
||||
assertNoRemovedContextSetting(parsed);
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Canonical daemon-side project-workspace scaffold.
|
||||
//
|
||||
// Keep this byte-compatible with
|
||||
// packages/cli/src/commands/config-init-workspace.ts. The two packages do not
|
||||
// depend on one another, and the parity test pins their shared output.
|
||||
// Canonical project-workspace scaffold. S01 owns these bytes; S05's instance
|
||||
// initializer calls this owner rather than copying its behavior.
|
||||
|
||||
import { lstatSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const WORKSPACE_DIRS = ["missions", "exhaust"] as const;
|
||||
|
||||
@@ -52,3 +52,102 @@ export function workspaceScaffoldFiles(): Array<{ relPath: string; content: stri
|
||||
{ relPath: ".gitignore", content: WORKSPACE_GITIGNORE },
|
||||
];
|
||||
}
|
||||
|
||||
export type ManagedPathKind = "missing" | "file" | "directory" | "other";
|
||||
|
||||
export interface InitializationFsOps {
|
||||
pathKind(path: string): ManagedPathKind;
|
||||
mkdirp(path: string): void;
|
||||
writeFile(path: string, content: string): void;
|
||||
}
|
||||
|
||||
export interface InitializationConflict {
|
||||
path: string;
|
||||
expected: "file" | "directory";
|
||||
actual: Exclude<ManagedPathKind, "missing">;
|
||||
}
|
||||
|
||||
export interface InitWorkspaceResult {
|
||||
ok: boolean;
|
||||
root: string;
|
||||
rootCreated: boolean;
|
||||
subdirs: Array<{ name: string; path: string; created: boolean }>;
|
||||
files: Array<{ relPath: string; absPath: string; created: boolean; skipped: "exists" | null }>;
|
||||
conflicts: InitializationConflict[];
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
export function nodeInitializationFs(): InitializationFsOps {
|
||||
return {
|
||||
pathKind(path) {
|
||||
try {
|
||||
const value = lstatSync(path);
|
||||
if (value.isDirectory()) return "directory";
|
||||
if (value.isFile()) return "file";
|
||||
return "other";
|
||||
} catch {
|
||||
return "missing";
|
||||
}
|
||||
},
|
||||
mkdirp: (path) => mkdirSync(path, { recursive: true }),
|
||||
writeFile: (path, content) => writeFileSync(path, content, "utf8"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Additively reconcile the S01 project workspace. All collisions are found
|
||||
* before the first write, so a malformed user-owned path never leaves a
|
||||
* half-created scaffold. */
|
||||
export function ensureDefaultWorkspace(options: {
|
||||
root: string;
|
||||
dryRun?: boolean;
|
||||
fs?: InitializationFsOps;
|
||||
}): InitWorkspaceResult {
|
||||
const fs = options.fs ?? nodeInitializationFs();
|
||||
const dryRun = options.dryRun ?? false;
|
||||
const rootKind = fs.pathKind(options.root);
|
||||
const subdirs = workspaceScaffoldDirs().map((name) => {
|
||||
const path = join(options.root, name);
|
||||
return { name, path, created: fs.pathKind(path) === "missing" };
|
||||
});
|
||||
const files = workspaceScaffoldFiles().map(({ relPath }) => {
|
||||
const absPath = join(options.root, relPath);
|
||||
const created = fs.pathKind(absPath) === "missing";
|
||||
return { relPath, absPath, created, skipped: created ? null : "exists" as const };
|
||||
});
|
||||
const conflicts: InitializationConflict[] = [];
|
||||
|
||||
if (rootKind !== "missing" && rootKind !== "directory") {
|
||||
conflicts.push({ path: options.root, expected: "directory", actual: rootKind });
|
||||
}
|
||||
for (const subdir of subdirs) {
|
||||
const actual = fs.pathKind(subdir.path);
|
||||
if (actual !== "missing" && actual !== "directory") {
|
||||
conflicts.push({ path: subdir.path, expected: "directory", actual });
|
||||
}
|
||||
}
|
||||
for (const file of files) {
|
||||
const actual = fs.pathKind(file.absPath);
|
||||
if (actual !== "missing" && actual !== "file") {
|
||||
conflicts.push({ path: file.absPath, expected: "file", actual });
|
||||
}
|
||||
}
|
||||
|
||||
if (conflicts.length === 0 && !dryRun) {
|
||||
if (rootKind === "missing") fs.mkdirp(options.root);
|
||||
for (const subdir of subdirs) if (subdir.created) fs.mkdirp(subdir.path);
|
||||
for (const file of workspaceScaffoldFiles()) {
|
||||
const absPath = join(options.root, file.relPath);
|
||||
if (fs.pathKind(absPath) === "missing") fs.writeFile(absPath, file.content);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: conflicts.length === 0,
|
||||
root: options.root,
|
||||
rootCreated: rootKind === "missing",
|
||||
subdirs,
|
||||
files,
|
||||
conflicts,
|
||||
dryRun,
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,11 @@ import {
|
||||
} from "./middleware/auth-bearer-token.js";
|
||||
import type { SlowOperationInstrumentation } from "./domain/slow-op-recorder.js";
|
||||
import type { ProviderService } from "./domain/provider/provider-service.js";
|
||||
import { SettingsStore } from "./domain/user-settings/settings-store.js";
|
||||
import {
|
||||
ensureOpenRigInstance,
|
||||
formatInstanceInitializationConflicts,
|
||||
} from "./domain/instance-initialization.js";
|
||||
|
||||
/** OPR.0.4.3.21 (51elv2) — bound the graceful-shutdown recorder drain. */
|
||||
export const SLOW_OP_SHUTDOWN_DRAIN_TIMEOUT_MS = 5_000;
|
||||
@@ -230,6 +235,17 @@ export async function startServer(port?: number) {
|
||||
// D15 — anchor the default db under OPENRIG_HOME, never a bare CWD-relative
|
||||
// filename (which could open the shared fleet db). Explicit OPENRIG_DB wins.
|
||||
const dbPath = resolveDaemonDbPath(readOpenRigEnv("OPENRIG_DB", "RIGGED_DB"), OPENRIG_HOME);
|
||||
const instanceConfig = new SettingsStore().resolveConfig();
|
||||
const initialization = ensureOpenRigInstance({
|
||||
home: OPENRIG_HOME,
|
||||
workspaceRoot: instanceConfig.workspaceRoot,
|
||||
contextRoot: instanceConfig.contextRoot,
|
||||
skillsRoot: instanceConfig.skillsRoot,
|
||||
topologyRoot: instanceConfig.topologyRoot,
|
||||
});
|
||||
if (!initialization.ok) {
|
||||
throw new Error(`OpenRig instance initialization blocked: ${formatInstanceInitializationConflicts(initialization)}`);
|
||||
}
|
||||
|
||||
// bug-fix slice auth-bearer-tailscale-trust: distinguish "explicit
|
||||
// operator opt-in" from "default" by treating an undefined env as
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export {
|
||||
ensureOpenRigInstance,
|
||||
formatInstanceInitializationConflicts,
|
||||
type OpenRigInstanceInitializationOptions,
|
||||
type OpenRigInstanceInitializationResult,
|
||||
} from "./domain/instance-initialization.js";
|
||||
export {
|
||||
ensureDefaultWorkspace,
|
||||
nodeInitializationFs,
|
||||
workspaceScaffoldDirs,
|
||||
workspaceScaffoldFiles,
|
||||
type InitializationConflict,
|
||||
type InitializationFsOps,
|
||||
type InitWorkspaceResult,
|
||||
type ManagedPathKind,
|
||||
} from "./domain/workspace/default-workspace-scaffold.js";
|
||||
@@ -456,11 +456,9 @@ async function routeAgentImagesAfterBootstrap(bundlePath: string): Promise<Route
|
||||
* the operator context-packs library. Returns null when bundle has no
|
||||
* context_packs[] (no-op).
|
||||
*
|
||||
* Target resolution: <openrigHome>/context-packs — per startup.ts:496
|
||||
* (the user-file root the live ContextPackLibraryService is constructed
|
||||
* against). No SettingsStore complexity (unlike workflow_specs);
|
||||
* context-packs canonical path is OPENRIG_HOME-rooted per
|
||||
* context-pack-types.ts:9-10.
|
||||
* Target resolution uses context.root, exactly like the live
|
||||
* ContextPackLibraryService. A configured root replaces the default; bundle
|
||||
* routing must not silently create a second writable library.
|
||||
*
|
||||
* Mirror of routeSkillsAfterBootstrap / routePluginsAfterBootstrap /
|
||||
* routeWorkflowSpecsAfterBootstrap pattern: takes bundlePath only
|
||||
@@ -468,7 +466,7 @@ async function routeAgentImagesAfterBootstrap(bundlePath: string): Promise<Route
|
||||
* fire on the dual-override path too).
|
||||
*/
|
||||
async function routeContextPacksAfterBootstrap(bundlePath: string): Promise<RouteContextPacksResult | null> {
|
||||
const targetContextPacksDir = getDefaultOpenRigPath("context-packs");
|
||||
const targetContextPacksDir = new ContextPackSettingsStore().resolveOne("context.root").value as string;
|
||||
const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-context-packs-route-"));
|
||||
try {
|
||||
await unpack(bundlePath, tmpDir);
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
// agents stay on CLI-shell-out per the shipped openrig-user-settings skill.
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
SETTINGS_VALID_KEYS,
|
||||
isSettingsValidKey,
|
||||
parseFeedHostSubscriptionKey,
|
||||
removedContextSettingMessage,
|
||||
type SettingsStore,
|
||||
} from "../domain/user-settings/settings-store.js";
|
||||
import {
|
||||
workspaceScaffoldDirs,
|
||||
workspaceScaffoldFiles,
|
||||
ensureDefaultWorkspace,
|
||||
} from "../domain/workspace/default-workspace-scaffold.js";
|
||||
|
||||
interface InitWorkspaceBody {
|
||||
@@ -41,7 +39,11 @@ export function configRoutes(): Hono {
|
||||
if (!store) return c.json({ error: "settings_unavailable" }, 503);
|
||||
// OPR.0.4.4.15: dynamic-class enumeration rides ADDITIVELY beside the
|
||||
// static settings map (existing consumers of `settings` unaffected).
|
||||
return c.json({ settings: store.resolveAllWithSource(), feedHostSubscriptions: store.listFeedHostSubscriptions() });
|
||||
try {
|
||||
return c.json({ settings: store.resolveAllWithSource(), feedHostSubscriptions: store.listFeedHostSubscriptions() });
|
||||
} catch (err) {
|
||||
return c.json({ error: (err as Error).message }, 400);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/init-workspace", async (c) => {
|
||||
@@ -50,49 +52,17 @@ export function configRoutes(): Hono {
|
||||
const body = (await c.req.json<InitWorkspaceBody>().catch(() => ({}))) as InitWorkspaceBody;
|
||||
const root = body.root ?? (store.resolveOne("workspace.root").value as string);
|
||||
const dryRun = !!body.dryRun;
|
||||
// Precompute both sides before mutation so the daemon and CLI retain the
|
||||
// same inspect-then-write shape.
|
||||
let scaffoldFiles: ReturnType<typeof workspaceScaffoldFiles>;
|
||||
let scaffoldDirs: ReturnType<typeof workspaceScaffoldDirs>;
|
||||
try {
|
||||
scaffoldFiles = workspaceScaffoldFiles();
|
||||
scaffoldDirs = workspaceScaffoldDirs();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return c.json({ error: "init_workspace_scaffold_unavailable", message }, 400);
|
||||
}
|
||||
|
||||
const subdirs: Array<{ name: string; path: string; created: boolean }> = [];
|
||||
const files: Array<{ relPath: string; absPath: string; created: boolean; skipped: "exists" | null }> = [];
|
||||
|
||||
const rootExists = existsSync(root);
|
||||
if (!rootExists && !dryRun) mkdirSync(root, { recursive: true });
|
||||
|
||||
for (const sub of scaffoldDirs) {
|
||||
const subPath = join(root, sub);
|
||||
const subExists = existsSync(subPath);
|
||||
if (!subExists && !dryRun) mkdirSync(subPath, { recursive: true });
|
||||
subdirs.push({ name: sub, path: subPath, created: !subExists });
|
||||
}
|
||||
|
||||
for (const file of scaffoldFiles) {
|
||||
const absPath = join(root, file.relPath);
|
||||
const fileExists = existsSync(absPath);
|
||||
if (fileExists) {
|
||||
files.push({ relPath: file.relPath, absPath, created: false, skipped: "exists" });
|
||||
} else {
|
||||
if (!dryRun) writeFileSync(absPath, file.content, "utf-8");
|
||||
files.push({ relPath: file.relPath, absPath, created: true, skipped: null });
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({ root, rootCreated: !rootExists, subdirs, files, dryRun });
|
||||
const result = ensureDefaultWorkspace({ root, dryRun });
|
||||
if (!result.ok) return c.json({ error: "init_workspace_conflict", ...result }, 409);
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
router.get("/:key", (c) => {
|
||||
const store = c.get("settingsStore" as never) as SettingsStore | undefined;
|
||||
if (!store) return c.json({ error: "settings_unavailable" }, 503);
|
||||
const key = c.req.param("key");
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) return c.json({ error: removedMessage, replacement: "context.root" }, 400);
|
||||
// OPR.0.4.4.15: the ONE registered dynamic class resolves here; every
|
||||
// other unknown key keeps the 400 below byte-for-byte.
|
||||
const dynamic = store.resolveFeedHostSubscription(key);
|
||||
@@ -107,6 +77,8 @@ export function configRoutes(): Hono {
|
||||
const store = c.get("settingsStore" as never) as SettingsStore | undefined;
|
||||
if (!store) return c.json({ error: "settings_unavailable" }, 503);
|
||||
const key = c.req.param("key");
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) return c.json({ error: removedMessage, replacement: "context.root" }, 400);
|
||||
const isDynamic = parseFeedHostSubscriptionKey(key) !== null;
|
||||
if (!isDynamic && !isSettingsValidKey(key)) {
|
||||
return c.json({ error: `Unknown config key '${key}'`, validKeys: SETTINGS_VALID_KEYS }, 400);
|
||||
@@ -127,6 +99,8 @@ export function configRoutes(): Hono {
|
||||
const store = c.get("settingsStore" as never) as SettingsStore | undefined;
|
||||
if (!store) return c.json({ error: "settings_unavailable" }, 503);
|
||||
const key = c.req.param("key");
|
||||
const removedMessage = removedContextSettingMessage(key);
|
||||
if (removedMessage) return c.json({ error: removedMessage, replacement: "context.root" }, 400);
|
||||
const isDynamic = parseFeedHostSubscriptionKey(key) !== null;
|
||||
if (!isDynamic && !isSettingsValidKey(key)) {
|
||||
return c.json({ error: `Unknown config key '${key}'`, validKeys: SETTINGS_VALID_KEYS }, 400);
|
||||
|
||||
@@ -109,6 +109,7 @@ import { PluginDiscoveryService } from "./domain/plugin-discovery-service.js";
|
||||
// Slice 28 Checkpoint C-3 — skill-library discovery (SC-29 #11 cumulative).
|
||||
import { SkillLibraryDiscoveryService } from "./domain/skill-library-discovery.js";
|
||||
import { ContextPackLibraryService } from "./domain/context-packs/context-pack-library-service.js";
|
||||
import { openRigContextLibraryRoots } from "./domain/instance-initialization.js";
|
||||
import { AgentImageLibraryService } from "./domain/agent-images/agent-image-library-service.js";
|
||||
import { SnapshotCapturer } from "./domain/agent-images/snapshot-capturer.js";
|
||||
import { SettingsStore as ContextPackSettingsStore } from "./domain/user-settings/settings-store.js";
|
||||
@@ -749,18 +750,21 @@ export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult>
|
||||
// routes. Startup context-pack expansion is intentionally unsupported;
|
||||
// dedicated send/broadcast/walk/queue verbs own delivery.
|
||||
const contextPackLibrary = (() => {
|
||||
// OPR.0.5.3.7 R4 — config-resolved (env > config > $OPENRIG_HOME/context-packs),
|
||||
// the same key `rig context add` writes to, so both agree on the landing zone.
|
||||
const userPacksRoot = new ContextPackSettingsStore().resolveOne("context.packs_root").value as string;
|
||||
try { fs.mkdirSync(userPacksRoot, { recursive: true }); } catch { /* best-effort */ }
|
||||
// OPR.0.5.9.5 Wave B — config-resolved canonical context library. The
|
||||
// shared instance initializer normally creates both roots; the mkdirs keep
|
||||
// direct createDaemon test harnesses compatible.
|
||||
const userPacksRoot = new ContextPackSettingsStore().resolveOne("context.root").value as string;
|
||||
const [contextRoot, systemPacksRoot] = openRigContextLibraryRoots(userPacksRoot);
|
||||
try { fs.mkdirSync(systemPacksRoot, { recursive: true }); } catch { /* best-effort */ }
|
||||
const roots: Array<{ path: string; sourceType: "builtin" | "user_file" | "workspace" }> = [
|
||||
{ path: userPacksRoot, sourceType: "user_file" },
|
||||
{ path: contextRoot, sourceType: "user_file" },
|
||||
{ path: systemPacksRoot, sourceType: "user_file" },
|
||||
];
|
||||
try {
|
||||
const settingsStore = new ContextPackSettingsStore();
|
||||
const cfg = settingsStore.resolveConfig();
|
||||
const workspacePacksRoot = nodePath.join(cfg.workspaceRoot, ".openrig", "context-packs");
|
||||
if (workspacePacksRoot !== userPacksRoot && fs.existsSync(workspacePacksRoot)) {
|
||||
if (workspacePacksRoot !== userPacksRoot && workspacePacksRoot !== systemPacksRoot && fs.existsSync(workspacePacksRoot)) {
|
||||
roots.push({ path: workspacePacksRoot, sourceType: "workspace" });
|
||||
}
|
||||
} catch { /* settings unavailable; fall through with user-file root only */ }
|
||||
|
||||
@@ -43,7 +43,7 @@ function packFixture(packParentDir: string): { dirs: string[]; files: string[] }
|
||||
}
|
||||
|
||||
const BUNDLE_ROOT = "/bundle/root";
|
||||
const TARGET = "/operator/.openrig/context-packs";
|
||||
const TARGET = "/operator/.openrig/context";
|
||||
|
||||
function makeInput(overrides?: Partial<RouteContextPacksInput>): RouteContextPacksInput {
|
||||
return {
|
||||
|
||||
@@ -2310,16 +2310,19 @@ describe("Bundle API routes", () => {
|
||||
});
|
||||
|
||||
// Item 6 / Checkpoint 7.3f step 3: /install routes declared context_packs
|
||||
// after successful bootstrap. Target = <openrigHome>/context-packs
|
||||
// after successful bootstrap. Target = the configured context.root
|
||||
// (startup.ts:496 user-file root). Bundle has context_packs[] with paths
|
||||
// to context-pack manifest.yaml files; router copies the parent dir to
|
||||
// <openrigHome>/context-packs/<dirname>/. Consumer-scan reachability
|
||||
// <context.root>/<dirname>/. Consumer-scan reachability
|
||||
// proven via real ContextPackLibraryService.scan() against the routed
|
||||
// root (guard d491eca9 + 3cd581e3 file-vs-dir discrimination lessons).
|
||||
it("POST /api/bundles/install routes declared context_packs after successful bootstrap (completed branch) — proves consumer-scan reachability", async () => {
|
||||
const origHome = process.env.OPENRIG_HOME;
|
||||
const origContextRoot = process.env.OPENRIG_CONTEXT_ROOT;
|
||||
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-context-packs-route-test-"));
|
||||
const configuredContextRoot = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-context-root-test-"));
|
||||
process.env.OPENRIG_HOME = auditHome;
|
||||
process.env.OPENRIG_CONTEXT_ROOT = configuredContextRoot;
|
||||
const origBootstrap = setup.bootstrapOrchestrator.bootstrap.bind(setup.bootstrapOrchestrator);
|
||||
try {
|
||||
const { specPath } = seedPackage();
|
||||
@@ -2385,7 +2388,7 @@ files:
|
||||
// Router copies the PARENT DIR of manifest.yaml: declared
|
||||
// "packages/test-pkg/context-packs/intent/manifest.yaml" → parent
|
||||
// basename "intent" → target/intent/
|
||||
const expectedPackDir = path.join(auditHome, "context-packs", "intent");
|
||||
const expectedPackDir = path.join(configuredContextRoot, "intent");
|
||||
expect(fs.existsSync(expectedPackDir)).toBe(true);
|
||||
expect(fs.existsSync(path.join(expectedPackDir, "manifest.yaml"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(expectedPackDir, "brief.md"))).toBe(true);
|
||||
@@ -2394,7 +2397,7 @@ files:
|
||||
// against the routed target root + assert the pack is visible.
|
||||
const { ContextPackLibraryService } = await import("../src/domain/context-packs/context-pack-library-service.js");
|
||||
const consumer = new ContextPackLibraryService({
|
||||
roots: [{ path: path.join(auditHome, "context-packs"), sourceType: "user_file" }],
|
||||
roots: [{ path: configuredContextRoot, sourceType: "user_file" }],
|
||||
});
|
||||
const scanResult = consumer.scan();
|
||||
expect(scanResult.count).toBeGreaterThanOrEqual(1);
|
||||
@@ -2412,7 +2415,10 @@ files:
|
||||
(setup.bootstrapOrchestrator as unknown as { bootstrap: typeof origBootstrap }).bootstrap = origBootstrap;
|
||||
if (origHome === undefined) delete process.env.OPENRIG_HOME;
|
||||
else process.env.OPENRIG_HOME = origHome;
|
||||
if (origContextRoot === undefined) delete process.env.OPENRIG_CONTEXT_ROOT;
|
||||
else process.env.OPENRIG_CONTEXT_ROOT = origContextRoot;
|
||||
fs.rmSync(auditHome, { recursive: true, force: true });
|
||||
fs.rmSync(configuredContextRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2421,8 +2427,10 @@ files:
|
||||
// 5f410eee decoupling lesson; PROACTIVELY shipped).
|
||||
it("POST /api/bundles/install routes declared context_packs even when both --skip-version-check and --force are set (B1 mirror)", async () => {
|
||||
const origHome = process.env.OPENRIG_HOME;
|
||||
const origContextRoot = process.env.OPENRIG_CONTEXT_ROOT;
|
||||
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-cpacks-dual-override-"));
|
||||
process.env.OPENRIG_HOME = auditHome;
|
||||
process.env.OPENRIG_CONTEXT_ROOT = path.join(auditHome, "context");
|
||||
const origBootstrap = setup.bootstrapOrchestrator.bootstrap.bind(setup.bootstrapOrchestrator);
|
||||
try {
|
||||
const { specPath } = seedPackage();
|
||||
@@ -2482,7 +2490,7 @@ files:
|
||||
expect(body.contextPacksRouting).toBeDefined();
|
||||
expect(body.contextPacksRouting.routedCount).toBe(1);
|
||||
expect(body.contextPacksRouting.records[0].status).toBe("routed");
|
||||
expect(fs.existsSync(path.join(auditHome, "context-packs", "dualcontext", "manifest.yaml"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(auditHome, "context", "dualcontext", "manifest.yaml"))).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(targetRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -2490,6 +2498,8 @@ files:
|
||||
(setup.bootstrapOrchestrator as unknown as { bootstrap: typeof origBootstrap }).bootstrap = origBootstrap;
|
||||
if (origHome === undefined) delete process.env.OPENRIG_HOME;
|
||||
else process.env.OPENRIG_HOME = origHome;
|
||||
if (origContextRoot === undefined) delete process.env.OPENRIG_CONTEXT_ROOT;
|
||||
else process.env.OPENRIG_CONTEXT_ROOT = origContextRoot;
|
||||
fs.rmSync(auditHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -2500,8 +2510,10 @@ files:
|
||||
// status=missing, NO pack copied to target.
|
||||
it("POST /api/bundles/install context_packs degenerate-input: declared manifest absent → status=missing, no false routedCount", async () => {
|
||||
const origHome = process.env.OPENRIG_HOME;
|
||||
const origContextRoot = process.env.OPENRIG_CONTEXT_ROOT;
|
||||
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-cpacks-degenerate-"));
|
||||
process.env.OPENRIG_HOME = auditHome;
|
||||
process.env.OPENRIG_CONTEXT_ROOT = path.join(auditHome, "context");
|
||||
const origBootstrap = setup.bootstrapOrchestrator.bootstrap.bind(setup.bootstrapOrchestrator);
|
||||
try {
|
||||
const { specPath } = seedPackage();
|
||||
@@ -2550,7 +2562,7 @@ files:
|
||||
expect(body.contextPacksRouting.rejectedCount).toBe(1);
|
||||
expect(body.contextPacksRouting.records[0].status).toBe("missing");
|
||||
// CRUCIAL: nothing landed at target
|
||||
expect(fs.existsSync(path.join(auditHome, "context-packs", "nonexistent"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(auditHome, "context", "nonexistent"))).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(targetRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -2558,6 +2570,8 @@ files:
|
||||
(setup.bootstrapOrchestrator as unknown as { bootstrap: typeof origBootstrap }).bootstrap = origBootstrap;
|
||||
if (origHome === undefined) delete process.env.OPENRIG_HOME;
|
||||
else process.env.OPENRIG_HOME = origHome;
|
||||
if (origContextRoot === undefined) delete process.env.OPENRIG_CONTEXT_ROOT;
|
||||
else process.env.OPENRIG_CONTEXT_ROOT = origContextRoot;
|
||||
fs.rmSync(auditHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, relative } from "node:path";
|
||||
import {
|
||||
ensureOpenRigInstance,
|
||||
openRigContextLibraryRoots,
|
||||
} from "../src/domain/instance-initialization.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
function freshRoot(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "openrig-instance-init-"));
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
function tree(root: string): Array<{ path: string; kind: "directory" | "file"; content?: string }> {
|
||||
const out: Array<{ path: string; kind: "directory" | "file"; content?: string }> = [];
|
||||
const walk = (dir: string): void => {
|
||||
for (const name of readdirSync(dir).sort()) {
|
||||
const abs = join(dir, name);
|
||||
const rel = relative(root, abs);
|
||||
if (statSync(abs).isDirectory()) {
|
||||
out.push({ path: rel, kind: "directory" });
|
||||
walk(abs);
|
||||
} else {
|
||||
out.push({ path: rel, kind: "file", content: readFileSync(abs, "utf8") });
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("canonical OpenRig instance initialization", () => {
|
||||
it("declares the canonical addressable and System World library roots together", () => {
|
||||
expect(openRigContextLibraryRoots("/instance/context")).toEqual([
|
||||
"/instance/context",
|
||||
"/instance/context/system",
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates the canonical roots plus the S01 workspace through one idempotent owner", () => {
|
||||
const parent = freshRoot();
|
||||
const home = join(parent, "home");
|
||||
|
||||
const first = ensureOpenRigInstance({ home });
|
||||
const firstTree = tree(home);
|
||||
const second = ensureOpenRigInstance({ home });
|
||||
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.createdPaths.length).toBeGreaterThan(0);
|
||||
expect(second).toMatchObject({ ok: true, createdPaths: [], conflicts: [] });
|
||||
expect(tree(home)).toEqual(firstTree);
|
||||
expect(firstTree).toEqual(expect.arrayContaining([
|
||||
{ path: "config.json", kind: "file", content: "{}\n" },
|
||||
{ path: "state", kind: "directory" },
|
||||
{ path: "context", kind: "directory" },
|
||||
{ path: "context/system", kind: "directory" },
|
||||
{ path: "skills", kind: "directory" },
|
||||
{ path: "workspace", kind: "directory" },
|
||||
{ path: "workspace/missions", kind: "directory" },
|
||||
{ path: "workspace/exhaust", kind: "directory" },
|
||||
{ path: "specs", kind: "directory" },
|
||||
{ path: "topology", kind: "directory" },
|
||||
{ path: "plugins", kind: "directory" },
|
||||
{ path: "run", kind: "directory" },
|
||||
{ path: "logs", kind: "directory" },
|
||||
{ path: "transcripts", kind: "directory" },
|
||||
{ path: "backups", kind: "directory" },
|
||||
{ path: "secrets", kind: "directory" },
|
||||
]));
|
||||
});
|
||||
|
||||
it("preserves user-owned files byte-for-byte", () => {
|
||||
const home = join(freshRoot(), "home");
|
||||
mkdirSync(join(home, "workspace"), { recursive: true });
|
||||
writeFileSync(join(home, "config.json"), "{\"operator\":true}\n");
|
||||
writeFileSync(join(home, "workspace", "SPEC.md"), "# Mine\n");
|
||||
|
||||
const result = ensureOpenRigInstance({ home });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(readFileSync(join(home, "config.json"), "utf8")).toBe("{\"operator\":true}\n");
|
||||
expect(readFileSync(join(home, "workspace", "SPEC.md"), "utf8")).toBe("# Mine\n");
|
||||
});
|
||||
|
||||
it("reports exact type conflicts and performs no partial writes", () => {
|
||||
const home = join(freshRoot(), "home");
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeFileSync(join(home, "context"), "owned file\n");
|
||||
|
||||
const result = ensureOpenRigInstance({ home });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.conflicts).toEqual([
|
||||
{ path: join(home, "context"), expected: "directory", actual: "file" },
|
||||
]);
|
||||
expect(existsSync(join(home, "state"))).toBe(false);
|
||||
expect(readFileSync(join(home, "context"), "utf8")).toBe("owned file\n");
|
||||
});
|
||||
|
||||
it("treats a user-owned symlink as a conflict instead of writing through it", () => {
|
||||
const parent = freshRoot();
|
||||
const home = join(parent, "home");
|
||||
const outside = join(parent, "outside");
|
||||
mkdirSync(home, { recursive: true });
|
||||
mkdirSync(outside);
|
||||
symlinkSync(outside, join(home, "context"));
|
||||
|
||||
const result = ensureOpenRigInstance({ home });
|
||||
|
||||
expect(result.conflicts).toContainEqual({
|
||||
path: join(home, "context"),
|
||||
expected: "directory",
|
||||
actual: "other",
|
||||
});
|
||||
expect(readdirSync(outside)).toEqual([]);
|
||||
expect(existsSync(join(home, "state"))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -177,7 +177,7 @@ function writeRigFixture(options: {
|
||||
{ relativePath: "world/install", sourceType: "user_file", sourcePath: "/fixture/private-world" },
|
||||
]);
|
||||
const configRoot = options.configRoot === undefined
|
||||
? '"${OPENRIG_CONTEXT_PACKS_ROOT:-/fixture/context-packs}"'
|
||||
? '"${OPENRIG_CONTEXT_ROOT:-/fixture/context}"'
|
||||
: `'${options.configRoot}'`;
|
||||
writeFileSync(
|
||||
join(fixturePath, "world-public-get.txt"),
|
||||
@@ -223,13 +223,13 @@ function writeRigFixture(options: {
|
||||
" fi",
|
||||
' [ "$4" = "--name" ] || exit 64',
|
||||
` if [ ${options.partialSync ? 1 : 0} -eq 1 ]; then`,
|
||||
' target="$OPENRIG_CONTEXT_PACKS_ROOT/$5"',
|
||||
' target="$OPENRIG_CONTEXT_ROOT/$5"',
|
||||
' mkdir -p "$target"',
|
||||
' printf \'%s\\n\' \'name: partial-sync-fixture\' > "$target/manifest.yaml"',
|
||||
' printf \'{"installedAt":"%s","syncError":"HTTP 503"}\\n\' "$target"',
|
||||
" exit 0",
|
||||
" fi",
|
||||
' target="$OPENRIG_CONTEXT_PACKS_ROOT/$5"',
|
||||
' target="$OPENRIG_CONTEXT_ROOT/$5"',
|
||||
' mkdir -p "$target"',
|
||||
' cp -R "$3"/. "$target"/',
|
||||
' printf \'%s\\n\' "$5" > "$install_state"',
|
||||
@@ -238,7 +238,7 @@ function writeRigFixture(options: {
|
||||
' if [ -f "$install_state" ]; then',
|
||||
' installed_ref=$(sed -n \'1p\' "$install_state")',
|
||||
' cat "$fixture_dir/context-list.json" | sed \'s/]$//\'',
|
||||
" printf ',{\"relativePath\":\"%s\",\"sourceType\":\"user_file\",\"sourcePath\":\"%s/%s\"}]\\n' \"$installed_ref\" \"$OPENRIG_CONTEXT_PACKS_ROOT\" \"$installed_ref\"",
|
||||
" printf ',{\"relativePath\":\"%s\",\"sourceType\":\"user_file\",\"sourcePath\":\"%s/%s\"}]\\n' \"$installed_ref\" \"$OPENRIG_CONTEXT_ROOT\" \"$installed_ref\"",
|
||||
" else",
|
||||
' cat "$fixture_dir/context-list.json"',
|
||||
" fi",
|
||||
@@ -246,10 +246,10 @@ function writeRigFixture(options: {
|
||||
` [ ${options.partialSync ? 1 : 0} -eq 0 ] || exit 72`,
|
||||
' [ -f "$install_state" ] || exit 1',
|
||||
' [ "$(sed -n \'1p\' "$install_state")" = "$3" ] || exit 1',
|
||||
' rm -rf -- "$OPENRIG_CONTEXT_PACKS_ROOT/$3"',
|
||||
' rm -rf -- "$OPENRIG_CONTEXT_ROOT/$3"',
|
||||
' rm -f "$install_state"',
|
||||
' printf \'{"removed":true,"ref":"%s"}\\n\' "$3"',
|
||||
'elif [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "context.packs_root" ]; then',
|
||||
'elif [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "context.root" ]; then',
|
||||
` printf '%s\\n' ${configRoot}`,
|
||||
'elif [ "$1" = "whoami" ]; then',
|
||||
` printf '%s\\n' '${options.identityJson ?? '{"identity":{"rigName":"fixture","memberId":"qa","sessionName":"qa@fixture"}}'}'`,
|
||||
@@ -425,7 +425,7 @@ describe("public world pack", () => {
|
||||
const result = runVerifier(redPack, {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -458,7 +458,7 @@ describe("public world pack", () => {
|
||||
const result = runVerifier(redPack, {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -505,7 +505,7 @@ describe("public world pack", () => {
|
||||
const result = runVerifier(redPack, {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -520,7 +520,7 @@ describe("public world pack", () => {
|
||||
const fixtureEnv = {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
};
|
||||
@@ -547,7 +547,7 @@ describe("public world pack", () => {
|
||||
const wrongIdentity = runVerifier(packDir, {
|
||||
...process.env,
|
||||
PATH: `${wrongIdentityPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -559,7 +559,7 @@ describe("public world pack", () => {
|
||||
const wrongTopology = runVerifier(packDir, {
|
||||
...process.env,
|
||||
PATH: `${wrongTopologyPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -585,7 +585,7 @@ describe("public world pack", () => {
|
||||
const result = runVerifier(publicWorldDir(), {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -596,11 +596,11 @@ describe("public world pack", () => {
|
||||
});
|
||||
|
||||
it("fails when the context store projection ignores its typed override", () => {
|
||||
const rigPath = writeRigFixture({ configRoot: "/fixture/wrong-context-packs" });
|
||||
const rigPath = writeRigFixture({ configRoot: "/fixture/wrong-context" });
|
||||
const result = runVerifier(publicWorldDir(), {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/expected-context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/expected-context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -614,7 +614,7 @@ describe("public world pack", () => {
|
||||
const result = runVerifier(publicWorldDir(), {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -632,7 +632,7 @@ describe("public world pack", () => {
|
||||
const result = runVerifier(publicWorldDir(), {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: "/fixture/context-packs",
|
||||
OPENRIG_CONTEXT_ROOT: "/fixture/context",
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -648,7 +648,7 @@ describe("public world pack", () => {
|
||||
const result = runVerifier(publicWorldDir(), {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: contextRoot,
|
||||
OPENRIG_CONTEXT_ROOT: contextRoot,
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
});
|
||||
@@ -662,7 +662,7 @@ describe("public world pack", () => {
|
||||
const strangerRoot = mkdtempSync(join(tmpdir(), "public-world-stranger-"));
|
||||
temporaryRoots.push(strangerRoot);
|
||||
const openrigHome = join(strangerRoot, "openrig-home");
|
||||
const contextRoot = join(openrigHome, "context-packs");
|
||||
const contextRoot = join(openrigHome, "context");
|
||||
mkdirSync(openrigHome);
|
||||
expect(readdirSync(openrigHome)).toEqual([]);
|
||||
|
||||
@@ -671,7 +671,7 @@ describe("public world pack", () => {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_HOME: openrigHome,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: contextRoot,
|
||||
OPENRIG_CONTEXT_ROOT: contextRoot,
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
};
|
||||
@@ -721,7 +721,7 @@ describe("public world pack", () => {
|
||||
const env = {
|
||||
...process.env,
|
||||
PATH: `${rigPath}:${process.env.PATH ?? ""}`,
|
||||
OPENRIG_CONTEXT_PACKS_ROOT: join(strangerRoot, "context-packs"),
|
||||
OPENRIG_CONTEXT_ROOT: join(strangerRoot, "context"),
|
||||
OPENRIG_SESSION_NAME: "qa@fixture",
|
||||
RIGGED_SESSION_NAME: "",
|
||||
};
|
||||
@@ -747,7 +747,7 @@ describe("public world pack", () => {
|
||||
expect(exercise).toContain("rig context get world-example");
|
||||
expect(exercise).toContain("rig context add <pack-directory>");
|
||||
expect(exercise).toContain("rig context list");
|
||||
expect(exercise).toContain("rig config get context.packs_root");
|
||||
expect(exercise).toContain("rig config get context.root");
|
||||
expect(exercise).not.toMatch(/identity\/|ontology\/|terrain\//);
|
||||
|
||||
const packDir = publicWorldDir();
|
||||
|
||||
@@ -22,6 +22,7 @@ function clearEnv(): () => void {
|
||||
"OPENRIG_PORT", "OPENRIG_FILES_ALLOWLIST", "OPENRIG_PROGRESS_SCAN_ROOTS",
|
||||
"OPENRIG_WORKSPACE_ROOT", "OPENRIG_DOGFOOD_EVIDENCE_ROOT",
|
||||
"OPENRIG_WORKSPACE_PROJECTS_ROOT", "OPENRIG_WORKSPACE_CATALOG_PATH",
|
||||
"OPENRIG_CONTEXT_ROOT", "OPENRIG_CONTEXT_PACKS_ROOT",
|
||||
"OPENRIG_POLICIES_CLAUDE_COMPACTION_ENABLED",
|
||||
"OPENRIG_POLICIES_CLAUDE_COMPACTION_THRESHOLD_PERCENT",
|
||||
"OPENRIG_POLICIES_CLAUDE_COMPACTION_PRE_COMPACT_INSTRUCTION",
|
||||
@@ -93,7 +94,7 @@ describe("config routes (User Settings v0)", () => {
|
||||
// + 2 B6 founder ruling (policies.idle_gate_qitem.auto_register /
|
||||
// opt_in_sessions — the not-default-on gate)
|
||||
// + 1 OPR.0.5.3.6 D1 (topology.root — the topology tree root)
|
||||
// + 1 OPR.0.5.3.7 R4 (context.packs_root)
|
||||
// + 1 OPR.0.5.9.5 Wave B (context.root)
|
||||
// + 1 S15 (onboarding.default_pack.enabled)
|
||||
// + 1 S04 (queue.pickup_stall_threshold_minutes)
|
||||
// + 2 S02 (queue.stuck_sweep_interval_seconds /
|
||||
@@ -113,6 +114,7 @@ describe("config routes (User Settings v0)", () => {
|
||||
expect(body.settings["onboarding.default_pack.enabled"]).toMatchObject({ value: true, source: "default" });
|
||||
expect(String(body.settings["workspace.projects_root"]?.value)).toMatch(/projects$/);
|
||||
expect(String(body.settings["workspace.catalog_path"]?.value)).toMatch(/workspace\.yaml$/);
|
||||
expect(String(body.settings["context.root"]?.value)).toMatch(/context$/);
|
||||
expect(body.settings["workspace.field_notes_root"]).toBeUndefined();
|
||||
expect(body.settings["workspace.dogfood_evidence_root"]).toBeUndefined();
|
||||
});
|
||||
@@ -153,6 +155,27 @@ describe("config routes (User Settings v0)", () => {
|
||||
expect(body.validKeys).toContain("workspace.root");
|
||||
});
|
||||
|
||||
it.each(["GET", "POST", "DELETE"] as const)(
|
||||
"%s /api/config/context.packs_root refuses the removed key with its replacement",
|
||||
async (method) => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/api/config/context.packs_root", {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: method === "POST" ? JSON.stringify({ value: "/legacy" }) : undefined,
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toMatchObject({ replacement: "context.root" });
|
||||
},
|
||||
);
|
||||
|
||||
it("GET /api/config refuses a persisted context.packsRoot before projecting settings", async () => {
|
||||
writeFileSync(configPath, JSON.stringify({ context: { packsRoot: "/legacy" } }));
|
||||
const res = await buildApp().request("/api/config");
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.json()).toMatchObject({ error: expect.stringContaining("context.root") });
|
||||
});
|
||||
|
||||
it("POST /api/config/:key sets the value and persists to disk", async () => {
|
||||
const app = buildApp();
|
||||
const res = await app.request("/api/config/workspace.slices_root", {
|
||||
|
||||
@@ -32,6 +32,7 @@ function clearEnv(): () => void {
|
||||
"OPENRIG_WORKSPACE_STEERING_PATH", "OPENRIG_WORKSPACE_FIELD_NOTES_ROOT",
|
||||
"OPENRIG_WORKSPACE_SPECS_ROOT", "OPENRIG_DOGFOOD_EVIDENCE_ROOT",
|
||||
"OPENRIG_WORKSPACE_PROJECTS_ROOT", "OPENRIG_WORKSPACE_CATALOG_PATH",
|
||||
"OPENRIG_CONTEXT_ROOT", "OPENRIG_CONTEXT_PACKS_ROOT",
|
||||
"OPENRIG_SKILLS_ROOT",
|
||||
"OPENRIG_FILES_ALLOWLIST", "OPENRIG_PROGRESS_SCAN_ROOTS",
|
||||
"OPENRIG_UI_PREVIEW_REFRESH_INTERVAL_SECONDS",
|
||||
@@ -101,7 +102,7 @@ describe("SettingsStore (User Settings v0)", () => {
|
||||
"workspace.catalog_path",
|
||||
// OPR.0.5.3.6 D1 — the topology tree root (instance at its top).
|
||||
"topology.root",
|
||||
"context.packs_root",
|
||||
"context.root",
|
||||
"skills.root",
|
||||
"onboarding.default_pack.enabled",
|
||||
"files.allowlist", "progress.scan_roots",
|
||||
@@ -162,6 +163,25 @@ describe("SettingsStore (User Settings v0)", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves context.root and refuses the removed key, file field, and env", () => {
|
||||
const store = new SettingsStore(configPath);
|
||||
expect(store.resolveOne("context.root")).toMatchObject({
|
||||
value: expect.stringMatching(/context$/),
|
||||
source: "default",
|
||||
});
|
||||
|
||||
process.env["OPENRIG_CONTEXT_ROOT"] = join(tmpDir, "context-library");
|
||||
expect(store.resolveOne("context.root")).toMatchObject({ value: join(tmpDir, "context-library"), source: "env" });
|
||||
delete process.env["OPENRIG_CONTEXT_ROOT"];
|
||||
|
||||
expect(() => store.set("context.packs_root", "/legacy")).toThrow(/removed.*context\.root/i);
|
||||
writeFileSync(configPath, JSON.stringify({ context: { packsRoot: "/legacy" } }));
|
||||
expect(() => store.resolveConfig()).toThrow(/context\.packs_root.*context\.root/i);
|
||||
writeFileSync(configPath, "{}\n");
|
||||
process.env["OPENRIG_CONTEXT_PACKS_ROOT"] = "/legacy-env";
|
||||
expect(() => store.resolveOne("context.root")).toThrow(/OPENRIG_CONTEXT_PACKS_ROOT.*OPENRIG_CONTEXT_ROOT/i);
|
||||
});
|
||||
|
||||
it("W2c idle-gate-qitem cadence defaults to scan=60 and active-wake=900", () => {
|
||||
const store = new SettingsStore(configPath);
|
||||
expect(() => store.resolveOne("policies.idle_gate_qitem.scan_interval_seconds"))
|
||||
|
||||
Reference in new issue
Block a user