Compare commits
3 Commits
8ad371f753
...
b19896a9a9
| Author | SHA1 | Date | |
|---|---|---|---|
| b19896a9a9 | |||
| 6018df03cf | |||
| f5f1323eb3 |
66
.custodian-brief.md
Normal file
66
.custodian-brief.md
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# Custodian Brief — the-custodian
|
||||
|
||||
**Domain:** custodian
|
||||
**Last synced:** 2026-03-26 16:45 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
||||
### FOS Hub Bootstrap — Identity, Hub Extraction, Ops Hub, Fin Hub
|
||||
Progress: 0/26 done | workstream_id: `293a74fe-a85a-4ad6-8933-23d52a72fe8b`
|
||||
|
||||
**Open tasks:**
|
||||
- · T01 — Complete NK-WP-0001: Keycloak + privacyIDEA on k3s `f55078b6`
|
||||
- · T02 — Complete NK-WP-0002: Local identity bootstrap `0d7792f7`
|
||||
- · T03 — IAM Profile integration test `e9894ac9`
|
||||
- · T04 — Canon standard: IAM Profile specification `69acc880`
|
||||
- · T05 — Create hub-core package `04bf480c`
|
||||
- · T06 — Hub-core FastMCP base server `6b49d94a`
|
||||
- · T07 — FOS §10 risk and alert tools `5a54af24`
|
||||
- … and 19 more open tasks
|
||||
|
||||
### Multi-User Onboarding and Environment Bootstrap
|
||||
Progress: 0/6 done | workstream_id: `a28d9e29-4119-4b73-9469-f921920253ef`
|
||||
|
||||
**Open tasks:**
|
||||
- · Git credential.helper setup for Gitea access `71628269`
|
||||
- · SSH key generation and authorization automation `fea965e9`
|
||||
- · Claude Code MCP registration for new machines `60318e9a`
|
||||
- · Environment bootstrap script (bootstrap-env.sh) `84a94761`
|
||||
- · Onboarding guide and user journey documentation `b0839802`
|
||||
- · State Hub multi-user model — domain-scoped access `d5df3302`
|
||||
|
||||
### Migrate Custodian State Hub to ThreePhoenix Cluster
|
||||
Progress: 0/9 done | workstream_id: `967baafb-d92d-405a-ba0b-0d00d37c4940`
|
||||
|
||||
**Open tasks:**
|
||||
- · T01 — Drill WSL2 backup restore end-to-end `b0caf112`
|
||||
- · T02 — Helm chart for State Hub `24887dd9`
|
||||
- · T03 — Build and push State Hub container image `79908ade`
|
||||
- · T04 — Deploy to cluster and run Alembic migrations `a7baf2eb`
|
||||
- · T05 — Migrate data from WSL2 to cluster `a307dd46`
|
||||
- · T06 — Drill cluster backup restore `03753b88`
|
||||
- · T07 — Cutover: redirect MCP config to cluster `ff1de25e`
|
||||
- … and 2 more open tasks
|
||||
|
||||
### State Hub v0.4 — Workstream Health Index (WHI) KPI Card
|
||||
Progress: 0/9 done | workstream_id: `9cc32158-2f5c-4ef6-9713-aacce4623d5e`
|
||||
|
||||
**Open tasks:**
|
||||
- · P1 — Verify dependency edge fields in open_workstreams `243646e0`
|
||||
- · P2.1 — Build directed dependency graph from openWs + completedIds `6dbef71f`
|
||||
- · P2.2 — Implement DFS cycle detection (CPI) `f0d5c107`
|
||||
- · P2.3 — Compute DD, BR, SPR, PEP, CDDR `6da60567`
|
||||
- · P2.4 — WHI formula: normalization + CPI penalty `29b2dbbd`
|
||||
- · P2.5 — Per-domain WHI breakdown `8ce5ef74`
|
||||
- · P3 — WHI KPI card UI `91efba5c`
|
||||
- … and 2 more open tasks
|
||||
|
||||
---
|
||||
## MCP Orientation (when available)
|
||||
|
||||
If the state-hub MCP server is reachable, call:
|
||||
`get_domain_summary("custodian")`
|
||||
This provides richer cross-domain context.
|
||||
If the MCP call fails, use this file as your orientation source.
|
||||
@@ -84,9 +84,11 @@ sudo service docker start
|
||||
Every Claude Code session in this repository must follow this ritual:
|
||||
|
||||
**On session start:**
|
||||
1. Call `get_state_summary()` via the `state-hub` MCP tool for orientation
|
||||
2. Check the agent inbox: `get_messages(to_agent="hub", unread_only=True)` — mark read and act on any messages
|
||||
3. Note any blocking decisions or blocked tasks before starting work
|
||||
1. Read `.custodian-brief.md` if it exists — offline-safe orientation that works without MCP
|
||||
2. Call `get_state_summary()` via the `state-hub` MCP tool for richer cross-domain context
|
||||
(if the MCP call fails, the brief is sufficient to begin work)
|
||||
3. Check the agent inbox: `get_messages(to_agent="hub", unread_only=True)` — mark read and act on any messages
|
||||
4. Note any blocking decisions or blocked tasks before starting work
|
||||
|
||||
**On session close (before ending):**
|
||||
1. Call `add_progress_event()` to log what was done, decided, or discovered
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Runbook — Gitea on COULOMBCORE
|
||||
tags: [gitea, coulombcore, k3s, postgresql-ha]
|
||||
created: 2026-03-25
|
||||
updated: 2026-03-25
|
||||
updated: 2026-03-26
|
||||
---
|
||||
|
||||
# Runbook: Gitea on COULOMBCORE
|
||||
@@ -143,6 +143,46 @@ When Gitea is down, work through this in order:
|
||||
|
||||
---
|
||||
|
||||
### 3. Node overload — runaway agent process (SSH dies, k3s unresponsive)
|
||||
|
||||
**Symptom:** SSH connections time out during banner exchange. k3s API returns TLS handshake
|
||||
timeout. `top` (via console) shows load average >100, 99.8% `sy` CPU, many running tasks,
|
||||
kswapd0 at high CPU. State-hub reverse tunnel may still be alive (it was established
|
||||
before the overload and requires no new connections).
|
||||
|
||||
**Root cause:** A runaway process (typically a Claude Code agent spawning subprocesses)
|
||||
exhausts the process/memory budget. With no swap, the kernel thrashes continuously.
|
||||
|
||||
**Triage (workstation):**
|
||||
```bash
|
||||
# Check if node is alive despite SSH being down
|
||||
curl -s --max-time 5 http://127.0.0.1:8000/state/health # via reverse tunnel
|
||||
|
||||
# k3s API — will timeout if node is thrashing
|
||||
kubectl get nodes # expect TLS timeout
|
||||
```
|
||||
|
||||
**Fix (requires console/VNC access):**
|
||||
```bash
|
||||
# 1. Identify runaway: look for high VIRT, many children, 99.8% sy in top
|
||||
# Runaway claude agents: massive VIRT (>50GB), user tegwick
|
||||
|
||||
# 2. Kill the offenders
|
||||
kill -9 <runaway-pid>
|
||||
kill -9 <apport-pid-if-in-D-state> # apport in D-state amplifies load
|
||||
|
||||
# 3. Wait ~60s for load to drop; SSH will start accepting connections
|
||||
# 4. Check PostgreSQL HA pods — may need 2-3 min to resync after OOM restarts
|
||||
kubectl get pods -l 'app.kubernetes.io/name=postgresql-ha'
|
||||
```
|
||||
|
||||
**Gitea does NOT need to be restarted** — it survives node overload. Once load drops
|
||||
and PostgreSQL HA resyncs, Gitea serves requests again.
|
||||
|
||||
**Prevention:** See "Robustness" section below.
|
||||
|
||||
---
|
||||
|
||||
## Node Resource Budget (approximate)
|
||||
|
||||
| Component | CPU Request |
|
||||
@@ -157,3 +197,71 @@ When Gitea is down, work through this in order:
|
||||
|
||||
Node capacity: ~2000m. Headroom is tight (~325m). Avoid adding workloads without
|
||||
reviewing resource requests first.
|
||||
|
||||
---
|
||||
|
||||
## Robustness — Hardening Checklist
|
||||
|
||||
These changes reduce blast radius from process/memory overload (INC-002, 2026-03-26):
|
||||
|
||||
### 1. Add swap (not yet done — highest priority)
|
||||
|
||||
```bash
|
||||
fallocate -l 4G /swapfile
|
||||
chmod 600 /swapfile
|
||||
mkswap /swapfile
|
||||
swapon /swapfile
|
||||
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||
```
|
||||
|
||||
Without swap, any memory spike causes immediate kernel thrash. 4GB swapfile = buffer time.
|
||||
|
||||
### 2. Cap tegwick user nproc (not yet done)
|
||||
|
||||
```bash
|
||||
# /etc/security/limits.conf
|
||||
tegwick hard nproc 512
|
||||
tegwick soft nproc 256
|
||||
```
|
||||
|
||||
Prevents a single agent from spawning 500+ processes. Claude Code agents survive fine
|
||||
within 256 soft / 512 hard.
|
||||
|
||||
### 3. Cap tegwick systemd user session memory (not yet done)
|
||||
|
||||
```bash
|
||||
# Create override for the tegwick user slice
|
||||
mkdir -p /etc/systemd/system/user-$(id -u tegwick).slice.d/
|
||||
cat > /etc/systemd/system/user-$(id -u tegwick).slice.d/limits.conf <<EOF
|
||||
[Slice]
|
||||
MemoryMax=1500M
|
||||
MemorySwapMax=512M
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
```
|
||||
|
||||
Prevents a rogue user process from consuming all 3.9GB.
|
||||
|
||||
### 4. Always-on agent guardrails (process hygiene)
|
||||
|
||||
- **Never run `/ralph-loop` directly on COULOMBCORE** — use `/ralph-workplan` which
|
||||
self-terminates when the workplan is complete (HEUREKA stop condition).
|
||||
- Set `--max-iterations` explicitly on any Ralph invocation.
|
||||
- Avoid large parallel agent fans (e.g., spawning 20 sub-agents simultaneously) on
|
||||
this resource-constrained node.
|
||||
|
||||
### 5. Add cluster health alerting (not yet done)
|
||||
|
||||
A per-service tunnel adds passive visibility but no alerting. A single cron covering the
|
||||
whole cluster is more useful — it catches Gitea, PGPool, and any other crashlooping pod.
|
||||
|
||||
```bash
|
||||
# /etc/cron.d/k3s-pod-health (on CoulombCore, run as tegwick)
|
||||
*/5 * * * * tegwick kubectl get pods -A 2>/dev/null | awk '$4 ~ /CrashLoop|OOMKill|Error/ && $5+0 > 3 {print}' | grep . && curl -s -X POST <notify-webhook> -d "k3s pod unhealthy on COULOMBCORE" || true
|
||||
```
|
||||
|
||||
Or via a state-hub progress event so it surfaces in the dashboard. Threshold: any pod
|
||||
with restart count > 3 and status not Running/Completed warrants a notification.
|
||||
|
||||
This single check covers the failure mode from INC-001 (PGPool crashlooping 13 days
|
||||
undetected) without adding tunnel infrastructure that can't help under node overload.
|
||||
|
||||
@@ -72,6 +72,7 @@ export default {
|
||||
pages: [
|
||||
{ name: "Capabilities", path: "/docs/capabilities" },
|
||||
{ name: "Connecting to the Hub", path: "/docs/connecting" },
|
||||
{ name: "Dashboard", path: "/docs/dashboard" },
|
||||
{ name: "Contributions", path: "/docs/contributions" },
|
||||
{ name: "Decision Health", path: "/docs/decisions-kpi" },
|
||||
{ name: "Decisions", path: "/docs/decisions" },
|
||||
|
||||
338
state-hub/dashboard/src/docs/dashboard.md
Normal file
338
state-hub/dashboard/src/docs/dashboard.md
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
title: Dashboard — Technical Reference
|
||||
---
|
||||
|
||||
# State Hub Dashboard — Technical Reference
|
||||
|
||||
The State Hub dashboard is the primary visual interface for the Custodian
|
||||
ecosystem. It provides live, reactive views of all tracked domains,
|
||||
workstreams, tasks, decisions, contributions, SBOM data, and agent activity —
|
||||
all sourced from the local FastAPI state service.
|
||||
|
||||
---
|
||||
|
||||
## Framework: Observable Framework
|
||||
|
||||
The dashboard is built on **[Observable Framework](https://observablehq.com/framework/)**,
|
||||
an open-source static-site framework from Observable, Inc. designed specifically
|
||||
for data-driven pages.
|
||||
|
||||
### Why Observable Framework?
|
||||
|
||||
| Requirement | How Observable Framework satisfies it |
|
||||
|---|---|
|
||||
| **Local-first, no build-time cloud dependency** | Compiles to a static site (`npm run build`); the preview server and data loaders run entirely on localhost. |
|
||||
| **Live data without a separate frontend service** | Pages poll the FastAPI backend directly from the browser via `fetch`. No BFF, no GraphQL, no WebSockets required. |
|
||||
| **Reactive updates without React complexity** | Observable's cell-based execution model re-runs any code block whose inputs change. Async generators produce new values every poll cycle and trigger re-renders automatically. |
|
||||
| **No JS bundler configuration** | `.md` files containing fenced JS code blocks are the entire source. No webpack, no Vite config, no `tsconfig.json`. |
|
||||
| **Native data visualisation** | First-class integration with `@observablehq/plot` — a concise, grammar-of-graphics library — for all charts. |
|
||||
| **Sovereignty-compatible** | The built output is a folder of static HTML/JS/CSS. It can be served by any web server, archived, or opened directly from disk. |
|
||||
| **Offline-graceful** | Data loaders (Python scripts that run at build time) produce JSON snapshots. If the API is unreachable at build time, the loader emits an empty-structure JSON so the page still renders with a clear error state instead of crashing. |
|
||||
|
||||
Observable Framework was chosen over alternatives (Grafana, Metabase, Streamlit,
|
||||
Next.js) because its design principles are uniquely aligned with the Custodian
|
||||
philosophy: **local-first**, **no vendor lock-in**, **sovereignty-preserving**,
|
||||
and **auditable** — the full data pipeline is visible in plain Markdown files.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
observablehq.config.js — site metadata, page registry, theme, global head
|
||||
components/ — shared JS modules
|
||||
data/ — Python data loaders (run at build time)
|
||||
docs/ — reference pages (this file lives here)
|
||||
*.md — one page per feature area
|
||||
```
|
||||
|
||||
### Data flow
|
||||
|
||||
There are two complementary data-fetching strategies:
|
||||
|
||||
**1. Static data loaders** (`src/data/*.json.py`)
|
||||
|
||||
Python scripts executed by the Observable build toolchain at `npm run build`
|
||||
or `npm run dev`. Each script calls the FastAPI backend via `urllib`, serialises
|
||||
the response to JSON on stdout, and Observable Framework captures that output
|
||||
as a static snapshot file that the page imports with `FileAttachment(...)`.
|
||||
|
||||
Current loaders:
|
||||
|
||||
| File | API endpoint |
|
||||
|---|---|
|
||||
| `summary.json.py` | `/state/summary` |
|
||||
| `workstreams.json.py` | `/workstreams/` |
|
||||
| `contributions.json.py` | `/contributions/` |
|
||||
| `decisions.json.py` | `/decisions/` |
|
||||
| `domains.json.py` | `/domains/` |
|
||||
| `messages.json.py` | `/messages/` |
|
||||
| `progress.json.py` | `/progress/` |
|
||||
| `repos.json.py` | `/repos/` |
|
||||
| `sbom.json.py` | `/sbom/aggregated` |
|
||||
| `gitea-inventory.json.py` | Gitea instance inventory |
|
||||
|
||||
**2. Live browser polling** (async generators in page `.md` files)
|
||||
|
||||
All interactive pages bypass the static snapshots for live data by using
|
||||
Observable's async generator pattern directly in the browser:
|
||||
|
||||
```js
|
||||
const summaryState = (async function*() {
|
||||
while (true) {
|
||||
const r = await fetch(`${API}/state/summary`);
|
||||
yield { data: r.ok ? await r.json() : {error: `HTTP ${r.status}`}, ok: r.ok };
|
||||
await new Promise(res => setTimeout(res, POLL));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
`POLL` is set to **15 000 ms** (15 seconds) in `src/components/config.js`.
|
||||
Observable's reactivity engine detects each new yield value and re-runs all
|
||||
dependent code blocks, updating charts, tables, and KPI cards automatically.
|
||||
A `●` live indicator in the top-left corner of each page shows the connection
|
||||
status and the last-updated time.
|
||||
|
||||
### Global configuration — `observablehq.config.js`
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Root directory | `src/` |
|
||||
| Site title | "Custodian State Hub" |
|
||||
| Theme | `["air", "near-midnight"]` — light body with dark sidebar |
|
||||
| Favicon | Inline SVG data URI (🗄️ emoji) |
|
||||
| Global head | KPI infobox styles, filter-bar styles, improvement-modal script |
|
||||
|
||||
The `improvement-modal.js` component is injected at the config level rather
|
||||
than imported per-page because Observable proxies `src/*.js` through its own
|
||||
bundler, which prevents them from being loaded as raw `<script>` tags in
|
||||
`<head>`. The config reads the file at build time, strips ES module export
|
||||
keywords, and injects the result as a plain inline `<script>`.
|
||||
|
||||
---
|
||||
|
||||
## Page Inventory
|
||||
|
||||
The dashboard has 30+ pages organised in four navigation groups:
|
||||
|
||||
### Top-level pages
|
||||
|
||||
| Page | Route | Purpose |
|
||||
|---|---|---|
|
||||
| Overview | `/` | Cross-domain summary — workstream chart, status KPIs, blocking decisions, recent activity |
|
||||
| Capabilities | `/capability-requests` | Capability request routing and fulfilment status |
|
||||
| Contributions | `/contributions` | Upstream contribution Kanban (bug reports, feature requests, upstream PRs) |
|
||||
| Domains | `/domains` | Per-domain health overview and management |
|
||||
| Goals | `/goals` | Domain goals and repo-scoped goals |
|
||||
| Inbox | `/inbox` | Agent message inbox and inter-repo communication |
|
||||
| Progress | `/progress` | Session progress event log |
|
||||
| Services (TPSC) | `/tpsc` | Third-party services catalog with GDPR maturity status |
|
||||
| Todo | `/todo` | Consolidated todo list across all repos |
|
||||
| Tools & Apps | `/tools` | Registered tools and applications |
|
||||
|
||||
### Repositories section
|
||||
|
||||
| Page | Route | Purpose |
|
||||
|---|---|---|
|
||||
| Repositories | `/repos` | All registered repos with DoI compliance tier |
|
||||
| Debt | `/techdept` | Technical debt registry |
|
||||
| Repo Sync | `/repo-sync` | Consistency checker results and sync status |
|
||||
| SBOM | `/sbom` | Software bill of materials — packages, licences, copyleft risk |
|
||||
|
||||
### Workstreams section
|
||||
|
||||
| Page | Route | Purpose |
|
||||
|---|---|---|
|
||||
| Workstreams | `/workstreams` | All workstreams with Workstream Health Index |
|
||||
| Decisions | `/decisions` | Decision log with resolve-in-place form |
|
||||
| Dependencies | `/dependencies` | Dependency graph explorer |
|
||||
| Extensions | `/extensions` | Extension point registry |
|
||||
| Interventions | `/interventions` | Tasks flagged for human intervention |
|
||||
| Tasks | `/tasks` | Task list with filters and status tracking |
|
||||
| UI Feedback | `/ui-feedback` | UI improvement feedback and issue tracking |
|
||||
|
||||
### Reference section
|
||||
|
||||
22 reference pages covering every feature, data model, and integration in detail.
|
||||
|
||||
---
|
||||
|
||||
## Component Library
|
||||
|
||||
All shared components live in `src/components/` and are imported as ES modules:
|
||||
|
||||
### `config.js`
|
||||
Exports two constants used by every live-polling page:
|
||||
- `API = "http://127.0.0.1:8000"` — the FastAPI base URL
|
||||
- `POLL = 15_000` — polling interval in milliseconds
|
||||
|
||||
### `entity-modal.js`
|
||||
A lightweight detail overlay for entities. Any table row or card can call
|
||||
`openEntityModal(entity, type)` to open a full-detail panel without navigating
|
||||
away from the page. Supports four entity types: `workstream`, `task`, `ep`
|
||||
(extension point), and `td` (technical debt).
|
||||
|
||||
Also exports `buildEntityTable()` — a function that constructs a consistent,
|
||||
clickable HTML table for any list of entities, with proportional column widths,
|
||||
overflow ellipsis, and native tooltip-on-hover for truncated values.
|
||||
|
||||
### `toc-sidebar.js`
|
||||
Provides `injectTocTop(id, element)` — injects a DOM element into the
|
||||
Observable Framework table-of-contents sidebar above the page's first section
|
||||
heading. Used on the Overview and Workstreams pages to embed live KPI infoboxes
|
||||
directly in the sidebar.
|
||||
|
||||
### `doc-overlay.js`
|
||||
Provides `withDocHelp(element, docPath)` — attaches a small `?` icon to any
|
||||
element that opens the linked reference page in a lightweight overlay panel
|
||||
without leaving the current page.
|
||||
|
||||
### `help-tip.js`
|
||||
A custom HTML element (`<help-tip>`) that renders an inline abbreviated label
|
||||
with an expandable tooltip containing a longer description and a link to the
|
||||
relevant reference page. Used in the Workstream Health Index card to annotate
|
||||
each metric abbreviation.
|
||||
|
||||
### `multiselect.js`
|
||||
A multi-value dropdown filter input compatible with Observable's `Inputs.form()`
|
||||
reactive pattern. Used on the Workstreams and Tasks pages for domain and status
|
||||
filtering.
|
||||
|
||||
### `improvement-modal.js`
|
||||
A floating feedback button that opens a modal form for submitting UI improvement
|
||||
suggestions. Injected globally via `observablehq.config.js` so it is available
|
||||
on every page.
|
||||
|
||||
### `action-confirm.js`
|
||||
A confirmation-dialog helper for destructive or irreversible actions triggered
|
||||
from the dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### Live polling with connection status
|
||||
|
||||
Every interactive page runs one or more async generator loops that poll the
|
||||
FastAPI backend every 15 seconds. A `●` indicator in the top-left corner
|
||||
shows green when the API is reachable and red with a restart command when it
|
||||
is not. This allows the dashboard to be used as a persistent, always-on monitor
|
||||
without requiring a page refresh.
|
||||
|
||||
### Workstream Health Index (WHI)
|
||||
|
||||
The Workstreams page computes a **Workstream Health Index** — a single
|
||||
composite score (0–100%) derived from five graph metrics:
|
||||
|
||||
| Metric | Abbrev. | Weight | Interpretation |
|
||||
|---|---|---|---|
|
||||
| Dependency Density | DD | 30% | Average deps per open workstream; high = tightly coupled |
|
||||
| Blocked Ratio | BR | 25% | Share of workstreams in a blocked state |
|
||||
| Single-Point Risk | SPR | 15% | Share of workstreams that others depend on but are not yet complete |
|
||||
| Parallel Execution Potential | PEP | 20% | Share of workstreams that could start/continue immediately |
|
||||
| Cross-Domain Dependency Ratio | CDDR | 10% | Share of edges crossing domain boundaries |
|
||||
|
||||
A **Cycle Presence Indicator** (CPI) detected via DFS halves the total score
|
||||
when a dependency cycle is found, since cyclic dependencies cause deadlock.
|
||||
The index is computed per-domain as well as globally and displayed in the
|
||||
TOC sidebar as a persistent KPI card.
|
||||
|
||||
### Multi-mode workstream chart
|
||||
|
||||
The Overview page renders a horizontal stacked bar chart using `@observablehq/plot`
|
||||
showing task counts (done / in progress / blocked / todo) per workstream.
|
||||
A `<select>` dropdown switches between:
|
||||
|
||||
- **Status modes**: active, accepted, finished, blocked, stalled, oldies
|
||||
- **Time modes**: last 1h, 24h, 7d, 30d, today, this week, this month
|
||||
|
||||
Domains are sorted by most recent workstream activity (most active domain at
|
||||
the top). Title labels and done/total counters are overlaid directly on the bars.
|
||||
|
||||
### Resolve-in-place for blocking decisions
|
||||
|
||||
Blocking decisions on the Overview page render with an expandable form
|
||||
(`<details>` element). The human can enter a rationale and click "Record & close"
|
||||
to call `POST /decisions/{id}/resolve` without leaving the page. The decision
|
||||
list refreshes after a successful resolve; other decisions remain unchanged and
|
||||
retain any in-progress text the user was typing.
|
||||
|
||||
### SBOM and licence-risk tracking
|
||||
|
||||
The Overview page shows three SBOM/contribution health KPI cards. The SBOM
|
||||
page renders a horizontal bar chart of package counts by licence, with
|
||||
highlighted cards for any detected copyleft licences (GPL, AGPL, LGPL, etc.)
|
||||
in direct production dependencies.
|
||||
|
||||
### Dependency graph
|
||||
|
||||
The Dependencies page and the Workstreams page both surface inter-workstream
|
||||
dependency data. Each workstream card shows the workstreams it depends on
|
||||
(`↳ depends on`) and the workstreams it blocks (`⊳ blocks`), derived from
|
||||
the `WorkstreamDependency` table.
|
||||
|
||||
### Entity modals
|
||||
|
||||
Any table row on any list page (workstreams, tasks, extension points, tech debt)
|
||||
can be clicked to open a detail modal with full field data, dependency lists,
|
||||
task progress, and timestamps — without a page navigation or a separate detail
|
||||
route.
|
||||
|
||||
### Graceful offline state
|
||||
|
||||
All async generator polls wrap API calls in `try/catch`. When the API is
|
||||
unreachable, pages display an error banner and a `make api` restart command
|
||||
rather than crashing or showing stale cached data without warning. Static
|
||||
data loaders emit an empty-structure JSON fallback so build-time failures do
|
||||
not block the dashboard from loading.
|
||||
|
||||
---
|
||||
|
||||
## Visualisation library: `@observablehq/plot`
|
||||
|
||||
All charts use **[@observablehq/plot](https://observablehq.com/plot/)** —
|
||||
Observable's concise, composable grammar-of-graphics library. It is imported
|
||||
on demand per page:
|
||||
|
||||
```js
|
||||
import * as Plot from "npm:@observablehq/plot";
|
||||
```
|
||||
|
||||
Observable Framework resolves `npm:` specifiers at build time (no `npm install`
|
||||
needed in the source directory). Typical mark types used across the dashboard:
|
||||
|
||||
| Mark | Used for |
|
||||
|---|---|
|
||||
| `Plot.barX` | Horizontal stacked task-count bars, SBOM licence distribution |
|
||||
| `Plot.text` | Workstream title labels and done/total counters overlaid on bars |
|
||||
| `Plot.ruleX([0])` | Zero-axis rule on all bar charts |
|
||||
|
||||
Charts are rendered as inline SVG and inherit Observable Framework's theme
|
||||
CSS variables, so they adapt correctly to both light (`air`) and dark
|
||||
(`near-midnight`) themes.
|
||||
|
||||
---
|
||||
|
||||
## Running the dashboard
|
||||
|
||||
```bash
|
||||
cd ~/the-custodian/state-hub/dashboard
|
||||
|
||||
npm run dev # Preview server on :3000 with hot reload
|
||||
npm run build # Static build into dist/
|
||||
```
|
||||
|
||||
The API must be running (`make api` in `state-hub/`) for the data loaders and
|
||||
live polling to work. If the API is not running, the dashboard loads with empty
|
||||
data and shows the offline error state on each page.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [State Hub — Reference](/docs/state-hub) — overall architecture and design principles
|
||||
- [Live Data](/docs/live-data) — polling mechanism and offline behaviour in detail
|
||||
- [Connecting to the Hub](/docs/connecting) — MCP server registration
|
||||
- [Overview](/docs/overview) — Overview page feature walkthrough
|
||||
- [Workstreams](/docs/workstreams) — Workstreams page and WHI in depth
|
||||
@@ -858,6 +858,143 @@ def _git_commit_writeback(
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Worker orientation brief (.custodian-brief.md)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_BRIEF_HEADER = "<!-- custodian-brief: generated by fix-consistency — do not edit manually -->"
|
||||
_TASK_STATUS_ICON = {"done": "✓", "cancelled": "✗", "in_progress": "►", "blocked": "!", "todo": "·"}
|
||||
_OPEN_STATUSES = {"todo", "in_progress", "blocked"}
|
||||
|
||||
|
||||
def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> bool:
|
||||
"""Generate .custodian-brief.md at the repo root and git-commit if changed.
|
||||
|
||||
The brief gives any agent — including subagents without MCP access and
|
||||
workers on remote machines — instant orientation without a live hub
|
||||
connection. Returns True if the file was written (content changed).
|
||||
"""
|
||||
import datetime as _dt
|
||||
from datetime import timezone as _tz
|
||||
|
||||
repo = _api_get(api_base, f"/repos/{repo_slug}")
|
||||
if not repo:
|
||||
return False
|
||||
|
||||
repo_id: str = repo.get("id", "")
|
||||
domain_slug: str = ""
|
||||
|
||||
# Resolve domain slug via the topic linked to any workstream
|
||||
workstreams = _api_get(api_base, "/workstreams", {"repo_id": repo_id, "status": "active"}) or []
|
||||
if isinstance(workstreams, list) and workstreams:
|
||||
topic = _api_get(api_base, f"/topics/{workstreams[0].get('topic_id', '')}")
|
||||
if topic:
|
||||
domain_slug = topic.get("domain_slug", "")
|
||||
|
||||
# Active repo goal (first active one if multiple)
|
||||
goal_text = ""
|
||||
goals = _api_get(api_base, "/repo-goals", {"repo_slug": repo_slug}) or []
|
||||
if isinstance(goals, list):
|
||||
active_goals = [g for g in goals if g.get("status") == "active"]
|
||||
if active_goals:
|
||||
g = active_goals[0]
|
||||
goal_text = g.get("title", "") or g.get("description", "")
|
||||
|
||||
now_utc = _dt.datetime.now(_tz.utc)
|
||||
ts = now_utc.strftime("%Y-%m-%d %H:%M UTC")
|
||||
|
||||
lines = [
|
||||
_BRIEF_HEADER,
|
||||
f"# Custodian Brief — {repo_slug}",
|
||||
"",
|
||||
f"**Domain:** {domain_slug or '(unknown)'} ",
|
||||
f"**Last synced:** {ts} ",
|
||||
"**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*",
|
||||
"",
|
||||
]
|
||||
|
||||
if goal_text:
|
||||
lines += ["## Current Goal", "", goal_text, ""]
|
||||
|
||||
if isinstance(workstreams, list) and workstreams:
|
||||
lines.append("## Active Workstreams")
|
||||
for ws in workstreams:
|
||||
ws_title = ws.get("title", ws.get("slug", "?"))
|
||||
ws_id = ws["id"]
|
||||
tasks = _api_get(api_base, "/tasks", {"workstream_id": ws_id}) or []
|
||||
if not isinstance(tasks, list):
|
||||
tasks = []
|
||||
|
||||
done = sum(1 for t in tasks if t.get("status") in ("done", "cancelled"))
|
||||
total = len(tasks)
|
||||
pct = f"{done}/{total}" if total else "no tasks"
|
||||
|
||||
open_tasks = [t for t in tasks if t.get("status") in _OPEN_STATUSES]
|
||||
# Show blocked first, then in_progress, then todo (cap at 5)
|
||||
priority_order = {"blocked": 0, "in_progress": 1, "todo": 2}
|
||||
open_tasks.sort(key=lambda t: priority_order.get(t.get("status", "todo"), 9))
|
||||
|
||||
lines += [
|
||||
"",
|
||||
f"### {ws_title}",
|
||||
f"Progress: {pct} done | workstream_id: `{ws_id}`",
|
||||
]
|
||||
|
||||
if open_tasks:
|
||||
lines.append("")
|
||||
lines.append("**Open tasks:**")
|
||||
for t in open_tasks[:7]:
|
||||
icon = _TASK_STATUS_ICON.get(t.get("status", "todo"), "·")
|
||||
title = t.get("title", t["id"])
|
||||
tid = t["id"]
|
||||
status = t.get("status", "")
|
||||
blocker = t.get("blocking_reason", "")
|
||||
task_line = f"- {icon} {title} `{tid[:8]}`"
|
||||
if status == "blocked" and blocker:
|
||||
task_line += f"\n *(blocked: {blocker})*"
|
||||
lines.append(task_line)
|
||||
if len(open_tasks) > 7:
|
||||
lines.append(f"- … and {len(open_tasks) - 7} more open tasks")
|
||||
else:
|
||||
lines += ["## Active Workstreams", "", "*(none — repo may need first-session setup)*"]
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"---",
|
||||
"## MCP Orientation (when available)",
|
||||
"",
|
||||
"If the state-hub MCP server is reachable, call:",
|
||||
f"`get_domain_summary(\"{domain_slug}\")`",
|
||||
"This provides richer cross-domain context.",
|
||||
"If the MCP call fails, use this file as your orientation source.",
|
||||
]
|
||||
|
||||
content = "\n".join(lines) + "\n"
|
||||
|
||||
brief_path = Path(repo_path) / ".custodian-brief.md"
|
||||
existing = brief_path.read_text(encoding="utf-8") if brief_path.exists() else ""
|
||||
|
||||
# Strip the timestamp line before comparing to avoid spurious writes
|
||||
def _strip_ts(text: str) -> str:
|
||||
return "\n".join(
|
||||
ln for ln in text.splitlines()
|
||||
if not ln.startswith("**Last synced:**")
|
||||
)
|
||||
|
||||
if _strip_ts(content) == _strip_ts(existing):
|
||||
return False # no meaningful change
|
||||
|
||||
brief_path.write_text(content, encoding="utf-8")
|
||||
|
||||
# Commit the brief so remote workers can pull it
|
||||
_git_commit_writeback(
|
||||
repo_path,
|
||||
brief_path,
|
||||
[f"update .custodian-brief.md for {repo_slug}"],
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fix engine
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1094,6 +1231,12 @@ def fix_repo(
|
||||
now_iso = _dt.datetime.now(_tz.utc).isoformat()
|
||||
_api_patch(api_base, f"/repos/{repo_slug}/", {"last_state_synced_at": now_iso})
|
||||
|
||||
# Write the worker orientation brief (.custodian-brief.md)
|
||||
if repo_path:
|
||||
brief_written = _write_custodian_brief(api_base, repo_slug, repo_path)
|
||||
if brief_written:
|
||||
report.fixes_applied.append("brief: .custodian-brief.md updated")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
State Hub: http://127.0.0.1:8000
|
||||
|
||||
**Step 1 — Orient**
|
||||
|
||||
Read the offline-safe brief first — it works without a live hub connection:
|
||||
```bash
|
||||
cat .custodian-brief.md
|
||||
```
|
||||
Then call the MCP tool for richer cross-domain context (skip if unreachable):
|
||||
```
|
||||
get_domain_summary("{DOMAIN}")
|
||||
```
|
||||
If offline: `cd ~/the-custodian/state-hub && make api`
|
||||
If the hub is offline: `cd ~/the-custodian/state-hub && make api`
|
||||
|
||||
**Step 2 — Check inbox**
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user