aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
402 lines (363 loc) • 14.4 kB
JavaScript
/**
* ConciergeIntentRouter — classifies user intent and dispatches to the correct
* AIWG skill, agent, or flow for the daemon Concierge.
*
* Pipeline: CLASSIFY → MATCH → CAPABILITY CHECK → DISPATCH → ABSORB
*
* Design: v1 uses pattern-matching intent classification (prompt-based upgrade
* is a future enhancement). The router never exposes internal
* skill/agent names in user-facing output — only the result reaches
* the user. Routing decisions are logged to daemon session state for
* steward diagnostics.
*
* @issue #606
* @tests @test/unit/daemon/concierge/intent-router.test.js
*/
// ---------------------------------------------------------------------------
// Intent category patterns
// ---------------------------------------------------------------------------
/**
* Ordered list of categories, each with a set of regex patterns.
* First match wins. 'conversational' is the catch-all fallback.
*/
const INTENT_PATTERNS = [
{
category: 'maintenance',
patterns: [
/\baiwg\s+(up\s+to\s+date|version|update|upgrade|sync|health|doctor|check)\b/i,
/\b(health\s+check|doctor|is\s+aiwg|update\s+aiwg|check\s+for\s+updates?)\b/i,
/\binstall(ation)?\s+(health|status)\b/i,
/\bvalidate[\s-]?metadata\b/i,
/\bcleanup\s+audit\b/i,
],
},
{
category: 'scheduling',
patterns: [
/\b(run|execute|do|schedule|set\s+up|create)\s+.{0,40}\b(every|each|daily|weekly|morning|night|hour|minute|cron|recurring|interval)\b/i,
/\b(cron|schedule|recurring|automation)\b/i,
/\brun\s+.{0,30}\s+at\s+\d/i,
],
},
{
category: 'agent-teams',
patterns: [
/\b(run|start|launch|kick\s+off|execute)\s+a?\s*(security|test|review|audit|analysis)\s*(team|squad|crew|group)?\b/i,
/\bagent\s+team\b/i,
/\bmulti.agent\b/i,
/\b(security|compliance|performance)\s+review\b/i,
/\bparallel\s+(agents?|review|execution)\b/i,
],
},
{
category: 'query',
patterns: [
/\b(what|which|how|where|when|who|why|list|show|what'?s)\b.*\b(command|skill|agent|feature|option|available|install|use|work|mean)\b/i,
/\b(help|how\s+do\s+i|how\s+to|can\s+(i|you|aiwg)|does\s+(it|aiwg)|what\s+is|explain)\b/i,
/\b(aiwg\s+kb|knowledge\s+base|documentation|docs)\b/i,
/\bwhat\s+(are|were|can)\s+.{3,30}\b(command|skill|agent|feature|option|available|install)\b/i,
/\bhow\s+(does|do|can|should)\s+.{3,60}\b(work|function|run|operate|behave)\b/i,
],
},
{
category: 'sdlc',
patterns: [
/\b(transition|move|start|begin|kick\s+off|enter)\s+(to\s+|into\s+)?(inception|elaboration|construction|transition|production)\b/i,
/\b(project\s+status|phase\s+(gate|check|status)|iteration\s+\d+|sprint\s+\d+)\b/i,
/\b(deploy(ment)?|release|rollout)\s+(to\s+)?(prod(uction)?|staging)\b/i,
/\b(requirements?|architecture|sad|adr|test\s+plan|risk\s+register)\b/i,
/\b(intake|onboard|handoff|retrospective|retro)\b/i,
/\b(fix|address|resolve|work\s+on)\s+(issue|bug|ticket|#\d+)\b/i,
/\baddress.issues?\b/i,
/\b(run|start|execute)\s+(the\s+)?ralph\s+loop\b/i,
],
},
{
category: 'conversational',
patterns: [
// Catch-all — always matches; must be last
/.*/,
],
},
];
// ---------------------------------------------------------------------------
// Default handler catalog (v1 — curated by category)
// ---------------------------------------------------------------------------
/**
* Maps each intent category to its default handler descriptor.
* Handler ids reference AIWG skills/agents/flows by their internal id.
* The id is used for dispatch but never surfaced to the user.
*/
const DEFAULT_HANDLERS = {
maintenance: {
id: 'aiwg-steward',
type: 'agent',
description: 'AIWG installation health and self-maintenance',
requires_feature: null,
},
scheduling: {
id: 'schedule',
type: 'skill',
description: 'Recurring task scheduling',
requires_feature: 'cron',
},
'agent-teams': {
id: 'flow-security-review-cycle',
type: 'flow',
description: 'Multi-agent review and team coordination',
requires_feature: 'agent_teams',
},
sdlc: {
id: 'sdlc-complete',
type: 'framework',
description: 'SDLC phase workflows and project management',
requires_feature: null,
},
query: {
id: 'aiwg-kb',
type: 'skill',
description: 'AIWG knowledge base and help',
requires_feature: null,
},
conversational: {
id: 'concierge-inline',
type: 'inline',
description: 'Direct concierge response',
requires_feature: null,
},
};
// ---------------------------------------------------------------------------
// Sensitive operation categories for discreet logging
// ---------------------------------------------------------------------------
const SENSITIVE_CATEGORIES = new Set(['maintenance']);
const SENSITIVE_KEYWORDS = /\b(token|secret|key|credential|password|auth|cert(ificate)?|rotate)\b/i;
// ---------------------------------------------------------------------------
// ConciergeIntentRouter
// ---------------------------------------------------------------------------
export class ConciergeIntentRouter {
/**
* @param {Object} [options]
* @param {Object|null} [options.capabilityMatrix] Loaded capability matrix
* (from src/providers/capability-matrix.ts). If null, capability checks
* are skipped.
* @param {Object|null} [options.catalog] Installed catalog for
* semantic search (future v2 enhancement). Currently unused.
* @param {Object|null} [options.sessionLog] Session state logger.
* Must expose: log(entry: Object) => void
* @param {number} [options.confidenceThreshold=0.7] Below this score,
* the router asks for clarification instead of dispatching.
* @param {string|null} [options.provider] Current provider key
* (e.g. 'claude-code'). Used for capability checks.
*/
constructor({
capabilityMatrix = null,
catalog = null,
sessionLog = null,
confidenceThreshold = 0.7,
provider = null,
} = {}) {
this._capabilityMatrix = capabilityMatrix;
this._catalog = catalog;
this._sessionLog = sessionLog;
this.confidenceThreshold = confidenceThreshold;
this.provider = provider;
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
/**
* Route a raw user message to the appropriate handler.
*
* Returns a routing result that the Concierge uses for dispatch.
* Never exposes internal handler names to the caller directly — the
* caller is responsible for keeping routing internals out of user output.
*
* @param {string} message Raw user message
* @param {Object} [ctx] Optional per-call context overrides
* @param {string} [ctx.provider] Override the instance-level provider
* @returns {RoutingResult}
*/
route(message, ctx = {}) {
const provider = ctx.provider ?? this.provider;
const { category, confidence } = this.classify(message);
const isSensitive = this._isSensitive(message, category);
if (confidence < this.confidenceThreshold) {
const result = this._buildFallback(message, 'low-confidence', { category, confidence });
this._logDecision({ message: isSensitive ? '[redacted]' : message, ...result, provider });
return result;
}
const handler = this._matchHandler(category, message);
if (!handler) {
const result = this._buildFallback(message, 'no-handler', { category, confidence });
this._logDecision({ message: isSensitive ? '[redacted]' : message, ...result, provider });
return result;
}
const available = this._checkCapability(handler, provider);
if (!available) {
const result = this._buildFallback(message, 'capability-unavailable', {
category,
confidence,
handler,
provider,
});
this._logDecision({ message: isSensitive ? '[redacted]' : message, ...result, provider });
return result;
}
const result = {
ok: true,
category,
confidence,
handler,
provider,
fallback: false,
isSensitive,
};
this._logDecision({ message: isSensitive ? '[redacted]' : message, ...result });
return result;
}
/**
* Classify the user message into an intent category.
*
* Returns the category and a confidence score in [0, 1].
* Confidence is 1.0 for a specific-category match, 0.5 for conversational.
*
* @param {string} message
* @returns {{ category: string, confidence: number }}
*/
classify(message) {
if (!message || typeof message !== 'string') {
return { category: 'conversational', confidence: 0.5 };
}
const normalized = message.trim();
for (const { category, patterns } of INTENT_PATTERNS) {
// Skip catch-all for primary classification pass
if (category === 'conversational') continue;
for (const pattern of patterns) {
if (pattern.test(normalized)) {
return { category, confidence: 1.0 };
}
}
}
return { category: 'conversational', confidence: 0.5 };
}
/**
* Expose the pattern catalog for testing and introspection.
* @returns {Array<{category: string, patterns: RegExp[]}>}
*/
getPatterns() {
return INTENT_PATTERNS.slice(0, -1); // exclude catch-all
}
/**
* Expose the handler catalog for testing and introspection.
* @returns {Record<string, Object>}
*/
getHandlers() {
return { ...DEFAULT_HANDLERS };
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
/**
* Find the best handler for the given category and message.
* v1: returns the default catalog handler for the category.
* v2: will do semantic search against the installed catalog.
*/
_matchHandler(category, _message) {
return DEFAULT_HANDLERS[category] ?? null;
}
/**
* Check whether the handler's required feature is available on the provider.
* Returns true if no capability matrix is configured or the handler has
* no feature requirement.
*/
_checkCapability(handler, provider) {
if (!this._capabilityMatrix || !provider || !handler.requires_feature) {
return true;
}
const caps = this._capabilityMatrix.providers?.[provider];
if (!caps) return true;
const feature = handler.requires_feature;
const native = caps.native_features?.[feature];
const emulated = caps.emulation?.[feature];
return !!(native || (emulated && emulated !== null));
}
/**
* Build a standardised fallback result.
*/
_buildFallback(message, reason, details = {}) {
return {
ok: false,
fallback: true,
reason,
category: details.category ?? 'unknown',
confidence: details.confidence ?? 0,
handler: null,
provider: details.provider ?? null,
isSensitive: this._isSensitive(message, details.category),
// Suggestion for in-persona user response (never expose raw reason)
suggestion: this._buildFallbackSuggestion(reason, details),
};
}
/**
* Build a user-friendly (in-persona) fallback suggestion.
* These strings are consumed by the Concierge response layer — they are NOT
* shown verbatim to the user; the Concierge applies tone rules before surfacing.
*/
_buildFallbackSuggestion(reason, details) {
switch (reason) {
case 'low-confidence':
return 'Could you clarify what you need? I want to make sure I route this correctly.';
case 'no-handler':
return `I don\'t have a handler for that category yet. Here\'s what I can help with: maintenance, scheduling, agent teams, SDLC workflows, and general questions.`;
case 'capability-unavailable': {
const feature = details.handler?.requires_feature;
const provider = details.provider;
return feature && provider
? `That feature (${feature}) isn\'t available on ${provider}. I can use AIWG emulation — would you like me to proceed that way?`
: 'That capability isn\'t available in the current environment. Would you like to see alternatives?';
}
default:
return 'I wasn\'t able to handle that request. Could you rephrase it?';
}
}
/**
* Determine if the message or category is sensitive (redact from logs).
*/
_isSensitive(message, category) {
if (SENSITIVE_CATEGORIES.has(category)) return false; // maintenance is not sensitive
return typeof message === 'string' && SENSITIVE_KEYWORDS.test(message);
}
/**
* Log the routing decision to the session log.
* Log entries are consumed by the steward for diagnostics — never surfaced
* directly to the user.
*/
_logDecision(entry) {
if (!this._sessionLog) return;
try {
this._sessionLog.log({
source: 'concierge:intent-router',
timestamp: new Date().toISOString(),
...entry,
});
} catch {
// Non-fatal — logging failures must not disrupt routing
}
}
}
// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------
export { INTENT_PATTERNS, DEFAULT_HANDLERS };
/**
* @typedef {Object} RoutingResult
* @property {boolean} ok True when a handler was matched and is available
* @property {string} category Classified intent category
* @property {number} confidence Classification confidence [0, 1]
* @property {Object|null} handler Matched handler descriptor (or null on fallback)
* @property {string|null} provider Provider key used for capability check
* @property {boolean} fallback True when ok=false and a fallback was triggered
* @property {boolean} isSensitive True when message contains sensitive keywords
* @property {string} [reason] Fallback reason (present when fallback=true)
* @property {string} [suggestion] In-persona fallback suggestion (when fallback=true)
*/