diff --git a/openspec/AGENTS.md b/openspec/AGENTS.md index 6c1703eef..7496fb7d2 100644 --- a/openspec/AGENTS.md +++ b/openspec/AGENTS.md @@ -18,7 +18,7 @@ Instructions for AI coding assistants using OpenSpec for spec-driven development Create proposal when you need to: - Add features or functionality - Make breaking changes (API, schema) -- Change architecture or patterns +- Change architecture or patterns - Optimize performance (changes behavior) - Update security patterns @@ -125,6 +125,7 @@ openspec validate [change] --strict --no-interactive ``` openspec/ ├── project.md # Project conventions +├── architecture.md # System architecture (optional, living document) ├── specs/ # Current truth - what IS built │ └── [capability]/ # Single focused capability │ ├── spec.md # Requirements and scenarios @@ -147,7 +148,7 @@ openspec/ ``` New request? ├─ Bug fix restoring spec behavior? → Fix directly -├─ Typo/format/comment? → Fix directly +├─ Typo/format/comment? → Fix directly ├─ New feature/capability? → Create proposal ├─ Breaking change? → Create proposal ├─ Architecture change? → Create proposal @@ -211,6 +212,8 @@ Create `design.md` if any of the following apply; otherwise omit it: - Security, performance, or migration complexity - Ambiguity that benefits from technical decisions before coding +**Note:** Architectural decisions in `design.md` are automatically merged to `architecture.md` when the change is archived. Keep `design.md` focused on the specific change; the global architecture evolves through archived decisions. + Minimal `design.md` skeleton: ```markdown ## Context @@ -432,6 +435,63 @@ Only add complexity with: 3. Review recent archives 4. Ask for clarification +## Architecture Documentation + +### Reading Architecture +Before creating change proposals that affect system structure, read `openspec/architecture.md` to understand: +- Current system components and relationships +- Integration points and data flows +- Past architectural decisions and their rationale + +### When to Update Architecture +A change has **architectural impact** if it: +- Adds or removes major components/services +- Changes component boundaries or responsibilities +- Adds new integration points or external dependencies +- Modifies core data flows +- Introduces new architectural patterns + +### Proposing Architectural Changes +For architecture-impacting changes: +1. Reference current state from `architecture.md` in `proposal.md` +2. Document architectural decisions in `design.md` +3. Include before/after diagrams showing the change +4. The archive command will automatically merge decisions to `architecture.md` + +### Diagram Standards +Use ASCII diagrams for maximum compatibility: + +``` +Component relationships: Data flow: Boundaries: +┌─────────┐ ──▶ direction ┌──────────┐ +│ Service │ ◀── bidirectional │ Internal │ +└────┬────┘ ├──────────┤ + │ State transitions: │ External │ + ▼ A ──[event]──▶ B └──────────┘ +┌─────────┐ +│ Service │ +└─────────┘ +``` + +## Post-Implementation Checklist + +After completing implementation and before archiving, verify: + +1. **Architecture currency**: If the change affected system structure, check if `architecture.md` reflects the new state: + - Are new components documented? + - Are data flows still accurate? + - Are integration points up to date? + +2. **Decision capture**: Ensure significant decisions are in `design.md` (they'll be auto-merged on archive) + +3. **Spec accuracy**: Verify specs match the implemented behavior + +**Quick check prompt:** +``` +"Review my changes and check if openspec/architecture.md needs updating + to reflect any structural changes I made" +``` + ## Quick Reference ### Stage Indicators @@ -444,6 +504,7 @@ Only add complexity with: - `tasks.md` - Implementation steps - `design.md` - Technical decisions - `spec.md` - Requirements and behavior +- `architecture.md` - System architecture and component relationships ### CLI Essentials ```bash diff --git a/openspec/architecture.md b/openspec/architecture.md new file mode 100644 index 000000000..c3f468e44 --- /dev/null +++ b/openspec/architecture.md @@ -0,0 +1,83 @@ +# System Architecture + +## Overview + + + + +``` +┌─────────────────────────────────────────────────────────────┐ +│ [Your System Name] │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ [Client] │───▶│ [API] │───▶│ [DB] │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Components + + + +### [Component Name] +- **Responsibility**: [What this component does] +- **Technology**: [Stack/framework used] +- **Key files**: [Main entry points, e.g., `src/core/component.ts`] + + + +## Data Flows + + + +### [Flow Name] +``` +[Source] ──▶ [Processing] ──▶ [Destination] +``` +- **Trigger**: [What initiates this flow] +- **Data**: [What data moves through] + +## Integration Points + + + +| External System | Purpose | Protocol | Notes | +|-----------------|---------|----------|-------| +| [Service] | [Why] | [How] | [Details] | + +## Architectural Decisions + + + + +| Date | Decision | Rationale | Status | +|------|----------|-----------|--------| + +## Constraints + + + +- [Constraint 1]: [Description and impact] +- [Constraint 2]: [Description and impact] + +--- + +## Diagram Reference + + + +``` +Components: Relationships: Boundaries: +┌─────────┐ ───▶ data flow ┌──────────┐ +│ Service │ ◀─── reverse flow │ Internal │ +└─────────┘ ←──▶ bidirectional ├──────────┤ + │ External │ +Grouping: States: └──────────┘ +┌─────────────┐ ○ start state +│ ┌───┐ ┌───┐ │ ● end state +│ │ A │ │ B │ │ □ intermediate +│ └───┘ └───┘ │ +└─────────────┘ +``` diff --git a/schemas/spec-driven/schema.yaml b/schemas/spec-driven/schema.yaml index d4a681349..295a7e1eb 100644 --- a/schemas/spec-driven/schema.yaml +++ b/schemas/spec-driven/schema.yaml @@ -105,6 +105,10 @@ artifacts: Reference the proposal for motivation and specs for requirements. Good design docs explain the "why" behind technical decisions. + + Note: Architectural decisions in design.md are automatically merged to + openspec/architecture.md when the change is archived. For structural changes, + verify architecture.md is updated after implementation. requires: - proposal diff --git a/src/core/architecture-apply.ts b/src/core/architecture-apply.ts new file mode 100644 index 000000000..961ef3d16 --- /dev/null +++ b/src/core/architecture-apply.ts @@ -0,0 +1,249 @@ +/** + * Architecture Application Logic + * + * Extracts architectural decisions from design.md and applies to architecture.md + * during the archive process. + */ + +import { promises as fs } from 'fs'; + +// ----------------------------------------------------------------------------- +// Types +// ----------------------------------------------------------------------------- + +export interface ArchitecturalDecision { + date: string; + decision: string; + rationale: string; + status: 'Active' | 'Superseded'; +} + +// ----------------------------------------------------------------------------- +// Public API +// ----------------------------------------------------------------------------- + +/** + * Check if a design.md content indicates architectural impact + */ +export function hasArchitecturalImpact(designContent: string): boolean { + // Keywords that suggest architectural decisions + const architecturalPatterns = [ + /\barchitecture\b/i, + /\bcomponent\b/i, + /\bservice\b/i, + /\bintegration\b/i, + /\bdata.?flow\b/i, + /\bmodule\b/i, + /\bmicroservice\b/i, + /\bAPI\b/i, + /\bdatabase\b/i, + /\bschema\b/i, + /\bpattern\b/i, + ]; + + return architecturalPatterns.some((pattern) => pattern.test(designContent)); +} + +/** + * Extract architectural decision from design.md content + */ +export function extractArchitecturalDecision( + designContent: string, + changeName: string, + date: string +): ArchitecturalDecision | null { + // Look for ## Decisions or ## Decision section + const decisionsMatch = designContent.match( + /##\s*Decisions?\s*\n([\s\S]*?)(?=\n##|$)/i + ); + + if (!decisionsMatch) { + // No explicit decisions section, try to extract from context or first heading + const firstContentMatch = designContent.match( + /##\s*(?:Context|Overview|Summary)\s*\n([\s\S]*?)(?=\n##|$)/i + ); + if (firstContentMatch) { + const content = firstContentMatch[1].trim(); + if (content.length > 20) { + return { + date, + decision: `Architectural change: ${changeName.replace(/-/g, ' ')}`, + rationale: content.substring(0, 150).replace(/\n/g, ' ').trim(), + status: 'Active', + }; + } + } + return null; + } + + const decisionsText = decisionsMatch[1].trim(); + if (!decisionsText || decisionsText.length < 20) { + return null; + } + + // Extract decision content + const lines = decisionsText + .split('\n') + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith('#')); + + // Get the first substantive line as the decision + let decision = ''; + let rationale = ''; + + for (const line of lines) { + const cleanLine = line.replace(/^[-*]\s*/, '').replace(/\*\*/g, ''); + if (!decision && cleanLine.length > 10) { + decision = cleanLine; + } else if (decision && !rationale && cleanLine.length > 10) { + rationale = cleanLine; + break; + } + } + + if (!decision) { + decision = `From change: ${changeName}`; + } + if (!rationale) { + rationale = 'See archived change for details'; + } + + return { + date, + decision: truncate(decision, 100), + rationale: truncate(rationale, 150), + status: 'Active', + }; +} + +/** + * Append architectural decision to architecture.md content + */ +export function appendArchitecturalDecision( + archContent: string, + decision: ArchitecturalDecision +): string { + const tableRow = `| ${decision.date} | ${escapeTableCell(decision.decision)} | ${escapeTableCell(decision.rationale)} | ${decision.status} |`; + + // Find the Architectural Decisions table header + const tableHeaderPattern = + /(\|\s*Date\s*\|\s*Decision\s*\|\s*Rationale\s*\|\s*Status\s*\|\s*\n\|[-|\s]+\|)/i; + const match = archContent.match(tableHeaderPattern); + + if (match) { + // Insert after table header row + const insertPos = match.index! + match[0].length; + return ( + archContent.slice(0, insertPos) + + '\n' + + tableRow + + archContent.slice(insertPos) + ); + } + + // Table not found, try to find the section and add table + const sectionPattern = /##\s*Architectural Decisions\s*\n/i; + const sectionMatch = archContent.match(sectionPattern); + + if (sectionMatch) { + const insertPos = sectionMatch.index! + sectionMatch[0].length; + const tableContent = ` +| Date | Decision | Rationale | Status | +|------|----------|-----------|--------| +${tableRow} +`; + return ( + archContent.slice(0, insertPos) + tableContent + archContent.slice(insertPos) + ); + } + + // Section not found, append at end + return ( + archContent + + ` + +## Architectural Decisions + +| Date | Decision | Rationale | Status | +|------|----------|-----------|--------| +${tableRow} +` + ); +} + +/** + * Apply architectural decisions from a change to architecture.md + * + * @param projectRoot - The project root directory + * @param changeName - Name of the change being archived + * @param changeDir - Path to the change directory + * @param archiveDate - Date string for the decision (YYYY-MM-DD) + * @returns True if architecture.md was updated + */ +export async function applyArchitecturalDecisions( + projectRoot: string, + changeName: string, + changeDir: string, + archiveDate: string +): Promise { + const architecturePath = `${projectRoot}/openspec/architecture.md`; + const designPath = `${changeDir}/design.md`; + + // Check if design.md exists + let designContent: string; + try { + designContent = await fs.readFile(designPath, 'utf-8'); + } catch { + // No design.md, nothing to apply + return false; + } + + // Check if it has architectural impact + if (!hasArchitecturalImpact(designContent)) { + return false; + } + + // Check if architecture.md exists + let archContent: string; + try { + archContent = await fs.readFile(architecturePath, 'utf-8'); + } catch { + // architecture.md doesn't exist, skip + return false; + } + + // Extract decision + const decision = extractArchitecturalDecision( + designContent, + changeName, + archiveDate + ); + + if (!decision) { + return false; + } + + // Apply decision + const updatedContent = appendArchitecturalDecision(archContent, decision); + + // Write updated content + await fs.writeFile(architecturePath, updatedContent); + + return true; +} + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +function truncate(text: string, maxLength: number): string { + if (text.length <= maxLength) { + return text; + } + return text.substring(0, maxLength - 3) + '...'; +} + +function escapeTableCell(text: string): string { + // Escape pipe characters and newlines for markdown table compatibility + return text.replace(/\|/g, '\\|').replace(/\n/g, ' '); +} diff --git a/src/core/archive.ts b/src/core/archive.ts index 1121ec259..3b23aa08a 100644 --- a/src/core/archive.ts +++ b/src/core/archive.ts @@ -9,6 +9,7 @@ import { writeUpdatedSpec, type SpecUpdate, } from './specs-apply.js'; +import { applyArchitecturalDecisions } from './architecture-apply.js'; export class ArchiveCommand { async execute( @@ -88,10 +89,10 @@ export class ArchiveCommand { hasDeltaSpecs = true; break; } - } catch {} + } catch { } } } - } catch {} + } catch { } if (hasDeltaSpecs) { const deltaReport = await validator.validateChangeDeltaSpecs(changeDir); if (!deltaReport.valid) { @@ -115,7 +116,7 @@ export class ArchiveCommand { } else { // Log warning when validation is skipped const timestamp = new Date().toISOString(); - + if (!options.yes) { const { confirm } = await import('@inquirer/prompts'); const proceed = await confirm({ @@ -129,7 +130,7 @@ export class ArchiveCommand { } else { console.log(chalk.yellow(`\n⚠️ WARNING: Skipping validation may archive invalid specs.`)); } - + console.log(chalk.yellow(`[${timestamp}] Validation skipped for change: ${changeName}`)); console.log(chalk.yellow(`Affected files: ${changeDir}`)); } @@ -162,7 +163,7 @@ export class ArchiveCommand { } else { // Find specs to update const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir); - + if (specUpdates.length > 0) { console.log('\nSpecs to update:'); for (const update of specUpdates) { @@ -227,8 +228,20 @@ export class ArchiveCommand { } } + // Apply architectural decisions to architecture.md if design.md has architectural impact + const archiveDate = this.getArchiveDate(); + const architectureUpdated = await applyArchitecturalDecisions( + targetPath, + changeName!, + changeDir, + archiveDate + ); + if (architectureUpdated) { + console.log('Updated architecture.md with architectural decisions.'); + } + // Create archive directory with date prefix - const archiveName = `${this.getArchiveDate()}-${changeName}`; + const archiveName = `${archiveDate}-${changeName}`; const archivePath = path.join(archiveDir, archiveName); // Check if archive already exists @@ -246,7 +259,7 @@ export class ArchiveCommand { // Move change to archive await fs.rename(changeDir, archivePath); - + console.log(`Change '${changeName}' archived as '${archiveName}'.`); } diff --git a/src/core/init.ts b/src/core/init.ts index ebc98c9c8..a991abf3f 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -66,18 +66,18 @@ const isSelectableChoice = ( type ToolWizardChoice = | { - kind: 'heading' | 'info'; - value: string; - label: ToolLabel; - selectable: false; - } + kind: 'heading' | 'info'; + value: string; + label: ToolLabel; + selectable: false; + } | { - kind: 'option'; - value: string; - label: ToolLabel; - configured: boolean; - selectable: true; - }; + kind: 'option'; + value: string; + label: ToolLabel; + configured: boolean; + selectable: true; + }; type ToolWizardConfig = { extendMode: boolean; @@ -567,8 +567,8 @@ export class InitCommand { : 'Which natively supported AI tools do you use?'; const initialNativeSelection = extendMode ? availableTools - .filter((tool) => existingTools[tool.value]) - .map((tool) => tool.value) + .filter((tool) => existingTools[tool.value]) + .map((tool) => tool.value) : []; const initialSelected = Array.from(new Set(initialNativeSelection)); @@ -592,13 +592,13 @@ export class InitCommand { })), ...(availableTools.length ? ([ - { - kind: 'info' as const, - value: LIST_SPACER_VALUE, - label: { primary: '' }, - selectable: false, - }, - ] as ToolWizardChoice[]) + { + kind: 'info' as const, + value: LIST_SPACER_VALUE, + label: { primary: '' }, + selectable: false, + }, + ] as ToolWizardChoice[]) : []), { kind: 'heading', @@ -822,33 +822,33 @@ export class InitCommand { const summaryLines = [ rootStubStatus === 'created' ? `${PALETTE.white('▌')} ${PALETTE.white( - 'Root AGENTS.md stub created for other assistants' - )}` + 'Root AGENTS.md stub created for other assistants' + )}` : null, rootStubStatus === 'updated' ? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray( - 'Root AGENTS.md stub refreshed for other assistants' - )}` + 'Root AGENTS.md stub refreshed for other assistants' + )}` : null, created.length ? `${PALETTE.white('▌')} ${PALETTE.white( - 'Created:' - )} ${this.formatToolNames(created)}` + 'Created:' + )} ${this.formatToolNames(created)}` : null, refreshed.length ? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray( - 'Refreshed:' - )} ${this.formatToolNames(refreshed)}` + 'Refreshed:' + )} ${this.formatToolNames(refreshed)}` : null, skippedExisting.length ? `${PALETTE.midGray('▌')} ${PALETTE.midGray( - 'Skipped (already configured):' - )} ${this.formatToolNames(skippedExisting)}` + 'Skipped (already configured):' + )} ${this.formatToolNames(skippedExisting)}` : null, skipped.length ? `${PALETTE.darkGray('▌')} ${PALETTE.darkGray( - 'Skipped:' - )} ${this.formatToolNames(skipped)}` + 'Skipped:' + )} ${this.formatToolNames(skipped)}` : null, ].filter((line): line is string => Boolean(line)); for (const line of summaryLines) { @@ -897,7 +897,18 @@ export class InitCommand { ' with details about my project, tech stack, and conventions"\n' ) ); - console.log(PALETTE.white('2. Create your first change proposal:')); + console.log(PALETTE.white('2. Document your system architecture:')); + console.log( + PALETTE.lightGray( + ' "Please analyze my codebase and update openspec/architecture.md' + ) + ); + console.log( + PALETTE.lightGray( + ' with the actual components, data flows, and integration points"\n' + ) + ); + console.log(PALETTE.white('3. Create your first change proposal:')); console.log( PALETTE.lightGray( ' "I want to add [YOUR FEATURE HERE]. Please create an' @@ -906,7 +917,7 @@ export class InitCommand { console.log( PALETTE.lightGray(' OpenSpec change proposal for this feature"\n') ); - console.log(PALETTE.white('3. Learn the OpenSpec workflow:')); + console.log(PALETTE.white('4. Learn the OpenSpec workflow:')); console.log( PALETTE.lightGray( ' "Please explain the OpenSpec workflow from openspec/AGENTS.md' @@ -944,9 +955,8 @@ export class InitCommand { const base = names.slice(0, -1).map((name) => PALETTE.white(name)); const last = PALETTE.white(names[names.length - 1]); - return `${base.join(PALETTE.midGray(', '))}${ - base.length ? PALETTE.midGray(', and ') : '' - }${last}`; + return `${base.join(PALETTE.midGray(', '))}${base.length ? PALETTE.midGray(', and ') : '' + }${last}`; } private renderBanner(_extendMode: boolean): void { diff --git a/src/core/templates/agents-template.ts b/src/core/templates/agents-template.ts index 2a4ad4c6a..b6c5b579c 100644 --- a/src/core/templates/agents-template.ts +++ b/src/core/templates/agents-template.ts @@ -18,7 +18,7 @@ Instructions for AI coding assistants using OpenSpec for spec-driven development Create proposal when you need to: - Add features or functionality - Make breaking changes (API, schema) -- Change architecture or patterns +- Change architecture or patterns - Optimize performance (changes behavior) - Update security patterns @@ -125,6 +125,7 @@ openspec validate [change] --strict --no-interactive \`\`\` openspec/ ├── project.md # Project conventions +├── architecture.md # System architecture (optional, living document) ├── specs/ # Current truth - what IS built │ └── [capability]/ # Single focused capability │ ├── spec.md # Requirements and scenarios @@ -147,7 +148,7 @@ openspec/ \`\`\` New request? ├─ Bug fix restoring spec behavior? → Fix directly -├─ Typo/format/comment? → Fix directly +├─ Typo/format/comment? → Fix directly ├─ New feature/capability? → Create proposal ├─ Breaking change? → Create proposal ├─ Architecture change? → Create proposal @@ -211,6 +212,8 @@ Create \`design.md\` if any of the following apply; otherwise omit it: - Security, performance, or migration complexity - Ambiguity that benefits from technical decisions before coding +**Note:** Architectural decisions in \`design.md\` are automatically merged to \`architecture.md\` when the change is archived. Keep \`design.md\` focused on the specific change; the global architecture evolves through archived decisions. + Minimal \`design.md\` skeleton: \`\`\`markdown ## Context @@ -432,6 +435,63 @@ Only add complexity with: 3. Review recent archives 4. Ask for clarification +## Architecture Documentation + +### Reading Architecture +Before creating change proposals that affect system structure, read \`openspec/architecture.md\` to understand: +- Current system components and relationships +- Integration points and data flows +- Past architectural decisions and their rationale + +### When to Update Architecture +A change has **architectural impact** if it: +- Adds or removes major components/services +- Changes component boundaries or responsibilities +- Adds new integration points or external dependencies +- Modifies core data flows +- Introduces new architectural patterns + +### Proposing Architectural Changes +For architecture-impacting changes: +1. Reference current state from \`architecture.md\` in \`proposal.md\` +2. Document architectural decisions in \`design.md\` +3. Include before/after diagrams showing the change +4. The archive command will automatically merge decisions to \`architecture.md\` + +### Diagram Standards +Use ASCII diagrams for maximum compatibility: + +\`\`\` +Component relationships: Data flow: Boundaries: +┌─────────┐ ──▶ direction ┌──────────┐ +│ Service │ ◀── bidirectional │ Internal │ +└────┬────┘ ├──────────┤ + │ State transitions: │ External │ + ▼ A ──[event]──▶ B └──────────┘ +┌─────────┐ +│ Service │ +└─────────┘ +\`\`\` + +## Post-Implementation Checklist + +After completing implementation and before archiving, verify: + +1. **Architecture currency**: If the change affected system structure, check if \`architecture.md\` reflects the new state: + - Are new components documented? + - Are data flows still accurate? + - Are integration points up to date? + +2. **Decision capture**: Ensure significant decisions are in \`design.md\` (they'll be auto-merged on archive) + +3. **Spec accuracy**: Verify specs match the implemented behavior + +**Quick check prompt:** +\`\`\` +"Review my changes and check if openspec/architecture.md needs updating + to reflect any structural changes I made" +\`\`\` + ## Quick Reference ### Stage Indicators @@ -444,6 +504,7 @@ Only add complexity with: - \`tasks.md\` - Implementation steps - \`design.md\` - Technical decisions - \`spec.md\` - Requirements and behavior +- \`architecture.md\` - System architecture and component relationships ### CLI Essentials \`\`\`bash diff --git a/src/core/templates/architecture-template.ts b/src/core/templates/architecture-template.ts new file mode 100644 index 000000000..d9a95981b --- /dev/null +++ b/src/core/templates/architecture-template.ts @@ -0,0 +1,91 @@ +/** + * Architecture Template + * + * Template for generating architecture.md - a living document that captures + * system architecture, components, data flows, and architectural decisions. + */ + +export const architectureTemplate = `# System Architecture + +## Overview + + + + +\`\`\` +┌─────────────────────────────────────────────────────────────┐ +│ [Your System Name] │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ [Client] │───▶│ [API] │───▶│ [DB] │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +\`\`\` + +## Components + + + +### [Component Name] +- **Responsibility**: [What this component does] +- **Technology**: [Stack/framework used] +- **Key files**: [Main entry points, e.g., \`src/core/component.ts\`] + + + +## Data Flows + + + +### [Flow Name] +\`\`\` +[Source] ──▶ [Processing] ──▶ [Destination] +\`\`\` +- **Trigger**: [What initiates this flow] +- **Data**: [What data moves through] + +## Integration Points + + + +| External System | Purpose | Protocol | Notes | +|-----------------|---------|----------|-------| +| [Service] | [Why] | [How] | [Details] | + +## Architectural Decisions + + + + +| Date | Decision | Rationale | Status | +|------|----------|-----------|--------| + +## Constraints + + + +- [Constraint 1]: [Description and impact] +- [Constraint 2]: [Description and impact] + +--- + +## Diagram Reference + + + +\`\`\` +Components: Relationships: Boundaries: +┌─────────┐ ───▶ data flow ┌──────────┐ +│ Service │ ◀─── reverse flow │ Internal │ +└─────────┘ ←──▶ bidirectional ├──────────┤ + │ External │ +Grouping: States: └──────────┘ +┌─────────────┐ ○ start state +│ ┌───┐ ┌───┐ │ ● end state +│ │ A │ │ B │ │ □ intermediate +│ └───┘ └───┘ │ +└─────────────┘ +\`\`\` +`; diff --git a/src/core/templates/index.ts b/src/core/templates/index.ts index 8dab4b5f6..bc2652b2b 100644 --- a/src/core/templates/index.ts +++ b/src/core/templates/index.ts @@ -1,5 +1,6 @@ import { agentsTemplate } from './agents-template.js'; import { projectTemplate, ProjectContext } from './project-template.js'; +import { architectureTemplate } from './architecture-template.js'; import { claudeTemplate } from './claude-template.js'; import { clineTemplate } from './cline-template.js'; import { costrictTemplate } from './costrict-template.js'; @@ -21,6 +22,10 @@ export class TemplateManager { { path: 'project.md', content: projectTemplate(context) + }, + { + path: 'architecture.md', + content: architectureTemplate } ]; } @@ -44,6 +49,10 @@ export class TemplateManager { static getSlashCommandBody(id: SlashCommandId): string { return getSlashCommandBody(id); } + + static getArchitectureTemplate(): string { + return architectureTemplate; + } } export { ProjectContext } from './project-template.js'; diff --git a/src/core/templates/skill-templates.ts b/src/core/templates/skill-templates.ts index cd67b634e..0da915069 100644 --- a/src/core/templates/skill-templates.ts +++ b/src/core/templates/skill-templates.ts @@ -9,9 +9,9 @@ */ export interface SkillTemplate { - name: string; - description: string; - instructions: string; + name: string; + description: string; + instructions: string; } /** @@ -19,10 +19,10 @@ export interface SkillTemplate { * Explore mode - adaptive thinking partner for exploring ideas and problems */ export function getExploreSkillTemplate(): SkillTemplate { - return { - name: 'openspec-explore', - description: 'Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.', - instructions: `Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. + return { + name: 'openspec-explore', + description: 'Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.', + instructions: `Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -50,6 +50,7 @@ Depending on what the user brings, you might: **Investigate the codebase** - Map existing architecture relevant to the discussion +- Read \`openspec/architecture.md\` for system overview and component relationships - Find integration points - Identify patterns already in use - Surface hidden complexity @@ -297,7 +298,7 @@ But this summary is optional. Sometimes the thinking IS the value. - **Do visualize** - A good diagram is worth many paragraphs - **Do explore the codebase** - Ground discussions in reality - **Do question assumptions** - Including the user's and your own` - }; + }; } /** @@ -305,10 +306,10 @@ But this summary is optional. Sometimes the thinking IS the value. * Based on /opsx:new command */ export function getNewChangeSkillTemplate(): SkillTemplate { - return { - name: 'openspec-new-change', - description: 'Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.', - instructions: `Start a new change using the experimental artifact-driven approach. + return { + name: 'openspec-new-change', + description: 'Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.', + instructions: `Start a new change using the experimental artifact-driven approach. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. @@ -372,7 +373,7 @@ After completing the steps, summarize: - 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 - Pass --schema if using a non-default workflow` - }; + }; } /** @@ -380,10 +381,10 @@ After completing the steps, summarize: * Based on /opsx:continue command */ export function getContinueChangeSkillTemplate(): SkillTemplate { - return { - name: 'openspec-continue-change', - description: 'Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.', - instructions: `Continue working on a change by creating the next artifact. + return { + name: 'openspec-continue-change', + description: 'Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.', + instructions: `Continue working on a change by creating the next artifact. **Input**: Optionally specify a change name. If omitted, MUST prompt for available changes. @@ -467,8 +468,11 @@ 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. - The Capabilities section is critical - each capability listed will need a spec file. + - For changes affecting system structure, reference \`architecture.md\`. - **specs/*.md**: Create one spec per capability listed in the proposal. - **design.md**: Document technical decisions, architecture, and implementation approach. + - For architectural changes, include before/after diagrams. + - Decisions here will be merged to \`architecture.md\` on archive. - **tasks.md**: Break down implementation into checkboxed tasks. **tdd schema** (spec → tests → implementation → docs): @@ -486,7 +490,7 @@ For other schemas, follow the \`instruction\` field from the CLI output. - If context is unclear, ask the user before creating - Verify the artifact file exists after writing before marking progress - Use the schema's artifact sequence, don't assume specific artifact names` - }; + }; } /** @@ -494,10 +498,10 @@ For other schemas, follow the \`instruction\` field from the CLI output. * For implementing tasks from a completed (or in-progress) change */ export function getApplyChangeSkillTemplate(): SkillTemplate { - return { - name: 'openspec-apply-change', - description: 'Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.', - instructions: `Implement tasks from an OpenSpec change. + return { + name: 'openspec-apply-change', + description: 'Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.', + instructions: `Implement tasks from an OpenSpec change. **Input**: Optionally specify a change name. If omitted, MUST prompt for available changes. @@ -644,7 +648,7 @@ This skill supports the "actions on a change" model: - **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` - }; + }; } /** @@ -652,10 +656,10 @@ This skill supports the "actions on a change" model: * 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. + 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. @@ -739,7 +743,7 @@ After completing all artifacts, summarize: - 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` - }; + }; } /** @@ -747,10 +751,10 @@ After completing all artifacts, summarize: * For syncing delta specs from a change to main specs (agent-driven) */ export function getSyncSpecsSkillTemplate(): SkillTemplate { - return { - name: 'openspec-sync-specs', - description: 'Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.', - instructions: `Sync delta specs from a change to main specs. + return { + name: 'openspec-sync-specs', + description: 'Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.', + instructions: `Sync delta specs from a change to main specs. This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). @@ -877,7 +881,7 @@ Main specs are now updated. The change remains active - archive when implementat - If something is unclear, ask for clarification - Show what you're changing as you go - The operation should be idempotent - running twice should give same result` - }; + }; } // ----------------------------------------------------------------------------- @@ -885,11 +889,11 @@ Main specs are now updated. The change remains active - archive when implementat // ----------------------------------------------------------------------------- export interface CommandTemplate { - name: string; - description: string; - category: string; - tags: string[]; - content: string; + name: string; + description: string; + category: string; + tags: string[]; + content: string; } /** @@ -897,12 +901,12 @@ export interface CommandTemplate { * Explore mode - adaptive thinking partner */ export function getOpsxExploreCommandTemplate(): CommandTemplate { - return { - name: 'OPSX: Explore', - description: 'Enter explore mode - think through ideas, investigate problems, clarify requirements', - category: 'Workflow', - tags: ['workflow', 'explore', 'experimental', 'thinking'], - content: `Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. + return { + name: 'OPSX: Explore', + description: 'Enter explore mode - think through ideas, investigate problems, clarify requirements', + category: 'Workflow', + tags: ['workflow', 'explore', 'experimental', 'thinking'], + content: `Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -1065,19 +1069,19 @@ When things crystallize, you might offer a summary - but it's optional. Sometime - **Do visualize** - A good diagram is worth many paragraphs - **Do explore the codebase** - Ground discussions in reality - **Do question assumptions** - Including the user's and your own` - }; + }; } /** * Template for /opsx:new slash command */ export function getOpsxNewCommandTemplate(): CommandTemplate { - return { - name: 'OPSX: New', - description: 'Start a new change using the experimental artifact workflow (OPSX)', - category: 'Workflow', - tags: ['workflow', 'artifacts', 'experimental'], - content: `Start a new change using the experimental artifact-driven approach. + return { + name: 'OPSX: New', + description: 'Start a new change using the experimental artifact workflow (OPSX)', + category: 'Workflow', + tags: ['workflow', 'artifacts', 'experimental'], + content: `Start a new change using the experimental artifact-driven approach. **Input**: The argument after \`/opsx:new\` is the change name (kebab-case), OR a description of what the user wants to build. @@ -1140,19 +1144,19 @@ After completing the steps, summarize: - 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 - Pass --schema if using a non-default workflow` - }; + }; } /** * Template for /opsx:continue slash command */ export function getOpsxContinueCommandTemplate(): CommandTemplate { - return { - name: 'OPSX: Continue', - description: 'Continue working on a change - create the next artifact (Experimental)', - category: 'Workflow', - tags: ['workflow', 'artifacts', 'experimental'], - content: `Continue working on a change by creating the next artifact. + return { + name: 'OPSX: Continue', + description: 'Continue working on a change - create the next artifact (Experimental)', + category: 'Workflow', + tags: ['workflow', 'artifacts', 'experimental'], + content: `Continue working on a change by creating the next artifact. **Input**: Optionally specify \`--change \` after \`/opsx:continue\`. If omitted, MUST prompt for available changes. @@ -1255,19 +1259,19 @@ For other schemas, follow the \`instruction\` field from the CLI output. - If context is unclear, ask the user before creating - Verify the artifact file exists after writing before marking progress - Use the schema's artifact sequence, don't assume specific artifact names` - }; + }; } /** * Template for /opsx:apply slash command */ export function getOpsxApplyCommandTemplate(): CommandTemplate { - return { - name: 'OPSX: Apply', - description: 'Implement tasks from an OpenSpec change (Experimental)', - category: 'Workflow', - tags: ['workflow', 'artifacts', 'experimental'], - content: `Implement tasks from an OpenSpec change. + return { + name: 'OPSX: Apply', + description: 'Implement tasks from an OpenSpec change (Experimental)', + category: 'Workflow', + tags: ['workflow', 'artifacts', 'experimental'], + content: `Implement tasks from an OpenSpec change. **Input**: Optionally specify \`--change \` after \`/opsx:apply\`. If omitted, MUST prompt for available changes. @@ -1414,7 +1418,7 @@ This skill supports the "actions on a change" model: - **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` - }; + }; } @@ -1422,12 +1426,12 @@ This skill supports the "actions on a change" model: * 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. + 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. @@ -1511,7 +1515,7 @@ After completing all artifacts, summarize: - 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` - }; + }; } /** @@ -1519,10 +1523,10 @@ After completing all artifacts, summarize: * For archiving completed changes in the experimental workflow */ export function getArchiveChangeSkillTemplate(): SkillTemplate { - return { - name: 'openspec-archive-change', - description: 'Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.', - instructions: `Archive a completed change in the experimental workflow. + return { + name: 'openspec-archive-change', + description: 'Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.', + instructions: `Archive a completed change in the experimental workflow. **Input**: Optionally specify a change name. If omitted, MUST prompt for available changes. @@ -1643,19 +1647,19 @@ All artifacts complete. All tasks complete. - Show clear summary of what happened - If sync is requested, use openspec-sync-specs approach (agent-driven) - Quick sync check: look for requirement names in delta specs, verify they exist in main specs` - }; + }; } /** * Template for /opsx:sync slash command */ export function getOpsxSyncCommandTemplate(): CommandTemplate { - return { - name: 'OPSX: Sync', - description: 'Sync delta specs from a change to main specs', - category: 'Workflow', - tags: ['workflow', 'specs', 'experimental'], - content: `Sync delta specs from a change to main specs. + return { + name: 'OPSX: Sync', + description: 'Sync delta specs from a change to main specs', + category: 'Workflow', + tags: ['workflow', 'specs', 'experimental'], + content: `Sync delta specs from a change to main specs. This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). @@ -1782,7 +1786,7 @@ Main specs are now updated. The change remains active - archive when implementat - If something is unclear, ask for clarification - Show what you're changing as you go - The operation should be idempotent - running twice should give same result` - }; + }; } /** @@ -1790,10 +1794,10 @@ Main specs are now updated. The change remains active - archive when implementat * For verifying implementation matches change artifacts before archiving */ export function getVerifyChangeSkillTemplate(): SkillTemplate { - return { - name: 'openspec-verify-change', - description: 'Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.', - instructions: `Verify that an implementation matches the change artifacts (specs, tasks, design). + return { + name: 'openspec-verify-change', + description: 'Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.', + instructions: `Verify that an implementation matches the change artifacts (specs, tasks, design). **Input**: Optionally specify a change name. If omitted, MUST prompt for available changes. @@ -1950,19 +1954,19 @@ Use clear markdown with: - Code references in format: \`file.ts:123\` - Specific, actionable recommendations - No vague suggestions like "consider reviewing"` - }; + }; } /** * Template for /opsx:archive slash command */ export function getOpsxArchiveCommandTemplate(): CommandTemplate { - return { - name: 'OPSX: Archive', - description: 'Archive a completed change in the experimental workflow', - category: 'Workflow', - tags: ['workflow', 'archive', 'experimental'], - content: `Archive a completed change in the experimental workflow. + return { + name: 'OPSX: Archive', + description: 'Archive a completed change in the experimental workflow', + category: 'Workflow', + tags: ['workflow', 'archive', 'experimental'], + content: `Archive a completed change in the experimental workflow. **Input**: Optionally specify \`--change \` after \`/opsx:archive\`. If omitted, MUST prompt for available changes. @@ -2130,19 +2134,19 @@ Target archive directory already exists. - Quick sync check: look for requirement names in delta specs, verify they exist in main specs - Show clear summary of what happened - If sync is requested, use /opsx:sync approach (agent-driven)` - }; + }; } /** * Template for /opsx:verify slash command */ export function getOpsxVerifyCommandTemplate(): CommandTemplate { - return { - name: 'OPSX: Verify', - description: 'Verify implementation matches change artifacts before archiving', - category: 'Workflow', - tags: ['workflow', 'verify', 'experimental'], - content: `Verify that an implementation matches the change artifacts (specs, tasks, design). + return { + name: 'OPSX: Verify', + description: 'Verify implementation matches change artifacts before archiving', + category: 'Workflow', + tags: ['workflow', 'verify', 'experimental'], + content: `Verify that an implementation matches the change artifacts (specs, tasks, design). **Input**: Optionally specify \`--change \` after \`/opsx:verify\`. If omitted, MUST prompt for available changes. @@ -2299,17 +2303,17 @@ Use clear markdown with: - Code references in format: \`file.ts:123\` - Specific, actionable recommendations - No vague suggestions like "consider reviewing"` - }; + }; } /** * Template for feedback skill * For collecting and submitting user feedback with context enrichment */ export function getFeedbackSkillTemplate(): SkillTemplate { - return { - name: 'feedback', - description: 'Collect and submit user feedback about OpenSpec with context enrichment and anonymization.', - instructions: `Help the user submit feedback about OpenSpec. + return { + name: 'feedback', + description: 'Collect and submit user feedback about OpenSpec with context enrichment and anonymization.', + instructions: `Help the user submit feedback about OpenSpec. **Goal**: Guide the user through collecting, enriching, and submitting feedback while ensuring privacy through anonymization. @@ -2407,5 +2411,5 @@ Does this look good? I can modify it if you'd like, or submit it as-is. \`\`\` Only proceed with submission after user confirms.` - }; + }; } diff --git a/src/core/update.ts b/src/core/update.ts index 41fd77208..7448e76a8 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -4,6 +4,7 @@ import { OPENSPEC_DIR_NAME } from './config.js'; import { ToolRegistry } from './configurators/registry.js'; import { SlashCommandRegistry } from './configurators/slash/registry.js'; import { agentsTemplate } from './templates/agents-template.js'; +import { TemplateManager } from './templates/index.js'; export class UpdateCommand { async execute(projectPath: string): Promise { @@ -21,6 +22,24 @@ export class UpdateCommand { await FileSystemUtils.writeFile(agentsPath, agentsTemplate); + // 2b. Create missing template files (e.g., architecture.md for existing projects) + const templates = TemplateManager.getTemplates({}); + const createdTemplates: string[] = []; + for (const template of templates) { + // Skip AGENTS.md (already handled above) and project.md (user content) + if (template.path === 'AGENTS.md' || template.path === 'project.md') { + continue; + } + const templatePath = path.join(openspecPath, template.path); + if (!await FileSystemUtils.fileExists(templatePath)) { + const content = typeof template.content === 'function' + ? template.content({}) + : template.content; + await FileSystemUtils.writeFile(templatePath, content); + createdTemplates.push(template.path); + } + } + // 3. Update existing AI tool configuration files only const configurators = ToolRegistry.getAll(); const slashConfigurators = SlashCommandRegistry.getAll(); @@ -59,8 +78,7 @@ export class UpdateCommand { } catch (error) { failedFiles.push(configurator.configFileName); console.error( - `Failed to update ${configurator.configFileName}: ${ - error instanceof Error ? error.message : String(error) + `Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error) }` ); } @@ -80,8 +98,7 @@ export class UpdateCommand { } catch (error) { failedSlashTools.push(slashConfigurator.toolId); console.error( - `Failed to update slash commands for ${slashConfigurator.toolId}: ${ - error instanceof Error ? error.message : String(error) + `Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error) }` ); } @@ -105,6 +122,10 @@ export class UpdateCommand { summaryParts.push(`Updated AI tool files: ${aiToolFiles.join(', ')}`); } + if (createdTemplates.length > 0) { + summaryParts.push(`Created: ${createdTemplates.join(', ')}`); + } + if (updatedSlashFiles.length > 0) { // Normalize to forward slashes for cross-platform log consistency const normalized = updatedSlashFiles.map((p) => FileSystemUtils.toPosixPath(p)); @@ -124,6 +145,12 @@ export class UpdateCommand { console.log(summaryParts.join(' | ')); - // No additional notes + // Show tip for newly created architecture.md + if (createdTemplates.includes('architecture.md')) { + console.log(); + console.log('Tip: Ask your AI assistant to populate the architecture document:'); + console.log(' "Please analyze my codebase and update openspec/architecture.md'); + console.log(' with the actual components, data flows, and integration points"'); + } } }