/** * Dependency-leaf project scope, root discovery and identity decoding. * * The public paths and project-info modules re-export or delegate here so store * ownership can reuse the same policy without importing store-aware routing. * @packageDocumentation */ import { AsyncLocalStorage } from 'node:async_hooks'; import type { OperationExecutionContext } from '@cleocode/contracts/jobs'; /** * Async context payload set by the spawn adapter when launching a subagent * inside a git worktree (ADR-041 §D3). * * @remarks * When `worktreeScope.run(scope, fn)` is active, `getProjectRoot()` returns * `scope.worktreeRoot` instead of walking ancestors. All DB path functions * that delegate to `getProjectRoot()` therefore direct their I/O to the * worktree's `.cleo/` directory, closing the T335 worktree-leak root cause. * * For processes that were spawned with `CLEO_WORKTREE_ROOT` in their * environment (but where AsyncLocalStorage is not in scope), callers should * populate the store via: * ```ts * worktreeScope.run( * { worktreeRoot: process.env.CLEO_WORKTREE_ROOT, projectHash: process.env.CLEO_PROJECT_HASH }, * () => { ... } * ); * ``` * * @task T380 * @public */ export interface WorktreeScope { /** Optional captured operation lifetime for guarded asynchronous storage work. */ readonly execution?: OperationExecutionContext; /** * Absolute path to the worktree directory (value of `CLEO_WORKTREE_ROOT`). */ worktreeRoot: string; /** * Project hash used to scope the worktree under the XDG worktree root * (value of `CLEO_PROJECT_HASH`). */ projectHash: string; } /** * AsyncLocalStorage instance that carries the active {@link WorktreeScope} * for the current async execution context. * * @remarks * Set by the spawn adapter (or any caller that wants to redirect CLEO path * resolution to a worktree directory) before invoking subagent logic. * `getProjectRoot()` checks this store BEFORE the `CLEO_ROOT` env-var and * ancestor-walk, so scoped callers transparently receive the worktree root. * * Callers outside a worktree context receive `undefined` from * `worktreeScope.getStore()` and fall through to the existing resolution * order unchanged. * * @example * ```ts * import { worktreeScope } from '@cleocode/core/paths'; * * worktreeScope.run( * { worktreeRoot: '/path/to/worktree', projectHash: 'abc123' }, * async () => { * const root = getProjectRoot(); // returns '/path/to/worktree' * } * ); * ``` * * @task T380 * @public */ export declare const worktreeScope: AsyncLocalStorage; /** * Capture explicit project ownership and compatible operation authority before awaiting. * @param projectRoot - Requested project root, resolved independently of ambient pins. * @param inherited - Caller scope captured at the same synchronous boundary. * @returns Immutable scope retaining the original execution context and canonical path hash. * @throws Error if execution is inactive or its project root or persisted identity differs. * @remarks A portable project ID is compared only with persisted identity, never * with the path hash used by worktree routing. Ambient environment pins are unchanged. * Existing task-accessor error messages are retained for compatibility. * @example * ```ts * const scope = captureProjectScope(projectRoot, worktreeScope.getStore()); * await worktreeScope.run(scope, () => performProjectOperation()); * ``` */ export declare function captureProjectScope(projectRoot: string, inherited: WorktreeScope | undefined): WorktreeScope; /** * Return the main checkout root of the linked git worktree at `dir`. * * A linked worktree's gitlink points at `/worktrees/`; the main * checkout is the parent of `` when that is a `.git` directory, or the * `core.worktree` of `` for a worktree of a submodule. A * submodule or `--separate-git-dir` gitlink points elsewhere (for example * `/.git/modules/`) and is its own repository, so it returns * `null` (T12562). * * @param dir - Directory to inspect. * @returns The main checkout root, or `null` when `dir` is not a linked worktree. * @example * ```ts * linkedWorktreeMainRoot('/data/cleo/worktrees/abc/T1'); // '/home/u/project' * linkedWorktreeMainRoot('/home/u/project/vendor/sub'); // null (submodule) * ``` * @task T12562 */ export declare function linkedWorktreeMainRoot(dir: string): string | null; /** * Name the project that owns the linked git worktree at `dir`, for refusals * that must say where the store actually lives (T12677). * * @param dir - Worktree root. * @returns e.g. `owning project /home/u/project (projectId abc)`, * `owning repository /home/u/project (not an initialised CLEO project)`, or * `owning repository unknown (unreadable gitlink)`. * @remarks Never throws. Reads only the gitlink, the main checkout's gitdir * config and its declared identity (`.cleo/project.json`, the legacy * `.cleo/project-id`, then `.cleo/project-info.json`). * @example * ```ts * describeWorktreeOwner('/data/cleo/worktrees/abc/T1'); * // 'owning project /home/u/project (projectId 7f3c…)' * ``` * @task T12677 */ export declare function describeWorktreeOwner(dir: string): string; /** * Return the bare repository that owns the linked worktree at `dir`: its * gitlink points at `/worktrees/` and `/config` sets * `core.bare = true`. * * @param dir - Directory to inspect. * @returns The bare repository's git directory, or `null` when `dir` is not a * worktree of a bare repository. * @remarks Never throws. * @example * ```ts * bareRepositoryOfWorktree('/src/app-wt'); // '/src/app.git' * ``` * @task T12680 */ export declare function bareRepositoryOfWorktree(dir: string): string | null; /** * Report whether `dir` is the root of a linked git worktree: its `.git` is a * gitlink FILE whose `gitdir` lies under `/worktrees/`. * * @param dir - Directory to inspect. * @returns `true` for a linked worktree, or for a gitlink that cannot be * parsed (conservatively). `false` for a real repository directory, a * missing `.git`, or a submodule / `--separate-git-dir` gitlink, which is its * own repository and may own its own store (T12562). * @remarks Never throws. * @example * ```ts * isGitLinkedCheckout('/home/u/.local/share/cleo/worktrees/abc/T1'); // true * isGitLinkedCheckout('/home/u/project'); // false: .git is a directory * ``` * @task T12460 * @task T12562 */ export declare function isGitLinkedCheckout(dir: string): boolean; /** * Map a candidate project root to the root that OWNS the project store. * * A CLEO worktree (ADR-055) is a linked git checkout whose `.cleo/` can exist * on disk: the project's own tracked `.cleo/` files, or the * `project-info.json` seeded for identity inheritance (T11033). Neither makes * the worktree a project. Its store is the PARENT project's `.cleo/cleo.db`. * When `root` is a linked checkout whose main repository is a valid CLEO * project, this returns that main repository; otherwise `root` unchanged. * * @param root - Candidate project root (worktree root, scope root, or walk hit). * @returns The parent project root for a CLEO worktree, else `root`. * @remarks Only `root` itself is inspected, never its ancestors, so a nested * fixture `.cleo/` inside a checkout keeps resolving to itself. A worktree * whose parent is not a CLEO project is returned unchanged; the store-open * guard then refuses it instead of creating a local store. * @example * ```ts * resolveStoreOwnerRoot('/data/cleo/worktrees/abc/T1'); // '/home/u/project' * resolveStoreOwnerRoot('/home/u/project'); // '/home/u/project' * ``` * @task T12460 */ export declare function resolveStoreOwnerRoot(root: string): string; /** * Attempt to resolve the main git repo root from a gitlink (.git as FILE). * Returns the main repo path if the gitlink is valid and the main repo is a * CLEO project; otherwise returns `null`. * * @param gitlinkDir - Directory containing the candidate Git link. * @returns Validated main repository root, or null when unavailable. * @internal * @task T11034 */ export declare function _resolveMainRepoFromGitlink(gitlinkDir: string): string | null; /** * Validate that a candidate project root directory is a legitimate CLEO * project root and not a stray parent `.cleo/` directory that happened to * be found by the walk-up algorithm. * * ## Primary path (T1864 — project-info.json contract) * * A candidate is **accepted** when `.cleo/project-info.json` exists and * parses as JSON with a non-empty `projectId` string field. This is the * canonical form written by `cleo init` and is the only form that guarantees * the directory is a proper CLEO project rather than a stray `.cleo/` left by * an old installation or a git worktree that auto-created its own `.cleo/`. * * ## Tracked identity (T12716) * * A candidate whose `.cleo/project.json` (or legacy `.cleo/project-id`) * declares a valid id is also accepted, without the legacy warning — a fresh * clone before `cleo init` is a project root. As in `resolveProjectByCwd`, a * tracked-only `.cleo/` counts only at a git toplevel (a real `.git/` * directory), so a committed id in a monorepo subdirectory never shadows its * parent, and a worktree (gitlink `.git` file) still resolves to its parent. * * ## Legacy fallback (backwards-compatibility) * * Projects initialized before `project-info.json` was introduced are still * accepted when **both** of the following are true: * 1. `.cleo/` exists in `candidate` * 2. `.git/` exists as a sibling of `.cleo/` in `candidate` * * A one-time stderr warning is emitted (guarded by `_legacyFallbackWarned`) * so operators know to re-run `cleo init` to upgrade the project metadata. * * **Breaking change vs. prior implementation**: bare `package.json` alone is * no longer sufficient. The old check `existsSync(gitDir) || existsSync(pkgJson)` * accepted any Node.js package directory as a valid project root, which caused * the monorepo-package bug where sub-packages inside `packages/` created their * own empty `.cleo/` databases. * * @param candidate - Absolute path to the directory being considered as the * project root (parent of the `.cleo/` directory). * @returns `true` when the candidate is a recognised CLEO project root. * @remarks This validates existing markers without creating directories or opening * stores. Git-link worktrees resolve through their parent instead of becoming roots. * * @example * ```typescript * // Project root with project-info.json — valid (primary path) * validateProjectRoot('/home/user/myproject'); // true * * // Legacy project root with .git/ but no project-info.json — valid + warning * validateProjectRoot('/home/user/legacy-project'); // true (+ stderr warning) * * // Stray .cleo in home dir with no markers — invalid * validateProjectRoot('/home/user'); // false * ``` * * @task T1463 * @task T1864 */ export declare function validateProjectRoot(candidate: string): boolean; /** * Get the project root by walking ancestor directories for `.cleo/` or `.git/`. * * Stops at the **first** ancestor directory that contains either sentinel * directory and never drifts past it — even when multiple nested projects * exist above the starting directory. * * Resolution order: * 1. `CLEO_ROOT` env var — bypasses walk entirely (CI / test override) * 2. `CLEO_DIR` env var (absolute path only) — derives project root from dirname * 3. Walk ancestors from `cwd` (or `process.cwd()`) toward filesystem root: * - `.cleo/` found with a sibling `.git/` or `package.json` → accept as root * - `.cleo/` found but **no** sibling marker → skip (stray/parent `.cleo/`) * - `.git/` found (without `.cleo/` sibling) → throw `E_NOT_INITIALIZED` * 4. Filesystem root reached without a valid root → throw `E_INVALID_PROJECT_ROOT` * (if at least one `.cleo/` was skipped) or `E_NO_PROJECT` (none found) * * @param cwd - Optional starting directory; defaults to `process.cwd()` * @returns Absolute path to the project root directory (parent of `.cleo/`) * @throws {CleoError} `ExitCode.CONFIG_ERROR` (`E_NOT_INITIALIZED`) when a * `.git/` is found but no `.cleo/` is present at that level. * @throws {CleoError} `ExitCode.CONFIG_ERROR` (`E_INVALID_PROJECT_ROOT`) when * one or more `.cleo/` directories are found but none have the required sibling * markers (`.git/` or `package.json`). This prevents accidental operations on * the wrong project when a stray parent `.cleo/` exists higher in the filesystem. * @throws {CleoError} `ExitCode.NOT_FOUND` (`E_NO_PROJECT`) when neither * sentinel is found in any ancestor. * * @remarks * `CLEO_ROOT` is an absolute-path escape hatch for environments where the * working directory is unrelated to the project (CI tmpdirs, monorepo scripts, * test harnesses). When set it is returned as-is without scanning ancestors. * * `CLEO_DIR` set to an absolute path (e.g. `/project/.cleo`) also bypasses * the walk: the project root is derived as its `dirname`. This preserves * backward compatibility for test harnesses that use `CLEO_DIR` to pin the * project root. * * NEVER auto-creates `.cleo/`. Project initialisation is an explicit opt-in * via `cleo init`. * * @example * ```typescript * // Running from packages/core inside the monorepo: * const root = getProjectRoot(); // "/mnt/projects/cleocode" * ``` */ export declare function getProjectRoot(cwd?: string): string; /** * Whether an error only says "there is no CLEO project here": the cwd is * outside every project (`Not inside a CLEO project`, `No CLEO project found`, * `E_NO_PROJECT`, `NEXUS_PROJECT_NOT_FOUND`) or the project is uninitialised * (`Run cleo init at …`). Project-independent commands (`decide`, `login`, * `setup`) run there routinely, so startup checks treat it as expected, not * as a failure. * * @param err - Any caught value. * @returns True for a missing-project signal; false for any other error. * @task T12733 */ export declare function isMissingProjectError(err: unknown): boolean; /** Fields consumed by logging, audit, and correlation subsystems. */ export interface ProjectInfo { /** Write-once 12-char hex identity key persisted at init (T12557: never re-derived once stored). */ projectHash: string; /** * Portable project id: the tracked `.cleo/project.json` / legacy * `.cleo/project-id` id, else the `projectId` cached in * `.cleo/project-info.json` (T12716). */ projectId: string; /** Absolute path to the project root directory. */ projectRoot: string; /** Display name from {@link getProjectDisplayName} (T12716). */ projectName: string; } /** * Compute the write-once `projectHash` for a project root. * * `projectHash` is an identity key (audit correlation, idempotency, release * ids), not a path fact. It is computed once, at init, and persisted. This * computes it from the real path of the MAIN checkout: a linked worktree maps * to its parent project, and symlinked spellings (`/tmp` vs `/private/tmp`) * collapse to one path, so every spelling of one project gets one hash. * * @param projectRoot - Project root or a linked worktree of it. * @returns 12-char hex hash of the canonical main-checkout path. * @example * ```ts * const hash = computeStableProjectHash('/tmp/project'); // same as '/private/tmp/project' * ``` * @task T12557 */ export declare function computeStableProjectHash(projectRoot: string): string; /** * Compute the id-derived `projectHash` for a project id (T12558 · T12716). * * Used ONLY for an identity minted on explicit request (`cleo init * --new-identity`, T12558). Every other project, a fresh init included, gets * {@link computeStableProjectHash}: a hash is only stable across the loss of * the untracked project-info.json if it can be re-derived from disk, and only * the path-derived value can (ADR-096, AC8). A stored hash is never * re-derived. If a cross-clone hash is ever needed, it belongs in * `.cleo/project.json` as a write-once field. * * @param projectId - The project's portable id. * @returns 12-char hex hash of `project-id:`. * @example * ```ts * computePortableProjectHash('c78d09c3a8ee'); // same value on every device * ``` * @task T12716 */ export declare function computePortableProjectHash(projectId: string): string; /** * The display name of a project (T12716) — the single accessor every * registration, encounter, Nexus link and label path uses. * * Order: the committed `.cleo/project.json` `name`; for a legacy project not * yet migrated by `cleo doctor project-identity --resolve`, the * `.cleo/project-info.json` `displayName` (what a legacy rename writes), then * its `name` (the init-time name, or what `cleo project rename` wrote before * T12716), then the stray `projectName` old `cleo upgrade --name` builds * wrote (T12712); otherwise the directory basename. * * @param projectRoot - Absolute project root. * @returns A non-empty name. Never throws. * @example * ```ts * getProjectDisplayName('/work/cleocode'); // 'cleocode' unless project.json names it * ``` * @task T12716 */ export declare function getProjectDisplayName(projectRoot: string): string; /** * Read metadata from a directory already resolved by the canonical path policy. * @param projectRoot - Captured root used for the returned identity and name. * @param cleoDir - Explicit data directory owned by that root. * @returns Validated project information, retaining the legacy empty portable ID. * @throws When reading or JSON decoding fails. * @remarks This leaf does not consult ambient environment pins. A legacy file * without `projectHash` is backfilled once (see {@link computeStableProjectHash}). * @example * ```ts * const info = await readProjectInfoAtDirectory(root, join(root, '.cleo')); * ``` */ export declare function readProjectInfoAtDirectory(projectRoot: string, cleoDir: string): Promise; /** * Strict synchronous metadata read for captured operation ownership validation. * @param projectRoot - Captured root used for the returned identity and name. * @param cleoDir - Explicit data directory owned by that root. * @returns Validated project information, retaining the legacy empty portable ID. * @throws When reading or JSON decoding fails. * @remarks Unlike the legacy public nullable wrapper, this preserves diagnostic failures. * A git worktree with no info file reads its primary checkout's (T13568). * @example * ```ts * const info = readProjectInfoAtDirectorySync(root, join(root, '.cleo')); * ``` */ export declare function readProjectInfoAtDirectorySync(projectRoot: string, cleoDir: string): ProjectInfo; //# sourceMappingURL=project-scope.d.ts.map