Skip to content

Commit e70f079

Browse files
committed
feat(gemini): update skill files to use YAML frontmatter format
1 parent 3e4e8cf commit e70f079

6 files changed

Lines changed: 70 additions & 57 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
[build-system]
2-
requires = ["uv_build>=0.10.2,<0.11.1"]
2+
requires = ["uv-build>=0.10.2,<0.11.2"]
33
build-backend = "uv_build"
44

55
[project]
66
name = "pb-spec"
7-
version = "0.8.8"
7+
version = "0.8.9"
88
description = "Plan-Build Spec (pb-spec): A CLI tool for managing AI coding assistant skills"
99
readme = "README.md"
1010
license = "Apache-2.0"

src/pb_spec/platforms/gemini.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,22 @@
22

33
from __future__ import annotations
44

5-
import json
65
from pathlib import Path
76

8-
from pb_spec.platforms.base import SKILL_METADATA, PromptOnlyPlatform
7+
from pb_spec.platforms.base import SKILL_METADATA, Platform
98

109

11-
class GeminiPlatform(PromptOnlyPlatform):
12-
"""Gemini CLI — custom commands in .gemini/commands/<name>.toml."""
10+
class GeminiPlatform(Platform):
11+
"""Gemini CLI — skills as .gemini/skills/<name>/SKILL.md."""
1312

1413
name = "gemini"
1514

1615
def get_skill_path(self, cwd: Path, skill_name: str, global_install: bool = False) -> Path:
1716
if global_install:
18-
return Path.home() / ".gemini" / "commands" / f"{skill_name}.toml"
19-
return cwd / ".gemini" / "commands" / f"{skill_name}.toml"
17+
return Path.home() / ".gemini" / "skills" / skill_name / "SKILL.md"
18+
return cwd / ".gemini" / "skills" / skill_name / "SKILL.md"
2019

2120
def render_skill(self, skill_name: str, template_content: str) -> str:
22-
description = SKILL_METADATA.get(skill_name, "")
23-
desc_value = json.dumps(description, ensure_ascii=False)
24-
escaped = template_content.rstrip().replace("\\", "\\\\").replace('"', '\\"')
25-
prompt_value = '"""\n' + escaped + '\n"""'
26-
return f"description = {desc_value}\nprompt = {prompt_value}\n"
21+
description = SKILL_METADATA.get(skill_name, "").replace('"', '\\"')
22+
frontmatter = f'---\nname: {skill_name}\ndescription: "{description}"\n---\n\n'
23+
return frontmatter + template_content

tests/test_e2e.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,19 @@ def test_e2e_init_all_creates_complete_structure(tmp_path, monkeypatch, runner):
7272
).exists()
7373

7474
# Gemini files
75-
assert (tmp_path / ".gemini" / "commands" / "pb-init.toml").exists()
76-
assert (tmp_path / ".gemini" / "commands" / "pb-plan.toml").exists()
77-
assert (tmp_path / ".gemini" / "commands" / "pb-refine.toml").exists()
78-
assert (tmp_path / ".gemini" / "commands" / "pb-build.toml").exists()
75+
assert (tmp_path / ".gemini" / "skills" / "pb-init" / "SKILL.md").exists()
76+
assert (tmp_path / ".gemini" / "skills" / "pb-plan" / "SKILL.md").exists()
77+
assert (tmp_path / ".gemini" / "skills" / "pb-refine" / "SKILL.md").exists()
78+
assert (tmp_path / ".gemini" / "skills" / "pb-build" / "SKILL.md").exists()
79+
assert (
80+
tmp_path / ".gemini" / "skills" / "pb-plan" / "references" / "design_template.md"
81+
).exists()
82+
assert (
83+
tmp_path / ".gemini" / "skills" / "pb-plan" / "references" / "tasks_template.md"
84+
).exists()
85+
assert (
86+
tmp_path / ".gemini" / "skills" / "pb-build" / "references" / "implementer_prompt.md"
87+
).exists()
7988

8089
# Codex files
8190
assert (tmp_path / ".codex" / "prompts" / "pb-init.md").exists()
@@ -118,15 +127,16 @@ def test_e2e_copilot_no_frontmatter(tmp_path, monkeypatch, runner):
118127
assert not content.startswith("---"), f"Copilot {prompt} should not have frontmatter"
119128

120129

121-
def test_e2e_gemini_toml_format(tmp_path, monkeypatch, runner):
122-
"""Verify Gemini command files are TOML with description and prompt fields."""
130+
def test_e2e_gemini_frontmatter(tmp_path, monkeypatch, runner):
131+
"""Verify Gemini SKILL.md files have YAML frontmatter."""
123132
monkeypatch.chdir(tmp_path)
124133
runner.invoke(main, ["init", "--ai", "gemini"])
125134

126-
for command in ["pb-init", "pb-plan", "pb-refine", "pb-build"]:
127-
content = (tmp_path / ".gemini" / "commands" / f"{command}.toml").read_text()
128-
assert content.startswith('description = "'), f"Gemini {command} missing description field"
129-
assert '\nprompt = """\n' in content, f"Gemini {command} missing prompt field"
135+
for skill in ["pb-init", "pb-plan", "pb-refine", "pb-build"]:
136+
content = (tmp_path / ".gemini" / "skills" / skill / "SKILL.md").read_text()
137+
assert content.startswith("---\n"), f"Gemini {skill} missing frontmatter"
138+
assert "name:" in content, f"Gemini {skill} missing name: in frontmatter"
139+
assert "description:" in content, f"Gemini {skill} missing description: in frontmatter"
130140

131141

132142
def test_e2e_codex_frontmatter(tmp_path, monkeypatch, runner):
@@ -152,6 +162,9 @@ def test_e2e_references_exist(tmp_path, monkeypatch, runner):
152162
".opencode/skills/pb-plan/references/design_template.md",
153163
".opencode/skills/pb-plan/references/tasks_template.md",
154164
".opencode/skills/pb-build/references/implementer_prompt.md",
165+
".gemini/skills/pb-plan/references/design_template.md",
166+
".gemini/skills/pb-plan/references/tasks_template.md",
167+
".gemini/skills/pb-build/references/implementer_prompt.md",
155168
}
156169
for ref in expected_refs:
157170
ref_path = tmp_path / ref
@@ -215,18 +228,15 @@ def test_e2e_copilot_no_references_dir(tmp_path, monkeypatch, runner):
215228
assert child.is_file(), f"Unexpected directory in Copilot prompts: {child}"
216229

217230

218-
def test_e2e_gemini_and_codex_no_references_dir(tmp_path, monkeypatch, runner):
219-
"""Verify Gemini/Codex platforms do not create references directories."""
231+
def test_e2e_codex_no_references_dir(tmp_path, monkeypatch, runner):
232+
"""Verify Codex platform does not create references directories."""
220233
monkeypatch.chdir(tmp_path)
221-
runner.invoke(main, ["init", "--ai", "all"])
234+
runner.invoke(main, ["init", "--ai", "codex"])
222235

223-
for directory in [
224-
tmp_path / ".gemini" / "commands",
225-
tmp_path / ".codex" / "prompts",
226-
]:
227-
assert directory.exists()
228-
for child in directory.iterdir():
229-
assert child.is_file(), f"Unexpected directory in prompt folder: {child}"
236+
prompts_dir = tmp_path / ".codex" / "prompts"
237+
assert prompts_dir.exists()
238+
for child in prompts_dir.iterdir():
239+
assert child.is_file(), f"Unexpected directory in Codex prompts: {child}"
230240

231241

232242
def test_e2e_rendered_platforms_preserve_workflow_contract_language(tmp_path, monkeypatch, runner):
@@ -252,11 +262,6 @@ def test_e2e_rendered_platforms_preserve_workflow_contract_language(tmp_path, mo
252262
"pb-build": tmp_path / ".github" / "prompts" / "pb-build.prompt.md",
253263
"pb-refine": tmp_path / ".github" / "prompts" / "pb-refine.prompt.md",
254264
},
255-
"gemini": {
256-
"pb-plan": tmp_path / ".gemini" / "commands" / "pb-plan.toml",
257-
"pb-build": tmp_path / ".gemini" / "commands" / "pb-build.toml",
258-
"pb-refine": tmp_path / ".gemini" / "commands" / "pb-refine.toml",
259-
},
260265
"codex": {
261266
"pb-plan": tmp_path / ".codex" / "prompts" / "pb-plan.md",
262267
"pb-build": tmp_path / ".codex" / "prompts" / "pb-build.md",

tests/test_init.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,29 @@ def test_init_opencode(tmp_path, monkeypatch, runner):
9595

9696

9797
def test_init_gemini(tmp_path, monkeypatch, runner):
98-
"""pb-spec init --ai gemini creates .gemini command TOML files."""
98+
"""pb-spec init --ai gemini creates .gemini/skills/ SKILL.md files with references."""
9999
monkeypatch.chdir(tmp_path)
100100
result = runner.invoke(main, ["init", "--ai", "gemini"])
101101

102102
assert result.exit_code == 0, result.output
103103

104-
assert (tmp_path / ".gemini" / "commands" / "pb-init.toml").exists()
105-
assert (tmp_path / ".gemini" / "commands" / "pb-plan.toml").exists()
106-
assert (tmp_path / ".gemini" / "commands" / "pb-refine.toml").exists()
107-
assert (tmp_path / ".gemini" / "commands" / "pb-build.toml").exists()
104+
assert (tmp_path / ".gemini" / "skills" / "pb-init" / "SKILL.md").exists()
105+
assert (tmp_path / ".gemini" / "skills" / "pb-plan" / "SKILL.md").exists()
106+
assert (tmp_path / ".gemini" / "skills" / "pb-refine" / "SKILL.md").exists()
107+
assert (tmp_path / ".gemini" / "skills" / "pb-build" / "SKILL.md").exists()
108+
109+
# Reference files for pb-plan
110+
assert (
111+
tmp_path / ".gemini" / "skills" / "pb-plan" / "references" / "design_template.md"
112+
).exists()
113+
assert (
114+
tmp_path / ".gemini" / "skills" / "pb-plan" / "references" / "tasks_template.md"
115+
).exists()
116+
117+
# Reference files for pb-build
118+
assert (
119+
tmp_path / ".gemini" / "skills" / "pb-build" / "references" / "implementer_prompt.md"
120+
).exists()
108121

109122

110123
# --- pb init --ai codex ---
@@ -140,7 +153,7 @@ def test_init_all(tmp_path, monkeypatch, runner):
140153
# OpenCode
141154
assert (tmp_path / ".opencode" / "skills" / "pb-init" / "SKILL.md").exists()
142155
# Gemini
143-
assert (tmp_path / ".gemini" / "commands" / "pb-init.toml").exists()
156+
assert (tmp_path / ".gemini" / "skills" / "pb-init" / "SKILL.md").exists()
144157
# Codex
145158
assert (tmp_path / ".codex" / "prompts" / "pb-init.md").exists()
146159

@@ -168,7 +181,7 @@ def test_init_all_global_installs_to_agent_home_dirs(tmp_path, monkeypatch, runn
168181
# OpenCode
169182
assert (config_home / "opencode" / "skills" / "pb-init" / "SKILL.md").exists()
170183
# Gemini
171-
assert (home / ".gemini" / "commands" / "pb-init.toml").exists()
184+
assert (home / ".gemini" / "skills" / "pb-init" / "SKILL.md").exists()
172185
# Codex
173186
assert (home / ".codex" / "prompts" / "pb-init.md").exists()
174187

@@ -234,14 +247,13 @@ def test_init_copilot_no_frontmatter(tmp_path, monkeypatch, runner):
234247
assert not content.startswith("---")
235248

236249

237-
def test_init_gemini_toml_shape(tmp_path, monkeypatch, runner):
238-
"""Gemini command files should be TOML with description and prompt fields."""
250+
def test_init_gemini_has_frontmatter(tmp_path, monkeypatch, runner):
251+
"""Gemini SKILL.md files should start with YAML frontmatter."""
239252
monkeypatch.chdir(tmp_path)
240253
runner.invoke(main, ["init", "--ai", "gemini"])
241254

242-
content = (tmp_path / ".gemini" / "commands" / "pb-init.toml").read_text()
243-
assert content.startswith('description = "')
244-
assert '\nprompt = """\n' in content
255+
content = (tmp_path / ".gemini" / "skills" / "pb-init" / "SKILL.md").read_text()
256+
assert content.startswith("---")
245257

246258

247259
def test_init_codex_has_frontmatter(tmp_path, monkeypatch, runner):
@@ -267,7 +279,6 @@ def test_init_prompt_only_pb_build_contains_shared_implementer_contract(
267279
).read_text()
268280
rendered_prompt_only_outputs = {
269281
"copilot": (tmp_path / ".github" / "prompts" / "pb-build.prompt.md").read_text(),
270-
"gemini": (tmp_path / ".gemini" / "commands" / "pb-build.toml").read_text(),
271282
"codex": (tmp_path / ".codex" / "prompts" / "pb-build.md").read_text(),
272283
}
273284

tests/test_platforms.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test_opencode_skill_path():
4949
def test_gemini_skill_path():
5050
p = GeminiPlatform()
5151
cwd = Path("/project")
52-
assert p.get_skill_path(cwd, "pb-init") == Path("/project/.gemini/commands/pb-init.toml")
52+
assert p.get_skill_path(cwd, "pb-init") == Path("/project/.gemini/skills/pb-init/SKILL.md")
5353

5454

5555
def test_codex_skill_path():
@@ -94,7 +94,7 @@ def test_gemini_global_skill_path(monkeypatch):
9494
p = GeminiPlatform()
9595
cwd = Path("/project")
9696
assert p.get_skill_path(cwd, "pb-init", global_install=True) == Path(
97-
"/users/alice/.gemini/commands/pb-init.toml"
97+
"/users/alice/.gemini/skills/pb-init/SKILL.md"
9898
)
9999

100100

@@ -133,11 +133,11 @@ def test_opencode_render_has_yaml_frontmatter():
133133
assert "# Hello" in result
134134

135135

136-
def test_gemini_render_toml_prompt():
136+
def test_gemini_render_has_yaml_frontmatter():
137137
p = GeminiPlatform()
138138
result = p.render_skill("pb-init", "# Hello")
139-
assert result.startswith('description = "')
140-
assert 'prompt = """' in result
139+
assert result.startswith("---\n")
140+
assert "name: pb-init" in result
141141
assert "# Hello" in result
142142

143143

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)