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.
The Command interface
Section titled “The Command interface”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.
How the catalog is assembled
Section titled “How the catalog is assembled”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).
Transports frame; they don’t decide
Section titled “Transports frame; they don’t decide”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 non-catalog holdouts
Section titled “The non-catalog holdouts”The catalog is the rule, not yet the whole map. A few surfaces still bypass it — document and trust these carefully.