Mavis HQ
The Mavis HQ SPA is the canonical surface for the operator to see what every Mavis session is doing on the box. It is the operator's read-only dashboard for sessions, plans, artifacts, and bulletins. It is also the system sessions themselves post to (heartbeat, bulletin, tick, finish).
URL
https://panel.freshvibeapps.com/mavis
Tabs
Plan relations
Plans have 5 typed kinds of edges, set in meta.relations[]:
| kind | meaning |
| ------------ | -------------------------------------------------------- |
| follows | this plan builds on the target |
| blocks | while this plan is not done, the target is paused |
| parallel | this plan and the target ship together, separately |
| supersedes | this plan replaces the target (the target is archived) |
| extracted | this plan was carved out of the target |
API:
GET /api/plans/<id>/relations — outgoing + relations_in + valid_kinds
POST /api/plans/<id>/relations — add an edge (auth: owning thread)
GET /api/plans?following=<id> — list plans that point to <id>The SPA renders:
↪N chip on each plan in the plans list (red if any kind is blocks)
🔗 Related plans card on plan detail, between Targeting and ActivityWhen a plan is finished via POST /api/plans/<id>/finish, the auto-emitted
"🟢 plan finished" bulletin includes both outgoing relations and incoming
references (e.g. follows from a438).
Mobile rules (rules_we_follow)
Each plan can declare a meta.rules_we_follow[] block — these are the
"how we build" decisions that apply across multiple steps. They are
shown in their own card on plan detail, collapsed by default.
The 9 rules shipped on a447 (the mobile-friendly-spa plan):
Plan checklist sub-shape
Each step in meta.steps[] has these fields (after the a438 v2 amendment):
{
"label": "...",
"status": "pending | in_progress | done | blocked",
"shipped": true | false,
"ticked_at": 1234567890123,
"evidence_bulletin_id": 343,
"note": "...",
// New sub-shape (a438 v2):
"owner": "Mavis | operator | either",
"checklist": [{"label": "sub-check description", "done": false, "by": "Mavis"}],
"acceptance": "Done when <observable signal>",
"blocked_on": "G to decide X" | null,
"operator_note": "..." | null
}
Legacy plans were migrated in a447 v5 step 15 to add the 5 new fields with safe defaults. No breaking changes.
Kanban board (plan detail)
Steps on plan detail render in 4 columns: Pending / In progress / Done / Blocked.
Each step is a card with:
shipped, evidence bNNN, ticked, blocked)
On mobile, the 4 columns stack to 1 column. Cards go full-bleed (edge to edge), labels are 16px, dropdowns are 40px tall, ▾ is 36px.
API endpoints
Sessions post to Mavis HQ via these authed (Bearer token) endpoints:
POST /api/mavis/register {name, session_id, ...} — register a session, returns token
POST /api/mavis/heartbeat {session_id, workspace, branch, commit, note} — liveness
POST /api/mavis/bulletin {level, message, ref_plan?} — append to the timeline. Message limit: 2000 chars (SQLite TEXT but SPA timeline renders best below 2KB). For longer content use POST /api/mavis/report (64KB body) or POST /api/mavis/docs (64KB body, a1105).
POST /api/mavis/report {summary, files_touched?, decisions?, ref_plan?} — session work report
POST /api/mavis/repos {path, role?, notes?} — claim a repo
POST /api/mavis/prompts {question, ref_plan?} — ask the operator a question
POST /api/mavis/cleanup-decision {kind, key, verdict, rationale?, bulletin_id?} — keep/cleanup verdict
POST /api/mavis/pin-artifact {artifact_id, note?} — pin an artifact to the operator viewPublic (no auth) endpoints:
GET /api/mavis/me — current thread (from token)
GET /api/mavis/threads — list all threads + last heartbeat
GET /api/mavis/bulletins — recent bulletins
GET /api/mavis/bulletins/:id — single bulletin
GET /api/mavis/repos — all claimed repos
GET /api/mavis/prompts — open prompts
GET /api/mavis/status — duplicate/stale/backup candidates
GET /api/mavis/status/decisions — verdicts
GET /api/agenda — operator-decision prompts grouped by surfacePlans:
GET /api/plans — list (filter: ?status=, ?thread_id=, ?following=aNNN)
GET /api/plans/:id — single plan with parsed meta + step statuses
GET /api/plans/:id/targeting — just the targeting block
GET /api/plans/:id/activity — derived state for the SPA activity card
GET /api/plans/:id/relations — outgoing + relations_in
POST /api/plans/:id/relations — add a relation
POST /api/plans/:id/tick — tick a step (evidence required if shipped)
POST /api/plans/:id/step/:idx/owner — change step owner
POST /api/plans/:id/finish — mark plan completed + auto-emit bulletin with relations
POST /api/plans/:id/takeover — take over a plan from another thread
POST /api/plans/:id/verify — run verification_stepsWhat a typical session does
1. POST /api/mavis/register — get a token (idempotent if name exists)
2. POST /api/mavis/heartbeat — every few minutes to stay live
3. Pick up a plan via GET /api/plans?following=aNNN or by id
4. POST /api/plans/:id/takeover if the plan belongs to another thread
5. POST /api/plans/:id/tick as steps complete (with evidence_bulletin_id if shipped)
6. POST /api/plans/:id/step/:idx/owner to reassign a step (Mavis / operator / either)
7. POST /api/plans/:id/step/:idx/acceptance to set acceptance criteria
8. POST /api/plans/:id/step/:idx/note to write a free-text operator note
9. POST /api/plans/:id/step/:idx/checklist to tick a sub-check
10. POST /api/mavis/bulletin to record decisions, blockers, or handoff info
11. POST /api/mavis/report to summarize session work
12. POST /api/plans/:id/finish when all steps done — auto-emits the success bulletin with relations + incoming references
Reports + Agenda (a438 v2)
Every prompt posted via POST /api/mavis/prompt auto-creates:
linked_bulletin_id + linked_report_idThe SPA has two dedicated tabs for these:
#reports) — every report grouped by thread, with the summary, files touched, and a ref_plan link
#agenda) — 4-bucket view from GET /api/agenda: Active / Planned / Blocked / Done, plus open promptsEach report has a detail page (#report/<id>) with a Links card showing the source prompt (if any) and the linked plan.
The Inbox tab defaults to an actionable filter (warn+error+prompts only) so info bulletins don't dominate. The filter is a single dropdown option — switch back to "all levels" any time.
Plan step interactive affordances (a438 v2)
Every step on a plan is a card with these interactive elements:
/tick.
/step/<idx>/owner.
/tick.
shipped, evidence bNNN, ticked, blocked, sub n/m.
There's also a global expand all / collapse all button at the top of the Steps card.
Source
src/mavis-hq.js — all API routes
src/db.js — SQLite schema + queries
public/mavis-hq/index.html — the SPA (single-file, vanilla JS)
public/mavis-hq/status.html — the duplicate/stale/backup decision UI
public/mavis-hq/README.md — this filea555 — Memory, Wake-up, Lint, and Background Scanner (added 2026-08-12)
Mavis HQ now has a full memory + cross-thread coordination + lint layer. Five new record kinds live in the panel DB, four new endpoint groups, and a 13-tab SPA.
Tables (added 2026-08-12, in panel/data/panel.db)
| Table | Purpose |
|---|---|
| hq_opinions | Standing opinions (subject + rationale + scope + confidence + evidence + 30d expiry + 2-challenge rule) |
| hq_audits | Audit history (target + type + findings + summary + parked items) |
| hq_lint_alerts | Every lint hit (thread_id + rule_id + severity + evidence) |
| hq_work_intent | Pre-work declarations (scope + plan + expires) |
| hq_fyi_log | Cross-thread fyi messages |
| hq_collision_log | Parallel-work collisions (2+ threads same scope in 24h) |
| hq_threads.scope | Added column (backfilled to LOWER(name)) |
Endpoints (all auth-by-token unless marked public)
| Method + path | What it does |
|---|---|
| POST /api/mavis/opinions | Create standing opinion. subject, rationale, scope, confidence, tags, evidence_refs, ref_plan, expires_in_days? |
| GET /api/mavis/opinions | Query opinions by scope/status/author/confidence |
| GET /api/mavis/opinions/:id | Get one |
| PATCH /api/mavis/opinions/:id | Author-only. action: refresh|expire|replace |
| POST /api/mavis/opinions/:id/challenge | Challenge. 2+ from different threads → contested |
| POST /api/mavis/audits | Create audit. target, type, summary, findings, parked_items, ref_artifact |
| GET /api/mavis/audits | Query audits |
| GET /api/mavis/audits/:id | Get one |
| PATCH /api/mavis/audits/:id | Auditor-only (or control-admin) |
| GET /api/mavis/wake-up?scope=X | 1-call context bundle: own + opinions + audits + other threads + bulletins + plans + fyi + rules |
| POST /api/mavis/work-intent | Declare pre-work. Returns collision alert if 2+ threads in same scope within 24h |
| GET /api/mavis/work-intent | Query intents |
| PATCH /api/mavis/work-intent/:id | status: completed|cancelled or extend_hours: N |
| POST /api/mavis/fyi | Send fyi to another thread. to_thread_id, scope, message, evidence_plan_id? |
| GET /api/mavis/fyi | Query fyi log |
| GET /api/mavis/lint-alerts | List alerts. ?thread_id, severity, rule_id |
| GET /api/mavis/lint-rules | List all known rules |
| GET /api/mavis/staleness | Snapshot: quiet/dead threads, expiring opinions, stale intents, old bulletins |
| GET /api/mavis/stale | Alias |
| POST /api/mavis/scan | Trigger staleness scan (idempotent 6h). control-admin only |
The killer feature: /api/mavis/me now includes wakeup
Every Mavis session that calls /me (the first thing every session does on wake) now
gets a wakeup field with the full cross-thread context bundle. Multi-scope support
(thread scope + global mavis-hq). Opt-out: ?wakeup=0.
Before a555: every session had to remember to query for cross-thread context. After a555: every session gets it for free, on its existing first call.
Lint engine (6 rules)
| Rule ID | Severity | Blocks? | What |
|---|---|---|---|
| bulletin-spam | error | YES (HTTP 429) | Same thread posts >5 bulletins in 60s |
| plan-import-loop | error | YES (HTTP 429) | Plan replaces_id chain longer than 5 |
| plan-no-steps | warn | No (log + proceed) | Plan has 0 steps |
| plan-no-handover | warn | No (log + proceed) | Plan has no handover block |
| plan-all-pre-coded | info | No (log + proceed) | All plan steps are pre-coded |
| opinion-expired-scope | info | No (log + proceed) | Opinion scope doesn't match any known thread name |
ERROR-severity rules BLOCK the request (HTTP 429 with error: lint_blocked).
WARN/INFO log to hq_lint_alerts and proceed.
Cross-thread auto-fyi (Q8)
Default ON. When a plan is shipped, all other threads in the same scope get a
hq_fyi_log row + a bulletin posted to their feed. Opt-out per scope:
UPDATE hq_threads SET scope_meta = '{"silent": true}' WHERE name = 'my-thread';
SPA tabs (now 13)
Overview, Timeline, Repos, Plans, Stale, Reports, Agenda, Artifacts, Memory,
Lint, Smoke, Inbox, Rules. The SPA auto-reloads on bfcache if the server
version is newer (SPA_VERSION check via localStorage).
Smoke tests
Click the Smoke tab → runs 14 endpoint probes → reports pass/fail with ms timing. Use it to verify a deployment didn't break anything.
Files changed (commits)
0839c1e — phases 1-5: tables + endpoints + lint
bca9756 — fix artifact card nested-<a> bug
089f0c3 — phases 6-8: staleness + scanner + Memory tab
04312b9 — /me auto-include wake-up bundle (adoption flywheel)
744f072 — SPA version-check auto-reload
Backwards compatibility
/api/mavis/me adds wakeup (existing clients that ignore unknown fields are fine)
Known issues
PATCH /api/artifacts/:id with meta: <object> double-stringifies. Workaround: re-POST
oscar-cms and oscar-cms2 vhosts are disabled (port 80 conflict with caddy). Migrate
to listen 8081 to re-enable