mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 20:36:23 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e3ba4eb53 | ||
|
|
45423d6bc6 | ||
|
|
a86ee0e8b6 | ||
|
|
8c85919f0f | ||
|
|
3cfc81ff31 | ||
|
|
2344eafdd9 | ||
|
|
0a126256e0 | ||
|
|
2bd97543cc | ||
|
|
ac4f646144 | ||
|
|
e5a03bffc8 | ||
|
|
3c11f4d90b |
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**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Kiro CLI, Amp, SHAI, Tabnine CLI, Antigravity, IBM Bob, Mistral Vibe, Kimi Code, Trae, Pi Coding Agent, iFlow CLI, Devin for Terminal
|
||||
**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, iFlow CLI, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, opencode, Pi Coding Agent, Qoder CLI, Qwen Code, Roo Code, RovoDev ACLI, SHAI, Tabnine CLI, Trae, Windsurf, ZCode, Zed
|
||||
|
||||
- type: input
|
||||
id: agent-name
|
||||
|
||||
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -62,24 +62,41 @@ body:
|
||||
label: AI Agent
|
||||
description: Which AI agent are you using?
|
||||
options:
|
||||
- Amp
|
||||
- Antigravity
|
||||
- Auggie CLI
|
||||
- Claude Code
|
||||
- Cline
|
||||
- CodeBuddy
|
||||
- Codex CLI
|
||||
- Cursor
|
||||
- Devin for Terminal
|
||||
- Firebender
|
||||
- Forge
|
||||
- Gemini CLI
|
||||
- GitHub Copilot
|
||||
- Cursor
|
||||
- Qwen Code
|
||||
- opencode
|
||||
- Codex CLI
|
||||
- Windsurf
|
||||
- Kilo Code
|
||||
- Auggie CLI
|
||||
- Roo Code
|
||||
- CodeBuddy
|
||||
- Qoder CLI
|
||||
- Kiro CLI
|
||||
- Amp
|
||||
- SHAI
|
||||
- Goose
|
||||
- Hermes Agent
|
||||
- IBM Bob
|
||||
- Antigravity
|
||||
- iFlow CLI
|
||||
- Junie
|
||||
- Kilo Code
|
||||
- Kimi Code
|
||||
- Kiro CLI
|
||||
- Lingma
|
||||
- Mistral Vibe
|
||||
- opencode
|
||||
- Pi Coding Agent
|
||||
- Qoder CLI
|
||||
- Qwen Code
|
||||
- Roo Code
|
||||
- RovoDev ACLI
|
||||
- SHAI
|
||||
- Tabnine CLI
|
||||
- Trae
|
||||
- Windsurf
|
||||
- ZCode
|
||||
- Zed
|
||||
- Not applicable
|
||||
validations:
|
||||
required: true
|
||||
|
||||
45
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
45
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -56,24 +56,41 @@ body:
|
||||
description: Does this feature relate to a specific AI agent?
|
||||
options:
|
||||
- All agents
|
||||
- Amp
|
||||
- Antigravity
|
||||
- Auggie CLI
|
||||
- Claude Code
|
||||
- Cline
|
||||
- CodeBuddy
|
||||
- Codex CLI
|
||||
- Cursor
|
||||
- Devin for Terminal
|
||||
- Firebender
|
||||
- Forge
|
||||
- Gemini CLI
|
||||
- GitHub Copilot
|
||||
- Cursor
|
||||
- Qwen Code
|
||||
- opencode
|
||||
- Codex CLI
|
||||
- Windsurf
|
||||
- Kilo Code
|
||||
- Auggie CLI
|
||||
- Roo Code
|
||||
- CodeBuddy
|
||||
- Qoder CLI
|
||||
- Kiro CLI
|
||||
- Amp
|
||||
- SHAI
|
||||
- Goose
|
||||
- Hermes Agent
|
||||
- IBM Bob
|
||||
- Antigravity
|
||||
- iFlow CLI
|
||||
- Junie
|
||||
- Kilo Code
|
||||
- Kimi Code
|
||||
- Kiro CLI
|
||||
- Lingma
|
||||
- Mistral Vibe
|
||||
- opencode
|
||||
- Pi Coding Agent
|
||||
- Qoder CLI
|
||||
- Qwen Code
|
||||
- Roo Code
|
||||
- RovoDev ACLI
|
||||
- SHAI
|
||||
- Tabnine CLI
|
||||
- Trae
|
||||
- Windsurf
|
||||
- ZCode
|
||||
- Zed
|
||||
- Not applicable
|
||||
|
||||
- type: textarea
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -2,6 +2,36 @@
|
||||
|
||||
<!-- insert new changelog below this comment -->
|
||||
|
||||
## [0.11.6] - 2026-06-23
|
||||
|
||||
### Changed
|
||||
|
||||
- [extension] Update Spec Kit Preview extension to v1.1.0 and sync Firebender agent lists (#3116)
|
||||
- Add Spec Kit Discovery Extension to community catalog (#3119)
|
||||
- Update Architecture Workflow extension to v1.2.1 (#3118)
|
||||
- docs: clarify project-defined constitution articles (#2994)
|
||||
- Add Intake extension to community catalog (#3117)
|
||||
- feat: add Firebender integration (Android Studio / IntelliJ) (#3077)
|
||||
- Update DocGuard — CDD Enforcement extension to v0.28.0 (#3115)
|
||||
- chore: sync issue template agent lists (#3052)
|
||||
- fix(shared-infra): remove stale managed scripts the core no longer ships (#3076) (#3098)
|
||||
- chore: release 0.11.5, begin 0.11.6.dev0 development (#3105)
|
||||
|
||||
## [0.11.5] - 2026-06-22
|
||||
|
||||
### Changed
|
||||
|
||||
- fix: register enabled extensions for agent on integration use/upgrade (#2949)
|
||||
- Add SicarioSpec Core preset to community catalog (#3102)
|
||||
- Update Game Narrative Writing preset to v1.1.0 (#3099)
|
||||
- feat: add PyPI publishing workflow and readme metadata (#2915)
|
||||
- refactor: move extension command handlers to extensions/_commands.py (PR-7/8) (#3014)
|
||||
- feat: add ZCode (Z.AI) integration (#3063)
|
||||
- fix(agent-context): support multiple context files safely (#2969)
|
||||
- Update DocGuard — CDD Enforcement extension to v0.27.0 (#3094)
|
||||
- fix(presets): use _repo_root() for bundled-core source-checkout fallback (#3086) (#3091)
|
||||
- chore: release 0.11.4, begin 0.11.5.dev0 development (#3092)
|
||||
|
||||
## [0.11.4] - 2026-06-22
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -167,7 +167,7 @@ the command templates in templates/commands/ to understand what each command
|
||||
invokes. Use these mapping rules:
|
||||
|
||||
- templates/commands/X.md → the command it defines
|
||||
- scripts/bash/Y.sh or scripts/powershell/Y.ps1 → every command that invokes that script (grep templates/commands/ for the script name). Also check transitive dependencies: if the changed script is sourced by other scripts (e.g., common.sh is sourced by create-new-feature.sh, check-prerequisites.sh, setup-plan.sh, update-agent-context.sh), then every command invoking those downstream scripts is also affected
|
||||
- scripts/bash/Y.sh or scripts/powershell/Y.ps1 → every command that invokes that script (grep templates/commands/ for the script name). Also check transitive dependencies: if the changed script is sourced by other scripts (e.g., common.sh is sourced by create-new-feature.sh, check-prerequisites.sh, setup-plan.sh), then every command invoking those downstream scripts is also affected
|
||||
- templates/Z-template.md → every command that consumes that template during execution
|
||||
- src/specify_cli/*.py → CLI commands (`specify init`, `specify check`, `specify extension *`, `specify preset *`); test the affected CLI command and, for init/scaffolding changes, at minimum test /speckit.specify
|
||||
- extensions/X/commands/* → the extension command it defines
|
||||
|
||||
@@ -31,7 +31,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| API Evolve | Managed API contract evolution — breaking-change detection, semver enforcement, deprecation orchestration, and lifecycle gates across REST, GraphQL, and gRPC | `process` | Read+Write | [spec-kit-api-evolve](https://github.com/Quratulain-bilal/spec-kit-api-evolve) |
|
||||
| Architect Impact Previewer | Predicts architectural impact, complexity, and risks of proposed changes before implementation. | `visibility` | Read-only | [spec-kit-architect-preview](https://github.com/UmmeHabiba1312/spec-kit-architect-preview) |
|
||||
| Architecture Guard | Framework-agnostic architecture review extension for validating implementation against governance and architecture constitutions, detecting architectural drift, and generating non-blocking refactor tasks | `process` | Read+Write | [spec-kit-architecture-guard](https://github.com/DyanGalih/spec-kit-architecture-guard) |
|
||||
| Architecture Workflow | Generate or reverse project-level 4+1 architecture view artifacts and synthesis | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
|
||||
| Architecture Workflow | Generate or reverse project-level 4+1 architecture views as separate commands | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
|
||||
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
|
||||
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
||||
| Blueprint | Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs | `docs` | Read+Write | [spec-kit-blueprint](https://github.com/chordpli/spec-kit-blueprint) |
|
||||
@@ -57,7 +57,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| GitHub Issues Integration 1 | Generate spec artifacts from GitHub Issues - import issues, sync updates, and maintain bidirectional traceability | `integration` | Read+Write | [spec-kit-github-issues](https://github.com/Fatima367/spec-kit-github-issues) |
|
||||
| GitHub Issues Integration 2 | Creates and syncs local specs from an existing GitHub issue | `integration` | Read+Write | [spec-kit-issue](https://github.com/aaronrsun/spec-kit-issue) |
|
||||
| Improve Extension | Audits any codebase as a senior advisor and writes prioritized, self-contained spec prompts under specs/ that the spec-kit lifecycle can process | `process` | Read+Write | [spec-kit-improve](https://github.com/d0whc3r/spec-kit-improve) |
|
||||
| Interactive HTML Preview | Generate self-contained interactive HTML prototypes from Spec Kit artifacts | `docs` | Read+Write | [spec-kit-preview](https://github.com/bigsmartben/spec-kit-preview) |
|
||||
| Intake | Normalize PRD, design, and test-case evidence into SDD-ready intake artifacts | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) |
|
||||
| Intelligent Agent Orchestrator | Cross-catalog agent discovery and intelligent prompt-to-command routing | `process` | Read+Write | [spec-kit-orchestrator](https://github.com/pragya247/spec-kit-orchestrator) |
|
||||
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
|
||||
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
||||
@@ -110,6 +110,8 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Spec Changelog | Auto-generate changelogs and release notes from spec git history and requirement diffs | `docs` | Read-only | [spec-kit-changelog](https://github.com/Quratulain-bilal/spec-kit-changelog) |
|
||||
| Spec Critique Extension | Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives | `docs` | Read-only | [spec-kit-critique](https://github.com/arunt14/spec-kit-critique) |
|
||||
| Spec Diagram | Auto-generate Mermaid diagrams of SDD workflow state, feature progress, and task dependencies | `visibility` | Read-only | [spec-kit-diagram-](https://github.com/Quratulain-bilal/spec-kit-diagram-) |
|
||||
| Spec Kit Discovery Extension | Run technical discovery commands for feasibility, technology selection, scenario-specific technical decisions, legacy codebase assessment, implementation understanding, and proof-of-concept validation | `process` | Read+Write | [spec-kit-discovery](https://github.com/bigsmartben/spec-kit-discovery) |
|
||||
| Spec Kit Preview | Generate evidence-backed low, mid, or high fidelity previews from Spec Kit artifacts as Markdown or self-contained HTML | `docs` | Read+Write | [spec-kit-preview](https://github.com/bigsmartben/spec-kit-preview) |
|
||||
| Spec Kit Schedule | Optimal multi-agent task scheduling via CP-SAT — DAG precedence, hallucination-aware caps, file-conflict avoidance, stochastic durations, replanning, and interactive HTML output | `process` | Read+Write | [spec-kit-schedule](https://github.com/jfranc38/spec-kit-schedule) |
|
||||
| Spec Kit TLDR | Render a feature's spec.md / plan.md into a review-oriented TLDR (self-contained HTML dashboard + PR-native Markdown) that surfaces risks for faster PR review. | `visibility` | Read+Write | [speckit-tldr](https://github.com/qurore/speckit-tldr) |
|
||||
| Spec Orchestrator | Cross-feature orchestration — track state, select tasks, and detect conflicts across parallel specs | `process` | Read-only | [spec-kit-orchestrator](https://github.com/Quratulain-bilal/spec-kit-orchestrator) |
|
||||
|
||||
@@ -15,6 +15,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
|
||||
| [Codex CLI](https://github.com/openai/codex) | `codex` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `$speckit-<command>` |
|
||||
| [Cursor](https://cursor.sh/) | `cursor-agent` | |
|
||||
| [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-<command>` |
|
||||
| [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ |
|
||||
| [Forge](https://forgecode.dev/) | `forge` | |
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |
|
||||
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | |
|
||||
@@ -185,6 +186,7 @@ The currently declared multi-install safe integrations are:
|
||||
| `codebuddy` | `.codebuddy/commands`, `CODEBUDDY.md` |
|
||||
| `codex` | `.agents/skills`, `AGENTS.md` |
|
||||
| `cursor-agent` | `.cursor/skills`, `.cursor/rules/specify-rules.mdc` |
|
||||
| `firebender` | `.firebender/commands`, `.firebender/rules/specify-rules.mdc` |
|
||||
| `gemini` | `.gemini/commands`, `GEMINI.md` |
|
||||
| `iflow` | `.iflow/commands`, `IFLOW.md` |
|
||||
| `junie` | `.junie/commands`, `.junie/AGENTS.md` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-06-22T00:00:00Z",
|
||||
"updated_at": "2026-06-23T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||
"extensions": {
|
||||
"aide": {
|
||||
@@ -187,10 +187,10 @@
|
||||
"arch": {
|
||||
"name": "Architecture Workflow",
|
||||
"id": "arch",
|
||||
"description": "Generate or reverse project-level 4+1 architecture view artifacts and synthesis",
|
||||
"description": "Generate or reverse project-level 4+1 architecture views as separate commands",
|
||||
"author": "bigsmartben",
|
||||
"version": "1.1.0",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.1.0.zip",
|
||||
"version": "1.2.1",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.2.1.zip",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-arch",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-arch",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-arch/blob/main/README.md",
|
||||
@@ -202,7 +202,7 @@
|
||||
"speckit_version": ">=0.8.10.dev0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 2,
|
||||
"commands": 10,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
@@ -215,7 +215,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-05-14T00:00:00Z",
|
||||
"updated_at": "2026-05-15T00:00:00Z"
|
||||
"updated_at": "2026-06-23T00:00:00Z"
|
||||
},
|
||||
"architect-preview": {
|
||||
"name": "Architect Impact Previewer",
|
||||
@@ -1001,13 +1001,47 @@
|
||||
"created_at": "2026-04-08T00:00:00Z",
|
||||
"updated_at": "2026-04-08T00:00:00Z"
|
||||
},
|
||||
"discovery": {
|
||||
"name": "Spec Kit Discovery Extension",
|
||||
"id": "discovery",
|
||||
"description": "Run technical discovery commands for feasibility, technology selection, scenario-specific technical decisions, legacy codebase assessment, implementation understanding, and proof-of-concept validation.",
|
||||
"author": "bigsmartben",
|
||||
"version": "0.2.0",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-discovery/archive/refs/tags/v0.2.0.zip",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-discovery",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-discovery",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-discovery/blob/main/docs/usage.md",
|
||||
"changelog": "https://github.com/bigsmartben/spec-kit-discovery/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 6,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"discovery",
|
||||
"workflow",
|
||||
"validation",
|
||||
"feasibility",
|
||||
"decision"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-23T00:00:00Z",
|
||||
"updated_at": "2026-06-23T00:00:00Z"
|
||||
},
|
||||
"docguard": {
|
||||
"name": "DocGuard — CDD Enforcement",
|
||||
"id": "docguard",
|
||||
"description": "Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. One pinned runtime dependency; pure Node.js otherwise.",
|
||||
"author": "raccioly",
|
||||
"version": "0.27.0",
|
||||
"download_url": "https://github.com/raccioly/docguard/releases/download/v0.27.0/spec-kit-docguard-v0.27.0.zip",
|
||||
"version": "0.28.0",
|
||||
"download_url": "https://github.com/raccioly/docguard/releases/download/v0.28.0/spec-kit-docguard-v0.28.0.zip",
|
||||
"repository": "https://github.com/raccioly/docguard",
|
||||
"homepage": "https://www.npmjs.com/package/docguard-cli",
|
||||
"documentation": "https://github.com/raccioly/docguard/blob/main/extensions/spec-kit-docguard/README.md",
|
||||
@@ -1043,7 +1077,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-03-13T00:00:00Z",
|
||||
"updated_at": "2026-06-22T00:00:00Z"
|
||||
"updated_at": "2026-06-23T00:00:00Z"
|
||||
},
|
||||
"doctor": {
|
||||
"name": "Project Health Check",
|
||||
@@ -1370,6 +1404,46 @@
|
||||
"created_at": "2026-06-16T00:00:00Z",
|
||||
"updated_at": "2026-06-16T00:00:00Z"
|
||||
},
|
||||
"intake": {
|
||||
"name": "Intake",
|
||||
"id": "intake",
|
||||
"description": "Normalize PRD, design, and test-case evidence into SDD-ready intake artifacts.",
|
||||
"author": "bigsmartben",
|
||||
"version": "0.1.2",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-intake/archive/refs/tags/v0.1.2.zip",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-intake",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-intake",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-intake/blob/main/README.md",
|
||||
"changelog": "https://github.com/bigsmartben/spec-kit-intake/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "docs",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.10.dev0",
|
||||
"tools": [
|
||||
{
|
||||
"name": "figma-mcp",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"intake",
|
||||
"sdd",
|
||||
"requirements",
|
||||
"validation",
|
||||
"figma"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-23T00:00:00Z",
|
||||
"updated_at": "2026-06-23T00:00:00Z"
|
||||
},
|
||||
"issue": {
|
||||
"name": "GitHub Issues Integration 2",
|
||||
"id": "issue",
|
||||
@@ -2347,12 +2421,12 @@
|
||||
"updated_at": "2026-03-18T00:00:00Z"
|
||||
},
|
||||
"preview": {
|
||||
"name": "Interactive HTML Preview",
|
||||
"name": "Spec Kit Preview",
|
||||
"id": "preview",
|
||||
"description": "Generate self-contained interactive HTML prototypes from Spec Kit artifacts",
|
||||
"description": "Generate evidence-backed low, mid, or high fidelity previews from Spec Kit artifacts as Markdown or self-contained HTML",
|
||||
"author": "bigsmartben",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-preview/archive/refs/tags/v1.0.0.zip",
|
||||
"version": "1.1.0",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-preview/archive/refs/tags/v1.1.0.zip",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-preview",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-preview",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-preview/blob/main/README.md",
|
||||
@@ -2364,20 +2438,21 @@
|
||||
"speckit_version": ">=0.8.10.dev0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"commands": 6,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"preview",
|
||||
"prototype",
|
||||
"html",
|
||||
"markdown",
|
||||
"ux"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-05-15T00:00:00Z",
|
||||
"updated_at": "2026-05-15T00:00:00Z"
|
||||
"updated_at": "2026-06-23T00:00:00Z"
|
||||
},
|
||||
"product": {
|
||||
"name": "Product Spec Extension",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-06-02T00:00:00Z",
|
||||
"updated_at": "2026-06-22T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
|
||||
"integrations": {
|
||||
"claude": {
|
||||
@@ -102,6 +102,15 @@
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["cli"]
|
||||
},
|
||||
"firebender": {
|
||||
"id": "firebender",
|
||||
"name": "Firebender",
|
||||
"version": "1.0.0",
|
||||
"description": "Firebender IDE integration for Android Studio / IntelliJ",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["ide"]
|
||||
},
|
||||
"forge": {
|
||||
"id": "forge",
|
||||
"name": "Forge",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "specify-cli"
|
||||
version = "0.11.5.dev0"
|
||||
version = "0.11.6"
|
||||
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"
|
||||
|
||||
@@ -318,6 +318,12 @@ No implementation code shall be written before:
|
||||
|
||||
This completely inverts traditional AI code generation. Instead of generating code and hoping it works, the LLM must first generate comprehensive tests that define behavior, get them approved, and only then generate implementation.
|
||||
|
||||
#### Articles IV, V & VI: Project-Defined Governance
|
||||
|
||||
Articles IV, V, and VI are intentionally defined by each project's constitution rather than prescribed by Spec Kit. The constitution template provides placeholder slots and example concerns such as integration testing, observability, versioning, and breaking changes, but teams replace those placeholders with the principles that match their system and organization.
|
||||
|
||||
This keeps the nine-article structure stable while allowing each project to encode its own non-negotiable standards. For one project, Article IV might govern security and access boundaries; for another, it might define integration test requirements. The `/speckit.analyze` command evaluates the concrete constitution in the project, so these project-defined articles participate in compliance checks just like the built-in examples.
|
||||
|
||||
#### Articles VII & VIII: Simplicity and Anti-Abstraction
|
||||
|
||||
These paired articles combat over-engineering:
|
||||
|
||||
@@ -58,6 +58,7 @@ def _register_builtins() -> None:
|
||||
from .copilot import CopilotIntegration
|
||||
from .cursor_agent import CursorAgentIntegration
|
||||
from .devin import DevinIntegration
|
||||
from .firebender import FirebenderIntegration
|
||||
from .forge import ForgeIntegration
|
||||
from .gemini import GeminiIntegration
|
||||
from .generic import GenericIntegration
|
||||
@@ -95,6 +96,7 @@ def _register_builtins() -> None:
|
||||
_register(CopilotIntegration())
|
||||
_register(CursorAgentIntegration())
|
||||
_register(DevinIntegration())
|
||||
_register(FirebenderIntegration())
|
||||
_register(ForgeIntegration())
|
||||
_register(GeminiIntegration())
|
||||
_register(GenericIntegration())
|
||||
|
||||
33
src/specify_cli/integrations/firebender/__init__.py
Normal file
33
src/specify_cli/integrations/firebender/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Firebender IDE integration.
|
||||
|
||||
Firebender (https://firebender.com/) is an AI coding agent for Android Studio
|
||||
and IntelliJ. It reads project-local custom slash commands from
|
||||
``.firebender/commands/*.mdc`` and project rules from ``.firebender/rules/*.mdc``,
|
||||
so Spec Kit installs its command templates as ``.mdc`` command files and writes
|
||||
the managed context section into a ``.firebender/rules/`` rule file.
|
||||
"""
|
||||
|
||||
from ..base import MarkdownIntegration
|
||||
|
||||
|
||||
class FirebenderIntegration(MarkdownIntegration):
|
||||
key = "firebender"
|
||||
config = {
|
||||
"name": "Firebender",
|
||||
"folder": ".firebender/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": "https://firebender.com/",
|
||||
"requires_cli": False,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".firebender/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".mdc",
|
||||
}
|
||||
context_file = ".firebender/rules/specify-rules.mdc"
|
||||
multi_install_safe = True
|
||||
|
||||
def command_filename(self, template_name: str) -> str:
|
||||
"""Firebender reads custom slash commands from ``.firebender/commands/*.mdc``."""
|
||||
return f"speckit.{template_name}.mdc"
|
||||
@@ -232,6 +232,30 @@ class IntegrationManifest:
|
||||
# transition. ``discard`` is a no-op when the key is absent.
|
||||
self._recovered_files.discard(normalized)
|
||||
|
||||
def remove(self, rel_path: str | Path) -> bool:
|
||||
"""Drop *rel_path* from the tracked file set and any recovered marker.
|
||||
|
||||
Operates purely on the manifest's recorded key; it does NOT touch the
|
||||
file on disk. Returns ``True`` if an entry was present and removed.
|
||||
Used to keep the manifest consistent after a caller deletes a stale
|
||||
managed file that the current install no longer ships.
|
||||
|
||||
Input is normalized through the same lexical pipeline as
|
||||
``record_existing`` / ``is_recovered``: absolute paths and paths
|
||||
containing ``..`` segments are rejected (return ``False``) — such paths
|
||||
can never be canonical manifest keys, so there is nothing to remove.
|
||||
"""
|
||||
rel = Path(rel_path)
|
||||
if rel.is_absolute() or ".." in rel.parts:
|
||||
return False
|
||||
try:
|
||||
abs_path = _validate_rel_path(rel, self.project_root)
|
||||
normalized = abs_path.relative_to(self.project_root).as_posix()
|
||||
except ValueError:
|
||||
return False
|
||||
self._recovered_files.discard(normalized)
|
||||
return self._files.pop(normalized, None) is not None
|
||||
|
||||
# -- Querying ---------------------------------------------------------
|
||||
|
||||
@property
|
||||
|
||||
@@ -304,7 +304,7 @@ def install_shared_infra(
|
||||
customization warning to tell the user which flag would overwrite their
|
||||
customizations.
|
||||
"""
|
||||
from .integrations.manifest import _sha256
|
||||
from .integrations.manifest import _sha256, _validate_rel_path
|
||||
|
||||
manifest = load_speckit_manifest(project_path, version=version, console=console)
|
||||
prior_hashes = dict(manifest.files)
|
||||
@@ -325,6 +325,11 @@ def install_shared_infra(
|
||||
symlinked_files: list[str] = []
|
||||
planned_copies: list[tuple[Path, str, bytes, int]] = []
|
||||
planned_templates: list[tuple[Path, str, str]] = []
|
||||
# Track every shared path the current bundle produces so we can detect
|
||||
# manifest entries the core no longer ships (stale-script cleanup, #3076).
|
||||
seen_rels: set[str] = set()
|
||||
scripts_scanned = False
|
||||
variant_dir = "bash" if script_type == "sh" else "powershell"
|
||||
|
||||
def _decide_overwrite(rel: str, dst: Path) -> tuple[bool, str | None]:
|
||||
"""Return (write, bucket) where bucket is 'skip', 'preserved', or None."""
|
||||
@@ -379,7 +384,6 @@ def install_shared_infra(
|
||||
if scripts_src.is_dir():
|
||||
dest_scripts = project_path / ".specify" / "scripts"
|
||||
if _ensure_or_bucket_dir(dest_scripts):
|
||||
variant_dir = "bash" if script_type == "sh" else "powershell"
|
||||
variant_src = scripts_src / variant_dir
|
||||
if variant_src.is_dir():
|
||||
dest_variant = dest_scripts / variant_dir
|
||||
@@ -387,10 +391,18 @@ def install_shared_infra(
|
||||
for src_path in variant_src.rglob("*"):
|
||||
if not src_path.is_file():
|
||||
continue
|
||||
# Mark scanned only once a real source file is seen. An
|
||||
# empty (or symlink-skipped) variant keeps this False, so
|
||||
# stale-cleanup is skipped — otherwise it would treat every
|
||||
# tracked script as obsolete and delete it. (The safety
|
||||
# hinge is this flag, not ``seen_rels``, which also holds
|
||||
# template paths populated later.)
|
||||
scripts_scanned = True
|
||||
|
||||
rel_path = src_path.relative_to(variant_src)
|
||||
dst_path = dest_variant / rel_path
|
||||
rel = dst_path.relative_to(project_path).as_posix()
|
||||
seen_rels.add(rel)
|
||||
if not _safe_dest_or_bucket(dst_path, rel, parent_must_exist=False):
|
||||
continue
|
||||
write, bucket = _decide_overwrite(rel, dst_path)
|
||||
@@ -442,6 +454,7 @@ def install_shared_infra(
|
||||
|
||||
dst = dest_templates / src.name
|
||||
rel = dst.relative_to(project_path).as_posix()
|
||||
seen_rels.add(rel)
|
||||
if not _safe_dest_or_bucket(dst, rel):
|
||||
continue
|
||||
write, bucket = _decide_overwrite(rel, dst)
|
||||
@@ -521,5 +534,63 @@ def install_shared_infra(
|
||||
if refresh_hint:
|
||||
console.print(refresh_hint)
|
||||
|
||||
# Remove stale managed scripts: paths a previous install recorded that the
|
||||
# current core no longer ships — e.g. the legacy
|
||||
# ``scripts/<variant>/update-agent-context.sh`` superseded by the bundled
|
||||
# agent-context extension. Left behind, such an orphan can crash when it
|
||||
# sources a refreshed ``common.sh`` (#3076). Only run when the script source
|
||||
# was actually scanned (so a missing/empty source never triggers mass
|
||||
# deletion), scoped to the active variant, and only for *managed* copies —
|
||||
# a user-customized file (hash diverges), a symlink, or a recovered entry is
|
||||
# preserved by ``_is_managed``.
|
||||
if scripts_scanned:
|
||||
stale_removed: list[str] = []
|
||||
script_prefix = f".specify/scripts/{variant_dir}/"
|
||||
for rel in list(prior_hashes):
|
||||
if rel in seen_rels or not rel.startswith(script_prefix):
|
||||
continue
|
||||
# Guard corrupted/hand-edited manifest keys BEFORE any filesystem
|
||||
# access: absolute, ``..``, or (on Windows) drive-relative keys such
|
||||
# as ``C:tmp`` are not ``is_absolute()`` yet discard the project root
|
||||
# when joined. The lexical check is a fast reject; ``_validate_rel_path``
|
||||
# resolves the join and confirms containment, catching the rest. A key
|
||||
# that still escapes is *skipped*, never turned into an install-time
|
||||
# hard failure. Mirrors IntegrationManifest.is_recovered / remove.
|
||||
rel_path = Path(rel)
|
||||
if rel_path.is_absolute() or ".." in rel_path.parts:
|
||||
continue
|
||||
try:
|
||||
_validate_rel_path(rel_path, project_path)
|
||||
except ValueError:
|
||||
continue
|
||||
dst = project_path / rel_path
|
||||
# Already gone from disk but still tracked: drop the orphaned manifest
|
||||
# entry so the manifest stays consistent (nothing to unlink).
|
||||
if not dst.exists() and not dst.is_symlink():
|
||||
manifest.remove(rel)
|
||||
continue
|
||||
if not _is_managed(rel, dst):
|
||||
continue # user-modified / symlink / recovered → preserve
|
||||
# Never unlink through a symlinked ancestor (writes/deletes could
|
||||
# escape the project root). The safe-destination check buckets such
|
||||
# paths under ``symlinked_files`` and we leave them in place.
|
||||
if not _safe_dest_or_bucket(dst, rel):
|
||||
continue
|
||||
try:
|
||||
dst.unlink()
|
||||
except OSError as exc:
|
||||
console.print(f"[yellow]⚠[/yellow] could not remove stale {rel}: {exc}")
|
||||
continue
|
||||
manifest.remove(rel)
|
||||
stale_removed.append(rel)
|
||||
|
||||
if stale_removed:
|
||||
console.print(
|
||||
f"[yellow]⚠[/yellow] Removed {len(stale_removed)} obsolete shared "
|
||||
"script(s) left by a previous install:"
|
||||
)
|
||||
for path in stale_removed:
|
||||
console.print(f" {path}")
|
||||
|
||||
manifest.save()
|
||||
return True
|
||||
|
||||
@@ -263,6 +263,206 @@ class TestInitIntegrationFlag:
|
||||
assert (scripts_dir / "setup-plan.sh").exists()
|
||||
assert (templates_dir / "plan-template.md").exists()
|
||||
|
||||
def test_shared_infra_removes_stale_managed_script(self, tmp_path):
|
||||
"""A managed script the core no longer ships (e.g. the legacy
|
||||
update-agent-context.sh, superseded by the agent-context extension) is
|
||||
removed, and the manifest stops tracking it (#3076)."""
|
||||
from specify_cli import _install_shared_infra
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
project = tmp_path / "stale-test"
|
||||
project.mkdir()
|
||||
(project / ".specify").mkdir()
|
||||
scripts_dir = project / ".specify" / "scripts" / "bash"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
|
||||
# Legacy orphan the current bundle no longer ships, recorded in the
|
||||
# manifest as a managed file (hash matches on disk) — a pre-refactor install.
|
||||
stale_rel = ".specify/scripts/bash/update-agent-context.sh"
|
||||
(scripts_dir / "update-agent-context.sh").write_text("# legacy orphan\n", encoding="utf-8")
|
||||
manifest = IntegrationManifest("speckit", project, version="test")
|
||||
manifest.record_existing(stale_rel)
|
||||
manifest.save()
|
||||
|
||||
_install_shared_infra(project, "sh", force=False)
|
||||
|
||||
# The orphan is gone and the manifest no longer tracks it.
|
||||
assert not (scripts_dir / "update-agent-context.sh").exists()
|
||||
refreshed = IntegrationManifest.load("speckit", project)
|
||||
assert stale_rel not in refreshed.files
|
||||
# Scripts the core DOES ship are installed and tracked.
|
||||
assert (scripts_dir / "common.sh").exists()
|
||||
assert ".specify/scripts/bash/common.sh" in refreshed.files
|
||||
|
||||
def test_shared_infra_preserves_modified_stale_script(self, tmp_path):
|
||||
"""A user-modified stale script is preserved (hash diverges from the
|
||||
managed baseline), never silently deleted (#3076)."""
|
||||
from specify_cli import _install_shared_infra
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
project = tmp_path / "stale-modified"
|
||||
project.mkdir()
|
||||
(project / ".specify").mkdir()
|
||||
scripts_dir = project / ".specify" / "scripts" / "bash"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
|
||||
stale = scripts_dir / "update-agent-context.sh"
|
||||
stale.write_text("# original managed\n", encoding="utf-8")
|
||||
manifest = IntegrationManifest("speckit", project, version="test")
|
||||
manifest.record_existing(".specify/scripts/bash/update-agent-context.sh")
|
||||
manifest.save()
|
||||
|
||||
# User customizes it after install → on-disk hash now diverges.
|
||||
stale.write_text("# user customization\n", encoding="utf-8")
|
||||
|
||||
_install_shared_infra(project, "sh", force=False)
|
||||
|
||||
# Preserved: it is no longer a managed (hash-matching) copy.
|
||||
assert stale.exists()
|
||||
assert stale.read_text(encoding="utf-8") == "# user customization\n"
|
||||
|
||||
def test_shared_infra_prunes_orphan_manifest_entry_when_file_absent(self, tmp_path):
|
||||
"""A stale manifest entry whose file is already gone from disk is pruned
|
||||
so the manifest stays consistent, not left tracked forever (#3076 review)."""
|
||||
from specify_cli import _install_shared_infra
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
project = tmp_path / "orphan-entry"
|
||||
project.mkdir()
|
||||
(project / ".specify").mkdir()
|
||||
scripts_dir = project / ".specify" / "scripts" / "bash"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
|
||||
stale_rel = ".specify/scripts/bash/update-agent-context.sh"
|
||||
stale = scripts_dir / "update-agent-context.sh"
|
||||
stale.write_text("# legacy orphan\n", encoding="utf-8")
|
||||
manifest = IntegrationManifest("speckit", project, version="test")
|
||||
manifest.record_existing(stale_rel)
|
||||
manifest.save()
|
||||
# File removed out of band, but the manifest still tracks it.
|
||||
stale.unlink()
|
||||
|
||||
_install_shared_infra(project, "sh", force=False)
|
||||
|
||||
refreshed = IntegrationManifest.load("speckit", project)
|
||||
assert stale_rel not in refreshed.files
|
||||
|
||||
def test_shared_infra_empty_script_source_keeps_tracked_scripts(self, tmp_path, monkeypatch):
|
||||
"""If the bundle's script source dir exists but is empty, stale-cleanup
|
||||
must NOT run (no source files seen → can't tell what's obsolete): a
|
||||
previously-tracked script is preserved, never mass-deleted (#3076 review)."""
|
||||
from specify_cli import _install_shared_infra, shared_infra
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
# Point the script source at an empty ``bash/`` directory.
|
||||
empty_src = tmp_path / "empty-bundle" / "scripts"
|
||||
(empty_src / "bash").mkdir(parents=True)
|
||||
monkeypatch.setattr(shared_infra, "shared_scripts_source", lambda **kw: empty_src)
|
||||
|
||||
project = tmp_path / "empty-source"
|
||||
project.mkdir()
|
||||
(project / ".specify").mkdir()
|
||||
scripts_dir = project / ".specify" / "scripts" / "bash"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
tracked_rel = ".specify/scripts/bash/common.sh"
|
||||
(scripts_dir / "common.sh").write_text("# tracked\n", encoding="utf-8")
|
||||
manifest = IntegrationManifest("speckit", project, version="test")
|
||||
manifest.record_existing(tracked_rel)
|
||||
manifest.save()
|
||||
|
||||
_install_shared_infra(project, "sh", force=False)
|
||||
|
||||
# Empty source → scripts_scanned stays False → nothing deleted.
|
||||
assert (scripts_dir / "common.sh").exists()
|
||||
refreshed = IntegrationManifest.load("speckit", project)
|
||||
assert tracked_rel in refreshed.files
|
||||
|
||||
def test_shared_infra_stale_cleanup_ignores_unsafe_manifest_keys(self, tmp_path):
|
||||
"""A corrupted/hand-edited manifest key with a ``..`` segment is skipped
|
||||
before any filesystem access — its traversal target is never deleted
|
||||
(#3076 review, containment guard)."""
|
||||
import hashlib
|
||||
import json
|
||||
from specify_cli import _install_shared_infra
|
||||
|
||||
project = tmp_path / "unsafe-key"
|
||||
project.mkdir()
|
||||
scripts_dir = project / ".specify" / "scripts" / "bash"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
manifest_dir = project / ".specify" / "integrations"
|
||||
manifest_dir.mkdir(parents=True)
|
||||
|
||||
# A file the traversal key would resolve to (outside scripts/bash/).
|
||||
victim = project / ".specify" / "scripts" / "keep-me.sh"
|
||||
victim_bytes = b"# do not touch\n"
|
||||
victim.write_bytes(victim_bytes)
|
||||
|
||||
# Hand-crafted manifest: a key under the script prefix but with a ``..``
|
||||
# segment, with the *matching* hash so that — absent the containment guard
|
||||
# — stale-cleanup would consider it managed and unlink the target.
|
||||
traversal_key = ".specify/scripts/bash/../keep-me.sh"
|
||||
(manifest_dir / "speckit.manifest.json").write_text(
|
||||
json.dumps({
|
||||
"integration": "speckit",
|
||||
"version": "test",
|
||||
"files": {traversal_key: hashlib.sha256(victim_bytes).hexdigest()},
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
_install_shared_infra(project, "sh", force=False)
|
||||
|
||||
# The unsafe key was skipped; its target file is untouched.
|
||||
assert victim.exists()
|
||||
assert victim.read_bytes() == victim_bytes
|
||||
|
||||
def test_shared_infra_stale_cleanup_skips_escaping_key_without_failing(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""A key that passes the lexical guard but escapes containment — e.g. a
|
||||
Windows drive-relative ``C:tmp`` that is not ``is_absolute()`` yet discards
|
||||
the project root when joined — is skipped via ``_validate_rel_path``, never
|
||||
unlinked, and never turned into an install-time hard failure (#3076 review
|
||||
round 4). Simulated portably by forcing ``_validate_rel_path`` to reject the
|
||||
managed key, since real drive-relative paths only escape on Windows."""
|
||||
from specify_cli import _install_shared_infra
|
||||
from specify_cli.integrations import manifest as manifest_mod
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
project = tmp_path / "escaping-key"
|
||||
project.mkdir()
|
||||
(project / ".specify").mkdir()
|
||||
scripts_dir = project / ".specify" / "scripts" / "bash"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
|
||||
# A managed stale orphan that would normally be removed.
|
||||
stale_rel = ".specify/scripts/bash/update-agent-context.sh"
|
||||
stale = scripts_dir / "update-agent-context.sh"
|
||||
stale.write_text("# legacy orphan\n", encoding="utf-8")
|
||||
manifest = IntegrationManifest("speckit", project, version="test")
|
||||
manifest.record_existing(stale_rel)
|
||||
manifest.save()
|
||||
|
||||
# Force the containment check to reject this key, as it would for a
|
||||
# drive-relative escape on Windows. The cleanup must skip it gracefully.
|
||||
real_validate = manifest_mod._validate_rel_path
|
||||
|
||||
def fake_validate(rel, root):
|
||||
if str(rel).endswith("update-agent-context.sh"):
|
||||
raise ValueError("simulated drive-relative escape")
|
||||
return real_validate(rel, root)
|
||||
|
||||
monkeypatch.setattr(manifest_mod, "_validate_rel_path", fake_validate)
|
||||
|
||||
# Must not raise (no install-time hard failure from a corrupted key).
|
||||
_install_shared_infra(project, "sh", force=False)
|
||||
|
||||
# The escaping key was skipped, so its file is left untouched...
|
||||
assert stale.exists()
|
||||
assert stale.read_text(encoding="utf-8") == "# legacy orphan\n"
|
||||
# ...yet the install otherwise completed: real scripts are installed.
|
||||
assert (scripts_dir / "common.sh").exists()
|
||||
|
||||
def test_shared_infra_skip_warning_displayed(self, tmp_path, capsys):
|
||||
"""Console warning is displayed when files are skipped."""
|
||||
from specify_cli import _install_shared_infra
|
||||
|
||||
45
tests/integrations/test_integration_firebender.py
Normal file
45
tests/integrations/test_integration_firebender.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Tests for FirebenderIntegration."""
|
||||
|
||||
from specify_cli.integrations import get_integration
|
||||
from specify_cli.integrations.manifest import IntegrationManifest
|
||||
|
||||
from .test_integration_base_markdown import MarkdownIntegrationTests
|
||||
|
||||
|
||||
class TestFirebenderIntegration(MarkdownIntegrationTests):
|
||||
KEY = "firebender"
|
||||
FOLDER = ".firebender/"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
REGISTRAR_DIR = ".firebender/commands"
|
||||
CONTEXT_FILE = ".firebender/rules/specify-rules.mdc"
|
||||
|
||||
# Firebender reads custom slash commands from ``.firebender/commands/*.mdc``,
|
||||
# so this integration uses the ``.mdc`` extension instead of the ``.md``
|
||||
# default the base mixin assumes. Override the two extension-specific tests.
|
||||
def test_registrar_config(self):
|
||||
i = get_integration(self.KEY)
|
||||
assert i.registrar_config["dir"] == self.REGISTRAR_DIR
|
||||
assert i.registrar_config["format"] == "markdown"
|
||||
assert i.registrar_config["args"] == "$ARGUMENTS"
|
||||
assert i.registrar_config["extension"] == ".mdc"
|
||||
|
||||
def test_setup_creates_files(self, tmp_path):
|
||||
i = get_integration(self.KEY)
|
||||
m = IntegrationManifest(self.KEY, tmp_path)
|
||||
created = i.setup(tmp_path, m)
|
||||
assert len(created) > 0
|
||||
cmd_files = [f for f in created if "scripts" not in f.parts]
|
||||
for f in cmd_files:
|
||||
assert f.exists()
|
||||
assert f.name.startswith("speckit.")
|
||||
assert f.name.endswith(".mdc")
|
||||
|
||||
def _expected_files(self, script_variant: str) -> list[str]:
|
||||
# Firebender emits ``.mdc`` command files, so remap the base mixin's
|
||||
# ``.md`` expectations for files under this integration's command dir.
|
||||
cmd_dir = get_integration(self.KEY).registrar_config["dir"]
|
||||
prefix = cmd_dir + "/"
|
||||
return sorted(
|
||||
f[:-3] + ".mdc" if f.startswith(prefix) and f.endswith(".md") else f
|
||||
for f in super()._expected_files(script_variant)
|
||||
)
|
||||
@@ -116,6 +116,34 @@ class TestManifestPathTraversal:
|
||||
assert len(removed) == 1
|
||||
assert removed[0].name == "safe.txt"
|
||||
|
||||
def test_remove_drops_entry_and_is_noop_second_time(self, tmp_path):
|
||||
(tmp_path / "f.txt").write_text("x", encoding="utf-8")
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_existing("f.txt")
|
||||
assert "f.txt" in m.files
|
||||
assert m.remove("f.txt") is True
|
||||
assert "f.txt" not in m.files
|
||||
assert m.remove("f.txt") is False # already gone → no-op
|
||||
|
||||
def test_remove_rejects_absolute_path(self, tmp_path):
|
||||
# Matches record_existing/is_recovered: an absolute key can never be a
|
||||
# canonical manifest key, so remove() rejects it lexically and leaves
|
||||
# the tracked entry untouched.
|
||||
(tmp_path / "f.txt").write_text("x", encoding="utf-8")
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_existing("f.txt")
|
||||
import sys
|
||||
abs_input = "C:\\tmp\\f.txt" if sys.platform == "win32" else "/tmp/f.txt"
|
||||
assert m.remove(abs_input) is False
|
||||
assert "f.txt" in m.files
|
||||
|
||||
def test_remove_rejects_parent_traversal(self, tmp_path):
|
||||
(tmp_path / "f.txt").write_text("x", encoding="utf-8")
|
||||
m = IntegrationManifest("test", tmp_path)
|
||||
m.record_existing("f.txt")
|
||||
assert m.remove("../f.txt") is False
|
||||
assert "f.txt" in m.files
|
||||
|
||||
|
||||
class TestManifestCheckModified:
|
||||
def test_unmodified_file(self, tmp_path):
|
||||
|
||||
@@ -23,7 +23,7 @@ ALL_INTEGRATION_KEYS = [
|
||||
# Stage 3 — standard markdown integrations
|
||||
"claude", "qwen", "opencode", "junie", "kilocode", "auggie",
|
||||
"roo", "rovodev", "codebuddy", "qodercli", "amp", "shai", "bob", "trae",
|
||||
"pi", "iflow", "kiro-cli", "windsurf", "vibe", "cursor-agent",
|
||||
"pi", "iflow", "kiro-cli", "windsurf", "vibe", "cursor-agent", "firebender",
|
||||
# Stage 4 — TOML integrations
|
||||
"gemini", "tabnine",
|
||||
# Stage 5 — skills, generic & option-driven integrations
|
||||
|
||||
@@ -1,15 +1,158 @@
|
||||
"""Consistency checks for agent configuration across runtime surfaces."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from specify_cli import AGENT_CONFIG
|
||||
from specify_cli.extensions import CommandRegistrar
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
ISSUE_TEMPLATE_AGENT_KEYS = [
|
||||
"amp",
|
||||
"agy",
|
||||
"auggie",
|
||||
"claude",
|
||||
"cline",
|
||||
"codebuddy",
|
||||
"codex",
|
||||
"cursor-agent",
|
||||
"devin",
|
||||
"firebender",
|
||||
"forge",
|
||||
"gemini",
|
||||
"copilot",
|
||||
"goose",
|
||||
"hermes",
|
||||
"bob",
|
||||
"iflow",
|
||||
"junie",
|
||||
"kilocode",
|
||||
"kimi",
|
||||
"kiro-cli",
|
||||
"lingma",
|
||||
"vibe",
|
||||
"opencode",
|
||||
"pi",
|
||||
"qodercli",
|
||||
"qwen",
|
||||
"roo",
|
||||
"rovodev",
|
||||
"shai",
|
||||
"tabnine",
|
||||
"trae",
|
||||
"windsurf",
|
||||
"zcode",
|
||||
"zed",
|
||||
]
|
||||
|
||||
|
||||
def _issue_template(path: str) -> dict:
|
||||
return yaml.safe_load((REPO_ROOT / path).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _body_item_by_id(template: dict, item_id: str) -> dict:
|
||||
for item in template["body"]:
|
||||
if item.get("id") == item_id:
|
||||
return item
|
||||
raise AssertionError(f"Expected issue template body item {item_id!r}")
|
||||
|
||||
|
||||
def _dropdown_options(path: str, item_id: str) -> list[str]:
|
||||
item = _body_item_by_id(_issue_template(path), item_id)
|
||||
return item["attributes"]["options"]
|
||||
|
||||
|
||||
def _normalized_markdown(text: str) -> str:
|
||||
return " ".join(text.split())
|
||||
|
||||
|
||||
def _markdown_value_containing(path: str, marker: str) -> str:
|
||||
template = _issue_template(path)
|
||||
normalized_marker = _normalized_markdown(marker)
|
||||
for item in template["body"]:
|
||||
if item.get("type") != "markdown":
|
||||
continue
|
||||
value = item["attributes"]["value"]
|
||||
if normalized_marker in _normalized_markdown(value):
|
||||
return value
|
||||
raise AssertionError(f"Expected issue template markdown containing {marker!r}")
|
||||
|
||||
|
||||
def _markdown_paragraph_containing(path: str, marker: str) -> str:
|
||||
value = _markdown_value_containing(path, marker)
|
||||
normalized_marker = _normalized_markdown(marker)
|
||||
for paragraph in re.split(r"\n\s*\n", value):
|
||||
if normalized_marker in _normalized_markdown(paragraph):
|
||||
return paragraph
|
||||
raise AssertionError(f"Expected issue template paragraph containing {marker!r}")
|
||||
|
||||
|
||||
def _supported_agent_names_from_agent_request_template() -> list[str]:
|
||||
marker = "**Currently supported agents**:"
|
||||
paragraph = _markdown_paragraph_containing(
|
||||
".github/ISSUE_TEMPLATE/agent_request.yml",
|
||||
marker,
|
||||
)
|
||||
supported_agents_text = _normalized_markdown(paragraph).split(marker, 1)[1].strip()
|
||||
return [agent.strip() for agent in supported_agents_text.split(",")]
|
||||
|
||||
|
||||
class TestAgentConfigConsistency:
|
||||
"""Ensure kiro-cli migration stays synchronized across key surfaces."""
|
||||
"""Ensure agent configuration stays synchronized across key surfaces."""
|
||||
|
||||
def test_issue_template_agent_lists_match_runtime_integrations(self):
|
||||
"""GitHub issue templates should list all concrete built-in agents."""
|
||||
concrete_agent_keys = set(AGENT_CONFIG) - {"generic"}
|
||||
issue_template_agent_keys = set(ISSUE_TEMPLATE_AGENT_KEYS)
|
||||
|
||||
missing_agent_keys = sorted(concrete_agent_keys - issue_template_agent_keys)
|
||||
unexpected_agent_keys = sorted(issue_template_agent_keys - concrete_agent_keys)
|
||||
duplicate_agent_keys = sorted(
|
||||
key
|
||||
for key in issue_template_agent_keys
|
||||
if ISSUE_TEMPLATE_AGENT_KEYS.count(key) > 1
|
||||
)
|
||||
assert not missing_agent_keys, (
|
||||
"Issue template agent list is missing AGENT_CONFIG keys: "
|
||||
f"{missing_agent_keys}"
|
||||
)
|
||||
assert not unexpected_agent_keys, (
|
||||
"Issue template agent list includes unknown AGENT_CONFIG keys: "
|
||||
f"{unexpected_agent_keys}"
|
||||
)
|
||||
assert not duplicate_agent_keys, (
|
||||
"Issue template agent list contains duplicate keys: "
|
||||
f"{duplicate_agent_keys}"
|
||||
)
|
||||
|
||||
issue_template_agent_names = [
|
||||
AGENT_CONFIG[key]["name"] for key in ISSUE_TEMPLATE_AGENT_KEYS
|
||||
]
|
||||
assert "Generic (bring your own agent)" not in issue_template_agent_names
|
||||
|
||||
bug_options = _dropdown_options(
|
||||
".github/ISSUE_TEMPLATE/bug_report.yml",
|
||||
"ai-agent",
|
||||
)
|
||||
assert bug_options == issue_template_agent_names + ["Not applicable"]
|
||||
|
||||
feature_options = _dropdown_options(
|
||||
".github/ISSUE_TEMPLATE/feature_request.yml",
|
||||
"ai-agent",
|
||||
)
|
||||
assert feature_options == [
|
||||
"All agents",
|
||||
*issue_template_agent_names,
|
||||
"Not applicable",
|
||||
]
|
||||
|
||||
assert (
|
||||
_supported_agent_names_from_agent_request_template()
|
||||
== issue_template_agent_names
|
||||
)
|
||||
|
||||
def test_runtime_config_uses_kiro_cli_and_removes_q(self):
|
||||
"""AGENT_CONFIG should include kiro-cli and exclude legacy q."""
|
||||
|
||||
Reference in New Issue
Block a user