generated from coulomb/repo-seed
feat(capability-requests): add cross-domain capability catalog and request routing
Introduces a capability catalog (CUST-WP-0022) so domains can advertise what they provide and agents can request capabilities from other domains with auto-routing, lifecycle tracking, and task-unblocking on completion. - New models: CapabilityCatalog, CapabilityRequest with full lifecycle (requested → accepted → in_progress → ready_for_review → completed/rejected/withdrawn) - Migration i6d7e8f9a0b1: capability_catalog + capability_requests tables - Router /capability-catalog and /capability-requests with accept/status endpoints - 7 new MCP tools: register_capability, list_capabilities, request_capability, accept_capability_request, update_capability_request_status, list_capability_requests, get_capability_request - StateSummary gains open_capability_requests count - Dashboard: capability-requests.md page + docs/capabilities.md + docs/scope.md - SCOPE.md: three seed capabilities documented (MCP registration, state tracking, SBOM) - scope.template: Provided Capabilities section with example block - scripts/ingest_capabilities.py + make ingest-capabilities[/-all] targets Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
250
dashboard/src/capability-requests.md
Normal file
250
dashboard/src/capability-requests.md
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
title: Capability Requests
|
||||
---
|
||||
|
||||
```js
|
||||
import {API} from "./components/config.js";
|
||||
const POLL = 30_000;
|
||||
```
|
||||
|
||||
```js
|
||||
// Live poll for capability requests
|
||||
const reqState = (async function*() {
|
||||
while (true) {
|
||||
let data = [], ok = false;
|
||||
try {
|
||||
const r = await fetch(`${API}/capability-requests/`);
|
||||
ok = r.ok;
|
||||
data = ok ? await r.json() : [];
|
||||
} catch {}
|
||||
yield {data, ok, ts: new Date()};
|
||||
await new Promise(res => setTimeout(res, POLL));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```js
|
||||
const requests = reqState.data ?? [];
|
||||
const _ok = reqState.ok ?? false;
|
||||
const _ts = reqState.ts;
|
||||
```
|
||||
|
||||
# Capability Requests
|
||||
|
||||
```js
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${_ok ? `Live · ${_ts?.toLocaleTimeString()}` : html`<span style="color:red">API offline</span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/live-data");
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
|
||||
const _h1 = document.querySelector("#observablehq-main h1");
|
||||
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/capabilities"); }
|
||||
```
|
||||
|
||||
```js
|
||||
// KPI sidebar
|
||||
const open = requests.filter(r => ["requested","accepted","in_progress","ready_for_review"].includes(r.status));
|
||||
const completed = requests.filter(r => r.status === "completed");
|
||||
const avgFulfill = completed.length > 0
|
||||
? (completed.reduce((s, r) => s + (new Date(r.completed_at) - new Date(r.created_at)), 0) / completed.length / 86400000).toFixed(1)
|
||||
: "—";
|
||||
const critical = open.filter(r => r.priority === "critical" || r.priority === "high").length;
|
||||
|
||||
const kpiEl = html`<div class="kpi-infobox">
|
||||
<div class="kpi-infobox-title">Capability Requests</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.4rem 0.8rem;font-size:0.82rem">
|
||||
<span>Open</span><strong>${open.length}</strong>
|
||||
<span>Avg fulfill</span><strong>${avgFulfill}d</strong>
|
||||
<span>High/Critical</span><strong style="color:${critical > 0 ? 'orange' : 'inherit'}">${critical}</strong>
|
||||
<span>Total</span><strong>${requests.length}</strong>
|
||||
</div>
|
||||
</div>`;
|
||||
injectTocTop("cap-req-kpi", kpiEl);
|
||||
```
|
||||
|
||||
```js
|
||||
// Filters
|
||||
const typeFilter = Inputs.select(
|
||||
["all", ...new Set(requests.map(r => r.capability_type))],
|
||||
{label: "Type", value: "all"}
|
||||
);
|
||||
const statFilter = Inputs.select(
|
||||
["all", "requested", "accepted", "in_progress", "ready_for_review", "completed", "rejected", "withdrawn"],
|
||||
{label: "Status", value: "all"}
|
||||
);
|
||||
const domFilter = Inputs.select(
|
||||
["all", ...new Set([...requests.map(r => r.requesting_domain_slug), ...requests.map(r => r.fulfilling_domain_slug).filter(Boolean)])],
|
||||
{label: "Domain", value: "all"}
|
||||
);
|
||||
display(html`<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem">
|
||||
${typeFilter}${statFilter}${domFilter}
|
||||
</div>`);
|
||||
```
|
||||
|
||||
```js
|
||||
const tf = typeFilter.value;
|
||||
const sf = statFilter.value;
|
||||
const df = domFilter.value;
|
||||
|
||||
const filtered = requests.filter(r =>
|
||||
(tf === "all" || r.capability_type === tf) &&
|
||||
(sf === "all" || r.status === sf) &&
|
||||
(df === "all" || r.requesting_domain_slug === df || r.fulfilling_domain_slug === df)
|
||||
);
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
```js
|
||||
const priorityColors = {critical: "#e53935", high: "orange", medium: "steelblue", low: "#aaa"};
|
||||
display(html`<div class="grid grid-cols-4" style="gap:1rem;margin-bottom:1.5rem">
|
||||
<div class="card"><h3>Requested</h3><p class="big-num">${requests.filter(r => r.status === "requested").length}</p></div>
|
||||
<div class="card"><h3>In Progress</h3><p class="big-num">${requests.filter(r => ["accepted","in_progress"].includes(r.status)).length}</p></div>
|
||||
<div class="card"><h3>Ready for Review</h3><p class="big-num">${requests.filter(r => r.status === "ready_for_review").length}</p></div>
|
||||
<div class="card"><h3>Completed</h3><p class="big-num">${completed.length}</p></div>
|
||||
</div>`);
|
||||
```
|
||||
|
||||
## Status Kanban
|
||||
|
||||
```js
|
||||
const statusCols = [
|
||||
{key: "requested", label: "Requested", color: "steelblue"},
|
||||
{key: "accepted", label: "Accepted", color: "#f0a500"},
|
||||
{key: "in_progress", label: "In Progress", color: "#2196f3"},
|
||||
{key: "ready_for_review", label: "Ready for Review", color: "#4caf50"},
|
||||
{key: "completed", label: "Completed", color: "#2e7d32"},
|
||||
{key: "rejected", label: "Rejected", color: "#e53935"},
|
||||
{key: "withdrawn", label: "Withdrawn", color: "#bbb"},
|
||||
];
|
||||
|
||||
const colMap = {};
|
||||
for (const r of filtered) {
|
||||
(colMap[r.status] = colMap[r.status] ?? []).push(r);
|
||||
}
|
||||
|
||||
const activeCols = statusCols.filter(s => colMap[s.key]?.length);
|
||||
if (activeCols.length === 0) {
|
||||
display(html`<p style="color:gray">No capability requests match the current filters.</p>`);
|
||||
} else {
|
||||
const ageDays = (r) => ((Date.now() - new Date(r.created_at)) / 86400000).toFixed(0);
|
||||
display(html`<div class="kanban">
|
||||
${activeCols.map(s => html`
|
||||
<div class="kanban-col">
|
||||
<div class="kanban-header" style="border-bottom:2px solid ${s.color}">${s.label} <span class="kanban-count">${colMap[s.key].length}</span></div>
|
||||
${colMap[s.key].map(r => html`
|
||||
<div class="cap-card">
|
||||
<div class="cap-type-badge" style="background:${priorityColors[r.priority] ?? '#aaa'}20;color:${priorityColors[r.priority] ?? '#aaa'}">${r.capability_type}</div>
|
||||
<div class="cap-priority-badge" style="color:${priorityColors[r.priority] ?? '#888'}">${r.priority}</div>
|
||||
<div class="cap-title">${r.title}</div>
|
||||
<div class="cap-domains">
|
||||
<span>${r.requesting_domain_slug}</span>
|
||||
${r.fulfilling_domain_slug ? html` → <strong>${r.fulfilling_domain_slug}</strong>` : html` → <em>unassigned</em>`}
|
||||
</div>
|
||||
<div class="cap-age">${ageDays(r)}d old</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`)}
|
||||
</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
## All Requests
|
||||
|
||||
```js
|
||||
display(Inputs.table(filtered.map(r => ({
|
||||
Type: r.capability_type,
|
||||
Title: r.title,
|
||||
Priority: r.priority,
|
||||
Status: r.status,
|
||||
Requester: r.requesting_domain_slug,
|
||||
Provider: r.fulfilling_domain_slug ?? "—",
|
||||
Agent: r.requesting_agent,
|
||||
Created: new Date(r.created_at).toLocaleDateString(),
|
||||
})), {maxWidth: 1000}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Capability Catalog
|
||||
|
||||
```js
|
||||
// Live poll for catalog entries
|
||||
const catalogState = (async function*() {
|
||||
while (true) {
|
||||
let data = [];
|
||||
try {
|
||||
const r = await fetch(`${API}/capability-catalog/?status=all`);
|
||||
if (r.ok) data = await r.json();
|
||||
} catch {}
|
||||
yield data;
|
||||
await new Promise(res => setTimeout(res, POLL));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```js
|
||||
const catalog = catalogState ?? [];
|
||||
```
|
||||
|
||||
```js
|
||||
if (catalog.length === 0) {
|
||||
display(html`<p style="color:gray">No capabilities registered yet. Add <code>```capability</code> blocks to SCOPE.md files and run <code>make ingest-capabilities-all</code>.</p>`);
|
||||
} else {
|
||||
// Group by domain
|
||||
const byDomain = {};
|
||||
for (const c of catalog) {
|
||||
(byDomain[c.domain_slug] = byDomain[c.domain_slug] ?? []).push(c);
|
||||
}
|
||||
const typeColors = {
|
||||
infrastructure: "#e65100", api: "#1565c0", data: "#2e7d32",
|
||||
security: "#c62828", documentation: "#6a1b9a", other: "#888"
|
||||
};
|
||||
display(html`<div class="catalog-grid">
|
||||
${Object.entries(byDomain).sort((a, b) => a[0].localeCompare(b[0])).map(([domain, caps]) => html`
|
||||
<div class="catalog-domain">
|
||||
<div class="catalog-domain-header">${domain} <span class="kanban-count">${caps.length}</span></div>
|
||||
${caps.map(c => html`
|
||||
<div class="catalog-entry ${c.status === 'deprecated' ? 'catalog-deprecated' : ''}">
|
||||
<div class="cap-type-badge" style="background:${(typeColors[c.capability_type] ?? '#888')}18;color:${typeColors[c.capability_type] ?? '#888'}">${c.capability_type}</div>
|
||||
<div class="catalog-title">${c.title}</div>
|
||||
${c.description ? html`<div class="catalog-desc">${c.description}</div>` : ""}
|
||||
${c.keywords?.length ? html`<div class="catalog-kw">${c.keywords.map(k => html`<span class="kw-tag">${k}</span>`)}</div>` : ""}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`)}
|
||||
</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
<style>
|
||||
.live-indicator { font-size: 0.8rem; color: gray; padding: 0.55rem 0.7rem; margin-bottom: 0.75rem; }
|
||||
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
|
||||
.big-num { font-size: 2.2rem; font-weight: bold; margin: 0.25rem 0; }
|
||||
.kanban { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
||||
.kanban-col { background: var(--theme-background-alt); border-radius: 8px; padding: 0.75rem; }
|
||||
.kanban-header { font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; padding-bottom: 0.5rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.4rem; }
|
||||
.kanban-count { font-size: 0.75rem; background: var(--theme-background); border-radius: 10px; padding: 0.1rem 0.4rem; font-weight: 500; }
|
||||
.cap-card { background: var(--theme-background); border-radius: 6px; padding: 0.6rem 0.75rem; margin-bottom: 0.5rem; }
|
||||
.cap-type-badge { display: inline-block; font-size: 0.65rem; font-weight: 700; border-radius: 3px; padding: 0.1rem 0.35rem; margin-bottom: 0.15rem; }
|
||||
.cap-priority-badge { display: inline-block; font-size: 0.6rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-left: 0.3rem; }
|
||||
.cap-title { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.2rem; line-height: 1.3; }
|
||||
.cap-domains { font-size: 0.75rem; color: steelblue; font-family: monospace; }
|
||||
.cap-age { font-size: 0.7rem; color: gray; margin-top: 0.3rem; }
|
||||
.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
||||
.catalog-domain { background: var(--theme-background-alt); border-radius: 8px; padding: 0.75rem; }
|
||||
.catalog-domain-header { font-weight: 600; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; padding-bottom: 0.5rem; margin-bottom: 0.5rem; border-bottom: 2px solid var(--theme-foreground-faint, #ddd); display: flex; align-items: center; gap: 0.4rem; }
|
||||
.catalog-entry { background: var(--theme-background); border-radius: 6px; padding: 0.6rem 0.75rem; margin-bottom: 0.5rem; }
|
||||
.catalog-deprecated { opacity: 0.5; }
|
||||
.catalog-title { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.15rem; }
|
||||
.catalog-desc { font-size: 0.75rem; color: var(--theme-foreground-muted, #666); line-height: 1.35; margin-bottom: 0.3rem; }
|
||||
.catalog-kw { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
.kw-tag { font-size: 0.6rem; background: var(--theme-background-alt, #f0f0f0); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 3px; padding: 0.05rem 0.3rem; font-family: monospace; color: var(--theme-foreground-muted, #666); }
|
||||
</style>
|
||||
228
dashboard/src/docs/capabilities.md
Normal file
228
dashboard/src/docs/capabilities.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: Capabilities — Reference
|
||||
---
|
||||
|
||||
# Capabilities — Reference
|
||||
|
||||
The Capability Requests page shows cross-domain provisioning requests — a
|
||||
decoupled mechanism for one domain to request something that another domain is
|
||||
responsible for, without needing to know *who* is responsible.
|
||||
|
||||
---
|
||||
|
||||
## What is a capability?
|
||||
|
||||
A **capability** is something a domain can provide to the broader ecosystem —
|
||||
infrastructure provisioning, API endpoints, security tooling, documentation,
|
||||
data pipelines, etc. Capabilities are registered in the **capability catalog**
|
||||
so the system knows which domain provides what.
|
||||
|
||||
A **capability request** is a structured declaration from a requester
|
||||
("I need X") that the system routes to the right provider automatically.
|
||||
|
||||
---
|
||||
|
||||
## Capability catalog
|
||||
|
||||
The catalog is the routing backbone. Each entry registers one thing a domain
|
||||
can provide.
|
||||
|
||||
**Origin of truth: SCOPE.md** — following ADR-001, capability declarations
|
||||
live in each repo's `SCOPE.md` file under the `## Provided Capabilities`
|
||||
section. The state-hub catalog table is a derived index, reconstructable from
|
||||
repo files via `make ingest-capabilities-all`.
|
||||
|
||||
### SCOPE.md capability blocks
|
||||
|
||||
Add fenced `capability` blocks to your repo's SCOPE.md:
|
||||
|
||||
````markdown
|
||||
## Provided Capabilities
|
||||
|
||||
```capability
|
||||
type: infrastructure
|
||||
title: Cluster provisioning
|
||||
description: Provision k8s clusters and managed instances for any domain.
|
||||
keywords: [cluster, k8s, privacy, instance]
|
||||
```
|
||||
````
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| **type** | Category — `infrastructure`, `api`, `data`, `security`, `documentation`, `other` |
|
||||
| **title** | Short name (unique within domain + type) |
|
||||
| **description** | What this capability provides, in one or two sentences |
|
||||
| **keywords** | Routing hints matched against request descriptions |
|
||||
|
||||
### Ingesting into the catalog
|
||||
|
||||
```bash
|
||||
make ingest-capabilities REPO=the-custodian # single repo
|
||||
make ingest-capabilities-all # all registered repos
|
||||
make ingest-capabilities REPO=railiance-infra DRY_RUN=1 # preview
|
||||
```
|
||||
|
||||
The ingest script reads `SCOPE.md` → parses `capability` blocks → upserts into
|
||||
the `capability_catalog` table via the API. Existing entries (same domain + type
|
||||
+ title) are skipped.
|
||||
|
||||
### Browsing the catalog
|
||||
|
||||
Via MCP:
|
||||
```
|
||||
list_capabilities(domain="railiance")
|
||||
```
|
||||
|
||||
Via API:
|
||||
```
|
||||
GET /capability-catalog/?domain=railiance
|
||||
```
|
||||
|
||||
The catalog is also shown at the bottom of the Capabilities dashboard page,
|
||||
grouped by domain with type badges and keyword tags.
|
||||
|
||||
---
|
||||
|
||||
## Routing algorithm
|
||||
|
||||
When a request is created, the system auto-routes it:
|
||||
|
||||
1. **Exact type match** — find catalog entries where `capability_type` matches
|
||||
2. **Single match** — auto-assign the providing domain
|
||||
3. **Multiple matches** — keyword-score the request description against each entry's keywords; pick the winner if unambiguous
|
||||
4. **No match or tie** — leave the provider unassigned and **broadcast** a notification to all domains so one can claim it
|
||||
|
||||
This means the requester never needs to know which domain owns a capability.
|
||||
|
||||
---
|
||||
|
||||
## Request lifecycle
|
||||
|
||||
```
|
||||
requested → accepted → in_progress → ready_for_review → completed
|
||||
↓ ↓ ↓ ↓
|
||||
withdrawn rejected withdrawn withdrawn
|
||||
withdrawn
|
||||
```
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| **requested** | Need declared; routed (or broadcast) to provider |
|
||||
| **accepted** | Provider acknowledged and claimed the request |
|
||||
| **in_progress** | Provider is actively working on it |
|
||||
| **ready_for_review** | Provider finished; requester should review and optimise |
|
||||
| **completed** | Requester confirmed; capability is live |
|
||||
| **rejected** | Provider cannot or will not fulfil the request |
|
||||
| **withdrawn** | Requester cancelled the request |
|
||||
|
||||
Transitions are enforced by the API — you cannot skip stages. Terminal states
|
||||
(`completed`, `rejected`, `withdrawn`) allow no further transitions.
|
||||
|
||||
---
|
||||
|
||||
## Auto-notifications
|
||||
|
||||
Every lifecycle transition creates an **AgentMessage** atomically:
|
||||
|
||||
| Transition | Notification to |
|
||||
|------------|----------------|
|
||||
| **requested** | Provider domain agent (or `broadcast` if unrouted) |
|
||||
| **accepted** | Requesting agent |
|
||||
| **in_progress** | Requesting agent |
|
||||
| **ready_for_review** | Requesting agent |
|
||||
| **completed** | Requesting agent |
|
||||
| **rejected** | Requesting agent (with reason) |
|
||||
|
||||
Notifications appear in the [Inbox](/inbox) page and are queryable via
|
||||
`get_messages(to_agent="<your-agent>")`.
|
||||
|
||||
---
|
||||
|
||||
## Auto-unblock
|
||||
|
||||
A request can optionally link to a **blocking task** via `blocking_task_id`.
|
||||
When the request reaches `completed`, the system automatically patches that
|
||||
task from `blocked` → `todo` and clears its `blocking_reason`. This means
|
||||
blocked work resumes without manual intervention.
|
||||
|
||||
---
|
||||
|
||||
## Creating a request
|
||||
|
||||
Via MCP:
|
||||
|
||||
```
|
||||
request_capability(
|
||||
title = "Privacy idea instance on cluster",
|
||||
description = "Need a privacy idea instance provisioned on the k8s cluster",
|
||||
capability_type = "infrastructure",
|
||||
requesting_agent = "net-kingdom-worker",
|
||||
requesting_domain = "custodian",
|
||||
requesting_workstream_id = "<uuid>", # optional
|
||||
priority = "high", # low | medium | high | critical
|
||||
blocking_task_id = "<task-uuid>" # optional — auto-unblocked on completion
|
||||
)
|
||||
```
|
||||
|
||||
The system routes this to `railiance` (if a matching catalog entry exists),
|
||||
creates an AgentMessage notification, and returns the request with
|
||||
`fulfilling_domain_slug: "railiance"`.
|
||||
|
||||
---
|
||||
|
||||
## Accepting and fulfilling
|
||||
|
||||
The provider agent checks their inbox, sees the request, and accepts:
|
||||
|
||||
```
|
||||
accept_capability_request(
|
||||
request_id = "<uuid>",
|
||||
fulfilling_agent = "railiance-worker",
|
||||
fulfilling_workstream_id = "<uuid>" # optional
|
||||
)
|
||||
```
|
||||
|
||||
Then advances through the lifecycle:
|
||||
|
||||
```
|
||||
update_capability_request_status(request_id, "in_progress")
|
||||
update_capability_request_status(request_id, "ready_for_review", note="Instance up at 10.0.1.42")
|
||||
```
|
||||
|
||||
The requester reviews and completes:
|
||||
|
||||
```
|
||||
update_capability_request_status(request_id, "completed", note="Verified, looks good")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard
|
||||
|
||||
The Capabilities page shows:
|
||||
|
||||
- **KPI sidebar** — open count, average fulfillment time, high/critical count
|
||||
- **Summary cards** — requested, in progress, ready for review, completed
|
||||
- **Kanban board** — cards grouped by status column
|
||||
- **Table** — all requests with filters by type, status, and domain
|
||||
|
||||
Each card shows the capability type, priority, requester → provider domains,
|
||||
and age in days.
|
||||
|
||||
---
|
||||
|
||||
## Relation to other concepts
|
||||
|
||||
| Concept | Relationship |
|
||||
|---------|-------------|
|
||||
| **SCOPE.md** | Defines what a repo *is responsible for* — the catalog registers what it *can provide* |
|
||||
| **Dependencies** | Workstream-to-workstream edges — capabilities are higher-level, domain-to-domain |
|
||||
| **Extension Points** | Design forks for *future* enhancement — capabilities are *operational* requests |
|
||||
| **Contributions** | Outbound upstream work — capabilities are *inbound* requests between internal domains |
|
||||
| **Human Interventions** | Flagged tasks for Bernd — capabilities are agent-to-agent coordination |
|
||||
|
||||
---
|
||||
|
||||
*Capability requests are a sanctioned write use case of the State Hub alongside
|
||||
`resolve_decision` and `get_next_steps`. They do not originate in workplan files —
|
||||
they are operational coordination.*
|
||||
177
dashboard/src/docs/scope.md
Normal file
177
dashboard/src/docs/scope.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
title: SCOPE.md — Reference
|
||||
---
|
||||
|
||||
# SCOPE.md — Reference
|
||||
|
||||
SCOPE.md is a lightweight, strategic orientation artifact placed at the root of
|
||||
every registered repository. It helps humans and agents quickly understand what
|
||||
a repo is about, when it is relevant, and where it fits in the ecosystem.
|
||||
|
||||
---
|
||||
|
||||
## What SCOPE.md is
|
||||
|
||||
SCOPE.md answers these questions **in under 60 seconds**:
|
||||
|
||||
- What is this repository for?
|
||||
- Should I care about it right now?
|
||||
- When is it relevant to my work?
|
||||
- Where does it fit in the ecosystem?
|
||||
- Is it mature enough to trust or reuse?
|
||||
- Does it overlap with something else?
|
||||
|
||||
It is **not** a README, not architecture documentation, and not marketing text.
|
||||
It is a pragmatic, scannable boundary definition.
|
||||
|
||||
---
|
||||
|
||||
## Template structure
|
||||
|
||||
Every SCOPE.md follows an 11-section template:
|
||||
|
||||
| Section | Purpose |
|
||||
|---------|---------|
|
||||
| **One-liner** | One precise sentence describing the repo's purpose |
|
||||
| **Core Idea** | Main capability and what problem it solves |
|
||||
| **In Scope** | What the repo is explicitly responsible for — concrete, not vague |
|
||||
| **Out of Scope** | What it deliberately does NOT do (often more important) |
|
||||
| **Relevant When** | Real usage scenarios when someone should consider this repo |
|
||||
| **Not Relevant When** | When someone should look elsewhere |
|
||||
| **Current State** | Maturity indicators: status, implementation, stability, usage |
|
||||
| **How It Fits** | Upstream dependencies, downstream consumers, often-used-with |
|
||||
| **Terminology** | Domain terms, potential confusions with similar concepts |
|
||||
| **Related / Overlapping** | Repos with similar or adjacent responsibilities |
|
||||
| **Provided Capabilities** | What this repo's domain can provide to others on request |
|
||||
| **Getting Oriented** | Entry points, key files, where to start |
|
||||
|
||||
The template is at `state-hub/scripts/project_rules/scope.template`.
|
||||
|
||||
---
|
||||
|
||||
## Current State indicators
|
||||
|
||||
The Current State section uses four axes:
|
||||
|
||||
| Axis | Values |
|
||||
|------|--------|
|
||||
| **Status** | concept / experimental / active / stable / deprecated |
|
||||
| **Implementation** | idea / partial / substantial / complete |
|
||||
| **Stability** | unstable / evolving / stable |
|
||||
| **Usage** | none / personal / internal / production |
|
||||
|
||||
These help an agent decide whether to depend on, extend, or avoid a repo
|
||||
without needing to read its full codebase.
|
||||
|
||||
---
|
||||
|
||||
## Design principles
|
||||
|
||||
- **Intentionally short and scannable** — not comprehensive documentation
|
||||
- **Pragmatic** — real usage scenarios, not ideals
|
||||
- **Easy to maintain** — update when scope changes, not on every commit
|
||||
- **Direct language** — no filler, no marketing, no invented features
|
||||
- **Honest about gaps** — if something is incomplete or unstable, say so
|
||||
|
||||
**Anti-goals:**
|
||||
- No long prose or verbose explanations
|
||||
- No repetition of README content
|
||||
- No hiding ambiguity behind vague language
|
||||
- No assumption of production readiness
|
||||
|
||||
---
|
||||
|
||||
## How SCOPE.md is created
|
||||
|
||||
### New repositories
|
||||
|
||||
When a repo is registered via `make register-project`, the scaffold copies
|
||||
`scope.template` → `SCOPE.md` at the repo root. The human or an agent then
|
||||
populates the sections from the repo's actual state.
|
||||
|
||||
### Existing repositories
|
||||
|
||||
The **scope-analyst** kaizen agent persona can be loaded to generate or refine
|
||||
a SCOPE.md:
|
||||
|
||||
```
|
||||
get_kaizen_agent("scope-analyst")
|
||||
```
|
||||
|
||||
This agent reads the repo's codebase, existing documentation, and CLAUDE.md
|
||||
to produce a SCOPE.md that accurately reflects the current state.
|
||||
|
||||
---
|
||||
|
||||
## Ecosystem coverage
|
||||
|
||||
SCOPE.md files exist across all custodian domains:
|
||||
|
||||
| Domain | Repos with SCOPE.md |
|
||||
|--------|-------------------|
|
||||
| **custodian** | the-custodian, kaizen-agentic, ops-bridge, activity-core |
|
||||
| **custodian** (netkingdom) | net-kingdom, key-cape |
|
||||
| **railiance** | railiance-apps, railiance-cluster, railiance-enablement, railiance-infra, railiance-platform |
|
||||
| **markitect** | markitect_project |
|
||||
|
||||
---
|
||||
|
||||
## Provided Capabilities section
|
||||
|
||||
The `## Provided Capabilities` section uses fenced `capability` blocks that
|
||||
are machine-readable and ingested into the state-hub capability catalog:
|
||||
|
||||
````markdown
|
||||
```capability
|
||||
type: infrastructure
|
||||
title: Cluster provisioning
|
||||
description: Provision k8s clusters and managed instances for any domain.
|
||||
keywords: [cluster, k8s, privacy, instance]
|
||||
```
|
||||
````
|
||||
|
||||
| Field | Required | Purpose |
|
||||
|-------|----------|---------|
|
||||
| **type** | yes | Category: `infrastructure`, `api`, `data`, `security`, `documentation`, `other` |
|
||||
| **title** | yes | Short name (unique within domain + type) |
|
||||
| **description** | no | What this capability provides |
|
||||
| **keywords** | no | Routing hints for auto-matching capability requests |
|
||||
|
||||
The ingest script (`make ingest-capabilities-all`) parses these blocks from all
|
||||
registered repos and populates the state-hub catalog table. This follows
|
||||
ADR-001: **files are the origin of truth, DB is cache/index**.
|
||||
|
||||
---
|
||||
|
||||
## Relation to capabilities
|
||||
|
||||
SCOPE.md is both the **human-readable boundary definition** and the **origin of
|
||||
truth for the capability catalog**:
|
||||
|
||||
| | SCOPE.md | Capability Catalog (DB) |
|
||||
|-|----------|------------------------|
|
||||
| **Role** | Origin of truth | Derived index |
|
||||
| **Granularity** | Per-repository | Per-domain (aggregated from repo files) |
|
||||
| **Purpose** | "What is this repo?" + "What can it provide?" | Routing engine for capability requests |
|
||||
| **Updates** | Edit the file, re-ingest | Auto-populated from SCOPE.md |
|
||||
| **Readable without hub** | Yes — just open the file | No — requires API |
|
||||
|
||||
This means a repo is fully self-describing: you can understand what it provides
|
||||
by reading SCOPE.md alone, without any centralized infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Relation to other concepts
|
||||
|
||||
| Concept | Relationship |
|
||||
|---------|-------------|
|
||||
| **CLAUDE.md** | Build/test/lint instructions — *how* to work with the repo |
|
||||
| **SCOPE.md** | Boundary definition — *what* the repo is and isn't |
|
||||
| **Capability Catalog** | Operational routing — *what the domain can provide* on request |
|
||||
| **Domain Goals** | Strategic direction — *where the domain is heading* |
|
||||
| **Project Charters** | Founding intent — *why the domain exists* (in `canon/projects/`) |
|
||||
|
||||
---
|
||||
|
||||
*SCOPE.md is the boundary definition layer. It tells you whether you are in the
|
||||
right place before you start reading code.*
|
||||
Reference in New Issue
Block a user