The post-commit hook re-invokes fix-consistency, which commits writeback changes, which re-triggers the hook — causing exponential process spawning. Fix: pass GIT_CUSTODIAN_SYNC=1 in the env for all writeback git commits. Update the post-commit hook (not tracked by git) to exit early when this variable is set. Also remove the --no-verify flag that was added as a failed attempt (it only skips pre-commit/commit-msg, not post-commit hooks). 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 SSE 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 SSE | 127.0.0.1:8001 |
| 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 # db + migrate + uvicorn :8000 (restarts if running)
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 |
db + wait + migrate + uvicorn (restarts if running) |
make dashboard |
Observable dev server (restarts if running) |
make check |
curl /state/health |
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
Runs as a persistent SSE service on :8001, independent of the Claude Code session.
Restart it anytime without restarting Claude Code.
make mcp-http # start (or restart) the MCP SSE server on :8001
Registered at user scope in ~/.claude.json:
{ "type": "sse", "url": "http://127.0.0.1:8001/sse" }
To re-register from scratch:
claude mcp remove state-hub -s user 2>/dev/null || true
claude mcp add-json -s user state-hub '{"type":"sse","url":"http://127.0.0.1:8001/sse"}'
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 |
Legacy: patched cwd for the old stdio registration (no longer needed) |
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. - MCP server is now SSE, not stdio: Re-registration is
claude mcp add-json -s user state-hub '{"type":"sse","url":"http://127.0.0.1:8001/sse"}'. Thepatch_mcp_cwd.pyscript and.mcp.jsonconfig are legacy artifacts from the old stdio setup. - AsyncSession concurrency: SQLAlchemy 2.0 async sessions don't support concurrent operations. All queries in
/state/summaryrun sequentially on a single session.