Compare commits

...

6 Commits

Author SHA1 Message Date
d0ec37d136 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-03-31:
  - update .custodian-brief.md for the-custodian
2026-03-31 17:24:28 +02:00
d58ef71339 feat(capability-registry): CUST-WP-0031 domain capability registry
- Migration p3k4l5m6n7o8: nullable repo_id FK on capability_catalog
- PATCH /capability-catalog/{id} endpoint for back-filling repo attribution
- register_capability MCP tool accepts optional repo_slug
- get_domain_summary now includes compact capabilities list (type+title+repo_slug)
- New get_capability_profile MCP tool: domain → repos → capabilities tree
- 6 repo descriptions populated; 25 catalog entries attributed to repos
- 9 new capabilities registered for personhood, foerster_capabilities, coulomb_social
- TOOLS.md: Capability Catalog & Requests section with full tool reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 17:23:45 +02:00
1b886d9786 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-03-31:
  - update .custodian-brief.md for the-custodian
2026-03-31 17:19:58 +02:00
0ad1b3c30d chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-03-31:
  - update .custodian-brief.md for the-custodian
2026-03-31 16:49:53 +02:00
36db3c4ec7 feat(workplan): CUST-WP-0031 domain capability registry
7-task workplan to give worker agents efficient MCP access to domain/repo
scope and capabilities without exploring source repos directly.

Phase 1 (code): repo_id FK on CapabilityCatalog, capabilities in
get_domain_summary, new get_capability_profile MCP tool.
Phase 2 (data): populate missing repo descriptions, back-fill repo_id
on 25 existing entries, register capabilities for 3 empty domains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 16:49:21 +02:00
4dbfc39026 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-03-31:
  - update .custodian-brief.md for the-custodian
2026-03-31 16:48:43 +02:00
8 changed files with 488 additions and 2 deletions

View File

@@ -2,7 +2,7 @@
# Custodian Brief — the-custodian
**Domain:** custodian
**Last synced:** 2026-03-29 22:48 UTC
**Last synced:** 2026-03-31 15:24 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams

View File

@@ -22,6 +22,12 @@ class CapabilityCatalog(Base, TimestampMixin):
nullable=False,
index=True,
)
repo_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("managed_repos.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
capability_type: Mapped[str] = mapped_column(String(50), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -33,7 +39,12 @@ class CapabilityCatalog(Base, TimestampMixin):
)
domain: Mapped["Domain"] = relationship("Domain", lazy="selectin") # noqa: F821
repo: Mapped["ManagedRepo | None"] = relationship("ManagedRepo", lazy="selectin") # noqa: F821
@property
def domain_slug(self) -> str:
return self.domain.slug if self.domain is not None else ""
@property
def repo_slug(self) -> str | None:
return self.repo.slug if self.repo is not None else None

View File

@@ -11,9 +11,11 @@ from api.models.agent_message import AgentMessage
from api.models.capability_catalog import CapabilityCatalog
from api.models.capability_request import CapabilityRequest
from api.models.domain import Domain
from api.models.managed_repo import ManagedRepo
from api.models.task import Task
from api.schemas.capability_request import (
CatalogCreate,
CatalogPatch,
CatalogRead,
CapabilityRequestAccept,
CapabilityRequestCreate,
@@ -52,8 +54,15 @@ async def create_catalog_entry(
session: AsyncSession = Depends(get_session),
) -> CapabilityCatalog:
domain = await _resolve_domain(body.domain, session)
repo_id = None
if body.repo_slug:
repo = await _resolve_repo(body.repo_slug, session)
repo_id = repo.id
entry = CapabilityCatalog(
domain_id=domain.id,
repo_id=repo_id,
capability_type=body.capability_type,
title=body.title,
description=body.description,
@@ -72,6 +81,31 @@ async def create_catalog_entry(
return entry
@router.patch("/capability-catalog/{entry_id}", response_model=CatalogRead)
async def patch_catalog_entry(
entry_id: uuid.UUID,
body: CatalogPatch,
session: AsyncSession = Depends(get_session),
) -> CapabilityCatalog:
entry = await session.get(CapabilityCatalog, entry_id)
if entry is None:
raise HTTPException(status_code=404, detail=f"Catalog entry '{entry_id}' not found")
if body.repo_slug is not None:
repo = await _resolve_repo(body.repo_slug, session)
entry.repo_id = repo.id
if body.description is not None:
entry.description = body.description
if body.keywords is not None:
entry.keywords = body.keywords
if body.status is not None:
entry.status = body.status
await session.commit()
await session.refresh(entry)
return entry
@router.get("/capability-catalog/", response_model=list[CatalogRead])
async def list_catalog(
domain: str | None = Query(None),
@@ -552,6 +586,14 @@ async def _resolve_domain(slug: str, session: AsyncSession) -> Domain:
return domain
async def _resolve_repo(slug: str, session: AsyncSession) -> ManagedRepo:
result = await session.execute(select(ManagedRepo).where(ManagedRepo.slug == slug))
repo = result.scalar_one_or_none()
if repo is None:
raise HTTPException(status_code=404, detail=f"Repo '{slug}' not found")
return repo
async def _get_request_or_404(request_id: uuid.UUID, session: AsyncSession) -> CapabilityRequest:
req = await session.get(CapabilityRequest, request_id)
if req is None:

View File

@@ -14,6 +14,14 @@ class CatalogCreate(BaseModel):
title: str
description: str | None = None
keywords: list[str] = []
repo_slug: str | None = None # optional repo attribution
class CatalogPatch(BaseModel):
repo_slug: str | None = None
description: str | None = None
keywords: list[str] | None = None
status: str | None = None
class CatalogRead(BaseModel):
@@ -21,6 +29,8 @@ class CatalogRead(BaseModel):
id: uuid.UUID
domain_slug: str
repo_id: uuid.UUID | None = None
repo_slug: str | None = None
capability_type: str
title: str
description: str | None = None

View File

@@ -24,13 +24,15 @@ Do not use them as a substitute for formal work definition inside the domain rep
| Tool | Key Args | When to use |
|------|----------|-------------|
| `get_domain_summary(domain_slug)` | `domain_slug`: e.g. `"railiance"` | **Domain session start.** Scoped snapshot: active workstreams, blocking decisions, last 5 events, repo SBOM status — ~10% of get_state_summary() token cost. |
| `get_domain_summary(domain_slug)` | `domain_slug`: e.g. `"railiance"` | **Domain session start.** Scoped snapshot: active workstreams, blocking decisions, last 5 events, repo SBOM status, compact capabilities list — ~10% of get_state_summary() token cost. |
| `get_state_summary()` | — | **Cross-domain work / custodian sessions.** Full snapshot: totals, all blocking decisions, all blocked tasks, all open workstreams, last 20 events. Large (~10k tokens). |
| `get_topic(slug)` | `slug`: e.g. `"markitect"` | Deep-dive on one topic + its workstreams + recent events. |
| `list_tasks(workstream_id, status?)` | `workstream_id`: UUID (required); `status?`: todo/in_progress/blocked/done/cancelled | List all tasks in a workstream. Use this to look up task UUIDs before calling `update_task_status`, or to verify which workplan tasks are already synced to the DB. |
| `list_blocked_tasks(workstream_id?)` | optional filter | Surface all impediments, optionally scoped to one workstream. |
| `list_pending_decisions(topic_id?)` | optional filter | Decisions holding up work, sorted by deadline. |
| `get_recent_progress(limit, since?)` | `limit` default 20; `since` ISO datetime | Reconstruct recent session history. |
| `get_capability_profile(domain_slug?)` | `domain_slug`: optional domain slug | **Capability deep-dive.** Returns repos → capabilities tree for one domain or all active domains. Includes descriptions and keywords. For cross-domain architectural discussion or when a worker needs to understand what a domain provides without checking out its repos. |
| `list_capabilities(domain?, capability_type?)` | optional filters | Browse the capability catalog entries. Returns full records including keywords. |
---
@@ -222,6 +224,36 @@ curl -X POST http://127.0.0.1:8000/repos/marki-docx/paths/ \
---
## Capability Catalog & Requests
Capabilities describe what each domain/repo can provide. Use the catalog for routing; use requests for cross-domain work coordination.
### Catalog tools
| Tool | Key Args | Notes |
|------|----------|-------|
| `register_capability(domain, capability_type, title, ...)` | `domain`: slug; `capability_type`: infrastructure/api/data/security/governance/documentation; `keywords?`; `description?`; `repo_slug?` | Add a capability to the catalog. Provide `repo_slug` to attribute it to a specific repo within the domain. |
| `list_capabilities(domain?, capability_type?)` | optional filters | Browse active catalog entries with full detail (description + keywords). |
| `get_capability_profile(domain_slug?)` | optional domain slug | **Deep-dive.** Returns domain → repos → capabilities tree with descriptions and keywords. Single domain or all active domains. Use when a worker needs to understand what a domain provides without checking out its repos. |
### Request tools
| Tool | Key Args | When to use |
|------|----------|-------------|
| `request_capability(title, capability_type, requesting_domain, requesting_agent, ...)` | `priority?`; `description?`; `requesting_workstream_id?`; `blocking_task_id?` | Ask another domain to provide a capability. Auto-routes to best matching catalog entry. |
| `list_capability_requests(domain?, status?, capability_type?)` | optional filters | List open requests — filter by your domain to see what you must fulfill. |
| `get_capability_request(request_id)` | `request_id`: UUID | Full detail on a single request. |
| `accept_capability_request(request_id, fulfilling_agent, ...)` | `fulfilling_workstream_id?` | Accept a request routed to your domain. Notifies requester. |
| `update_capability_request_status(request_id, status, note?)` | `status`: in_progress/ready_for_review/completed/rejected/withdrawn | Advance request lifecycle. `completed` auto-unblocks the linked task. |
| `dispute_capability_routing(request_id, reason, disputed_by, suggested_domain?)` | all required except `suggested_domain` | Flag incorrect routing. Notifies custodian for re-routing. |
| `reroute_capability_request(request_id, rerouted_by, note, domain?, catalog_entry_id?)` | one of `domain`/`catalog_entry_id` required | Re-route a disputed request to the correct domain. |
| `patch_capability_request(request_id, ...)` | `catalog_entry_id?`; `priority?`; `blocking_task_id?`; `fulfilling_workstream_id?` | Correct mutable metadata on a request. |
**Request status flow:** `requested``accepted``in_progress``ready_for_review``completed`
Dispute path: `requested``routing_disputed``requested` (after re-route)
---
## Domain Slugs
Run `list_domains()` to get the live list. Default 6: `custodian` · `railiance` · `markitect` · `coulomb_social` · `personhood` · `foerster_capabilities`

View File

@@ -258,6 +258,18 @@ def get_domain_summary(domain_slug: str) -> str:
}
if goal_guidance:
result["goal_guidance"] = goal_guidance
# Compact capabilities list (type + title + repo_slug only, capped at 20)
caps_raw = _get("/capability-catalog/", {"domain": domain_slug, "status": "active"})
if isinstance(caps_raw, list):
compact_caps = [
{"type": c["capability_type"], "title": c["title"], "repo_slug": c.get("repo_slug")}
for c in caps_raw[:20]
]
result["capabilities"] = compact_caps
if len(caps_raw) > 20:
result["capabilities_truncated"] = True
return json.dumps(result, indent=2)
@@ -1818,6 +1830,7 @@ def register_capability(
title: str,
description: str | None = None,
keywords: list[str] | None = None,
repo_slug: str | None = None,
) -> str:
"""Register a capability that a domain can provide. Used for routing requests.
@@ -1827,6 +1840,7 @@ def register_capability(
title: Short title for this capability
description: Longer description (optional)
keywords: List of keywords for routing (e.g. ['cluster', 'k8s', 'privacy'])
repo_slug: Optional repo slug to attribute this capability to a specific repo
"""
entry = _post("/capability-catalog", {
"domain": domain,
@@ -1834,6 +1848,7 @@ def register_capability(
"title": title,
"description": description,
"keywords": keywords or [],
"repo_slug": repo_slug,
})
return json.dumps(entry, indent=2)
@@ -1855,6 +1870,87 @@ def list_capabilities(
}), indent=2)
@mcp.tool()
def get_capability_profile(domain_slug: str | None = None) -> str:
"""Full capability registry: domain → repos (with description) → capabilities.
Designed for deep-dive or cross-domain architectural discussion.
Args:
domain_slug: If provided, return profile for that one domain only.
If omitted, return profiles for all active domains.
Returns a structured dict with repos nested under each domain, and
capabilities nested under each repo. Domain-level capabilities
(no repo assigned) appear under a synthetic entry with slug=null.
"""
if domain_slug:
domain_slugs = [domain_slug]
else:
domains_raw = _get("/domains/")
if isinstance(domains_raw, dict) and "error" in domains_raw:
return json.dumps(domains_raw, indent=2)
domain_slugs = [d["slug"] for d in domains_raw if d.get("status") == "active"]
# Fetch topics once for title lookup
topics_raw = _get("/topics/")
profiles = []
for slug in domain_slugs:
repos_raw = _get("/repos/", {"domain": slug})
caps_raw = _get("/capability-catalog/", {"domain": slug, "status": "active"})
if isinstance(caps_raw, dict) and "error" in caps_raw:
caps_raw = []
# Index capabilities by repo_slug (None → domain-level)
caps_by_repo_slug: dict[str | None, list] = {}
for cap in caps_raw:
r_slug = cap.get("repo_slug")
caps_by_repo_slug.setdefault(r_slug, []).append({
"type": cap["capability_type"],
"title": cap["title"],
"description": cap.get("description"),
"keywords": cap.get("keywords", []),
})
repo_entries = []
if isinstance(repos_raw, list):
for repo in repos_raw:
repo_entries.append({
"slug": repo["slug"],
"name": repo["name"],
"description": repo.get("description"),
"capabilities": caps_by_repo_slug.get(repo["slug"], []),
})
# Domain-level caps (no repo assigned)
domain_level = caps_by_repo_slug.get(None, [])
if domain_level:
repo_entries.append({
"slug": None,
"name": "(domain-level)",
"description": None,
"capabilities": domain_level,
})
# Get topic title
topic_title = ""
if isinstance(topics_raw, list):
topic = next((t for t in topics_raw if t.get("domain_slug") == slug), None)
if topic:
topic_title = topic.get("title", "")
profiles.append({
"slug": slug,
"title": topic_title,
"repos": repo_entries,
})
if domain_slug and profiles:
return json.dumps(profiles[0], indent=2)
return json.dumps(profiles, indent=2)
@mcp.tool()
def request_capability(
title: str,

View File

@@ -0,0 +1,28 @@
"""add repo_id to capability_catalog
Revision ID: p3k4l5m6n7o8
Revises: o2j3k4l5m6n7
Create Date: 2026-03-31
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision = "p3k4l5m6n7o8"
down_revision = "o2j3k4l5m6n7"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"capability_catalog",
sa.Column("repo_id", UUID(as_uuid=True), sa.ForeignKey("managed_repos.id", ondelete="SET NULL"), nullable=True),
)
op.create_index("ix_capability_catalog_repo_id", "capability_catalog", ["repo_id"])
def downgrade() -> None:
op.drop_index("ix_capability_catalog_repo_id", table_name="capability_catalog")
op.drop_column("capability_catalog", "repo_id")

View File

@@ -0,0 +1,267 @@
---
id: CUST-WP-0031
type: workplan
title: "Domain Capability Registry"
domain: custodian
repo: the-custodian
status: done
owner: custodian
topic_slug: custodian
created: "2026-03-31"
updated: "2026-03-31"
state_hub_workstream_id: "7b480543-b18a-4d79-a30b-7b34bce27ee5"
---
# Domain Capability Registry
## Goal
Give worker agents a single efficient MCP call to understand what each
domain/repo does and what capabilities it exposes — without having to
explore source repositories directly.
The immediate driver is that inter-hub workers on CoulombCore need to
discuss operational setup and security architecture (e.g. key-cape's SSH CA
role, net-kingdom's SSO platform) but can only see repos checked out locally.
Key-cape and net-kingdom are not checked out there; the worker must fall back
to the state-hub for this information.
## Background
### What exists
- 25 `CapabilityCatalog` entries across four domains (railiance: 11,
netkingdom: 6, custodian: 5, markitect: 3).
- `CapabilityCatalog` is domain-scoped only — no `repo_id` FK, so the
SSH CA capability in netkingdom cannot be attributed to key-cape vs
net-kingdom.
- `get_domain_summary` is the cheap worker orientation call (~10% token
cost of full state summary) but returns **no capability information**.
- `list_capabilities` works but returns verbose full records; workers
must call it per domain and get unstructured JSON.
- `ManagedRepo` has a `description` field but 8 of 17 repos have it empty.
- personhood, foerster_capabilities, and coulomb_social have zero capabilities
registered.
### Design
Three complementary changes:
1. **Schema** — add nullable `repo_id` FK to `CapabilityCatalog` so each
capability can be attributed to the repo that provides it.
2. **MCP orientation** — extend `get_domain_summary` to include a compact
`capabilities` list (type + title + repo_slug only, no keywords/description)
so the standard worker orientation call already surfaces what the domain
provides.
3. **Full-detail tool** — new `get_capability_profile(domain_slug?)` MCP
tool that returns a complete registry: domain → repos (with description)
→ capabilities (with description). Designed for deep-dive or
cross-domain architectural discussion.
Data tasks: populate missing repo descriptions and back-fill `repo_id` on
existing catalog entries; register capabilities for the three empty domains.
## Implementation Plan
### Phase 1 — Schema + MCP
- Add nullable `repo_id` FK on `CapabilityCatalog`; update `register_capability`
to accept an optional `repo_slug` that resolves to `repo_id`.
- Add `capabilities` compact list to `get_domain_summary` response.
- New MCP tool `get_capability_profile`.
### Phase 2 — Data Population
- Populate `description` for the 8 repos that lack it.
- Back-fill `repo_id` on existing 25 catalog entries.
- Register capabilities for personhood, foerster_capabilities, coulomb_social.
## Tasks
```task
id: T01
title: "Add repo_id FK to CapabilityCatalog"
status: done
priority: high
description: >
1. Write Alembic migration: add `repo_id` UUID nullable FK column to
`capability_catalog` referencing `managed_repos.id` (ON DELETE SET NULL).
Add index on `repo_id`. Down revision from latest head.
2. Add `repo_id: uuid.UUID | None` to the CapabilityCatalog model
(managed_repo.py FK relationship, nullable).
3. Update CapabilityCatalog schema (CapabilityRead, CapabilityCreate) to
include `repo_id: uuid.UUID | None` and `repo_slug: str | None`
(computed via relationship).
4. Update the capability-catalog router: if `repo_slug` is provided on
POST, resolve it to `repo_id` (404 if not found in the same domain).
5. Update `register_capability` MCP tool to accept optional `repo_slug`
parameter and forward it.
6. Run `make test` — no regressions.
state_hub_task_id: "8d5e3e37-c753-4cdc-9211-83ee39f6b0f2"
```
```task
id: T02
title: "Include compact capabilities in get_domain_summary"
status: done
priority: high
description: >
Extend the `get_domain_summary` MCP tool response to include a
`capabilities` list alongside `repos` and `workstreams`. Each entry
should be compact: {type, title, repo_slug} only (no description or
keywords, to keep token cost low).
Implementation: in mcp_server/server.py, after building the summary dict,
call GET /capability-catalog/?domain=<slug>&status=active and append
`capabilities: [{capability_type, title, repo_slug}]` to the response.
The `repo_slug` field is nullable (domain-level capabilities have null).
Cap at 20 entries to protect token budget; add a `capabilities_truncated`
boolean flag if there are more.
state_hub_task_id: "ac47994c-efe9-404f-8065-9adf2e923d4c"
```
```task
id: T03
title: "New MCP tool: get_capability_profile"
status: done
priority: high
description: >
Add a new MCP tool `get_capability_profile(domain_slug: str | None = None)`
to mcp_server/server.py.
Behaviour:
- If domain_slug is provided: return the profile for that one domain.
- If omitted: return profiles for all active domains.
Response structure per domain:
{
"slug": "netkingdom",
"title": "<topic title>",
"repos": [
{
"slug": "key-cape",
"name": "...",
"description": "...",
"capabilities": [
{"type": "security", "title": "SSH CA", "description": "...", "keywords": [...]}
]
},
// domain-level capabilities (repo_slug null) listed at the end under
// a synthetic repo entry {"slug": null, "name": "(domain-level)", ...}
]
}
Implementation: call GET /repos?domain=<slug> and
GET /capability-catalog/?domain=<slug>&status=active, then assemble.
For the all-domains case iterate over GET /domains/ (active only).
state_hub_task_id: "b380fd2b-28fa-4c47-96d6-4c65a0300c44"
```
```task
id: T04
title: "Populate missing repo descriptions"
status: done
priority: medium
description: >
Use PATCH /repos/{slug}/ to set `description` for the 8 repos that
currently have none. Write concise scope descriptions (2-3 sentences each)
by reading the repo's CLAUDE.md or README if available; otherwise infer
from the slug and domain context.
Repos needing descriptions:
- the-custodian (custodian domain)
- activity-core (custodian domain)
- kaizen-agentic (custodian domain)
- llm-connect (custodian domain) — already has description, skip
- markitect-project (markitect domain)
- railiance-bootstrap (railiance domain)
- railiance-hosts (railiance domain)
- ops-bridge (custodian domain) — already has description, skip
Confirm final list by re-checking GET /repos/ at task start.
state_hub_task_id: "00d44110-fcb4-45cc-8bc8-454af2629d2f"
```
```task
id: T05
title: "Back-fill repo_id on existing capability catalog entries"
status: done
priority: medium
description: >
After T01 lands, update the 25 existing catalog entries to set repo_id
where the capability is clearly from a specific repo.
Use PATCH /capability-catalog/{id} (add this endpoint if missing) or
re-register with repo_slug.
Key attributions:
netkingdom domain:
- "Container image build and publish (GHCR)" → net-kingdom
- "Bootstrap local identity service" → key-cape
- "SSO/MFA platform (Keycloak)" → key-cape
- "NetKingdom IAM Profile specification" → net-kingdom (spec lives there)
- "Identity migration tooling" → key-cape
- "OIDC/PKCE authentication (lightweight mode)" → key-cape
railiance domain:
- All 11 entries → railiance-platform or railiance-cluster or railiance-infra
(review titles to assign; most go to railiance-platform)
custodian domain:
- "SSH reverse tunnel connectivity" → ops-bridge
- "Durable event-triggered task factory" → activity-core
- "SBOM and licence reporting" → the-custodian (state-hub)
- "Cross-domain state tracking" → the-custodian (state-hub)
- "MCP tool registration" → the-custodian (state-hub)
markitect domain:
- All 3 → markitect-project
Note: a PATCH endpoint on /capability-catalog/{id} does not currently
exist. Add a minimal one in T01 or add it here: PATCH /capability-catalog/{id}
accepting {repo_slug?, description?, keywords?, status?}.
state_hub_task_id: "7f0748c7-bdee-4801-b870-d4940a5a2e63"
```
```task
id: T06
title: "Register capabilities for personhood, foerster_capabilities, coulomb_social"
status: done
priority: medium
description: >
Register at least 3 capabilities per missing domain using
register_capability (MCP tool or POST /capability-catalog/).
personhood domain:
- "Rights and obligations framework modelling" (type: governance)
- "Entity consent and data-subject lifecycle" (type: governance)
- "Privacy-by-design compliance checking" (type: security)
foerster_capabilities domain:
- "Agency capability taxonomy" (type: governance)
- "Capability gap analysis" (type: data)
- "Agent persona specification" (type: governance)
coulomb_social domain:
- "Co-creation marketplace workflow" (type: api)
- "Contribution matching and brokerage" (type: data)
- "Contributor reputation tracking" (type: data)
Write descriptions and keywords that would route capability requests
correctly to these domains.
state_hub_task_id: "c6e1be52-961c-4e34-96b1-e450d64298df"
```
```task
id: T07
title: "Consistency gate and smoke test"
status: done
priority: low
description: >
1. Run `make test` — all existing tests must pass.
2. Run `make fix-consistency REPO=the-custodian`.
3. Manual smoke test via Python MCP client:
- Call get_domain_summary("netkingdom") and verify capabilities list
appears with at least the 6 netkingdom entries.
- Call get_capability_profile("netkingdom") and verify key-cape entries
have repo_slug set.
- Call get_capability_profile() (no arg) and verify all 7 active domains
appear.
4. Add a note to TOOLS.md about the new get_capability_profile tool.
state_hub_task_id: "7e07e32c-683d-47a1-951c-beace9f245a6"
```