/** * `SkillsManager`. * * Walks every configured source, parses + validates each `SKILL.md`, then * builds an in-memory catalog keyed by fully-qualified * `/@` with shadowing. * * Construction is synchronous and cheap (validate config, store). The * `discover()` method does the disk IO; it's idempotent — subsequent * calls return the same already-built catalog. Public lookups via * `list()` / `get()` await discovery completion implicitly. * * Invariants: * - Catalog is built once per discovery; hot reload reruns discovery * to produce a fresh snapshot. * - Trust tier is derived from source type only; consumer-supplied * `trustedSources` is used to elevate third-party sources. * - Per-skill `min_sdk_version` exclusion lands here. */ import { type WalkerEnv } from './sources/index.js'; import type { SourceWalkResult } from './sources/index.js'; import type { ParsedSkill, SkillEvent, SkillSource, SkillsConfig } from './types.js'; /** * Lookup options for `SkillsManager.get`. Both fields optional — without * a version pin the manager returns the latest version of the matched * skill. */ export interface SkillGetOptions { readonly includeAllVersions?: boolean; } /** * Snapshot of catalog state for tests / introspection. Stable across * discovery completion; the manager never mutates the array after * `discover()` resolves. */ export interface CatalogSnapshot { readonly skills: readonly ParsedSkill[]; /** Source-by-source breakdown — useful for the slim-index source attribution. */ readonly bySource: readonly SourceWalkResult[]; /** Path-keyed map of skills that were dropped during catalog construction. */ readonly excluded: readonly { readonly source: SkillSource; readonly path: string; readonly reason: string; }[]; /** * Monotonically-increasing counter bumped on every successful build. * Starts at `1` after the initial `discover()`; each watcher-triggered * rebuild increments by one. Body caches can key by `(fqn, epoch)` to * evict stale entries without race-y read locks. */ readonly epoch: number; } /** * Resolved `requires:` cap configuration with defaults filled in. Each * cap is read by `SkillsManager` consumers (the MCP server's `get_skill` * resolver, the `TaskClient` caller-pin activation path) to enforce the * depth / breadth / token caps uniformly. */ export interface ResolvedRequiresCaps { readonly maxDepth: number; readonly maxTransitive: number; readonly maxTransitiveTokens: number; } export declare class SkillsManager { private readonly config; private readonly env; private readonly emit; private readonly trustedSourceIdentities; private readonly requiresCaps; /** * Per-client active-skill set. Populated by caller-pin activation * (`TaskClient`) and by LLM-driven `get_skill` calls (the MCP server, * via `markLoaded`). Cleared by `resetActive()`. * * SDK 2.0 has no per-task AsyncLocalStorage env-scope (the design that * shipped on SDK 1.x's `EnvironmentServices`); active-state lives on * the manager, scoped to the client lifetime. Concurrent dispatches * against the same client observe a merged set — documented in the * architectural-translation note. `disconnect()` calls * `skillsMcpServer.dispose()` which calls `resetActive()` (transitively * through the existing `activeFqns` clear), so a fresh `connect()` * starts cold. */ private readonly activeFqns; private discoverPromise; private snapshot; /** * Active `fs.watch` handles keyed by the absolute directory path * being watched. Populated by `startWatching()`, cleared by * `stopWatching()` / `dispose()`. */ private watchers; /** Pending watcher-triggered rebuild timer, debounced. */ private rebuildTimer; /** Promise that resolves with the next rebuilt snapshot, if a rebuild is in-flight. */ private rebuildPromise; /** Optional listener fired after every successful rebuild — used by tests / consumers. */ private rebuildListeners; private epoch; /** * In-flight `startWatching()` promise. Concurrent calls await the * same promise so the install pass is serialised — defends against * the watcher-duplication race when auto-start + manual start fire * close together. */ private startWatchingPromise; /** * Marks that a watcher event arrived while a rebuild was already * in flight. The current rebuild's `.then` re-schedules so the late * change isn't dropped. */ private pendingRebuild; constructor(config: SkillsConfig, env?: WalkerEnv); /** * Forward a renderer-produced event batch to the configured sink. * Used by `TaskClient`'s skills injection helpers — the renderer is * pure and returns events as data; the manager owns the sink wiring * so consumers see one consistent emission surface. */ forwardEvents(events: readonly SkillEvent[]): void; /** * Walk every configured source, parse + validate, build the FQN-keyed * catalog. Idempotent — concurrent calls share one in-flight promise. */ discover(): Promise; /** Resolved `true` once `discover()` has completed at least once. */ get isReady(): boolean; /** Synchronous snapshot accessor. Returns `null` until `discover()` resolves. */ get currentSnapshot(): CatalogSnapshot | null; /** * Constructor-supplied default `activate` set. `TaskClient` reads this * when the per-call `opts.skills` is `undefined`. Returned read-only; * `[]` and `undefined` are distinct (`[]` = explicit empty, `undefined` * = "no default set"). */ get defaultActivate(): readonly string[] | undefined; /** * Constructor-supplied `indexCharBudget`. `TaskClient` forwards this * to `renderSlimIndex()` so consumers can tighten the model-visible * block below the 8000-char default. `undefined` means "renderer * picks the default". */ get indexCharBudget(): number | undefined; /** * Constructor-supplied `!command` preprocessing config. * `undefined` means "default disabled" — the MCP server's `get_skill` * handler short-circuits when this is `undefined` or `mode: 'disabled'`. */ get preprocessingConfig(): SkillsConfig['preprocessing'] | undefined; /** * Constructor-supplied embedder, if any. `undefined` means * "no pre-filter; rely on render-side truncation". */ get embedder(): SkillsConfig['embedder'] | undefined; /** * Constructor-supplied embedding-pre-filter tuning. `undefined` means * "use documented defaults" (`topK: 8`, `minScore: 0.35`). */ get activationConfig(): SkillsConfig['activation'] | undefined; /** * List every catalog entry. Awaits discovery on first call. Returns * latest-version-only by default; pass `{ includeAllVersions: true }` * to surface every version recorded during discovery. */ list(opts?: SkillGetOptions): Promise; /** * Resolve a skill specifier (`name`, `vendor/name`, or `vendor/name@version`) * to a `ParsedSkill` or `null` if no match. * * - Bare name: returns the latest-version match of any vendor; if multiple * vendors ship a skill of that name, the highest-precedence source wins. * - `vendor/name`: latest version of that vendor's skill. * - `vendor/name@version`: exact pin. */ get(specifier: string): Promise; /** * Verify every name in `specifiers` resolves. Throws on the first * unresolved entry — the SDK uses this from the wire boundary to fail * loud on caller-pinned activation per `skills-activation.md` §2. */ resolveOrThrow(specifiers: readonly string[]): Promise; /** * Whether `skill` is trusted enough to honor its declarative affordances * (`allowed-tools`, `disable-model-invocation: false`, `hooks`). True for * the `'trusted'` / `'operator'` tiers; for `'third-party'` true iff the * skill's source identity is in `trustedSources`. */ isTrusted(skill: ParsedSkill): boolean; /** * Resolved `requires:` caps with defaults filled in. Read by the * activation pipeline (`TaskClient` caller-pin + `QodoSkillsMcpServer.get_skill`). */ get requiresCapsResolved(): ResolvedRequiresCaps; /** * Snapshot of currently active skill FQNs. The active set is union of * (a) caller-pinned skills the SDK injected for the latest dispatch and * (b) skills the model loaded via `qodo-skills.get_skill`. Returned * sorted for stable test output. */ getActiveSkills(): readonly string[]; /** * Mark a skill FQN as active. Idempotent — re-marking emits no extra * event. Returns `true` iff the FQN was newly added (i.e., this is a * fresh activation, not a cache hit). * * The caller is responsible for emitting `sdk.skill.activated` — the * event carries `source: 'caller' | 'llm'` which only the caller knows. * `markActive` just owns the set membership. */ markActive(fqn: string): boolean; /** * Remove a skill FQN from the active set. Returns `true` iff it was * present. Used by `QodoSkillsMcpServer.dispose` and (rarely) by * consumers programmatically rolling back activation. */ deactivate(fqn: string): boolean; /** * Clear the active set entirely. Called by `QodoSkillsMcpServer.dispose` * on `disconnect()` so the next `connect()` starts cold. */ resetActive(): void; /** * Begin watching every configured local source directory for SKILL.md * changes. Idempotent — calling twice has no extra effect. * * Requires `discover()` to have completed; the watcher targets only * directories already known to be on disk. New top-level source dirs * appearing post-startup are NOT auto-watched (operators would * typically `stopWatching()` + reconfigure + start a new client). * * `package` and `bundled` sources are intentionally NOT watched — * those ship with the application code and shouldn't churn at * runtime. Watching them would expose tampering surface without * meaningful upside. * * Default behavior is determined by `SkillsConfig.watch`: * `undefined`/`false` ⇒ no-op; `true` ⇒ start watching after the first * `discover()` resolves. */ startWatching(): Promise; private doStartWatching; /** * Tear down every active watcher and cancel any pending debounce * timer. Safe to call multiple times. Called automatically by * `dispose()`. */ stopWatching(): void; /** * Tear down the manager — equivalent to `stopWatching()`. Provided as * a parallel to `QodoSkillsMcpServer.dispose()` so consumer code can * call both in a uniform shutdown sequence. */ dispose(): void; /** Current catalog epoch counter. `0` before the first `discover()`. */ get catalogEpoch(): number; /** * Subscribe to rebuild events. The listener fires after every * successful watcher-triggered rebuild (NOT on the initial discover). * Returns an unsubscribe function. Used primarily by tests. */ onRebuild(listener: (snapshot: CatalogSnapshot) => void): () => void; /** * Trigger an out-of-band rebuild. Test seam — production code goes * through the debounced watcher path. The rebuild is in-flight * already if another rebuild is queued; returns the same in-flight * promise to avoid double-rebuild races. */ rebuildNow(): Promise; private installWatcher; private scheduleRebuild; private doRebuild; private diffAndEmit; /** * Walk every configured source + parse + validate + assemble the * catalog. Pure with respect to `this.snapshot` — the caller decides * how to publish the returned snapshot (initial discover stamps the * epoch; rebuild diffs against the prior snapshot first). */ private buildCatalog; /** * Locate + apply the lockfile per `SkillsConfig.lockfile`. When * `'off'`, returns immediately. When `'discover'`, searches the * configured CWD; when an explicit `{ path }`, loads it directly. * * Validation outcomes: * * - All entries match → return silently. * - At least one drifts → emit `sdk.skill.lock_drift` per offender + throw. * - At least one unreachable → emit `sdk.skill.lock_unreachable` + throw. * * Throws `LockfileValidationError` so the wire boundary fails loud * rather than serving a divergent catalog. */ private applyLockfile; private resolveSourcesList; private parseAndValidate; /** * Inspect the skill body for `[label](path)` and `get_skill_file("path")` * references; emit: * * - `sdk.skill.error severity: 'warning'` when the referenced file * doesn't exist under the skill directory. * - `sdk.skill.error severity: 'warning'` when a reference has depth * greater than 1 (e.g. `scripts/helpers/foo.py`). Authors who * intentionally nest files can ignore this warning. * * The check is best-effort: any `fs.stat` failure other than ENOENT is * silently ignored so a transient filesystem error doesn't poison * discovery. */ private auditBodyReferences; private emitEvent; } /** * Thrown by `SkillsManager.resolveOrThrow` when a caller-pinned specifier * doesn't match anything in the catalog. The wire boundary (TaskClient * startWithAgent / startWithGraph) re-throws these so consumers see a * loud failure per `skills-activation.md` §2. */ export declare class SkillNotFoundError extends Error { readonly specifier: string; constructor(specifier: string); } /** * Thrown when a bare-name caller pin matches multiple skills under * different vendors (`acme/code-review` and `local/code-review` both * shipping `name: code-review`). The first-match-wins behavior silently * picked one, which could change at runtime depending on source order. * Force the caller to disambiguate with a `vendor/name` pin. */ export declare class SkillAmbiguousPinError extends Error { readonly specifier: string; readonly candidates: readonly string[]; constructor(specifier: string, candidates: readonly string[]); } /** * Thrown when caller-pinned skills cannot fit inside the slim-index * hard cap (`2 × charBudget`, floored at `charBudget + 4096`). Pinning * is a "must include" contract — silently dropping pins would make the * model behave as if the caller never pinned them; throwing forces the * caller to fix the pin list or raise the budget. */ export declare class SkillsBudgetExceededError extends Error { readonly omittedSkills: readonly string[]; readonly hardCap: number; constructor(omittedSkills: readonly string[], hardCap: number); } /** * Thrown when caller-pinned activation can't proceed because of a * `requires:` chain failure — trust violation, depth/breadth/token cap * breach, cycle, or unresolved dependency. The `kind` discriminator * carries the specific reason. Caller-pin * activation is an authoritative contract; failure must be loud rather * than silently dropping the affected skill. */ export declare class QodoSkillError extends Error { readonly skill: string; readonly reason: 'requires_trust_violation' | 'requires_cap_exceeded' | 'requires_cycle' | 'requires_unresolved' | 'not_found'; constructor(skill: string, reason: QodoSkillError['reason'], message: string); } //# sourceMappingURL=manager.d.ts.map