- improvement-modal.js: API expects `domain` not `domain_slug` (422 fix) - todo.md: section heading and KPI label renamed to "Suggestions" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
State Hub v0.1
The operational brain of the Custodian: a local PostgreSQL database, FastAPI REST service, FastMCP stdio server for Claude Code, Observable Framework dashboard, and a custodian CLI.
Stack
| Layer | Technology | Port |
|---|---|---|
| Database | PostgreSQL 16-alpine (Docker) | 127.0.0.1:5432 |
| API | FastAPI + SQLAlchemy 2.0 async + asyncpg | 127.0.0.1:8000 |
| MCP server | FastMCP stdio (Claude Code native) | stdio |
| Dashboard | Observable Framework | 127.0.0.1:3000 |
| CLI | custodian (Python, uv entry point) |
— |
All services bind to 127.0.0.1 only — nothing exposed to the network.
Setup
Prerequisites
- Docker Engine (WSL2: see
CLAUDE.mdin repo root → Docker Setup) - Python 3.12+ with
uv(pip install uv) - Node.js 18+ (dashboard only)
First-time
cd state-hub
cp .env.example .env # edit POSTGRES_PASSWORD
make install # uv sync
make db # docker compose up postgres
make migrate # alembic upgrade head (creates 5 tables)
make seed # insert 6 canonical topics
make api # uvicorn :8000 --reload
Shortcut
make start # db + sleep + migrate + api
Dashboard
make dashboard # Observable dev server on :3000
CLI
make install-cli # symlink .venv/bin/custodian → ~/.local/bin
custodian status # API health + summary totals
custodian register-project # register cwd as a Custodian project
Makefile Targets
| Target | What it does |
|---|---|
make install |
uv sync — install Python deps + entry points |
make install-cli |
Symlink custodian to ~/.local/bin |
make db |
Start postgres container |
make db-tools |
Start postgres + pgadmin (http://127.0.0.1:5050) |
make migrate |
alembic upgrade head |
make seed |
Insert 6 canonical topics |
make api |
uvicorn api.main:app --reload |
make dashboard |
Observable dev server |
make check |
curl /state/health |
make start |
db + wait + migrate + api |
make register-project DOMAIN=x PROJECT_PATH=y |
Register a project |
make clean |
docker compose down -v (destroys DB volume) |
Database Schema
Five tables in dependency order:
topics
└── workstreams
└── tasks (self-FK: parent_task_id)
└── progress_events
decisions (FK: topic_id, workstream_id — at least one required)
└── progress_events
Enums
| Enum | Values |
|---|---|
topic_status |
active · paused · archived |
workstream_status |
active · blocked · completed · archived |
task_status |
todo · in_progress · blocked · done · cancelled |
task_priority |
low · medium · high · critical |
decision_type |
made · pending |
decision_status |
open · resolved · escalated · superseded |
domain |
custodian · railiance · markitect · coulomb_social · personhood · foerster_capabilities |
Governance constraints encoded in schema
- No hard DELETE endpoints — only soft:
archived,cancelled,superseded progress_eventshas noupdated_atand no DELETE endpoint (append-only per constitution §5)decisionswith financial/legal keywords +pendingtype → auto-setescalation_note(§4)
API
Interactive docs at http://127.0.0.1:8000/docs once the API is running.
Key endpoint: /state/summary
Returns a full snapshot in one call — used by both the MCP server and dashboard:
{
"generated_at": "...",
"totals": {
"topics": { "active": 6, "paused": 0, "archived": 0, "total": 6 },
"workstreams": { "active": 1, "blocked": 0, "completed": 1, "total": 2 },
"tasks": { "todo": 9, "in_progress": 0, "blocked": 0, "done": 11, "total": 20 },
"decisions": { "open": 1, "resolved": 0, "escalated": 0, "total": 1 }
},
"topics": [...], // topics with nested workstream stubs
"blocking_decisions": [...], // pending decisions only
"blocked_tasks": [...],
"recent_progress": [...], // last 20 events
"open_workstreams": [...]
}
Router summary
| Prefix | Operations |
|---|---|
/topics |
CRUD (soft-delete: archived) |
/workstreams |
CRUD (soft-delete: archived) |
/tasks |
CRUD (soft-delete: cancelled); PATCH updates status |
/decisions |
CRUD (soft-delete: superseded); auto-escalation |
/progress |
GET list + POST append — no DELETE |
/state/summary |
Full snapshot |
/state/health |
DB connectivity check |
MCP Server
Registered in ~/.claude.json at user scope. Config in .mcp.json (repo root).
Uses absolute path + PYTHONPATH so cwd is not required:
{
"command": "/home/worsch/the-custodian/state-hub/.venv/bin/python",
"args": ["/home/worsch/the-custodian/state-hub/mcp_server/server.py"],
"env": { "PYTHONPATH": "/home/worsch/the-custodian/state-hub", "API_BASE": "http://127.0.0.1:8000" }
}
See mcp_server/TOOLS.md for the full tool reference card (30 lines, faster than reading server.py).
Tools at a glance
Query (read-only): get_state_summary · get_topic · list_blocked_tasks · list_pending_decisions · get_recent_progress
Mutate (each auto-emits a progress event): create_task · update_task_status · record_decision · resolve_decision · add_progress_event · update_workstream_status
Resources: state://summary · state://topics · state://workstreams/{topic_slug} · state://decisions/blocking · state://tasks/blocked
custodian CLI
Installed into .venv/bin/custodian by uv sync; symlinked to ~/.local/bin by make install-cli.
custodian register-project [--domain DOMAIN] [--path PATH]
--pathdefaults to current working directory--domainis auto-detected fromproject_charter_v*.mdfrontmatter if omitted
custodian status
Prints API health, totals, and any blocking decisions.
What register-project does
- Verifies the API is reachable (fails fast with
make apihint) - Looks up the topic ID for the domain via
/topics/?status=active - Checks that
state-hubis in~/.claude.json - Writes
$PROJECT_PATH/CLAUDE.mdfromscripts/project_claude_md.template - Posts a
milestoneprogress event recording the registration
Project Registration Scripts
| Script | Purpose |
|---|---|
scripts/register_project.sh |
Shell version of custodian register-project |
scripts/patch_mcp_cwd.py |
Patches cwd into ~/.claude.json after claude mcp add-json drops it |
scripts/project_claude_md.template |
CLAUDE.md template with {PROJECT_NAME}, {DOMAIN}, {TOPIC_ID} |
scripts/seed.py |
Insert the 6 canonical topics into a fresh database |
scripts/pull_image.py |
WSL2 workaround: pull Docker images via Python urllib with Range-request chunking |
Dashboard
Four pages at http://127.0.0.1:3000 (dev) or built with npm run build:
| Page | Content |
|---|---|
| Overview | Status cards, task-by-status chart, recent activity feed, decisions due within 7 days |
| Workstreams | Filterable table by domain/status/owner; selected workstream task list; progress timeline |
| Decisions | Pending tab (with escalation highlights) and Made tab; resolution velocity chart |
| Progress | Append-only event feed with author badges; 30-day event volume chart |
Data loaders (src/data/*.json.py) are Python scripts that call the local API. They run at dev-server start and on npm run build. Clear the cache if data appears stale:
rm -rf dashboard/src/.observablehq/cache/
Known Issues / WSL2 Notes
- TLS bad record MAC on large downloads: WSL2 corrupts packets on big TCP transfers. Use
scripts/pull_image.pyinstead ofdocker pullfor future image pulls. claude mcp add-jsondropscwd: Known Claude Code bug. Runpython3 scripts/patch_mcp_cwd.pyafter any re-registration. The current.mcp.jsonuses absolute path +PYTHONPATHsocwdis not strictly needed.- AsyncSession concurrency: SQLAlchemy 2.0 async sessions don't support concurrent operations. All queries in
/state/summaryrun sequentially on a single session.