# Phase 8 — Hub API: `/capabilities`, `/ask` (read-only), `/notify` Status: **built, deployed and proven against the live service** (2026-10-15). The live-Hub registration is the one remaining step; see *The Hub wiring* below. Spec: `AGENTS.md` §4 FR-11, §8 (integrations), §10 rule 5 (`/ask` is read-only), §11 Phase 8. Contract: `central_ai_llm_master/docs/ask-contract.md` (frozen). ## What the phase does The service exposes the project side of the frozen Hub contract on its own port (8792). The Hub reads `GET /capabilities` to route a question, calls `POST /ask` to get one short answer, and may call `POST /notify` to have a person told something. All three need the shared bearer token. | Method | Path | Body / reply | Write? | |---|---|---|---| | `GET` | `/capabilities` | `{"project":"compass_nhs","topics":[…],"people":["harry","finn"]}` | no | | `POST` | `/ask` | request `{"question","asker","person","source"}`; reply `{"answer","data","expires"}` or `{"answer":null,"reason"}` | **no** | | `POST` | `/notify` | request `{"from","level","message","person"}`; reply `{"ok":true}` | Apprise only | `GET /health` now also carries `"ok": true` and `"hub_api": true`, so the Hub's existing health shape and this service's richer shape both work. ## Configuration | Variable | Meaning | |---|---| | `SCHOOL_HUB_TOKEN` | Shared bearer token the Hub sends. Empty turns the three endpoints off with `503`; the dashboard is unaffected. | The token is a secret. It lives only in `environment.d/school-service.env` (gitignored). The Hub holds the same value as `HUB_PROJECT_COMPASS_NHS_TOKEN` in `~/.config/environment.d/10-secrets.conf` and reaches this service through `HUB_PROJECTS=compass_nhs=http://192.168.20.13:8792`. ## `/ask` — how the answer is built The answers are deterministic. No model is on the path, so the Hub's 3 second cap is never the reason an answer is missing. `/ask` reads only: - `children` (find the person by name, case-insensitively), - `items` (the child's stored Compass items), - `scans` (the child's scan health). It reuses `dashboard.Classify`, the same code that builds the dashboard and the morning briefs, so the answer and the page never disagree. The question picks a focus by plain keywords: | The question mentions | The answer covers | |---|---| | "overdue", "not done", "behind" | overdue tasks | | "this week" | overdue, then due this week | | "notice", "message", "news" | school notices | | "today", "timetable", "schedule", "class", "at school" | today's lessons, tasks due today, overdue | | "due", "homework", "assessment", "exam" or anything else | the whole picture | The reply is **one short sentence**, for example: ``` Harry has 8 school events today from 8:55am to 6:00pm and 6 overdue tasks. ``` The structured detail (per bucket, with subject, teacher and due time) is in `data`; the Hub may ignore it. `expires` is the end of the family day. ### Fail loudly (FR-12, AGENTS.md §10 rule 2) A failed or missing scan is **never** an empty day. `/ask` answers: - no scan yet — `I have no school data for Harry yet: the Compass scan has not run, so due work is unknown.` - last scan failed — `I cannot say what Harry has due: the last Compass scan on