Skip to content

Commit 6bb1846

Browse files
author
iamaeroplane
committed
feat(extensions): neutral behavior vocabulary, invocability fix, and extension path rewriting
1 parent ab9c702 commit 6bb1846

25 files changed

Lines changed: 3235 additions & 36 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ env/
3333
*.swo
3434
.DS_Store
3535
*.tmp
36+
.venv312
37+
.specify
38+
.gitignore
39+
.claude
3640

3741
# Project specific
3842
*.log
@@ -45,6 +49,8 @@ env/
4549
*.zip
4650
sdd-*/
4751
docs/dev
52+
specs
53+
4854

4955
# Extension system
5056
.specify/extensions/.cache/
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Extension Behavior & Deployment — RFC Addendum
2+
3+
## Overview
4+
5+
Extension commands can declare two new frontmatter sections:
6+
7+
1. **`behavior:`** — agent-neutral intent vocabulary
8+
2. **`agents:`** — per-agent escape hatch for fields with no neutral equivalent
9+
10+
Deployment target is fully derived from `behavior.execution` — no separate manifest field is needed.
11+
12+
---
13+
14+
## `behavior:` Vocabulary
15+
16+
```yaml
17+
behavior:
18+
execution: command | isolated | agent
19+
capability: fast | balanced | strong
20+
effort: low | medium | high | max
21+
tools: none | read-only | write | full | <custom>
22+
invocation: explicit | automatic
23+
visibility: user | model | both
24+
color: red | blue | green | yellow | purple | orange | pink | cyan
25+
```
26+
27+
### Per-agent translation
28+
29+
| behavior field | value | Claude | Copilot | Codex | Others |
30+
|---|---|---|---|---|---|
31+
| `execution` | `isolated` | `context: fork` | — | — | — |
32+
| `execution` | `agent` | routing only (see Deployment section) | — | — | — |
33+
| `capability` | `fast` | `model: claude-haiku-4-5-20251001` | `model: Claude Haiku 4.5` | — | — |
34+
| `capability` | `balanced` | `model: claude-sonnet-4-6` | `model: Claude Sonnet 4.5` | — | — |
35+
| `capability` | `strong` | `model: claude-opus-4-6` | `model: Claude Opus 4.5` | — | — |
36+
| `effort` | any | `effort: {value}` | — | `effort: {value}` | — |
37+
| `tools` | `read-only` | `allowed-tools: Read Grep Glob` | `tools: [read_file, list_directory, search_files]` | — | — |
38+
| `tools` | `write` | `allowed-tools: Read Write Edit Grep Glob` | `tools: ["*"]` | — | — |
39+
| `tools` | `none` | `allowed-tools: ""` | `tools: []` | — | — |
40+
| `tools` | `full` | — (no restriction, all tools available) | `tools: ["*"]` | — | — |
41+
| `tools` | `<custom string>` | `allowed-tools: <value>` (literal passthrough) | — | — | — |
42+
| `tools` | `<yaml list>` | `allowed-tools: <space-joined items>` | — | — | — |
43+
| `invocation` | `explicit` | `disable-model-invocation: true` | `disable-model-invocation: true` | — | — |
44+
| `invocation` | `automatic` | `disable-model-invocation: false` | `disable-model-invocation: false` | — | — |
45+
| `visibility` | `user` | `user-invocable: true` | `user-invocable: true` | — | — |
46+
| `visibility` | `model` | `user-invocable: false` | `user-invocable: false` | — | — |
47+
| `visibility` | `both` | — | — | — | — |
48+
| `color` | any valid value | `color: {value}` | — | — | — |
49+
50+
Cells marked `—` mean "no concept, field omitted silently."
51+
52+
> **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.
53+
54+
### `tools` presets and custom values (Claude)
55+
56+
The `tools` field accepts four named presets or a custom value:
57+
58+
| value | `allowed-tools` written | use case |
59+
|---|---|---|
60+
| `none` | `""` (empty — no tools) | pure reasoning, no file access |
61+
| `read-only` | `Read Grep Glob` | read/search, no writes |
62+
| `write` | `Read Write Edit Grep Glob` | file reads + writes, no shell |
63+
| `full` | _(key omitted)_ | all tools including Bash |
64+
65+
For anything outside these presets, pass a **custom string** or **YAML list** — it is written verbatim as `allowed-tools`:
66+
67+
```yaml
68+
# Custom string (space-separated)
69+
behavior:
70+
tools: "Read Write Bash"
71+
72+
# YAML list (joined with spaces)
73+
behavior:
74+
tools:
75+
- Read
76+
- Write
77+
- Bash
78+
```
79+
80+
> Custom values bypass preset lookup entirely and are not validated. Use named presets whenever possible.
81+
82+
### `color` (Claude Code only)
83+
84+
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.
85+
86+
---
87+
88+
## `agents:` Escape Hatch
89+
90+
For fields with no neutral equivalent, declare them per-agent:
91+
92+
```yaml
93+
agents:
94+
claude:
95+
paths: "src/**"
96+
argument-hint: "Path to the codebase"
97+
copilot:
98+
someCustomKey: someValue
99+
```
100+
101+
Agent-specific overrides win over `behavior:` translations.
102+
103+
---
104+
105+
## Deployment Routing from `behavior.execution`
106+
107+
Deployment target is fully derived from `behavior.execution` in the command file — no separate manifest field needed.
108+
109+
| `behavior.execution` | Claude | Copilot | Codex | Others |
110+
|---|---|---|---|---|
111+
| `command` (default) | `.claude/skills/{name}/SKILL.md` | `.github/agents/{name}.agent.md` | `.agents/skills/{name}/SKILL.md` | per-agent format |
112+
| `isolated` | `.claude/skills/{name}/SKILL.md` + `context: fork` | `.github/agents/{name}.agent.md` + `mode: agent` | per-agent format | per-agent format |
113+
| `agent` | `.claude/agents/{name}.md` | `.github/agents/{name}.agent.md` + `mode: agent` + `tools:` | not supported | not supported |
114+
115+
### Agent definition format (Claude, `execution: agent`)
116+
117+
Spec-kit writes a Claude agent definition file at `.claude/agents/{name}.md`.
118+
The body becomes the **system prompt**. Frontmatter is minimal — no
119+
`user-invocable`, `disable-model-invocation`, `context`, or `metadata` keys.
120+
121+
```markdown
122+
---
123+
name: speckit-revenge-analyzer
124+
description: Codebase analyzer subagent
125+
model: claude-opus-4-6
126+
tools: Read Grep Glob
127+
---
128+
You are a codebase analysis specialist...
129+
```
130+
131+
### Deferred: `execution: isolated` as agent definition
132+
133+
It is theoretically possible to want a command that runs in an isolated
134+
context (`context: fork`) AND is deployed as a named agent definition
135+
(`.claude/agents/`). These two concerns are orthogonal — isolation is a
136+
runtime concern, agent definition is a deployment concern.
137+
138+
This combination is **not supported** in this implementation. `execution:
139+
isolated` always deploys as a skill file. Decoupling runtime context from
140+
deployment target is deferred until a concrete use case requires it.
141+
142+
---
143+
144+
## Full Example: Orchestrator + Reusable Subagent
145+
146+
**`extension.yml`** (no manifest `type` field — deployment derived from command frontmatter):
147+
```yaml
148+
provides:
149+
commands:
150+
- name: speckit.revenge.extract
151+
file: commands/extract.md
152+
153+
- name: speckit.revenge.analyzer
154+
file: commands/analyzer.md
155+
```
156+
157+
**`commands/extract.md`** (orchestrator skill — no `execution:` → deploys to skills):
158+
```markdown
159+
---
160+
description: Run the extraction pipeline
161+
behavior:
162+
invocation: automatic
163+
agents:
164+
claude:
165+
argument-hint: "Path to codebase (optional)"
166+
---
167+
Orchestrate extraction for $ARGUMENTS...
168+
```
169+
170+
**`commands/analyzer.md`** (reusable subagent — `execution: agent` → deploys to `.claude/agents/`):
171+
```markdown
172+
---
173+
description: Analyze codebase structure and extract domain information
174+
behavior:
175+
execution: agent
176+
capability: strong
177+
tools: read-only
178+
color: green
179+
agents:
180+
claude:
181+
paths: "src/**"
182+
---
183+
You are a codebase analysis specialist.
184+
Analyze $ARGUMENTS and return structured domain findings.
185+
```
186+
187+
The deployed `.claude/agents/speckit-revenge-analyzer.md` will contain:
188+
189+
```markdown
190+
---
191+
name: speckit-revenge-analyzer
192+
description: Analyze codebase structure and extract domain information
193+
model: claude-opus-4-6
194+
tools: Read Grep Glob
195+
color: green
196+
---
197+
You are a codebase analysis specialist.
198+
...
199+
```
200+
201+
### `tools: write` example
202+
203+
Use `write` when an agent needs to create or modify files but does not need shell access (Bash):
204+
205+
```yaml
206+
behavior:
207+
execution: agent
208+
capability: strong
209+
tools: write # Read Write Edit Grep Glob — no Bash
210+
color: yellow
211+
```
212+
213+
### `tools: full` example
214+
215+
Use `full` when an agent needs unrestricted access including Bash (running tests, git commands, CLI tools):
216+
217+
```yaml
218+
behavior:
219+
execution: agent
220+
capability: strong
221+
tools: full # all tools; no allowed-tools key injected
222+
color: red
223+
```

extensions/RFC-EXTENSION-SYSTEM.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
16. [Resolved Questions](#resolved-questions)
2828
17. [Open Questions (Remaining)](#open-questions-remaining)
2929
18. [Appendices](#appendices)
30+
19. [RFC Addenda](#rfc-addenda)
3031

3132
---
3233

@@ -597,6 +598,8 @@ def convert_to_claude(
597598
dest.write_text(render_frontmatter(frontmatter) + "\n" + body)
598599
```
599600

601+
> **See also:** [RFC-EXTENSION-BEHAVIOR-DEPLOYMENT.md](RFC-EXTENSION-BEHAVIOR-DEPLOYMENT.md) — addendum covering agent-neutral `behavior:` vocabulary, per-agent translation, `agents:` escape hatch, and deployment routing (`behavior.execution: agent` → `.claude/agents/`).
602+
600603
---
601604

602605
## Configuration Management
@@ -1960,3 +1963,13 @@ This RFC proposes a comprehensive extension system for Spec Kit that:
19601963
3. Should we support extension dependencies (extension A requires extension B)?
19611964
4. How should we handle extension deprecation/removal from catalog?
19621965
5. What level of sandboxing/permissions do we need in v1.0?
1966+
1967+
---
1968+
1969+
## RFC Addenda
1970+
1971+
Addenda extend this RFC with post-initial-implementation decisions. They are authoritative and supersede any conflicting content in the main RFC.
1972+
1973+
| Addendum | Topic | Status |
1974+
|---|---|---|
1975+
| [RFC-EXTENSION-BEHAVIOR-DEPLOYMENT.md](RFC-EXTENSION-BEHAVIOR-DEPLOYMENT.md) | Agent-neutral `behavior:` vocabulary, deployment routing from `behavior.execution`, `agents:` escape hatch | Implemented (2026-04-08) |

src/specify_cli/__init__.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4568,12 +4568,13 @@ def extension_update(
45684568
shutil.rmtree(backup_ext_dir)
45694569
shutil.copytree(extension_dir, backup_ext_dir)
45704570

4571-
# Backup config files separately so they can be restored
4572-
# after a successful install (install_from_directory clears dest dir).
4573-
config_files = list(extension_dir.glob("*-config.yml")) + list(
4574-
extension_dir.glob("*-config.local.yml")
4575-
)
4576-
for cfg_file in config_files:
4571+
# Backup all top-level .yml files (except extension.yml) so they
4572+
# can be restored after install. Covers user configs (*-config.yml,
4573+
# *-config.local.yml) and any runtime-generated state files that
4574+
# extensions write to their own directory.
4575+
for cfg_file in extension_dir.glob("*.yml"):
4576+
if cfg_file.name == "extension.yml":
4577+
continue
45774578
backup_config_dir.mkdir(parents=True, exist_ok=True)
45784579
shutil.copy2(cfg_file, backup_config_dir / cfg_file.name)
45794580

@@ -4652,12 +4653,24 @@ def extension_update(
46524653
# 8. Install new version
46534654
_ = manager.install_from_zip(zip_path, speckit_version)
46544655

4655-
# Restore user config files from backup after successful install.
4656+
# Restore backed-up files after successful install.
4657+
# User config files (*-config.yml, *-config.local.yml) always
4658+
# overwrite the new extension defaults — user settings win.
4659+
# All other .yml files (runtime-generated state) are restored
4660+
# only when the new extension version didn't ship that file,
4661+
# so updated source files are never rolled back.
46564662
new_extension_dir = manager.extensions_dir / extension_id
46574663
if backup_config_dir.exists() and new_extension_dir.exists():
46584664
for cfg_file in backup_config_dir.iterdir():
4659-
if cfg_file.is_file():
4660-
shutil.copy2(cfg_file, new_extension_dir / cfg_file.name)
4665+
if not cfg_file.is_file():
4666+
continue
4667+
dest = new_extension_dir / cfg_file.name
4668+
is_user_config = (
4669+
cfg_file.name.endswith("-config.yml")
4670+
or cfg_file.name.endswith("-config.local.yml")
4671+
)
4672+
if is_user_config or not dest.exists():
4673+
shutil.copy2(cfg_file, dest)
46614674

46624675
# 9. Restore metadata from backup (installed_at, enabled state)
46634676
if backup_registry_entry and isinstance(backup_registry_entry, dict):

0 commit comments

Comments
 (0)