/** * Registry-backed persona resolver — T898 / T899. * * When the static keyword rules in {@link classifyTask} fail to clear the * confidence floor, {@link resolvePersonaFromRegistry} walks the 4-tier * filesystem hierarchy looking for an installed `.cant` agent whose * `agent_id` matches a keyword derived from the task, then synthesises a * {@link PersonaResolution} envelope so the caller can use it as an * `agentId` override in {@link composeSpawnPayload}. * * ## Tier precedence (T899) * * Resolution walks these directories in order — first hit wins: * * 1. **Project** (`/.cleo/cant/agents/*.cant`) — project-local * overrides have the highest priority. * 2. **Global** (`~/.local/share/cleo/cant/agents/*.cant`) — user-installed * agents. * 3. **Packaged** (`packages/agents/seed-agents/*.cant`) — canonical seeds * bundled with the cleo npm. * 4. **Fallback** — no match; caller receives `null`. * * The registry DB (`signaldock.db`) is consulted when available so that the * resolver can read structured metadata (description, labels, role). When a * DB row is present for a tier, the description is used for keyword scoring. * When no DB row exists (fallback tier), the resolver falls back to the * `.cant` filename stem as the candidate agent id. * * ## Keyword matching * * The resolver scores task keywords against the `agent_id` string (which is * typically a kebab-case descriptor, e.g. `cleo-rust-lead`). A candidate * is accepted when its `agent_id` shares at least one keyword with the task's * labels, title, or description. The first matching candidate in tier order * is returned; tie-breaking is alphabetical within each tier. * * @module orchestration/registry-resolver * @task T898 Registry-backed persona resolution * @task T899 Global→project→packaged→fallback tier precedence * @epic T889 */ import type { DatabaseSync } from 'node:sqlite'; /** * Source tier from which the persona was resolved. * * Mirrors the `AgentTier` discriminant from `@cleocode/contracts` but * expressed as a plain union here to avoid adding a dependency on the full * contracts package inside what is otherwise a lightweight resolver module. * * @task T899 */ export type PersonaTier = 'project' | 'global' | 'packaged' | 'fallback'; /** * Result returned by {@link resolvePersonaFromRegistry} when a matching agent * is found. Callers should pass `agentId` directly to * `composeSpawnPayload({ agentId: resolution.agentId })` so the composer can * resolve the full registry envelope. * * @task T898 */ export interface PersonaResolution { /** Resolved agent business identifier (e.g. `cleo-rust-lead`). */ readonly agentId: string; /** * Tier the agent was found at. * * Reflects the T899 walk order: `project` > `global` > `packaged`. * `fallback` is used when the agent id was derived from a filesystem scan * rather than a DB row. */ readonly tier: PersonaTier; /** Absolute path to the `.cant` file that resolved to this persona. */ readonly cantPath: string; /** * Source of this resolution: * - `"registry"` when the agent id was found via a DB row query. * - `"filesystem"` when the agent id was found by scanning the tier * directory and matching the filename stem. */ readonly source: 'registry' | 'filesystem'; /** Human-readable description of why this agent was chosen. */ readonly reason: string; } /** * Minimal task input consumed by {@link resolvePersonaFromRegistry}. * * Mirrors the subset of {@link Task} actually needed for scoring — callers * can pass a full `Task` since it is a structural supertype. * * @task T898 */ export interface ClassifyInput { /** Task identifier (used in reason strings). */ readonly id: string; /** Task title (lowercased for scoring). */ readonly title: string; /** Task description (lowercased for scoring). Optional. */ readonly description?: string | null; /** Labels (lowercased for scoring). Optional. */ readonly labels?: readonly string[] | null; } /** * Resolve a persona from the agent registry using the 4-tier precedence walk. * * Called by {@link classifyTask} callers when the static keyword rules fail to * clear the confidence floor (i.e. `classifyTask` returned `usedFallback: * true`). The resolver finds the best-matching installed `.cant` file across * the tier hierarchy and returns it as a first-class {@link PersonaResolution}. * * Resolution order (T899): * 1. Project tier: `/.cleo/cant/agents/` * 2. Global tier: `~/.local/share/cleo/cant/agents/` * 3. Packaged tier: bundled `packages/agents/seed-agents/` * 4. Fallback: `null` (caller should use generic `cleo-subagent`) * * @param task - Task to classify (title, description, labels). * @param options - Optional configuration overrides. * @returns The highest-precedence matching persona, or `null` when none match. * * @example * ```typescript * // After classifyTask returns usedFallback=true: * const resolution = await resolvePersonaFromRegistry(task, { projectRoot: cwd }); * if (resolution) { * const payload = await composeSpawnPayload(db, task, { * agentId: resolution.agentId, * }); * } * ``` * * @task T898 * @task T899 */ export declare function resolvePersonaFromRegistry(task: ClassifyInput, options?: { /** Absolute project root for the project-tier lookup. Default: `process.cwd()`. */ projectRoot?: string; /** Pre-opened handle to `signaldock.db`. When not provided, only filesystem scoring is used. */ db?: DatabaseSync | null; /** Override the packaged seed directory (useful for tests). */ packagedSeedDir?: string; }): Promise; /** * Build an ordered list of tier directories for inspection. * * Returns each tier that exists on disk as `{ tier, dir }` in the canonical * precedence order. Useful for tooling (e.g. `cleo agent doctor`) that wants * to enumerate all installed tiers without actually resolving a task. * * @param projectRoot - Absolute project root (for the project tier). * @param packagedSeedDir - Override for the packaged tier. * @returns Ordered list of `{ tier, dir }` entries for existing directories. * * @task T899 */ export declare function listTierDirectories(projectRoot?: string, packagedSeedDir?: string): Array<{ tier: PersonaTier; dir: string; }>; //# sourceMappingURL=registry-resolver.d.ts.map