-
Notifications
You must be signed in to change notification settings - Fork 0
Capability and Topology Registry
CW keeps two registries so a capability or a topology is declared in ONE place
and reaches every surface, with no hand-wiring across 4+ files. Capabilities are
declared as data — one row per capability in a single table that both front doors
read — so each command appears on the CLI and as an MCP tool at once. Topologies
self-register and auto-appear in topology list, topology validate, and
topology apply.
BSD discipline: mechanism (one table / one Map) separate from policy (the rows / the entries). Fail-closed on unknown ids.
Every runtime capability is one row in a single declarative table —
BUILTIN_CAPABILITIES: CapabilityDescriptor[] in src/capability-registry.ts.
There is no runtime "register a handler" step for capabilities and no per-surface
wiring: the table is the mechanism, and the CLI command plus the MCP tool are two
renderings of the same row.
export interface CapabilityDescriptor {
capability: string; // canonical dot-namespaced id, e.g. "worker.list"
summary: string;
entry: string; // the ONE shared core entry both surfaces run
surface: "both" | "cli-only" | "mcp-only";
cli?: { path: string[]; jsonMode: "default" | "flag" | "human" };
mcp?: { tool: string }; // e.g. "cw_worker_list"
payloadIdentical?: boolean; // cw <cmd> --json === cw_<tool> result
reason?: string; // required when surface !== "both" or payload diverges
}Add one row to the table. The row names the shared core entry both surfaces
route through, plus its cli and mcp bindings:
// src/capability-registry.ts — BUILTIN_CAPABILITIES
{
capability: "my.new.tool",
summary: "Does something.",
entry: "myNewTool", // a shared core entry in capability-core.ts
surface: "both",
cli: { path: ["my", "new-tool"], jsonMode: "default" },
mcp: { tool: "cw_my_new_tool" },
payloadIdentical: true,
}No second registration call. Both front doors read this one table, so the
capability appears on the CLI and as an MCP tool together, and the fail-closed
parity gate (npm run parity:check) blocks the release if the two ever drift.
Each front door routes to the row's shared core entry with its own explicit
switch (there is no runtime dispatcher; the table is what the switches are
validated against):
CLI: cw my new-tool --json
-> cli.ts switch matches "my new-tool" -> the "my.new.tool" row's shared core `entry`
-> render terse text (or --json)
MCP: cw_my_new_tool
-> mcp-server.ts switch matches "cw_my_new_tool" -> the SAME shared core `entry`
-> render structured JSON
- One source of truth: 199 capabilities, 186 MCP tools, each declared once.
- Both surfaces run the row's single shared
entry— no logic is stranded incli.tsormcp-server.ts. - The parity gate fails closed on any drift: a peer present on one surface but
missing from the other, an undeclared live tool, a reasonless surface-only
capability, or a payload divergence on a
payloadIdenticalrow each block the release.
interface MultiAgentTopologyDefinition {
schemaVersion: 1;
id: string; // open string (was "map-reduce" | "debate" | "judge-panel")
title: string;
summary: string;
roles: TopologyRoleSpec[];
groups: Array<{ id: string; title: string; roleIds: string[] }>;
blackboardTopics: Array<{ id: string; title: string; description: string }>;
phases: TopologyPhaseSpec[];
fanoutStrategy: string;
faninStrategy: string;
requiredEvidence: string[];
coordinatorDecisions: CoordinatorDecisionKind[];
candidateExpectations: string[];
verifierGates: string[];
}
interface TopologyRoleSpec {
id: string;
title: string;
responsibilities: string[];
count?: number; // NEW: materialize N instances
requiredEvidence: string[];
expectedArtifacts: string[];
faninObligations: string[];
}import { registerTopology } from "./topology";
registerTopology({
schemaVersion: 1,
id: "swarm",
title: "Swarm",
summary: "Parallel swarm agents with consensus voting.",
roles: [
{ id: "swarm-agent", title: "Swarm Agent",
responsibilities: ["Produce shard result with evidence."],
requiredEvidence: ["swarm output artifact"],
expectedArtifacts: ["swarm result"],
faninObligations: ["indexed swarm artifact"],
count: 5 }
],
groups: [{ id: "swarm", title: "Swarm Group", roleIds: ["swarm-agent"] }],
blackboardTopics: [
{ id: "swarm-outputs", title: "Swarm Outputs", description: "..." }
],
phases: [
{ id: "execute", title: "Execute", roleIds: ["swarm-agent"],
fanout: true, fanin: false,
requiredEvidence: ["swarm output artifact"],
coordinatorDecisionKinds: ["artifact-index"] },
{ id: "consensus", title: "Consensus", roleIds: ["synthesizer"],
fanout: false, fanin: true,
requiredEvidence: ["all swarm evidence"],
coordinatorDecisionKinds: ["candidate-synthesis"] }
],
fanoutStrategy: "one membership per swarm agent role",
faninStrategy: "consensus requires all swarm agent evidence",
requiredEvidence: ["swarm output artifact", "consensus synthesis"],
coordinatorDecisions: ["artifact-index", "candidate-synthesis"],
candidateExpectations: ["Synthesis cites swarm agent provenance."],
verifierGates: ["Swarm fanin must be ready before commit."]
});Before v0.1.53, materializedRoles() hardcoded:
if (role.id === "mapper") { ... expand N times ... }
else if (role.id === "judge") { ... expand N times ... }
else { single instance }
Now it checks role.count:
-
role.count > 1: createsrole-1,role-2, ...,role-N -
role.countundefined or 1: single role instance - Backward compat:
mapperCount/judgeCountoverrides still work for official topologies
-
Mechanism over policy, per layer. Capabilities are a static declared table (
BUILTIN_CAPABILITIES, read at build/check time — noregister()at runtime). The topology registry is the open runtime Map — aMap<string, MultiAgentTopologyDefinition>withregisterTopology()andgetTopologyDefinition(). No DI container, no lazy loading. -
Policy is declaration at import time. What exists is what was registered when modules loaded. No runtime config file that can silently change behavior.
-
Fail closed. Unknown capability id →
CapabilityError("not-found"). Unknown topology id → validation error"unknown-topology". Never silent fallback. -
One source of truth. All 199 capabilities and 3 official topologies are declared once. (v0.1.53 also shipped a dynamic capability dispatcher meant to route handlers at runtime; it had zero call sites and was removed as dead code in v0.1.81 (#131). Capabilities are declared, not runtime-dispatched.)
-
Single import surface.
capability-registry.tsis the one place the declared capability table and its parity helpers live;cli.tsandmcp-server.tsare validated against it. Consumers import from one place.
- Home
- Architecture Principles (principle 6: mechanism vs policy)
- Multi-Agent Topologies
- CLI MCP Parity
- Roadmap
- Repo doc:
docs/capability-topology-registry.7.md
Organized from local Obsidian notes and reconciled with the current
coo1white/cool-workflow repository state.
Start here
Go deeper
- Workflow Apps
- Architecture
- Trust And Audit
- Recovery And Restore
- Commands or API
- MCP And Manifests
- Operations
- FAQ
Source docs