-
Notifications
You must be signed in to change notification settings - Fork 1k
Feat/custom-template #307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/custom-template #307
Changes from 9 commits
091d1fc
34a9ed9
5639a7f
37e2427
4edeb36
085ea1d
a39b562
f80e99a
1717e3c
0dad4c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,25 +5,45 @@ const baseGuardrails = `**Guardrails** | |||||
| - Keep changes tightly scoped to the requested outcome. | ||||||
| - Refer to \`openspec/AGENTS.md\` (located inside the \`openspec/\` directory—run \`ls openspec\` or \`openspec update\` if you don't see it) if you need additional OpenSpec conventions or clarifications.`; | ||||||
|
|
||||||
| const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`; | ||||||
| const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files. | ||||||
| - Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.`; | ||||||
|
|
||||||
| 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Complete the spec.md fallback instruction. Line 30 has two gaps that could lead to validation failures:
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
Suggested change
🤖 Prompt for AI Agents |
||||||
| - 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align spec.md fallback instructions with spec-template validation rules The proposal steps for templates and paths under 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 🤖 Prompt for AI Agents |
||||||
|
|
||||||
|
|
||||||
| 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. | ||||||
|
|
@@ -40,7 +60,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** | ||||||
|
|
||||||
There was a problem hiding this comment.
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 scaffoldcommand 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
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 scaffoldshould 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?
There was a problem hiding this comment.
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?