---
phase: 25-file-system
plan: 07
type: execute
wave: 2
depends_on: ["25-06"]
files_modified:
  - server/src/services/chat-files.ts
  - server/src/routes/chat-files.ts
  - server/src/services/placeholder-service.ts
  - packages/shared/src/types/chat.ts
  - .planning/REQUIREMENTS.md
autonomous: true
gap_closure: true
requirements: [FILE-08, FILE-11]

must_haves:
  truths:
    - "Agent-generated files are stored via the upload API with source=agent_generated and linked to task/conversation"
    - "Placeholder files are tracked in a PLACEHOLDERS.md manifest in the project directory"
    - "Replacing a placeholder updates the manifest and records the replacement chain in the DB"
  artifacts:
    - path: "server/src/services/placeholder-service.ts"
      provides: "PLACEHOLDERS.md manifest management: add, remove, replace entries"
      min_lines: 40
    - path: "server/src/services/chat-files.ts"
      provides: "markAsPlaceholder method"
    - path: "server/src/routes/chat-files.ts"
      provides: "POST /files/:fileId/replace endpoint for placeholder replacement"
  key_links:
    - from: "server/src/routes/chat-files.ts"
      to: "server/src/services/placeholder-service.ts"
      via: "placeholderService.addEntry and replaceEntry"
      pattern: "placeholderService"
---

<objective>
Add agent-generated file support and placeholder asset tracking.

Purpose: FILE-08 requires agent-generated files to be stored and linked to tasks/conversations. FILE-11 requires a PLACEHOLDERS.md manifest tracking placeholder assets with replacement chains. The upload API already supports source: "agent_generated" but no code path uses it, and no placeholder tracking exists.

Output: Placeholder service, replace endpoint, updated service methods, 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(),
  source: text("source").notNull(),
  category: text("category"),
  projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }),
});
```

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

From packages/shared/src/validators/chat.ts:
```typescript
source: z.enum(["user_upload", "agent_generated"]).default("user_upload"),
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create placeholderService and add markAsPlaceholder method</name>
  <files>server/src/services/placeholder-service.ts, server/src/services/chat-files.ts, packages/shared/src/types/chat.ts, packages/shared/src/index.ts</files>
  <read_first>
    - server/src/services/chat-files.ts
    - packages/shared/src/types/chat.ts
    - packages/shared/src/index.ts
    - packages/db/src/schema/chat_files.ts
    - server/src/home-paths.ts
  </read_first>
  <action>
1. Add ChatPlaceholderEntry type to packages/shared/src/types/chat.ts:
```typescript
export interface ChatPlaceholderEntry {
  fileId: string;
  filename: string;
  description: string;
  createdAt: string;
  replacedByFileId?: string;
}
```
Export from packages/shared/src/index.ts alongside other chat types.

2. Create server/src/services/placeholder-service.ts with three methods:

addEntry(projectDir, entry): Reads existing PLACEHOLDERS.md (or creates new), adds entry to Active Placeholders table, writes back.

replaceEntry(projectDir, oldFileId, newFileId): Reads PLACEHOLDERS.md, moves the entry from Active to Replaced section with replacedByFileId, writes back.

listEntries(projectDir): Reads and returns parsed entries.

The PLACEHOLDERS.md format:
```markdown
# Placeholder Assets

Auto-maintained by Nexus. Do not edit manually.

## Active Placeholders

| File | Description | File ID |
|------|-------------|---------|
| logo.png | Generated by agent | abc-123 |

## Replaced

| Original | Description | Replaced By |
|----------|-------------|-------------|
| old-logo.png | Generated by agent | def-456 |
```

Use readFile/writeFile from node:fs/promises. Use existsSync to check if file exists. Use mkdir with recursive:true to ensure projectDir exists before writing.

The serialize function builds the markdown table strings from an array of PlaceholderEntry objects ({ fileId, filename, description, replacedByFileId? }).

The parse function reads markdown tables using a regex like /^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|$/ to extract rows, tracking which section (Active vs Replaced) each row belongs to.

3. In server/src/services/chat-files.ts, add markAsPlaceholder method to the returned object:
```typescript
markAsPlaceholder(fileId: string) {
  return db
    .update(chatFiles)
    .set({ category: "placeholder", updatedAt: new Date() })
    .where(eq(chatFiles.id, fileId))
    .returning()
    .then((rows) => rows[0] ?? null);
},
```
  </action>
  <verify>
    <automated>cd /opt/nexus && test -f server/src/services/placeholder-service.ts && echo "placeholder-service exists" && grep "ChatPlaceholderEntry" packages/shared/src/types/chat.ts && grep "markAsPlaceholder" server/src/services/chat-files.ts</automated>
  </verify>
  <acceptance_criteria>
    - File server/src/services/placeholder-service.ts exists
    - Contains addEntry, replaceEntry, listEntries exported methods
    - Generates PLACEHOLDERS.md with Active Placeholders and Replaced markdown tables
    - packages/shared/src/types/chat.ts contains export interface ChatPlaceholderEntry
    - packages/shared/src/index.ts exports ChatPlaceholderEntry
    - server/src/services/chat-files.ts contains markAsPlaceholder method
  </acceptance_criteria>
  <done>PlaceholderService manages PLACEHOLDERS.md manifest; chatFileService has markAsPlaceholder method</done>
</task>

<task type="auto">
  <name>Task 2: Add placeholder and agent-generated file routes</name>
  <files>server/src/routes/chat-files.ts, .planning/REQUIREMENTS.md</files>
  <read_first>
    - server/src/routes/chat-files.ts
    - server/src/services/placeholder-service.ts
    - server/src/services/chat-files.ts
    - server/src/services/git-file-service.ts
    - server/src/home-paths.ts
    - .planning/REQUIREMENTS.md
  </read_first>
  <action>
1. Update server/src/routes/chat-files.ts:

   a. Import placeholderService:
   ```typescript
   import { placeholderService } from "../services/placeholder-service.js";
   ```

   b. Inside chatFileRoutes, instantiate:
   ```typescript
   const phSvc = placeholderService();
   ```

   c. In the existing POST /conversations/:id/files upload route, after creating the chatFile DB record (after the git commit line from Plan 25-06), add placeholder handling:
   ```typescript
   // Track placeholder if agent-generated and project-scoped
   if (parsedMeta.data.source === "agent_generated" && chatFile.projectId) {
     const projectDir = path.join(storageDir, "..", "..", "projects", chatFile.projectId);
     phSvc.addEntry(projectDir, {
       fileId: chatFile.id,
       filename: chatFile.originalFilename,
       description: "Generated by agent",
     }).catch(() => {});
   }
   ```

   d. Add POST /files/:fileId/replace endpoint for replacing a placeholder with a final asset. Place it near other /files/:fileId routes:
   ```typescript
   router.post("/files/:fileId/replace", async (req, res) => {
     assertBoard(req);
     const fileId = req.params.fileId as string;
     const oldFile = await fileSvc.getById(fileId);
     if (!oldFile) { res.status(404).json({ error: "File not found" }); return; }
     assertCompanyAccess(req, oldFile.companyId);

     const { newFileId } = req.body ?? {};
     if (!newFileId || typeof newFileId !== "string") {
       res.status(400).json({ error: "newFileId is required" }); return;
     }
     const newFile = await fileSvc.getById(newFileId);
     if (!newFile) { res.status(404).json({ error: "Replacement file not found" }); return; }

     // Update placeholder manifest if project-scoped
     if (oldFile.projectId) {
       const projectDir = path.join(storageDir, "..", "..", "projects", oldFile.projectId);
       await phSvc.replaceEntry(projectDir, fileId, newFileId);
     }

     // Create reference linking replacement to original context
     await fileSvc.createReference({
       fileId: newFileId,
       conversationId: oldFile.conversationId ?? "",
       messageId: oldFile.messageId ?? undefined,
     });

     res.json({ replaced: fileId, replacedBy: newFileId });
   });
   ```

2. Update .planning/REQUIREMENTS.md:
   - Change FILE-08 from `- [ ] **FILE-08**` to `- [x] **FILE-08**`
   - Change FILE-11 from `- [ ] **FILE-11**` to `- [x] **FILE-11**`
   - In Traceability table, change FILE-08 and FILE-11 from Pending to Complete
  </action>
  <verify>
    <automated>cd /opt/nexus && grep -n "placeholderService\|phSvc\|replace\|agent_generated" server/src/routes/chat-files.ts | head -10 && grep "FILE-08\|FILE-11" .planning/REQUIREMENTS.md | head -6</automated>
  </verify>
  <acceptance_criteria>
    - server/src/routes/chat-files.ts imports placeholderService
    - Contains phSvc.addEntry call for agent_generated files with projectId
    - Contains router.post("/files/:fileId/replace") endpoint
    - Replace endpoint calls phSvc.replaceEntry and fileSvc.createReference
    - .planning/REQUIREMENTS.md contains `- [x] **FILE-08**`
    - .planning/REQUIREMENTS.md contains `- [x] **FILE-11**`
  </acceptance_criteria>
  <done>Agent-generated files trigger placeholder manifest update; replacement endpoint exists; FILE-08 and FILE-11 marked Complete</done>
</task>

</tasks>

<verification>
- npx tsc --noEmit -p server/tsconfig.json passes
- grep "placeholderService" server/src/routes/chat-files.ts matches
- grep "/replace" server/src/routes/chat-files.ts matches
- grep "\[x\].*FILE-08" .planning/REQUIREMENTS.md matches
- grep "\[x\].*FILE-11" .planning/REQUIREMENTS.md matches
</verification>

<success_criteria>
- Agent-generated files uploaded with source=agent_generated trigger PLACEHOLDERS.md update when project-scoped
- POST /files/:fileId/replace updates manifest and creates reference chain
- FILE-08 and FILE-11 marked Complete in REQUIREMENTS.md
- TypeScript compiles without errors
</success_criteria>

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