Files
github-spec-kit/AGENTS.md
Copilot 50da3a0f77 Extract agent context updates into bundled agent-context extension (#2546)
* Initial plan

* Extract agent context updates into bundled agent-context extension

* Potential fix for pull request finding 'Unused import'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'Unused import'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: address review comments on agent-context extension

- bash: parse init-options.json with a single python3 invocation instead
  of three separate read_json_field calls, for parity with the PowerShell
  ConvertFrom-Json approach and to avoid divergent error semantics
- bash: use parameter expansion to strip PROJECT_ROOT prefix from plan
  path instead of sed interpolation, avoiding special-character fragility
- powershell: limit Get-ChildItem to -Depth 1 so plan.md discovery matches
  the bash glob specs/*/plan.md (one level deep) — fixes cross-platform
  inconsistency with nested plan.md files
- powershell: replace Substring+Length relative-path with
  [System.IO.Path]::GetRelativePath for robustness across case/PSDrive
  differences
- __init__.py: move agent-context extension install to after
  save_init_options so init-options.json is present when hooks run
- __init__.py: seed context_markers in init-options only when
  context_file is truthy; avoids noise for integrations without a context
  file
- integrations/base.py: narrow blanket except Exception in
  _resolve_context_markers to ImportError / (OSError, ValueError) so
  unexpected bugs surface instead of being silently swallowed

* fix: gate context_markers in _update_init_options_for_integration on context_file

Apply the same gating logic used during `specify init`: only write
context_markers to init-options.json when the integration actually has a
context_file set.  When switching to an integration without a context file
the stale markers are removed, keeping the two init paths consistent.

* fix: move context_file/context_markers from init-options.json to agent-context extension config

* Potential fix for pull request finding 'Unused global variable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: clarify local import comment in agents.py

* Fix remaining agent-context review findings

* Fix follow-up agent-context review issues

* Address review feedback: narrow except, improve PyYAML messaging, surface config-written note

* Fix double-space in PyYAML install hint message

* Potential fix for pull request finding 'Empty except'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'Empty except'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Address latest agent-context review feedback

* Harden bash config parse output handling

* Clarify ImportError-only fallback comment

* Apply review feedback: drop dead try/except, guard ext-config creation, explicit ConvertFrom-Yaml check

* Remove redundant $Options = $null in PS1 catch block

* Add constitution directives, deprecation warning, agent-context auto-install, and init flow fix

- Add constitution-loading directive to specify, clarify, tasks, checklist, taskstoissues commands
- Add deprecation warning (v0.12.0) in upsert_context_section()
- Auto-install agent-context extension during specify init
- Move context_file from init-options.json to agent-context extension config
- Add tests: deprecation warning, corrupt config, constitution directives
- Update file inventories across all integration tests

* Address review: fix init ordering, test coverage, and hermes inventory

- Move agent-context extension install after init-options.json is saved
  so skill registration can read ai_skills + integration key
- Write extension config after install (avoids template overwriting context_file)
- Fix test_defaults_when_markers_field_missing to truly test missing markers key
- Update hermes tests to allow extension-installed agent-context skill

* Address review: chmod ordering, preserve markers, PS1 Python check, YAML key order

- Move ensure_executable_scripts after agent-context extension install
  so extension scripts get execute bits set
- Use preserve_markers=True on reinit to keep user-customized markers
- Add Python 3 version check in PowerShell fallback (matching bash behavior)
- Add sort_keys=False to yaml.safe_dump for stable config output

* Address review: path traversal guards and docstring fix

- Reject absolute paths and '..' segments in context_file in both bash and
  PowerShell scripts to prevent writes outside the project root
- Fix docstring in _update_init_options_for_integration to accurately
  describe marker preservation behavior

* Address review: strict enabled check, docstring, segment-level path traversal

- Use 'is not False' for enabled check so only literal False disables
- Update upsert_context_section docstring to mention disabled-extension return
- Fix path traversal guards to check actual path segments, not substrings
  (allows filenames like 'notes..md' while rejecting '../' traversal)

* Address review: UnicodeError handling, missing extension warning

- Add UnicodeError to exception tuples in _load_agent_context_config and
  _resolve_context_markers so garbled UTF-8 config files fall back to defaults
- Emit error (with reinstall command) instead of silent skip when bundled
  agent-context extension is not found during init

* Address review: bash backslash traversal guard, wheel packaging

- Reject backslash separators and Windows drive-letter paths in bash
  context_file validation (prevents traversal on Git-Bash/Windows)
- Add extensions/agent-context to pyproject.toml force-include so the
  bundled extension is included in wheel builds

* Address review: write extension config before init-options.json

- Reorder writes in _update_init_options_for_integration so the
  agent-context extension config is updated first; if it fails,
  init-options.json remains consistent with the previous state

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-05-30 06:37:18 -05:00

437 lines
18 KiB
Markdown

# AGENTS.md
## About Spec Kit and Specify
**GitHub Spec Kit** is a comprehensive toolkit for implementing Spec-Driven Development (SDD) - a methodology that emphasizes creating clear specifications before implementation. The toolkit includes templates, scripts, and workflows that guide development teams through a structured approach to building software.
**Specify CLI** is the command-line interface that bootstraps projects with the Spec Kit framework. It sets up the necessary directory structures, templates, and AI agent integrations to support the Spec-Driven Development workflow.
The toolkit supports multiple AI coding assistants, allowing teams to use their preferred tools while maintaining consistent project structure and development practices.
---
## Integration Architecture
Each AI agent is a self-contained **integration subpackage** under `src/specify_cli/integrations/<key>/`. The subpackage exposes a single class that declares all metadata and inherits setup/teardown logic from a base class. Built-in integrations are then instantiated and added to the global `INTEGRATION_REGISTRY` by `src/specify_cli/integrations/__init__.py` via `_register_builtins()`.
```
src/specify_cli/integrations/
├── __init__.py # INTEGRATION_REGISTRY + _register_builtins()
├── base.py # IntegrationBase, MarkdownIntegration, TomlIntegration, YamlIntegration, SkillsIntegration
├── manifest.py # IntegrationManifest (file tracking)
├── claude/ # Example: SkillsIntegration subclass
│ └── __init__.py # ClaudeIntegration class
├── gemini/ # Example: TomlIntegration subclass
│ └── __init__.py
├── windsurf/ # Example: MarkdownIntegration subclass
│ └── __init__.py
├── copilot/ # Example: IntegrationBase subclass (custom setup)
│ └── __init__.py
└── ... # One subpackage per supported agent
```
The registry is the **single source of truth for Python integration metadata**. Supported agents, their directories, formats, capabilities, and context files are derived from the integration classes for the Python integration layer.
---
## Adding a New Integration
### 1. Choose a base class
| Your agent needs… | Subclass |
|---|---|
| Standard markdown commands (`.md`) | `MarkdownIntegration` |
| TOML-format commands (`.toml`) | `TomlIntegration` |
| YAML recipe files (`.yaml`) | `YamlIntegration` |
| Skill directories (`speckit-<name>/SKILL.md`) | `SkillsIntegration` |
| Fully custom output (companion files, settings merge, etc.) | `IntegrationBase` directly |
Most agents only need `MarkdownIntegration` — a minimal subclass with zero method overrides.
### 2. Create the subpackage
Create `src/specify_cli/integrations/<package_dir>/__init__.py`, where `<package_dir>` is the Python-safe directory name derived from `<key>`: use the key as-is when it contains no hyphens (e.g., key `"gemini"``gemini/`), or replace hyphens with underscores when it does (e.g., key `"kiro-cli"``kiro_cli/`). The `IntegrationBase.key` class attribute always retains the original hyphenated value, since that is what the CLI and registry use. For CLI-based integrations (`requires_cli: True`), the `key` should match the actual CLI tool name (the executable users install and run) so CLI checks can resolve it correctly. For IDE-based integrations (`requires_cli: False`), use the canonical integration identifier instead.
**Minimal example — Markdown agent (Windsurf):**
```python
"""Windsurf IDE integration."""
from ..base import MarkdownIntegration
class WindsurfIntegration(MarkdownIntegration):
key = "windsurf"
config = {
"name": "Windsurf",
"folder": ".windsurf/",
"commands_subdir": "workflows",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".windsurf/workflows",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = ".windsurf/rules/specify-rules.md"
```
**TOML agent (Gemini):**
```python
"""Gemini CLI integration."""
from ..base import TomlIntegration
class GeminiIntegration(TomlIntegration):
key = "gemini"
config = {
"name": "Gemini CLI",
"folder": ".gemini/",
"commands_subdir": "commands",
"install_url": "https://github.com/google-gemini/gemini-cli",
"requires_cli": True,
}
registrar_config = {
"dir": ".gemini/commands",
"format": "toml",
"args": "{{args}}",
"extension": ".toml",
}
context_file = "GEMINI.md"
```
**Skills agent (Codex):**
```python
"""Codex CLI integration — skills-based agent."""
from __future__ import annotations
from ..base import IntegrationOption, SkillsIntegration
class CodexIntegration(SkillsIntegration):
key = "codex"
config = {
"name": "Codex CLI",
"folder": ".agents/",
"commands_subdir": "skills",
"install_url": "https://github.com/openai/codex",
"requires_cli": True,
}
registrar_config = {
"dir": ".agents/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "AGENTS.md"
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (default for Codex)",
),
]
```
#### Required fields
| Field | Location | Purpose |
|---|---|---|
| `key` | Class attribute | Unique identifier; for CLI-based integrations (`requires_cli: True`), must match the CLI executable name |
| `config` | Class attribute (dict) | Agent metadata: `name`, `folder`, `commands_subdir`, `install_url`, `requires_cli` |
| `registrar_config` | Class attribute (dict) | Command output config: `dir`, `format`, `args` placeholder, file `extension` |
| `context_file` | Class attribute (str or None) | Path to agent context/instructions file (e.g., `"CLAUDE.md"`, `".github/copilot-instructions.md"`) |
**Key design rule:** For CLI-based integrations (`requires_cli: True`), `key` must be the actual executable name (e.g., `"cursor-agent"` not `"cursor"`). This ensures `shutil.which(key)` works for CLI-tool checks without special-case mappings. IDE-based integrations (`requires_cli: False`) should use their canonical identifier (e.g., `"windsurf"`, `"copilot"`).
### 3. Register it
In `src/specify_cli/integrations/__init__.py`, add one import and one `_register()` call inside `_register_builtins()`. Both lists are alphabetical:
```python
def _register_builtins() -> None:
# -- Imports (alphabetical) -------------------------------------------
from .claude import ClaudeIntegration
# ...
from .newagent import NewAgentIntegration # ← add import
# ...
# -- Registration (alphabetical) --------------------------------------
_register(ClaudeIntegration())
# ...
_register(NewAgentIntegration()) # ← add registration
# ...
```
### 4. Context file behavior
Set `context_file` on the integration class. The base integration setup creates or updates the managed Spec Kit section in that file, and uninstall removes the managed section when appropriate.
The managed section is owned by the bundled `agent-context` extension (`extensions/agent-context/`). All configuration flows through the extension's own config file at `.specify/extensions/agent-context/agent-context-config.yml`:
```yaml
# Path to the coding agent context file managed by this extension
context_file: CLAUDE.md
# Delimiters for the managed Spec Kit section
context_markers:
start: "<!-- SPECKIT START -->"
end: "<!-- SPECKIT END -->"
```
- `context_file` is written automatically from the integration's class attribute when `specify init` or `specify integration use` is run.
- `context_markers.{start,end}` defaults to `IntegrationBase.CONTEXT_MARKER_START` / `CONTEXT_MARKER_END`. Users who want custom markers edit `agent-context-config.yml` directly — both the Python layer (`upsert_context_section()` / `remove_context_section()`) and the bundled scripts (`extensions/agent-context/scripts/bash/update-agent-context.sh` and `.ps1`) read from this single source of truth.
Users can opt out entirely with `specify extension disable agent-context`; while disabled, Spec Kit skips context-file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
Only add custom setup logic when the agent needs non-standard behavior. Integrations no longer require per-agent thin wrapper scripts or shared context-update dispatcher scripts — the `agent-context` extension is fully generic.
### 5. Test it
```bash
# Install into a test project
specify init my-project --integration <key>
# Verify files were created in the commands directory configured by
# config["folder"] + config["commands_subdir"] (for example, .windsurf/workflows/)
ls -R my-project/.windsurf/workflows/
# Uninstall cleanly
cd my-project && specify integration uninstall <key>
```
Each integration also has a dedicated test file at `tests/integrations/test_integration_<key>.py`. Note that hyphens in the key are replaced with underscores in the filename (e.g., key `cursor-agent``test_integration_cursor_agent.py`, key `kiro-cli``test_integration_kiro_cli.py`). Run it with:
```bash
pytest tests/integrations/test_integration_<key_with_underscores>.py -v
```
### 6. Optional overrides
The base classes handle most work automatically. Override only when the agent deviates from standard patterns:
| Override | When to use | Example |
|---|---|---|
| `command_filename(template_name)` | Custom file naming or extension | Copilot → `speckit.{name}.agent.md` |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag, Copilot → `--skills` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` (default) or `speckit-<name>/SKILL.md` (skills mode) |
| `teardown()` | Custom uninstall logic | Rarely needed; base handles manifest-tracked files |
**Example — Copilot (fully custom `setup`):**
Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. It also supports a `--skills` mode that scaffolds `speckit-<name>/SKILL.md` under `.github/skills/` using composition with an internal `_CopilotSkillsHelper`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
### 7. Update Devcontainer files (Optional)
For agents that have VS Code extensions or require CLI installation, update the devcontainer configuration files:
#### VS Code Extension-based Agents
For agents available as VS Code extensions, add them to `.devcontainer/devcontainer.json`:
```jsonc
{
"customizations": {
"vscode": {
"extensions": [
// ... existing extensions ...
"[New Agent Extension ID]"
]
}
}
}
```
#### CLI-based Agents
For agents that require CLI tools, add installation commands to `.devcontainer/post-create.sh`:
```bash
#!/bin/bash
# Existing installations...
echo -e "\n🤖 Installing [New Agent Name] CLI..."
# run_command "npm install -g [agent-cli-package]@latest"
echo "✅ Done"
```
---
## Command File Formats
### Markdown Format
**Standard format:**
```markdown
---
description: "Command description"
---
Command content with {SCRIPT} and $ARGUMENTS placeholders.
```
**GitHub Copilot Chat Mode format:**
```markdown
---
description: "Command description"
mode: speckit.command-name
---
Command content with {SCRIPT} and $ARGUMENTS placeholders.
```
### TOML Format
```toml
description = "Command description"
prompt = """
Command content with {SCRIPT} and {{args}} placeholders.
"""
```
### YAML Format
Used by: Goose
```yaml
version: 1.0.0
title: "Command Title"
description: "Command description"
author:
contact: spec-kit
extensions:
- type: builtin
name: developer
activities:
- Spec-Driven Development
prompt: |
Command content with {SCRIPT} and {{args}} placeholders.
```
## Argument Patterns
Different agents use different argument placeholders. The placeholder used in command files is always taken from `registrar_config["args"]` for each integration — check there first when in doubt:
- **Markdown/prompt-based**: `$ARGUMENTS` (default for most markdown agents)
- **TOML-based**: `{{args}}` (e.g., Gemini)
- **YAML-based**: `{{args}}` (e.g., Goose)
- **Custom**: some agents override the default (e.g., Forge uses `{{parameters}}`)
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
- **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
**Skills mode (`--skills`):** Copilot also supports an alternative skills-based layout
via `--integration-options="--skills"`. When enabled:
- Commands are scaffolded as `speckit-<name>/SKILL.md` under `.github/skills/`
- No companion `.prompt.md` files are generated
- No `.vscode/settings.json` merge
- `post_process_skill_content()` injects a `mode: speckit.<stem>` frontmatter field
- `build_command_invocation()` returns `/speckit-<stem>` instead of bare args
The two modes are mutually exclusive — a project uses one or the other:
```bash
# Default mode: .agent.md agents + .prompt.md companions + settings merge
specify init my-project --integration copilot
# Skills mode: speckit-<name>/SKILL.md under .github/skills/
specify init my-project --integration copilot --integration-options="--skills"
```
### 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
### Goose Integration
Goose is a YAML-format agent using Block's recipe system:
- Uses `.goose/recipes/` directory for YAML recipe files
- Uses `{{args}}` argument placeholder
- Produces YAML with `prompt: |` block scalar for command content
Implementation: Extends `YamlIntegration` (parallel to `TomlIntegration`):
1. Processes templates through the standard placeholder pipeline
2. Extracts title and description from frontmatter
3. Renders output as Goose recipe YAML (version, title, description, author, extensions, activities, prompt)
4. Uses `yaml.safe_dump()` for header fields to ensure proper escaping
5. Sets `context_file = "AGENTS.md"` so the base setup manages the Spec Kit context section there
## Branch Naming Convention
Branches follow one of two patterns depending on whether an issue exists:
```
<type>/<number>-<short-slug> # when an issue is created first
<type>/<short-slug> # when no issue exists (PR-only changes)
```
When an issue exists, include its number immediately after the prefix — this is what makes branches traceable. For small or self-contained changes that go straight to a PR without a tracking issue, omit the number.
| Prefix | When to use | Example |
|---|---|---|
| `feat/` | New features | `feat/2342-workflow-cli-alignment` |
| `fix/` | Bug fixes | `fix/2653-paths-only-validation` |
| `docs/` | Documentation changes | `docs/2677-branch-naming-convention`, `docs/update-landing-stats` |
| `community/` | Community catalog additions | `community/2492-add-mde-extension` |
| `chore/` | Maintenance, tooling, CI | `chore/2366-editorconfig` |
**Rules:**
1. Include the issue number when one exists — this is what makes branches traceable
2. Use kebab-case for the slug
3. Keep the slug short — enough to identify the work without looking up the issue
---
## Common Pitfalls
1. **Using shorthand keys for CLI-based integrations**: For CLI-based integrations (`requires_cli: True`), the `key` must match the executable name (e.g., `"cursor-agent"` not `"cursor"`). `shutil.which(key)` is used for CLI tool checks — mismatches require special-case mappings. IDE-based integrations (`requires_cli: False`) are not subject to this constraint.
2. **Forgetting context configuration**: The bundled `agent-context` extension reads from `.specify/extensions/agent-context/agent-context-config.yml`. New integrations only need to set `context_file` on the class — markers and dispatcher scripts are managed centrally.
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that have a CLI tool; set to `False` for IDE-based agents.
4. **Wrong argument format**: Use `$ARGUMENTS` for Markdown agents, `{{args}}` for TOML agents.
5. **Skipping registration**: The import and `_register()` call in `_register_builtins()` must both be added.
---
*This documentation should be updated whenever new integrations are added to maintain accuracy and completeness.*