claude-use 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ import type { CredentialTargetVar } from "../config/schema";
2
+ /**
3
+ * The environment variables that authenticate Claude Code directly from the process environment, ahead of any stored identity credential. If any of these is set, every identity would silently authenticate as the same key/token/backend while it's set — defeating the entire premise of separate identities. Order here is also lookup order: `detectAmbientCredential` reports the first one found.
4
+ */
5
+ export declare const AMBIENT_CREDENTIAL_VARS: readonly ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"];
6
+ export type AmbientCredentialVar = (typeof AMBIENT_CREDENTIAL_VARS)[number];
7
+ /** Which guarded variable was found set, and to what. */
8
+ export interface AmbientCredentialDetection {
9
+ readonly variable: AmbientCredentialVar;
10
+ }
11
+ /** A credential this launch itself exports to the child: the variable it lands in and its value. */
12
+ export interface InjectedCredential {
13
+ readonly variable: CredentialTargetVar;
14
+ readonly token: string;
15
+ }
16
+ /**
17
+ * Detects whether any of `AMBIENT_CREDENTIAL_VARS` is set to a non-empty value in `env`, returning the first one found in declared order, or undefined when none are set.
18
+ *
19
+ * An empty string counts as unset, not set — confirmed load-bearing: one of Joe's real wrapper scripts (`o`, running Claude Code against OpenRouter) does `export ANTHROPIC_API_KEY=""` specifically to *clear* it so `ANTHROPIC_AUTH_TOKEN` takes effect instead, and this must never trip the guard.
20
+ *
21
+ * `injected`, when given, is the credential this launch exports for its own identity. Its variable holding exactly that value is not ambient: it is what a claude-use launch of the same identity left in the environment of the session this one starts from (a `claude @work` run inside a `claude @work` session). The same variable holding any other value still counts.
22
+ */
23
+ export declare function detectAmbientCredential(env: Readonly<Record<string, string | undefined>>, injected?: InjectedCredential): AmbientCredentialDetection | undefined;
24
+ /**
25
+ * Builds the exact refusal message: which variable was found, why it matters, and the two ways to opt in (a one-off env var, or a persistent per-identity setting). Mirrors the message documented in the project's README. `identityName` is included in the persistent-opt-in command when known; when no identity has been resolved yet (e.g. the `CLAUDE_CONFIG_DIR`-already-set escape hatch, or no identity resolved at all), a generic `<name>` placeholder is used instead, matching the README's own generic wording.
26
+ */
27
+ export declare function formatAmbientCredentialGuardMessage(variable: AmbientCredentialVar, identityName?: string): string;
28
+ /** Inputs to the ambient-credential guard evaluation. */
29
+ export interface EvaluateAmbientCredentialGuardParams {
30
+ readonly env: Readonly<Record<string, string | undefined>>;
31
+ /** The active identity's own `allowAmbientCredential` setting from its `identity.json`, or false when no identity is known. */
32
+ readonly allowAmbientCredential: boolean;
33
+ /** True when `CLAUDE_USE_ALLOW_AMBIENT_CREDENTIAL=1` is set for this one invocation. */
34
+ readonly allowAmbientCredentialOverride: boolean;
35
+ /** The active identity's name, for the message's persistent-opt-in command. Undefined when no identity is known. */
36
+ readonly identityName?: string;
37
+ /**
38
+ * True when this launch routes through an API provider. The guard inspects the PARENT environment, before `buildEnv` runs; a provider launch has its `ANTHROPIC_AUTH_TOKEN` injected and its `ANTHROPIC_API_KEY` cleared by `buildEnv` itself, so an ambient credential in the parent environment never reaches the child and there is nothing left for this guard to protect against. Everything else about the guard (including `IdentitySchema.allowAmbientCredential`) is unchanged by this flag.
39
+ */
40
+ readonly providerSelected?: boolean;
41
+ /** The launching identity's own credential, when its credential block resolved: see `detectAmbientCredential`. */
42
+ readonly injectedCredential?: InjectedCredential;
43
+ }
44
+ /** The result of one guard evaluation: either launch may proceed, or it must be refused with an explanatory message. */
45
+ export type AmbientCredentialGuardResult = {
46
+ readonly ok: true;
47
+ } | {
48
+ readonly ok: false;
49
+ readonly variable: AmbientCredentialVar;
50
+ readonly message: string;
51
+ };
52
+ /**
53
+ * Evaluates the ambient-credential guard: refuses unless no guarded variable is set, or the active identity opted in (`allowAmbientCredential: true`), or this one invocation opted in (`CLAUDE_USE_ALLOW_AMBIENT_CREDENTIAL=1`), or a provider takes over the child's credential (see `providerSelected`).
54
+ *
55
+ * This guard is about credential isolation, not identity/config-dir selection — it must still run even when the `CLAUDE_CONFIG_DIR`-already-set escape hatch applies (callers pass `allowAmbientCredential: false` and no `identityName` in that case, since there is no active identity to consult).
56
+ */
57
+ export declare function evaluateAmbientCredentialGuard(params: EvaluateAmbientCredentialGuardParams): AmbientCredentialGuardResult;
@@ -0,0 +1,41 @@
1
+ import { CliError } from "../cliError";
2
+ import type { FarmFs } from "./ports";
3
+ /** A held identity lock. Releasing is idempotent and never removes a lock some other process has since taken. */
4
+ export interface IdentityLock {
5
+ readonly path: string;
6
+ readonly token: string;
7
+ readonly release: () => void;
8
+ }
9
+ /** Raised when another process holds the identity's lock and did not release it within the retry budget. */
10
+ export declare class IdentityLockBusyError extends CliError {
11
+ readonly identity: string;
12
+ readonly lockPath: string;
13
+ readonly holderPid: number | undefined;
14
+ constructor(identity: string, lockPath: string, holderPid: number | undefined);
15
+ }
16
+ /** Everything `acquireIdentityLock` needs, all of it injected so a test never sleeps for real, never reads a real pid table, and never writes outside its own fake filesystem. */
17
+ export interface AcquireIdentityLockParams {
18
+ readonly identity: string;
19
+ /** The directory the lock file lives in — `~/.claude-use/identities`, alongside (never inside) the farm the lock protects, so a farm swap can rename the farm root out from under itself without disturbing the lock. */
20
+ readonly dir: string;
21
+ readonly fs: FarmFs;
22
+ readonly nowMs: () => number;
23
+ readonly pid: number;
24
+ /** Answers whether a process is still running, so a lock left behind by a crash is recognised rather than waited out for the full staleness window. Zombie-aware: a holder that exited without being reaped still answers signal 0 as alive, but will never release the lock itself. */
25
+ readonly isRunning: (pid: number) => boolean;
26
+ /** Blocks for the given number of milliseconds. Synchronous by necessity: the whole launcher is synchronous, right through to `spawnSync`. */
27
+ readonly sleep: (ms: number) => void;
28
+ readonly staleAfterMs?: number;
29
+ readonly retryDelayMs?: number;
30
+ readonly maxAttempts?: number;
31
+ }
32
+ /** The path of one identity's lock file. */
33
+ export declare function identityLockPath(dir: string, identity: string): string;
34
+ /**
35
+ * Takes the per-identity resync lock, held for the whole of a resync and released before the real `claude` binary is spawned.
36
+ *
37
+ * Two terminals in two different client directories under one identity is exactly the pattern directory rules exist to support, and it is exactly the pattern that races: both launches resolve their own cascade and both want to rewrite the same identity's farm toward two different states. This serialises them.
38
+ *
39
+ * Mutual exclusion rests on one exclusive-create — a read-then-write pair would leave a window where both processes see no lock. A lock whose holder is no longer running, or whose record is older than the staleness window, or whose contents cannot be parsed at all, is removed and retaken: a machine that crashed mid-resync must not need manual cleanup before `claude` works again. A lock genuinely held by a live process is waited on, and only after the full retry budget does this give up — loudly, with the holder's pid, rather than proceeding unsynchronised.
40
+ */
41
+ export declare function acquireIdentityLock(params: AcquireIdentityLockParams): IdentityLock;
@@ -0,0 +1,121 @@
1
+ import type { ConfigFileReader } from "../config/load";
2
+ /**
3
+ * Filesystem operations the launcher needs, injected so no test in this project ever touches a real filesystem — consistent with the resolver core's own injected-facts approach in `src/resolve/types.ts`.
4
+ */
5
+ export interface FsPort {
6
+ /** Reads a file's contents as UTF-8 text, or returns undefined when it does not exist. Used for plain-text files like the persisted active-identity file. */
7
+ readonly readFileUtf8: (filePath: string) => string | undefined;
8
+ /** Reads and parses a config file (JSON/YAML/JS via cosmiconfig in the real implementation), returning undefined when it does not exist. Mirrors `src/config/load.ts`'s own injected reader shape so `loadIdentity` can compose directly with `loadConfigFile`. */
9
+ readonly readConfigFile: ConfigFileReader;
10
+ /** Lists one directory's immediate child names, or an empty list when it does not exist. Used to name the known providers in an unknown-provider refusal. */
11
+ readonly readdir: (dir: string) => readonly string[];
12
+ }
13
+ /** What one node of a tree is, as reported by an `lstat` that never follows symlinks. */
14
+ type FarmNodeKind = "dir" | "file" | "symlink";
15
+ /** The subset of stat information the farm builder and the fact builder actually read. */
16
+ export interface FarmStat {
17
+ readonly kind: FarmNodeKind;
18
+ readonly mtimeMs: number;
19
+ readonly sizeBytes: number;
20
+ }
21
+ /**
22
+ * Every filesystem operation the farm resync performs, injected so the whole of `src/launcher/farm.ts` — symlink creation, materialisation, adoption copies, and the atomic swap itself — runs against an in-memory fake in tests and never against a real `~/.claude` or `~/.claude-use`.
23
+ *
24
+ * `lstat` never follows symlinks: a `~/.claude` that contains a directory symlink escaping the tree (a skills directory linked out to a separate dotfiles repository, say) must be recorded as one symlink entry rather than recursed into, both to keep the fact manifest finite and because the farm links such an entry at `<claudeHome>/<rel>` and lets the OS resolve the remaining hops.
25
+ */
26
+ export interface FarmFs {
27
+ /** Stats one path without following symlinks, or returns undefined when nothing is there. */
28
+ readonly lstat: (path: string) => FarmStat | undefined;
29
+ /** Lists one directory's immediate child names, or an empty list when it does not exist. */
30
+ readonly readdir: (path: string) => readonly string[];
31
+ /** Creates a directory and any missing parents, succeeding silently when it already exists. */
32
+ readonly mkdirp: (path: string) => void;
33
+ /** Creates a symbolic link at `linkPath` pointing at `target`. */
34
+ readonly symlink: (target: string, linkPath: string) => void;
35
+ /** Renames `from` to `to`. Both are always within one directory tree on one filesystem in this module's own usage, so this is the atomic primitive the farm swap is built on. */
36
+ readonly rename: (from: string, to: string) => void;
37
+ /** Removes a path and everything beneath it, succeeding silently when it does not exist. */
38
+ readonly removeRecursive: (path: string) => void;
39
+ /** Copies a file, symlink, or whole directory tree from `from` to `to`. */
40
+ readonly copyRecursive: (from: string, to: string) => void;
41
+ /** Reads a file's contents as UTF-8 text, or undefined when it does not exist. */
42
+ readonly readFileUtf8: (path: string) => string | undefined;
43
+ /** Writes a file's full contents as UTF-8 text, creating or truncating it. */
44
+ readonly writeFileUtf8: (path: string, contents: string) => void;
45
+ /** Creates a file only if it does not already exist, returning false when it does. This is the exclusive-create the identity lock depends on for its mutual exclusion — a read-then-write pair would race. */
46
+ readonly writeFileExclusive: (path: string, contents: string) => boolean;
47
+ /** A stable content hash of one file, or undefined when it is missing or is not a regular file. Used only to recognise farm data that has already been adopted into the canonical tree. */
48
+ readonly hashFile: (path: string) => string | undefined;
49
+ }
50
+ /** The outcome of one `spawnSync` call, mirroring Node's own `SpawnSyncReturns` shape narrowly to what the launcher actually reads. */
51
+ export interface SpawnResult {
52
+ /** The child's exit code, or null when it was terminated by a signal instead of exiting normally. */
53
+ readonly status: number | null;
54
+ /** The signal that terminated the child, or null when it exited normally. */
55
+ readonly signal: NodeJS.Signals | null;
56
+ /** Set when the child process could not even be spawned (e.g. the binary does not exist). */
57
+ readonly error?: Error;
58
+ }
59
+ /** Runs the real `claude` binary synchronously, injected so no test ever spawns a real process. The Node equivalent of the legacy bash tool's `exec` is `spawnSync` with `stdio: "inherit"` followed by propagating the child's own exit code — see `src/launcher/spawn.ts`. */
60
+ export interface SpawnPort {
61
+ readonly spawnSync: (command: string, args: readonly string[], options: {
62
+ readonly stdio: "inherit";
63
+ readonly env: Readonly<Record<string, string | undefined>>;
64
+ }) => SpawnResult;
65
+ }
66
+ /** The outcome of one synchronous external-command run (distinct from `SpawnPort`, which is specifically for replacing this process with the real `claude` binary — `RunPort` is for auxiliary commands like `git rev-parse` for branch detection, or `security find-generic-password` for the macOS Keychain diagnostic in `check.ts`, whose output this process needs to keep running and read). */
67
+ interface RunResult {
68
+ readonly status: number | null;
69
+ readonly stdout: string;
70
+ readonly stderr: string;
71
+ }
72
+ /** Runs an external command and captures its output, injected so no test ever shells out for real. Not used by this phase's own modules yet — declared here so every later module (branch-conditioned `when` evaluation, `check.ts`'s Keychain lookup) shares one injectable shape rather than each inventing its own. */
73
+ export interface RunPort {
74
+ readonly run: (command: string, args: readonly string[]) => RunResult;
75
+ }
76
+ /**
77
+ * The running process itself: its environment, its logical argv, and the ability to terminate with an exit code.
78
+ *
79
+ * `argv` is the *logical* CLI arguments only — whatever the user typed after `claude`/`claude-use`, already stripped of the Node executable path and the script path (`process.argv.slice(2)` in the real implementation). `exit` mirrors `process.exit`'s own `never` return type: a real process never returns from it, and a test fake enforces the same shape by throwing a marker so control flow unwinds the same way in both cases.
80
+ */
81
+ export interface ProcPort {
82
+ readonly env: Readonly<Record<string, string | undefined>>;
83
+ readonly argv: readonly string[];
84
+ readonly exit: (code: number) => never;
85
+ }
86
+ /** Where the launcher reports what it decided and why — refusals, warnings about an identity that could not be loaded, and (per the "strictly-better than the legacy script" goal) which identity/profile ended up selected. */
87
+ export interface LogPort {
88
+ readonly info: (message: string) => void;
89
+ readonly warn: (message: string) => void;
90
+ readonly error: (message: string) => void;
91
+ }
92
+ /** The running headroom daemon one launch routed through, as `HeadroomPort.ensure` reports it. */
93
+ export interface HeadroomUp {
94
+ /** The loopback port the local headroom proxy listens on. */
95
+ readonly port: number;
96
+ /** The loopback port the supervisor's MITM CONNECT proxy listens on: what an OAuth launch points HTTPS_PROXY at. */
97
+ readonly mitmPort: number;
98
+ /** Path to the MITM proxy's CA certificate, what an OAuth launch points NODE_EXTRA_CA_CERTS at so the child trusts the terminated TLS. */
99
+ readonly caCertPath: string;
100
+ /** The project identity headroom scopes memory state to: the git repo root of the launch directory, or the directory itself outside a repository. */
101
+ readonly projectId: string;
102
+ }
103
+ /**
104
+ * Everything the launcher needs from the headroom daemon, injected so its daemon-supervision side effects stay behind a port and the launcher's own logic stays pure over it (`src/realPorts.ts` wires the real one; tests wire fakes).
105
+ *
106
+ * `ensure` brings the daemon up (spawning the detached supervisor when nothing healthy is running) and registers this launch in its session registry; `release` removes that registration when the spawned `claude` exits.
107
+ */
108
+ export interface HeadroomPort {
109
+ readonly ensure: () => HeadroomUp;
110
+ readonly release: () => void;
111
+ }
112
+ /**
113
+ * Everything the launcher needs from the front-door daemon, injected like `HeadroomPort`: `ensure` brings the door up (spawning its detached supervisor when nothing healthy is serving), registers this launch in its session registry and returns the listener's loopback port; `release` removes that registration when the spawned `claude` exits.
114
+ */
115
+ export interface FrontDoorPort {
116
+ readonly ensure: () => {
117
+ readonly port: number;
118
+ };
119
+ readonly release: () => void;
120
+ }
121
+ export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The resolved set of paths claude-use reads and writes under its own root.
3
+ *
4
+ * `root` is always CLAUDE_USE_HOME when that environment variable is set (used by every test in this project, and by any real installation that wants to relocate its state), falling back to `~/.claude-use` only when the variable is unset. Every other field is derived from `root` so there is exactly one place a path can go wrong.
5
+ */
6
+ export interface LayoutPaths {
7
+ /** The resolved root directory — CLAUDE_USE_HOME, or ~/.claude-use when unset. */
8
+ readonly root: string;
9
+ /** Directory holding one subdirectory per identity (symlink farm + local credentials/daemon state). */
10
+ readonly identitiesDir: string;
11
+ /** Directory holding named, reusable configuration profile JSON files. */
12
+ readonly configProfilesDir: string;
13
+ /** Directory holding one `<name>.json` provider definition per API provider. */
14
+ readonly providersDir: string;
15
+ /** Path to the directory-rules.json file describing directory-scoped identity/profile pins. */
16
+ readonly directoryRulesFile: string;
17
+ /** Path to the persisted active-identity file (the identity `claude-use identity use` selected). */
18
+ readonly activeIdentityFile: string;
19
+ /** Path to the global config.json (user-global override layer, and default profile/walk-limit settings). */
20
+ readonly globalConfigFile: string;
21
+ /** Path to the categories.local.json overlay recording answers to "unclassified entry" prompts. */
22
+ readonly categoriesLocalFile: string;
23
+ /** Path to the claude-shim.json marker recording where `claude-use shim enable` last placed a `claude`-named copy of this executable, and how. */
24
+ readonly claudeShimFile: string;
25
+ /** Directory holding the headroom daemon's coordination state: state.json, the start lock, and the session registry. */
26
+ readonly headroomDir: string;
27
+ /** Directory holding the MITM proxy's certificate authority: ca.pem (public, handed to children as NODE_EXTRA_CA_CERTS) and ca.key (mode 0600). */
28
+ readonly headroomCaDir: string;
29
+ /** Path to the MITM proxy's CA certificate, generated once and stable across restarts so children keep trusting it. */
30
+ readonly headroomCaCertFile: string;
31
+ /** Path to the MITM proxy's CA private key, mode 0600. */
32
+ readonly headroomCaKeyFile: string;
33
+ /** Path to the headroom supervisor's state.json: pids, port, version, allowlist hash, last error. */
34
+ readonly headroomStateFile: string;
35
+ /** Path to the exclusive-create marker guarding "who spawns the supervisor" so concurrent launches start at most one. */
36
+ readonly headroomLockFile: string;
37
+ /** Directory holding one `<launcher-pid>.json` session-registry entry per live launch routed through headroom. */
38
+ readonly headroomSessionsDir: string;
39
+ /** Directory holding daemon logs (the headroom proxy's stdout/stderr and the supervisor's own output). */
40
+ readonly logsDir: string;
41
+ /** Path to the headroom daemon's combined log, appended to by both the supervisor and the proxy it owns. */
42
+ readonly headroomLogPath: string;
43
+ /** Directory holding the front-door daemon's coordination state: state.json, the start lock, and the session registry. */
44
+ readonly frontdoorDir: string;
45
+ /** Path to the front door's state.json: its supervisor pid, its port, and the sticky port it last served on. */
46
+ readonly frontdoorStateFile: string;
47
+ /** Path to the exclusive-create marker guarding "who spawns the front-door supervisor" so concurrent launches start at most one. */
48
+ readonly frontdoorLockFile: string;
49
+ /** Directory holding one `<launcher-pid>.json` session-registry entry per live launch routed through the front door. */
50
+ readonly frontdoorSessionsDir: string;
51
+ /** Path to the front door's log: one line per lifecycle event and routed failure, never a token. */
52
+ readonly frontdoorLogPath: string;
53
+ }
54
+ /**
55
+ * Resolves the current CLAUDE_USE_HOME root: the environment variable when set to a non-empty string, otherwise `~/.claude-use`. An empty string counts as unset, consistent with how this project treats empty-string environment variables elsewhere (see the ambient-credential guard).
56
+ */
57
+ export declare function resolveClaudeUseHome(): string;
58
+ /**
59
+ * Resolves the canonical `~/.claude` directory every farm symlink points back into: `CLAUDE_USE_CLAUDE_HOME` when set to a non-empty string, otherwise `~/.claude`.
60
+ *
61
+ * The override exists for the same reason `CLAUDE_USE_HOME` does. Exercising a real resync end to end means building and swapping real directories, and doing that against a real, in-use `~/.claude` to find out whether the code is correct is not an acceptable way to find out. Pointing both variables at throwaway directories makes a full end-to-end run safe.
62
+ */
63
+ export declare function resolveClaudeHome(): string;
64
+ /** Builds the full LayoutPaths structure from a given root directory. */
65
+ export declare function buildLayoutPaths(root: string): LayoutPaths;
66
+ /** Resolves CLAUDE_USE_HOME and builds the full LayoutPaths structure in one call. */
67
+ export declare function resolveLayoutPaths(): LayoutPaths;
@@ -0,0 +1,37 @@
1
+ import { type WhenCondition } from "../config/schema";
2
+ import type { EntryFact } from "./types";
3
+ /** Parses a duration literal like `90d` or `500ms` into milliseconds. Throws on anything the schema's own regex would already have rejected, so a malformed value can never be silently treated as zero. */
4
+ export declare function parseDuration(value: string): number;
5
+ /** True when the value would satisfy the schema's duration regex. */
6
+ export declare function isDuration(value: string): boolean;
7
+ /** Everything a `when` condition can be evaluated against, injected rather than read. */
8
+ export interface ConditionContext {
9
+ readonly nowMs: number;
10
+ /** Facts about the specific entry being decided. Absent when evaluating a rule-level condition that has no single entry (a directory rule's own `when`). */
11
+ readonly fact?: EntryFact;
12
+ /** The branch checked out at `cwd`, or undefined when `cwd` is not in a repository. */
13
+ readonly branch?: string;
14
+ /** True when the repository is in detached-HEAD state, in which case no `branch` condition can match. */
15
+ readonly branchDetached?: boolean;
16
+ readonly env: Readonly<Record<string, string | undefined>>;
17
+ }
18
+ /** The result of evaluating one `when` object. */
19
+ export interface WhenEvaluation {
20
+ readonly passed: boolean;
21
+ /** Which condition fields were present and evaluated. */
22
+ readonly checked: readonly string[];
23
+ /** Which of those fields did not hold. Empty when `passed` is true. */
24
+ readonly failed: readonly string[];
25
+ }
26
+ /** True when `branch` matches `pattern`. The pattern is glob-capable (`client/*`); a detached HEAD or a non-repository directory never matches. */
27
+ export declare function matchBranch(pattern: string, branch: string | undefined, detached?: boolean): boolean;
28
+ /**
29
+ * Evaluates a `when` object. Every present field must hold — conditions AND together within one object.
30
+ *
31
+ * An absent condition is vacuously true, so `when: {}` passes; `claude-use check` warns about that rather than erroring, since an empty object is more likely a half-finished edit than an intentional statement.
32
+ *
33
+ * `newerThan`, `olderThan`, and `maxSizeBytes` read the subtree-aggregated facts (`latestMtimeMs`, `totalSizeBytes`), never the entry's own inode stat: a directory's own mtime does not change when a file three levels beneath it is rewritten, and its own size is a ~4KB inode figure that says nothing about what it contains.
34
+ */
35
+ export declare function evaluateWhen(when: WhenCondition | undefined, context: ConditionContext): WhenEvaluation;
36
+ /** True when the `when` object is present but empty, which is vacuously true and therefore has no effect. */
37
+ export declare function isVacuousWhen(when: WhenCondition | undefined): boolean;
@@ -0,0 +1,45 @@
1
+ import { type CategoryName } from "../config/schema";
2
+ import type { CompiledRule, Decision, Diagnostic, EliminatedRule, EntryFact, EntryFacts, FlattenedCascade } from "./types";
3
+ /** Everything phase two needs, all of it injected. */
4
+ export interface DecideParams {
5
+ readonly flattened: FlattenedCascade;
6
+ readonly facts: EntryFacts;
7
+ /** Every top-level `~/.claude` entry name mapped to its category, or to null when nothing recognises it. */
8
+ readonly classification: ReadonlyMap<string, CategoryName | null>;
9
+ }
10
+ /** A rule chosen by `selectRule`, alongside the candidates eliminated on the way to it. */
11
+ export interface RuleSelection {
12
+ readonly rule?: CompiledRule;
13
+ readonly eliminated: readonly EliminatedRule[];
14
+ }
15
+ /**
16
+ * Picks the winning entries rule for one path.
17
+ *
18
+ * Candidates are ranked by `compareSpecificity` and taken in order. A candidate whose `when` condition **fails is eliminated** and resolution continues to the next-most-specific matching rule — it never falls straight through to the category default, and a failed condition never inverts the rule's boolean value, which would not have a coherent meaning anyway. Only when no candidate survives does the caller fall back to the category.
19
+ */
20
+ export declare function selectRule(relPath: string, flattened: FlattenedCascade, context: {
21
+ nowMs: number;
22
+ branch?: string;
23
+ branchDetached?: boolean;
24
+ env: Readonly<Record<string, string | undefined>>;
25
+ fact?: EntryFact;
26
+ }): RuleSelection;
27
+ /**
28
+ * Resolves one path to a sharing decision, in exactly this order:
29
+ *
30
+ * 1. **The secret floor.** If the path's top-level entry classifies as `secret`, the answer is `false` unconditionally. This check sits *above* the entries lookup, not merely at the least-specific end of the cascade, and it fires on the path's **real classification**, never on a lexical reading of the key that matched it — so a glob written under a `runtime/` prefix that happens to reach `.credentials.json` is neutralised exactly like a deliberate `secret/` key would be.
31
+ * 2. **Unclassified.** A top-level entry nothing recognises is not shared, and is reported rather than silently guessed at.
32
+ * 3. **Entries rules**, most specific first, with a failing condition eliminating a candidate rather than ending the search.
33
+ * 4. **The category**: a layer's own toggle if any layer set one, otherwise the shipped default.
34
+ */
35
+ export declare function resolveEntry(relPath: string, params: DecideParams): {
36
+ decision: Decision;
37
+ diagnostics: Diagnostic[];
38
+ };
39
+ /** Every decision the resolver reaches, plus every diagnostic raised along the way (phase-one diagnostics included, and de-duplicated). */
40
+ export interface ResolveAllResult {
41
+ readonly decisions: ReadonlyMap<string, Decision>;
42
+ readonly diagnostics: readonly Diagnostic[];
43
+ }
44
+ /** Resolves every entry in the fact manifest. */
45
+ export declare function resolveAll(params: DecideParams): ResolveAllResult;
@@ -0,0 +1,41 @@
1
+ import type { ConfigProfile } from "../config/schema";
2
+ import type { Diagnostic, Layer, LayerId } from "./types";
3
+ /** One configuration profile as loaded from disk, with its entries key order captured at load time. */
4
+ export interface ProfileSource {
5
+ readonly name: string;
6
+ readonly profile: ConfigProfile;
7
+ readonly entryOrder?: readonly string[];
8
+ /** Where it was loaded from, for diagnostics. Defaults to the profile name. */
9
+ readonly filepath?: string;
10
+ }
11
+ /** Loads a profile by name. Injected, so nothing in the resolver reads a file. Returns undefined when no profile of that name exists. */
12
+ export type ProfileLoader = (name: string) => ProfileSource | undefined;
13
+ /** The linearised `extends` chain plus anything that went wrong resolving it. */
14
+ export interface LinearisedProfiles {
15
+ /** Profile names, base-first: every profile appears exactly once, after everything it extends. */
16
+ readonly order: readonly string[];
17
+ readonly diagnostics: readonly Diagnostic[];
18
+ }
19
+ /**
20
+ * Linearises a profile's `extends` graph into a base-first order.
21
+ *
22
+ * The walker keeps **two separate structures**, and the distinction is load-bearing rather than stylistic:
23
+ *
24
+ * - `stack` is the current depth-first path. A name already on it means the graph genuinely loops back on itself, which is the only real cycle.
25
+ * - `visited` is every name already emitted anywhere in the walk. A name already in it is a diamond — two branches sharing a base — and must simply be skipped, not re-emitted.
26
+ *
27
+ * Collapsing the two into one shared set produces a silent wrong answer in either direction. Using `stack` alone re-emits a shared base a second time in a diamond, so the base's values get re-applied on top of an intermediate profile that deliberately overrode them — the override silently disappears. Using `visited` alone false-flags every legitimate diamond as a cycle. Neither failure crashes; both just resolve to the wrong configuration.
28
+ *
29
+ * Emission is **post-order**: a profile is appended only after everything it extends, so `c extends [a, b]` where both extend `base` linearises to `[base, a, b, c]` — never `[base, a, base, b, c]`.
30
+ */
31
+ export declare function lineariseProfile(rootName: string, load: ProfileLoader): LinearisedProfiles;
32
+ /**
33
+ * Turns a profile's `extends` chain into cascade layers, base-first, starting at `startId`.
34
+ *
35
+ * A resolved profile is not a separate mechanism from the outer cascade: its chain flattens through the same ordered-layer sequence everything else uses, so a profile's resolved patch is simply one more run of inputs to the same algorithm.
36
+ */
37
+ export declare function profileLayers(rootName: string, load: ProfileLoader, startId: LayerId): {
38
+ layers: Layer[];
39
+ nextId: LayerId;
40
+ diagnostics: readonly Diagnostic[];
41
+ };
@@ -0,0 +1,13 @@
1
+ import type { CompiledRule, FlattenedCascade, Layer } from "./types";
2
+ /**
3
+ * Cascade phase one: walk the ordered layer sequence once, spreading each layer's `categories` and `entries` over an accumulator. A later layer's value for the exact same category name, or the exact same key, replaces an earlier layer's value for that identical key. No path-specificity reasoning happens here — that is phase two's job.
4
+ *
5
+ * The entries accumulator is keyed by the **canonical** pattern, not the raw written key, so two differently-spelled but equivalent keys (`history/projects/~/work/x` and `history/projects/$HOME/work/x` after expansion) collapse into one rule here instead of racing as two separate candidates in phase two. Every surviving rule carries its own `{ layer, ordinal }`, which is what lets phase two tell a same-layer conflict from a cross-layer one without a separate provenance side-table.
6
+ *
7
+ * A key written under the `secret/` prefix is rejected outright with its own diagnostic and contributes no rule at all: `secret` is the one category no layer may open, and a deliberate attempt to name a secret path deserves a clearer error than the silent neutralisation the resolve-time floor check applies to a glob that reaches one incidentally.
8
+ */
9
+ export declare function flattenLayers(layers: readonly Layer[], options: Readonly<{
10
+ home: string;
11
+ }>): FlattenedCascade;
12
+ /** Every compiled rule that matches `relPath`, ranked most-specific first. */
13
+ export declare function matchingRules(flattened: FlattenedCascade, relPath: string): CompiledRule[];
@@ -0,0 +1,58 @@
1
+ import { type CategoryName } from "../config/schema";
2
+ import { CliError } from "../cliError";
3
+ import type { CompiledRule } from "./types";
4
+ /** True when `pattern` contains no glob metacharacter, so it can only ever match one literal path. */
5
+ export declare function isExactPattern(pattern: string): boolean;
6
+ /** The pattern's leading run of characters before the first glob metacharacter — the part that is definitely literal. */
7
+ export declare function literalPrefixOf(pattern: string): string;
8
+ /** Normalises a relative path fragment: collapses repeated slashes, drops `./` segments, and strips a trailing slash. Deliberately case-preserving and case-sensitive. */
9
+ export declare function normaliseRelative(fragment: string): string;
10
+ /** The outcome of canonicalising one entries key. */
11
+ export interface CanonicalKey {
12
+ readonly declaredCategory: CategoryName;
13
+ /** The pattern with its category prefix stripped, relative to `~/.claude`, with any `history/projects/` path fragment already encoded. */
14
+ readonly canonicalPattern: string;
15
+ /** The `history/projects/` path fragment as written, before encoding — retained so ambiguity reporting can name what the author actually typed. */
16
+ readonly projectFragment?: string;
17
+ }
18
+ /** Raised when an entries key cannot be canonicalised. Carries the offending key so the diagnostic can name it. */
19
+ export declare class EntryKeyError extends CliError {
20
+ readonly key: string;
21
+ readonly reason: "unrooted-project-path" | "malformed";
22
+ constructor(key: string, message: string, reason: "unrooted-project-path" | "malformed");
23
+ }
24
+ /**
25
+ * Canonicalises one entries key into a pattern over paths relative to `~/.claude`.
26
+ *
27
+ * Every key is `<category>/<real-relative-path>`, so the category prefix is stripped and the remainder kept as written — with one deliberately narrow exception. Anything written after the literal `history/projects/` prefix is a real absolute working directory (optionally globbed), not a literal child directory name, because that directory's only real children are Claude Code's own encoded names and there is nothing else meaningful to reference there. Those fragments get `~`-expanded and forward-encoded; every other key in the whole design is a plain literal path or an ordinary glob over one, matched exactly as written.
28
+ */
29
+ export declare function canonicaliseEntryKey(key: string, options: Readonly<{
30
+ home: string;
31
+ }>): CanonicalKey;
32
+ /**
33
+ * Compiles a canonical pattern into a matcher that covers the pattern itself *and* everything beneath it, so a rule written as `knowledge/skills` decides `skills/commit/SKILL.md` too. Two picomatch matchers are needed rather than one: `skills/**` alone covers the subtree but `projects/*` does not match `projects/x/y`, and `projects/*` alone does not cover the files inside a matched project directory.
34
+ *
35
+ * Matching is deliberately case-sensitive (`nocase: false`) whatever the host filesystem does, so the same config behaves identically on a case-insensitive APFS volume and a case-sensitive one. `dot: true` so a glob can match a leading-dot entry.
36
+ */
37
+ export declare function compileMatcher(canonicalPattern: string): (relPath: string) => boolean;
38
+ /**
39
+ * True when some path *underneath* `dir` could match `canonicalPattern`, whether or not such a path exists today.
40
+ *
41
+ * This is what makes a conditional rule force materialisation: a directory Claude Code will write new children into cannot be symlinked wholesale if a rule might have something to say about those not-yet-existing children. Answering that from the pattern's structure rather than from the current file listing is the whole point — asking "does any existing child match" would pass the obvious test and fail the real one.
42
+ */
43
+ export declare function patternCouldReachUnder(canonicalPattern: string, dir: string): boolean;
44
+ /**
45
+ * Ranks two compiled rules that both match the same path. A positive result means `a` wins.
46
+ *
47
+ * The order, exactly:
48
+ *
49
+ * 1. **Different layers — the later layer wins, period.** This rank sits deliberately *above* exactness. Ranking exactness first would let an untrusted committed `.claude-use.json`'s exact key beat your own later, personal glob override, breaking the design's stated trust property that a directory-scoped local rule can only ever tighten what a committed file opened, never the reverse. Nothing in the design asserts that an earlier exact key beats a later glob, so nothing is lost by closing that hole.
50
+ * 2. Same layer: an exact literal beats a glob.
51
+ * 3. Same layer: the longer literal prefix wins.
52
+ * 4. Same layer: more path segments wins, which separates a one-wildcard pattern from a two-wildcard one at the same depth.
53
+ * 5. Same layer: the later ordinal — source order within the file — wins.
54
+ * 6. Canonical pattern, compared as a string, as a last-resort determinism guarantee.
55
+ *
56
+ * The order must be **total**: returning 0 for two distinct rules would make the winner depend on iteration order, which tests would only catch intermittently.
57
+ */
58
+ export declare function compareSpecificity(a: CompiledRule, b: CompiledRule): number;
@@ -0,0 +1,30 @@
1
+ import { type ClassifyResult } from "../config/classify";
2
+ import type { CategoryClassification, CategoryClassificationOverlay } from "../config/schema";
3
+ import { type ResolveAllResult } from "./decide";
4
+ import { type FarmPlan } from "./plan";
5
+ import { type AssembledCascade, type CascadeInput } from "./walk";
6
+ import type { Diagnostic, EntryFacts, FlattenedCascade } from "./types";
7
+ /** Everything one launch needs resolved, in one call. */
8
+ export interface ResolveDecisionsInput {
9
+ readonly facts: EntryFacts;
10
+ readonly cascade: CascadeInput;
11
+ readonly classification: {
12
+ readonly defaults: CategoryClassification;
13
+ readonly overlay?: CategoryClassificationOverlay;
14
+ };
15
+ }
16
+ /** The fully resolved state of one launch: the layers that composed it, every entry's decision, the farm layout, and everything worth reporting. */
17
+ export interface ResolvedState {
18
+ readonly assembled: AssembledCascade;
19
+ readonly flattened: FlattenedCascade;
20
+ readonly classification: ClassifyResult;
21
+ readonly decisions: ResolveAllResult["decisions"];
22
+ readonly farm: FarmPlan;
23
+ readonly diagnostics: readonly Diagnostic[];
24
+ }
25
+ /** The top-level `~/.claude` entry names present in a fact manifest, in sorted order. */
26
+ export declare function topLevelNames(facts: EntryFacts): string[];
27
+ /**
28
+ * Runs the whole pipeline for one launch: assemble the ordered layer sequence, flatten it, classify the real `~/.claude` entries, decide every path, and plan the resulting farm.
29
+ */
30
+ export declare function resolveDecisions(input: ResolveDecisionsInput): ResolvedState;
@@ -0,0 +1,57 @@
1
+ import type { Decision, EntryFacts, FlattenedCascade } from "./types";
2
+ /** What the farm should contain at one path. */
3
+ type FarmPlanEntry =
4
+ /** A single symlink standing in for this whole path — always absolute, always under `claudeHome`. */
5
+ {
6
+ readonly kind: "link";
7
+ readonly rel: string;
8
+ readonly target: string;
9
+ }
10
+ /** A real local directory the farm builds itself, because the decision beneath it is not uniform or because a conditional rule could touch it. */
11
+ | {
12
+ readonly kind: "materialise";
13
+ readonly rel: string;
14
+ readonly reason: MaterialiseReason;
15
+ }
16
+ /** Nothing at all in the farm for this path. */
17
+ | {
18
+ readonly kind: "omit";
19
+ readonly rel: string;
20
+ };
21
+ /** Why a directory has to be built rather than symlinked. */
22
+ type MaterialiseReason =
23
+ /** Descendants of this directory resolve to different decisions, so a single symlink cannot express the outcome. */
24
+ "split-decision"
25
+ /** A conditional entries rule could match something under this directory, so the decision has to stay live. */
26
+ | "conditional-rule";
27
+ /** The full farm plan for one resolved cascade. */
28
+ export interface FarmPlan {
29
+ /** Every planned path, parents before children. */
30
+ readonly entries: readonly FarmPlanEntry[];
31
+ /** Directories the farm builds itself, in plan order. Written into the farm manifest so the next resync can tell them apart from directories Claude Code created. */
32
+ readonly materialised: readonly string[];
33
+ readonly links: readonly {
34
+ readonly rel: string;
35
+ readonly target: string;
36
+ }[];
37
+ }
38
+ /** Everything `planFarm` needs, all of it already resolved. */
39
+ export interface PlanFarmParams {
40
+ readonly facts: EntryFacts;
41
+ readonly decisions: ReadonlyMap<string, Decision>;
42
+ readonly flattened: FlattenedCascade;
43
+ }
44
+ /** Indexes the fact manifest by parent path, so the plan can descend the tree without touching a filesystem. */
45
+ export declare function buildChildIndex(facts: EntryFacts): Map<string, string[]>;
46
+ /**
47
+ * Turns a resolved set of decisions into a concrete farm layout.
48
+ *
49
+ * A directory becomes one symlink when the decision is uniform across its whole subtree, which is what keeps the common resync cheap — a few dozen top-level symlinks rather than a rebuilt tree. It is materialised, and recursed into, when either:
50
+ *
51
+ * - its subtree's decisions genuinely disagree, so no single symlink can express the outcome; or
52
+ * - **any** conditional entries rule could reach something under it, even when today's evaluation happens to be uniform. This second case is the one a naive "is the decision uniform right now" check gets wrong: a symlinked directory freezes the decision for every file Claude Code creates *after* the resync, so a `newerThan` condition meant to share recent sessions would never see a session written five minutes from now. The bug only surfaces for files that do not exist yet, which is precisely why the check has to be structural rather than observational.
53
+ *
54
+ * Every symlink target is `<claudeHome>/<rel>`, unconditionally. That matters for the entries inside a materialised directory that are themselves relative symlinks escaping `~/.claude` (a real condition, not a hypothetical one — a skills directory linked out to a separate dotfiles repository, for instance). Copying such a link's own relative target verbatim would resolve to a different place from the farm's own depth, and copying its resolved absolute target would pin the farm to wherever that link pointed at resync time. Linking at `<claudeHome>/<rel>` and letting the OS follow the remaining hops from there is the only form that stays correct.
55
+ */
56
+ export declare function planFarm(params: PlanFarmParams): FarmPlan;
57
+ export {};