Workshop
Last verified against implementation: 2026-07-15
The Vita Workshop is the fast, agent-authored interface layer over Vita's capability system. It turns a prompt such as βbuild a workshop page for standing agendasβ into a live, writable page without adding a React feature, a bespoke API, or another data store.
The central distinction is:
- The web application is the product: durable, curated, tested, and higher ceremony.
- The Workshop is the workbench: personal, prompt-speed, disposable, and expected to change quickly.
A useful Workshop page can graduate into the React application. A page that stops being useful can be deleted without a migration project because its domain state never belonged to the page in the first place.
This is different from the Forge Workshop, which is the
problem-first idea-development pipeline. βWorkshopβ in this document means the
live HTML workbench served at /workshop/.
Why it exists
Before the Workshop, a personal dashboard usually forced one of two bad trade-offs:
- make a permanent product feature before the interaction has earned that permanence; or
- make a one-off page that reads files or invents private endpoints, creating another data island.
The Workshop avoids both. Pages are cheap, but their data access is not ad hoc. Every domain read or write goes through the same typed, provenance-stamped capability used by agents, the CLI, and other interfaces.
The design is based on four rules:
- No page-owned domain data. Pages compose capabilities; they do not own a database or read files directly.
- Freshness is visible. A page renders
meta.as_ofandmeta.sourceso a stale or unknown answer cannot masquerade as a current zero. - Rejections are visible. A policy rejection is a distinct amber state, not a silent failed click and not a generic red system error.
- Successful patterns graduate. Shared UI becomes a block; an enduring workflow becomes a product feature.
System shape
prompt: "build a workshop page for X"
β
βΌ
workshop/pages/<name>/
βββ index.html
βββ app.js
β imports
βββββββββββ΄ββββββββββ
βΌ βΌ
workshop/lib/vita.v1.js workshop/components/vita-blocks.js
data + policy SDK reusable UI vocabulary
β
βΌ
POST /api/v1/capabilities/call/<name>
β
βΌ
typed capability β existing owner module β canonical domain store
β
βββ v1 result envelope + provenance
βββ usage / idempotency / policy records
discovery side path:
workshop/pages/* β scan_pages()
βββ GET /api/v1/workshop/pages
βββ vita.workshop.pages
βββ React /workbench index
There is no Workshop build system. api/src/main.py mounts the repository's
workshop/ directory with FastAPI StaticFiles, so a new page is available as
soon as its files exist. The web application on port 33801 proxies the same
paths.
The two shared spines
| Spine | Location | Responsibility |
|---|---|---|
| Data | workshop/lib/vita.v1.js |
Capability discovery, page allowlist, calls, v1 envelopes, idempotency keys, page preferences |
| UI | workshop/components/vita-blocks.js |
Framework-independent web components shared across pages |
Keeping these separate matters. A block can change presentation without creating a second data path, while a page can use a new capability without adding a React dependency or bundler.
Page lifecycle
1. Demand appears
The trigger is normally natural language: βbuild a workshop page for X,β βput
X on the workshop,β or βI want a HUD for Y.β The page-building workflow is
defined in .claude/skills/workshop-page/SKILL.md.
Before authoring UI, the agent inspects the promoted vocabulary with:
./run capabilities list
or the generated reference under docs/api/capabilities/. If the required
operation is absent, the page is treated as a demand signal for a new
capability. The agent wraps the existing owner function in
scripts/capabilities/namespaces/, regenerates the specification, adds
contract tests, and only then uses it from the page. Reimplementing the domain
logic in JavaScript would defeat the architecture.
2. The page is created
The minimum page is:
workshop/pages/<name>/index.html
workshop/pages/<name>/app.js
index.html supplies a meaningful <title> and a leading HTML comment. Those
two fields become the title and description shown by the Workshop index.
The JavaScript establishes a page identity and a least-privilege vocabulary:
import { createVita } from '/workshop/lib/vita.v1.js';
import { registerVitaBlocks } from '/workshop/components/vita-blocks.js';
const vita = await createVita({
page: 'standing-agendas',
allow: ['vita.people.*', 'vita.workshop.*'],
});
registerVitaBlocks(vita);
The page name becomes write provenance (browser:standing-agendas) through
the X-Vita-Page request header. The allow list prevents accidental use of
unrelated capabilities in the browser SDK. It is a page-authoring guardrail,
not a replacement for the server's transport policy or the internal-only host
boundary.
3. The page calls capabilities
const result = await vita.call(
'vita.people.agenda_add',
{ person: 'example-person', text: 'review the launch sequence' },
{ idempotencyKey: vita.newIdempotencyKey() },
);
The SDK returns the server's envelope rather than throwing for domain or policy failures:
{
"ok": true,
"data": { "status": "created" },
"error": null,
"meta": {
"v": 1,
"capability": "vita.people.agenda_add",
"request_id": "req_example",
"as_of": "2026-07-15T12:00:00Z",
"source": "precedent agenda store",
"transport": "js"
}
}
Callers branch on ok and error.code. In addition to server errors, the SDK
can synthesize three client-side failures:
| Code | Meaning |
|---|---|
unknown_capability |
The requested name was absent from discovery |
page_not_allowed |
The name was outside the page's declared allow patterns |
sdk_blocked |
A browser attempted a send- or spawn-class capability |
network_error |
Discovery or call transport failed |
The server also rejects browser send and spawn calls. The SDK check merely
fails faster. Write calls should always provide an idempotency key so a retry
cannot duplicate the mutation.
4. The page is discovered automatically
scripts/workshop/pages_index.py::scan_pages() scans workshop/pages/* on
every call and returns pages newest-updated first. It deliberately does not
cache: static pages become live without a restart, so a cached index would be
wrong immediately.
The same owner function feeds both:
GET /api/v1/workshop/pages, used by the React/workbenchroute; andvita.workshop.pages, available through the capability layer.
This shared owner prevents the HTTP index and capability index from drifting.
The older /workshop/ landing page is a small hand-maintained directory; the
React /workbench index is automatic.
5. It is promoted or deleted
Usage is recorded centrally in data/capabilities/usage.db. Repeatedly useful
page-local UI can move into workshop/components/vita-blocks.js. A mature
workflow can graduate into the React app while retaining the same capability
contracts.
Deleting workshop/pages/<name>/ removes the page from the live index. Its
view-chrome rows become cold data in data/workshop/page-prefs.db; no domain
records disappear.
State boundaries
Domain state
Anything another interface might need is domain state. It must be read or written through a capability and stored by that capability's existing owner module. Examples include tasks, agenda entries, links, health readings, and education records.
View chrome
Collapsed panels, sort order, and purely visual toggles may be stored through:
vita.workshop.prefs_getvita.workshop.prefs_set
The browser SDK exposes these as:
const chrome = await vita.prefs.load();
await vita.prefs.set({ 'collapsed:scheduled': true });
await vita.prefs.remove(['show-completed']);
Preferences are scoped by page and stored as arbitrary JSON in
data/workshop/page-prefs.db, owned by
scripts/workshop/page_prefs.py. Server-side storage makes chrome follow the
same person across devices. The store is intentionally second-class: it is not
mined, joined into domain analysis, or used as an alternate source of truth.
page normally comes from X-Vita-Page. Python and CLI callers can pass it
explicitly. Calls without either identity return invalid_input.
Shared blocks
workshop/components/vita-blocks.js is the promoted block vocabulary. Current
examples include <vita-sleep-gate> and <vita-search>. They are native web
components, so Workshop pages can reuse them without React or a build step.
Forking a shared block inside a page is allowed when a page genuinely needs a variant. The failure mode is an unmanaged family of near-identical forks. When a local component proves broadly useful, it is promoted back into the shared file.
Current pages
| Page | Purpose | Primary capability domains |
|---|---|---|
pages/tasks/ |
Task and habit workbench | vita.tasks.* |
pages/agendas/ |
Standing per-person agendas joined with upcoming meetings | vita.people.*, calendar reads |
pages/start/ |
Browser start page with link tiers, frequents, and a today HUD | vita.links.*, health/calendar reads |
These are examples, not a closed product catalog. The defining property of the Workshop is that a new personal surface can appear without changing the React route tree.
Exposure and security
The Workshop and capability HTTP endpoints are internal surfaces:
- available on localhost and the private/Tailscale path;
- excluded from public tunnel host allowlists by
api/src/middleware/domain_guard.py; - not exposed through
vita-docs.ham.xyzor other public report domains.
Static page code is trusted internal code, but it still receives mechanical
constraints. Browser transport cannot invoke send or spawn; page identity
is recorded; page authors declare an allowlist; mutations use idempotency; and
capability-level failures return structured rejection codes.
Workshop pages must never embed secrets or personal source records in their HTML. They request the minimum data needed at runtime.
Verification contract
A new page is not complete merely because it renders. Verification exercises the real interface and then checks the canonical state:
- open the page through the internal API or web proxy;
- confirm blocks and data render with freshness metadata;
- perform one real write through the UI;
- verify the write in the capability provenance ledger or backing owner;
- reload and confirm page chrome persisted from the server;
- exercise a rejected state and confirm it is visibly distinct from an error.
The page-building skill uses agent-browser for this local-dev interaction.
Element references invalidate after navigation and DOM snapshots, so every
mutation is verified against provenance or backing state rather than trusting
an automation tool's βclickedβ response.
Known limitations and improvement opportunities
- Two indexes exist.
/workbenchdiscovers pages automatically, whileworkshop/index.htmlhas a hand-maintained list that can lag. - Metadata is inferred from HTML. The scanner reads the
<title>and first leading comment from the first 4 KB; there is no explicit page manifest or validation command. - The page allowlist is client-side. It prevents accidental overreach, but same-origin trusted code can bypass the SDK. The hard controls are transport restrictions and the private host boundary.
- Preferences have no garbage collector. Deleting a page leaves cold rows
in
page-prefs.db. - No automatic graduation rule exists. Usage can identify a durable page, but promotion into the product remains a judgment call.
- Interaction testing is page-specific. There is no common automated contract test that loads every page and verifies freshness/rejection chrome.
Reasonable next improvements are a small page manifest/schema, one generated landing page instead of two indexes, an orphaned-preferences cleanup report, and a shared smoke test for the non-negotiable page contract.
File map
| Purpose | Path |
|---|---|
| Design doctrine | workshop/README.md |
| Page-authoring workflow | .claude/skills/workshop-page/SKILL.md |
| Static pages | workshop/pages/<name>/ |
| Browser capability SDK | workshop/lib/vita.v1.js |
| Shared style layer | workshop/lib/workshop.css |
| Shared blocks | workshop/components/vita-blocks.js |
| Static mount and internal routes | api/src/main.py |
| React product-side index | web/src/routes/workbench.tsx |
| Page index router | api/src/routers/workshop.py |
| Shared page scanner | scripts/workshop/pages_index.py |
| Workshop capability namespace | scripts/capabilities/namespaces/workshop.py |
| Preference owner | scripts/workshop/page_prefs.py |
| Preference store | data/workshop/page-prefs.db |
| Capability contract tests | tests/test_capabilities.py |
Changelog
- 2026-07-15: Initial public system documentation for the Workshop, including capability composition, browser surfaces, page discovery, cross-device chrome, lifecycle, and security boundaries.