Capability Layer
Last verified against implementation: 2026-07-15
Vita's capability layer is the typed, transport-independent front door to the
small set of operations that multiple runtimes actually need. A capability is
a named operation such as vita.tasks.create, vita.health.sleep_gate, or
vita.telegram.send. It declares its input and output schemas, side-effect
class, backing source, examples, and implementation once. Python, the CLI,
the internal HTTP API, and the Workshop browser SDK all dispatch through the
same scripts.capabilities.execute() function.
The layer does not attempt to wrap every API router or data directory. It exists to promote proven, repeatedly used operations out of ad hoc source imports and direct file access. This keeps the supported vocabulary small enough to atrophy when it stops being useful while giving important mutations a consistent contract.
The current registry contains 55 capabilities across 10 namespaces. The number and schemas are generated from code; use live discovery rather than treating the count in this narrative page as an API contract.
See Workshop for the primary browser consumer,
Telegram for topic participation and direct sends,
Scheduling for the proactive queue, and
Precedent for the task system behind vita.tasks.*.
Why this layer exists
Before the capability layer, the same operation could be reached through a
FastAPI route, a direct scripts/ import, a hand-written CLI, or a raw read of
data/. Each path could disagree about validation, errors, freshness, and
whether a mutation had actually landed. A caller often had to grep the source
to learn whether failure meant an exception, -1, an empty result, or a
partially written file.
The capability layer addresses that failure class with six decisions:
- One execution path. Every live transport calls
execute(), so behavior parity is structural rather than a convention between adapters. - Typed boundaries. Pydantic input and output models generate the machine schema and reject known bad inputs, including timezone-naive scheduled datetimes.
- Stable, non-throwing results. Capability-level failure is data in a v1
envelope with a stable
error.code, not a sentinel or transport exception. - First-class side effects. Every operation is classified as
read,write,send, orspawn; transports can refuse unsafe classes before an implementation runs. - Mutation attribution. Executed non-read calls append provenance with the capability, actor, transport, request ID, outcome, and bounded input.
- Generated reference. Schemas, namespace reference pages, and MCP-shaped tool declarations are deterministic projections of the registry.
This is intentionally a promoted interface over existing owner modules, not a
replacement for them. For example, vita.tasks.create still delegates to
Precedent and vita.schedule.enqueue_message still delegates to the proactive
queue. The capability wrapper supplies the shared boundary; the domain module
continues to own its storage and business logic.
Architecture
declaration plane
scripts/capabilities/namespaces/*.py
@capability(name, models, side_effect, source, examples)
β import-time registration
βΌ
CapabilityDef registry
β
βββββββββββββββββββββΌββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
discovery + spec generated reference MCP-shaped schemas
GET /capabilities docs/api/capabilities/ mcp-tools.json
GET /spec spec.json + *.md (server not mounted)
execution plane
Python CLI internal HTTP Workshop JavaScript
β β β β
βββββββββββββ΄βββββββββββββββββ΄ββββββββββββββββββββββββ
β
βΌ
scripts.capabilities.execute(name, input, context)
β
resolve β transport policy β idempotency β validate input
β
βΌ
existing owner implementation
β
βΌ
v1 envelope + usage counter + mutation provenance
β
successful mutation only
βΌ
idempotency cache
There are two important separations in this design:
- The registry and codec describe what can be called. They are pure enough to generate stable, diffable artifacts without timestamps.
- The executor decides whether a call may run and normalizes its result. Transport adapters remain thin and do not reimplement domain behavior.
Registry shape
scripts/capabilities/core.py defines CapabilityDef and the
@capability(...) decorator. Every definition supplies:
| Field | Meaning |
|---|---|
name |
Stable vita.<namespace>.<verb> name; lowercase letters and underscores only |
description |
Human and generated-reference description |
side_effect |
One of read, write, send, or spawn |
input_model |
Pydantic model used to validate input and generate JSON Schema |
output_model |
Declared output schema |
impl |
Python callable receiving the validated input and a Context |
source |
Human-readable backing-store or service attribution |
examples |
Example input objects; contract tests validate them against the input model |
Namespace membership is explicit in core.py::NAMESPACE_MODULES; it is not a
directory glob. load() imports those modules once, and registration occurs at
import time. Explicit membership makes deployment order and the supported
surface reviewable. Duplicate names and names outside the required pattern
fail registration.
The canonical import is scripts.capabilities. The package aliases the bare
capabilities module name to the same module object because both repository
root and scripts/ can be on PYTHONPATH; this avoids accidentally creating
two in-memory registries in one process.
Execution lifecycle
scripts/capabilities/execute.py::execute() is the only execution path. A call
passes through these stages in order:
- Load and resolve. Explicit namespace modules are loaded and the name is
resolved. A missing name returns
unknown_capability. - Normalize transport. Known transports are
python,http,js,cli,mcp, and the reservedpublicclass. An unknown value becomesjs, the most restricted live transport. - Enforce transport policy. Browser/
jscalls cannot invokesendorspawn. The reservedpublicclass permits reads only. A refusal returnstransport_forbiddenbefore input validation or implementation execution. - Check replay protection. A non-read call with an idempotency key checks
the 48-hour cache. A matching entry returns the original successful envelope
with
meta.idempotent_replay: true; a key used by another capability returnsidempotency_reuse_mismatch. - Validate input.
input_model.model_validate()converts the raw object into the declared model. Pydantic errors becomeinvalid_input, with the structured validation list inerror.details.errors. - Run the implementation. The wrapper calls the existing domain owner with
the typed input and a
Contextcontaining actor, transport, request ID, optional Workshop page, and optional idempotency key. - Build the envelope. A
BaseModelresult is serialized in JSON mode; a raw result passes through. ACapabilityErrormaps to its declared stable code. Selected upstream exception classes map toupstream_unavailable/admission_rejected; any other implementation exception becomesinternal_errorand is logged. - Record gauges and attribution. Calls that reached the implementation attempt update the daily usage counter. Every executed mutation, whether it succeeded or failed, appends a provenance line. Reads do not append provenance.
- Cache successful mutations. A successful non-read call with an idempotency key stores its complete envelope for later replay. Failed calls are not cached.
Instrumentation failures are deliberately non-fatal to the domain result: usage, provenance, and idempotency-store errors are logged, but do not replace an otherwise valid capability response. This preserves availability, although it also means the ledgers are observability aids rather than a transactional commit record.
Freshness context
The Context lets an implementation override meta.as_of and meta.source
with data-derived truth. For example, a Garmin reader can report the synced
file's timestamp instead of the wall-clock time of the request. If it does not
override them, a successful response uses execution time and the definition's
source string.
Successful envelopes also receive duration_ms. Early failures such as an
unknown capability, forbidden transport, replay-key mismatch, or invalid input
contain the base metadata but do not currently carry the full
as_of/source/duration_ms set. Callers should not assume those optional
diagnostic fields exist on every error.
The v1 envelope
Every server-side transport returns one of these shapes:
{
"ok": true,
"data": {
"is_blocked": false,
"avg_sleep_hours": 6.8,
"days_with_data": 7,
"threshold_hours": 6.5,
"message": "sleep gate open (sanitized example)"
},
"error": null,
"meta": {
"v": 1,
"capability": "vita.health.sleep_gate",
"request_id": "example-request-id",
"actor": "agent:training-plan",
"transport": "python",
"side_effect": "read",
"as_of": "2026-07-15T08:00:00-07:00",
"source": "data/garmin/*.json + data/sleep/overrides.jsonl",
"duration_ms": 4
}
}
{
"ok": false,
"data": null,
"error": {
"code": "invalid_input",
"message": "input failed validation for vita.schedule.enqueue_message",
"retryable": false,
"details": {
"errors": [
{ "type": "timezone_aware", "loc": ["send_at"] }
]
}
},
"meta": {
"v": 1,
"capability": "vita.schedule.enqueue_message",
"request_id": "example-request-id",
"actor": "agent:reminders",
"transport": "python",
"side_effect": "send"
}
}
The examples above are sanitized envelope examples; consult the generated
schema for the complete data shape. The stable contract is the top-level
ok, data, error, and meta split. Capability-level failures are values,
not exceptions, so callers branch on ok and error.code.
Stable server error catalog
| Code | Retryable | Meaning |
|---|---|---|
invalid_input |
no | Typed input validation failed; details carry Pydantic errors |
unknown_capability |
no | The name is absent from the registry |
transport_forbidden |
no | The transport cannot invoke this side-effect class |
admission_rejected |
yes | Queue admission policy rejected the operation |
duplicate_suppressed |
no | Domain deduplication suppressed an in-window duplicate |
denylisted |
no | The trigger or operation is denylisted |
not_found |
no | A referenced entity does not exist |
conflict |
no | Uniqueness or current-state conflict |
upstream_unavailable |
yes | A backing service or index is unavailable |
idempotency_reuse_mismatch |
no | The key was already used for another capability |
internal_error |
no | An unexpected implementation exception occurred |
Codes live in scripts/capabilities/envelope.py::ERROR_CODES. Adding a code is
additive; renaming an existing code would break the v1 contract.
The Workshop SDK adds client-only network_error, page_not_allowed, and
sdk_blocked results. It can also synthesize unknown_capability from its
discovery map. These do not pass through the server error catalog and carry
meta.client_side: true.
Side-effect and mutation policy
Side-effect class describes the operation, not merely the storage mechanism:
| Class | Definition | Examples |
|---|---|---|
read |
Does not mutate domain state | sleep summary, memory search, calendar events |
write |
Mutates local files, SQLite, or Precedent state | task update, page preference, curriculum score |
send |
Causes communication to leave the machine, immediately or through a delivery queue | Telegram send, queued reminder |
spawn |
Starts or schedules an agent/job | deferred prompt, education packet generation |
The mechanically enforced policy is deliberately small:
| Transport | Read | Write | Send | Spawn |
|---|---|---|---|---|
| Python | yes | yes | yes | yes |
| CLI | yes | yes | yes | yes |
Internal HTTP with X-Vita-Transport: http |
yes | yes | yes | yes |
Workshop/JavaScript (js) |
yes | yes | no | no |
Reserved public class |
yes | no | no | no |
The browser SDK also applies a per-page allowlist and blocks send/spawn
client-side, while the server repeats the side-effect check. The allowlist is
an authoring guardrail; the server policy and internal-host perimeter remain
the real boundary.
The HTTP capability router is not allowlisted on Vita's public tunnel domains.
Within localhost/Tailscale, the layer follows Vita's identity-is-the-perimeter
model: there is no endpoint-level authentication. X-Vita-Actor is
attribution supplied by the trusted caller, not cryptographic identity. Raw
internal HTTP can request the powerful http transport, so it must not be
treated as a public API.
Provenance
scripts/capabilities/store.py::record_provenance() appends every executed
non-read call to data/capabilities/provenance.jsonl with:
{
"at": "2026-07-15T08:00:00-07:00",
"capability": "vita.tasks.create",
"side_effect": "write",
"actor": "agent:planning",
"transport": "cli",
"request_id": "example-request-id",
"ok": true,
"input": "{\"text\": \"review the launch checklist\"}"
}
Optional page and error_code fields are added when relevant. The serialized
input is truncated to 800 characters. Validation failures and transport
refusals occur before the implementation and are not provenance entries;
idempotent replays return before writing a second entry.
Provenance answers βwhich supported interface attempted this mutation?β It does not prove that instrumentation and domain data committed atomically. Each wrapper still depends on its owner module to use safe writes or a proper SQLite transaction.
Idempotency
Mutation callers may provide an Idempotency-Key header or the equivalent
Python/CLI argument. The contract is:
- only
write,send, andspawncalls use the key; reads ignore it; - only successful envelopes are stored;
- entries expire after 48 hours and are pruned during lookup;
- replaying the same key for the same capability returns the original envelope
and request ID, plus
meta.idempotent_replay: true; - reusing the key for another capability returns
idempotency_reuse_mismatch; - the key is not a payload hash. Reusing it for the same capability with different input still replays the original result.
Use a key that identifies one intended mutation, not merely a UI session. The
store is data/capabilities/idempotency.db; tests can redirect all layer-owned
state with VITA_CAPABILITIES_DATA_DIR.
Domain-level deduplication remains separate. For example, the scheduling
namespace can report duplicate_suppressed or denylisted even when the
generic idempotency cache was not used.
Transports and discovery
Python
Python is the direct, fully privileged transport:
from scripts.capabilities import execute
result = execute(
"vita.health.sleep_gate",
{"component": "intervals"},
actor="agent:training-plan",
transport="python",
)
if result["ok"]:
blocked = result["data"]["is_blocked"]
else:
code = result["error"]["code"]
actor is required. Import from the package root rather than a namespace
module so the registry alias and single execution path remain intact.
CLI
The CLI is implemented by scripts/capabilities/cli.py and exposed through
./run:
# compact discovery
./run capabilities list
# full schema, optionally filtered by namespace
./run capabilities spec --ns vita.schedule
# read call
./run capabilities call vita.health.sleep_gate \
--input '{"component":"intervals"}' \
--actor 'cli:operator'
# idempotent mutation
./run capabilities call vita.tasks.create \
--input '{"text":"review the launch checklist","due_date":"2026-07-17"}' \
--actor 'agent:planning' \
--idempotency-key 'planning-review-2026-07-17'
call prints the complete JSON envelope. It exits 0 for ok: true, 1 for a
capability error envelope, and 2 when the --input argument is not valid JSON.
Internal HTTP API
api/src/routers/capabilities.py mounts three routes on the internal API at
port 33800:
| Method and path | Purpose |
|---|---|
GET /api/v1/capabilities |
Compact discovery: name, namespace, class, description |
GET /api/v1/capabilities/spec |
Full generated spec; filter with ?namespace=vita.tasks |
POST /api/v1/capabilities/call/{name} |
Execute; request body is the raw input object |
curl -s http://localhost:33800/api/v1/capabilities/call/vita.tasks.create \
-H 'Content-Type: application/json' \
-H 'X-Vita-Transport: http' \
-H 'X-Vita-Actor: agent:planning' \
-H 'Idempotency-Key: planning-review-2026-07-17' \
--data '{"text":"review the launch checklist","due_date":"2026-07-17"}'
Request headers are:
| Header | Behavior |
|---|---|
X-Vita-Transport |
Router accepts http or js; missing/unknown values become js |
X-Vita-Actor |
Attribution; defaults to http:anonymous unless a page is present |
X-Vita-Page |
Workshop page identity; default actor becomes browser:<page> |
Idempotency-Key |
Optional replay protection for non-read operations |
Capability errors return HTTP 200 with ok: false; branch on error.code, not
HTTP status. A malformed request that never reaches the layer, such as a
non-object JSON body, returns HTTP 422.
Workshop JavaScript SDK
workshop/lib/vita.v1.js::createVita() discovers the registry, requires a page
identity, applies exact or vita.namespace.* allow patterns, and sends calls
to the HTTP route with X-Vita-Transport: js and X-Vita-Page.
import { createVita } from '/workshop/lib/vita.v1.js';
const vita = await createVita({
page: 'planning-workbench',
allow: ['vita.tasks.*', 'vita.workshop.*'],
});
const result = await vita.call(
'vita.tasks.create',
{ text: 'review the launch checklist', due_date: '2026-07-17' },
{ idempotencyKey: vita.newIdempotencyKey() },
);
The SDK returns server envelopes verbatim and converts fetch/allowlist failures
into client-side envelopes. Its keepalive option lets a write survive
same-tab navigation. vita.prefs is a convenience wrapper around
vita.workshop.prefs_get and prefs_set for view-chrome state only.
MCP projection
MCP is not a live transport yet. docs/api/capabilities/mcp-tools.json is
a generated, transport-ready projection whose tool names replace dots with
underscores. It includes each input schema plus MCP annotations:
readOnlyHintis true only forreadcapabilities;destructiveHintis currently true forwriteandsendcapabilities.
No MCP server is mounted and nothing dispatches these declarations today. The projection exists so a real consumer can pull the adapter into existence without inventing a second registry. Until that happens, describing MCP as an available way to call Vita would be inaccurate.
Current namespace organization
This is the registry snapshot generated on 2026-07-15. The full field-level contract belongs to the live spec and generated namespace pages.
| Namespace | Class mix | Verbs | Backing domain |
|---|---|---|---|
vita.calendar |
1 read | events |
Canonical read over data/calendar/events.json |
vita.education |
10 read, 5 write, 1 spawn | activity, choose, complete_packet, frontier, generate_packet, graph, kids, map, node, packets, progress, queue, retests, score, set_level, set_status |
Curriculum graph, per-kid ledgers, teacher workflow, packet library, Gumball integration |
vita.health |
3 read | garmin_day, sleep_gate, sleep_summary |
Garmin daily files and sleep overrides |
vita.links |
2 read, 5 write | add, archive, frequents, list, reorder_pins, update, visit |
Start-page link store and read-only Chromium history |
vita.memory |
1 read | search |
Hybrid vector + BM25 search index |
vita.people |
1 read, 5 write | agenda_add, agenda_list, agenda_reorder, agenda_resolve, agenda_update, create |
Precedent person agenda vaults |
vita.schedule |
1 read, 1 write, 1 send, 1 spawn | cancel, enqueue_message, enqueue_prompt, queue_status |
VitaScheduler proactive queue |
vita.tasks |
3 read, 6 write | add_note, agenda, complete, create, delete, habit_log, habits_list, list, update |
Precedent on port 5424 |
vita.telegram |
3 write, 2 send | deregister_participant, open_topic, poll_replies, register_participant, send |
Bot API, topic ledger, participant tables/inbox |
vita.workshop |
2 read, 1 write | pages, prefs_get, prefs_set |
Live page index and page-preference store |
Namespaces are deployable vocabulary, not a claim that every domain operation has been standardized. Large existing systems such as feed, reports, photos, and email still primarily use their established routers and owner modules. They should enter this registry only when a second consumer or recurring hard-way access pattern creates real demand.
Specification, generated reference, and drift control
scripts/capabilities/codec.py is a pure projection of the loaded registry.
It sorts capabilities, emits no timestamps, and generates:
| Artifact | Purpose |
|---|---|
docs/api/capabilities/spec.json |
Complete machine spec: envelope, HTTP contract, transports, errors, namespaces, schemas, examples |
docs/api/capabilities/mcp-tools.json |
MCP-shaped input schemas and side-effect annotations |
docs/api/capabilities/index.md |
Generated envelope/error/transport summary and capability index |
docs/api/capabilities/vita.<namespace>.md |
Field tables and examples for one namespace |
The files under docs/api/capabilities/ are generated artifacts; do not edit
them by hand. They are repository references, separate from the authored
MkDocs pages under docs/pages/. The full machine spec is also available live
from the internal /api/v1/capabilities/spec endpoint.
Regenerate after any definition, schema, description, example, or namespace change:
./run gen-capabilities
Check without writing:
./run capabilities gen --check
The gen --check path compares every generated artifact. The contract test in
tests/test_capabilities.py::test_spec_artifacts_are_current independently
compares the committed spec.json with the live registry, while
test_spec_covers_all_capabilities verifies registry coverage. Other tests
assert envelope shape, stable errors, timezone-aware boundaries, transport
denials, provenance, idempotency, namespace behavior, and that declared
examples validate.
This division is intentional:
- This page explains architecture, policy, and limitations to a reader who cannot inspect the repository.
- Generated namespace pages and
spec.jsonare the exact callable reference and should change mechanically with the code.
Layer-owned data and state
The registry itself is in-memory code. The shared layer owns only a small amount of instrumentation and replay state:
| Path | Format | Purpose |
|---|---|---|
data/capabilities/provenance.jsonl |
append-only JSONL | Executed mutation attribution and bounded input |
data/capabilities/usage.db |
SQLite | Daily calls/errors by capability and transport |
data/capabilities/idempotency.db |
SQLite | Successful mutation envelopes, retained for 48-hour replay |
data/sleep/staging/capabilities.json |
generated JSON | Optional nightly digest consumed by the /sleep gather path |
docs/api/capabilities/ |
generated JSON/Markdown | Committed machine and field-level reference |
VITA_CAPABILITIES_DATA_DIR relocates the first three stores. Contract tests
use a temporary directory so they never modify production instrumentation.
Namespace implementations own and isolate their own domain state separately.
Usage rows contain counts only: capability, local day, transport, calls, and errors. Mutation content belongs in provenance, while read inputs are not recorded. Calls rejected before implementation execution and idempotent replays are not included in the usage counter.
Promotion and demotion loop
The layer is designed to maintain itself without turning βAPI coverageβ into a target. It has one gauge for operations that may be dying and another for hard-way access that may deserve promotion.
Demotion digest
scripts/capabilities/digest.py::build_digest() combines the registry,
usage.db, and recent provenance into:
- call and error counts per capability;
- last-called date and transport mix;
never_called,stale, anderror_heavyflags;- mutation volume by transport and actor over the last seven days.
The current thresholds are quiet for more than 60 days for stale, and at
least five calls with an error rate of 20% or higher for error_heavy. These
are explicitly gauges, not targets. A stale flag asks whether a capability
should be pruned; it is not a reason to manufacture usage.
./run capabilities digest
./run capabilities digest --stage
--stage writes data/sleep/staging/capabilities.json. The digest reports
tracking_since because atrophy flags are not meaningful until the counter
has accumulated enough history.
Promotion miner
scripts/capabilities/promotion_miner.py::mine() passively scans recent Claude
Code tool-use transcripts for repeated data-plane access performed the
hard way: direct reads, greps/globs, shell access to data/, memory/,
profile/, or goals/, and sqlite3 calls into databases.
The current algorithm:
- scans files modified in the last seven days, newest first;
- stops at an 800 MiB total scan ceiling and reports truncation;
- clusters calls by access kind and data domain;
- drops clusters seen in fewer than two sessions;
- ranks the remainder by the declared proxy
sessions Γ calls; - returns the top 15;
- labels a known promoted domain as
migrationand a new domain aspromotion.
It deliberately excludes source-code reads as development noise and reports
failure as UNKNOWN, never as an empty candidate list. The score is a proxy
for repeated friction, not token cost or business value. The nightly digest
places candidate selection back in a human taste loop: promote the operation
only if a real consumer and stable owner contract exist.
Adding or promoting a capability
A new capability should be pulled into the layer by demonstrated demand, not by a coverage goal:
- Name the waiting consumer and hard-way path. If no caller needs the operation, leave the owner module alone.
- Keep one owner. Wrap the existing domain function; do not copy its business logic into the capability namespace.
- Declare the boundary. Add Pydantic input/output models and a
@capabilitydefinition underscripts/capabilities/namespaces/<domain>.py. Give it an honest side-effect class, source, description, and sanitized examples. - Add explicit membership. For a new namespace, append its module to
core.py::NAMESPACE_MODULES. - Normalize failures. Convert expected domain failures into existing
CapabilityErrorcodes. Add a new stable code only when the distinction is meaningful to callers. - Preserve owner integrity. Use the domain's atomic writer or SQLite owner module. The generic executor does not make an unsafe implementation atomic.
- Test the contract. Cover validation, failures, side-effect transport policy, real owner behavior, and mutation isolation. Add an example that validates against the input model.
- Regenerate and check. Run
./run gen-capabilities, the capability tests, and./run capabilities gen --check. - Migrate one real caller. The capability has not earned its existence until a consumer stops using the hard path.
- Watch the gauges. Let usage and promotion/demotion reporting reveal whether the wrapper is useful, broken, or obsolete.
Known limitations and improvement opportunities
The layer is useful precisely because its boundary is explicit. The following gaps are real, not implied future features:
- No mounted MCP server. The JSON tool projection is generated but cannot be invoked. Build the adapter only when an external MCP consumer is waiting.
- Output schemas are declared but not universally enforced at runtime.
BaseModelresults serialize safely, but a wrapper returning a rawdictpasses through withoutoutput_model.model_validate(). Validating every result would make the machine spec a stronger runtime guarantee. - Atomicity is delegated. Provenance and idempotency are not in the same
transaction as a domain mutation, and the registry does not lint an
implementation's write method. Owner-module transactions and
atomic_*calls remain essential. - Policy is coarse. The registry stores side-effect class, but not per-capability authorization, review gates, rate limits, or destructive confirmation policy. Domain wrappers enforce their own admission and dedup rules.
- Internal HTTP trusts caller headers. Actor/page values are useful provenance inside the trusted perimeter, not authentication. A future wider network surface would need server-derived identity and explicit allowlists.
- Idempotency does not bind the payload. Same key + same capability replays even if a caller accidentally changes the input. Persisting an input hash would turn that mistake into a conflict.
- Instrumentation is best-effort. A failed usage/provenance/idempotency write logs and returns the domain result. A health signal should make such gaps visible without making the primary operation unavailable.
- Early failures are undercounted. Usage and provenance do not capture unknown names, transport refusals, input failures, or replays. A separate boundary-rejection gauge would improve migration and abuse diagnostics.
- Promotion mining is runtime-biased. It reads the Claude Code transcript corpus only, so direct access from Codex, services, or other runtimes is invisible. A shared multi-runtime tool-event ledger would give a truer demand signal.
- MCP annotations are incomplete for spawn. The current projection marks
writeandsendas destructive but notspawn; a future live adapter should review that policy before exposing tools. - Generated references live outside the MkDocs source directory.
docs/api/capabilities/is the committed machine reference while this authored site builds fromdocs/pages/. The live internal spec closes the machine-consumer gap, but serving field-level generated references on the docs site would improve human discoverability.
These are improvement candidates, not reasons to route around the layer. A new direct file reader or bespoke endpoint recreates the original split-brain problem; improve the shared boundary when a real consumer exposes a gap.
File map
| Path | Responsibility |
|---|---|
scripts/capabilities/core.py |
Registry, decorator, context, explicit namespace loading, side-effect enum |
scripts/capabilities/execute.py |
Single dispatch path, policy, validation, error mapping, envelope, instrumentation |
scripts/capabilities/envelope.py |
Frozen v1 envelope helpers and stable error catalog |
scripts/capabilities/codec.py |
Deterministic spec, Markdown, and MCP projections |
scripts/capabilities/cli.py |
list, spec, call, gen, and digest commands |
scripts/capabilities/store.py |
Provenance, usage counters, 48-hour idempotency cache |
scripts/capabilities/digest.py |
Demotion/health gauge and /sleep staging output |
scripts/capabilities/promotion_miner.py |
Passive repeated hard-way-access miner |
scripts/capabilities/namespaces/*.py |
Pydantic contracts and wrappers around domain owners |
api/src/routers/capabilities.py |
Thin internal HTTP discovery/spec/call transport |
workshop/lib/vita.v1.js |
Browser discovery, page allowlist, client envelopes, calls, preferences |
docs/api/capabilities/ |
Generated machine spec, MCP schemas, index, and namespace references |
tests/test_capabilities.py |
Core contract plus namespace behavior and artifact drift tests |
tests/test_promotion_miner.py |
Miner clustering, recurrence, truncation, and failure-honesty tests |
data/reports/2026-07-11-vita-api-standardization-survey.md |
Original design survey and failure-class traceability |
The shortest reliable way to inspect the live surface is:
./run capabilities list
./run capabilities spec
./run capabilities gen --check