Constructdocs
section · guides

Root semantics

The three directory concepts in Construct and which subsystems use each one.

3 min read·guides / concepts / root-semantics

The three roots

Construct operates across two directory concepts — the package and the project — plus the raw working directory. Conflating them causes bundled assets to be looked up in user repos, or user state to be written into the install directory. lib/roots.mjs defines canonical exports for all three.

packageRoot

The directory where the Construct npm package is installed. Resolved from import.meta.url at module load time, stable across global install, npx, and local dev symlinks.

import { packageRoot } from './lib/roots.mjs';

Use packageRoot to locate assets that ship with Construct itself: persona prompts, skill files, schemas, hook scripts, templates, and the registry. These assets are always relative to the package, regardless of where the user's project lives.

resolveProjectRoot(cwd?)

The root of the user's repository being operated on. Resolved at runtime by inspecting --project \<dir> in process.argv, then walking upward from cwd looking for .construct/ (Construct project marker) or package.json (generic JS root). Falls back to cwd when no marker is found.

import { resolveProjectRoot } from './lib/roots.mjs';

const projectRoot = resolveProjectRoot(); // uses process.cwd()

Pass an explicit cwd in tests or sub-process workers to control the starting directory without relying on the ambient process cwd.

Use projectRoot for everything that reads from or writes to the user's repo: .construct/ state (observations, sessions, intake, task graphs), oracle verdicts, telemetry, beads, and provider configs.

process.cwd()

The raw working directory at the moment the process started. Useful for resolving relative paths before resolveProjectRoot has been called. Callers should prefer resolveProjectRoot(cwd) once it is determined.

Which subsystem uses which root

SubsystemRoot usedReason
Persona loader (lib/persona.mjs)packageRootPersonas ship with the package
Skill loader (lib/skills/)packageRootSkills are bundled assets
Schema validation (schemas/)packageRootSchemas are bundled with the package
Hook scripts (lib/hooks/)packageRootHooks reference the install's lib/
Templates (templates/)packageRootDoc templates ship with the package
Observation store (.construct/observations/)projectRootPer-project durable state
Session store (.construct/sessions/)projectRootPer-project durable state
Intake queue (.construct/intake/)projectRootPer-project durable state
Task graphs (.construct/task-graphs/)projectRootPer-project durable state
Oracle verdicts (.construct/oracle/)projectRootPer-project durable state
Telemetry (.construct/telemetry/)projectRootPer-project durable state
Beads tracker (.beads/)projectRootPer-project issue tracker
Provider config (.construct/providers.yaml)projectRootPer-project credentials scope
construct.config.jsonprojectRootPer-project configuration

Concrete example

Construct is installed globally at /usr/local/lib/node_modules/construct. The user runs construct init inside /myproject.

ExpressionResolves to
packageRoot/usr/local/lib/node_modules/construct
resolveProjectRoot()/myproject (found .construct/ marker)
process.cwd()/myproject (where the shell is)

In this scenario, packageRoot and resolveProjectRoot() differ. A skill loaded from packageRoot/skills/engineering.md is the same file regardless of which project the user is in. An observation written to projectRoot/.construct/observations/ is scoped to /myproject.

When Construct is run inside its own source tree (self-hosting), the two roots coincide: both resolve to the construct repo. This is the only case where they are the same path.

History

Prior to lib/roots.mjs, both concepts were conflated under a single ROOT_DIR constant, which broke correct resolution when Construct was installed globally and operated on a project in a separate directory. lib/roots.mjs was introduced to make the distinction explicit and enforce the correct root at each call site.