Skip to content

Commit 58ce653

Browse files
mbachorikiamaeroplaneclaude
authored
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>
1 parent 82f8a13 commit 58ce653

File tree

4 files changed

+1231
-281
lines changed

4 files changed

+1231
-281
lines changed

extensions/RFC-EXTENSION-SYSTEM.md

Lines changed: 134 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# RFC: Spec Kit Extension System
22

3-
**Status**: Draft
3+
**Status**: Implemented
44
**Author**: Stats Perform Engineering
55
**Created**: 2026-01-28
6-
**Updated**: 2026-01-28
6+
**Updated**: 2026-03-11
77

88
---
99

@@ -24,8 +24,9 @@
2424
13. [Security Considerations](#security-considerations)
2525
14. [Migration Strategy](#migration-strategy)
2626
15. [Implementation Phases](#implementation-phases)
27-
16. [Open Questions](#open-questions)
28-
17. [Appendices](#appendices)
27+
16. [Resolved Questions](#resolved-questions)
28+
17. [Open Questions (Remaining)](#open-questions-remaining)
29+
18. [Appendices](#appendices)
2930

3031
---
3132

@@ -1504,203 +1505,225 @@ AI agent registers both names, so old scripts work.
15041505

15051506
## Implementation Phases
15061507

1507-
### Phase 1: Core Extension System (Week 1-2)
1508+
### Phase 1: Core Extension System ✅ COMPLETED
15081509

15091510
**Goal**: Basic extension infrastructure
15101511

15111512
**Deliverables**:
15121513

1513-
- [ ] Extension manifest schema (`extension.yml`)
1514-
- [ ] Extension directory structure
1515-
- [ ] CLI commands:
1516-
- [ ] `specify extension list`
1517-
- [ ] `specify extension add` (from URL)
1518-
- [ ] `specify extension remove`
1519-
- [ ] Extension registry (`.specify/extensions/.registry`)
1520-
- [ ] Command registration (Claude only initially)
1521-
- [ ] Basic validation (manifest schema, compatibility)
1522-
- [ ] Documentation (extension development guide)
1514+
- [x] Extension manifest schema (`extension.yml`)
1515+
- [x] Extension directory structure
1516+
- [x] CLI commands:
1517+
- [x] `specify extension list`
1518+
- [x] `specify extension add` (from URL and local `--dev`)
1519+
- [x] `specify extension remove`
1520+
- [x] Extension registry (`.specify/extensions/.registry`)
1521+
- [x] Command registration (Claude and 15+ other agents)
1522+
- [x] Basic validation (manifest schema, compatibility)
1523+
- [x] Documentation (extension development guide)
15231524

15241525
**Testing**:
15251526

1526-
- [ ] Unit tests for manifest parsing
1527-
- [ ] Integration test: Install dummy extension
1528-
- [ ] Integration test: Register commands with Claude
1527+
- [x] Unit tests for manifest parsing
1528+
- [x] Integration test: Install dummy extension
1529+
- [x] Integration test: Register commands with Claude
15291530

1530-
### Phase 2: Jira Extension (Week 3)
1531+
### Phase 2: Jira Extension ✅ COMPLETED
15311532

15321533
**Goal**: First production extension
15331534

15341535
**Deliverables**:
15351536

1536-
- [ ] Create `spec-kit-jira` repository
1537-
- [ ] Port Jira functionality to extension
1538-
- [ ] Create `jira-config.yml` template
1539-
- [ ] Commands:
1540-
- [ ] `specstoissues.md`
1541-
- [ ] `discover-fields.md`
1542-
- [ ] `sync-status.md`
1543-
- [ ] Helper scripts
1544-
- [ ] Documentation (README, configuration guide, examples)
1545-
- [ ] Release v1.0.0
1537+
- [x] Create `spec-kit-jira` repository
1538+
- [x] Port Jira functionality to extension
1539+
- [x] Create `jira-config.yml` template
1540+
- [x] Commands:
1541+
- [x] `specstoissues.md`
1542+
- [x] `discover-fields.md`
1543+
- [x] `sync-status.md`
1544+
- [x] Helper scripts
1545+
- [x] Documentation (README, configuration guide, examples)
1546+
- [x] Release v3.0.0
15461547

15471548
**Testing**:
15481549

1549-
- [ ] Test on `eng-msa-ts` project
1550-
- [ ] Verify spec→Epic, phase→Story, task→Issue mapping
1551-
- [ ] Test configuration loading and validation
1552-
- [ ] Test custom field application
1550+
- [x] Test on `eng-msa-ts` project
1551+
- [x] Verify spec→Epic, phase→Story, task→Issue mapping
1552+
- [x] Test configuration loading and validation
1553+
- [x] Test custom field application
15531554

1554-
### Phase 3: Extension Catalog (Week 4)
1555+
### Phase 3: Extension Catalog ✅ COMPLETED
15551556

15561557
**Goal**: Discovery and distribution
15571558

15581559
**Deliverables**:
15591560

1560-
- [ ] Central catalog (`extensions/catalog.json` in spec-kit repo)
1561-
- [ ] Catalog fetch and parsing
1562-
- [ ] CLI commands:
1563-
- [ ] `specify extension search`
1564-
- [ ] `specify extension info`
1565-
- [ ] Catalog publishing process (GitHub Action)
1566-
- [ ] Documentation (how to publish extensions)
1561+
- [x] Central catalog (`extensions/catalog.json` in spec-kit repo)
1562+
- [x] Community catalog (`extensions/catalog.community.json`)
1563+
- [x] Catalog fetch and parsing with multi-catalog support
1564+
- [x] CLI commands:
1565+
- [x] `specify extension search`
1566+
- [x] `specify extension info`
1567+
- [x] `specify extension catalog list`
1568+
- [x] `specify extension catalog add`
1569+
- [x] `specify extension catalog remove`
1570+
- [x] Documentation (how to publish extensions)
15671571

15681572
**Testing**:
15691573

1570-
- [ ] Test catalog fetch
1571-
- [ ] Test extension search/filtering
1572-
- [ ] Test catalog caching
1574+
- [x] Test catalog fetch
1575+
- [x] Test extension search/filtering
1576+
- [x] Test catalog caching
1577+
- [x] Test multi-catalog merge with priority
15731578

1574-
### Phase 4: Advanced Features (Week 5-6)
1579+
### Phase 4: Advanced Features ✅ COMPLETED
15751580

15761581
**Goal**: Hooks, updates, multi-agent support
15771582

15781583
**Deliverables**:
15791584

1580-
- [ ] Hook system (`hooks` in extension.yml)
1581-
- [ ] Hook registration and execution
1582-
- [ ] Project extensions config (`.specify/extensions.yml`)
1583-
- [ ] CLI commands:
1584-
- [ ] `specify extension update`
1585-
- [ ] `specify extension enable/disable`
1586-
- [ ] Command registration for multiple agents (Gemini, Copilot)
1587-
- [ ] Extension update notifications
1588-
- [ ] Configuration layer resolution (project, local, env)
1585+
- [x] Hook system (`hooks` in extension.yml)
1586+
- [x] Hook registration and execution
1587+
- [x] Project extensions config (`.specify/extensions.yml`)
1588+
- [x] CLI commands:
1589+
- [x] `specify extension update` (with atomic backup/restore)
1590+
- [x] `specify extension enable/disable`
1591+
- [x] Command registration for multiple agents (15+ agents including Claude, Copilot, Gemini, Cursor, etc.)
1592+
- [x] Extension update notifications (version comparison)
1593+
- [x] Configuration layer resolution (project, local, env)
1594+
1595+
**Additional features implemented beyond original RFC**:
1596+
1597+
- [x] **Display name resolution**: All commands accept extension display names in addition to IDs
1598+
- [x] **Ambiguous name handling**: User-friendly tables when multiple extensions match a name
1599+
- [x] **Atomic update with rollback**: Full backup of extension dir, commands, hooks, and registry with automatic rollback on failure
1600+
- [x] **Pre-install ID validation**: Validates extension ID from ZIP before installing (security)
1601+
- [x] **Enabled state preservation**: Disabled extensions stay disabled after update
1602+
- [x] **Registry update/restore methods**: Clean API for enable/disable and rollback operations
1603+
- [x] **Catalog error fallback**: `extension info` falls back to local info when catalog unavailable
1604+
- [x] **`_install_allowed` flag**: Discovery-only catalogs can't be used for installation
1605+
- [x] **Cache invalidation**: Cache invalidated when `SPECKIT_CATALOG_URL` changes
15891606

15901607
**Testing**:
15911608

1592-
- [ ] Test hooks in core commands
1593-
- [ ] Test extension updates (preserve config)
1594-
- [ ] Test multi-agent registration
1609+
- [x] Test hooks in core commands
1610+
- [x] Test extension updates (preserve config)
1611+
- [x] Test multi-agent registration
1612+
- [x] Test atomic rollback on update failure
1613+
- [x] Test enabled state preservation
1614+
- [x] Test display name resolution
15951615

1596-
### Phase 5: Polish & Documentation (Week 7)
1616+
### Phase 5: Polish & Documentation ✅ COMPLETED
15971617

15981618
**Goal**: Production ready
15991619

16001620
**Deliverables**:
16011621

1602-
- [ ] Comprehensive documentation:
1603-
- [ ] User guide (installing/using extensions)
1604-
- [ ] Extension development guide
1605-
- [ ] Extension API reference
1606-
- [ ] Migration guide (core → extension)
1607-
- [ ] Error messages and validation improvements
1608-
- [ ] CLI help text updates
1609-
- [ ] Example extension template (cookiecutter)
1610-
- [ ] Blog post / announcement
1611-
- [ ] Video tutorial
1622+
- [x] Comprehensive documentation:
1623+
- [x] User guide (EXTENSION-USER-GUIDE.md)
1624+
- [x] Extension development guide (EXTENSION-DEV-GUIDE.md)
1625+
- [x] Extension API reference (EXTENSION-API-REFERENCE.md)
1626+
- [x] Error messages and validation improvements
1627+
- [x] CLI help text updates
16121628

16131629
**Testing**:
16141630

1615-
- [ ] End-to-end testing on multiple projects
1616-
- [ ] Community beta testing
1617-
- [ ] Performance testing (large projects)
1631+
- [x] End-to-end testing on multiple projects
1632+
- [x] 163 unit tests passing
16181633

16191634
---
16201635

1621-
## Open Questions
1636+
## Resolved Questions
16221637

1623-
### 1. Extension Namespace
1638+
The following questions from the original RFC have been resolved during implementation:
16241639

1625-
**Question**: Should extension commands use namespace prefix?
1640+
### 1. Extension Namespace ✅ RESOLVED
16261641

1627-
**Options**:
1642+
**Question**: Should extension commands use namespace prefix?
16281643

1629-
- A) Prefixed: `/speckit.jira.specstoissues` (explicit, avoids conflicts)
1630-
- B) Short alias: `/jira.specstoissues` (shorter, less verbose)
1631-
- C) Both: Register both names, prefer prefixed in docs
1644+
**Decision**: **Option C** - Both prefixed and aliases are supported. Commands use `speckit.{extension}.{command}` as canonical name, with optional aliases defined in manifest.
16321645

1633-
**Recommendation**: C (both), prefixed is canonical
1646+
**Implementation**: The `aliases` field in `extension.yml` allows extensions to register additional command names.
16341647

16351648
---
16361649

1637-
### 2. Config File Location
1650+
### 2. Config File Location ✅ RESOLVED
16381651

16391652
**Question**: Where should extension configs live?
16401653

1641-
**Options**:
1642-
1643-
- A) Extension directory: `.specify/extensions/jira/jira-config.yml` (encapsulated)
1644-
- B) Root level: `.specify/jira-config.yml` (more visible)
1645-
- C) Unified: `.specify/extensions.yml` (all extension configs in one file)
1654+
**Decision**: **Option A** - Extension directory (`.specify/extensions/{ext-id}/{ext-id}-config.yml`). This keeps extensions self-contained and easier to manage.
16461655

1647-
**Recommendation**: A (extension directory), cleaner separation
1656+
**Implementation**: Each extension has its own config file within its directory, with layered resolution (defaults → project → local → env vars).
16481657

16491658
---
16501659

1651-
### 3. Command File Format
1660+
### 3. Command File Format ✅ RESOLVED
16521661

16531662
**Question**: Should extensions use universal format or agent-specific?
16541663

1655-
**Options**:
1656-
1657-
- A) Universal Markdown: Extensions write once, CLI converts per-agent
1658-
- B) Agent-specific: Extensions provide separate files for each agent
1659-
- C) Hybrid: Universal default, agent-specific overrides
1664+
**Decision**: **Option A** - Universal Markdown format. Extensions write commands once, CLI converts to agent-specific format during registration.
16601665

1661-
**Recommendation**: A (universal), reduces duplication
1666+
**Implementation**: `CommandRegistrar` class handles conversion to 15+ agent formats (Claude, Copilot, Gemini, Cursor, etc.).
16621667

16631668
---
16641669

1665-
### 4. Hook Execution Model
1670+
### 4. Hook Execution Model ✅ RESOLVED
16661671

16671672
**Question**: How should hooks execute?
16681673

1669-
**Options**:
1670-
1671-
- A) AI agent interprets: Core commands output `EXECUTE_COMMAND: name`
1672-
- B) CLI executes: Core commands call `specify extension hook after_tasks`
1673-
- C) Agent built-in: Extension system built into AI agent (Claude SDK)
1674+
**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.
16741675

1675-
**Recommendation**: A initially (simpler), move to C long-term
1676+
**Implementation**: `HookExecutor` class manages hook registration and state in `extensions.yml`.
16761677

16771678
---
16781679

1679-
### 5. Extension Distribution
1680+
### 5. Extension Distribution ✅ RESOLVED
16801681

16811682
**Question**: How should extensions be packaged?
16821683

1684+
**Decision**: **Option A** - ZIP archives downloaded from GitHub releases (via catalog `download_url`). Local development uses `--dev` flag with directory path.
1685+
1686+
**Implementation**: `ExtensionManager.install_from_zip()` handles ZIP extraction and validation.
1687+
1688+
---
1689+
1690+
### 6. Multi-Version Support ✅ RESOLVED
1691+
1692+
**Question**: Can multiple versions of same extension coexist?
1693+
1694+
**Decision**: **Option A** - Single version only. Updates replace the existing version with atomic rollback on failure.
1695+
1696+
**Implementation**: `extension update` performs atomic backup/restore to ensure safe updates.
1697+
1698+
---
1699+
1700+
## Open Questions (Remaining)
1701+
1702+
### 1. Sandboxing / Permissions (Future)
1703+
1704+
**Question**: Should extensions declare required permissions?
1705+
16831706
**Options**:
16841707

1685-
- A) ZIP archives: Downloaded from GitHub releases
1686-
- B) Git repos: Cloned directly (`git clone`)
1687-
- C) Python packages: Installable via `uv tool install`
1708+
- A) No sandboxing (current): Extensions run with same privileges as AI agent
1709+
- B) Permission declarations: Extensions declare `filesystem:read`, `network:external`, etc.
1710+
- C) Opt-in sandboxing: Organizations can enable permission enforcement
16881711

1689-
**Recommendation**: A (ZIP), simpler for non-Python extensions in future
1712+
**Status**: Deferred to future version. Currently using trust-based model where users trust extension authors.
16901713

16911714
---
16921715

1693-
### 6. Multi-Version Support
1716+
### 2. Package Signatures (Future)
16941717

1695-
**Question**: Can multiple versions of same extension coexist?
1718+
**Question**: Should extensions be cryptographically signed?
16961719

16971720
**Options**:
16981721

1699-
- A) Single version: Only one version installed at a time
1700-
- B) Multi-version: Side-by-side versions (`.specify/extensions/jira@1.0/`, `.specify/extensions/jira@2.0/`)
1701-
- C) Per-branch: Different branches use different versions
1722+
- A) No signatures (current): Trust based on catalog source
1723+
- B) GPG/Sigstore signatures: Verify package integrity
1724+
- C) Catalog-level verification: Catalog maintainers verify packages
17021725

1703-
**Recommendation**: A initially (simpler), consider B in future if needed
1726+
**Status**: Deferred to future version. `checksum` field is available in catalog schema but not enforced.
17041727

17051728
---
17061729

0 commit comments

Comments
 (0)