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>
2026-02-24 17:47:49 +01:00
---
title: Workstreams
---
2026-02-24 23:19:26 +01:00
```js
const API = "http://127.0.0.1:8000";
const POLL = 15_000;
```
```js
2026-02-25 23:33:14 +01:00
// Fetch workstreams + topics + summary (for dep graph) in parallel
2026-02-24 23:19:26 +01:00
const wsState = (async function*() {
while (true) {
2026-02-25 23:33:14 +01:00
let data = [], openWs = [], ok = false;
2026-02-24 23:19:26 +01:00
try {
2026-02-25 23:33:14 +01:00
const [rw, rt, rs] = await Promise.all([
2026-02-24 23:19:26 +01:00
fetch(`${API}/workstreams/` ),
fetch(`${API}/topics/` ),
2026-02-25 23:33:14 +01:00
fetch(`${API}/state/summary` ),
2026-02-24 23:19:26 +01:00
]);
2026-02-25 23:33:14 +01:00
ok = rw.ok & & rt.ok & & rs.ok;
2026-02-24 23:19:26 +01:00
if (ok) {
2026-02-25 23:33:14 +01:00
const [wsList, topicList, summary] = await Promise.all([rw.json(), rt.json(), rs.json()]);
2026-02-24 23:19:26 +01:00
const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
data = wsList.map(w => ({
...w,
domain: topicMap[w.topic_id]?.domain ?? "unknown",
topic_title: topicMap[w.topic_id]?.title ?? "—",
}));
2026-02-25 23:33:14 +01:00
// open_workstreams from summary carry depends_on / blocks lists
openWs = summary.open_workstreams ?? [];
2026-02-24 23:19:26 +01:00
}
} catch {}
2026-02-25 23:33:14 +01:00
yield {data, openWs, ok, ts: new Date()};
2026-02-24 23:19:26 +01:00
await new Promise(res => setTimeout(res, POLL));
}
})();
```
```js
2026-02-25 23:33:14 +01:00
const data = wsState.data ?? [];
const openWs = wsState.openWs ?? [];
const _ok = wsState.ok ?? false;
const _ts = wsState.ts;
2026-02-24 23:19:26 +01:00
```
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>
2026-02-24 17:47:49 +01:00
# Workstreams
```js
dashboard: move live indicator to TOC sidebar on all pages; add live-data docs
- All four pages (index, workstreams, decisions, progress) now inject the
live indicator into #observablehq-toc via injectTocTop("live-indicator", el)
Left-aligned (no text-align: right), position:relative + padding-right for
the ? button affordance
- decisions.md: splits the former combined "decisions-sidebar" widget into two
separate injectTocTop calls — KPI box first (ends lower), live indicator
second (ends at top); both now have their own stable ids
- withDocHelp(_liveEl, "/docs/live-data") wires the ? button on every page
- src/docs/live-data.md: new documentation page explaining poll interval (15s),
indicator colour semantics, offline recovery, and which endpoints each page hits
- Removes the .live-bar CSS class from all pages; replaces with .live-indicator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 16:18:09 +01:00
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
const _liveEl = html`< div class = "live-indicator" >
2026-02-24 23:19:26 +01:00
< span style = "color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}" > ●< / span >
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
dashboard: move live indicator to TOC sidebar on all pages; add live-data docs
- All four pages (index, workstreams, decisions, progress) now inject the
live indicator into #observablehq-toc via injectTocTop("live-indicator", el)
Left-aligned (no text-align: right), position:relative + padding-right for
the ? button affordance
- decisions.md: splits the former combined "decisions-sidebar" widget into two
separate injectTocTop calls — KPI box first (ends lower), live indicator
second (ends at top); both now have their own stable ids
- withDocHelp(_liveEl, "/docs/live-data") wires the ? button on every page
- src/docs/live-data.md: new documentation page explaining poll interval (15s),
indicator colour semantics, offline recovery, and which endpoints each page hits
- Removes the .live-bar CSS class from all pages; replaces with .live-indicator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 16:18:09 +01:00
: html`<span style="color:red">Offline — run: <code>make api</code></span>` }
< / div > `;
withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);
Add Decisions and Workstreams reference docs with heading help wiring
- Remove residual constitution footnote from progress page header
- Create src/docs/decisions.md: types, statuses, resolution history chart,
filter bar, card anatomy, Decision Health KPI, escalation protocol
- Create src/docs/workstreams.md: status distribution chart, filter bar,
table columns, dependency graph, create/update patterns
- Wire withDocHelp(h1) on Decisions and Workstreams pages pointing to new docs
- Add both pages to Reference nav section in observablehq.config.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 18:12:12 +01:00
const _h1 = document.querySelector("#observablehq -main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/workstreams"); }
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>
2026-02-24 17:47:49 +01:00
```
```js
2026-02-26 00:19:58 +01:00
import {MultiSelect} from "./components/multiselect.js";
2026-02-26 00:05:58 +01:00
// Static options — no dependency on `data` , so selections survive polls
const DOMAINS = ["custodian", "railiance", "markitect", "coulomb_social", "personhood", "foerster_capabilities"];
const STATUSES = ["active", "blocked", "completed", "archived"];
2026-02-26 16:49:33 +01:00
// Create filter form without displaying — shown below the chart
const _filtersForm = Inputs.form(
2026-02-26 00:05:58 +01:00
{
2026-02-26 00:19:58 +01:00
domain: MultiSelect(DOMAINS, {label: "Domain", placeholder: "All domains"}),
status: MultiSelect(STATUSES, {label: "Status", placeholder: "All statuses"}),
owner: Inputs.text({placeholder: "Owner…", style: "width:120px"}),
2026-02-26 00:05:58 +01:00
},
{
template: ({domain, status, owner}) => html`< div class = "filter-bar" >
2026-02-26 00:19:58 +01:00
${domain}${status}
< div class = "filter-owner" > ${owner}< / div >
2026-02-26 00:05:58 +01:00
< / div > `,
}
2026-02-26 16:49:33 +01:00
);
```
```js
const filters = Generators.input(_filtersForm);
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>
2026-02-24 17:47:49 +01:00
```
```js
2026-02-26 00:05:58 +01:00
// Empty array = no filter applied (show all)
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>
2026-02-24 17:47:49 +01:00
const filtered = data.filter(w =>
2026-02-26 00:05:58 +01:00
(filters.domain.length === 0 || filters.domain.includes(w.domain)) & &
(filters.status.length === 0 || filters.status.includes(w.status)) & &
(!filters.owner || (w.owner ?? "").toLowerCase().includes(filters.owner.toLowerCase()))
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>
2026-02-24 17:47:49 +01:00
);
```
2026-02-24 23:19:26 +01:00
## Status Distribution
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>
2026-02-24 17:47:49 +01:00
```js
import * as Plot from "npm:@observablehq/plot ";
2026-02-24 23:19:26 +01:00
const byStatus = Object.entries(
filtered.reduce((acc, w) => { acc[w.status] = (acc[w.status] ?? 0) + 1; return acc; }, {})
).map(([status, count]) => ({status, count}));
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>
2026-02-24 17:47:49 +01:00
display(Plot.plot({
marks: [
2026-02-24 23:19:26 +01:00
Plot.barX(byStatus, {y: "status", x: "count", fill: "status", tip: true}),
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>
2026-02-24 17:47:49 +01:00
Plot.ruleX([0]),
],
marginLeft: 80,
width: 500,
}));
```
2026-02-24 23:19:26 +01:00
2026-02-26 16:50:44 +01:00
## All Workstreams
2026-02-26 16:49:33 +01:00
```js
display(_filtersForm);
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}));
```
2026-02-25 23:33:14 +01:00
## Dependencies
```js
// Build dep cards from the enriched open_workstreams in the summary
2026-02-26 00:05:58 +01:00
const wsWithDeps = openWs.filter(w => {
const domain = data.find(d => d.id === w.id)?.domain ?? "unknown";
return (filters.domain.length === 0 || filters.domain.includes(domain)) & &
(filters.status.length === 0 || filters.status.includes(w.status)) & &
(w.depends_on.length > 0 || w.blocks.length > 0);
});
2026-02-25 23:33:14 +01:00
if (wsWithDeps.length === 0) {
display(html`<p class="dim">No dependency edges recorded for the current filter. Use <code>create_dependency()</code> via the MCP server to link workstreams.</p>` );
} else {
display(html`< div class = "dep-grid" > ${wsWithDeps.map(w => {
const depRows = w.depends_on.map(d =>
html`<div class="dep-row dep-on">↳ depends on <strong>${d.workstream_title}</strong>${d.description ? html` < span class = "dep-desc" > — ${d.description}</ span > ` : ""}</div>`
);
const blockRows = w.blocks.map(d =>
html`<div class="dep-row dep-block">⊳ blocks <strong>${d.workstream_title}</strong>${d.description ? html` < span class = "dep-desc" > — ${d.description}</ span > ` : ""}</div>`
);
return html`< div class = "dep-card" >
< div class = "dep-title" > ${w.title}< / div >
< div class = "dep-status dep-status-${w.status}" > ${w.status}< / div >
${depRows}${blockRows}
< / div > `;
})}< / div > `);
}
```
2026-02-24 23:19:26 +01:00
< style >
2026-02-26 16:42:00 +01:00
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
2026-02-25 23:33:14 +01:00
.dim { color: gray; font-style: italic; }
2026-02-26 00:19:58 +01:00
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
.filter-owner { display: flex; align-items: center; }
.filter-owner input { height: 30px; font-size: 0.85rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--theme-foreground-faint, #ccc ); background: var(--theme-background, #fff ); font-family: inherit; color: inherit; }
2026-02-25 23:33:14 +01:00
.dep-grid { display: flex; flex-direction: column; gap: 0.75rem; }
.dep-card { border: 1px solid #e0e0e0 ; border-radius: 6px; padding: 0.75rem 1rem; background: var(--theme-background-alt, #fafafa ); }
.dep-title { font-weight: 600; margin-bottom: 0.25rem; }
.dep-status { display: inline-block; font-size: 0.7rem; padding: 1px 6px; border-radius: 10px; margin-bottom: 0.5rem; text-transform: uppercase; }
.dep-status-active { background: #d4edda ; color: #155724 ; }
.dep-status-blocked { background: #f8d7da ; color: #721c24 ; }
.dep-status-completed { background: #cce5ff ; color: #004085 ; }
.dep-row { font-size: 0.85rem; margin: 0.2rem 0 0 0.5rem; color: #444 ; }
.dep-on { color: #1a5276 ; }
.dep-block { color: #6e2f00 ; }
.dep-desc { color: #888 ; font-size: 0.8rem; }
2026-02-24 23:19:26 +01:00
< / style >