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,70 @@
---
title: Decisions
---
# Decisions
```js
const decisions = await FileAttachment("data/decisions.json").json();
const data = Array.isArray(decisions) ? decisions : [];
const pending = data.filter(d => d.decision_type === "pending");
const made = data.filter(d => d.decision_type === "made");
```
```js
const tab = view(Inputs.select(["Pending", "Made"], { label: "View" }));
```
```js
const shown = tab === "Pending" ? pending : made;
display(Inputs.table(shown.map(d => ({
Title: d.title,
Status: d.status + (d.escalation_note ? " ⚠️" : ""),
Decided_by: d.decided_by ?? "—",
Deadline: d.deadline ? new Date(d.deadline).toLocaleDateString() : "—",
Rationale: (d.rationale ?? "").slice(0, 80),
Updated: new Date(d.updated_at).toLocaleDateString(),
})), { rows: 30 }));
```
## Resolution Velocity
```js
import * as Plot from "npm:@observablehq/plot";
const resolved = made.filter(d => d.decided_at);
const byMonth = resolved.reduce((acc, d) => {
const m = d.decided_at.slice(0, 7);
acc[m] = (acc[m] ?? 0) + 1;
return acc;
}, {});
display(Plot.plot({
title: "Decisions Resolved per Month",
x: { label: "Month", tickRotate: -30 },
y: { label: "Count", grid: true },
marks: [
Plot.barY(
Object.entries(byMonth).map(([month, count]) => ({ month, count })),
{ x: "month", y: "count", fill: "steelblue", tip: true }
),
Plot.ruleY([0]),
],
marginBottom: 60,
width: 700,
}));
```
```js
if (tab === "Pending" && pending.filter(d => d.escalation_note).length > 0) {
display(html`<div class="escalation-box">
<strong>⚠️ Escalated decisions require human approval before any action is taken (constitution §4).</strong>
<ul>${pending.filter(d => d.escalation_note).map(d => html`<li><b>${d.title}</b>: ${d.escalation_note}</li>`)}</ul>
</div>`);
}
```
<style>
.escalation-box { background: #fff3cd; border: 2px solid orange; border-radius: 8px; padding: 1rem; margin-top: 1rem; }
</style>