@softspark/ai-toolkit 4.12.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 (40) hide show
  1. package/CHANGELOG.md +30 -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/global-install-model.md +24 -3
  21. package/kb/reference/hooks-catalog.md +2 -3
  22. package/kb/reference/plugin-pack-conventions.md +5 -5
  23. package/kb/reference/skills-catalog.md +2 -0
  24. package/kb/reference/supported-tools-registry.md +30 -13
  25. package/kb/reference/unique-features.md +3 -2
  26. package/llms-full.txt +179 -60
  27. package/manifest.json +8 -8
  28. package/package.json +4 -3
  29. package/scripts/claude_app.py +347 -0
  30. package/scripts/doctor.py +85 -4
  31. package/scripts/ecosystem_tools.json +33 -5
  32. package/scripts/generate_devin_hooks.py +3 -4
  33. package/scripts/generate_windsurf_skills.py +5 -6
  34. package/scripts/install.py +23 -21
  35. package/scripts/install_steps/ai_tools.py +96 -6
  36. package/scripts/install_steps/install_state.py +2 -0
  37. package/scripts/update_projects.py +12 -1
  38. package/scripts/validate.py +34 -2
  39. package/AGENTS.md +0 -655
  40. package/scripts/generate_windsurf_hooks.py +0 -152
@@ -756,34 +756,36 @@ def main() -> None:
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
@@ -110,7 +110,10 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
110
110
  if "cline" in eds:
111
111
  if dry_run:
112
112
  print(" Would generate: ~/Documents/Cline/Rules/ai-toolkit-*.md")
113
- 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")
114
117
  else:
115
118
  _install_cline_global(target_dir, rules_dir)
116
119
  installed.append("cline")
@@ -277,6 +280,80 @@ def _install_windsurf_global(target_dir: Path, rules_dir: Path) -> None:
277
280
  )
278
281
 
279
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
+
280
357
  def _install_cline_global(target_dir: Path, rules_dir: Path) -> None:
281
358
  """Install Cline global rules in the documented ~/.cline directory."""
282
359
  from generate_cline_rules import generate as gen_cline_rules
@@ -941,6 +1018,18 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
941
1018
  if ed in eds:
942
1019
  print(msg)
943
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
+
944
1033
  # Profile-driven extras (matrix in kb/reference/global-install-model.md)
945
1034
  if "copilot" in eds and add_copilot_dir:
946
1035
  print(" Would generate: .github/instructions/ + .github/prompts/ (profile >= standard)")
@@ -950,7 +1039,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
950
1039
  if "cursor" in eds:
951
1040
  print(" Would generate: .cursor/hooks.json + .cursor/agents/ + .cursor/skills/ (profile=full)")
952
1041
  if "windsurf" in eds:
953
- 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)")
954
1043
  if "cline" in eds:
955
1044
  print(" Would generate: .cline/skills/ (profile=full)")
956
1045
  if "augment" in eds:
@@ -1174,6 +1263,7 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
1174
1263
  _try_generator("generate_cursor_skills", cwd, emit_skill_pointer=emit_pointer)
1175
1264
 
1176
1265
  if "windsurf" in eds:
1266
+ _cleanup_retired_windsurf_surfaces(cwd)
1177
1267
  inject_with_rules(
1178
1268
  "generate-windsurf.sh",
1179
1269
  cwd / ".windsurfrules",
@@ -1183,11 +1273,11 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
1183
1273
  gen_windsurf_rules(cwd, language_modules=language_modules,
1184
1274
  rules_dir=rules_dir)
1185
1275
  if add_native_surfaces:
1186
- # .windsurf/hooks.json is Cascade-scoped and dies 2026-07-01;
1187
- # .devin/hooks.v1.json is the Devin CLI replacement (Claude format).
1188
- _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.
1189
1278
  _try_generator("generate_devin_hooks", cwd)
1190
- # 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.
1191
1281
  _try_generator("generate_windsurf_skills", cwd)
1192
1282
 
1193
1283
  if "cline" in eds:
@@ -197,6 +197,8 @@ def record_install(
197
197
  state["profile"] = profile
198
198
  if auto_detected is not None:
199
199
  state["auto_detected_languages"] = sorted(auto_detected)
200
+ else:
201
+ state.pop("auto_detected_languages", None)
200
202
 
201
203
  if extends_info is not None:
202
204
  state["extends"] = {
@@ -28,8 +28,19 @@ def _update_project(project: dict[str, Any], install_script: str, extra_args: li
28
28
  # Pass saved editors from registry so update re-installs the same editors
29
29
  cmd_args = ["python3", install_script, "--local"] + extra_args
30
30
  project_editors = project.get("editors", [])
31
- if project_editors:
31
+ has_editor_override = any(
32
+ arg == "--editors" or arg.startswith("--editors=")
33
+ for arg in extra_args
34
+ )
35
+ if project_editors and not has_editor_override:
32
36
  cmd_args.extend(["--editors", ",".join(project_editors)])
37
+ has_profile_override = any(
38
+ arg == "--profile" or arg.startswith("--profile=")
39
+ for arg in extra_args
40
+ )
41
+ project_profile = project.get("profile", "")
42
+ if project_profile and not has_profile_override:
43
+ cmd_args.extend(["--profile", project_profile])
33
44
 
34
45
  try:
35
46
  proc = subprocess.run(
@@ -124,6 +124,10 @@ LANGUAGE_RULE_CATEGORIES = frozenset({
124
124
 
125
125
  PLANNED_ASSETS = [
126
126
  "app/.claude-plugin/plugin.json",
127
+ "app/claude-app/hooks/hooks.json",
128
+ "app/claude-app/skills/ai-toolkit-rules/SKILL.md",
129
+ "app/claude-app/global-instructions.md",
130
+ "scripts/claude_app.py",
127
131
  "scripts/doctor.py",
128
132
  "scripts/benchmark_ecosystem.py",
129
133
  "scripts/harvest_ecosystem.py",
@@ -561,9 +565,37 @@ def validate_planned_assets(tk_dir: Path, vr: ValidationResult) -> None:
561
565
  assert d["name"]
562
566
  assert d["version"]
563
567
  assert d["description"]
564
- print(" OK: plugin manifest JSON is valid")
568
+ assert isinstance(d.get("repository"), str)
569
+ ignored = {"npm", "install", "marketplace", "claude-code"} & set(d)
570
+ assert not ignored
571
+ for path_field in ("hooks", "skills"):
572
+ relative = str(d[path_field]).removeprefix("./")
573
+ assert (tk_dir / "app" / relative).exists()
574
+ print(" OK: plugin manifest matches the official schema surface")
565
575
  except (json.JSONDecodeError, KeyError, AssertionError):
566
- vr.error("Invalid plugin manifest JSON or missing required fields")
576
+ vr.error("Invalid plugin manifest JSON, official fields, or component paths")
577
+
578
+ try:
579
+ from claude_app import (
580
+ GLOBAL_INSTRUCTIONS_PATH,
581
+ PLUGIN_HOOKS_PATH,
582
+ PLUGIN_RULES_SKILL_PATH,
583
+ render_global_instructions,
584
+ render_plugin_hooks,
585
+ render_rules_skill,
586
+ )
587
+ generated = (
588
+ (PLUGIN_HOOKS_PATH, render_plugin_hooks()),
589
+ (PLUGIN_RULES_SKILL_PATH, render_rules_skill()),
590
+ (GLOBAL_INSTRUCTIONS_PATH, render_global_instructions()),
591
+ )
592
+ stale = [path for path, expected in generated if path.read_text(encoding="utf-8") != expected]
593
+ if stale:
594
+ vr.error("Stale Claude app assets; run: python3 scripts/claude_app.py sync")
595
+ else:
596
+ print(" OK: Claude app plugin assets match canonical rules/hooks")
597
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
598
+ vr.error(f"Invalid Claude app generated assets: {exc}")
567
599
 
568
600
  # Validate benchmark dashboard JSON
569
601
  benchmark_dashboard = tk_dir / "benchmarks" / "ecosystem-dashboard.json"