import { StatusLevel } from '#src/core/types.js'; import { type RawConfigValidationResult } from '#src/config/schema.js'; import type { CommandLineConfigOverrides, ConsoleLevelInput, EvalToolCoverageConfig, GthConfig, RawGthConfig } from '#src/config/types.js'; /** * Find THE project config by walking up from cwd toward a stop boundary, returning the FIRST * match (first-match-win: nearest dir, then format precedence within that dir — NOT a merged * stack). Detection ({@link hasProjectConfig}/{@link hasAnyConfig}) and loading ({@link initConfig}) * both go through this, so they can never disagree. * * Stop boundary — the dir is SEARCHED, then ascent stops at: a dir containing `.git` (the git * root), the user's home dir, or the filesystem root — whichever comes first. So a config IN the * git root (or home) is found; a config ABOVE it is not. * * A `customConfigPath` override wins outright (no walking). * * NOTE (identity profile): with an `identityProfile` set, each dir's per-format resolver * (`resolveConfigPath`) tries the profile path `.gsloth/.gsloth-settings//` * but FALLS BACK to the plain `/` when the profile file is absent. So a match here does * NOT prove the named profile itself has a config — it may be a plain (non-profile) config. Use * {@link resolveIdentityProfileConfigPath} when you need to know a profile specifically resolved. * * @returns the matched `{ dir, path }`, or `undefined` when no project config exists within the * boundary. */ export declare function findProjectConfigPath(commandLineConfigOverrides: CommandLineConfigOverrides): { dir: string; path: string; } | undefined; /** * STRICT existence check for an EXPLICITLY-named identity profile: does * `.gsloth/.gsloth-settings//` resolve to a real config file anywhere in * the same up-tree search {@link findProjectConfigPath} walks? Returns the resolved profile config * path (nearest dir, then format precedence) when the profile has its OWN config, `undefined` * otherwise. * * Unlike {@link findProjectConfigPath}, it matches ONLY the profile-specific path — it NEVER falls * through to a plain `/` and NEVER falls back to the global config. That strictness is * the whole point: it lets a caller distinguish "this named profile really exists" from "a bare * config happens to be present / a global config exists," a distinction the loader's fall-through * deliberately blurs. * * PURE PREDICATE — never throws, never calls `exit`, so it can be asked the question without * committing to an outcome. {@link initConfig} uses it to enforce that an explicitly-named profile * really exists (raising a catchable {@link ConfigDiscoveryError} when it does not), and callers * that want to CLASSIFY rather than fail — BATCH-12's identity matrix checks every declared identity * up front so one message can name them all — ask it directly. A blank/whitespace-only name counts * as "no profile" → `undefined`. * * @param identityProfile The explicitly-requested identity profile name. * @param options CFG-56 — `globalOnly` searches `~/.gsloth/.gsloth-settings//` INSTEAD of * the up-tree project walk, for a `--global` run. * @returns The resolved profile config path, or `undefined` when the profile has no config. */ export declare function resolveIdentityProfileConfigPath(identityProfile: string, options?: { globalOnly?: boolean; }): string | undefined; /** * Loads the global gsloth config (if present) from the global `~/.gsloth` folder. * * Precedence support: the returned raw config is intended to act as the BASE that the * project config (and CLI overrides) merge on top of, so any value here is the lowest * user-controlled layer (still above {@link DEFAULT_CONFIG}). * * Lookup order within the global folder, first match wins: * `.gsloth.config.json` -> `.gsloth.config.jsonc` -> `.gsloth.config.js` -> `.gsloth.config.mjs` * * Absence of every variant is a no-op: returns `undefined` so behaviour is unchanged. * * NOTE: secrets (API keys) may live in this file; this function must never log its * contents. Only non-sensitive diagnostics (the resolved path / parse failure) are emitted. * * @param options CFG-56 — `identityProfile` relocates the lookup to * `~/.gsloth/.gsloth-settings//` and `globalOnly` scopes the profile names this layer refers * to. Both are set ONLY for a `--global` run; see `globalLayerProfile`. * @returns The raw global config object, or `undefined` when no global config exists. */ export declare function loadGlobalRawConfig(options?: { identityProfile?: string; globalOnly?: boolean; }): Promise | undefined>; /** * GS2-41 — resolve a named profile's `extends` inheritance into a single composed raw config, * riding the SAME GS2-1 deep-merge the config LAYERS use (NO second merge engine). When the given * profile config declares `extends: ""`, the base profile's config resolves FIRST * (recursively — a base may itself extend another, so base-of-base resolves first), then this * profile's own fields merge on top with last-wins semantics: the child overrides the base, nested * objects merge, arrays REPLACE except the additive-array fields (`allowDirs`, `aiignore.patterns` * and the three `approvals` rule lists, see `isAdditiveArrayField`) which accumulate * base+child. The `extends` key itself is consumed and never leaks into the composed output. * * A config WITHOUT `extends` is returned UNCHANGED — every non-inheriting config (the vast * majority) is untouched and behaves exactly as before. * * Composition is CONFINED to the profile-dir layer: it produces the single raw config that then * acts as the project-file layer the global config underlays and CLI flags overlay, preserving * GS2-33's outer precedence `CLI flags > profile (base+child composed) > global > defaults`. It is * therefore invoked in {@link initConfig} on the loaded project/profile config BEFORE * `applyGlobalConfigBase`. * * The base profile is discovered with {@link resolveIdentityProfileConfigPath} (the SAME strict * up-tree profile walk `--profile` uses), so `extends` names a profile exactly as a user selects * one; a name with no config dir is a hard, clearly-named error. * * CYCLE GUARD: the chain of profile NAMES is tracked (seeded with the selected profile's own name); * because `extends` is single-valued the chain is linear, so a repeated name — `A extends B extends * A`, or a self-extend — is an unambiguous cycle and fails fast with a clear error NAMING the cycle, * never infinite-looping / stack-overflowing. A hard `MAX_EXTENDS_CHAIN_DEPTH` cap backstops * it regardless of how the base path was derived. * * @param rawConfig the just-loaded, schema-validated raw config that MAY declare `extends`. * @param profileLabel the selected profile's own name (for cycle detection + messages); undefined * for a plain (non-profile) project config. */ export declare function resolveConfigExtends(rawConfig: Record, profileLabel: string | undefined, options?: { globalOnly?: boolean; }): Promise>; /** * ORDERING INVARIANT (GS2-11): detection ({@link hasProjectConfig}/{@link hasAnyConfig}) MUST run * before {@link initConfig} in a given process. Both resolve cwd-level candidates via * `getGslothConfigReadPath`, which reads `getProjectDir()`; {@link initConfig} clears `projectDir` * at the start of its run, so detection stays cwd-correct as long as it precedes initConfig (it * does: startSession calls hasAnyConfig before any initConfig, and the ACP/agent path calls * initConfig directly without detection). Calling detection AFTER an initConfig with a changed cwd * in a long-lived process would read a stale projectDir (currently unreachable). If that call * order is ever introduced, decouple discovery's cwd-branch from `getProjectDir()`. */ /** * Returns true when a project-level config file (json/jsonc/js/mjs) exists for the given * overrides. Honours `customConfigPath` and the active identity profile so the check * matches exactly what {@link initConfig} would attempt to load. * * This is the project half of CFG-10's "is any config present?" detection; the global * half is {@link loadGlobalRawConfig} (used by {@link hasAnyConfig}). */ export declare function hasProjectConfig(commandLineConfigOverrides: CommandLineConfigOverrides): boolean; /** * CFG-10 — true when ANY usable configuration is present, either a project config file * (json/jsonc/js/mjs) or a standalone global config (`~/.gsloth/.gsloth.config.*`). When this * returns false the caller should run the first-run dialog instead of erroring. * * Reuses CFG-8's project + global detection so the two paths can never disagree. */ export declare function hasAnyConfig(commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * CFG-37 — the layered value of the top-level `tui` config key, read BEFORE a session picks its * surface. `chat`/`code` choose between the Ink TUI and the readline session in the app's * `startSession` dispatcher, and each surface then loads its own config via {@link initConfig} — so * at the moment of the choice there is no resolved {@link GthConfig} to consult, and this is the * seam that supplies the one key the choice needs. * * Layering matches a run: the discovered PROJECT layer wins over the GLOBAL one, and a layer that * does not set `tui` defers to the next rather than overriding it with `undefined`. A scalar needs * no deep merge, so this reads the two layers and picks — it does not fork the merge engine. * * QUIET and fail-soft for the layers it reads ITSELF: it does not validate them and does not * `exit` on a malformed one, because the caller runs moments before {@link initConfig}, which * validates every layer and reports the very same problem — warning twice about one file is worse * than not warning here. A failed read resolves to `undefined`, i.e. "nobody set it", and the * surface auto-detects exactly as it does for a run with no config. * * ONE EXCEPTION, and it is deliberate: a `tui` may be INHERITED through a GS2-41 profile `extends` * chain, so this walks that chain via the SHARED {@link resolveConfigExtends} rather than forking * it — and that traversal owns its own reporting. It validates each base layer (so a base's own * unknown-key warning can appear here as well as from `initConfig`) and hard-`exit`s on a cycle, a * missing base or a malformed base. What the user sees is unchanged — `initConfig` exits on the * same chain with the same message a moment later — but it now happens EARLIER, at surface * selection rather than at config load. Forking the walk to silence it would mean a second * inheritance engine; ignoring `extends` would mean silently dropping an inherited `tui`. * * The chain is walked for BOTH layers, each on the branch a run walks it on: the project layer * always, the global layer only when NO project layer was discovered (see the note at that call). * The global half is the branch `--global` always takes, where the global layer is the only layer * and an inherited `tui` is therefore the whole answer rather than an underlay. * * Ordering: this is DETECTION, so per the GS2-11 invariant above it must run before any * {@link initConfig} in the process — as it does, alongside {@link hasAnyConfig} in `startSession`. */ export declare function loadConfiguredTui(commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * The configured `consoleLevel`, resolved from the config layers BEFORE the CLI parses its * command line — or `undefined` when nobody set a usable one. * * ## Why the level has to be answerable this early * * `readStdin` blocks a piped run until its input ends, and draws a `reading STDIN` progress line * while it waits. That happens before `program.parseAsync()`, and the level the run finally uses is * applied inside the command's action — after the parse. A writer that consults the level before * anything set it reads the INFO default, so the one progress line a piped run draws was the one * line no `consoleLevel` could quieten. The ordering it sits in is fixed for its own reason (the * notice's line must be closed before the run header starts at column 0, see * `maintenance/ux-guidelines.md`), so the level is what moves. * * ## Why this cannot disagree with the level the run ends up on * * It is the same key, read from the same files, discovered the same way, and narrowed by the same * `resolveConsoleLevel`, so the answer is the one a full load reaches. It overrides nothing: where * a run goes on to build its config, `mergeConfig` re-applies the identical value and has the last * word (the defaults always carry a `consoleLevel`, so it never leaves the setting alone). A run * that never builds one — the config-decoupled read-only surfaces, or an exit before the load — * keeps what is read here, which is still the level its user configured. The two readings can * therefore differ only by one of them being absent, never by disagreeing. * * An unusable value resolves to `undefined` here and is left at the default, exactly as the merge * leaves it, and the warning naming it is written once, by the merge, rather than twice. * * ## Quiet, deliberately * * Every failure resolves to `undefined` and a debug line: a config this cannot read is a config * with no level in it as far as this reader is concerned. It must not be the first thing to report * a broken config, because it runs before the parse — earlier than the CLI's top-level guard, which * is where a malformed config, an unresolvable profile and a broken `extends` chain are reported * today. Answering "no level" leaves that report exactly where it is. * * This is {@link loadConfiguredTui}'s twin and deliberately not merged with it: that one narrows to * a boolean and lets a malformed `extends` reach the top level, because it runs inside a command * action where that is the right place for it. */ export declare function loadConfiguredConsoleLevel(commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * GS2-119 — the configured `history.dbPath`, read without building a model, or `undefined` when no * layer sets a usable one. * * The read-only history commands (`gth history list` / `search` / `show` / `prune` / `rebuild` and * `gth insights`) must open the store the recorder writes, so they resolve `--db`, then this, then * the default. They cannot use {@link initConfig}: it builds the model, and a machine with no API key * in its environment, or with no config at all, must still be able to list what it recorded. * * Layering matches a run for this one key: the project layer (through its `extends` chain) wins * over the global one, and a layer that does not set it defers. Quiet and fail-soft like * {@link loadConfiguredConsoleLevel}: a config it cannot read is a config with no `dbPath`, and the * command falls back to the default store — the same one a run with that config would fail to load * before reaching its recorder. */ export declare function loadConfiguredHistoryDbPath(commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * BATCH-48 — what {@link loadConfiguredEvalToolCoverage} found. * * `found` is whether the run has a base config at all, which is a different answer from "the base * config does not set the key": a run with no base config has no floor and must say so, while a * base config without the key is the ordinary, silent case. */ export interface ConfiguredEvalToolCoverage { /** Whether a base config exists for these overrides — a project/profile file, or a global one. */ found: boolean; /** Which layer the run's base is: a discovered project/profile file, or the global config. */ layer?: 'project' | 'global'; /** The validated `evalToolCoverage` value, or `undefined` when the base config does not set it. */ value?: EvalToolCoverageConfig; } /** * BATCH-51 — the run-level `gth eval` settings {@link loadRunLevelEvalConfig} found in the run's * BASE config. `found` and `layer` mean what they mean on {@link ConfiguredEvalToolCoverage}. */ export interface RunLevelEvalConfig { /** Whether a base config exists for these overrides — a project/profile file, or a global one. */ found: boolean; /** Which layer the run's base is: a discovered project/profile file, or the global config. */ layer?: 'project' | 'global'; /** The validated `evalToolCoverage` value, or `undefined` when the base config does not set it. */ evalToolCoverage?: EvalToolCoverageConfig; /** The validated `reporters` map, or `undefined` when the base config does not set it. */ reporters?: Record; /** BATCH-52 — the validated `evalToolCallArgs` value, or `undefined` when the base config does not set it. */ evalToolCallArgs?: boolean; } /** * BATCH-48 — the `evalToolCoverage` value of the run's BASE config, read once, before `gth eval` * runs any suite. A view of {@link loadRunLevelEvalConfig}, which reads every run-level eval setting * through the one path described there. */ export declare function loadConfiguredEvalToolCoverage(commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * BATCH-48 / BATCH-51 / BATCH-52 — the run-level `gth eval` settings (`evalToolCoverage`, * `reporters`, `evalToolCallArgs`) of the run's BASE config, read once, before `gth eval` runs any suite. * * ## Why a reader, and not a config the eval command already builds * * `gth eval` builds its configs per suite, and a suite that declares `identities:` builds one per * identity and never builds a base config at all — it must not, because a matrix-only project may * have none. So there is no run-level config object to read the run floor from, and reading it off * whichever config a suite happened to build would let the floor move: a sweep cell's merged * `config`, or an identity's profile, would supply the threshold. This reads the base the run was * STARTED with (`-i`, `-c`, `-g`, or plain discovery) and nothing else, so the floor is one value * per run by construction, and never an identity's. * * ## Why this cannot disagree with the value a run reaches * * It takes the path {@link initConfig} takes for the same overrides: the same refusals first (the * `-g` + `-c` pair, a missing `-c` file, an explicitly named profile with no config of its own — * {@link findProjectConfigPath} would otherwise fall back to the plain config and the output would * name a profile the value did not come from), then the same discovery, the same `extends` * composition, and the same global underlay with the same `deepMerge`. Neither key has a * default or a CLI override, so the merge with `DEFAULT_CONFIG` cannot change them. * * ## Quiet about everything except its own keys * * It does not validate the whole config: every suite's own `initConfig` does that and reports it, * and warning twice about one file is worse than not warning here. The keys it reads it validates * with `evalToolCoverageSchema`, `evalReportersSchema` and `evalToolCallArgsSchema` — the same rules the full parse applies — * and a malformed value THROWS: a floor that failed to parse must not quietly become no floor, * because a floor quietly skipped reports green forever over a run it never measured. * * ## Why reporters are read here too * * A matrix suite builds no base config, so reading `reporters` off a config a suite built meant * reading the first identity's profile: a reporter registered in the `-i` profile was unknown to * such a suite, and one registered in an identity's profile was used or not depending on the * identity order. Reporters, like the floor, describe the run, not an identity. */ export declare function loadRunLevelEvalConfig(commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * CFG-56 / CFG-57 — the one statement of why `global` and `customConfigPath` cannot travel * together: both choose WHERE configuration comes from, so honouring either makes the other a * silent no-op. Exported because the CLI refuses the pair at flag-parse time (before anything is * read) and the loader refuses it wherever config is BUILT; two spellings of one rule is how the * two guards drift apart. */ export declare const CONFLICTING_CONFIG_SOURCES_MESSAGE: string; /** * Initialize configuration by loading from available config files * @returns The loaded GthConfig */ export declare function initConfig(commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * CFG-75 — is this `llm` config value ALREADY usable as a model, with nothing left for the provider * layer to build? * * Decided by what the value can DO — answer a call — and never by what it looks like. A model a * user's own `configure()` built keeps its methods on its prototype and has no own `type` property, * so a `type`-key test calls exactly the documented shape unusable; that is the assumption this * predicate exists to replace. It is the positive half of `needsProviderRouting`, kept as one * definition rather than two so the two questions cannot drift apart. * * The two references to `needsProviderRouting` here are code spans and not `@link`s on purpose: * this predicate is exported and that one is not, so a link would render as a broken reference on * every page that documents this function. Exporting a second symbol to satisfy a cross-reference * would widen the public surface to fix a docs warning, which is the wrong way round. * * It is NOT the negation of `needsProviderRouting`: that is false for an ABSENT `llm` too, so * a branch that accepted `!needsProviderRouting(llm)` would accept a config with no model at all. * Ask this one when the question is "may this be used?", and ask it FIRST wherever a `type` test * could also match — an object that answers calls and happens to carry a `type` field is a working * model the user built, and sending it to the provider layer would quietly swap it for another. * * CFG-61 — exported, because the loader is not the only place that has to know whether a model * actually resolved. The AG-UI server's `/health` and `/info` answer that same question about the * config they were handed, and a second copy of the test is how the two would come to disagree: * the loader would route a value the server called a model, or the server would report `ok` about * a value the loader would have sent away to be built. One definition, asked from both sides. */ export declare function isUsableModel(llm: unknown): boolean; /** * Process JSON LLM config by creating the appropriate LLM instance * @param jsonConfig - The parsed JSON config * @param commandLineConfigOverrides - command line config overrides * @returns Promise */ export declare function tryJsonConfig(jsonConfig: RawGthConfig, commandLineConfigOverrides: CommandLineConfigOverrides): Promise; /** * Resolve a fully-merged {@link GthConfig} from a partial config + CLI overrides WITHOUT * any global side effects. It deep-merges defaults, applies CLI overrides, resolves the numeric * `consoleLevel` (warning + defaulting to INFO on an invalid value), and computes * `canInterruptInferenceWithEsc` and `useColour`. The process-global setters (`setUseColour` / * `setConsoleLevel`) are applied separately by `mergeConfig`, so this function can be * reasoned about and reused without touching global state. * * It WRITES nothing globally, but it does READ the environment: `canInterruptInferenceWithEsc` * consults stdin's TTY status, and (CFG-30) `useColour` consults `FORCE_COLOR`, `NO_COLOR` and * stdout's TTY status. So it is deterministic for a given environment rather than a pure function * of its arguments — a test that pins a resolved config should declare the terminal and the colour * environment it expects in its setup. The ladder itself is a pure helper * ({@link @gaunt-sloth/core!config/colour.resolveUseColour | resolveUseColour}) so it can be tested rung by rung without process globals. */ export declare function resolveConfig(partialConfig: Omit, 'consoleLevel'> & { consoleLevel?: ConsoleLevelInput; }, commandLineConfigOverrides: CommandLineConfigOverrides): GthConfig; /** * One config LAYER's validation outcome inside a {@link ConfigValidationReport}: the pure * read-side result ({@link @gaunt-sloth/core!config/schema.validateRawGthConfig | validateRawGthConfig}) plus the source label so a consumer can name * WHICH file carried a warning/error (the project path, or `" (global)"`). */ export interface ConfigLayerValidationReport extends RawConfigValidationResult { /** The resolved config path (project layer) or `" (global)"` (global layer). */ sourceLabel: string; } /** * The outcome of `gth config validate`: whether any config was found, and the per-layer verdict * for EVERY layer a run would validate. Pure/read-side — it neither builds an LLM nor mutates * process globals, so it can report a verdict without the run-path's side effects. The command * layer turns this into console output + an exit code. * * GS2-29 — `validateConfig` mirrors the layer set `initConfig` validates: the discovered PROJECT * layer (if any) AND the GLOBAL layer (if any). A run validates both and exits(1) if EITHER * carries a problem, so a removed shape in the global config (with a clean project config) shows * up here exactly as the run would reject it. Both layers are kept in {@link layers} (in run * order) so the offending file is always identifiable. */ export interface ConfigValidationReport { /** False when neither a project nor a global config exists within the discovery boundary. */ found: boolean; /** True only when a config was found AND every present layer validates OK. */ ok: boolean; /** * Each config layer a run would load + validate, in run order: the discovered PROJECT layer * (if any) first, then the GLOBAL layer (if any). Empty when `found` is false. */ layers: ConfigLayerValidationReport[]; } /** * Locate and validate the effective raw config against the schema WITHOUT building the LLM * or merging defaults (the read-side of GS2-1). Honours `--config`, up-tree discovery, and the * identity profile via {@link findProjectConfigPath}. * * GS2-29 — validates the SAME layer set a real run does: the discovered PROJECT layer (if any) * AND the GLOBAL layer (if any), mirroring `initConfig`'s `validateRawConfigLayer(project)` + * `applyGlobalConfigBase` → `loadGlobalRawConfig(global)`. Each present layer is validated * independently ({@link @gaunt-sloth/core!config/schema.validateRawGthConfig | validateRawGthConfig}) * and its outcome recorded in {@link * ConfigValidationReport.layers}, so a removed shape in EITHER file is reported (labelled with * its source) rather than under-reported. * * A PROJECT-layer JSONC/module parse failure is thrown to the caller (surfaced as a clear * "invalid config" error + non-zero exit). A GLOBAL-layer parse failure is treated as an absent * global (no layer added) but is surfaced with a `displayWarning` — exactly as a run does (it * warns the user while ignoring the broken global's value) — see * `loadGlobalRawConfigUnvalidated`. * * GS2-73 — for the PROJECT layer it also walks the GS2-41 profile `extends` chain (via the SAME * `composeExtends`/`resolveExtendsChain` the run path uses), so a cycle or a missing * base — which fail a real run — is reported here as a not-ok layer instead of passing OK and only * failing at run time. The GLOBAL layer is walked on exactly the branch a run walks it: when NO * project layer was discovered, so the global config is what `initConfig` loads and resolves * `extends` on. With a project layer present the run underlays the raw global config * (`applyGlobalConfigBase`) and never resolves its `extends`, so neither does this. */ export declare function validateConfig(commandLineConfigOverrides: CommandLineConfigOverrides): Promise;