diff --git a/docs/schema-customization.md b/docs/schema-customization.md new file mode 100644 index 00000000..d4cc3f25 --- /dev/null +++ b/docs/schema-customization.md @@ -0,0 +1,211 @@ +# Schema Customization + +This document describes how users can customize OpenSpec schemas and templates, the current manual process, and the gap that needs to be addressed. + +--- + +## Overview + +OpenSpec uses a 2-level schema resolution system following the XDG Base Directory Specification: + +1. **User override**: `${XDG_DATA_HOME}/openspec/schemas//` +2. **Package built-in**: `/schemas//` + +When a schema is requested (e.g., `spec-driven`), the resolver checks the user directory first. If found, that entire schema directory is used. Otherwise, it falls back to the package's built-in schema. + +--- + +## Current Manual Process + +To override the default `spec-driven` schema, a user must: + +### 1. Determine the correct directory path + +| Platform | Path | +|----------|------| +| macOS/Linux | `~/.local/share/openspec/schemas/` | +| Windows | `%LOCALAPPDATA%\openspec\schemas\` | +| All (if set) | `$XDG_DATA_HOME/openspec/schemas/` | + +### 2. Create the directory structure + +```bash +# macOS/Linux example +mkdir -p ~/.local/share/openspec/schemas/spec-driven/templates +``` + +### 3. Find and copy the default schema files + +The user must locate the installed npm package to copy the defaults: + +```bash +# Find the package location (varies by install method) +npm list -g openspec --parseable +# or +which openspec && readlink -f $(which openspec) + +# Copy files from the package's schemas/ directory +cp /schemas/spec-driven/schema.yaml ~/.local/share/openspec/schemas/spec-driven/ +cp /schemas/spec-driven/templates/*.md ~/.local/share/openspec/schemas/spec-driven/templates/ +``` + +### 4. Modify the copied files + +Edit `schema.yaml` to change the workflow structure: + +```yaml +name: spec-driven +version: 1 +description: My custom workflow +artifacts: + - id: proposal + generates: proposal.md + description: Initial proposal + template: proposal.md + requires: [] + # Add, remove, or modify artifacts... +``` + +Edit templates in `templates/` to customize the content guidance. + +### 5. Verify the override is active + +Currently there's no command to verify which schema is being used. Users must trust that the file exists in the right location. + +--- + +## Gap Analysis + +The current process has several friction points: + +| Issue | Impact | +|-------|--------| +| **Path discovery** | Users must know XDG conventions and platform-specific paths | +| **Package location** | Finding the npm package path varies by install method (global, local, pnpm, yarn, volta, etc.) | +| **No scaffolding** | Users must manually create directories and copy files | +| **No verification** | No way to confirm which schema is actually being resolved | +| **No diffing** | When upgrading openspec, users can't see what changed in built-in templates | +| **Full copy required** | Must copy entire schema even to change one template | + +### User Stories Not Currently Supported + +1. *"I want to add a `research` artifact before `proposal`"* — requires manual copy and edit +2. *"I want to customize just the proposal template"* — must copy entire schema +3. *"I want to see what the default schema looks like"* — must find package path +4. *"I want to revert to defaults"* — must delete files and hope paths are correct +5. *"I upgraded openspec, did the templates change?"* — no way to diff + +--- + +## Proposed Solution: Schema Configurator + +A CLI command (or set of commands) that handles path resolution and file operations for users. + +### Option A: Single `openspec schema` command + +```bash +# List available schemas (built-in and user overrides) +openspec schema list + +# Show where a schema resolves from +openspec schema which spec-driven +# Output: /Users/me/.local/share/openspec/schemas/spec-driven/ (user override) +# Output: /usr/local/lib/node_modules/openspec/schemas/spec-driven/ (built-in) + +# Copy a built-in schema to user directory for customization +openspec schema copy spec-driven +# Creates ~/.local/share/openspec/schemas/spec-driven/ with all files + +# Show diff between user override and built-in +openspec schema diff spec-driven + +# Remove user override (revert to built-in) +openspec schema reset spec-driven + +# Validate a schema +openspec schema validate spec-driven +``` + +### Option B: Dedicated `openspec customize` command + +```bash +# Interactive schema customization +openspec customize +# Prompts: Which schema? What do you want to change? etc. + +# Copy and open for editing +openspec customize spec-driven +# Copies to user dir, prints path, optionally opens in $EDITOR +``` + +### Option C: Init-time schema selection + +```bash +# During project init, offer schema customization +openspec init +# ? Select a workflow schema: +# > spec-driven (default) +# tdd +# minimal +# custom (copy and edit) +``` + +### Recommended Approach + +**Option A** provides the most flexibility and follows Unix conventions (subcommands for discrete operations). Key commands in priority order: + +1. `openspec schema list` — see what's available +2. `openspec schema which ` — debug resolution +3. `openspec schema copy ` — scaffold customization +4. `openspec schema diff ` — compare with built-in +5. `openspec schema reset ` — revert to defaults + +--- + +## Implementation Considerations + +### Path Resolution + +The resolver already exists in `src/core/artifact-graph/resolver.ts`: + +```typescript +export function getPackageSchemasDir(): string { ... } +export function getUserSchemasDir(): string { ... } +export function getSchemaDir(name: string): string | null { ... } +export function listSchemas(): string[] { ... } +``` + +New commands would leverage these existing functions. + +### File Operations + +- Copy should preserve file permissions +- Copy should not overwrite existing user files without `--force` +- Reset should prompt for confirmation + +### Template-Only Overrides + +A future enhancement could support overriding individual templates without copying the entire schema. This would require changes to the resolution logic: + +``` +Current: schema dir (user) OR schema dir (built-in) +Future: schema.yaml from user OR built-in + + each template from user OR built-in (independent fallback) +``` + +This adds complexity but enables the "I just want to change one template" use case. + +--- + +## Related Documents + +- [Schema Workflow Gaps](./schema-workflow-gaps.md) — End-to-end workflow analysis and phased implementation plan + +## Related Files + +| File | Purpose | +|------|---------| +| `src/core/artifact-graph/resolver.ts` | Schema resolution logic | +| `src/core/artifact-graph/instruction-loader.ts` | Template loading | +| `src/core/global-config.ts` | XDG path helpers | +| `schemas/spec-driven/` | Default schema and templates | diff --git a/docs/schema-workflow-gaps.md b/docs/schema-workflow-gaps.md new file mode 100644 index 00000000..c378bc1e --- /dev/null +++ b/docs/schema-workflow-gaps.md @@ -0,0 +1,380 @@ +# Schema Workflow: End-to-End Analysis + +This document analyzes the complete user journey for working with schemas in OpenSpec, identifies gaps, and proposes a phased solution. + +--- + +## Current State + +### What Exists + +| Component | Status | +|-----------|--------| +| Schema resolution (XDG) | 2-level: user override → package built-in | +| Built-in schemas | `spec-driven`, `tdd` | +| Artifact workflow commands | `status`, `next`, `instructions`, `templates` with `--schema` flag | +| Change creation | `openspec new change ` — no schema binding | + +### What's Missing + +| Component | Status | +|-----------|--------| +| Schema bound to change | Not stored — must pass `--schema` every time | +| Project-local schemas | Not supported — can't version control with repo | +| Schema management CLI | None — manual path discovery required | +| Project default schema | None — hardcoded to `spec-driven` | + +--- + +## User Journey Analysis + +### Scenario 1: Using a Non-Default Schema + +**Goal:** User wants to use TDD workflow for a new feature. + +**Today's experience:** +```bash +openspec new change add-auth +# Creates directory, no schema info stored + +openspec status --change add-auth +# Shows spec-driven artifacts (WRONG - user wanted TDD) + +# User realizes mistake... +openspec status --change add-auth --schema tdd +# Correct, but must remember --schema every time + +# 6 months later... +openspec status --change add-auth +# Wrong again - nobody remembers this was TDD +``` + +**Problems:** +- Schema is a runtime argument, not persisted +- Easy to forget `--schema` and get wrong results +- No record of intended schema for future reference + +--- + +### Scenario 2: Customizing a Schema + +**Goal:** User wants to add a "research" artifact before "proposal". + +**Today's experience:** +```bash +# Step 1: Figure out where to put overrides +# Must know XDG conventions: +# macOS/Linux: ~/.local/share/openspec/schemas/ +# Windows: %LOCALAPPDATA%\openspec\schemas/ + +# Step 2: Create directory structure +mkdir -p ~/.local/share/openspec/schemas/my-workflow/templates + +# Step 3: Find the npm package to copy defaults +npm list -g openspec --parseable +# Output varies by package manager: +# npm: /usr/local/lib/node_modules/openspec +# pnpm: ~/.local/share/pnpm/global/5/node_modules/openspec +# volta: ~/.volta/tools/image/packages/openspec/... +# yarn: ~/.config/yarn/global/node_modules/openspec + +# Step 4: Copy files +cp -r /schemas/spec-driven/* \ + ~/.local/share/openspec/schemas/my-workflow/ + +# Step 5: Edit schema.yaml and templates +# No way to verify override is active +# No way to diff against original +``` + +**Problems:** +- Must know XDG path conventions +- Finding npm package path varies by install method +- No tooling to scaffold or verify +- No diff capability when upgrading openspec + +--- + +### Scenario 3: Team Sharing Custom Workflow + +**Goal:** Team wants everyone to use the same custom schema. + +**Today's options:** +1. Everyone manually sets up XDG override — error-prone, drift risk +2. Document setup in README — still manual, easy to miss +3. Publish separate npm package — overkill for most teams +4. Check schema into repo — **not supported** (no project-local resolution) + +**Problems:** +- No project-local schema resolution +- Can't version control custom schemas with the codebase +- No single source of truth for team workflow + +--- + +## Gap Summary + +| Gap | Impact | Workaround | +|-----|--------|------------| +| Schema not bound to change | Wrong results, forgotten context | Remember to pass `--schema` | +| No project-local schemas | Can't share via repo | Manual XDG setup per machine | +| No schema management CLI | Manual path hunting | Know XDG + find npm package | +| No project default schema | Must specify every time | Always pass `--schema` | +| No init-time schema selection | Missed setup opportunity | Manual config | + +--- + +## Proposed Architecture + +### New File Structure + +``` +openspec/ +├── config.yaml # Project config (NEW) +├── schemas/ # Project-local schemas (NEW) +│ └── my-workflow/ +│ ├── schema.yaml +│ └── templates/ +│ ├── research.md +│ ├── proposal.md +│ └── ... +└── changes/ + └── add-auth/ + ├── change.yaml # Change metadata (NEW) + ├── proposal.md + └── ... +``` + +### config.yaml (Project Config) + +```yaml +# openspec/config.yaml +defaultSchema: spec-driven +``` + +Sets the project-wide default schema. Used when: +- Creating new changes without `--schema` +- Running commands on changes without `change.yaml` + +### change.yaml (Change Metadata) + +```yaml +# openspec/changes/add-auth/change.yaml +schema: tdd +created: 2025-01-15T10:30:00Z +description: Add user authentication system +``` + +Binds a specific schema to a change. Created automatically by `openspec new change`. + +### Schema Resolution Order + +``` +1. ./openspec/schemas// # Project-local +2. ~/.local/share/openspec/schemas// # User global (XDG) +3. /schemas// # Built-in +``` + +Project-local takes priority, enabling version-controlled custom schemas. + +### Schema Selection Order (Per Command) + +``` +1. --schema CLI flag # Explicit override +2. change.yaml in change directory # Change-specific binding +3. openspec/config.yaml defaultSchema # Project default +4. "spec-driven" # Hardcoded fallback +``` + +--- + +## Ideal User Experience + +### Creating a Change + +```bash +# Uses project default (from config.yaml, or spec-driven) +openspec new change add-auth +# Creates openspec/changes/add-auth/change.yaml: +# schema: spec-driven +# created: 2025-01-15T10:30:00Z + +# Explicit schema for this change +openspec new change add-auth --schema tdd +# Creates change.yaml with schema: tdd +``` + +### Working with Changes + +```bash +# Auto-reads schema from change.yaml — no --schema needed +openspec status --change add-auth +# Output: "Change: add-auth (schema: tdd)" + +openspec next --change add-auth +# Knows to use TDD artifacts + +# Explicit override still works (with informational message) +openspec status --change add-auth --schema spec-driven +# "Note: change.yaml specifies 'tdd', using 'spec-driven' per --schema flag" +``` + +### Customizing Schemas + +```bash +# See what's available +openspec schema list +# Built-in: +# spec-driven proposal → specs → design → tasks +# tdd spec → tests → implementation → docs +# Project: (none) +# User: (none) + +# Copy to project for customization +openspec schema copy spec-driven my-workflow +# Created ./openspec/schemas/my-workflow/ +# Edit schema.yaml and templates/ to customize + +# Copy to global (user-level override) +openspec schema copy spec-driven --global +# Created ~/.local/share/openspec/schemas/spec-driven/ + +# See where a schema resolves from +openspec schema which spec-driven +# ./openspec/schemas/spec-driven/ (project) +# or: ~/.local/share/openspec/schemas/spec-driven/ (user) +# or: /usr/local/lib/node_modules/openspec/schemas/spec-driven/ (built-in) + +# Compare override with built-in +openspec schema diff spec-driven +# Shows diff between user/project version and package built-in + +# Remove override, revert to built-in +openspec schema reset spec-driven +# Removes ./openspec/schemas/spec-driven/ (or --global for user dir) +``` + +### Project Setup + +```bash +openspec init +# ? Select default workflow schema: +# > spec-driven (proposal → specs → design → tasks) +# tdd (spec → tests → implementation → docs) +# (custom schemas if detected) +# +# Writes to openspec/config.yaml: +# defaultSchema: spec-driven +``` + +--- + +## Implementation Phases + +### Phase 1: Change Metadata (change.yaml) + +**Priority:** High +**Solves:** "Forgot --schema", lost context, wrong results + +**Scope:** +- Create `change.yaml` when running `openspec new change` +- Store `schema`, `created` timestamp +- Modify workflow commands to read schema from `change.yaml` +- `--schema` flag overrides (with informational message) +- Backwards compatible: missing `change.yaml` → use default + +**change.yaml format:** +```yaml +schema: tdd +created: 2025-01-15T10:30:00Z +``` + +**Migration:** +- Existing changes without `change.yaml` continue to work +- Default to `spec-driven` (current behavior) +- Optional: `openspec migrate` to add `change.yaml` to existing changes + +--- + +### Phase 2: Project-Local Schemas + +**Priority:** High +**Solves:** Team sharing, version control, no XDG knowledge needed + +**Scope:** +- Add `./openspec/schemas/` to resolution order (first priority) +- `openspec schema copy [new-name]` creates in project by default +- `--global` flag for user-level XDG directory +- Teams can commit `openspec/schemas/` to repo + +**Resolution order:** +``` +1. ./openspec/schemas// # Project-local (NEW) +2. ~/.local/share/openspec/schemas// # User global +3. /schemas// # Built-in +``` + +--- + +### Phase 3: Schema Management CLI + +**Priority:** Medium +**Solves:** Path discovery, scaffolding, debugging + +**Commands:** +```bash +openspec schema list # Show available schemas with sources +openspec schema which # Show resolution path +openspec schema copy [to] # Copy for customization +openspec schema diff # Compare with built-in +openspec schema reset # Remove override +openspec schema validate # Validate schema.yaml structure +``` + +--- + +### Phase 4: Project Config + Init Enhancement + +**Priority:** Low +**Solves:** Project-wide defaults, streamlined setup + +**Scope:** +- Add `openspec/config.yaml` with `defaultSchema` field +- `openspec init` prompts for schema selection +- Store selection in `config.yaml` +- Commands use as fallback when no `change.yaml` exists + +**config.yaml format:** +```yaml +defaultSchema: spec-driven +``` + +--- + +## Backwards Compatibility + +| Scenario | Behavior | +|----------|----------| +| Existing change without `change.yaml` | Uses `--schema` flag or project default or `spec-driven` | +| Existing project without `config.yaml` | Falls back to `spec-driven` | +| `--schema` flag provided | Overrides `change.yaml` (with info message) | +| No project-local schemas dir | Skipped in resolution, checks user/built-in | + +All existing functionality continues to work. New features are additive. + +--- + +## Related Documents + +- [Schema Customization](./schema-customization.md) — Details on manual override process and CLI gaps +- [Artifact POC](./artifact_poc.md) — Core artifact graph architecture + +## Related Code + +| File | Purpose | +|------|---------| +| `src/core/artifact-graph/resolver.ts` | Schema resolution logic | +| `src/core/artifact-graph/instruction-loader.ts` | Template loading | +| `src/core/global-config.ts` | XDG path helpers | +| `src/commands/artifact-workflow.ts` | CLI commands | +| `src/utils/change-utils.ts` | Change creation utilities | diff --git a/schemas/spec-driven/schema.yaml b/schemas/spec-driven/schema.yaml index d7cea150..03df6f9d 100644 --- a/schemas/spec-driven/schema.yaml +++ b/schemas/spec-driven/schema.yaml @@ -6,23 +6,131 @@ artifacts: generates: proposal.md description: Initial proposal document outlining the change template: proposal.md + instruction: | + Create the proposal document that establishes WHY this change is needed. + + Sections: + - **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now? + - **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**. + - **Impact**: Two parts: + - Affected specs: List capabilities that will have spec deltas + - Affected code: Key files, APIs, dependencies, or systems + + Keep it concise (1-2 pages). Focus on the "why" not the "how" - + implementation details belong in design.md. + + This is the foundation - specs, design, and tasks all build on this. requires: [] + - id: specs generates: "specs/**/*.md" description: Detailed specifications for the change template: spec.md + instruction: | + Create specification files that define WHAT the system should do. + + Create one spec file per capability/feature area in specs//spec.md. + + Delta operations (use ## headers): + - **ADDED Requirements**: New capabilities + - **MODIFIED Requirements**: Changed behavior - MUST include full updated content + - **REMOVED Requirements**: Deprecated features - MUST include **Reason** and **Migration** + - **RENAMED Requirements**: Name changes only - use FROM:/TO: format + + Format requirements: + - Each requirement: `### Requirement: ` followed by description + - Use SHALL/MUST for normative requirements (avoid should/may) + - Each scenario: `#### Scenario: ` with WHEN/THEN format + - **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently. + - Every requirement MUST have at least one scenario. + + MODIFIED requirements workflow: + 1. Locate the existing requirement in openspec/specs//spec.md + 2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios) + 3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior + 4. Ensure header text matches exactly (whitespace-insensitive) + + Common pitfall: Using MODIFIED with partial content loses detail at archive time. + If adding new concerns without changing existing behavior, use ADDED instead. + + Example: + ``` + ## ADDED Requirements + + ### Requirement: User can export data + The system SHALL allow users to export their data in CSV format. + + #### Scenario: Successful export + - **WHEN** user clicks "Export" button + - **THEN** system downloads a CSV file with all user data + + ## REMOVED Requirements + + ### Requirement: Legacy export + **Reason**: Replaced by new export system + **Migration**: Use new export endpoint at /api/v2/export + ``` + + Specs should be testable - each scenario is a potential test case. requires: - proposal + - id: design generates: design.md description: Technical design document with implementation details template: design.md + instruction: | + Create the design document that explains HOW to implement the change. + + When to include design.md (create only if any apply): + - Cross-cutting change (multiple services/modules) or new architectural pattern + - New external dependency or significant data model changes + - Security, performance, or migration complexity + - Ambiguity that benefits from technical decisions before coding + + Sections: + - **Context**: Background, current state, constraints, stakeholders + - **Goals / Non-Goals**: What this design achieves and explicitly excludes + - **Decisions**: Key technical choices with rationale (why X over Y?). Include alternatives considered for each decision. + - **Risks / Trade-offs**: Known limitations, things that could go wrong. Format: [Risk] → Mitigation + - **Migration Plan**: Steps to deploy, rollback strategy (if applicable) + - **Open Questions**: Outstanding decisions or unknowns to resolve + + Focus on architecture and approach, not line-by-line implementation. + Reference the proposal for motivation and specs for requirements. + + Good design docs explain the "why" behind technical decisions. requires: - proposal + - id: tasks generates: tasks.md description: Implementation tasks derived from specs and design template: tasks.md + instruction: | + Create the task list that breaks down the implementation work. + + Guidelines: + - Group related tasks under ## numbered headings + - Each task is a checkbox: - [ ] X.Y Task description + - Tasks should be small enough to complete in one session + - Order tasks by dependency (what must be done first?) + + Example: + ``` + ## 1. Setup + + - [ ] 1.1 Create new module structure + - [ ] 1.2 Add dependencies to package.json + + ## 2. Core Implementation + + - [ ] 2.1 Implement data export function + - [ ] 2.2 Add CSV formatting utilities + ``` + + Reference specs for what needs to be built, design for how to build it. + Each task should be verifiable - you know when it's done. requires: - specs - design diff --git a/schemas/tdd/schema.yaml b/schemas/tdd/schema.yaml index 7b1b5bad..c36d9c1c 100644 --- a/schemas/tdd/schema.yaml +++ b/schemas/tdd/schema.yaml @@ -6,22 +6,201 @@ artifacts: generates: spec.md description: Feature specification defining requirements template: spec.md + instruction: | + Create the feature specification that defines WHAT to build. + + Sections: + - **Feature**: Name and high-level description of the feature's purpose and user value + - **Requirements**: List of specific requirements. Use SHALL/MUST for normative language. + - **Acceptance Criteria**: Testable criteria in WHEN/THEN format + + Format requirements: + - Each requirement should be specific and testable + - Use `#### Scenario: ` with WHEN/THEN format for acceptance criteria + - Define edge cases and error scenarios explicitly + - Every requirement MUST have at least one scenario + + Example: + ``` + ## Feature: User Authentication + + Users can securely log into the application. + + ## Requirements + + ### Requirement: Password validation + The system SHALL validate passwords meet minimum security requirements. + + #### Scenario: Valid password accepted + - **WHEN** password has 8+ chars, uppercase, lowercase, and number + - **THEN** password is accepted + + #### Scenario: Weak password rejected + - **WHEN** password is less than 8 characters + - **THEN** system displays "Password too short" error + ``` + + This spec drives test creation - each scenario becomes a test case. requires: [] + - id: tests generates: "tests/*.test.ts" description: Test files written before implementation template: test.md + instruction: | + Write tests BEFORE implementation (TDD red phase). + + File naming: + - Create test files as `tests/.test.ts` + - One test file per feature/capability + - Use descriptive names matching the spec + + Test structure: + - Use Given/When/Then format matching spec scenarios + - Group related tests with `describe()` blocks + - Each scenario from spec becomes at least one `it()` test + + Coverage requirements: + - Cover each requirement from the spec + - Include happy path (success cases) + - Include edge cases (boundary conditions) + - Include error scenarios (invalid input, failures) + - Tests should fail initially (no implementation yet) + + Example: + ```typescript + describe('Password validation', () => { + it('accepts valid password with all requirements', () => { + // GIVEN a password meeting all requirements + const password = 'SecurePass1'; + // WHEN validating + const result = validatePassword(password); + // THEN it should be accepted + expect(result.valid).toBe(true); + }); + + it('rejects password shorter than 8 characters', () => { + // GIVEN a short password + const password = 'Short1'; + // WHEN validating + const result = validatePassword(password); + // THEN it should be rejected with message + expect(result.valid).toBe(false); + expect(result.error).toBe('Password too short'); + }); + }); + ``` + + Follow the spec requirements exactly - tests verify the spec. requires: - spec + - id: implementation generates: "src/*.ts" description: Implementation code to pass the tests template: implementation.md + instruction: | + Implement the feature to make tests pass (TDD green phase). + + TDD workflow: + 1. Run tests - confirm they fail (red) + 2. Write minimal code to pass ONE test + 3. Run tests - confirm that test passes (green) + 4. Refactor if needed while keeping tests green + 5. Repeat for next failing test + + Implementation guidelines: + - Write minimal code to pass each test - no more, no less + - Run tests frequently to verify progress + - Keep functions small and focused + - Use clear, descriptive names + + Code organization: + - Create source files in `src/.ts` + - Export public API clearly + - Keep implementation details private + - Add JSDoc comments for public functions + + Example structure: + ```typescript + /** + * Validates a password meets security requirements. + * @param password - The password to validate + * @returns Validation result with valid flag and optional error + */ + export function validatePassword(password: string): ValidationResult { + if (password.length < 8) { + return { valid: false, error: 'Password too short' }; + } + // ... additional checks + return { valid: true }; + } + ``` + + Don't over-engineer - implement only what tests require. requires: - tests + - id: docs generates: "docs/*.md" description: Documentation for the implemented feature template: docs.md + instruction: | + Document the implemented feature. + + Sections: + - **Overview**: What the feature does and why it exists (1-2 paragraphs) + - **Getting Started**: Quick start guide to use the feature immediately + - **Examples**: Code examples showing common use cases + - **Reference**: Detailed API documentation, configuration options + + Guidelines: + - Write for the user, not the developer + - Start with the most common use case + - Include copy-pasteable code examples + - Document all configuration options with defaults + - Note any limitations, edge cases, or gotchas + - Link to related features or specs + + Example structure: + ```markdown + ## Overview + + Password validation ensures user passwords meet security requirements + before account creation or password changes. + + ## Getting Started + + Import and use the validation function: + + ```typescript + import { validatePassword } from './password'; + + const result = validatePassword('MySecurePass1'); + if (!result.valid) { + console.error(result.error); + } + ``` + + ## Examples + + ### Basic validation + ... + + ### Custom error handling + ... + + ## Reference + + ### validatePassword(password) + + | Parameter | Type | Description | + |-----------|------|-------------| + | password | string | The password to validate | + + **Returns**: `{ valid: boolean, error?: string }` + ``` + + Reference the spec for requirements, implementation for details. requires: - implementation diff --git a/src/commands/artifact-workflow.ts b/src/commands/artifact-workflow.ts index 001ce4e6..7ac2d089 100644 --- a/src/commands/artifact-workflow.ts +++ b/src/commands/artifact-workflow.ts @@ -312,38 +312,93 @@ async function instructionsCommand( } function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void { + const { + artifactId, + changeName, + schemaName, + changeDir, + outputPath, + description, + instruction, + template, + dependencies, + unlocks, + } = instructions; + + // Opening tag + console.log(``); + console.log(); + + // Warning for blocked artifacts if (isBlocked) { - console.log(chalk.yellow('Warning: This artifact has unmet dependencies.')); + const missing = dependencies.filter((d) => !d.done).map((d) => d.id); + console.log(''); + console.log('This artifact has unmet dependencies. Complete them first or proceed with caution.'); + console.log(`Missing: ${missing.join(', ')}`); + console.log(''); console.log(); } - console.log(`Artifact: ${instructions.artifactId}`); - console.log(`Output: ${instructions.outputPath}`); - console.log(`Description: ${instructions.description}`); + // Task directive + console.log(''); + console.log(`Create the ${artifactId} artifact for change "${changeName}".`); + console.log(description); + console.log(''); console.log(); - console.log('Dependencies:'); - if (instructions.dependencies.length === 0) { - console.log(' (none)'); - } else { - for (const dep of instructions.dependencies) { - const status = dep.done ? chalk.green('[done]') : chalk.red('[missing]'); - console.log(` ${status} ${dep.id}`); + // Context (dependencies) + if (dependencies.length > 0) { + console.log(''); + console.log('Read these files for context before creating this artifact:'); + console.log(); + for (const dep of dependencies) { + const status = dep.done ? 'done' : 'missing'; + const fullPath = path.join(changeDir, dep.path); + console.log(``); + console.log(` ${fullPath}`); + console.log(` ${dep.description}`); + console.log(''); } + console.log(''); + console.log(); } + + // Output location + console.log(''); + console.log(`Write to: ${path.join(changeDir, outputPath)}`); + console.log(''); console.log(); - if (instructions.unlocks.length > 0) { - console.log('Unlocks:'); - for (const unlocked of instructions.unlocks) { - console.log(` ${unlocked}`); - } + // Instruction (guidance) + if (instruction) { + console.log(''); + console.log(instruction.trim()); + console.log(''); + console.log(); + } + + // Template + console.log(''); + console.log(); + + // Success criteria placeholder + console.log(''); + console.log(''); + console.log(''); + console.log(); + + // Unlocks + if (unlocks.length > 0) { + console.log(''); + console.log(`Completing this artifact enables: ${unlocks.join(', ')}`); + console.log(''); console.log(); } - console.log('Template:'); - console.log('─'.repeat(40)); - console.log(instructions.template); + // Closing tag + console.log(''); } // ----------------------------------------------------------------------------- diff --git a/src/core/artifact-graph/index.ts b/src/core/artifact-graph/index.ts index f88c3269..a8b4bbe1 100644 --- a/src/core/artifact-graph/index.ts +++ b/src/core/artifact-graph/index.ts @@ -36,7 +36,7 @@ export { TemplateLoadError, type ChangeContext, type ArtifactInstructions, - type DependencyStatus, + type DependencyInfo, type ArtifactStatus, type ChangeStatus, } from './instruction-loader.js'; diff --git a/src/core/artifact-graph/instruction-loader.ts b/src/core/artifact-graph/instruction-loader.ts index 08cdb2de..2db20ec6 100644 --- a/src/core/artifact-graph/instruction-loader.ts +++ b/src/core/artifact-graph/instruction-loader.ts @@ -44,26 +44,34 @@ export interface ArtifactInstructions { artifactId: string; /** Schema name */ schemaName: string; + /** Full path to change directory */ + changeDir: string; /** Output path pattern (e.g., "proposal.md") */ outputPath: string; /** Artifact description */ description: string; - /** Template content */ + /** Guidance on how to create this artifact (from schema instruction field) */ + instruction: string | undefined; + /** Template content (structure to follow) */ template: string; - /** Dependencies with completion status */ - dependencies: DependencyStatus[]; + /** Dependencies with completion status and paths */ + dependencies: DependencyInfo[]; /** Artifacts that become available after completing this one */ unlocks: string[]; } /** - * Dependency status information. + * Dependency information including path and description. */ -export interface DependencyStatus { +export interface DependencyInfo { /** Artifact ID */ id: string; /** Whether the dependency is completed */ done: boolean; + /** Relative output path of the dependency (e.g., "proposal.md") */ + path: string; + /** Description of the dependency artifact */ + description: string; } /** @@ -176,15 +184,17 @@ export function generateInstructions( } const template = loadTemplate(context.schemaName, artifact.template); - const dependencies = getDependencyStatus(artifact, context.completed); + const dependencies = getDependencyInfo(artifact, context.graph, context.completed); const unlocks = getUnlockedArtifacts(context.graph, artifactId); return { changeName: context.changeName, artifactId: artifact.id, schemaName: context.schemaName, + changeDir: context.changeDir, outputPath: artifact.generates, description: artifact.description, + instruction: artifact.instruction, template, dependencies, unlocks, @@ -192,16 +202,22 @@ export function generateInstructions( } /** - * Gets dependency status for an artifact. + * Gets dependency info including paths and descriptions. */ -function getDependencyStatus( +function getDependencyInfo( artifact: Artifact, + graph: ArtifactGraph, completed: CompletedSet -): DependencyStatus[] { - return artifact.requires.map(id => ({ - id, - done: completed.has(id), - })); +): DependencyInfo[] { + return artifact.requires.map(id => { + const depArtifact = graph.getArtifact(id); + return { + id, + done: completed.has(id), + path: depArtifact?.generates ?? id, + description: depArtifact?.description ?? '', + }; + }); } /** diff --git a/src/core/artifact-graph/types.ts b/src/core/artifact-graph/types.ts index 1758b27e..1175e405 100644 --- a/src/core/artifact-graph/types.ts +++ b/src/core/artifact-graph/types.ts @@ -6,6 +6,7 @@ export const ArtifactSchema = z.object({ generates: z.string().min(1, { error: 'generates field is required' }), description: z.string(), template: z.string().min(1, { error: 'template field is required' }), + instruction: z.string().optional(), requires: z.array(z.string()).default([]), }); diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 38a68556..9a5df479 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -259,9 +259,9 @@ describe('artifact-workflow CLI commands', () => { cwd: tempDir, }); expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('Artifact: proposal'); + expect(result.stdout).toContain(''); }); it('shows instructions for design artifact', async () => { @@ -271,9 +271,9 @@ describe('artifact-workflow CLI commands', () => { cwd: tempDir, }); expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('Artifact: design'); + expect(result.stdout).toContain(''); }); it('shows blocked warning for artifact with unmet dependencies', async () => { @@ -284,8 +284,8 @@ describe('artifact-workflow CLI commands', () => { cwd: tempDir, }); expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('Warning: This artifact has unmet dependencies'); - expect(result.stdout).toContain('[missing]'); + expect(result.stdout).toContain(''); + expect(result.stdout).toContain('status="missing"'); }); it('outputs JSON for instructions', async () => { diff --git a/test/core/list.test.ts b/test/core/list.test.ts index edc53001..5a678919 100644 --- a/test/core/list.test.ts +++ b/test/core/list.test.ts @@ -115,19 +115,19 @@ Regular text that should be ignored expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true); }); - it('should sort changes alphabetically', async () => { + it('should sort changes alphabetically when sort=name', async () => { const changesDir = path.join(tempDir, 'openspec', 'changes'); await fs.mkdir(path.join(changesDir, 'zebra'), { recursive: true }); await fs.mkdir(path.join(changesDir, 'alpha'), { recursive: true }); await fs.mkdir(path.join(changesDir, 'middle'), { recursive: true }); const listCommand = new ListCommand(); - await listCommand.execute(tempDir); + await listCommand.execute(tempDir, 'changes', { sort: 'name' }); - const changeLines = logOutput.filter(line => + const changeLines = logOutput.filter(line => line.includes('alpha') || line.includes('middle') || line.includes('zebra') ); - + expect(changeLines[0]).toContain('alpha'); expect(changeLines[1]).toContain('middle'); expect(changeLines[2]).toContain('zebra');