@softspark/ai-toolkit 3.0.1 → 3.1.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 (53) hide show
  1. package/AGENTS.md +13 -0
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +34 -20
  4. package/app/.claude-plugin/plugin.json +1 -1
  5. package/app/skills/cpp-rules/SKILL.md +275 -0
  6. package/app/skills/csharp-rules/SKILL.md +282 -0
  7. package/app/skills/dart-rules/SKILL.md +299 -0
  8. package/app/skills/golang-rules/SKILL.md +262 -0
  9. package/app/skills/hook-creator/SKILL.md +7 -3
  10. package/app/skills/introspect/SKILL.md +1 -1
  11. package/app/skills/java-rules/SKILL.md +273 -0
  12. package/app/skills/kotlin-rules/SKILL.md +271 -0
  13. package/app/skills/medplum-rules/SKILL.md +271 -0
  14. package/app/skills/php-rules/SKILL.md +292 -0
  15. package/app/skills/python-rules/SKILL.md +257 -0
  16. package/app/skills/ruby-rules/SKILL.md +286 -0
  17. package/app/skills/rust-rules/SKILL.md +276 -0
  18. package/app/skills/swift-rules/SKILL.md +293 -0
  19. package/app/skills/typescript-rules/SKILL.md +249 -0
  20. package/benchmarks/ecosystem-doctor-snapshot.json +14 -14
  21. package/bin/ai-toolkit.js +5 -1
  22. package/kb/history/completed/deep-coverage-v3-20260423.md +3 -3
  23. package/kb/history/completed/ecosystem-deep-sweep-20260423.md +1 -1
  24. package/kb/procedures/release-preparation-sop.md +4 -4
  25. package/kb/procedures/release-verification-sop.md +11 -12
  26. package/kb/reference/architecture-overview.md +1 -1
  27. package/kb/reference/cli-reference.md +14 -3
  28. package/kb/reference/competitive-features-implementation.md +9 -9
  29. package/kb/reference/global-install-model.md +29 -6
  30. package/kb/reference/hooks-catalog.md +8 -2
  31. package/kb/reference/language-rules.md +54 -18
  32. package/kb/reference/mcp-editor-compatibility.md +4 -3
  33. package/kb/reference/mcp-templates.md +3 -2
  34. package/kb/reference/supported-tools-registry.md +10 -8
  35. package/kb/reference/windows-support.md +50 -0
  36. package/llms-full.txt +220 -72
  37. package/llms.txt +1 -0
  38. package/manifest.json +3 -3
  39. package/package.json +14 -3
  40. package/scripts/_common.py +21 -0
  41. package/scripts/check_deps.py +14 -0
  42. package/scripts/codex_skill_adapter.py +19 -3
  43. package/scripts/ecosystem_tools.json +7 -7
  44. package/scripts/generate_cline_rules.py +17 -8
  45. package/scripts/generate_codex_skills.py +33 -96
  46. package/scripts/generate_language_rules_skills.py +232 -0
  47. package/scripts/generate_roo_rules.py +11 -3
  48. package/scripts/install.py +6 -1
  49. package/scripts/install_steps/ai_tools.py +154 -51
  50. package/scripts/install_steps/install_state.py +14 -2
  51. package/scripts/mcp_editors.py +7 -0
  52. package/scripts/stats.py +126 -39
  53. package/scripts/validate.py +160 -4
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env python3
2
+ """Generate language-rules knowledge skills from app/rules/<lang>/*.md.
3
+
4
+ Each language directory under ``app/rules/`` (except ``common/``) is compiled
5
+ into a single ``app/skills/<lang>-rules/SKILL.md`` knowledge skill. The skill
6
+ is ``user-invocable: false`` so Claude loads it contextually when the
7
+ description triggers match (file extensions, framework names).
8
+
9
+ This is the proper progressive-disclosure replacement for the v1.3.8 pointer
10
+ block in ``.claude/CLAUDE.md``: instead of nudging Claude to Read absolute
11
+ nvm-pinned paths on demand, the rules ride on the Agent Skills mechanism.
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.
15
+
16
+ Idempotent: rerunning overwrites generated SKILL.md but leaves any other
17
+ files in the skill directory alone.
18
+
19
+ Usage:
20
+ python3 scripts/generate_language_rules_skills.py # write all
21
+ python3 scripts/generate_language_rules_skills.py --check # dry-run
22
+ python3 scripts/generate_language_rules_skills.py --langs python,rust
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import sys
28
+ from pathlib import Path
29
+ from textwrap import dedent
30
+
31
+ ROOT = Path(__file__).resolve().parent.parent
32
+ RULES_DIR = ROOT / "app" / "rules"
33
+ SKILLS_DIR = ROOT / "app" / "skills"
34
+
35
+ # Per-language description triggers. Concrete file extensions and framework
36
+ # names give Claude a high-signal match against user prompts and file paths,
37
+ # so the skill activates reliably when the user is actually working in that
38
+ # language.
39
+ TRIGGERS: dict[str, dict[str, str]] = {
40
+ "python": {
41
+ "label": "Python",
42
+ "triggers": ".py, .pyi, pyproject.toml, requirements.txt, Pipfile, FastAPI, Django, Flask, pytest, SQLAlchemy, ruff, mypy",
43
+ },
44
+ "typescript": {
45
+ "label": "TypeScript/JavaScript",
46
+ "triggers": ".ts, .tsx, .js, .jsx, package.json, tsconfig.json, React, Next.js, Vue, Vite, Vitest, Jest, ESLint",
47
+ },
48
+ "golang": {
49
+ "label": "Go",
50
+ "triggers": ".go, go.mod, go.sum, Gin, Echo, Gorilla, testing, gofmt",
51
+ },
52
+ "rust": {
53
+ "label": "Rust",
54
+ "triggers": ".rs, Cargo.toml, Cargo.lock, Tokio, Axum, Serde, clippy, cargo test",
55
+ },
56
+ "java": {
57
+ "label": "Java",
58
+ "triggers": ".java, pom.xml, build.gradle, Spring, Spring Boot, JPA, Hibernate, JUnit, Maven, Gradle",
59
+ },
60
+ "kotlin": {
61
+ "label": "Kotlin",
62
+ "triggers": ".kt, .kts, build.gradle.kts, Ktor, Jetpack Compose, coroutines, kotlinx",
63
+ },
64
+ "swift": {
65
+ "label": "Swift",
66
+ "triggers": ".swift, Package.swift, .xcodeproj, SwiftUI, Combine, async/await, XCTest",
67
+ },
68
+ "dart": {
69
+ "label": "Dart/Flutter",
70
+ "triggers": ".dart, pubspec.yaml, Flutter, Riverpod, Bloc, widget, StatelessWidget, StatefulWidget",
71
+ },
72
+ "csharp": {
73
+ "label": "C#/.NET",
74
+ "triggers": ".cs, .csproj, .sln, ASP.NET, ASP.NET Core, EF Core, LINQ, NUnit, xUnit, dotnet",
75
+ },
76
+ "php": {
77
+ "label": "PHP",
78
+ "triggers": ".php, composer.json, Laravel, Symfony, PHPUnit, PSR-12, Composer",
79
+ },
80
+ "cpp": {
81
+ "label": "C++",
82
+ "triggers": ".cpp, .cc, .cxx, .hpp, .h, CMakeLists.txt, Makefile, GoogleTest, clang-tidy",
83
+ },
84
+ "ruby": {
85
+ "label": "Ruby",
86
+ "triggers": ".rb, Gemfile, .gemspec, Rails, ActiveRecord, Sidekiq, RSpec, Sorbet, rubocop",
87
+ },
88
+ "medplum": {
89
+ "label": "Medplum (FHIR healthcare)",
90
+ "triggers": "medplum.config.mts, medplum.config.ts, FHIR, Medplum, Bot, Subscription, Questionnaire",
91
+ },
92
+ }
93
+
94
+
95
+ def _strip_frontmatter(text: str) -> str:
96
+ """Remove YAML frontmatter (--- ... ---) if present."""
97
+ if not text.startswith("---"):
98
+ return text.lstrip("\n")
99
+ end = text.find("\n---", 3)
100
+ if end == -1:
101
+ return text.lstrip("\n")
102
+ return text[end + 4:].lstrip("\n")
103
+
104
+
105
+ def _category_title(stem: str) -> str:
106
+ """Convert filename stem (e.g. ``coding-style``) to a section title."""
107
+ return " ".join(part.capitalize() for part in stem.split("-"))
108
+
109
+
110
+ def _build_skill_body(lang_dir: Path) -> str:
111
+ """Concatenate all rule category files into a skill body."""
112
+ parts: list[str] = []
113
+ for f in sorted(lang_dir.glob("*.md")):
114
+ body = _strip_frontmatter(f.read_text(encoding="utf-8")).rstrip()
115
+ # If the source file already starts with a top-level "# Title", keep
116
+ # it. Otherwise, prepend a "## Category" header so the skill body
117
+ # has structure.
118
+ if body.lstrip().startswith("#"):
119
+ parts.append(body)
120
+ else:
121
+ parts.append(f"## {_category_title(f.stem)}\n\n{body}")
122
+ return "\n\n".join(parts) + "\n"
123
+
124
+
125
+ def _build_description(lang: str) -> str:
126
+ meta = TRIGGERS.get(lang)
127
+ if not meta:
128
+ return (
129
+ f"{lang.capitalize()} coding rules: coding-style, frameworks, "
130
+ f"patterns, security, testing. Load when writing or reviewing "
131
+ f"{lang.capitalize()} code."
132
+ )
133
+ label = meta["label"]
134
+ triggers = meta["triggers"]
135
+ return (
136
+ f"{label} coding rules from ai-toolkit: coding-style, frameworks, "
137
+ f"patterns, security, testing. "
138
+ f"Triggers: {triggers}. "
139
+ f"Load when writing, reviewing, or editing {label} code."
140
+ )
141
+
142
+
143
+ def _build_skill_md(lang: str, lang_dir: Path) -> str:
144
+ description = _build_description(lang)
145
+ body = _build_skill_body(lang_dir)
146
+ label = TRIGGERS.get(lang, {}).get("label", lang.capitalize())
147
+ frontmatter = dedent(
148
+ f"""\
149
+ ---
150
+ name: {lang}-rules
151
+ description: "{description}"
152
+ effort: medium
153
+ user-invocable: false
154
+ allowed-tools: Read
155
+ ---
156
+
157
+ # {label} Rules
158
+
159
+ These rules come from `app/rules/{lang}/` in ai-toolkit. They cover
160
+ the project's standards for coding style, frameworks, patterns,
161
+ security, and testing in {label}. Apply them when writing or
162
+ reviewing {label} code.
163
+
164
+ """
165
+ )
166
+ return frontmatter + body
167
+
168
+
169
+ def discover_languages() -> list[str]:
170
+ """List language directories under app/rules/ excluding ``common``."""
171
+ if not RULES_DIR.is_dir():
172
+ return []
173
+ out: list[str] = []
174
+ for d in sorted(RULES_DIR.iterdir()):
175
+ if not d.is_dir() or d.name == "common":
176
+ continue
177
+ if any(d.glob("*.md")):
178
+ out.append(d.name)
179
+ return out
180
+
181
+
182
+ def generate(langs: list[str] | None = None, check: bool = False) -> int:
183
+ """Generate skills. Returns count of skills written (or that would be)."""
184
+ languages = langs if langs else discover_languages()
185
+ written = 0
186
+ for lang in languages:
187
+ lang_dir = RULES_DIR / lang
188
+ if not lang_dir.is_dir():
189
+ print(f" SKIP: {lang} (directory missing)", file=sys.stderr)
190
+ continue
191
+ skill_dir = SKILLS_DIR / f"{lang}-rules"
192
+ skill_md = skill_dir / "SKILL.md"
193
+ content = _build_skill_md(lang, lang_dir)
194
+
195
+ if check:
196
+ existing = skill_md.read_text(encoding="utf-8") if skill_md.is_file() else ""
197
+ status = "OK" if existing == content else "DIFF"
198
+ print(f" [{status}] {skill_md.relative_to(ROOT)}")
199
+ if existing != content:
200
+ written += 1
201
+ continue
202
+
203
+ skill_dir.mkdir(parents=True, exist_ok=True)
204
+ skill_md.write_text(content, encoding="utf-8")
205
+ print(f" Wrote: {skill_md.relative_to(ROOT)}")
206
+ written += 1
207
+ return written
208
+
209
+
210
+ def main() -> int:
211
+ ap = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
212
+ ap.add_argument(
213
+ "--langs",
214
+ default="",
215
+ help="Comma-separated language list (default: all)",
216
+ )
217
+ ap.add_argument(
218
+ "--check",
219
+ action="store_true",
220
+ help="Dry-run: report which skills would change without writing",
221
+ )
222
+ args = ap.parse_args()
223
+ langs = [s.strip() for s in args.langs.split(",") if s.strip()] or None
224
+ n = generate(langs=langs, check=args.check)
225
+ if args.check and n > 0:
226
+ print(f"\n{n} skill(s) out of date. Re-run without --check.", file=sys.stderr)
227
+ return 1
228
+ return 0
229
+
230
+
231
+ if __name__ == "__main__":
232
+ sys.exit(main())
@@ -23,12 +23,20 @@ from dir_rules_shared import (
23
23
 
24
24
  def generate(target_dir: Path, *,
25
25
  language_modules: list[str] | None = None,
26
- rules_dir: Path | None = None) -> None:
27
- """Write .roo/rules/*.md files to target_dir."""
26
+ rules_dir: Path | None = None,
27
+ output_root: Path | None = None) -> None:
28
+ """Write Roo Code rule files.
29
+
30
+ By default writes project-local ``target_dir/.roo/rules/*.md``. When
31
+ ``output_root`` is provided, writes directly into that directory for
32
+ documented global rules such as ``~/.roo/rules``.
33
+ """
28
34
  rules = dict(STANDARD_RULES)
29
35
  rules.update(build_language_rules(language_modules))
30
36
  rules.update(build_registered_rules(rules_dir))
31
- write_rules(target_dir, rules, ".roo/rules")
37
+ root = output_root.parent if output_root is not None else target_dir
38
+ subdir = output_root.name if output_root is not None else ".roo/rules"
39
+ write_rules(root, rules, subdir)
32
40
 
33
41
 
34
42
  def main() -> None:
@@ -12,9 +12,14 @@ Claude Code (~/.claude/):
12
12
  - Rules injected into ~/.claude/CLAUDE.md
13
13
 
14
14
  Other tools (global config locations):
15
- - Cursor: ~/.cursor/rules
16
15
  - Windsurf: ~/.codeium/windsurf/memories/global_rules.md
17
16
  - Gemini: ~/.gemini/GEMINI.md
17
+ - Cline: ~/Documents/Cline/Rules/
18
+ - Roo Code: ~/.roo/rules/
19
+ - Aider: ~/.aider.conf.yml (created only if absent)
20
+ - Augment: ~/.augment/rules/ai-toolkit.md
21
+ - Codex: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json
22
+ - opencode: ~/.config/opencode/
18
23
 
19
24
  Registered rules (~/.softspark/ai-toolkit/rules/*.md) are also injected into
20
25
  all of the above. Add rules with: ai-toolkit add-rule <rule.md>
@@ -1,4 +1,4 @@
1
- """Install AI tool configs (Cursor, Windsurf, Gemini, Augment, Codex) and local project setup."""
1
+ """Install global and project-local AI tool configs."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import shutil
@@ -53,14 +53,6 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
53
53
  # control Claude components like agents, hooks, rules). If an editor is
54
54
  # in the requested set, install it unconditionally.
55
55
 
56
- if "cursor" in eds:
57
- cursor_file = target_dir / ".cursor" / "rules"
58
- if dry_run:
59
- print(" Would inject: ~/.cursor/rules")
60
- else:
61
- inject_with_rules("generate-cursor-rules.sh", cursor_file, rules_dir)
62
- installed.append("cursor")
63
-
64
56
  if "windsurf" in eds:
65
57
  windsurf_file = target_dir / ".codeium" / "windsurf" / "memories" / "global_rules.md"
66
58
  if dry_run:
@@ -85,6 +77,27 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
85
77
  inject_with_rules("generate-augment.sh", augment_file, rules_dir)
86
78
  installed.append("augment")
87
79
 
80
+ if "cline" in eds:
81
+ if dry_run:
82
+ print(" Would generate: ~/Documents/Cline/Rules/ai-toolkit-*.md")
83
+ else:
84
+ _install_cline_global(target_dir, rules_dir)
85
+ installed.append("cline")
86
+
87
+ if "roo" in eds:
88
+ if dry_run:
89
+ print(" Would generate: ~/.roo/rules/ai-toolkit-*.md")
90
+ else:
91
+ _install_roo_global(target_dir, rules_dir)
92
+ installed.append("roo")
93
+
94
+ if "aider" in eds:
95
+ if dry_run:
96
+ print(" Would create if missing: ~/.aider.conf.yml + ~/.aider-ai-toolkit-CONVENTIONS.md")
97
+ else:
98
+ _install_aider_global(target_dir, rules_dir)
99
+ installed.append("aider")
100
+
88
101
  if "codex" in eds:
89
102
  if dry_run:
90
103
  print(" Would inject: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json")
@@ -102,7 +115,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
102
115
 
103
116
  print()
104
117
  print(f" Available: {', '.join(GLOBAL_CAPABLE_EDITORS)}")
105
- print(" Note: Copilot, Cline, Roo Code, Aider, Antigravity have no global config -- use 'ai-toolkit install --local' per project")
118
+ print(" Note: Cursor, Copilot, and Antigravity rule installs are project-local; use 'ai-toolkit install --local' for those.")
106
119
 
107
120
  return installed
108
121
 
@@ -136,6 +149,77 @@ def _install_codex_global(target_dir: Path, rules_dir: Path) -> None:
136
149
  _install_codex_skills(target_dir)
137
150
 
138
151
 
152
+ def _install_cline_global(target_dir: Path, rules_dir: Path) -> None:
153
+ """Install Cline global rules in the documented user rules directory."""
154
+ from generate_cline_rules import generate as gen_cline_rules
155
+
156
+ rules_root = target_dir / "Documents" / "Cline" / "Rules"
157
+ gen_cline_rules(
158
+ target_dir,
159
+ rules_dir=rules_dir,
160
+ output_root=rules_root,
161
+ emit_workflows=False,
162
+ managed_scopes=("standard", "custom"),
163
+ )
164
+ print(" Created: ~/Documents/Cline/Rules/ai-toolkit-*.md")
165
+
166
+
167
+ def _install_roo_global(target_dir: Path, rules_dir: Path) -> None:
168
+ """Install Roo Code global rules in ~/.roo/rules."""
169
+ from generate_roo_rules import generate as gen_roo_rules
170
+
171
+ gen_roo_rules(target_dir, rules_dir=rules_dir, output_root=target_dir / ".roo" / "rules")
172
+ print(" Created: ~/.roo/rules/ai-toolkit-*.md")
173
+
174
+
175
+ def _install_aider_global(target_dir: Path, rules_dir: Path) -> None:
176
+ """Install an Aider global config only when it can be created safely.
177
+
178
+ Aider supports ~/.aider.conf.yml, but YAML merging without a parser risks
179
+ clobbering user settings. For existing files we leave the user's config
180
+ untouched and print an explicit next step.
181
+ """
182
+ conventions_file = target_dir / ".aider-ai-toolkit-CONVENTIONS.md"
183
+ inject_with_rules("generate_conventions.py", conventions_file, rules_dir)
184
+
185
+ config_file = target_dir / ".aider.conf.yml"
186
+ if config_file.exists():
187
+ print(" Kept: ~/.aider.conf.yml (already exists; not merging YAML automatically)")
188
+ print(f" Available: {conventions_file} for manual read: entry")
189
+ return
190
+
191
+ default_model = _aider_default_model()
192
+ config_file.write_text(
193
+ "\n".join([
194
+ "# Aider configuration generated by ai-toolkit",
195
+ "# Aider docs: https://aider.chat/docs/config/aider_conf.html",
196
+ "",
197
+ "architect: true",
198
+ "auto-accept-architect: true",
199
+ "",
200
+ "read:",
201
+ f' - "{conventions_file}"',
202
+ "",
203
+ f"model: {default_model}",
204
+ f"editor-model: {default_model}",
205
+ "",
206
+ 'commit-prompt: "Write a short, concise commit message following Conventional Commits (feat/fix/chore/docs/test/refactor)."',
207
+ "attribute-co-authored-by: false",
208
+ "attribute-commit-message-author: false",
209
+ "attribute-commit-message-committer: false",
210
+ "",
211
+ ]),
212
+ encoding="utf-8",
213
+ )
214
+ print(" Created: ~/.aider.conf.yml")
215
+
216
+
217
+ def _aider_default_model() -> str:
218
+ from _common import DEFAULT_CLAUDE_MODELS
219
+
220
+ return DEFAULT_CLAUDE_MODELS["sonnet"]
221
+
222
+
139
223
  def _install_opencode_global(target_dir: Path, rules_dir: Path) -> None:
140
224
  """Install opencode at the global level (~/.config/opencode/).
141
225
 
@@ -331,10 +415,10 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
331
415
  - ``full``: adds every native surface an editor can host (subagents,
332
416
  custom commands, hooks, skill-catalogue pointers).
333
417
 
334
- ``codex_skills`` (opt-in, off by default) additionally writes a full skill
335
- mirror under ``.codex/skills/``. Required even for ``--profile full``
336
- the mirror is intentionally gated behind an extra flag to keep the default
337
- footprint small.
418
+ ``codex_skills`` (opt-in, off by default) explicitly refreshes the full
419
+ Codex skill catalog under ``.agents/skills/``. ``--editors codex`` already
420
+ installs this catalog for normal local installs; the flag is kept as a
421
+ direct generator contract for scripts and dry-run verification.
338
422
 
339
423
  If ``merged_config`` is provided (from .softspark-toolkit.json extends resolution),
340
424
  additional rules and constitution amendments from the base config are injected.
@@ -491,53 +575,67 @@ def _apply_extends_config(cwd: Path, merged: dict) -> None:
491
575
 
492
576
 
493
577
  def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
494
- """Inject language-specific rule summary into project's .claude/CLAUDE.md.
578
+ """Inject ``app/rules/common/*.md`` content into project's ``.claude/CLAUDE.md``.
579
+
580
+ Per-language rules (``app/rules/<lang>/``) are NOT injected here -- they
581
+ ship as ``<lang>-rules`` knowledge skills under ``app/skills/`` and load
582
+ contextually via the Agent Skills progressive-disclosure mechanism. This
583
+ keeps ``CLAUDE.md`` small while ensuring language-specific guidance still
584
+ reaches Claude when relevant.
495
585
 
496
- Instead of injecting full rule content (hundreds of lines), injects a
497
- compact summary with the key rules per category. Full rules are available
498
- as knowledge skills that Claude auto-loads contextually.
586
+ Common rules are language-agnostic (security, git workflow, testing,
587
+ coding-style, performance) and stay inlined so they remain in scope for
588
+ every prompt.
499
589
  """
500
590
  if not language_modules:
501
591
  return
502
592
 
503
593
  rules_src = app_dir / "rules"
504
- if not rules_src.is_dir():
594
+ common_dir = rules_src / "common"
595
+ if not common_dir.is_dir():
505
596
  return
506
597
 
507
- # Detect language names
598
+ # Detect requested per-language modules so we can name the linked skills
599
+ # in the marker block. The modules themselves are not inlined.
508
600
  langs: list[str] = []
509
601
  for mod in language_modules:
510
602
  if mod.startswith("rules-"):
511
- langs.append(mod[6:])
603
+ name = mod[6:]
604
+ if name != "common":
605
+ langs.append(name)
606
+
607
+ # Inline full content of every common rule file, stripping YAML
608
+ # frontmatter so the resulting block reads as plain Markdown.
609
+ inlined: list[str] = []
610
+ for f in sorted(common_dir.glob("*.md")):
611
+ body = f.read_text(encoding="utf-8")
612
+ if body.startswith("---"):
613
+ end = body.find("\n---", 3)
614
+ if end != -1:
615
+ body = body[end + 4:].lstrip("\n")
616
+ inlined.append(body.rstrip())
512
617
 
513
- if not langs:
514
- return
515
-
516
- # Build a lightweight reference pointer — NOT the full rules content.
517
- # Full rules are available as knowledge skills (auto-loaded by Claude)
518
- # and as files Claude can Read on demand.
519
- toolkit_pkg = app_dir.parent
520
618
  lines: list[str] = ["# Language Rules", ""]
521
- lines.append(f"This project uses: **{', '.join(langs)}**")
619
+ lines.append(
620
+ "Common (language-agnostic) rules apply to every change in this "
621
+ "project. Language-specific rules live in `<lang>-rules` knowledge "
622
+ "skills (e.g. `python-rules`, `typescript-rules`) and load "
623
+ "automatically when their triggers match -- you do not need to "
624
+ "Read them manually."
625
+ )
626
+ if langs:
627
+ skill_names = ", ".join(f"`{l}-rules`" for l in langs)
628
+ lines.append("")
629
+ lines.append(f"Detected languages: {skill_names}.")
522
630
  lines.append("")
523
- lines.append("When writing or reviewing code, use the Glob and Read tools to read the rules:")
524
- # Resolve actual installed path for the rules
525
- rules_resolved = str(rules_src.resolve())
526
- all_dirs: list[str] = ["common"]
527
- for l in langs:
528
- if l not in all_dirs:
529
- all_dirs.append(l)
530
- for lang in all_dirs:
531
- lang_path = rules_src / lang
532
- if lang_path.is_dir():
533
- categories = ", ".join(f.stem for f in sorted(lang_path.glob("*.md")))
534
- lines.append(f"- `{lang_path.resolve()}/` ({categories})")
631
+ lines.append("---")
535
632
  lines.append("")
536
- lines.append("Read the relevant rule files before making code changes. Do NOT guess — read first.")
633
+ lines.extend(inlined)
537
634
 
538
- # Write summary to temp file, then inject as section
635
+ # Write to temp file, then inject as a single named section so reruns are
636
+ # idempotent (existing block is replaced, not duplicated).
539
637
  import tempfile
540
- combined = "\n".join(lines)
638
+ combined = "\n\n".join(lines).rstrip() + "\n"
541
639
  with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False,
542
640
  encoding="utf-8") as tmp:
543
641
  tmp.write(combined)
@@ -545,8 +643,11 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
545
643
 
546
644
  try:
547
645
  inject_section(tmp_path, cwd / ".claude" / "CLAUDE.md", "language-rules")
548
- lang_names = [l for l in langs if l != "common"]
549
- print(f" Injected: language rules summary (common + {', '.join(lang_names)})")
646
+ if langs:
647
+ print(f" Injected: common rules + {len(langs)} language skill(s) "
648
+ f"({', '.join(langs)})")
649
+ else:
650
+ print(" Injected: common rules")
550
651
  finally:
551
652
  tmp_path.unlink(missing_ok=True)
552
653
 
@@ -600,8 +701,10 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
600
701
  "$HOME/.augment/settings.json + .augment/skills/ (profile=full)")
601
702
  if "gemini" in eds:
602
703
  print(" Would generate: .gemini/commands/ + .gemini/skills/ (profile=full)")
603
- if "codex" in eds and codex_skills:
604
- print(" Would generate: .codex/skills/ full mirror (--codex-skills)")
704
+ if "codex" in eds:
705
+ print(" Would generate: .agents/skills/ Codex skills")
706
+ if codex_skills:
707
+ print(" Would refresh: .agents/skills/ via --codex-skills")
605
708
 
606
709
  if not eds:
607
710
  print(" No editors selected (use --editors <list> or --editors all)")
@@ -881,10 +984,10 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
881
984
  from generate_codex_hooks import generate as gen_codex_hooks
882
985
  gen_codex_hooks(cwd)
883
986
  print(" Created: .codex/hooks.json")
884
- # .agents/skills/ — filtered symlinks (Codex-compatible skills only)
987
+ # .agents/skills/ — Codex discovery path for repo-local skills
885
988
  _install_codex_skills(cwd)
886
- # .codex/skills/ full mirror, gated behind explicit --codex-skills flag
887
- # (does NOT activate automatically on --profile full — must opt in).
989
+ # --codex-skills explicitly re-runs the same Codex skill sync path.
990
+ # Codex upstream discovers skills from .agents/skills/, not .codex/skills/.
888
991
  if codex_skills:
889
992
  _try_generator("generate_codex_skills", cwd,
890
993
  enable_codex_skills=True)
@@ -88,8 +88,20 @@ def remove_mcp_template(name: str) -> None:
88
88
  # Default global install: Claude only — no other editors unless --editors is used
89
89
  DEFAULT_GLOBAL_EDITORS: list[str] = []
90
90
 
91
- # All editors that support global install (opt-in via --editors)
92
- GLOBAL_CAPABLE_EDITORS = ["augment", "codex", "cursor", "gemini", "opencode", "windsurf"]
91
+ # All editors that support global install (opt-in via --editors).
92
+ #
93
+ # Cursor intentionally stays out of this list: its documented global rules
94
+ # surface is the Settings UI, not a stable file path we can merge safely.
95
+ GLOBAL_CAPABLE_EDITORS = [
96
+ "aider",
97
+ "augment",
98
+ "cline",
99
+ "codex",
100
+ "gemini",
101
+ "opencode",
102
+ "roo",
103
+ "windsurf",
104
+ ]
93
105
 
94
106
 
95
107
  def get_global_editors() -> list[str]:
@@ -41,6 +41,13 @@ EDITOR_SPECS: dict[str, dict[str, str | None]] = {
41
41
  "format": "json",
42
42
  "doc_scope": "project + global",
43
43
  },
44
+ "roo": {
45
+ "label": "Roo Code",
46
+ "project_path": ".roo/mcp.json",
47
+ "global_path": None,
48
+ "format": "json",
49
+ "doc_scope": "project",
50
+ },
44
51
  "windsurf": {
45
52
  "label": "Windsurf",
46
53
  "project_path": None,