Last updated: 2026-02-26 · Doc version: 2.0.0
All conventions are enforced by the existing codebase. Follow them exactly when adding or modifying code.
- Python 3.11 is required.
- Every module must start with
from __future__ import annotations. - Every module must have a
# Purpose: <one-line description>.comment at the very top (after the future import).
# Purpose: What this module does in one line.
from __future__ import annotations- Never import
BaseModelorFielddirectly frompydantic. - Always import from the compat shim:
from closed_claw.compat import BaseModel, FieldThis ensures Pydantic v1/v2 compatibility across installs. The shim also provides a pure-Python fallback BaseModel when pydantic is not installed at all.
Settings.from_env()is the only way to load config.- Call it at the CLI boundary (in
cmd_*functions), never at module import time. - Never hardcode file paths. All paths come from
Settingsfields (db_path,agents_dir,run_logs_dir, etc.). - Approval modes include
interactive,approve,deny, andweb(web UI approval queue).
# Correct
def cmd_run(args):
settings = Settings.from_env()
db = settings.db_path
# Wrong
DB_PATH = Path(".closed_claw/registry.db") # don't do this- Coordinator graph nodes are
async def. asyncio.run()is used at the CLI boundary when invoking the graph.AgentRunneris synchronous internally (subprocess I/O loop).- Async nodes call
AgentRunnerviaasyncio.get_event_loop().run_in_executoror directly if already on a thread.
- Raise specific exceptions rather than bare
Exception. AgentRuntimeError— agent protocol violations or subprocess errors.ToolExecutionError— tool-level failures.ValueError— bad input/state in coordinator nodes.- Always propagate errors up to the coordinator node level; do not swallow them silently.
When adding new message types:
- Add a Pydantic model to
closed_claw/runtime/protocol.py. - Update
parse_agent_line()to attempt parsing the new type. - Add the corresponding handler in
CoordinatorNodes.execute_task_pool.
Message types must have a type: Literal["<type_name>"] discriminator field.
- Add the tool name string to
SUPPORTED_TOOLSlist inexecutor.py. - Add an entry to
TOOL_REGISTRYdict withdescriptionandargs_schema. - Implement
_run_<tool_name>(self, **args)inToolExecutor. - Dispatch via the
if tool == "<tool_name>":block inToolExecutor.execute(). - Write a unit test in
tests/unit/test_tools_executor.py.
- Write
cmd_<name>(args: argparse.Namespace) -> intincli.py. - Register a subparser in
_build_parser():p = sub.add_parser("name", help="...") p.set_defaults(func=cmd_<name>)
- Return
0on success, non-zero on failure.
- Add
async def <node_name>(self, state: dict[str, Any]) -> dict[str, Any]toCoordinatorNodesinnodes.py. - Always use
self._merge(state, **updates)to return updated state — never mutate the input dict. - Use
self._emit_runlog(state, "event_name", {...})for observability. - Register the node in
graph.py:graph.add_node("node_name", nodes.node_name) graph.add_edge("prev_node", "node_name") graph.add_edge("node_name", "next_node")
Base skill modules live in agents/skills/ and form Layer 1 of every agent's system prompt. They describe how to use a specific tool domain at an expert level.
- Create
agents/skills/<name>.md. The file should cover:- When to use this capability
- Concrete patterns / example tool call arguments
- Error handling and edge cases
- Output format expectations
- Add the module name string to
_BASE_SKILL_IDSinclosed_claw/registry/search.py— this makes the LLM aware of the module when generating new agent profiles. - Add a short faithful scope description to
_BASE_SKILL_DESCRIPTIONSin the same file. This description is injected into the profile-gen prompt via_build_skill_catalog(). Without it, the LLM only sees the bare ID and hallucinates capabilities into permanentskill_md(same bug class as the bare-tool-names Fix A in_build_tool_catalog). - The LLM will automatically assign the module to relevant agents via
generate_agent_profile. You can also manually add the name to an existingmanifest.jsonskill_idslist.
Rules:
- Module names must be lowercase
snake_caseand match the filename without.md. - Never include agent-specific content in a base skill module — that belongs in
agents/<agent_id>/skill.md. skill_idsinAgentManifestare always validated against_BASE_SKILL_IDSin_normalize_profile_payload; unknown IDs are silently dropped._BASE_SKILL_DESCRIPTIONSis the only ground truth the profile prompt sees while theagents/skills/*.mdfiles don't exist on disk — keep descriptions faithful to what the underlying tools actually do.
- One test file per module:
tests/unit/test_<module_name>.py. - Use
pytestfixtures; no unittest classes unless unavoidable. - Mock external calls (HTTP, subprocess, sqlite-vec) in unit tests.
- Integration tests in
tests/integration/may use real SQLite but should not hit external APIs. - Run all tests:
pytest -q.
- Edit
closed_claw/registry/schema.sql(DDL-only,CREATE TABLE IF NOT EXISTS). - Update
RegistryStoremethods instore.pyto use new columns/tables. - If adding a column to
agents, updateAgentManifestinstore.py. - The schema is applied fresh on each
RegistryStore.__init__viaexecutescript.
| Element | Convention | Example |
|---|---|---|
| Modules | snake_case |
registry/store.py |
| Classes | PascalCase |
RegistryStore |
| Functions/methods | snake_case |
cmd_delete_agent |
| Constants | UPPER_SNAKE |
SUPPORTED_TOOLS |
| Private methods | _snake_case |
_run_terminal |
| Env vars | CLOSED_CLAW_<NAME> |
CLOSED_CLAW_DB_PATH |
| Agent IDs | uuid4().hex (32 chars) |
a1b2c3d4... |
| Run IDs | uuid4().hex (32 chars) |
e5f6g7h8... |
- Never use
print()inside library code (coordinator, registry, tools, etc.). - Use
RunLogger.emit(event, payload)for structured run events. - Use
AuditStore.record(...)for compliance/audit events. - Rich
Console.print()is allowed incli.py,interactive.py,setup_wizard.py, andapproval.py.
- Agent tool calls go through
ToolExecutorwhich enforcesallowed_roots(fromSettings.extra_allowed_paths). - Never allow agents to write outside their capsule dir or explicitly allowed paths.
sql_querytool always validates the query starts withSELECTbefore execution.