mirror of
https://github.com/github/spec-kit.git
synced 2026-04-03 03:03:09 +00:00
Compare commits
2 Commits
main
...
chore/rele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e9bf3d55 | ||
|
|
4fafe71bf5 |
44
AGENTS.md
44
AGENTS.md
@@ -48,7 +48,6 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **Kimi Code** | `.kimi/skills/` | Markdown | `kimi` | Kimi Code CLI (Moonshot AI) |
|
| **Kimi Code** | `.kimi/skills/` | Markdown | `kimi` | Kimi Code CLI (Moonshot AI) |
|
||||||
| **Pi Coding Agent** | `.pi/prompts/` | Markdown | `pi` | Pi terminal coding agent |
|
| **Pi Coding Agent** | `.pi/prompts/` | Markdown | `pi` | Pi terminal coding agent |
|
||||||
| **iFlow CLI** | `.iflow/commands/` | Markdown | `iflow` | iFlow CLI (iflow-ai) |
|
| **iFlow CLI** | `.iflow/commands/` | Markdown | `iflow` | iFlow CLI (iflow-ai) |
|
||||||
| **Forge** | `.forge/commands/` | Markdown | `forge` | Forge CLI (forgecode.dev) |
|
|
||||||
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
||||||
| **Trae** | `.trae/rules/` | Markdown | N/A (IDE-based) | Trae IDE |
|
| **Trae** | `.trae/rules/` | Markdown | N/A (IDE-based) | Trae IDE |
|
||||||
| **Antigravity** | `.agent/commands/` | Markdown | N/A (IDE-based) | Antigravity IDE (`--ai agy --ai-skills`) |
|
| **Antigravity** | `.agent/commands/` | Markdown | N/A (IDE-based) | Antigravity IDE (`--ai agy --ai-skills`) |
|
||||||
@@ -334,7 +333,6 @@ Require a command-line tool to be installed:
|
|||||||
- **Mistral Vibe**: `vibe` CLI
|
- **Mistral Vibe**: `vibe` CLI
|
||||||
- **Pi Coding Agent**: `pi` CLI
|
- **Pi Coding Agent**: `pi` CLI
|
||||||
- **iFlow CLI**: `iflow` CLI
|
- **iFlow CLI**: `iflow` CLI
|
||||||
- **Forge**: `forge` CLI
|
|
||||||
|
|
||||||
### IDE-Based Agents
|
### IDE-Based Agents
|
||||||
|
|
||||||
@@ -353,7 +351,7 @@ Work within integrated development environments:
|
|||||||
|
|
||||||
### Markdown Format
|
### Markdown Format
|
||||||
|
|
||||||
Used by: Claude, Cursor, GitHub Copilot, opencode, Windsurf, Junie, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen, Pi, Codex, Auggie, CodeBuddy, Qoder, Roo Code, Kilo Code, Trae, Antigravity, Mistral Vibe, iFlow, Forge
|
Used by: Claude, Cursor, GitHub Copilot, opencode, Windsurf, Junie, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen, Pi, Codex, Auggie, CodeBuddy, Qoder, Roo Code, Kilo Code, Trae, Antigravity, Mistral Vibe, iFlow
|
||||||
|
|
||||||
**Standard format:**
|
**Standard format:**
|
||||||
|
|
||||||
@@ -421,49 +419,9 @@ Different agents use different argument placeholders:
|
|||||||
|
|
||||||
- **Markdown/prompt-based**: `$ARGUMENTS`
|
- **Markdown/prompt-based**: `$ARGUMENTS`
|
||||||
- **TOML-based**: `{{args}}`
|
- **TOML-based**: `{{args}}`
|
||||||
- **Forge-specific**: `{{parameters}}` (uses custom parameter syntax)
|
|
||||||
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
|
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
|
||||||
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
|
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
|
||||||
|
|
||||||
## Special Processing Requirements
|
|
||||||
|
|
||||||
Some agents require custom processing beyond the standard template transformations:
|
|
||||||
|
|
||||||
### Copilot Integration
|
|
||||||
|
|
||||||
GitHub Copilot has unique requirements:
|
|
||||||
- Commands use `.agent.md` extension (not `.md`)
|
|
||||||
- Each command gets a companion `.prompt.md` file in `.github/prompts/`
|
|
||||||
- Installs `.vscode/settings.json` with prompt file recommendations
|
|
||||||
- Context file lives at `.github/copilot-instructions.md`
|
|
||||||
|
|
||||||
Implementation: Extends `IntegrationBase` with custom `setup()` method that:
|
|
||||||
1. Processes templates with `process_template()`
|
|
||||||
2. Generates companion `.prompt.md` files
|
|
||||||
3. Merges VS Code settings
|
|
||||||
|
|
||||||
### Forge Integration
|
|
||||||
|
|
||||||
Forge has special frontmatter and argument requirements:
|
|
||||||
- Uses `{{parameters}}` instead of `$ARGUMENTS`
|
|
||||||
- Strips `handoffs` frontmatter key (Forge-specific collaboration feature)
|
|
||||||
- Injects `name` field into frontmatter when missing
|
|
||||||
|
|
||||||
Implementation: Extends `MarkdownIntegration` with custom `setup()` method that:
|
|
||||||
1. Inherits standard template processing from `MarkdownIntegration`
|
|
||||||
2. Adds extra `$ARGUMENTS` → `{{parameters}}` replacement after template processing
|
|
||||||
3. Applies Forge-specific transformations via `_apply_forge_transformations()`
|
|
||||||
4. Strips `handoffs` frontmatter key
|
|
||||||
5. Injects missing `name` fields
|
|
||||||
6. Ensures the shared `update-agent-context.*` scripts include a `forge` case that maps context updates to `AGENTS.md` (similar to `opencode`/`codex`/`pi`) and lists `forge` in their usage/help text
|
|
||||||
|
|
||||||
### Standard Markdown Agents
|
|
||||||
|
|
||||||
Most agents (Bob, Claude, Windsurf, etc.) use `MarkdownIntegration`:
|
|
||||||
- Simple subclass with just `key`, `config`, `registrar_config` set
|
|
||||||
- Inherits standard processing from `MarkdownIntegration.setup()`
|
|
||||||
- No custom processing needed
|
|
||||||
|
|
||||||
## Testing New Agent Integration
|
## Testing New Agent Integration
|
||||||
|
|
||||||
1. **Build test**: Run package creation script locally
|
1. **Build test**: Run package creation script locally
|
||||||
|
|||||||
@@ -2,14 +2,6 @@
|
|||||||
|
|
||||||
<!-- insert new changelog below this comment -->
|
<!-- insert new changelog below this comment -->
|
||||||
|
|
||||||
## [0.5.0] - 2026-04-02
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Introduces DEVELOPMENT.md (#2069)
|
|
||||||
- Update cc-sdd reference to cc-spex in Community Friends (#2007)
|
|
||||||
- chore: release 0.4.5, begin 0.4.6.dev0 development (#2064)
|
|
||||||
|
|
||||||
## [0.4.5] - 2026-04-02
|
## [0.4.5] - 2026-04-02
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# Development Notes
|
|
||||||
|
|
||||||
Spec Kit is a toolkit for spec-driven development. At its core, it is a coordinated set of prompts, templates, scripts, and CLI/integration assets that define and deliver a spec-driven workflow for AI coding agents. This document is a starting point for people modifying Spec Kit itself, with a compact orientation to the key project documents and repository organization.
|
|
||||||
|
|
||||||
**Essential project documents:**
|
|
||||||
|
|
||||||
| Document | Role |
|
|
||||||
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
||||||
| [README.md](README.md) | Primary user-facing overview of Spec Kit and its workflow. |
|
|
||||||
| [DEVELOPMENT.md](DEVELOPMENT.md) | This document. |
|
|
||||||
| [spec-driven.md](spec-driven.md) | End-to-end explanation of the Spec-Driven Development workflow supported by Spec Kit. |
|
|
||||||
| [RELEASE-PROCESS.md](.github/workflows/RELEASE-PROCESS.md) | Release workflow, versioning rules, and changelog generation process. |
|
|
||||||
| [docs/index.md](docs/index.md) | Entry point to the `docs/` documentation set. |
|
|
||||||
| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution process, review expectations, and required development practices. |
|
|
||||||
| [TESTING.md](TESTING.md) | Validation strategy and testing procedures. |
|
|
||||||
|
|
||||||
**Main repository components:**
|
|
||||||
|
|
||||||
| Directory | Role |
|
|
||||||
| ------------------ | ------------------------------------------------------------------------------------------- |
|
|
||||||
| `templates/` | Prompt assets and templates that define the core workflow behavior and generated artifacts. |
|
|
||||||
| `scripts/` | Supporting scripts used by the workflow, setup, and repository tooling. |
|
|
||||||
| `src/specify_cli/` | Python source for the `specify` CLI, including agent-specific assets. |
|
|
||||||
| `extensions/` | Extension-related docs, catalogs, and supporting assets. |
|
|
||||||
| `presets/` | Preset-related docs, catalogs, and supporting assets. |
|
|
||||||
13
README.md
13
README.md
@@ -191,7 +191,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
||||||
| Extensify | Create and validate extensions and extension catalogs | `process` | Read+Write | [extensify](https://github.com/mnriem/spec-kit-extensions/tree/main/extensify) |
|
| Extensify | Create and validate extensions and extension catalogs | `process` | Read+Write | [extensify](https://github.com/mnriem/spec-kit-extensions/tree/main/extensify) |
|
||||||
| 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) |
|
| 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) |
|
| 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) |
|
||||||
| 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) |
|
| 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) |
|
| 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) |
|
||||||
@@ -270,7 +269,7 @@ See Spec-Driven Development in action across different scenarios with these comm
|
|||||||
|
|
||||||
Community projects that extend, visualize, or build on Spec Kit:
|
Community projects that extend, visualize, or build on Spec Kit:
|
||||||
|
|
||||||
- **[cc-spex](https://github.com/rhuss/cc-spex)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
|
- **[cc-sdd](https://github.com/rhuss/cc-sdd)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
|
||||||
|
|
||||||
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
|
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
|
||||||
|
|
||||||
@@ -286,7 +285,6 @@ Community projects that extend, visualize, or build on Spec Kit:
|
|||||||
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
|
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
|
||||||
| [Codex CLI](https://github.com/openai/codex) | ✅ | Requires `--ai-skills`. Codex recommends [skills](https://developers.openai.com/codex/skills) and treats [custom prompts](https://developers.openai.com/codex/custom-prompts) as deprecated. Spec-kit installs Codex skills into `.agents/skills` and invokes them as `$speckit-<command>`. |
|
| [Codex CLI](https://github.com/openai/codex) | ✅ | Requires `--ai-skills`. Codex recommends [skills](https://developers.openai.com/codex/skills) and treats [custom prompts](https://developers.openai.com/codex/custom-prompts) as deprecated. Spec-kit installs Codex skills into `.agents/skills` and invokes them as `$speckit-<command>`. |
|
||||||
| [Cursor](https://cursor.sh/) | ✅ | |
|
| [Cursor](https://cursor.sh/) | ✅ | |
|
||||||
| [Forge](https://forgecode.dev/) | ✅ | CLI tool: `forge` |
|
|
||||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
|
||||||
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
|
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
|
||||||
| [IBM Bob](https://www.ibm.com/products/bob) | ✅ | IDE-based agent with slash command support |
|
| [IBM Bob](https://www.ibm.com/products/bob) | ✅ | IDE-based agent with slash command support |
|
||||||
@@ -316,14 +314,14 @@ The `specify` command supports the following options:
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `init` | Initialize a new Specify project from the latest template |
|
| `init` | Initialize a new Specify project from the latest template |
|
||||||
| `check` | Check for installed tools: `git` plus all CLI-based agents configured in `AGENT_CONFIG` (for example: `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `junie`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, `forge`, etc.) |
|
| `check` | Check for installed tools: `git` plus all CLI-based agents configured in `AGENT_CONFIG` (for example: `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `junie`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, etc.) |
|
||||||
|
|
||||||
### `specify init` Arguments & Options
|
### `specify init` Arguments & Options
|
||||||
|
|
||||||
| Argument/Option | Type | Description |
|
| Argument/Option | Type | Description |
|
||||||
| ---------------------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ---------------------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
||||||
| `--ai` | Option | AI assistant to use (see `AGENT_CONFIG` for the full, up-to-date list). Common options include: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `junie`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, `forge`, or `generic` (requires `--ai-commands-dir`) |
|
| `--ai` | Option | AI assistant to use (see `AGENT_CONFIG` for the full, up-to-date list). Common options include: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `junie`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, or `generic` (requires `--ai-commands-dir`) |
|
||||||
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
|
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
|
||||||
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||||
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
||||||
@@ -378,9 +376,6 @@ specify init my-project --ai codex --ai-skills
|
|||||||
# Initialize with Antigravity support
|
# Initialize with Antigravity support
|
||||||
specify init my-project --ai agy --ai-skills
|
specify init my-project --ai agy --ai-skills
|
||||||
|
|
||||||
# Initialize with Forge support
|
|
||||||
specify init my-project --ai forge
|
|
||||||
|
|
||||||
# Initialize with an unsupported agent (generic / bring your own agent)
|
# Initialize with an unsupported agent (generic / bring your own agent)
|
||||||
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
|
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
|
||||||
|
|
||||||
@@ -626,7 +621,7 @@ specify init . --force --ai claude
|
|||||||
specify init --here --force --ai claude
|
specify init --here --force --ai claude
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, Forge, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify init <project_name> --ai claude --ignore-agent-tools
|
specify init <project_name> --ai claude --ignore-agent-tools
|
||||||
|
|||||||
@@ -363,37 +363,6 @@
|
|||||||
"created_at": "2026-04-01T00:00:00Z",
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
"updated_at": "2026-04-01T00:00:00Z"
|
"updated_at": "2026-04-01T00:00:00Z"
|
||||||
},
|
},
|
||||||
"fixit": {
|
|
||||||
"name": "FixIt Extension",
|
|
||||||
"id": "fixit",
|
|
||||||
"description": "Spec-aware bug fixing: maps bugs to spec artifacts, proposes a plan, applies minimal changes.",
|
|
||||||
"author": "ismaelJimenez",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/speckit-community/spec-kit-fixit/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/speckit-community/spec-kit-fixit",
|
|
||||||
"homepage": "https://github.com/speckit-community/spec-kit-fixit",
|
|
||||||
"documentation": "https://github.com/speckit-community/spec-kit-fixit/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/speckit-community/spec-kit-fixit/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 1,
|
|
||||||
"hooks": 0
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"debugging",
|
|
||||||
"fixit",
|
|
||||||
"spec-alignment",
|
|
||||||
"post-implementation"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-30T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-30T00:00:00Z"
|
|
||||||
},
|
|
||||||
"fleet": {
|
"fleet": {
|
||||||
"name": "Fleet Orchestrator",
|
"name": "Fleet Orchestrator",
|
||||||
"id": "fleet",
|
"id": "fleet",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.5.1.dev0"
|
version = "0.4.6.dev0"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
#
|
#
|
||||||
# 5. Multi-Agent Support
|
# 5. Multi-Agent Support
|
||||||
# - Handles agent-specific file paths and naming conventions
|
# - Handles agent-specific file paths and naming conventions
|
||||||
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Forge, Antigravity or Generic
|
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Antigravity or Generic
|
||||||
# - Can update single agents or all existing agent files
|
# - Can update single agents or all existing agent files
|
||||||
# - Creates default Claude file if no agent files exist
|
# - Creates default Claude file if no agent files exist
|
||||||
#
|
#
|
||||||
# Usage: ./update-agent-context.sh [agent_type]
|
# Usage: ./update-agent-context.sh [agent_type]
|
||||||
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|generic
|
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic
|
||||||
# Leave empty to update all existing agent files
|
# Leave empty to update all existing agent files
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -74,7 +74,7 @@ AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
|
|||||||
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
|
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
|
||||||
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
|
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
|
||||||
QODER_FILE="$REPO_ROOT/QODER.md"
|
QODER_FILE="$REPO_ROOT/QODER.md"
|
||||||
# Amp, Kiro CLI, IBM Bob, Pi, and Forge all share AGENTS.md — use AGENTS_FILE to avoid
|
# Amp, Kiro CLI, IBM Bob, and Pi all share AGENTS.md — use AGENTS_FILE to avoid
|
||||||
# updating the same file multiple times.
|
# updating the same file multiple times.
|
||||||
AMP_FILE="$AGENTS_FILE"
|
AMP_FILE="$AGENTS_FILE"
|
||||||
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
||||||
@@ -86,7 +86,6 @@ VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
|||||||
KIMI_FILE="$REPO_ROOT/KIMI.md"
|
KIMI_FILE="$REPO_ROOT/KIMI.md"
|
||||||
TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md"
|
TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md"
|
||||||
IFLOW_FILE="$REPO_ROOT/IFLOW.md"
|
IFLOW_FILE="$REPO_ROOT/IFLOW.md"
|
||||||
FORGE_FILE="$AGENTS_FILE"
|
|
||||||
|
|
||||||
# Template file
|
# Template file
|
||||||
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
||||||
@@ -691,15 +690,12 @@ update_specific_agent() {
|
|||||||
iflow)
|
iflow)
|
||||||
update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1
|
update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1
|
||||||
;;
|
;;
|
||||||
forge)
|
|
||||||
update_agent_file "$AGENTS_FILE" "Forge" || return 1
|
|
||||||
;;
|
|
||||||
generic)
|
generic)
|
||||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log_error "Unknown agent type '$agent_type'"
|
log_error "Unknown agent type '$agent_type'"
|
||||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|generic"
|
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -743,7 +739,10 @@ update_all_existing_agents() {
|
|||||||
_update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false
|
_update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false
|
||||||
_update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false
|
_update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false
|
||||||
_update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false
|
_update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false
|
||||||
_update_if_new "$AGENTS_FILE" "Codex/opencode/Amp/Kiro/Bob/Pi/Forge" || _all_ok=false
|
_update_if_new "$AGENTS_FILE" "Codex/opencode" || _all_ok=false
|
||||||
|
_update_if_new "$AMP_FILE" "Amp" || _all_ok=false
|
||||||
|
_update_if_new "$KIRO_FILE" "Kiro CLI" || _all_ok=false
|
||||||
|
_update_if_new "$BOB_FILE" "IBM Bob" || _all_ok=false
|
||||||
_update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false
|
_update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false
|
||||||
_update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false
|
_update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false
|
||||||
_update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false
|
_update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false
|
||||||
@@ -784,7 +783,7 @@ print_summary() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|generic]"
|
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]"
|
||||||
}
|
}
|
||||||
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
|
|||||||
2. Plan Data Extraction
|
2. Plan Data Extraction
|
||||||
3. Agent File Management (create from template or update existing)
|
3. Agent File Management (create from template or update existing)
|
||||||
4. Content Generation (technology stack, recent changes, timestamp)
|
4. Content Generation (technology stack, recent changes, timestamp)
|
||||||
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, junie, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, trae, pi, iflow, forge, generic)
|
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, junie, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, trae, pi, iflow, generic)
|
||||||
|
|
||||||
.PARAMETER AgentType
|
.PARAMETER AgentType
|
||||||
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
|
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
|
||||||
@@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1
|
|||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
[Parameter(Position=0)]
|
[Parameter(Position=0)]
|
||||||
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','junie','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','vibe','qodercli','kimi','trae','pi','iflow','forge','generic')]
|
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','junie','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','kimi','trae','pi','iflow','generic')]
|
||||||
[string]$AgentType
|
[string]$AgentType
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,7 +67,6 @@ $VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md'
|
|||||||
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
|
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
|
||||||
$TRAE_FILE = Join-Path $REPO_ROOT '.trae/rules/AGENTS.md'
|
$TRAE_FILE = Join-Path $REPO_ROOT '.trae/rules/AGENTS.md'
|
||||||
$IFLOW_FILE = Join-Path $REPO_ROOT 'IFLOW.md'
|
$IFLOW_FILE = Join-Path $REPO_ROOT 'IFLOW.md'
|
||||||
$FORGE_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
|
||||||
|
|
||||||
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
||||||
|
|
||||||
@@ -416,66 +415,36 @@ function Update-SpecificAgent {
|
|||||||
'trae' { Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae' }
|
'trae' { Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae' }
|
||||||
'pi' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Pi Coding Agent' }
|
'pi' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Pi Coding Agent' }
|
||||||
'iflow' { Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI' }
|
'iflow' { Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI' }
|
||||||
'forge' { Update-AgentFile -TargetFile $FORGE_FILE -AgentName 'Forge' }
|
|
||||||
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
||||||
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|generic'; return $false }
|
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic'; return $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Update-AllExistingAgents {
|
function Update-AllExistingAgents {
|
||||||
$found = $false
|
$found = $false
|
||||||
$ok = $true
|
$ok = $true
|
||||||
$updatedPaths = @()
|
if (Test-Path $CLAUDE_FILE) { if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }; $found = $true }
|
||||||
|
if (Test-Path $GEMINI_FILE) { if (-not (Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }; $found = $true }
|
||||||
# Helper function to update only if file exists and hasn't been updated yet
|
if (Test-Path $COPILOT_FILE) { if (-not (Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }; $found = $true }
|
||||||
function Update-IfNew {
|
if (Test-Path $CURSOR_FILE) { if (-not (Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }; $found = $true }
|
||||||
param(
|
if (Test-Path $QWEN_FILE) { if (-not (Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }; $found = $true }
|
||||||
[Parameter(Mandatory=$true)]
|
if (Test-Path $AGENTS_FILE) { if (-not (Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex/opencode')) { $ok = $false }; $found = $true }
|
||||||
[string]$FilePath,
|
if (Test-Path $WINDSURF_FILE) { if (-not (Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }; $found = $true }
|
||||||
[Parameter(Mandatory=$true)]
|
if (Test-Path $JUNIE_FILE) { if (-not (Update-AgentFile -TargetFile $JUNIE_FILE -AgentName 'Junie')) { $ok = $false }; $found = $true }
|
||||||
[string]$AgentName
|
if (Test-Path $KILOCODE_FILE) { if (-not (Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }; $found = $true }
|
||||||
)
|
if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true }
|
||||||
|
if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }; $found = $true }
|
||||||
if (-not (Test-Path $FilePath)) { return $true }
|
if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true }
|
||||||
|
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
|
||||||
# Get the real path to detect duplicates (e.g., AMP_FILE, KIRO_FILE, BOB_FILE all point to AGENTS.md)
|
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
|
||||||
$realPath = (Get-Item -LiteralPath $FilePath).FullName
|
if (Test-Path $TABNINE_FILE) { if (-not (Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false }; $found = $true }
|
||||||
|
if (Test-Path $KIRO_FILE) { if (-not (Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false }; $found = $true }
|
||||||
# Check if we've already updated this file
|
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
|
||||||
if ($updatedPaths -contains $realPath) {
|
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
|
||||||
return $true
|
if (Test-Path $VIBE_FILE) { if (-not (Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }; $found = $true }
|
||||||
}
|
if (Test-Path $KIMI_FILE) { if (-not (Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }; $found = $true }
|
||||||
|
if (Test-Path $TRAE_FILE) { if (-not (Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae')) { $ok = $false }; $found = $true }
|
||||||
# Record the file as seen before attempting the update
|
if (Test-Path $IFLOW_FILE) { if (-not (Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false }; $found = $true }
|
||||||
# Use parent scope (1) to modify Update-AllExistingAgents' local variables
|
|
||||||
Set-Variable -Name updatedPaths -Value ($updatedPaths + $realPath) -Scope 1
|
|
||||||
Set-Variable -Name found -Value $true -Scope 1
|
|
||||||
|
|
||||||
# Perform the update
|
|
||||||
return (Update-AgentFile -TargetFile $FilePath -AgentName $AgentName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Update-IfNew -FilePath $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $AGENTS_FILE -AgentName 'Codex/opencode/Amp/Kiro/Bob/Pi/Forge')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $JUNIE_FILE -AgentName 'Junie')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $TRAE_FILE -AgentName 'Trae')) { $ok = $false }
|
|
||||||
if (-not (Update-IfNew -FilePath $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false }
|
|
||||||
|
|
||||||
if (-not $found) {
|
if (-not $found) {
|
||||||
Write-Info 'No existing agent files found, creating default Claude file...'
|
Write-Info 'No existing agent files found, creating default Claude file...'
|
||||||
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
|
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
|
||||||
@@ -490,7 +459,7 @@ function Print-Summary {
|
|||||||
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
||||||
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|generic]'
|
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Main {
|
function Main {
|
||||||
|
|||||||
@@ -117,10 +117,10 @@ CLAUDE_NPM_LOCAL_PATH = Path.home() / ".claude" / "local" / "node_modules" / ".b
|
|||||||
BANNER = """
|
BANNER = """
|
||||||
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
|
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
|
||||||
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
|
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
|
||||||
███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
|
███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
|
||||||
╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
|
╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
|
||||||
███████║██║ ███████╗╚██████╗██║██║ ██║
|
███████║██║ ███████╗╚██████╗██║██║ ██║
|
||||||
╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
|
╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TAGLINE = "GitHub Spec Kit - Spec-Driven Development Toolkit"
|
TAGLINE = "GitHub Spec Kit - Spec-Driven Development Toolkit"
|
||||||
@@ -232,12 +232,12 @@ def get_key():
|
|||||||
def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str:
|
def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
Interactive selection using arrow keys with Rich Live display.
|
Interactive selection using arrow keys with Rich Live display.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
options: Dict with keys as option keys and values as descriptions
|
options: Dict with keys as option keys and values as descriptions
|
||||||
prompt_text: Text to show above the options
|
prompt_text: Text to show above the options
|
||||||
default_key: Default option key to start with
|
default_key: Default option key to start with
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Selected option key
|
Selected option key
|
||||||
"""
|
"""
|
||||||
@@ -365,11 +365,11 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
|
|||||||
|
|
||||||
def check_tool(tool: str, tracker: StepTracker = None) -> bool:
|
def check_tool(tool: str, tracker: StepTracker = None) -> bool:
|
||||||
"""Check if a tool is installed. Optionally update tracker.
|
"""Check if a tool is installed. Optionally update tracker.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tool: Name of the tool to check
|
tool: Name of the tool to check
|
||||||
tracker: Optional StepTracker to update with results
|
tracker: Optional StepTracker to update with results
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if tool is found, False otherwise
|
True if tool is found, False otherwise
|
||||||
"""
|
"""
|
||||||
@@ -385,27 +385,27 @@ def check_tool(tool: str, tracker: StepTracker = None) -> bool:
|
|||||||
if tracker:
|
if tracker:
|
||||||
tracker.complete(tool, "available")
|
tracker.complete(tool, "available")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if tool == "kiro-cli":
|
if tool == "kiro-cli":
|
||||||
# Kiro currently supports both executable names. Prefer kiro-cli and
|
# Kiro currently supports both executable names. Prefer kiro-cli and
|
||||||
# accept kiro as a compatibility fallback.
|
# accept kiro as a compatibility fallback.
|
||||||
found = shutil.which("kiro-cli") is not None or shutil.which("kiro") is not None
|
found = shutil.which("kiro-cli") is not None or shutil.which("kiro") is not None
|
||||||
else:
|
else:
|
||||||
found = shutil.which(tool) is not None
|
found = shutil.which(tool) is not None
|
||||||
|
|
||||||
if tracker:
|
if tracker:
|
||||||
if found:
|
if found:
|
||||||
tracker.complete(tool, "available")
|
tracker.complete(tool, "available")
|
||||||
else:
|
else:
|
||||||
tracker.error(tool, "not found")
|
tracker.error(tool, "not found")
|
||||||
|
|
||||||
return found
|
return found
|
||||||
|
|
||||||
def is_git_repo(path: Path = None) -> bool:
|
def is_git_repo(path: Path = None) -> bool:
|
||||||
"""Check if the specified path is inside a git repository."""
|
"""Check if the specified path is inside a git repository."""
|
||||||
if path is None:
|
if path is None:
|
||||||
path = Path.cwd()
|
path = Path.cwd()
|
||||||
|
|
||||||
if not path.is_dir():
|
if not path.is_dir():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -423,11 +423,11 @@ def is_git_repo(path: Path = None) -> bool:
|
|||||||
|
|
||||||
def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Optional[str]]:
|
def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Optional[str]]:
|
||||||
"""Initialize a git repository in the specified path.
|
"""Initialize a git repository in the specified path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_path: Path to initialize git repository in
|
project_path: Path to initialize git repository in
|
||||||
quiet: if True suppress console output (tracker handles status)
|
quiet: if True suppress console output (tracker handles status)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (success: bool, error_message: Optional[str])
|
Tuple of (success: bool, error_message: Optional[str])
|
||||||
"""
|
"""
|
||||||
@@ -449,7 +449,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Option
|
|||||||
error_msg += f"\nError: {e.stderr.strip()}"
|
error_msg += f"\nError: {e.stderr.strip()}"
|
||||||
elif e.stdout:
|
elif e.stdout:
|
||||||
error_msg += f"\nOutput: {e.stdout.strip()}"
|
error_msg += f"\nOutput: {e.stdout.strip()}"
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
console.print(f"[red]Error initializing git repository:[/red] {e}")
|
console.print(f"[red]Error initializing git repository:[/red] {e}")
|
||||||
return False, error_msg
|
return False, error_msg
|
||||||
@@ -911,7 +911,7 @@ def init(
|
|||||||
console.print("[yellow]Example:[/yellow] specify init --ai claude --here")
|
console.print("[yellow]Example:[/yellow] specify init --ai claude --here")
|
||||||
console.print(f"[yellow]Available agents:[/yellow] {', '.join(AGENT_CONFIG.keys())}")
|
console.print(f"[yellow]Available agents:[/yellow] {', '.join(AGENT_CONFIG.keys())}")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
if ai_commands_dir and ai_commands_dir.startswith("--"):
|
if ai_commands_dir and ai_commands_dir.startswith("--"):
|
||||||
console.print(f"[red]Error:[/red] Invalid value for --ai-commands-dir: '{ai_commands_dir}'")
|
console.print(f"[red]Error:[/red] Invalid value for --ai-commands-dir: '{ai_commands_dir}'")
|
||||||
console.print("[yellow]Hint:[/yellow] Did you forget to provide a value for --ai-commands-dir?")
|
console.print("[yellow]Hint:[/yellow] Did you forget to provide a value for --ai-commands-dir?")
|
||||||
@@ -1023,8 +1023,8 @@ def init(
|
|||||||
# Create options dict for selection (agent_key: display_name)
|
# Create options dict for selection (agent_key: display_name)
|
||||||
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
|
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
|
||||||
selected_ai = select_with_arrows(
|
selected_ai = select_with_arrows(
|
||||||
ai_choices,
|
ai_choices,
|
||||||
"Choose your AI assistant:",
|
"Choose your AI assistant:",
|
||||||
"copilot"
|
"copilot"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1262,7 +1262,7 @@ def init(
|
|||||||
|
|
||||||
console.print(tracker.render())
|
console.print(tracker.render())
|
||||||
console.print("\n[bold green]Project ready.[/bold green]")
|
console.print("\n[bold green]Project ready.[/bold green]")
|
||||||
|
|
||||||
# Show git error details if initialization failed
|
# Show git error details if initialization failed
|
||||||
if git_error_message:
|
if git_error_message:
|
||||||
console.print()
|
console.print()
|
||||||
@@ -1410,9 +1410,9 @@ def version():
|
|||||||
"""Display version and system information."""
|
"""Display version and system information."""
|
||||||
import platform
|
import platform
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
|
||||||
show_banner()
|
show_banner()
|
||||||
|
|
||||||
# Get CLI version from package metadata
|
# Get CLI version from package metadata
|
||||||
cli_version = "unknown"
|
cli_version = "unknown"
|
||||||
try:
|
try:
|
||||||
@@ -1428,15 +1428,15 @@ def version():
|
|||||||
cli_version = data.get("project", {}).get("version", "unknown")
|
cli_version = data.get("project", {}).get("version", "unknown")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fetch latest template release version
|
# Fetch latest template release version
|
||||||
repo_owner = "github"
|
repo_owner = "github"
|
||||||
repo_name = "spec-kit"
|
repo_name = "spec-kit"
|
||||||
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
||||||
|
|
||||||
template_version = "unknown"
|
template_version = "unknown"
|
||||||
release_date = "unknown"
|
release_date = "unknown"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
api_url,
|
api_url,
|
||||||
|
|||||||
@@ -408,12 +408,6 @@ class CommandRegistrar:
|
|||||||
|
|
||||||
frontmatter = self._adjust_script_paths(frontmatter)
|
frontmatter = self._adjust_script_paths(frontmatter)
|
||||||
|
|
||||||
for key in agent_config.get("strip_frontmatter_keys", []):
|
|
||||||
frontmatter.pop(key, None)
|
|
||||||
|
|
||||||
if agent_config.get("inject_name") and not frontmatter.get("name"):
|
|
||||||
frontmatter["name"] = cmd_name
|
|
||||||
|
|
||||||
body = self._convert_argument_placeholder(
|
body = self._convert_argument_placeholder(
|
||||||
body, "$ARGUMENTS", agent_config["args"]
|
body, "$ARGUMENTS", agent_config["args"]
|
||||||
)
|
)
|
||||||
@@ -442,30 +436,11 @@ class CommandRegistrar:
|
|||||||
|
|
||||||
for alias in cmd_info.get("aliases", []):
|
for alias in cmd_info.get("aliases", []):
|
||||||
alias_output_name = self._compute_output_name(agent_name, alias, agent_config)
|
alias_output_name = self._compute_output_name(agent_name, alias, agent_config)
|
||||||
|
alias_output = output
|
||||||
# For agents with inject_name, render with alias-specific frontmatter
|
if agent_config["extension"] == "/SKILL.md":
|
||||||
if agent_config.get("inject_name"):
|
alias_output = self.render_skill_command(
|
||||||
alias_frontmatter = deepcopy(frontmatter)
|
agent_name, alias_output_name, frontmatter, body, source_id, cmd_file, project_root
|
||||||
alias_frontmatter["name"] = alias
|
)
|
||||||
|
|
||||||
if agent_config["extension"] == "/SKILL.md":
|
|
||||||
alias_output = self.render_skill_command(
|
|
||||||
agent_name, alias_output_name, alias_frontmatter, body, source_id, cmd_file, project_root
|
|
||||||
)
|
|
||||||
elif agent_config["format"] == "markdown":
|
|
||||||
alias_output = self.render_markdown_command(alias_frontmatter, body, source_id, context_note)
|
|
||||||
elif agent_config["format"] == "toml":
|
|
||||||
alias_output = self.render_toml_command(alias_frontmatter, body, source_id)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported format: {agent_config['format']}")
|
|
||||||
else:
|
|
||||||
# For other agents, reuse the primary output
|
|
||||||
alias_output = output
|
|
||||||
if agent_config["extension"] == "/SKILL.md":
|
|
||||||
alias_output = self.render_skill_command(
|
|
||||||
agent_name, alias_output_name, frontmatter, body, source_id, cmd_file, project_root
|
|
||||||
)
|
|
||||||
|
|
||||||
alias_file = commands_dir / f"{alias_output_name}{agent_config['extension']}"
|
alias_file = commands_dir / f"{alias_output_name}{agent_config['extension']}"
|
||||||
alias_file.parent.mkdir(parents=True, exist_ok=True)
|
alias_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
alias_file.write_text(alias_output, encoding="utf-8")
|
alias_file.write_text(alias_output, encoding="utf-8")
|
||||||
@@ -565,3 +540,4 @@ try:
|
|||||||
CommandRegistrar._ensure_configs()
|
CommandRegistrar._ensure_configs()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ def _register_builtins() -> None:
|
|||||||
from .codebuddy import CodebuddyIntegration
|
from .codebuddy import CodebuddyIntegration
|
||||||
from .copilot import CopilotIntegration
|
from .copilot import CopilotIntegration
|
||||||
from .cursor_agent import CursorAgentIntegration
|
from .cursor_agent import CursorAgentIntegration
|
||||||
from .forge import ForgeIntegration
|
|
||||||
from .gemini import GeminiIntegration
|
from .gemini import GeminiIntegration
|
||||||
from .generic import GenericIntegration
|
from .generic import GenericIntegration
|
||||||
from .iflow import IflowIntegration
|
from .iflow import IflowIntegration
|
||||||
@@ -84,7 +83,6 @@ def _register_builtins() -> None:
|
|||||||
_register(CodebuddyIntegration())
|
_register(CodebuddyIntegration())
|
||||||
_register(CopilotIntegration())
|
_register(CopilotIntegration())
|
||||||
_register(CursorAgentIntegration())
|
_register(CursorAgentIntegration())
|
||||||
_register(ForgeIntegration())
|
|
||||||
_register(GeminiIntegration())
|
_register(GeminiIntegration())
|
||||||
_register(GenericIntegration())
|
_register(GenericIntegration())
|
||||||
_register(IflowIntegration())
|
_register(IflowIntegration())
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
"""Forge integration — forgecode.dev AI coding agent.
|
|
||||||
|
|
||||||
Forge has several unique behaviors compared to standard markdown agents:
|
|
||||||
- Uses `{{parameters}}` instead of `$ARGUMENTS` for argument passing
|
|
||||||
- Strips `handoffs` frontmatter key (Claude Code feature that causes Forge to hang)
|
|
||||||
- Injects `name` field into frontmatter when missing
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from ..base import MarkdownIntegration
|
|
||||||
from ..manifest import IntegrationManifest
|
|
||||||
|
|
||||||
|
|
||||||
class ForgeIntegration(MarkdownIntegration):
|
|
||||||
"""Integration for Forge (forgecode.dev).
|
|
||||||
|
|
||||||
Extends MarkdownIntegration to add Forge-specific processing:
|
|
||||||
- Replaces $ARGUMENTS with {{parameters}}
|
|
||||||
- Strips 'handoffs' frontmatter key (incompatible with Forge)
|
|
||||||
- Injects 'name' field into frontmatter when missing
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "forge"
|
|
||||||
config = {
|
|
||||||
"name": "Forge",
|
|
||||||
"folder": ".forge/",
|
|
||||||
"commands_subdir": "commands",
|
|
||||||
"install_url": "https://forgecode.dev/docs/",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".forge/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "{{parameters}}",
|
|
||||||
"extension": ".md",
|
|
||||||
"strip_frontmatter_keys": ["handoffs"],
|
|
||||||
"inject_name": True,
|
|
||||||
}
|
|
||||||
context_file = "AGENTS.md"
|
|
||||||
|
|
||||||
def setup(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
"""Install Forge commands with custom processing.
|
|
||||||
|
|
||||||
Extends MarkdownIntegration.setup() to inject Forge-specific transformations
|
|
||||||
after standard template processing.
|
|
||||||
"""
|
|
||||||
templates = self.list_command_templates()
|
|
||||||
if not templates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
project_root_resolved = project_root.resolve()
|
|
||||||
if manifest.project_root != project_root_resolved:
|
|
||||||
raise ValueError(
|
|
||||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
|
||||||
f"project_root ({project_root_resolved})"
|
|
||||||
)
|
|
||||||
|
|
||||||
dest = self.commands_dest(project_root).resolve()
|
|
||||||
try:
|
|
||||||
dest.relative_to(project_root_resolved)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(
|
|
||||||
f"Integration destination {dest} escapes "
|
|
||||||
f"project root {project_root_resolved}"
|
|
||||||
) from exc
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
script_type = opts.get("script_type", "sh")
|
|
||||||
arg_placeholder = self.registrar_config.get("args", "{{parameters}}")
|
|
||||||
created: list[Path] = []
|
|
||||||
|
|
||||||
for src_file in templates:
|
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
|
||||||
# Process template with standard MarkdownIntegration logic
|
|
||||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
|
||||||
|
|
||||||
# FORGE-SPECIFIC: Ensure any remaining $ARGUMENTS placeholders are
|
|
||||||
# converted to {{parameters}}
|
|
||||||
processed = processed.replace("$ARGUMENTS", arg_placeholder)
|
|
||||||
|
|
||||||
# FORGE-SPECIFIC: Apply frontmatter transformations
|
|
||||||
processed = self._apply_forge_transformations(processed, src_file.stem)
|
|
||||||
|
|
||||||
dst_name = self.command_filename(src_file.stem)
|
|
||||||
dst_file = self.write_file_and_record(
|
|
||||||
processed, dest / dst_name, project_root, manifest
|
|
||||||
)
|
|
||||||
created.append(dst_file)
|
|
||||||
|
|
||||||
# Install integration-specific update-context scripts
|
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
|
||||||
|
|
||||||
return created
|
|
||||||
|
|
||||||
def _apply_forge_transformations(self, content: str, template_name: str) -> str:
|
|
||||||
"""Apply Forge-specific transformations to processed content.
|
|
||||||
|
|
||||||
1. Strip 'handoffs' frontmatter key (from Claude Code templates; incompatible with Forge)
|
|
||||||
2. Inject 'name' field if missing
|
|
||||||
"""
|
|
||||||
# Parse frontmatter
|
|
||||||
lines = content.split('\n')
|
|
||||||
if not lines or lines[0].strip() != '---':
|
|
||||||
return content
|
|
||||||
|
|
||||||
# Find end of frontmatter
|
|
||||||
frontmatter_end = -1
|
|
||||||
for i in range(1, len(lines)):
|
|
||||||
if lines[i].strip() == '---':
|
|
||||||
frontmatter_end = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if frontmatter_end == -1:
|
|
||||||
return content
|
|
||||||
|
|
||||||
frontmatter_lines = lines[1:frontmatter_end]
|
|
||||||
body_lines = lines[frontmatter_end + 1:]
|
|
||||||
|
|
||||||
# 1. Strip 'handoffs' key
|
|
||||||
filtered_frontmatter = []
|
|
||||||
skip_until_outdent = False
|
|
||||||
for line in frontmatter_lines:
|
|
||||||
if skip_until_outdent:
|
|
||||||
# Skip indented lines under handoffs:
|
|
||||||
if line and (line[0] == ' ' or line[0] == '\t'):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
skip_until_outdent = False
|
|
||||||
|
|
||||||
if line.strip().startswith('handoffs:'):
|
|
||||||
skip_until_outdent = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
filtered_frontmatter.append(line)
|
|
||||||
|
|
||||||
# 2. Inject 'name' field if missing
|
|
||||||
has_name = any(line.strip().startswith('name:') for line in filtered_frontmatter)
|
|
||||||
if not has_name:
|
|
||||||
# Use the template name as the command name (e.g., "plan" -> "speckit.plan")
|
|
||||||
cmd_name = f"speckit.{template_name}"
|
|
||||||
filtered_frontmatter.insert(0, f'name: {cmd_name}')
|
|
||||||
|
|
||||||
# Reconstruct content
|
|
||||||
result = ['---'] + filtered_frontmatter + ['---'] + body_lines
|
|
||||||
return '\n'.join(result)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# update-context.ps1 — Forge integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$sharedScript = "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1"
|
|
||||||
|
|
||||||
# Always delegate to the shared updater; fail clearly if it is unavailable.
|
|
||||||
if (-not (Test-Path $sharedScript)) {
|
|
||||||
Write-Error "Error: shared agent context updater not found: $sharedScript"
|
|
||||||
Write-Error "Forge integration requires support in scripts/powershell/update-agent-context.ps1."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
& $sharedScript -AgentType forge
|
|
||||||
exit $LASTEXITCODE
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — Forge integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
shared_script="$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh"
|
|
||||||
|
|
||||||
# Always delegate to the shared updater; fail clearly if it is unavailable.
|
|
||||||
if [ ! -x "$shared_script" ]; then
|
|
||||||
echo "Error: shared agent context updater not found or not executable:" >&2
|
|
||||||
echo " $shared_script" >&2
|
|
||||||
echo "Forge integration requires support in scripts/bash/update-agent-context.sh." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$shared_script" forge
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
"""Tests for ForgeIntegration."""
|
|
||||||
|
|
||||||
from specify_cli.integrations import get_integration
|
|
||||||
from specify_cli.integrations.manifest import IntegrationManifest
|
|
||||||
|
|
||||||
|
|
||||||
class TestForgeIntegration:
|
|
||||||
def test_forge_key_and_config(self):
|
|
||||||
forge = get_integration("forge")
|
|
||||||
assert forge is not None
|
|
||||||
assert forge.key == "forge"
|
|
||||||
assert forge.config["folder"] == ".forge/"
|
|
||||||
assert forge.config["commands_subdir"] == "commands"
|
|
||||||
assert forge.config["requires_cli"] is True
|
|
||||||
assert forge.registrar_config["args"] == "{{parameters}}"
|
|
||||||
assert forge.registrar_config["extension"] == ".md"
|
|
||||||
assert forge.context_file == "AGENTS.md"
|
|
||||||
|
|
||||||
def test_command_filename_md(self):
|
|
||||||
forge = get_integration("forge")
|
|
||||||
assert forge.command_filename("plan") == "speckit.plan.md"
|
|
||||||
|
|
||||||
def test_setup_creates_md_files(self, tmp_path):
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
created = forge.setup(tmp_path, m)
|
|
||||||
assert len(created) > 0
|
|
||||||
# Separate command files from scripts
|
|
||||||
command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"]
|
|
||||||
assert len(command_files) > 0
|
|
||||||
for f in command_files:
|
|
||||||
assert f.name.endswith(".md")
|
|
||||||
|
|
||||||
def test_setup_installs_update_scripts(self, tmp_path):
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
created = forge.setup(tmp_path, m)
|
|
||||||
script_files = [f for f in created if "scripts" in f.parts]
|
|
||||||
assert len(script_files) > 0
|
|
||||||
sh_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.sh"
|
|
||||||
ps_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.ps1"
|
|
||||||
assert sh_script in created
|
|
||||||
assert ps_script in created
|
|
||||||
assert sh_script.exists()
|
|
||||||
assert ps_script.exists()
|
|
||||||
|
|
||||||
def test_all_created_files_tracked_in_manifest(self, tmp_path):
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
created = forge.setup(tmp_path, m)
|
|
||||||
for f in created:
|
|
||||||
rel = f.resolve().relative_to(tmp_path.resolve()).as_posix()
|
|
||||||
assert rel in m.files, f"Created file {rel} not tracked in manifest"
|
|
||||||
|
|
||||||
def test_install_uninstall_roundtrip(self, tmp_path):
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
created = forge.install(tmp_path, m)
|
|
||||||
assert len(created) > 0
|
|
||||||
m.save()
|
|
||||||
for f in created:
|
|
||||||
assert f.exists()
|
|
||||||
removed, skipped = forge.uninstall(tmp_path, m)
|
|
||||||
assert len(removed) == len(created)
|
|
||||||
assert skipped == []
|
|
||||||
|
|
||||||
def test_modified_file_survives_uninstall(self, tmp_path):
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
created = forge.install(tmp_path, m)
|
|
||||||
m.save()
|
|
||||||
# Modify a command file (not a script)
|
|
||||||
command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"]
|
|
||||||
modified_file = command_files[0]
|
|
||||||
modified_file.write_text("user modified this", encoding="utf-8")
|
|
||||||
removed, skipped = forge.uninstall(tmp_path, m)
|
|
||||||
assert modified_file.exists()
|
|
||||||
assert modified_file in skipped
|
|
||||||
|
|
||||||
def test_directory_structure(self, tmp_path):
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
forge.setup(tmp_path, m)
|
|
||||||
commands_dir = tmp_path / ".forge" / "commands"
|
|
||||||
assert commands_dir.is_dir()
|
|
||||||
|
|
||||||
# Derive expected command names from the Forge command templates so the test
|
|
||||||
# stays in sync if templates are added/removed.
|
|
||||||
templates = forge.list_command_templates()
|
|
||||||
expected_commands = {t.stem for t in templates}
|
|
||||||
assert len(expected_commands) > 0, "No command templates found"
|
|
||||||
|
|
||||||
# Check generated files match templates
|
|
||||||
command_files = sorted(commands_dir.glob("speckit.*.md"))
|
|
||||||
assert len(command_files) == len(expected_commands)
|
|
||||||
actual_commands = {f.name.removeprefix("speckit.").removesuffix(".md") for f in command_files}
|
|
||||||
assert actual_commands == expected_commands
|
|
||||||
|
|
||||||
def test_templates_are_processed(self, tmp_path):
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
forge.setup(tmp_path, m)
|
|
||||||
commands_dir = tmp_path / ".forge" / "commands"
|
|
||||||
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
||||||
content = cmd_file.read_text(encoding="utf-8")
|
|
||||||
# Check standard replacements
|
|
||||||
assert "{SCRIPT}" not in content, f"{cmd_file.name} has unprocessed {{SCRIPT}}"
|
|
||||||
assert "__AGENT__" not in content, f"{cmd_file.name} has unprocessed __AGENT__"
|
|
||||||
assert "{ARGS}" not in content, f"{cmd_file.name} has unprocessed {{ARGS}}"
|
|
||||||
# Check Forge-specific: $ARGUMENTS should be replaced with {{parameters}}
|
|
||||||
assert "$ARGUMENTS" not in content, f"{cmd_file.name} has unprocessed $ARGUMENTS"
|
|
||||||
# Frontmatter sections should be stripped
|
|
||||||
assert "\nscripts:\n" not in content
|
|
||||||
assert "\nagent_scripts:\n" not in content
|
|
||||||
|
|
||||||
def test_forge_specific_transformations(self, tmp_path):
|
|
||||||
"""Test Forge-specific processing: name injection and handoffs stripping."""
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
forge.setup(tmp_path, m)
|
|
||||||
commands_dir = tmp_path / ".forge" / "commands"
|
|
||||||
|
|
||||||
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
||||||
content = cmd_file.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
# Check that name field is injected in frontmatter
|
|
||||||
assert "\nname: " in content, f"{cmd_file.name} missing injected 'name' field"
|
|
||||||
|
|
||||||
# Check that handoffs frontmatter key is stripped
|
|
||||||
assert "\nhandoffs:" not in content, f"{cmd_file.name} has unstripped 'handoffs' key"
|
|
||||||
|
|
||||||
def test_uses_parameters_placeholder(self, tmp_path):
|
|
||||||
"""Verify Forge replaces $ARGUMENTS with {{parameters}} in generated files."""
|
|
||||||
from specify_cli.integrations.forge import ForgeIntegration
|
|
||||||
forge = ForgeIntegration()
|
|
||||||
|
|
||||||
# The registrar_config should specify {{parameters}}
|
|
||||||
assert forge.registrar_config["args"] == "{{parameters}}"
|
|
||||||
|
|
||||||
# Generate files and verify $ARGUMENTS is replaced with {{parameters}}
|
|
||||||
from specify_cli.integrations.manifest import IntegrationManifest
|
|
||||||
m = IntegrationManifest("forge", tmp_path)
|
|
||||||
forge.setup(tmp_path, m)
|
|
||||||
commands_dir = tmp_path / ".forge" / "commands"
|
|
||||||
|
|
||||||
# Check all generated command files
|
|
||||||
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
||||||
content = cmd_file.read_text(encoding="utf-8")
|
|
||||||
# $ARGUMENTS should be replaced with {{parameters}}
|
|
||||||
assert "$ARGUMENTS" not in content, (
|
|
||||||
f"{cmd_file.name} still contains $ARGUMENTS - it should be replaced with {{{{parameters}}}}"
|
|
||||||
)
|
|
||||||
# At least some files should have {{parameters}} (those with user input sections)
|
|
||||||
# We'll check the checklist file specifically as it has a User Input section
|
|
||||||
|
|
||||||
# Verify checklist specifically has {{parameters}} in the User Input section
|
|
||||||
checklist = commands_dir / "speckit.checklist.md"
|
|
||||||
if checklist.exists():
|
|
||||||
content = checklist.read_text(encoding="utf-8")
|
|
||||||
assert "{{parameters}}" in content, (
|
|
||||||
"checklist should contain {{parameters}} in User Input section"
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user