Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ env/
*.swo
.DS_Store
*.tmp
.venv312
.specify
.gitignore
.claude

# Project specific
*.log
Expand All @@ -45,6 +49,8 @@ env/
*.zip
sdd-*/
docs/dev
specs


# Extension system
.specify/extensions/.cache/
Expand Down
40 changes: 37 additions & 3 deletions extensions/EXTENSION-API-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ provides:

config: # Optional, array of config files
- name: string # Config file name
template: string # Template file path
template: string # Template file path (copied to `name` on install if missing)
description: string
required: boolean # Default: false

Expand Down Expand Up @@ -514,11 +514,30 @@ field_mappings:

### Config Layers

1. **Extension Defaults** (from `extension.yml` `defaults` section)
1. **Extension Defaults** (from `extension.yml` `config.defaults` section)
2. **Project Config** (`{extension-id}-config.yml`)
3. **Local Override** (`{extension-id}-config.local.yml`, gitignored)
3. **Local Override** (`local-config.yml`, gitignored)
4. **Environment Variables** (`SPECKIT_{EXTENSION}_*`)

Backward compatibility: `local.yml` is also loaded as a legacy local-override file, but `local-config.yml` is canonical and takes precedence when both exist.

### Config Resolution Command

Use the built-in resolver instead of re-implementing YAML merge logic in each extension:

```bash
# Emit merged config as JSON
specify extension config resolve jira --format json

# Emit flattened env assignments for shell scripts
specify extension config resolve jira --format env --prefix JIRA_CFG_
```

For `--format env`, nested keys are flattened with `_`:

- `project.key` -> `JIRA_CFG_PROJECT_KEY`
- `feature.enabled` -> `JIRA_CFG_FEATURE_ENABLED`

### Environment Variable Pattern

Format: `SPECKIT_{EXTENSION}_{KEY}`
Expand Down Expand Up @@ -622,6 +641,21 @@ EXECUTE_COMMAND: {command}

**Output**: List of installed extensions with metadata

### extension config resolve

**Usage**: `specify extension config resolve EXTENSION [OPTIONS]`

Resolves layered extension configuration and emits either JSON or shell env assignments.

**Options**:

- `--format [json|env]` - Output mode (default: `json`)
- `--prefix TEXT` - Env key prefix for `--format env` (default: `EXTCFG_`)

**Arguments**:

- `EXTENSION` - Installed extension ID or display name

### extension catalog list

**Usage**: `specify extension catalog list`
Expand Down
35 changes: 22 additions & 13 deletions extensions/EXTENSION-DEVELOPMENT-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,30 +311,39 @@ credentials:
api_key: "${MY_EXT_API_KEY}"
```

When the extension is installed, each `provides.config` entry copies its
`template` file to `name` if the target file does not already exist.

### Config Loading

In your command, load config with layered precedence:
Use the built-in resolver command to load layered config:

1. Extension defaults (`extension.yml` → `defaults`)
1. Extension defaults (`extension.yml` → `config.defaults`)
2. Project config (`.specify/extensions/my-ext/my-ext-config.yml`)
3. Local overrides (`.specify/extensions/my-ext/my-ext-config.local.yml` - gitignored)
3. Local overrides (`.specify/extensions/my-ext/local-config.yml` - gitignored)
4. Environment variables (`SPECKIT_MY_EXT_*`)

**Example loading script**:
Note: `local.yml` is still read for backward compatibility, but `local-config.yml` is the canonical filename and wins when both are present.

**Recommended command usage**:

```bash
#!/usr/bin/env bash
EXT_DIR=".specify/extensions/my-ext"
# JSON for structured consumers
config_json=$(specify extension config resolve my-ext --format json)

# Load and merge config
config=$(yq eval '.' "$EXT_DIR/my-ext-config.yml" -o=json)
# Or flattened env vars for shell scripts
eval "$(specify extension config resolve my-ext --format env --prefix MY_EXT_CFG_)"
```

# Apply env overrides
if [ -n "${SPECKIT_MY_EXT_API_KEY:-}" ]; then
config=$(echo "$config" | jq ".api.api_key = \"$SPECKIT_MY_EXT_API_KEY\"")
fi
**Example script usage after env export**:

```bash
#!/usr/bin/env bash
set -euo pipefail

echo "$config"
eval "$(specify extension config resolve my-ext --format env --prefix MY_EXT_CFG_)"
echo "Endpoint: ${MY_EXT_CFG_API_ENDPOINT:-}"
echo "Timeout: ${MY_EXT_CFG_API_TIMEOUT:-30}"
```

---
Expand Down
16 changes: 16 additions & 0 deletions extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,20 @@ specify extension add <extension-name> --from https://github.com/<org>/<repo>/ar
specify extension list
```

## Resolving Extension Config

Extensions can expose layered config (defaults + project file + local overrides + env vars).
Use the built-in resolver instead of hand-rolling merge logic in every script:

```bash
# Emit merged config as JSON
specify extension config resolve <extension-id> --format json

# Emit flattened env assignments for shell scripts
eval "$(specify extension config resolve <extension-id> --format env --prefix EXT_CFG_)"
```

When an extension declares `provides.config` entries, install now materializes each
declared `name` from its `template` when the target file is missing.

For more information, see the [Extension User Guide](EXTENSION-USER-GUIDE.md).
223 changes: 223 additions & 0 deletions extensions/RFC-EXTENSION-BEHAVIOR-DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Extension Behavior & Deployment — RFC Addendum

## Overview

Extension commands can declare two new frontmatter sections:

1. **`behavior:`** — agent-neutral intent vocabulary
2. **`agents:`** — per-agent escape hatch for fields with no neutral equivalent

Deployment target is fully derived from `behavior.execution` — no separate manifest field is needed.

---

## `behavior:` Vocabulary

```yaml
behavior:
execution: command | isolated | agent
capability: fast | balanced | strong
effort: low | medium | high | max
tools: none | read-only | write | full | <custom>
invocation: explicit | automatic
visibility: user | model | both
color: red | blue | green | yellow | purple | orange | pink | cyan
```

### Per-agent translation

| behavior field | value | Claude | Copilot | Codex | Others |
|---|---|---|---|---|---|
| `execution` | `isolated` | `context: fork` | — | — | — |
| `execution` | `agent` | routing only (see Deployment section) | — | — | — |
| `capability` | `fast` | `model: claude-haiku-4-5-20251001` | `model: Claude Haiku 4.5` | — | — |
| `capability` | `balanced` | `model: claude-sonnet-4-6` | `model: Claude Sonnet 4.5` | — | — |
| `capability` | `strong` | `model: claude-opus-4-6` | `model: Claude Opus 4.5` | — | — |
| `effort` | any | `effort: {value}` | — | `effort: {value}` | — |
| `tools` | `read-only` | `allowed-tools: Read Grep Glob` | `tools: [read_file, list_directory, search_files]` | — | — |
| `tools` | `write` | `allowed-tools: Read Write Edit Grep Glob` | `tools: ["*"]` | — | — |
| `tools` | `none` | `allowed-tools: ""` | `tools: []` | — | — |
| `tools` | `full` | — (no restriction, all tools available) | `tools: ["*"]` | — | — |
| `tools` | `<custom string>` | `allowed-tools: <value>` (literal passthrough) | — | — | — |
| `tools` | `<yaml list>` | `allowed-tools: <space-joined items>` | — | — | — |
| `invocation` | `explicit` | `disable-model-invocation: true` | `disable-model-invocation: true` | — | — |
| `invocation` | `automatic` | `disable-model-invocation: false` | `disable-model-invocation: false` | — | — |
| `visibility` | `user` | `user-invocable: true` | `user-invocable: true` | — | — |
| `visibility` | `model` | `user-invocable: false` | `user-invocable: false` | — | — |
| `visibility` | `both` | — | — | — | — |
| `color` | any valid value | `color: {value}` | — | — | — |

Cells marked `—` mean "no concept, field omitted silently."

> **Note:** For Claude agent definitions (`execution: agent`), the `allowed-tools` key is automatically remapped to `tools` by spec-kit during deployment. The table above shows the `allowed-tools` form used in skill files (SKILL.md); the agent definition example below shows the resulting `tools` key after remapping.

### `tools` presets and custom values (Claude)

The `tools` field accepts four named presets or a custom value:

| value | `allowed-tools` written | use case |
|---|---|---|
| `none` | `""` (empty — no tools) | pure reasoning, no file access |
| `read-only` | `Read Grep Glob` | read/search, no writes |
| `write` | `Read Write Edit Grep Glob` | file reads + writes, no shell |
| `full` | _(key omitted)_ | all tools including Bash |

For anything outside these presets, pass a **custom string** or **YAML list** — it is written verbatim as `allowed-tools`:

```yaml
# Custom string (space-separated)
behavior:
tools: "Read Write Bash"

# YAML list (joined with spaces)
behavior:
tools:
- Read
- Write
- Bash
```

> Custom values bypass preset lookup entirely and are not validated. Use named presets whenever possible.

### `color` (Claude Code only)

Controls the UI color of the agent entry in the Claude Code task list and transcript. Accepted values: `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan`. The value is passed through verbatim to the agent definition frontmatter — no translation occurs. Other agents ignore this field.

---

## `agents:` Escape Hatch

For fields with no neutral equivalent, declare them per-agent:

```yaml
agents:
claude:
paths: "src/**"
argument-hint: "Path to the codebase"
copilot:
someCustomKey: someValue
```

Agent-specific overrides win over `behavior:` translations.

---

## Deployment Routing from `behavior.execution`

Deployment target is fully derived from `behavior.execution` in the command file — no separate manifest field needed.

| `behavior.execution` | Claude | Copilot | Codex | Others |
|---|---|---|---|---|
| `command` (default) | `.claude/skills/{name}/SKILL.md` | `.github/agents/{name}.agent.md` | `.agents/skills/{name}/SKILL.md` | per-agent format |
| `isolated` | `.claude/skills/{name}/SKILL.md` + `context: fork` | `.github/agents/{name}.agent.md` + `mode: agent` | per-agent format | per-agent format |
| `agent` | `.claude/agents/{name}.md` | `.github/agents/{name}.agent.md` + `mode: agent` + `tools:` | not supported | not supported |

### Agent definition format (Claude, `execution: agent`)

Spec-kit writes a Claude agent definition file at `.claude/agents/{name}.md`.
The body becomes the **system prompt**. Frontmatter is minimal — no
`user-invocable`, `disable-model-invocation`, `context`, or `metadata` keys.

```markdown
---
name: speckit-revenge-analyzer
description: Codebase analyzer subagent
model: claude-opus-4-6
tools: Read Grep Glob
---
You are a codebase analysis specialist...
```

### Deferred: `execution: isolated` as agent definition

It is theoretically possible to want a command that runs in an isolated
context (`context: fork`) AND is deployed as a named agent definition
(`.claude/agents/`). These two concerns are orthogonal — isolation is a
runtime concern, agent definition is a deployment concern.

This combination is **not supported** in this implementation. `execution:
isolated` always deploys as a skill file. Decoupling runtime context from
deployment target is deferred until a concrete use case requires it.

---

## Full Example: Orchestrator + Reusable Subagent

**`extension.yml`** (no manifest `type` field — deployment derived from command frontmatter):
```yaml
provides:
commands:
- name: speckit.revenge.extract
file: commands/extract.md

- name: speckit.revenge.analyzer
file: commands/analyzer.md
```

**`commands/extract.md`** (orchestrator skill — no `execution:` → deploys to skills):
```markdown
---
description: Run the extraction pipeline
behavior:
invocation: automatic
agents:
claude:
argument-hint: "Path to codebase (optional)"
---
Orchestrate extraction for $ARGUMENTS...
```

**`commands/analyzer.md`** (reusable subagent — `execution: agent` → deploys to `.claude/agents/`):
```markdown
---
description: Analyze codebase structure and extract domain information
behavior:
execution: agent
capability: strong
tools: read-only
color: green
agents:
claude:
paths: "src/**"
---
You are a codebase analysis specialist.
Analyze $ARGUMENTS and return structured domain findings.
```

The deployed `.claude/agents/speckit-revenge-analyzer.md` will contain:

```markdown
---
name: speckit-revenge-analyzer
description: Analyze codebase structure and extract domain information
model: claude-opus-4-6
tools: Read Grep Glob
color: green
---
You are a codebase analysis specialist.
...
```

### `tools: write` example

Use `write` when an agent needs to create or modify files but does not need shell access (Bash):

```yaml
behavior:
execution: agent
capability: strong
tools: write # Read Write Edit Grep Glob — no Bash
color: yellow
```

### `tools: full` example

Use `full` when an agent needs unrestricted access including Bash (running tests, git commands, CLI tools):

```yaml
behavior:
execution: agent
capability: strong
tools: full # all tools; no allowed-tools key injected
color: red
```
Loading