work-blockers-escalationsrun.md00_intake/stub.mdThe Work queue is built around blockers the expert can't see in real data today: each item carries a
blocker description, who is blocking (Customer | CSM | Expert | Partner), a
suggestedContact, and an escalation trail (escalationCount, lastEscalation, plus the escalated
count in the Action required summary). There is no blocker or escalation model — nothing records
what is impeding a piece of work, who owns clearing it, or how often it has been escalated.
customer-dashboard-data/project-blockers (per-lead:
label, owner, impact, open/resolved) rather than creating a second model. Add the expert-facing
fields the workbench needs:who / owner extended to include Partner (customer epic uses Customer | Your team | Expert).suggestedContact (who the expert should chase: Customer | CSM).escalationCount, lastEscalatedAt, and an escalated flag — recorded when
a blocker is escalated.expert-work-queue can render the
blocker, who, suggested contact and escalation columns, and count escalated items.lastEscalatedAt; the Action required
"escalated" tally reflects it.expert-work-queue; this
feature supplies the blocker/escalation data it reads).sla-stage-targets owns time-in-stage; this owns blocker/escalation state).customer-dashboard-data/project-blockers — if that feature has landed, extend
its model; if not, this feature may define the base + the extensions together. Avoid two blocker
models.customersWaiting (Action required) likely derives from open blockers where who = Customer — keep
the data shaped so the work-queue feature can compute it.touches: packages/services/src/db/models (blocker model — extend), packages/services/src/server,
apps/web/components/dashboard/expert/ (capture entry point).
</content>
</invoke>01_define/output/spec.mdThe expert workbench's Work queue is built around blockers that have no backing data today. Each work
item in the mock (apps/web/components/dashboard/expert/work-queue-row.tsx, lib/mock/expert.ts)
carries a blocker description, a who is blocking (Customer | CSM | Expert | Partner), a
suggestedContact, and an escalation trail (escalationCount, lastEscalation, plus the escalated
and customersWaiting tallies in the Action required summary) — but there is no blocker or
escalation model. Nothing records what is impeding a piece of work, who owns clearing it, or how
often it has been escalated, so none of those columns can show real per-tenant data. This is the
blocker/escalation data feature of the expert-dashboard-data epic (sequence 3 of 8), advancing
Build the Bridge → Q2 2026 Objective 3 (Validate Technical Infrastructure & Payout Flow): real
delivery work can't be unblocked, escalated, or measured until the impediment state it depends on
exists in the database.
packages/services/src/db/models/blocker.ts. The customer
epic's project-blockers feature has not landed (no blocker model or collection exists yet), so
per the intake note this feature defines the base record and the expert extensions together —
one model, one collection, no duplicate. The record is per-lead (lead ref) and tenant-scoped
(standard tenantPlugin + softDeletePlugin + schemaPlugin, matching lead.ts), with:project-blockers will reuse: a label/description, an impact, a status
(open | resolved), and resolved metadata.who — the blocking party, Customer | CSM | Expert | Partner (the customer epic's
Customer | Your team | Expert set, extended with Partner for the workbench).suggestedContact — who the expert should chase: Customer | CSM.escalationCount (number, default 0), lastEscalatedAt (date), and an
escalated flag — recorded when a blocker is escalated.packages/services/src/db/services/blocker/, exported through
db/services/index.ts so it is reachable via @sustentus/services/server:label, who, suggestedContact (and optional
impact).escalationCount, stamp lastEscalatedAt, set escalated = true.resolved (so it leaves the open list).escalated =
open blockers with escalated = true; customersWaiting = open blockers where who = Customer).app/(app)/expert/dashboard/), wrapping raise / escalate / clear so the Expert and CSM can invoke
them. These actions are the callable seam; the actual buttons live in the work-queue table the
consumer feature renders (see Out of scope). Reuse the established auth/tenant-context pattern other
expert dashboard server work uses, scoping every mutation to the caller's tenant.project-blockers epic will reuse, with no second blocker collection introduced.who (Customer | CSM | Expert | Partner) and
suggestedContact (Customer | CSM); it then appears in the open-blockers query for that lead.escalationCount, stamps lastEscalatedAt, and sets the
escalated flag.escalated and customersWaiting (open where
who = Customer) tallies.@sustentus/services/server (exported via the models
and services index files).expert-work-queue; this feature supplies the data it reads
and the server actions it calls.time-stuck, sla) — owned by sla-stage-targets; this owns blocker and
escalation state only.expert-workbench-foundation once both are present; this feature exposes the query for it.apps/dashboards (the standalone mock app stays on its mock).02_build/output/notes.mdfeat: work-blockers-escalations — blocker model, service + capture actionspackages/services/src/db/models/blocker.ts (new): one shared, per-lead, tenant-scoped blocker
model. Base fields the customer project-blockers epic will reuse (label, impact, status
open|resolved, resolved metadata) plus the expert-workbench extensions: who
(Customer | CSM | Expert | Partner), suggestedContact (Customer | CSM), and the escalation trail
(escalationCount, escalated, lastEscalatedAt). Standard schemaPlugin + softDeletePlugin +
tenantPlugin, matching lead.ts/milestone.ts. Indexed on {tenantId, lead, status} (open
blockers per lead) and {tenantId, status, who} (the open + who=Customer slice the
customersWaiting tally reads).packages/services/src/db/services/blocker/{index.ts,instance.ts} (new): BlockerService exposing
raise, escalate, clear, findOpenByLead, and findOpenByLeads — all tenant-scoped. Returns
the OpenBlocker shape (blocker text + who + suggestedContact + escalation data) that the
work-queue consumer groups per lead to derive the escalated and customersWaiting tallies.packages/services/src/db/models/index.ts, …/db/services/index.ts: export the model and
blockerService / BlockerService / OpenBlocker / RaiseBlockerInput, so all are reachable from
@sustentus/services/server.apps/web/app/(app)/expert/dashboard/actions.ts (new): raiseBlocker / escalateBlocker /
clearBlocker server actions — the capture entry point. Each Zod-validates input and resolves the
action context with allowedRoles: ["expert", "csm"], so only Expert and CSM can invoke them, every
mutation scoped to the caller's tenant; they revalidatePath("/expert/dashboard").blocker model with the base + extension fields;
no second collection.who + suggestedContact → it appears in findOpenByLead.escalationCount, stamps lastEscalatedAt, sets escalated (atomic
findOneAndUpdate on an open blocker).resolved (+ resolvedAt/resolvedBy) so it leaves the open list.findOpenByLead) and for a set of
leads (findOpenByLeads); the OpenBlocker shape lets the consumer compute escalated and
customersWaiting (open where who="Customer").@sustentus/services/server (exported via both index files).as const tuple + z.enum pattern and the getModel + plugin model pattern both match existing
code (tenant-integration.ts, milestone.ts), so no known failures expected.expert-work-queue (per the spec's chosen
scope). findOpenByLeads returns a flat list (each row carries its lead) so the consumer groups
and tallies; no loader wiring is done here.03_ship/output/investor-update.mdWe shipped the impediment layer behind expert delivery: Sustentus now records what is blocking a piece of work, who owns clearing it, and how many times it has been escalated. This is foundational work for Build the Bridge — getting a live service transaction through end-to-end — and directly advances Q2 Objective 3, Validate Technical Infrastructure & Payout Flow, because a stalled, untracked blocker is the single biggest threat to lead-to-payout cycle time. With blockers now first-class data, the platform can see, chase, and clear what stands between a started project and a paid one.
03_ship/output/pr.mdDuring Ship, project-blockers (customer epic) and expert-workbench-foundation both landed on
main — so the blocker model + BlockerService already existed. Per the spec's own contingency
("if project-blockers has landed, extend its model — avoid two blocker models"), the duplicate model
this run originally built was dropped and the change re-worked to extend the landed model
additively:
BlockerOwner gains "Partner"; the model gains suggestedContact? + the escalation trail
(escalationCount, escalated, lastEscalatedAt), all optional/defaulted so the customer
project-blockers flow is unaffected.BlockerService keeps raise / resolve / listOpenByLead; gains escalate() and
listOpenByLeads(); RaiseBlockerInput gains optional suggestedContact.raise with owner+impact, escalate,
resolve). The customer lead-blockers display type was widened to accept "Partner".owner field (now incl. Partner),
not a separate who field; the spec's who="CSM" maps to the landed owner="Your team". This is
the reconciliation choice made under "reconcile now on branch" (no re-Define).blocker needs no doc change)expert-work-queue's job)db/models/blocker.tsfindOpenByLead — service + actionlastEscalatedAt, sets escalated — atomic findOneAndUpdateclear()actions.ts with allowedRoles@sustentus/services/server — both index files export it