@softspark/ai-toolkit 4.16.0 → 4.17.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 (50) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +11 -16
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/hooks/session-end.sh +1 -13
  5. package/app/hooks.json +0 -10
  6. package/benchmarks/ecosystem-doctor-snapshot.json +14 -15
  7. package/bin/ai-toolkit.js +0 -2
  8. package/kb/history/completed/output-filter-retirement-20260726.md +128 -0
  9. package/kb/reference/architecture-overview.md +2 -3
  10. package/kb/reference/cli-reference.md +3 -13
  11. package/kb/reference/enterprise-config-guide.md +1 -21
  12. package/kb/reference/hooks-catalog.md +3 -60
  13. package/kb/reference/supported-tools-registry.md +0 -4
  14. package/llms-full.txt +143 -395
  15. package/llms.txt +1 -1
  16. package/manifest.json +147 -36
  17. package/package.json +1 -2
  18. package/scripts/claude_app.py +2 -21
  19. package/scripts/config_cli.py +4 -0
  20. package/scripts/config_merger.py +0 -17
  21. package/scripts/config_validator.py +11 -138
  22. package/scripts/doctor.py +3 -20
  23. package/scripts/generate_copilot.py +35 -4
  24. package/scripts/install.py +7 -2
  25. package/scripts/install_steps/ai_tools.py +28 -99
  26. package/scripts/install_steps/hooks.py +26 -24
  27. package/scripts/merge-hooks.py +33 -2
  28. package/scripts/output_filter_retirement.py +395 -0
  29. package/scripts/schemas/ai-toolkit-config.schema.json +0 -60
  30. package/scripts/uninstall.py +13 -27
  31. package/app/hooks/filter-tool-output.sh +0 -76
  32. package/app/output-filter-policy.json +0 -15
  33. package/benchmarks/output-filter/README.md +0 -11
  34. package/benchmarks/output-filter/scenarios.json +0 -25
  35. package/kb/reference/tool-output-filter.md +0 -288
  36. package/scripts/benchmark_output_filter.py +0 -343
  37. package/scripts/output_filter_cli.py +0 -347
  38. package/scripts/output_filter_hook.py +0 -23
  39. package/scripts/tool_output_filter/__init__.py +0 -33
  40. package/scripts/tool_output_filter/contracts.py +0 -173
  41. package/scripts/tool_output_filter/engine.py +0 -260
  42. package/scripts/tool_output_filter/hook_runtime.py +0 -369
  43. package/scripts/tool_output_filter/input.py +0 -56
  44. package/scripts/tool_output_filter/invariants.py +0 -40
  45. package/scripts/tool_output_filter/policy.py +0 -153
  46. package/scripts/tool_output_filter/profiles/__init__.py +0 -68
  47. package/scripts/tool_output_filter/profiles/repeat_lines.py +0 -71
  48. package/scripts/tool_output_filter/profiles/tap_success.py +0 -154
  49. package/scripts/tool_output_filter/recovery.py +0 -846
  50. package/scripts/tool_output_filter/telemetry.py +0 -13
@@ -1,40 +0,0 @@
1
- """Stateful safety invariants shared by runtime adapters."""
2
-
3
- from __future__ import annotations
4
-
5
- DEFAULT_FAILURE_LIMIT = 3
6
-
7
- # "never-worse" (savings margin missed after the recovery marker) is a benign
8
- # outcome, not a safety failure: nothing was emitted. Only genuine anomalies
9
- # may open the breaker.
10
- _CIRCUIT_FAILURES = frozenset(
11
- {
12
- "profile-failed",
13
- "recovery-failed",
14
- "recovery-verification-failed",
15
- }
16
- )
17
-
18
-
19
- class SessionCircuitBreaker:
20
- """Open a session-local bypass after consecutive safety failures."""
21
-
22
- __slots__ = ("failure_limit", "consecutive_failures")
23
-
24
- def __init__(
25
- self,
26
- failure_limit: int = DEFAULT_FAILURE_LIMIT,
27
- consecutive_failures: int = 0,
28
- ) -> None:
29
- self.failure_limit = failure_limit
30
- self.consecutive_failures = consecutive_failures
31
-
32
- @property
33
- def is_open(self) -> bool:
34
- return self.consecutive_failures >= self.failure_limit
35
-
36
- def record(self, fallback_reason: str | None) -> None:
37
- if fallback_reason in _CIRCUIT_FAILURES:
38
- self.consecutive_failures += 1
39
- else:
40
- self.consecutive_failures = 0
@@ -1,153 +0,0 @@
1
- """Strict materialized policy contract."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
-
8
- from .contracts import (
9
- DEFAULT_MAX_INPUT_BYTES,
10
- DEFAULT_MIN_SAVINGS_BYTES,
11
- DEFAULT_MIN_SAVINGS_RATIO,
12
- FilterMode,
13
- )
14
- from .recovery import DEFAULT_MAX_SESSION_BYTES, DEFAULT_TTL_MINUTES
15
-
16
- _KNOWN_PROFILES = frozenset({"repeat-lines", "tap-success"})
17
- _TOP_LEVEL_KEYS = frozenset(
18
- {
19
- "mode",
20
- "profiles",
21
- "maxInputBytes",
22
- "minSavingsBytes",
23
- "minSavingsRatio",
24
- "recovery",
25
- }
26
- )
27
- _RECOVERY_KEYS = frozenset({"mode", "ttlMinutes", "maxSessionBytes"})
28
-
29
-
30
- class OutputFilterPolicy:
31
- __slots__ = (
32
- "max_input_bytes",
33
- "max_session_bytes",
34
- "min_savings_bytes",
35
- "min_savings_ratio",
36
- "mode",
37
- "profiles",
38
- "ttl_minutes",
39
- )
40
-
41
- def __init__(
42
- self,
43
- mode: FilterMode,
44
- profiles: tuple[str, ...],
45
- max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
46
- min_savings_bytes: int = DEFAULT_MIN_SAVINGS_BYTES,
47
- min_savings_ratio: float = DEFAULT_MIN_SAVINGS_RATIO,
48
- ttl_minutes: int = DEFAULT_TTL_MINUTES,
49
- max_session_bytes: int = DEFAULT_MAX_SESSION_BYTES,
50
- ) -> None:
51
- self.mode = mode
52
- self.profiles = profiles
53
- self.max_input_bytes = max_input_bytes
54
- self.min_savings_bytes = min_savings_bytes
55
- self.min_savings_ratio = min_savings_ratio
56
- self.ttl_minutes = ttl_minutes
57
- self.max_session_bytes = max_session_bytes
58
-
59
-
60
- def _integer(data: dict[str, object], key: str, default: int) -> int:
61
- value = data.get(key, default)
62
- if isinstance(value, bool) or not isinstance(value, int):
63
- raise ValueError(f"{key} must be an integer")
64
- return value
65
-
66
-
67
- def _number(data: dict[str, object], key: str, default: float) -> float:
68
- value = data.get(key, default)
69
- if isinstance(value, bool) or not isinstance(value, (int, float)):
70
- raise ValueError(f"{key} must be a number")
71
- return float(value)
72
-
73
-
74
- def _validate_ranges(
75
- max_input_bytes: int,
76
- min_savings_bytes: int,
77
- min_savings_ratio: float,
78
- ttl_minutes: int,
79
- max_session_bytes: int,
80
- ) -> None:
81
- if not 1 <= max_input_bytes <= DEFAULT_MAX_INPUT_BYTES:
82
- raise ValueError("maxInputBytes is outside the safe range")
83
- if not 0 <= min_savings_bytes <= max_input_bytes:
84
- raise ValueError("minSavingsBytes is outside the safe range")
85
- if not 0.0 <= min_savings_ratio <= 1.0:
86
- raise ValueError("minSavingsRatio is outside the safe range")
87
- if ttl_minutes <= 0 or max_session_bytes <= 0:
88
- raise ValueError("recovery limits must be positive")
89
-
90
-
91
- def load_policy(path: str | os.PathLike[str]) -> OutputFilterPolicy:
92
- """Load an unwrapped, materialized policy object."""
93
-
94
- with open(path, encoding="utf-8") as policy_file:
95
- data = json.load(policy_file)
96
- if not isinstance(data, dict):
97
- raise ValueError("output-filter policy must be an object")
98
- if set(data) - _TOP_LEVEL_KEYS:
99
- raise ValueError("output-filter policy contains unknown keys")
100
- mode = FilterMode(data["mode"])
101
- raw_profiles = data.get("profiles", [])
102
- if not isinstance(raw_profiles, list) or not all(
103
- isinstance(profile, str) for profile in raw_profiles
104
- ):
105
- raise ValueError("profiles must be a string array")
106
- if len(set(raw_profiles)) != len(raw_profiles):
107
- raise ValueError("profiles must not contain duplicates")
108
- if any(profile not in _KNOWN_PROFILES for profile in raw_profiles):
109
- raise ValueError("profiles contains an unknown profile")
110
- recovery = data.get("recovery", {})
111
- if not isinstance(recovery, dict):
112
- raise ValueError("recovery must be an object")
113
- if set(recovery) - _RECOVERY_KEYS:
114
- raise ValueError("recovery contains unknown keys")
115
- if recovery.get("mode", "ephemeral") != "ephemeral":
116
- raise ValueError("only ephemeral recovery is supported")
117
- max_input_bytes = _integer(
118
- data,
119
- "maxInputBytes",
120
- DEFAULT_MAX_INPUT_BYTES,
121
- )
122
- min_savings_bytes = _integer(
123
- data,
124
- "minSavingsBytes",
125
- DEFAULT_MIN_SAVINGS_BYTES,
126
- )
127
- min_savings_ratio = _number(
128
- data,
129
- "minSavingsRatio",
130
- DEFAULT_MIN_SAVINGS_RATIO,
131
- )
132
- ttl_minutes = _integer(recovery, "ttlMinutes", DEFAULT_TTL_MINUTES)
133
- max_session_bytes = _integer(
134
- recovery,
135
- "maxSessionBytes",
136
- DEFAULT_MAX_SESSION_BYTES,
137
- )
138
- _validate_ranges(
139
- max_input_bytes,
140
- min_savings_bytes,
141
- min_savings_ratio,
142
- ttl_minutes,
143
- max_session_bytes,
144
- )
145
- return OutputFilterPolicy(
146
- mode=mode,
147
- profiles=tuple(raw_profiles),
148
- max_input_bytes=max_input_bytes,
149
- min_savings_bytes=min_savings_bytes,
150
- min_savings_ratio=min_savings_ratio,
151
- ttl_minutes=ttl_minutes,
152
- max_session_bytes=max_session_bytes,
153
- )
@@ -1,68 +0,0 @@
1
- """Built-in deterministic output profiles."""
2
-
3
- from .repeat_lines import PROFILE_ID as REPEAT_LINES_ID
4
- from .repeat_lines import PROFILE_VERSION as REPEAT_LINES_VERSION
5
- from .repeat_lines import transform as transform_repeat_lines
6
- from .tap_success import PROFILE_ID as TAP_SUCCESS_ID
7
- from .tap_success import PROFILE_VERSION as TAP_SUCCESS_VERSION
8
- from .tap_success import transform as transform_tap_success
9
-
10
-
11
- class ProfileTransform:
12
- __slots__ = ("accepted", "output", "profile_id", "profile_version")
13
-
14
- def __init__(
15
- self,
16
- profile_id: str,
17
- profile_version: int,
18
- output: str,
19
- accepted: bool,
20
- ) -> None:
21
- self.profile_id = profile_id
22
- self.profile_version = profile_version
23
- self.output = output
24
- self.accepted = accepted
25
-
26
- def __eq__(self, other: object) -> bool:
27
- if not isinstance(other, ProfileTransform):
28
- return NotImplemented
29
- return (
30
- self.profile_id == other.profile_id
31
- and self.profile_version == other.profile_version
32
- and self.output == other.output
33
- and self.accepted == other.accepted
34
- )
35
-
36
-
37
- def apply_profile(
38
- profile_id: str,
39
- output: str,
40
- ) -> ProfileTransform | None:
41
- if profile_id == REPEAT_LINES_ID:
42
- return ProfileTransform(
43
- profile_id,
44
- REPEAT_LINES_VERSION,
45
- transform_repeat_lines(output),
46
- True,
47
- )
48
- if profile_id == TAP_SUCCESS_ID:
49
- transformed = transform_tap_success(output)
50
- return ProfileTransform(
51
- profile_id,
52
- TAP_SUCCESS_VERSION,
53
- transformed if transformed is not None else output,
54
- transformed is not None,
55
- )
56
- return None
57
-
58
-
59
- __all__ = [
60
- "ProfileTransform",
61
- "REPEAT_LINES_ID",
62
- "REPEAT_LINES_VERSION",
63
- "TAP_SUCCESS_ID",
64
- "TAP_SUCCESS_VERSION",
65
- "apply_profile",
66
- "transform_repeat_lines",
67
- "transform_tap_success",
68
- ]
@@ -1,71 +0,0 @@
1
- """Compact adjacent identical output lines."""
2
-
3
- from __future__ import annotations
4
-
5
- PROFILE_ID = "repeat-lines"
6
- PROFILE_VERSION = 1
7
-
8
- _DIAGNOSTIC_PREFIXES = (
9
- "error",
10
- "warning",
11
- "warn:",
12
- "fatal",
13
- "fail",
14
- "not ok",
15
- "npm err!",
16
- "critical",
17
- "segmentation fault",
18
- "traceback",
19
- "exception",
20
- "panic",
21
- "assert",
22
- "e ",
23
- "✕",
24
- "✗",
25
- "security",
26
- "vulnerab",
27
- "cve-",
28
- "denied",
29
- "unauthor",
30
- "permission",
31
- )
32
-
33
-
34
- def _is_diagnostic(line: str) -> bool:
35
- if not line.strip():
36
- return True
37
- stripped = line.lstrip().lower()
38
- return (
39
- stripped.startswith(_DIAGNOSTIC_PREFIXES)
40
- or stripped.startswith("#")
41
- or stripped.startswith("[ai-toolkit-output-filter ")
42
- or "\x1b" in line
43
- or "\x00" in line
44
- )
45
-
46
-
47
- def transform(output: str) -> str:
48
- """Collapse adjacent identical lines while preserving their first copy."""
49
-
50
- lines = output.splitlines(keepends=True)
51
- if not lines:
52
- return output
53
-
54
- transformed: list[str] = []
55
- index = 0
56
- while index < len(lines):
57
- line = lines[index]
58
- run_end = index + 1
59
- while run_end < len(lines) and lines[run_end] == line:
60
- run_end += 1
61
- run_length = run_end - index
62
- transformed.append(line)
63
- if run_length > 1 and not _is_diagnostic(line):
64
- transformed.append(
65
- "[ai-toolkit-output-filter repeat-lines/v1: "
66
- f"{run_length - 1} adjacent copies omitted]\n"
67
- )
68
- elif run_length > 1:
69
- transformed.extend(lines[index + 1 : run_end])
70
- index = run_end
71
- return "".join(transformed)
@@ -1,154 +0,0 @@
1
- """Conservative compaction for valid, fully successful TAP output."""
2
-
3
- from __future__ import annotations
4
-
5
- PROFILE_ID = "tap-success"
6
- PROFILE_VERSION = 1
7
-
8
- _DIAGNOSTIC_COMMENT_PREFIXES = (
9
- "# error",
10
- "# stack",
11
- "# message",
12
- "# operator",
13
- "# expected",
14
- "# actual",
15
- "# severity",
16
- "# at ",
17
- )
18
-
19
-
20
- def _line_ending(lines: list[str]) -> str:
21
- for line in lines:
22
- if line.endswith("\r\n"):
23
- return "\r\n"
24
- if line.endswith("\n"):
25
- return "\n"
26
- return "\n"
27
-
28
-
29
- def _parse_plan(line: str) -> int | None:
30
- content = line.strip()
31
- if not content.startswith("1.."):
32
- return None
33
- count_text = content[3:].split("#", 1)[0].strip()
34
- if not count_text.isdecimal():
35
- return None
36
- return int(count_text)
37
-
38
-
39
- def _parse_ok_number(line: str) -> tuple[int, bool] | None:
40
- content = line.strip()
41
- if not content.startswith("ok "):
42
- return None
43
- remainder = content[3:]
44
- number_text = remainder.split(maxsplit=1)[0]
45
- if not number_text.isdecimal():
46
- return None
47
- has_directive = False
48
- if "#" in content:
49
- directive = content.rsplit("#", 1)[1].strip().upper()
50
- if not directive.startswith(("SKIP", "TODO")):
51
- return None
52
- has_directive = True
53
- return int(number_text), has_directive
54
-
55
-
56
- def _has_nonzero_failure_summary(content: str) -> bool:
57
- lowered = content.lower()
58
- for prefix in ("# fail", "# cancel"):
59
- if not lowered.startswith(prefix):
60
- continue
61
- remainder = lowered[len(prefix):]
62
- # Accept spelling variants ("# failed", "# failures:") but treat any
63
- # non-zero or unparseable count as a failure signal.
64
- value = remainder.lstrip("abcdefghijklmnopqrstuvwxyz").strip(" :\t")
65
- return not value.isdecimal() or int(value) != 0
66
- return False
67
-
68
-
69
- def _parse_success_summary(content: str) -> tuple[str, int] | None:
70
- lowered = content.lower()
71
- for name in ("tests", "pass"):
72
- prefix = f"# {name} "
73
- if not lowered.startswith(prefix):
74
- continue
75
- value = lowered[len(prefix):].strip()
76
- return name, int(value) if value.isdecimal() else -1
77
- return None
78
-
79
-
80
- def transform(output: str) -> str | None:
81
- """Return compact TAP, or ``None`` when the stream is not strictly safe."""
82
-
83
- lines = output.splitlines(keepends=True)
84
- if not lines or any(
85
- "[ai-toolkit-output-filter " in line for line in lines
86
- ):
87
- return None
88
-
89
- plan_count: int | None = None
90
- result_numbers: list[int] = []
91
- summary_counts: dict[str, int] = {}
92
- omitted_count = 0
93
- transformed: list[str] = []
94
- newline = _line_ending(lines)
95
-
96
- def flush_omitted() -> None:
97
- nonlocal omitted_count
98
- if omitted_count:
99
- transformed.append(
100
- "[ai-toolkit-output-filter tap-success/v1: "
101
- f"{omitted_count} successful result lines omitted]"
102
- f"{newline}"
103
- )
104
- omitted_count = 0
105
-
106
- for line in lines:
107
- content = line.strip()
108
- if content == "TAP version 13":
109
- flush_omitted()
110
- transformed.append(line)
111
- continue
112
- parsed_plan = _parse_plan(line)
113
- if parsed_plan is not None:
114
- if plan_count is not None:
115
- return None
116
- plan_count = parsed_plan
117
- flush_omitted()
118
- transformed.append(line)
119
- continue
120
- parsed_ok = _parse_ok_number(line)
121
- if parsed_ok is not None:
122
- number, has_directive = parsed_ok
123
- result_numbers.append(number)
124
- if has_directive:
125
- flush_omitted()
126
- transformed.append(line)
127
- else:
128
- omitted_count += 1
129
- continue
130
- if content.lower().startswith(_DIAGNOSTIC_COMMENT_PREFIXES):
131
- return None
132
- if _has_nonzero_failure_summary(content):
133
- return None
134
- parsed_summary = _parse_success_summary(content)
135
- if parsed_summary is not None:
136
- name, value = parsed_summary
137
- if value < 0 or name in summary_counts:
138
- return None
139
- summary_counts[name] = value
140
- if not content or content.startswith("#"):
141
- flush_omitted()
142
- transformed.append(line)
143
- continue
144
- return None
145
-
146
- flush_omitted()
147
- if (
148
- plan_count is None
149
- or plan_count == 0
150
- or result_numbers != list(range(1, plan_count + 1))
151
- or any(count != plan_count for count in summary_counts.values())
152
- ):
153
- return None
154
- return "".join(transformed)