mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd8953696 |
2
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
2
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
@@ -8,7 +8,7 @@ body:
|
||||
value: |
|
||||
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
|
||||
|
||||
**Currently supported agents**: Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Cursor, Devin for Terminal, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Grok Build, Hermes Agent, IBM Bob, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, Oh My Pi, opencode, Pi Coding Agent, Qoder CLI, Qwen Code, RovoDev ACLI, SHAI, Tabnine CLI, Trae, ZCode, Zed
|
||||
**Currently supported agents**: Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Cursor, Devin for Terminal, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Hermes Agent, IBM Bob, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, Oh My Pi, opencode, Pi Coding Agent, Qoder CLI, Qwen Code, RovoDev ACLI, SHAI, Tabnine CLI, Trae, ZCode, Zed
|
||||
|
||||
- type: input
|
||||
id: agent-name
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -76,7 +76,6 @@ body:
|
||||
- Gemini CLI
|
||||
- GitHub Copilot
|
||||
- Goose
|
||||
- Grok Build
|
||||
- Hermes Agent
|
||||
- IBM Bob
|
||||
- Junie
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -70,7 +70,6 @@ body:
|
||||
- Gemini CLI
|
||||
- GitHub Copilot
|
||||
- Goose
|
||||
- Grok Build
|
||||
- Hermes Agent
|
||||
- IBM Bob
|
||||
- Junie
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -53,10 +53,9 @@ docs/dev
|
||||
|
||||
# The following directories/file are intentionally ignored so that they are not accidentally
|
||||
# committed to the repository. They contain the scaffolding `specify init --integration copilot`
|
||||
# (or other agents) does and they are meant for dogfooding Spec Kit during its own feature development.
|
||||
# does and they are meant for dogfooding Spec Kit during its own feature development.
|
||||
.github/agents/
|
||||
.github/prompts/
|
||||
.github/copilot-instructions.md
|
||||
.grok/
|
||||
.specify/
|
||||
specs/
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -2,21 +2,6 @@
|
||||
|
||||
<!-- insert new changelog below this comment -->
|
||||
|
||||
## [0.12.17] - 2026-07-16
|
||||
|
||||
### Changed
|
||||
|
||||
- fix(extensions): resolve __SPECKIT_COMMAND tokens in auto-registered skills (#3544)
|
||||
- fix(workflows): fail if/switch steps on non-list branch instead of crashing (#3515)
|
||||
- feat(integrations): add Grok Build skills-based integration (#3535)
|
||||
- fix(extensions/git): reject negative -Number in create-new-feature-branch.ps1 (#3538)
|
||||
- test: cover preset constitution seeding through init CLI (#3297)
|
||||
- fix(integration): preserve ai_skills on `use` for skills-mode Copilot (#3550) (#3551)
|
||||
- [extension] Add Figma Starter extension to community catalog (#3547)
|
||||
- [extension] Add Spec-Kit BDD extension to community catalog (#3548)
|
||||
- [extension] Update Quality Gates (Enforcement Layer) extension to v0.3.2 (#3542)
|
||||
- chore: release 0.12.16, begin 0.12.17.dev0 development (#3549)
|
||||
|
||||
## [0.12.16] - 2026-07-15
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -54,7 +54,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| DocGuard — CDD Enforcement | Doc-integrity engine with MCP server, SARIF output, and zero-LLM core. Validates, scores, and traces documentation against code — 24 validators, stable finding codes, spec-kit hooks. Pure Node.js. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
||||
| EARS Requirements Syntax | Author, lint, and convert requirements using EARS - the five industry-standard sentence patterns for unambiguous, testable requirements | `docs` | Read+Write | [spec-kit-ears](https://github.com/dhruv-15-03/spec-kit-ears) |
|
||||
| Extensify | Create and validate extensions and extension catalogs | `process` | Read+Write | [extensify](https://github.com/mnriem/spec-kit-extensions/tree/main/extensify) |
|
||||
| Figma Starter | Turns a Figma section's screens into per-screen spec.md files, an app-level user-stories.md, and a build-order.md, then hands off to /speckit.specify | `integration` | Read+Write | [spec-kit-figma-starter](https://github.com/wavemaker/spec-kit-figma-starter) |
|
||||
| Fix Findings | Automated analyze-fix-reanalyze loop that resolves spec findings until clean | `code` | Read+Write | [spec-kit-fix-findings](https://github.com/Quratulain-bilal/spec-kit-fix-findings) |
|
||||
| FixIt Extension | Spec-aware bug fixing — maps bugs to spec artifacts, proposes a plan, applies minimal changes | `code` | Read+Write | [spec-kit-fixit](https://github.com/speckit-community/spec-kit-fixit) |
|
||||
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
||||
@@ -101,7 +100,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
|
||||
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
|
||||
| QA Testing Extension | Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec | `code` | Read-only | [spec-kit-qa](https://github.com/arunt14/spec-kit-qa) |
|
||||
| Quality Gates (Enforcement Layer) | Deterministic quality enforcement for Spec Kit across agent hooks, git checks, and CI pipelines with one policy file and one verify entrypoint for identical results at every boundary. | `process` | Read+Write | [spec-gates](https://github.com/schwichtgit/spec-gates) |
|
||||
| Quality Gates (Enforcement Layer) | Deterministic quality enforcement for Spec Kit projects at three boundaries — agent hooks, git pre-commit, CI — with one policy file and provable enforcement (attestations, canaries, verified parity). | `process` | Read+Write | [spec-gates](https://github.com/schwichtgit/spec-gates) |
|
||||
| RAG Azure Builder | Spec Kit extension for onboarding and operating an Azure RAG stack with guided workflows. | `process` | Read+Write | [spec-kit-extension-rag-azure-builder](https://github.com/Sertxito/spec-kit-extension-rag-azure-builder) |
|
||||
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss-Projects/spec-kit-ralph) |
|
||||
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
|
||||
@@ -135,7 +134,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
||||
| Spec Trace | Build a requirement → test traceability matrix from spec.md and the test suite — surface untested requirements and orphan tests | `code` | Read+Write | [spec-kit-trace](https://github.com/Quratulain-bilal/spec-kit-trace) |
|
||||
| Spec Validate | Comprehension validation, review gating, and approval state for spec-kit artifacts — staged quizzes, peer review SLA, and a hard gate before /speckit.implement | `process` | Read+Write | [spec-kit-spec-validate](https://github.com/aeltayeb/spec-kit-spec-validate) |
|
||||
| Spec-Kit BDD | ATDD/BDD extension: convert specs to Gherkin scenarios, scaffold step definitions, and verify acceptance test coverage | `process` | Read+Write | [spec-kit-bdd](https://github.com/RSginer/spec-kit-bdd) |
|
||||
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure | `process` | Read+Write | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
|
||||
| SpecKit Companion | Live spec-driven progress — lifecycle capture, status, resume, and a turbo pipeline profile | `visibility` | Read+Write | [speckit-companion](https://github.com/alfredoperez/speckit-companion) |
|
||||
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
|
||||
|
||||
@@ -20,7 +20,6 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |
|
||||
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | Defaults to legacy markdown mode: `.agent.md` command files under `.github/agents/`, companion `.prompt.md` files under `.github/prompts/`, and a `.vscode/settings.json` merge. Pass `--integration-options="--skills"` to scaffold skills as `speckit-<command>/SKILL.md` under `.github/skills/` instead. Legacy markdown mode is deprecated and will stop being the default in a future release. |
|
||||
| [Goose](https://goose-docs.ai/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
|
||||
| [Grok Build](https://docs.x.ai/build/overview) | `grok` | Skills-based integration; installs skills into `.grok/skills` and invokes them as `/speckit-<command>` |
|
||||
| [Hermes](https://github.com/NousResearch/hermes-agent) | `hermes` | Skills-based integration; installs skills globally into `~/.hermes/skills/` |
|
||||
| [IBM Bob](https://www.ibm.com/products/bob) | `bob` | IDE-based agent |
|
||||
| [Junie](https://junie.jetbrains.com/) | `junie` | |
|
||||
@@ -250,11 +249,7 @@ Spec Kit tracks one default integration in `.specify/integration.json` with `def
|
||||
|
||||
### Which integrations are multi-install safe?
|
||||
|
||||
An integration is multi-install safe when it uses a static, unique agent root and command directory, stable command invocation settings, and a separate install manifest whose managed files do not overlap another safe integration. Registry tests enforce those path and manifest invariants. Shared Spec Kit templates remain aligned to the single default integration.
|
||||
|
||||
The Isolation column below lists paths Spec Kit manages for that integration (skills/commands roots and any integration-owned rule files). It is not a full inventory of every file an agent may read.
|
||||
|
||||
**Agent-context defaults are separate.** The optional agent-context extension maps each integration to a default context file in `extensions/agent-context/agent-context-defaults.json`. Those defaults are independent of multi-install safety: several agents may share a root file such as `AGENTS.md` when the extension is enabled. Multi-install safety does not require a unique context file per safe integration.
|
||||
An integration is multi-install safe when it uses isolated agent directories, a dedicated context file that does not collide with another safe integration, stable command invocation settings, and a separate install manifest. Shared Spec Kit templates remain aligned to the single default integration.
|
||||
|
||||
The currently declared multi-install safe integrations are:
|
||||
|
||||
@@ -268,7 +263,6 @@ The currently declared multi-install safe integrations are:
|
||||
| `cursor-agent` | `.cursor/skills`, `.cursor/rules/specify-rules.mdc` |
|
||||
| `firebender` | `.firebender/commands`, `.firebender/rules/specify-rules.mdc` |
|
||||
| `gemini` | `.gemini/commands`, `GEMINI.md` |
|
||||
| `grok` | `.grok/skills` |
|
||||
| `junie` | `.junie/commands`, `.junie/AGENTS.md` |
|
||||
| `kilocode` | `.kilocode/workflows`, `.kilocode/rules/specify-rules.md` |
|
||||
| `qodercli` | `.qoder/commands`, `QODER.md` |
|
||||
@@ -278,7 +272,7 @@ The currently declared multi-install safe integrations are:
|
||||
| `trae` | `.trae/skills`, `.trae/rules/project_rules.md` |
|
||||
| `zcode` | `.zcode/skills`, `ZCODE.md` |
|
||||
|
||||
Integrations that share a command directory with another integration, require dynamic install paths such as `--commands-dir`, or merge shared tool settings are not declared safe by default. They can still be installed alongside another integration with `--force`.
|
||||
Integrations that share a context file or command directory with another integration, require dynamic install paths such as `--commands-dir`, or merge shared tool settings are not declared safe by default. They can still be installed alongside another integration with `--force`.
|
||||
|
||||
### What happens to my changes when I uninstall or switch?
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"gemini": "GEMINI.md",
|
||||
"generic": "AGENTS.md",
|
||||
"goose": "AGENTS.md",
|
||||
"grok": "AGENTS.md",
|
||||
"hermes": "AGENTS.md",
|
||||
"junie": ".junie/AGENTS.md",
|
||||
"kilocode": ".kilocode/rules/specify-rules.md",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-07-15T14:10:00Z",
|
||||
"updated_at": "2026-07-15T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||
"extensions": {
|
||||
"aide": {
|
||||
@@ -395,66 +395,6 @@
|
||||
"created_at": "2026-03-03T00:00:00Z",
|
||||
"updated_at": "2026-03-03T00:00:00Z"
|
||||
},
|
||||
"bdd": {
|
||||
"name": "Spec-Kit BDD",
|
||||
"id": "bdd",
|
||||
"description": "ATDD/BDD extension: convert specs to Gherkin scenarios, scaffold step definitions, and verify acceptance test coverage.",
|
||||
"author": "RSginer",
|
||||
"version": "1.0.2",
|
||||
"download_url": "https://github.com/RSginer/spec-kit-bdd/archive/refs/tags/v1.0.2.zip",
|
||||
"repository": "https://github.com/RSginer/spec-kit-bdd",
|
||||
"homepage": "https://github.com/RSginer/spec-kit-bdd",
|
||||
"documentation": "https://github.com/RSginer/spec-kit-bdd/blob/main/docs/usage.md",
|
||||
"changelog": "https://github.com/RSginer/spec-kit-bdd/releases",
|
||||
"license": "MIT",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.2.0",
|
||||
"tools": [
|
||||
{
|
||||
"name": "pytest-bdd",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "behave",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "@cucumber/cucumber",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "cucumber",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "io.cucumber",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "SpecFlow",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 2
|
||||
},
|
||||
"tags": [
|
||||
"bdd",
|
||||
"gherkin",
|
||||
"atdd",
|
||||
"acceptance-testing",
|
||||
"tdd"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-15T00:00:00Z",
|
||||
"updated_at": "2026-07-15T00:00:00Z"
|
||||
},
|
||||
"blueprint": {
|
||||
"name": "Blueprint",
|
||||
"id": "blueprint",
|
||||
@@ -1355,43 +1295,6 @@
|
||||
"created_at": "2026-07-08T00:00:00Z",
|
||||
"updated_at": "2026-07-08T00:00:00Z"
|
||||
},
|
||||
"figma-starter": {
|
||||
"name": "Figma Starter",
|
||||
"id": "figma-starter",
|
||||
"description": "Turns a Figma section's screens into per-screen spec.md files, an app-level user-stories.md, and a build-order.md, then hands off to /speckit.specify.",
|
||||
"author": "WaveMaker",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/wavemaker/spec-kit-figma-starter/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/wavemaker/spec-kit-figma-starter",
|
||||
"homepage": "https://github.com/wavemaker/spec-kit-figma-starter",
|
||||
"documentation": "https://github.com/wavemaker/spec-kit-figma-starter/blob/main/README.md",
|
||||
"changelog": "https://github.com/wavemaker/spec-kit-figma-starter/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "integration",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0",
|
||||
"tools": [
|
||||
{ "name": "python3", "version": ">=3.8", "required": true }
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"figma",
|
||||
"design",
|
||||
"design-to-spec",
|
||||
"ui",
|
||||
"frontend"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-15T00:00:00Z",
|
||||
"updated_at": "2026-07-15T00:00:00Z"
|
||||
},
|
||||
"fix-findings": {
|
||||
"name": "Fix Findings",
|
||||
"id": "fix-findings",
|
||||
@@ -1535,14 +1438,13 @@
|
||||
"gates": {
|
||||
"name": "Quality Gates (Enforcement Layer)",
|
||||
"id": "gates",
|
||||
"description": "Deterministic quality enforcement for Spec Kit across agent hooks, git checks, and CI pipelines with one policy file and one verify entrypoint for identical results at every boundary.",
|
||||
"description": "Deterministic quality enforcement for Spec Kit projects at three boundaries — agent hooks, git pre-commit, CI — with one policy file and provable enforcement (attestations, canaries, verified parity).",
|
||||
"author": "schwichtgit",
|
||||
"version": "0.3.2",
|
||||
"download_url": "https://github.com/schwichtgit/spec-gates/releases/download/v0.3.2/gates-0.3.2.zip",
|
||||
"version": "0.1.0",
|
||||
"download_url": "https://github.com/schwichtgit/spec-gates/releases/download/v0.1.0/gates-0.1.0.zip",
|
||||
"repository": "https://github.com/schwichtgit/spec-gates",
|
||||
"homepage": "https://github.com/schwichtgit/spec-gates",
|
||||
"documentation": "https://github.com/schwichtgit/spec-gates/blob/main/docs/how-it-works.md",
|
||||
"changelog": "https://github.com/schwichtgit/spec-gates/releases",
|
||||
"license": "MIT",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
@@ -1555,7 +1457,7 @@
|
||||
},
|
||||
{
|
||||
"name": "git",
|
||||
"required": false
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
@@ -1568,8 +1470,8 @@
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 8,
|
||||
"hooks": 2
|
||||
"commands": 5,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"quality",
|
||||
@@ -1582,7 +1484,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-09T00:00:00Z",
|
||||
"updated_at": "2026-07-15T00:00:00Z"
|
||||
"updated_at": "2026-07-13T00:00:00Z"
|
||||
},
|
||||
"github-issues": {
|
||||
"name": "GitHub Issues Integration 1",
|
||||
|
||||
@@ -41,16 +41,6 @@ if ($Help) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# -Number is [long], so PowerShell binds "-5" as -5 rather than rejecting it
|
||||
# the way the bash/Python twins do (`^[0-9]+$`). A negative value would format
|
||||
# via '{0:000}' to e.g. "-005" and produce a branch name starting with "-",
|
||||
# which git refuses (refs cannot begin with a dash). Reject it here, before the
|
||||
# description check, matching the bash twin's parse-time validation order.
|
||||
if ($Number -lt 0) {
|
||||
Write-Error 'Error: --number must be a non-negative integer'
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
||||
Write-Error "Usage: ./create-new-feature-branch.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
||||
exit 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-07-15T00:00:00Z",
|
||||
"updated_at": "2026-06-23T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
|
||||
"integrations": {
|
||||
"claude": {
|
||||
@@ -282,15 +282,6 @@
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["cli"]
|
||||
},
|
||||
"grok": {
|
||||
"id": "grok",
|
||||
"name": "Grok Build",
|
||||
"version": "1.0.0",
|
||||
"description": "xAI Grok Build CLI skills-based integration",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["cli", "skills", "xai"]
|
||||
},
|
||||
"hermes": {
|
||||
"id": "hermes",
|
||||
"name": "Hermes Agent",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "specify-cli"
|
||||
version = "0.12.17"
|
||||
version = "0.12.16"
|
||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
DOLLAR_SKILLS_AGENTS: frozenset[str] = frozenset({"codex", "zcode"})
|
||||
|
||||
# Agents that always render /speckit-<name>, regardless of ai_skills.
|
||||
ALWAYS_SLASH_AGENTS: frozenset[str] = frozenset({"devin", "grok", "trae", "zed"})
|
||||
ALWAYS_SLASH_AGENTS: frozenset[str] = frozenset({"devin", "trae", "zed"})
|
||||
|
||||
# Agents that render /speckit-<name> only when ai_skills is enabled.
|
||||
CONDITIONAL_SLASH_AGENTS: frozenset[str] = frozenset(
|
||||
|
||||
@@ -701,7 +701,6 @@ def register(app: typer.Typer) -> None:
|
||||
copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration
|
||||
devin_skill_mode = selected_ai == "devin"
|
||||
zed_skill_mode = selected_ai == "zed" and _is_skills_integration
|
||||
grok_skill_mode = selected_ai == "grok" and _is_skills_integration
|
||||
cline_skill_mode = selected_ai == "cline"
|
||||
native_skill_mode = (
|
||||
codex_skill_mode
|
||||
@@ -714,7 +713,6 @@ def register(app: typer.Typer) -> None:
|
||||
or copilot_skill_mode
|
||||
or devin_skill_mode
|
||||
or zed_skill_mode
|
||||
or grok_skill_mode
|
||||
)
|
||||
|
||||
if codex_skill_mode:
|
||||
@@ -747,11 +745,6 @@ def register(app: typer.Typer) -> None:
|
||||
f"{step_num}. Start Zed in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]"
|
||||
)
|
||||
step_num += 1
|
||||
if grok_skill_mode:
|
||||
steps_lines.append(
|
||||
f"{step_num}. Start Grok Build in this project directory; spec-kit skills were installed to [cyan].grok/skills[/cyan]"
|
||||
)
|
||||
step_num += 1
|
||||
usage_label = "skills" if native_skill_mode else "slash commands"
|
||||
|
||||
from .._invocation_style import (
|
||||
|
||||
@@ -1004,7 +1004,6 @@ class ExtensionManager:
|
||||
from .. import load_init_options
|
||||
from ..agents import CommandRegistrar
|
||||
from ..integrations import get_integration
|
||||
from ..integrations.base import IntegrationBase
|
||||
|
||||
written: List[str] = []
|
||||
opts = load_init_options(self.project_root)
|
||||
@@ -1016,30 +1015,6 @@ class ExtensionManager:
|
||||
registrar = CommandRegistrar()
|
||||
agent_config = registrar.AGENT_CONFIGS.get(selected_ai, {})
|
||||
integration = get_integration(selected_ai)
|
||||
ai_skills_enabled = is_ai_skills_enabled(opts)
|
||||
|
||||
def _resolve_command_ref_tokens(body: str) -> str:
|
||||
"""Resolve explicit command-ref tokens with the active skill style."""
|
||||
|
||||
def _replacement(match: re.Match[str]) -> str:
|
||||
command_name = "speckit." + match.group(1).lower().replace("_", ".")
|
||||
if is_dollar_skills_agent(selected_ai, ai_skills_enabled):
|
||||
return "$" + command_name.replace("speckit.", "speckit-").replace(
|
||||
".", "-"
|
||||
)
|
||||
if is_slash_skills_agent(selected_ai, ai_skills_enabled):
|
||||
return "/" + command_name.replace("speckit.", "speckit-").replace(
|
||||
".", "-"
|
||||
)
|
||||
if integration is not None:
|
||||
return integration.build_command_invocation(command_name)
|
||||
return IntegrationBase.resolve_command_refs(
|
||||
match.group(0), agent_config.get("invoke_separator", ".")
|
||||
)
|
||||
|
||||
return re.sub(
|
||||
r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__", _replacement, body
|
||||
)
|
||||
|
||||
for cmd_info in manifest.commands:
|
||||
cmd_name = cmd_info["name"]
|
||||
@@ -1111,7 +1086,6 @@ class ExtensionManager:
|
||||
body = registrar.resolve_skill_placeholders(
|
||||
selected_ai, frontmatter, body, self.project_root, extension_id=manifest.id
|
||||
)
|
||||
body = _resolve_command_ref_tokens(body)
|
||||
|
||||
original_desc = frontmatter.get("description", "")
|
||||
description = original_desc or f"Extension command: {cmd_name}"
|
||||
|
||||
@@ -63,7 +63,6 @@ def _register_builtins() -> None:
|
||||
from .gemini import GeminiIntegration
|
||||
from .generic import GenericIntegration
|
||||
from .goose import GooseIntegration
|
||||
from .grok import GrokIntegration
|
||||
from .hermes import HermesIntegration
|
||||
from .junie import JunieIntegration
|
||||
from .kilocode import KilocodeIntegration
|
||||
@@ -100,7 +99,6 @@ def _register_builtins() -> None:
|
||||
_register(GeminiIntegration())
|
||||
_register(GenericIntegration())
|
||||
_register(GooseIntegration())
|
||||
_register(GrokIntegration())
|
||||
_register(HermesIntegration())
|
||||
_register(JunieIntegration())
|
||||
_register(KilocodeIntegration())
|
||||
|
||||
@@ -260,7 +260,6 @@ def _update_init_options_for_integration(
|
||||
project_root: Path,
|
||||
integration: Any,
|
||||
script_type: str | None = None,
|
||||
parsed_options: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Update init-options.json to reflect *integration* as the active one.
|
||||
|
||||
@@ -279,17 +278,7 @@ def _update_init_options_for_integration(
|
||||
opts["speckit_version"] = _get_speckit_version()
|
||||
if script_type:
|
||||
opts["script"] = script_type
|
||||
# Skills mode is either intrinsic (SkillsIntegration), set on the instance
|
||||
# during setup() (_skills_mode), or requested via parsed options (e.g.
|
||||
# Copilot's --skills, persisted as parsed_options["skills"]). The latter is
|
||||
# the only signal available on the `use` path, where no setup() runs and a
|
||||
# fresh integration instance has _skills_mode == False (issue #3550).
|
||||
skills_mode = (
|
||||
isinstance(integration, SkillsIntegration)
|
||||
or getattr(integration, "_skills_mode", False)
|
||||
or bool((parsed_options or {}).get("skills"))
|
||||
)
|
||||
if skills_mode:
|
||||
if isinstance(integration, SkillsIntegration) or getattr(integration, "_skills_mode", False):
|
||||
opts["ai_skills"] = True
|
||||
else:
|
||||
opts.pop("ai_skills", None)
|
||||
@@ -345,9 +334,7 @@ def _set_default_integration(
|
||||
) from exc
|
||||
|
||||
_write_integration_json(project_root, key, installed_keys, settings)
|
||||
_update_init_options_for_integration(
|
||||
project_root, integration, script_type=resolved_script, parsed_options=parsed_options
|
||||
)
|
||||
_update_init_options_for_integration(project_root, integration, script_type=resolved_script)
|
||||
|
||||
|
||||
def _set_default_integration_or_exit(*args: Any, **kwargs: Any) -> None:
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Grok Build integration — skills-based agent.
|
||||
|
||||
Grok Build discovers project skills from ``.grok/skills/speckit-<name>/SKILL.md``
|
||||
(and also scans ``.agents/skills/``). Spec Kit installs into the native
|
||||
``.grok/skills`` tree so skills take highest local priority.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..base import SkillsIntegration
|
||||
|
||||
|
||||
class GrokIntegration(SkillsIntegration):
|
||||
"""Integration for xAI Grok Build CLI."""
|
||||
|
||||
key = "grok"
|
||||
config = {
|
||||
"name": "Grok Build",
|
||||
"folder": ".grok/",
|
||||
"commands_subdir": "skills",
|
||||
"install_url": "https://docs.x.ai/build/overview",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".grok/skills",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
multi_install_safe = True
|
||||
|
||||
def build_exec_args(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
model: str | None = None,
|
||||
output_json: bool = True,
|
||||
) -> list[str] | None:
|
||||
"""Build CLI arguments for non-interactive ``grok`` execution.
|
||||
|
||||
Mandatory headless flag:
|
||||
|
||||
* ``--always-approve`` — auto-approve tool executions so workflow
|
||||
dispatch and ``dispatch_command()`` are not blocked at permission
|
||||
gates (same role as Cursor's ``--force`` / Copilot's ``--yolo``).
|
||||
"""
|
||||
if not self.config or not self.config.get("requires_cli"):
|
||||
return None
|
||||
args = [
|
||||
self._resolve_executable(),
|
||||
"-p",
|
||||
prompt,
|
||||
"--always-approve",
|
||||
]
|
||||
self._apply_extra_args_env_var(args)
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if output_json:
|
||||
args.extend(["--output-format", "json"])
|
||||
return args
|
||||
@@ -22,33 +22,10 @@ class IfThenStep(StepBase):
|
||||
result = evaluate_condition(condition, context)
|
||||
|
||||
if result:
|
||||
branch_name = "then"
|
||||
branch = config.get("then", [])
|
||||
else:
|
||||
branch_name = "else"
|
||||
branch = config.get("else", [])
|
||||
|
||||
# The engine does not auto-validate step config (see
|
||||
# ``WorkflowEngine.load_workflow``), and it feeds ``next_steps`` straight
|
||||
# into ``_execute_steps`` which iterates them as step mappings. A
|
||||
# non-list branch (a single mapping or scalar authoring mistake) would
|
||||
# otherwise be iterated element-wise — a dict yields its string keys, a
|
||||
# str its characters — and crash the whole run with AttributeError on
|
||||
# ``.get()``. ``validate`` already rejects a non-list branch; fail this
|
||||
# step loudly on an unvalidated run instead, mirroring the switch/fan-out
|
||||
# steps. A missing ``else`` defaults to ``[]`` and stays valid.
|
||||
if branch is None and branch_name == "else":
|
||||
branch = []
|
||||
elif not isinstance(branch, list):
|
||||
return StepResult(
|
||||
status=StepStatus.FAILED,
|
||||
output={"condition_result": result},
|
||||
error=(
|
||||
f"If step {config.get('id', '?')!r}: {branch_name!r} must be "
|
||||
f"a list of steps, got {type(branch).__name__}."
|
||||
),
|
||||
)
|
||||
|
||||
return StepResult(
|
||||
status=StepStatus.COMPLETED,
|
||||
output={"condition_result": result},
|
||||
|
||||
@@ -42,10 +42,6 @@ class SwitchStep(StepBase):
|
||||
)
|
||||
for case_key, case_steps in cases.items():
|
||||
if str(case_key) == str_value:
|
||||
if not isinstance(case_steps, list):
|
||||
return self._non_list_branch_failure(
|
||||
config, f"case {str(case_key)!r}", case_steps, value
|
||||
)
|
||||
return StepResult(
|
||||
status=StepStatus.COMPLETED,
|
||||
output={"matched_case": str(case_key), "expression_value": value},
|
||||
@@ -54,41 +50,12 @@ class SwitchStep(StepBase):
|
||||
|
||||
# Default fallback
|
||||
default_steps = config.get("default", [])
|
||||
if default_steps is None:
|
||||
default_steps = []
|
||||
elif not isinstance(default_steps, list):
|
||||
return self._non_list_branch_failure(
|
||||
config, "'default'", default_steps, value
|
||||
)
|
||||
return StepResult(
|
||||
status=StepStatus.COMPLETED,
|
||||
output={"matched_case": "__default__", "expression_value": value},
|
||||
next_steps=default_steps,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _non_list_branch_failure(
|
||||
config: dict[str, Any], branch_label: str, branch: Any, value: Any
|
||||
) -> StepResult:
|
||||
"""Fail the step for a non-list branch instead of crashing the run.
|
||||
|
||||
``validate`` rejects a non-list case/default branch, but the engine does
|
||||
not auto-validate and feeds ``next_steps`` straight into
|
||||
``_execute_steps``, which iterates them as step mappings. A non-list
|
||||
branch would be iterated element-wise (a dict yields its keys, a str its
|
||||
characters) and crash the whole run with AttributeError on ``.get()``.
|
||||
Fail this step loudly on an unvalidated run instead, mirroring the
|
||||
non-mapping ``cases`` guard above.
|
||||
"""
|
||||
return StepResult(
|
||||
status=StepStatus.FAILED,
|
||||
output={"matched_case": None, "expression_value": value},
|
||||
error=(
|
||||
f"Switch step {config.get('id', '?')!r}: {branch_label} must be "
|
||||
f"a list of steps, got {type(branch).__name__}."
|
||||
),
|
||||
)
|
||||
|
||||
def validate(self, config: dict[str, Any]) -> list[str]:
|
||||
errors = super().validate(config)
|
||||
if "expression" not in config:
|
||||
|
||||
@@ -653,19 +653,6 @@ class TestCreateFeatureBash:
|
||||
assert data["BRANCH_NAME"] == "000-zero"
|
||||
assert data["FEATURE_NUM"] == "000"
|
||||
|
||||
def test_negative_number_rejected(self, tmp_path: Path):
|
||||
"""A negative --number is rejected. Pins the canonical behavior the
|
||||
PowerShell twin must mirror; a negative value would otherwise format to
|
||||
e.g. '-005' and produce a branch name starting with '-', which git
|
||||
refuses (refs cannot begin with a dash)."""
|
||||
project = _setup_project(tmp_path)
|
||||
result = _run_bash(
|
||||
"create-new-feature-branch.sh", project,
|
||||
"--json", "--dry-run", "--number", "-5", "--short-name", "neg", "Negative feature",
|
||||
)
|
||||
assert result.returncode != 0
|
||||
assert "--number must be a non-negative integer" in result.stderr
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_PWSH, reason="pwsh not available")
|
||||
class TestCreateFeaturePowerShell:
|
||||
@@ -987,21 +974,6 @@ class TestCreateFeaturePowerShell:
|
||||
assert data["BRANCH_NAME"] == "000-zero"
|
||||
assert data["FEATURE_NUM"] == "000"
|
||||
|
||||
def test_negative_number_rejected(self, tmp_path: Path):
|
||||
"""A negative -Number is rejected, matching the bash/Python twins'
|
||||
'--number must be a non-negative integer'. Regression guard: -Number is
|
||||
[long], so PowerShell binds '-5' as -5 rather than rejecting it the way
|
||||
the twins' `^[0-9]+$` check does; the value would then format via
|
||||
'{0:000}' to '-005' and yield a branch name starting with '-', which
|
||||
git refuses (refs cannot begin with a dash)."""
|
||||
project = _setup_project(tmp_path)
|
||||
result = _run_pwsh(
|
||||
"create-new-feature-branch.ps1", project,
|
||||
"-Json", "-DryRun", "-Number", "-5", "-ShortName", "neg", "Negative feature",
|
||||
)
|
||||
assert result.returncode != 0
|
||||
assert "--number must be a non-negative integer" in result.stderr
|
||||
|
||||
|
||||
# ── auto-commit.sh Tests ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -224,66 +224,6 @@ class TestInitIntegrationFlag:
|
||||
assert "Continuing without the optional preset" in normalized
|
||||
assert "Project ready" in normalized
|
||||
|
||||
def test_init_with_local_preset_seeds_manifest_constitution(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
from specify_cli.presets import PresetManager
|
||||
|
||||
monkeypatch.setattr(
|
||||
PresetManager,
|
||||
"_seed_constitution_from_preset",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
|
||||
preset_dir = tmp_path / "constitution-preset"
|
||||
(preset_dir / "organization").mkdir(parents=True)
|
||||
preset_content = "# Ratified Organization Constitution\n"
|
||||
(preset_dir / "organization" / "ratified.md").write_text(preset_content)
|
||||
(preset_dir / "preset.yml").write_text(
|
||||
yaml.safe_dump({
|
||||
"schema_version": "1.0",
|
||||
"preset": {
|
||||
"id": "constitution-preset",
|
||||
"name": "Constitution Preset",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides a ratified constitution",
|
||||
},
|
||||
"requires": {"speckit_version": ">=0.1.0"},
|
||||
"provides": {
|
||||
"templates": [{
|
||||
"type": "template",
|
||||
"name": "constitution-template",
|
||||
"file": "organization/ratified.md",
|
||||
"strategy": "replace",
|
||||
}]
|
||||
},
|
||||
})
|
||||
)
|
||||
project = tmp_path / "init-with-preset"
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
str(project),
|
||||
"--integration",
|
||||
"copilot",
|
||||
"--script",
|
||||
"sh",
|
||||
"--ignore-agent-tools",
|
||||
"--preset",
|
||||
str(preset_dir),
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert (
|
||||
project / ".specify" / "memory" / "constitution.md"
|
||||
).read_text() == preset_content
|
||||
|
||||
def test_integration_claude_here_preserves_preexisting_commands(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
"""Tests for GrokIntegration."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from specify_cli.integrations import get_integration
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
from .test_integration_base_skills import SkillsIntegrationTests
|
||||
|
||||
|
||||
class TestGrokIntegration(SkillsIntegrationTests):
|
||||
KEY = "grok"
|
||||
FOLDER = ".grok/"
|
||||
COMMANDS_SUBDIR = "skills"
|
||||
REGISTRAR_DIR = ".grok/skills"
|
||||
|
||||
def test_options_include_skills_flag(self):
|
||||
"""Not applicable — Grok Build is always skills-based."""
|
||||
pytest.skip("Grok Build is always skills-based and does not expose a --skills option")
|
||||
|
||||
def test_options_do_not_include_skills_flag(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i is not None
|
||||
opts = i.options()
|
||||
skills_opts = [o for o in opts if o.name == "--skills"]
|
||||
assert len(skills_opts) == 0
|
||||
|
||||
def test_requires_cli_is_true(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i is not None
|
||||
assert i.config["requires_cli"] is True
|
||||
assert i.config["name"] == "Grok Build"
|
||||
assert i.multi_install_safe is True
|
||||
|
||||
|
||||
class TestGrokInitFlow:
|
||||
"""--integration grok creates expected files."""
|
||||
|
||||
def test_integration_grok_creates_skills(self, tmp_path):
|
||||
"""--integration grok should create skills in .grok/skills."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "test-proj"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
str(target),
|
||||
"--integration",
|
||||
"grok",
|
||||
"--ignore-agent-tools",
|
||||
"--script",
|
||||
"sh",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, f"init --integration grok failed: {result.output}"
|
||||
assert (target / ".grok" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
assert (target / ".grok" / "skills" / "speckit-specify" / "SKILL.md").exists()
|
||||
|
||||
def test_plan_skill_has_no_context_placeholder(self, tmp_path):
|
||||
"""Core skills must not carry a context-file placeholder."""
|
||||
target = tmp_path / "test-proj"
|
||||
target.mkdir()
|
||||
|
||||
integration = get_integration("grok")
|
||||
manifest = IntegrationManifest("grok", target)
|
||||
integration.setup(target, manifest, script_type="sh")
|
||||
|
||||
plan_skill = target / ".grok" / "skills" / "speckit-plan" / "SKILL.md"
|
||||
content = plan_skill.read_text(encoding="utf-8")
|
||||
assert "__CONTEXT_FILE__" not in content
|
||||
|
||||
def test_build_exec_args_uses_headless_prompt_flag(self):
|
||||
integration = get_integration("grok")
|
||||
args = integration.build_exec_args("hello", model="grok-build", output_json=True)
|
||||
assert args is not None
|
||||
assert args[0] == "grok" or args[0].endswith("/grok")
|
||||
assert "-p" in args
|
||||
assert "hello" in args
|
||||
assert "--always-approve" in args
|
||||
assert "--model" in args
|
||||
assert "grok-build" in args
|
||||
assert "--output-format" in args
|
||||
assert "json" in args
|
||||
|
||||
|
||||
class TestGrokNextSteps:
|
||||
"""CLI output tests for Grok next-steps display."""
|
||||
|
||||
def test_init_next_steps_show_grok_skill_guidance(self, tmp_path):
|
||||
"""init --integration grok should guide users to .grok/skills and /speckit-*."""
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "grok-next-steps"
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
str(target),
|
||||
"--integration",
|
||||
"grok",
|
||||
"--ignore-agent-tools",
|
||||
"--script",
|
||||
"sh",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, f"init --integration grok failed: {result.output}"
|
||||
assert "Start Grok Build" in result.output, (
|
||||
f"Expected Grok start guidance in next steps but got:\n{result.output}"
|
||||
)
|
||||
assert ".grok/skills" in result.output, (
|
||||
f"Expected .grok/skills install path in next steps but got:\n{result.output}"
|
||||
)
|
||||
assert "/speckit-plan" in result.output, (
|
||||
f"Expected /speckit-plan in next steps but got:\n{result.output}"
|
||||
)
|
||||
assert "/speckit.plan" not in result.output, (
|
||||
f"Should not show /speckit.plan for Grok skills mode:\n{result.output}"
|
||||
)
|
||||
|
||||
|
||||
class TestGrokInitOptions:
|
||||
"""Init-options persistence for always-skills Grok."""
|
||||
|
||||
def test_init_persists_ai_skills_for_grok(self, tmp_path, monkeypatch):
|
||||
"""specify init --integration grok must persist ai_skills: true,
|
||||
so HookExecutor renders slash-skill invocations without manual
|
||||
init-options manipulation.
|
||||
"""
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from specify_cli import app
|
||||
from specify_cli.extensions import HookExecutor
|
||||
|
||||
project = tmp_path / "grok-init-test"
|
||||
project.mkdir()
|
||||
monkeypatch.chdir(project)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"init",
|
||||
"--here",
|
||||
"--integration",
|
||||
"grok",
|
||||
"--script",
|
||||
"sh",
|
||||
"--ignore-agent-tools",
|
||||
],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
|
||||
opts_path = project / ".specify" / "init-options.json"
|
||||
assert opts_path.exists()
|
||||
opts = json.loads(opts_path.read_text(encoding="utf-8"))
|
||||
assert opts.get("ai") == "grok"
|
||||
assert opts.get("ai_skills") is True, (
|
||||
f"init must persist ai_skills=true for Grok, got: {opts.get('ai_skills')}"
|
||||
)
|
||||
|
||||
hook_executor = HookExecutor(project)
|
||||
message = hook_executor.format_hook_message(
|
||||
"before_plan",
|
||||
[
|
||||
{
|
||||
"extension": "test-ext",
|
||||
"command": "speckit.plan",
|
||||
"optional": False,
|
||||
}
|
||||
],
|
||||
)
|
||||
assert "Executing: `/speckit-plan`" in message, (
|
||||
"Hook rendering must produce /speckit-plan for Grok without hint injection"
|
||||
)
|
||||
assert "EXECUTE_COMMAND_INVOCATION: /speckit-plan" in message
|
||||
@@ -1566,43 +1566,6 @@ class TestIntegrationUse:
|
||||
assert opts["integration"] == "codex"
|
||||
assert opts["ai"] == "codex"
|
||||
|
||||
def test_use_preserves_copilot_skills_mode(self, tmp_path):
|
||||
"""`use` on a skills-mode Copilot keeps ``ai_skills`` (issue #3550).
|
||||
|
||||
Re-selecting the same skills-mode Copilot must not drop ``ai_skills``
|
||||
from init-options.json nor regenerate extension commands in the legacy
|
||||
``.agent.md``/``.prompt.md`` layout.
|
||||
"""
|
||||
project = _init_project(tmp_path, "copilot", integration_options="--skills")
|
||||
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
|
||||
assert opts.get("ai_skills") is True, "precondition: init recorded skills mode"
|
||||
|
||||
result = _run_in_project(project, ["extension", "add", "git"])
|
||||
assert result.exit_code == 0, f"extension add failed: {result.output}"
|
||||
|
||||
# Simulate a fresh process: `use` in real life runs in its own process
|
||||
# where the registry's Copilot instance has _skills_mode == False (it is
|
||||
# only set during setup()). In-process test invocations otherwise reuse
|
||||
# the singleton left in skills mode by init, masking the bug (#3550).
|
||||
from specify_cli.integrations import get_integration
|
||||
|
||||
get_integration("copilot")._skills_mode = False
|
||||
|
||||
result = _run_in_project(project, ["integration", "use", "copilot"])
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
|
||||
assert opts.get("ai_skills") is True, "ai_skills must survive `use copilot`"
|
||||
|
||||
# No legacy command-layout files should be regenerated for the
|
||||
# skills-mode agent.
|
||||
assert not (project / ".github" / "agents" / "speckit.git.feature.agent.md").exists()
|
||||
assert not (project / ".github" / "prompts" / "speckit.git.feature.prompt.md").exists()
|
||||
assert (
|
||||
project / ".github" / "skills" / "speckit-git-feature" / "SKILL.md"
|
||||
).exists()
|
||||
|
||||
def test_use_requires_installed_integration(self, tmp_path):
|
||||
project = _init_project(tmp_path, "claude")
|
||||
old_cwd = os.getcwd()
|
||||
|
||||
@@ -141,8 +141,6 @@ class TestSlashSkillsSets:
|
||||
# ALWAYS_SLASH_AGENTS — unconditional on ai_skills
|
||||
("devin", True, "/speckit-plan"),
|
||||
("devin", False, "/speckit-plan"),
|
||||
("grok", True, "/speckit-plan"),
|
||||
("grok", False, "/speckit-plan"),
|
||||
("trae", True, "/speckit-plan"),
|
||||
("trae", False, "/speckit-plan"),
|
||||
("zed", True, "/speckit-plan"),
|
||||
|
||||
@@ -25,7 +25,6 @@ ISSUE_TEMPLATE_AGENT_KEYS = [
|
||||
"gemini",
|
||||
"copilot",
|
||||
"goose",
|
||||
"grok",
|
||||
"hermes",
|
||||
"bob",
|
||||
"junie",
|
||||
|
||||
@@ -970,113 +970,6 @@ class TestExtensionSkillRegistration:
|
||||
assert "Read agents/control" not in content
|
||||
assert "and knowledge-base/" not in content
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ai", "expected_invocation"),
|
||||
[
|
||||
("claude", "/speckit-plan"),
|
||||
("copilot", "/speckit-plan"),
|
||||
("codex", "$speckit-plan"),
|
||||
("kimi", "/skill:speckit-plan"),
|
||||
("zcode", "$speckit-plan"),
|
||||
],
|
||||
)
|
||||
def test_skill_registration_resolves_command_ref_tokens(
|
||||
self, project_dir, temp_dir, ai, expected_invocation
|
||||
):
|
||||
"""Auto-registered skills should resolve explicit command ref tokens."""
|
||||
_create_init_options(project_dir, ai=ai, ai_skills=True)
|
||||
skills_dir = _create_skills_dir(project_dir, ai=ai)
|
||||
|
||||
ext_dir = temp_dir / "command-ref-ext"
|
||||
ext_dir.mkdir()
|
||||
manifest_data = {
|
||||
"schema_version": "1.0",
|
||||
"extension": {
|
||||
"id": "command-ref-ext",
|
||||
"name": "Command Ref Extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
},
|
||||
"requires": {"speckit_version": ">=0.1.0"},
|
||||
"provides": {
|
||||
"commands": [
|
||||
{
|
||||
"name": "speckit.command-ref-ext.run",
|
||||
"file": "commands/run.md",
|
||||
"description": "Run command",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
with open(ext_dir / "extension.yml", "w") as f:
|
||||
yaml.safe_dump(manifest_data, f)
|
||||
|
||||
(ext_dir / "commands").mkdir()
|
||||
(ext_dir / "commands" / "run.md").write_text(
|
||||
"---\n"
|
||||
"description: Run command\n"
|
||||
"---\n\n"
|
||||
"Use __SPECKIT_COMMAND_PLAN__ before proceeding.\n"
|
||||
)
|
||||
|
||||
manager = ExtensionManager(project_dir)
|
||||
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
||||
|
||||
content = (skills_dir / "speckit-command-ref-ext-run" / "SKILL.md").read_text()
|
||||
assert "__SPECKIT_COMMAND_PLAN__" not in content
|
||||
assert expected_invocation in content
|
||||
|
||||
def test_skill_registration_does_not_rewrite_literal_speckit_text(
|
||||
self, project_dir, temp_dir
|
||||
):
|
||||
"""Auto-registered skills should leave literal speckit text untouched."""
|
||||
_create_init_options(project_dir, ai="codex", ai_skills=True)
|
||||
skills_dir = _create_skills_dir(project_dir, ai="codex")
|
||||
|
||||
ext_dir = temp_dir / "literal-ref-ext"
|
||||
ext_dir.mkdir()
|
||||
manifest_data = {
|
||||
"schema_version": "1.0",
|
||||
"extension": {
|
||||
"id": "literal-ref-ext",
|
||||
"name": "Literal Ref Extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Test",
|
||||
},
|
||||
"requires": {"speckit_version": ">=0.1.0"},
|
||||
"provides": {
|
||||
"commands": [
|
||||
{
|
||||
"name": "speckit.literal-ref-ext.run",
|
||||
"file": "commands/run.md",
|
||||
"description": "Run command",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
with open(ext_dir / "extension.yml", "w") as f:
|
||||
yaml.safe_dump(manifest_data, f)
|
||||
|
||||
(ext_dir / "commands").mkdir()
|
||||
(ext_dir / "commands" / "run.md").write_text(
|
||||
"---\n"
|
||||
"description: Run command\n"
|
||||
"---\n\n"
|
||||
"Literal slash form: /speckit.foo.bar\n"
|
||||
"Literal skill form: /speckit-plan\n"
|
||||
"Literal bare form: speckit.foo.bar\n"
|
||||
)
|
||||
|
||||
manager = ExtensionManager(project_dir)
|
||||
manager.install_from_directory(ext_dir, "0.1.0", register_commands=False)
|
||||
|
||||
content = (skills_dir / "speckit-literal-ref-ext-run" / "SKILL.md").read_text()
|
||||
assert "/speckit.foo.bar" in content
|
||||
assert "/speckit-plan" in content
|
||||
assert "speckit.foo.bar" in content
|
||||
assert "/speckit-foo-bar" not in content
|
||||
assert "$speckit-plan" not in content
|
||||
|
||||
def test_missing_command_file_skipped(self, skills_project, temp_dir):
|
||||
"""Commands with missing source files should be skipped gracefully."""
|
||||
project_dir, skills_dir = skills_project
|
||||
|
||||
@@ -2112,69 +2112,6 @@ class TestIfThenStep:
|
||||
errors = step.validate({"id": "test", "then": []})
|
||||
assert any("missing 'condition'" in e for e in errors)
|
||||
|
||||
@pytest.mark.parametrize("bad_branch", [{"id": "x"}, "oops", 5])
|
||||
def test_execute_non_list_then_fails_loudly(self, bad_branch):
|
||||
"""A non-list ``then`` must fail the step, not crash the run.
|
||||
|
||||
``validate`` rejects a non-list ``then``, but the engine does not
|
||||
auto-validate (see ``WorkflowEngine.load_workflow``) and feeds
|
||||
``next_steps`` straight into ``_execute_steps``, which iterates them as
|
||||
step mappings. Before the guard, a non-list ``then`` (a single mapping
|
||||
or scalar authoring mistake) was iterated element-wise and raised
|
||||
AttributeError on ``.get()``, taking down the whole run. Mirrors the
|
||||
switch/fan-out non-list handling.
|
||||
"""
|
||||
from specify_cli.workflows.steps.if_then import IfThenStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = IfThenStep()
|
||||
ctx = StepContext(inputs={})
|
||||
result = step.execute(
|
||||
{"id": "branch", "condition": "true", "then": bad_branch}, ctx
|
||||
)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert "'then' must be a list of steps" in (result.error or "")
|
||||
assert result.next_steps == []
|
||||
|
||||
@pytest.mark.parametrize("bad_branch", [{"id": "x"}, "oops", 5])
|
||||
def test_execute_non_list_else_fails_loudly(self, bad_branch):
|
||||
"""A non-list ``else`` selected at runtime must fail the step, not crash.
|
||||
|
||||
Same asymmetry as ``then``: the ``else`` branch is only reached when the
|
||||
condition is false, so a non-list ``else`` reaches ``next_steps`` and
|
||||
would crash the engine's step iteration on an unvalidated run.
|
||||
"""
|
||||
from specify_cli.workflows.steps.if_then import IfThenStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = IfThenStep()
|
||||
ctx = StepContext(inputs={})
|
||||
result = step.execute(
|
||||
{"id": "branch", "condition": "false", "then": [], "else": bad_branch},
|
||||
ctx,
|
||||
)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert "'else' must be a list of steps" in (result.error or "")
|
||||
assert result.next_steps == []
|
||||
|
||||
def test_execute_none_else_stays_empty(self):
|
||||
"""An explicit ``else: null`` selected at runtime stays an empty branch.
|
||||
|
||||
``validate`` deliberately accepts ``else: None``; the execute guard must
|
||||
normalize it to an empty branch (COMPLETED) rather than failing a
|
||||
validator-approved workflow when the condition is false.
|
||||
"""
|
||||
from specify_cli.workflows.steps.if_then import IfThenStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = IfThenStep()
|
||||
ctx = StepContext(inputs={})
|
||||
result = step.execute(
|
||||
{"id": "branch", "condition": "false", "then": [], "else": None}, ctx
|
||||
)
|
||||
assert result.status == StepStatus.COMPLETED
|
||||
assert result.next_steps == []
|
||||
|
||||
@pytest.mark.parametrize("bad_else", [False, 0, "", {}, 42])
|
||||
def test_validate_rejects_non_list_else(self, bad_else):
|
||||
"""A non-list 'else' must be rejected even when it is falsy.
|
||||
@@ -2308,91 +2245,6 @@ class TestSwitchStep:
|
||||
# expression is still evaluated, so its value is surfaced for context.
|
||||
assert result.output["expression_value"] == "approve"
|
||||
|
||||
@pytest.mark.parametrize("bad_branch", [{"id": "x"}, "oops", 5])
|
||||
def test_execute_non_list_matched_case_fails_loudly(self, bad_branch):
|
||||
"""A matched case with a non-list body must fail the step, not crash.
|
||||
|
||||
``validate`` rejects a non-list case body, but the engine does not
|
||||
auto-validate (see ``WorkflowEngine.load_workflow``) and feeds the
|
||||
selected branch straight into ``_execute_steps``, which iterates it as
|
||||
step mappings. A non-list body (a single mapping or scalar authoring
|
||||
mistake) would be iterated element-wise and raise AttributeError on
|
||||
``.get()``, taking down the whole run. Mirrors the non-mapping
|
||||
``cases`` guard.
|
||||
"""
|
||||
from specify_cli.workflows.steps.switch import SwitchStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = SwitchStep()
|
||||
ctx = StepContext(steps={"review": {"output": {"choice": "approve"}}})
|
||||
result = step.execute(
|
||||
{
|
||||
"id": "route",
|
||||
"expression": "{{ steps.review.output.choice }}",
|
||||
"cases": {"approve": bad_branch},
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert "case 'approve' must be a list of steps" in (result.error or "")
|
||||
assert result.next_steps == []
|
||||
# expression is still evaluated, so its value is surfaced for context.
|
||||
assert result.output["expression_value"] == "approve"
|
||||
|
||||
@pytest.mark.parametrize("bad_branch", [{"id": "x"}, "oops", 5])
|
||||
def test_execute_non_list_default_fails_loudly(self, bad_branch):
|
||||
"""A non-list ``default`` reached at runtime must fail, not crash.
|
||||
|
||||
Same asymmetry as the case body: ``default`` is only selected when no
|
||||
case matches, so a non-list ``default`` reaches ``next_steps`` and would
|
||||
crash the engine's step iteration on an unvalidated run.
|
||||
"""
|
||||
from specify_cli.workflows.steps.switch import SwitchStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = SwitchStep()
|
||||
ctx = StepContext(steps={"review": {"output": {"choice": "other"}}})
|
||||
result = step.execute(
|
||||
{
|
||||
"id": "route",
|
||||
"expression": "{{ steps.review.output.choice }}",
|
||||
"cases": {"approve": [{"id": "plan", "command": "speckit.plan"}]},
|
||||
"default": bad_branch,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert "'default' must be a list of steps" in (result.error or "")
|
||||
assert result.next_steps == []
|
||||
# expression is still evaluated, so its value is surfaced for context.
|
||||
assert result.output["expression_value"] == "other"
|
||||
|
||||
@pytest.mark.parametrize("ok_default", [None, [], [{"id": "x", "command": "/y"}]])
|
||||
def test_execute_none_default_stays_empty(self, ok_default):
|
||||
"""An explicit ``default: null`` or a list default stays valid.
|
||||
|
||||
``validate`` deliberately accepts ``default: None``; the execute guard
|
||||
must normalize it to an empty branch (COMPLETED) rather than failing a
|
||||
validator-approved workflow.
|
||||
"""
|
||||
from specify_cli.workflows.steps.switch import SwitchStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = SwitchStep()
|
||||
ctx = StepContext(steps={"review": {"output": {"choice": "other"}}})
|
||||
result = step.execute(
|
||||
{
|
||||
"id": "route",
|
||||
"expression": "{{ steps.review.output.choice }}",
|
||||
"cases": {"approve": [{"id": "plan", "command": "speckit.plan"}]},
|
||||
"default": ok_default,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
assert result.status == StepStatus.COMPLETED
|
||||
assert result.output["matched_case"] == "__default__"
|
||||
assert result.next_steps == (ok_default or [])
|
||||
|
||||
def test_validate_missing_expression(self):
|
||||
from specify_cli.workflows.steps.switch import SwitchStep
|
||||
|
||||
|
||||
Reference in New Issue
Block a user