Add state-hub v0.1 — local-first state service for the Custodian

Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.

- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 17:47:49 +01:00
commit 0ea2788943
48 changed files with 8567 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
---
title: Workstreams
---
# Workstreams
```js
const workstreams = await FileAttachment("data/workstreams.json").json();
const data = Array.isArray(workstreams) ? workstreams : [];
```
```js
const domainFilter = view(Inputs.select(
["(all)", ...new Set(data.map(w => w.domain ?? "unknown"))],
{ label: "Domain" }
));
const statusFilter = view(Inputs.select(
["(all)", "active", "blocked", "completed", "archived"],
{ label: "Status" }
));
const ownerFilter = view(Inputs.text({ label: "Owner contains" }));
```
```js
const filtered = data.filter(w =>
(domainFilter === "(all)" || w.domain === domainFilter) &&
(statusFilter === "(all)" || w.status === statusFilter) &&
(!ownerFilter || (w.owner ?? "").toLowerCase().includes(ownerFilter.toLowerCase()))
);
const STATUS_COLOR = {
active: "green",
blocked: "orange",
completed: "blue",
archived: "gray",
};
display(Inputs.table(filtered.map(w => ({
Title: w.title,
Domain: w.domain,
Status: w.status,
Owner: w.owner ?? "—",
"Due": w.due_date ?? "—",
"Updated": new Date(w.updated_at).toLocaleDateString(),
})), {
rows: 20,
}));
```
```js
import * as Plot from "npm:@observablehq/plot";
display(Plot.plot({
title: "Workstream Status Distribution",
marks: [
Plot.barX(
Object.entries(
filtered.reduce((acc, w) => { acc[w.status] = (acc[w.status] ?? 0) + 1; return acc; }, {})
).map(([status, count]) => ({ status, count })),
{ y: "status", x: "count", fill: "status", tip: true }
),
Plot.ruleX([0]),
],
marginLeft: 80,
width: 500,
}));
```