← Back
← Back
# Plan: Mavis HQ memory + lint (one build)
**Session**: 418281651208440 (operator-panel-mavis, id 7)
**Date**: 2026-08-12 10:13 BST
**For**: thread 7 (operator-panel-mavis)
**Source brief**: a551 (36-judgment-memory-brief from fvre-mavis) + G's directive 2026-08-12 10:13 ("I don't really mind how you do it just do it")
**Parent**: a478 (FvwMavisHQ MegaPlan v3)
## Goal
Ship one build that:
1. Adds the 2 record kinds (standing_opinion + audit_history) so Mavis sessions have queryable memory of judgment + audit work
2. Adds a checker agent / server-side lint so the panel can detect and (for the worst cases) block the bad patterns G sees over and over (bulletin-spam, plan-import-loop, scope-drift, missing-targeting, self-answerable, repeat-question, heartbeat-no-progress)
The memory system is the data layer. The lint is the enforcement layer. They compose. Ship them together so the lint can flag "you posted an opinion without checking existing opinions first" the moment the memory table goes live.
## My recommendations (filled in for G to override)
Q1. Is the gap real? **YES, build it.**
Q2. 2 record kinds right? **YES, both. Add `replaces` to standing_opinion and `supersedes` to audit_history.**
Q3. Confidence: **high / medium / low. Sticky to evidence, not author.**
Q4. Default expiry: **30d opinions, 90d audits. Operator can override per-record.**
Q5. Challenge threshold: **2 challenges auto-flip to `contested`. Scoped to one scope, not global.**
Q6. Team-opinion view: **per-thread only on v1. v2 adds "recent across all threads" digest in Inbox. Skippable if G never reads it.**
Q7. Migration: **NO manual backfill. One-shot GLM-powered import script in v2.**
Q8 (new). Cross-thread auto-fyi on shipped step in another thread's scope: **ON by default, opt-out per scope.**
Q9 (new). Pre-work intent declaration: **YES, ship in v1.**
## Lint recommendations (G's enforcement ask)
The lint lives inside the panel as a checker agent. 3 jobs:
**Job 1 — bulletin-content linter** (run on POST /api/mavis/bulletin):
- Asks permission for things with a public API → WARN
- Repeats a question the thread asked < 24h ago → INFO
- Asks the operator a question that has a default → WARN
- Posts a status update that should have been a heartbeat → INFO
**Job 2 — work-pattern detector** (background scan every 5 min):
- Heartbeat > 15 min with no shipped step → 🟥 bulletin on the thread
- > 3 bulletins in 5 min with no progress → ERROR (block on the 4th)
- Importing the same plan 2+ times with same content → ERROR
- Asking the operator a question answerable from /api/mavis/me → INFO
**Job 3 — drift-on-wake detector** (run on first bulletin of a session):
- Bulletin scope doesn't match thread name → WARN
- Re-ask a question already on the thread < 24h ago → INFO
- Plan uploaded without `meta.targeting` → WARN
Enforcement: ERROR severity returns 400. WARN logs and proceeds. INFO is just a hint in the response.
## Build steps
### Step 1: schema + migration
- Add `hq_opinions` table (id, subject, rationale, scope, tags, confidence, evidence_refs JSON, author JSON, created_at, expires_at, status, challenge JSON, replaces_id, version_seq)
- Add `hq_audits` table (id, target JSON, type, findings JSON, summary, parked_items JSON, auditor JSON, created_at, ref_artifact, status)
- Add `hq_lint_alerts` table (id, thread_id, kind, severity, payload, bulletin_id, created_at)
- Indexes on: scope, tag, author.thread_id, status (opinions + audits); thread_id, created_at (lint)
- Migration: `panel/src/db.js runHqMigrations` extends to v? (next available)
### Step 2: opinion endpoints
- `POST /api/mavis/opinions` (owning thread) — create
- `GET /api/mavis/opinions` (any thread) — query by scope/tag/status/include
- `POST /api/mavis/opinions/:id/challenge` (any thread) — add challenge
- `POST /api/mavis/opinions/:id/refresh` (owning thread) — reset expires_at
- `POST /api/mavis/opinions/:id/supersede` (owning thread) — mark replaced
- `POST /api/mavis/opinions/:id/reaffirm` (owning thread) — re-affirm with new evidence
### Step 3: audit endpoints
- `POST /api/mavis/audits` (owning thread) — create
- `GET /api/mavis/audits` (any thread) — query by target/type/status
- `GET /api/mavis/audits/:id` — single audit
- `PATCH /api/mavis/audits/:id` (owning thread) — update finding status
### Step 4: wake-up bundle
- Extend `GET /api/mavis/me?scope=X&include=opinions,audits,recent_bulletins,recent_plans`
- Returns: thread info + opinions in scope (sorted by confidence) + audits in scope (open first) + recent bulletins from other threads in same scope (last 7d) + recent plans from other threads in same scope
- This is the "pick up all the context" call the operator asked for
### Step 5: cross-thread auto-fyi
- `POST /api/plans/:id/tick` (existing) — when a step ticks shipped, if plan.meta.targeting.scope is owned by another thread, auto-post `↪ fyi @other-thread: I shipped X, evidence aNNN, see bNNN` bulletin
- Scope ownership: derived from `hq_threads.scope` field (new, default = thread name)
- Opt-out: scopes can be marked `silent: true` in hq_threads.scope_meta
### Step 6: pre-work intent
- `POST /api/mavis/work-intent` — posts `🔵 starting work: aNNN — <title>, scope=X, by=this-thread` bulletin
- System checks for other threads' work-intent on same scope in last 24h; if 2+, posts a `⚠ parallel collision` alert on both threads
- `GET /api/mavis/work-intent?scope=X` — recent intents on a scope (so a waking thread can see "is anyone else working on this?")
### Step 7: lint module
- `panel/src/lint.js` (NEW, ~600 lines)
- 3 sub-modules: `lintBulletin`, `lintPlanUpload`, `lintHeartbeatStream` (background)
- 6 lint rules (see Job 1/2/3 above)
- Returns `{ ok, severity: info|warn|error, warnings[], hint? }`
- `panel/src/mavis-hq.js` (modify): wrap POST /bulletin, POST /heartbeat, POST /artifacts (type:plan) with the lint
- Error severity returns 400. Warn logs and proceeds.
- `GET /api/mavis/lint?thread_id=N` — read lint history
### Step 8: lint background scanner
- `panel/src/lint-triggers.js` (NEW, ~200 lines)
- Runs every 5 min via a setInterval in server.js (or a cron if I add one)
- Scans all threads for: heartbeat-no-progress, bulletin-spam, plan-import-loop, self-answerable
- Posts 1 `🟥 lint_alert` bulletin per violation, with the lint name + evidence + recommendation
- Cooldown: don't re-post the same lint on the same thread for 30 min
### Step 9: staleness + auto-challenge
- `panel/src/staleness.js` (NEW, ~100 lines)
- Runs daily (or on every GET that includes expired opinions)
- Marks opinions as `expired` if `now > expires_at`
- Marks opinions as `contested` if 2+ challenges from different threads
### Step 10: SPA tab
- New `🧠 Opinions` tab between Plans and Artifacts
- 3 views: per-scope (default), by-author, recently-challenged
- Cards color-coded by confidence (green=high, amber=medium, red=low)
- Stale badges: 🟢 fresh (refresh < 14d) / 🟡 stale (14-30d) / ⚪ expired (hidden by default, queryable)
- Click → detail view with rationale + evidence refs + challenges
- "last refreshed 3 days ago" timestamp
### Step 11: SPA lint UI
- Lint badge on Overview tab: small red dot on threads with active lint warnings
- Lint card on thread detail: "🟥 3 bulletin-spam warnings in last 1h"
- Inbox card for self-alerts: when lint posts a bulletin, it shows in the same thread's inbox
- Optional: lint filter on Inbox (`?lint=bulletin-spam` to see only lint alerts)
### Step 12: work-intent SPA card
- On plan detail: a "🔵 active intents" card showing which threads are currently working on this scope
- On the SPA's "Threads" tab: a badge per thread if it has an active work-intent
### Step 13: tests + verification
- Unit test each endpoint (10 tests for opinions + 5 for audits + 6 for lint)
- E2E test: helper-mavis creates an opinion, fvre-mavis reads it via wake-up bundle, fvre-mavis challenges it, status flips to contested after 2nd challenge from another thread
- E2E test: thread posts 3 bulletins in 1 min, 4th returns 400 lint_blocked
- E2E test: scope-drift detection — fvre-mavis posts about freshcloud-mail, lint returns warn, post succeeds
- E2E test: missing-targeting — plan uploaded without targeting, lint returns warn, post succeeds
- Playwright iPhone 13 check on new tab
### Step 14: docs + bulletin
- Update `panel/public/mavis-hq/README.md` with the 2 new record kinds, the wake-up bundle, the lint, the work-intent
- Post bullet on HQ explaining the system
- Cross-post on thread 36 (fvre) for visibility
## Verification
After build, the following must all pass:
- `curl POST /api/mavis/opinions` with valid payload returns 200 with opinion id
- `curl GET /api/mavis/me?scope=fvre&include=opinions` returns opinions array
- `curl POST /api/mavis/bulletins` (4th rapid) returns 400 lint_blocked
- `curl POST /api/mavis/work-intent` posts a bulletin visible in the thread
- SPA at `/mavis/opinions` renders the 3 views
- Playwright iPhone 13 screenshot of the new tab
## Off-track triggers
STOP and post a 🟥 if:
- Mavis quota burn > 50% before the lint + memory module shipped
- Schema migration breaks existing data (BACKUP panel.db before the migration)
- The lint blocks a legitimate bulletin from a busy thread (the blockable lints should be only the 2 worst: bulletin-spam, plan-import-loop)
- The wake-up bundle takes > 2s to return (it's a single call, should be < 200ms)
- The cross-thread auto-fyi fans out to > 10 threads from one tick (means scope is too broad)
## Estimated build cost
- Backend (steps 1-9): 1.5 days
- SPA (steps 10-12): 0.5 day
- Tests + verification (step 13): 0.5 day
- Docs + bulletin (step 14): 0.25 day
- **Total: ~2.75 days, ~2.2k lines**
## Build order (to surface issues early)
1. Schema + migration FIRST (do a backup of panel.db, test on a snapshot)
2. Opinion + audit endpoints (no SPA yet, curl-testable)
3. Wake-up bundle (curl-testable)
4. Lint module (curl-testable, observe what fires)
5. Background scanner (run for 24h before adding SPA)
6. Cross-thread auto-fyi (test with a fake scope)
7. Pre-work intent (curl + bulletin)
8. Staleness + auto-challenge (background)
9. SPA tab (now that the data layer is solid)
10. SPA lint UI
11. SPA work-intent card
12. Tests + verification
13. Docs + bulletin
## Open questions for G (before I start)
None — G said "just do it". The 9 question recommendations are filled in. If G disagrees on any, ping me before step 1 starts.
## Handoff
If I run out of session:
- The plan is at `/workspace/.plan/418281651208440-judgment-memory-and-lint.md`
- State at last check: step 0 (about to start step 1)
- Next step: schema + migration on panel.db with a backup
- Blocking: nothing