-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat: inject config.yaml context and rules into apply/verify phases #887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6610014
317838c
d192f08
afb54d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,10 @@ import { | |
| import { | ||
| validateChangeExists, | ||
| validateSchemaExists, | ||
| readPhaseConfig, | ||
| type TaskItem, | ||
| type ApplyInstructions, | ||
| type VerifyInstructions, | ||
| } from './shared.js'; | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
|
|
@@ -37,6 +39,12 @@ export interface ApplyInstructionsOptions { | |
| json?: boolean; | ||
| } | ||
|
|
||
| export interface VerifyInstructionsOptions { | ||
| change?: string; | ||
| schema?: string; | ||
| json?: boolean; | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Artifact Instructions Command | ||
| // ----------------------------------------------------------------------------- | ||
|
|
@@ -386,6 +394,9 @@ export async function generateApplyInstructions( | |
| instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.'; | ||
| } | ||
|
|
||
| // Read project config for context and rules injection | ||
| const phaseConfig = readPhaseConfig(projectRoot, 'apply'); | ||
|
|
||
| return { | ||
| changeName, | ||
| changeDir, | ||
|
|
@@ -396,6 +407,8 @@ export async function generateApplyInstructions( | |
| state, | ||
| missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined, | ||
| instruction, | ||
| context: phaseConfig.context, | ||
| rules: phaseConfig.rules, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -429,7 +442,7 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions | |
| } | ||
|
|
||
| export function printApplyInstructionsText(instructions: ApplyInstructions): void { | ||
| const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions; | ||
| const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, context, rules } = instructions; | ||
|
|
||
| console.log(`## Apply: ${changeName}`); | ||
| console.log(`Schema: ${schemaName}`); | ||
|
|
@@ -475,7 +488,160 @@ export function printApplyInstructionsText(instructions: ApplyInstructions): voi | |
| console.log(); | ||
| } | ||
|
|
||
| // Project context (if present) | ||
| if (context) { | ||
| console.log('### Project Context'); | ||
| console.log(context); | ||
| console.log(); | ||
| } | ||
|
|
||
| // Rules (if present) | ||
| if (rules && rules.length > 0) { | ||
| console.log('### Rules'); | ||
| for (const rule of rules) { | ||
| console.log(`- ${rule}`); | ||
| } | ||
| console.log(); | ||
| } | ||
|
|
||
| // Instruction | ||
| console.log('### Instruction'); | ||
| console.log(instruction); | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Verify Instructions Command | ||
| // ----------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Generates verify instructions for validating implementation against artifacts. | ||
| * Reads config.yaml context and rules.verify for custom verification strategies. | ||
| */ | ||
| export async function generateVerifyInstructions( | ||
| projectRoot: string, | ||
| changeName: string, | ||
| schemaName?: string | ||
| ): Promise<VerifyInstructions> { | ||
| const context = loadChangeContext(projectRoot, changeName, schemaName); | ||
| const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName); | ||
|
|
||
| const schema = resolveSchema(context.schemaName, projectRoot); | ||
|
|
||
| // Build context files from all existing artifacts in schema | ||
| const contextFiles: Record<string, string> = {}; | ||
| for (const artifact of schema.artifacts) { | ||
| if (artifactOutputExists(changeDir, artifact.generates)) { | ||
| contextFiles[artifact.id] = path.join(changeDir, artifact.generates); | ||
| } | ||
|
Comment on lines
+530
to
+535
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolve globbed artifact outputs before returning
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // Parse tasks if tracking file exists (reuse apply logic) | ||
| const applyConfig = schema.apply; | ||
| const tracksFile = applyConfig?.tracks ?? null; | ||
| let tasks: TaskItem[] = []; | ||
| if (tracksFile) { | ||
| const tracksPath = path.join(changeDir, tracksFile); | ||
| if (fs.existsSync(tracksPath)) { | ||
| const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8'); | ||
| tasks = parseTasksFile(tasksContent); | ||
| } | ||
| } | ||
|
|
||
| const total = tasks.length; | ||
| const complete = tasks.filter((t) => t.done).length; | ||
| const remaining = total - complete; | ||
|
|
||
| // Read project config for context and rules injection | ||
| const phaseConfig = readPhaseConfig(projectRoot, 'verify'); | ||
|
|
||
| return { | ||
| changeName, | ||
| changeDir, | ||
| schemaName: context.schemaName, | ||
| contextFiles, | ||
| progress: { total, complete, remaining }, | ||
| tasks, | ||
| context: phaseConfig.context, | ||
| rules: phaseConfig.rules, | ||
| }; | ||
| } | ||
|
|
||
| export async function verifyInstructionsCommand(options: VerifyInstructionsOptions): Promise<void> { | ||
| const spinner = ora('Generating verify instructions...').start(); | ||
|
|
||
| try { | ||
| const projectRoot = process.cwd(); | ||
| const changeName = await validateChangeExists(options.change, projectRoot); | ||
|
|
||
| if (options.schema) { | ||
| validateSchemaExists(options.schema, projectRoot); | ||
| } | ||
|
|
||
| const instructions = await generateVerifyInstructions(projectRoot, changeName, options.schema); | ||
|
|
||
| spinner.stop(); | ||
|
|
||
| if (options.json) { | ||
| console.log(JSON.stringify(instructions, null, 2)); | ||
| return; | ||
| } | ||
|
|
||
| printVerifyInstructionsText(instructions); | ||
| } catch (error) { | ||
| spinner.stop(); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| export function printVerifyInstructionsText(instructions: VerifyInstructions): void { | ||
| const { changeName, schemaName, contextFiles, progress, tasks, context, rules } = instructions; | ||
|
|
||
| console.log(`## Verify: ${changeName}`); | ||
| console.log(`Schema: ${schemaName}`); | ||
| console.log(); | ||
|
|
||
| // Context files | ||
| const contextFileEntries = Object.entries(contextFiles); | ||
| if (contextFileEntries.length > 0) { | ||
| console.log('### Context Files'); | ||
| for (const [artifactId, filePath] of contextFileEntries) { | ||
| console.log(`- ${artifactId}: ${filePath}`); | ||
| } | ||
| console.log(); | ||
| } | ||
|
|
||
| // Progress | ||
| if (progress.total > 0) { | ||
| console.log('### Progress'); | ||
| console.log(`${progress.complete}/${progress.total} tasks complete`); | ||
| console.log(); | ||
| } | ||
|
|
||
| // Project context (if present) | ||
| if (context) { | ||
| console.log('### Project Context'); | ||
| console.log(context); | ||
| console.log(); | ||
| } | ||
|
|
||
| // Rules (if present) | ||
| if (rules && rules.length > 0) { | ||
| console.log('### Rules'); | ||
| for (const rule of rules) { | ||
| console.log(`- ${rule}`); | ||
| } | ||
| console.log(); | ||
| } | ||
|
|
||
| // Incomplete tasks | ||
| if (tasks.length > 0) { | ||
| const incomplete = tasks.filter(t => !t.done); | ||
| if (incomplete.length > 0) { | ||
| console.log('### Incomplete Tasks'); | ||
| for (const task of incomplete) { | ||
| console.log(`- [ ] ${task.description}`); | ||
| } | ||
| console.log(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import * as fs from 'node:fs'; | ||
| import { parse as parseYaml } from 'yaml'; | ||
| import { SchemaYamlSchema, type SchemaYaml, type Artifact } from './types.js'; | ||
| import { RESERVED_PHASE_IDS } from '../project-config.js'; | ||
|
|
||
| export class SchemaValidationError extends Error { | ||
| constructor(message: string) { | ||
|
|
@@ -32,6 +33,9 @@ export function parseSchema(yamlContent: string): SchemaYaml { | |
|
|
||
| const schema = result.data; | ||
|
|
||
| // Check for reserved phase IDs used as artifact IDs | ||
| validateNoReservedIds(schema.artifacts); | ||
|
|
||
| // Check for duplicate artifact IDs | ||
| validateNoDuplicateIds(schema.artifacts); | ||
|
|
||
|
|
@@ -44,6 +48,21 @@ export function parseSchema(yamlContent: string): SchemaYaml { | |
| return schema; | ||
| } | ||
|
|
||
| /** | ||
| * Validates that no artifact uses a reserved phase ID ('apply', 'verify'). | ||
| * These names are reserved for phase-level commands in the CLI. | ||
| */ | ||
| function validateNoReservedIds(artifacts: Artifact[]): void { | ||
| for (const artifact of artifacts) { | ||
| if (RESERVED_PHASE_IDS.has(artifact.id.toLowerCase())) { | ||
| throw new SchemaValidationError( | ||
| `Artifact ID '${artifact.id}' is reserved for phase commands. ` + | ||
| `Reserved IDs: ${Array.from(RESERVED_PHASE_IDS).join(', ')}` | ||
| ); | ||
| } | ||
| } | ||
|
Comment on lines
+55
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check the definition of RESERVED_PHASE_IDS
echo "=== RESERVED_PHASE_IDS definition ==="
rg -n -A5 'RESERVED_PHASE_IDS' src/core/project-config.ts
# Check how CLI routes the instructions command to see if it's case-sensitive
echo -e "\n=== CLI routing for instructions command ==="
rg -n -B2 -A5 'instructions' src/cli/index.ts | head -50Repository: Fission-AI/OpenSpec Length of output: 1595 🏁 Script executed: #!/bin/bash
# Check if artifact IDs are normalized to lowercase anywhere
echo "=== Artifact ID normalization ==="
rg -n -i 'toLowerCase|normalize' src/core/ | grep -i artifact | head -20
# Check the full schema validation file to understand artifact ID handling
echo -e "\n=== Artifact ID schema validation ==="
rg -n 'artifact.*id|id.*artifact' src/core/artifact-graph/schema.ts -i | head -20
# Check if there's any case-insensitive comparison or normalization
echo -e "\n=== Case-insensitive comparisons ==="
rg -n 'toLowerCase\|toUpperCase' src/ | head -20
# Look at the verifyInstructionsCommand and instructionsCommand handlers
echo -e "\n=== Verify instructions command handler ==="
rg -n -A10 'verifyInstructionsCommand' src/cli/ | head -40Repository: Fission-AI/OpenSpec Length of output: 3150 🏁 Script executed: #!/bin/bash
# Check the Artifact interface definition to see if case is preserved
echo "=== Artifact interface definition ==="
rg -n -B2 -A5 'interface Artifact\|type Artifact' src/ | head -40
# Check how instructionsCommand handler processes the artifactId
echo -e "\n=== Instructions command handler implementation ==="
rg -n -A15 'export.*instructionsCommand|async.*instructionsCommand' src/cli/ | head -50
# Check how the artifact ID is retrieved/validated in the commands
echo -e "\n=== Artifact retrieval in commands ==="
rg -n 'getArtifact|findArtifact' src/cli/ -A3 | head -40
# Check if there's any example schema with mixed-case artifact IDs
echo -e "\n=== Sample artifact definitions ==="
find . -name "*.yaml" -o -name "*.yml" | xargs grep -l "artifacts:" 2>/dev/null | head -5 | xargs head -100Repository: Fission-AI/OpenSpec Length of output: 5139 🏁 Script executed: #!/bin/bash
# Search more broadly for Artifact type
echo "=== Artifact type/interface definition ==="
rg -n 'Artifact' src/core/artifact-graph/schema.ts | head -20
# Check the imports and types in schema.ts
echo -e "\n=== Schema.ts imports and types ==="
head -30 src/core/artifact-graph/schema.ts
# Search for the actual type definition in artifact-graph directory
echo -e "\n=== Types in artifact-graph ==="
fd '\.ts' src/core/artifact-graph/ | xargs grep -l 'interface Artifact\|type Artifact'
# Find the commands implementation file
echo -e "\n=== Find commands file ==="
find src -name '*command*' -type f
# Check the instructions command more deeply
echo -e "\n=== Instructions command in commands file ==="
rg -n 'instructionsCommand' src/ -lRepository: Fission-AI/OpenSpec Length of output: 1928 🏁 Script executed: #!/bin/bash
# Read the Artifact type definition
echo "=== Artifact type definition in types.ts ==="
cat src/core/artifact-graph/types.ts
# Read the instructions command handler
echo -e "\n=== Instructions command handler ==="
cat src/commands/workflow/instructions.ts
# Check if there's any validation of artifact IDs in the resolver
echo -e "\n=== Artifact resolution/retrieval ==="
rg -n 'artifact\.id|artifactId' src/commands/workflow/ -A2 -B2 | head -80Repository: Fission-AI/OpenSpec Length of output: 28155 🏁 Script executed: #!/bin/bash
# Find and examine the getArtifact implementation
echo "=== getArtifact implementation ==="
rg -n 'getArtifact' src/core/artifact-graph/ -A5 | head -50
# Check if artifact IDs are used in case-sensitive lookups
echo -e "\n=== Artifact ID lookups in graph ==="
rg -n 'class.*Graph|getArtifact|find.*artifact' src/core/artifact-graph/ -B2 -A8 | head -100Repository: Fission-AI/OpenSpec Length of output: 4805 Normalize artifact IDs to lowercase when validating reserved IDs. The check uses exact string matching ( 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * Validates that there are no duplicate artifact IDs. | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.