Reference implementation for the Phoenix Architecture. Work in progress.
aicoding.leaflet.pub/
ai
coding
crazy
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}