Skip to content

Commit 1562530

Browse files
committed
fix: address PR #562 review feedback
1 parent ce23b59 commit 1562530

30 files changed

Lines changed: 786 additions & 264 deletions

.gitignore

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ node_modules
3030

3131
# Codex CLI
3232
.codex/
33-
# AGENTS.md is committed as the canonical context file (per OpenAI harness engineering).
34-
# Codex/Cursor/OpenCode read it directly; Claude Code reads it via @AGENTS.md import
35-
# in CLAUDE.md; Gemini CLI reads it via .gemini/settings.json `context.fileName`.
3633

3734
# Cursor
3835
.cursor/
@@ -42,23 +39,18 @@ node_modules
4239
.opencode/
4340
opencode.json
4441

45-
.codex/
46-
.agents/
47-
48-
# OpenCode runtime (skills + workflows)
49-
.agents/
50-
51-
# Cursor agent workflows
52-
.agent/
42+
# Copilot
43+
.copilot/
5344

5445
# Gemini CLI (legacy paths kept for compatibility)
5546
commands/**/*.toml
5647
/commands/
5748
/agents/
5849
/skills/
5950

60-
# Local agent caches
61-
.agent/
62-
.agents/
51+
# Antigravity CLI
6352
.antigravity/
6453

54+
# Local agent runtime caches (IDE/2.0 discovery)
55+
.agent/
56+
.agents/

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ make generate HARNESS=codex # emits .codex/skills, .codex/agents
4545
make generate HARNESS=cursor # emits .cursor-plugin/, .cursor/rules/
4646
make generate HARNESS=opencode # emits .opencode/agents/, .opencode/commands/, .opencode/skills/
4747
make generate HARNESS=gemini # emits skills/, agents/, commands/ at extension root
48-
make generate HARNESS=antigravity # emits .antigravity/agents/, .antigravity/skills/
49-
make generate-all # all five
48+
make generate HARNESS=antigravity # emits .antigravity/agents/, .antigravity/skills/, .antigravity/workflows/
49+
make generate-all # all six
5050
make install-opencode # symlink generated OpenCode artifacts into global config
5151
```
5252

@@ -61,7 +61,7 @@ Source-of-truth lives only under `plugins/`. Generated artifacts are gitignored
6161
- **OpenCode**: mirrored to `.opencode/skills/<plugin>-<skill>/` using hyphenated names for global install
6262
- **Cursor**: reads `.claude/skills/` directly (no re-emit)
6363
- **Gemini CLI**: native skills at `skills/<plugin>__<skill>/SKILL.md`
64-
- **Antigravity CLI**: `.antigravity/skills/<plugin>-<skill>/SKILL.md` (lowercase, hyphen-separated names)
64+
- **Antigravity CLI**: `.antigravity/skills/<plugin>__<skill>/SKILL.md` (lowercase, double-underscore namespacing)
6565

6666
Top-level `skills/` is Gemini output; do not use it for OpenCode installs.
6767

@@ -73,7 +73,7 @@ Top-level `skills/` is Gemini output; do not use it for OpenCode installs.
7373
- **OpenCode**: `.opencode/agents/<plugin>__<agent>.md` with `mode: subagent` + `permission:` block (locked agents — those with source `tools: []` — get deny-everything except base `skill`/`task`)
7474
- **Gemini**: `agents/<plugin>__<agent>.md` (April 2026 subagent spec)
7575
- **Cursor**: reads `.claude/agents/` directly
76-
- **Antigravity**: `.antigravity/agents/<agent>/agent.json` (Gemini model aliases, lowercase tool names)
76+
- **Antigravity**: `.antigravity/agents/<plugin>__<agent>/agent.json` (Gemini model aliases, lowercase tool names)
7777

7878
## Why this file is short
7979

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ help:
2222
@echo "claude-agents — multi-harness plugin marketplace"
2323
@echo "================================================="
2424
@echo ""
25-
@echo "Multi-harness adapter (Codex / Cursor / OpenCode / Gemini):"
25+
@echo "Multi-harness adapter (Codex / Cursor / OpenCode / Gemini / Copilot / Antigravity):"
2626
@echo " make generate HARNESS=<h> [PLUGIN=<p>] Generate per-harness artifacts (defaults to all plugins)"
2727
@echo " make generate-all Generate for ALL harnesses + ALL plugins"
2828
@echo " make clean-generated [HARNESS=<h>] Remove generated artifacts"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ emits harness-native artifacts (not lowest-common-denominator translations):
8484
| **OpenCode** | `.opencode/agents/`, `.opencode/commands/`, `.opencode/skills/` | `permission:` block from `tools:` allowlist; OpenCode-safe skill names |
8585
| **Gemini CLI** | `skills/`, `agents/`, `commands/` (TOML) | Native skills + subagents (April 2026 spec) |
8686
| **Copilot** | `.copilot/agents/`, `.copilot/skills/`, `.copilot/commands/` | Markdown agent profiles + SKILL.md skills + commands-as-skills; model maps to GPT-5 family |
87-
| **Antigravity CLI** | `.antigravity/agents/`, `.antigravity/skills/`, `.antigravity/commands/` | Lowercase agent.json per agent + SKILL.md skills + commands-as-skills; model maps to Gemini 2.5 family |
87+
| **Antigravity CLI** | `.antigravity/agents/`, `.antigravity/skills/`, `.antigravity/workflows/` | Lowercase agent.json per agent + SKILL.md skills + workflows (commands); model maps to Gemini 2.5 family |
8888

8989
```bash
9090
make generate-all # all six
@@ -142,7 +142,7 @@ integrations for this marketplace's other supported harnesses.
142142
| OpenCode | [integrations/opencode-plugin](https://github.com/major7apps/pensyve/tree/main/integrations/opencode-plugin) |
143143
| Gemini CLI | `gemini extensions install https://github.com/major7apps/pensyve` |
144144
| Copilot | `.copilot/` in repo root or `~/.copilot/` via `make install-copilot` |
145-
| Antigravity CLI | `.antigravity/` in repo root or `~/.antigravity/` via `make install-antigravity` |
145+
| Antigravity CLI | `.antigravity/` in repo root or `~/.gemini/antigravity-cli/` via `make install-antigravity` |
146146

147147
## License
148148

docs/authoring.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ clean naming — pick distinct names for skill/command pairs within a plugin.
106106
### Model aliases
107107

108108
| Source field | Codex | Cursor | OpenCode | Gemini | Copilot | Antigravity CLI |
109-
|---|---|---|---|---|---|---|---|---|
109+
|---|---|---|---|---|---|---|---|
110110
| `model: opus` | `gpt-5` | `inherit` | `anthropic/claude-opus-4-7` | `gemini-2.5-pro` | `gpt-5` | `gemini-2.5-pro` |
111111
| `model: sonnet` | `gpt-5-mini` | `inherit` | `anthropic/claude-sonnet-4-6` | `gemini-2.5-pro` | `gpt-5-mini` | `gemini-2.5-pro` |
112112
| `model: haiku` | `gpt-5-nano` | `inherit` | `anthropic/claude-haiku-4-5-20251001` | `gemini-2.5-flash` | `gpt-5-nano` | `gemini-2.5-flash` |

docs/harnesses.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ as Claude Code markdown. Per-harness artifacts are generated by adapters under `
2323
|:---|---|---|---|---|---|---|
2424
| Skills (SKILL.md native) ||| ✅ via `.claude/` | ✅ via `.opencode/skills/` | ✅ (auto-discovered) ||
2525
| Subagents (markdown native) || TOML format | ✅ via `.claude/` | ✅ (different frontmatter) | ✅ (April 2026 spec) | JSON format |
26-
| Slash commands || converted to skills ||| TOML at `commands/` | |
26+
| Slash commands || converted to skills ||| TOML at `commands/` | ✅ via `.antigravity/workflows/` |
2727
| Plugin marketplace ||| ✅ (2.5+) || — (direct URL install) ||
2828
| Parallel subagents ||||| ✅ (April 2026) ||
2929
| Per-agent tool allowlist | ✅ (`tools:`) | only `sandbox_mode` | only `readonly:` | ✅ (`permission:` block) | ✅ (`tools:`) | ✅ (`tools:`) |
@@ -50,7 +50,7 @@ rules to write portable content.
5050
| `TodoWrite` in body | no equivalent — leave as-is | no equivalent — leave as-is | works as-is | no equivalent | no equivalent |
5151
| Skill body > 8 KB | split into `references/details.md` | passed through | passed through | passed through | passed through |
5252
| Agent named `worker` | namespaced to `<plugin>__worker` | passed through | passed through | passed through | namespaced to `<plugin>__<agent>` |
53-
| Slash command (`commands/<x>.md`) | converted to skill | passed through | rewritten to `.opencode/commands/` | TOML at `commands/<plugin>/<x>.toml` | converted to skill |
53+
| Slash command (`commands/<x>.md`) | converted to skill | passed through | rewritten to `.opencode/commands/` | TOML at `commands/<plugin>/<x>.toml` | rewritten to `.antigravity/workflows/<plugin>-<x>.md` |
5454

5555
## Output paths (all gitignored)
5656

tools/adapters/antigravity.py

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json
6+
import re
67
from pathlib import Path
78

89
from tools.adapters.base import (
@@ -17,28 +18,64 @@
1718
from tools.adapters.capabilities import TOOL_NAME_MAPS, resolve_model
1819

1920

21+
_YAML_NEEDS_QUOTING = re.compile(r"[:#{}[\]&*?|<>!=%@`,\n]|^\s+|\s+$|^- |^[>|]")
22+
23+
24+
def _needs_yaml_quoting(value: str) -> bool:
25+
"""True when a scalar value must be quoted to remain valid YAML."""
26+
if not isinstance(value, str):
27+
return True
28+
if not value:
29+
return True
30+
if value.lower() in ("true", "false", "yes", "no", "on", "off", "null", "~"):
31+
return True
32+
try:
33+
int(value)
34+
return True
35+
except ValueError:
36+
pass
37+
return bool(_YAML_NEEDS_QUOTING.search(value))
38+
39+
40+
def _yaml_value(value: str) -> str:
41+
"""Serialize a scalar value as safe YAML (quoting if necessary)."""
42+
if _needs_yaml_quoting(value):
43+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
44+
return f'"{escaped}"'
45+
return value
46+
47+
2048
def _antigravity_frontmatter(fm: dict) -> str:
2149
"""Format YAML frontmatter for Antigravity skill files."""
2250
lines = ["---"]
2351
for k, v in fm.items():
2452
if isinstance(v, list):
25-
value = ", ".join(str(x) for x in v)
26-
lines.append(f"{k}: [{value}]")
53+
items = ", ".join(_yaml_value(str(x)) for x in v)
54+
lines.append(f"{k}: [{items}]")
2755
elif isinstance(v, bool):
2856
lines.append(f"{k}: {'true' if v else 'false'}")
2957
elif v is not None:
3058
value = str(v).replace("\n", " ").strip()
31-
lines.append(f"{k}: {value}")
59+
lines.append(f"{k}: {_yaml_value(value)}")
3260
lines.append("---")
3361
return "\n".join(lines)
3462

3563

64+
_REWRITE_TOOL_RE = re.compile(r"`(\w+)`|\b(Read|Write|Edit|Grep|Bash|Task|Glob)\b")
65+
66+
3667
def _rewrite_body_lowercase_tools(body: str) -> str:
37-
"""Rewrite Claude Code CamelCase tool names in backticked references to Antigravity names."""
38-
out = body
39-
for camel, replacement in TOOL_NAME_MAPS["antigravity"].items():
40-
out = out.replace(f"`{camel}`", f"`{replacement}`")
41-
return out
68+
"""Rewrite Claude Code CamelCase tool names to Antigravity lowercase names."""
69+
tool_map = TOOL_NAME_MAPS["antigravity"]
70+
71+
def _replace(m: re.Match) -> str:
72+
camel = m.group(1) or m.group(2)
73+
lower = tool_map.get(camel, camel.lower())
74+
if m.group(1):
75+
return f"`{lower}`"
76+
return lower
77+
78+
return _REWRITE_TOOL_RE.sub(_replace, body)
4279

4380

4481
class AntigravityAdapter(HarnessAdapter):
@@ -71,7 +108,9 @@ def emit_global(self, plugins: list[PluginSource]) -> EmitResult:
71108
"""No cross-plugin artifacts needed for Antigravity."""
72109
return EmitResult()
73110

74-
def _emit_agent(self, plugin: PluginSource, agent: AgentSource, result: EmitResult) -> None:
111+
def _emit_agent(
112+
self, plugin: PluginSource, agent: AgentSource, result: EmitResult
113+
) -> None:
75114
"""Emit one custom agent configuration file (`agent.json`)."""
76115
agent_id = f"{plugin.name}__{agent.name}"
77116
agent_dir = Path(".antigravity") / "agents" / agent_id
@@ -105,15 +144,15 @@ def _emit_agent(self, plugin: PluginSource, agent: AgentSource, result: EmitResu
105144
"description": description,
106145
"hidden": False,
107146
"model": model,
108-
"customAgentSpec": {
109-
"customAgent": custom_agent
110-
}
147+
"customAgentSpec": {"customAgent": custom_agent},
111148
}
112149

113150
content = json.dumps(agent_config, indent=2) + "\n"
114151
result.written.append(self.write(rel_file, content))
115152

116-
def _emit_skill(self, plugin: PluginSource, skill: SkillSource, result: EmitResult) -> None:
153+
def _emit_skill(
154+
self, plugin: PluginSource, skill: SkillSource, result: EmitResult
155+
) -> None:
117156
"""Emit one skill into ``.antigravity/skills/<skill_id>/SKILL.md``."""
118157
skill_id = f"{plugin.name}__{skill.name}"
119158
skill_dir = Path(".antigravity") / "skills" / skill_id
@@ -130,9 +169,13 @@ def _emit_skill(self, plugin: PluginSource, skill: SkillSource, result: EmitResu
130169
for ref in sorted(skill.references_dir.rglob("*")):
131170
if ref.is_file():
132171
rel = ref.relative_to(skill.references_dir)
133-
result.written.append(self.mirror_file(ref, skill_dir / "references" / rel))
172+
result.written.append(
173+
self.mirror_file(ref, skill_dir / "references" / rel)
174+
)
134175

135-
def _emit_command_as_skill(self, plugin: PluginSource, command: CommandSource, result: EmitResult) -> None:
176+
def _emit_command_as_skill(
177+
self, plugin: PluginSource, command: CommandSource, result: EmitResult
178+
) -> None:
136179
"""Emit one command as an Antigravity skill."""
137180
skill_id = f"{plugin.name}-{command.name}"
138181
skill_dir = Path(".antigravity") / "skills" / skill_id
@@ -159,13 +202,19 @@ def _emit_command_as_skill(self, plugin: PluginSource, command: CommandSource, r
159202
extra_skill_dir = skill_parent / "skills" / skill_id
160203
result.written.append(self.write(extra_skill_dir / "SKILL.md", content))
161204

162-
def _emit_command_as_workflow(self, plugin: PluginSource, command: CommandSource, result: EmitResult) -> None:
205+
def _emit_command_as_workflow(
206+
self, plugin: PluginSource, command: CommandSource, result: EmitResult
207+
) -> None:
163208
"""Emit one command as an Antigravity workflow (``.md``) for IDE/2.0 slash command support."""
164209
name = f"{plugin.name}-{command.name}"
165210
workflow_dir = Path(".antigravity") / "workflows"
166211
rel_file = workflow_dir / f"{name}.md"
167212

168-
description = command.description or h1_from_body(command.body) or name.replace("-", " ").title()
213+
description = (
214+
command.description
215+
or h1_from_body(command.body)
216+
or name.replace("-", " ").title()
217+
)
169218
body = _rewrite_body_lowercase_tools(command.body).rstrip() + "\n"
170219

171220
fm: dict = {"description": description}

tools/adapters/base.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ def parse_frontmatter(content: str) -> tuple[dict, str]:
9999
text = line.strip()
100100
if text:
101101
existing = fields.get(current_key) or ""
102-
fields[current_key] = (existing + " " + text).strip() if existing else text
102+
fields[current_key] = (
103+
(existing + " " + text).strip() if existing else text
104+
)
103105
elif in_list and (
104106
isinstance(fields.get(current_key), list)
105107
or (isinstance(fields.get(current_key), str) and fields[current_key] == "")
@@ -123,7 +125,11 @@ def parse_frontmatter(content: str) -> tuple[dict, str]:
123125
item = stripped.strip('",[] ')
124126
if item and item != "]":
125127
fields[current_key].append(item)
126-
elif current_key and isinstance(fields.get(current_key), str) and line.startswith(" "):
128+
elif (
129+
current_key
130+
and isinstance(fields.get(current_key), str)
131+
and line.startswith(" ")
132+
):
127133
fields[current_key] += " " + line.strip().strip('"')
128134

129135
return fields, body
@@ -350,7 +356,9 @@ def load_plugin(plugin_name: str) -> PluginSource | None:
350356
for md in sorted(agents_dir.glob("*.md")):
351357
fm, body = parse_frontmatter(read_file(md))
352358
plugin.agents.append(
353-
AgentSource(plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body)
359+
AgentSource(
360+
plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body
361+
)
354362
)
355363

356364
skills_dir = plugin_dir / "skills"
@@ -361,15 +369,19 @@ def load_plugin(plugin_name: str) -> PluginSource | None:
361369
continue
362370
fm, body = parse_frontmatter(read_file(skill_file))
363371
plugin.skills.append(
364-
SkillSource(plugin=plugin_name, name=sd.name, dir=sd, frontmatter=fm, body=body)
372+
SkillSource(
373+
plugin=plugin_name, name=sd.name, dir=sd, frontmatter=fm, body=body
374+
)
365375
)
366376

367377
commands_dir = plugin_dir / "commands"
368378
if commands_dir.is_dir():
369379
for md in sorted(commands_dir.glob("*.md")):
370380
fm, body = parse_frontmatter(read_file(md))
371381
plugin.commands.append(
372-
CommandSource(plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body)
382+
CommandSource(
383+
plugin=plugin_name, name=md.stem, path=md, frontmatter=fm, body=body
384+
)
373385
)
374386

375387
return plugin
@@ -437,7 +449,9 @@ def write(self, rel_path: str | Path, content: str) -> Path:
437449
target = (self.output_root / rel_path).resolve()
438450
root = self.output_root.resolve()
439451
if not target.is_relative_to(root):
440-
raise ValueError(f"refusing to write outside output_root: {target} (root={root})")
452+
raise ValueError(
453+
f"refusing to write outside output_root: {target} (root={root})"
454+
)
441455
target.parent.mkdir(parents=True, exist_ok=True)
442456
target.write_text(content, encoding="utf-8")
443457
return target
@@ -447,7 +461,9 @@ def write_bytes(self, rel_path: str | Path, content: bytes) -> Path:
447461
target = (self.output_root / rel_path).resolve()
448462
root = self.output_root.resolve()
449463
if not target.is_relative_to(root):
450-
raise ValueError(f"refusing to write outside output_root: {target} (root={root})")
464+
raise ValueError(
465+
f"refusing to write outside output_root: {target} (root={root})"
466+
)
451467
target.parent.mkdir(parents=True, exist_ok=True)
452468
target.write_bytes(content)
453469
return target

0 commit comments

Comments
 (0)