core/src/paths
The workspace path helpers (#59) — THE one place that walks the filesystem for roots.
Before this module, seven call sites each computed the repo root by counting ..
segments off their own import.meta.url — a shape that breaks silently when a file
moves (dev.ts carried 4×".."). An anchor test (no-duplicate-paths.test.ts) now
refuses an eighth copy: fileURLToPath and inline .enso path assembly outside this
module fail the suite by name.
⚠ REALPATH FIRST, then walk — load-bearing, observed live (PR #58 receipts): bun does
NOT realpath import.meta.url through the print-harness symlink mirror (printHarnessDir), so a
walk from the un-resolved module path starts inside the mirror. The mirror’s entries
are symlinks back into the real tree, so resolving first lands the walk in the real
checkout; walking first would search the wrong ancestry.
⚠ These helpers run under node/bun only (node:fs). They are barrel-exported and the
browser bundle tolerates that because nothing browser-side imports them — probed
2026-09-04: bun build index.html --target=browser tree-shakes the module out.
Workspace paths
Section titled “Workspace paths”RepoRootNotFoundError
Section titled “RepoRootNotFoundError”Defined in: core/src/paths.ts:49
Thrown when no ancestor of the start path is a workspace root.
Extends
Section titled “Extends”Error
Constructors
Section titled “Constructors”Constructor
Section titled “Constructor”new RepoRootNotFoundError(
startPath):RepoRootNotFoundError
Defined in: core/src/paths.ts:50
Parameters
Section titled “Parameters”startPath
Section titled “startPath”string
Returns
Section titled “Returns”Overrides
Section titled “Overrides”Error.constructor
EnsoHomeMigration
Section titled “EnsoHomeMigration”Defined in: core/src/paths.ts:177
What moving the checkout’s .enso/ into ENSO_HOME did (#421).
Properties
Section titled “Properties”
readonlyfrom:string
Defined in: core/src/paths.ts:179
The checkout’s .enso/ the files came from.
readonlyheld:boolean
Defined in: core/src/paths.ts:190
The checkout HAS an old .enso/ that this launch did not move, because ENSO_HOME was set
explicitly (migrateCheckoutEnsoOnLaunch). Said, so it is not silently ignored.
readonlykept: readonlystring[]
Defined in: core/src/paths.ts:185
Files LEFT in from: to already holds one at that path. Never overwritten — the user decides.
readonlymoved: readonlystring[]
Defined in: core/src/paths.ts:183
Files moved, relative to both.
readonlyto:string
Defined in: core/src/paths.ts:181
ENSO_HOME.
checkoutEnsoDir()
Section titled “checkoutEnsoDir()”checkoutEnsoDir(
repoRoot):string
Defined in: core/src/paths.ts:168
The checkout-local .enso/ every runtime path lived in before #421. Read by the migration
(migrateCheckoutEnso) and nothing else.
Parameters
Section titled “Parameters”repoRoot
Section titled “repoRoot”string
Returns
Section titled “Returns”string
ensoAgentDir()
Section titled “ensoAgentDir()”ensoAgentDir(
home):string
Defined in: core/src/paths.ts:294
pi’s scratch agent dir — settings, extensions, auth, sessions, trust. Shared by the TUI launcher
and the web server. ⚠ Deliberately NOT ~/.pi/agent: the operator’s own pi packages and
credentials stay invisible to Enso’s sessions (agent-host.ts).
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
ensoConfigPath()
Section titled “ensoConfigPath()”ensoConfigPath(
home):string
Defined in: core/src/paths.ts:418
The user’s config file (#59, #421): ENSO_HOME/config.json. Absent is the defaults (parseEnsoConfig).
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
ensoHome()
Section titled “ensoHome()”ensoHome(
environment):string
Defined in: core/src/paths.ts:155
The user-level Enso directory (#421): ENSO_HOME when set, else ~/.enso — pi’s own pattern,
PI_CODING_AGENT_DIR else ~/.pi/agent. Every runtime path resolves under it: pi’s agent dir,
the secrets file, the logs, the project list. One per user, so a second checkout or worktree
shares its logins, sessions and projects, and nothing runtime lives in a source tree.
Read from the environment it is given, never from process.env directly, so a test passes its
own and the launchers pass theirs — and a child the terminal launcher spawns inherits the same.
Parameters
Section titled “Parameters”environment
Section titled “environment”Readonly<Record<string, string | undefined>>
Returns
Section titled “Returns”string
ensoLogsDir()
Section titled “ensoLogsDir()”ensoLogsDir(
home):string
Defined in: core/src/paths.ts:360
Where the harness’s log lives (#132): ENSO_HOME/logs/enso-YYYY-MM-DD.jsonl, ONE file per day
that every process appends to (dated, never renamed, so a second writer is safe — see
web/server/logging.ts).
Under ENSO_HOME so bun never autoloads from it and the write gate already knows it (guard.ts).
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
ensoPermissionRulesPath()
Section titled “ensoPermissionRulesPath()”ensoPermissionRulesPath(
home):string
Defined in: core/src/paths.ts:391
The permission policy pi runs under (#446, #496): ENSO_HOME/permissions.json. The host and
the terminal launcher run preparePermissionPolicy (@enso/core/permission-rules) on it,
which seeds it and links the permission engine’s config path to it before the engine reads it.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
ensoProjectsPath()
Section titled “ensoProjectsPath()”ensoProjectsPath(
home):string
Defined in: core/src/paths.ts:370
The projects registered from a page (#395): ENSO_HOME/projects.json. Server STATE, not config —
the roots they may sit under are config (ENSO_HOME/config.json).
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
ensoPromptsDir()
Section titled “ensoPromptsDir()”ensoPromptsDir(
home):string
Defined in: core/src/paths.ts:380
The prompt library (#443): ENSO_HOME/prompts/<id>.md, one markdown file per prompt. The user’s,
never committed with a project.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
ensoSecretsPath()
Section titled “ensoSecretsPath()”ensoSecretsPath(
home):string
Defined in: core/src/paths.ts:325
The harness’s secrets file (#89): ENSO_HOME/secrets.env, NOT .env at the root.
bun auto-loads .env* from the cwd into EVERY bun process — including a bun -e the model
runs from the repo root — so a root .env reaches a tool child no matter what the parent
scrubbed (verified with env -i).
ENSO_HOME is write-protected (ENSO_PERSISTENCE_WRITE_DIRECTORIES) and read-denied on BOTH
tool routes by the /.enso/ fragment in
DEFAULT_GUARD_POLICY: the reader gate for every tool
carrying a path, and the shell deny (#204) for a literal path word in bash/powershell.
⚠ The shell leg is a bar, not a boundary — shell-secret-paths.ts states what a
runtime-assembled name still gets past. ⚠ An ENSO_HOME whose path has no .enso segment is
NOT matched by that fragment: keep the directory named .enso.
⚠ THE core/src! QUALIFIER IS LOAD-BEARING, and the obvious simplification does not work
(#188). guard.ts is not imported here, so TypeScript’s own link resolution — which TypeDoc
prefers by default — has nothing in scope, and a bare {@link DEFAULT_GUARD_POLICY} is an
unresolved link. The documented root-scoped form {@link !DEFAULT_GUARD_POLICY} does not
help either: with several entry points, the PROJECT’s children are the modules, not their
symbols, so resolution from the root finds nothing. Tested, all three spellings. The module
source is therefore named, which couples this link to the entry-point layout in
typedoc.json — and that coupling breaks LOUDLY, because an unresolved link is a fatal
warning in the api-docs gate.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
fileUrlToPath()
Section titled “fileUrlToPath()”fileUrlToPath(
fileUrl):string
Defined in: core/src/paths.ts:113
Decode a file:// URL to the filesystem path it names.
The ONLY fileURLToPath call site in the repository (the anchor test enforces it). Two
callers with different reasons share it: findRepoRoot accepts import.meta.url,
and the harness guard decodes a file:// path a tool was handed — pi’s own tools decode
one before touching the filesystem, so the guard must judge the decoded path (#53).
Parameters
Section titled “Parameters”fileUrl
Section titled “fileUrl”string
Returns
Section titled “Returns”string
findRepoRoot()
Section titled “findRepoRoot()”findRepoRoot(
startPathOrFileUrl):string
Defined in: core/src/paths.ts:130
The repository root: the nearest ancestor whose package.json declares workspaces.
Accepts a filesystem path OR a file:// URL, so callers pass import.meta.url
directly and never touch fileURLToPath themselves (the anchor test enforces that).
The start path is realpath-resolved BEFORE walking — see the module header for why.
⚠ This finds the root of the checkout the given FILE lives in — for a module inside
this repo that is always the Enso checkout, never the session cwd. That
distinction is what config.ts relies on; do not “fix” this to consult process.cwd().
Parameters
Section titled “Parameters”startPathOrFileUrl
Section titled “startPathOrFileUrl”string
Returns
Section titled “Returns”string
harnessPackageDir()
Section titled “harnessPackageDir()”harnessPackageDir(
repoRoot):string
Defined in: core/src/paths.ts:400
The harness package — the directory enso.ts hands to pi’s -e.
Parameters
Section titled “Parameters”repoRoot
Section titled “repoRoot”string
Returns
Section titled “Returns”string
isFileUrl()
Section titled “isFileUrl()”isFileUrl(
candidate):boolean
Defined in: core/src/paths.ts:99
True when the string is a file:// URL rather than a filesystem path.
Parameters
Section titled “Parameters”candidate
Section titled “candidate”string
Returns
Section titled “Returns”boolean
migrateCheckoutEnso()
Section titled “migrateCheckoutEnso()”migrateCheckoutEnso(
repoRoot,home):EnsoHomeMigration
Defined in: core/src/paths.ts:209
Move the checkout’s .enso/ into ENSO_HOME (#421) — pi’s runMigrations pattern: automatic
on startup, idempotent, never overwriting. Each file whose path is free in ENSO_HOME is moved;
one that is taken is left where it is and reported in kept, so two checkouts’ logins or
sessions conflict loudly instead of one silently replacing the other. Emptied directories are
removed, the checkout’s .enso/ with them once nothing is left in it.
print-harness/ is not moved but deleted: a build cache of the checkout, regenerated at its new
place (printHarnessDir) on the next terminal launch, whose relative links would break moved.
Tolerant of a second launcher migrating at the same moment (bun run dev starts two): a file
the other process moved first is skipped, not reported. ENSO_HOME is created 0700 — it
holds credentials.
Parameters
Section titled “Parameters”repoRoot
Section titled “repoRoot”string
string
Returns
Section titled “Returns”migrateCheckoutEnsoOnLaunch()
Section titled “migrateCheckoutEnsoOnLaunch()”migrateCheckoutEnsoOnLaunch(
repoRoot,environment):EnsoHomeMigration
Defined in: core/src/paths.ts:232
The migration a LAUNCH runs (#421): into the default ENSO_HOME (~/.enso) only. With
ENSO_HOME set explicitly nothing moves — the checkout’s old .enso/ is held and reported.
⚠ Why: a launcher run from a checkout with a scratch or test ENSO_HOME (ENSO_HOME=/tmp/x)
would otherwise move that checkout’s logins and sessions into the scratch directory, to be lost
with it — which happened, once, while this was being built. ~/.enso is the one place a user’s
state is meant to live, so it is the one place a launch moves it to without being asked. Someone
who chose another home moves the old directory by hand, and the launch says so.
Parameters
Section titled “Parameters”repoRoot
Section titled “repoRoot”string
environment
Section titled “environment”Readonly<Record<string, string | undefined>>
Returns
Section titled “Returns”printHarnessDir()
Section titled “printHarnessDir()”printHarnessDir(
repoRoot):string
Defined in: core/src/paths.ts:347
Where the print variant of the harness package is materialized (#20): a build cache of THIS
checkout, so it stays in the checkout — its links point into this checkout’s harness, and two
checkouts sharing one ENSO_HOME would otherwise relink it to whichever launched last (#421).
Under node_modules/.cache, the conventional home of a regenerated artifact, gitignored and
outside every .enso the guards protect.
Parameters
Section titled “Parameters”repoRoot
Section titled “repoRoot”string
Returns
Section titled “Returns”string
projectConfigPath()
Section titled “projectConfigPath()”projectConfigPath(
projectDirectory):string
Defined in: core/src/paths.ts:429
A project’s own config (#421): <project>/.enso/config.json. Read only once the project is
trusted, over the user’s (ensoConfigPath); the project directory only, never its parents, as
pi reads <cwd>/.pi/.
Parameters
Section titled “Parameters”projectDirectory
Section titled “projectDirectory”string
Returns
Section titled “Returns”string
readTextIfPresent()
Section titled “readTextIfPresent()”readTextIfPresent(
path):string|undefined
Defined in: core/src/paths.ts:440
A file’s text, or undefined when there is no file — for a file whose absence means its
defaults (the config). Any other failure to read it throws: a file that is there and unreadable
is not a missing one.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string | undefined
realpathOrSelf()
Section titled “realpathOrSelf()”realpathOrSelf(
path):string
Defined in: core/src/paths.ts:64
realpathSync, or the input unchanged when the path (or a segment of it) does not exist.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
rootDotenvPath()
Section titled “rootDotenvPath()”rootDotenvPath(
repoRoot):string
Defined in: core/src/paths.ts:334
The root .env — where secrets must NOT live (#89); reported as misplaced when they do.
Parameters
Section titled “Parameters”repoRoot
Section titled “repoRoot”string
Returns
Section titled “Returns”string
webPackageDir()
Section titled “webPackageDir()”webPackageDir(
repoRoot):string
Defined in: core/src/paths.ts:409
The web package — the cwd both dev surfaces run in.
Parameters
Section titled “Parameters”repoRoot
Section titled “repoRoot”string
Returns
Section titled “Returns”string
