@softspark/ai-toolkit 4.16.1 → 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.
- package/CHANGELOG.md +32 -0
- package/README.md +9 -15
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/hooks/session-end.sh +1 -13
- package/app/hooks.json +0 -10
- package/benchmarks/ecosystem-doctor-snapshot.json +14 -15
- package/bin/ai-toolkit.js +0 -2
- package/kb/history/completed/output-filter-retirement-20260726.md +128 -0
- package/kb/reference/architecture-overview.md +2 -3
- package/kb/reference/cli-reference.md +3 -13
- package/kb/reference/enterprise-config-guide.md +1 -21
- package/kb/reference/hooks-catalog.md +3 -60
- package/kb/reference/supported-tools-registry.md +0 -4
- package/llms-full.txt +143 -395
- package/llms.txt +1 -1
- package/manifest.json +147 -36
- package/package.json +1 -2
- package/scripts/claude_app.py +2 -21
- package/scripts/config_cli.py +4 -0
- package/scripts/config_merger.py +0 -17
- package/scripts/config_validator.py +11 -138
- package/scripts/doctor.py +3 -20
- package/scripts/install.py +2 -1
- package/scripts/install_steps/ai_tools.py +28 -99
- package/scripts/install_steps/hooks.py +26 -24
- package/scripts/merge-hooks.py +33 -2
- package/scripts/output_filter_retirement.py +395 -0
- package/scripts/schemas/ai-toolkit-config.schema.json +0 -60
- package/scripts/uninstall.py +13 -27
- package/app/hooks/filter-tool-output.sh +0 -76
- package/app/output-filter-policy.json +0 -15
- package/benchmarks/output-filter/README.md +0 -11
- package/benchmarks/output-filter/scenarios.json +0 -25
- package/kb/reference/tool-output-filter.md +0 -288
- package/scripts/benchmark_output_filter.py +0 -343
- package/scripts/output_filter_cli.py +0 -347
- package/scripts/output_filter_hook.py +0 -23
- package/scripts/tool_output_filter/__init__.py +0 -33
- package/scripts/tool_output_filter/contracts.py +0 -173
- package/scripts/tool_output_filter/engine.py +0 -260
- package/scripts/tool_output_filter/hook_runtime.py +0 -369
- package/scripts/tool_output_filter/input.py +0 -56
- package/scripts/tool_output_filter/invariants.py +0 -40
- package/scripts/tool_output_filter/policy.py +0 -153
- package/scripts/tool_output_filter/profiles/__init__.py +0 -68
- package/scripts/tool_output_filter/profiles/repeat_lines.py +0 -71
- package/scripts/tool_output_filter/profiles/tap_success.py +0 -154
- package/scripts/tool_output_filter/recovery.py +0 -846
- 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.
|
|
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": [
|
|
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": [
|
|
19
|
+
"tags": [
|
|
20
|
+
"core",
|
|
21
|
+
"skills",
|
|
22
|
+
"commands"
|
|
23
|
+
]
|
|
17
24
|
},
|
|
18
25
|
"hooks": {
|
|
19
|
-
"description": "
|
|
20
|
-
"path": "app/hooks.json + app/hooks/*.sh
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
91
|
+
"includes": [
|
|
92
|
+
"hooks",
|
|
93
|
+
"constitution"
|
|
94
|
+
]
|
|
65
95
|
},
|
|
66
96
|
"standard": {
|
|
67
97
|
"description": "Full toolkit (recommended)",
|
|
68
|
-
"includes": [
|
|
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": [
|
|
108
|
+
"includes": [
|
|
109
|
+
"agents",
|
|
110
|
+
"hooks",
|
|
111
|
+
"constitution"
|
|
112
|
+
]
|
|
73
113
|
},
|
|
74
114
|
"skills-only": {
|
|
75
115
|
"description": "Skills without agents",
|
|
76
|
-
"includes": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
165
|
+
"auto_detect": [
|
|
166
|
+
"go.mod"
|
|
167
|
+
]
|
|
111
168
|
},
|
|
112
169
|
"rules-rust": {
|
|
113
170
|
"description": "Rust-specific rules",
|
|
114
|
-
"auto_detect": [
|
|
171
|
+
"auto_detect": [
|
|
172
|
+
"Cargo.toml"
|
|
173
|
+
]
|
|
115
174
|
},
|
|
116
175
|
"rules-java": {
|
|
117
176
|
"description": "Java-specific rules",
|
|
118
|
-
"auto_detect": [
|
|
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": [
|
|
185
|
+
"auto_detect": [
|
|
186
|
+
"build.gradle.kts"
|
|
187
|
+
]
|
|
123
188
|
},
|
|
124
189
|
"rules-swift": {
|
|
125
190
|
"description": "Swift-specific rules",
|
|
126
|
-
"auto_detect": [
|
|
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": [
|
|
198
|
+
"auto_detect": [
|
|
199
|
+
"pubspec.yaml"
|
|
200
|
+
]
|
|
131
201
|
},
|
|
132
202
|
"rules-csharp": {
|
|
133
203
|
"description": "C#/.NET-specific rules",
|
|
134
|
-
"auto_detect": [
|
|
204
|
+
"auto_detect": [
|
|
205
|
+
"*.csproj",
|
|
206
|
+
"*.sln"
|
|
207
|
+
]
|
|
135
208
|
},
|
|
136
209
|
"rules-php": {
|
|
137
210
|
"description": "PHP-specific rules",
|
|
138
|
-
"auto_detect": [
|
|
211
|
+
"auto_detect": [
|
|
212
|
+
"composer.json"
|
|
213
|
+
]
|
|
139
214
|
},
|
|
140
215
|
"rules-cpp": {
|
|
141
216
|
"description": "C++-specific rules",
|
|
142
|
-
"auto_detect": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
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",
|
|
171
|
-
"
|
|
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.
|
|
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",
|
package/scripts/claude_app.py
CHANGED
|
@@ -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
|
package/scripts/config_cli.py
CHANGED
|
@@ -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:
|
package/scripts/config_merger.py
CHANGED
|
@@ -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
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
package/scripts/install.py
CHANGED
|
@@ -51,7 +51,7 @@ from emission import agent_count as count_agents, skill_count as count_skills
|
|
|
51
51
|
|
|
52
52
|
# Step modules
|
|
53
53
|
from install_steps.symlinks import install_agents, install_skills, clean_legacy_commands
|
|
54
|
-
from install_steps.hooks import install_hooks
|
|
54
|
+
from install_steps.hooks import cleanup_retired_output_filter, install_hooks
|
|
55
55
|
from install_steps.markers import install_marker_files, inject_rules, refresh_url_hooks, refresh_url_mcp
|
|
56
56
|
from install_steps.ai_tools import install_ai_tools, install_local_project, run_script
|
|
57
57
|
from install_steps.install_state import (
|
|
@@ -435,6 +435,7 @@ def install_claude_code(target_dir: Path, hooks_scripts_dir: Path,
|
|
|
435
435
|
install_agents(claude_dir, only, skip, dry_run)
|
|
436
436
|
install_skills(claude_dir, only, skip, dry_run)
|
|
437
437
|
clean_legacy_commands(claude_dir, dry_run)
|
|
438
|
+
cleanup_retired_output_filter(hooks_scripts_dir, dry_run)
|
|
438
439
|
install_hooks(claude_dir, hooks_scripts_dir, only, skip, dry_run)
|
|
439
440
|
install_marker_files(claude_dir, only, skip, dry_run)
|
|
440
441
|
|