# 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 `<AgentList projectId={projectId} />`)
- `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<string, unknown>`. 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 `<ActivityFeed entityType="project" entityId={projectId} companyId={companyId} />`.

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