| name | intent-router |
| description | Interprets --just-* and --skip-* command flags for /legion:build and /legion:review, validates flag combinations against rules, and resolves the matching team template from intent-teams.yaml. Use when the user passes intent flags to build or review commands, asks about flag combinations, or needs to filter agent teams by intent. |
| triggers | ["intent","flags","routing","validation","--just-","--skip-","filter agents","team template","flag combinations","build flags","review flags"] |
| token_cost | low |
| summary | Interprets --just-* and --skip-* flags for /legion:build and /legion:review. Validates flag combinations, resolves team templates from intent-teams.yaml, and produces filtered execution instructions. |
Intent Router
Interprets semantic intent flags (--just-* and --skip-*) to route execution to specific agent teams and filter criteria. Validates flag combinations, resolves templates from intent-teams.yaml, and produces execution instructions.
Used by /legion:build and /legion:review when intent flags are present.
Section 1: Intent Flag Parsing
Extract and normalize intent flags from command arguments.
parseIntentFlags(arguments)
Supported Flags:
| Flag | Intent | Description |
|---|
--just-harden | harden | Security audit mode |
--just-document | document | Documentation-only mode |
--just-security | security-only | Security review mode (review command only) |
--skip-frontend | skip-frontend | Exclude frontend/UI tasks |
--skip-backend | skip-backend | Exclude backend/API tasks |
Parsing Rules:
- Equals syntax supported:
--just-harden=true or --just-harden
- Multiple --just- flags detected*: Flag as conflict (only one primary intent allowed)
- Case insensitive:
--JUST-HARDEN normalizes to --just-harden
- Unknown flags fail fast (strict validation — no silent warning):
- The authoritative set of valid flags is
.planning/config/intent-teams.yaml under intents.* keys (plus the global flags listed in this section's Supported Flags table).
- If an arg begins with
-- and is not in that set:
- Emit a VALIDATION error, halt the command.
- Suggest the closest valid flag using a Levenshtein distance ≤ 2 against the union of declared intent flag names. Format:
Unknown flag: --just-hardne. Did you mean --just-harden?
- If no candidate is within distance 2: list the full valid-flag set and exit.
- Escape hatch:
--unsafe-unknown-flags opts into permissive mode (emit an INFO log for each unknown flag and continue). This flag MUST be passed explicitly — it is never the default.
- Rationale: silent-drop of a typo flag (e.g.,
--just-hardne → build proceeds without hardening) has been the observed failure mode. Fail fast unless the user has explicitly asked for forward-compat behavior.
- Cross-reference:
validateFlagCombination in Section 2 follows the same fail-fast contract.
- Duplicate flags: Deduplicate, use first occurrence
Implementation:
function parseIntentFlags(args) {
const result = {
rawFlags: [],
intents: [],
filters: {
skipFrontend: false,
skipBackend: false
},
primaryIntent: null,
hasConflicts: false
};
const seenFlags = new Set();
for (const arg of args) {
const flag = arg.toLowerCase().split('=')[0];
if (seenFlags.has(flag)) continue;
seenFlags.add(flag);
switch (flag) {
case '--just-harden':
result.rawFlags.push(flag);
result.intents.push('harden');
result.primaryIntent = 'harden';
break;
case '--just-document':
result.rawFlags.push(flag);
result.intents.push('document');
result.primaryIntent = 'document';
break;
case '--just-security':
result.rawFlags.push(flag);
result.intents.push('security-only');
result.primaryIntent = 'security-only';
break;
case '--skip-frontend':
result.rawFlags.push(flag);
result.filters.skipFrontend = true;
break;
case '--skip-backend':
result.rawFlags.push(flag);
result.filters.skipBackend = true;
break;
default:
if (flag.startsWith('--just-') || flag.startsWith('--skip-')) {
console.warn(`Unknown intent flag: ${arg}`);
}
}
}
if (result.intents.length > 1) {
result.hasConflicts = true;
}
return result;
}
Section 2: Validation Engine
Validate flag combinations against rules from intent-teams.yaml.
validateFlagCombination(intents, command)
Validation Rules (from intent-teams.yaml):
- Mutual Exclusion: Certain flags cannot be combined
- Command Context: Some flags only valid for specific commands
- Redundancy Detection: Flag combinations that make no sense
- Empty Result Detection: Flags that would result in no work
Implementation:
function validateFlagCombination(intents, command) {
const errors = [];
const suggestions = [];
const rules = loadValidationRules();
for (const rule of rules.mutual_exclusion) {
const hasAllFlags = rule.flags.every(flag =>
intents.intents.includes(flag) ||
(flag === 'skip-frontend' && intents.filters.skipFrontend) ||
(flag === 'skip-backend' && intents.filters.skipBackend)
);
if (hasAllFlags) {
errors.push(rule.error);
if (rule.flags.length === 2) {
suggestions.push(`Use only --${rule.flags[0].replace('_', '-')} for this operation`);
}
}
}
for (const rule of rules.requires_command) {
if (intents.intents.includes(rule.flag)) {
if (!rule.commands.includes(command)) {
errors.push(`${rule.error} (used with ${command})`);
if (rule.commands.length === 1) {
suggestions.push(`Use /legion:${rule.commands[0]} instead`);
}
}
}
}
if (intents.filters.skipFrontend && intents.filters.skipBackend) {
errors.push('Cannot skip both frontend and backend — nothing to build.');
suggestions.push('Remove one skip flag to proceed');
}
if (intents.intents.includes('document') && intents.filters.skipFrontend) {
errors.push('--just-document already excludes implementation; --skip-frontend is redundant.');
suggestions.push('Use --just-document alone');
}
return {
valid: errors.length === 0,
errors,
suggestions
};
}
Error Message Format
❌ Intent Validation Failed
Errors:
1. Cannot use --just-harden and --just-document together. Choose one intent.
2. --just-harden is only valid for /legion:build (used with review)
Suggestions:
- Use only --just-harden for this operation
- Use /legion:build instead
Section 3: Team Template Resolution
Load and resolve team templates from intent-teams.yaml.
loadIntentTeams()
function loadIntentTeams() {
const configPath = '.planning/config/intent-teams.yaml';
const emptyConfig = {
intents: {},
nl_patterns: {},
command_routes: {},
context_rules: {}
};
let content;
try {
content = fs.readFileSync(configPath, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') {
console.warn(`intent-router: ${configPath} not found. Intent routing disabled.`);
return emptyConfig;
}
console.warn(`intent-router: failed to read ${configPath}: ${err.message}. Intent routing disabled.`);
return emptyConfig;
}
let parsed;
try {
parsed = parseYaml(content);
} catch (err) {
console.warn(`intent-router: failed to parse ${configPath}: ${err.message}. Intent routing disabled.`);
return emptyConfig;
}
const schemaPath = '.planning/config/intent-teams.schema.json';
try {
if (fs.existsSync(schemaPath)) {
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
const validation = validateAgainstSchema(parsed, schema);
if (!validation.valid) {
console.warn(`intent-router: ${configPath} failed schema validation: ${validation.errors.join('; ')}. Using subset that passed.`);
}
}
} catch (err) {
console.warn(`intent-router: schema validation error: ${err.message}. Continuing with unvalidated config.`);
}
return {
intents: parsed.intents || {},
nl_patterns: parsed.nl_patterns || {},
command_routes: parsed.command_routes || {},
context_rules: parsed.context_rules || {}
};
}
resolveTeamTemplate(intentName)
function resolveTeamTemplate(intentName) {
const config = loadIntentTeams();
const intent = config.intents[intentName];
if (!intent) {
throw new Error(`Unknown intent: ${intentName}`);
}
return {
intent: intentName,
description: intent.description,
mode: intent.mode,
agents: intent.agents || { primary: [], secondary: [] },
domains: intent.domains || [],
filters: intent.filter || null
};
}
resolveFilterCriteria(intentName)
function resolveFilterCriteria(intentName) {
const config = loadIntentTeams();
const intent = config.intents[intentName];
if (!intent || !intent.filter) {
return null;
}
return {
includeTaskTypes: intent.filter.include_task_types || [],
excludeTaskTypes: intent.filter.exclude_task_types || [],
excludeAgents: intent.filter.exclude_agents || [],
excludeFilePatterns: intent.filter.exclude_file_patterns || []
};
}
mapDomainsToAgents(domains)
function mapDomainsToAgents(domains) {
const authorityMatrix = loadAuthorityMatrix();
const mapping = {};
for (const domain of domains) {
for (const [agentId, agentData] of Object.entries(authorityMatrix.agents)) {
if (agentData.exclusive_domains?.includes(domain)) {
mapping[domain] = agentId;
break;
}
}
}
return mapping;
}
Section 4: Execution Mode Detection
Determine execution strategy based on intent configuration.
detectExecutionMode(intent)
function detectExecutionMode(intent) {
return intent.mode || 'ad_hoc';
}
getExecutionInstructions(mode)
function getExecutionInstructions(mode) {
const instructions = {
ad_hoc: {
description: 'Spawn intent-specific team dynamically',
steps: [
'Resolve team template from intent-teams.yaml',
'Load primary and secondary agent personalities',
'Phase 1: construct prompts for all team members',
'Phase 2: dispatch via wave-executor Section 4 (honors adapter.parallel_execution)',
'Collect results and synthesize output',
'Generate intent-specific summary report'
],
manages_own_fanout: true,
agentCount: 'from template'
},
filter_plans: {
description: 'Filter phase plans by intent criteria',
steps: [
'Load all plans for current phase',
'Apply task type filters from intent',
'Apply file pattern exclusions',
'Remove plans matching exclude criteria',
'Execute remaining plans with standard wave executor'
],
manages_own_fanout: false,
agentCount: 'from filtered plans'
},
filter_review: {
description: 'Filter review findings by intent domains',
steps: [
'Execute standard review process',
'Collect all findings from reviewers',
'Filter findings to intent domains only',
'Apply severity threshold if specified',
'Generate filtered review report'
],
parallel: false,
agentCount: 'standard review panel'
}
};
return instructions[mode] || instructions.ad_hoc;
}
Dispatch specification — Intent-routed execution (canonical)
Intent-router does NOT spawn agents directly. It computes the plan/team input set and delegates all agent spawning to wave-executor (for ad_hoc and filter_plans) or review-loop (for filter_review). The specification below documents the downstream dispatch for each mode.
| Mode | When | Why parallel is safe | How many | Mechanism |
|---|
ad_hoc | After resolveTeamTemplate() returns a team and loadIntentTeams() precondition check passes. Fires once per /legion:build or /legion:quick invocation with a matched intent. | Team members from intent-teams.yaml are given distinct task descriptions with non-overlapping files_modified. Wave-executor's sequential_files check still runs; any overlap forces sequential fallback. | len(team.primary) + len(team.secondary) — exact count from resolved template; no rounding or deduplication beyond agent ID uniqueness. | Delegate to wave-executor Section 4. Pass team members as a single wave. Parallel vs sequential decided by adapter.parallel_execution AND sequential_files overlap check. |
filter_plans | After loadPhasePlans() and applyFilterPredicates(intent.filter) complete. Fires once per /legion:build or /legion:review invocation with a filter_plans intent. | Filter is subtractive only — it removes plans from the phase set but never duplicates or mutates them. Remaining plans have their original files_modified declarations; wave-executor applies the same overlap detection and sequential fallback. | Count of plans remaining after filter = len(phase_plans) - len(excluded_plans). Zero is valid — log and exit cleanly. | Delegate filtered plan set to wave-executor Section 4. Wave-executor owns the agent spawn decision; intent-router does not call the Agent/Bash tool directly. |
filter_review | After review-loop completes and emits findings. Fires once per /legion:review invocation with a filter_review intent. | No parallel dispatch — filter is applied to already-collected findings. Sequential by construction. | 0 — no additional agent spawns; only findings are pruned. | Pure data transform on review output JSON. No Agent or Bash tool invocation. |
Preconditions (before entering any mode):
loadIntentTeams() must return a config (possibly empty).
- If the resolved intent specifies
mode: filter_plans, verify .planning/phases/{NN}/ exists. If missing → emit <escalation>severity: blocker, type: scope, decision: Cannot filter plans — phase directory missing, context: .planning/phases/{NN}/ not found</escalation> and STOP.
- If the resolved intent specifies
mode: filter_review, verify a prior review cycle artifact exists at .planning/phases/{NN}/REVIEW.md. If missing → emit <escalation>severity: blocker, type: scope, decision: Cannot filter review — no prior review cycle, context: REVIEW.md missing</escalation> and STOP.
Section 5: Filter Predicates
Create reusable filter functions for plan and review filtering.
createAgentFilter(excludeAgents)
function createAgentFilter(excludeAgents) {
const excludeSet = new Set(excludeAgents);
return (agentId) => !excludeSet.has(agentId);
}
createFileFilter(patterns)
function createFileFilter(patterns) {
const minimatch = require('minimatch');
return (filePath) => {
for (const pattern of patterns) {
const isNegation = pattern.startsWith('!');
const actualPattern = isNegation ? pattern.slice(1) : pattern;
const matches = minimatch(filePath, actualPattern);
if (matches && !isNegation) return false;
if (matches && isNegation) return true;
}
return true;
};
}
function simpleGlobMatch(path, pattern) {
const regex = pattern
.replace(/\*\*/g, '<<<DOUBLESTAR>>>')
.replace(/\*/g, '[^/]*')
.replace(/<<<DOUBLESTAR>>>/g, '.*');
return new RegExp(`^${regex}$`).test(path);
}
createTaskFilter(includeTypes, excludeTypes)
function createTaskFilter(includeTypes, excludeTypes) {
const includeSet = new Set(includeTypes);
const excludeSet = new Set(excludeTypes);
return (taskType) => {
if (includeTypes.length > 0 && !includeSet.has(taskType)) {
return false;
}
if (excludeSet.has(taskType)) {
return false;
}
return true;
};
}
combineFilters(filters)
function combineFilters(filters) {
return (item) => filters.every(filter => filter(item));
}
const filters = [
createAgentFilter(['engineering-frontend-developer']),
createTaskFilter(['security-audit'], ['feature-implementation']),
createFileFilter(['!src/frontend/**'])
];
const combined = combineFilters(filters);
const passes = combined({ agent: 'testing-api-tester', taskType: 'security-audit', file: 'src/api/auth.ts' });
Section 6: Integration Guide
How to use intent-router in commands.
In commands/build.md
const { parseIntentFlags, validateFlagCombination, resolveTeamTemplate, detectExecutionMode } =
require('./skills/intent-router');
function buildCommand(args) {
const intents = parseIntentFlags(args);
if (intents.rawFlags.length > 0) {
const validation = validateFlagCombination(intents, 'build');
if (!validation.valid) {
console.error('❌ Intent Validation Failed\n');
validation.errors.forEach((err, i) => console.error(`${i + 1}. ${err}`));
console.error('\nSuggestions:');
validation.suggestions.forEach(s => console.error(`- ${s}`));
process.exit(1);
}
const template = resolveTeamTemplate(intents.primaryIntent);
const mode = detectExecutionMode(template);
if (mode === 'ad_hoc') {
return executeAdHocTeam(template);
} else if (mode === 'filter_plans') {
const filteredPlans = filterPlansByIntent(template);
return executeFilteredPlans(filteredPlans);
}
}
return standardBuild(args);
}
In commands/review.md
const { parseIntentFlags, validateFlagCombination, resolveTeamTemplate } =
require('./skills/intent-router');
function reviewCommand(args) {
const intents = parseIntentFlags(args);
if (intents.intents.includes('security-only')) {
const validation = validateFlagCombination(intents, 'review');
if (!validation.valid) {
reportValidationErrors(validation);
return;
}
const template = resolveTeamTemplate('security-only');
return executeSecurityReview(template);
}
return standardReview(args);
}
Error Handling Pattern
function handleIntentErrors(validation) {
if (!validation.valid) {
const output = [
'❌ Intent Validation Failed',
'',
'Errors:'
];
validation.errors.forEach((err, i) => {
output.push(`${i + 1}. ${err}`);
});
if (validation.suggestions.length > 0) {
output.push('', 'Suggestions:');
validation.suggestions.forEach(s => {
output.push(`- ${s}`);
});
}
console.error(output.join('\n'));
process.exit(1);
}
}
Command-Line Usage Examples
/legion:build --just-harden
/legion:build --just-document
/legion:build --skip-frontend
/legion:review --just-security
/legion:build --just-harden --just-document
Section 7: Natural Language Intent Parsing
Parse free-text user input into structured command+flags with confidence scoring. Enables users to type natural language queries (e.g., "fix the tests", "harden security", "write the docs") instead of memorizing exact --just-* and --skip-* flag names.
7.1 parseNaturalLanguage(input)
function parseNaturalLanguage(input) {
const normalizedInput = input.toLowerCase().trim();
const { nlPatterns, commandRoutes } = loadNLPatterns();
const candidates = [];
for (const [intentName, patterns] of Object.entries(nlPatterns)) {
const score = scoreCandidate(normalizedInput, patterns);
if (score > 0) {
const intentToFlag = {
'harden': { command: '/legion:build', flags: ['--just-harden'] },
'document': { command: '/legion:build', flags: ['--just-document'] },
'skip-frontend': { command: '/legion:build', flags: ['--skip-frontend'] },
'skip-backend': { command: '/legion:build', flags: ['--skip-backend'] },
'security-only': { command: '/legion:review', flags: ['--just-security'] }
};
const route = intentToFlag[intentName] || { command: null, flags: [] };
candidates.push({
command: route.command,
flags: route.flags,
confidence: score,
label: `${route.command} ${route.flags.join(' ')}`.trim()
});
}
}
for (const [commandName, patterns] of Object.entries(commandRoutes)) {
const score = scoreCandidate(normalizedInput, patterns);
if (score > 0) {
candidates.push({
command: `/legion:${commandName}`,
flags: [],
confidence: score,
label: `/legion:${commandName}`
});
}
}
candidates.sort((a, b) => b.confidence - a.confidence);
if (candidates.length === 0) {
return {
command: null,
flags: [],
confidence: 0,
fallbackSuggestion: 'No matching command found. Try /legion:status for available commands.'
};
}
const best = candidates[0];
if (best.confidence >= 0.8) {
return {
command: best.command,
flags: best.flags,
confidence: best.confidence,
fallbackSuggestion: null
};
}
if (best.confidence >= 0.5) {
return {
command: best.command,
flags: best.flags,
confidence: best.confidence,
fallbackSuggestion: best.label
};
}
const top3 = candidates.slice(0, 3).map((c, i) => `${i + 1}. ${c.label}`).join('\n');
return {
command: null,
flags: [],
confidence: best.confidence,
fallbackSuggestion: `Did you mean: ${top3}`
};
}
7.2 Pattern Matching Strategy
Two-tier matching system for balancing speed and accuracy:
Tier 1: Keyword Cluster Matching (fast, pattern-based)
- Each intent/command has a set of weighted keywords (word to weight, 0.0-1.0)
- Tokenize user input into words
- Score = sum of matched keyword weights / total possible weight for that cluster
- Fast O(n*m) lookup, good for single-word or multi-word queries
Tier 2: Phrase Template Matching (structured, higher confidence)
- Regex-like templates for common expressions
- Templates use
{option1|option2} syntax for alternation and {word}? for optional words
- Example:
"fix {the|my|our} {tests|testing|test suite}" matches "fix the tests", "fix my testing"
- Template match provides a confidence bonus
Scoring Formula:
final_score = max(normalized_keyword_score, template_match) + exact_name_bonus
Where:
normalized_keyword_score = (sum of matched keyword weights) / (sum of TOP-K weights) [0-1]
K = min(3, count(keywords in cluster))
template_match = 0.8 if any phrase template matches, 0 otherwise
exact_name_bonus = 0.2 if input contains the exact command name (word-boundary match), 0 otherwise
final_score is capped at 1.0.
Why top-K normalization:
Dividing by the sum of ALL keyword weights penalized clusters with broad keyword lists (a 10-keyword cluster with 2 matches scored ~0.2 even for an unambiguous match). Normalizing against the sum of the top-K (K=3) heaviest weights maps realistic 2-3 keyword matches into the HIGH tier without requiring keyword-exhaustive user input.
Tie-break rules (applied in order — no ambiguity):
- If two candidates score within 0.1 of each other AND one is an
nl_patterns match while the other is a command_routes match: prefer the nl_patterns (intent flag) match — it is more specific than a generic command route.
- If a tie remains: prefer the candidate whose exact command name appears in the input (word-boundary match).
- If a tie remains: prefer the candidate listed earlier in
intent-teams.yaml.
Calibration examples (validate any edit to the formula against these):
| Input | Expected tier | Expected command |
|---|
start | HIGH | /legion:start (exact-name dominates) |
start a new project | HIGH | /legion:start |
build | HIGH | /legion:build |
run the build | HIGH | /legion:build |
harden the auth code | HIGH | /legion:build --just-harden |
review the code for security | HIGH | /legion:review --just-security |
what's the status | HIGH | /legion:status |
help me figure out what next | MEDIUM | /legion:status (suggested) |
do the thing | LOW | none — top-3 suggestions |
function scoreCandidate(input, patterns) {
const inputTokens = input.split(/\s+/);
const keywordEntries = Object.entries(patterns.keywords);
let matchedWeight = 0;
for (const [keyword, weight] of keywordEntries) {
const keywordRegex = new RegExp(
'\\b' + keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i');
if (keywordRegex.test(input)) {
matchedWeight += weight;
}
}
const K = Math.min(3, keywordEntries.length);
const topKWeightSum = keywordEntries
.map(([, w]) => w)
.sort((a, b) => b - a)
.slice(0, K)
.reduce((s, w) => s + w, 0);
const normalizedKeywordScore =
topKWeightSum > 0 ? Math.min(1.0, matchedWeight / topKWeightSum) : 0;
let templateScore = 0;
if (patterns.phrases && patterns.phrases.length > 0) {
for (const phrase of patterns.phrases) {
const regex = phraseToRegex(phrase);
if (regex.test(input)) {
templateScore = 0.8;
break;
}
}
}
let exactNameBonus = 0;
const primaryKeyword = keywordEntries.find(([, weight]) => weight >= 1.0);
if (primaryKeyword) {
const nameRegex = new RegExp(
'\\b' + primaryKeyword[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b',
'i');
if (nameRegex.test(input)) {
exactNameBonus = 0.2;
}
}
return Math.min(
1.0,
Math.max(normalizedKeywordScore, templateScore) + exactNameBonus
);
}
function phraseToRegex(phrase) {
let pattern = phrase
.replace(/\{([^}]+)\}\?/g, '(?:$1\\s*)?')
.replace(/\{([^}]+)\}/g, '(?:$1)')
.replace(/\s+/g, '\\s+');
return new RegExp(pattern, 'i');
}
7.3 Command Route Mapping
Natural language phrases mapped to Legion commands. Intent flags are matched via nl_patterns in intent-teams.yaml; direct command routes are matched via command_routes.
| Pattern Category | Example Phrases | Routes To |
|---|
| Start/initialize | "start a new project", "initialize", "set up" | /legion:start |
| Plan/decompose | "plan the next phase", "break down phase 3" | /legion:plan |
| Build/execute | "build it", "execute the plans", "run the build" | /legion:build |
| Review/test | "review the code", "run tests", "check quality" | /legion:review |
| Status/progress | "show progress", "where are we", "what's next" | /legion:status |
| Quick task | "quick fix", "run a one-off task" | /legion:quick |
| Advise/consult | "get advice on", "ask about", "consult on" | /legion:advise |
| Map/codebase analysis | "map the codebase", "analyze this repo", "refresh codebase map", "generate semantic index" | /legion:map |
| Explore/design research | "explore options", "research this idea", "design discovery", "compare approaches" | /legion:explore |
| Harden | "harden security", "security audit", "fix vulnerabilities" | /legion:build --just-harden |
| Document | "write docs", "generate documentation" | /legion:build --just-document |
Cross-command detection: When NL parsing within one command (e.g., /legion:build) matches a different command (e.g., /legion:review), the system suggests the correct command rather than silently redirecting:
You ran /legion:build, but your input "review the code" matches /legion:review.
Did you mean to run /legion:review instead?
7.4 Confidence Thresholds and Fallback
Three confidence tiers determine routing behavior:
| Level | Range | Behavior | Return Shape |
|---|
| HIGH | >= 0.8 | Auto-route to matched command | { command, flags, confidence, fallbackSuggestion: null } |
| MEDIUM | 0.5 - 0.79 | Suggest command, ask user to confirm | { command, flags, confidence, fallbackSuggestion: "<matched command>" } |
| LOW | < 0.5 | Show top 3 suggestions, ask user to pick | { command: null, flags: [], confidence, fallbackSuggestion: "Did you mean: 1. ... 2. ... 3. ..." } |
Caller behavior by confidence level:
function handleNLResult(result, currentCommand) {
if (result.confidence >= 0.8) {
if (result.command !== currentCommand) {
console.log(`Your input matches ${result.command}. Did you mean to run that instead?`);
return;
}
return executeWithFlags(result.flags);
}
if (result.confidence >= 0.5) {
const confirmed = adapter.ask_user(
`Did you mean: ${result.fallbackSuggestion}?`,
['Yes, proceed', 'No, show other options', 'Cancel']
);
if (confirmed === 'Yes, proceed') {
return executeWithFlags(result.flags);
}
}
console.log(result.fallbackSuggestion);
}
Edge cases:
- Empty input: return
{ command: null, flags: [], confidence: 0, fallbackSuggestion: null } (no-op)
- Input is an exact flag (e.g., "--just-harden"): defer to
parseIntentFlags() in Section 1 (NL parsing is only for non-flag text)
- Multiple high-confidence matches with equal scores: prefer intent-flag matches over command routes (more specific)
7.5 loadNLPatterns()
Load natural language patterns from the nl_patterns and command_routes sections of intent-teams.yaml.
function loadNLPatterns() {
const config = loadIntentTeams();
return {
nlPatterns: config.nl_patterns || {},
commandRoutes: config.command_routes || {}
};
}
Caching: loadNLPatterns() uses the same loadIntentTeams() function from Section 3, which reads and parses intent-teams.yaml once per session. No additional file I/O is needed if the config is already loaded.
Graceful degradation: If nl_patterns or command_routes sections are missing from the config file (e.g., older version of intent-teams.yaml), the function returns empty objects and parseNaturalLanguage() will return a LOW confidence result with no suggestions. The system falls back to standard flag parsing via Section 1.
Section 8: Context-Aware Suggestions
Detect project lifecycle position from STATE.md and return ranked next-action suggestions. Used by /legion:status to show proactive recommendations, and by the NL parser (Section 7) to augment low-confidence fallbacks.
8.1 getContextSuggestions(statePath)
function getContextSuggestions(statePath = '.planning/STATE.md') {
try {
const stateData = parseStateMd(statePath);
const position = detectLifecyclePosition(stateData);
const suggestions = mapPositionToSuggestions(position, stateData);
return {
currentPosition: {
phase: stateData.phase || null,
status: stateData.status || 'unknown',
lastActivity: stateData.lastActivity || null
},
suggestions,
rawState: stateData
};
} catch (error) {
return {
currentPosition: { phase: null, status: 'unknown', lastActivity: null },
suggestions: [
{ command: '/legion:start', description: 'Initialize a new project', priority: 1, reason: 'Unable to read project state' },
{ command: '/legion:status', description: 'Check project status', priority: 2, reason: 'Retry after resolving state issues' }
],
rawState: {}
};
}
}
parseStateMd(statePath)
function parseStateMd(statePath) {
if (!fs.existsSync(statePath)) {
return { exists: false };
}
const content = fs.readFileSync(statePath, 'utf8');
const lines = content.split('\n');
const result = { exists: true };
for (const line of lines) {
const phaseMatch = line.match(/Phase:\s*(\d+)\s*of\s*(\d+)/i);
if (phaseMatch) {
result.phase = parseInt(phaseMatch[1], 10);
result.totalPhases = parseInt(phaseMatch[2], 10);
}
const statusMatch = line.match(/^Status:\s*(.+)/i);
if (statusMatch) {
result.status = statusMatch[1].trim().toLowerCase();
}
const activityMatch = line.match(/^Last Activity:\s*(.+)/i);
if (activityMatch) {
result.lastActivity = activityMatch[1].trim();
}
const nextMatch = line.match(/^Next Action:\s*(.+)/i);
if (nextMatch) {
result.nextAction = nextMatch[1].trim();
}
}
return result;
}
8.2 Project Lifecycle Position Detection
Parse STATE.md to determine where the project sits in its lifecycle. The position drives which suggestions are shown.
function detectLifecyclePosition(stateData) {
if (!stateData.exists) {
return 'no_project';
}
const status = (stateData.status || '').toLowerCase();
const phase = stateData.phase || 0;
const totalPhases = stateData.totalPhases || 0;
if (phase === 1 && status.includes('initialized')) {
return 'just_started';
}
if (phase === totalPhases && status.includes('complete')) {
return 'milestone_complete';
}
if (status.includes('executing') || status.includes('in progress')) {
return 'building';
}
if (status.includes('planned')) {
return 'planned_not_built';
}
if (status.includes('pending review') || status.includes('built') || status.includes('executed')) {
return 'needs_review';
}
if (status.includes('reviewing')) {
return 'review_in_progress';
}
if (status.includes('needs work') || status.includes('rework')) {
return 'review_failed';
}
if (status.includes('complete')) {
return 'phase_complete';
}
return 'unknown';
}
Lifecycle Position Reference:
| Position | Detection Rule | Description |
|---|
no_project | No STATE.md or no phase info | Project not initialized |
just_started | Phase 1, status contains "initialized" | Just ran /legion:start |
planned_not_built | Current phase status contains "planned" | Plans exist, need execution |
building | Current phase status contains "executing" or "in progress" | Build in progress |
needs_review | Current phase status contains "pending review", "built", or "executed" | Build done, needs review (checked before "complete" to avoid false match) |
review_in_progress | Current phase status contains "reviewing" | Review cycle active |
review_failed | Current phase status contains "needs work" or "rework" | Review found issues |
phase_complete | Current phase status contains "complete" (not milestone) | Phase done, covers "needs planning" scenario too |
milestone_complete | All phases complete (phase == totalPhases) | Ready for milestone wrap-up |
8.3 Position-to-Suggestion Mapping
Map each lifecycle position to 2-3 ranked action suggestions. Suggestions are loaded from the context_rules section of intent-teams.yaml and interpolated with runtime state data.
function mapPositionToSuggestions(position, stateData) {
const rules = loadContextRules();
const positionRules = rules[position];
if (!positionRules || !positionRules.suggestions) {
return [
{ command: '/legion:status', description: 'Check project status', priority: 1, reason: 'Default orientation action' },
{ command: '/legion:start', description: 'Initialize a project', priority: 2, reason: 'Start here if no project exists' }
];
}
return positionRules.suggestions.map((suggestion, index) => ({
command: suggestion.command,
description: interpolate(suggestion.description, stateData),
priority: index + 1,
reason: interpolate(suggestion.reason, stateData)
}));
}
function loadContextRules() {
const config = loadIntentTeams();
return config.context_rules || {};
}
function interpolate(template, stateData) {
if (!template) return '';
return template
.replace(/\{phase\}/g, stateData.phase || '?')
.replace(/\{phase_name\}/g, stateData.phaseName || 'current phase')
.replace(/\{next_phase\}/g, (stateData.phase || 0) + 1);
}
8.4 Graceful Degradation
Context-aware suggestions must never cause errors or block the status dashboard. The system degrades through multiple levels:
| Failure Mode | Behavior | Result |
|---|
| STATE.md does not exist | Return position: 'no_project' | Suggests /legion:start |
| STATE.md is malformed (no parseable fields) | Return position: 'unknown' | Generic safe defaults |
intent-teams.yaml missing context_rules | Use hardcoded fallback suggestions | /legion:status + /legion:start |
| Position detected but no matching rule | Use default suggestions | /legion:status + /legion:start |
| Any unexpected error | Catch in getContextSuggestions() | Return valid empty-state response |
Key invariant: getContextSuggestions() always returns a valid object with a non-empty suggestions array. Callers never need to null-check the response.
const result = getContextSuggestions('/nonexistent/STATE.md');
8.5 Integration with NL Parser
When parseNaturalLanguage() (Section 7) returns LOW confidence (< 0.5), augment the fallback suggestions with context-aware suggestions. This provides users with relevant next actions even when their input is ambiguous.
function augmentWithContextSuggestions(nlResult) {
if (nlResult.confidence >= 0.5) {
return nlResult;
}
const contextResult = getContextSuggestions();
if (contextResult.suggestions.length === 0) {
return nlResult;
}
const contextLines = contextResult.suggestions
.slice(0, 3)
.map((s, i) => `${i + 1}. ${s.command} — ${s.description}`)
.join(' ');
const position = contextResult.currentPosition;
const positionLabel = position.phase
? `Phase ${position.phase} (${position.status})`
: 'no active project';
const originalSuggestion = nlResult.fallbackSuggestion || '';
const contextBlock = `\n\nBased on your current position (${positionLabel}):\n${contextLines}`;
return {
...nlResult,
fallbackSuggestion: originalSuggestion + contextBlock,
contextSuggestions: contextResult.suggestions
};
}
Usage in NL parser flow (Section 7.4):
function handleNLResult(result, currentCommand) {
const enhanced = augmentWithContextSuggestions(result);
if (enhanced.confidence >= 0.8) {
return executeWithFlags(enhanced.flags);
}
if (enhanced.confidence >= 0.5) {
return confirmAndExecute(enhanced);
}
console.log(enhanced.fallbackSuggestion);
}
Appendix: Intent Reference
| Intent | Mode | Primary Agents | Use Case |
|---|
| harden | ad_hoc | testing-qa-verification-specialist, engineering-security-engineer | Security audit |
| document | filter_plans | product-technical-writer | Generate docs only |
| skip-frontend | filter_plans | n/a | Exclude UI tasks |
| skip-backend | filter_plans | n/a | Exclude API tasks |
| security-only | filter_review | engineering-security-engineer | Security review |
See Also
.planning/config/intent-teams.yaml — Team template registry
skills/agent-registry/SKILL.md — Agent resolution
skills/wave-executor/SKILL.md — Plan execution
skills/review-panel/SKILL.md — Review composition