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 8e705f1c8..b9e407e0c 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. **IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with \`/opsx:new\` or \`/opsx:ff\`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. @@ -53,6 +53,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 @@ -301,7 +302,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` - }; + }; } /** @@ -309,10 +310,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. @@ -376,7 +377,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` - }; + }; } /** @@ -384,10 +385,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, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -471,8 +472,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): @@ -490,7 +494,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` - }; + }; } /** @@ -498,10 +502,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, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -647,7 +651,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` - }; + }; } /** @@ -655,10 +659,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. @@ -742,7 +746,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` - }; + }; } /** @@ -750,10 +754,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). @@ -880,7 +884,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` - }; + }; } // ----------------------------------------------------------------------------- @@ -888,11 +892,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; } /** @@ -900,12 +904,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. **IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with \`/opsx:new\` or \`/opsx:ff\`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. @@ -1072,19 +1076,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. @@ -1147,19 +1151,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 a change name after \`/opsx:continue\` (e.g., \`/opsx:continue add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -1262,19 +1266,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 a change name (e.g., \`/opsx:apply add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -1420,7 +1424,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` - }; + }; } @@ -1428,12 +1432,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. @@ -1517,7 +1521,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` - }; + }; } /** @@ -1525,10 +1529,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, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -1631,7 +1635,7 @@ All artifacts complete. All tasks complete. - Show clear summary of what happened - If sync is requested, use openspec-sync-specs approach (agent-driven) - If delta specs exist, always run the sync assessment and show the combined summary before prompting` - }; + }; } /** @@ -1884,12 +1888,12 @@ No active changes found. Use \`/opsx:new\` to create a new change. * 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). @@ -2016,7 +2020,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` - }; + }; } /** @@ -2024,10 +2028,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, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -2184,19 +2188,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 a change name after \`/opsx:archive\` (e.g., \`/opsx:archive add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -2346,7 +2350,7 @@ Target archive directory already exists. - Show clear summary of what happened - If sync is requested, use /opsx:sync approach (agent-driven) - If delta specs exist, always run the sync assessment and show the combined summary before prompting` - }; + }; } /** @@ -2600,12 +2604,12 @@ No active changes found. Use \`/opsx:new\` to create a new change. * 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 a change name after \`/opsx:verify\` (e.g., \`/opsx:verify add-auth\`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -2762,17 +2766,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. @@ -2870,5 +2874,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"'); + } } }