diff --git a/apps/server/agent/file-io.test.ts b/apps/server/agent/file-io.test.ts index 58ebb39..7eb4dc3 100644 --- a/apps/server/agent/file-io.test.ts +++ b/apps/server/agent/file-io.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs'; +import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'crypto'; @@ -116,6 +116,34 @@ describe('writeInputFiles', () => { writeInputFiles({ agentWorkdir: testDir }); expect(existsSync(join(testDir, '.cw', 'input'))).toBe(true); }); + + it('writes context/index.json grouping tasks by phaseId', () => { + writeInputFiles({ + agentWorkdir: testDir, + tasks: [ + { id: 't1', name: 'Task A', phaseId: 'ph1', status: 'pending', category: 'execute', type: 'auto', priority: 'medium' } as Task, + { id: 't2', name: 'Task B', phaseId: 'ph1', status: 'completed', category: 'execute', type: 'auto', priority: 'medium', summary: 'Done' } as Task, + { id: 't3', name: 'Task C', phaseId: 'ph2', status: 'pending', category: 'research', type: 'auto', priority: 'high' } as Task, + ], + }); + + const indexPath = join(testDir, '.cw', 'input', 'context', 'index.json'); + expect(existsSync(indexPath)).toBe(true); + const index = JSON.parse(readFileSync(indexPath, 'utf-8')); + expect(index.tasksByPhase.ph1).toHaveLength(2); + expect(index.tasksByPhase.ph2).toHaveLength(1); + expect(index.tasksByPhase.ph1[0]).toEqual({ + file: 'context/tasks/t1.md', + id: 't1', + name: 'Task A', + status: 'pending', + }); + }); + + it('does not write context/index.json when no tasks', () => { + writeInputFiles({ agentWorkdir: testDir }); + expect(existsSync(join(testDir, '.cw', 'input', 'context', 'index.json'))).toBe(false); + }); }); describe('readSummary', () => { diff --git a/apps/server/agent/file-io.ts b/apps/server/agent/file-io.ts index f51aa41..468e1f8 100644 --- a/apps/server/agent/file-io.ts +++ b/apps/server/agent/file-io.ts @@ -262,6 +262,29 @@ export function writeInputFiles(options: WriteInputFilesOptions): void { } } + // Write context index — groups tasks by phaseId so agents can look up relevant files + // without bulk-reading every context file + if (options.tasks && options.tasks.length > 0) { + const tasksByPhase: Record> = {}; + for (const t of options.tasks) { + const phaseId = t.phaseId ?? '_unassigned'; + if (!tasksByPhase[phaseId]) tasksByPhase[phaseId] = []; + tasksByPhase[phaseId].push({ + file: `context/tasks/${t.id}.md`, + id: t.id, + name: t.name, + status: t.status, + }); + } + const contextDir = join(inputDir, 'context'); + mkdirSync(contextDir, { recursive: true }); + writeFileSync( + join(contextDir, 'index.json'), + JSON.stringify({ tasksByPhase }, null, 2) + '\n', + 'utf-8', + ); + } + // Write manifest listing exactly which files were created writeFileSync( join(inputDir, 'manifest.json'), diff --git a/apps/server/agent/prompts/shared.ts b/apps/server/agent/prompts/shared.ts index 071483d..7309e5c 100644 --- a/apps/server/agent/prompts/shared.ts +++ b/apps/server/agent/prompts/shared.ts @@ -25,12 +25,14 @@ Read \`.cw/input/manifest.json\` first. It contains two arrays: - \`pages/\` — one per page; frontmatter: title, parentPageId, sortOrder; body: markdown **Context Files** (read-only, read on-demand) +- \`context/index.json\` — **read this first** when you need context. Contains \`tasksByPhase\`: a map of phaseId → array of \`{ file, id, name, status }\`. Use it to find relevant task files without bulk-reading. - \`context/phases/\` — frontmatter: id, name, status, dependsOn; body: description - \`context/tasks/\` — frontmatter: id, name, phaseId, parentTaskId, category, type, priority, status, summary; body: description Completed tasks include a \`summary\` field with what the previous agent accomplished. -Context files provide awareness of the broader initiative. There may be dozens — do NOT bulk-read them. -When you need to check a specific phase or task, read that one file. Do not duplicate or contradict context file content in your output. +Context files provide awareness of the broader initiative. There may be dozens — do NOT bulk-read them all. +Use \`context/index.json\` to find which task files belong to a specific phase, then read only those. +Do not duplicate or contradict context file content in your output. `; export const ID_GENERATION = `