feat: Add intelligent auto-router and enhanced integrations

- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
/**
* Todo Service Error Codes
*/
export enum TodoErrorCode {
// Service lifecycle errors
SERVICE_NOT_INITIALIZED = 'TODO_SERVICE_NOT_INITIALIZED',
// Todo management errors
TODO_LIMIT_EXCEEDED = 'TODO_LIMIT_EXCEEDED',
INVALID_TODO_STATUS = 'TODO_INVALID_TODO_STATUS',
// Database errors
DATABASE_ERROR = 'TODO_DATABASE_ERROR',
}

View File

@@ -0,0 +1,77 @@
/**
* Todo Service Errors
*
* Error factory for todo list management operations
*/
import { DextoRuntimeError, ErrorScope, ErrorType } from '@dexto/core';
import { TodoErrorCode } from './error-codes.js';
/**
* Error scope for todo-related errors.
* Uses a custom string since todo is a custom tool, not part of core.
*/
const TODO_ERROR_SCOPE = 'todo';
/**
* Factory class for creating Todo-related errors
*/
export class TodoError {
private constructor() {
// Private constructor prevents instantiation
}
/**
* Service not initialized error
*/
static notInitialized(): DextoRuntimeError {
return new DextoRuntimeError(
TodoErrorCode.SERVICE_NOT_INITIALIZED,
TODO_ERROR_SCOPE,
ErrorType.SYSTEM,
'TodoService has not been initialized',
{},
'Initialize the TodoService before using it'
);
}
/**
* Todo limit exceeded error
*/
static todoLimitExceeded(current: number, max: number): DextoRuntimeError {
return new DextoRuntimeError(
TodoErrorCode.TODO_LIMIT_EXCEEDED,
TODO_ERROR_SCOPE,
ErrorType.USER,
`Todo limit exceeded: ${current} todos. Maximum allowed: ${max}`,
{ current, max },
'Complete or delete existing todos before adding new ones'
);
}
/**
* Invalid todo status error
*/
static invalidStatus(status: string): DextoRuntimeError {
return new DextoRuntimeError(
TodoErrorCode.INVALID_TODO_STATUS,
TODO_ERROR_SCOPE,
ErrorType.USER,
`Invalid todo status: ${status}. Must be 'pending', 'in_progress', or 'completed'`,
{ status }
);
}
/**
* Database error
*/
static databaseError(operation: string, cause: string): DextoRuntimeError {
return new DextoRuntimeError(
TodoErrorCode.DATABASE_ERROR,
TODO_ERROR_SCOPE,
ErrorType.SYSTEM,
`Database error during ${operation}: ${cause}`,
{ operation, cause }
);
}
}

View File

@@ -0,0 +1,21 @@
/**
* @dexto/tools-todo
*
* Todo/task tracking tools provider for Dexto agents.
* Provides the todo_write tool for managing task lists.
*/
// Main provider export
export { todoToolsProvider } from './tool-provider.js';
// Service and utilities (for advanced use cases)
export { TodoService } from './todo-service.js';
export { TodoError } from './errors.js';
export { TodoErrorCode } from './error-codes.js';
// Types
export type { Todo, TodoInput, TodoStatus, TodoUpdateResult, TodoConfig } from './types.js';
export { TODO_STATUS_VALUES } from './types.js';
// Tool implementations (for custom integrations)
export { createTodoWriteTool } from './todo-write-tool.js';

View File

@@ -0,0 +1,226 @@
/**
* TodoService Unit Tests
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TodoService } from './todo-service.js';
import type { Database, AgentEventBus, IDextoLogger } from '@dexto/core';
import type { TodoInput } from './types.js';
// Mock database
function createMockDatabase(): Database {
const store = new Map<string, unknown>();
return {
get: vi.fn().mockImplementation(async (key: string) => store.get(key)),
set: vi.fn().mockImplementation(async (key: string, value: unknown) => {
store.set(key, value);
}),
delete: vi.fn().mockImplementation(async (key: string) => {
store.delete(key);
}),
list: vi.fn().mockResolvedValue([]),
append: vi.fn().mockResolvedValue(undefined),
getRange: vi.fn().mockResolvedValue([]),
connect: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn().mockResolvedValue(undefined),
isConnected: vi.fn().mockReturnValue(true),
getStoreType: vi.fn().mockReturnValue('mock'),
} as Database;
}
// Mock event bus
function createMockEventBus(): AgentEventBus {
return {
emit: vi.fn(),
on: vi.fn().mockReturnThis(),
once: vi.fn().mockReturnThis(),
off: vi.fn().mockReturnThis(),
} as unknown as AgentEventBus;
}
// Mock logger
function createMockLogger(): IDextoLogger {
return {
debug: vi.fn(),
silly: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
trackException: vi.fn(),
createChild: vi.fn(() => createMockLogger()),
setLevel: vi.fn(),
getLevel: vi.fn().mockReturnValue('info'),
getLogFilePath: vi.fn().mockReturnValue(null),
destroy: vi.fn().mockResolvedValue(undefined),
} as unknown as IDextoLogger;
}
describe('TodoService', () => {
let service: TodoService;
let mockDb: Database;
let mockEventBus: AgentEventBus;
let mockLogger: IDextoLogger;
beforeEach(async () => {
mockDb = createMockDatabase();
mockEventBus = createMockEventBus();
mockLogger = createMockLogger();
service = new TodoService(mockDb, mockEventBus, mockLogger);
await service.initialize();
});
describe('initialize', () => {
it('should initialize successfully', async () => {
const newService = new TodoService(mockDb, mockEventBus, mockLogger);
await expect(newService.initialize()).resolves.not.toThrow();
});
it('should be idempotent', async () => {
await expect(service.initialize()).resolves.not.toThrow();
});
});
describe('updateTodos', () => {
const sessionId = 'test-session';
it('should create new todos', async () => {
const todoInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
{ content: 'Task 2', activeForm: 'Working on Task 2', status: 'in_progress' },
];
const result = await service.updateTodos(sessionId, todoInputs);
expect(result.todos).toHaveLength(2);
expect(result.created).toBe(2);
expect(result.updated).toBe(0);
expect(result.deleted).toBe(0);
expect(result.todos[0]?.content).toBe('Task 1');
expect(result.todos[1]?.content).toBe('Task 2');
});
it('should preserve existing todo IDs when updating', async () => {
// Create initial todos
const initialInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
];
const initialResult = await service.updateTodos(sessionId, initialInputs);
const originalId = initialResult.todos[0]?.id;
// Update with same content but different status
const updatedInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'completed' },
];
const updatedResult = await service.updateTodos(sessionId, updatedInputs);
expect(updatedResult.todos[0]?.id).toBe(originalId);
expect(updatedResult.todos[0]?.status).toBe('completed');
expect(updatedResult.updated).toBe(1);
expect(updatedResult.created).toBe(0);
});
it('should track deleted todos', async () => {
// Create initial todos
const initialInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
{ content: 'Task 2', activeForm: 'Working on Task 2', status: 'pending' },
];
await service.updateTodos(sessionId, initialInputs);
// Update with only one todo
const updatedInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'completed' },
];
const result = await service.updateTodos(sessionId, updatedInputs);
expect(result.todos).toHaveLength(1);
expect(result.deleted).toBe(1);
});
it('should emit service:event when todos are updated', async () => {
const todoInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
];
await service.updateTodos(sessionId, todoInputs);
expect(mockEventBus.emit).toHaveBeenCalledWith('service:event', {
service: 'todo',
event: 'updated',
sessionId,
data: expect.objectContaining({
todos: expect.any(Array),
stats: expect.objectContaining({
created: 1,
updated: 0,
deleted: 0,
}),
}),
});
});
it('should respect maxTodosPerSession limit', async () => {
const limitedService = new TodoService(mockDb, mockEventBus, mockLogger, {
maxTodosPerSession: 2,
});
await limitedService.initialize();
const todoInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
{ content: 'Task 2', activeForm: 'Working on Task 2', status: 'pending' },
{ content: 'Task 3', activeForm: 'Working on Task 3', status: 'pending' },
];
await expect(limitedService.updateTodos(sessionId, todoInputs)).rejects.toThrow(
/Todo limit exceeded/
);
});
it('should not emit events when enableEvents is false', async () => {
const silentService = new TodoService(mockDb, mockEventBus, mockLogger, {
enableEvents: false,
});
await silentService.initialize();
const todoInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
];
await silentService.updateTodos(sessionId, todoInputs);
expect(mockEventBus.emit).not.toHaveBeenCalled();
});
});
describe('getTodos', () => {
const sessionId = 'test-session';
it('should return empty array for new session', async () => {
const todos = await service.getTodos(sessionId);
expect(todos).toEqual([]);
});
it('should return todos after update', async () => {
const todoInputs: TodoInput[] = [
{ content: 'Task 1', activeForm: 'Working on Task 1', status: 'pending' },
];
await service.updateTodos(sessionId, todoInputs);
const todos = await service.getTodos(sessionId);
expect(todos).toHaveLength(1);
expect(todos[0]?.content).toBe('Task 1');
});
});
describe('error handling', () => {
it('should throw if not initialized', async () => {
const uninitService = new TodoService(mockDb, mockEventBus, mockLogger);
await expect(uninitService.getTodos('session')).rejects.toThrow(/not been initialized/);
await expect(uninitService.updateTodos('session', [])).rejects.toThrow(
/not been initialized/
);
});
});
});

View File

@@ -0,0 +1,209 @@
/**
* Todo Service
*
* Manages todo lists for tracking agent workflow and task progress.
* Emits events through the AgentEventBus using the service:event pattern.
*/
import { nanoid } from 'nanoid';
import type { Database, AgentEventBus, IDextoLogger } from '@dexto/core';
import { DextoRuntimeError } from '@dexto/core';
import { TodoError } from './errors.js';
import type { Todo, TodoInput, TodoUpdateResult, TodoConfig, TodoStatus } from './types.js';
import { TODO_STATUS_VALUES } from './types.js';
const DEFAULT_MAX_TODOS = 100;
const TODOS_KEY_PREFIX = 'todos:';
/**
* TodoService - Manages todo lists for agent workflow tracking
*/
export class TodoService {
private database: Database;
private eventBus: AgentEventBus;
private logger: IDextoLogger;
private config: Required<TodoConfig>;
private initialized: boolean = false;
constructor(
database: Database,
eventBus: AgentEventBus,
logger: IDextoLogger,
config: TodoConfig = {}
) {
this.database = database;
this.eventBus = eventBus;
this.logger = logger;
this.config = {
maxTodosPerSession: config.maxTodosPerSession ?? DEFAULT_MAX_TODOS,
enableEvents: config.enableEvents ?? true,
};
}
/**
* Initialize the service
*/
async initialize(): Promise<void> {
if (this.initialized) {
this.logger.debug('TodoService already initialized');
return;
}
this.initialized = true;
this.logger.info('TodoService initialized successfully');
}
/**
* Update todos for a session (replaces entire list)
*/
async updateTodos(sessionId: string, todoInputs: TodoInput[]): Promise<TodoUpdateResult> {
if (!this.initialized) {
throw TodoError.notInitialized();
}
// Validate todo count
if (todoInputs.length > this.config.maxTodosPerSession) {
throw TodoError.todoLimitExceeded(todoInputs.length, this.config.maxTodosPerSession);
}
try {
// Get existing todos
const existing = await this.getTodos(sessionId);
const existingMap = new Map(existing.map((t) => [this.getTodoKey(t), t]));
// Create new todos with IDs
const now = new Date();
const newTodos: Todo[] = [];
const stats = { created: 0, updated: 0, deleted: 0 };
for (let i = 0; i < todoInputs.length; i++) {
const input = todoInputs[i]!;
this.validateTodoStatus(input.status);
// Generate consistent key for matching
const todoKey = this.getTodoKeyFromInput(input);
const existingTodo = existingMap.get(todoKey);
if (existingTodo) {
// Update existing todo
const updated: Todo = {
...existingTodo,
status: input.status,
updatedAt: now,
position: i,
};
newTodos.push(updated);
stats.updated++;
existingMap.delete(todoKey);
} else {
// Create new todo
const created: Todo = {
id: nanoid(),
sessionId,
content: input.content,
activeForm: input.activeForm,
status: input.status,
position: i,
createdAt: now,
updatedAt: now,
};
newTodos.push(created);
stats.created++;
}
}
// Remaining items in existingMap are deleted
stats.deleted = existingMap.size;
// Save to database
const key = this.getTodosDatabaseKey(sessionId);
await this.database.set(key, newTodos);
// Emit event using the service:event pattern
if (this.config.enableEvents) {
this.eventBus.emit('service:event', {
service: 'todo',
event: 'updated',
sessionId,
data: {
todos: newTodos,
stats,
},
});
}
this.logger.debug(
`Updated todos for session ${sessionId}: ${stats.created} created, ${stats.updated} updated, ${stats.deleted} deleted`
);
return {
todos: newTodos,
sessionId,
...stats,
};
} catch (error) {
if (error instanceof DextoRuntimeError) {
throw error;
}
throw TodoError.databaseError(
'updateTodos',
error instanceof Error ? error.message : String(error)
);
}
}
/**
* Get todos for a session
*/
async getTodos(sessionId: string): Promise<Todo[]> {
if (!this.initialized) {
throw TodoError.notInitialized();
}
try {
const key = this.getTodosDatabaseKey(sessionId);
const todos = await this.database.get<Todo[]>(key);
return todos || [];
} catch (error) {
if (error instanceof DextoRuntimeError) {
throw error;
}
throw TodoError.databaseError(
'getTodos',
error instanceof Error ? error.message : String(error)
);
}
}
/**
* Generate database key for session todos
*/
private getTodosDatabaseKey(sessionId: string): string {
return `${TODOS_KEY_PREFIX}${sessionId}`;
}
/**
* Generate consistent key for todo matching (content + activeForm)
* Uses JSON encoding to prevent collisions when fields contain delimiters
*/
private getTodoKey(todo: Todo | TodoInput): string {
return JSON.stringify([todo.content, todo.activeForm]);
}
/**
* Generate key from TodoInput
* Uses JSON encoding to prevent collisions when fields contain delimiters
*/
private getTodoKeyFromInput(input: TodoInput): string {
return JSON.stringify([input.content, input.activeForm]);
}
/**
* Validate todo status
*/
private validateTodoStatus(status: TodoStatus): void {
if (!TODO_STATUS_VALUES.includes(status)) {
throw TodoError.invalidStatus(status);
}
}
}

View File

@@ -0,0 +1,95 @@
/**
* Todo Write Tool
*
* Manages task lists for tracking agent progress and workflow organization
*/
import { z } from 'zod';
import type { InternalTool, ToolExecutionContext } from '@dexto/core';
import type { TodoService } from './todo-service.js';
import { TODO_STATUS_VALUES } from './types.js';
/**
* Zod schema for todo item input
*/
const TodoItemSchema = z
.object({
content: z
.string()
.min(1)
.describe('Task description in imperative form (e.g., "Fix authentication bug")'),
activeForm: z
.string()
.min(1)
.describe(
'Present continuous form shown during execution (e.g., "Fixing authentication bug")'
),
status: z
.enum(TODO_STATUS_VALUES)
.describe(
'Task status: pending (not started), in_progress (currently working), completed (finished)'
),
})
.strict();
/**
* Zod schema for todo_write tool input
*/
const TodoWriteInputSchema = z
.object({
todos: z
.array(TodoItemSchema)
.min(1)
.describe('Array of todo items representing the complete task list for the session'),
})
.strict()
.superRefine((value, ctx) => {
const inProgressCount = value.todos.filter((todo) => todo.status === 'in_progress').length;
if (inProgressCount > 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Only one todo may be in_progress at a time.',
path: ['todos'],
});
}
})
.describe(
'Manage task list for current session. Replaces the entire todo list with the provided tasks.'
);
/**
* Create todo_write internal tool
*/
export function createTodoWriteTool(todoService: TodoService): InternalTool {
return {
id: 'todo_write',
description: `Track progress on multi-step tasks. Use for:
- Implementation tasks with 3+ steps (features, refactors, bug fixes)
- Tasks where the user asks for a plan or breakdown
- Complex workflows where progress visibility helps
Do NOT use for simple single-file edits, quick questions, or explanations.
IMPORTANT: This replaces the entire todo list. Always include ALL tasks (pending, in_progress, completed). Only ONE task should be in_progress at a time. Update status as you work: pending → in_progress → completed.`,
inputSchema: TodoWriteInputSchema,
execute: async (input: unknown, context?: ToolExecutionContext): Promise<unknown> => {
// Validate input against schema
const validatedInput = TodoWriteInputSchema.parse(input);
// Use session_id from context, otherwise default
const sessionId = context?.sessionId ?? 'default';
// Update todos in todo service
const result = await todoService.updateTodos(sessionId, validatedInput.todos);
// Count by status for summary
const completed = result.todos.filter((t) => t.status === 'completed').length;
const inProgress = result.todos.filter((t) => t.status === 'in_progress').length;
const pending = result.todos.filter((t) => t.status === 'pending').length;
// Return simple summary - TodoPanel shows full state
return `Updated tasks: ${completed}/${result.todos.length} completed${inProgress > 0 ? `, 1 in progress` : ''}${pending > 0 ? `, ${pending} pending` : ''}`;
},
};
}

View File

@@ -0,0 +1,92 @@
/**
* Todo Tools Provider
*
* Provides task tracking tools by wrapping TodoService.
* When registered, the provider initializes TodoService and creates the
* todo_write tool for managing task lists.
*/
import { z } from 'zod';
import type { CustomToolProvider, ToolCreationContext, InternalTool } from '@dexto/core';
import { TodoService } from './todo-service.js';
import { createTodoWriteTool } from './todo-write-tool.js';
/**
* Default configuration constants for Todo tools.
*/
const DEFAULT_MAX_TODOS_PER_SESSION = 100;
const DEFAULT_ENABLE_EVENTS = true;
/**
* Configuration schema for Todo tools provider.
*/
const TodoToolsConfigSchema = z
.object({
type: z.literal('todo-tools'),
maxTodosPerSession: z
.number()
.int()
.positive()
.default(DEFAULT_MAX_TODOS_PER_SESSION)
.describe(`Maximum todos per session (default: ${DEFAULT_MAX_TODOS_PER_SESSION})`),
enableEvents: z
.boolean()
.default(DEFAULT_ENABLE_EVENTS)
.describe('Enable real-time events for todo updates'),
})
.strict();
type TodoToolsConfig = z.output<typeof TodoToolsConfigSchema>;
/**
* Todo tools provider.
*
* Wraps TodoService and provides the todo_write tool for managing task lists.
*
* When registered via customToolRegistry, TodoService is automatically
* initialized and the todo_write tool becomes available to the agent.
*/
export const todoToolsProvider: CustomToolProvider<'todo-tools', TodoToolsConfig> = {
type: 'todo-tools',
configSchema: TodoToolsConfigSchema,
create: (config: TodoToolsConfig, context: ToolCreationContext): InternalTool[] => {
const { logger, agent, services } = context;
logger.debug('Creating TodoService for todo tools');
// Get required services from context
const storageManager = services?.storageManager;
if (!storageManager) {
throw new Error(
'TodoService requires storageManager service. Ensure it is available in ToolCreationContext.'
);
}
const database = storageManager.getDatabase();
const eventBus = agent.agentEventBus;
// Create TodoService with validated config
const todoService = new TodoService(database, eventBus, logger, {
maxTodosPerSession: config.maxTodosPerSession,
enableEvents: config.enableEvents,
});
// Start initialization in background
todoService.initialize().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
logger.error(`TodoToolsProvider.create: Failed to initialize TodoService: ${message}`);
});
logger.debug('TodoService created - initialization will complete on first tool use');
// Create and return the todo_write tool
return [createTodoWriteTool(todoService)];
},
metadata: {
displayName: 'Todo Tools',
description: 'Task tracking and workflow management (todo_write)',
category: 'workflow',
},
};

View File

@@ -0,0 +1,60 @@
/**
* Todo Service Types
*
* Types for todo list management and workflow tracking
*/
/**
* Valid todo status values
* Centralized constant to prevent duplication across domains
*/
export const TODO_STATUS_VALUES = ['pending', 'in_progress', 'completed'] as const;
/**
* Todo item status
*/
export type TodoStatus = (typeof TODO_STATUS_VALUES)[number];
/**
* Todo item with system metadata
*/
export interface Todo {
id: string;
sessionId: string;
content: string;
activeForm: string;
status: TodoStatus;
position: number;
createdAt: Date;
updatedAt: Date;
}
/**
* Todo input from tool (without system metadata)
*/
export interface TodoInput {
content: string;
activeForm: string;
status: TodoStatus;
}
/**
* Todo list update result
*/
export interface TodoUpdateResult {
todos: Todo[];
sessionId: string;
created: number;
updated: number;
deleted: number;
}
/**
* Configuration for TodoService
*/
export interface TodoConfig {
/** Maximum todos per session */
maxTodosPerSession?: number;
/** Enable real-time events */
enableEvents?: boolean;
}