/** * Task-to-agent classifier — T891. * * Examines task attributes (labels, type, size, title keywords) and returns a * `ClassifyResult` that names the agent id, the intended spawn role, and a * confidence score. The resolved agent is fetched from the 4-tier registry * (ADR-049) so that `cleo agent attach/detach/remove` directly affect which * persona gets spawned. * * Resolution order: * 1. Label-exact match — task.labels contains a known persona label * 2. Keyword match — task.title / task.description contain trigger words * 3. Type/size heuristic — e.g. type=epic → orchestrator, size=small → worker * 4. Fallback — `cleo-subagent` with confidence 0.0 + warning * * Confidence floor: 0.5 (per T377 memory). Scores below the floor map to the * generic `cleo-subagent` fallback and emit a meta warning so callers can * surface the degradation in telemetry / spawn manifests. * * The five canonical role personas (ADR-055 D032 clean-forward, T1258 E1): * - `project-orchestrator` — coordinates multi-agent workflows * - `project-dev-lead` — general-purpose development lead * - `project-code-worker` — implementation / code execution worker * - `project-docs-worker` — documentation / canon worker * - `project-security-worker` — security-focused worker * * @module orchestration/classify * @task T891 CANT persona wiring * @task T1258 E1 canonical naming refactor * @task T1936 live-registry source for getRegisteredAgentIds + startup validation * @epic T889 / T1929 */ import type { DatabaseSync } from 'node:sqlite'; import type { Task } from '@cleocode/contracts'; /** * Minimum confidence required to route to a named persona. * * Results below this floor are promoted to the generic `cleo-subagent` fallback * so that low-confidence routing does not silently pick the wrong specialist. */ export declare const CLASSIFY_CONFIDENCE_FLOOR = 0.5; /** * Generic fallback agent id used when no persona clears the confidence floor. * * The fallback is still a valid agent — it resolves via the 4-tier registry * at the `packaged` or `fallback` tier. It carries no specialist skills, which * is intentional: the orchestrator should treat low-confidence routing as a * signal to decompose the task further or escalate for human review. */ export declare const CLASSIFY_FALLBACK_AGENT_ID = "cleo-subagent"; /** * Classification result returned by {@link classifyTask}. */ export interface ClassifyResult { /** * Resolved canonical agent id. * * One of: `project-orchestrator`, `project-dev-lead`, `project-code-worker`, * `project-docs-worker`, `project-security-worker`, or `cleo-subagent`. */ agentId: string; /** * Intended spawn role. Derived from the agent's default role but clamped to * the registry taxonomy so callers can feed it directly into * {@link ComposeSpawnPayloadOptions.role}. * * - `orchestrator` — only `project-orchestrator` + explicit orchestrator labels * - `lead` — specialist lead agents (dev-lead) * - `worker` — generic subagent or explicitly worker-labelled tasks */ role: 'orchestrator' | 'lead' | 'worker'; /** Confidence in [0, 1]. Values < {@link CLASSIFY_CONFIDENCE_FLOOR} use fallback. */ confidence: number; /** Human-readable explanation of why this agent was chosen. */ reason: string; /** * When `true`, the result was demoted to `cleo-subagent` because no rule * cleared the confidence floor. Callers should surface this in telemetry. */ usedFallback: boolean; /** * Warning message present only when `usedFallback === true`. * Callers SHOULD surface this in the spawn manifest / prompt meta. */ warning?: string; } /** * Returns the set of agent IDs that the classifier is allowed to emit. * * When `db` is provided (an open handle to `signaldock.db`), the live * `agents` table is queried for all rows whose `tier` is one of * `'project'`, `'global'`, or `'packaged'`. The result set is unique * and order-stable (alphabetical within tier groups). The generic * `cleo-subagent` fallback is always appended last. * * When `db` is omitted or the query fails, the function falls back to the * five canonical worker template names (the ADR-068 install set). This * preserves correctness in CI bootstrap scenarios and unit tests that run * without a live registry. * * Per ADR-068 (T1929): the canonical source-of-truth is the installed * templates set registered in `signaldock.db.agents` by `cleo init` (T1934). * Passing the open DB handle here wires that source to the classifier. * * @param db - Optional open `DatabaseSync` handle to `signaldock.db`. * When supplied the live `agents` table is used; when omitted the static * 5-template fallback list is returned instead. * @returns Readonly array of valid agent IDs (unique). * * @example Without DB (static fallback): * ```typescript * const ids = getRegisteredAgentIds(); * // ['project-orchestrator', 'project-dev-lead', 'project-code-worker', * // 'project-docs-worker', 'project-security-worker', 'cleo-subagent'] * ``` * * @example With live DB (registry-backed): * ```typescript * const db = new DatabaseSync(getGlobalAgentRegistryDbPath()); * const ids = getRegisteredAgentIds(db); * // [...all installed agents..., 'cleo-subagent'] * db.close(); * ``` * * @task T1326 * @task T1936 * @epic T1323 / T1929 */ export declare function getRegisteredAgentIds(db?: DatabaseSync): readonly string[]; /** * Validate that every agent ID referenced in {@link CLASSIFIER_RULES} is * present in the live registry. Throws {@link ClassifierUnregisteredAgentError} * at the first rule whose `agentId` is absent from the registered set. * * Call this once on classifier init (e.g. at `cleo session start`) to catch * drift between hardcoded classifier vocabulary and registered agents at startup * time rather than at task-classification time. The static fallback is always * accepted as a valid ID source when no DB is provided. * * Per T1326 contract: the error class, code (`E_CLASSIFIER_UNREGISTERED_AGENT`), * and field names are unchanged — only the validation trigger point moves from * per-classify-call to once-at-startup. * * @param db - Optional open `DatabaseSync` handle to `signaldock.db`. * When supplied the live registry is used for the registered-id lookup; * when omitted the static 5-template fallback is used (always passes for * the canonical rule set). * @throws {@link ClassifierUnregisteredAgentError} when a rule references an * agent ID that is absent from the registered vocabulary. * * @example At session start: * ```typescript * const db = new DatabaseSync(getGlobalAgentRegistryDbPath()); * validateClassifierRules(db); // throws immediately if rules are stale * db.close(); * ``` * * @task T1936 * @epic T1929 */ export declare function validateClassifierRules(db?: DatabaseSync): void; /** * Options for {@link classifyTask}. * * @task T1326 * @epic T1323 */ export interface ClassifyOptions { /** * Override the set of valid agent IDs used for output validation. * * When provided, the classifier throws {@link ClassifierUnregisteredAgentError} * if its result is not in this set. When omitted, the classifier validates * against {@link getRegisteredAgentIds()} — the built-in rule vocabulary. * * Pass the live registry IDs (`AgentRegistryAPI.list()` → `.map(a => a.agentId)`) * here to enforce that the classifier can only route to agents that are * currently attached and enabled in the project. */ allowedAgentIds?: readonly string[]; } /** * Classify a task against the known persona table and return the best-matching * agent id, role, and confidence score. * * **Confidence floor**: any result below {@link CLASSIFY_CONFIDENCE_FLOOR} * (0.5) is replaced by the generic `cleo-subagent` fallback with * `usedFallback: true` so callers can emit a warning. * * **Registry validation**: after scoring, the resolved agent ID is checked * against `opts.allowedAgentIds` (when provided) or the built-in vocabulary * from {@link getRegisteredAgentIds()}. If the emitted ID is not present, * {@link ClassifierUnregisteredAgentError} is thrown with a fix-hint listing * valid IDs. This ensures the classifier output space is always a strict subset * of the registry input space (Council 2026-04-24 FP atomic truth #3). * * @param task - Full task record (at minimum: id, title, type, labels). * @param opts - Optional classification options (registry override for validation). * @returns Classification result with agentId, role, confidence, and reason. * @throws {@link ClassifierUnregisteredAgentError} when the resolved agent ID * is absent from the allowed vocabulary. * * @example * ```typescript * const result = classifyTask(task); * if (result.usedFallback) { * console.warn(`[classify] ${result.warning}`); * } * // Pass result directly into composeSpawnPayload options: * const payload = await composeSpawnPayload(db, task, { * agentId: result.agentId, * role: result.role, * }); * ``` * * @example Registry-constrained classification (live DB agents only): * ```typescript * const liveIds = (await registry.list()).map(a => a.agentId); * const result = classifyTask(task, { allowedAgentIds: liveIds }); * ``` * * @task T891 * @task T1258 E1 canonical naming refactor * @task T1326 classifier↔registry contract */ export declare function classifyTask(task: Task, opts?: ClassifyOptions): ClassifyResult; //# sourceMappingURL=classify.d.ts.map