@softspark/ai-toolkit 4.11.0 → 4.13.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 (46) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +33 -15
  3. package/app/.claude-plugin/plugin.json +11 -19
  4. package/app/ARCHITECTURE.md +6 -6
  5. package/app/claude-app/global-instructions.md +10 -0
  6. package/app/claude-app/hooks/hooks.json +284 -0
  7. package/app/claude-app/skills/ai-toolkit-rules/SKILL.md +359 -0
  8. package/app/hooks/config-desync-guard.sh +63 -23
  9. package/app/plugins/README.md +4 -1
  10. package/benchmarks/ecosystem-doctor-snapshot.json +91 -25
  11. package/bin/ai-toolkit.js +21 -3
  12. package/kb/planning/drop-cascade-hooks-after-sunset.md +13 -8
  13. package/kb/procedures/ecosystem-sync-sop.md +5 -5
  14. package/kb/procedures/maintenance-sop.md +42 -4
  15. package/kb/procedures/release-preparation-sop.md +5 -1
  16. package/kb/procedures/release-verification-sop.md +25 -9
  17. package/kb/reference/architecture-overview.md +7 -2
  18. package/kb/reference/claude-ecosystem-expansion-foundations.md +12 -3
  19. package/kb/reference/cli-reference.md +4 -2
  20. package/kb/reference/codex-cli-compatibility.md +15 -10
  21. package/kb/reference/global-install-model.md +37 -11
  22. package/kb/reference/hooks-catalog.md +2 -3
  23. package/kb/reference/plugin-pack-conventions.md +5 -5
  24. package/kb/reference/skills-catalog.md +2 -0
  25. package/kb/reference/supported-tools-registry.md +59 -33
  26. package/kb/reference/unique-features.md +3 -2
  27. package/llms-full.txt +236 -98
  28. package/manifest.json +8 -8
  29. package/package.json +4 -3
  30. package/scripts/claude_app.py +347 -0
  31. package/scripts/doctor.py +85 -4
  32. package/scripts/ecosystem_tools.json +45 -9
  33. package/scripts/generate_antigravity.py +15 -0
  34. package/scripts/generate_codex_hooks.py +8 -5
  35. package/scripts/generate_copilot.py +10 -3
  36. package/scripts/generate_devin_hooks.py +3 -4
  37. package/scripts/generate_windsurf_skills.py +5 -6
  38. package/scripts/inject_hook_cli.py +16 -2
  39. package/scripts/install.py +25 -23
  40. package/scripts/install_steps/ai_tools.py +234 -14
  41. package/scripts/install_steps/install_state.py +11 -2
  42. package/scripts/plugin.py +31 -8
  43. package/scripts/update_projects.py +12 -1
  44. package/scripts/validate.py +34 -2
  45. package/AGENTS.md +0 -790
  46. package/scripts/generate_windsurf_hooks.py +0 -152
@@ -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"
@@ -3,10 +3,9 @@
3
3
 
4
4
  Devin CLI uses a hook format **compatible with Claude Code hooks**
5
5
  (docs.devin.ai/cli/extensibility/hooks/overview). This generator is the
6
- replacement for the deprecated Cascade ``.windsurf/hooks.json`` surface,
7
- which stops working when Cascade sunsets on 2026-07-01. Devin Local / Devin
8
- CLI do NOT read ``.windsurf/hooks.json`` as a fallback, so the hooks must be
9
- regenerated onto this new file.
6
+ replacement for the removed Cascade ``.windsurf/hooks.json`` surface, which
7
+ stopped working on 2026-07-01. Devin Local and the standalone Devin CLI do not
8
+ read that legacy file as a fallback, so hooks are emitted here.
10
9
 
11
10
  Output file: ``<target>/.devin/hooks.v1.json``. Per the Devin docs the
12
11
  standalone ``hooks.v1.json`` file's entire contents ARE the hooks object —
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- """Generate a Devin Desktop (formerly Windsurf) skill pointer.
2
+ """Generate a Devin/Windsurf skill pointer.
3
3
 
4
- Dual-emits under ``.devin/skills/`` (primary since the 2026-06-02 rebrand)
5
- and ``.windsurf/skills/`` (legacy fallback). Pass an explicit ``skill_root``
6
- to emit a single location instead (used for the ``~/.codeium/windsurf/``
7
- global install path).
4
+ Current Devin docs list ``.windsurf/skills/`` as a supported cross-tool skill
5
+ path and do not list ``.devin/skills/``. Pass an explicit ``skill_root`` for a
6
+ single alternate location (used by the legacy HOME-scoped Windsurf install).
8
7
  """
9
8
  from __future__ import annotations
10
9
 
@@ -14,7 +13,7 @@ from pathlib import Path
14
13
  sys.path.insert(0, str(Path(__file__).resolve().parent))
15
14
  from skill_pointer import POINTER_SKILL_NAME, write_pointer_skill
16
15
 
17
- DEFAULT_SKILL_ROOTS: tuple[str, ...] = (".devin/skills", ".windsurf/skills")
16
+ DEFAULT_SKILL_ROOTS: tuple[str, ...] = (".windsurf/skills",)
18
17
 
19
18
 
20
19
  def generate(target_dir: Path, *, emit_skill_pointer: bool = True,
@@ -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
  # ---------------------------------------------------------------------------
@@ -18,7 +18,7 @@ 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
24
  Registered rules (~/.softspark/ai-toolkit/rules/*.md) are also synced into
@@ -750,40 +750,42 @@ 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
 
757
757
  # Record install state (skip for dry-run)
758
758
  if not dry_run:
759
- auto_detected = None
760
- if auto_detect:
761
- auto_detected = detect_languages(project_dir, toolkit_dir)
762
-
763
- # Determine what modules to record
764
- if resolved_modules is not None:
765
- record_modules = resolved_modules
766
- else:
767
- # Legacy mode: infer modules from profile/only
768
- record_modules = _infer_modules_from_legacy(profile, only)
769
-
770
759
  # Extract extends metadata if available
771
760
  extends_info = None
772
761
  merged = cfg.get("_merged_config")
773
762
  if merged and merged.get("_extends_meta"):
774
763
  extends_info = merged["_extends_meta"]
775
764
 
776
- record_install(
777
- version=_get_toolkit_version(),
778
- modules=record_modules,
779
- profile=profile or "standard",
780
- auto_detected=auto_detected,
781
- extends_info=extends_info,
782
- )
765
+ # state.json describes the global install and drives future `update`
766
+ # invocations. Project-local installs belong in projects.json and must
767
+ # never replace the global profile/modules with the last project's
768
+ # auto-detected language set.
769
+ if not local:
770
+ auto_detected = None
771
+ if auto_detect:
772
+ auto_detected = detect_languages(project_dir, toolkit_dir)
773
+
774
+ if resolved_modules is not None:
775
+ record_modules = resolved_modules
776
+ else:
777
+ record_modules = _infer_modules_from_legacy(profile, only)
778
+
779
+ record_install(
780
+ version=_get_toolkit_version(),
781
+ modules=record_modules,
782
+ profile=profile or "standard",
783
+ auto_detected=auto_detected,
784
+ extends_info=extends_info,
785
+ )
783
786
 
784
- # Record global editors (only for global install, not --local)
785
- if not local and installed_eds:
786
- record_global_editors(installed_eds)
787
+ if installed_eds:
788
+ record_global_editors(installed_eds)
787
789
 
788
790
  # Register project in global registry (for `ai-toolkit update` propagation)
789
791
  # Skipped when called from update_projects.py (--skip-register) to avoid
@@ -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,22 +74,46 @@ 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:
81
111
  if dry_run:
82
112
  print(" Would generate: ~/Documents/Cline/Rules/ai-toolkit-*.md")
83
- print(" Would generate: ~/.cline/skills/ai-toolkit-skill-catalogue/SKILL.md")
113
+ if _claude_skills_discoverable(target_dir):
114
+ print(" Would reuse: ~/.claude/skills/ (Cline-compatible discovery)")
115
+ else:
116
+ print(" Would generate: ~/.cline/skills/ai-toolkit-skill-catalogue/SKILL.md")
84
117
  else:
85
118
  _install_cline_global(target_dir, rules_dir)
86
119
  installed.append("cline")
@@ -88,8 +121,15 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
88
121
  if "roo" in eds:
89
122
  if dry_run:
90
123
  print(" Would generate: ~/.roo/rules/ai-toolkit-*.md")
124
+ if "codex" not in eds:
125
+ print(" Would generate: ~/.agents/skills/* (Roo native skill discovery)")
91
126
  else:
92
127
  _install_roo_global(target_dir, rules_dir)
128
+ # Roo/Zoo Code natively discover skills from ~/.agents/skills. Reuse
129
+ # the shared installer, but skip when codex is also selected — its
130
+ # branch populates the same directory — to avoid a duplicate pass.
131
+ if "codex" not in eds:
132
+ _install_codex_skills(target_dir)
93
133
  installed.append("roo")
94
134
 
95
135
  if "aider" in eds:
@@ -101,7 +141,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
101
141
 
102
142
  if "codex" in eds:
103
143
  if dry_run:
104
- print(" Would inject: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json")
144
+ print(" Would inject: ~/.codex/AGENTS.md, ~/.agents/skills/, ~/.codex/hooks.json")
105
145
  else:
106
146
  _install_codex_global(target_dir, rules_dir)
107
147
  installed.append("codex")
@@ -114,28 +154,85 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
114
154
  _install_opencode_global(target_dir, rules_dir)
115
155
  installed.append("opencode")
116
156
 
157
+ if "cursor" in eds:
158
+ # Cursor RULES have no mergeable global file surface (Settings UI only),
159
+ # but ~/.cursor/hooks.json is a documented user-level hooks scope — so a
160
+ # global Cursor install activates the safety/quality hooks everywhere.
161
+ if dry_run:
162
+ if add_hooks:
163
+ print(" Would generate: ~/.cursor/hooks.json (global hooks)")
164
+ elif add_hooks:
165
+ _try_generator("generate_cursor_hooks", target_dir)
166
+ installed.append("cursor")
167
+
168
+ if "copilot" in eds:
169
+ # Copilot CLI reads user-level instructions from ~/.copilot/. RULES have
170
+ # a documented global surface here even though .github/ stays repo-only.
171
+ copilot_root = target_dir / ".copilot"
172
+ if dry_run:
173
+ print(" Would inject: ~/.copilot/copilot-instructions.md")
174
+ print(" Would generate: ~/.copilot/instructions/ai-toolkit-*.instructions.md")
175
+ else:
176
+ inject_with_rules(
177
+ "generate_copilot.py",
178
+ copilot_root / "copilot-instructions.md",
179
+ rules_dir,
180
+ )
181
+ _try_generator("generate_copilot", target_dir,
182
+ rules_dir=rules_dir, config_root=copilot_root,
183
+ emit_prompts=False)
184
+ installed.append("copilot")
185
+
186
+ if "antigravity" in eds:
187
+ # Antigravity RULES stay project-local, but the skill pointer has a
188
+ # documented global surface: ~/.gemini/config/skills (all Antigravity
189
+ # products) and ~/.gemini/antigravity-cli/skills (CLI-private).
190
+ if dry_run:
191
+ print(" Would generate: ~/.gemini/config/skills/, ~/.gemini/antigravity-cli/skills/ (skill pointer)")
192
+ else:
193
+ from generate_antigravity import generate_global as gen_antigravity_global
194
+ gen_antigravity_global(target_dir)
195
+ installed.append("antigravity")
196
+
117
197
  print()
118
198
  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.")
199
+ print(" Note: Cursor and Antigravity RULES stay project-local (no mergeable "
200
+ "global file surface); their global installs cover hooks/skills only. "
201
+ "Use 'ai-toolkit install --local' for full per-project setup.")
120
202
 
121
203
  return installed
122
204
 
123
205
 
124
206
  def _install_codex_global(target_dir: Path, rules_dir: Path) -> None:
125
- """Install Codex at the global level (~/ layer).
207
+ """Install Codex at the global level (~/.codex layer).
126
208
 
127
209
  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)
210
+ - ~/.codex/AGENTS.md (marker injection; universal coding rules inlined
211
+ here). Codex reads GLOBAL instructions from ``$CODEX_HOME/AGENTS.md``
212
+ (default ~/.codex/AGENTS.md), NOT ~/AGENTS.md — a home-root AGENTS.md
213
+ is only loaded in the degenerate case where a session's cwd is $HOME.
214
+ - ~/.agents/skills/* (skill symlinks; a documented Codex skill dir)
131
215
  - ~/.codex/hooks.json (lifecycle hooks)
132
216
  """
133
217
  inject_with_rules(
134
218
  "generate_codex.py",
135
- target_dir / "AGENTS.md",
219
+ target_dir / ".codex" / "AGENTS.md",
136
220
  rules_dir,
137
221
  )
138
222
 
223
+ # Migration: earlier versions wrote global instructions to ~/AGENTS.md,
224
+ # which Codex never loads as global instructions. Strip that stale toolkit
225
+ # section so upgraders are not left with dead, unread content.
226
+ if _strip_toolkit_sections(target_dir / "AGENTS.md"):
227
+ print(" Migrated: removed stale ai-toolkit section from ~/AGENTS.md")
228
+
229
+ override = target_dir / ".codex" / "AGENTS.override.md"
230
+ if override.is_file() and override.read_text(encoding="utf-8").strip():
231
+ print(
232
+ " Warning: ~/.codex/AGENTS.override.md exists and takes precedence "
233
+ "over ~/.codex/AGENTS.md — toolkit rules will be masked."
234
+ )
235
+
139
236
  from generate_codex_hooks import generate as gen_codex_hooks
140
237
  gen_codex_hooks(target_dir)
141
238
  print(" Created: ~/.codex/hooks.json")
@@ -166,6 +263,13 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
166
263
  windsurf_file = target_dir / ".codeium" / "windsurf" / "memories" / "global_rules.md"
167
264
  inject_with_rules("generate-windsurf.sh", windsurf_file, rules_dir)
168
265
 
266
+ # Devin CLI reads global rules from ~/.config/devin/AGENTS.md, NOT the
267
+ # Desktop ~/.codeium/windsurf/memories/global_rules.md (that path is absent
268
+ # from read_config_from.windsurf). Cover editor-only Devin CLI installs
269
+ # where no global Claude install (~/.claude/CLAUDE.md compat read) exists.
270
+ devin_agents = target_dir / ".config" / "devin" / "AGENTS.md"
271
+ inject_with_rules("generate-windsurf.sh", devin_agents, rules_dir)
272
+
169
273
  from generate_windsurf_skills import generate as gen_windsurf_skills
170
274
  # Windsurf is excluded from the .claude/skills conditional: its native scan
171
275
  # of .claude/skills is gated behind a Devin "Claude Code config reading"
@@ -176,6 +280,80 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
176
280
  )
177
281
 
178
282
 
283
+ def _cleanup_retired_windsurf_surfaces(cwd: Path) -> None:
284
+ """Remove only ai-toolkit content from retired/undocumented Devin paths."""
285
+ import json
286
+
287
+ legacy_hooks = cwd / ".windsurf" / "hooks.json"
288
+ if legacy_hooks.is_file():
289
+ try:
290
+ document = json.loads(legacy_hooks.read_text(encoding="utf-8"))
291
+ except (OSError, json.JSONDecodeError):
292
+ print(" Warning: kept invalid .windsurf/hooks.json for manual review")
293
+ else:
294
+ hooks = document.get("hooks") if isinstance(document, dict) else None
295
+ changed = False
296
+ if isinstance(hooks, dict):
297
+ kept_hooks: dict = {}
298
+ for event, entries in hooks.items():
299
+ if not isinstance(entries, list):
300
+ kept_hooks[event] = entries
301
+ continue
302
+ survivors = [
303
+ entry
304
+ for entry in entries
305
+ if not (
306
+ isinstance(entry, dict)
307
+ and (
308
+ entry.get("_source") == "ai-toolkit"
309
+ or any(
310
+ isinstance(handler, dict)
311
+ and handler.get("_source") == "ai-toolkit"
312
+ for handler in entry.get("hooks", [])
313
+ )
314
+ )
315
+ )
316
+ ]
317
+ if len(survivors) != len(entries):
318
+ changed = True
319
+ if survivors:
320
+ kept_hooks[event] = survivors
321
+ if changed:
322
+ if kept_hooks:
323
+ document["hooks"] = kept_hooks
324
+ else:
325
+ document.pop("hooks", None)
326
+ if document:
327
+ legacy_hooks.write_text(
328
+ json.dumps(
329
+ document,
330
+ indent=4,
331
+ ensure_ascii=False,
332
+ sort_keys=True,
333
+ ) + "\n",
334
+ encoding="utf-8",
335
+ )
336
+ else:
337
+ legacy_hooks.unlink()
338
+ print(" Migrated: removed retired ai-toolkit Cascade hooks")
339
+
340
+ retired_pointer = (
341
+ cwd / ".devin" / "skills" / "ai-toolkit-skill-catalogue"
342
+ )
343
+ skill_file = retired_pointer / "SKILL.md"
344
+ if skill_file.is_file() and "name: ai-toolkit-skill-catalogue" in (
345
+ skill_file.read_text(encoding="utf-8")
346
+ ):
347
+ if retired_pointer.is_symlink():
348
+ retired_pointer.unlink()
349
+ else:
350
+ shutil.rmtree(retired_pointer)
351
+ for parent in (retired_pointer.parent, retired_pointer.parent.parent):
352
+ if parent.is_dir() and not any(parent.iterdir()):
353
+ parent.rmdir()
354
+ print(" Migrated: removed undocumented .devin/skills toolkit pointer")
355
+
356
+
179
357
  def _install_cline_global(target_dir: Path, rules_dir: Path) -> None:
180
358
  """Install Cline global rules in the documented ~/.cline directory."""
181
359
  from generate_cline_rules import generate as gen_cline_rules
@@ -352,6 +530,35 @@ def inject_with_rules(
352
530
  print(f" Updated: {target_file}")
353
531
 
354
532
 
533
+ def _strip_toolkit_sections(target_file: Path) -> bool:
534
+ """Remove ai-toolkit ``<!-- TOOLKIT:* -->`` marker sections from an existing
535
+ file, preserving any non-toolkit content. Returns True if the file changed.
536
+
537
+ Used to clean a stale ~/AGENTS.md after Codex global instructions moved to
538
+ ~/.codex/AGENTS.md. If stripping leaves the file empty (it only ever held
539
+ toolkit content), the file is removed.
540
+ """
541
+ if not target_file.is_file():
542
+ return False
543
+ import re
544
+
545
+ original = target_file.read_text(encoding="utf-8")
546
+ stripped = re.sub(
547
+ r"<!-- TOOLKIT:[^ ]+ START -->.*?<!-- TOOLKIT:[^ ]+ END -->\n?",
548
+ "",
549
+ original,
550
+ flags=re.DOTALL,
551
+ )
552
+ if stripped == original:
553
+ return False
554
+ stripped = stripped.lstrip("\n")
555
+ if stripped.strip():
556
+ target_file.write_text(stripped, encoding="utf-8")
557
+ else:
558
+ target_file.unlink()
559
+ return True
560
+
561
+
355
562
  def _inject_text_section(target_file: Path, section: str, text: str) -> None:
356
563
  """Inject generated text into one marker section without touching others."""
357
564
  target_file.parent.mkdir(parents=True, exist_ok=True)
@@ -811,6 +1018,18 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
811
1018
  if ed in eds:
812
1019
  print(msg)
813
1020
 
1021
+ if "windsurf" in eds:
1022
+ if (Path.cwd() / ".windsurf" / "hooks.json").is_file():
1023
+ print(" Would migrate: remove retired ai-toolkit Cascade hooks")
1024
+ if (
1025
+ Path.cwd()
1026
+ / ".devin"
1027
+ / "skills"
1028
+ / "ai-toolkit-skill-catalogue"
1029
+ / "SKILL.md"
1030
+ ).is_file():
1031
+ print(" Would migrate: remove undocumented .devin/skills toolkit pointer")
1032
+
814
1033
  # Profile-driven extras (matrix in kb/reference/global-install-model.md)
815
1034
  if "copilot" in eds and add_copilot_dir:
816
1035
  print(" Would generate: .github/instructions/ + .github/prompts/ (profile >= standard)")
@@ -820,7 +1039,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
820
1039
  if "cursor" in eds:
821
1040
  print(" Would generate: .cursor/hooks.json + .cursor/agents/ + .cursor/skills/ (profile=full)")
822
1041
  if "windsurf" in eds:
823
- print(" Would generate: .devin/hooks.v1.json + .windsurf/hooks.json (Cascade, deprecated) + .devin/skills/ + .windsurf/skills/ (profile=full)")
1042
+ print(" Would generate: .devin/hooks.v1.json + .windsurf/skills/ (profile=full)")
824
1043
  if "cline" in eds:
825
1044
  print(" Would generate: .cline/skills/ (profile=full)")
826
1045
  if "augment" in eds:
@@ -1044,6 +1263,7 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
1044
1263
  _try_generator("generate_cursor_skills", cwd, emit_skill_pointer=emit_pointer)
1045
1264
 
1046
1265
  if "windsurf" in eds:
1266
+ _cleanup_retired_windsurf_surfaces(cwd)
1047
1267
  inject_with_rules(
1048
1268
  "generate-windsurf.sh",
1049
1269
  cwd / ".windsurfrules",
@@ -1053,11 +1273,11 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
1053
1273
  gen_windsurf_rules(cwd, language_modules=language_modules,
1054
1274
  rules_dir=rules_dir)
1055
1275
  if add_native_surfaces:
1056
- # .windsurf/hooks.json is Cascade-scoped and dies 2026-07-01;
1057
- # .devin/hooks.v1.json is the Devin CLI replacement (Claude format).
1058
- _try_generator("generate_windsurf_hooks", cwd)
1276
+ # Cascade's .windsurf/hooks.json surface ended on 2026-07-01.
1277
+ # Devin CLI uses the Claude-compatible .devin/hooks.v1.json format.
1059
1278
  _try_generator("generate_devin_hooks", cwd)
1060
- # Windsurf pointer stays unconditional (its .claude scan is gated).
1279
+ # Current Devin docs list .windsurf/skills as a supported path;
1280
+ # .devin/skills is not documented.
1061
1281
  _try_generator("generate_windsurf_skills", cwd)
1062
1282
 
1063
1283
  if "cline" in eds:
@@ -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",
@@ -190,6 +197,8 @@ def record_install(
190
197
  state["profile"] = profile
191
198
  if auto_detected is not None:
192
199
  state["auto_detected_languages"] = sorted(auto_detected)
200
+ else:
201
+ state.pop("auto_detected_languages", None)
193
202
 
194
203
  if extends_info is not None:
195
204
  state["extends"] = {