Skip to content

The command catalog

There is exactly one place where a Chorus primitive’s behavior lives: a Command in the catalog. Validation, business logic, and side effects all sit behind one execute(). Every transport — HTTP, JSON-RPC, MCP — is a thin shim that frames a request around the same call. This is the architectural keystone (ADR-0001), and it’s why the same operation behaves identically no matter which wire you reach it on.

A command is a small record (src/commands/catalog.ts:64-70):

interface Command<TSchema extends z.ZodType = z.ZodType, TOutput = unknown> {
id: string; // e.g. "tasks.create", "memory.store"
description: string;
schema: TSchema; // zod schema for the input
validationErrorCode?: string; // error code on schema failure
execute(input: any, ctx: CommandContext): Promise<TOutput>;
}

executeCommand is the single entry point (catalog.ts:93-114). It looks the command up by id, zod-validates the input, and only then calls execute(parsed, ctx):

const command = commandCatalog[args.id];
if (!command) throw new ChorusError("COMMAND_NOT_FOUND", ..., 404);
const parsed = command.schema.safeParse(args.input);
if (!parsed.success) {
throw new ChorusError(command.validationErrorCode ?? SIGNAL_EMIT_VALIDATION_FAILED, ..., 400, parsed.error.issues);
}
return command.execute(parsed.data, args.ctx);

Validation failure is a single 400 with the per-command code (or the signal-emit default); an unknown id is a single 404 COMMAND_NOT_FOUND. Transports never re-implement either.

commandCatalog is a frozen map built from 14 handler groups (catalog.ts:72-89): signals, memory, memory-admin, workspace, tasks, circles, admin, workstreams, webhooks, artifacts, approvals, org, identity, workflow. The audit counted 136 command ids plus 8 generated signals.emit.<foundation> entries (gap-4 §2.3).

export const commandCatalog = Object.freeze(
Object.fromEntries([...signalCommands, ...memoryCommands, /* …14 groups */ ]
.map((command) => [command.id, command])),
);

The handler logic lives in src/commands/handlers/*.ts. Directories like src/tasks, src/execution, src/best-of, and src/workspace hold route shims, zod types, and pure policy helpers — not the business logic (gap-3 §1.1).

Each transport’s job is to turn its wire envelope into {id, input, ctx} and call executeCommand. The context is built once per transport (gap-3 §1.1, gap-4 §2.3):

Transport Builder Role
HTTP commandContextFromHono (catalog.ts:125) parse envelope → executeCommand → frame {data, meta}
JSON-RPC passthrough via executeCatalogCommand method name → catalog id
MCP task/workspace tools server.tool(name, …) → executeCommand
Internal commandContextFromRpc (catalog.ts:148) catalog-to-catalog passthrough

The catalog is the rule, not yet the whole map. A few surfaces still bypass it — document and trust these carefully.