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('');
console.log(template.trim());
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 wrapper (CLI handles that)
- expect(instructions.template).not.toContain('');
+ expect(instructions.context).toBeUndefined();
+ expect(instructions.rules).toBeUndefined();
expect(instructions.template).toContain('## Why'); // Actual template content
});
@@ -247,7 +247,7 @@ context: |
const context = loadChangeContext(tempDir, 'my-change');
const instructions = generateInstructions(context, 'proposal', tempDir);
- expect(instructions.template).toContain('Line 1\nLine 2\nLine 3');
+ expect(instructions.context).toContain('Line 1\nLine 2\nLine 3');
});
it('should preserve special characters in context', () => {
@@ -265,10 +265,10 @@ context: |
const context = loadChangeContext(tempDir, 'my-change');
const instructions = generateInstructions(context, 'proposal', tempDir);
- expect(instructions.template).toContain('Special: < > & " \' @ # $ % [ ] { }');
+ expect(instructions.context).toContain('Special: < > & " \' @ # $ % [ ] { }');
});
- it('should inject rules only for matching artifact', () => {
+ it('should return rules only for matching artifact', () => {
// Create project config with rules
const configDir = path.join(tempDir, 'openspec');
fs.mkdirSync(configDir, { recursive: true });
@@ -288,21 +288,16 @@ rules:
// Check proposal artifact has its rules
const proposalInstructions = generateInstructions(context, 'proposal', tempDir);
- expect(proposalInstructions.template).toContain('');
- expect(proposalInstructions.template).toContain('- Include rollback plan');
- expect(proposalInstructions.template).toContain('- Identify affected teams');
- expect(proposalInstructions.template).not.toContain('Given/When/Then');
- expect(proposalInstructions.template).toContain('');
+ expect(proposalInstructions.rules).toEqual(['Include rollback plan', 'Identify affected teams']);
+ expect(proposalInstructions.template).not.toContain('rollback plan');
// Check specs artifact has its rules
const specsInstructions = generateInstructions(context, 'specs', tempDir);
- expect(specsInstructions.template).toContain('');
- expect(specsInstructions.template).toContain('- Use Given/When/Then format');
- expect(specsInstructions.template).not.toContain('rollback plan');
- expect(specsInstructions.template).toContain('');
+ expect(specsInstructions.rules).toEqual(['Use Given/When/Then format']);
+ expect(specsInstructions.template).not.toContain('Given/When/Then');
});
- it('should not inject rules for non-matching artifact', () => {
+ it('should return undefined rules for non-matching artifact', () => {
// Create project config with rules only for proposal
const configDir = path.join(tempDir, 'openspec');
fs.mkdirSync(configDir, { recursive: true });
@@ -317,12 +312,12 @@ rules:
const context = loadChangeContext(tempDir, 'my-change');
- // Check design artifact (no rules configured) has no rules section
+ // Check design artifact (no rules configured) has undefined rules
const designInstructions = generateInstructions(context, 'design', tempDir);
- expect(designInstructions.template).not.toContain('');
+ expect(designInstructions.rules).toBeUndefined();
});
- it('should not inject rules when empty array', () => {
+ it('should return undefined rules when empty array', () => {
// Create project config with empty rules array
const configDir = path.join(tempDir, 'openspec');
fs.mkdirSync(configDir, { recursive: true });
@@ -338,11 +333,11 @@ rules:
const context = loadChangeContext(tempDir, 'my-change');
const instructions = generateInstructions(context, 'proposal', tempDir);
- expect(instructions.template).toContain('');
- expect(instructions.template).not.toContain('');
+ expect(instructions.context).toBe('Some context');
+ expect(instructions.rules).toBeUndefined();
});
- it('should maintain correct order: context -> rules -> template', () => {
+ it('should keep context, rules, and template as separate fields', () => {
// Create project config with both context and rules
const configDir = path.join(tempDir, 'openspec');
fs.mkdirSync(configDir, { recursive: true });
@@ -359,17 +354,13 @@ rules:
const context = loadChangeContext(tempDir, 'my-change');
const instructions = generateInstructions(context, 'proposal', tempDir);
- const contextIdx = instructions.template.indexOf('');
- const rulesIdx = instructions.template.indexOf('');
- const templateContentIdx = instructions.template.indexOf('## Why');
-
- expect(contextIdx).toBeGreaterThan(-1);
- expect(rulesIdx).toBeGreaterThan(-1);
- expect(templateContentIdx).toBeGreaterThan(-1);
- expect(contextIdx).toBeLessThan(rulesIdx);
- expect(rulesIdx).toBeLessThan(templateContentIdx);
- // Template field should not include wrapper (CLI handles that)
- expect(instructions.template).not.toContain('');
+ // All three should be separate
+ expect(instructions.context).toBe('Project context here');
+ expect(instructions.rules).toEqual(['Rule 1']);
+ expect(instructions.template).toContain('## Why');
+ // Template should not contain context or rules
+ expect(instructions.template).not.toContain('Project context here');
+ expect(instructions.template).not.toContain('Rule 1');
});
it('should handle context without rules', () => {
@@ -386,11 +377,9 @@ context: Project context only
const context = loadChangeContext(tempDir, 'my-change');
const instructions = generateInstructions(context, 'proposal', tempDir);
- expect(instructions.template).toContain('');
- expect(instructions.template).not.toContain('');
- // Template field should not include wrapper (CLI handles that)
- expect(instructions.template).not.toContain('');
- expect(instructions.template).toContain('## Why'); // Actual template content
+ expect(instructions.context).toBe('Project context only');
+ expect(instructions.rules).toBeUndefined();
+ expect(instructions.template).toContain('## Why');
});
it('should handle rules without context', () => {
@@ -409,21 +398,18 @@ rules:
const context = loadChangeContext(tempDir, 'my-change');
const instructions = generateInstructions(context, 'proposal', tempDir);
- expect(instructions.template).not.toContain('');
- expect(instructions.template).toContain('');
- // Template field should not include wrapper (CLI handles that)
- expect(instructions.template).not.toContain('');
- expect(instructions.template).toContain('## Why'); // Actual template content
+ expect(instructions.context).toBeUndefined();
+ expect(instructions.rules).toEqual(['Rule only']);
+ expect(instructions.template).toContain('## Why');
});
it('should work without project root parameter', () => {
const context = loadChangeContext(tempDir, 'my-change');
const instructions = generateInstructions(context, 'proposal'); // No projectRoot
- expect(instructions.template).not.toContain('');
- // Template field should not include wrapper (CLI handles that)
- expect(instructions.template).not.toContain('');
- expect(instructions.template).toContain('## Why'); // Actual template content
+ expect(instructions.context).toBeUndefined();
+ expect(instructions.rules).toBeUndefined();
+ expect(instructions.template).toContain('## Why');
});
});