/** * Settings singleton with sync get/set and background persistence. * * Usage: * import { settings } from "./settings"; * * const enabled = settings.get("compaction.enabled"); // sync read * settings.set("theme.dark", "red-claw"); // sync write, saves in background * * For tests, `Settings.isolated()` seeds explicit user/global settings: * const isolated = Settings.isolated({ "compaction.enabled": false }); */ import type { ModelRole } from "../config/model-registry"; import { type NotificationSettingsReader, type NotificationSettingsSnapshot } from "../sdk/bus/config"; import { AgentStorage } from "../session/agent-storage"; import { type EditMode, type EditVariantMatch } from "../utils/edit-mode"; import { type CasReceipt } from "./atomic-yaml-patch"; import { type AutoroutingEffective } from "./autorouting-contract"; import { type ModelSelectorValue } from "./model-selector-value"; import { type BashInterceptorRule, type GroupPrefix, type GroupTypeMap, type SettingPath, type SettingsSchemaReport, type SettingValue } from "./settings-schema"; export type * from "./settings-schema"; export * from "./settings-schema"; /** Raw settings object as stored in YAML */ export interface RawSettings { [key: string]: unknown; } /** * Test-only seams for the config-root workflow migration. Production code * never sets these; tests use them to interleave external filesystem changes * at exact points of the migration state machine (mirroring * `FileLockTestHooks` in file-lock.ts). */ export declare const SettingsMigrationTestHooks: { /** Fires after the no-replace backup copy is created and its identity * (inode + sha256) has been captured, immediately before the source is * re-hashed for the move verification. */ afterBackupIdentityCaptured?: (backupPath: string) => void | Promise; /** Fires after a quarantined backup is verified as this run's file, * immediately before the quarantined entry is unlinked. */ beforeQuarantineRemoval?: (backupPath: string) => void | Promise; /** Fires immediately before the project migration's POST-publication marker * re-read, after the migrated values already committed: test seams use it * to make the marker unreadable so the rollback path is exercised. */ beforeProjectMarkerMerge?: () => void | Promise; }; export type SettingsAtomicPatch = { path: SettingPath; op: "set"; value: unknown; } | { path: SettingPath; op: "unset"; }; export type SettingsAtomicReceipt = CasReceipt; export interface SettingsOptions { /** Current working directory for project settings discovery */ cwd?: string; /** Agent directory for config.yml storage */ agentDir?: string; /** Don't persist to disk (for tests) */ inMemory?: boolean; /** Initial overrides */ overrides?: Partial>; /** * Read the canonical config.yml from disk but never persist: no DB open, * no legacy/config-root/project migrations, no file writes, renames, locks, * or mtime changes. Used by read-only inspection surfaces (`gjc customize * doctor`). When true, inMemory must be false so config.yml is still read. */ readonly?: boolean; } /** Additional layer setup for {@link Settings.isolated}. */ export interface IsolatedSettingsOptions { /** Initial runtime overrides. Notification paths are rejected. */ overrides?: Partial>; } /** Raised when an ephemeral override attempts to change global-only notification settings. */ export declare class NotificationSettingsOverrideError extends Error { readonly path: SettingPath; constructor(path: SettingPath); } export declare class Settings implements NotificationSettingsReader { #private; private constructor(); /** * Initialize the global singleton. * Call once at startup before accessing `settings`. */ static init(options?: SettingsOptions): Promise; /** * Load settings for an explicit workspace without changing the global singleton. * Managed-session policy resolution must be bound to the workspace being opened. */ static loadForScope(options: { cwd: string; agentDir?: string; }): Promise; /** * Load settings for read-only inspection without any persistence side-effects: * no DB open, no legacy/config-root/project migrations, no file writes, * renames, locks, or mtime changes. Reads the canonical config.yml and * discovers/merges project settings exactly like the durable path, but the * result is a transient snapshot. Does not affect the global singleton. * * Used by `gjc customize doctor` to honor the read-only product contract. */ static loadReadonly(options: { cwd?: string; agentDir?: string; }): Promise; /** * Create an isolated instance for testing with explicit user/global settings. * Does not affect the global singleton. */ static isolated(globalSettings?: Partial>, options?: IsolatedSettingsOptions): Settings; /** * Get the global singleton. * Throws if not initialized. */ static get instance(): Settings; /** * Get a setting value (sync). * Returns the merged value from global + project + overrides, or the default. */ get

(path: P): SettingValue

; /** * Get a setting value from the user/global config only. * * Use for machine-local command hooks and other settings that must not be * activated by project-scoped config files. */ getGlobal

(path: P): SettingValue

| undefined; /** * Read the remote-notification settings from the user/global layer only. * Schema defaults are applied per path; project settings and runtime overrides * are deliberately excluded from this trust boundary. */ getNotificationSettingsSnapshot(): NotificationSettingsSnapshot; /** Check whether a setting is present in loaded settings/overrides rather than coming from schema defaults. */ has(path: SettingPath): boolean; /** Diagnostics from schema reconciliation during the most recent load. */ getSchemaReport(): SettingsSchemaReport; /** Effective merged autorouting state shared by settings diagnostics and routing policy. */ getEffectiveAutorouting(): AutoroutingEffective; onChanged(listener: (path: SettingPath) => void): () => void; /** Whether durable settings mutations are permitted for the loaded configuration. */ canWriteDurableConfig(): boolean; /** * Set a setting value (sync). * Updates global settings and reserves its background persistence slot before * returning, so later durable batches cannot overtake this mutation. */ set

(path: P, value: SettingValue

| undefined): void; /** * Delete a global setting (sync), rather than serializing an ambiguous YAML * `undefined` value. Defaults/project settings become visible immediately. */ unset

(path: P): void; /** * Persist a tagged batch as one atomic YAML replacement. Unlike ordinary * {@link set}, canonical state and hooks change only after the rename succeeds. */ commitAtomicBatch(patches: readonly SettingsAtomicPatch[]): Promise; /** Build a durable batch from the current on-disk YAML under the shared queue and file lock. */ commitAtomicBatchWithCurrent(buildPatches: (current: Readonly) => Promise | readonly SettingsAtomicPatch[]): Promise; /** * Apply runtime overrides (not persisted). */ override

(path: P, value: SettingValue

): void; /** Read the exact runtime override without merged defaults. */ getOverride

(path: P): SettingValue

| undefined; /** * Clear a runtime override. */ clearOverride(path: SettingPath): void; /** Flush a reserved debounced save without allowing it to be overtaken. */ flush(): Promise; /** Like {@link flush}, but reports a durable save failure to the caller. */ flushOrThrow(): Promise; cloneForCwd(cwd: string): Promise; getStorage(): AgentStorage | null; /** Flush and close storage owned by an isolated settings scope. */ close(): Promise; getCwd(): string; getAgentDir(): string; getPlansDirectory(): string; /** * Get shell configuration based on settings. */ getShellConfig(): import("@gajae-code/utils/shell-config").ShellConfig; /** * Get all settings in a group with full type safety. */ getGroup(prefix: G): GroupTypeMap[G]; /** * Get the edit variant for a specific model. * Returns "patch", "replace", "hashline", "vim", "apply_patch", or null (use global default). * Skips invalid values; prefer `matchEditVariantForModel` when invalid * matches must fail closed instead of falling through. */ getEditVariantForModel(model: string | undefined): EditMode | null; /** * First matching `edit.modelVariants` rule for a model, with its raw * (unvalidated) value. The edit-mode resolver uses this discriminated * result so a matched-but-invalid value fails closed with a diagnostic * rather than silently resolving to another mode. */ matchEditVariantForModel(model: string | undefined): EditVariantMatch | null; /** * Get bash interceptor rules (typed accessor for complex array config). */ getBashInterceptorRules(): BashInterceptorRule[]; /** * Set a model role (helper for modelRoles record). */ setModelRole(role: ModelRole | string, modelId: ModelSelectorValue): void; setGlobalModelRole(role: ModelRole | string, modelId: ModelSelectorValue | undefined): void; setGlobalModelRoleAndFlush(role: ModelRole | string, modelId: ModelSelectorValue | undefined): Promise; restoreGlobalDefaultModelRoleIfCurrent(commit: CasReceipt): Promise; /** * Set an agent model override while keeping any live runtime override aligned. * * Runtime model profiles override `task.agentModelOverrides` for the current * session. A user-selected role assignment must win immediately in that same * session, but only the explicit agent change should be persisted. */ setAgentModelOverride(agentName: string, modelId: ModelSelectorValue): void; /** * Get a model role (helper for modelRoles record). */ getModelRole(role: ModelRole | string): ModelSelectorValue | undefined; /** * Get all model roles (helper for modelRoles record). */ getModelRoles(): Readonly>; overrideModelRoles(roles: Readonly>): void; /** * Set disabled providers (for compatibility with discovery system). */ setDisabledProviders(ids: string[]): void; } /** * Subscribe to append-only mode setting changes. * Returns an unsubscribe function. Multiple sessions (main + subagents) * can register independently without overwriting each other. */ export declare function onAppendOnlyModeChanged(cb: (value: string) => void): () => void; export declare function isSettingsInitialized(): boolean; /** * Reset the global singleton for testing. * @internal */ export declare function resetSettingsForTest(): void; /** * Ensure the one-time legacy workflow settings migrations have run for `cwd` * before a workflow runtime resolves settings. The direct CLI commands * (`gjc ralplan`, `gjc deep-interview`, `gjc ultragoal`) never initialize * Settings, so without this the project `.gjc/settings.json` and config-root * `settings.json` workflow keys would silently fall back to defaults after the * resolver stopped reading settings.json. No-op when the global Settings * singleton is already initialized (session path). Best-effort: a settings * load failure must not block the native command, whose resolver falls back to * defaults exactly as before. */ export declare function ensureWorkflowSettingsMigrated(cwd: string): Promise; /** * The global settings singleton. * Must call `Settings.init()` before using. */ export declare const settings: Settings;