273 lines
10 KiB
Markdown
273 lines
10 KiB
Markdown
---
|
|
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>
|