Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
at main 189 lines 6.7 kB view raw
1/** 2 * Bot Router — parses commands and routes to the right handler. 3 * 4 * Command format: BotName: action arg1=val1 arg2=val2 5 * No fuzzy NLU in v1 — strict grammar only. 6 */ 7 8import type { BotCommand, BotResponse, BotName } from './models/bot.js'; 9import { sha256 } from './semhash.js'; 10 11const VALID_BOTS = new Set<BotName>(['SpecBot', 'ImplBot', 'PolicyBot']); 12 13const BOT_COMMANDS: Record<BotName, Record<string, { mutating: boolean; description: string }>> = { 14 SpecBot: { 15 ingest: { mutating: true, description: 'Ingest a spec document' }, 16 diff: { mutating: false, description: 'Show clause diff for a document' }, 17 clauses: { mutating: false, description: 'List clauses for a document' }, 18 help: { mutating: false, description: 'Show available commands' }, 19 commands: { mutating: false, description: 'List commands' }, 20 version: { mutating: false, description: 'Show version' }, 21 }, 22 ImplBot: { 23 plan: { mutating: true, description: 'Plan IUs from canonical graph' }, 24 regen: { mutating: true, description: 'Regenerate code for an IU' }, 25 drift: { mutating: false, description: 'Check for drift in generated files' }, 26 help: { mutating: false, description: 'Show available commands' }, 27 commands: { mutating: false, description: 'List commands' }, 28 version: { mutating: false, description: 'Show version' }, 29 }, 30 PolicyBot: { 31 status: { mutating: false, description: 'Show trust dashboard' }, 32 evidence: { mutating: false, description: 'Show evidence for an IU' }, 33 cascade: { mutating: false, description: 'Show cascade effects' }, 34 evaluate: { mutating: false, description: 'Evaluate policy for an IU' }, 35 help: { mutating: false, description: 'Show available commands' }, 36 commands: { mutating: false, description: 'List commands' }, 37 version: { mutating: false, description: 'Show version' }, 38 }, 39}; 40 41/** 42 * Parse a raw command string into a BotCommand. 43 */ 44export function parseCommand(raw: string): BotCommand | { error: string } { 45 const trimmed = raw.trim(); 46 47 // Match: BotName: action ...args 48 const match = trimmed.match(/^(\w+):\s+(\w+)\s*(.*)?$/); 49 if (!match) { 50 return { error: `Invalid command format. Expected: BotName: action [args]. Got: "${trimmed}"` }; 51 } 52 53 const botName = match[1] as BotName; 54 const action = match[2]; 55 const argsStr = (match[3] || '').trim(); 56 57 if (!VALID_BOTS.has(botName)) { 58 return { error: `Unknown bot: ${botName}. Valid bots: ${[...VALID_BOTS].join(', ')}` }; 59 } 60 61 const botCommands = BOT_COMMANDS[botName]; 62 if (!botCommands[action]) { 63 return { error: `Unknown command: ${botName}: ${action}. Try: ${botName}: help` }; 64 } 65 66 // Parse key=value args 67 const args: Record<string, string> = {}; 68 if (argsStr) { 69 const parts = argsStr.match(/(\w+)=([^\s]+)/g); 70 if (parts) { 71 for (const part of parts) { 72 const [key, ...val] = part.split('='); 73 args[key] = val.join('='); 74 } 75 } else { 76 // Single positional arg 77 args['_'] = argsStr; 78 } 79 } 80 81 return { bot: botName, action, args, raw: trimmed }; 82} 83 84/** 85 * Route a parsed command and produce a response. 86 * For mutating commands, returns the intent for confirmation. 87 * For read-only commands, executes immediately. 88 */ 89export function routeCommand(cmd: BotCommand): BotResponse { 90 const commandDef = BOT_COMMANDS[cmd.bot]?.[cmd.action]; 91 if (!commandDef) { 92 return { bot: cmd.bot, action: cmd.action, mutating: false, message: `Unknown command: ${cmd.action}` }; 93 } 94 95 // Handle help/commands/version for all bots 96 if (cmd.action === 'help' || cmd.action === 'commands') { 97 const cmds = BOT_COMMANDS[cmd.bot]; 98 const lines = Object.entries(cmds).map(([name, def]) => { 99 const tag = def.mutating ? ' [mutating]' : ''; 100 return ` ${cmd.bot}: ${name}${tag}${def.description}`; 101 }); 102 return { 103 bot: cmd.bot, 104 action: cmd.action, 105 mutating: false, 106 result: Object.keys(cmds), 107 message: `${cmd.bot} commands:\n${lines.join('\n')}`, 108 }; 109 } 110 111 if (cmd.action === 'version') { 112 return { 113 bot: cmd.bot, 114 action: 'version', 115 mutating: false, 116 result: { version: '0.1.0' }, 117 message: `${cmd.bot} v0.1.0 (Phoenix VCS)`, 118 }; 119 } 120 121 // Mutating commands: echo intent, require confirmation 122 if (commandDef.mutating) { 123 const confirmId = sha256(`${cmd.bot}:${cmd.action}:${JSON.stringify(cmd.args)}:${Date.now()}`).slice(0, 12); 124 const intent = describeIntent(cmd); 125 return { 126 bot: cmd.bot, 127 action: cmd.action, 128 mutating: true, 129 confirm_id: confirmId, 130 intent, 131 message: `${cmd.bot} wants to: ${intent}\n\nReply 'ok' or 'phx confirm ${confirmId}' to proceed.`, 132 }; 133 } 134 135 // Read-only commands: execute immediately (return description) 136 return { 137 bot: cmd.bot, 138 action: cmd.action, 139 mutating: false, 140 message: describeReadAction(cmd), 141 }; 142} 143 144function describeIntent(cmd: BotCommand): string { 145 switch (`${cmd.bot}:${cmd.action}`) { 146 case 'SpecBot:ingest': 147 return `Ingest spec document: ${cmd.args['_'] || cmd.args['doc'] || '(no doc specified)'}`; 148 case 'ImplBot:plan': 149 return 'Plan Implementation Units from the current canonical graph'; 150 case 'ImplBot:regen': { 151 const iu = cmd.args['iu'] || cmd.args['_'] || '(all)'; 152 return `Regenerate code for IU: ${iu}`; 153 } 154 default: 155 return `${cmd.bot} ${cmd.action} ${JSON.stringify(cmd.args)}`; 156 } 157} 158 159function describeReadAction(cmd: BotCommand): string { 160 switch (`${cmd.bot}:${cmd.action}`) { 161 case 'SpecBot:diff': 162 return `Showing clause diff for: ${cmd.args['_'] || cmd.args['doc'] || '(current)'}`; 163 case 'SpecBot:clauses': 164 return `Listing clauses for: ${cmd.args['_'] || cmd.args['doc'] || '(all)'}`; 165 case 'ImplBot:drift': 166 return 'Checking drift status for all generated files'; 167 case 'PolicyBot:status': 168 return 'Trust dashboard: showing full phoenix status'; 169 case 'PolicyBot:evidence': 170 return `Evidence for IU: ${cmd.args['iu'] || cmd.args['_'] || '(all)'}`; 171 case 'PolicyBot:cascade': 172 return 'Showing cascade effects from current failures'; 173 case 'PolicyBot:evaluate': 174 return `Policy evaluation for IU: ${cmd.args['iu'] || cmd.args['_'] || '(all)'}`; 175 default: 176 return `${cmd.bot}: ${cmd.action}`; 177 } 178} 179 180/** 181 * Get all available commands across all bots. 182 */ 183export function getAllCommands(): Record<BotName, string[]> { 184 const result: Record<string, string[]> = {}; 185 for (const [bot, cmds] of Object.entries(BOT_COMMANDS)) { 186 result[bot] = Object.keys(cmds); 187 } 188 return result as Record<BotName, string[]>; 189}