Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ program
.command('init [path]')
.description('Initialize OpenSpec in your project')
.option('--tools <tools>', toolsOptionDescription)
.action(async (targetPath = '.', options?: { tools?: string }) => {
.option('--templates', 'Generate change templates in openspec/templates/')
.action(async (targetPath = '.', options?: { tools?: string; templates?: boolean }) => {
try {
// Validate that the path is a valid directory
const resolvedPath = path.resolve(targetPath);
Expand All @@ -64,6 +65,7 @@ program

const initCommand = new InitCommand({
tools: options?.tools,
templates: options?.templates,
});
await initCommand.execute(targetPath);
} catch (error) {
Expand Down
29 changes: 28 additions & 1 deletion src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import chalk from 'chalk';
import ora from 'ora';
import { FileSystemUtils } from '../utils/file-system.js';
import { TemplateManager, ProjectContext } from './templates/index.js';
import { TemplateManager, ProjectContext, ChangeTemplateManager } from './templates/index.js';
import { ToolRegistry } from './configurators/registry.js';
import { SlashCommandRegistry } from './configurators/slash/registry.js';
import {
Expand Down Expand Up @@ -371,15 +371,18 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
type InitCommandOptions = {
prompt?: ToolSelectionPrompt;
tools?: string;
templates?: boolean;
};

export class InitCommand {
private readonly prompt: ToolSelectionPrompt;
private readonly toolsArg?: string;
private readonly generateTemplates?: boolean;

constructor(options: InitCommandOptions = {}) {
this.prompt = options.prompt ?? ((config) => toolSelectionWizard(config));
this.toolsArg = options.tools;
this.generateTemplates = options.templates;
}

async execute(targetPath: string): Promise<void> {
Expand Down Expand Up @@ -435,6 +438,9 @@ export class InitCommand {
await this.ensureTemplateFiles(openspecPath, config);
}

// Step 1.5: Optionally generate change templates
await this.maybeGenerateChangeTemplates(openspecPath);

// Step 2: Configure AI tools
const toolSpinner = this.startSpinner('Configuring AI tools...');
const rootStubStatus = await this.configureAITools(
Expand Down Expand Up @@ -718,6 +724,27 @@ export class InitCommand {
}
}

private async maybeGenerateChangeTemplates(openspecPath: string): Promise<void> {
// Only generate if explicitly requested via --templates flag
if (!this.generateTemplates) {
return;
}

// Skip if templates already exist
const hasTemplates = await ChangeTemplateManager.hasCustomTemplates(openspecPath);
if (hasTemplates) {
ora({ stream: process.stdout }).info(
PALETTE.midGray('ℹ Templates directory already exists, skipping template generation.')
);
return;
}

await ChangeTemplateManager.writeDefaultTemplates(openspecPath);
ora({ stream: process.stdout }).succeed(
PALETTE.white('Change templates generated in openspec/templates/')
);
}

private async generateFiles(
openspecPath: string,
config: OpenSpecConfig
Expand Down
32 changes: 29 additions & 3 deletions src/core/templates/agents-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ Skip proposal for:

**Workflow**
1. Review \`openspec/project.md\`, \`openspec list\`, and \`openspec list --specs\` to understand current context.
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, optional \`design.md\`, and spec deltas under \`openspec/changes/<id>/\`.
3. Draft spec deltas using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement.
4. Run \`openspec validate <id> --strict\` and resolve any issues before sharing the proposal.
2. Check for custom templates: If \`openspec/templates/\` directory exists, use those templates when creating files (see Custom Templates section below).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jax-max I try and reduce the amount of decisions an LLM has to make and try and make decisions here programatic if possible.

I have something in mind on how to acccomplish this by creating an openspec scaffold command that sets up default templates for an Agent to fill out.

Check it out here: https://github.com/Fission-AI/OpenSpec/tree/main/openspec/changes/add-scaffold-command

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @TabishB I've seen the changes related to the scaffold – the last update was two months ago, so I'm not sure if the plan is still moving forward.
Personally, I think the scaffold should be project-scoped rather than change-scoped. Therefore, it would be better to handle template initialization during the openspec init phase: first generate the spec, task, design, and proposal templates, then modify these templates to meet the project's specific requirements. After that, all subsequent changes should adhere to these pre-configured templates.
I’ve also thought about making decisions programmatic if possible, but I don’t want to introduce additional commands (like bash or PowerShell) the way spec-kit does.
Do you have any better suggestions? Feel free to share them – I’m open to discussing this further!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve also considered the spec file customization risk – We need to restrict users from modifying tags in spec deltas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TabishB By the way, I’ve already dropped you a message on Discord, PTAL , hh

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jax-max, I think we're on the same page here. The scaffold should ideally be project-scoped not change scoped, I agree!

The openspec scaffold should generate whatever the project defined template is (Should default to the standard OpenSpec template structure if it has not been modified.)

We can store this somewhere in openspec/tempalate/*

The job of the scaffold command will be to just make a copy of the template into whatever change directory. (So should take as input).

We then need to update the instructions to ask the agent to "fill in the documents" after creation.

I'll try and see if I can create a sequence diagram for this. We just need to be a bit careful with this change and we could possibly degrade the performance/quality of spec generation for current users.

If there's another way to test this out without affecting the core flow, I'd be keen to integrate that first.

I'll work on the scaffold tomorrow and you can let me know if it works the way you expect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @TabishB , That's awesome! I'm really looking forward to the scaffold feature. Regarding "asking the agent to 'fill in the documents' after creation", we actually prefer to handle this step manually. For large-scale backend projects, the agent’s analysis hasn’t been as effective as expected. Anyway, I’m still super excited for the scaffold to go live! Should I close the current PR?

3. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, optional \`design.md\`, and spec deltas under \`openspec/changes/<id>/\`.
4. Draft spec deltas using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement.
5. Run \`openspec validate <id> --strict\` and resolve any issues before sharing the proposal.

### Stage 2: Implementing Changes
Track these steps as TODOs and complete them one by one.
Expand Down Expand Up @@ -154,6 +155,31 @@ New request?
└─ Unclear? → Create proposal (safer)
\`\`\`

### Custom Templates

Projects can customize change file templates by creating \`openspec/templates/\` directory. To generate default templates, run:

\`\`\`bash
openspec init --templates
\`\`\`

This creates template files that you can edit:
- \`proposal.md.template\`
- \`tasks.md.template\`
- \`design.md.template\`
- \`spec.md.template\`

When creating change files, the system will:

- **Check for templates**: Before creating files, check if \`openspec/templates/\` exists
- **Use custom templates**: If template files exist, use them instead of defaults
- **Variable replacement**: Replace placeholders in templates:
- \`{{changeId}}\` → actual change-id (e.g., \`add-user-auth\`)
- \`{{date}}\` → current date in ISO format (e.g., \`2025-11-14\`)
- \`{{capability}}\` → capability name (only in spec.md, e.g., \`user-auth\`)
- **Fallback**: If template file doesn't exist, use the default structure shown below
- **Spec template validation**: Custom \`spec.md.template\` must include required tags (\`## ADDED Requirements\`, \`### Requirement:\`, \`#### Scenario:\`) or it will fall back to default

### Proposal Structure

1. **Create directory:** \`changes/[change-id]/\` (kebab-case, verb-led, unique)
Expand Down
192 changes: 192 additions & 0 deletions src/core/templates/change-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { promises as fs } from 'fs';
import path from 'path';

export type ChangeTemplateType = 'proposal' | 'tasks' | 'design' | 'spec';

export interface TemplateContext {
changeId?: string;
date?: string;
capability?: string;
}

/**
* Default templates for change files
*/
const defaultTemplates: Record<ChangeTemplateType, string> = {
proposal: `## Why
TODO: Explain why {{changeId}} is needed

## What Changes
- TODO: List changes
- Created on {{date}}

## Impact
- Affected specs: TODO
- Affected code: TODO`,

tasks: `## 1. Implementation
- [ ] 1.1 TODO: First task for {{changeId}}
- [ ] 1.2 TODO: Second task`,

design: `## Context
TODO: Background and constraints for {{changeId}}

## Goals / Non-Goals
- Goals: TODO
- Non-Goals: TODO

## Decisions
TODO: Technical decisions and rationale

## Risks / Trade-offs
TODO: Risks and mitigation strategies`,

spec: `## ADDED Requirements
### Requirement: [Replace with actual requirement name]
The system SHALL [describe the requirement].

#### Scenario: [Replace with scenario name]
- **WHEN** [condition]
- **THEN** [expected result]`,
};

/**
* Validates that a spec template contains required tags for archive to work correctly
*/
function validateSpecTemplate(content: string): { valid: boolean; errors: string[] } {
const errors: string[] = [];

// Check for at least one delta section
const hasDeltaSection = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements/m.test(content);
if (!hasDeltaSection) {
errors.push('Missing required delta section (## ADDED Requirements, ## MODIFIED Requirements, etc.)');
}

// Check for Requirement header
const hasRequirement = /^###\s+Requirement:/m.test(content);
if (!hasRequirement) {
errors.push('Missing required ### Requirement: header');
}

// Check for Scenario header
const hasScenario = /^####\s+Scenario:/m.test(content);
if (!hasScenario) {
errors.push('Missing required #### Scenario: header');
}

return {
valid: errors.length === 0,
errors,
};
}

/**
* Replaces template variables with actual values
*/
function replaceVariables(content: string, context: TemplateContext): string {
let result = content;

if (context.changeId !== undefined) {
result = result.replace(/\{\{changeId\}\}/g, context.changeId);
}

if (context.date !== undefined) {
result = result.replace(/\{\{date\}\}/g, context.date);
}

if (context.capability !== undefined) {
result = result.replace(/\{\{capability\}\}/g, context.capability);
}

return result;
}

/**
* Manages change file templates with support for custom templates and variable substitution
*/
export class ChangeTemplateManager {
/**
* Loads a template from file system or returns default template
* @param openspecDir Path to openspec directory
* @param type Template type
* @param context Variables to replace in template
* @returns Rendered template content
*/
static async loadTemplate(
openspecDir: string,
type: ChangeTemplateType,
context: TemplateContext = {}
): Promise<string> {
const templatePath = path.join(openspecDir, 'templates', `${type}.md.template`);

let content: string;
let isCustom = false;

try {
// Try to load custom template
content = await fs.readFile(templatePath, 'utf-8');
isCustom = true;
} catch {
// File doesn't exist, use default
content = defaultTemplates[type];
}

// Validate spec template if it's custom
if (isCustom && type === 'spec') {
const validation = validateSpecTemplate(content);
if (!validation.valid) {
console.warn(
`Warning: Custom spec template at ${templatePath} is missing required tags:\n ${validation.errors.join('\n ')}\n Falling back to default template.`
);
content = defaultTemplates[type];
}
}

// Replace variables
const rendered = replaceVariables(content, {
changeId: context.changeId || '',
date: context.date || new Date().toISOString().split('T')[0],
capability: context.capability || '',
});

return rendered;
}

/**
* Gets the default template content for a given type
* @param type Template type
* @returns Default template content
*/
static getDefaultTemplate(type: ChangeTemplateType): string {
return defaultTemplates[type];
}

/**
* Checks if custom templates directory exists
* @param openspecDir Path to openspec directory
* @returns True if templates directory exists
*/
static async hasCustomTemplates(openspecDir: string): Promise<boolean> {
const templatesDir = path.join(openspecDir, 'templates');
try {
const stat = await fs.stat(templatesDir);
return stat.isDirectory();
} catch {
return false;
}
}

/**
* Writes default templates to the templates directory
* @param openspecDir Path to openspec directory
*/
static async writeDefaultTemplates(openspecDir: string): Promise<void> {
const templatesDir = path.join(openspecDir, 'templates');
await fs.mkdir(templatesDir, { recursive: true });

for (const [type, content] of Object.entries(defaultTemplates)) {
const templatePath = path.join(templatesDir, `${type}.md.template`);
await fs.writeFile(templatePath, content, 'utf-8');
}
}
}
1 change: 1 addition & 0 deletions src/core/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export class TemplateManager {

export { ProjectContext } from './project-template.js';
export type { SlashCommandId } from './slash-command-templates.js';
export { ChangeTemplateManager, type ChangeTemplateType, type TemplateContext } from './change-templates.js';
35 changes: 27 additions & 8 deletions src/core/templates/slash-command-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,41 @@ const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous

const proposalSteps = `**Steps**
1. Review \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\`, and inspect related code or docs (e.g., via \`rg\`/\`ls\`) to ground the proposal in current behaviour; note any gaps that require clarification.
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
5. Draft spec deltas in \`changes/<id>/specs/<capability>/spec.md\` (one folder per capability) using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
7. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
2. Choose a unique verb-led \`change-id\` and create the change directory \`openspec/changes/<id>/\`.
3. Create \`proposal.md\` in \`openspec/changes/<id>/\`:
- **CRITICAL**: Check if \`openspec/templates/proposal.md.template\` exists
- If exists: Read the template file and replace variables (\`{{changeId}}\` → actual change-id, \`{{date}}\` → current date in YYYY-MM-DD format)
- If not exists: Use the default structure from \`openspec/AGENTS.md\` (see "Proposal Structure" section)
4. Create \`tasks.md\` in \`openspec/changes/<id>/\`:
- **CRITICAL**: Check if \`openspec/templates/tasks.md.template\` exists
- If exists: Read the template file and replace variables (\`{{changeId}}\` → actual change-id, \`{{date}}\` → current date in YYYY-MM-DD format)
- If not exists: Use the default structure from \`openspec/AGENTS.md\` (see "Proposal Structure" section)
5. Create \`design.md\` in \`openspec/changes/<id>/\` (only when needed—see criteria in \`openspec/AGENTS.md\`):
- **CRITICAL**: Check if \`openspec/templates/design.md.template\` exists
- If exists: Read the template file and replace variables (\`{{changeId}}\` → actual change-id, \`{{date}}\` → current date in YYYY-MM-DD format)
- If not exists: Use the default structure from \`openspec/AGENTS.md\` (see "Proposal Structure" section)
6. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
7. Create spec deltas in \`openspec/changes/<id>/specs/<capability>/spec.md\` (one folder per capability):
- **CRITICAL**: Check if \`openspec/templates/spec.md.template\` exists
- If exists: Read the template file and replace variables (\`{{changeId}}\` → actual change-id, \`{{date}}\` → current date in YYYY-MM-DD format, \`{{capability}}\` → capability name)
- If not exists: Use \`## ADDED|MODIFIED|REMOVED Requirements\` format with at least one \`#### Scenario:\` per requirement
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Complete the spec.md fallback instruction.

Line 30 has two gaps that could lead to validation failures:

  1. Missing RENAMED: The instruction lists ADDED|MODIFIED|REMOVED but omits RENAMED, while the test validation (test/core/init.test.ts:1707) expects all four delta types.
  2. Missing requirement header guidance: The instruction only mentions #### Scenario: headers but doesn't specify the requirement header format (e.g., ### Requirement:), while the test (line 1708) expects both.

This issue was flagged in a previous review but remains unresolved.

Apply this diff to add the missing guidance:

-   - If not exists: Use `## ADDED|MODIFIED|REMOVED Requirements` format with at least one `#### Scenario:` per requirement
+   - If not exists: Use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements` format with `### Requirement:` headers and at least one `#### Scenario:` per requirement
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- If not exists: Use \`## ADDED|MODIFIED|REMOVED Requirements\` format with at least one \`#### Scenario:\` per requirement
- If not exists: Use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements` format with `### Requirement:` headers and at least one `#### Scenario:` per requirement
🤖 Prompt for AI Agents
In src/core/templates/slash-command-templates.ts around line 30, the spec.md
fallback instruction is missing guidance for the RENAMED delta type and does not
specify the requirement header format; update the sentence to include all four
delta types (ADDED|MODIFIED|REMOVED|RENAMED) and add explicit requirement header
guidance (e.g., require a `### Requirement:` header for each requirement) while
preserving the existing note that each requirement must include at least one
`#### Scenario:`; ensure the text matches the validation expectations in
test/core/init.test.ts.

- Cross-reference related capabilities when relevant
8. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
Comment on lines +13 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align spec.md fallback instructions with spec-template validation rules

The proposal steps for templates and paths under openspec/changes/<id>/ look solid and consistent, but Line 30’s spec fallback (Use \## ADDED|MODIFIED|REMOVED Requirements` format with at least one `#### Scenario:` per requirement`) doesn’t mention requirement headers, while the PR summary notes that spec-template validation requires both requirement and scenario headers.

Following these instructions as written could produce spec deltas that still fail validation. Consider updating the spec.md bullet(s) to explicitly include whatever “Requirement” heading pattern the validator and AGENTS.md expect (e.g., a Requirement: heading per requirement), and ideally reference the relevant section in openspec/AGENTS.md so authors can mirror the exact structure.

🤖 Prompt for AI Agents
In src/core/templates/slash-command-templates.ts around lines 13 to 32, the
fallback instruction for spec.md omits explicit requirement header guidance
required by the spec-template validator; update the spec.md fallback bullet to
state that each requirement must include the required "Requirement" heading
pattern (e.g., a "Requirement:" or "#### Requirement:" header as used by
AGENTS.md and the validator), and include a short parenthetical mapping to the
exact AGENTS.md section or header pattern to follow (so authors produce a
Requirements header plus at least one "#### Scenario:" per requirement).




const proposalReferences = `**Reference**
- **Template Processing**: For each file type (proposal, tasks, design, spec), always check \`openspec/templates/<type>.md.template\` first. If the template exists, read it and replace all variables before writing the file. Template variables:
- \`{{changeId}}\` → the actual change-id (e.g., \`add-user-auth\`)
- \`{{date}}\` → current date in YYYY-MM-DD format (e.g., \`2025-11-14\`)
- \`{{capability}}\` → capability name (only used in spec.md, e.g., \`user-auth\`)
- Use \`openspec show <id> --json --deltas-only\` or \`openspec show <spec> --type spec\` to inspect details when validation fails.
- Search existing requirements with \`rg -n "Requirement:|Scenario:" openspec/specs\` before writing new ones.
- Explore the codebase with \`rg <keyword>\`, \`ls\`, or direct file reads so proposals align with current implementation realities.`;

const applySteps = `**Steps**
Track these steps as TODOs and complete them one by one.
1. Read \`changes/<id>/proposal.md\`, \`design.md\` (if present), and \`tasks.md\` to confirm scope and acceptance criteria.
1. Read \`openspec/changes/<id>/proposal.md\`, \`design.md\` (if present), and \`tasks.md\` to confirm scope and acceptance criteria.
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
3. Confirm completion before updating statuses—make sure every item in \`tasks.md\` is finished.
4. Update the checklist after all work is done so each task is marked \`- [x]\` and reflects reality.
Expand All @@ -42,7 +61,7 @@ const archiveSteps = `**Steps**
- If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
2. Validate the change ID by running \`openspec list\` (or \`openspec show <id>\`) and stop if the change is missing, already archived, or otherwise not ready to archive.
3. Run \`openspec archive <id> --yes\` so the CLI moves the change and applies spec updates without prompts (use \`--skip-specs\` only for tooling-only work).
4. Review the command output to confirm the target specs were updated and the change landed in \`changes/archive/\`.
4. Review the command output to confirm the target specs were updated and the change landed in \`openspec/changes/archive/\`.
5. Validate with \`openspec validate --strict\` and inspect with \`openspec show <id>\` if anything looks off.`;

const archiveReferences = `**Reference**
Expand Down
Loading
Loading