fix: Write context/index.json so agents can look up tasks by phase

Agents were bulk-reading all context task files (39 files) because
filenames are opaque IDs and there was no way to find phase-relevant
tasks without reading every file. Now writeInputFiles generates a
context/index.json with tasksByPhase mapping phaseId to task metadata
(file, id, name, status). Prompt updated to direct agents to read
the index first.
This commit is contained in:
Lukas May
2026-03-04 11:55:22 +01:00
parent 26ed9e0395
commit 5d8830d2d3
3 changed files with 56 additions and 3 deletions

View File

@@ -3,7 +3,7 @@
*/ */
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 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 { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
@@ -116,6 +116,34 @@ describe('writeInputFiles', () => {
writeInputFiles({ agentWorkdir: testDir }); writeInputFiles({ agentWorkdir: testDir });
expect(existsSync(join(testDir, '.cw', 'input'))).toBe(true); 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', () => { describe('readSummary', () => {

View File

@@ -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<string, Array<{ file: string; id: string; name: string; status: string }>> = {};
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 // Write manifest listing exactly which files were created
writeFileSync( writeFileSync(
join(inputDir, 'manifest.json'), join(inputDir, 'manifest.json'),

View File

@@ -25,12 +25,14 @@ Read \`.cw/input/manifest.json\` first. It contains two arrays:
- \`pages/\` — one per page; frontmatter: title, parentPageId, sortOrder; body: markdown - \`pages/\` — one per page; frontmatter: title, parentPageId, sortOrder; body: markdown
**Context Files** (read-only, read on-demand) **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/phases/\` — frontmatter: id, name, status, dependsOn; body: description
- \`context/tasks/\` — frontmatter: id, name, phaseId, parentTaskId, category, type, priority, status, summary; 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. 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. Context files provide awareness of the broader initiative. There may be dozens — do NOT bulk-read them all.
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. 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.
</input_files>`; </input_files>`;
export const ID_GENERATION = ` export const ID_GENERATION = `