/**
 * Active-pack discovery — scan a scope root for `active.json` + `packs/<name>/`.
 *
 * G.1 wires the real loader path. Given a scope root (either user-scope from
 * `resolveUserScopeRoot()` or project-scope from `resolveProjectScopeRoot(cwd)`),
 * this module:
 *
 *   1. Reads `<scopeRoot>/active.json` — schema `{ packs: string[] }`.
 *   2. For each entry, loads `<scopeRoot>/packs/<name>/` through the
 *      existing `loadPack` (Phase-2 pack format: `manifest.yaml` + `skills/`).
 *
 * Behavior contract:
 *   - `scopeRoot === null`           → returns `[]` (project absent case).
 *   - `active.json` ENOENT           → returns `[]` (scope present, no opt-in).
 *   - `active.json` malformed JSON   → throws path-bearing error.
 *   - `active.json` missing `packs:` → throws with clear "missing field" message.
 *   - `active.json` references a pack folder that doesn't exist or fails to
 *     load → `loadPack` throws; we propagate verbatim.
 *
 * Fail-loud is intentional per `project_opensquid_runtime_failure_handling`
 * memory: a user-authored config bug must surface, not silent-fail to the
 * "allow everything" path. The two test seams in `bootstrap.ts`
 * (`OPENSQUID_TEST_PACK`, `OPENSQUID_TEST_PACK_DIR`) keep their own
 * fail-open contracts because their fixtures are opensquid-authored, not
 * user-authored.
 *
 * Harness-agnostic: this module knows nothing about Claude Code, hook
 * formats, or any consumer-side concept. The settings-writer that lives in
 * `src/setup/wizard/` is the ONLY G.1 module that names the consumer.
 *
 * Imports from: node:fs/promises, node:path, packs/loader.
 * Imported by: src/runtime/bootstrap.ts (the real loader path).
 */
import { type DetectionContext } from '../runtime/detection.js';
import type { Pack } from '../runtime/types.js';
import { type MergeResult } from '../runtime/versioning.js';
/** LP.5 — bootstrap calls this on SessionStart to clear per-session cache. */
export declare function clearMergeCache(): void;
/** LP.5 — exposed for tests to peek cached count. */
export declare function _mergeCacheSize(): number;
/**
 * LP.5 — lazy 3-way merge trigger. For an installed pack at `packStateDir`,
 * check whether the on-disk vanilla is newer than the recorded base AND has
 * lessons to preserve AND hasn't been merged yet. If so, trigger LP.2's
 * runThreeWayMerge against the vanilla source (passed in as `vanillaDir`).
 *
 * Cached per-session — same (packId, base, vanilla, revisionId) tuple
 * resolves once per session lifetime.
 *
 * Returns null in any skip-merge case (not installed, no lessons, already
 * merged, not an upgrade). Returns the MergeResult when the merge actually
 * runs.
 *
 * Per L10 LAZY: NO background poller; this fires on next discovery only.
 * Per L11 IMMUTABLE base_version: only `last_merged_vanilla` mutates;
 * base_version stays the install-time value.
 */
export declare function checkAndMergeUpgrades(packStateDir: string, vanillaManifest: {
    name: string;
    version: string;
}, vanillaDir: string): Promise<MergeResult | null>;
/**
 * LP.3 — path-traversal-safe validator for pack ids that come from
 * potentially-untrusted sources (manifest.yaml name field, CLI input).
 * Rejects ids containing `/`, `\`, `..`, or starting with `.`. Use before
 * any path construction with the id.
 */
export declare function validatePackId(packId: string): void;
/**
 * LP.3 — resolve the on-disk state directory for a pack by id. User scope:
 * `~/.opensquid/packs/<id>/`. Project scope: `<projectCwd>/.opensquid/
 * packs/<id>/` (projectCwd required). Path resolver only; does NOT verify
 * directory exists. Caller mkdir-recursive before writing.
 *
 * Honors OPENSQUID_HOME env override (tests). validatePackId is called
 * before path construction.
 */
export declare function resolvePackStateDir(packId: string, scope?: 'user' | 'project', projectCwd?: string): string;
export interface ActiveJson {
    /** Pack names (folder names under `packs/`) declared active in this scope. */
    packs: string[];
}
/**
 * Scan a scope root for active packs. See module header for the full
 * contract; in short: returns `[]` on the two "scope-absent" branches
 * (`null` arg, ENOENT active.json) and throws on anything malformed.
 *
 * IDF.3 — optional `ctx` second arg gates loading on the per-pack
 * `detected_by[]` rules via IDF.2's pure evaluator. When `ctx` is
 * `null`/`undefined`, legacy behavior applies (every opted-in pack
 * loads). When `ctx` is provided, each opted-in pack only loads if its
 * `detectedBy` matches the staged context (empty `detectedBy[]` always
 * matches — back-compat). Opt-in invariant preserved end-to-end: a pack
 * NOT listed in `active.json` is never loaded by this fn, regardless of
 * what its `detectedBy` would say.
 */
export declare function discoverActivePacks(scopeRoot: string | null, ctx?: DetectionContext | null, builtinRoot?: string | null): Promise<Pack[]>;
//# sourceMappingURL=discovery.d.ts.map