feat(gateway): CommandRegistryService + CommandExecutorService (P8-010)
Adds gateway-side command registry system: - CommandRegistryService with onModuleInit registering 12 core commands - CommandExecutorService routing command:execute socket events - CommandsModule exporting both services - ChatGateway wired: emits commands:manifest on connect, handles command:execute - AppModule and ChatModule updated to import CommandsModule - Unit tests for CommandRegistryService (6 tests, all passing) Closes #163 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import { SkillsModule } from './skills/skills.module.js';
|
|||||||
import { PluginModule } from './plugin/plugin.module.js';
|
import { PluginModule } from './plugin/plugin.module.js';
|
||||||
import { McpModule } from './mcp/mcp.module.js';
|
import { McpModule } from './mcp/mcp.module.js';
|
||||||
import { AdminModule } from './admin/admin.module.js';
|
import { AdminModule } from './admin/admin.module.js';
|
||||||
|
import { CommandsModule } from './commands/commands.module.js';
|
||||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -38,6 +39,7 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|||||||
PluginModule,
|
PluginModule,
|
||||||
McpModule,
|
McpModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
|
CommandsModule,
|
||||||
],
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import {
|
|||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
|
||||||
import type { Auth } from '@mosaic/auth';
|
import type { Auth } from '@mosaic/auth';
|
||||||
import type { SetThinkingPayload } from '@mosaic/types';
|
import type { SetThinkingPayload, SlashCommandPayload } from '@mosaic/types';
|
||||||
import { AgentService } from '../agent/agent.service.js';
|
import { AgentService } from '../agent/agent.service.js';
|
||||||
import { AUTH } from '../auth/auth.tokens.js';
|
import { AUTH } from '../auth/auth.tokens.js';
|
||||||
|
import { CommandRegistryService } from '../commands/command-registry.service.js';
|
||||||
|
import { CommandExecutorService } from '../commands/command-executor.service.js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ChatSocketMessageDto } from './chat.dto.js';
|
import { ChatSocketMessageDto } from './chat.dto.js';
|
||||||
import { validateSocketSession } from './chat.gateway-auth.js';
|
import { validateSocketSession } from './chat.gateway-auth.js';
|
||||||
@@ -38,6 +40,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(AgentService) private readonly agentService: AgentService,
|
@Inject(AgentService) private readonly agentService: AgentService,
|
||||||
@Inject(AUTH) private readonly auth: Auth,
|
@Inject(AUTH) private readonly auth: Auth,
|
||||||
|
@Inject(CommandRegistryService) private readonly commandRegistry: CommandRegistryService,
|
||||||
|
@Inject(CommandExecutorService) private readonly commandExecutor: CommandExecutorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
afterInit(): void {
|
afterInit(): void {
|
||||||
@@ -55,6 +59,9 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
|||||||
client.data.user = session.user;
|
client.data.user = session.user;
|
||||||
client.data.session = session.session;
|
client.data.session = session.session;
|
||||||
this.logger.log(`Client connected: ${client.id}`);
|
this.logger.log(`Client connected: ${client.id}`);
|
||||||
|
|
||||||
|
// Broadcast command manifest to the newly connected client
|
||||||
|
client.emit('commands:manifest', { manifest: this.commandRegistry.getManifest() });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnect(client: Socket): void {
|
handleDisconnect(client: Socket): void {
|
||||||
@@ -184,6 +191,16 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('command:execute')
|
||||||
|
async handleCommandExecute(
|
||||||
|
@ConnectedSocket() client: Socket,
|
||||||
|
@MessageBody() payload: SlashCommandPayload,
|
||||||
|
): Promise<void> {
|
||||||
|
const userId = (client.data.user as { id: string } | undefined)?.id ?? 'unknown';
|
||||||
|
const result = await this.commandExecutor.execute(payload, userId);
|
||||||
|
client.emit('command:result', result);
|
||||||
|
}
|
||||||
|
|
||||||
private relayEvent(client: Socket, conversationId: string, event: AgentSessionEvent): void {
|
private relayEvent(client: Socket, conversationId: string, event: AgentSessionEvent): void {
|
||||||
if (!client.connected) {
|
if (!client.connected) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CommandsModule } from '../commands/commands.module.js';
|
||||||
import { ChatGateway } from './chat.gateway.js';
|
import { ChatGateway } from './chat.gateway.js';
|
||||||
import { ChatController } from './chat.controller.js';
|
import { ChatController } from './chat.controller.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [CommandsModule],
|
||||||
controllers: [ChatController],
|
controllers: [ChatController],
|
||||||
providers: [ChatGateway],
|
providers: [ChatGateway],
|
||||||
})
|
})
|
||||||
|
|||||||
127
apps/gateway/src/commands/command-executor.service.ts
Normal file
127
apps/gateway/src/commands/command-executor.service.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaic/types';
|
||||||
|
import { AgentService } from '../agent/agent.service.js';
|
||||||
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommandExecutorService {
|
||||||
|
private readonly logger = new Logger(CommandExecutorService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(CommandRegistryService) private readonly registry: CommandRegistryService,
|
||||||
|
@Inject(AgentService) private readonly agentService: AgentService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(payload: SlashCommandPayload, _userId: string): Promise<SlashCommandResultPayload> {
|
||||||
|
const { command, args, conversationId } = payload;
|
||||||
|
|
||||||
|
const def = this.registry.getManifest().commands.find((c) => c.name === command);
|
||||||
|
if (!def) {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
conversationId,
|
||||||
|
success: false,
|
||||||
|
message: `Unknown command: /${command}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case 'model':
|
||||||
|
return await this.handleModel(args ?? null, conversationId);
|
||||||
|
case 'thinking':
|
||||||
|
return await this.handleThinking(args ?? null, conversationId);
|
||||||
|
case 'new':
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: 'Start a new conversation by selecting New Conversation.',
|
||||||
|
};
|
||||||
|
case 'clear':
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: 'Conversation display cleared.',
|
||||||
|
};
|
||||||
|
case 'compact':
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: 'Context compaction requested.',
|
||||||
|
};
|
||||||
|
case 'retry':
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: 'Retry last message requested.',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
conversationId,
|
||||||
|
success: false,
|
||||||
|
message: `Command /${command} is not yet implemented.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Command /${command} failed: ${err}`);
|
||||||
|
return { command, conversationId, success: false, message: String(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleModel(
|
||||||
|
args: string | null,
|
||||||
|
conversationId: string,
|
||||||
|
): Promise<SlashCommandResultPayload> {
|
||||||
|
if (!args) {
|
||||||
|
return {
|
||||||
|
command: 'model',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: 'Usage: /model <model-name>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Update agent session model if session is active
|
||||||
|
// For now, acknowledge the request — full wiring done in P8-012
|
||||||
|
const session = this.agentService.getSession(conversationId);
|
||||||
|
if (!session) {
|
||||||
|
return {
|
||||||
|
command: 'model',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: `Model switch to "${args}" requested. No active session for this conversation.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
command: 'model',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: `Model switch to "${args}" requested.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleThinking(
|
||||||
|
args: string | null,
|
||||||
|
conversationId: string,
|
||||||
|
): Promise<SlashCommandResultPayload> {
|
||||||
|
const level = args?.toLowerCase();
|
||||||
|
if (!level || !['none', 'low', 'medium', 'high', 'auto'].includes(level)) {
|
||||||
|
return {
|
||||||
|
command: 'thinking',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: 'Usage: /thinking <none|low|medium|high|auto>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
command: 'thinking',
|
||||||
|
conversationId,
|
||||||
|
success: true,
|
||||||
|
message: `Thinking level set to "${level}".`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
53
apps/gateway/src/commands/command-registry.service.spec.ts
Normal file
53
apps/gateway/src/commands/command-registry.service.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
|
import type { CommandDef } from '@mosaic/types';
|
||||||
|
|
||||||
|
const mockCmd: CommandDef = {
|
||||||
|
name: 'test',
|
||||||
|
description: 'Test command',
|
||||||
|
aliases: ['t'],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CommandRegistryService', () => {
|
||||||
|
let service: CommandRegistryService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new CommandRegistryService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts with empty manifest', () => {
|
||||||
|
expect(service.getManifest().commands).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registers a command', () => {
|
||||||
|
service.registerCommand(mockCmd);
|
||||||
|
expect(service.getManifest().commands).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates existing command by name', () => {
|
||||||
|
service.registerCommand(mockCmd);
|
||||||
|
service.registerCommand({ ...mockCmd, description: 'Updated' });
|
||||||
|
expect(service.getManifest().commands).toHaveLength(1);
|
||||||
|
expect(service.getManifest().commands[0]?.description).toBe('Updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onModuleInit registers core commands', () => {
|
||||||
|
service.onModuleInit();
|
||||||
|
const manifest = service.getManifest();
|
||||||
|
expect(manifest.commands.length).toBeGreaterThan(5);
|
||||||
|
expect(manifest.commands.some((c) => c.name === 'model')).toBe(true);
|
||||||
|
expect(manifest.commands.some((c) => c.name === 'help')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('manifest includes skills array', () => {
|
||||||
|
const manifest = service.getManifest();
|
||||||
|
expect(Array.isArray(manifest.skills)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('manifest version is 1', () => {
|
||||||
|
expect(service.getManifest().version).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
177
apps/gateway/src/commands/command-registry.service.ts
Normal file
177
apps/gateway/src/commands/command-registry.service.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { Injectable, type OnModuleInit } from '@nestjs/common';
|
||||||
|
import type { CommandDef, CommandManifest } from '@mosaic/types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommandRegistryService implements OnModuleInit {
|
||||||
|
private readonly commands: CommandDef[] = [];
|
||||||
|
|
||||||
|
registerCommand(def: CommandDef): void {
|
||||||
|
const existing = this.commands.findIndex((c) => c.name === def.name);
|
||||||
|
if (existing >= 0) {
|
||||||
|
this.commands[existing] = def;
|
||||||
|
} else {
|
||||||
|
this.commands.push(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCommands(defs: CommandDef[]): void {
|
||||||
|
for (const def of defs) {
|
||||||
|
this.registerCommand(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getManifest(): CommandManifest {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
commands: [...this.commands],
|
||||||
|
skills: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit(): void {
|
||||||
|
this.registerCommands([
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
description: 'Switch the active model',
|
||||||
|
aliases: ['m'],
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'model-name',
|
||||||
|
type: 'string',
|
||||||
|
optional: false,
|
||||||
|
description: 'Model name to switch to',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'thinking',
|
||||||
|
description: 'Set thinking level (none/low/medium/high/auto)',
|
||||||
|
aliases: ['t'],
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'level',
|
||||||
|
type: 'enum',
|
||||||
|
optional: false,
|
||||||
|
values: ['none', 'low', 'medium', 'high', 'auto'],
|
||||||
|
description: 'Thinking level',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'new',
|
||||||
|
description: 'Start a new conversation',
|
||||||
|
aliases: ['n'],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'clear',
|
||||||
|
description: 'Clear conversation context and GC session artifacts',
|
||||||
|
aliases: [],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'compact',
|
||||||
|
description: 'Request context compaction',
|
||||||
|
aliases: [],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'retry',
|
||||||
|
description: 'Retry the last message',
|
||||||
|
aliases: [],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'socket',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rename',
|
||||||
|
description: 'Rename current conversation',
|
||||||
|
aliases: [],
|
||||||
|
args: [
|
||||||
|
{ name: 'name', type: 'string', optional: false, description: 'New conversation name' },
|
||||||
|
],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'rest',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'history',
|
||||||
|
description: 'Show conversation history',
|
||||||
|
aliases: [],
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'limit',
|
||||||
|
type: 'string',
|
||||||
|
optional: true,
|
||||||
|
description: 'Number of messages to show',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'rest',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'export',
|
||||||
|
description: 'Export conversation to markdown or JSON',
|
||||||
|
aliases: [],
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'format',
|
||||||
|
type: 'enum',
|
||||||
|
optional: true,
|
||||||
|
values: ['md', 'json'],
|
||||||
|
description: 'Export format',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'rest',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preferences',
|
||||||
|
description: 'View or set user preferences',
|
||||||
|
aliases: ['pref'],
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'action',
|
||||||
|
type: 'enum',
|
||||||
|
optional: true,
|
||||||
|
values: ['show', 'set', 'reset'],
|
||||||
|
description: 'Action to perform',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'rest',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
description: 'Show session and connection status',
|
||||||
|
aliases: ['s'],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'hybrid',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'help',
|
||||||
|
description: 'Show available commands',
|
||||||
|
aliases: ['h'],
|
||||||
|
scope: 'core',
|
||||||
|
execution: 'local',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
apps/gateway/src/commands/commands.module.ts
Normal file
9
apps/gateway/src/commands/commands.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CommandRegistryService } from './command-registry.service.js';
|
||||||
|
import { CommandExecutorService } from './command-executor.service.js';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [CommandRegistryService, CommandExecutorService],
|
||||||
|
exports: [CommandRegistryService, CommandExecutorService],
|
||||||
|
})
|
||||||
|
export class CommandsModule {}
|
||||||
72
docs/scratchpads/p8-010-command-registry.md
Normal file
72
docs/scratchpads/p8-010-command-registry.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# P8-010 Scratchpad — Gateway Phase 2: CommandRegistryService + CommandExecutorService
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement gateway-side command registry system:
|
||||||
|
|
||||||
|
- `CommandRegistryService` — owns canonical command manifest, broadcasts on connect
|
||||||
|
- `CommandExecutorService` — routes `command:execute` socket events
|
||||||
|
- `CommandsModule` — NestJS wiring
|
||||||
|
- Wire into `ChatGateway` and `AppModule`
|
||||||
|
- Register core commands
|
||||||
|
- Tests for CommandRegistryService
|
||||||
|
|
||||||
|
## Key Findings from Codebase
|
||||||
|
|
||||||
|
### CommandDef shape (from packages/types/src/commands/index.ts)
|
||||||
|
|
||||||
|
- `scope: 'core' | 'agent' | 'skill' | 'plugin' | 'admin'` (NOT `category`)
|
||||||
|
- `args?: CommandArgDef[]` — array of arg defs, each with `name`, `type`, `optional`, `values?`, `description?`
|
||||||
|
- No `aliases` required (it's listed but optional-ish... wait, it IS in the interface)
|
||||||
|
- `aliases: string[]` — IS present
|
||||||
|
|
||||||
|
### SlashCommandResultPayload requires `conversationId`
|
||||||
|
|
||||||
|
- The task spec shows `{ command, success, error }` without `conversationId` but actual type requires it
|
||||||
|
- Must include `conversationId` in all return values
|
||||||
|
|
||||||
|
### CommandManifest has `skills: SkillCommandDef[]`
|
||||||
|
|
||||||
|
- Must include `skills` array in manifest
|
||||||
|
|
||||||
|
### userId extraction in ChatGateway
|
||||||
|
|
||||||
|
- `client.data.user` holds the user object (set in `handleConnection`)
|
||||||
|
- `client.data.user.id` or similar for userId
|
||||||
|
|
||||||
|
### AgentModule not imported in ChatModule
|
||||||
|
|
||||||
|
- ChatGateway imports AgentService via DI
|
||||||
|
- ChatModule doesn't declare imports — AgentModule must be global or imported
|
||||||
|
|
||||||
|
### Worktree branch
|
||||||
|
|
||||||
|
- Branch: `feat/p8-010-command-registry`
|
||||||
|
- Working in: `/home/jwoltje/src/mosaic-mono-v1/.claude/worktrees/agent-ac85b3b2`
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Create `apps/gateway/src/commands/command-registry.service.ts`
|
||||||
|
2. Create `apps/gateway/src/commands/command-executor.service.ts`
|
||||||
|
3. Create `apps/gateway/src/commands/commands.module.ts`
|
||||||
|
4. Modify `apps/gateway/src/app.module.ts` — add CommandsModule
|
||||||
|
5. Modify `apps/gateway/src/chat/chat.module.ts` — import CommandsModule
|
||||||
|
6. Modify `apps/gateway/src/chat/chat.gateway.ts` — inject services, add handler, emit manifest
|
||||||
|
7. Create `apps/gateway/src/commands/command-registry.service.spec.ts`
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
- [ ] Create CommandRegistryService
|
||||||
|
- [ ] Create CommandExecutorService
|
||||||
|
- [ ] Create CommandsModule
|
||||||
|
- [ ] Update AppModule
|
||||||
|
- [ ] Update ChatModule
|
||||||
|
- [ ] Update ChatGateway
|
||||||
|
- [ ] Write tests
|
||||||
|
- [ ] Run quality gates
|
||||||
|
- [ ] Commit + push + PR
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- CommandDef `args` shape mismatch from task spec — must use actual type
|
||||||
|
- `SlashCommandResultPayload.conversationId` is required — handle missing conversationId
|
||||||
Reference in New Issue
Block a user