@softspark/ai-toolkit 4.4.1 → 4.5.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.
package/scripts/doctor.py CHANGED
@@ -60,6 +60,7 @@ EXPECTED_HOOKS = [
60
60
  "config-desync-guard.sh",
61
61
  "instructions-audit.sh",
62
62
  "post-tool-use.sh",
63
+ "loop-guard.sh",
63
64
  "quality-check.sh",
64
65
  "quality-gate.sh",
65
66
  "revert-guard.sh",
@@ -549,6 +550,55 @@ def check_url_hooks(dr: DiagResult, fix_mode: bool) -> None:
549
550
  print(f" Could not re-fetch: {exc}")
550
551
 
551
552
 
553
+ # ---------------------------------------------------------------------------
554
+ # Check 10: Language Rules Drift (project-local)
555
+ # ---------------------------------------------------------------------------
556
+
557
+ def check_language_drift(dr: DiagResult) -> None:
558
+ """Warn when the current project gained a language but its rules were not injected.
559
+
560
+ Runs only inside a local-install project (cwd has .claude/CLAUDE.md with the
561
+ language-rules block). A new Cargo.toml / go.mod that appears after install
562
+ otherwise leaves the matching <lang>-rules unlinked with zero signal.
563
+ """
564
+ print()
565
+ print("## 10. Language Rules Drift")
566
+
567
+ claude_md = Path.cwd() / ".claude" / "CLAUDE.md"
568
+ if not claude_md.is_file():
569
+ dr.skip("not a local-install project (no .claude/CLAUDE.md)")
570
+ return
571
+ try:
572
+ content = claude_md.read_text(encoding="utf-8")
573
+ except OSError:
574
+ dr.skip("could not read .claude/CLAUDE.md")
575
+ return
576
+ if "TOOLKIT:language-rules" not in content:
577
+ dr.skip("no language-rules block in .claude/CLAUDE.md")
578
+ return
579
+
580
+ try:
581
+ from install_steps.detect_language import detect_languages
582
+ except Exception:
583
+ dr.skip("language detection unavailable")
584
+ return
585
+
586
+ missing = []
587
+ for module in detect_languages(Path.cwd(), toolkit_dir):
588
+ if not module.startswith("rules-"):
589
+ continue
590
+ skill = f"{module[len('rules-'):]}-rules"
591
+ if skill not in content:
592
+ missing.append(skill)
593
+
594
+ if not missing:
595
+ dr.ok("project language rules in sync")
596
+ return
597
+ for skill in missing:
598
+ lang = skill[: -len("-rules")]
599
+ dr.warn(f"{lang} detected but {skill} not injected — run: ai-toolkit install --local --lang {lang}")
600
+
601
+
552
602
  # ---------------------------------------------------------------------------
553
603
  # Main
554
604
  # ---------------------------------------------------------------------------
@@ -571,6 +621,7 @@ def main() -> None:
571
621
  check_benchmark_freshness(dr)
572
622
  check_stale_rules(dr, fix_mode)
573
623
  check_url_hooks(dr, fix_mode)
624
+ check_language_drift(dr)
574
625
 
575
626
  # Summary
576
627
  print("========================")
@@ -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-05-25",
4
+ "last_updated": "2026-05-30",
5
5
  "tools": [
6
6
  {
7
7
  "id": "claude-code",
@@ -24,12 +24,16 @@
24
24
  "capability_markers": [
25
25
  "PreToolUse",
26
26
  "PostToolUse",
27
+ "PostToolUseFailure",
28
+ "PostToolBatch",
27
29
  "SessionStart",
28
30
  "SessionEnd",
29
31
  "SubagentStart",
30
32
  "SubagentStop",
31
33
  "UserPromptSubmit",
34
+ "UserPromptExpansion",
32
35
  "Notification",
36
+ "MessageDisplay",
33
37
  "Stop",
34
38
  "StopFailure",
35
39
  "PreCompact",
@@ -452,6 +456,10 @@
452
456
  "hook event: UserPromptSubmit",
453
457
  "hook event: Stop",
454
458
  "hook event: PermissionRequest",
459
+ "hook event: PreCompact",
460
+ "hook event: PostCompact",
461
+ "hook event: SubagentStart",
462
+ "hook event: SubagentStop",
455
463
  "hook handler: command",
456
464
  "hook handler: prompt",
457
465
  "hook handler: agent",
@@ -4,10 +4,11 @@
4
4
  Maps compatible ai-toolkit hooks to Codex lifecycle events.
5
5
  Hook scripts are shared with Claude Code (stored in ~/.softspark/ai-toolkit/hooks/).
6
6
 
7
- Codex supports 6 events (PascalCase in config.toml / hooks.json):
8
- ``PreToolUse``, ``PostToolUse``, ``PermissionRequest``, ``SessionStart``,
9
- ``UserPromptSubmit``, ``Stop``. PreToolUse/PostToolUse only support
10
- the ``Bash`` matcher.
7
+ Codex exposes 10 lifecycle events (PascalCase in config.toml / hooks.json):
8
+ ``PreToolUse``, ``PostToolUse``, ``PermissionRequest``, ``PreCompact``,
9
+ ``PostCompact``, ``SessionStart``, ``UserPromptSubmit``, ``SubagentStart``,
10
+ ``SubagentStop``, ``Stop``. PreToolUse/PostToolUse only support the ``Bash``
11
+ matcher. We currently wire the subset defined in ``CODEX_HOOKS`` below.
11
12
 
12
13
  Handler types in Codex: ``command`` (what we emit), ``prompt``, and ``agent``.
13
14
  Reference: codex-rs/config/src/hook_config.rs.
@@ -785,6 +785,9 @@ def validate_metadata_contracts(
785
785
  # Cross-validate versions: package.json vs manifest.json vs plugin.json
786
786
  _validate_version_sync(tk_dir, vr)
787
787
 
788
+ # Cross-validate the README platform matrix's Hooks column against reality
789
+ _validate_editor_hooks_honesty(tk_dir, vr)
790
+
788
791
  print()
789
792
  return actual_tests
790
793
 
@@ -836,6 +839,87 @@ def _validate_version_sync(tk_dir: Path, vr: ValidationResult) -> None:
836
839
  )
837
840
 
838
841
 
842
+ # Editors that receive lifecycle hook enforcement without a generate_*_hooks.py
843
+ # generator: Claude is native (app/hooks), opencode bridges via its plugin
844
+ # (generate_opencode_plugin.py). Every other hook-enabled editor is derived
845
+ # from the generate_<editor>_hooks.py set so this check stays honest as
846
+ # generators come and go.
847
+ _NATIVE_HOOK_EDITORS = {"claude", "opencode"}
848
+
849
+ # README platform label (lowercased) -> canonical editor key.
850
+ _README_PLATFORM_KEY = {
851
+ "claude code": "claude",
852
+ "cursor": "cursor",
853
+ "windsurf": "windsurf",
854
+ "gemini cli": "gemini",
855
+ "github copilot": "copilot",
856
+ "cline": "cline",
857
+ "roo code": "roo",
858
+ "aider": "aider",
859
+ "augment": "augment",
860
+ "google antigravity": "antigravity",
861
+ "codex cli": "codex",
862
+ "opencode": "opencode",
863
+ }
864
+
865
+
866
+ def _validate_editor_hooks_honesty(tk_dir: Path, vr: ValidationResult) -> None:
867
+ """Ensure the README platform matrix's Hooks column matches reality.
868
+
869
+ Guards against the overclaim that every editor gets hook enforcement when
870
+ only a subset does — the machine-enforced constitution needs generated
871
+ hooks, and rules-only editors receive guidance text without blocking hooks.
872
+ """
873
+ scripts_dir = tk_dir / "scripts"
874
+ readme = tk_dir / "README.md"
875
+ if not scripts_dir.is_dir() or not readme.is_file():
876
+ return # installed copy without source — nothing to cross-check
877
+
878
+ # Actual hook-enabled editors: native set + generate_<editor>_hooks.py stems.
879
+ actual = set(_NATIVE_HOOK_EDITORS)
880
+ for gen in scripts_dir.glob("generate_*_hooks.py"):
881
+ stem = gen.name[len("generate_"):-len("_hooks.py")]
882
+ actual.add(stem)
883
+
884
+ content = readme.read_text(encoding="utf-8")
885
+ if "| Hooks |" not in content:
886
+ return # matrix has no Hooks column to validate
887
+
888
+ claimed: set[str] = set()
889
+ parsed_any = False
890
+ for line in content.splitlines():
891
+ if not line.startswith("|"):
892
+ continue
893
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
894
+ if len(cells) < 4:
895
+ continue
896
+ key = _README_PLATFORM_KEY.get(cells[0].lower())
897
+ if key is None:
898
+ continue # header, separator, or unknown row
899
+ parsed_any = True
900
+ if "✅" in cells[2]: # ✅
901
+ claimed.add(key)
902
+
903
+ if not parsed_any:
904
+ return
905
+
906
+ missing = actual - claimed # has hooks but README omits the ✅
907
+ overclaimed = claimed - actual # README claims ✅ but no generator exists
908
+ if missing:
909
+ vr.error(
910
+ "README Hooks column understates enforcement for: "
911
+ + ", ".join(sorted(missing))
912
+ )
913
+ if overclaimed:
914
+ vr.error(
915
+ "README Hooks column overclaims enforcement for: "
916
+ + ", ".join(sorted(overclaimed))
917
+ + " (no hook generator or native bridge found)"
918
+ )
919
+ if not missing and not overclaimed:
920
+ print(f" OK: editor hooks honesty ({len(claimed)} hook-enabled editors)")
921
+
922
+
839
923
  ROMAN_NUMERALS = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"]
840
924
 
841
925