Skip to content

Commit d65931b

Browse files
authored
fix: add missing agents to AGENT_CONFIGS (#76)
* fix: include extension commands in release packages for all agents Previously, only base spec commands (spec.*) were generated in agent directories. This commit adds generation of extension commands (product.*, levelup.*, architect.*, tdd.*, quick.*) with both prefixed and adlc.* versions for all supported agents. This fixes the issue where agy (and other agents) were missing extension commands in their release packages. * Revert "fix: include extension commands in release packages for all agents" This reverts commit d44591e. * fix: add missing agents to AGENT_CONFIGS for extension command registration This adds agy, vibe, generic, q, codex and renames cursor to cursor-agent to match the main AGENT_CONFIG in __init__.py. These agents were missing from CommandRegistrar.AGENT_CONFIGS, which caused extension commands to not be registered during init. * fix: update test to expect q in AGENT_CONFIGS
1 parent 4e57250 commit d65931b

2 files changed

Lines changed: 98 additions & 38 deletions

File tree

src/specify_cli/extensions.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,18 @@ class CommandRegistrar:
626626
"args": "$ARGUMENTS",
627627
"extension": ".agent.md",
628628
},
629-
"cursor": {
629+
"cursor-agent": {
630630
"dir": ".cursor/commands",
631631
"format": "markdown",
632632
"args": "$ARGUMENTS",
633633
"extension": ".md",
634634
},
635+
"codex": {
636+
"dir": ".codex/prompts",
637+
"format": "markdown",
638+
"args": "$ARGUMENTS",
639+
"extension": ".md",
640+
},
635641
"qwen": {
636642
"dir": ".qwen/commands",
637643
"format": "toml",
@@ -710,6 +716,30 @@ class CommandRegistrar:
710716
"args": "$ARGUMENTS",
711717
"extension": ".md",
712718
},
719+
"agy": {
720+
"dir": ".agent/workflows",
721+
"format": "markdown",
722+
"args": "$ARGUMENTS",
723+
"extension": ".md",
724+
},
725+
"vibe": {
726+
"dir": ".vibe/prompts",
727+
"format": "markdown",
728+
"args": "$ARGUMENTS",
729+
"extension": ".md",
730+
},
731+
"generic": {
732+
"dir": ".speckit/commands",
733+
"format": "markdown",
734+
"args": "$ARGUMENTS",
735+
"extension": ".md",
736+
},
737+
"q": {
738+
"dir": ".amazonq/prompts",
739+
"format": "markdown",
740+
"args": "$ARGUMENTS",
741+
"extension": ".md",
742+
},
713743
}
714744

715745
@staticmethod

tests/test_extensions.py

Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
# ===== Fixtures =====
3535

36+
3637
@pytest.fixture
3738
def temp_dir():
3839
"""Create a temporary directory for tests."""
@@ -87,8 +88,9 @@ def extension_dir(temp_dir, valid_manifest_data):
8788

8889
# Write manifest
8990
import yaml
91+
9092
manifest_path = ext_dir / "extension.yml"
91-
with open(manifest_path, 'w') as f:
93+
with open(manifest_path, "w") as f:
9294
yaml.dump(valid_manifest_data, f)
9395

9496
# Create commands directory
@@ -124,6 +126,7 @@ def project_dir(temp_dir):
124126

125127
# ===== ExtensionManifest Tests =====
126128

129+
127130
class TestExtensionManifest:
128131
"""Test ExtensionManifest validation and parsing."""
129132

@@ -144,7 +147,7 @@ def test_missing_required_field(self, temp_dir):
144147
import yaml
145148

146149
manifest_path = temp_dir / "extension.yml"
147-
with open(manifest_path, 'w') as f:
150+
with open(manifest_path, "w") as f:
148151
yaml.dump({"schema_version": "1.0"}, f) # Missing 'extension'
149152

150153
with pytest.raises(ValidationError, match="Missing required field"):
@@ -157,7 +160,7 @@ def test_invalid_extension_id(self, temp_dir, valid_manifest_data):
157160
valid_manifest_data["extension"]["id"] = "Invalid_ID" # Uppercase not allowed
158161

159162
manifest_path = temp_dir / "extension.yml"
160-
with open(manifest_path, 'w') as f:
163+
with open(manifest_path, "w") as f:
161164
yaml.dump(valid_manifest_data, f)
162165

163166
with pytest.raises(ValidationError, match="Invalid extension ID"):
@@ -170,7 +173,7 @@ def test_invalid_version(self, temp_dir, valid_manifest_data):
170173
valid_manifest_data["extension"]["version"] = "invalid"
171174

172175
manifest_path = temp_dir / "extension.yml"
173-
with open(manifest_path, 'w') as f:
176+
with open(manifest_path, "w") as f:
174177
yaml.dump(valid_manifest_data, f)
175178

176179
with pytest.raises(ValidationError, match="Invalid version"):
@@ -183,7 +186,7 @@ def test_invalid_command_name(self, temp_dir, valid_manifest_data):
183186
valid_manifest_data["provides"]["commands"][0]["name"] = "invalid-name"
184187

185188
manifest_path = temp_dir / "extension.yml"
186-
with open(manifest_path, 'w') as f:
189+
with open(manifest_path, "w") as f:
187190
yaml.dump(valid_manifest_data, f)
188191

189192
with pytest.raises(ValidationError, match="Invalid command name"):
@@ -196,7 +199,7 @@ def test_no_commands(self, temp_dir, valid_manifest_data):
196199
valid_manifest_data["provides"]["commands"] = []
197200

198201
manifest_path = temp_dir / "extension.yml"
199-
with open(manifest_path, 'w') as f:
202+
with open(manifest_path, "w") as f:
200203
yaml.dump(valid_manifest_data, f)
201204

202205
with pytest.raises(ValidationError, match="must provide at least one command"):
@@ -214,6 +217,7 @@ def test_manifest_hash(self, extension_dir):
214217

215218
# ===== ExtensionRegistry Tests =====
216219

220+
217221
class TestExtensionRegistry:
218222
"""Test ExtensionRegistry operations."""
219223

@@ -281,6 +285,7 @@ def test_registry_persistence(self, temp_dir):
281285

282286
# ===== ExtensionManager Tests =====
283287

288+
284289
class TestExtensionManager:
285290
"""Test ExtensionManager installation and removal."""
286291

@@ -309,7 +314,7 @@ def test_install_from_directory(self, extension_dir, project_dir):
309314
manifest = manager.install_from_directory(
310315
extension_dir,
311316
"0.1.0",
312-
register_commands=False # Skip command registration for now
317+
register_commands=False, # Skip command registration for now
313318
)
314319

315320
assert manifest.id == "test-ext"
@@ -330,7 +335,9 @@ def test_install_duplicate(self, extension_dir, project_dir):
330335

331336
# Try to install again
332337
with pytest.raises(ExtensionError, match="already installed"):
333-
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
338+
manager.install_from_directory(
339+
extension_dir, "0.1.0", register_commands=False
340+
)
334341

335342
def test_remove_extension(self, extension_dir, project_dir):
336343
"""Test removing an installed extension."""
@@ -399,14 +406,15 @@ def test_config_backup_on_remove(self, extension_dir, project_dir):
399406

400407
# ===== CommandRegistrar Tests =====
401408

409+
402410
class TestCommandRegistrar:
403411
"""Test CommandRegistrar command registration."""
404412

405413
def test_kiro_cli_agent_config_present(self):
406-
"""Kiro CLI should be mapped to .kiro/prompts and legacy q removed."""
414+
"""Kiro CLI should be mapped to .kiro/prompts and q should be present."""
407415
assert "kiro-cli" in CommandRegistrar.AGENT_CONFIGS
408416
assert CommandRegistrar.AGENT_CONFIGS["kiro-cli"]["dir"] == ".kiro/prompts"
409-
assert "q" not in CommandRegistrar.AGENT_CONFIGS
417+
assert "q" in CommandRegistrar.AGENT_CONFIGS
410418

411419
def test_parse_frontmatter_valid(self):
412420
"""Test parsing valid YAML frontmatter."""
@@ -440,10 +448,7 @@ def test_parse_frontmatter_no_frontmatter(self):
440448

441449
def test_render_frontmatter(self):
442450
"""Test rendering frontmatter to YAML."""
443-
frontmatter = {
444-
"description": "Test command",
445-
"tools": ["tool1", "tool2"]
446-
}
451+
frontmatter = {"description": "Test command", "tools": ["tool1", "tool2"]}
447452

448453
registrar = CommandRegistrar()
449454
output = registrar.render_frontmatter(frontmatter)
@@ -463,9 +468,7 @@ def test_register_commands_for_claude(self, extension_dir, project_dir):
463468

464469
registrar = CommandRegistrar()
465470
registered = registrar.register_commands_for_claude(
466-
manifest,
467-
extension_dir,
468-
project_dir
471+
manifest, extension_dir, project_dir
469472
)
470473

471474
assert len(registered) == 1
@@ -510,18 +513,22 @@ def test_command_with_aliases(self, project_dir, temp_dir):
510513
},
511514
}
512515

513-
with open(ext_dir / "extension.yml", 'w') as f:
516+
with open(ext_dir / "extension.yml", "w") as f:
514517
yaml.dump(manifest_data, f)
515518

516519
(ext_dir / "commands").mkdir()
517-
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nTest")
520+
(ext_dir / "commands" / "cmd.md").write_text(
521+
"---\ndescription: Test\n---\n\nTest"
522+
)
518523

519524
claude_dir = project_dir / ".claude" / "commands"
520525
claude_dir.mkdir(parents=True)
521526

522527
manifest = ExtensionManifest(ext_dir / "extension.yml")
523528
registrar = CommandRegistrar()
524-
registered = registrar.register_commands_for_claude(manifest, ext_dir, project_dir)
529+
registered = registrar.register_commands_for_claude(
530+
manifest, ext_dir, project_dir
531+
)
525532

526533
assert len(registered) == 2
527534
assert "speckit.alias.cmd" in registered
@@ -570,7 +577,9 @@ def test_copilot_companion_prompt_created(self, extension_dir, project_dir):
570577
)
571578

572579
# Verify companion .prompt.md file exists
573-
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
580+
prompt_file = (
581+
project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
582+
)
574583
assert prompt_file.exists()
575584

576585
# Verify content has correct agent frontmatter
@@ -647,6 +656,7 @@ def test_non_copilot_agent_no_companion_file(self, extension_dir, project_dir):
647656

648657
# ===== Utility Function Tests =====
649658

659+
650660
class TestVersionSatisfies:
651661
"""Test version_satisfies utility function."""
652662

@@ -675,6 +685,7 @@ def test_version_satisfies_invalid(self):
675685

676686
# ===== Integration Tests =====
677687

688+
678689
class TestIntegration:
679690
"""Integration tests for complete workflows."""
680691

@@ -686,11 +697,7 @@ def test_full_install_and_remove_workflow(self, extension_dir, project_dir):
686697
manager = ExtensionManager(project_dir)
687698

688699
# Install
689-
manager.install_from_directory(
690-
extension_dir,
691-
"0.1.0",
692-
register_commands=True
693-
)
700+
manager.install_from_directory(extension_dir, "0.1.0", register_commands=True)
694701

695702
# Verify installation
696703
assert manager.registry.is_installed("test-ext")
@@ -707,8 +714,7 @@ def test_full_install_and_remove_workflow(self, extension_dir, project_dir):
707714
registered_commands = metadata["registered_commands"]
708715
# Check that the command is registered for at least one agent
709716
assert any(
710-
"speckit.test.hello" in cmds
711-
for cmds in registered_commands.values()
717+
"speckit.test.hello" in cmds for cmds in registered_commands.values()
712718
)
713719

714720
# Remove
@@ -734,7 +740,9 @@ def test_copilot_cleanup_removes_prompt_files(self, extension_dir, project_dir):
734740

735741
# Verify files exist before cleanup
736742
agent_file = agents_dir / "speckit.test.hello.agent.md"
737-
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
743+
prompt_file = (
744+
project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
745+
)
738746
assert agent_file.exists()
739747
assert prompt_file.exists()
740748

@@ -773,17 +781,23 @@ def test_multiple_extensions(self, temp_dir, project_dir):
773781
},
774782
}
775783

776-
with open(ext_dir / "extension.yml", 'w') as f:
784+
with open(ext_dir / "extension.yml", "w") as f:
777785
yaml.dump(manifest_data, f)
778786

779787
(ext_dir / "commands").mkdir()
780-
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\nTest")
788+
(ext_dir / "commands" / "cmd.md").write_text(
789+
"---\ndescription: Test\n---\nTest"
790+
)
781791

782792
manager = ExtensionManager(project_dir)
783793

784794
# Install both
785-
manager.install_from_directory(temp_dir / "ext1", "0.1.0", register_commands=False)
786-
manager.install_from_directory(temp_dir / "ext2", "0.1.0", register_commands=False)
795+
manager.install_from_directory(
796+
temp_dir / "ext1", "0.1.0", register_commands=False
797+
)
798+
manager.install_from_directory(
799+
temp_dir / "ext2", "0.1.0", register_commands=False
800+
)
787801

788802
# Verify both installed
789803
installed = manager.list_installed()
@@ -1235,6 +1249,7 @@ def test_clear_cache(self, temp_dir):
12351249

12361250
# ===== CatalogEntry Tests =====
12371251

1252+
12381253
class TestCatalogEntry:
12391254
"""Test CatalogEntry dataclass."""
12401255

@@ -1254,6 +1269,7 @@ def test_catalog_entry_creation(self):
12541269

12551270
# ===== Catalog Stack Tests =====
12561271

1272+
12571273
class TestCatalogStack:
12581274
"""Test multi-catalog stack support."""
12591275

@@ -1421,7 +1437,9 @@ def test_load_catalog_config_missing_file(self, temp_dir):
14211437
project_dir = self._make_project(temp_dir)
14221438
catalog = ExtensionCatalog(project_dir)
14231439

1424-
result = catalog._load_catalog_config(project_dir / ".specify" / "nonexistent.yml")
1440+
result = catalog._load_catalog_config(
1441+
project_dir / ".specify" / "nonexistent.yml"
1442+
)
14251443
assert result is None
14261444

14271445
def test_load_catalog_config_localhost_allowed(self, temp_dir):
@@ -1487,13 +1505,20 @@ def test_merge_conflict_higher_priority_wins(self, temp_dir):
14871505
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
14881506
catalog.cache_file.write_text(json.dumps(primary_data))
14891507
catalog.cache_metadata_file.write_text(
1490-
json.dumps({"cached_at": datetime.now(timezone.utc).isoformat(), "catalog_url": "http://test.com"})
1508+
json.dumps(
1509+
{
1510+
"cached_at": datetime.now(timezone.utc).isoformat(),
1511+
"catalog_url": "http://test.com",
1512+
}
1513+
)
14911514
)
14921515

14931516
# Write secondary cache (URL-hash-based) with jira v1.0.0 (should lose)
14941517
import hashlib
14951518

1496-
url_hash = hashlib.sha256(ExtensionCatalog.COMMUNITY_CATALOG_URL.encode()).hexdigest()[:16]
1519+
url_hash = hashlib.sha256(
1520+
ExtensionCatalog.COMMUNITY_CATALOG_URL.encode()
1521+
).hexdigest()[:16]
14971522
secondary_cache = catalog.cache_dir / f"catalog-{url_hash}.json"
14981523
secondary_meta = catalog.cache_dir / f"catalog-{url_hash}-metadata.json"
14991524
secondary_data = {
@@ -1515,7 +1540,12 @@ def test_merge_conflict_higher_priority_wins(self, temp_dir):
15151540
}
15161541
secondary_cache.write_text(json.dumps(secondary_data))
15171542
secondary_meta.write_text(
1518-
json.dumps({"cached_at": datetime.now(timezone.utc).isoformat(), "catalog_url": ExtensionCatalog.COMMUNITY_CATALOG_URL})
1543+
json.dumps(
1544+
{
1545+
"cached_at": datetime.now(timezone.utc).isoformat(),
1546+
"catalog_url": ExtensionCatalog.COMMUNITY_CATALOG_URL,
1547+
}
1548+
)
15191549
)
15201550

15211551
results = catalog.search()

0 commit comments

Comments
 (0)