/** * DshSkillProvider — rolebox's pull-style skill source for dsh's global * `ctx.skills` registry. * * dsh resolves model- and user-facing skills EXCLUSIVELY through the * `dsh-skill` registry (`ctx.skills`), a layered merge of * provider catalogs. rolebox never registered into it, so under dsh the role * prompt's `` block (`buildSkillBlock`, * `src/prompt/builder.ts:127`) advertised names the `skill` tool could not * resolve — the defect this module closes. * * The registry's plugin seam is `ctx.skills.registerProvider(create)` — a * FACTORY: `create(control)` is invoked once and must return a * {@link DshSkillProvider} (`control = {signal, invalidate}`). Verified * against the installed rc.6 dist: * - `dsh-skill/lib/types/index.d.ts:249` — * `registerProvider(create: (control: SkillProviderControl) => SkillProvider)` * - `dsh-skill/lib/types/index.d.ts:190-195` — `SkillProviderControl` * (`{signal: AbortSignal, invalidate: () => void}`) * - `dsh-skill/lib/types/index.d.ts:168-188` — `SkillProvider` * (`name` / `list(options)` / `get(candidate, options)`) * - `dsh-skill/lib/types/index.d.ts:88-93` — `SkillLookupOptions` is * `{cwd?, signal?}` only: a provider receives NO scope/agent/session. * * ── Why a provider, not `ctx.skills.register()` ──────────────────────────── * `ctx.skills.register(skill)` requires an EAGER `content` body * (`SkillRegistration = Omit`, * `index.d.ts:71-79`), forcing every role's every skill to be read at boot. * `registerProvider` is lazy: `list()` advertises metadata, `get()` loads the * body only when the model actually invokes the skill — mirroring how the * Pi surface references skill files and reads them on demand. * * ── Candidate role set ───────────────────────────────────────────────────── * The provider advertises the skills of {roles currently active in the * workspace} ∪ {the promoted default/primary role}, walking each role's own * `skills` first and then, recursively, its `subagents[].skills` * (`ResolvedSubAgent` nests arbitrarily). The active set is read through the * injected per-session {@link DshActiveRoleSnapshot} (the rolebox dsh * `ActiveRoleRef`, `role-switcher.ts:131`, satisfies it structurally). The * provider contract exposes no session id (`index.d.ts:88-93`), so the union * over every recorded session plus the default role is the honest * workspace-wide answer. * * ── Guards (one bad candidate poisons the WHOLE catalog) ─────────────────── * `validateCandidate` (`dsh-skill/lib/index.js:454`) runs OUTSIDE the * provider-list try/catch (`:349-355`), so a single malformed candidate makes * every `list()` reject: * 1. name pre-filter — the grammar is `/^[a-z0-9]+(?:-[a-z0-9]+)*$/` * (`index.js:17`, `:29-31`); rejected names are dropped with ONE warning * each. rolebox role names like `emperor--chancellor` are NOT valid dsh * skill names — only grammar-valid skills can be surfaced. * 2. dedupe by name, first-wins in deterministic order (role skills before * subagent skills; roles in stable id order) — one provider must never * return a duplicate name. * 3. skip skills whose `filePath` no longer exists. * 4. honor `options.signal`, settling promptly on abort. * A candidate with an empty `description` would also throw (`index.js:457`), * so an empty description falls back to the skill name — keeping a real skill * loadable rather than poisoning the catalog. * * The dsh surface is consumed STRUCTURALLY (duck typing). This module does * NOT import any platform SDK package — no dsh and no opencode SDK imports. * * @module */ import type { ResolvedRole } from "../../../types.ts"; /** dsh provider name registered on `ctx.skills` (echoed by every candidate). */ export declare const ROLEBOX_SKILL_PROVIDER = "rolebox"; /** Custom `source` bucket for rolebox contributions (`index.d.ts:24` accepts it). */ export declare const ROLEBOX_SKILL_SOURCE = "rolebox"; /** * Precedence rank for rolebox skills. dsh reserves 100 project-dsh / * 200 project-agents / 300 custom / 400 user-dsh / 500 user-agents / 600 * bundled; 450 places rolebox above the custom bucket but below bundled * skills. Duplicate names are decided within one layer by this rank, lower * wins (`dsh-skill/lib/index.js:314`, `:518-520`). */ export declare const ROLEBOX_SKILL_RANK = 450; /** Structural dsh `SkillInvocationPolicy` (`index.d.ts:30-36`). */ export interface DshSkillInvocationPolicy { /** Whether model-facing catalogs and loaders include this skill. */ readonly modelInvocable: boolean; /** Whether human-facing command catalogs and loaders include this skill. */ readonly userInvocable: boolean; } /** Structural dsh resource base, directory member (`index.d.ts:26-28`). */ export interface DshSkillResourceBaseDirectory { readonly kind: "directory"; readonly path: string; } /** Structural dsh `SkillCandidate` (`index.d.ts:44-70`). */ export interface DshSkillCandidate { /** Kebab-case identifier used to address the skill. */ readonly name: string; /** Short routing description (must be non-empty, `index.js:457`). */ readonly description: string; /** Optional extra routing guidance. */ readonly whenToUse?: string; /** Resolved model and user invocation controls. */ readonly invocation: DshSkillInvocationPolicy; /** Discovery source bucket. */ readonly source: string; /** Owning provider name — must equal the registered provider (`index.js:462`). */ readonly provider: string; /** Provider-specific base for relative resources. */ readonly resourceBase?: DshSkillResourceBaseDirectory; /** Lower ranks win duplicate names (`index.js:314`). */ readonly rank: number; /** Opaque provider-owned handle handed back to `get()`. */ readonly locator: unknown; /** Absolute file path when the provider has one. */ readonly path?: string; /** Optional provider-specific metadata. */ readonly metadata?: Readonly>; } /** Structural dsh `SkillDefinition` (`index.d.ts:72-83`). */ export interface DshSkillDefinition { readonly name: string; readonly description: string; readonly whenToUse?: string; readonly invocation: DshSkillInvocationPolicy; readonly source: string; readonly provider: string; readonly resourceBase?: DshSkillResourceBaseDirectory; /** Markdown instruction body read lazily by `get()`. */ readonly content: string; /** Absolute file path when the skill came from disk. */ readonly path?: string; readonly metadata?: Readonly>; } /** Structural dsh `SkillLookupOptions` (`index.d.ts:88-93`) — `{cwd?, signal?}`. */ export interface DshSkillLookupOptions { /** Workspace selector for the current lookup. */ readonly cwd?: string | undefined; /** Abort discovery or loading work for the current caller. */ readonly signal?: AbortSignal | undefined; } /** Structural dsh `SkillProviderControl` (`index.d.ts:190-195`). */ export interface DshSkillProviderControl { /** Aborts when the exact provider registration is disposed. */ readonly signal: AbortSignal; /** Invalidate completed catalogs for the active registration. */ readonly invalidate: () => void; } /** Structural dsh `SkillProvider` (`index.d.ts:168-188`). */ export interface DshSkillProviderLike { /** Unique provider name in the `ctx.skills` registry. */ readonly name: string; /** List skill candidates for the current lookup context. */ readonly list: (options: DshSkillLookupOptions) => Promise; /** Load a complete skill body for a previously listed candidate. */ readonly get: (candidate: DshSkillCandidate, options: DshSkillLookupOptions) => Promise; } /** * Structural subset of the dsh rolebox `ActiveRoleRef` (`role-switcher.ts:131`) * the provider reads to resolve the active role set. Only `snapshot()` is * consumed; the concrete `ActiveRoleRef` satisfies this shape. */ export interface DshActiveRoleSnapshot { /** Snapshot the session→entry map; each entry carries the active role id. */ snapshot(): ReadonlyMap; } /** Dependencies for {@link DshSkillProvider}. */ export interface DshSkillProviderDeps { /** * Every fully-resolved role in the workspace, or a provider for them. The * provider filters this set down to the active ∪ default roles. */ roles: readonly ResolvedRole[] | (() => readonly ResolvedRole[] | Promise); /** * Per-session active-role holder (structural {@link DshActiveRoleSnapshot}). * Omit to advertise only the default/primary role. */ activeRole?: DshActiveRoleSnapshot; /** Role id promoted to primary; its skills are always candidates. */ defaultRoleId?: string; } /** * rolebox's dsh `SkillProvider`. * * Construct through {@link createDshSkillProviderFactory}: the factory form * is exactly what `ctx.skills.registerProvider(create)` requires, and it * captures the registration `control` so {@link invalidate} can request a * catalog refresh after a role switch (subtask 7). */ export declare class DshSkillProvider implements DshSkillProviderLike { readonly name = "rolebox"; private readonly deps; private control; /** * @param deps - Candidate role source + active/default role resolution. * @param control - The registration control borrowed from the factory; * held for {@link invalidate}. Omit in isolated tests. */ constructor(deps: DshSkillProviderDeps, control?: DshSkillProviderControl); /** * Ask the registry to refresh cached catalogs for this registration — the * single refresh seam the dsh plugin invokes on an active-role change * (through the role switcher) or a role re-resolution. * * Three guarantees: * 1. **No control** (the constructor was called without one, e.g. an * isolated unit test) → a silent no-op. * 2. **Disposed registration** → a no-op. The control's abort signal fires * when the exact provider registration is torn down * (`dsh-skill/lib/index.js:170`), so a stale provider from a superseded * role resolution never pokes a dead registry. * 3. **A throwing `invalidate`** is contained by try/catch + `log.debug` — * a catalog refresh must never break the role switch that requested it. */ invalidate(): void; /** * List the candidate role set's skills as dsh candidates. * * Guards (see the module docstring): grammar pre-filter with one warning per * rejected name, name dedupe first-wins in deterministic order, vanished * files skipped, and prompt abort handling. Never throws — a role-provider * failure degrades to an empty catalog rather than rejecting `list()`. */ list(options: DshSkillLookupOptions): Promise; /** * Load a candidate's body lazily via `loadSkillContent`. * * Returns `undefined` (never throws) when the locator is not a resolved * skill or the SKILL.md vanished after listing. */ get(candidate: DshSkillCandidate, options: DshSkillLookupOptions): Promise; /** * Resolve the candidate role set: active roles (union over every recorded * session) ∪ the default/primary role, in stable id order. */ private resolveCandidateRoles; /** * Walk a subagent subtree depth-first (pre-order): each subagent's own * skills, then its nested subagents. `ResolvedSubAgent` nests arbitrarily. */ private collectSubAgentSkills; /** * Emit one candidate per grammar-valid, existing, not-yet-seen skill. * * Order matters for guard (ii): the first occurrence in walk order claims * the name, so a later duplicate is dropped even if its file is missing. */ private collectSkills; } /** * Build the `(control) => SkillProvider` factory `ctx.skills.registerProvider` * demands. * * @param deps - Candidate role source + active/default role resolution. * @returns A factory that constructs one {@link DshSkillProvider} per * registration and captures its control for {@link DshSkillProvider.invalidate}. */ export declare function createDshSkillProviderFactory(deps: DshSkillProviderDeps): (control: DshSkillProviderControl) => DshSkillProvider; //# sourceMappingURL=skill-provider.d.ts.map