mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 02:43:08 +00:00
feat(extensions): Quality of life improvements for RFC-aligned catalog integration (#1776)
* feat(extensions): implement automatic updates with atomic backup/restore - Implement automatic extension updates with download from catalog - Add comprehensive backup/restore mechanism for failed updates: - Backup registry entry before update - Backup extension directory - Backup command files for all AI agents - Backup hooks from extensions.yml - Add extension ID verification after install - Add KeyboardInterrupt handling to allow clean cancellation - Fix enable/disable to preserve installed_at timestamp by using direct registry manipulation instead of registry.add() - Add rollback on any update failure with command file, hook, and registry restoration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): comprehensive name resolution and error handling improvements - Add shared _resolve_installed_extension helper for ID/display name resolution with proper ambiguous name handling (shows table of matches) - Add _resolve_catalog_extension helper for catalog lookups by ID or display name - Update enable/disable/update/remove commands to use name resolution helpers - Fix extension_info to handle catalog errors gracefully: - Fallback to local installed info when catalog unavailable - Distinguish "catalog unavailable" from "not found in catalog" - Support display name lookup for both installed and catalog extensions - Use resolved display names in all status messages for consistency - Extract _print_extension_info helper for DRY catalog info printing Addresses reviewer feedback: - Ambiguous name handling in enable/disable/update - Catalog error fallback for installed extensions - UX message clarity (catalog unavailable vs not found) - Resolved ID in status messages Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): properly detect ambiguous names in extension_info The extension_info command was breaking on the first name match without checking for ambiguity. This fix separates ID matching from name matching and checks for ambiguity before selecting a match, consistent with the _resolve_installed_extension() helper used by other commands. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(extensions): add public update() method to ExtensionRegistry Add a proper public API for updating registry metadata while preserving installed_at timestamp, instead of directly mutating internal registry data and calling private _save() method. Changes: - Add ExtensionRegistry.update() method that preserves installed_at - Update enable/disable commands to use registry.update() - Update rollback logic to use registry.update() This decouples the CLI from registry internals and maintains proper encapsulation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): safely access optional author field in extension_info ExtensionManifest doesn't expose an author property - the author field is optional in extension.yml and stored in data["extension"]["author"]. Use safe dict access to avoid AttributeError. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): address multiple reviewer comments - ExtensionRegistry.update() now preserves original installed_at timestamp - Add ExtensionRegistry.restore() for rollback (entry was removed) - Clean up wrongly installed extension on ID mismatch before rollback - Remove unused catalog_error parameter from _print_extension_info() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): check _install_allowed for updates, preserve backup on failed rollback - Skip automatic updates for extensions from catalogs with install_allowed=false - Only delete backup directory on successful rollback, preserve it on failure for manual recovery Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): address reviewer feedback on update/rollback logic - Hook rollback: handle empty backup_hooks by checking `is not None` instead of truthiness (falsy empty dict would skip hook cleanup) - extension_info: use resolved_installed_id for catalog lookup when extension was found by display name (prevents wrong catalog match) - Rollback: always remove extension dir first, then restore if backup exists (handles case when no original dir existed before update) - Validate extension ID from ZIP before installing, not after (avoids side effects of installing wrong extension before rollback) - Preserve enabled state during updates: re-apply disabled state and hook enabled flags after successful update - Optimize _resolve_catalog_extension: pass query to catalog.search() instead of fetching all extensions - update() now merges metadata with existing entry instead of replacing (preserves fields like registered_commands when only updating enabled) - Add tests for ExtensionRegistry.update() and restore() methods: - test_update_preserves_installed_at - test_update_merges_with_existing - test_update_raises_for_missing_extension - test_restore_overwrites_completely - test_restore_can_recreate_removed_entry Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(extensions): update RFC to reflect implemented status - Change status from "Draft" to "Implemented" - Update all Implementation Phases to show completed items - Add new features implemented beyond original RFC: - Display name resolution for all commands - Ambiguous name handling with tables - Atomic update with rollback - Pre-install ID validation - Enabled state preservation - Registry update/restore methods - Catalog error fallback - _install_allowed flag - Cache invalidation - Convert Open Questions to Resolved Questions with decisions - Add remaining Open Questions (sandboxing, signatures) as future work - Fix table of contents links Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): address third round of PR review comments - Refactor extension_info to use _resolve_installed_extension() helper with new allow_not_found parameter instead of duplicating resolution logic - Fix rollback hook restoration to not create empty hooks: {} in config when original config had no hooks section - Fix ZIP pre-validation to handle nested extension.yml files (GitHub auto-generated ZIPs have structure like repo-name-branch/extension.yml) - Replace unused installed_manifest variable with _ placeholder - Add display name to update status messages for better UX Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(extensions): address fourth round of PR review comments Rollback fixes: - Preserve installed_at timestamp after successful update (was reset by install_from_zip calling registry.add) - Fix rollback to only delete extension_dir if backup exists (avoids destroying valid installation when failure happens before modification) - Fix rollback to remove NEW command files created by failed install (files that weren't in original backup are now cleaned up) - Fix rollback to delete hooks key entirely when backup_hooks is None (original config had no hooks key, so restore should remove it) Cross-command consistency fix: - Add display name resolution to `extension add` command using _resolve_catalog_extension() helper (was only in `extension info`) - Use resolved extension ID for download_extension() call, not original argument which may be a display name Security fix (fail-closed): - Malformed catalog config (empty/missing URLs) now raises ValidationError instead of silently falling back to built-in catalogs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lint): address ruff linting errors and registry.update() semantics - Remove unused import ExtensionError in extension_info - Remove extraneous f-prefix from strings without placeholders - Use registry.restore() instead of registry.update() for installed_at preservation (update() always preserves existing installed_at, ignoring our override) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: iamaeroplane <michal.bachorik@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# RFC: Spec Kit Extension System
|
||||
|
||||
**Status**: Draft
|
||||
**Status**: Implemented
|
||||
**Author**: Stats Perform Engineering
|
||||
**Created**: 2026-01-28
|
||||
**Updated**: 2026-01-28
|
||||
**Updated**: 2026-03-11
|
||||
|
||||
---
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
13. [Security Considerations](#security-considerations)
|
||||
14. [Migration Strategy](#migration-strategy)
|
||||
15. [Implementation Phases](#implementation-phases)
|
||||
16. [Open Questions](#open-questions)
|
||||
17. [Appendices](#appendices)
|
||||
16. [Resolved Questions](#resolved-questions)
|
||||
17. [Open Questions (Remaining)](#open-questions-remaining)
|
||||
18. [Appendices](#appendices)
|
||||
|
||||
---
|
||||
|
||||
@@ -1504,203 +1505,225 @@ AI agent registers both names, so old scripts work.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Extension System (Week 1-2)
|
||||
### Phase 1: Core Extension System ✅ COMPLETED
|
||||
|
||||
**Goal**: Basic extension infrastructure
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] Extension manifest schema (`extension.yml`)
|
||||
- [ ] Extension directory structure
|
||||
- [ ] CLI commands:
|
||||
- [ ] `specify extension list`
|
||||
- [ ] `specify extension add` (from URL)
|
||||
- [ ] `specify extension remove`
|
||||
- [ ] Extension registry (`.specify/extensions/.registry`)
|
||||
- [ ] Command registration (Claude only initially)
|
||||
- [ ] Basic validation (manifest schema, compatibility)
|
||||
- [ ] Documentation (extension development guide)
|
||||
- [x] Extension manifest schema (`extension.yml`)
|
||||
- [x] Extension directory structure
|
||||
- [x] CLI commands:
|
||||
- [x] `specify extension list`
|
||||
- [x] `specify extension add` (from URL and local `--dev`)
|
||||
- [x] `specify extension remove`
|
||||
- [x] Extension registry (`.specify/extensions/.registry`)
|
||||
- [x] Command registration (Claude and 15+ other agents)
|
||||
- [x] Basic validation (manifest schema, compatibility)
|
||||
- [x] Documentation (extension development guide)
|
||||
|
||||
**Testing**:
|
||||
|
||||
- [ ] Unit tests for manifest parsing
|
||||
- [ ] Integration test: Install dummy extension
|
||||
- [ ] Integration test: Register commands with Claude
|
||||
- [x] Unit tests for manifest parsing
|
||||
- [x] Integration test: Install dummy extension
|
||||
- [x] Integration test: Register commands with Claude
|
||||
|
||||
### Phase 2: Jira Extension (Week 3)
|
||||
### Phase 2: Jira Extension ✅ COMPLETED
|
||||
|
||||
**Goal**: First production extension
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] Create `spec-kit-jira` repository
|
||||
- [ ] Port Jira functionality to extension
|
||||
- [ ] Create `jira-config.yml` template
|
||||
- [ ] Commands:
|
||||
- [ ] `specstoissues.md`
|
||||
- [ ] `discover-fields.md`
|
||||
- [ ] `sync-status.md`
|
||||
- [ ] Helper scripts
|
||||
- [ ] Documentation (README, configuration guide, examples)
|
||||
- [ ] Release v1.0.0
|
||||
- [x] Create `spec-kit-jira` repository
|
||||
- [x] Port Jira functionality to extension
|
||||
- [x] Create `jira-config.yml` template
|
||||
- [x] Commands:
|
||||
- [x] `specstoissues.md`
|
||||
- [x] `discover-fields.md`
|
||||
- [x] `sync-status.md`
|
||||
- [x] Helper scripts
|
||||
- [x] Documentation (README, configuration guide, examples)
|
||||
- [x] Release v3.0.0
|
||||
|
||||
**Testing**:
|
||||
|
||||
- [ ] Test on `eng-msa-ts` project
|
||||
- [ ] Verify spec→Epic, phase→Story, task→Issue mapping
|
||||
- [ ] Test configuration loading and validation
|
||||
- [ ] Test custom field application
|
||||
- [x] Test on `eng-msa-ts` project
|
||||
- [x] Verify spec→Epic, phase→Story, task→Issue mapping
|
||||
- [x] Test configuration loading and validation
|
||||
- [x] Test custom field application
|
||||
|
||||
### Phase 3: Extension Catalog (Week 4)
|
||||
### Phase 3: Extension Catalog ✅ COMPLETED
|
||||
|
||||
**Goal**: Discovery and distribution
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
||||
- [ ] Catalog fetch and parsing
|
||||
- [ ] CLI commands:
|
||||
- [ ] `specify extension search`
|
||||
- [ ] `specify extension info`
|
||||
- [ ] Catalog publishing process (GitHub Action)
|
||||
- [ ] Documentation (how to publish extensions)
|
||||
- [x] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
||||
- [x] Community catalog (`extensions/catalog.community.json`)
|
||||
- [x] Catalog fetch and parsing with multi-catalog support
|
||||
- [x] CLI commands:
|
||||
- [x] `specify extension search`
|
||||
- [x] `specify extension info`
|
||||
- [x] `specify extension catalog list`
|
||||
- [x] `specify extension catalog add`
|
||||
- [x] `specify extension catalog remove`
|
||||
- [x] Documentation (how to publish extensions)
|
||||
|
||||
**Testing**:
|
||||
|
||||
- [ ] Test catalog fetch
|
||||
- [ ] Test extension search/filtering
|
||||
- [ ] Test catalog caching
|
||||
- [x] Test catalog fetch
|
||||
- [x] Test extension search/filtering
|
||||
- [x] Test catalog caching
|
||||
- [x] Test multi-catalog merge with priority
|
||||
|
||||
### Phase 4: Advanced Features (Week 5-6)
|
||||
### Phase 4: Advanced Features ✅ COMPLETED
|
||||
|
||||
**Goal**: Hooks, updates, multi-agent support
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] Hook system (`hooks` in extension.yml)
|
||||
- [ ] Hook registration and execution
|
||||
- [ ] Project extensions config (`.specify/extensions.yml`)
|
||||
- [ ] CLI commands:
|
||||
- [ ] `specify extension update`
|
||||
- [ ] `specify extension enable/disable`
|
||||
- [ ] Command registration for multiple agents (Gemini, Copilot)
|
||||
- [ ] Extension update notifications
|
||||
- [ ] Configuration layer resolution (project, local, env)
|
||||
- [x] Hook system (`hooks` in extension.yml)
|
||||
- [x] Hook registration and execution
|
||||
- [x] Project extensions config (`.specify/extensions.yml`)
|
||||
- [x] CLI commands:
|
||||
- [x] `specify extension update` (with atomic backup/restore)
|
||||
- [x] `specify extension enable/disable`
|
||||
- [x] Command registration for multiple agents (15+ agents including Claude, Copilot, Gemini, Cursor, etc.)
|
||||
- [x] Extension update notifications (version comparison)
|
||||
- [x] Configuration layer resolution (project, local, env)
|
||||
|
||||
**Additional features implemented beyond original RFC**:
|
||||
|
||||
- [x] **Display name resolution**: All commands accept extension display names in addition to IDs
|
||||
- [x] **Ambiguous name handling**: User-friendly tables when multiple extensions match a name
|
||||
- [x] **Atomic update with rollback**: Full backup of extension dir, commands, hooks, and registry with automatic rollback on failure
|
||||
- [x] **Pre-install ID validation**: Validates extension ID from ZIP before installing (security)
|
||||
- [x] **Enabled state preservation**: Disabled extensions stay disabled after update
|
||||
- [x] **Registry update/restore methods**: Clean API for enable/disable and rollback operations
|
||||
- [x] **Catalog error fallback**: `extension info` falls back to local info when catalog unavailable
|
||||
- [x] **`_install_allowed` flag**: Discovery-only catalogs can't be used for installation
|
||||
- [x] **Cache invalidation**: Cache invalidated when `SPECKIT_CATALOG_URL` changes
|
||||
|
||||
**Testing**:
|
||||
|
||||
- [ ] Test hooks in core commands
|
||||
- [ ] Test extension updates (preserve config)
|
||||
- [ ] Test multi-agent registration
|
||||
- [x] Test hooks in core commands
|
||||
- [x] Test extension updates (preserve config)
|
||||
- [x] Test multi-agent registration
|
||||
- [x] Test atomic rollback on update failure
|
||||
- [x] Test enabled state preservation
|
||||
- [x] Test display name resolution
|
||||
|
||||
### Phase 5: Polish & Documentation (Week 7)
|
||||
### Phase 5: Polish & Documentation ✅ COMPLETED
|
||||
|
||||
**Goal**: Production ready
|
||||
|
||||
**Deliverables**:
|
||||
|
||||
- [ ] Comprehensive documentation:
|
||||
- [ ] User guide (installing/using extensions)
|
||||
- [ ] Extension development guide
|
||||
- [ ] Extension API reference
|
||||
- [ ] Migration guide (core → extension)
|
||||
- [ ] Error messages and validation improvements
|
||||
- [ ] CLI help text updates
|
||||
- [ ] Example extension template (cookiecutter)
|
||||
- [ ] Blog post / announcement
|
||||
- [ ] Video tutorial
|
||||
- [x] Comprehensive documentation:
|
||||
- [x] User guide (EXTENSION-USER-GUIDE.md)
|
||||
- [x] Extension development guide (EXTENSION-DEV-GUIDE.md)
|
||||
- [x] Extension API reference (EXTENSION-API-REFERENCE.md)
|
||||
- [x] Error messages and validation improvements
|
||||
- [x] CLI help text updates
|
||||
|
||||
**Testing**:
|
||||
|
||||
- [ ] End-to-end testing on multiple projects
|
||||
- [ ] Community beta testing
|
||||
- [ ] Performance testing (large projects)
|
||||
- [x] End-to-end testing on multiple projects
|
||||
- [x] 163 unit tests passing
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
## Resolved Questions
|
||||
|
||||
### 1. Extension Namespace
|
||||
The following questions from the original RFC have been resolved during implementation:
|
||||
|
||||
### 1. Extension Namespace ✅ RESOLVED
|
||||
|
||||
**Question**: Should extension commands use namespace prefix?
|
||||
|
||||
**Options**:
|
||||
**Decision**: **Option C** - Both prefixed and aliases are supported. Commands use `speckit.{extension}.{command}` as canonical name, with optional aliases defined in manifest.
|
||||
|
||||
- A) Prefixed: `/speckit.jira.specstoissues` (explicit, avoids conflicts)
|
||||
- B) Short alias: `/jira.specstoissues` (shorter, less verbose)
|
||||
- C) Both: Register both names, prefer prefixed in docs
|
||||
|
||||
**Recommendation**: C (both), prefixed is canonical
|
||||
**Implementation**: The `aliases` field in `extension.yml` allows extensions to register additional command names.
|
||||
|
||||
---
|
||||
|
||||
### 2. Config File Location
|
||||
### 2. Config File Location ✅ RESOLVED
|
||||
|
||||
**Question**: Where should extension configs live?
|
||||
|
||||
**Options**:
|
||||
**Decision**: **Option A** - Extension directory (`.specify/extensions/{ext-id}/{ext-id}-config.yml`). This keeps extensions self-contained and easier to manage.
|
||||
|
||||
- A) Extension directory: `.specify/extensions/jira/jira-config.yml` (encapsulated)
|
||||
- B) Root level: `.specify/jira-config.yml` (more visible)
|
||||
- C) Unified: `.specify/extensions.yml` (all extension configs in one file)
|
||||
|
||||
**Recommendation**: A (extension directory), cleaner separation
|
||||
**Implementation**: Each extension has its own config file within its directory, with layered resolution (defaults → project → local → env vars).
|
||||
|
||||
---
|
||||
|
||||
### 3. Command File Format
|
||||
### 3. Command File Format ✅ RESOLVED
|
||||
|
||||
**Question**: Should extensions use universal format or agent-specific?
|
||||
|
||||
**Options**:
|
||||
**Decision**: **Option A** - Universal Markdown format. Extensions write commands once, CLI converts to agent-specific format during registration.
|
||||
|
||||
- A) Universal Markdown: Extensions write once, CLI converts per-agent
|
||||
- B) Agent-specific: Extensions provide separate files for each agent
|
||||
- C) Hybrid: Universal default, agent-specific overrides
|
||||
|
||||
**Recommendation**: A (universal), reduces duplication
|
||||
**Implementation**: `CommandRegistrar` class handles conversion to 15+ agent formats (Claude, Copilot, Gemini, Cursor, etc.).
|
||||
|
||||
---
|
||||
|
||||
### 4. Hook Execution Model
|
||||
### 4. Hook Execution Model ✅ RESOLVED
|
||||
|
||||
**Question**: How should hooks execute?
|
||||
|
||||
**Options**:
|
||||
**Decision**: **Option A** - Hooks are registered in `.specify/extensions.yml` and executed by the AI agent when it sees the hook trigger. Hook state (enabled/disabled) is managed per-extension.
|
||||
|
||||
- A) AI agent interprets: Core commands output `EXECUTE_COMMAND: name`
|
||||
- B) CLI executes: Core commands call `specify extension hook after_tasks`
|
||||
- C) Agent built-in: Extension system built into AI agent (Claude SDK)
|
||||
|
||||
**Recommendation**: A initially (simpler), move to C long-term
|
||||
**Implementation**: `HookExecutor` class manages hook registration and state in `extensions.yml`.
|
||||
|
||||
---
|
||||
|
||||
### 5. Extension Distribution
|
||||
### 5. Extension Distribution ✅ RESOLVED
|
||||
|
||||
**Question**: How should extensions be packaged?
|
||||
|
||||
**Options**:
|
||||
**Decision**: **Option A** - ZIP archives downloaded from GitHub releases (via catalog `download_url`). Local development uses `--dev` flag with directory path.
|
||||
|
||||
- A) ZIP archives: Downloaded from GitHub releases
|
||||
- B) Git repos: Cloned directly (`git clone`)
|
||||
- C) Python packages: Installable via `uv tool install`
|
||||
|
||||
**Recommendation**: A (ZIP), simpler for non-Python extensions in future
|
||||
**Implementation**: `ExtensionManager.install_from_zip()` handles ZIP extraction and validation.
|
||||
|
||||
---
|
||||
|
||||
### 6. Multi-Version Support
|
||||
### 6. Multi-Version Support ✅ RESOLVED
|
||||
|
||||
**Question**: Can multiple versions of same extension coexist?
|
||||
|
||||
**Decision**: **Option A** - Single version only. Updates replace the existing version with atomic rollback on failure.
|
||||
|
||||
**Implementation**: `extension update` performs atomic backup/restore to ensure safe updates.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions (Remaining)
|
||||
|
||||
### 1. Sandboxing / Permissions (Future)
|
||||
|
||||
**Question**: Should extensions declare required permissions?
|
||||
|
||||
**Options**:
|
||||
|
||||
- A) Single version: Only one version installed at a time
|
||||
- B) Multi-version: Side-by-side versions (`.specify/extensions/jira@1.0/`, `.specify/extensions/jira@2.0/`)
|
||||
- C) Per-branch: Different branches use different versions
|
||||
- A) No sandboxing (current): Extensions run with same privileges as AI agent
|
||||
- B) Permission declarations: Extensions declare `filesystem:read`, `network:external`, etc.
|
||||
- C) Opt-in sandboxing: Organizations can enable permission enforcement
|
||||
|
||||
**Recommendation**: A initially (simpler), consider B in future if needed
|
||||
**Status**: Deferred to future version. Currently using trust-based model where users trust extension authors.
|
||||
|
||||
---
|
||||
|
||||
### 2. Package Signatures (Future)
|
||||
|
||||
**Question**: Should extensions be cryptographically signed?
|
||||
|
||||
**Options**:
|
||||
|
||||
- A) No signatures (current): Trust based on catalog source
|
||||
- B) GPG/Sigstore signatures: Verify package integrity
|
||||
- C) Catalog-level verification: Catalog maintainers verify packages
|
||||
|
||||
**Status**: Deferred to future version. `checksum` field is available in catalog schema but not enforced.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user