From 9ca039eb99f8a94c4e58dce8070e6b00b75331a8 Mon Sep 17 00:00:00 2001 From: Bruno da Cunha Date: Fri, 20 Feb 2026 01:25:01 -0300 Subject: [PATCH 1/2] fill: process skills before agents --- src/services/ai/tools/fillScaffoldingTool.ts | 42 ++++++++++++++-- src/services/fill/fillService.test.ts | 51 ++++++++++++++++++++ src/services/fill/fillService.ts | 13 +++-- src/services/mcp/gateway/types.ts | 2 +- src/services/mcp/mcpServer.ts | 2 +- 5 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/services/ai/tools/fillScaffoldingTool.ts b/src/services/ai/tools/fillScaffoldingTool.ts index 591566eb..c2a28870 100644 --- a/src/services/ai/tools/fillScaffoldingTool.ts +++ b/src/services/ai/tools/fillScaffoldingTool.ts @@ -1,6 +1,7 @@ import { tool } from 'ai'; import * as path from 'path'; import * as fs from 'fs-extra'; +import { glob } from 'glob'; import { z } from 'zod'; import { SemanticContextBuilder } from '../../semantic/contextBuilder'; import { DEFAULT_EXCLUDE_PATTERNS } from '../../semantic/types'; @@ -68,7 +69,7 @@ export async function cleanupSharedContext(): Promise { const ListFilesToFillInputSchema = z.object({ repoPath: z.string().describe('Repository path'), outputDir: z.string().optional().describe('Scaffold directory (default: ./.context)'), - target: z.enum(['docs', 'agents', 'plans', 'all']).default('all').optional() + target: z.enum(['docs', 'skills', 'agents', 'plans', 'all']).default('all').optional() .describe('Which scaffolding to list') }); @@ -77,7 +78,7 @@ export type ListFilesToFillInput = z.infer; interface FileToFillInfo { path: string; relativePath: string; - type: 'doc' | 'agent' | 'plan'; + type: 'doc' | 'skill' | 'agent' | 'plan'; } export const listFilesToFillTool = tool({ @@ -118,6 +119,23 @@ Use this first to get the list, then call fillSingleFile for each file.`, } } + // Collect skills + if (target === 'all' || target === 'skills') { + const skillsDir = path.join(outputDir, 'skills'); + if (await fs.pathExists(skillsDir)) { + const skillFiles = await glob('**/*.md', { cwd: skillsDir, absolute: true }); + for (const skillFile of skillFiles) { + if (path.basename(skillFile).toLowerCase() === 'readme.md') continue; + const skillRelativePath = path.relative(skillsDir, skillFile).replace(/\\/g, '/'); + files.push({ + path: skillFile, + relativePath: `skills/${skillRelativePath}`, + type: 'skill' + }); + } + } + } + // Collect agents if (target === 'all' || target === 'agents') { const agentsDir = path.join(outputDir, 'agents'); @@ -256,7 +274,7 @@ Use this context to generate intelligent content, then write the content to the const FillScaffoldingInputSchema = z.object({ repoPath: z.string().describe('Repository path'), outputDir: z.string().optional().describe('Scaffold directory (default: ./.context)'), - target: z.enum(['docs', 'agents', 'plans', 'all']).default('all').optional() + target: z.enum(['docs', 'skills', 'agents', 'plans', 'all']).default('all').optional() .describe('Which scaffolding to fill'), offset: z.number().optional().describe('Skip first N files (for pagination)'), limit: z.number().optional().describe('Max files to return (default: 3, use 0 for all)') @@ -312,7 +330,7 @@ Supports pagination with offset/limit. Generate content for each file using its const semanticContext = await getOrBuildContext(resolvedRepoPath); // Collect all file paths first - const allFiles: { path: string; relativePath: string; type: 'doc' | 'agent' | 'plan' }[] = []; + const allFiles: { path: string; relativePath: string; type: 'doc' | 'skill' | 'agent' | 'plan' }[] = []; // Collect docs if (target === 'all' || target === 'docs') { @@ -330,6 +348,22 @@ Supports pagination with offset/limit. Generate content for each file using its } } + // Collect skills + if (target === 'all' || target === 'skills') { + const skillsDir = path.join(outputDir, 'skills'); + if (await fs.pathExists(skillsDir)) { + const skillFiles = await glob('**/*.md', { cwd: skillsDir, absolute: true }); + for (const skillFile of skillFiles) { + if (path.basename(skillFile).toLowerCase() === 'readme.md') continue; + allFiles.push({ + path: skillFile, + relativePath: path.relative(outputDir, skillFile), + type: 'skill' + }); + } + } + } + // Collect agents if (target === 'all' || target === 'agents') { const agentsDir = path.join(outputDir, 'agents'); diff --git a/src/services/fill/fillService.test.ts b/src/services/fill/fillService.test.ts index 80b13190..5d3d3e56 100644 --- a/src/services/fill/fillService.test.ts +++ b/src/services/fill/fillService.test.ts @@ -158,6 +158,18 @@ describe('FillService', () => { expect(mockUI.displaySuccess).toHaveBeenCalled(); }); + it('should work when only skills directory exists', async () => { + await fs.ensureDir(path.join(outputDir, 'skills')); + await fs.writeFile( + path.join(outputDir, 'skills', 'commit-message.md'), + '# Commit Message' + ); + + await service.run(tempDir, { output: outputDir }); + + expect(mockUI.displaySuccess).toHaveBeenCalled(); + }); + it('should display warning when no target files exist', async () => { // Create empty dirs await fs.ensureDir(path.join(outputDir, 'docs')); @@ -212,6 +224,45 @@ describe('FillService', () => { expect(content).toContain('# Updated Playbook'); }); + it('should process skills before agents', async () => { + await fs.ensureDir(path.join(outputDir, 'docs')); + await fs.ensureDir(path.join(outputDir, 'skills')); + await fs.ensureDir(path.join(outputDir, 'agents')); + + await fs.writeFile( + path.join(outputDir, 'docs', 'architecture.md'), + '# Architecture\n\nTODO: fill in' + ); + await fs.writeFile( + path.join(outputDir, 'skills', 'commit-message.md'), + '# Commit Message\n\nTODO: fill in' + ); + await fs.writeFile( + path.join(outputDir, 'agents', 'code-reviewer.md'), + '# Code Reviewer\n\nTODO: fill in' + ); + + await service.run(tempDir, { output: outputDir }); + + const { DocumentationAgent } = require('../ai/agents/documentationAgent'); + const { PlaybookAgent } = require('../ai/agents/playbookAgent'); + const documentationTargets = DocumentationAgent.mock.results.flatMap((result: any) => + result.value.generateDocumentation.mock.calls.map((call: any[]) => call[0].targetFile) + ); + expect(documentationTargets).toEqual([ + path.join('docs', 'architecture.md'), + path.join('skills', 'commit-message.md') + ]); + + const documentationCallOrders = DocumentationAgent.mock.results + .flatMap((result: any) => result.value.generateDocumentation.mock.invocationCallOrder); + const playbookCallOrders = PlaybookAgent.mock.results + .flatMap((result: any) => result.value.generatePlaybook.mock.invocationCallOrder); + const lastDocumentationCallOrder = Math.max(...documentationCallOrders); + const firstPlaybookCallOrder = Math.min(...playbookCallOrders); + expect(lastDocumentationCallOrder).toBeLessThan(firstPlaybookCallOrder); + }); + it('should respect limit option', async () => { // Create dirs and multiple files await fs.ensureDir(path.join(outputDir, 'docs')); diff --git a/src/services/fill/fillService.ts b/src/services/fill/fillService.ts index 9c562bfe..1fbd027b 100644 --- a/src/services/fill/fillService.ts +++ b/src/services/fill/fillService.ts @@ -53,6 +53,7 @@ interface ResolvedFillOptions { repoPath: string; outputDir: string; docsDir: string; + skillsDir: string; agentsDir: string; include?: string[]; exclude?: string[]; @@ -110,13 +111,15 @@ export class FillService { const resolvedRepo = path.resolve(repoPath); const outputDir = path.resolve(rawOptions.output || './.context'); const docsDir = path.join(outputDir, 'docs'); + const skillsDir = path.join(outputDir, 'skills'); const agentsDir = path.join(outputDir, 'agents'); - // At least one of docs or agents must exist + // At least one fillable scaffold directory must exist const docsExists = await fs.pathExists(docsDir); + const skillsExists = await fs.pathExists(skillsDir); const agentsExists = await fs.pathExists(agentsDir); - if (!docsExists && !agentsExists) { + if (!docsExists && !skillsExists && !agentsExists) { throw new Error(this.t('errors.fill.missingScaffold')); } @@ -144,6 +147,7 @@ export class FillService { repoPath: resolvedRepo, outputDir, docsDir, + skillsDir, agentsDir, include: rawOptions.include, exclude: rawOptions.exclude, @@ -350,10 +354,13 @@ export class FillService { const docFiles = (await fs.pathExists(options.docsDir)) ? await glob('**/*.md', { cwd: options.docsDir, absolute: true }) : []; + const skillFiles = (await fs.pathExists(options.skillsDir)) + ? await glob('**/*.md', { cwd: options.skillsDir, absolute: true }) + : []; const agentFiles = (await fs.pathExists(options.agentsDir)) ? await glob('**/*.md', { cwd: options.agentsDir, absolute: true }) : []; - const candidates = [...docFiles, ...agentFiles]; + const candidates = [...docFiles, ...skillFiles, ...agentFiles]; const targets: TargetFile[] = []; diff --git a/src/services/mcp/gateway/types.ts b/src/services/mcp/gateway/types.ts index b4a067d6..23d0ed45 100644 --- a/src/services/mcp/gateway/types.ts +++ b/src/services/mcp/gateway/types.ts @@ -46,7 +46,7 @@ export interface ContextParams { exclude?: string[]; autoFill?: boolean; skipContentGeneration?: boolean; - target?: 'docs' | 'agents' | 'plans' | 'all'; + target?: 'docs' | 'skills' | 'agents' | 'plans' | 'all'; offset?: number; limit?: number; filePath?: string; diff --git a/src/services/mcp/mcpServer.ts b/src/services/mcp/mcpServer.ts index 9b338708..f1bcceaf 100644 --- a/src/services/mcp/mcpServer.ts +++ b/src/services/mcp/mcpServer.ts @@ -180,7 +180,7 @@ export class AIContextMCPServer { .describe('(init, scaffoldPlan) Auto-fill with codebase content'), skipContentGeneration: z.boolean().optional() .describe('(init) Skip pre-generating content'), - target: z.enum(['docs', 'agents', 'plans', 'all']).optional() + target: z.enum(['docs', 'skills', 'agents', 'plans', 'all']).optional() .describe('(fill, listToFill) Which scaffolding to target'), offset: z.number().optional() .describe('(fill) Skip first N files'), From b004e5dfbb2842687cc02454ebacb17ccf59636d Mon Sep 17 00:00:00 2001 From: Bruno da Cunha Date: Fri, 20 Feb 2026 11:33:02 -0300 Subject: [PATCH 2/2] feat(mcp): enforce docs->skills->agents fill order with warnings --- src/prompts/defaults.ts | 17 +- src/services/ai/schemas.ts | 6 +- src/services/ai/toolRegistry.ts | 7 +- .../ai/tools/fillScaffoldingTool.test.ts | 127 +++++++ src/services/ai/tools/fillScaffoldingTool.ts | 325 ++++++++++-------- .../ai/tools/initializeContextTool.test.ts | 54 +++ .../ai/tools/initializeContextTool.ts | 84 ++++- src/services/mcp/README.md | 4 +- src/services/mcp/gateway/context.ts | 5 +- src/services/mcp/gateway/response.ts | 8 +- src/services/mcp/mcpServer.ts | 2 +- 11 files changed, 468 insertions(+), 171 deletions(-) create mode 100644 src/services/ai/tools/fillScaffoldingTool.test.ts create mode 100644 src/services/ai/tools/initializeContextTool.test.ts diff --git a/src/prompts/defaults.ts b/src/prompts/defaults.ts index 819d40e0..f8a922ec 100644 --- a/src/prompts/defaults.ts +++ b/src/prompts/defaults.ts @@ -1,7 +1,7 @@ -export const UPDATE_SCAFFOLD_PROMPT_FALLBACK = `# Prompt: Update Repository Documentation and Agent Playbooks +export const UPDATE_SCAFFOLD_PROMPT_FALLBACK = `# Prompt: Update Repository Docs, Skills, and Agent Playbooks ## Purpose -You are an AI assistant responsible for refreshing the documentation (\`docs/\`) and agent playbooks (\`agents/\`). Your goal is to bring every guide up to date with the latest repository state and maintain cross-references between docs and agent instructions. +You are an AI assistant responsible for refreshing documentation (\`docs/\`), skills (\`skills/\`), and agent playbooks (\`agents/\`). Your goal is to bring every guide up to date with the latest repository state and maintain cross-references. ## Context Gathering 1. Review the repository structure and recent changes. @@ -9,17 +9,26 @@ You are an AI assistant responsible for refreshing the documentation (\`docs/\`) 3. Check \`docs/README.md\` for the current document map. ## Update Procedure -1. **Update Documentation** +1. **Update Documentation (docs/)** - Replace TODO placeholders with accurate, current information. - Verify that links between docs remain valid. - If you add new guides or sections, update \`docs/README.md\`. -2. **Agent Playbook Alignment** +2. **Update Skills (skills/)** + - Ensure each skill has codebase-specific activation rules and steps. + - Add concrete command/examples using repository conventions. + - Keep scope narrow and task-driven per skill. + +3. **Agent Playbook Alignment (agents/)** - For each change in \`docs/\`, adjust the related \`agents/*.md\` playbooks. - Update responsibilities, best practices, and documentation touchpoints. +4. **Required Fill Order** + - Fill files in this exact order: \`docs -> skills -> agents\`. + ## Acceptance Criteria - No unresolved TODO placeholders remain unless they require explicit human input. +- Skills contain concrete, codebase-specific instructions. - Agent playbooks list accurate responsibilities and best practices. - Changes are self-contained, well-formatted Markdown. diff --git a/src/services/ai/schemas.ts b/src/services/ai/schemas.ts index f2365e7a..62ac996a 100644 --- a/src/services/ai/schemas.ts +++ b/src/services/ai/schemas.ts @@ -271,7 +271,7 @@ export const RequiredActionSchema = z.object({ order: z.number().describe('Sequence order for this action'), actionType: z.enum(['WRITE_FILE', 'CALL_TOOL', 'VERIFY']).describe('Type of action to perform'), filePath: z.string().describe('Absolute path to the file'), - fileType: z.enum(['doc', 'agent']).describe('Type of scaffold file'), + fileType: z.enum(['doc', 'skill', 'agent']).describe('Type of scaffold file'), instructions: z.string().describe('Instructions for filling this file'), suggestedContent: z.string().optional().describe('Pre-generated content to write to the file'), status: ActionStatusEnum.describe('Current status of this action'), @@ -357,11 +357,11 @@ export const InitializeContextOutputSchema = z.object({ // Fill instructions (the UPDATE_SCAFFOLD_PROMPT) fillInstructions: z.string().optional() - .describe('Standard prompt with guidelines for HOW to fill the scaffolded files'), + .describe('Standard prompt with guidelines for HOW to fill the scaffolded files (order: docs -> skills -> agents)'), // Pending writes (renamed from requiredActions for clarity) pendingWrites: z.array(RequiredActionSchema).optional() - .describe('Files that MUST be written. Each has content ready to write.'), + .describe('Files that MUST be written in order. Each has content ready to write.'), // Legacy: requiredActions (kept for backwards compatibility) requiredActions: z.array(RequiredActionSchema).optional() diff --git a/src/services/ai/toolRegistry.ts b/src/services/ai/toolRegistry.ts index a32bed40..21fc0bc2 100644 --- a/src/services/ai/toolRegistry.ts +++ b/src/services/ai/toolRegistry.ts @@ -84,10 +84,11 @@ WORKFLOW: 1. Call initializeContext - scaffolding is created 2. Response has status: "incomplete" with pendingWrites[] array 3. Read the fillInstructions field - it contains guidelines for HOW to fill -4. For EACH file in pendingWrites: +4. Fill in REQUIRED ORDER: docs -> skills -> agents +5. For EACH file in pendingWrites: - Call fillSingleFile({ repoPath, filePath }) to get AI-generated content - Call Write({ file_path, content: suggestedContent }) to save -5. ONLY after ALL writes succeed → report "initialization complete" +6. ONLY after ALL writes succeed → report "initialization complete" IMPORTANT: - status: "incomplete" means you MUST complete the pendingWrites @@ -121,6 +122,7 @@ IMPORTANT: After calling this, write each suggestedContent to its corresponding description: 'List scaffold files that need to be filled. Returns only file paths (no content).', extendedDescription: `List scaffold files that need to be filled. Returns only file paths (no content). Use this first to get the list, then call fillSingleFile for each file. +Required fill order is docs -> skills -> agents. This is more efficient than fillScaffolding for large projects.` }, @@ -129,6 +131,7 @@ This is more efficient than fillScaffolding for large projects.` description: 'Generate suggested content for a single scaffold file.', extendedDescription: `Generate suggested content for a single scaffold file. Call listFilesToFill first to get file paths, then call this for each file. +If you choose files out of order, the tool returns a warning with the next required phase. This avoids output size limits by processing one file at a time.` } }; diff --git a/src/services/ai/tools/fillScaffoldingTool.test.ts b/src/services/ai/tools/fillScaffoldingTool.test.ts new file mode 100644 index 00000000..c32f69f3 --- /dev/null +++ b/src/services/ai/tools/fillScaffoldingTool.test.ts @@ -0,0 +1,127 @@ +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; +import { + cleanupSharedContext, + fillSingleFileTool, + listFilesToFillTool, +} from './fillScaffoldingTool'; + +const toolOptions = { toolCallId: 'test-call', messages: [] } as any; + +async function writeUnfilled(filePath: string): Promise { + const content = `--- +status: unfilled +--- + +placeholder +`; + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content, 'utf8'); +} + +async function writeFilled(filePath: string): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, '# Filled\n', 'utf8'); +} + +describe('fillScaffoldingTool ordering', () => { + let tempDir: string; + let contextDir: string; + let agentFilePath: string; + let skillFilePath: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fill-scaffolding-tool-')); + contextDir = path.join(tempDir, '.context'); + agentFilePath = path.join(contextDir, 'agents', 'code-reviewer.md'); + skillFilePath = path.join(contextDir, 'skills', 'commit-message', 'SKILL.md'); + + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile(path.join(tempDir, 'src', 'index.ts'), 'export const value = 42;\n'); + + await writeUnfilled(path.join(contextDir, 'docs', 'project-overview.md')); + await writeFilled(path.join(contextDir, 'docs', 'README.md')); + await writeUnfilled(skillFilePath); + await writeUnfilled(agentFilePath); + }); + + afterEach(async () => { + await cleanupSharedContext(); + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('lists only unfilled files and keeps docs -> skills -> agents order', async () => { + const result = await listFilesToFillTool.execute!({ + repoPath: tempDir, + target: 'all', + }, toolOptions); + + const typedResult = result as { + success: boolean; + totalCount: number; + files: Array<{ type: 'doc' | 'skill' | 'agent' | 'plan'; relativePath: string }>; + }; + + expect(typedResult.success).toBe(true); + expect(typedResult.totalCount).toBe(3); + expect(typedResult.files.map((f) => f.type)).toEqual(['doc', 'skill', 'agent']); + expect(typedResult.files.some((f) => f.relativePath.toLowerCase() === 'docs/readme.md')).toBe(false); + }); + + it('warns when requesting agents while docs are still pending', async () => { + const result = await listFilesToFillTool.execute!({ + repoPath: tempDir, + target: 'agents', + }, toolOptions); + + const typedResult = result as { + warning?: string; + nextRequiredPhase?: string; + files: Array<{ type: string }>; + }; + + expect(typedResult.files).toHaveLength(1); + expect(typedResult.files[0]?.type).toBe('agent'); + expect(typedResult.warning).toContain('Fill order warning'); + expect(typedResult.nextRequiredPhase).toBe('docs'); + }); + + it('detects skill files and includes warning when filling out of order', async () => { + const skillResult = await fillSingleFileTool.execute!({ + repoPath: tempDir, + filePath: skillFilePath, + }, toolOptions); + + const typedSkillResult = skillResult as { + success: boolean; + fileType: string; + documentName: string; + warning?: string; + nextRequiredPhase?: string; + }; + + expect(typedSkillResult.success).toBe(true); + expect(typedSkillResult.fileType).toBe('skill'); + expect(typedSkillResult.documentName).toBe('commit-message'); + expect(typedSkillResult.warning).toContain('Fill order warning'); + expect(typedSkillResult.nextRequiredPhase).toBe('docs'); + + const agentResult = await fillSingleFileTool.execute!({ + repoPath: tempDir, + filePath: agentFilePath, + }, toolOptions); + + const typedAgentResult = agentResult as { + success: boolean; + fileType: string; + warning?: string; + nextRequiredPhase?: string; + }; + + expect(typedAgentResult.success).toBe(true); + expect(typedAgentResult.fileType).toBe('agent'); + expect(typedAgentResult.warning).toContain('Fill order warning'); + expect(typedAgentResult.nextRequiredPhase).toBe('docs'); + }); +}); diff --git a/src/services/ai/tools/fillScaffoldingTool.ts b/src/services/ai/tools/fillScaffoldingTool.ts index c2a28870..b42e3f41 100644 --- a/src/services/ai/tools/fillScaffoldingTool.ts +++ b/src/services/ai/tools/fillScaffoldingTool.ts @@ -6,11 +6,75 @@ import { z } from 'zod'; import { SemanticContextBuilder } from '../../semantic/contextBuilder'; import { DEFAULT_EXCLUDE_PATTERNS } from '../../semantic/types'; import { getScaffoldStructure, serializeStructureForAI } from '../../../generators/shared/scaffoldStructures'; +import { needsFill } from '../../../utils/frontMatter'; // Shared context builder instance for efficiency let sharedContextBuilder: SemanticContextBuilder | null = null; let cachedContext: { repoPath: string; context: string } | null = null; +type FillTarget = 'docs' | 'skills' | 'agents' | 'plans' | 'all'; +type FillPhase = 'docs' | 'skills' | 'agents'; + +const REQUIRED_FILL_ORDER: FillPhase[] = ['docs', 'skills', 'agents']; + +const FILE_TYPE_ORDER: Record = { + doc: 1, + skill: 2, + agent: 3, + plan: 4, +}; + +function isTargetMatch(fileType: FileToFillInfo['type'], target: FillTarget): boolean { + if (target === 'all') return true; + if (target === 'docs') return fileType === 'doc'; + if (target === 'skills') return fileType === 'skill'; + if (target === 'agents') return fileType === 'agent'; + return fileType === 'plan'; +} + +function sortByFillOrder(files: T[]): T[] { + return [...files].sort((a, b) => { + const typeDiff = FILE_TYPE_ORDER[a.type] - FILE_TYPE_ORDER[b.type]; + if (typeDiff !== 0) return typeDiff; + return a.relativePath.localeCompare(b.relativePath); + }); +} + +function buildPendingByPhase(files: FileToFillInfo[]): Record { + return { + docs: files.filter((f) => f.type === 'doc').length, + skills: files.filter((f) => f.type === 'skill').length, + agents: files.filter((f) => f.type === 'agent').length, + }; +} + +function getOrderWarning( + target: FillTarget, + pendingByPhase: Record +): { warning?: string; nextRequiredPhase?: FillPhase } { + if (target === 'all' || target === 'docs' || target === 'plans') { + return {}; + } + + const phaseIndex = REQUIRED_FILL_ORDER.indexOf(target as FillPhase); + if (phaseIndex <= 0) { + return {}; + } + + for (let i = 0; i < phaseIndex; i++) { + const phase = REQUIRED_FILL_ORDER[i]; + const pending = pendingByPhase[phase]; + if (pending > 0) { + return { + warning: `Fill order warning: ${pending} ${phase} file(s) are still unfilled. Required order is docs -> skills -> agents.`, + nextRequiredPhase: phase, + }; + } + } + + return {}; +} + /** * Get or build semantic context for a repository. * Caches the context for efficiency when processing multiple files. @@ -81,9 +145,76 @@ interface FileToFillInfo { type: 'doc' | 'skill' | 'agent' | 'plan'; } +async function collectAllPendingFiles(outputDir: string): Promise { + const files: FileToFillInfo[] = []; + + const docsDir = path.join(outputDir, 'docs'); + if (await fs.pathExists(docsDir)) { + const docFiles = await fs.readdir(docsDir); + for (const file of docFiles) { + if (!file.endsWith('.md')) continue; + if (file.toLowerCase() === 'readme.md') continue; + const filePath = path.join(docsDir, file); + if (!await needsFill(filePath)) continue; + files.push({ + path: filePath, + relativePath: `docs/${file}`, + type: 'doc' + }); + } + } + + const skillsDir = path.join(outputDir, 'skills'); + if (await fs.pathExists(skillsDir)) { + const skillFiles = await glob('**/*.md', { cwd: skillsDir, absolute: true }); + for (const skillFile of skillFiles) { + if (path.basename(skillFile).toLowerCase() === 'readme.md') continue; + if (!await needsFill(skillFile)) continue; + const skillRelativePath = path.relative(skillsDir, skillFile).replace(/\\/g, '/'); + files.push({ + path: skillFile, + relativePath: `skills/${skillRelativePath}`, + type: 'skill' + }); + } + } + + const agentsDir = path.join(outputDir, 'agents'); + if (await fs.pathExists(agentsDir)) { + const agentFiles = await fs.readdir(agentsDir); + for (const file of agentFiles) { + if (!file.endsWith('.md') || file.toLowerCase() === 'readme.md') continue; + const filePath = path.join(agentsDir, file); + if (!await needsFill(filePath)) continue; + files.push({ + path: filePath, + relativePath: `agents/${file}`, + type: 'agent' + }); + } + } + + const plansDir = path.join(outputDir, 'plans'); + if (await fs.pathExists(plansDir)) { + const planFiles = await fs.readdir(plansDir); + for (const file of planFiles) { + if (!file.endsWith('.md') || file.toLowerCase() === 'readme.md') continue; + const filePath = path.join(plansDir, file); + if (!await needsFill(filePath)) continue; + files.push({ + path: filePath, + relativePath: `plans/${file}`, + type: 'plan' + }); + } + } + + return sortByFillOrder(files); +} + export const listFilesToFillTool = tool({ description: `List scaffold files that need to be filled. Returns only file paths (no content) for efficient listing. -Use this first to get the list, then call fillSingleFile for each file.`, +Use this first to get the list, then call fillSingleFile for each file in order docs -> skills -> agents.`, inputSchema: ListFilesToFillInputSchema, execute: async (input: ListFilesToFillInput) => { const { repoPath, outputDir: customOutputDir, target = 'all' } = input; @@ -101,78 +232,20 @@ Use this first to get the list, then call fillSingleFile for each file.`, }; } - const files: FileToFillInfo[] = []; - - // Collect docs - if (target === 'all' || target === 'docs') { - const docsDir = path.join(outputDir, 'docs'); - if (await fs.pathExists(docsDir)) { - const docFiles = await fs.readdir(docsDir); - for (const file of docFiles) { - if (!file.endsWith('.md')) continue; - files.push({ - path: path.join(docsDir, file), - relativePath: `docs/${file}`, - type: 'doc' - }); - } - } - } - - // Collect skills - if (target === 'all' || target === 'skills') { - const skillsDir = path.join(outputDir, 'skills'); - if (await fs.pathExists(skillsDir)) { - const skillFiles = await glob('**/*.md', { cwd: skillsDir, absolute: true }); - for (const skillFile of skillFiles) { - if (path.basename(skillFile).toLowerCase() === 'readme.md') continue; - const skillRelativePath = path.relative(skillsDir, skillFile).replace(/\\/g, '/'); - files.push({ - path: skillFile, - relativePath: `skills/${skillRelativePath}`, - type: 'skill' - }); - } - } - } - - // Collect agents - if (target === 'all' || target === 'agents') { - const agentsDir = path.join(outputDir, 'agents'); - if (await fs.pathExists(agentsDir)) { - const agentFiles = await fs.readdir(agentsDir); - for (const file of agentFiles) { - if (!file.endsWith('.md') || file.toLowerCase() === 'readme.md') continue; - files.push({ - path: path.join(agentsDir, file), - relativePath: `agents/${file}`, - type: 'agent' - }); - } - } - } - - // Collect plans - if (target === 'all' || target === 'plans') { - const plansDir = path.join(outputDir, 'plans'); - if (await fs.pathExists(plansDir)) { - const planFiles = await fs.readdir(plansDir); - for (const file of planFiles) { - if (!file.endsWith('.md') || file.toLowerCase() === 'readme.md') continue; - files.push({ - path: path.join(plansDir, file), - relativePath: `plans/${file}`, - type: 'plan' - }); - } - } - } + const allPendingFiles = await collectAllPendingFiles(outputDir); + const files = allPendingFiles.filter((file) => isTargetMatch(file.type, target)); + const pendingByPhase = buildPendingByPhase(allPendingFiles); + const { warning, nextRequiredPhase } = getOrderWarning(target, pendingByPhase); + const targetLabel = target === 'all' ? 'all requested' : target; + const orderHint = 'Required fill order: docs -> skills -> agents.'; return { success: true, files, totalCount: files.length, - instructions: `Found ${files.length} files to fill. Call fillSingleFile for each file path to get suggested content.` + pendingByPhase, + ...(warning && { warning, nextRequiredPhase }), + instructions: `Found ${files.length} unfilled file(s) for ${targetLabel}. ${orderHint} Call fillSingleFile for each file path to get semantic context and scaffold guidance, then generate and write the content.` }; } catch (error) { return { @@ -197,7 +270,7 @@ export type FillSingleFileInput = z.infer; export const fillSingleFileTool = tool({ description: `Get context and structure guidance for filling a single scaffold file. Returns semantic context (codebase analysis) and scaffold structure (section guidance, tone, audience). -Use this context to generate intelligent content, then write the content to the file path.`, +Use this context to generate intelligent content, then write the content to the file path. If the file is out of order, returns a warning.`, inputSchema: FillSingleFileInputSchema, execute: async (input: FillSingleFileInput) => { const { repoPath, filePath } = input; @@ -219,24 +292,25 @@ Use this context to generate intelligent content, then write the content to the // Read current content (frontmatter/template) const currentContent = await fs.readFile(resolvedFilePath, 'utf-8'); const fileName = path.basename(resolvedFilePath); - const parentDir = path.basename(path.dirname(resolvedFilePath)); + const normalizedFilePath = resolvedFilePath.replace(/\\/g, '/'); + const outputDir = path.resolve(resolvedRepoPath, '.context'); // Determine file type and get scaffold structure let fileType: 'doc' | 'agent' | 'plan' | 'skill'; let documentName: string; - if (parentDir === 'docs') { + if (normalizedFilePath.includes('/.context/docs/')) { fileType = 'doc'; documentName = path.basename(fileName, '.md'); - } else if (parentDir === 'agents') { + } else if (normalizedFilePath.includes('/.context/agents/')) { fileType = 'agent'; documentName = path.basename(fileName, '.md'); - } else if (parentDir === 'plans') { + } else if (normalizedFilePath.includes('/.context/plans/')) { fileType = 'plan'; documentName = path.basename(fileName, '.md'); - } else if (parentDir === 'skills') { + } else if (normalizedFilePath.includes('/.context/skills/')) { fileType = 'skill'; - documentName = path.basename(fileName, '.md'); + documentName = path.basename(path.dirname(resolvedFilePath)); } else { fileType = 'doc'; documentName = path.basename(fileName, '.md'); @@ -245,12 +319,21 @@ Use this context to generate intelligent content, then write the content to the // Get scaffold structure and serialize for AI const structure = getScaffoldStructure(documentName); const scaffoldStructure = structure ? serializeStructureForAI(structure) : undefined; + const allPendingFiles = await collectAllPendingFiles(outputDir); + const pendingByPhase = buildPendingByPhase(allPendingFiles); + const targetForWarning: FillTarget = + fileType === 'doc' ? 'docs' : + fileType === 'skill' ? 'skills' : + fileType === 'agent' ? 'agents' : 'plans'; + const { warning, nextRequiredPhase } = getOrderWarning(targetForWarning, pendingByPhase); return { success: true, filePath: resolvedFilePath, fileType, documentName, + pendingByPhase, + ...(warning && { warning, nextRequiredPhase }), // Context for intelligent generation semanticContext, scaffoldStructure, @@ -294,7 +377,7 @@ interface FileToFill { export const fillScaffoldingTool = tool({ description: `Get context and structure guidance for filling multiple scaffold files. Returns semantic context (shared) and scaffold structure per file for intelligent content generation. -Supports pagination with offset/limit. Generate content for each file using its scaffoldStructure, then write to its path.`, +Supports pagination with offset/limit. Generate content for each file using its scaffoldStructure, then write to its path in order docs -> skills -> agents.`, inputSchema: FillScaffoldingInputSchema, execute: async (input: FillScaffoldingInput) => { const { @@ -329,84 +412,28 @@ Supports pagination with offset/limit. Generate content for each file using its // Get or build semantic context (cached for efficiency) const semanticContext = await getOrBuildContext(resolvedRepoPath); - // Collect all file paths first - const allFiles: { path: string; relativePath: string; type: 'doc' | 'skill' | 'agent' | 'plan' }[] = []; - - // Collect docs - if (target === 'all' || target === 'docs') { - const docsDir = path.join(outputDir, 'docs'); - if (await fs.pathExists(docsDir)) { - const docFiles = await fs.readdir(docsDir); - for (const file of docFiles) { - if (!file.endsWith('.md')) continue; - allFiles.push({ - path: path.join(docsDir, file), - relativePath: path.relative(outputDir, path.join(docsDir, file)), - type: 'doc' - }); - } - } - } - - // Collect skills - if (target === 'all' || target === 'skills') { - const skillsDir = path.join(outputDir, 'skills'); - if (await fs.pathExists(skillsDir)) { - const skillFiles = await glob('**/*.md', { cwd: skillsDir, absolute: true }); - for (const skillFile of skillFiles) { - if (path.basename(skillFile).toLowerCase() === 'readme.md') continue; - allFiles.push({ - path: skillFile, - relativePath: path.relative(outputDir, skillFile), - type: 'skill' - }); - } - } - } - - // Collect agents - if (target === 'all' || target === 'agents') { - const agentsDir = path.join(outputDir, 'agents'); - if (await fs.pathExists(agentsDir)) { - const agentFiles = await fs.readdir(agentsDir); - for (const file of agentFiles) { - if (!file.endsWith('.md') || file.toLowerCase() === 'readme.md') continue; - allFiles.push({ - path: path.join(agentsDir, file), - relativePath: path.relative(outputDir, path.join(agentsDir, file)), - type: 'agent' - }); - } - } - } - - // Collect plans - if (target === 'all' || target === 'plans') { - const plansDir = path.join(outputDir, 'plans'); - if (await fs.pathExists(plansDir)) { - const planFiles = await fs.readdir(plansDir); - for (const file of planFiles) { - if (!file.endsWith('.md') || file.toLowerCase() === 'readme.md') continue; - allFiles.push({ - path: path.join(plansDir, file), - relativePath: path.relative(outputDir, path.join(plansDir, file)), - type: 'plan' - }); - } - } - } - - const totalCount = allFiles.length; + const allPendingFiles = await collectAllPendingFiles(outputDir); + const targetPendingFiles = allPendingFiles + .filter((file) => isTargetMatch(file.type, target)) + .map((file) => ({ + ...file, + relativePath: file.relativePath.replace(/\\/g, '/'), + })); + const pendingByPhase = buildPendingByPhase(allPendingFiles); + const { warning, nextRequiredPhase } = getOrderWarning(target, pendingByPhase); + const totalCount = targetPendingFiles.length; // Apply pagination (limit=0 means all files) const effectiveLimit = limit === 0 ? totalCount : limit; - const paginatedFiles = allFiles.slice(offset, offset + effectiveLimit); + const paginatedFiles = targetPendingFiles.slice(offset, offset + effectiveLimit); // Build context info for paginated files const filesToFill: FileToFill[] = []; for (const fileInfo of paginatedFiles) { const currentContent = await fs.readFile(fileInfo.path, 'utf-8'); - const documentName = path.basename(fileInfo.path, '.md'); + const documentName = fileInfo.type === 'skill' + ? path.basename(path.dirname(fileInfo.path)) + : path.basename(fileInfo.path, '.md'); // Get scaffold structure for this file const structure = getScaffoldStructure(documentName); @@ -429,6 +456,8 @@ Supports pagination with offset/limit. Generate content for each file using its // Shared semantic context for all files semanticContext, filesToFill, + pendingByPhase, + ...(warning && { warning, nextRequiredPhase }), pagination: { offset, limit: effectiveLimit, @@ -437,8 +466,8 @@ Supports pagination with offset/limit. Generate content for each file using its hasMore }, instructions: hasMore - ? `Returned ${filesToFill.length} of ${totalCount} files. Generate content for each file using semanticContext + its scaffoldStructure. Call again with offset=${offset + paginatedFiles.length} to continue.` - : `All ${totalCount} files ready. Generate content for each file using semanticContext + its scaffoldStructure. Write each generated content to its file path.` + ? `Returned ${filesToFill.length} of ${totalCount} unfilled file(s). Required fill order: docs -> skills -> agents. Generate content for each file using semanticContext + its scaffoldStructure. Call again with offset=${offset + paginatedFiles.length} to continue.` + : `All ${totalCount} unfilled file(s) for this target are ready. Required fill order: docs -> skills -> agents. Generate content for each file using semanticContext + its scaffoldStructure. Write each generated content to its file path.` }; } catch (error) { return { diff --git a/src/services/ai/tools/initializeContextTool.test.ts b/src/services/ai/tools/initializeContextTool.test.ts new file mode 100644 index 00000000..3ded5e10 --- /dev/null +++ b/src/services/ai/tools/initializeContextTool.test.ts @@ -0,0 +1,54 @@ +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; +import { initializeContextTool } from './initializeContextTool'; + +const toolOptions = { toolCallId: 'test-call', messages: [] } as any; + +describe('initializeContextTool', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'initialize-context-tool-')); + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile(path.join(tempDir, 'src', 'index.ts'), 'export const value = 1;\n'); + await fs.writeFile(path.join(tempDir, 'package.json'), '{"name":"fixture","version":"1.0.0"}\n'); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('includes skills in pendingWrites and preserves docs -> skills -> agents order', async () => { + const result = await initializeContextTool.execute!({ + repoPath: tempDir, + type: 'both', + disableFiltering: true, + generateQA: false, + generateSkills: true, + autoFill: true, + skipContentGeneration: true, + }, toolOptions); + + const typedResult = result as { + status: string; + pendingWrites?: Array<{ fileType: 'doc' | 'skill' | 'agent'; filePath: string }>; + }; + + expect(typedResult.status).toBe('incomplete'); + expect(Array.isArray(typedResult.pendingWrites)).toBe(true); + expect(typedResult.pendingWrites && typedResult.pendingWrites.length).toBeGreaterThan(0); + + const pendingWrites = typedResult.pendingWrites || []; + const fileTypes = pendingWrites.map((item) => item.fileType); + + expect(fileTypes).toContain('doc'); + expect(fileTypes).toContain('skill'); + expect(fileTypes).toContain('agent'); + + const rank: Record<'doc' | 'skill' | 'agent', number> = { doc: 1, skill: 2, agent: 3 }; + const ranks = fileTypes.map((fileType) => rank[fileType]); + const sortedRanks = [...ranks].sort((a, b) => a - b); + expect(ranks).toEqual(sortedRanks); + }); +}); diff --git a/src/services/ai/tools/initializeContextTool.ts b/src/services/ai/tools/initializeContextTool.ts index 36d66c19..f76463bd 100644 --- a/src/services/ai/tools/initializeContextTool.ts +++ b/src/services/ai/tools/initializeContextTool.ts @@ -1,6 +1,7 @@ import { tool } from 'ai'; import * as path from 'path'; import * as fs from 'fs-extra'; +import { glob } from 'glob'; import { InitializeContextInputSchema, type InitializeContextInput, type RequiredAction } from '../schemas'; import { FileMapper } from '../../../utils/fileMapper'; import { DocumentationGenerator } from '../../../generators/documentation/documentationGenerator'; @@ -158,7 +159,7 @@ The AI agent MUST then fill each generated file using the provided context and i interface FileInfo { path: string; relativePath: string; - type: 'doc' | 'agent'; + type: 'doc' | 'skill' | 'agent'; fillInstructions: string; } const generatedFiles: FileInfo[] = []; @@ -188,6 +189,36 @@ The AI agent MUST then fill each generated file using the provided context and i }); } } + if (generateSkills) { + const skillsDir = path.join(outputDir, 'skills'); + if (await fs.pathExists(skillsDir)) { + const skillFiles = await glob('**/*.md', { cwd: skillsDir, absolute: true }); + for (const skillFile of skillFiles) { + if (path.basename(skillFile).toLowerCase() === 'readme.md') continue; + const skillRelativePath = path.relative(skillsDir, skillFile).replace(/\\/g, '/'); + const skillSlug = path.basename(path.dirname(skillFile)); + generatedFiles.push({ + path: skillFile, + relativePath: `skills/${skillRelativePath}`, + type: 'skill', + fillInstructions: getSkillFillInstructions(skillSlug), + }); + } + } + } + + generatedFiles.sort((a, b) => { + const phaseOrder: Record = { + doc: 1, + skill: 2, + agent: 3, + }; + + const phaseDiff = phaseOrder[a.type] - phaseOrder[b.type]; + if (phaseDiff !== 0) return phaseDiff; + + return a.relativePath.localeCompare(b.relativePath); + }); // Build requiredActions - always return lightweight info, LLM will use fillSingleFile for context const requiredActions: RequiredAction[] = []; @@ -233,8 +264,10 @@ You MUST fill each file with appropriate content based on the codebase. DO THIS NOW: ${generatedFiles.slice(0, 5).map((f, i) => `${i + 1}. Read and fill: ${f.relativePath}`).join('\n')}${generatedFiles.length > 5 ? `\n... and ${generatedFiles.length - 5} more files` : ''} -Use fillSingleFile tool for each file to get AI-generated content suggestions. -After getting suggestions, write the content using the Write tool. +Follow this required order: docs -> skills -> agents. + +Use fillSingleFile tool for each file to get semantic context and scaffold guidance. +Then generate the markdown content and write it using the Write tool. DO NOT say "initialization complete" until ALL files are filled.` : undefined; @@ -260,7 +293,7 @@ DO NOT say "initialization complete" until ALL files are filled.` // Next step guidance nextStep: hasFilesToFill ? { - action: 'Call fillSingleFile for each file to get content, then Write to save', + action: 'Call fillSingleFile for each file in order docs -> skills -> agents, then Write to save', example: `fillSingleFile({ repoPath: "${resolvedRepoPath}", filePath: "${pendingWrites[0]?.filePath || ''}" })`, } : undefined, @@ -307,6 +340,8 @@ You MUST fill each file with appropriate content based on the codebase. DO THIS NOW: ${requiredActions.slice(0, 5).map((a, i) => `${i + 1}. Call fillSingleFile for: ${a.filePath}`).join('\n')}${requiredActions.length > 5 ? `\n... and ${requiredActions.length - 5} more files` : ''} +Follow this required order: docs -> skills -> agents. + fillSingleFile returns semantic context and scaffold structure for intelligent content generation. After generating content, write it using the Write tool. @@ -323,7 +358,7 @@ DO NOT say "initialization complete" until ALL files are filled.` complete: !hasActionsRequired, operationType: 'initialize_and_fill', completionCriteria: hasActionsRequired - ? 'Call fillSingleFile for each file, generate content using the returned context, then write to file' + ? 'Call fillSingleFile for each file in order docs -> skills -> agents, generate content using the returned context, then write to file' : undefined, // Fill instructions (the standard prompt for HOW to fill) @@ -340,7 +375,7 @@ DO NOT say "initialization complete" until ALL files are filled.` // Explicit next step with example nextStep: hasActionsRequired ? { - action: 'Call fillSingleFile for each file to get context, generate content, then Write to save', + action: 'Call fillSingleFile for each file in order docs -> skills -> agents, generate content, then Write to save', example: `fillSingleFile({ repoPath: "${resolvedRepoPath}", filePath: "${requiredActions[0]?.filePath || ''}" })`, } : undefined, @@ -425,6 +460,43 @@ function getDocFillInstructions(fileName: string): string { return `Fill this documentation file with relevant content based on the codebase analysis. Focus on accuracy and usefulness for developers.`; } +/** + * Get fill instructions for a skill file based on skill slug + */ +function getSkillFillInstructions(skillSlug: string): string { + const slug = skillSlug.toLowerCase(); + + if (slug.includes('code-review') || slug.includes('pr-review')) { + return `Fill this skill with: +- Codebase-specific review checklist +- High-risk areas and anti-patterns to inspect first +- Required evidence for approvals (tests, logs, metrics) +- Standard output format for findings`; + } + + if (slug.includes('test')) { + return `Fill this skill with: +- Test strategy for this repository +- Test pyramid and required coverage areas +- Fixture/mocking patterns used by the project +- Commands and criteria for test validation`; + } + + if (slug.includes('security')) { + return `Fill this skill with: +- Security checks relevant to this stack +- Authentication/authorization validation steps +- Secrets and configuration hardening checks +- Security regression verification workflow`; + } + + return `Fill this skill with: +- Clear trigger conditions for when to use this skill +- Step-by-step workflow tailored to this codebase +- Concrete examples from repository files and commands +- Guardrails, pitfalls, and validation checklist`; +} + /** * Get fill instructions for an agent playbook based on agent type */ diff --git a/src/services/mcp/README.md b/src/services/mcp/README.md index 46ba95f7..f2a4d081 100644 --- a/src/services/mcp/README.md +++ b/src/services/mcp/README.md @@ -48,7 +48,7 @@ Does .context/ folder exist? ``` Do the template files have content? -├─ No → For each file: +├─ No → Fill in order docs -> skills -> agents: │ └─ Use context({ action: "fillSingle", filePath: "path" }) │ ├─ Returns semantic context from codebase │ ├─ Returns scaffold structure with guidance @@ -73,7 +73,7 @@ Do you need structured development? Simplified Flow: context({ action: "init" }) └─ Returns: pendingFiles[] - └─ For each file: + └─ For each file in order docs -> skills -> agents: └─ context({ action: "fillSingle", filePath }) └─ Write enhanced content └─ workflow-init({ name: "feature" }) diff --git a/src/services/mcp/gateway/context.ts b/src/services/mcp/gateway/context.ts index 0c80e699..31e001a9 100644 --- a/src/services/mcp/gateway/context.ts +++ b/src/services/mcp/gateway/context.ts @@ -72,10 +72,11 @@ export async function handleContext( NEXT ACTIONS REQUIRED: 1. Fill scaffold files with content using fillSingle for each pending file + Required order: docs -> skills -> agents 2. Initialize a PREVC workflow to enable structured development WORKFLOW: -Step 1: Use context with action "fillSingle" for each file in pendingFiles array +Step 1: Use context with action "fillSingle" for each file in pendingFiles array, following docs -> skills -> agents Step 2: Use workflow-init with name parameter to create workflow (creates .context/workflow/) Skip workflow-init ONLY if making trivial changes (typos, single-line edits).`; @@ -85,7 +86,7 @@ Skip workflow-init ONLY if making trivial changes (typos, single-line edits).`; pendingFiles: pendingWrites.map(p => p.filePath), enhancementPrompt, nextSteps: [ - 'REQUIRED: Call context with action "fillSingle" for each file in pendingFiles', + 'REQUIRED: Call context with action "fillSingle" for each file in pendingFiles, in order docs -> skills -> agents', 'RECOMMENDED: Call workflow-init with name parameter after filling files', 'OPTIONAL: Skip workflow-init only for trivial changes (typos, single edits)' ], diff --git a/src/services/mcp/gateway/response.ts b/src/services/mcp/gateway/response.ts index db3e4688..aede3599 100644 --- a/src/services/mcp/gateway/response.ts +++ b/src/services/mcp/gateway/response.ts @@ -96,6 +96,7 @@ export function createScaffoldResponse( // Clear next steps nextSteps: customNextSteps || [ 'Call context({ action: "listToFill" }) to get files needing content', + 'Follow required order: docs -> skills -> agents', 'For each file, call context({ action: "fillSingle", filePath: "..." })', 'Generate content based on the semantic context returned', 'Write enhanced content using the Write tool', @@ -132,9 +133,10 @@ Files to enhance: ${filesList}${moreFiles} REQUIRED WORKFLOW: -1. Call context({ action: "fillSingle", filePath: "" }) for each file -2. Use the returned semantic context to generate rich content -3. Write the enhanced content to the file +1. Fill files in order: docs -> skills -> agents +2. Call context({ action: "fillSingle", filePath: "" }) for each file +3. Use the returned semantic context to generate rich content +4. Write the enhanced content to the file ${repoPath ? `Repository: ${repoPath}` : ''} diff --git a/src/services/mcp/mcpServer.ts b/src/services/mcp/mcpServer.ts index f1bcceaf..91030a4a 100644 --- a/src/services/mcp/mcpServer.ts +++ b/src/services/mcp/mcpServer.ts @@ -159,7 +159,7 @@ export class AIContextMCPServer { **Important:** Agents should provide repoPath on the FIRST call, then it will be cached: 1. First call: context({ action: "check", repoPath: "/path/to/project" }) 2. Subsequent calls can omit repoPath - it will use cached value from step 1 -3. After context init, call fillSingle for each pending file +3. After context init, call fillSingle for each pending file in order: docs -> skills -> agents 4. Call workflow-init to enable PREVC workflow (unless trivial change)`, inputSchema: { action: z.enum(['check', 'init', 'fill', 'fillSingle', 'listToFill', 'getMap', 'buildSemantic', 'scaffoldPlan'])