feat(agents): add JetBrains AI Assistant and Junie support#23
feat(agents): add JetBrains AI Assistant and Junie support#23wai-calvin wants to merge 2 commits into
Conversation
Add two new agents for JetBrains IDEs: - JetBrains AI Assistant: generates project rules in .aiassistant/rules/ as plain markdown with tracking comments (reuses KIRO format) - Junie: generates Agent Skills in .junie/skills/<name>/SKILL.md with YAML frontmatter (name + description) per the Agent Skills spec Introduces subdirectory layout support in AgentConfig for Junie's skill-per-directory structure, with corresponding writer changes for path construction, file discovery, and cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds support for two additional “agents”/output formats so the tool can generate prompt artifacts for JetBrains IDEs (AI Assistant rules) and Junie (Agent Skills), including handling Junie’s skill-per-directory layout.
Changes:
- Add
jetbrains-ai-assistantagent targeting.aiassistant/rulesusing the existing Kiro (plain markdown + tracking comment) generator. - Add
junieagent and a newJunieCommandGeneratorthat emitsSKILL.mdwith YAML frontmatter and tracking metadata. - Extend the writer to support subdirectory layouts for agents like Junie (path building, discovery, backup discovery, and cleanup of empty skill dirs).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
slash_commands/config.py |
Adds JUNIE format and registers jetbrains-ai-assistant + junie agent configs (Junie uses subdirectory layout + fixed filename). |
slash_commands/generators.py |
Introduces JunieCommandGenerator and wires it into the generator factory. |
slash_commands/writer.py |
Adds _build_output_path() and updates generation/discovery/cleanup to support subdirectory-based outputs. |
tests/test_config.py |
Updates config invariants/tests for the new format and layout fields. |
tests/test_generators.py |
Adds tests for JetBrains (via Kiro) and Junie generator behavior. |
tests/test_writer.py |
Adds writer integration tests for JetBrains rules and Junie skill directory output, discovery, and cleanup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Sanitize name per Agent Skills spec: lowercase a-z, numbers, hyphens only, | ||
| # 1-64 chars, no leading/trailing hyphens, no consecutive hyphens. | ||
| skill_name = _strip_ordering_prefix(prompt.name).lower() | ||
| skill_name = re.sub(r"[^a-z0-9-]", "-", skill_name) | ||
| skill_name = re.sub(r"-{2,}", "-", skill_name).strip("-")[:64] | ||
|
|
||
| # Build Junie YAML frontmatter | ||
| frontmatter: dict[str, Any] = { | ||
| "name": skill_name, | ||
| "description": (description or prompt.name)[:1024], | ||
| } |
There was a problem hiding this comment.
Junie skill names can become an empty string after sanitization (e.g., prompt.name with only punctuation). The Agent Skills spec requires a 1–64 char name, but skill_name is not validated/fallbacked after strip('-')[:64], so this can emit invalid frontmatter (name: ''). Add a post-sanitization fallback (and ideally raise/skip with a clear error) when skill_name ends up empty.
| if agent.use_subdirectory_layout and agent.fixed_filename: | ||
| dir_name = self._sanitize_filename(prompt_name, "") | ||
| return self.base_path / agent.get_command_dir() / dir_name / agent.fixed_filename | ||
| filename = self._sanitize_filename(prompt_name, agent.command_file_extension) | ||
| return self.base_path / agent.get_command_dir() / filename |
There was a problem hiding this comment.
For subdirectory-layout agents, _build_output_path() derives the directory name via _sanitize_filename(prompt_name, ''), which allows uppercase/underscores/dots and does not strip ordering prefixes. Junie’s generator independently sanitizes the skill name (lowercase, hyphens only, strips ordering prefixes). This can produce mismatches like .junie/skills/SDD-1-My_Complex.Prompt/SKILL.md while frontmatter name is my-complex-prompt, conflicting with the PR’s intended .junie/skills/<name>/SKILL.md layout. Consider deriving the directory name from the same sanitized skill-name logic used by the Junie generator (or centralize this into a shared helper/AgentConfig).
| # No consecutive hyphens | ||
| assert "--" not in name | ||
|
|
||
|
|
There was a problem hiding this comment.
Current Junie generator tests validate character constraints but don’t cover the edge case where sanitization yields an empty name (e.g., prompt names consisting only of punctuation). Adding a test for that case would lock in the required 1–64 char constraint and prevent emitting invalid frontmatter.
| def test_junie_generator_sanitized_name_is_never_empty(): | |
| """Test that sanitization never produces an empty Junie frontmatter name.""" | |
| from pathlib import Path | |
| from mcp_server.prompt_utils import MarkdownPrompt | |
| prompt = MarkdownPrompt( | |
| name="!!!...___---", | |
| description="Test", | |
| tags=[], | |
| arguments=[], | |
| enabled=True, | |
| body="# Test", | |
| path=Path("test.md"), | |
| meta={}, | |
| ) | |
| agent = get_agent_config("junie") | |
| generator = JunieCommandGenerator() | |
| generated = generator.generate(prompt, agent) | |
| frontmatter, _ = _extract_frontmatter_and_body(generated) | |
| name = frontmatter["name"] | |
| assert 1 <= len(name) <= 64 | |
| assert name == name.lower() | |
| assert all(c in "abcdefghijklmnopqrstuvwxyz0123456789-" for c in name) | |
| assert not name.startswith("-") | |
| assert not name.endswith("-") | |
| assert "--" not in name |
| def test_writer_generates_junie_skill_in_subdirectory(mock_prompt_load: Path, tmp_path): | ||
| """Test that writer generates Junie skills in subdirectory layout.""" | ||
| prompts_dir = mock_prompt_load | ||
|
|
||
| writer = SlashCommandWriter( | ||
| prompts_dir=prompts_dir, | ||
| agents=["junie"], | ||
| dry_run=False, | ||
| base_path=tmp_path, | ||
| ) | ||
|
|
||
| writer.generate() | ||
|
|
||
| skills_output_dir = tmp_path / ".junie" / "skills" | ||
| assert skills_output_dir.exists() | ||
|
|
||
| # Should create a subdirectory with SKILL.md inside | ||
| skill_file = skills_output_dir / "test-prompt" / "SKILL.md" | ||
| assert skill_file.exists() |
There was a problem hiding this comment.
The writer’s Junie subdirectory-layout test only exercises a simple prompt name (test-prompt). Consider adding a case with an ordering prefix / punctuation (e.g., SDD-1-My_Complex.Prompt!Name) and asserting the output directory matches the sanitized skill name. This would catch mismatches between directory naming and Junie frontmatter name generation.
Add two new agents for JetBrains IDEs:
Introduces subdirectory layout support in AgentConfig for Junie's skill-per-directory structure, with corresponding writer changes for path construction, file discovery, and cleanup.
Why?
What Changed?
Testing
Additional Notes
Checklist
Closes #123)pre-commit run --all-files