mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15f8fe1d61 | ||
|
|
d73705736f | ||
|
|
ed924ffcff |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user