@softspark/ai-toolkit 4.10.1 → 4.12.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.
@@ -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-06-23",
4
+ "last_updated": "2026-06-30",
5
5
  "tools": [
6
6
  {
7
7
  "id": "claude-code",
@@ -226,7 +226,10 @@
226
226
  ".gemini/extensions/gemini-extension.json"
227
227
  ],
228
228
  "our_generators": [
229
- "scripts/generate_gemini.py"
229
+ "scripts/generate_gemini.py",
230
+ "scripts/generate_gemini_hooks.py",
231
+ "scripts/generate_gemini_commands.py",
232
+ "scripts/generate_gemini_skills.py"
230
233
  ],
231
234
  "capability_markers": [
232
235
  "GEMINI.md",
@@ -310,18 +313,23 @@
310
313
  "kind": "editor",
311
314
  "urls": {
312
315
  "docs": "https://roocodeinc.github.io/Roo-Code",
313
- "release_notes": "https://github.com/RooCodeInc/Roo-Code/releases",
316
+ "release_notes": "https://github.com/Zoo-Code-Org/Zoo-Code/releases",
314
317
  "changelog": null
315
318
  },
316
- "status": "archived",
317
- "status_note": "Upstream RooCodeInc/Roo-Code repo archived/read-only as of 2026-05-15, frozen at v3.54.0; docs.roocode.com 301-redirects to roocodeinc.github.io/Roo-Code. A community fork reportedly continues maintenance. JSON .roomodes is NOT deprecated; both generators remain valid.",
319
+ "status": "succeeded-by-fork",
320
+ "status_note": "Upstream RooCodeInc/Roo-Code archived/read-only 2026-05-15, frozen at v3.54.0; its releases feed is dead. Successor project by former Roo contributors: Zoo-Code-Org/Zoo-Code (marketplace ZooCodeOrganization.zoo-code, docs.zoocode.dev), active through v3.64.0 (2026-06-26) continuing Roo's version line. Config compat is total: same .roomodes (JSON 'still fully supported and won't be deprecated'), .roo/rules/, .roo/rules-{slug}/, .roo/skills/, .roo/commands/, ~/.roo/* paths — both generators remain valid. Watch Zoo GitHub releases as the authoritative feed (docs.zoocode.dev/update-notes is stale at v3.50.0). Skills (~/.roo/skills, ~/.agents/skills) shipped in v3.46.0/v3.47.2; ai-toolkit now installs them globally via ~/.agents/skills. Roo team pivoted to roomote.dev and recommended Cline.",
318
321
  "config_paths": [
319
322
  ".roomodes",
320
323
  ".roo/rules/*.md",
321
324
  ".roo/rules-{slug}/*.md",
325
+ ".roo/skills/*/SKILL.md",
326
+ "~/.roo/skills/*/SKILL.md",
327
+ ".agents/skills/*/SKILL.md",
328
+ "~/.agents/skills/*",
329
+ ".roo/commands/*.md",
330
+ "~/.roo/commands/*.md",
322
331
  ".roo/mcp.json",
323
332
  "~/.roo/rules/",
324
- "~/.roo/custom_modes.yaml",
325
333
  "mcp_settings.json"
326
334
  ],
327
335
  "our_generators": [
@@ -486,8 +494,11 @@
486
494
  },
487
495
  "config_paths": [
488
496
  "AGENTS.md",
497
+ "~/.codex/AGENTS.md",
498
+ "~/.codex/AGENTS.override.md",
489
499
  ".agents/skills/*/SKILL.md",
490
500
  ".codex/hooks.json",
501
+ "~/.codex/hooks.json",
491
502
  ".codex/config.toml",
492
503
  "~/.codex/config.toml"
493
504
  ],
@@ -99,6 +99,21 @@ def _write_skill_pointer(target_dir: Path) -> None:
99
99
  # Main
100
100
  # ---------------------------------------------------------------------------
101
101
 
102
+ def generate_global(target_dir: Path) -> None:
103
+ """Write the skill pointer to Antigravity's documented HOME-scoped skill
104
+ dirs. ``~/.gemini/config/skills/`` is shared across all Antigravity products
105
+ (IDE, editor, CLI); ``~/.gemini/antigravity-cli/skills/`` is CLI-private.
106
+ Antigravity RULES have no documented global file surface, so only the skill
107
+ pointer is emitted globally (rules stay project-local).
108
+ """
109
+ content = _pointer_skill_md()
110
+ for rel in (".gemini/config/skills", ".gemini/antigravity-cli/skills"):
111
+ skill_dir = target_dir / rel / POINTER_SKILL_NAME
112
+ skill_dir.mkdir(parents=True, exist_ok=True)
113
+ (skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
114
+ print(f" Generated: {rel}/{POINTER_SKILL_NAME}/SKILL.md")
115
+
116
+
102
117
  def generate(target_dir: Path, *,
103
118
  language_modules: list[str] | None = None,
104
119
  rules_dir: Path | None = None,
@@ -8,11 +8,14 @@ Codex exposes 10 lifecycle events (PascalCase in config.toml / hooks.json):
8
8
  ``PreToolUse``, ``PostToolUse``, ``PermissionRequest``, ``PreCompact``,
9
9
  ``PostCompact``, ``SessionStart``, ``UserPromptSubmit``, ``SubagentStart``,
10
10
  ``SubagentStop``, ``Stop``. PreToolUse/PostToolUse only support the ``Bash``
11
- matcher. We wire all 10 events in ``CODEX_HOOKS`` below to the shared toolkit
12
- hook scripts, mirroring the Claude Code mapping in ``app/hooks.json``.
13
-
14
- Handler types in Codex: ``command`` (what we emit), ``prompt``, and ``agent``.
15
- Reference: codex-rs/config/src/hook_config.rs.
11
+ matcher. We wire 9 of the 10 events in ``CODEX_HOOKS`` below to the shared
12
+ toolkit hook scripts, mirroring the Claude Code mapping in ``app/hooks.json``.
13
+ ``PostCompact`` is intentionally unwired (its only hook was the removed
14
+ environment-snapshot probe).
15
+
16
+ Handler types in Codex: ``command`` (what we emit). ``prompt`` and ``agent``
17
+ are parsed by Codex but not yet executed, so hand-authored handlers of those
18
+ types are inert. Reference: codex-rs/config/src/hook_config.rs.
16
19
 
17
20
  Usage:
18
21
  python3 scripts/generate_codex_hooks.py [target-dir]
@@ -228,17 +228,24 @@ def generate(target_dir: Path, *,
228
228
  language_modules: list[str] | None = None,
229
229
  rules_dir: Path | None = None,
230
230
  emit_prompts: bool = True,
231
- emit_instructions: bool = True) -> None:
231
+ emit_instructions: bool = True,
232
+ config_root: Path | None = None) -> None:
232
233
  """Write Copilot path-specific instructions and prompt files.
233
234
 
235
+ By default writes to ``<target_dir>/.github/`` (project-local). Pass
236
+ ``config_root=~/.copilot`` for the Copilot CLI user-level global layout,
237
+ where instructions land in ``~/.copilot/instructions/*.instructions.md``.
238
+
234
239
  ``.github/copilot-instructions.md`` is intentionally not written here —
235
240
  the legacy ``main()`` entry point still emits it to stdout so existing
236
241
  scripts (including ``ai-toolkit install``) keep working unchanged.
237
242
  """
238
243
  github_dir = target_dir / ".github"
244
+ instr_root = config_root if config_root is not None else github_dir
245
+ instr_label = "~/.copilot/instructions" if config_root is not None else ".github/instructions"
239
246
 
240
247
  if emit_instructions:
241
- instr_dir = github_dir / "instructions"
248
+ instr_dir = instr_root / "instructions"
242
249
  instr_dir.mkdir(parents=True, exist_ok=True)
243
250
 
244
251
  instruction_files: dict[str, callable] = dict(_make_instruction_files())
@@ -269,7 +276,7 @@ def generate(target_dir: Path, *,
269
276
 
270
277
  for name, content_fn in instruction_files.items():
271
278
  (instr_dir / name).write_text(content_fn(), encoding="utf-8")
272
- print(f" Generated: .github/instructions/{name}")
279
+ print(f" Generated: {instr_label}/{name}")
273
280
 
274
281
  if emit_prompts:
275
282
  prompt_dir = github_dir / "prompts"
@@ -10,8 +10,10 @@ This is the proper progressive-disclosure replacement for the v1.3.8 pointer
10
10
  block in ``.claude/CLAUDE.md``: instead of nudging Claude to Read absolute
11
11
  nvm-pinned paths on demand, the rules ride on the Agent Skills mechanism.
12
12
 
13
- Common rules (``app/rules/common/``) stay inlined in ``CLAUDE.md`` because
14
- they are language-agnostic and should be visible regardless of context.
13
+ Common rules (``app/rules/common/``) are installed as Claude Code
14
+ path-scoped ``.claude/rules/ai-toolkit-*.md`` files by ``install --local``.
15
+ This keeps ``CLAUDE.md`` below Claude's current size guidance while still
16
+ loading common guidance when project files are opened.
15
17
 
16
18
  Idempotent: rerunning overwrites generated SKILL.md but leaves any other
17
19
  files in the skill directory alone.
@@ -49,8 +49,22 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
49
49
  # Protected source tag -- this CLI must never touch ai-toolkit's own entries.
50
50
  PROTECTED_SOURCE = "ai-toolkit"
51
51
 
52
- # Codex CLI supports only these 5 hook events.
53
- CODEX_EVENTS = {"SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"}
52
+ # Codex CLI's HookEventName enum defines 10 events; we propagate the 9 that
53
+ # generate_codex_hooks.py also wires (all except PostCompact, whose only hook
54
+ # was the removed environment-snapshot probe). Keeping this in sync with the
55
+ # generator prevents injected custom hooks from silently losing events Codex
56
+ # supports.
57
+ CODEX_EVENTS = {
58
+ "SessionStart",
59
+ "PreToolUse",
60
+ "PostToolUse",
61
+ "PermissionRequest",
62
+ "UserPromptSubmit",
63
+ "SubagentStart",
64
+ "SubagentStop",
65
+ "PreCompact",
66
+ "Stop",
67
+ }
54
68
 
55
69
 
56
70
  # ---------------------------------------------------------------------------
@@ -9,7 +9,7 @@ Claude Code (~/.claude/):
9
9
  - Per-file symlinks: agents/*.md, skills/*/ (merges with user files)
10
10
  - Merged JSON: hooks.json (toolkit entries tagged with _source)
11
11
  - Marker injection: constitution.md, ARCHITECTURE.md (preserves user content)
12
- - Rules injected into ~/.claude/CLAUDE.md
12
+ - User-level rules: rules/ai-toolkit-*.md plus compact CLAUDE.md index
13
13
 
14
14
  Other tools (global config locations):
15
15
  - Windsurf: ~/.codeium/windsurf/memories/global_rules.md + ~/.codeium/windsurf/skills/
@@ -18,10 +18,10 @@ Other tools (global config locations):
18
18
  - Roo Code: ~/.roo/rules/
19
19
  - Aider: ~/.aider.conf.yml (created only if absent)
20
20
  - Augment: ~/.augment/rules/ai-toolkit.md
21
- - Codex: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json
21
+ - Codex: ~/.codex/AGENTS.md, ~/.agents/skills/, ~/.codex/hooks.json
22
22
  - opencode: ~/.config/opencode/
23
23
 
24
- Registered rules (~/.softspark/ai-toolkit/rules/*.md) are also injected into
24
+ Registered rules (~/.softspark/ai-toolkit/rules/*.md) are also synced into
25
25
  all of the above. Add rules with: ai-toolkit add-rule <rule.md>
26
26
 
27
27
  Usage:
@@ -412,7 +412,7 @@ def print_summary(local: bool = False) -> None:
412
412
  else:
413
413
  print()
414
414
  print("Next steps:")
415
- print(" 1. Edit ~/.claude/CLAUDE.md -- add your global rules above the toolkit sections")
415
+ print(" 1. Add hand-written global rules under ~/.claude/rules/ (avoid ai-toolkit-* prefix)")
416
416
  print(" 2. Per project: ai-toolkit install --local --editors all")
417
417
  print(" 3. To update: npm install -g @softspark/ai-toolkit@latest && ai-toolkit update")
418
418
  print(" 4. To register rules from other tools: ai-toolkit add-rule <rule.md>")
@@ -750,7 +750,7 @@ def main() -> None:
750
750
  global_eds = get_global_editors() or None
751
751
 
752
752
  installed_eds = install_ai_tools(target_dir, rules_dir, dry_run,
753
- editors=global_eds)
753
+ editors=global_eds, profile=profile)
754
754
  install_persona(target_dir, persona, dry_run)
755
755
  install_strict_git_hooks(profile, local, dry_run)
756
756
 
@@ -20,12 +20,16 @@ from injection import (
20
20
 
21
21
  def install_ai_tools(target_dir: Path, rules_dir: Path,
22
22
  dry_run: bool,
23
- editors: list[str] | None = None) -> list[str]:
23
+ editors: list[str] | None = None,
24
+ profile: str = "standard") -> list[str]:
24
25
  """Install global editor configs.
25
26
 
26
27
  Args:
27
28
  editors: Explicit list of editors to install globally. If None,
28
29
  uses DEFAULT_GLOBAL_EDITORS (empty = Claude only).
30
+ profile: Install profile. Gates each editor's optional native surfaces
31
+ the same way the local install does — hooks at
32
+ standard/strict/full, sub-agents/commands/skills at full.
29
33
 
30
34
  Returns:
31
35
  List of editors that were actually installed (for state tracking).
@@ -43,6 +47,10 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
43
47
  if not eds:
44
48
  return []
45
49
 
50
+ _profile = profile if profile in {"minimal", "standard", "strict", "full"} else "standard"
51
+ add_hooks = _profile in {"standard", "strict", "full"}
52
+ add_native_surfaces = _profile == "full"
53
+
46
54
  print()
47
55
  print("## Other AI Tools (global)")
48
56
  print()
@@ -57,6 +65,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
57
65
  if dry_run:
58
66
  print(" Would inject: ~/.codeium/windsurf/memories/global_rules.md")
59
67
  print(" Would generate: ~/.codeium/windsurf/skills/ai-toolkit-skill-catalogue/SKILL.md")
68
+ print(" Would inject: ~/.config/devin/AGENTS.md (Devin CLI global rules)")
60
69
  else:
61
70
  _install_windsurf_global(target_dir, rules_dir)
62
71
  installed.append("windsurf")
@@ -65,16 +74,37 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
65
74
  gemini_file = target_dir / ".gemini" / "GEMINI.md"
66
75
  if dry_run:
67
76
  print(" Would inject: ~/.gemini/GEMINI.md")
77
+ if add_hooks:
78
+ print(" Would generate: ~/.gemini/settings.json (hooks)")
79
+ if add_native_surfaces:
80
+ print(" Would generate: ~/.gemini/commands/, ~/.gemini/skills/")
68
81
  else:
69
82
  inject_with_rules("generate-gemini.sh", gemini_file, rules_dir)
83
+ # Hooks (~/.gemini/settings.json), commands (~/.gemini/commands/),
84
+ # and the skills pointer (~/.gemini/skills/) are documented user-tier
85
+ # surfaces — install them globally, gated like the local install.
86
+ if add_hooks:
87
+ _try_generator("generate_gemini_hooks", target_dir)
88
+ if add_native_surfaces:
89
+ _try_generator("generate_gemini_commands", target_dir)
90
+ _try_generator("generate_gemini_skills", target_dir)
70
91
  installed.append("gemini")
71
92
 
72
93
  if "augment" in eds:
73
94
  augment_file = target_dir / ".augment" / "rules" / "ai-toolkit.md"
74
95
  if dry_run:
75
96
  print(" Would inject: ~/.augment/rules/ai-toolkit.md")
97
+ if add_native_surfaces:
98
+ print(" Would generate: ~/.augment/agents/, ~/.augment/commands/, ~/.augment/settings.json (hooks)")
76
99
  else:
77
100
  inject_with_rules("generate-augment.sh", augment_file, rules_dir)
101
+ # Sub-agents, commands, and hooks all have documented user-tier
102
+ # surfaces under ~/.augment — a global-only Augment user otherwise
103
+ # gets no hooks/agents/commands. Gate them like the local install.
104
+ if add_native_surfaces:
105
+ _try_generator("generate_augment_agents", target_dir)
106
+ _try_generator("generate_augment_commands", target_dir)
107
+ _try_generator("generate_augment_hooks", target_dir)
78
108
  installed.append("augment")
79
109
 
80
110
  if "cline" in eds:
@@ -88,8 +118,15 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
88
118
  if "roo" in eds:
89
119
  if dry_run:
90
120
  print(" Would generate: ~/.roo/rules/ai-toolkit-*.md")
121
+ if "codex" not in eds:
122
+ print(" Would generate: ~/.agents/skills/* (Roo native skill discovery)")
91
123
  else:
92
124
  _install_roo_global(target_dir, rules_dir)
125
+ # Roo/Zoo Code natively discover skills from ~/.agents/skills. Reuse
126
+ # the shared installer, but skip when codex is also selected — its
127
+ # branch populates the same directory — to avoid a duplicate pass.
128
+ if "codex" not in eds:
129
+ _install_codex_skills(target_dir)
93
130
  installed.append("roo")
94
131
 
95
132
  if "aider" in eds:
@@ -101,7 +138,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
101
138
 
102
139
  if "codex" in eds:
103
140
  if dry_run:
104
- print(" Would inject: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json")
141
+ print(" Would inject: ~/.codex/AGENTS.md, ~/.agents/skills/, ~/.codex/hooks.json")
105
142
  else:
106
143
  _install_codex_global(target_dir, rules_dir)
107
144
  installed.append("codex")
@@ -114,28 +151,85 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
114
151
  _install_opencode_global(target_dir, rules_dir)
115
152
  installed.append("opencode")
116
153
 
154
+ if "cursor" in eds:
155
+ # Cursor RULES have no mergeable global file surface (Settings UI only),
156
+ # but ~/.cursor/hooks.json is a documented user-level hooks scope — so a
157
+ # global Cursor install activates the safety/quality hooks everywhere.
158
+ if dry_run:
159
+ if add_hooks:
160
+ print(" Would generate: ~/.cursor/hooks.json (global hooks)")
161
+ elif add_hooks:
162
+ _try_generator("generate_cursor_hooks", target_dir)
163
+ installed.append("cursor")
164
+
165
+ if "copilot" in eds:
166
+ # Copilot CLI reads user-level instructions from ~/.copilot/. RULES have
167
+ # a documented global surface here even though .github/ stays repo-only.
168
+ copilot_root = target_dir / ".copilot"
169
+ if dry_run:
170
+ print(" Would inject: ~/.copilot/copilot-instructions.md")
171
+ print(" Would generate: ~/.copilot/instructions/ai-toolkit-*.instructions.md")
172
+ else:
173
+ inject_with_rules(
174
+ "generate_copilot.py",
175
+ copilot_root / "copilot-instructions.md",
176
+ rules_dir,
177
+ )
178
+ _try_generator("generate_copilot", target_dir,
179
+ rules_dir=rules_dir, config_root=copilot_root,
180
+ emit_prompts=False)
181
+ installed.append("copilot")
182
+
183
+ if "antigravity" in eds:
184
+ # Antigravity RULES stay project-local, but the skill pointer has a
185
+ # documented global surface: ~/.gemini/config/skills (all Antigravity
186
+ # products) and ~/.gemini/antigravity-cli/skills (CLI-private).
187
+ if dry_run:
188
+ print(" Would generate: ~/.gemini/config/skills/, ~/.gemini/antigravity-cli/skills/ (skill pointer)")
189
+ else:
190
+ from generate_antigravity import generate_global as gen_antigravity_global
191
+ gen_antigravity_global(target_dir)
192
+ installed.append("antigravity")
193
+
117
194
  print()
118
195
  print(f" Available: {', '.join(GLOBAL_CAPABLE_EDITORS)}")
119
- print(" Note: Cursor, Copilot, and Antigravity rule installs are project-local; use 'ai-toolkit install --local' for those.")
196
+ print(" Note: Cursor and Antigravity RULES stay project-local (no mergeable "
197
+ "global file surface); their global installs cover hooks/skills only. "
198
+ "Use 'ai-toolkit install --local' for full per-project setup.")
120
199
 
121
200
  return installed
122
201
 
123
202
 
124
203
  def _install_codex_global(target_dir: Path, rules_dir: Path) -> None:
125
- """Install Codex at the global level (~/ layer).
204
+ """Install Codex at the global level (~/.codex layer).
126
205
 
127
206
  Creates:
128
- - ~/AGENTS.md (marker injection; universal coding rules inlined here —
129
- Codex reads instructions only from AGENTS.md, not .agents/rules/)
130
- - ~/.agents/skills/* (skill symlinks)
207
+ - ~/.codex/AGENTS.md (marker injection; universal coding rules inlined
208
+ here). Codex reads GLOBAL instructions from ``$CODEX_HOME/AGENTS.md``
209
+ (default ~/.codex/AGENTS.md), NOT ~/AGENTS.md — a home-root AGENTS.md
210
+ is only loaded in the degenerate case where a session's cwd is $HOME.
211
+ - ~/.agents/skills/* (skill symlinks; a documented Codex skill dir)
131
212
  - ~/.codex/hooks.json (lifecycle hooks)
132
213
  """
133
214
  inject_with_rules(
134
215
  "generate_codex.py",
135
- target_dir / "AGENTS.md",
216
+ target_dir / ".codex" / "AGENTS.md",
136
217
  rules_dir,
137
218
  )
138
219
 
220
+ # Migration: earlier versions wrote global instructions to ~/AGENTS.md,
221
+ # which Codex never loads as global instructions. Strip that stale toolkit
222
+ # section so upgraders are not left with dead, unread content.
223
+ if _strip_toolkit_sections(target_dir / "AGENTS.md"):
224
+ print(" Migrated: removed stale ai-toolkit section from ~/AGENTS.md")
225
+
226
+ override = target_dir / ".codex" / "AGENTS.override.md"
227
+ if override.is_file() and override.read_text(encoding="utf-8").strip():
228
+ print(
229
+ " Warning: ~/.codex/AGENTS.override.md exists and takes precedence "
230
+ "over ~/.codex/AGENTS.md — toolkit rules will be masked."
231
+ )
232
+
139
233
  from generate_codex_hooks import generate as gen_codex_hooks
140
234
  gen_codex_hooks(target_dir)
141
235
  print(" Created: ~/.codex/hooks.json")
@@ -166,6 +260,13 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
166
260
  windsurf_file = target_dir / ".codeium" / "windsurf" / "memories" / "global_rules.md"
167
261
  inject_with_rules("generate-windsurf.sh", windsurf_file, rules_dir)
168
262
 
263
+ # Devin CLI reads global rules from ~/.config/devin/AGENTS.md, NOT the
264
+ # Desktop ~/.codeium/windsurf/memories/global_rules.md (that path is absent
265
+ # from read_config_from.windsurf). Cover editor-only Devin CLI installs
266
+ # where no global Claude install (~/.claude/CLAUDE.md compat read) exists.
267
+ devin_agents = target_dir / ".config" / "devin" / "AGENTS.md"
268
+ inject_with_rules("generate-windsurf.sh", devin_agents, rules_dir)
269
+
169
270
  from generate_windsurf_skills import generate as gen_windsurf_skills
170
271
  # Windsurf is excluded from the .claude/skills conditional: its native scan
171
272
  # of .claude/skills is gated behind a Devin "Claude Code config reading"
@@ -352,6 +453,35 @@ def inject_with_rules(
352
453
  print(f" Updated: {target_file}")
353
454
 
354
455
 
456
+ def _strip_toolkit_sections(target_file: Path) -> bool:
457
+ """Remove ai-toolkit ``<!-- TOOLKIT:* -->`` marker sections from an existing
458
+ file, preserving any non-toolkit content. Returns True if the file changed.
459
+
460
+ Used to clean a stale ~/AGENTS.md after Codex global instructions moved to
461
+ ~/.codex/AGENTS.md. If stripping leaves the file empty (it only ever held
462
+ toolkit content), the file is removed.
463
+ """
464
+ if not target_file.is_file():
465
+ return False
466
+ import re
467
+
468
+ original = target_file.read_text(encoding="utf-8")
469
+ stripped = re.sub(
470
+ r"<!-- TOOLKIT:[^ ]+ START -->.*?<!-- TOOLKIT:[^ ]+ END -->\n?",
471
+ "",
472
+ original,
473
+ flags=re.DOTALL,
474
+ )
475
+ if stripped == original:
476
+ return False
477
+ stripped = stripped.lstrip("\n")
478
+ if stripped.strip():
479
+ target_file.write_text(stripped, encoding="utf-8")
480
+ else:
481
+ target_file.unlink()
482
+ return True
483
+
484
+
355
485
  def _inject_text_section(target_file: Path, section: str, text: str) -> None:
356
486
  """Inject generated text into one marker section without touching others."""
357
487
  target_file.parent.mkdir(parents=True, exist_ok=True)
@@ -661,17 +791,16 @@ def _apply_extends_config(cwd: Path, merged: dict) -> None:
661
791
 
662
792
 
663
793
  def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
664
- """Inject ``app/rules/common/*.md`` content into project's ``.claude/CLAUDE.md``.
794
+ """Install Claude language-rule entrypoints for a project.
665
795
 
666
796
  Per-language rules (``app/rules/<lang>/``) are NOT injected here -- they
667
797
  ship as ``<lang>-rules`` knowledge skills under ``app/skills/`` and load
668
- contextually via the Agent Skills progressive-disclosure mechanism. This
669
- keeps ``CLAUDE.md`` small while ensuring language-specific guidance still
670
- reaches Claude when relevant.
798
+ contextually via the Agent Skills progressive-disclosure mechanism.
671
799
 
672
- Common rules are language-agnostic (security, git workflow, testing,
673
- coding-style, performance) and stay inlined so they remain in scope for
674
- every prompt.
800
+ Common rules are written as Claude Code path-scoped rules under
801
+ ``.claude/rules/``. Current Claude Code guidance targets under 200 lines
802
+ per ``CLAUDE.md`` file; path-scoped rules keep startup context smaller
803
+ while still loading the rule bodies when project files are opened.
675
804
  """
676
805
  if not language_modules:
677
806
  return
@@ -681,6 +810,8 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
681
810
  if not common_dir.is_dir():
682
811
  return
683
812
 
813
+ rule_files = _sync_claude_common_rules(cwd, common_dir)
814
+
684
815
  # Detect requested per-language modules so we can name the linked skills
685
816
  # in the marker block. The modules themselves are not inlined.
686
817
  langs: list[str] = []
@@ -690,38 +821,30 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
690
821
  if name != "common":
691
822
  langs.append(name)
692
823
 
693
- # Inline full content of every common rule file, stripping YAML
694
- # frontmatter so the resulting block reads as plain Markdown.
695
- inlined: list[str] = []
696
- for f in sorted(common_dir.glob("*.md")):
697
- body = f.read_text(encoding="utf-8")
698
- if body.startswith("---"):
699
- end = body.find("\n---", 3)
700
- if end != -1:
701
- body = body[end + 4:].lstrip("\n")
702
- inlined.append(body.rstrip())
703
-
704
824
  lines: list[str] = ["# Language Rules", ""]
705
825
  lines.append(
706
- "Common (language-agnostic) rules apply to every change in this "
707
- "project. Language-specific rules live in `<lang>-rules` knowledge "
708
- "skills (e.g. `python-rules`, `typescript-rules`) and load "
709
- "automatically when their triggers match -- you do not need to "
710
- "Read them manually."
826
+ "Common ai-toolkit rules live in `.claude/rules/ai-toolkit-*.md` "
827
+ "with Claude Code `paths` frontmatter so they load when project files "
828
+ "are opened instead of expanding this CLAUDE.md at session startup."
829
+ )
830
+ if rule_files:
831
+ lines.append("")
832
+ lines.append("Common rule files: " + ", ".join(f"`{p}`" for p in rule_files) + ".")
833
+ lines.append("")
834
+ lines.append(
835
+ "Language-specific rules live in `<lang>-rules` knowledge skills "
836
+ "(e.g. `python-rules`, `typescript-rules`) and load automatically "
837
+ "when their triggers match -- you do not need to Read them manually."
711
838
  )
712
839
  if langs:
713
840
  skill_names = ", ".join(f"`{l}-rules`" for l in langs)
714
841
  lines.append("")
715
842
  lines.append(f"Detected languages: {skill_names}.")
716
- lines.append("")
717
- lines.append("---")
718
- lines.append("")
719
- lines.extend(inlined)
720
843
 
721
844
  # Write to temp file, then inject as a single named section so reruns are
722
845
  # idempotent (existing block is replaced, not duplicated).
723
846
  import tempfile
724
- combined = "\n\n".join(lines).rstrip() + "\n"
847
+ combined = "\n".join(lines).rstrip() + "\n"
725
848
  with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False,
726
849
  encoding="utf-8") as tmp:
727
850
  tmp.write(combined)
@@ -738,6 +861,51 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
738
861
  tmp_path.unlink(missing_ok=True)
739
862
 
740
863
 
864
+ def _strip_rule_frontmatter(text: str) -> str:
865
+ if text.startswith("---"):
866
+ end = text.find("\n---", 3)
867
+ if end != -1:
868
+ return text[end + 4:].lstrip("\n")
869
+ return text
870
+
871
+
872
+ def _sync_claude_common_rules(cwd: Path, common_dir: Path) -> list[str]:
873
+ """Write common ai-toolkit rules as Claude Code path-scoped rules.
874
+
875
+ Only ``ai-toolkit-*.md`` files are managed. User-authored files in
876
+ ``.claude/rules/`` are preserved.
877
+ """
878
+ rules_dir = cwd / ".claude" / "rules"
879
+ rules_dir.mkdir(parents=True, exist_ok=True)
880
+
881
+ source_files = sorted(common_dir.glob("*.md"))
882
+ expected = {f"ai-toolkit-{src.stem}.md" for src in source_files}
883
+ for stale in sorted(rules_dir.glob("ai-toolkit-*.md")):
884
+ if stale.name not in expected:
885
+ stale.unlink()
886
+
887
+ written: list[str] = []
888
+ for src in source_files:
889
+ body = _strip_rule_frontmatter(src.read_text(encoding="utf-8")).rstrip()
890
+ rel = Path(".claude") / "rules" / f"ai-toolkit-{src.stem}.md"
891
+ target = cwd / rel
892
+ target.write_text(
893
+ "\n".join([
894
+ "---",
895
+ "paths:",
896
+ ' - "**/*"',
897
+ "---",
898
+ "",
899
+ body,
900
+ "",
901
+ ]),
902
+ encoding="utf-8",
903
+ )
904
+ written.append(rel.as_posix())
905
+
906
+ return written
907
+
908
+
741
909
  def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
742
910
  profile: str = "standard",
743
911
  codex_skills: bool = False) -> None:
@@ -750,6 +918,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
750
918
  print(" Would create: CLAUDE.md (if missing)")
751
919
  print(" Would create: .claude/settings.local.json (if missing)")
752
920
  print(" Would inject: .claude/constitution.md")
921
+ print(" Would generate: .claude/rules/ai-toolkit-*.md")
753
922
 
754
923
  add_copilot_dir = profile in {"standard", "strict", "full"}
755
924
  add_gemini_hooks = profile in {"standard", "strict", "full"}
@@ -131,13 +131,20 @@ DEFAULT_GLOBAL_EDITORS: list[str] = []
131
131
 
132
132
  # All editors that support global install (opt-in via --editors).
133
133
  #
134
- # Cursor intentionally stays out of this list: its documented global rules
135
- # surface is the Settings UI, not a stable file path we can merge safely.
134
+ # Scope varies by editor's documented global file surfaces:
135
+ # - cursor: HOOKS only (~/.cursor/hooks.json). Cursor RULES stay project-local
136
+ # — their only global surface is the Settings UI, not a mergeable file.
137
+ # - copilot: user-level instructions (~/.copilot/, read by Copilot CLI).
138
+ # - antigravity: global skill pointer (~/.gemini/config/skills,
139
+ # ~/.gemini/antigravity-cli/skills). Rules stay project-local.
136
140
  GLOBAL_CAPABLE_EDITORS = [
137
141
  "aider",
142
+ "antigravity",
138
143
  "augment",
139
144
  "cline",
140
145
  "codex",
146
+ "copilot",
147
+ "cursor",
141
148
  "gemini",
142
149
  "opencode",
143
150
  "roo",