# Nexus Phase 11.5 — Per-Project Scoping Follow-Up **Status:** Drafted 2026-04-11. Not yet started. Queued after Wave 3 completes, unless the `TabPlaceholder` UX pain surfaces earlier. **Why this exists:** Phase 11's subagent discovered that 5 of the 6 new Project Detail "Builder" tabs have **no existing projectId-aware list component** to reuse, and the underlying type system doesn't carry project association on the relevant entities. Rather than halt Phase 11, the subagent shipped visible `TabPlaceholder` components that render a "PHASE 11 DATA GAP" badge with a precise description of what each tab needs. Those placeholders work as honest diagnostic UI but are not the final product. **Source of truth:** `docs/plans/2026-04-11-nexus-phase-11-projects-builder.md` (original phase 11 plan), Phase 11's final subagent report, and `docs/specs/2026-04-11-nexus-layout-overhaul.md` §7.2 for the target UX. --- ## 1. The five gaps Each gap is a **self-contained ticket** that can be worked independently. Ordering is flexible but the agents/gates/costs trio share some backend patterns and are best done together; activity and org are their own work. ### 1.1 `AGENTS` tab — per-project agent list **Goal:** Render the agents assigned to or actively working on a specific project, in the `AgentsTab` component currently showing a `TabPlaceholder`. **Current state:** - `ui/src/pages/Agents.tsx` — 100+ lines of inline list + filter UI, not extracted as a reusable component - `Agent` type in `@paperclipai/shared` has **no `projectId` field** and no reverse index to projects - `agentsApi.list(companyId)` returns all agents for a company, no filter options **Scope:** 1. **Define the agent ↔ project relationship.** Two options: - (a) Add `projectId: string | null` to the `Agent` type + DB column, backfilled by looking at the agents' currently-assigned issues. Simplest, requires a migration. - (b) Derive the relationship from assigned issues: an agent is "on project X" if it has any open issue in X. No schema change; frontend join; more expensive to query. - (c) Add a `project_agents` join table for explicit assignment; requires migration and a separate assignment UI. 2. **Extract `AgentList` as a reusable component** from `Agents.tsx`, with a `projectId?: string | null` prop. 3. **Wire into `AgentsTab`** — replace the `TabPlaceholder`. **Recommendation:** start with (b) — pure frontend derivation from the issue query already in memory (Phase 11.5 already fetches all issues for ProjectCard derivatives; piggyback on that cache). If the UX turns out to need explicit assignment semantics (e.g., "assign this agent to project X even though they have no issues yet"), escalate to (a) or (c) later. **Files to touch:** - `ui/src/components/agents/AgentList.tsx` (new — extract from Agents.tsx) - `ui/src/components/projects/tabs/AgentsTab.tsx` (update — remove TabPlaceholder, render ``) - `ui/src/pages/Agents.tsx` (small refactor — consume the new AgentList for the unscoped case) **Acceptance:** AgentsTab renders the subset of company agents that have any open issue whose `projectId` matches the current project. Hover/click behavior matches the existing Agents page. --- ### 1.2 `GATES` tab — per-project pending approvals **Goal:** Render the approvals (display: "gates") for a specific project in the `GatesTab`. **Current state:** - `Approval.payload` is `Record`. Some approval types carry `payload.projectId` by convention; most don't. - `ApprovalsList` or equivalent doesn't exist as a standalone component; `ui/src/pages/Approvals.tsx` is inline. - `approvalsApi.list(companyId, status)` returns all approvals scoped by status, no project filter. **Scope:** 1. **Audit approval types for `projectId` coverage.** Grep the server and UI for approval payload shapes; list which types carry `projectId` and which don't. Types that don't need either: - (a) have a `projectId` added to their payload going forward (backward-compat default: `null`) - (b) be filtered out of the per-project view and only surfaced in the global `/approvals` route 2. **Lift the frontend filter** already used by Projects.tsx (`buildProjectDerivatives` probes `payload.projectId`) into a shared `filterApprovalsByProject(approvals, projectId)` helper. 3. **Extract `ApprovalsList`** as a reusable component with a `projectId?: string | null` prop and a display-mode toggle for "Gates" vs "Approvals" copy. 4. **Wire into `GatesTab`** — replace TabPlaceholder, use "Gates" copy. **Files to touch:** - `ui/src/lib/approval-filters.ts` (new — shared filter helper) - `ui/src/components/approvals/ApprovalsList.tsx` (new — extract from Approvals.tsx) - `ui/src/components/projects/tabs/GatesTab.tsx` (update) - `ui/src/pages/Approvals.tsx` (refactor — consume new component) **Acceptance:** GatesTab renders pending+historical approvals whose `payload.projectId` matches. Copy uses "Gate" / "Gates" everywhere. Underlying API calls remain on `/api/approvals`. --- ### 1.3 `COSTS` tab — per-project cost burn **Goal:** Render cost breakdown for a specific project in `CostsTab`. **Current state:** - `ui/src/pages/Costs.tsx` is ~1100 lines of inline content — aggregation, charting, filters, and layout all in one file - No `CostsBreakdown` standalone component - `costsApi` probably has per-company totals; per-project breakdown depends on the cost event records carrying `projectId` (likely yes because costs roll up to projects in the existing UI somewhere) **Scope:** 1. **Read `Costs.tsx` and identify the breakdown section** (the chart/table that shows costs by project or by agent). If it exists, extract to `CostsBreakdown.tsx` with a `projectId?: string | null` prop. 2. **If the existing costs API doesn't support per-project filtering**, add a server-side filter to the endpoint (small backend change) or filter on the client (acceptable if cost events are bounded). 3. **Wire into `CostsTab`** — replace TabPlaceholder. **Files to touch:** - `ui/src/components/costs/CostsBreakdown.tsx` (new — extract) - `ui/src/components/projects/tabs/CostsTab.tsx` (update) - `ui/src/pages/Costs.tsx` (refactor) - Possibly `server/src/routes/costs.ts` (add projectId filter param) **Acceptance:** CostsTab renders cost events for the current project with an agent-by-agent breakdown. The global `/costs` page still works. --- ### 1.4 `ACTIVITY` tab — per-project activity feed **Goal:** Render an activity feed scoped to a single project in `ActivityTab`. **Current state:** - `activityApi.list(entityType, entityId)` **already supports** filtering by entity — this is the easiest of the five gaps - `ui/src/pages/Activity.tsx` renders the unscoped feed inline, no standalone component **Scope:** 1. **Extract `ActivityFeed` as a reusable component** from `Activity.tsx` with a `{ entityType: string; entityId: string | null; companyId: string }` prop shape. 2. **Wire into `ActivityTab`** — call ``. **Files to touch:** - `ui/src/components/activity/ActivityFeed.tsx` (new — extract) - `ui/src/components/projects/tabs/ActivityTab.tsx` (update) - `ui/src/pages/Activity.tsx` (refactor) **Acceptance:** ActivityTab renders a filtered feed for the current project. Global `/activity` still works. The proxy in Wave 2.5's ProjectCard (newest issue updatedAt) can optionally be replaced with this feed's latest event. **This is the lowest-effort ticket of the five.** Start here if you want a quick win. --- ### 1.5 `ORG` tab — per-project org chart **Goal:** Render the subset of the org chart relevant to a specific project in `OrgTab`, hidden entirely for single-agent projects. **Current state:** - `agentsApi.org(companyId)` returns a company-wide tree - `ui/src/pages/Org.tsx` (or similarly named) renders the full tree inline - No way to query "which agents and what reporting structure are relevant to project X" **Scope:** 1. **Define what "project-scoped org chart" means.** Options: - (a) Show the subset of agents assigned to the project (from ticket 1.1) with their existing reporting relationships preserved. - (b) Show the full company org with non-project agents visually dimmed. - (c) Drop the tab entirely for Phase 11.5 — the ORG tab is already hidden for single-agent projects, and multi-agent projects might be better served by the global /org page than a per-project subview. 2. **If (a) or (b)**, extract `OrgChart` as a standalone component with a `highlightAgentIds?: string[]` or `agentIdFilter?: string[]` prop. **Recommendation:** **(c) — drop the ORG tab entirely for Phase 11.5** unless you actively miss it. The single-agent auto-hide already reduces the surface; multi-agent projects are rare enough that the global org page suffices. If the tab is dropped, delete `OrgTab.tsx` and update `BuilderTabStrip.tsx` to unconditionally hide it. **Files to touch (if not dropped):** - `ui/src/components/agents/OrgChart.tsx` (new — extract) - `ui/src/components/projects/tabs/OrgTab.tsx` (update) - `ui/src/pages/Org.tsx` (refactor) **Acceptance (if not dropped):** OrgTab renders a subtree of agents relevant to the project. Still hidden for single-agent projects. --- ## 2. Dependency order and parallelism ``` Ticket 1.4 (Activity) ← lowest effort, start here Ticket 1.1 (Agents) ← unblocks Ticket 1.5 if we keep Org Ticket 1.2 (Gates) ← independent Ticket 1.3 (Costs) ← independent, backend work TBD Ticket 1.5 (Org) ← drop it, or wait on Ticket 1.1 ``` Tickets 1.2, 1.3, 1.4 are pure-frontend extractions and can run in parallel. Ticket 1.1 may involve a schema decision that blocks parallelization. Ticket 1.5 is suggested to drop. ## 3. Non-scope (things Phase 11.5 does NOT do) - Adding real `milestoneProgress`, `phasesCompleted`, `phasesTotal` fields to the `Project` record. That's a Phase 11.6 backend phase if wanted, or accepted as permanent client-side proxy. - Adding `originConversationId` to the `Project` record. Same disposition. - Adding `costBurnedCents` aggregate to the `Project` record. Same. - Adding explicit `project_agents` join table. That's (c) in Ticket 1.1 and deferred unless issue-derivation proves insufficient. - Phase 14's ⌘K globalization (that's its own phase in Wave 3). - Phase 16 cleanup of dead top-level routes like `/approvals`, `/agents`, `/activity`, `/org`, `/costs`. Those pages stay for backwards compat until Phase 16 decides their fate. ## 4. Triggering condition This plan sits in the backlog until either: 1. **The `TabPlaceholder` UX hurts** — user hits one of the 5 blocked tabs frequently and the "data gap" badge becomes a friction point. At that moment, promote whichever tab is hurting most to active work. 2. **Wave 3 completes** and the team has bandwidth for backend-adjacent tickets. 3. **A v1.8+ milestone** explicitly pulls one of these scoping stories into scope (e.g., "project-level cost budgeting" would pull in Ticket 1.3). None of these tickets is time-critical. The placeholders are honest and diagnosable — users can see "PHASE 11 DATA GAP: project-scoped costs not yet implemented" and navigate to the global `/costs` page if they need the data. ## 5. Execution When ready to start, write a narrow implementation plan per ticket (one plan per ticket), using the same format as Phase 8's task-by-task TDD plan. Dispatch via subagent-driven-development for tickets 1.2–1.5 and in-session for ticket 1.1 (because it requires an architectural decision). Do not dispatch all five tickets as one subagent — they touch disjoint subsystems and deserve independent review cycles.