---
phase: 25-file-system
plan: 05
type: execute
wave: 1
depends_on: ["25-01"]
files_modified:
  - server/src/services/chat-files.ts
  - server/src/routes/chat-files.ts
  - ui/src/components/ChatFileCard.tsx
  - ui/src/api/chat.ts
  - .planning/REQUIREMENTS.md
autonomous: true
gap_closure: true
requirements: [FILE-12]

must_haves:
  truths:
    - "User can promote a chat-scoped file to project scope via a single action"
    - "PATCH /files/:fileId/promote endpoint sets projectId on a chat file"
  artifacts:
    - path: "server/src/services/chat-files.ts"
      provides: "promoteToProject service method"
    - path: "server/src/routes/chat-files.ts"
      provides: "PATCH /files/:fileId/promote endpoint"
    - path: "ui/src/components/ChatFileCard.tsx"
      provides: "Promote button when file has no projectId"
  key_links:
    - from: "ui/src/components/ChatFileCard.tsx"
      to: "ui/src/api/chat.ts"
      via: "chatApi.promoteFile call"
      pattern: "promoteFile"
    - from: "server/src/routes/chat-files.ts"
      to: "server/src/services/chat-files.ts"
      via: "fileSvc.promoteToProject"
      pattern: "promoteToProject"
---

<objective>
Add file scope promotion: a chat-scoped file can be promoted to a project scope.

Purpose: FILE-12 requires that chat-scoped files can be promoted to project scope. The schema already has a nullable `projectId` FK on chatFiles, but there is no API endpoint or UI to set it. This plan adds a PATCH /files/:fileId/promote endpoint and a promote button on ChatFileCard.

Output: Service method, API endpoint, UI promote button, updated REQUIREMENTS.md
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/25-file-system/25-01-SUMMARY.md

<interfaces>
From packages/db/src/schema/chat_files.ts:
```typescript
export const chatFiles = pgTable("chat_files", {
  id: uuid("id").primaryKey().defaultRandom(),
  projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }),
  // ... other columns
});
```

From server/src/services/chat-files.ts:
```typescript
export function chatFileService(db: Db) {
  return {
    create(...), getById(...), listByConversation(...), listByMessage(...),
    createReference(...), listReferences(...), attachToMessage(...)
  };
}
```

From ui/src/api/chat.ts:
```typescript
export const chatApi = {
  uploadFile(...), attachFilesToMessage(...)
};
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add promoteToProject service method and API endpoint</name>
  <files>server/src/services/chat-files.ts, server/src/routes/chat-files.ts</files>
  <read_first>
    - server/src/services/chat-files.ts
    - server/src/routes/chat-files.ts
    - packages/db/src/schema/chat_files.ts
  </read_first>
  <action>
1. In `server/src/services/chat-files.ts`, add a new method `promoteToProject` to the returned object:
```typescript
promoteToProject(fileId: string, projectId: string) {
  return db
    .update(chatFiles)
    .set({ projectId, updatedAt: new Date() })
    .where(eq(chatFiles.id, fileId))
    .returning()
    .then((rows) => rows[0] ?? null);
},
```

2. In `server/src/routes/chat-files.ts`, add a new route BEFORE the existing `PATCH /files/:fileId` route:
```typescript
// PATCH /files/:fileId/promote — Promote chat file to project scope
router.patch("/files/:fileId/promote", async (req, res) => {
  assertBoard(req);

  const fileId = req.params.fileId as string;
  const chatFile = await fileSvc.getById(fileId);
  if (!chatFile) {
    res.status(404).json({ error: "File not found" });
    return;
  }
  assertCompanyAccess(req, chatFile.companyId);

  const { projectId } = req.body ?? {};
  if (!projectId || typeof projectId !== "string") {
    res.status(400).json({ error: "projectId is required" });
    return;
  }

  const updated = await fileSvc.promoteToProject(fileId, projectId);
  if (!updated) {
    res.status(404).json({ error: "File not found" });
    return;
  }
  res.json(updated);
});
```

Place this route BEFORE the `PATCH /files/:fileId` route so Express matches `/files/:fileId/promote` before the catch-all `/files/:fileId`.
  </action>
  <verify>
    <automated>cd /opt/nexus && grep -n "promoteToProject" server/src/services/chat-files.ts server/src/routes/chat-files.ts</automated>
  </verify>
  <acceptance_criteria>
    - `server/src/services/chat-files.ts` contains `promoteToProject(fileId: string, projectId: string)` method
    - `server/src/routes/chat-files.ts` contains `router.patch("/files/:fileId/promote"` route
    - The promote route appears BEFORE the generic `router.patch("/files/:fileId"` route
    - Route validates `projectId` is present and is a string
    - Route calls `fileSvc.promoteToProject(fileId, projectId)`
  </acceptance_criteria>
  <done>PATCH /files/:fileId/promote endpoint exists, validates input, updates projectId on chat file</done>
</task>

<task type="auto">
  <name>Task 2: Add promote button to ChatFileCard and API client method</name>
  <files>ui/src/api/chat.ts, ui/src/components/ChatFileCard.tsx, .planning/REQUIREMENTS.md</files>
  <read_first>
    - ui/src/api/chat.ts
    - ui/src/components/ChatFileCard.tsx
    - .planning/REQUIREMENTS.md
  </read_first>
  <action>
1. In `ui/src/api/chat.ts`, add a new method to `chatApi`:
```typescript
promoteFile(fileId: string, projectId: string) {
  return api.patch<ChatFile>(`/files/${fileId}/promote`, { projectId });
},
```
Also add `ChatFile` to the import from `@paperclipai/shared` if not already imported.

2. In `ui/src/components/ChatFileCard.tsx`:
   - Add optional props: `projectId?: string | null` and `onPromoted?: (file: ChatFile) => void` to ChatFileCardProps
   - Import `FolderUp` from lucide-react (for the promote icon)
   - Import `chatApi` from `../api/chat`
   - Import `ChatFile` from `@paperclipai/shared`
   - Add state: `const [promoting, setPromoting] = useState(false)`
   - Add a promote button that appears ONLY when `file.projectId === null && projectId && onPromoted`:
     ```tsx
     {file.projectId === null && projectId && onPromoted && (
       <button
         onClick={async (e) => {
           e.stopPropagation();
           setPromoting(true);
           try {
             const updated = await chatApi.promoteFile(file.id, projectId);
             onPromoted(updated);
           } finally {
             setPromoting(false);
           }
         }}
         disabled={promoting}
         className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
         aria-label="Promote to project"
         title="Promote to project"
       >
         <FolderUp className="h-4 w-4" />
       </button>
     )}
     ```
   - Place this button before the download button

3. Update `.planning/REQUIREMENTS.md`:
   - Change FILE-12 line from `- [ ] **FILE-12**` to `- [x] **FILE-12**`
   - In Traceability table, change FILE-12 status from `Pending` to `Complete`
  </action>
  <verify>
    <automated>cd /opt/nexus && grep -n "promoteFile" ui/src/api/chat.ts && grep -n "FolderUp\|onPromoted\|promoteToProject" ui/src/components/ChatFileCard.tsx && grep "FILE-12" .planning/REQUIREMENTS.md | head -3</automated>
  </verify>
  <acceptance_criteria>
    - `ui/src/api/chat.ts` contains `promoteFile(fileId: string, projectId: string)` method
    - `ui/src/components/ChatFileCard.tsx` contains `FolderUp` import from lucide-react
    - `ui/src/components/ChatFileCard.tsx` contains `onPromoted` prop
    - `ui/src/components/ChatFileCard.tsx` contains `chatApi.promoteFile` call
    - `ui/src/components/ChatFileCard.tsx` conditionally renders promote button only when `file.projectId === null && projectId && onPromoted`
    - `.planning/REQUIREMENTS.md` contains `- [x] **FILE-12**`
  </acceptance_criteria>
  <done>Chat files can be promoted to project scope via UI button; FILE-12 marked Complete</done>
</task>

</tasks>

<verification>
- `npx tsc --noEmit -p ui/tsconfig.json` passes
- `npx tsc --noEmit -p server/tsconfig.json` passes
- `grep "promoteToProject" server/src/services/chat-files.ts` matches
- `grep "promoteFile" ui/src/api/chat.ts` matches
- `grep "\[x\].*FILE-12" .planning/REQUIREMENTS.md` matches
</verification>

<success_criteria>
- PATCH /files/:fileId/promote endpoint sets projectId on a chat file
- ChatFileCard shows a promote button for chat-scoped files when a project context is available
- FILE-12 marked Complete in REQUIREMENTS.md
- TypeScript compiles without errors
</success_criteria>

<output>
After completion, create `.planning/phases/25-file-system/25-05-SUMMARY.md`
</output>
