diff --git a/src/commands/artifact-workflow.ts b/src/commands/artifact-workflow.ts index a58c2c00bd..a5dfe8a830 100644 --- a/src/commands/artifact-workflow.ts +++ b/src/commands/artifact-workflow.ts @@ -313,6 +313,8 @@ function printInstructionsText(instructions: ArtifactInstructions, isBlocked: bo outputPath, description, instruction, + context, + rules, template, dependencies, unlocks, @@ -339,9 +341,29 @@ function printInstructionsText(instructions: ArtifactInstructions, isBlocked: bo console.log(''); console.log(); - // Context (dependencies) + // Project context (AI constraint - do not include in output) + if (context) { + console.log(''); + console.log(''); + console.log(context); + console.log(''); + console.log(); + } + + // Rules (AI constraint - do not include in output) + if (rules && rules.length > 0) { + console.log(''); + console.log(''); + for (const rule of rules) { + console.log(`- ${rule}`); + } + console.log(''); + console.log(); + } + + // Dependencies (files to read for context) if (dependencies.length > 0) { - console.log(''); + console.log(''); console.log('Read these files for context before creating this artifact:'); console.log(); for (const dep of dependencies) { @@ -352,7 +374,7 @@ function printInstructionsText(instructions: ArtifactInstructions, isBlocked: bo console.log(` ${dep.description}`); console.log(''); } - console.log(''); + console.log(''); console.log(); } @@ -372,6 +394,7 @@ function printInstructionsText(instructions: ArtifactInstructions, isBlocked: bo // Template console.log(''); console.log(); diff --git a/src/core/artifact-graph/instruction-loader.ts b/src/core/artifact-graph/instruction-loader.ts index 195f403a61..e5852b43dd 100644 --- a/src/core/artifact-graph/instruction-loader.ts +++ b/src/core/artifact-graph/instruction-loader.ts @@ -59,7 +59,11 @@ export interface ArtifactInstructions { description: string; /** Guidance on how to create this artifact (from schema instruction field) */ instruction: string | undefined; - /** Template content (structure to follow) */ + /** Project context from config (constraints/background for AI, not to be included in output) */ + context: string | undefined; + /** Artifact-specific rules from config (constraints for AI, not to be included in output) */ + rules: string[] | undefined; + /** Template content (structure to follow - this IS the output format) */ template: string; /** Dependencies with completion status and paths */ dependencies: DependencyInfo[]; @@ -218,14 +222,11 @@ export function generateInstructions( const dependencies = getDependencyInfo(artifact, context.graph, context.completed); const unlocks = getUnlockedArtifacts(context.graph, artifactId); - // Build enriched template with project config injections - let enrichedTemplate = ''; - let projectConfig = null; - // Use projectRoot from context if not explicitly provided const effectiveProjectRoot = projectRoot ?? context.projectRoot; - // Try to read project config + // Try to read project config for context and rules + let projectConfig = null; if (effectiveProjectRoot) { try { projectConfig = readProjectConfig(effectiveProjectRoot); @@ -252,23 +253,10 @@ export function generateInstructions( } } - // 1. Add context (all artifacts) - if (projectConfig?.context) { - enrichedTemplate += `\n${projectConfig.context}\n\n\n`; - } - - // 2. Add rules (only for matching artifact) + // Extract context and rules as separate fields (not prepended to template) + const configContext = projectConfig?.context?.trim() || undefined; const rulesForArtifact = projectConfig?.rules?.[artifactId]; - if (rulesForArtifact && rulesForArtifact.length > 0) { - enrichedTemplate += `\n`; - for (const rule of rulesForArtifact) { - enrichedTemplate += `- ${rule}\n`; - } - enrichedTemplate += `\n\n`; - } - - // 3. Add original template (without wrapper - CLI handles XML structure) - enrichedTemplate += templateContent; + const configRules = rulesForArtifact && rulesForArtifact.length > 0 ? rulesForArtifact : undefined; return { changeName: context.changeName, @@ -278,7 +266,9 @@ export function generateInstructions( outputPath: artifact.generates, description: artifact.description, instruction: artifact.instruction, - template: enrichedTemplate, + context: configContext, + rules: configRules, + template: templateContent, dependencies, unlocks, }; diff --git a/src/core/templates/skill-templates.ts b/src/core/templates/skill-templates.ts index 8e705f1c8c..bea1abcb99 100644 --- a/src/core/templates/skill-templates.ts +++ b/src/core/templates/skill-templates.ts @@ -434,10 +434,17 @@ export function getContinueChangeSkillTemplate(): SkillTemplate { \`\`\`bash openspec instructions --change "" --json \`\`\` - - Parse the JSON to get template, dependencies, and what it unlocks - - **Create the artifact file** using the template as a starting point: + - Parse the JSON. The key fields are: + - \`context\`: Project background (constraints for you - do NOT include in output) + - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) + - \`template\`: The structure to use for your output file + - \`instruction\`: Schema-specific guidance + - \`outputPath\`: Where to write the artifact + - \`dependencies\`: Completed artifacts to read for context + - **Create the artifact file**: - Read any completed dependency files for context - - Fill in the template based on context and user's goals + - Use \`template\` as the structure - fill in its sections + - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file - Write to the output path specified in instructions - Show what was created and what's now unlocked - STOP after creating ONE artifact @@ -489,7 +496,10 @@ For other schemas, follow the \`instruction\` field from the CLI output. - Never skip artifacts or create out of order - If context is unclear, ask the user before creating - Verify the artifact file exists after writing before marking progress -- Use the schema's artifact sequence, don't assume specific artifact names` +- Use the schema's artifact sequence, don't assume specific artifact names +- **IMPORTANT**: \`context\` and \`rules\` are constraints for YOU, not content for the file + - Do NOT copy \`\`, \`\`, \`\` blocks into the artifact + - These guide what you write, but should never appear in the output` }; } @@ -699,12 +709,15 @@ export function getFfChangeSkillTemplate(): SkillTemplate { openspec instructions --change "" --json \`\`\` - The instructions JSON includes: - - \`template\`: The template content to use + - \`context\`: Project background (constraints for you - do NOT include in output) + - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) + - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance for this artifact type - \`outputPath\`: Where to write the artifact - \`dependencies\`: Completed artifacts to read for context - Read any completed dependency files for context - - Create the artifact file following the schema's \`instruction\` + - Create the artifact file using \`template\` as the structure + - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file - Show brief progress: "✓ Created " b. **Continue until all \`applyRequires\` artifacts are complete** @@ -734,7 +747,10 @@ After completing all artifacts, summarize: - Follow the \`instruction\` field from \`openspec instructions\` for each artifact type - The schema defines what each artifact should contain - follow it - Read dependency artifacts for context before creating new ones -- Use the \`template\` as a starting point, filling in based on context +- Use \`template\` as the structure for your output file - fill in its sections +- **IMPORTANT**: \`context\` and \`rules\` are constraints for YOU, not content for the file + - Do NOT copy \`\`, \`\`, \`\` blocks into the artifact + - These guide what you write, but should never appear in the output **Guardrails** - Create ALL artifacts needed for implementation (as defined by schema's \`apply.requires\`) @@ -1206,10 +1222,17 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate { \`\`\`bash openspec instructions --change "" --json \`\`\` - - Parse the JSON to get template, dependencies, and what it unlocks - - **Create the artifact file** using the template as a starting point: + - Parse the JSON. The key fields are: + - \`context\`: Project background (constraints for you - do NOT include in output) + - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) + - \`template\`: The structure to use for your output file + - \`instruction\`: Schema-specific guidance + - \`outputPath\`: Where to write the artifact + - \`dependencies\`: Completed artifacts to read for context + - **Create the artifact file**: - Read any completed dependency files for context - - Fill in the template based on context and user's goals + - Use \`template\` as the structure - fill in its sections + - Apply \`context\` and \`rules\` as constraints when writing - but do NOT copy them into the file - Write to the output path specified in instructions - Show what was created and what's now unlocked - STOP after creating ONE artifact @@ -1261,7 +1284,10 @@ For other schemas, follow the \`instruction\` field from the CLI output. - Never skip artifacts or create out of order - If context is unclear, ask the user before creating - Verify the artifact file exists after writing before marking progress -- Use the schema's artifact sequence, don't assume specific artifact names` +- Use the schema's artifact sequence, don't assume specific artifact names +- **IMPORTANT**: \`context\` and \`rules\` are constraints for YOU, not content for the file + - Do NOT copy \`\`, \`\`, \`\` blocks into the artifact + - These guide what you write, but should never appear in the output` }; } @@ -1474,12 +1500,15 @@ export function getOpsxFfCommandTemplate(): CommandTemplate { openspec instructions --change "" --json \`\`\` - The instructions JSON includes: - - \`template\`: The template content to use + - \`context\`: Project background (constraints for you - do NOT include in output) + - \`rules\`: Artifact-specific rules (constraints for you - do NOT include in output) + - \`template\`: The structure to use for your output file - \`instruction\`: Schema-specific guidance for this artifact type - \`outputPath\`: Where to write the artifact - \`dependencies\`: Completed artifacts to read for context - Read any completed dependency files for context - - Create the artifact file following the schema's \`instruction\` + - Create the artifact file using \`template\` as the structure + - Apply \`context\` and \`rules\` as constraints - but do NOT copy them into the file - Show brief progress: "✓ Created " b. **Continue until all \`applyRequires\` artifacts are complete** diff --git a/test/core/artifact-graph/instruction-loader.test.ts b/test/core/artifact-graph/instruction-loader.test.ts index 0b231e3c64..bc9a67cc37 100644 --- a/test/core/artifact-graph/instruction-loader.test.ts +++ b/test/core/artifact-graph/instruction-loader.test.ts @@ -198,7 +198,7 @@ describe('instruction-loader', () => { }); describe('project config integration', () => { - it('should inject context for all artifacts', () => { + it('should return context as separate field for all artifacts', () => { // Create project config const configDir = path.join(tempDir, 'openspec'); fs.mkdirSync(configDir, { recursive: true }); @@ -214,19 +214,19 @@ context: | const context = loadChangeContext(tempDir, 'my-change'); const instructions = generateInstructions(context, 'proposal', tempDir); - expect(instructions.template).toContain(''); - expect(instructions.template).toContain('Tech stack: TypeScript, React'); - expect(instructions.template).toContain('API style: RESTful'); - expect(instructions.template).toContain(''); + // Context should be in separate field, not in template + expect(instructions.context).toContain('Tech stack: TypeScript, React'); + expect(instructions.context).toContain('API style: RESTful'); + expect(instructions.template).not.toContain('Tech stack'); + expect(instructions.template).toContain('## Why'); // Actual template content }); - it('should not inject context when config is absent', () => { + it('should return undefined context when config is absent', () => { const context = loadChangeContext(tempDir, 'my-change'); const instructions = generateInstructions(context, 'proposal', tempDir); - expect(instructions.template).not.toContain(''); - // Template field should not include