nexus/.planning/phases/25-file-system/25-07-PLAN.md

10 KiB

phase plan type wave depends_on files_modified autonomous gap_closure requirements must_haves
25-file-system 07 execute 2
25-06
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
true true
FILE-08
FILE-11
truths artifacts key_links
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
path provides min_lines
server/src/services/placeholder-service.ts PLACEHOLDERS.md manifest management: add, remove, replace entries 40
path provides
server/src/services/chat-files.ts markAsPlaceholder method
path provides
server/src/routes/chat-files.ts POST /files/:fileId/replace endpoint for placeholder replacement
from to via pattern
server/src/routes/chat-files.ts server/src/services/placeholder-service.ts placeholderService.addEntry and replaceEntry 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

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

@.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:

export function chatFileService(db: Db) {
  return { create(...), getById(...), attachToMessage(...), promoteToProject(...), createReference(...) };
}

From packages/shared/src/validators/chat.ts:

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.
  1. 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:

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

  1. In server/src/services/chat-files.ts, add markAsPlaceholder method to the returned object:
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:

import { placeholderService } from "../services/placeholder-service.js";

b. Inside chatFileRoutes, instantiate:

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:

// 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:

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 });
});
  1. 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 <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> 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

<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>
After completion, create `.planning/phases/25-file-system/25-07-SUMMARY.md`