Rubric-scored knowledge graph health report — checks theme attention, graph density, hub health, co-occurrence alignment, dedup pressure, stale queue, synthesis health, entity landscape, and cross-metric patterns via 12 MCP tool calls. Rubric-scored with cross-run memory (findings tracking, auto-downgrade, BASELINE.md overrides). Persists to research/brain-health/YYYY-MM-DD-brain-health.md. Scope: knowledge graph structure and quality. Calls MCP tools (analyze, thought_stats, dedup_review, review_stale, list_thoughts, list_entities, serendipity_digest) to assess graph health. NOT for: pipeline capture health — that's /pulse. NOT for: TRACKER.md document quality — that's /tracker-health. NOT for: deep research on recent thoughts — that's /discover. Use when the user says "brain health", "graph health", "how's my knowledge graph", "attention map", "theme check", or invokes /brain-health. Accepts optional days argument (default 7).
Produce a rubric-scored health report for Open Brain's knowledge graph: theme attention, graph structure, maintenance health, entity landscape, and serendipity picks. Cross-run memory for longitudinal tracking.
Parse from user message:
/brain-health or /brain-health 14.Set N = days value for all tool calls below.
Set TODAY = current date in YYYY-MM-DD format.
Call all 12 MCP tools in parallel. There are no dependencies between them.
Parallel calls:
analyze(type="themes") — theme attention map: velocity, lifecycle, centroid drift
thought_stats(days=N) — current period breakdown (type, theme, topics, people)
thought_stats(days=N*2) — double-window stats (subtract current to derive prior period)
analyze(type="density") — graph connectivity at 0.70/0.75/0.80/0.85 thresholds
analyze(type="hubs", min_connections=5) — cluster nuclei
analyze(type="co_occurrence") — usage-driven edges, session stats, decay
dedup_review(limit=30) — near-dupe zone histogram + candidate pairs
review_stale(action="list") — pending Tier 3 archival candidates
list_entities(entity_type="tool", min_thoughts=3) — tool frequency landscape
list_entities(entity_type="person", min_thoughts=3) — people frequency landscape
serendipity_digest() — forgotten high-quality thoughts
list_thoughts(type="synthesis", days=N*2, min_quality=0, limit=50) — Dream Phase C synthesis thoughts for health assessment
Glob research/brain-health/*.md, sort filenames descending, skip any
file matching TODAY's date, take the first match. Read its YAML
frontmatter and extract the findings array. If no prior report exists
or the prior report has no findings array, set prior_findings to
an empty list.
Read research/brain-health/BASELINE.md (if it exists). Parse YAML to
extract suppress and force lists. If the file does not exist, set
both lists to empty.
Items 13-14 have no dependencies on MCP tool calls — run them in parallel with everything else.
Record all results. These feed into every subsequent phase.
Deriving the prior period:
thought_stats(days=N*2) returns counts for the full 2N-day window.
To get the prior period (the N days before the current period):
prior_total = double_window_total - current_period_totalprior_theme_X = double_theme_X - current_theme_X((current - prior) / prior) * 100 (use "N/A" if prior is 0)Theme velocity note: Theme velocity comes directly from
analyze(type="themes") — it's EMA-smoothed by the weekly dream-themes
batch. Do not recompute velocity from thought_stats deltas.
Build each section from the Phase 1 data. Apply rubrics to assign status indicators.
Status indicators:
[GREEN] — healthy, no action needed[YELLOW] — warning, worth monitoring[RED] — critical, action neededBefore assembling sections, prepare the cross-run context:
prior_findings (from Phase 1, step 12) into a map keyed by key.suppress and force lists (from Phase 1, step 13).current_findings list and an empty resolved_findings list.As you assemble each section below, generate finding entries for every RED or YELLOW condition. Each finding gets:
key: deterministic identifier (see Finding Key Reference below)section: which section produced itseverity: RED or YELLOWvalue: the metric value as a stringsummary: human-readable one-linerAfter generating each finding, classify it by diffing against prior_findings:
| Current finding | In prior_findings? | Same severity? | Label | Occurrences |
|---|---|---|---|---|
| Yes | No | — | new | 1 |
| Yes | Yes | Yes | stable | prior.occurrences + 1 |
| Yes | Yes | Current < Prior | improved | 1 |
| Yes | Yes | Current > Prior | worsened | 1 |
Value stability check: For stable findings, compare values numerically
where possible. If the value changed by more than 5% relative, reset to
label=new, occurrences=1 instead. This prevents a worsening metric from
hiding behind a "stable" label.
After all sections are assembled, compute resolved_findings: any key in
prior_findings that is not in current_findings. Record each with its
prior severity.
Apply BASELINE overrides (in this order):
force list: mark as forced —
it stays in Suggested Actions regardless of occurrences.suppress list AND NOT in force:
mark as known — it goes to Known Conditions regardless of occurrences.occurrences >= 3, mark as known
(auto-downgraded). Otherwise, it stays active.Partition findings:
new, stable with occurrences < 3,
worsened, improved, or forced) → feed into Suggested Actions.known via suppress or auto-downgrade) →
feed into Known Conditions section.Finding Key Reference:
| Finding type | Key pattern | Example |
|---|---|---|
| Theme declining | theme-declining-{name} | theme-declining-infrastructure |
| Theme dormant | theme-dormant-{name} | theme-dormant-side-projects |
| Theme emerging (informational) | theme-emerging-{name} | theme-emerging-ml-research |
| Theme drift | theme-drift-{name} | theme-drift-ml-research |
| Graph orphan ratio | graph-orphan-ratio | graph-orphan-ratio |
| Graph sparse | graph-sparse | graph-sparse |
| Low hub count | graph-low-hubs | graph-low-hubs |
| Co-occurrence no edges | cooccurrence-no-edges | cooccurrence-no-edges |
| Co-occurrence stale sessions | cooccurrence-stale-sessions | cooccurrence-stale-sessions |
| Dedup zone 0.95+ | dedup-zone-95plus | dedup-zone-95plus |
| Dedup high pressure | dedup-high-pressure | dedup-high-pressure |
| Stale queue backlog | stale-queue-backlog | stale-queue-backlog |
| Synthesis inactive | synthesis-inactive | synthesis-inactive |
| Synthesis stale | synthesis-stale | synthesis-stale |
| Synthesis low coverage | synthesis-low-coverage | synthesis-low-coverage |
| Entity concentration | pattern-entity-concentration-{name} | pattern-entity-concentration-openai |
| Attention narrowing | pattern-attention-narrowing |
Data source: analyze(type="themes") result + thought_stats(days=N) derived delta.
Format as a table:
| Theme | Lifecycle | Velocity | Thoughts | Period Delta | Centroid Drift |
|---|
One row per theme from the themes data. Sort by thought_count descending.
Velocity is thoughts/week (EMA-smoothed by dream batch). Period delta is
derived from thought_stats current vs prior period for the by_theme
breakdown.
Overall section rubric:
[GREEN]: All themes are active, emerging, or mature[YELLOW]: 1-2 themes are declining[RED]: 3+ themes declining OR any theme dormantFinding annotations: For each declining theme, generate a finding with
key theme-declining-{name}. For each dormant theme, generate a finding
with key theme-dormant-{name}. For each emerging theme, generate an
informational finding (YELLOW) with key theme-emerging-{name} — this is
a positive signal, not a warning; annotate it as "(informational)".
Annotate each finding in the output with its label: (new), (stable, Nth run), (worsened), (improved), or (known).
Data source: analyze(type="themes") — centroid_drift field.
List any themes with centroid_drift > 0, sorted descending. Show the drift value and a brief interpretation:
0.05: significant drift — theme may be splitting
Bootstrap gate: If the latest_snapshot_date field shows only one
snapshot exists (all themes have the same date and it matches the first
backfill), output: "Insufficient snapshots for drift analysis — drift
requires 2+ weekly dream batch runs." Score as [GREEN] and skip
finding generation.
Rubric (when sufficient data):
[GREEN]: All centroid_drift < 0.03[YELLOW]: Any drift 0.03-0.05[RED]: Any drift > 0.05Finding key: theme-drift-{name} for each theme exceeding 0.03.
Data source: analyze(type="density") result.
Format as a table:
| Threshold | Avg Connections | Median | Zero-Link (Orphans) | 10+ Links |
|---|
One row per threshold from the density data (typically 0.70, 0.75, 0.80, 0.85).
Compute orphan ratio from the 0.70 threshold row:
orphan_ratio = zero_link_count / (zero_link_count + non_zero_count)
If orphan ratio is not directly available, compute from total thoughts
(from thought_stats) minus non-orphan count.
Rubric:
[GREEN]: Orphan ratio <15% at 0.70 AND avg connections >= 2.0 at 0.70[YELLOW]: Orphans 15-30% OR avg connections 1.0-2.0[RED]: Orphans >30% OR avg connections < 1.0Finding keys:
graph-orphan-ratio if orphan ratio >= 15%graph-sparse if avg connections < 2.0 at 0.70Data source: analyze(type="hubs", min_connections=5) result.
Count total hubs returned. Show the top 5 hubs with:
| # | Preview | Source | Connections |
|---|
Rubric:
[GREEN]: 5+ hubs[YELLOW]: 2-4 hubs[RED]: 0-1 hubsFinding key: graph-low-hubs if hub count < 5.
Data source: analyze(type="co_occurrence") result.
Report:
Bootstrap gate: If total edges < 20, output: "Co-occurrence layer is
bootstrapping ([N] edges). Shipped 2026-04-06 — building up from retrieval
sessions." Score as [GREEN] and skip finding generation.
Rubric (when sufficient data):
[GREEN]: Edges exist and sessions recorded in last 7 days[YELLOW]: Low edge count (<10 after bootstrap) OR no sessions in 3+ days[RED]: Zero edges (after bootstrap period) OR no sessions in 7+ daysFinding keys:
cooccurrence-no-edges if zero edges after bootstrapcooccurrence-stale-sessions if no sessions in 3+ daysData source: dedup_review(limit=30) result.
Format the zone histogram:
| Zone | Pairs | Meaning |
|---|---|---|
| 0.95+ | N | Should have auto-merged (Dream Phase A) |
| 0.92-0.95 | N | LLM confirmation zone |
| 0.88-0.92 | N | Near-miss territory |
| 0.85-0.88 | N | Normal similarity |
Below the histogram, show the top 3 candidate pairs from the 0.95+ zone (if any) with similarity score and content previews.
Rubric:
[GREEN]: 0.95+ zone has < 3 pairs[YELLOW]: 0.95+ zone has 3-10 pairs[RED]: 0.95+ zone has > 10 pairs (auto-merge may be broken)Finding keys:
dedup-zone-95plus if 0.95+ zone >= 3 pairsdedup-high-pressure if total candidates across all zones > 20Data source: review_stale(action="list") result.
Report:
Rubric:
[GREEN]: 0 pending[YELLOW]: 1-5 pending[RED]: 6+ pendingFinding key: stale-queue-backlog if pending > 0.
Data source: list_thoughts(type="synthesis", days=N*2, min_quality=0) result
thought_stats(days=N) and thought_stats(days=N*2) type breakdowns.Derive counts from thought_stats by_type field:
current_syntheses = by_type.synthesis from thought_stats(days=N) (0 if absent)double_syntheses = by_type.synthesis from thought_stats(days=N*2) (0 if absent)prior_syntheses = double_syntheses - current_synthesesFrom list_thoughts result, compute:
avg_coverage = mean of metadata.coverage_score across returned thoughtsavg_cluster_size = mean of metadata.cluster_size across returned thoughtstotal_syntheses = count of returned thoughts (all syntheses in 2N window)Format as a table:
| Metric | Value |
|---|---|
| Syntheses this period | N (delta vs prior: +/-X) |
| Avg coverage score | 0.XX |
| Avg cluster size | N.N |
| Total (last 2N days) | N |
If syntheses exist in the period, show the top 3 most recent with:
| # | Theme | Coverage | Cluster Size | Created |
|---|---|---|---|---|
| 1 | theme | 0.XX | N | YYYY-MM-DD |
Bootstrap gate: If total_syntheses = 0, output: "Dream Phase C has
not produced insights yet — first results expected after the next weekly
run (Sunday 09:00 UTC)." Score as [GREEN] and skip finding generation.
Rubric (when data exists):
[GREEN]: current_syntheses >= 1 AND avg_coverage >= 0.75[YELLOW]: current_syntheses = 0 (no new clusters processed this period)
OR avg_coverage between 0.70 and 0.75[RED]: current_syntheses = 0 AND prior_syntheses = 0 (no synthesis
output for 2× the reporting period — Phase C may be broken) OR
avg_coverage < 0.70Finding keys:
synthesis-inactive if current_syntheses = 0 but total_syntheses > 0
(Phase C didn't produce output this period)synthesis-stale if current_syntheses = 0 AND prior_syntheses = 0
AND total_syntheses > 0 (no output for 2× period — RED)synthesis-low-coverage if avg_coverage < 0.75Detect patterns only when conditions are met. Only include patterns where the detection condition fires — omit the rest entirely.
| Pattern | Detection Condition | Template |
|---|---|---|
| Attention narrowing | Dominant theme >40% of total thoughts AND hubs from analyze(type="hubs") are concentrated (>50%) in that same theme | "[Theme] holds [X]% of thoughts and [Y]% of hubs — attention narrowing, breadth declining" |
| Capture-connection gap | Capture volume delta >20% increase (from thought_stats) BUT orphan ratio also increased vs prior run | "Capture volume up [X]% but orphan ratio worsened to [Y]% — new thoughts not connecting" |
| Velocity-quality divergence | Any theme has velocity >5 AND that theme's avg_quality (from theme_tracking in thought_stats) is declining | "[Theme] velocity [X] but avg quality dropping — volume outpacing signal" |
| Stale theme accumulation | Any declining/dormant theme's thoughts appear in stale queue candidates | "[Theme] is [lifecycle] and has [N] thoughts in stale queue — natural decay in progress" |
| Entity concentration | Any entity from list_entities has thought_count > 15% of total thoughts | "[Entity] appears in [N] thoughts ([X]% of total) — over-indexed on this [type]" |
Finding keys: pattern-attention-narrowing, pattern-capture-connection-gap,
pattern-velocity-quality-divergence, pattern-stale-theme-accumulation,
pattern-entity-concentration-{name}
For each detected pattern, generate a YELLOW finding. Annotate with cross-run label as usual.
Data source: list_entities(entity_type="tool") + list_entities(entity_type="person") results.
Format as two tables:
Top Tools:
| Tool | Thoughts | % of Total |
|---|
Top People:
| Person | Thoughts | % of Total |
|---|
Show top 5 of each. Compute % of total from thought_stats total count.
Flag any entity exceeding 15% (feeds into cross-metric pattern detection).
This section is informational — no rubric scoring.
Data source: serendipity_digest() result.
Show 1-2 forgotten high-quality thoughts with:
No rubric — this is a "you might want to revisit" nudge at the end of the report. Frame it as: "Forgotten gems — high-quality thoughts that haven't been accessed recently."
List all findings classified as known (via BASELINE.md suppress or
auto-downgrade at 3+ occurrences). For each:
Only include this section if known findings exist.
Generate concrete, data-backed actions from active RED and YELLOW findings only. Findings classified as Known Conditions are excluded — they appear in the Known Conditions section instead.
Action generation rules:
For each RED rubric in Sections 1-8, generate a specific action:
review_stale(action='list')"run-dream-synthesis GitHub Action is running (Sunday 09:00 UTC)"For each YELLOW rubric, generate a monitoring note:
For each detected cross-metric pattern, generate a follow-up:
Always include if applicable:
dedup_review 0.95+ zone > 0:
"Review [N] dedup candidates in 0.95+ zone via dedup_review()"review_stale()"Render the full report inline to the user with all sections. Use markdown formatting — the terminal renders it.
Start with a one-line summary:
Brain Health: [X] GREEN, [Y] YELLOW, [Z] RED — [top finding summary or "all clear"]
If resolved_findings is non-empty, add immediately after the summary:
"Resolved since last brain-health: {key1} (was {severity1}), {key2} (was {severity2}), ..."
Omit if nothing resolved.
End with: "Report saved to research/brain-health/[TODAY]-brain-health.md."
Write the report to research/brain-health/YYYY-MM-DD-brain-health.md
using the Write tool. If a file already exists for today, overwrite it
(latest run wins).
File structure:
Frontmatter:
---
pattern-attention-narrowing| Capture-connection gap | pattern-capture-connection-gap | pattern-capture-connection-gap |
| Velocity-quality divergence | pattern-velocity-quality-divergence | pattern-velocity-quality-divergence |
| Stale theme accumulation | pattern-stale-theme-accumulation | pattern-stale-theme-accumulation |