diff --git a/.github/workflows/scripts/create-release-packages.ps1 b/.github/workflows/scripts/create-release-packages.ps1 index ed04d9cd3f..9487bfef42 100644 --- a/.github/workflows/scripts/create-release-packages.ps1 +++ b/.github/workflows/scripts/create-release-packages.ps1 @@ -298,7 +298,7 @@ function Build-Variant { } 'qwen' { $cmdDir = Join-Path $baseDir ".qwen/commands" - Generate-Commands -Agent 'qwen' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script + Generate-Commands -Agent 'qwen' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script if (Test-Path "agent_templates/qwen/QWEN.md") { Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md") } diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 1b2ced3ea3..a195274ce1 100755 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -154,8 +154,8 @@ build_variant() { [[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; } # NOTE: We substitute {ARGS} internally. Outward tokens differ intentionally: - # * Markdown/prompt (claude, copilot, cursor-agent, opencode): $ARGUMENTS - # * TOML (gemini, qwen): {{args}} + # * Markdown/prompt (claude, copilot, cursor-agent, opencode, qwen): $ARGUMENTS + # * TOML (gemini): {{args}} # This keeps formats readable without extra abstraction. case $agent in @@ -180,7 +180,7 @@ build_variant() { generate_commands cursor-agent md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;; qwen) mkdir -p "$base_dir/.qwen/commands" - generate_commands qwen toml "{{args}}" "$base_dir/.qwen/commands" "$script" + generate_commands qwen md "\$ARGUMENTS" "$base_dir/.qwen/commands" "$script" [[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;; opencode) mkdir -p "$base_dir/.opencode/command" diff --git a/AGENTS.md b/AGENTS.md index d8dc0f08f7..f0e60ed90f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ Specify supports multiple AI agents by generating agent-specific command files a | **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI | | **GitHub Copilot** | `.github/agents/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code | | **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI | -| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI | +| **Qwen Code** | `.qwen/commands/` | Markdown | `qwen` | Alibaba's Qwen Code CLI | | **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI | | **Codex CLI** | `.codex/commands/` | Markdown | `codex` | Codex CLI | | **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows | @@ -360,7 +360,7 @@ Command content with {SCRIPT} and $ARGUMENTS placeholders. ### TOML Format -Used by: Gemini, Qwen +Used by: Gemini ```toml description = "Command description" diff --git a/tests/test_ai_skills.py b/tests/test_ai_skills.py index 3eec4a419c..675f444ca0 100644 --- a/tests/test_ai_skills.py +++ b/tests/test_ai_skills.py @@ -132,6 +132,16 @@ def commands_dir_gemini(project_dir): return cmd_dir +@pytest.fixture +def commands_dir_qwen(project_dir): + """Create a populated .qwen/commands directory (Markdown format).""" + cmd_dir = project_dir / ".qwen" / "commands" + cmd_dir.mkdir(parents=True, exist_ok=True) + for name in ["speckit.specify.md", "speckit.plan.md", "speckit.tasks.md"]: + (cmd_dir / name).write_text(f"# {name}\nContent here\n") + return cmd_dir + + # ===== _get_skills_dir Tests ===== class TestGetSkillsDir: @@ -380,6 +390,28 @@ def test_non_md_commands_dir_falls_back(self, project_dir): # .toml commands should be untouched assert (cmds_dir / "speckit.specify.toml").exists() + def test_qwen_md_commands_dir_installs_skills(self, project_dir): + """Qwen now uses Markdown format; skills should install directly from .qwen/commands/.""" + cmds_dir = project_dir / ".qwen" / "commands" + cmds_dir.mkdir(parents=True) + (cmds_dir / "speckit.specify.md").write_text( + "---\ndescription: Create or update the feature specification.\n---\n\n# Specify\n\nBody.\n" + ) + (cmds_dir / "speckit.plan.md").write_text( + "---\ndescription: Generate implementation plan.\n---\n\n# Plan\n\nBody.\n" + ) + + result = install_ai_skills(project_dir, "qwen") + + assert result is True + skills_dir = project_dir / ".qwen" / "skills" + assert skills_dir.exists() + skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()] + assert len(skill_dirs) >= 1 + # .md commands should be untouched + assert (cmds_dir / "speckit.specify.md").exists() + assert (cmds_dir / "speckit.plan.md").exists() + @pytest.mark.parametrize("agent_key", [k for k in AGENT_CONFIG.keys() if k != "generic"]) def test_skills_install_for_all_agents(self, temp_dir, agent_key): """install_ai_skills should produce skills for every configured agent.""" @@ -433,6 +465,15 @@ def test_existing_commands_preserved_gemini(self, project_dir, templates_dir, co remaining = list(commands_dir_gemini.glob("speckit.*")) assert len(remaining) == 3 + def test_existing_commands_preserved_qwen(self, project_dir, templates_dir, commands_dir_qwen): + """install_ai_skills must NOT remove pre-existing .qwen/commands files.""" + assert len(list(commands_dir_qwen.glob("speckit.*"))) == 3 + + install_ai_skills(project_dir, "qwen") + + remaining = list(commands_dir_qwen.glob("speckit.*")) + assert len(remaining) == 3 + def test_commands_dir_not_removed(self, project_dir, templates_dir, commands_dir_claude): """install_ai_skills must not remove the commands directory.""" install_ai_skills(project_dir, "claude")