diff --git a/libs/ai-orchestrator/src/__tests__/mesh-router.spec.ts b/libs/ai-orchestrator/src/__tests__/mesh-router.spec.ts index c274ac7..79214de 100644 --- a/libs/ai-orchestrator/src/__tests__/mesh-router.spec.ts +++ b/libs/ai-orchestrator/src/__tests__/mesh-router.spec.ts @@ -6,6 +6,7 @@ import { createMeshRouterNode, DEFAULT_MESH_CONFIG, _test_createDefaultMeshClient, + MESH_SYSTEM_PROMPT, type MeshRouterConfig, } from '../orchestrator/mesh-router'; import { createDefaultAgentState } from '../schema'; @@ -119,6 +120,68 @@ describe('analyzeMeshQuery', () => { expect(result).toEqual({ target: null }); }); }); + + describe('generic query inference (without explicit persona tags)', () => { + it('infers po for status/requirements queries', async () => { + const client = fakeClient('{"target": "po"}'); + const messages = [ + makeMessage('@isolate- What is the status of this issue?'), + ]; + const result = await analyzeMeshQuery(messages, client); + expect(result).toEqual({ target: 'po' }); + }); + + it('infers po for design token queries', async () => { + const client = fakeClient('{"target": "po"}'); + const messages = [ + makeMessage('@isolate- What design tokens are available for colors?'), + ]; + const result = await analyzeMeshQuery(messages, client); + expect(result).toEqual({ target: 'po' }); + }); + + it('infers architect for monorepo structure queries', async () => { + const client = fakeClient('{"target": "architect"}'); + const messages = [ + makeMessage( + '@isolate- How are the presets structured in the monorepo?', + ), + ]; + const result = await analyzeMeshQuery(messages, client); + expect(result).toEqual({ target: 'architect' }); + }); + + it('infers architect for dependency queries', async () => { + const client = fakeClient('{"target": "architect"}'); + const messages = [ + makeMessage('@isolate- What dependencies do we use for testing?'), + ]; + const result = await analyzeMeshQuery(messages, client); + expect(result).toEqual({ target: 'architect' }); + }); + + it('infers a11y for accessibility compliance queries', async () => { + const client = fakeClient('{"target": "a11y"}'); + const messages = [ + makeMessage( + '@isolate- How do we ensure ARIA compliance in components?', + ), + ]; + const result = await analyzeMeshQuery(messages, client); + expect(result).toEqual({ target: 'a11y' }); + }); + + it('infers a11y for keyboard navigation queries', async () => { + const client = fakeClient('{"target": "a11y"}'); + const messages = [ + makeMessage( + '@isolate- What keyboard navigation patterns are supported?', + ), + ]; + const result = await analyzeMeshQuery(messages, client); + expect(result).toEqual({ target: 'a11y' }); + }); + }); }); // ── createMeshRouterNode ────────────────────────────────────────────────────── @@ -339,3 +402,26 @@ describe('_test_createDefaultMeshClient', () => { expect(() => _test_createDefaultMeshClient()).toThrow(/OPENAI_API_KEY/); }); }); + +describe('MESH_SYSTEM_PROMPT', () => { + it('includes all six persona domain descriptions with proper bullet format', () => { + const domainBullets = [ + '- po:', + '- architect:', + '- dev:', + '- a11y:', + '- qa:', + '- docs:', + ]; + + domainBullets.forEach((bullet) => { + expect(MESH_SYSTEM_PROMPT).toContain(bullet); + }); + }); + + it('contains required routing instruction keywords', () => { + expect(MESH_SYSTEM_PROMPT).toContain('@isolate-'); + expect(MESH_SYSTEM_PROMPT).toContain('ONLY'); + expect(MESH_SYSTEM_PROMPT).toContain('JSON'); + }); +}); diff --git a/libs/ai-orchestrator/src/orchestrator/mesh-router.ts b/libs/ai-orchestrator/src/orchestrator/mesh-router.ts index 4b10d1d..7cc849e 100644 --- a/libs/ai-orchestrator/src/orchestrator/mesh-router.ts +++ b/libs/ai-orchestrator/src/orchestrator/mesh-router.ts @@ -47,15 +47,26 @@ export interface MeshQueryResult { // ── LLM prompt ──────────────────────────────────────────────────────────────── -const MESH_SYSTEM_PROMPT = `You are a routing classifier for an AI multi-agent orchestration system. +export const MESH_SYSTEM_PROMPT = `You are a routing classifier for an AI multi-agent orchestration system. -Analyze the provided message and determine whether it contains a directed query or request aimed at a specific agent persona. +Analyze the provided message and identify the target specialist persona, or return null if no cross-persona routing is needed. -Valid persona IDs: po, architect, dev, a11y, qa, docs +Valid persona IDs and their domains: +- po: Project status, requirements, design tokens, high-level planning +- architect: Monorepo structure, tooling, dependencies, presets +- dev: Code implementation, Panda CSS logic, TypeScript types +- a11y: Accessibility standards, ARIA, keyboard navigation +- qa: Test coverage, edge cases, bug verification +- docs: Documentation, Storybook, usage examples Rules: -- Return a non-null target ONLY when the message explicitly asks a question of, or directs a task to, a specific persona — for example: "I need @isolate-po to clarify the token choice" or "Can qa verify this edge case?". -- Return null for standard work outputs, APPROVED/REJECTED decisions, inline explanations, or any message that does not address a specific persona as the recipient of a query. +- ONLY for messages starting with @isolate- that contain a query (?): infer the best-fit persona based on question content. For example: + * "What is the status of this issue?" → po + * "How are the presets structured?" → architect + * "How do we ensure ARIA compliance?" → a11y +- For explicit persona tags (e.g., "@isolate-po", "Can qa verify"): return the explicitly named persona. +- For ALL OTHER messages (no @isolate- prefix and no explicit persona tag): return null, even if they contain questions. +- Return null for standard work outputs, APPROVED/REJECTED decisions, and inline explanations. Respond with ONLY valid JSON in this exact format, with no additional text: {"target": "persona_id"}