Health Center
The Health Center is vita's unified health surface: a read-only web app at /health that composes Garmin telemetry, lab/DEXA extracts, a clinical sub-agent's working memory, and an open-loops board into one "command center." Eleven tabs render a single read-model contract; every concrete number, band, and status is computed below the API boundary so the React client never parses a raw data file.
Scope note. This page documents the engine β the data model, normalization, banding math, and where state lives β not anyone's actual health contents. Example markers, ranges, and values below are illustrative placeholders. For the predictive sleep model see Sleep Guardian; for the daily input path see Check-in; for model routing see Model Guidance.
What it is (and isn't)
The Health Center is the headline health overhaul shipped mid-2026. A few orienting facts a reimplementer needs up front:
- It is read-only. The web UI never mutates health data. Writes happen elsewhere (the clinical sub-agent, the loops CLI, Garmin sync, the Check-in flow, Goals).
- There is no
/healthslash command. The "command center" is the web home β the index tab is literally labeled Command. (Slash commands like/checkin,/goal,/insights,/sleep-risk,/sleep-guardian-tuneare separate touchpoints, documented on their own pages.) - Each tab is backed by a thin FastAPI router that delegates to a service; all services share one
HealthRepositorynormalization layer. The client uses react-query caching.
web (/health, 11 tabs) api (port 33800) on-disk state
βββββββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββββββββ
HealthLayout (tabs) routers/health_*.py data/garmin/YYYY-MM-DD.json
β³ react-query hooks ββ GET ββΆ β³ services/health_*.py memory/extracted/medical/*
(useHealth.ts) β³ repositories/ memory/extracted/performance/*
typed read-model health_center.py data/health/{metric_registry,loops,events}.json
(features/health/types.ts) (HealthRepository) data/doctor/* Β· data/sleep/* Β· data/checkins/*
profile/medications.json Β· goals/active/*
The 11 tabs
Each browser route under /health mounts one React view and calls one (or two) endpoints. Routes are TanStack-Router file routes under web/src/routes/health/.
| Tab | Route | View component | Endpoint(s) |
|---|---|---|---|
| Command | /health |
HealthDashboard |
GET /api/v1/health/overview |
| Explore | /health/explore |
MetricExplorer |
GET /api/v1/health/metrics, /metrics/{key}/series |
| Rhythm | /health/rhythm |
RhythmView |
GET /api/v1/health/rhythm/{metric} |
| Sleep | /health/sleep |
SleepView |
GET /api/v1/health/sleep |
| Body | /health/body |
BodyCompView |
GET /api/v1/health/body-comp |
| Regimen | /health/regimen |
RegimenView |
GET /api/v1/health/regimen |
| Doctor | /health/doctor |
DoctorReadView |
GET /api/v1/health/doctor-read |
| Check-ins | /health/checkins |
CheckinsView |
GET /api/v1/health/checkins |
| Visits | /health/visits |
VisitsView |
GET /api/v1/health/visits |
| Genetics | /health/genetics |
GeneticsView |
GET /api/v1/health/genetics |
| Prep | /health/prep |
AppointmentPrep |
GET /api/v1/health/prep |
HealthLayout.tsx owns the tab bar and a dark/light theme toggle (persisted to localStorage); every tab renders inside its <Outlet/>.
API surface
All endpoints live under the /api/v1/health prefix and are registered in api/src/main.py. They are spread across eight routers (the bare /api/v1/health healthcheck lives in a ninth, routers/health.py, and does not collide because the center uses only subpaths).
| Method Β· Path | Router | Query params | Returns |
|---|---|---|---|
GET /overview |
health_center |
days (7β365, default 30) |
HealthOverview |
GET /metrics |
health_center |
days |
MetricTile[] (every registry metric with data) |
GET /metrics/{key}/series |
health_center |
days (default 90) |
MetricSeriesResponse |
GET /rhythm/{metric} |
health_center |
days (3β60, default 14) |
RhythmResponse |
GET /prep |
health_center |
β | PrepPacket |
GET /source-pdf |
health_center |
path (required) |
FileResponse (PDF) |
GET /regimen |
health_regimen |
β | RegimenResponse |
GET /doctor-read |
health_doctor |
β | DoctorReadResponse |
GET /body-comp |
health_bodycomp |
β | BodyCompResponse |
GET /checkins |
health_checkins |
days (14β365, default 60) |
CheckinsResponse |
GET /visits |
health_visits |
β | VisitsResponse |
GET /genetics |
health_genetics |
β | GeneticsResponse |
GET /sleep |
health_sleep |
days (14β365, default 90) |
SleepResponse |
Dependency injection wires two @lru_cache singletons (api/src/dependencies_data.py): get_health_center_repository() constructs a HealthRepository(settings.data_dir), and get_health_center_service() wraps it in a HealthCenterService. All endpoints are synchronous def (single-user personal app β sync I/O is fine; the client caches).
The normalization layer (HealthRepository)
api/src/repositories/health_center.py is the heart of the system. It reads heterogeneous on-disk sources and projects them into clean dicts the services compose. A reimplementer should treat this file as the contract for "how messy real health data gets coerced into something chartable."
Source directories
| Logical source | On-disk location |
|---|---|
| Garmin daily | data/garmin/YYYY-MM-DD.json |
| Lab extracts | memory/extracted/medical/labs_*.json (~22 files) |
| Performance extracts (DEXA / VO2max) | memory/extracted/performance/performance_*.json (7 files) |
| Original lab/DEXA PDFs | memory/sources/medical/, data/medical/labs/ |
| Metric registry | data/health/metric_registry.json |
| Open loops | data/health/loops.json |
| Health-event annotations | data/health/events.json |
| Sleep Guardian | data/sleep/{risk-model,guardian_config}.json, risk-log.jsonl, outcomes.jsonl |
| Clinical sub-agent | data/doctor/{working-memory.md, protocols.jsonl, interventions.jsonl, research-queue.jsonl, observations.jsonl} |
| Medications | profile/medications.json |
| Check-ins | data/checkins/YYYY-MM-DD.json |
| Calendar | data/calendar/events.json (mtime-cached; ~11 MB) |
| Live task mirror | Precedent at http://localhost:5424 (1.5 s timeout, 30 s cache) |
Lab extract parsing β one recursive walk for many shapes
Lab/DEXA extracts arrive in at least five on-disk shapes across vendors and years:
A) categories β { markers: { key: {value...} } }
B) flat top-level marker keys { marker_x: {value...} }
C) panels: [ { panel_name, results: [ {marker, value} ] } ]
D) nested { marker_x: { marker_x_assay: {value...} } }
E) history { marker, results|history: [ {date, value} ] }
A single recursive walk() handles all of them: descend the tree, carry the nearest marker-name seen, and emit any leaf with a value whose own-or-inherited name resolves (via the registry alias map) to a canonical metric. Key behaviors:
- Key normalization (
_norm_key): drops a trailing assay qualifier β"Marker Name (Direct)","(LC/MS)","(calc)"β and lowercases/underscores, so a labeled assay still resolves to its base marker alias. - Reference-range parsing (
parse_ref_range): turns range strings into(low, high)β e.g."52-328","82β237","25.8~60.7","4 to 15","<0.5" β (None, 0.5),">1500" β (1500, None). - Censored values: a bounded assay result (
">358.86","<0.5") is an inequality, not a point. It is flaggedcensored=True, excluded from baselines/averages, and the UI marks it as a bound rather than plotting it as exact. - Per-marker PDF linking (
_match_pdf): when one lab file holds several markers and several PDFs, link each marker to the PDF whose filename tokens best overlap the marker name. A single candidate is used directly; ties with no real overlap link to nothing β "a wrong source link is a lying instrument; no link is the honest fallback." - Dedup: one record per
(marker, date), preferring the richest (has reference range, then PDF, then status). A global second pass dedups across files.
Source-PDF gatekeeper
GET /source-pdf?path=... serves an original lab/DEXA PDF as a FileResponse. It is path-traversal-safe and internal-only: resolve_medical_pdf() resolves the request and returns it only if the resolved path is a real .pdf file living inside one of the medical archive dirs. Crucially, the PDF producer (_pdf_candidates) enforces the same archive-membership rule, so it never advertises a path the gatekeeper would 404 β the two halves agree on the trust boundary.
Metric registry
data/health/metric_registry.json is the single source of truth for "what /health can chart, how to band it, and how to parse it from labs." It is read-only from the web UI.
{
"_comment": "READ-ONLY from the web UI ...",
"updated_at": "YYYY-MM-DD",
"metrics": [
{
"key": "example_marker",
"label": "Example Marker",
"unit": "mg/dL",
"domain": "metabolic",
"source": "labs",
"directionality": "lower_better",
"decimals": 0,
"optimal_min": null, "optimal_max": 99,
"reference_min": 70, "reference_max": 140,
"gate": null,
"aliases": ["raw lab key 1", "raw lab key 2"]
}
]
}
| Field | Meaning |
|---|---|
key / label / unit / decimals |
Canonical id, display label, units, render precision |
domain |
Grouping (cardio, endocrine, metabolic, sleep, recovery, training, body_comp, inflammation, liver, kidney, β¦) |
source |
garmin | labs | performance β where the data comes from |
directionality |
higher_better | lower_better | range β drives status logic |
optimal_* |
Opinionated functional target band (a clinical goal, not just population reference) |
reference_* |
Population/lab reference band (the softer fallback) |
target / gate |
Optional goal line / hard threshold |
aliases[] |
Raw lab keys that the normalizer maps onto this canonical marker |
The registry currently defines on the order of two dozen metrics spanning Garmin recovery/training signals and lab/DEXA markers. Count them from the file rather than hard-coding a number.
Metric tiles: banding, baseline, z-score, status
The Command and Explore tabs render MetricTiles. The service (api/src/services/health_center.py) builds each tile from the metric's data points:
- latest / previous / delta β last two points.
- baseline β the mean of the windowed points (
statistics.fmean). - z-score β
(latest β baseline) / pstdev, only when there are β₯ 4 points and stdev > 0 (elsenull). This is the falsifiable "how far from your own normal is today" signal. - status β
good | watch | bad | neutralfrom value + bands + directionality (_status_for). - stale_days β days since the latest point; attention is
Truewhen status isbad.
_status_for logic by directionality (simplified):
| Directionality | good | watch | bad |
|---|---|---|---|
lower_better |
β€ optimal_max |
β€ reference_max |
above reference |
higher_better |
β₯ optimal_min |
β₯ reference_min (or within 90% of optimal) |
below |
range |
inside optimal band | inside reference band | outside both |
A gate below the value short-circuits to bad.
Lab tiles judge against the point's own reference range
This is the subtle correctness feature. For lab/performance markers, the tile's mini-trend and status stay within the latest source/assay (never plot a cross-assay jump as a trend), and status is computed by _lab_status:
- If the registry defines a functional optimal band (an intentional clinical target), use that.
- Otherwise judge against the latest point's own reference range parsed from the lab itself β never a stale single-assay registry band. A value can read green against the assay that produced it even if a registry band copied from a different assay would call it out of range. When status is judged this way, the tile caption shows that range so the colored dot and the "ref β¦" label agree.
- Fall back to the registry reference band only if the point has no two-sided parseable range.
_unit_consistent also drops points whose unit differs from the latest non-null unit, preventing a false trend/z when a marker's unit changed across labs (e.g. ng/dL β pg/mL).
Metric series (drill-down)
GET /metrics/{key}/series returns the full history for one marker. For multi-assay lab markers it returns all sources as LabMarkerPoints β each carrying its parsed ref_low/ref_high, a normalized position within its own range ((value β low) / (high β low), so different-assay values become comparable as "% of range"), the censored flag, the source label, and the linked PDF path. multi_source + sources[] let the UI segment by assay. annotations[] overlays health-event markers from data/health/events.json that fall inside the displayed range.
The Command overview
GET /overview composes the whole command screen in one payload (HealthOverview):
| Section | How it's built |
|---|---|
hero[] |
Headline HeroStates: sleep gate, sleep risk, open-loops count, next-up event, data-freshness. |
loops[] |
Ranked open clinical loops (see below). |
upcoming[] |
Health-relevant calendar events + protocol-scheduled panels, de-duped, next 8. |
freshness[] |
Per-source live | stale | offline for Garmin, CGM, Labs, and the sub-agent's working memory. |
metrics[] |
A curated pinned grid of tiles (clinical markers first, then recovery/training). |
goals[] |
Active physical/fitness goals with progress (read from goals/active/*.json). |
Upcoming events are filtered by a health regex over event titles (lab draws, scans, endocrine/cardiology visits, etc.) and classified lab | test | appointment. Freshness deliberately watches the sub-agent's own instruments too: a stale working-memory file is surfaced as stale with a "working memory Nd stale" detail rather than silently trusted β making a lying instrument legible.
Open clinical loops board
A Tier-0 "needs attention" board of open clinical action items (a draw to schedule, a scan to book, a result to chase, a follow-up). It is read-only on the dashboard; the write side is owned by scripts/health/loops.py (launcher bin/health-loops), and the clinical sub-agent maintains it through that CLI.
./bin/health-loops list [--all] # current board (or incl. done)
./bin/health-loops show <id>
./bin/health-loops add <id> --title "..." --importance high --urgency this_week \
--next-action "..." [--due YYYY-MM-DD] [--consequence "..."] [--kids] \
[--precedent-task <key>] [--source "..."]
./bin/health-loops update <id> [--status ...] [--due ...]
./bin/health-loops close <id> --reason "..." # resolved β drops off the board
./bin/health-loops snooze <id> --until YYYY-MM-DD --reason "..."
# equivalently: python -m health.loops <cmd>
Stored in data/health/loops.json. Per-loop schema and enums:
| Field | Values |
|---|---|
importance |
critical \| high \| medium \| low |
urgency |
now \| this_week \| this_month \| eventual |
status |
open \| waiting \| scheduled \| monitoring \| snoozed \| done |
owner |
me \| doctor \| lab \| pcp |
| plus | id, title, category, opened, due, next_action, consequence, kids_relevant, sources[], precedent_task |
Ranking (service-side): importance weight + urgency weight + +25 overdue + +15 if relevant to dependents + +10 if waiting/open. Precedent mirroring: a loop with a precedent_task key has its status/due mirrored live from Precedent (read-through overlay, 30 s cache) β if the linked task is completed, the loop drops off the board; if Precedent is unreachable, it falls back to the curated fields. Loops with status: done are filtered out entirely.
Long-horizon clinical commitments can be represented as monitoring loops and
paired with a high-tier proactive reminder ahead of the due date. The standing
six-month lab-panel cadence uses this pattern: the loop is the durable clinical
expectation shown by Health Center and the doctor digest, while the deduplicated
reminder is delivery plumbing. Health cadence reminders have an isolated
admission tier so ordinary outreach volume cannot starve a rare, high-value
health prompt.
Rhythm: the "typical day" envelope
GET /rhythm/{metric} builds an AGP-style typical-day envelope (median + percentile bands) over a window for an intraday Garmin metric β so you see the shape of a normal day rather than a single daily number.
- Supported metrics:
heart_rate,stress,body_battery(an unknown metric 404s). - Intraday samples are bucketed into 30-minute bins of local minute-of-day (UTC offset derived per day from
sleep_start_localvssleep_start_utc), with Garmin's negative no-data sentinels dropped. - Each
RhythmBincarriesmedian,p25/p75,p10/p90, andn(sample count).
Sleep page
GET /sleep unifies four sleep layers the rest of vita keeps in separate places, into one SleepResponse:
- Nightly architecture β per-night
SleepNight: duration, score, deep/light/rem/awake (hours + % of asleep), bedtime/waketime, a trailing 7-night rolling mean, HRV/RHR/respiration, and abelow_gateflag. - Training sleep-gate β
GateState: the 7-day average over the last 7 calendar days vs the 6.5 h board-resolution threshold, withmargin,blocked, anddays_with_data. The window matchesscripts/scheduling/sleep_gate.pyexactly so the page never claims "blocked" when the real gate isn't (e.g. during a sync gap). The gate suspends max-effort intervals only, not all training. - Consistency/regularity β bed/wake midpoint stdev computed here, labeled
very regular | regular | variable | irregular, plus median typical bed/wake clock times. A monotonic "minutes after 18:00" axis sorts evening bedtimes below morning wakes. - Sleep Guardian β the predictive risk model and its calibration.
Nightly values are override-aware. A correction in
data/sleep/overrides.jsonl replaces a bad Garmin duration without mutating the
raw instrument file; a night marked invalid is excluded rather than counted as
zero. If the recording itself was broken, stage/score/bedtime fields are
suppressed for that night while independently measured HRV/RHR/respiration can
remain. This is the same read contract used by the gate and risk scorer, so the
web page cannot disagree with the training decision.
Guardian calibration (honest scoring)
Calibration scores the Sleep Guardian over its last ~30 resolved nights against its own recorded outcomes (prediction_correct):
accuracy_pctβ the Guardian's hit-rate.base_rate_pctβ how often nights were actually poor.trivial_pctβ accuracy of the trivial baseline (always guess the common outcome).precision_pctβ of risk calls (yellow/red), the fraction that were truly poor.has_edgeβaccuracy > trivial. A risk model is only useful if it beats the trivial baseline; the summary says so in plain language and tells you to treat a flag as a prompt, not a verdict, when there's no edge. The displayed nights are exactly the ones the accuracy is computed over.
The page reconciles the gate as a single source of truth: the Guardian's stored gate_margin (a different snapshot) is overwritten with the live gate's margin.
Body composition (DEXA / VO2max)
GET /body-comp parses the DEXA and VO2max performance extracts into a longitudinal read-model (api/src/services/health_bodycomp.py). Two correctness features dominate:
Defensive label matching. Vendor panel/marker names drift across report years (a ratio called "A/G Ratio" in one, "Android/Gynoid Ratio" in another; "Visceral Fat Content (VAT)" vs "Visceral Fat (VAT)"). Every scan is flattened to a {marker: value} map and matched by case-insensitive substring with exclusions, never exact label β so whole-body "Lean Tissue" isn't confused with "Trunk Lean Tissue."
LSC noise-gating β the headline correctness feature. DEXA has precision error, so a delta smaller than the Least Significant Change is measurement noise, not a trend. Every headline delta carries a real_change flag set False when |delta| β€ LSC, and the LSC threshold itself is returned to the UI as a footnote. Per-metric thresholds (illustrative method constants, in each metric's own units):
| Metric | LSC |
|---|---|
| Total body-fat % | 0.8 pct-pts |
| Lean mass (whole body) | 1.5 lb |
| Regional lean | 1.0 lb |
| Regional fat % | 1.5 pct-pts |
| Visceral fat | 0.05 lb |
| RSMI | 0.30 kg/mΒ² |
| A/G ratio | 0.05 |
| Total BMD | 0.020 g/cmΒ² |
The payload also includes a regional composition table (arms/legs/trunk, noise-gated), limb symmetry (left/right lean gap banded by injury-risk thresholds: <5% good, 5β10% watch, >10% bad), bone BMD by site leading with the age-matched Z-score (T-score deliberately omitted), and a VAT trend series across scans.
Regimen & Research
GET /regimen composes the active-interventions view from three sources, normalized in api/src/services/health_regimen.py:
- Medications β
profile/medications.jsonprescriptions (name, dose, frequency, status, prescriber, reason). - Supplements / topicals β the sub-agent's
protocols.jsonl, bucketed by keyword (topicals vs supplements; diagnostics/test-kits/subscriptions are excluded by name). - Research verdicts β the sub-agent's
interventions.jsonlβ each anADOPT | MONITOR | SKIPevaluation with an evidence grade, relevance, overall score, summary, and a linked report slug.
Two notable behaviors: a protocol that merely restates an existing prescription is folded into that medication's detail rather than duplicated (with near-duplicate notes suppressed so you don't get a redundant tail); and a research verdict whose compound name-matches an active medication is annotated with prescribed_match β a SKIP on a drug already prescribed is a net-new-adoption question, not advice to stop taking it. The updated date is the max ISO date across medications.json and every surfaced protocol change, so the surface doesn't read stale when a supplement changed without a meds-file edit.
Doctor's Read + the clinical sub-agent
The Health Center surfaces a domain-expert clinical sub-agent. Two layers:
The sub-agent (data/agents/doctor.json, prompt data/agents/prompts/doctor.md)
A personal longevity researcher β explicitly not a clinician. It does not diagnose or prescribe; it observes trends in Garmin/CGM/check-in/medical data, maintains working memory, tracks a protocol stack, queues research questions for a real doctor, and maintains the open-loops board. State lives in data/doctor/:
| File | Role |
|---|---|
working-memory.md |
Current signals / open questions / data gaps (starts with a Last updated: line) |
observations.jsonl |
Notable measurements (timestamp, metric, value, baseline, note) |
protocols.jsonl |
Active interventions (name, started, reason, source, status) |
interventions.jsonl |
Research verdicts (ADOPT/MONITOR/SKIP) |
research-queue.jsonl |
Questions to bring to a real clinician (priority, rationale) |
intake-log.jsonl |
Audit log of data consumed each run |
Triggers (from doctor.json): a weekly digest (scheduled, Saturday evening PT) that reviews the week and reconciles the loops board, a per-garmin-sync daily review (event-triggered) for anomaly scanning, and an on-demand Telegram topic. A staleness.max_age_hours of 168 (7 days) marks the agent's state stale past a week.
The Doctor's Read surface (GET /doctor-read)
api/src/services/health_doctor.py parses the agent's working-memory.md markdown into three lists β current signals, open questions, data gaps β by matching ##/### headers case-insensitively (header wording is allowed to drift). It pulls the newest ~10 observations.jsonl entries into a newest-first timeline, and computes a stale_days freshness from the file's Last updated: line (falling back to mtime). The working memory is deliberately allowed to drift stale; stale_days is passed straight through so the UI shows a visible badge β an instrument made honest rather than hidden.
Check-ins (subjective vs measured)
GET /checkins reads the daily check-in files and aligns each day's self-report (energy, sleep quality, mood, notes) with same-day Garmin recovery (overnight HRV, measured sleep) so felt-vs-measured divergence is chartable. It handles two on-disk shapes (a modern snapshot{...} form and a legacy responses[] Q&A form), maps qualitative ratings (high/good/fair) to a coarse 0β3 scale, and is honest about provenance:
- Auto-generated entries (synthesized from telemetry, e.g.
source: conversation_exhaustor non-humandepth) are flaggedinferred=True. They still chart (they're the only signal available) but are labeled "inferred (auto)." confident_dayscounts only genuine self-reports (a realdeep/quick/standarddepth and a non-auto source), so machine-synthesized entries can't inflate self-report coverage β a guard against the chart's subjective line silently tracking the very telemetry it's plotted against.
The Garmin overlay spans the actual calendar range of the returned check-in points (not a fixed today β days window), because the repository returns the last N check-in files, which can be sparse and reach further back.
The read-only Check-ins view is distinct from the interactive Check-in command/skill that writes
data/checkins/YYYY-MM-DD.json.
Visits
GET /visits composes a chronological (newest-first) provider visit history from heterogeneous on-disk visit records. The source fields are irregular β some strings, some lists, some nested dicts β so the service coerces everything into one typed Visit (date, provider, type, summary, recommendations, action items, nutrition guidance, follow-up). Unparseable/missing dates sink to the bottom rather than floating up via lexical comparison.
Genetics (anticipatory)
GET /genetics renders an anticipatory "what's coming" state for an ordered whole-genome panel until results land. The contract is the same shape in both states so the frontend never special-cases a missing payload:
availabletracks parsed results, not raw file presence β a stray/partial JSON in a genetics dir can't flip the header to "results in" while the body shows none.pending[]describes the ordered panel (vendor, status pulled from the protocol stack, expected results window, and the report categories it will return).- Result parsing is wired-on-arrival:
_results_present()scans thegenetics/genomics/dnaextract directories; when a results file appears, parsing populatesresults[]andavailableflips True naturally.
Appointment prep
GET /prep builds a doctor-facing prep packet for the next clinical appointment so a real visit starts from current data. The service picks the next upcoming appointment, derives a focus from its title (endocrine vs body-composition vs general β a different relevant marker set), and assembles:
markers[]β the latest value + reference range + delta for each focus marker (status via_lab_status).protocols[]β currently active interventions.open_questions[]β the top research-queue items by priority.observations[]β recent sub-agent observations.- a one-line
summary(focus, appointment, marker count, count outside optimal, open-question count).
File locations
| Path | Role |
|---|---|
web/src/features/health/ |
All 11 view components + HealthLayout, types.ts |
web/src/routes/health/*.tsx |
TanStack file routes (one per tab) |
web/src/hooks/useHealth.ts |
react-query hooks (overview/metrics/series/rhythm/prep) |
api/src/routers/health_*.py |
Eight thin routers (+ health.py healthcheck) |
api/src/services/health_*.py |
Read-model composition |
api/src/repositories/health_center.py |
The HealthRepository normalization layer |
api/src/schemas/health_*.py |
Typed read-model contracts |
api/src/dependencies_data.py |
@lru_cache repo/service factories |
data/health/metric_registry.json |
Chartable-metric source of truth |
data/health/loops.json |
Open clinical loops board (write via bin/health-loops) |
scripts/health/loops.py Β· bin/health-loops |
Loops write-side CLI |
data/agents/doctor.json Β· data/agents/prompts/doctor.md |
Clinical sub-agent config + prompt |
data/doctor/ |
Sub-agent state (working memory, observations, protocols, interventions, research queue) |
memory/extracted/{medical,performance}/ |
Lab + DEXA/VO2max JSON extracts |
memory/sources/medical/, data/medical/labs/ |
Original lab/DEXA PDFs |
Where to go next
- Sleep Guardian β the predictive risk model and learning loop the Sleep tab surfaces.
- Check-in β the interactive flow that writes the data the Check-ins tab reads.
- Goals β the active-goals store the overview pulls progress from.
- Garmin β the daily telemetry that feeds metrics, rhythm, sleep, and recovery overlays.
- Model Guidance β model routing for sub-agents (the clinical agent runs on the configured frontier model).