Team — ladder, levels, reviews, moments, and the contractor policy

This article explains what the Team module in the Pepper console does and why it behaves the way it does: the W-2 skill ladder and how a level change is recorded, how reviews are built from 7shifts and Dripos evidence, what "moments" are for, how the effective-rate and attendance cards are computed, and why contractors get none of the development instruments. It is the reference behind the console's short labels (Ladder, Action items, Logged moments, All-time avg · at current rate, Attendance — lates and overstays >15 min, by Mon–Sun week).

What the module is

The Team tab's subtitle reads Skill ladder · rolling feedback · reviews grounded in 7shifts + Dripos evidence (the content audit slates it for trimming once this article is published). That is the whole design in one line: reviews open with data, not memory. The module is pepper-ui/backend/team-data.js plus pepper-ui/frontend/src/lib/components/TeamView.svelte. Records live in Supabase tables team_profiles, team_levels, team_notes, team_reviews and team_skills; the objective numbers are pulled live from 7shifts (shifts, punches) and Dripos (timecards, tips) over a rolling 90-day window. The aggregate is cached for 10 minutes; any write busts it.

The four cards at the top

CardWhat it counts
Active teamActive profiles, split W-2 / contractors.
Reviews dueW-2 only. A member is "due" when there is no shared review, or the last shared review is older than 120 days (REVIEW_DUE_DAYS = 120 — the "~quarterly" cadence). Contractors are never due, by construction. Open drafts are shown alongside.
Kudos · 30dNotes of kind kudos logged in the last 30 days, across active members.
Last calibrationAge of the most recent note of kind calibration. The card's meta line — "monthly tasting keeps quality drift out" — is the reason the metric exists: a monthly group tasting is how we keep everyone's palate and dial-in anchored to the same standard. The console only shows how long since the last one was logged.

The W-2 ladder

The Levels & criteria button opens the published ladder. Each level comes from team_levels: a code, a name, a café rate and a catering rate, a one-line summary, and a bulleted list of criteria. Two rules the panel states on screen (slated for trimming once this article is published):

  • The criteria are published and visible to employees, and a level change is a raise. The ladder is the standard someone is measured against, not a private scorecard.
  • It does not apply to 1099 contractors. Their rates are negotiated commercially (see the contractor section below). The server enforces this: setting level_code on a non-W-2 profile is rejected with "Ladder levels are W-2 only — contractor rates are negotiated, not 'earned through levels' (misclassification risk)".

[check: the ladder criteria themselves — the level names and bullet lists in team_levels — are meant to be visible to employees. Confirm they are published in the Employee Handbook workspace; if not, that is a separate Handbook article. This article covers only how the console handles them.]

Changing a level

Level buttons in a member's sheet use the same arm-then-confirm pattern as the payroll Pay button: the first tap arms (L2?), the second tap commits, Cancel disarms. One stray tap must never rewrite the ladder — the pattern exists because a level was fat-fingered on day one. Escape or closing the sheet also disarms; an armed change never carries across members.

What a confirmed change does, in order:

  1. Writes the new level_code and sets level_since to today (Chicago).
  2. Logs a system note of kind milestone: Level change: L1 → L2. That is the "logs a milestone" in the confirm line.
  3. Nothing else. Pay is not changed.

This is the ladder-versus-payroll split the console's Ladder link points at: the ladder is the published criteria; payroll stays the pay truth. W-2 rates are effective-dated entries in pepper-ui/backend/payroll-data.js#EMPLOYEES, and the live rate has to be updated in 7shifts and Dripos by hand. scripts/dripos-rate-audit.py (Step 0 of /payroll-audit, so it rides the com.pepper.payroll-audit run, Tuesday 9am CDT biweekly) asserts every W-2's resolved Dripos rate against that roster and exits non-zero on drift — a Dripos role can hold several rate definitions and the newest one silently wins, which is how a promotion has already repriced a timecard wrongly once.

Skills

The skill matrix is ten fixed skills (espresso & dial-in, milk & latte art, pour-over & batch, POS & cash, opening, closing, service & recovery, catering setup, van driving, training others). Tapping a chip cycles learning → solid → teaching → (cleared); teaching shows a star. W-2 only — the server rejects a skill write for a contractor because a development plan for a 1099 is behavioral-control evidence.

Reviews

Reviews are a W-2 instrument. Creating one snapshots the evidence at that moment — the 90-day stats and the last 15 notes — so a review finished weeks later is still anchored to the period it covers (period_start = today minus 90 days). The composer has five sections, each rated Below / Meets / Exceeds / Outstanding with a hint of what good feedback looks like:

SectionHint (as shown in the composer)
Coffee craftCite specifics: dial-in, milk texture, consistency — "dialed in the new drop solo on Jul 3, balanced by 8am" beats "makes good coffee".
Service & teamworkConcrete moments beat adjectives — "handled the Jul 4 rush alone, zero comps" not "great with people".
Reliability & opsThe evidence panel has shifts/punctuality — comment on the pattern, not one-offs.
Growth & goalsWhat should they learn next quarter? Tie it to the ladder criteria for their next level.
Overall & levelTwo-three sentences. Recommend a level change only if the published criteria are met.

Then a Summary, a Level recommendation (a level code, or none), and Action items.

  • Action items — the heading's tail, "feedback that goes nowhere kills engagement", is the rule: every review ends with concrete follow-ups (e.g. "Lead the August calibration tasting") with a checkbox, so the next review can open on whether they happened.
  • Logged moments — the right-hand panel lists the notes logged during the period. The label "anti-recency-bias" is why: a review written from the notes of the whole quarter is not a review of the last two weeks. If no notes were logged, the panel says so — that is the fix for next quarter, not a shortfall of the review.
  • Evidence snapshot — hours, café shifts / catering events, tips (and per hour), lates as a fraction of checked café punches.

Mechanics: autosave fires 1.5 seconds after the last keystroke (badge shows saved / saving… / unsaved). Draft with Pepper fills only empty sections from the evidence and notes (one-shot claude --print, sonnet); your own words are never overwritten, and drafted text carries a "Pepper draft — edit before sharing" note. Mark shared sets shared_at, locks the period and restarts the 120-day due clock; share it in the 1:1.

Moments (notes)

The Log a moment card on a W-2 sheet takes a kind — Note, Kudos, Coaching, Calibration, 1:1 — and a body. Its label, "10 seconds now beats recalling it in October", is the policy: a dated, specific line logged the day it happens is what feeds the next review; nothing else does. Milestones are written by the system on level changes. The team-wide Recent feedback list shows the latest eight notes.

Contractors — the policy

1099 contractors are vendors. They get no reviews, no skill plans, no ladder, no coaching. We track outcomes and decide whether to re-book. Evaluating how a contractor works is IRS behavioral-control evidence and strains IL 820 ILCS 405/212 — see Contractor classification watch — signals, statute, what to do for the signals. What they get instead:

InstrumentWhat it is
Standing rates — negotiatedCatering $/hr, café $/hr and the Mercury recipient ID feed the Payroll Pay button's math directly (payOwed() in pepper-ui/backend/payroll-data.js), so saving is arm-then-confirm ("This changes what {name} gets paid"). The Van premium $/hr field is informational only: team_profiles.rate_van is read into the roster as vanRate and echoed in the People-table description, but the premium actually paid is the fixed +$3/hr DRIVER_PREMIUM (see Contractor pay rules — owed queue, café↔café, van premium, no-shows, Mercury setup). A dead field — either wire it into payOwed() or remove it. Without a Mercury recipient ID the person cannot be paid from the dashboard. Rate changes are commercial negotiation; log the conversation as a note.
Booking statusPreferred / Active / Backup / Paused / Don't book. The re-book decision, recorded.
Event log — outcomes, not coachingNote kinds are Event note, Kudos, Incident. The placeholder shows the shape: "Jul 12 wedding — setup solo, client praised the bar, van back by 4:10" — facts, not coaching. The server rejects coaching, calibration and 1:1 kinds on a contractor profile.

Server-side guards match the UI: level codes and skills are W-2 only; standing-rate fields are 1099 only (W-2 pay lives in payroll-data.js#EMPLOYEES); createReview refuses a contractor.

Evidence · last 90 days

Per member, from the same plumbing the payroll module uses:

  • Hours — Dripos timecard hours over the window, plus 7shifts catering punch hours where they exist (catering punches are fallback colour, not pay math).
  • Café shifts / Catering events — 7shifts shifts by department (catering = CATERING_DEPT); catering events count distinct dates.
  • Tips — Dripos timecards; shown per hour when hours and tips are both positive. Blank if Dripos was unreachable.
  • Late clock-ins (>5 min) — café shifts only, first punch of the day against scheduled start with a 5-minute tolerance; catering excluded because no-punch is the norm there. This is a different threshold from the payroll audit's 15 minutes below — the evidence card is a soft punctuality read, the attendance card is the audited record.

Effective hourly rate — base + tips

One fetch of the member's whole history, sliced client-side into Week / Month / Qtr / YTD / Last yr / All time. For each worked day: café hours × café rate + catering hours × catering rate + tips; days with no work are skipped, not counted as zero. The sparkline buckets adapt to the span (daily up to 14 days, weekly to 120, fortnightly to 400, monthly beyond). Annualized at this pace = all-time effective rate × average hours per week × 52. History starts at hire_date; profiles without one fall back to a one-year window and start "all time" at first observed activity rather than overstating it. Cached 15 minutes per member.

All-time avg · at current rate (contractors only). W-2 rates are effective-dated in payroll-data.js#EMPLOYEES, so past periods are priced at the rate that applied then. Contractor rates are a single current value on team_profiles — there is no rate history — so every past period is priced at today's rate, and periods before a rate change will be off. The label says so instead of presenting the number as precise.

Attendance — lates and overstays >15 min, by Mon–Sun week

This card is not computed by the Team module. It reads data/attendance-history.json, which scheduled-tasks/payroll-audit/payroll_compare.py appends to on every run (com.pepper.payroll-audit, Tuesday 9am CDT, biweekly, and any manual /payroll-audit). Each late and each overstay is recorded individually with its minutes — Maciej, 2026-08-06: "I want to see each overstay and each late separately — how many minutes each — and the weekly total."

RuleValue in the audit
LateClock-in more than 15 minutes after scheduled start (LATE_THRESHOLD = 15). A deviation over 120 minutes is treated as a schedule swap (SCHEDULE_MISMATCH), not lateness, and is not recorded here.
OverstayClock-out more than 15 minutes after scheduled end (OVERSTAY_THRESHOLD = 15). In the audit this is a needs-input flag, not a warning — unbudgeted hours get a conscious yes/no before payroll approves them.
WeekMonday–Sunday, matching how the schedule is built. The pay period is Tuesday–Monday; bucketing by pay period would split one closing week across two buckets and make the totals read wrong.
MinutesDisplayed as 1h 17m rather than 77m so an 80-minute overstay is not under-read.

The file is keyed by 7shifts display name; the profile's name resolves to it. Nothing on record shows as "No lates or overstays on record."


Was this article helpful?