Skip to content

Commit 6610014

Browse files
k1r3zzclaude
andcommitted
feat: inject config.yaml context and rules into apply/verify phases
Previously, config.yaml rules only worked for artifact generation phases (proposal, specs, design, tasks). The apply and verify phases had no access to project-level rules, making it impossible to customize execution strategies (e.g., subagent dispatching, parallel verification scans). Changes: - Apply: generateApplyInstructions() now reads config.yaml context + rules.apply - Verify: new generateVerifyInstructions() reads config.yaml context + rules.verify - CLI: `openspec instructions verify` is now a valid command - Validation: apply/verify are accepted as valid rule keys in config.yaml - Templates: generated command/skill prompts instruct AI to follow rules as mandatory execution constraints This enables teams to define custom execution strategies in config.yaml: rules: apply: - "Use subagent for each task" verify: - "Run 4 parallel scan subagents" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent afdca0d commit 6610014

File tree

7 files changed

+249
-12
lines changed

7 files changed

+249
-12
lines changed

src/cli/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import {
2020
statusCommand,
2121
instructionsCommand,
2222
applyInstructionsCommand,
23+
verifyInstructionsCommand,
2324
templatesCommand,
2425
schemasCommand,
2526
newChangeCommand,
2627
DEFAULT_SCHEMA,
2728
type StatusOptions,
2829
type InstructionsOptions,
30+
type VerifyInstructionsOptions,
2931
type TemplatesOptions,
3032
type SchemasOptions,
3133
type NewChangeOptions,
@@ -445,9 +447,11 @@ program
445447
.option('--json', 'Output as JSON')
446448
.action(async (artifactId: string | undefined, options: InstructionsOptions) => {
447449
try {
448-
// Special case: "apply" is not an artifact, but a command to get apply instructions
450+
// Special case: "apply" and "verify" are not artifacts, but phase commands
449451
if (artifactId === 'apply') {
450452
await applyInstructionsCommand(options);
453+
} else if (artifactId === 'verify') {
454+
await verifyInstructionsCommand(options as VerifyInstructionsOptions);
451455
} else {
452456
await instructionsCommand(artifactId, options);
453457
}

src/commands/workflow/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
export { statusCommand } from './status.js';
88
export type { StatusOptions } from './status.js';
99

10-
export { instructionsCommand, applyInstructionsCommand } from './instructions.js';
11-
export type { InstructionsOptions } from './instructions.js';
10+
export { instructionsCommand, applyInstructionsCommand, verifyInstructionsCommand } from './instructions.js';
11+
export type { InstructionsOptions, VerifyInstructionsOptions } from './instructions.js';
1212

1313
export { templatesCommand } from './templates.js';
1414
export type { TemplatesOptions } from './templates.js';

src/commands/workflow/instructions.ts

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
validateSchemaExists,
2020
type TaskItem,
2121
type ApplyInstructions,
22+
type VerifyInstructions,
2223
} from './shared.js';
24+
import { readProjectConfig } from '../../core/project-config.js';
2325

2426
// -----------------------------------------------------------------------------
2527
// Types
@@ -37,6 +39,12 @@ export interface ApplyInstructionsOptions {
3739
json?: boolean;
3840
}
3941

42+
export interface VerifyInstructionsOptions {
43+
change?: string;
44+
schema?: string;
45+
json?: boolean;
46+
}
47+
4048
// -----------------------------------------------------------------------------
4149
// Artifact Instructions Command
4250
// -----------------------------------------------------------------------------
@@ -386,6 +394,21 @@ export async function generateApplyInstructions(
386394
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
387395
}
388396

397+
// Read project config for context and rules injection
398+
let configContext: string | undefined;
399+
let configRules: string[] | undefined;
400+
try {
401+
const projectConfig = readProjectConfig(projectRoot);
402+
if (projectConfig?.context?.trim()) {
403+
configContext = projectConfig.context.trim();
404+
}
405+
if (projectConfig?.rules?.['apply'] && projectConfig.rules['apply'].length > 0) {
406+
configRules = projectConfig.rules['apply'];
407+
}
408+
} catch {
409+
// If config read fails, continue without config
410+
}
411+
389412
return {
390413
changeName,
391414
changeDir,
@@ -396,6 +419,8 @@ export async function generateApplyInstructions(
396419
state,
397420
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
398421
instruction,
422+
context: configContext,
423+
rules: configRules,
399424
};
400425
}
401426

@@ -429,7 +454,7 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions
429454
}
430455

431456
export function printApplyInstructionsText(instructions: ApplyInstructions): void {
432-
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
457+
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, context, rules } = instructions;
433458

434459
console.log(`## Apply: ${changeName}`);
435460
console.log(`Schema: ${schemaName}`);
@@ -475,7 +500,172 @@ export function printApplyInstructionsText(instructions: ApplyInstructions): voi
475500
console.log();
476501
}
477502

503+
// Project context (if present)
504+
if (context) {
505+
console.log('### Project Context');
506+
console.log(context);
507+
console.log();
508+
}
509+
510+
// Rules (if present)
511+
if (rules && rules.length > 0) {
512+
console.log('### Rules');
513+
for (const rule of rules) {
514+
console.log(`- ${rule}`);
515+
}
516+
console.log();
517+
}
518+
478519
// Instruction
479520
console.log('### Instruction');
480521
console.log(instruction);
481522
}
523+
524+
// -----------------------------------------------------------------------------
525+
// Verify Instructions Command
526+
// -----------------------------------------------------------------------------
527+
528+
/**
529+
* Generates verify instructions for validating implementation against artifacts.
530+
* Reads config.yaml context and rules.verify for custom verification strategies.
531+
*/
532+
export async function generateVerifyInstructions(
533+
projectRoot: string,
534+
changeName: string,
535+
schemaName?: string
536+
): Promise<VerifyInstructions> {
537+
const context = loadChangeContext(projectRoot, changeName, schemaName);
538+
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
539+
540+
const schema = resolveSchema(context.schemaName, projectRoot);
541+
542+
// Build context files from all existing artifacts in schema
543+
const contextFiles: Record<string, string> = {};
544+
for (const artifact of schema.artifacts) {
545+
if (artifactOutputExists(changeDir, artifact.generates)) {
546+
contextFiles[artifact.id] = path.join(changeDir, artifact.generates);
547+
}
548+
}
549+
550+
// Parse tasks if tracking file exists (reuse apply logic)
551+
const applyConfig = schema.apply;
552+
const tracksFile = applyConfig?.tracks ?? null;
553+
let tasks: TaskItem[] = [];
554+
if (tracksFile) {
555+
const tracksPath = path.join(changeDir, tracksFile);
556+
if (fs.existsSync(tracksPath)) {
557+
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
558+
tasks = parseTasksFile(tasksContent);
559+
}
560+
}
561+
562+
const total = tasks.length;
563+
const complete = tasks.filter((t) => t.done).length;
564+
const remaining = total - complete;
565+
566+
// Read project config for context and rules injection
567+
let configContext: string | undefined;
568+
let configRules: string[] | undefined;
569+
try {
570+
const projectConfig = readProjectConfig(projectRoot);
571+
if (projectConfig?.context?.trim()) {
572+
configContext = projectConfig.context.trim();
573+
}
574+
if (projectConfig?.rules?.['verify'] && projectConfig.rules['verify'].length > 0) {
575+
configRules = projectConfig.rules['verify'];
576+
}
577+
} catch {
578+
// If config read fails, continue without config
579+
}
580+
581+
return {
582+
changeName,
583+
changeDir,
584+
schemaName: context.schemaName,
585+
contextFiles,
586+
progress: { total, complete, remaining },
587+
tasks,
588+
context: configContext,
589+
rules: configRules,
590+
};
591+
}
592+
593+
export async function verifyInstructionsCommand(options: VerifyInstructionsOptions): Promise<void> {
594+
const spinner = ora('Generating verify instructions...').start();
595+
596+
try {
597+
const projectRoot = process.cwd();
598+
const changeName = await validateChangeExists(options.change, projectRoot);
599+
600+
if (options.schema) {
601+
validateSchemaExists(options.schema, projectRoot);
602+
}
603+
604+
const instructions = await generateVerifyInstructions(projectRoot, changeName, options.schema);
605+
606+
spinner.stop();
607+
608+
if (options.json) {
609+
console.log(JSON.stringify(instructions, null, 2));
610+
return;
611+
}
612+
613+
printVerifyInstructionsText(instructions);
614+
} catch (error) {
615+
spinner.stop();
616+
throw error;
617+
}
618+
}
619+
620+
export function printVerifyInstructionsText(instructions: VerifyInstructions): void {
621+
const { changeName, schemaName, contextFiles, progress, tasks, context, rules } = instructions;
622+
623+
console.log(`## Verify: ${changeName}`);
624+
console.log(`Schema: ${schemaName}`);
625+
console.log();
626+
627+
// Context files
628+
const contextFileEntries = Object.entries(contextFiles);
629+
if (contextFileEntries.length > 0) {
630+
console.log('### Context Files');
631+
for (const [artifactId, filePath] of contextFileEntries) {
632+
console.log(`- ${artifactId}: ${filePath}`);
633+
}
634+
console.log();
635+
}
636+
637+
// Progress
638+
if (progress.total > 0) {
639+
console.log('### Progress');
640+
console.log(`${progress.complete}/${progress.total} tasks complete`);
641+
console.log();
642+
}
643+
644+
// Project context (if present)
645+
if (context) {
646+
console.log('### Project Context');
647+
console.log(context);
648+
console.log();
649+
}
650+
651+
// Rules (if present)
652+
if (rules && rules.length > 0) {
653+
console.log('### Rules');
654+
for (const rule of rules) {
655+
console.log(`- ${rule}`);
656+
}
657+
console.log();
658+
}
659+
660+
// Incomplete tasks
661+
if (tasks.length > 0) {
662+
const incomplete = tasks.filter(t => !t.done);
663+
if (incomplete.length > 0) {
664+
console.log('### Incomplete Tasks');
665+
for (const task of incomplete) {
666+
console.log(`- [ ] ${task.description}`);
667+
}
668+
console.log();
669+
}
670+
}
671+
}

src/commands/workflow/shared.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ export interface ApplyInstructions {
3535
state: 'blocked' | 'all_done' | 'ready';
3636
missingArtifacts?: string[];
3737
instruction: string;
38+
context?: string;
39+
rules?: string[];
40+
}
41+
42+
export interface VerifyInstructions {
43+
changeName: string;
44+
changeDir: string;
45+
schemaName: string;
46+
contextFiles: Record<string, string>;
47+
progress: {
48+
total: number;
49+
complete: number;
50+
remaining: number;
51+
};
52+
tasks: TaskItem[];
53+
context?: string;
54+
rules?: string[];
3855
}
3956

4057
// -----------------------------------------------------------------------------

src/core/project-config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,16 @@ export function validateConfigRules(
175175
validArtifactIds: Set<string>,
176176
schemaName: string
177177
): string[] {
178+
// Phase IDs that are valid rule targets but not artifacts
179+
const validPhaseIds = new Set(['apply', 'verify']);
178180
const warnings: string[] = [];
179181

180182
for (const artifactId of Object.keys(rules)) {
181-
if (!validArtifactIds.has(artifactId)) {
183+
if (!validArtifactIds.has(artifactId) && !validPhaseIds.has(artifactId)) {
182184
const validIds = Array.from(validArtifactIds).sort().join(', ');
183185
warnings.push(
184186
`Unknown artifact ID in rules: "${artifactId}". ` +
185-
`Valid IDs for schema "${schemaName}": ${validIds}`
187+
`Valid IDs for schema "${schemaName}": ${validIds}, apply, verify`
186188
);
187189
}
188190
}

src/core/templates/workflows/apply-change.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export function getApplyChangeSkillTemplate(): SkillTemplate {
4444
- Progress (total, complete, remaining)
4545
- Task list with status
4646
- Dynamic instruction based on current state
47+
- \`context\`: Project context from config.yaml (if present)
48+
- \`rules\`: Apply-phase rules from config.yaml (if present) — **these are mandatory constraints, you MUST follow them**
4749
4850
**Handle states:**
4951
- If \`state: "blocked"\` (missing artifacts): show message, suggest using openspec-continue-change
@@ -64,9 +66,13 @@ export function getApplyChangeSkillTemplate(): SkillTemplate {
6466
- Progress: "N/M tasks complete"
6567
- Remaining tasks overview
6668
- Dynamic instruction from CLI
69+
- If rules are present, display them as "Apply Rules" section
6770
6871
6. **Implement tasks (loop until done or blocked)**
6972
73+
**IMPORTANT**: If \`rules\` are present in the apply instructions output, you MUST follow them as mandatory execution constraints. Rules may specify execution strategies (e.g., using subagents, specific testing requirements, retry policies). Follow them exactly.
74+
75+
If no rules are present, use the default behavior:
7076
For each pending task:
7177
- Show which task is being worked on
7278
- Make the code changes required
@@ -201,6 +207,8 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate {
201207
- Progress (total, complete, remaining)
202208
- Task list with status
203209
- Dynamic instruction based on current state
210+
- \`context\`: Project context from config.yaml (if present)
211+
- \`rules\`: Apply-phase rules from config.yaml (if present) — **these are mandatory constraints, you MUST follow them**
204212
205213
**Handle states:**
206214
- If \`state: "blocked"\` (missing artifacts): show message, suggest using \`/opsx:continue\`
@@ -221,9 +229,13 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate {
221229
- Progress: "N/M tasks complete"
222230
- Remaining tasks overview
223231
- Dynamic instruction from CLI
232+
- If rules are present, display them as "Apply Rules" section
224233
225234
6. **Implement tasks (loop until done or blocked)**
226235
236+
**IMPORTANT**: If \`rules\` are present in the apply instructions output, you MUST follow them as mandatory execution constraints. Rules may specify execution strategies (e.g., using subagents, specific testing requirements, retry policies). Follow them exactly.
237+
238+
If no rules are present, use the default behavior:
227239
For each pending task:
228240
- Show which task is being worked on
229241
- Make the code changes required

0 commit comments

Comments
 (0)