project-blockersrun.md00_intake/stub.mdThe blockers half of the Risk/Blockers section is dummy (label, owner, impact). There is no
blockers model anywhere — nothing records what is currently impeding a project, who owns the
resolution, or its impact. The health engine also needs open blockers as an input to compute risk and
go-live confidence, so this data has to exist before health can be real.
lead): label, owner
(Customer | Your team | Expert), impact (free text, e.g. "Delays go live by 3 days"), and
an open/resolved state.project-health-engine can consume them.project-health-engine (this feature only
supplies the raw blockers it reads).touches: packages/services/src/db/models (new blocker model or lead sub-doc),
packages/services/src/server, apps/web/components/dashboard/customer/risk-blockers-feedback.tsx.01_define/output/spec.mdThe Risk/Blockers section on the customer dashboard renders dummy blocker rows (label, owner, impact) and there is no blockers model anywhere in the platform — nothing records what is impeding a project, who owns the resolution, or its impact. This blocks two things: the customer cannot be shown the real state of their delivery, and the upcoming project-health-engine has no real signal to compute risk and go-live confidence from. This advances Build the Bridge → Q2 2026 Objective 3 (Validate technical infrastructure & payout flow) by turning a mock surface into real, queryable delivery data that the health engine can consume.
Blocker collection (separate, indexed — not an embedded sub-doc — because open blockers are queried per project frequently and written by multiple personas). Each record references a project (leadId) and carries label, owner (Customer | Your team | Expert), impact (free text, e.g. "Delays go live by 3 days"), and a status of open | resolved (with resolved metadata). Indexed on { tenantId, leadId, status } so "open blockers for this project" is a covered, cheap query.blockerService in packages/services/src/db/services/blocker/, exported via @sustentus/services/server, with: raise(leadId, { label, owner, impact }), resolve(blockerId), and listOpenByLead(leadId). listOpenByLead is the read project-health-engine will consume.apps/web/app/(app)/projects/[id]), scoped to that leadId and available to the delivery team (Expert, CSM, SDM): list the project's open blockers, raise a new one (label + owner + impact), and mark one resolved. Backed by "use server" actions following the existing (app)/.../actions.ts pattern (zod-validated, resolveActionContext / runActionBody).The customer dashboard's blockers list stays on mock data this run — wiring it to real open blockers depends on customer-project-foundation (sequence 1, not yet built), which establishes the signed-in customer's active-project read on that page. This feature supplies the real, queryable data; the foundation feature consumes it for the customer view.
Blocker model exists, tenant-scoped, attached to a project (leadId), with label, owner (Customer | Your team | Expert), impact, and status (open | resolved), indexed on { tenantId, leadId, status }.blockerService exposes raise, resolve, and listOpenByLead, exported from @sustentus/services/server.listOpenByLead for that project (its status becomes resolved).listOpenByLead(leadId) so project-health-engine can consume them.project-health-engine; this feature only supplies the raw blockers it reads.customer-project-foundation (which provides the customer's active-project read); the customer-facing list stays on mock this run.02_build/output/notes.mdfeat: project-blockers — Blocker model + blockerServicefeat: project-blockers — delivery-team capture on project detail pagepackages/services/src/db/models/blocker.ts (new): tenant-scoped Blocker model
referencing a project (lead), with label, owner (Customer | Your team | Expert),
impact, status (open | resolved) plus resolvedAt / resolvedBy / createdBy. Uses the
standard schemaPlugin / softDeletePlugin / tenantPlugin. Indexed on
{ tenantId, lead, status } — the hot "open blockers for this project" read.packages/services/src/db/services/blocker/{index,instance}.ts (new): BlockerService with
raise(tenantId, leadId, {label, owner, impact, createdBy}), resolve(tenantId, blockerId, resolvedBy) (open → resolved), and listOpenByLead(tenantId, leadId) (open only, newest first).
Exported as blockerService (+ BlockerService, RaiseBlockerInput) from
@sustentus/services/server via the db barrels.apps/web/app/(app)/projects/[id]/actions.ts (new): raiseBlockerAction /
resolveBlockerAction — "use server", zod-validated, gated to delivery roles
(admin/csm/sdm/expert) via resolveActionContext, wrapped in runActionBody.apps/web/components/service-leads/lead-blockers/ (new): lead-blockers.tsx (server: fetchesblockers-panel.tsx (client: list, raise form, resolve buttons,
clean "no blockers" empty state), loading.tsx (skeleton).apps/web/app/(app)/projects/[id]/page.tsx: renders <LeadBlockers> in the right column,
only for delivery roles (admin/csm/sdm/expert).Blocker model exists, tenant-scoped, attached to lead, with label/owner/impact/status,
indexed { tenantId, lead, status } — models/blocker.ts.blockerService exposes raise, resolve, listOpenByLead, exported from
@sustentus/services/server.blockers-panel + raiseBlockerAction, router.refresh().listOpenByLead (status → resolved) — resolveBlockerAction.listOpenByLead(leadId) for project-health-engine.gh pr checks. No check is expected to fail.Customer | Your team | Expert) exactly as the spec
and the existing customer-dashboard mock express it — it is a free label, not a user reference.admin from DELIVERY_ROLES.customer-project-foundation. This feature only supplies the queryable data + capture.03_ship/output/changelog.mdPersona: expert, csm, sdm
Delivery teams can now record and clear blockers on a project, so everyone can see what's holding up go-live:
03_ship/output/investor-update.mdWe've given the platform its first real record of what is impeding active projects. Delivery teams — experts, customer success, and service delivery managers — can now raise a blocker on any project, naming who owns it and the impact on go-live, and clear it once resolved. Until now this was placeholder content with nothing behind it; it is now real, per-project data the team works from.
This advances the Build the Bridge initiative and our Q2 2026 objective to validate technical infrastructure & payout flow — turning project delivery risk into real, trackable data that protects the lead-to-payout cycle.
03_ship/output/pr.mdtechnical/packages/services page is structural and does not enumerate individual models/services)apps/docs/app/business/feature-role-matrix/projects/page.mdx — added "Raise blocker on project" and "Resolve blocker" rows03_ship/output/investor-update.md) + changelog entry published in apps/help/app/changelog/page.mdxraiseBlockerAction / resolveBlockerAction gate on role only — any expert/CSM/SDM/admin in the tenant could raise/resolve a blocker on any project in that tenant by calling the action directly. Blast radius is within-tenant and the actors are trusted internal delivery staff, so it's not a data-leak; it's an authZ/UX-consistency gap. Logged as a follow-up rather than expanded here (spec scoped this to "minimal raise/clear"). resolve would need a blocker→lead lookup to enforce expert assignment.@sustentus/services/server consumption, action-import path matches the brd/[id]/actions precedent), and reuse (model/service/action all follow the csat/quote patterns). No other findings.Blocker model — tenant-scoped, attached to lead, label/owner/impact/status, indexed { tenantId, lead, status } (packages/services/src/db/models/blocker.ts).blockerService exposes raise / resolve / listOpenByLead, exported from @sustentus/services/server.listOpenByLead (status → resolved).listOpenByLead(leadId) for project-health-engine.gate:merge-approved).