--- 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" --- 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 @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/25-file-system/25-01-SUMMARY.md 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"), ``` Task 1: Create placeholderService and add markAsPlaceholder method server/src/services/placeholder-service.ts, server/src/services/chat-files.ts, packages/shared/src/types/chat.ts, packages/shared/src/index.ts - 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 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); }, ``` 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 - 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 PlaceholderService manages PLACEHOLDERS.md manifest; chatFileService has markAsPlaceholder method Task 2: Add placeholder and agent-generated file routes server/src/routes/chat-files.ts, .planning/REQUIREMENTS.md - 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 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 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 - 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**` Agent-generated files trigger placeholder manifest update; replacement endpoint exists; FILE-08 and FILE-11 marked Complete - 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 - 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 After completion, create `.planning/phases/25-file-system/25-07-SUMMARY.md`