Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 15f8fe1d61 feat: add /opsx:ff command for fast-forward artifact creation
Adds a new fast-forward command that creates all artifacts needed for
implementation in one go, instead of stepping through them individually.

Changes:
- Add `applyRequires` field to status JSON output (shows which artifacts
  are required before the apply phase can begin)
- Add skill template for openspec-ff-change
- Add command template for /opsx:ff slash command
- Register templates in artifact-workflow setup command

The fast-forward command uses the schema's `apply.requires` configuration
to determine when to stop creating artifacts, making it schema-agnostic.
2026-01-06 11:42:08 -08:00
Tabish Bidiwale d73705736f feat: make apply instructions schema-aware (#444)
* feat: make apply instructions schema-aware

The `generateApplyInstructions` function was hardcoded to check for
`spec-driven` artifacts. This change makes it read artifact definitions
from the schema's new `apply` block, enabling support for different
workflows like TDD.

Changes:
- Add `ApplyPhaseSchema` Zod schema with `requires`, `tracks`, and
  `instruction` fields to types.ts
- Update `SchemaYamlSchema` to include optional `apply` field
- Add `apply` block to spec-driven and tdd schemas
- Refactor `generateApplyInstructions` to:
  - Load schema via `resolveSchema()`
  - Read `apply.requires` for required artifacts
  - Check artifact existence dynamically (supports glob patterns)
  - Use `apply.tracks` for progress tracking (or skip if null)
  - Use `apply.instruction` for custom guidance
  - Build `contextFiles` from all existing artifacts in schema
- Handle fallback when schema has no `apply` block (require all artifacts)
- Add 8 new tests for schema-aware apply behavior

* fix: improve apply instructions robustness and consistency

- Fix artifactOutputExists to properly handle glob patterns cross-platform
  by using path.sep and verifying actual file matches
- Remove redundant if/else in contextFiles loop
- Distinguish between missing tracks file vs empty tracks file
- Use consistent { error: } key in ApplyPhaseSchema validation
- Rename fallback test to accurately describe what it tests

* test: add fallback behavior tests for schemas without apply block

Add two tests that verify the fallback logic when a schema lacks an
apply block:
- Blocks when not all artifacts exist (requires ALL artifacts)
- Ready state with default instruction when all artifacts exist

Uses XDG_DATA_HOME to create temporary user schemas for testing.
2026-01-05 22:45:27 -08:00
Tabish Bidiwale ed924ffcff feat: add agent schema selection to experimental artifact workflow (#445)
- Add `openspec schemas` CLI command with `--json` option for agents
- Add `listSchemasWithInfo()` helper to get schema metadata (name, description, artifacts)
- Update `openspec-new-change` skill to prompt for schema selection
- Update `openspec-continue-change` skill to read schema dynamically from status
- Update `openspec-apply-change` skill to use schema-specific context files
- Update all slash commands (/opsx:new, /opsx:continue, /opsx:apply) to be schema-agnostic
- Add documentation for when to use each schema (spec-driven vs tdd)

Agents can now create changes with different workflow schemas and the skills
dynamically adapt based on the selected schema's artifact sequence.
2026-01-05 22:32:22 -08:00
11 changed files with 882 additions and 177 deletions
@@ -1,32 +1,32 @@
## Prerequisites
- [ ] 0.1 Implement `add-per-change-schema-metadata` change first
- [x] 0.1 Implement `add-per-change-schema-metadata` change first
## 1. Schema Discovery
- [ ] 1.1 Add CLI command or helper to list schemas with descriptions (for agent use)
- [ ] 1.2 Ensure `openspec templates --schema <name>` returns artifact list for any schema
- [x] 1.1 Add CLI command or helper to list schemas with descriptions (for agent use)
- [x] 1.2 Ensure `openspec templates --schema <name>` returns artifact list for any schema
## 2. Update New Change Skill
- [ ] 2.1 Add schema selection prompt using AskUserQuestion tool
- [ ] 2.2 Present available schemas with descriptions (spec-driven, tdd, etc.)
- [ ] 2.3 Pass selected schema to `openspec new change --schema <name>`
- [ ] 2.4 Update output to show which schema/workflow was selected
- [x] 2.1 Add schema selection prompt using AskUserQuestion tool
- [x] 2.2 Present available schemas with descriptions (spec-driven, tdd, etc.)
- [x] 2.3 Pass selected schema to `openspec new change --schema <name>`
- [x] 2.4 Update output to show which schema/workflow was selected
## 3. Update Continue Change Skill
- [ ] 3.1 Remove hardcoded artifact references (proposal, specs, design, tasks)
- [ ] 3.2 Read artifact list dynamically from `openspec status --json`
- [ ] 3.3 Adjust artifact creation guidelines to be schema-agnostic
- [ ] 3.4 Handle schema-specific artifact types (e.g., TDD's `tests` artifact)
- [x] 3.1 Remove hardcoded artifact references (proposal, specs, design, tasks)
- [x] 3.2 Read artifact list dynamically from `openspec status --json`
- [x] 3.3 Adjust artifact creation guidelines to be schema-agnostic
- [x] 3.4 Handle schema-specific artifact types (e.g., TDD's `tests` artifact)
## 4. Update Apply Change Skill
- [ ] 4.1 Make task detection work with different schema structures
- [ ] 4.2 Adjust context file reading for schema-specific artifacts
- [x] 4.1 Make task detection work with different schema structures
- [x] 4.2 Adjust context file reading for schema-specific artifacts
## 5. Documentation
- [ ] 5.1 Add schema descriptions to help text or skill instructions
- [ ] 5.2 Document when to use each schema (TDD for bug fixes, spec-driven for features, etc.)
- [x] 5.1 Add schema descriptions to help text or skill instructions
- [x] 5.2 Document when to use each schema (TDD for bug fixes, spec-driven for features, etc.)
@@ -1,35 +1,35 @@
## Prerequisites
- [ ] 0.1 Implement `add-per-change-schema-metadata` first (to auto-detect schema)
- [x] 0.1 Implement `add-per-change-schema-metadata` first (to auto-detect schema)
## 1. Schema Format
- [ ] 1.1 Add `ApplyPhaseSchema` Zod schema to `src/core/artifact-graph/types.ts`
- [ ] 1.2 Update `SchemaYamlSchema` to include optional `apply` field
- [ ] 1.3 Export `ApplyPhase` type
- [x] 1.1 Add `ApplyPhaseSchema` Zod schema to `src/core/artifact-graph/types.ts`
- [x] 1.2 Update `SchemaYamlSchema` to include optional `apply` field
- [x] 1.3 Export `ApplyPhase` type
## 2. Update Existing Schemas
- [ ] 2.1 Add `apply` block to `schemas/spec-driven/schema.yaml`
- [ ] 2.2 Add `apply` block to `schemas/tdd/schema.yaml`
- [x] 2.1 Add `apply` block to `schemas/spec-driven/schema.yaml`
- [x] 2.2 Add `apply` block to `schemas/tdd/schema.yaml`
## 3. Refactor generateApplyInstructions
- [ ] 3.1 Load schema via `resolveSchema(schemaName)`
- [ ] 3.2 Read `apply.requires` to determine required artifacts
- [ ] 3.3 Check artifact existence dynamically (not hardcoded paths)
- [ ] 3.4 Use `apply.tracks` for progress tracking (or skip if null)
- [ ] 3.5 Use `apply.instruction` for the instruction text
- [ ] 3.6 Build `contextFiles` from all existing artifacts in schema
- [x] 3.1 Load schema via `resolveSchema(schemaName)`
- [x] 3.2 Read `apply.requires` to determine required artifacts
- [x] 3.3 Check artifact existence dynamically (not hardcoded paths)
- [x] 3.4 Use `apply.tracks` for progress tracking (or skip if null)
- [x] 3.5 Use `apply.instruction` for the instruction text
- [x] 3.6 Build `contextFiles` from all existing artifacts in schema
## 4. Handle Fallback
- [ ] 4.1 If schema has no `apply` block, require all artifacts to exist
- [ ] 4.2 Default instruction: "All artifacts complete. Proceed with implementation."
- [x] 4.1 If schema has no `apply` block, require all artifacts to exist
- [x] 4.2 Default instruction: "All artifacts complete. Proceed with implementation."
## 5. Tests
- [ ] 5.1 Test apply instructions with spec-driven schema
- [ ] 5.2 Test apply instructions with tdd schema
- [ ] 5.3 Test fallback when schema has no apply block
- [ ] 5.4 Test blocked state when required artifacts missing
- [x] 5.1 Test apply instructions with spec-driven schema
- [x] 5.2 Test apply instructions with tdd schema
- [x] 5.3 Test fallback when schema has no apply block
- [x] 5.4 Test blocked state when required artifacts missing
+7
View File
@@ -139,3 +139,10 @@ artifacts:
requires:
- specs
- design
apply:
requires: [tasks]
tracks: tasks.md
instruction: |
Read context files, work through pending tasks, mark complete as you go.
Pause if you hit blockers or need clarification.
+7
View File
@@ -204,3 +204,10 @@ artifacts:
Reference the spec for requirements, implementation for details.
requires:
- implementation
apply:
requires: [tests]
tracks: null
instruction: |
Run tests to see failures. Implement minimal code to pass each test.
Refactor while keeping tests green.
+185 -58
View File
@@ -19,14 +19,16 @@ import {
formatChangeStatus,
generateInstructions,
listSchemas,
listSchemasWithInfo,
getSchemaDir,
resolveSchema,
ArtifactGraph,
type ChangeStatus,
type ArtifactInstructions,
type SchemaInfo,
} from '../core/artifact-graph/index.js';
import { createChange, validateChangeName } from '../utils/change-utils.js';
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate } from '../core/templates/skill-templates.js';
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate } from '../core/templates/skill-templates.js';
import { FileSystemUtils } from '../utils/file-system.js';
// -----------------------------------------------------------------------------
@@ -42,12 +44,8 @@ interface TaskItem {
interface ApplyInstructions {
changeName: string;
changeDir: string;
contextFiles: {
proposal?: string;
specs: string;
design?: string;
tasks: string;
};
schemaName: string;
contextFiles: Record<string, string>;
progress: {
total: number;
complete: number;
@@ -427,8 +425,72 @@ function parseTasksFile(content: string): TaskItem[] {
return tasks;
}
/**
* Checks if an artifact output exists in the change directory.
* Supports glob patterns (e.g., "specs/*.md") by verifying at least one matching file exists.
*/
function artifactOutputExists(changeDir: string, generates: string): boolean {
// Normalize the generates path to use platform-specific separators
const normalizedGenerates = generates.split('/').join(path.sep);
const fullPath = path.join(changeDir, normalizedGenerates);
// If it's a glob pattern (contains ** or *), check for matching files
if (generates.includes('*')) {
// Extract the directory part before the glob pattern
const parts = normalizedGenerates.split(path.sep);
const dirParts: string[] = [];
let patternPart = '';
for (const part of parts) {
if (part.includes('*')) {
patternPart = part;
break;
}
dirParts.push(part);
}
const dirPath = path.join(changeDir, ...dirParts);
// Check if directory exists
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
return false;
}
// Extract expected extension from pattern (e.g., "*.md" -> ".md")
const extMatch = patternPart.match(/\*(\.[a-zA-Z0-9]+)$/);
const expectedExt = extMatch ? extMatch[1] : null;
// Recursively check for matching files
const hasMatchingFiles = (dir: string): boolean => {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
// For ** patterns, recurse into subdirectories
if (generates.includes('**') && hasMatchingFiles(path.join(dir, entry.name))) {
return true;
}
} else if (entry.isFile()) {
// Check if file matches expected extension (or any file if no extension specified)
if (!expectedExt || entry.name.endsWith(expectedExt)) {
return true;
}
}
}
} catch {
return false;
}
return false;
};
return hasMatchingFiles(dirPath);
}
return fs.existsSync(fullPath);
}
/**
* Generates apply instructions for implementing tasks from a change.
* Schema-aware: reads apply phase configuration from schema to determine
* required artifacts, tracking file, and instruction.
*/
async function generateApplyInstructions(
projectRoot: string,
@@ -439,39 +501,43 @@ async function generateApplyInstructions(
const context = loadChangeContext(projectRoot, changeName, schemaName);
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
// Check if required artifacts exist (tasks.md is the minimum requirement)
const tasksPath = path.join(changeDir, 'tasks.md');
const proposalPath = path.join(changeDir, 'proposal.md');
const designPath = path.join(changeDir, 'design.md');
const specsPath = path.join(changeDir, 'specs');
// Get the full schema to access the apply phase configuration
const schema = resolveSchema(context.schemaName);
const applyConfig = schema.apply;
const hasProposal = fs.existsSync(proposalPath);
const hasDesign = fs.existsSync(designPath);
const hasTasks = fs.existsSync(tasksPath);
const hasSpecs = fs.existsSync(specsPath);
// Determine required artifacts and tracking file from schema
// Fallback: if no apply block, require all artifacts
const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
const tracksFile = applyConfig?.tracks ?? null;
const schemaInstruction = applyConfig?.instruction ?? null;
// Determine state and missing artifacts
// Check which required artifacts are missing
const missingArtifacts: string[] = [];
if (!hasTasks) {
// Check what's missing to create tasks (design is optional)
if (!hasProposal) missingArtifacts.push('proposal');
if (!hasSpecs) missingArtifacts.push('specs');
if (missingArtifacts.length === 0) missingArtifacts.push('tasks');
for (const artifactId of requiredArtifactIds) {
const artifact = schema.artifacts.find((a) => a.id === artifactId);
if (artifact && !artifactOutputExists(changeDir, artifact.generates)) {
missingArtifacts.push(artifactId);
}
}
// Build context files object
const contextFiles: ApplyInstructions['contextFiles'] = {
specs: path.join(changeDir, 'specs/**/*.md'),
tasks: tasksPath,
};
if (hasProposal) contextFiles.proposal = proposalPath;
if (hasDesign) contextFiles.design = designPath;
// 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);
}
}
// Parse tasks if file exists
// Parse tasks if tracking file exists
let tasks: TaskItem[] = [];
if (hasTasks) {
const tasksContent = await fs.promises.readFile(tasksPath, 'utf-8');
tasks = parseTasksFile(tasksContent);
let tracksFileExists = false;
if (tracksFile) {
const tracksPath = path.join(changeDir, tracksFile);
tracksFileExists = fs.existsSync(tracksPath);
if (tracksFileExists) {
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
tasks = parseTasksFile(tasksContent);
}
}
// Calculate progress
@@ -479,27 +545,39 @@ async function generateApplyInstructions(
const complete = tasks.filter((t) => t.done).length;
const remaining = total - complete;
// Determine state
// Determine state and instruction
let state: ApplyInstructions['state'];
let instruction: string;
if (!hasTasks || missingArtifacts.length > 0) {
if (missingArtifacts.length > 0) {
state = 'blocked';
instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nUse the openspec-continue-change skill to create the missing artifacts first.`;
} else if (remaining === 0 && total > 0) {
} else if (tracksFile && !tracksFileExists) {
// Tracking file configured but doesn't exist yet
const tracksFilename = path.basename(tracksFile);
state = 'blocked';
instruction = `The ${tracksFilename} file is missing and must be created.\nUse openspec-continue-change to generate the tracking file.`;
} else if (tracksFile && tracksFileExists && total === 0) {
// Tracking file exists but contains no tasks
const tracksFilename = path.basename(tracksFile);
state = 'blocked';
instruction = `The ${tracksFilename} file exists but contains no tasks.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
} else if (tracksFile && remaining === 0 && total > 0) {
state = 'all_done';
instruction = 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.';
} else if (total === 0) {
state = 'blocked';
instruction = 'The tasks.md file exists but contains no tasks.\nAdd tasks to tasks.md or regenerate it with openspec-continue-change.';
} else if (!tracksFile) {
// No tracking file (e.g., TDD schema) - ready to apply
state = 'ready';
instruction = schemaInstruction?.trim() ?? 'All required artifacts complete. Proceed with implementation.';
} else {
state = 'ready';
instruction = 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
}
return {
changeName,
changeDir,
schemaName: context.schemaName,
contextFiles,
progress: { total, complete, remaining },
tasks,
@@ -539,9 +617,10 @@ async function applyInstructionsCommand(options: ApplyInstructionsOptions): Prom
}
function printApplyInstructionsText(instructions: ApplyInstructions): void {
const { changeName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
console.log(`## Apply: ${changeName}`);
console.log(`Schema: ${schemaName}`);
console.log();
// Warning for blocked state
@@ -553,26 +632,26 @@ function printApplyInstructionsText(instructions: ApplyInstructions): void {
console.log();
}
// Context files
console.log('### Context Files');
if (contextFiles.proposal) {
console.log(`- proposal: ${contextFiles.proposal}`);
// Context files (dynamically from schema)
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();
}
console.log(`- specs: ${contextFiles.specs}`);
if (contextFiles.design) {
console.log(`- design: ${contextFiles.design}`);
}
console.log(`- tasks: ${contextFiles.tasks}`);
console.log();
// Progress
console.log('### Progress');
if (state === 'all_done') {
console.log(`${progress.complete}/${progress.total} complete ✓`);
} else {
console.log(`${progress.complete}/${progress.total} complete`);
// Progress (only show if we have tracking)
if (progress.total > 0 || tasks.length > 0) {
console.log('### Progress');
if (state === 'all_done') {
console.log(`${progress.complete}/${progress.total} complete ✓`);
} else {
console.log(`${progress.complete}/${progress.total} complete`);
}
console.log();
}
console.log();
// Tasks
if (tasks.length > 0) {
@@ -717,17 +796,20 @@ async function artifactExperimentalSetupCommand(): Promise<void> {
const newChangeSkill = getNewChangeSkillTemplate();
const continueChangeSkill = getContinueChangeSkillTemplate();
const applyChangeSkill = getApplyChangeSkillTemplate();
const ffChangeSkill = getFfChangeSkillTemplate();
// Get command templates
const newCommand = getOpsxNewCommandTemplate();
const continueCommand = getOpsxContinueCommandTemplate();
const applyCommand = getOpsxApplyCommandTemplate();
const ffCommand = getOpsxFfCommandTemplate();
// Create skill directories and SKILL.md files
const skills = [
{ template: newChangeSkill, dirName: 'openspec-new-change' },
{ template: continueChangeSkill, dirName: 'openspec-continue-change' },
{ template: applyChangeSkill, dirName: 'openspec-apply-change' },
{ template: ffChangeSkill, dirName: 'openspec-ff-change' },
];
const createdSkillFiles: string[] = [];
@@ -755,6 +837,7 @@ ${template.instructions}
{ template: newCommand, fileName: 'new.md' },
{ template: continueCommand, fileName: 'continue.md' },
{ template: applyCommand, fileName: 'apply.md' },
{ template: ffCommand, fileName: 'ff.md' },
];
const createdCommandFiles: string[] = [];
@@ -810,6 +893,7 @@ ${template.content}
console.log(' • /opsx:new - Start a new change');
console.log(' • /opsx:continue - Create the next artifact');
console.log(' • /opsx:apply - Implement tasks');
console.log(' • /opsx:ff - Fast-forward: create all artifacts at once');
console.log();
console.log(chalk.yellow('💡 This is an experimental feature.'));
console.log(' Feedback welcome at: https://github.com/Fission-AI/OpenSpec/issues');
@@ -820,6 +904,34 @@ ${template.content}
}
}
// -----------------------------------------------------------------------------
// Schemas Command
// -----------------------------------------------------------------------------
interface SchemasOptions {
json?: boolean;
}
async function schemasCommand(options: SchemasOptions): Promise<void> {
const schemas = listSchemasWithInfo();
if (options.json) {
console.log(JSON.stringify(schemas, null, 2));
return;
}
console.log('Available schemas:');
console.log();
for (const schema of schemas) {
const sourceLabel = schema.source === 'user' ? chalk.dim(' (user override)') : '';
console.log(` ${chalk.bold(schema.name)}${sourceLabel}`);
console.log(` ${schema.description}`);
console.log(` Artifacts: ${schema.artifacts.join(' → ')}`);
console.log();
}
}
// -----------------------------------------------------------------------------
// Command Registration
// -----------------------------------------------------------------------------
@@ -884,6 +996,21 @@ export function registerArtifactWorkflowCommands(program: Command): void {
}
});
// Schemas command
program
.command('schemas')
.description('[Experimental] List available workflow schemas with descriptions')
.option('--json', 'Output as JSON (for agent use)')
.action(async (options: SchemasOptions) => {
try {
await schemasCommand(options);
} catch (error) {
console.log();
ora().fail(`Error: ${(error as Error).message}`);
process.exit(1);
}
});
// New command group with change subcommand
const newCmd = program.command('new').description('[Experimental] Create new items');
+2
View File
@@ -21,10 +21,12 @@ export { detectCompleted } from './state.js';
export {
resolveSchema,
listSchemas,
listSchemasWithInfo,
getSchemaDir,
getPackageSchemasDir,
getUserSchemasDir,
SchemaLoadError,
type SchemaInfo,
} from './resolver.js';
// Instruction loading
@@ -99,6 +99,8 @@ export interface ChangeStatus {
schemaName: string;
/** Whether all artifacts are complete */
isComplete: boolean;
/** Artifact IDs required before apply phase (from schema's apply.requires) */
applyRequires: string[];
/** Status of each artifact */
artifacts: ArtifactStatus[];
}
@@ -252,6 +254,10 @@ function getUnlockedArtifacts(graph: ArtifactGraph, artifactId: string): string[
* @returns Formatted change status
*/
export function formatChangeStatus(context: ChangeContext): ChangeStatus {
// Load schema to get apply phase configuration
const schema = resolveSchema(context.schemaName);
const applyRequires = schema.apply?.requires ?? schema.artifacts.map(a => a.id);
const artifacts = context.graph.getAllArtifacts();
const ready = new Set(context.graph.getNextArtifacts(context.completed));
const blocked = context.graph.getBlocked(context.completed);
@@ -290,6 +296,7 @@ export function formatChangeStatus(context: ChangeContext): ChangeStatus {
changeName: context.changeName,
schemaName: context.schemaName,
isComplete: context.graph.isComplete(context.completed),
applyRequires,
artifacts: artifactStatuses,
};
}
+68
View File
@@ -156,3 +156,71 @@ export function listSchemas(): string[] {
return Array.from(schemas).sort();
}
/**
* Schema info with metadata (name, description, artifacts).
*/
export interface SchemaInfo {
name: string;
description: string;
artifacts: string[];
source: 'package' | 'user';
}
/**
* Lists all available schemas with their descriptions and artifact lists.
* Useful for agent skills to present schema selection to users.
*/
export function listSchemasWithInfo(): SchemaInfo[] {
const schemas: SchemaInfo[] = [];
const seenNames = new Set<string>();
// Add user override schemas first (they take precedence)
const userDir = getUserSchemasDir();
if (fs.existsSync(userDir)) {
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
try {
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
schemas.push({
name: entry.name,
description: schema.description || '',
artifacts: schema.artifacts.map((a) => a.id),
source: 'user',
});
seenNames.add(entry.name);
} catch {
// Skip invalid schemas
}
}
}
}
}
// Add package built-in schemas (if not overridden)
const packageDir = getPackageSchemasDir();
if (fs.existsSync(packageDir)) {
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
if (entry.isDirectory() && !seenNames.has(entry.name)) {
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
if (fs.existsSync(schemaPath)) {
try {
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
schemas.push({
name: entry.name,
description: schema.description || '',
artifacts: schema.artifacts.map((a) => a.id),
source: 'package',
});
} catch {
// Skip invalid schemas
}
}
}
}
}
return schemas.sort((a, b) => a.name.localeCompare(b.name));
}
+13
View File
@@ -10,16 +10,29 @@ export const ArtifactSchema = z.object({
requires: z.array(z.string()).default([]),
});
// Apply phase configuration for schema-aware apply instructions
export const ApplyPhaseSchema = z.object({
// Artifact IDs that must exist before apply is available
requires: z.array(z.string()).min(1, { error: 'At least one required artifact' }),
// Path to file with checkboxes for progress (relative to change dir), or null if no tracking
tracks: z.string().nullable().optional(),
// Custom guidance for the apply phase
instruction: z.string().optional(),
});
// Full schema YAML structure
export const SchemaYamlSchema = z.object({
name: z.string().min(1, { error: 'Schema name is required' }),
version: z.number().int().positive({ error: 'Version must be a positive integer' }),
description: z.string().optional(),
artifacts: z.array(ArtifactSchema).min(1, { error: 'At least one artifact required' }),
// Optional apply phase configuration (for schema-aware apply instructions)
apply: ApplyPhaseSchema.optional(),
});
// Derived TypeScript types
export type Artifact = z.infer<typeof ArtifactSchema>;
export type ApplyPhase = z.infer<typeof ApplyPhaseSchema>;
export type SchemaYaml = z.infer<typeof SchemaYamlSchema>;
// Per-change metadata schema
+357 -86
View File
@@ -37,40 +37,54 @@ export function getNewChangeSkillTemplate(): SkillTemplate {
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
\`\`\`bash
openspec new change "<name>"
\`\`\`
This creates a scaffolded change at \`openspec/changes/<name>/\`.
2. **Select a workflow schema**
3. **Show the artifact status**
Run \`openspec schemas --json\` to get available schemas with descriptions.
Use the **AskUserQuestion tool** to let the user choose a workflow:
- Present each schema with its description
- Mark \`spec-driven\` as "(default)" if it's available
- Example options: "spec-driven - proposal → specs → design → tasks (default)", "tdd - tests → implementation → docs"
If user doesn't have a preference, default to \`spec-driven\`.
3. **Create the change directory**
\`\`\`bash
openspec new change "<name>" --schema "<selected-schema>"
\`\`\`
This creates a scaffolded change at \`openspec/changes/<name>/\` with the selected schema.
4. **Show the artifact status**
\`\`\`bash
openspec status --change "<name>"
\`\`\`
This shows which artifacts need to be created and which are ready (dependencies satisfied).
4. **Get instructions for the first artifact**
The first artifact is always \`proposal\` (no dependencies).
5. **Get instructions for the first artifact**
The first artifact depends on the schema (e.g., \`proposal\` for spec-driven, \`spec\` for tdd).
Check the status output to find the first artifact with status "ready".
\`\`\`bash
openspec instructions proposal --change "<name>"
openspec instructions <first-artifact-id> --change "<name>"
\`\`\`
This outputs the template and context for creating the proposal.
This outputs the template and context for creating the first artifact.
5. **STOP and wait for user direction**
6. **STOP and wait for user direction**
**Output**
After completing the steps, summarize:
- Change name and location
- Current status (0/4 artifacts complete)
- The template for the proposal artifact
- Prompt: "Ready to create the proposal? Just describe what this change is about and I'll draft the proposal, or ask me to continue."
- Selected schema/workflow and its artifact sequence
- Current status (0/N artifacts complete)
- The template for the first artifact
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the proposal template
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
- If a change with that name already exists, suggest continuing that change instead`
- If a change with that name already exists, suggest continuing that change instead
- Always pass --schema to preserve the user's workflow choice`
};
}
@@ -94,6 +108,7 @@ export function getContinueChangeSkillTemplate(): SkillTemplate {
Present the top 3-4 most recently modified changes as options, showing:
- Change name
- Schema (from \`schema\` field if present, otherwise "spec-driven")
- Status (e.g., "0/5 tasks", "complete", "no tasks")
- How recently it was modified (from \`lastModified\` field)
@@ -105,7 +120,10 @@ export function getContinueChangeSkillTemplate(): SkillTemplate {
\`\`\`bash
openspec status --change "<name>" --json
\`\`\`
Parse the JSON to understand current state.
Parse the JSON to understand current state. The response includes:
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven", "tdd")
- \`artifacts\`: Array of artifacts with their status ("done", "ready", "blocked")
- \`isComplete\`: Boolean indicating if all artifacts are complete
3. **Act based on status**:
@@ -113,7 +131,7 @@ export function getContinueChangeSkillTemplate(): SkillTemplate {
**If all artifacts are complete (\`isComplete: true\`)**:
- Congratulate the user
- Show final status
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change or archive it."
- STOP
@@ -148,30 +166,39 @@ export function getContinueChangeSkillTemplate(): SkillTemplate {
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
- What artifacts are now unlocked
- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
**Artifact Creation Guidelines**
When filling in templates:
The artifact types and their purpose depend on the schema. Use the \`instruction\` field from the instructions output to understand what to create.
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- **IMPORTANT**: The Capabilities section is critical. Before filling it in:
- Check \`openspec/specs/\` for existing capabilities
- List new capabilities with kebab-case names (e.g., \`user-auth\`, \`data-export\`)
- List modified capabilities that need spec updates
- Each capability listed will need a corresponding spec file in the next phase.
- **specs/*.md**: Create one spec per capability listed in the proposal. Use \`specs/<capability-name>/spec.md\` path.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/*.md**: Create one spec per capability listed in the proposal.
- **design.md**: Document technical decisions, architecture, and implementation approach.
- **tasks.md**: Break down implementation into checkboxed tasks based on specs and design.
- **tasks.md**: Break down implementation into checkboxed tasks.
**tdd schema** (spec → tests → implementation → docs):
- **spec.md**: Feature specification defining what to build.
- **tests/*.test.ts**: Write tests BEFORE implementation (TDD red phase).
- **src/*.ts**: Implement to make tests pass (TDD green phase).
- **docs/*.md**: Document the implemented feature.
For other schemas, follow the \`instruction\` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- 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`
- Verify the artifact file exists after writing before marking progress
- Use the schema's artifact sequence, don't assume specific artifact names`
};
}
@@ -193,19 +220,28 @@ export function getApplyChangeSkillTemplate(): SkillTemplate {
Run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have tasks.md (implementation-ready).
Show changes that are implementation-ready (have tasks artifact).
Include the schema used for each change if available.
Mark changes with incomplete tasks as "(In Progress)".
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Get apply instructions**
2. **Check status to understand the schema**
\`\`\`bash
openspec status --change "<name>" --json
\`\`\`
Parse the JSON to understand:
- \`schemaName\`: The workflow being used (e.g., "spec-driven", "tdd")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
\`\`\`bash
openspec instructions apply --change "<name>" --json
\`\`\`
This returns:
- Context file paths (proposal, specs, design, tasks)
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
@@ -215,28 +251,29 @@ export function getApplyChangeSkillTemplate(): SkillTemplate {
- If \`state: "all_done"\`: congratulate, suggest archive
- Otherwise: proceed to implementation
3. **Read context files**
4. **Read context files**
Read the files listed in \`contextFiles\`:
- \`proposal\` - why and what
- \`specs\` - requirements and scenarios (use glob pattern to find all)
- \`design\` - technical approach (if exists)
- \`tasks\` - the implementation checklist
Read the files listed in \`contextFiles\` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- **tdd**: spec, tests, implementation, docs
- Other schemas: follow the contextFiles from CLI output
4. **Show current progress**
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
5. **Implement tasks (loop until done or blocked)**
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in tasks.md: \`- [ ]\` → \`- [x]\`
- Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
- Continue to next task
**Pause if:**
@@ -245,7 +282,7 @@ export function getApplyChangeSkillTemplate(): SkillTemplate {
- Error or blocker encountered → report and wait for guidance
- User interrupts
6. **On completion or pause, show status**
7. **On completion or pause, show status**
Display:
- Tasks completed this session
@@ -256,7 +293,7 @@ export function getApplyChangeSkillTemplate(): SkillTemplate {
**Output During Implementation**
\`\`\`
## Implementing: <change-name>
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
@@ -273,6 +310,7 @@ Working on task 4/7: <task description>
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
@@ -289,6 +327,7 @@ All tasks complete! Ready to archive this change.
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
@@ -304,22 +343,118 @@ What would you like to do?
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context before starting (specs, design)
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks.md exists), after partial implementation, interleaved with other actions
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly`
};
}
/**
* Template for openspec-ff-change skill
* Fast-forward through artifact creation
*/
export function getFfChangeSkillTemplate(): SkillTemplate {
return {
name: 'openspec-ff-change',
description: 'Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.',
instructions: `Fast-forward through artifact creation - generate everything needed to start implementation in one go.
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → \`add-user-auth\`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
\`\`\`bash
openspec new change "<name>"
\`\`\`
This creates a scaffolded change at \`openspec/changes/<name>/\`.
3. **Get the artifact build order**
\`\`\`bash
openspec status --change "<name>" --json
\`\`\`
Parse the JSON to get:
- \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`)
- \`artifacts\`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is \`ready\` (dependencies satisfied)**:
- Get instructions:
\`\`\`bash
openspec instructions <artifact-id> --change "<name>" --json
\`\`\`
- The instructions JSON includes:
- \`template\`: The template content to use
- \`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\`
- Show brief progress: "✓ Created <artifact-id>"
b. **Continue until all \`applyRequires\` artifacts are complete**
- After creating each artifact, re-run \`openspec status --change "<name>" --json\`
- Check if every artifact ID in \`applyRequires\` has \`status: "done"\` in the artifacts array
- Stop when all \`applyRequires\` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
\`\`\`bash
openspec status --change "<name>"
\`\`\`
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run \`/opsx:apply\` or ask me to implement to start working on the tasks."
**Artifact Creation Guidelines**
- 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
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's \`apply.requires\`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, suggest continuing that change instead
- Verify each artifact file exists after writing before proceeding to next`
};
}
// -----------------------------------------------------------------------------
// Slash Command Templates
// -----------------------------------------------------------------------------
@@ -356,40 +491,53 @@ export function getOpsxNewCommandTemplate(): CommandTemplate {
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
\`\`\`bash
openspec new change "<name>"
\`\`\`
This creates a scaffolded change at \`openspec/changes/<name>/\`.
2. **Select a workflow schema**
3. **Show the artifact status**
Run \`openspec schemas --json\` to get available schemas with descriptions.
Use the **AskUserQuestion tool** to let the user choose a workflow:
- Present each schema with its description
- Mark \`spec-driven\` as "(default)" if it's available
- Example options: "spec-driven - proposal → specs → design → tasks (default)", "tdd - tests → implementation → docs"
If user doesn't have a preference, default to \`spec-driven\`.
3. **Create the change directory**
\`\`\`bash
openspec new change "<name>" --schema "<selected-schema>"
\`\`\`
This creates a scaffolded change at \`openspec/changes/<name>/\` with the selected schema.
4. **Show the artifact status**
\`\`\`bash
openspec status --change "<name>"
\`\`\`
This shows which artifacts need to be created and which are ready (dependencies satisfied).
4. **Get instructions for the first artifact**
The first artifact is always \`proposal\` (no dependencies).
5. **Get instructions for the first artifact**
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
\`\`\`bash
openspec instructions proposal --change "<name>"
openspec instructions <first-artifact-id> --change "<name>"
\`\`\`
This outputs the template and context for creating the proposal.
This outputs the template and context for creating the first artifact.
5. **STOP and wait for user direction**
6. **STOP and wait for user direction**
**Output**
After completing the steps, summarize:
- Change name and location
- Current status (0/4 artifacts complete)
- The template for the proposal artifact
- Prompt: "Ready to create the proposal? Run \`/opsx:continue\` or just describe what this change is about and I'll draft the proposal."
- Selected schema/workflow and its artifact sequence
- Current status (0/N artifacts complete)
- The template for the first artifact
- Prompt: "Ready to create the first artifact? Run \`/opsx:continue\` or just describe what this change is about and I'll draft it."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the proposal template
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
- If a change with that name already exists, suggest using \`/opsx:continue\` instead`
- If a change with that name already exists, suggest using \`/opsx:continue\` instead
- Always pass --schema to preserve the user's workflow choice`
};
}
@@ -414,6 +562,7 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate {
Present the top 3-4 most recently modified changes as options, showing:
- Change name
- Schema (from \`schema\` field if present, otherwise "spec-driven")
- Status (e.g., "0/5 tasks", "complete", "no tasks")
- How recently it was modified (from \`lastModified\` field)
@@ -425,7 +574,10 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate {
\`\`\`bash
openspec status --change "<name>" --json
\`\`\`
Parse the JSON to understand current state.
Parse the JSON to understand current state. The response includes:
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven", "tdd")
- \`artifacts\`: Array of artifacts with their status ("done", "ready", "blocked")
- \`isComplete\`: Boolean indicating if all artifacts are complete
3. **Act based on status**:
@@ -433,7 +585,7 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate {
**If all artifacts are complete (\`isComplete: true\`)**:
- Congratulate the user
- Show final status
- Show final status including the schema used
- Suggest: "All artifacts created! You can now implement this change or archive it."
- STOP
@@ -468,30 +620,39 @@ export function getOpsxContinueCommandTemplate(): CommandTemplate {
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
- What artifacts are now unlocked
- Prompt: "Run \`/opsx:continue\` to create the next artifact"
**Artifact Creation Guidelines**
When filling in templates:
The artifact types and their purpose depend on the schema. Use the \`instruction\` field from the instructions output to understand what to create.
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- **IMPORTANT**: The Capabilities section is critical. Before filling it in:
- Check \`openspec/specs/\` for existing capabilities
- List new capabilities with kebab-case names (e.g., \`user-auth\`, \`data-export\`)
- List modified capabilities that need spec updates
- Each capability listed will need a corresponding spec file in the next phase.
- **specs/*.md**: Create one spec per capability listed in the proposal. Use \`specs/<capability-name>/spec.md\` path.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/*.md**: Create one spec per capability listed in the proposal.
- **design.md**: Document technical decisions, architecture, and implementation approach.
- **tasks.md**: Break down implementation into checkboxed tasks based on specs and design.
- **tasks.md**: Break down implementation into checkboxed tasks.
**tdd schema** (spec → tests → implementation → docs):
- **spec.md**: Feature specification defining what to build.
- **tests/*.test.ts**: Write tests BEFORE implementation (TDD red phase).
- **src/*.ts**: Implement to make tests pass (TDD green phase).
- **docs/*.md**: Document the implemented feature.
For other schemas, follow the \`instruction\` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- 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`
- Verify the artifact file exists after writing before marking progress
- Use the schema's artifact sequence, don't assume specific artifact names`
};
}
@@ -514,19 +675,28 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate {
Run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show changes that have tasks.md (implementation-ready).
Show changes that are implementation-ready (have tasks artifact).
Include the schema used for each change if available.
Mark changes with incomplete tasks as "(In Progress)".
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Get apply instructions**
2. **Check status to understand the schema**
\`\`\`bash
openspec status --change "<name>" --json
\`\`\`
Parse the JSON to understand:
- \`schemaName\`: The workflow being used (e.g., "spec-driven", "tdd")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
3. **Get apply instructions**
\`\`\`bash
openspec instructions apply --change "<name>" --json
\`\`\`
This returns:
- Context file paths (proposal, specs, design, tasks)
- Context file paths (varies by schema)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
@@ -536,28 +706,29 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate {
- If \`state: "all_done"\`: congratulate, suggest archive
- Otherwise: proceed to implementation
3. **Read context files**
4. **Read context files**
Read the files listed in \`contextFiles\`:
- \`proposal\` - why and what
- \`specs\` - requirements and scenarios (use glob pattern to find all)
- \`design\` - technical approach (if exists)
- \`tasks\` - the implementation checklist
Read the files listed in \`contextFiles\` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- **tdd**: spec, tests, implementation, docs
- Other schemas: follow the contextFiles from CLI output
4. **Show current progress**
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
5. **Implement tasks (loop until done or blocked)**
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in tasks.md: \`- [ ]\` → \`- [x]\`
- Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
- Continue to next task
**Pause if:**
@@ -566,7 +737,7 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate {
- Error or blocker encountered → report and wait for guidance
- User interrupts
6. **On completion or pause, show status**
7. **On completion or pause, show status**
Display:
- Tasks completed this session
@@ -577,7 +748,7 @@ export function getOpsxApplyCommandTemplate(): CommandTemplate {
**Output During Implementation**
\`\`\`
## Implementing: <change-name>
## Implementing: <change-name> (schema: <schema-name>)
Working on task 3/7: <task description>
[...implementation happening...]
@@ -594,6 +765,7 @@ Working on task 4/7: <task description>
## Implementation Complete
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 7/7 tasks complete ✓
### Completed This Session
@@ -610,6 +782,7 @@ All tasks complete! Ready to archive this change.
## Implementation Paused
**Change:** <change-name>
**Schema:** <schema-name>
**Progress:** 4/7 tasks complete
### Issue Encountered
@@ -625,18 +798,116 @@ What would you like to do?
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context before starting (specs, design)
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
- If implementation reveals issues, pause and suggest artifact updates
- Keep code changes minimal and scoped to each task
- Update task checkbox immediately after completing each task
- Pause on errors, blockers, or unclear requirements - don't guess
- Use contextFiles from CLI output, don't assume specific file names
**Fluid Workflow Integration**
This skill supports the "actions on a change" model:
- **Can be invoked anytime**: Before all artifacts are done (if tasks.md exists), after partial implementation, interleaved with other actions
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly`
};
}
/**
* Template for /opsx:ff slash command
*/
export function getOpsxFfCommandTemplate(): CommandTemplate {
return {
name: 'OPSX: Fast Forward',
description: 'Create a change and generate all artifacts needed for implementation in one go',
category: 'Workflow',
tags: ['workflow', 'artifacts', 'experimental'],
content: `Fast-forward through artifact creation - generate everything needed to start implementation.
**Input**: The argument after \`/opsx:ff\` is the change name (kebab-case), OR a description of what the user wants to build.
**Steps**
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → \`add-user-auth\`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
\`\`\`bash
openspec new change "<name>"
\`\`\`
This creates a scaffolded change at \`openspec/changes/<name>/\`.
3. **Get the artifact build order**
\`\`\`bash
openspec status --change "<name>" --json
\`\`\`
Parse the JSON to get:
- \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`)
- \`artifacts\`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is \`ready\` (dependencies satisfied)**:
- Get instructions:
\`\`\`bash
openspec instructions <artifact-id> --change "<name>" --json
\`\`\`
- The instructions JSON includes:
- \`template\`: The template content to use
- \`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\`
- Show brief progress: "✓ Created <artifact-id>"
b. **Continue until all \`applyRequires\` artifacts are complete**
- After creating each artifact, re-run \`openspec status --change "<name>" --json\`
- Check if every artifact ID in \`applyRequires\` has \`status: "done"\` in the artifacts array
- Stop when all \`applyRequires\` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
\`\`\`bash
openspec status --change "<name>"
\`\`\`
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
- Prompt: "Run \`/opsx:apply\` to start implementing."
**Artifact Creation Guidelines**
- 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
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's \`apply.requires\`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
- If a change with that name already exists, ask if user wants to continue it or create a new one
- Verify each artifact file exists after writing before proceeding to next`
};
}
+203
View File
@@ -348,6 +348,209 @@ describe('artifact-workflow CLI commands', () => {
});
});
describe('instructions apply command', () => {
it('shows apply instructions for spec-driven schema with tasks', async () => {
await createTestChange('apply-change', ['proposal', 'design', 'specs', 'tasks']);
const result = await runCLI(['instructions', 'apply', '--change', 'apply-change'], {
cwd: tempDir,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('## Apply: apply-change');
expect(result.stdout).toContain('Schema: spec-driven');
expect(result.stdout).toContain('### Context Files');
expect(result.stdout).toContain('### Instruction');
});
it('shows blocked state when required artifacts are missing', async () => {
// Only create proposal - missing tasks (required by spec-driven apply block)
await createTestChange('blocked-apply', ['proposal']);
const result = await runCLI(['instructions', 'apply', '--change', 'blocked-apply'], {
cwd: tempDir,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Blocked');
expect(result.stdout).toContain('Missing artifacts: tasks');
});
it('outputs JSON for apply instructions', async () => {
await createTestChange('json-apply', ['proposal', 'design', 'specs', 'tasks']);
const result = await runCLI(
['instructions', 'apply', '--change', 'json-apply', '--json'],
{ cwd: tempDir }
);
expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.changeName).toBe('json-apply');
expect(json.schemaName).toBe('spec-driven');
expect(json.state).toBe('ready');
expect(json.contextFiles).toBeDefined();
expect(typeof json.contextFiles).toBe('object');
});
it('shows schema instruction from apply block', async () => {
await createTestChange('instr-apply', ['proposal', 'design', 'specs', 'tasks']);
const result = await runCLI(['instructions', 'apply', '--change', 'instr-apply'], {
cwd: tempDir,
});
expect(result.exitCode).toBe(0);
// Should show the instruction from spec-driven schema apply block
expect(result.stdout).toContain('work through pending tasks');
});
it('shows all_done state when all tasks are complete', async () => {
const changeDir = await createTestChange('done-apply', [
'proposal',
'design',
'specs',
'tasks',
]);
// Overwrite tasks with all completed
await fs.writeFile(
path.join(changeDir, 'tasks.md'),
'## Tasks\n- [x] Task 1\n- [x] Task 2'
);
const result = await runCLI(['instructions', 'apply', '--change', 'done-apply'], {
cwd: tempDir,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('complete ✓');
expect(result.stdout).toContain('ready to be archived');
});
it('uses tdd schema apply configuration', async () => {
// Create a TDD-style change with spec and tests
const changeDir = path.join(changesDir, 'tdd-apply');
await fs.mkdir(changeDir, { recursive: true });
await fs.writeFile(path.join(changeDir, 'spec.md'), '## Feature\nTest spec.');
const testsDir = path.join(changeDir, 'tests');
await fs.mkdir(testsDir, { recursive: true });
await fs.writeFile(path.join(testsDir, 'test.test.ts'), 'test("works", () => {})');
const result = await runCLI(
['instructions', 'apply', '--change', 'tdd-apply', '--schema', 'tdd'],
{ cwd: tempDir }
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Schema: tdd');
// TDD schema has no task tracking, so should show schema instruction
expect(result.stdout).toContain('Run tests to see failures');
});
it('spec-driven schema uses apply block configuration', async () => {
// Verify that spec-driven schema uses its apply block (requires: [tasks])
await createTestChange('apply-config-test', ['proposal', 'design', 'specs', 'tasks']);
const result = await runCLI(
['instructions', 'apply', '--change', 'apply-config-test', '--json'],
{ cwd: tempDir }
);
expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
// spec-driven schema has apply block with requires: [tasks], so should be ready
expect(json.schemaName).toBe('spec-driven');
expect(json.state).toBe('ready');
});
it('fallback: requires all artifacts when schema has no apply block', async () => {
// Create a minimal schema without an apply block in user schemas dir
const userDataDir = path.join(tempDir, 'user-data');
const noApplySchemaDir = path.join(userDataDir, 'openspec', 'schemas', 'no-apply');
const templatesDir = path.join(noApplySchemaDir, 'templates');
await fs.mkdir(templatesDir, { recursive: true });
// Minimal schema with 2 artifacts, no apply block
const schemaContent = `
name: no-apply
version: 1
description: Test schema without apply block
artifacts:
- id: first
generates: first.md
description: First artifact
template: first.md
requires: []
- id: second
generates: second.md
description: Second artifact
template: second.md
requires: [first]
`;
await fs.writeFile(path.join(noApplySchemaDir, 'schema.yaml'), schemaContent);
await fs.writeFile(path.join(templatesDir, 'first.md'), '# First\n');
await fs.writeFile(path.join(templatesDir, 'second.md'), '# Second\n');
// Create a change with only the first artifact (missing second)
const changeDir = path.join(changesDir, 'no-apply-test');
await fs.mkdir(changeDir, { recursive: true });
await fs.writeFile(path.join(changeDir, 'first.md'), '# First artifact content');
// Run with XDG_DATA_HOME pointing to our temp user data dir
const result = await runCLI(
['instructions', 'apply', '--change', 'no-apply-test', '--schema', 'no-apply', '--json'],
{
cwd: tempDir,
env: { XDG_DATA_HOME: userDataDir },
}
);
expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
// Without apply block, fallback requires ALL artifacts - second is missing
expect(json.schemaName).toBe('no-apply');
expect(json.state).toBe('blocked');
expect(json.missingArtifacts).toContain('second');
});
it('fallback: ready when all artifacts exist for schema without apply block', async () => {
// Create a minimal schema without an apply block
const userDataDir = path.join(tempDir, 'user-data-2');
const noApplySchemaDir = path.join(userDataDir, 'openspec', 'schemas', 'no-apply-full');
const templatesDir = path.join(noApplySchemaDir, 'templates');
await fs.mkdir(templatesDir, { recursive: true });
const schemaContent = `
name: no-apply-full
version: 1
description: Test schema without apply block
artifacts:
- id: only
generates: only.md
description: Only artifact
template: only.md
requires: []
`;
await fs.writeFile(path.join(noApplySchemaDir, 'schema.yaml'), schemaContent);
await fs.writeFile(path.join(templatesDir, 'only.md'), '# Only\n');
// Create a change with the artifact present
const changeDir = path.join(changesDir, 'no-apply-full-test');
await fs.mkdir(changeDir, { recursive: true });
await fs.writeFile(path.join(changeDir, 'only.md'), '# Content');
const result = await runCLI(
['instructions', 'apply', '--change', 'no-apply-full-test', '--schema', 'no-apply-full', '--json'],
{
cwd: tempDir,
env: { XDG_DATA_HOME: userDataDir },
}
);
expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
// All artifacts exist, should be ready with default instruction
expect(json.schemaName).toBe('no-apply-full');
expect(json.state).toBe('ready');
expect(json.instruction).toContain('All required artifacts complete');
});
});
describe('help text', () => {
it('marks status command as experimental in help', async () => {
const result = await runCLI(['status', '--help']);