Skip to Content

← All archived runs

Run: work-blockers-escalations

run.md

Run: work-blockers-escalations

  • issue: #462
  • branch: claude/laughing-brahmagupta-46zmgl
  • pr: #464

00_intake/stub.md

Stub: Work blockers + escalations

  • feature-slug: work-blockers-escalations
  • epic: expert-dashboard-data
  • personas: Expert (view + raise/escalate); CSM (raise/clear/escalate)
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: expert-workbench-foundation
  • sequence: 3 of 8

Problem

The 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.

Proposed change

  • Reuse and extend the blocker record from 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).
    • Escalation trail: escalationCount, lastEscalatedAt, and an escalated flag — recorded when a blocker is escalated.
  • Build the minimal capture: expert/CSM can raise a blocker on a lead, escalate it (bumping the count + timestamp), and clear it.
  • Expose open blockers per lead (with escalation data) so expert-work-queue can render the blocker, who, suggested contact and escalation columns, and count escalated items.

Acceptance criteria (rough)

  • A blocker can be raised on a lead with who's-blocking + suggested contact; it surfaces on the expert's work queue.
  • Escalating a blocker increments the count and stamps lastEscalatedAt; the Action required "escalated" tally reflects it.
  • Clearing a blocker removes it from the open list.
  • Open blockers (with escalation data) are queryable per lead for the work-queue feature.
  • Extends the existing customer-epic blocker model — no duplicate blocker collection.

Out of scope (this feature)

  • The full Action required summary + work-queue table rendering (that's expert-work-queue; this feature supplies the blocker/escalation data it reads).
  • An escalation workflow (auto-routing, assignment, reminders) beyond manual raise/escalate/clear.
  • SLA/stage timing (sla-stage-targets owns time-in-stage; this owns blocker/escalation state).

Notes for Define

  • Coordinate with 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.md

Spec: Work blockers and escalations data

  • slug: work-blockers-escalations
  • issue: #462
  • personas: Expert, CSM
  • touches: packages/services/src/db/models/blocker.ts (new), packages/services/src/db/models/index.ts, packages/services/src/db/services/blocker/ (new), packages/services/src/db/services/index.ts, apps/web/app/(app)/expert/dashboard/ (server actions — capture entry point)
  • complexity: standard

Problem

The 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.

Proposed change

  • Define one shared blocker model at 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:
    • the base shape 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.
    • the escalation trail: escalationCount (number, default 0), lastEscalatedAt (date), and an escalated flag — recorded when a blocker is escalated.
  • Build the blocker service at packages/services/src/db/services/blocker/, exported through db/services/index.ts so it is reachable via @sustentus/services/server:
    • raise — create an open blocker on a lead with label, who, suggestedContact (and optional impact).
    • escalate — increment escalationCount, stamp lastEscalatedAt, set escalated = true.
    • clear — mark the blocker resolved (so it leaves the open list).
    • query open blockers per lead — return open blockers (with their escalation data) for a single lead and for a set of leads, tenant-scoped, shaped so the consumer can render the blocker / who / suggested-contact / escalation columns and derive the Action required tallies (escalated = open blockers with escalated = true; customersWaiting = open blockers where who = Customer).
  • Expose the capture entry point as server actions under the apps/web expert dashboard (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.

Acceptance criteria

  • A single shared blocker model/collection backs the feature — it carries the base fields the customer project-blockers epic will reuse, with no second blocker collection introduced.
  • A blocker can be raised on a lead with who (Customer | CSM | Expert | Partner) and suggestedContact (Customer | CSM); it then appears in the open-blockers query for that lead.
  • Escalating an open blocker increments escalationCount, stamps lastEscalatedAt, and sets the escalated flag.
  • Clearing a blocker marks it resolved and removes it from the open-blockers list.
  • Open blockers (with their escalation data) are queryable per lead and for a set of leads, and the shape lets the consumer compute the escalated and customersWaiting (open where who = Customer) tallies.
  • Raise / escalate / clear are invocable by the Expert and CSM through apps/web server actions, each scoped to the caller's tenant.
  • The model and service are reachable from @sustentus/services/server (exported via the models and services index files).

Out of scope

  • The work-queue table and Action required summary rendering, and the placement of the raise/escalate/clear buttons — that is expert-work-queue; this feature supplies the data it reads and the server actions it calls.
  • An escalation workflow (auto-routing, assignment, reminders) beyond manual raise/escalate/clear.
  • SLA / stage timing (time-stuck, sla) — owned by sla-stage-targets; this owns blocker and escalation state only.
  • Wiring open-blocker data into the expert dashboard loader — that composition belongs to the consumer / expert-workbench-foundation once both are present; this feature exposes the query for it.
  • Mock-data changes in apps/dashboards (the standalone mock app stays on its mock).

Open questions

  • none

02_build/output/notes.md

Build notes: work-blockers-escalations

  • branch: claude/laughing-brahmagupta-46zmgl
  • commits: feat: work-blockers-escalations — blocker model, service + capture actions

What changed

  • packages/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").

Acceptance criteria status

  • Single shared blocker model/collection — one blocker model with the base + extension fields; no second collection.
  • Raise a blocker on a lead with who + suggestedContact → it appears in findOpenByLead.
  • Escalating increments escalationCount, stamps lastEscalatedAt, sets escalated (atomic findOneAndUpdate on an open blocker).
  • Clearing marks it resolved (+ resolvedAt/resolvedBy) so it leaves the open list.
  • Open blockers (with escalation data) queryable per lead (findOpenByLead) and for a set of leads (findOpenByLeads); the OpenBlocker shape lets the consumer compute escalated and customersWaiting (open where who="Customer").
  • Raise/escalate/clear invocable by Expert and CSM via apps/web server actions, tenant-scoped.
  • Model + service reachable from @sustentus/services/server (exported via both index files).

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. The 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.

Notes for review

  • Capture surface is intentionally data + service + server actions only — the work-queue table and the raise/escalate/clear buttons belong to the consumer 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.md

Sustentus can now track and escalate the blockers that stall delivery

We 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.

  • Blockers are captured against real work — each one names who is blocking (customer, CSM, expert, or partner) and who the expert should chase, so nothing stalls silently.
  • Escalation is tracked, not anecdotal — every escalation bumps a count and timestamp, giving us the signal to step in before a delivery slips.
  • Groundwork for cycle time — this is the data the expert workbench reads next to surface and act on blockers, keeping the lead-to-payout cycle moving.

03_ship/output/pr.md

Ship: work-blockers-escalations

Reconciliation with landed main

During 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.
  • The expert capture actions call the reconciled API (raise with owner+impact, escalate, resolve). The customer lead-blockers display type was widened to accept "Partner".
  • Design note for review: the blocking party is the existing 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).
  • technical docs: no technical docs impact (the services page documents structure, not individual models; adding blocker needs no doc change)
  • business docs: no business docs impact (no user-visible behaviour yet — the work-queue UI is the consumer expert-work-queue's job)
  • release notes: investor-only — investor draft in this PR; no end-user changelog (internal data-foundation change)

Review summary

  • (filled after /code-review)

Acceptance check (vs spec)

  • Single shared blocker model/collection (base + extensions, no duplicate) — db/models/blocker.ts
  • Raise with who + suggestedContact → surfaces in findOpenByLead — service + action
  • Escalate increments count, stamps lastEscalatedAt, sets escalated — atomic findOneAndUpdate
  • Clear → resolved, leaves open list — clear()
  • Open blockers queryable per lead and per set; shape supports escalated / customersWaiting tallies
  • Raise/escalate/clear invocable by Expert + CSM, tenant-scoped — actions.ts with allowedRoles
  • Reachable from @sustentus/services/server — both index files export it

Merge & deploy

  • merged: no — awaiting explicit human approval (merge gate)
  • deploy: pending merge