@softspark/ai-toolkit 4.24.0 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +35 -15
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/skills/hook-creator/SKILL.md +18 -4
  5. package/app/surface.json +4 -0
  6. package/benchmarks/ecosystem-doctor-snapshot.json +67 -19
  7. package/bin/ai-toolkit.js +27 -3
  8. package/kb/reference/architecture-overview.md +13 -5
  9. package/kb/reference/claude-ecosystem-expansion-foundations.md +30 -5
  10. package/kb/reference/cli-reference.md +27 -2
  11. package/kb/reference/codex-cli-compatibility.md +101 -11
  12. package/kb/reference/global-install-model.md +10 -8
  13. package/kb/reference/hooks-catalog.md +41 -5
  14. package/kb/reference/mcp-editor-compatibility.md +3 -3
  15. package/kb/reference/mcp-templates.md +3 -3
  16. package/kb/reference/opencode-compatibility.md +53 -5
  17. package/kb/reference/supported-tools-registry.md +31 -26
  18. package/llms-full.txt +312 -73
  19. package/manifest.json +1 -1
  20. package/package.json +6 -2
  21. package/scripts/antigravity_plugin.py +570 -0
  22. package/scripts/codex_plugin.py +764 -0
  23. package/scripts/ecosystem_tools.json +92 -17
  24. package/scripts/generate_antigravity.py +16 -14
  25. package/scripts/generate_antigravity_agents.py +255 -0
  26. package/scripts/generate_antigravity_hooks.py +344 -0
  27. package/scripts/generate_cline_hooks.py +391 -0
  28. package/scripts/generate_cline_rules.py +210 -43
  29. package/scripts/generate_cline_skills.py +65 -2
  30. package/scripts/generate_codex_hooks.py +70 -8
  31. package/scripts/generate_gemini_agents.py +197 -0
  32. package/scripts/generate_gemini_hooks.py +24 -4
  33. package/scripts/generate_opencode_skills.py +544 -0
  34. package/scripts/inject_hook_cli.py +4 -26
  35. package/scripts/install.py +11 -10
  36. package/scripts/install_steps/ai_tools.py +209 -34
  37. package/scripts/mcp_editors.py +9 -1
  38. package/scripts/plugin.py +21 -0
  39. package/scripts/plugin_schema.py +8 -7
  40. package/scripts/secure_fs.py +35 -0
  41. package/scripts/uninstall.py +162 -18
  42. package/scripts/validate.py +42 -12
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "description": "Authoritative registry of tools ai-toolkit integrates with. Consumed by scripts/ecosystem_doctor.py to detect upstream doc/version drift.",
4
- "last_updated": "2026-07-14",
4
+ "last_updated": "2026-08-19",
5
5
  "tools": [
6
6
  {
7
7
  "id": "claude-code",
@@ -20,9 +20,23 @@
20
20
  ".claude/rules/*.md",
21
21
  ".claude/agents/*.md",
22
22
  ".claude/skills/*/SKILL.md",
23
- "~/.claude/themes/*.json"
23
+ "~/.claude/themes/*.json",
24
+ ".claude-plugin/plugin.json",
25
+ "skills/*/SKILL.md",
26
+ "commands/*.md",
27
+ "agents/*.md",
28
+ "workflows/",
29
+ "output-styles/",
30
+ "themes/",
31
+ "hooks/hooks.json",
32
+ ".mcp.json",
33
+ ".lsp.json",
34
+ "monitors/monitors.json",
35
+ "bin/*",
36
+ "settings.json"
24
37
  ],
25
38
  "our_generators": [],
39
+ "status_note": "Claude Code auto-discovers native plugin skills, commands, agents, workflows, output styles, themes, hooks, MCP servers, LSP servers, monitors, executables, and root settings. ai-toolkit's primary install emits its existing rules, skills, agents, and hooks; plugin workflows, output styles, themes, monitors, LSP servers, channels, and root plugin settings are recognized class C surfaces that ai-toolkit does not generate. Themes and monitors are experimental, and plugin settings support only agent and subagentStatusLine.",
26
40
  "capability_markers": [
27
41
  "PreToolUse",
28
42
  "PostToolUse",
@@ -52,8 +66,18 @@
52
66
  "CwdChanged",
53
67
  "FileChanged",
54
68
  "ConfigChange",
69
+ "DirectoryAdded",
55
70
  "Setup",
56
71
  "InstructionsLoaded",
72
+ "plugins",
73
+ "workflows",
74
+ "outputStyles",
75
+ "themes",
76
+ "LSP servers",
77
+ "monitors",
78
+ "channels",
79
+ "userConfig",
80
+ "settings.json",
57
81
  "slash command",
58
82
  "MCP server",
59
83
  "sub-agent",
@@ -265,16 +289,20 @@
265
289
  "~/.gemini/settings.json",
266
290
  ".gemini/commands/*.toml",
267
291
  ".gemini/skills/*/SKILL.md",
292
+ ".gemini/agents/*.md",
268
293
  ".agents/skills/*/SKILL.md",
269
294
  "~/.gemini/skills/*/SKILL.md",
295
+ "~/.gemini/agents/*.md",
270
296
  "~/.agents/skills/*/SKILL.md",
271
- ".gemini/extensions/gemini-extension.json"
297
+ "<extension-package>/gemini-extension.json",
298
+ "~/.gemini/extensions/<name>/gemini-extension.json"
272
299
  ],
273
300
  "our_generators": [
274
301
  "scripts/generate_gemini.py",
275
302
  "scripts/generate_gemini_hooks.py",
276
303
  "scripts/generate_gemini_commands.py",
277
- "scripts/generate_gemini_skills.py"
304
+ "scripts/generate_gemini_skills.py",
305
+ "scripts/generate_gemini_agents.py"
278
306
  ],
279
307
  "capability_markers": [
280
308
  "GEMINI.md",
@@ -295,9 +323,10 @@
295
323
  "SKILL.md",
296
324
  "activate_skill",
297
325
  "custom commands",
326
+ ".gemini/agents/*.md",
298
327
  "gemini-extension.json"
299
328
  ],
300
- "status_note": "Gemini CLI drops free + paid (AI Pro/Ultra) tiers on 2026-06-18 in favor of the new Antigravity CLI (we ship generate_antigravity.py); enterprise Gemini Code Assist Standard/Enterprise keeps full Gemini CLI support. The .gemini/* generators remain valid for the enterprise audience.",
329
+ "status_note": "Gemini CLI drops free + paid (AI Pro/Ultra) tiers on 2026-06-18 in favor of the new Antigravity CLI (we ship generate_antigravity.py); enterprise Gemini Code Assist Standard/Enterprise keeps full Gemini CLI support. Profile full now emits project and user-tier native agents. Gemini extension export remains class C / not adopted; extension manifests live at the package root and installed extensions live under ~/.gemini/extensions/<name>/.",
301
330
  "version_probe": {
302
331
  "kind": "command",
303
332
  "command": "gemini --version"
@@ -319,13 +348,16 @@
319
348
  ".clinerules/hooks/",
320
349
  ".cline/rules/*.md",
321
350
  ".cline/hooks/",
351
+ ".cline/agents/",
322
352
  ".cline/plugins/",
323
353
  ".cline/skills/*/SKILL.md",
324
354
  "~/.cline/rules/",
325
355
  "~/.cline/hooks/",
356
+ "~/.cline/agents/",
326
357
  "~/.cline/plugins/",
327
358
  "~/.cline/skills/*/SKILL.md",
328
- "~/Documents/Cline/Rules/Hooks/",
359
+ "~/Documents/Cline/Rules/",
360
+ "~/Documents/Cline/Hooks/",
329
361
  "~/.cline/data/settings/cline_mcp_settings.json",
330
362
  ".claude/skills/*/SKILL.md",
331
363
  "AGENTS.md",
@@ -334,7 +366,8 @@
334
366
  "our_generators": [
335
367
  "scripts/generate_cline.py",
336
368
  "scripts/generate_cline_rules.py",
337
- "scripts/generate_cline_skills.py"
369
+ "scripts/generate_cline_skills.py",
370
+ "scripts/generate_cline_hooks.py"
338
371
  ],
339
372
  "capability_markers": [
340
373
  "clinerules",
@@ -501,19 +534,33 @@
501
534
  ".agents/rules/*.md",
502
535
  ".agents/workflows/*.md",
503
536
  ".agents/skills/*/SKILL.md",
537
+ ".agents/agents/ai-toolkit-*/agent.md",
504
538
  ".agents/hooks.json",
539
+ ".agents/hooks/ai-toolkit-antigravity-hook.py",
540
+ ".agents/plugins/*/plugin.json",
505
541
  ".agents/mcp_config.json",
542
+ "~/.gemini/config/skills/*/SKILL.md",
543
+ "~/.gemini/config/agents/ai-toolkit-*/agent.md",
544
+ "~/.gemini/config/hooks.json",
545
+ "~/.gemini/config/hooks/ai-toolkit-antigravity-hook.py",
546
+ "~/.gemini/config/plugins/*/plugin.json",
506
547
  "~/.gemini/config/mcp_config.json",
507
548
  ".agent/rules/*.md",
508
549
  ".agent/workflows/*.md",
509
550
  ".agent/skills/*/SKILL.md",
551
+ "~/.gemini/antigravity-cli/skills/*/SKILL.md",
552
+ "~/.gemini/antigravity-cli/plugins/*/plugin.json",
510
553
  "AGENTS.md",
511
554
  "GEMINI.md"
512
555
  ],
513
556
  "our_generators": [
514
- "scripts/generate_antigravity.py"
557
+ "scripts/generate_antigravity.py",
558
+ "scripts/generate_antigravity_hooks.py",
559
+ "scripts/generate_antigravity_agents.py",
560
+ "scripts/antigravity_plugin.py",
561
+ "scripts/mcp_editors.py"
515
562
  ],
516
- "status_note": "Antigravity CLI (GA 2026-05-19) reads workspace skills from .agents/skills/ (plural); the IDE still uses .agent/skills/ (singular) our generator dual-emits the skill pointer to both. CLI hooks live in .agents/hooks.json with Claude-style event names (PreToolUse, not BeforeTool) and a JSON stdin/stdout decision contract. Native MCP configs are .agents/mcp_config.json (workspace) and ~/.gemini/config/mcp_config.json (global). The 2.1.4 changelog confirms that url is accepted alongside serverUrl; httpUrl remains unsupported. scripts/mcp_editors.py ships project and global Antigravity adapters with transactional JSON merge/remove.",
563
+ "status_note": "Antigravity 2.0 v2.8.1, Antigravity CLI v1.1.14, IDE v2.5.5 (verified 2026-08-19). Project skills use .agents/skills/; only .agent/skills/ is legacy. Global skills use ~/.gemini/antigravity-cli/skills/ for CLI and ~/.gemini/config/skills/ for the IDE/shared product. Native hooks use .agents/hooks.json or ~/.gemini/config/hooks.json with exact events PreToolUse, PostToolUse, PreInvocation, PostInvocation, and Stop plus a self-contained adjacent adapter. Full-profile installs add native subagents. antigravity-plugin exports an opt-in deterministic package for .agents/plugins/, ~/.gemini/antigravity-cli/plugins/, or ~/.gemini/config/plugins/. Native MCP output consumes portable transport metadata and uses serverUrl; legacy url input is normalized and httpUrl is rejected. Unrelated config data is preserved transactionally.",
517
564
  "capability_markers": [
518
565
  "agent manager",
519
566
  "artifacts",
@@ -522,9 +569,20 @@
522
569
  "rules",
523
570
  "skills",
524
571
  "hooks",
572
+ "PreToolUse",
573
+ "PostToolUse",
574
+ "PreInvocation",
575
+ "PostInvocation",
576
+ "Stop",
577
+ "subagents",
578
+ "plugins",
525
579
  "AGENTS.md",
526
580
  "GEMINI.md",
527
- "agent permissions"
581
+ "agent permissions",
582
+ "global skills",
583
+ "global hooks",
584
+ "global agents",
585
+ "serverUrl"
528
586
  ],
529
587
  "version_probe": null
530
588
  },
@@ -547,13 +605,18 @@
547
605
  ".codex/hooks.json",
548
606
  "~/.codex/hooks.json",
549
607
  ".codex/config.toml",
550
- "~/.codex/config.toml"
608
+ "~/.codex/config.toml",
609
+ ".codex-plugin/plugin.json",
610
+ "hooks/hooks.json",
611
+ "skills/*/SKILL.md",
612
+ ".agents/plugins/marketplace.json"
551
613
  ],
552
614
  "our_generators": [
553
615
  "scripts/generate_codex.py",
554
616
  "scripts/generate_codex_agents.py",
555
617
  "scripts/generate_codex_hooks.py",
556
- "scripts/generate_codex_skills.py"
618
+ "scripts/generate_codex_skills.py",
619
+ "scripts/codex_plugin.py"
557
620
  ],
558
621
  "capability_markers": [
559
622
  "AGENTS.md",
@@ -564,6 +627,7 @@
564
627
  "hook event: PreToolUse",
565
628
  "hook event: PostToolUse",
566
629
  "hook event: SessionStart",
630
+ "hook event: SessionEnd",
567
631
  "hook event: UserPromptSubmit",
568
632
  "hook event: Stop",
569
633
  "hook event: PermissionRequest",
@@ -575,7 +639,12 @@
575
639
  "hook handler: prompt",
576
640
  "hook handler: agent",
577
641
  ".agents/skills",
578
- ".codex/agents"
642
+ ".codex/agents",
643
+ ".codex-plugin/plugin.json",
644
+ "hooks/hooks.json",
645
+ "PLUGIN_ROOT",
646
+ "/plugins",
647
+ "plugin marketplace"
579
648
  ],
580
649
  "version_probe": {
581
650
  "kind": "command",
@@ -593,6 +662,7 @@
593
662
  },
594
663
  "config_paths": [
595
664
  "opencode.json",
665
+ "opencode.jsonc",
596
666
  ".opencode/agents/*.md",
597
667
  ".opencode/commands/*.md",
598
668
  ".opencode/plugins/*",
@@ -602,15 +672,18 @@
602
672
  ".agents/skills/*/SKILL.md",
603
673
  "~/.config/opencode/skills/*/SKILL.md",
604
674
  "~/.claude/skills/*/SKILL.md",
605
- "~/.agents/skills/*/SKILL.md"
675
+ "~/.agents/skills/*/SKILL.md",
676
+ "skills/*.md",
677
+ "skills/**/SKILL.md"
606
678
  ],
607
- "status_note": "opencode v1.16.0 (2026-06-05) ships an experimental v2 skill registry with flat-file skills and a 'slash' frontmatter key; undocumented as of 2026-06-09. Watch item: re-check next sync whether the v2 format reaches the documented SKILL.md spec before touching generate_opencode*.py. Note sst/opencode redirects to anomalyco/opencode.",
679
+ "status_note": "OpenCode's v2 plugin API is beta. Root plugin packages can expose flat skills/*.md and nested skills/**/SKILL.md entries, and config can add local paths or HTTP URLs through the skills array. No migration: the stable JS plugin generator remains unchanged until v2 stabilizes. Note sst/opencode redirects to anomalyco/opencode.",
608
680
  "our_generators": [
609
681
  "scripts/generate_opencode.py",
610
682
  "scripts/generate_opencode_agents.py",
611
683
  "scripts/generate_opencode_commands.py",
612
684
  "scripts/generate_opencode_json.py",
613
- "scripts/generate_opencode_plugin.py"
685
+ "scripts/generate_opencode_plugin.py",
686
+ "scripts/generate_opencode_skills.py"
614
687
  ],
615
688
  "capability_markers": [
616
689
  "opencode.json",
@@ -632,7 +705,9 @@
632
705
  "plugin event: tool.execute.after",
633
706
  "plugin event: permission.asked",
634
707
  "plugin event: command.executed",
635
- "plugin event: message.updated"
708
+ "plugin event: message.updated",
709
+ "plugin event: message.part.updated",
710
+ "skills array (local paths + HTTP URLs)"
636
711
  ],
637
712
  "version_probe": {
638
713
  "kind": "command",
@@ -15,10 +15,10 @@ Antigravity reads rules from:
15
15
  * ``.agents/rules/*.md`` — per-category rule files (this generator)
16
16
  * ``.agents/workflows/*.md`` — workflow templates (this generator)
17
17
 
18
- Antigravity also supports the Agent Skills standard. The IDE reads
19
- ``.agent/skills/<skill-name>/SKILL.md`` (singular), while the Antigravity CLI
20
- (GA 2026-05-19) reads ``.agents/skills/`` (plural) so the skill pointer is
21
- dual-emitted to both. We do not duplicate our full skill catalogue; instead we
18
+ Antigravity also supports the Agent Skills standard. The canonical project
19
+ surface is ``.agents/skills/<skill-name>/SKILL.md``. The older singular
20
+ ``.agent/skills/`` path remains a compatibility output only, so the skill
21
+ pointer is dual-emitted. We do not duplicate our full skill catalogue; instead we
22
22
  emit a single pointer skill that teaches Antigravity to look up the real
23
23
  catalogue in ``~/.softspark/ai-toolkit/app/skills/`` (global install) or
24
24
  ``.claude/skills/`` (local install).
@@ -27,7 +27,8 @@ Usage:
27
27
  python3 scripts/generate_antigravity.py [target-dir]
28
28
 
29
29
  Writes rules/workflows to target-dir/.agents/{rules,workflows}/ and the skill
30
- pointer to target-dir/.agent/skills/ (IDE) + target-dir/.agents/skills/ (CLI).
30
+ pointer to target-dir/.agents/skills/ (canonical) + target-dir/.agent/skills/
31
+ (legacy compatibility).
31
32
  """
32
33
  from __future__ import annotations
33
34
 
@@ -88,11 +89,11 @@ def _pointer_skill_md() -> str:
88
89
  def _write_skill_pointer(target_dir: Path) -> None:
89
90
  """Write the pointer SKILL.md for both the IDE and the CLI surface.
90
91
 
91
- The IDE reads ``.agent/skills/`` (singular); the Antigravity CLI reads
92
- ``.agents/skills/`` (plural).
92
+ ``.agents/skills/`` is canonical. ``.agent/skills/`` is retained only for
93
+ older Antigravity installations.
93
94
  """
94
95
  content = _pointer_skill_md()
95
- for tree in (".agent", ".agents"):
96
+ for tree in (".agents", ".agent"):
96
97
  skill_dir = target_dir / tree / "skills" / POINTER_SKILL_NAME
97
98
  skill_dir.mkdir(parents=True, exist_ok=True)
98
99
  (skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
@@ -105,10 +106,10 @@ def _write_skill_pointer(target_dir: Path) -> None:
105
106
 
106
107
  def generate_global(target_dir: Path) -> None:
107
108
  """Write the skill pointer to Antigravity's documented HOME-scoped skill
108
- dirs. ``~/.gemini/config/skills/`` is shared across all Antigravity products
109
- (IDE, editor, CLI); ``~/.gemini/antigravity-cli/skills/`` is CLI-private.
110
- Antigravity RULES have no documented global file surface, so only the skill
111
- pointer is emitted globally (rules stay project-local).
109
+ dirs. ``~/.gemini/antigravity-cli/skills/`` is canonical for the CLI;
110
+ ``~/.gemini/config/skills/`` is canonical for the IDE/shared product.
111
+ Antigravity RULES have no documented global file surface, so only the
112
+ product-specific skill pointers are emitted globally (rules stay local).
112
113
  """
113
114
  content = _pointer_skill_md()
114
115
  for rel in (".gemini/config/skills", ".gemini/antigravity-cli/skills"):
@@ -124,8 +125,9 @@ def generate(target_dir: Path, *,
124
125
  emit_skill_pointer: bool = True) -> None:
125
126
  """Write ``.agents/{rules,workflows}/`` and the dual skill pointers.
126
127
 
127
- ``emit_skill_pointer`` controls whether the pointer SKILL.md is written
128
- to ``.agent/skills/`` (IDE) and ``.agents/skills/`` (CLI). Set to
128
+ ``emit_skill_pointer`` controls whether the pointer SKILL.md is written.
129
+ ``.agents/skills/`` is the canonical workspace surface, while
130
+ ``.agent/skills/`` is a legacy compatibility copy. Set this option to
129
131
  ``False`` if you manage those directories yourself.
130
132
  """
131
133
  rules = dict(STANDARD_RULES)
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Copyright 2024-2026 Lukasz Krzemien (biuro@softspark.eu)
4
+ # Source: https://github.com/softspark/ai-toolkit
5
+
6
+ """Generate native Antigravity subagents under ``.agents/agents``."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
15
+
16
+ from emission import agents_dir
17
+ from frontmatter import frontmatter_field
18
+ from secure_fs import (
19
+ SecureDestination,
20
+ SecureTransaction,
21
+ lexical_absolute,
22
+ nearest_existing_root,
23
+ run_secure_transaction,
24
+ )
25
+
26
+
27
+ AGENT_PREFIX = "ai-toolkit-"
28
+ MANAGED_MARKER = "<!-- ai-toolkit-managed: antigravity-agent -->"
29
+ SAFE_NAME = re.compile(r"^[a-z0-9][a-z0-9-]*$")
30
+ TOOL_MAPPING: dict[str, tuple[str, ...]] = {
31
+ "Read": ("view_file",),
32
+ "Write": ("write_to_file",),
33
+ "Edit": ("replace_file_content", "multi_replace_file_content"),
34
+ "Bash": ("run_command",),
35
+ "Grep": ("grep_search",),
36
+ "Glob": ("find_by_name", "list_dir"),
37
+ "Agent": ("invoke_subagent",),
38
+ "TeamCreate": ("manage_subagents",),
39
+ "TeamDelete": ("manage_subagents",),
40
+ "SendMessage": ("send_message",),
41
+ "TaskCreate": ("manage_task",),
42
+ "TaskList": ("manage_task",),
43
+ "TaskUpdate": ("manage_task",),
44
+ }
45
+
46
+
47
+ def _source_tools(source: Path) -> list[str]:
48
+ raw = frontmatter_field(source, "tools")
49
+ return [item.strip() for item in raw.split(",") if item.strip()]
50
+
51
+
52
+ def _mapped_tools(source: Path) -> list[str]:
53
+ mapped: list[str] = []
54
+ unknown: list[str] = []
55
+ for tool in _source_tools(source):
56
+ native = TOOL_MAPPING.get(tool)
57
+ if native is None:
58
+ unknown.append(tool)
59
+ continue
60
+ for name in native:
61
+ if name not in mapped:
62
+ mapped.append(name)
63
+ if unknown:
64
+ raise ValueError(
65
+ f"Unknown Antigravity source tool(s) in {source}: "
66
+ + ", ".join(sorted(unknown))
67
+ )
68
+ return mapped
69
+
70
+
71
+ def _body(source: Path) -> str:
72
+ text = source.read_text(encoding="utf-8")
73
+ if not text.startswith("---\n"):
74
+ return text.strip()
75
+ parts = text.split("---", 2)
76
+ return (parts[2] if len(parts) == 3 else text).strip()
77
+
78
+
79
+ def _quote(value: str) -> str:
80
+ return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
81
+
82
+
83
+ def render_agent(source: Path) -> tuple[str, str]:
84
+ """Render and return ``(name, agent.md)`` after strict source validation."""
85
+ if source.is_symlink() or not source.is_file():
86
+ raise RuntimeError(f"Unsafe Antigravity agent source: {source}")
87
+ name = frontmatter_field(source, "name")
88
+ description = frontmatter_field(source, "description")
89
+ if not SAFE_NAME.fullmatch(name):
90
+ raise ValueError(f"Invalid Antigravity agent name in {source}: {name!r}")
91
+ if not description:
92
+ raise ValueError(f"Missing Antigravity agent description in {source}")
93
+ tools = _mapped_tools(source)
94
+ lines = [
95
+ "---",
96
+ f"name: {name}",
97
+ f"description: {_quote(description)}",
98
+ "mainAgent: false",
99
+ "subagent: true",
100
+ "model: inherit",
101
+ "commandExecutionPolicy: sandbox",
102
+ "tools:",
103
+ ]
104
+ lines.extend(f" - {tool}" for tool in tools)
105
+ lines.extend(["---", "", MANAGED_MARKER, "", _body(source), ""])
106
+ return name, "\n".join(lines)
107
+
108
+
109
+ def _is_managed(content: bytes | None) -> bool:
110
+ return content is not None and MANAGED_MARKER.encode() in content[:2048]
111
+
112
+
113
+ def _source_plan(source_dir: Path) -> list[tuple[str, str]]:
114
+ if source_dir.is_symlink() or not source_dir.is_dir():
115
+ raise RuntimeError(f"Unsafe Antigravity agents source directory: {source_dir}")
116
+ plan: list[tuple[str, str]] = []
117
+ seen: set[str] = set()
118
+ for source in sorted(source_dir.glob("*.md")):
119
+ name, rendered = render_agent(source)
120
+ if name in seen:
121
+ raise ValueError(f"Duplicate Antigravity agent name: {name}")
122
+ seen.add(name)
123
+ plan.append((name, rendered))
124
+ return plan
125
+
126
+
127
+ def _stale_files(output_dir: Path, expected: set[str]) -> list[Path]:
128
+ if not output_dir.exists():
129
+ return []
130
+ if output_dir.is_symlink() or not output_dir.is_dir():
131
+ raise RuntimeError(f"Unsafe Antigravity agents directory: {output_dir}")
132
+ stale: list[Path] = []
133
+ for child in sorted(output_dir.iterdir()):
134
+ if child.name in expected or not child.name.startswith(AGENT_PREFIX):
135
+ continue
136
+ if child.is_symlink() or not child.is_dir():
137
+ continue
138
+ agent_file = child / "agent.md"
139
+ if agent_file.is_symlink() or not agent_file.is_file():
140
+ continue
141
+ try:
142
+ content = agent_file.read_bytes()
143
+ except OSError:
144
+ continue
145
+ if _is_managed(content):
146
+ stale.append(agent_file)
147
+ return stale
148
+
149
+
150
+ def generate(
151
+ target_dir: Path,
152
+ *,
153
+ source_dir: Path | None = None,
154
+ config_root: Path | None = None,
155
+ ) -> tuple[int, int]:
156
+ """Generate agents atomically and remove only stale managed definitions."""
157
+ source_root = lexical_absolute(source_dir or agents_dir)
158
+ plan = _source_plan(source_root) # Preflight all source tools before mutation.
159
+ target = lexical_absolute(target_dir)
160
+ if target.is_symlink() or not target.is_dir():
161
+ raise RuntimeError(f"Unsafe Antigravity target directory: {target}")
162
+ base = lexical_absolute(config_root) if config_root is not None else target / ".agents"
163
+ output_dir = base / "agents"
164
+ if base.is_symlink() or output_dir.is_symlink():
165
+ raise RuntimeError(f"Unsafe Antigravity config path: {output_dir}")
166
+ expected = {f"{AGENT_PREFIX}{name}" for name, _ in plan}
167
+ stale = _stale_files(output_dir, expected)
168
+ root = nearest_existing_root(target)
169
+
170
+ writes: list[tuple[SecureDestination, bytes]] = []
171
+ for name, rendered in plan:
172
+ path = output_dir / f"{AGENT_PREFIX}{name}" / "agent.md"
173
+ writes.append(
174
+ (
175
+ SecureDestination(path, root, f"Antigravity agent {name}"),
176
+ rendered.encode(),
177
+ )
178
+ )
179
+ stale_destinations = [
180
+ SecureDestination(path, root, f"stale Antigravity agent {path.parent.name}")
181
+ for path in stale
182
+ ]
183
+ destinations = [destination for destination, _ in writes] + stale_destinations
184
+
185
+ def apply(transaction: SecureTransaction) -> tuple[int, int]:
186
+ written = 0
187
+ removed = 0
188
+ for destination, content in writes:
189
+ existing = transaction.initial_content(destination)
190
+ if existing is not None and not _is_managed(existing):
191
+ continue
192
+ transaction.atomic_write(destination, content, 0o644)
193
+ written += 1
194
+ for destination in stale_destinations:
195
+ existing = transaction.initial_content(destination)
196
+ if _is_managed(existing):
197
+ transaction.unlink(destination)
198
+ removed += 1
199
+ return written, removed
200
+
201
+ written, removed = run_secure_transaction(destinations, apply)
202
+ for stale_file in stale:
203
+ directory = stale_file.parent
204
+ if directory.is_dir() and not directory.is_symlink():
205
+ try:
206
+ directory.rmdir()
207
+ except OSError:
208
+ pass
209
+ return written, removed
210
+
211
+
212
+ def cleanup(target_dir: Path, *, config_root: Path | None = None) -> int:
213
+ """Remove managed Antigravity agents while preserving every user agent."""
214
+ target = lexical_absolute(target_dir)
215
+ if target.is_symlink() or not target.is_dir():
216
+ raise RuntimeError(f"Unsafe Antigravity target directory: {target}")
217
+ base = lexical_absolute(config_root) if config_root is not None else target / ".agents"
218
+ output_dir = base / "agents"
219
+ stale = _stale_files(output_dir, set())
220
+ if not stale:
221
+ return 0
222
+ root = nearest_existing_root(target)
223
+ destinations = [
224
+ SecureDestination(path, root, f"Antigravity agent {path.parent.name}")
225
+ for path in stale
226
+ ]
227
+
228
+ def apply(transaction: SecureTransaction) -> int:
229
+ removed = 0
230
+ for destination in destinations:
231
+ if _is_managed(transaction.initial_content(destination)):
232
+ transaction.unlink(destination)
233
+ removed += 1
234
+ return removed
235
+
236
+ removed = run_secure_transaction(destinations, apply)
237
+ for path in stale:
238
+ try:
239
+ path.parent.rmdir()
240
+ except OSError:
241
+ pass
242
+ return removed
243
+
244
+
245
+ def main() -> None:
246
+ target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
247
+ written, removed = generate(target)
248
+ print(
249
+ f"Generated: .agents/agents/ ({written} agents, "
250
+ f"{removed} stale removed)"
251
+ )
252
+
253
+
254
+ if __name__ == "__main__":
255
+ main()