# Auto-Participants — Automatically Include People

Built 2026-09-30 from the client task "Auto-Participants / Automatically Include People in Trips" and Nash's same-day clarifications (`docs/TASKS-2026-09-30-AUTO-PARTICIPANTS.md`, Sections 1 and 16). Nash: "I define the people who normally work with me once, and HeavyHaul Agent automatically brings them into the right Trips."

## Words to use

- **Auto-Participants**, user-facing **Automatically Include People**. Never "dependent".
- **Personal Auto-Participant** — configured by one person for themselves: "whenever I join or create a Trip, include Maria."
- **Company Auto-Participant** — configured by a Company Admin: "whenever someone from CP Star takes part in a Trip, include the Safety Manager."
- Auto-Participant is **how someone got onto a trip, never a role**. There is no `role_type = auto_participant`; the person keeps their own role.

## The model

| Table | Purpose |
|---|---|
| `auto_participant_rules` (0049) | One rule: `rule_type` personal/company, owner (`owner_user_id` or `owner_company_id`), the person (`participant_user_id`, email, names for people without an account), `participant_role_type` (their own role; null until chosen), `scope_type`, `status`, acceptance fields, `accept_token`, `effective_from`, `conditions` (reserved). Live rules are unique per owner + email + scope. |
| `trip_participants` + context columns (0049) | `context_company_id`, `context_role_type`: which company and role the person acts with on THIS trip. |
| `trip_participants` + provenance (0049) | `addition_method`, `triggering_user_id`, `triggering_company_id`, `auto_participant_rule_id`, `rules_evaluated_at`. Null on rows from before the feature. |
| `trip_participant_sources` (0049) | Every rule that supports one participant's presence (one row can qualify through several rules). |
| `auto_participant_rule_events` (0049) | Audit: requested, created, accepted, declined, disabled, reactivated, removed, scope changed, suspended, stopped by participant, participant added / already present / skipped / removed from trip. Mirrored into `operation_events` (`area: system`). |
| `membership_roles.role_type` (0050) | Four carrier office roles with Carrier Dispatcher powers: `carrier_safety_manager`, `carrier_permit_manager`, `carrier_accounting`, `carrier_fleet_manager`. |

Statuses: `pending → active | declined | revoked`; `active → disabled | revoked | suspended`; `disabled → active | revoked`; `suspended → active | revoked`. Scopes: `all_personal_trips` (personal), `all_company_trips`, `carrier_trips`, `freight_broker_trips`, `pilot_company_trips` (company).

## Where things live

| Piece | Path |
|---|---|
| Pure rules: labels, transitions, role mapping, scope matching, the add/skip decision, the chooser | `src/lib/domain/auto-participants.ts` |
| Who may remove a participant from a trip | `src/lib/domain/participants.ts` `canRemoveParticipant` |
| Role types, carrier office roles | `src/lib/domain/modes.ts` (`CARRIER_DISPATCH_ROLES`, `isCarrierDispatchRole`) |
| Central resolver, per-trip context resolution | `src/lib/data/auto-participants.ts` |
| Rule management (create, accept, decline, stop, ops, suspension) | `src/lib/data/auto-participant-rules.ts` |
| Audit trail | `src/lib/data/auto-participant-events.ts` |
| Removal from a trip | `src/lib/data/participant-removal.ts` |
| Company Admin guard (active mode + membership re-read) | `src/lib/api-guard.ts` `requireCompanyAdminContext` |
| Personal rules API | `src/app/api/auto-participants/route.ts`, `…/[ruleId]/route.ts`, `…/respond/route.ts` |
| Company rules API | `src/app/api/company/auto-participants/route.ts`, `…/[ruleId]/route.ts` |
| Per-trip context chooser API | `src/app/api/trips/[id]/context/route.ts` |
| Remove participant API | `src/app/api/trips/[id]/participants/[participantId]/remove/route.ts` |
| Internal evaluate endpoint (Synchron import) | `src/app/api/internal/auto-participants/evaluate/route.ts` (Bearer `CRON_SECRET`) |
| Settings page (My / Who includes you / Company) | `src/app/settings/auto-participants/` |
| Accept / decline landing | `src/app/auto-participants/[token]/` |
| People tab line + Remove, context chooser | `src/components/app/trip-workspace/trip-workspace.tsx`, `src/components/app/trip-context-chooser.tsx` |
| Emails | `auto_participant_request`, `auto_participant_company_request` (account security, always sent), `auto_participant_added` (trip operational, preferences apply) in `src/lib/email-templates.ts`; editable in `/admin/email-templates` |
| Synchron import | `scripts/synchron/import-carrier.mjs`, `scripts/synchron/lib/map.mjs` |
| Migrations / verify | `supabase/migrations/0049_auto_participants.sql`, `0050_carrier_roles.sql`; `scripts/verify-migration-0049.mjs`, `0050` |
| Tests | `tests/auto-participants-*.test.ts`, `tests/modes.test.ts` |

## How a rule fires

1. A person is **placed on a trip** (created as `invited` or `active`) or a row is **activated** (acceptance, claim, sign-in linking, Synchron import). Every such path calls `evaluateParticipantRules` once; `rules_evaluated_at` makes it fire once per row.
2. **Context** (decision D13): the row must know which company and role the person acts with. The creator's context is the active mode. Others with exactly one matching context get it silently; with several they see the chooser the first time they open the trip (`/api/trips/[id]/context`), and nothing fires until they choose; with none, only personal rules apply.
3. **Personal rules** of the triggering person fire. **Company rules** of the row's `context_company_id` fire when `companyScopeMatches(scope, context_role_type)`.
4. For each rule `addAutoParticipant` applies the gates, in order: `rule_not_active`, `recursive` (the trigger was itself auto-added — §5), `admin_preview` (session acting as someone else — §39), `before_effective_from` (§30; for a claim the trip's import date is compared — D10), `participant_unresolved` (email not yet an account — D5), `self`, `participant_blocked` (§37), `participant_not_claimed` (§31), `membership_lost` (§36), `already_on_trip` (records a second reason instead of a second row — §18/§19), `explicitly_removed` (a removed person is never re-added — D11), `imported_row_exists`.
5. The row is inserted `active` with the person's **own** role (D1), `invited_by` = the triggering person, provenance columns, and — when the trigger is a pilot — role `pilot` with the trigger's own state/permit limit copied onto the `participant_auto_added` trip event (D14; read back by `loadPilotAccess`).
6. One `auto_participant_added` email goes to the person (D8, preferences apply).

Nothing in the app evaluates rules against existing trips (§30). `resolveAutoParticipantsForTrip` exists for tests and a future explicit backfill only.

## Removing a rule, leaving, removing from a trip

- Removing / disabling / stopping a rule changes future trips only; existing participation is untouched (§20–§22). Copy: "This person will no longer be added automatically to future Trips. Existing Trip participation remains unchanged."
- The person who is the Auto-Participant sees who includes them and can **Stop being included** (D16).
- Removing someone from a live trip (D17): the invite matrix, the person whose participation fired a personal rule, a Company Admin of the triggering company (in that company's mode), or a platform admin. The rule is not changed.
- Losing a company membership suspends that person's company rules (`suspended`, reason logged — §36). Reactivation requires the membership to be approved again.

## Synchron orders

The import scripts write a new order's people who already have a real account as **active** participants and call the internal evaluate endpoint, so their Auto-Participants join (D9). People without an account stay `imported` until they claim; a claim fires rules only for trips imported on or after the rule (D10). **Pending on Synchron:** when `order.created` carries the order's people, `src/app/api/webhooks/synchron/route.ts` will apply the same behaviour in-process. No payload is assumed.

## Deferred

- Trip notification engine and the §23 event notifications (only the "added" email exists).
- Backfilling existing trips when a rule is created; "Remove from current active Trips" bulk action; "leave this trip or stop being included?" prompt.
- Groups / teams as Auto-Participants; advanced scope filters (`conditions` is reserved).
- Brokerage and pilot company office role sets (carrier only now).
