Skip to content

Commit ed924ff

Browse files
authored
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.
1 parent 1786684 commit ed924ff

File tree

5 files changed

+291
-97
lines changed

5 files changed

+291
-97
lines changed
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
## Prerequisites
22

3-
- [ ] 0.1 Implement `add-per-change-schema-metadata` change first
3+
- [x] 0.1 Implement `add-per-change-schema-metadata` change first
44

55
## 1. Schema Discovery
66

7-
- [ ] 1.1 Add CLI command or helper to list schemas with descriptions (for agent use)
8-
- [ ] 1.2 Ensure `openspec templates --schema <name>` returns artifact list for any schema
7+
- [x] 1.1 Add CLI command or helper to list schemas with descriptions (for agent use)
8+
- [x] 1.2 Ensure `openspec templates --schema <name>` returns artifact list for any schema
99

1010
## 2. Update New Change Skill
1111

12-
- [ ] 2.1 Add schema selection prompt using AskUserQuestion tool
13-
- [ ] 2.2 Present available schemas with descriptions (spec-driven, tdd, etc.)
14-
- [ ] 2.3 Pass selected schema to `openspec new change --schema <name>`
15-
- [ ] 2.4 Update output to show which schema/workflow was selected
12+
- [x] 2.1 Add schema selection prompt using AskUserQuestion tool
13+
- [x] 2.2 Present available schemas with descriptions (spec-driven, tdd, etc.)
14+
- [x] 2.3 Pass selected schema to `openspec new change --schema <name>`
15+
- [x] 2.4 Update output to show which schema/workflow was selected
1616

1717
## 3. Update Continue Change Skill
1818

19-
- [ ] 3.1 Remove hardcoded artifact references (proposal, specs, design, tasks)
20-
- [ ] 3.2 Read artifact list dynamically from `openspec status --json`
21-
- [ ] 3.3 Adjust artifact creation guidelines to be schema-agnostic
22-
- [ ] 3.4 Handle schema-specific artifact types (e.g., TDD's `tests` artifact)
19+
- [x] 3.1 Remove hardcoded artifact references (proposal, specs, design, tasks)
20+
- [x] 3.2 Read artifact list dynamically from `openspec status --json`
21+
- [x] 3.3 Adjust artifact creation guidelines to be schema-agnostic
22+
- [x] 3.4 Handle schema-specific artifact types (e.g., TDD's `tests` artifact)
2323

2424
## 4. Update Apply Change Skill
2525

26-
- [ ] 4.1 Make task detection work with different schema structures
27-
- [ ] 4.2 Adjust context file reading for schema-specific artifacts
26+
- [x] 4.1 Make task detection work with different schema structures
27+
- [x] 4.2 Adjust context file reading for schema-specific artifacts
2828

2929
## 5. Documentation
3030

31-
- [ ] 5.1 Add schema descriptions to help text or skill instructions
32-
- [ ] 5.2 Document when to use each schema (TDD for bug fixes, spec-driven for features, etc.)
31+
- [x] 5.1 Add schema descriptions to help text or skill instructions
32+
- [x] 5.2 Document when to use each schema (TDD for bug fixes, spec-driven for features, etc.)

src/commands/artifact-workflow.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import {
1919
formatChangeStatus,
2020
generateInstructions,
2121
listSchemas,
22+
listSchemasWithInfo,
2223
getSchemaDir,
2324
resolveSchema,
2425
ArtifactGraph,
2526
type ChangeStatus,
2627
type ArtifactInstructions,
28+
type SchemaInfo,
2729
} from '../core/artifact-graph/index.js';
2830
import { createChange, validateChangeName } from '../utils/change-utils.js';
2931
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate } from '../core/templates/skill-templates.js';
@@ -820,6 +822,34 @@ ${template.content}
820822
}
821823
}
822824

825+
// -----------------------------------------------------------------------------
826+
// Schemas Command
827+
// -----------------------------------------------------------------------------
828+
829+
interface SchemasOptions {
830+
json?: boolean;
831+
}
832+
833+
async function schemasCommand(options: SchemasOptions): Promise<void> {
834+
const schemas = listSchemasWithInfo();
835+
836+
if (options.json) {
837+
console.log(JSON.stringify(schemas, null, 2));
838+
return;
839+
}
840+
841+
console.log('Available schemas:');
842+
console.log();
843+
844+
for (const schema of schemas) {
845+
const sourceLabel = schema.source === 'user' ? chalk.dim(' (user override)') : '';
846+
console.log(` ${chalk.bold(schema.name)}${sourceLabel}`);
847+
console.log(` ${schema.description}`);
848+
console.log(` Artifacts: ${schema.artifacts.join(' → ')}`);
849+
console.log();
850+
}
851+
}
852+
823853
// -----------------------------------------------------------------------------
824854
// Command Registration
825855
// -----------------------------------------------------------------------------
@@ -884,6 +914,21 @@ export function registerArtifactWorkflowCommands(program: Command): void {
884914
}
885915
});
886916

917+
// Schemas command
918+
program
919+
.command('schemas')
920+
.description('[Experimental] List available workflow schemas with descriptions')
921+
.option('--json', 'Output as JSON (for agent use)')
922+
.action(async (options: SchemasOptions) => {
923+
try {
924+
await schemasCommand(options);
925+
} catch (error) {
926+
console.log();
927+
ora().fail(`Error: ${(error as Error).message}`);
928+
process.exit(1);
929+
}
930+
});
931+
887932
// New command group with change subcommand
888933
const newCmd = program.command('new').description('[Experimental] Create new items');
889934

src/core/artifact-graph/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ export { detectCompleted } from './state.js';
2121
export {
2222
resolveSchema,
2323
listSchemas,
24+
listSchemasWithInfo,
2425
getSchemaDir,
2526
getPackageSchemasDir,
2627
getUserSchemasDir,
2728
SchemaLoadError,
29+
type SchemaInfo,
2830
} from './resolver.js';
2931

3032
// Instruction loading

src/core/artifact-graph/resolver.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,71 @@ export function listSchemas(): string[] {
156156

157157
return Array.from(schemas).sort();
158158
}
159+
160+
/**
161+
* Schema info with metadata (name, description, artifacts).
162+
*/
163+
export interface SchemaInfo {
164+
name: string;
165+
description: string;
166+
artifacts: string[];
167+
source: 'package' | 'user';
168+
}
169+
170+
/**
171+
* Lists all available schemas with their descriptions and artifact lists.
172+
* Useful for agent skills to present schema selection to users.
173+
*/
174+
export function listSchemasWithInfo(): SchemaInfo[] {
175+
const schemas: SchemaInfo[] = [];
176+
const seenNames = new Set<string>();
177+
178+
// Add user override schemas first (they take precedence)
179+
const userDir = getUserSchemasDir();
180+
if (fs.existsSync(userDir)) {
181+
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
182+
if (entry.isDirectory()) {
183+
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
184+
if (fs.existsSync(schemaPath)) {
185+
try {
186+
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
187+
schemas.push({
188+
name: entry.name,
189+
description: schema.description || '',
190+
artifacts: schema.artifacts.map((a) => a.id),
191+
source: 'user',
192+
});
193+
seenNames.add(entry.name);
194+
} catch {
195+
// Skip invalid schemas
196+
}
197+
}
198+
}
199+
}
200+
}
201+
202+
// Add package built-in schemas (if not overridden)
203+
const packageDir = getPackageSchemasDir();
204+
if (fs.existsSync(packageDir)) {
205+
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
206+
if (entry.isDirectory() && !seenNames.has(entry.name)) {
207+
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
208+
if (fs.existsSync(schemaPath)) {
209+
try {
210+
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
211+
schemas.push({
212+
name: entry.name,
213+
description: schema.description || '',
214+
artifacts: schema.artifacts.map((a) => a.id),
215+
source: 'package',
216+
});
217+
} catch {
218+
// Skip invalid schemas
219+
}
220+
}
221+
}
222+
}
223+
}
224+
225+
return schemas.sort((a, b) => a.name.localeCompare(b.name));
226+
}

0 commit comments

Comments
 (0)