diff --git a/openspec/changes/add-amp-support/proposal.md b/openspec/changes/add-amp-support/proposal.md new file mode 100644 index 000000000..cf5ed393b --- /dev/null +++ b/openspec/changes/add-amp-support/proposal.md @@ -0,0 +1,11 @@ +## Why +Amp is a popular AI coding agent from Sourcegraph that uses Skills (`.agents/skills/`) to extend its capabilities. Today OpenSpec can scaffold slash commands for many IDEs but not Amp, so Amp users cannot run the proposal/apply/archive flows from their workflow. + +## What Changes +- Add Amp as a selectable native tool in `openspec init` so it creates `.agents/skills/openspec-proposal/SKILL.md`, `.agents/skills/openspec-apply/SKILL.md`, and `.agents/skills/openspec-archive/SKILL.md` with YAML frontmatter containing `name` and `description` fields plus the standard OpenSpec-managed body. +- Ensure `openspec update` refreshes the body of any existing Amp skills inside `.agents/skills/` without creating missing files, mirroring the behavior of other tools. +- Share e2e/template coverage confirming the generator writes the proper directory, filename casing, and frontmatter format so Amp picks up the skills. + +## Impact +- Affected specs: `specs/cli-init`, `specs/cli-update` +- Expected code: CLI init/update tool registries, slash-command configurator, associated tests diff --git a/openspec/changes/add-amp-support/specs/cli-init/spec.md b/openspec/changes/add-amp-support/specs/cli-init/spec.md new file mode 100644 index 000000000..0b079026b --- /dev/null +++ b/openspec/changes/add-amp-support/specs/cli-init/spec.md @@ -0,0 +1,9 @@ +## MODIFIED Requirements +### Requirement: Slash Command Configuration +The init command SHALL generate slash command files for supported editors using shared templates. + +#### Scenario: Generating slash commands for Amp +- **WHEN** the user selects Amp during initialization +- **THEN** create `.agents/skills/openspec-proposal/SKILL.md`, `.agents/skills/openspec-apply/SKILL.md`, and `.agents/skills/openspec-archive/SKILL.md` +- **AND** ensure each file begins with YAML frontmatter that contains `name: ` and `description: ` fields followed by the shared OpenSpec workflow instructions wrapped in managed markers +- **AND** populate the skill body with the same proposal/apply/archive guidance used for other tools so Amp behaves like other AI coding assistants while pointing to the `.agents/skills/` directory diff --git a/openspec/changes/add-amp-support/specs/cli-update/spec.md b/openspec/changes/add-amp-support/specs/cli-update/spec.md new file mode 100644 index 000000000..ee6ccc23d --- /dev/null +++ b/openspec/changes/add-amp-support/specs/cli-update/spec.md @@ -0,0 +1,8 @@ +## MODIFIED Requirements +### Requirement: Slash Command Updates +The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments. + +#### Scenario: Updating slash commands for Amp +- **WHEN** `.agents/skills/` contains `openspec-proposal/SKILL.md`, `openspec-apply/SKILL.md`, and `openspec-archive/SKILL.md` +- **THEN** refresh the OpenSpec-managed portion of each file so the skill copy matches other tools while preserving the existing `name` and `description` frontmatter +- **AND** skip creating any missing skill files during update, mirroring the behavior for other IDEs diff --git a/openspec/changes/add-amp-support/tasks.md b/openspec/changes/add-amp-support/tasks.md new file mode 100644 index 000000000..798536caa --- /dev/null +++ b/openspec/changes/add-amp-support/tasks.md @@ -0,0 +1,13 @@ +## 1. CLI init support +- [x] 1.1 Surface Amp in the native-tool picker (interactive + `--tools`) so it toggles alongside other IDEs. +- [x] 1.2 Generate `.agents/skills/openspec-proposal/SKILL.md`, `.agents/skills/openspec-apply/SKILL.md`, and `.agents/skills/openspec-archive/SKILL.md` with YAML frontmatter containing `name` and `description` fields for each stage and wrap the body in OpenSpec markers. +- [x] 1.3 Confirm workspace scaffolding covers missing directory creation and re-run scenarios so repeated init refreshes the managed block. + +## 2. CLI update support +- [x] 2.1 Detect existing Amp skill files during `openspec update` and refresh only the managed body, skipping creation when files are missing. +- [x] 2.2 Ensure update logic preserves the `name` and `description` frontmatter block exactly as written by init, including case and spacing, and refreshes body templates alongside other tools. + +## 3. Templates and tests +- [x] 3.1 Add AmpSlashCommandConfigurator that reuses the shared proposal/apply/archive templates but targets `.agents/skills/` and includes the proper frontmatter. +- [x] 3.2 Register the configurator in the slash command registry. +- [x] 3.3 Expand automated coverage (unit or integration) verifying init and update produce the expected file paths and frontmatter + body markers for Amp. diff --git a/openspec/changes/refactor-workflow-configurator-naming/proposal.md b/openspec/changes/refactor-workflow-configurator-naming/proposal.md new file mode 100644 index 000000000..7f582f4cf --- /dev/null +++ b/openspec/changes/refactor-workflow-configurator-naming/proposal.md @@ -0,0 +1,26 @@ +## Why + +The base class `SlashCommandConfigurator` has a semantic mismatch with the actual tool ecosystem. Different AI tools use different terminology for their integration files: +- Claude/Cursor/Windsurf: "slash commands" or "workflows" +- Amp: "skills" (`.agents/skills/`) +- Codex/GitHub Copilot: "prompts" +- Gemini: "commands" (TOML format) + +The current naming misleadingly implies these configurators only handle slash commands, when they actually configure "OpenSpec workflow operations" (proposal/apply/archive) across diverse tool formats. + +## What Changes + +- Rename `SlashCommandConfigurator` → `WorkflowConfigurator` in `src/core/configurators/slash/base.ts` +- Rename `SlashCommandRegistry` → `WorkflowRegistry` in `src/core/configurators/slash/registry.ts` +- Rename `SlashCommandTarget` → `WorkflowTarget` interface +- Rename `SlashCommandId` → `WorkflowId` type in templates +- Rename directory `src/core/configurators/slash/` → `src/core/configurators/workflow/` +- Export old names as type aliases for backward compatibility +- Update all configurator subclasses and consumers to use new names +- Remove explanatory documentation workarounds added during Amp implementation + +## Impact + +- Affected specs: `cli-init`, `cli-update` (terminology in scenarios) +- Affected code: All files in `src/core/configurators/slash/`, template exports, init/update command implementations +- Breaking changes: None if aliases are maintained; internal refactor only diff --git a/openspec/changes/refactor-workflow-configurator-naming/specs/cli-init/spec.md b/openspec/changes/refactor-workflow-configurator-naming/specs/cli-init/spec.md new file mode 100644 index 000000000..7cb55edf9 --- /dev/null +++ b/openspec/changes/refactor-workflow-configurator-naming/specs/cli-init/spec.md @@ -0,0 +1,44 @@ +## MODIFIED Requirements + +### Requirement: Slash Command Configuration + +The init command SHALL generate workflow configuration files for supported editors using shared templates. + +#### Scenario: Generating workflow files for Claude Code + +- **WHEN** the user selects Claude Code during initialization +- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md` +- **AND** populate each file from shared templates so command text matches other tools +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating workflow files for Amp + +- **WHEN** the user selects Amp during initialization +- **THEN** create `.agents/skills/openspec-proposal/SKILL.md`, `.agents/skills/openspec-apply/SKILL.md`, and `.agents/skills/openspec-archive/SKILL.md` +- **AND** populate each file with YAML frontmatter containing `name` and `description` fields +- **AND** wrap the body in OpenSpec markers so `openspec update` can refresh the content +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating workflow files for Codex + +- **WHEN** the user selects Codex during initialization +- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set) +- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text) +- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes + +#### Scenario: Generating workflow files for GitHub Copilot + +- **WHEN** the user selects GitHub Copilot during initialization +- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md` +- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage +- **AND** include `$ARGUMENTS` placeholder to capture user input +- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content +- **AND** each template includes instructions for the relevant OpenSpec workflow stage + +#### Scenario: Generating workflow files for Gemini CLI + +- **WHEN** the user selects Gemini CLI during initialization +- **THEN** create `.gemini/commands/openspec/proposal.toml`, `.gemini/commands/openspec/apply.toml`, and `.gemini/commands/openspec/archive.toml` +- **AND** populate each file as TOML that sets a stage-specific `description = ""` and a multi-line `prompt = """` block with the shared OpenSpec template +- **AND** wrap the OpenSpec managed markers (`` / ``) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing +- **AND** ensure the workflow copy matches the existing proposal/apply/archive templates used by other tools diff --git a/openspec/changes/refactor-workflow-configurator-naming/tasks.md b/openspec/changes/refactor-workflow-configurator-naming/tasks.md new file mode 100644 index 000000000..b0d89b918 --- /dev/null +++ b/openspec/changes/refactor-workflow-configurator-naming/tasks.md @@ -0,0 +1,30 @@ +## 1. Core renaming + +- [ ] 1.1 Rename `SlashCommandConfigurator` → `WorkflowConfigurator` in `src/core/configurators/slash/base.ts` +- [ ] 1.2 Rename `SlashCommandTarget` → `WorkflowTarget` interface in `base.ts` +- [ ] 1.3 Rename `SlashCommandRegistry` → `WorkflowRegistry` in `registry.ts` +- [ ] 1.4 Rename `SlashCommandId` → `WorkflowId` in `src/core/templates/slash-command-templates.ts` +- [ ] 1.5 Rename directory `src/core/configurators/slash/` → `src/core/configurators/workflow/` + +## 2. Compatibility aliases + +- [ ] 2.1 Export `SlashCommandConfigurator` as alias for `WorkflowConfigurator` +- [ ] 2.2 Export `SlashCommandTarget` as alias for `WorkflowTarget` +- [ ] 2.3 Export `SlashCommandRegistry` as alias for `WorkflowRegistry` +- [ ] 2.4 Export `SlashCommandId` as alias for `WorkflowId` +- [ ] 2.5 Add deprecation JSDoc comments to all aliases + +## 3. Update consumers + +- [ ] 3.1 Update all configurator subclasses to extend `WorkflowConfigurator` +- [ ] 3.2 Update init command to use `WorkflowRegistry` +- [ ] 3.3 Update update command to use `WorkflowRegistry` +- [ ] 3.4 Update template exports in `src/core/templates/index.ts` +- [ ] 3.5 Remove workaround documentation from `amp.ts` explaining the naming mismatch + +## 4. Tests and validation + +- [ ] 4.1 Update existing unit tests to use new names +- [ ] 4.2 Verify backward compatibility by testing alias imports +- [ ] 4.3 Run full test suite to ensure no regressions +- [ ] 4.4 Run typecheck and lint diff --git a/src/core/config.ts b/src/core/config.ts index c0c3da9e2..cc683e6cf 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -18,6 +18,7 @@ export interface AIToolOption { export const AI_TOOLS: AIToolOption[] = [ { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' }, + { name: 'Amp', value: 'amp', available: true, successLabel: 'Amp' }, { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity' }, { name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie' }, { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' }, diff --git a/src/core/configurators/slash/amp.ts b/src/core/configurators/slash/amp.ts new file mode 100644 index 000000000..8be7a3c89 --- /dev/null +++ b/src/core/configurators/slash/amp.ts @@ -0,0 +1,39 @@ +import { SlashCommandConfigurator } from './base.js'; +import { SlashCommandId } from '../../templates/index.js'; +import { getSkillBody, getSkillMetadata, SkillId } from '../../templates/skill-templates.js'; + +/** + * AmpSlashCommandConfigurator generates Amp "skills" for OpenSpec workflows. + * + * Amp uses Skills (not slash commands) located in .agents/skills//SKILL.md. + * Each skill file contains YAML frontmatter with `name` and `description` fields, + * followed by the OpenSpec workflow instructions wrapped in managed markers. + * + * Despite extending SlashCommandConfigurator (a historical naming choice), + * this configurator generates Amp-native skill files that Amp discovers and + * presents to users as available skills. + */ + +const FILE_PATHS: Record = { + proposal: '.agents/skills/openspec-proposal/SKILL.md', + apply: '.agents/skills/openspec-apply/SKILL.md', + archive: '.agents/skills/openspec-archive/SKILL.md' +}; + +export class AmpSlashCommandConfigurator extends SlashCommandConfigurator { + readonly toolId = 'amp'; + readonly isAvailable = true; + + protected getRelativePath(id: SlashCommandId): string { + return FILE_PATHS[id]; + } + + protected getFrontmatter(id: SlashCommandId): string | undefined { + const metadata = getSkillMetadata(id as SkillId); + return `---\nname: ${metadata.name}\ndescription: ${metadata.description}\n---`; + } + + protected getBody(id: SlashCommandId): string { + return getSkillBody(id as SkillId).trim(); + } +} diff --git a/src/core/configurators/slash/registry.ts b/src/core/configurators/slash/registry.ts index 8020940ec..7fe2d4b39 100644 --- a/src/core/configurators/slash/registry.ts +++ b/src/core/configurators/slash/registry.ts @@ -19,6 +19,7 @@ import { QwenSlashCommandConfigurator } from './qwen.js'; import { RooCodeSlashCommandConfigurator } from './roocode.js'; import { AntigravitySlashCommandConfigurator } from './antigravity.js'; import { IflowSlashCommandConfigurator } from './iflow.js'; +import { AmpSlashCommandConfigurator } from './amp.js'; export class SlashCommandRegistry { private static configurators: Map = new Map(); @@ -44,6 +45,7 @@ export class SlashCommandRegistry { const roocode = new RooCodeSlashCommandConfigurator(); const antigravity = new AntigravitySlashCommandConfigurator(); const iflow = new IflowSlashCommandConfigurator(); + const amp = new AmpSlashCommandConfigurator(); this.configurators.set(claude.toolId, claude); this.configurators.set(codeBuddy.toolId, codeBuddy); @@ -65,6 +67,7 @@ export class SlashCommandRegistry { this.configurators.set(roocode.toolId, roocode); this.configurators.set(antigravity.toolId, antigravity); this.configurators.set(iflow.toolId, iflow); + this.configurators.set(amp.toolId, amp); } static register(configurator: SlashCommandConfigurator): void { diff --git a/src/core/templates/index.ts b/src/core/templates/index.ts index 8dab4b5f6..c9d971bf1 100644 --- a/src/core/templates/index.ts +++ b/src/core/templates/index.ts @@ -5,6 +5,7 @@ import { clineTemplate } from './cline-template.js'; import { costrictTemplate } from './costrict-template.js'; import { agentsRootStubTemplate } from './agents-root-stub.js'; import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js'; +import { getSkillBody, getSkillMetadata, SkillId, SkillMetadata } from './skill-templates.js'; export interface Template { path: string; @@ -44,7 +45,16 @@ export class TemplateManager { static getSlashCommandBody(id: SlashCommandId): string { return getSlashCommandBody(id); } + + static getSkillBody(id: SkillId): string { + return getSkillBody(id); + } + + static getSkillMetadata(id: SkillId): SkillMetadata { + return getSkillMetadata(id); + } } export { ProjectContext } from './project-template.js'; export type { SlashCommandId } from './slash-command-templates.js'; +export type { SkillId, SkillMetadata } from './skill-templates.js'; diff --git a/src/core/templates/skill-templates.ts b/src/core/templates/skill-templates.ts new file mode 100644 index 000000000..228b6f93d --- /dev/null +++ b/src/core/templates/skill-templates.ts @@ -0,0 +1,197 @@ +export type SkillId = 'proposal' | 'apply' | 'archive'; + +export interface SkillMetadata { + name: string; + description: string; +} + +const proposalBody = `# Create OpenSpec Proposal + +## Critical constraint + +**No implementation code during proposal stage.** Create only design artifacts: \`proposal.md\`, \`tasks.md\`, \`design.md\`, and spec deltas. Implementation happens in the apply stage after approval. + +## Guardrails + +- Favor minimal scope; add complexity only when requested +- Identify ambiguities and ask clarifying questions before editing files +- Refer to \`openspec/AGENTS.md\` for additional conventions + +## Execution checklist + +Copy and track progress: +\`\`\` +- [ ] Review context: read \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\` +- [ ] Inspect related code/docs to ground proposal in current behavior +- [ ] Note gaps requiring clarification; ask questions if needed +- [ ] Choose unique verb-led change-id (e.g., \`add-retry-logic\`, \`remove-legacy-auth\`) +- [ ] Scaffold \`openspec/changes//\` with proposal.md and tasks.md +- [ ] Create design.md if needed +- [ ] Draft spec deltas in \`changes//specs//spec.md\` +- [ ] Draft tasks.md as ordered, verifiable work items +- [ ] Run \`openspec validate --strict\` and resolve all issues +\`\`\` + +## When to create design.md + +Create \`design.md\` when the change: +- Spans multiple systems or components +- Introduces new architectural patterns +- Requires trade-off discussion before committing + +Skip \`design.md\` for isolated, straightforward changes. + +## Spec delta format + +Each spec delta goes in \`changes//specs//spec.md\`: +\`\`\`markdown +## ADDED Requirements + +### REQ-XXX-001: [Requirement title] +[Description] + +#### Scenario: [Happy path] +Given [context] +When [action] +Then [outcome] + +#### Scenario: [Edge case] +... + +## MODIFIED Requirements +... + +## REMOVED Requirements +... +\`\`\` + +Cross-reference related capabilities when relevant. + +## tasks.md format + +Ordered list of small, verifiable work items: +- Each task delivers user-visible progress +- Include validation steps (tests, tooling) +- Note dependencies and parallelizable work + +## Reference commands + +| Command | Purpose | +|---------|---------| +| \`openspec validate --strict\` | Validate proposal before sharing | +| \`openspec show --json --deltas-only\` | Inspect details when validation fails | +| \`openspec show --type spec\` | View existing spec | +| \`rg -n "Requirement:\\|Scenario:" openspec/specs\` | Search existing requirements | +| \`rg \` | Explore codebase for implementation context | + +## Example + +**Input:** "Propose adding rate limiting to the API" + +**Agent actions:** +1. Reviews \`openspec/project.md\` and existing specs +2. Searches codebase: \`rg "rate" openspec/specs\` +3. Creates \`openspec/changes/add-rate-limiting/\` +4. Scaffolds \`proposal.md\` with problem statement and approach +5. Creates \`design.md\` (spans API gateway and backend services) +6. Drafts spec delta with ADDED requirements and scenarios +7. Creates \`tasks.md\` with implementation steps +8. Validates: \`openspec validate add-rate-limiting --strict\``; + +const applyBody = `# Apply OpenSpec Change + +## Guardrails + +- Favor straightforward, minimal implementations; add complexity only when requested or clearly required +- Keep changes tightly scoped to the requested outcome +- Refer to \`openspec/AGENTS.md\` for additional conventions + +## Execution checklist + +Copy and track progress: +\`\`\` +- [ ] Read \`changes//proposal.md\`, \`design.md\` (if present), and \`tasks.md\` +- [ ] Confirm scope and acceptance criteria understood +- [ ] Complete each task sequentially with minimal, focused edits +- [ ] Verify all items in tasks.md are finished +- [ ] Update tasks.md: mark each task \`- [x]\` +\`\`\` + +## When blocked + +Run \`openspec show --json --deltas-only\` to review proposal context. + +## Example + +**Input:** "Apply openspec change: add-amp-support" + +**Agent actions:** +1. Reads \`changes/add-amp-support/proposal.md\` and \`tasks.md\` +2. Implements each task with minimal edits +3. Marks all tasks complete in \`tasks.md\` +4. Reports completion summary`; + +const archiveBody = `# Archive OpenSpec Change + +## Guardrails + +- Favor straightforward, minimal implementations; add complexity only when requested or clearly required +- Keep changes tightly scoped to the requested outcome +- Refer to \`openspec/AGENTS.md\` for additional conventions + +## Execution checklist + +Copy and track progress: +\`\`\` +- [ ] Identify the change ID (from input or run \`openspec list\`) +- [ ] Validate the change exists and is ready to archive +- [ ] Run \`openspec archive --yes\` +- [ ] Confirm specs were updated and change moved to \`changes/archive/\` +- [ ] Validate with \`openspec validate --strict\` +\`\`\` + +## When blocked + +Run \`openspec list\` to see available changes and their status. + +## Example + +**Input:** "Archive the add-amp-support change" + +**Agent actions:** +1. Runs \`openspec list\` to confirm change ID +2. Executes \`openspec archive add-amp-support --yes\` +3. Verifies change moved to \`changes/archive/\` +4. Runs \`openspec validate --strict\` to confirm specs are valid`; + +export const skillBodies: Record = { + proposal: proposalBody, + apply: applyBody, + archive: archiveBody +}; + +export const skillMetadata: Record = { + proposal: { + name: 'openspec-proposal', + description: + 'Creates structured OpenSpec change proposals with spec deltas, task breakdowns, and design documentation. Activates when the user wants to propose, plan, or design a new change, create a spec, start an RFC, or says "new change" or "propose: ". Does not write implementation code—only design artifacts.' + }, + apply: { + name: 'openspec-apply', + description: + 'Implements approved OpenSpec changes by executing task checklists and updating completion status. Activates when the user asks to apply, implement, or execute an OpenSpec change, work on proposal tasks, or says "apply " or "implement change ".' + }, + archive: { + name: 'openspec-archive', + description: + 'Archives deployed OpenSpec changes and updates specs. Activates when the user asks to archive a change, complete a proposal, or says "archive ".' + } +}; + +export function getSkillBody(id: SkillId): string { + return skillBodies[id]; +} + +export function getSkillMetadata(id: SkillId): SkillMetadata { + return skillMetadata[id]; +} diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 7fdd5c4fd..018288ea2 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -257,6 +257,68 @@ describe('InitCommand', () => { expect(archiveContent).not.toContain('auto_execution_mode'); }); + it('should create Amp skills when Amp is selected', async () => { + queueSelections('amp', DONE); + + await initCommand.execute(testDir); + + const ampProposal = path.join( + testDir, + '.agents/skills/openspec-proposal/SKILL.md' + ); + const ampApply = path.join( + testDir, + '.agents/skills/openspec-apply/SKILL.md' + ); + const ampArchive = path.join( + testDir, + '.agents/skills/openspec-archive/SKILL.md' + ); + + expect(await fileExists(ampProposal)).toBe(true); + expect(await fileExists(ampApply)).toBe(true); + expect(await fileExists(ampArchive)).toBe(true); + + const proposalContent = await fs.readFile(ampProposal, 'utf-8'); + expect(proposalContent).toContain('---'); + expect(proposalContent).toContain('name: openspec-proposal'); + expect(proposalContent).toContain('description: Creates structured OpenSpec change proposals'); + expect(proposalContent).toContain(''); + expect(proposalContent).toContain(''); + expect(proposalContent).toContain('# Create OpenSpec Proposal'); + expect(proposalContent).toContain('## Critical constraint'); + // Verify frontmatter appears before markers + expect(proposalContent.indexOf('name: openspec-proposal')).toBeLessThan( + proposalContent.indexOf('') + ); + + const applyContent = await fs.readFile(ampApply, 'utf-8'); + expect(applyContent).toContain('---'); + expect(applyContent).toContain('name: openspec-apply'); + expect(applyContent).toContain('description: Implements approved OpenSpec changes'); + expect(applyContent).toContain(''); + expect(applyContent).toContain(''); + expect(applyContent).toContain('# Apply OpenSpec Change'); + expect(applyContent).toContain('## Execution checklist'); + // Verify frontmatter appears before markers + expect(applyContent.indexOf('name: openspec-apply')).toBeLessThan( + applyContent.indexOf('') + ); + + const archiveContent = await fs.readFile(ampArchive, 'utf-8'); + expect(archiveContent).toContain('---'); + expect(archiveContent).toContain('name: openspec-archive'); + expect(archiveContent).toContain('description: Archives deployed OpenSpec changes'); + expect(archiveContent).toContain(''); + expect(archiveContent).toContain(''); + expect(archiveContent).toContain('# Archive OpenSpec Change'); + expect(archiveContent).toContain('openspec archive --yes'); + // Verify frontmatter appears before markers + expect(archiveContent.indexOf('name: openspec-archive')).toBeLessThan( + archiveContent.indexOf('') + ); + }); + it('should always create AGENTS.md in project root', async () => { queueSelections(DONE); diff --git a/test/core/update.test.ts b/test/core/update.test.ts index b6fe974c8..bf7f17a86 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -495,6 +495,84 @@ Old body consoleSpy.mockRestore(); }); + it('should refresh existing Amp skills', async () => { + const ampPath = path.join( + testDir, + '.agents/skills/openspec-apply/SKILL.md' + ); + await fs.mkdir(path.dirname(ampPath), { recursive: true }); + const initialContent = `--- +name: openspec-apply +description: Implement an approved OpenSpec change and keep tasks in sync. +--- + + +Old body +`; + await fs.writeFile(ampPath, initialContent); + + const consoleSpy = vi.spyOn(console, 'log'); + + await updateCommand.execute(testDir); + + const updated = await fs.readFile(ampPath, 'utf-8'); + expect(updated).toContain('# Apply OpenSpec Change'); + expect(updated).toContain('## Execution checklist'); + expect(updated).not.toContain('Old body'); + expect(updated).toContain('name: openspec-apply'); + // Frontmatter is preserved by update, so original description remains + expect(updated).toContain('description: Implement an approved OpenSpec change'); + // Verify markers remain after update + expect(updated).toContain(''); + expect(updated).toContain(''); + // Verify frontmatter appears before markers + expect(updated.indexOf('name: openspec-apply')).toBeLessThan( + updated.indexOf('') + ); + + const [logMessage] = consoleSpy.mock.calls[0]; + expect(logMessage).toContain( + 'Updated slash commands: .agents/skills/openspec-apply/SKILL.md' + ); + consoleSpy.mockRestore(); + }); + + it('should not create missing Amp skills on update', async () => { + const ampApply = path.join( + testDir, + '.agents/skills/openspec-apply/SKILL.md' + ); + + // Only create apply; leave proposal and archive missing + await fs.mkdir(path.dirname(ampApply), { recursive: true }); + await fs.writeFile( + ampApply, + `--- +name: openspec-apply +description: Implement an approved OpenSpec change and keep tasks in sync. +--- + + +Old +` + ); + + await updateCommand.execute(testDir); + + const ampProposal = path.join( + testDir, + '.agents/skills/openspec-proposal/SKILL.md' + ); + const ampArchive = path.join( + testDir, + '.agents/skills/openspec-archive/SKILL.md' + ); + + // Confirm they weren't created by update + await expect(FileSystemUtils.fileExists(ampProposal)).resolves.toBe(false); + await expect(FileSystemUtils.fileExists(ampArchive)).resolves.toBe(false); + }); + it('should refresh existing Codex prompts', async () => { const codexPath = path.join( testDir,