PPactDocs
Productivity

Feedback

Bottom-up internal feedback threads that route to a direct manager and enforce manager-chain privacy — with an AI summary banner for handlers.

Feedback

Feedback is Pact's bottom-up internal comms channel. An employee opens a thread, it routes to their direct manager, and a strict privacy invariant controls exactly who can ever see it. It's the mirror image of Broadcasts, which flow top-down.

Live

Backed by api/routes/feedback.py and core.comms.feedback, with tables from alembic migration 0073_comms_broadcasts_feedback. Every route is tenant-scoped via get_tenant_id.

Lifecycle

  1. 1

    Create

    POST /v1/feedback opens a thread. It routes to the sender's direct manager (resolved from tenant_users.manager_id).
  2. 2

    Discuss

    POST /v1/feedback/{id}/messages appends a message; GET /v1/feedback/{id} returns the privacy-checked thread detail.
  3. 3

    Escalate

    POST /v1/feedback/{id}/escalate re-routes the thread one step up the manager chain when the current handler can't resolve it.
  4. 4

    Resolve or share

    POST /v1/feedback/{id}/resolve closes the thread; POST /v1/feedback/{id}/share issues a peer-share badge (handler-only).

Two inbox views back the UI: GET /v1/feedback/inbox is the handler side (the threads routed to you, with an AI summary banner), and GET /v1/feedback/mine lists the threads you sent.

The privacy invariant

This is the load-bearing part of the feature. Visibility is computed in exactly one place — core.comms.feedback.visible_to_user — and the routes never recompute it independently. The rules:

  • The sender and the current handler can always see the thread.
  • Every node on the manager chain between the original handler and the current handler can see it (the escalation path).
  • Down-tree subordinates of a handler cannot see it — a director's report cannot read a thread the director is handling.
  • Up-tree managers above the current handler cannot see it unless the thread was explicitly escalated to them.

The manager chain is walked leaf-first and defensively capped at 64 hops. Because visibility lives in a single function, escalation and peer-share can change who sees a thread without any route re-deriving the rule.

AI summary banner

The handler inbox carries an AI-generated summary via core.comms.summarizer.refresh_handler_summary, so a manager triaging a queue of threads gets the gist before opening each one.

Attribution

Write routes resolve the caller's user id from the bearer token (tenant_users.api_key_hash) so every message and state change is attributed to a real user, and role checks use get_caller_role.