Skip to main content

WiseAI Realtor — Aria Feedback Loop (capture + triage-queue foundation) Expected Output Acceptance Spec

📍 CANONICAL LOCATION NOTE (read first)

The canonical home for portfolio acceptance specs is the separate C:\dev\knowledge git repo at knowledge/acceptance/realtor-feedback-loop-mvp.md. This agent built the feature inside the isolated churchwiseai-web worktree and could not write to the sibling knowledge/ repo from here, so the spec is committed alongside the code in docs/acceptance/ for reviewability. REVIEWER ACTION: copy this file to knowledge/acceptance/realtor-feedback-loop-mvp.md, add it to knowledge/acceptance/README.md's index if needed, and run pnpm derive from C:\dev\knowledge. (Same pattern the founder uses for cross-repo features per project_cdev_three_git_repos.)

WiseAI Realtor — Aria Feedback Loop (capture + triage-queue) Expected Output Spec

⛔ STATUS: DRAFT — NOT APPROVED. CLAUDE.md Rule #17 (HARD GATE) is NOT satisfied.

Stage-1 agent research. The MVP code (capture + queue) is authored against this spec for the QA reviewer, but the migration is FOUNDER-GATED (do not apply) and the triage half is DESIGN + STUB ONLY. Do not schedule any cron or build the triage agent until the founder approves this spec and confirms the open items in §11.

Sourced from (read-only, 2026-07-02): the founder's vision — "the realtor types an issue to Aria — a bug, or 'I don't understand X' — that gets captured and routed so Claude agents can later triage it on a schedule and PROPOSE a fix the founder approves before anything ships"; do-not-reinvent.md (Aria today is READ-ONLY, setup-only, zero write tools — onboarding/assistant/*); auth-rbac.md (the realtor RBAC resolver + settings:profile:read floor held by every active role and the legacy synthetic owner); ai-config/route.ts (the canonical per-tenant realtor API pattern — RBAC guard, account_id from query/header never body, honest degrade when a migration is unapplied); the CRITICAL memory rules (per-account writes UPSERT not .update().eq; authed client fetches send ?account_id= in the query because middleware strips the header; verify DB columns before use).


0. Scope — what this MVP covers, and what it does NOT

Covers (this PR):

  1. A durable, tenant-scoped capture queue table realtor_feedback (migration, founder-gated — reviewer/founder applies it).
  2. A capture APIPOST /api/realtor/feedback (append a feedback row) and GET /api/realtor/feedback (list this account's rows, paginated) — owner/agent authed, tenant-scoped, account_id in the query.
  3. A capture UI — a small, honest "Report an issue or ask for help" affordance in the realtor dashboard (surfaced from the Aria panel) that posts the message
    • current screen route.
  4. A triage-loop DESIGN (this spec §6) + a read-only cron stub proving the auth + query shape for the future founder-approved triage agent.

Does NOT cover (explicitly out of scope / deferred to a founder-approved phase):

  • Any auto-classification, auto-diagnosis, or auto-fix by an LLM.
  • Any auto-merge, auto-deploy, or auto-send (email/SMS/push) of anything.
  • Wiring the triage cron into vercel.json (a schedule is a founder action).
  • Giving the Aria LLM any write tool — Aria stays read-only; capture is a deterministic form, not an LLM tool call.
  • A full founder review console (Phase 2 — for the MVP the founder reads the realtor_feedback table directly; a dashboard read-view is optional, §6.3).
  • Editing/deleting feedback rows from the client; a realtor can only append and list their own.

Build posture: mirror ai-config/route.ts exactly (RBAC guard, account_id resolution, honest degrade on unapplied migration). This is a capture-and-queue foundation, not a fix-it engine. The "self-annealing" name describes the intended END STATE (agents propose, founder approves, fixes ship) — this MVP builds only the first link (capture) and specs the rest.


1. The vision this serves (why capture, why a queue)

The realtor is the customer. When something breaks or confuses them, that signal today is lost — Aria can only point at existing controls; there is no path for "this is wrong" to reach the team. This MVP gives that signal a durable home so a future scheduled Claude agent can read it, propose a diagnosis/fix into ai_triage, and surface it for the founder to approve. Nothing about the loop ever acts on the customer's account or ships code without the founder pressing go — consistent with the AI-Bridge principle (we facilitate a human, we don't silently act) and every CLAUDE.md guardrail (founder presses sends/merges/deploys).


2. Data model — realtor_feedback (the queue)

One append-only row per submission. Columns (migration churchwiseai-web/migrations/20260702_realtor_feedback.sql, founder-gated):

ColumnTypeNotes
iduuid PK (gen_random_uuid())
business_iduuid NOT NULL → local_businesses(id)tenant anchor (== account)
account_iduuid NOT NULLmirrors business_id for query symmetry with the RBAC resolver; both set to the resolved account
submitted_by_user_iduuid NULLSupabase Auth uid of the submitter, or NULL for a legacy synthetic-owner session
screen_routetext NULLthe /realtor/app/* route the realtor was on (context for triage)
messagetext NOT NULLthe realtor's words (bounded length; see §3)
categorytext NOT NULL DEFAULT 'unclassified'CHECK ∈ {bug, confusion, feature_request, unclassified} — the realtor may self-tag; the future agent may re-classify
statustext NOT NULL DEFAULT 'new'CHECK ∈ {new, triaged, approved, dismissed, resolved}
ai_triagejsonb NULLRESERVED for the future agent's proposed diagnosis/fix — never written by this MVP's capture path
founder_notetext NULLRESERVED for the founder's decision note
created_at / updated_attimestamptz NOT NULL DEFAULT now()updated_at maintained by the shared local_business_set_updated_at() trigger

Indexes: (business_id), (status), (business_id, created_at DESC) for the tenant list.

RLS + grants: ENABLE ROW LEVEL SECURITY deny-by-default; GRANT … TO service_role only (no anon/authenticated grant, no permissive policy) — identical to the phase-6 tables. The app mediates all access through the service-role client

  • the RBAC resolver (app-layer authz is the pattern; get_advisors stays clean).

Status lifecycle (who moves it):

new ──(future triage agent, writes ai_triage)──▶ triaged
triaged ──(FOUNDER, approves proposal)──▶ approved ──▶ resolved
triaged ──(FOUNDER, rejects)──▶ dismissed

This MVP only ever creates rows in new. Every later transition is a founder-gated action; the cron stub in this PR does NOT transition rows (§6.2).


3. Capture API — POST / GET /api/realtor/feedback

Auth: requireRealtorCapability(request, 'settings:profile:read') — the same floor Aria's endpoint uses; held by every active role template AND the legacy synthetic owner, so any legitimate signed-in realtor can report an issue, and a suspended/no-membership caller gets 401 (fail-closed).

Tenancy: account_id resolved via resolveRealtorContext(request) (header → query). A business_id/account_id in the POST BODY is IGNORED — a caller only ever writes their own account (mirrors ai-config/route.ts).

POST — appends one row:

  • Body: { message: string, category?: 'bug'|'confusion'|'feature_request', screen_route?: string }.
  • Input is sanitized by the pure sanitizeFeedbackInput() (client-safe, unit-tested): message trimmed, required, ≤ 4000 chars; category coerced to the enum (unknown → unclassified); screen_route accepted only if it is a /realtor/app… path, else dropped to null.
  • Writes { business_id: accountId, account_id: accountId, submitted_by_user_id: ctx.user?.id ?? null, screen_route, message, category, status: 'new' } via a plain .insert() (append-only — NO upsert needed; each report is a new row).
  • Error-checked: a DB error → 500 with the message; a missing-table error (42P01, migration unapplied) → 503 with an honest "not enabled yet" message, never a fake success. On success returns { id } (the created row id) + 201.

GET — lists this account's feedback (the read side for a future review surface):

  • Tenant-scoped .eq('business_id', accountId), newest first, paginated with .range() (default page 50, cap 100) — never an unbounded select.
  • Returns { items: [...], nextOffset }. Missing table → { items: [], available: false } (honest degrade).

Never: accept a cross-tenant id from the body; send any email/SMS/push; call an LLM; write to any table other than realtor_feedback.


4. Capture UI — "Report an issue or ask for help"

A small, on-brand (navy #1B365D / teal #0F766E) affordance reachable from the Aria panel footer ("Something not working, or unclear? Report it"), opening a lightweight modal:

  • Fields: a short category chooser (Bug · I don't understand this · Feature idea — maps to bug/confusion/feature_request) and a message textarea. The current screen route is captured automatically (from usePathname()), shown read-only so the realtor knows what context is attached.
  • Honest expectation-setting copy (load-bearing, AI-Bridge): "This goes to the WiseAI team. They'll review it and follow up. Nothing on your account is changed automatically." No promised timeline, no fake "an agent is fixing this now" language.
  • States: idle → submitting → success ("Thanks — we've got it. The WiseAI team will review and follow up.") / error (honest retry). On the migration-pending 503, a clear "Issue reporting isn't switched on yet" message, never a false "saved".
  • Submits via feedback-client.tsPOST /api/realtor/feedback with ?account_id= in the query (middleware strips the header — CRITICAL memory rule).
  • AODA: labelled modal dialog, Esc closes, focus moves to the textarea on open, status announced via aria-live, buttons labelled (not icon-only).

Aria's LLM gains NO new tool — the panel simply surfaces a deterministic form. Aria stays a read-only concierge; the capture path is plain HTTP.


5. Role-based visibility

BucketRolesCan report?
Any active memberevery role template (agentbrokerage_owner) + legacy synthetic ownerYES — settings:profile:read is universal; anyone who can use the dashboard can report an issue
Suspended / invited / no membershipNO — 401 fail-closed

Feedback is intentionally NOT gated to owners: the person who hits a bug is whoever should be able to report it. A future review console (§6.3) MAY gate the READ side more tightly (e.g. owners only) — deferred, and called out as an open item (§11).


6. Triage loop — DESIGN + STUB ONLY (no auto-fix)

6.1 The intended (founder-approved, future) loop

  1. A scheduled cloud agent (repo /schedule routine or a vercel.json cron) runs on a cadence and reads realtor_feedback rows where status = 'new'.
  2. For each row it classifies (bug/confusion/feature_request), and — for bugs — proposes a diagnosis and a candidate fix (files, approach), writing that PROPOSAL into ai_triage (jsonb) and moving the row to triaged.
  3. It surfaces the proposals for the founder (a digest, or a review view).
  4. The founder approves or dismisses each proposal. Only on approval does any code change get authored — through the normal branch → PR → founder-merge flow. The agent NEVER edits the customer's account, NEVER merges, NEVER deploys, and NEVER auto-applies a fix. founder_note records the decision.

This is "self-annealing" in the CLAUDE.md sense: a bounded, founder-gated propose-and-approve loop — explicitly NOT a fix-it-all machine.

6.2 What ships in THIS PR (the stub)

GET /api/cron/realtor-feedback-triageCRON_SECRET-gated (Bearer or Vercel's x-vercel-cron), runtime = nodejs. In this MVP it is read-only: it counts status = 'new' rows and logs a summary, then returns { new: n, note: 'stub — triage agent not built; no LLM, no writes, no state change' }. It does NOT transition rows, does NOT call an LLM, does NOT write ai_triage. It is NOT wired into vercel.json (scheduling is a founder action). Its only job is to prove the auth + query shape the real triage build will extend.

Design decision (recorded for the reviewer): the task allowed the stub to mark rows triaged. We deliberately kept it read-only instead — a row in triaged implies a human/agent actually looked at it, so an unattended cron flipping status with no real triage content would be dishonest state and could mask un-reviewed feedback. Read-only is the honest, lowest-risk stub and still proves the wiring. The real transition to triaged happens only when the founder-approved agent writes an actual ai_triage proposal.

6.3 Founder review surface (Phase 2 — optional for MVP)

For the MVP the founder reads realtor_feedback directly (Supabase). A read-only in-dashboard list (owner-gated) that renders GET /api/realtor/feedback is a nice-to-have and may ship later; it is NOT required for this spec to pass. The GET endpoint is built now so that surface has a data source.


7. Guardrails (the honesty + safety contract the reviewer checks)

  • No auto-fix / auto-classify / auto-merge / auto-deploy / auto-send anywhere in this PR. Grep proof: the only writer of realtor_feedback is the capture POST (status new); nothing writes ai_triage, status != new, or founder_note; the cron stub performs no writes.
  • Founder-approval gate on every downstream transition — documented here, enforced by there being NO code path that transitions a row in this PR.
  • Tenant isolation — every query filters business_id = accountId; the POST ignores any body-supplied id; RLS deny-by-default + service-role-only grants.
  • Honest degrade — an unapplied migration yields a clear 503 / available: false, never a fabricated success (mirrors ai-config/isMissingTableError).
  • account_id in the query on every authed client fetch (middleware strips the header — CRITICAL memory rule).
  • Append-only capture.insert() not upsert; the upsert-vs-update footgun (silent 0-row no-op) does not apply because we never update an existing row on the capture path.
  • Aria stays read-only — no LLM write tool is added; capture is a plain form.
  • Column verification — the route only touches columns defined in 20260702_realtor_feedback.sql; the reviewer must confirm the migration applied cleanly (information_schema.columns) before the endpoint returns anything but a 503. (This agent cannot run SQL — the migration is the source of truth and the reviewer applies + verifies it.)

8. Empty / loading / error states

StateExpected output
Table not provisioned (migration unapplied)POST → 503 honest "not switched on yet"; GET → { items: [], available: false }; modal shows the honest not-enabled message, never a false "saved".
Empty message400 "message required"; the modal disables submit until there's text.
Message too long (> 4000)client trims/limits; server truncates via sanitizeFeedbackInput (never 500).
Submittingbutton shows a busy state; double-submit prevented.
Successthe modal shows the honest thank-you + the returned id; the composer clears.
DB error500 with the message surfaced; modal shows an honest retry.
Loading the list (GET)(future review view) skeletons, not a spinner.

9. Accessibility (AODA → WCAG 2.1 AA)

  • Modal is a labelled role="dialog" aria-modal="true"; Esc closes; focus moves to the textarea on open and returns to the trigger on close.
  • Category chooser is a real fieldset/radio group or labelled buttons; status changes announced via aria-live.
  • Contrast ≥ 4.5:1; reduced-motion honored; no icon-only actionable controls.

10. Acceptance checklist (QA runs on the deployed URL)

Behavioural verification on wiseaiagency.com (real host — the realtor app runs behind the hostname rewrite) against the demo tenant (Terry & Sheri Real Estate, …0c01); never "build passes". Sample at ≥2 timepoints. All test.fixme until the migration is applied + founder approves.

test.fixme('an active member can open the Report-an-issue modal from the Aria panel and submit; a row lands in realtor_feedback with status=new, the correct business_id/account_id, the captured screen_route, and submitted_by_user_id', () => {});
test.fixme('POST ignores a body-supplied business_id/account_id — the row is written for the RESOLVED account only (cross-tenant write is impossible)', () => {});
test.fixme('a suspended / no-membership caller gets 401 from both POST and GET (fail-closed)', () => {});
test.fixme('GET returns only THIS account\'s rows, newest first, paginated via .range() (never unbounded)', () => {});
test.fixme('with the migration UNAPPLIED, POST returns an honest 503 and the modal shows the not-enabled message — never a false "saved"', () => {});
test.fixme('the modal copy sets honest expectations ("goes to the WiseAI team… nothing changed automatically") — no promised timeline, no "an agent is fixing this now"', () => {});
test.fixme('the cron stub is CRON_SECRET-gated, performs NO writes and NO state transition, and is NOT scheduled in vercel.json', () => {});
test.fixme('grep proof: no code path in this PR writes ai_triage, sets status!=new, writes founder_note, sends email/SMS, or calls an LLM', () => {});
test.fixme('Aria\'s LLM has no new write tool — the assistant endpoint\'s tool set is unchanged', () => {});

11. Guardrails for agents building against this spec + open items

  • Rule #17 not satisfied — do not treat as approved; the migration is founder-gated (DO NOT apply as an agent).
  • Reuse, do not reinvent: the RBAC guard (rbac-server.ts), the account_id-in-query pattern (aria-client.ts), the honest-degrade helper (isMissingTableError), the shared updated_at trigger, and the Aria panel/store pattern. Do not add a new auth path.
  • Do NOT add any LLM step, cron schedule, review-console write path, or status-transition endpoint in this PR.
  • If code diverges from this spec, update the spec first (founder approval), then the code.

Open items for Stage-2 founder interview:

  1. Should the GET/review side be gated tighter than the POST (e.g. owners-only read), or is any member seeing their account's feedback fine?
  2. Cadence + delivery for the future triage digest (in-dashboard? email to the founder? a /schedule routine?).
  3. Is a Phase-2 in-dashboard review console wanted, or does the founder prefer to read the table directly indefinitely?
  4. Should the realtor get any acknowledgement follow-up (email) when the team acts on their report — and if so, that is a founder-gated send, not automatic.

End of spec. STATUS: DRAFT — NOT APPROVED. This MVP builds capture + queue only; the triage/self-annealing half is design + a read-only stub, gated on founder approval at every downstream step.