@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
package/llms.txt CHANGED
@@ -19,6 +19,7 @@
19
19
  - [Spike: F2 MCP Context Trim — Hook Feasibility & Path Decision](kb/history/completed/f2-mcp-trim-spike-20260504.md)
20
20
  - [Implementation Plan: Native Tool Output Filter](kb/history/completed/native-tool-output-filter-plan.md)
21
21
  - [Plan: Offline-First SLM Profile — Lightweight Mode for Local Models](kb/history/completed/offline-slm-profile-plan-20260411.md)
22
+ - [Retirement: Native Tool-Output Filter](kb/history/completed/output-filter-retirement-20260726.md)
22
23
  - [Plan: Output & Token Discipline](kb/history/completed/output-token-discipline-plan-20260504.md)
23
24
  - [How-To Guides](kb/howto/README.md)
24
25
  - [Plan: Cloud Security Pack — Multi-Cloud Audit](kb/planning/cloud-security-pack-plan.md)
@@ -63,7 +64,6 @@
63
64
  - [Usage Statistics](kb/reference/stats.md)
64
65
  - [Supported Tools Registry](kb/reference/supported-tools-registry.md)
65
66
  - [Config Sync](kb/reference/sync.md)
66
- - [Native Tool Output Filter](kb/reference/tool-output-filter.md)
67
67
  - [Unique Features & Differentiators](kb/reference/unique-features.md)
68
68
  - [Windows Support](kb/reference/windows-support.md)
69
69
  - [Troubleshooting](kb/troubleshooting/README.md)
package/manifest.json CHANGED
@@ -1,83 +1,130 @@
1
1
  {
2
- "version": "4.16.0",
2
+ "version": "4.17.0",
3
3
  "components": {
4
4
  "agents": {
5
5
  "description": "44 specialized agents (orchestrator, backend, frontend, security, devops, etc.)",
6
6
  "path": "app/agents",
7
7
  "target": ".claude/agents",
8
8
  "type": "symlink",
9
- "tags": ["core", "agents"]
9
+ "tags": [
10
+ "core",
11
+ "agents"
12
+ ]
10
13
  },
11
14
  "skills": {
12
15
  "description": "108 skills (32 task + 30 hybrid + 46 knowledge)",
13
16
  "path": "app/skills",
14
17
  "target": ".claude/skills",
15
18
  "type": "symlink",
16
- "tags": ["core", "skills", "commands"]
19
+ "tags": [
20
+ "core",
21
+ "skills",
22
+ "commands"
23
+ ]
17
24
  },
18
25
  "hooks": {
19
- "description": "29 hook entries across 14 lifecycle events plus statusLine",
20
- "path": "app/hooks.json + app/hooks/*.sh + scripts/tool_output_filter/ + scripts/output_filter_{hook,cli}.py",
26
+ "description": "28 hook entries across 14 lifecycle events plus statusLine",
27
+ "path": "app/hooks.json + app/hooks/*.sh",
21
28
  "target": "~/.claude/settings.json (merge) + ~/.softspark/ai-toolkit/{hooks,scripts}/ (copy)",
22
29
  "type": "merge+copy",
23
- "tags": ["core", "hooks", "quality", "output-filter"]
30
+ "tags": [
31
+ "core",
32
+ "hooks",
33
+ "quality"
34
+ ]
24
35
  },
25
36
  "plugin-packs": {
26
37
  "description": "Experimental domain plugin packs and optional hook/policy modules",
27
38
  "path": "app/plugins",
28
39
  "target": "opt-in / not installed by default",
29
40
  "type": "experimental",
30
- "tags": ["plugins", "packs", "experimental"]
41
+ "tags": [
42
+ "plugins",
43
+ "packs",
44
+ "experimental"
45
+ ]
31
46
  },
32
47
  "constitution": {
33
48
  "description": "7-article machine-enforced safety constitution",
34
49
  "path": "app/constitution.md",
35
50
  "target": ".claude/constitution.md",
36
51
  "type": "symlink",
37
- "tags": ["core", "safety", "constitution"]
52
+ "tags": [
53
+ "core",
54
+ "safety",
55
+ "constitution"
56
+ ]
38
57
  },
39
58
  "architecture": {
40
59
  "description": "System architecture documentation",
41
60
  "path": "app/ARCHITECTURE.md",
42
61
  "target": ".claude/ARCHITECTURE.md",
43
62
  "type": "symlink",
44
- "tags": ["docs"]
63
+ "tags": [
64
+ "docs"
65
+ ]
45
66
  },
46
67
  "output-styles": {
47
68
  "description": "System prompt output style overrides (e.g. Golden Rules enforcement)",
48
69
  "path": "app/output-styles",
49
70
  "target": ".claude/output-styles",
50
71
  "type": "symlink",
51
- "tags": ["core", "output-styles"]
72
+ "tags": [
73
+ "core",
74
+ "output-styles"
75
+ ]
52
76
  },
53
77
  "rules": {
54
78
  "description": "Rules synced to Claude Code rule files and exported as a Claude app plugin skill",
55
79
  "path": "app/rules",
56
80
  "target": "~/.claude/rules + .claude/rules + Claude app plugin skill",
57
81
  "type": "sync+plugin",
58
- "tags": ["core", "rules"]
82
+ "tags": [
83
+ "core",
84
+ "rules"
85
+ ]
59
86
  }
60
87
  },
61
88
  "bundles": {
62
89
  "minimal": {
63
90
  "description": "Hooks and constitution only -- safety without agents",
64
- "includes": ["hooks", "constitution"]
91
+ "includes": [
92
+ "hooks",
93
+ "constitution"
94
+ ]
65
95
  },
66
96
  "standard": {
67
97
  "description": "Full toolkit (recommended)",
68
- "includes": ["agents", "skills", "hooks", "constitution", "rules"]
98
+ "includes": [
99
+ "agents",
100
+ "skills",
101
+ "hooks",
102
+ "constitution",
103
+ "rules"
104
+ ]
69
105
  },
70
106
  "agents-only": {
71
107
  "description": "Agents without skills (for custom skill setup)",
72
- "includes": ["agents", "hooks", "constitution"]
108
+ "includes": [
109
+ "agents",
110
+ "hooks",
111
+ "constitution"
112
+ ]
73
113
  },
74
114
  "skills-only": {
75
115
  "description": "Skills without agents",
76
- "includes": ["skills", "hooks", "constitution"]
116
+ "includes": [
117
+ "skills",
118
+ "hooks",
119
+ "constitution"
120
+ ]
77
121
  },
78
122
  "safety": {
79
123
  "description": "Constitution + hooks only (zero agent overhead)",
80
- "includes": ["hooks", "constitution"]
124
+ "includes": [
125
+ "hooks",
126
+ "constitution"
127
+ ]
81
128
  }
82
129
  },
83
130
  "modules": {
@@ -99,55 +146,93 @@
99
146
  },
100
147
  "rules-typescript": {
101
148
  "description": "TypeScript-specific rules",
102
- "auto_detect": ["package.json", "tsconfig.json"]
149
+ "auto_detect": [
150
+ "package.json",
151
+ "tsconfig.json"
152
+ ]
103
153
  },
104
154
  "rules-python": {
105
155
  "description": "Python-specific rules",
106
- "auto_detect": ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"]
156
+ "auto_detect": [
157
+ "requirements.txt",
158
+ "pyproject.toml",
159
+ "setup.py",
160
+ "Pipfile"
161
+ ]
107
162
  },
108
163
  "rules-golang": {
109
164
  "description": "Go-specific rules",
110
- "auto_detect": ["go.mod"]
165
+ "auto_detect": [
166
+ "go.mod"
167
+ ]
111
168
  },
112
169
  "rules-rust": {
113
170
  "description": "Rust-specific rules",
114
- "auto_detect": ["Cargo.toml"]
171
+ "auto_detect": [
172
+ "Cargo.toml"
173
+ ]
115
174
  },
116
175
  "rules-java": {
117
176
  "description": "Java-specific rules",
118
- "auto_detect": ["pom.xml", "build.gradle", "build.gradle.kts"]
177
+ "auto_detect": [
178
+ "pom.xml",
179
+ "build.gradle",
180
+ "build.gradle.kts"
181
+ ]
119
182
  },
120
183
  "rules-kotlin": {
121
184
  "description": "Kotlin-specific rules",
122
- "auto_detect": ["build.gradle.kts"]
185
+ "auto_detect": [
186
+ "build.gradle.kts"
187
+ ]
123
188
  },
124
189
  "rules-swift": {
125
190
  "description": "Swift-specific rules",
126
- "auto_detect": ["Package.swift", "*.xcodeproj"]
191
+ "auto_detect": [
192
+ "Package.swift",
193
+ "*.xcodeproj"
194
+ ]
127
195
  },
128
196
  "rules-dart": {
129
197
  "description": "Dart/Flutter-specific rules",
130
- "auto_detect": ["pubspec.yaml"]
198
+ "auto_detect": [
199
+ "pubspec.yaml"
200
+ ]
131
201
  },
132
202
  "rules-csharp": {
133
203
  "description": "C#/.NET-specific rules",
134
- "auto_detect": ["*.csproj", "*.sln"]
204
+ "auto_detect": [
205
+ "*.csproj",
206
+ "*.sln"
207
+ ]
135
208
  },
136
209
  "rules-php": {
137
210
  "description": "PHP-specific rules",
138
- "auto_detect": ["composer.json"]
211
+ "auto_detect": [
212
+ "composer.json"
213
+ ]
139
214
  },
140
215
  "rules-cpp": {
141
216
  "description": "C++-specific rules",
142
- "auto_detect": ["CMakeLists.txt", "Makefile", "*.cpp"]
217
+ "auto_detect": [
218
+ "CMakeLists.txt",
219
+ "Makefile",
220
+ "*.cpp"
221
+ ]
143
222
  },
144
223
  "rules-ruby": {
145
224
  "description": "Ruby-specific rules",
146
- "auto_detect": ["Gemfile", "*.gemspec"]
225
+ "auto_detect": [
226
+ "Gemfile",
227
+ "*.gemspec"
228
+ ]
147
229
  },
148
230
  "rules-medplum": {
149
231
  "description": "Medplum/FHIR healthcare platform rules",
150
- "auto_detect": ["medplum.config.mts", "medplum.config.ts"]
232
+ "auto_detect": [
233
+ "medplum.config.mts",
234
+ "medplum.config.ts"
235
+ ]
151
236
  },
152
237
  "mcp-templates": {
153
238
  "description": "26 MCP server config templates",
@@ -155,11 +240,32 @@
155
240
  }
156
241
  },
157
242
  "profiles": {
158
- "minimal": ["core"],
159
- "standard": ["core", "agents", "skills", "rules-common"],
160
- "strict": ["core", "agents", "skills", "rules-common", "mcp-templates"],
161
- "full": ["core", "agents", "skills", "rules-common", "mcp-templates"],
162
- "offline-slm": ["core"]
243
+ "minimal": [
244
+ "core"
245
+ ],
246
+ "standard": [
247
+ "core",
248
+ "agents",
249
+ "skills",
250
+ "rules-common"
251
+ ],
252
+ "strict": [
253
+ "core",
254
+ "agents",
255
+ "skills",
256
+ "rules-common",
257
+ "mcp-templates"
258
+ ],
259
+ "full": [
260
+ "core",
261
+ "agents",
262
+ "skills",
263
+ "rules-common",
264
+ "mcp-templates"
265
+ ],
266
+ "offline-slm": [
267
+ "core"
268
+ ]
163
269
  },
164
270
  "config_inheritance": {
165
271
  "schema": "scripts/schemas/ai-toolkit-config.schema.json",
@@ -167,8 +273,13 @@
167
273
  "base_config": "ai-toolkit.config.json",
168
274
  "lock_file": ".softspark-toolkit.lock.json",
169
275
  "v1_fields": [
170
- "extends", "profile", "agents", "plugins", "rules", "constitution",
171
- "enforce", "toolOutputFilter"
276
+ "extends",
277
+ "profile",
278
+ "agents",
279
+ "plugins",
280
+ "rules",
281
+ "constitution",
282
+ "enforce"
172
283
  ],
173
284
  "max_extends_depth": 5
174
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "4.16.0",
3
+ "version": "4.17.0",
4
4
  "description": "AI coding toolkit: 108 skills, 44 agents, 12 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
5
5
  "keywords": [
6
6
  "claude",
@@ -12,7 +12,6 @@
12
12
  "llm",
13
13
  "toolkit",
14
14
  "developer-tools",
15
- "output-filtering",
16
15
  "cursor",
17
16
  "windsurf",
18
17
  "copilot",
@@ -38,7 +38,6 @@ PLUGIN_SCRIPT_FILES = (
38
38
  "test_cohesion.py",
39
39
  "version_check.py",
40
40
  )
41
- CLAUDE_CODE_ONLY_HOOKS = frozenset({"filter-tool-output.sh"})
42
41
  RULE_FILES = tuple(sorted((APP_DIR / "rules").glob("*.md"))) + tuple(
43
42
  sorted((APP_DIR / "rules" / "common").glob("*.md"))
44
43
  )
@@ -66,25 +65,9 @@ def render_plugin_hooks() -> str:
66
65
  source = json.loads((APP_DIR / "hooks.json").read_text(encoding="utf-8"))
67
66
  hooks = source.get("hooks", {})
68
67
 
69
- def supports_claude_app(handler: object) -> bool:
70
- if not isinstance(handler, dict):
71
- return True
72
- command = handler.get("command")
73
- if not isinstance(command, str):
74
- return True
75
- return not any(name in command for name in CLAUDE_CODE_ONLY_HOOKS)
76
-
77
68
  app_hooks = {}
78
69
  for event, groups in hooks.items():
79
- app_groups = []
80
- for group in groups:
81
- handlers = [
82
- handler
83
- for handler in group.get("hooks", [])
84
- if supports_claude_app(handler)
85
- ]
86
- if handlers:
87
- app_groups.append({**group, "hooks": handlers})
70
+ app_groups = [group for group in groups if group.get("hooks")]
88
71
  if app_groups:
89
72
  app_hooks[event] = app_groups
90
73
 
@@ -161,9 +144,7 @@ def _copy_tree(source: Path, destination: Path) -> None:
161
144
  for path in sorted(source.rglob("*")):
162
145
  relative = path.relative_to(source)
163
146
  if any(
164
- part in SKIP_NAMES
165
- or part in CLAUDE_CODE_ONLY_HOOKS
166
- or part.endswith(".pyc")
147
+ part in SKIP_NAMES or part.endswith(".pyc")
167
148
  for part in relative.parts
168
149
  ):
169
150
  continue
@@ -26,6 +26,7 @@ from config_resolver import (
26
26
  )
27
27
  from config_merger import ConfigMergeError, merge_config_chain
28
28
  from config_validator import (
29
+ RETIRED_CONFIG_FIELDS,
29
30
  validate_merged_config,
30
31
  validate_project_config,
31
32
  )
@@ -54,6 +55,9 @@ def cmd_validate(args: list[str]) -> int:
54
55
  return 2
55
56
 
56
57
  print(f" Validating {project_dir / PROJECT_CONFIG_FILENAME}...")
58
+ for key, reason in sorted(RETIRED_CONFIG_FIELDS.items()):
59
+ if key in config:
60
+ print(f" ! Retired key '{key}': {reason}")
57
61
  errors = validate_project_config(config, project_dir)
58
62
 
59
63
  if errors:
@@ -77,13 +77,6 @@ def merge_config_chain(
77
77
  # Merge project over accumulated base
78
78
  result.merged = _merge_project_over_base(accumulated_base, project_config, result)
79
79
 
80
- if "toolOutputFilter" in accumulated_base or "toolOutputFilter" in project_config:
81
- configured = result.merged.get("toolOutputFilter", {})
82
- result.merged["toolOutputFilter"] = _deep_merge(
83
- _load_default_output_filter_policy(),
84
- configured,
85
- )
86
-
87
80
  # Validate enforce constraints
88
81
  _validate_enforce(accumulated_base, result.merged)
89
82
 
@@ -95,16 +88,6 @@ def merge_two(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
95
88
  return _deep_merge(base, overlay)
96
89
 
97
90
 
98
- def _load_default_output_filter_policy() -> dict[str, Any]:
99
- """Load the canonical disabled output-filter policy."""
100
- policy_path = Path(__file__).resolve().parent.parent / "app" / "output-filter-policy.json"
101
- with open(policy_path, encoding="utf-8") as handle:
102
- policy = json.load(handle)
103
- if not isinstance(policy, dict):
104
- raise ConfigMergeError(f"Invalid output-filter policy: {policy_path}")
105
- return policy
106
-
107
-
108
91
  # ---------------------------------------------------------------------------
109
92
  # Internal: project-over-base merge (with enforcement)
110
93
  # ---------------------------------------------------------------------------
@@ -20,19 +20,24 @@ from typing import Any
20
20
 
21
21
  VALID_PROFILES = {"minimal", "standard", "strict", "full", "offline-slm"}
22
22
  VALID_HOOK_PROFILES = {"minimal", "standard", "strict"}
23
- VALID_OUTPUT_FILTER_MODES = {"off", "observe", "safe"}
24
- VALID_OUTPUT_FILTER_PROFILES = {"repeat-lines", "tap-success"}
25
- MAX_OUTPUT_FILTER_INPUT_BYTES = 8_388_608
26
- DEFAULT_OUTPUT_FILTER_MIN_SAVINGS_BYTES = 1_024
27
23
  HOOK_PROFILE_ORDER = {"minimal": 0, "standard": 1, "strict": 2}
28
24
  IMMUTABLE_ARTICLES = frozenset({1, 2, 3, 4, 5, 6, 7})
29
25
  MIN_JUSTIFICATION_LEN = 20
30
26
  VALID_CONFIG_FIELDS = frozenset({
31
27
  "$schema", "extends", "name", "version", "description", "profile",
32
28
  "agents", "plugins", "rules", "constitution", "enforce", "overrides",
33
- "toolOutputFilter",
34
29
  })
35
30
 
31
+ # Keys from removed features. Accepted and ignored so an upgrade never fails
32
+ # validation on a config the user has not cleaned up yet, but reported so the
33
+ # line does not sit there forever pretending to configure something.
34
+ RETIRED_CONFIG_FIELDS = {
35
+ "toolOutputFilter": (
36
+ "the native tool-output filter was removed in v4.17.0; this key is "
37
+ "ignored and can be deleted"
38
+ ),
39
+ }
40
+
36
41
 
37
42
  # ---------------------------------------------------------------------------
38
43
  # Exceptions
@@ -125,7 +130,7 @@ def validate_merged_config(
125
130
  def _validate_schema(config: dict[str, Any], errors: list[str]) -> None:
126
131
  """Validate structural correctness of config."""
127
132
 
128
- unknown = set(config) - VALID_CONFIG_FIELDS
133
+ unknown = set(config) - VALID_CONFIG_FIELDS - set(RETIRED_CONFIG_FIELDS)
129
134
  if unknown:
130
135
  errors.append(
131
136
  f"Unknown top-level config keys: {', '.join(sorted(unknown))}."
@@ -174,10 +179,6 @@ def _validate_schema(config: dict[str, Any], errors: list[str]) -> None:
174
179
  if overrides is not None:
175
180
  _validate_overrides_block(overrides, errors)
176
181
 
177
- output_filter = config.get("toolOutputFilter")
178
- if output_filter is not None:
179
- _validate_output_filter_block(output_filter, errors)
180
-
181
182
 
182
183
  def _validate_agents_block(agents: Any, errors: list[str]) -> None:
183
184
  """Validate agents section structure."""
@@ -356,134 +357,6 @@ def _validate_overrides_block(overrides: Any, errors: list[str]) -> None:
356
357
  )
357
358
 
358
359
 
359
- def _validate_output_filter_block(output_filter: Any, errors: list[str]) -> None:
360
- """Validate native tool-output filter configuration."""
361
- if not isinstance(output_filter, dict):
362
- errors.append("'toolOutputFilter' must be an object.")
363
- return
364
-
365
- valid_keys = {
366
- "mode", "profiles", "maxInputBytes", "minSavingsBytes",
367
- "minSavingsRatio", "recovery",
368
- }
369
- unknown = set(output_filter) - valid_keys
370
- if unknown:
371
- errors.append(
372
- f"Unknown keys in 'toolOutputFilter': {', '.join(sorted(unknown))}."
373
- )
374
-
375
- mode = output_filter.get("mode")
376
- if mode is not None and mode not in VALID_OUTPUT_FILTER_MODES:
377
- errors.append(
378
- f"Invalid 'toolOutputFilter.mode' '{mode}'. "
379
- f"Valid: {', '.join(sorted(VALID_OUTPUT_FILTER_MODES))}."
380
- )
381
-
382
- profiles = output_filter.get("profiles")
383
- if profiles is not None:
384
- if not isinstance(profiles, list) or not all(
385
- isinstance(profile, str) for profile in profiles
386
- ):
387
- errors.append("'toolOutputFilter.profiles' must be an array of strings.")
388
- else:
389
- invalid = set(profiles) - VALID_OUTPUT_FILTER_PROFILES
390
- if invalid:
391
- errors.append(
392
- "Invalid 'toolOutputFilter.profiles': "
393
- f"{', '.join(sorted(invalid))}."
394
- )
395
- if len(profiles) != len(set(profiles)):
396
- errors.append(
397
- "'toolOutputFilter.profiles' must not contain duplicate items."
398
- )
399
-
400
- _validate_output_filter_limits(output_filter, errors)
401
- recovery = output_filter.get("recovery")
402
- if recovery is not None:
403
- _validate_output_filter_recovery(recovery, errors)
404
-
405
-
406
- def _validate_output_filter_limits(
407
- output_filter: dict[str, Any],
408
- errors: list[str],
409
- ) -> None:
410
- """Validate output-filter byte and ratio bounds."""
411
- _validate_bounded_integer(
412
- output_filter.get("maxInputBytes"),
413
- "toolOutputFilter.maxInputBytes",
414
- errors,
415
- minimum=1,
416
- maximum=MAX_OUTPUT_FILTER_INPUT_BYTES,
417
- )
418
- _validate_bounded_integer(
419
- output_filter.get("minSavingsBytes"),
420
- "toolOutputFilter.minSavingsBytes",
421
- errors,
422
- minimum=0,
423
- maximum=MAX_OUTPUT_FILTER_INPUT_BYTES,
424
- )
425
- _validate_output_filter_savings_relation(output_filter, errors)
426
-
427
- ratio = output_filter.get("minSavingsRatio")
428
- if ratio is not None and (
429
- isinstance(ratio, bool)
430
- or not isinstance(ratio, (int, float))
431
- or not 0 <= ratio <= 1
432
- ):
433
- errors.append("'toolOutputFilter.minSavingsRatio' must be between 0 and 1.")
434
-
435
-
436
- def _validate_output_filter_savings_relation(
437
- output_filter: dict[str, Any],
438
- errors: list[str],
439
- ) -> None:
440
- """Ensure the savings threshold can be reached within the input limit."""
441
- max_input = output_filter.get(
442
- "maxInputBytes",
443
- MAX_OUTPUT_FILTER_INPUT_BYTES,
444
- )
445
- min_savings = output_filter.get(
446
- "minSavingsBytes",
447
- DEFAULT_OUTPUT_FILTER_MIN_SAVINGS_BYTES,
448
- )
449
- values = (max_input, min_savings)
450
- if any(
451
- not isinstance(value, int) or isinstance(value, bool)
452
- for value in values
453
- ):
454
- return
455
- if min_savings > max_input:
456
- errors.append(
457
- "'toolOutputFilter.minSavingsBytes' must not exceed "
458
- "'toolOutputFilter.maxInputBytes'."
459
- )
460
-
461
-
462
- def _validate_output_filter_recovery(recovery: Any, errors: list[str]) -> None:
463
- """Validate secure ephemeral recovery settings."""
464
- if not isinstance(recovery, dict):
465
- errors.append("'toolOutputFilter.recovery' must be an object.")
466
- return
467
-
468
- valid_keys = {"mode", "ttlMinutes", "maxSessionBytes"}
469
- unknown = set(recovery) - valid_keys
470
- if unknown:
471
- errors.append(
472
- "Unknown keys in 'toolOutputFilter.recovery': "
473
- f"{', '.join(sorted(unknown))}."
474
- )
475
-
476
- if recovery.get("mode") not in (None, "ephemeral"):
477
- errors.append("'toolOutputFilter.recovery.mode' must be 'ephemeral'.")
478
- for key in ("ttlMinutes", "maxSessionBytes"):
479
- _validate_bounded_integer(
480
- recovery.get(key),
481
- f"toolOutputFilter.recovery.{key}",
482
- errors,
483
- minimum=1,
484
- )
485
-
486
-
487
360
  def _validate_bounded_integer(
488
361
  value: Any,
489
362
  field: str,
package/scripts/doctor.py CHANGED
@@ -75,7 +75,6 @@ EXPECTED_HOOKS = [
75
75
  "subagent-start.sh",
76
76
  "subagent-stop.sh",
77
77
  "session-end.sh",
78
- "filter-tool-output.sh",
79
78
  ]
80
79
 
81
80
  PLANNED_ASSETS = [
@@ -442,25 +441,9 @@ def check_generated_artifacts(dr: DiagResult, fix_mode: bool) -> None:
442
441
  else:
443
442
  dr.warn(f"{artifact} missing (run: ai-toolkit generate-all)")
444
443
 
445
- # Check if AGENTS.md is stale (agents added/removed since generation)
446
- agents_md = toolkit_dir / "AGENTS.md"
447
- if agents_md.is_file():
448
- agents_dir = toolkit_dir / "app" / "agents"
449
- actual_agents = sum(1 for f in agents_dir.glob("*.md") if f.is_file()) if agents_dir.is_dir() else 0
450
- content = agents_md.read_text(encoding="utf-8")
451
- mentioned_agents = len(re.findall(r"^### `", content, re.MULTILINE))
452
- if actual_agents != mentioned_agents:
453
- if fix_mode:
454
- result = subprocess.run(
455
- ["python3", str(toolkit_dir / "scripts" / "generate_agents_md.py")],
456
- capture_output=True,
457
- text=True,
458
- )
459
- if result.returncode == 0:
460
- agents_md.write_text(result.stdout, encoding="utf-8")
461
- dr.fixed(f"regenerated stale AGENTS.md ({mentioned_agents} -> {actual_agents} entries)")
462
- else:
463
- dr.warn(f"AGENTS.md may be stale: {mentioned_agents} entries vs {actual_agents} agent files")
444
+ # AGENTS.md no longer embeds the agent catalog (agents are discovered from
445
+ # their native directories), so there is no per-entry staleness to check —
446
+ # presence is covered by the generated-artifacts loop above.
464
447
 
465
448
  print()
466
449