claude-dev-env 2.35.0 → 2.36.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 (42) hide show
  1. package/.agents/skills/autoconverge/SKILL.md +19 -15
  2. package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
  3. package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
  4. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
  5. package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
  6. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
  7. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
  8. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
  9. package/bin/cde.mjs +175 -0
  10. package/bin/cde.test.mjs +222 -0
  11. package/bin/install-constants.mjs +4 -0
  12. package/bin/install.cursor-rules.test.mjs +233 -2
  13. package/bin/install.mjs +49 -3
  14. package/bin/pstack-model-preferences.codex.json +23 -0
  15. package/bin/select-pstack-models.test.mjs +378 -0
  16. package/hooks/hooks_constants/tool_names.py +20 -0
  17. package/hooks/validators/fast_save_validators.py +63 -89
  18. package/hooks/validators/run_all_validators.py +16 -8
  19. package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
  20. package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
  21. package/package.json +3 -2
  22. package/rules/pstack-models.md +119 -0
  23. package/scripts/cde_lint.py +258 -0
  24. package/scripts/policy_lint/__init__.py +41 -0
  25. package/scripts/policy_lint/adapter_configuration.py +91 -0
  26. package/scripts/policy_lint/adapter_detectors.py +328 -0
  27. package/scripts/policy_lint/adapter_pairing.py +145 -0
  28. package/scripts/policy_lint/adapter_support.py +97 -0
  29. package/scripts/policy_lint/adapters.py +352 -0
  30. package/scripts/policy_lint/config/__init__.py +1 -0
  31. package/scripts/policy_lint/config/constants.py +66 -0
  32. package/scripts/policy_lint/engine.py +224 -0
  33. package/scripts/policy_lint/model.py +387 -0
  34. package/scripts/policy_lint/registry.py +162 -0
  35. package/scripts/policy_lint/render.py +91 -0
  36. package/scripts/policy_lint/selection.py +372 -0
  37. package/scripts/policy_lint/selection_git.py +147 -0
  38. package/scripts/select_pstack_models.mjs +304 -0
  39. package/scripts/tests/test_cde_lint.py +374 -0
  40. package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
  41. package/scripts/tests/test_policy_lint_rules_registry.py +252 -0
  42. package/scripts/tests/test_policy_lint_selection.py +373 -0
@@ -0,0 +1,378 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import {
7
+ allPstackRoleRequirements,
8
+ selectPstackDelegation,
9
+ } from '../scripts/select_pstack_models.mjs';
10
+
11
+ const ALL_PSTACK_ROLES = [
12
+ 'feature, refactoring',
13
+ 'bug-fix',
14
+ 'perf-issue',
15
+ 'hillclimb',
16
+ 'judgment and prose',
17
+ 'hardest tasks',
18
+ 'how explorer',
19
+ 'how explainer',
20
+ 'how critics',
21
+ 'why investigators',
22
+ 'why synthesizer',
23
+ 'reflect tooling',
24
+ 'reflect judgment, divergent, synthesizer',
25
+ 'arena runners',
26
+ 'arena cross-judge pool',
27
+ 'swarm workers',
28
+ 'architect runners',
29
+ 'interrogate reviewers',
30
+ ];
31
+
32
+ function writeHostPreferences(preferencesDirectory, host, modelsByRole) {
33
+ mkdirSync(preferencesDirectory, { recursive: true });
34
+ writeFileSync(
35
+ join(preferencesDirectory, 'pstack-model-preferences.' + host + '.json'),
36
+ JSON.stringify({ host, modelsByRole }),
37
+ );
38
+ }
39
+
40
+ function selectWithPreferences(preferencesDirectory, overrides) {
41
+ return selectPstackDelegation({
42
+ host: 'codex',
43
+ inventoryHost: 'codex',
44
+ role: 'feature, refactoring',
45
+ delegationIndex: 0,
46
+ availableModelIds: ['gpt-5.6-sol'],
47
+ confirmedSuitableModelIds: [],
48
+ parentFallback: {
49
+ isAllowed: false,
50
+ hasMaterialCapabilityLoss: false,
51
+ },
52
+ preferencesDirectory,
53
+ ...overrides,
54
+ });
55
+ }
56
+
57
+ test('portable policy represents every pstack role without model ids', () => {
58
+ assert.deepEqual(Object.keys(allPstackRoleRequirements), ALL_PSTACK_ROLES);
59
+ const serializedRequirements = JSON.stringify(allPstackRoleRequirements);
60
+ assert.doesNotMatch(serializedRequirements, /gpt-|claude-|grok-/i);
61
+ });
62
+
63
+ test('Codex preferences select every available model in a diverse panel', () => {
64
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-codex-'));
65
+ try {
66
+ writeHostPreferences(preferencesDirectory, 'codex', {
67
+ 'how critics': ['gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-6-astra'],
68
+ });
69
+ const selection = selectWithPreferences(preferencesDirectory, {
70
+ role: 'how critics',
71
+ delegationIndex: 1,
72
+ availableModelIds: ['gpt-5.6-sol', 'gpt-6-astra', 'gpt-5.6-terra'],
73
+ });
74
+ assert.equal(selection.canDelegate, true);
75
+ assert.equal(selection.selectedHost, 'codex');
76
+ assert.equal(selection.selectionSource, 'host-preference');
77
+ assert.deepEqual(selection.panel.selectedModelIds, [
78
+ 'gpt-5.6-terra',
79
+ 'gpt-5.6-sol',
80
+ 'gpt-6-astra',
81
+ ]);
82
+ assert.equal(selection.panel.isCrossModelDiverse, true);
83
+ assert.deepEqual(selection.nativeSpawnArguments, { model: 'gpt-5.6-sol' });
84
+ assert.equal(selection.omitNativeModelArgument, false);
85
+ assert.equal(selection.availabilityValidated, true);
86
+ } finally {
87
+ rmSync(preferencesDirectory, { recursive: true, force: true });
88
+ }
89
+ });
90
+
91
+ test('a foreign host reads only its own saved preferences', () => {
92
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-host-'));
93
+ try {
94
+ writeHostPreferences(preferencesDirectory, 'codex', {
95
+ 'feature, refactoring': ['gpt-5.6-sol'],
96
+ });
97
+ writeHostPreferences(preferencesDirectory, 'claude', {
98
+ 'feature, refactoring': ['confirmed-claude-model'],
99
+ });
100
+ const selection = selectWithPreferences(preferencesDirectory, {
101
+ host: 'claude',
102
+ inventoryHost: 'claude',
103
+ availableModelIds: ['confirmed-claude-model', 'gpt-5.6-sol'],
104
+ });
105
+ assert.equal(selection.selectedHost, 'claude');
106
+ assert.deepEqual(selection.nativeSpawnArguments, {
107
+ model: 'confirmed-claude-model',
108
+ });
109
+ } finally {
110
+ rmSync(preferencesDirectory, { recursive: true, force: true });
111
+ }
112
+ });
113
+
114
+ test('an unavailable preferred model uses a confirmed host alternative', () => {
115
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-alternative-'));
116
+ try {
117
+ writeHostPreferences(preferencesDirectory, 'codex', {
118
+ 'feature, refactoring': ['gpt-5.6-sol'],
119
+ });
120
+ const selection = selectWithPreferences(preferencesDirectory, {
121
+ availableModelIds: ['confirmed-current-model'],
122
+ confirmedSuitableModelIds: ['confirmed-current-model'],
123
+ });
124
+ assert.equal(selection.canDelegate, true);
125
+ assert.equal(selection.selectionSource, 'confirmed-host-alternative');
126
+ assert.deepEqual(selection.nativeSpawnArguments, {
127
+ model: 'confirmed-current-model',
128
+ });
129
+ } finally {
130
+ rmSync(preferencesDirectory, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ test('the arena cross-judge pool selects one model other than the parent', () => {
135
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-judge-'));
136
+ try {
137
+ writeHostPreferences(preferencesDirectory, 'codex', {
138
+ 'arena cross-judge pool': ['gpt-5.6-sol', 'gpt-6-astra'],
139
+ });
140
+ const selection = selectWithPreferences(preferencesDirectory, {
141
+ role: 'arena cross-judge pool',
142
+ parentModelId: 'gpt-5.6-sol',
143
+ availableModelIds: ['gpt-5.6-sol', 'gpt-6-astra'],
144
+ });
145
+ assert.equal(selection.panel.agentCount, 1);
146
+ assert.deepEqual(selection.nativeSpawnArguments, { model: 'gpt-6-astra' });
147
+ } finally {
148
+ rmSync(preferencesDirectory, { recursive: true, force: true });
149
+ }
150
+ });
151
+ test('the arena cross-judge prefers a real alternative over parent inheritance', () => {
152
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-judge-parent-'));
153
+ try {
154
+ writeHostPreferences(preferencesDirectory, 'codex', {
155
+ 'arena cross-judge pool': ['auto', 'confirmed-other-model'],
156
+ });
157
+ const selection = selectWithPreferences(preferencesDirectory, {
158
+ role: 'arena cross-judge pool',
159
+ parentModelId: 'current-parent-model',
160
+ availableModelIds: ['confirmed-other-model'],
161
+ parentFallback: {
162
+ isAllowed: true,
163
+ hasMaterialCapabilityLoss: false,
164
+ },
165
+ });
166
+ assert.deepEqual(selection.nativeSpawnArguments, {
167
+ model: 'confirmed-other-model',
168
+ });
169
+ assert.equal(selection.omitNativeModelArgument, false);
170
+ } finally {
171
+ rmSync(preferencesDirectory, { recursive: true, force: true });
172
+ }
173
+ });
174
+
175
+ test('a same-model swarm reports its actual panel composition', () => {
176
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-swarm-'));
177
+ try {
178
+ writeHostPreferences(preferencesDirectory, 'codex', {
179
+ 'swarm workers': ['gpt-5.6-luna'],
180
+ });
181
+ const selection = selectWithPreferences(preferencesDirectory, {
182
+ role: 'swarm workers',
183
+ delegationIndex: 2,
184
+ panel: {
185
+ agentCount: 4,
186
+ requiresDistinctModels: false,
187
+ },
188
+ availableModelIds: ['gpt-5.6-luna'],
189
+ });
190
+ assert.equal(selection.canDelegate, true);
191
+ assert.equal(selection.panel.agentCount, 4);
192
+ assert.deepEqual(selection.panel.selectedModelIds, [
193
+ 'gpt-5.6-luna',
194
+ 'gpt-5.6-luna',
195
+ 'gpt-5.6-luna',
196
+ 'gpt-5.6-luna',
197
+ ]);
198
+ assert.equal(selection.panel.isCrossModelDiverse, false);
199
+ assert.deepEqual(selection.nativeSpawnArguments, { model: 'gpt-5.6-luna' });
200
+ } finally {
201
+ rmSync(preferencesDirectory, { recursive: true, force: true });
202
+ }
203
+ });
204
+
205
+ test('a diversity-required panel fails without enough distinct real models', () => {
206
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-diversity-'));
207
+ try {
208
+ writeHostPreferences(preferencesDirectory, 'codex', {
209
+ 'interrogate reviewers': ['gpt-5.6-terra', 'auto', 'gpt-5.6-sol'],
210
+ });
211
+ const selection = selectWithPreferences(preferencesDirectory, {
212
+ role: 'interrogate reviewers',
213
+ availableModelIds: ['gpt-5.6-terra', 'gpt-5.6-sol'],
214
+ });
215
+ assert.equal(selection.canDelegate, false);
216
+ assert.equal(selection.requiresUserChoice, true);
217
+ assert.equal(selection.failure, 'distinct-models-unavailable');
218
+ assert.equal(selection.panel.isCrossModelDiverse, false);
219
+ assert.deepEqual(selection.nativeSpawnArguments, {});
220
+ } finally {
221
+ rmSync(preferencesDirectory, { recursive: true, force: true });
222
+ }
223
+ });
224
+
225
+ test('parent inheritance omits the native model argument', () => {
226
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-parent-'));
227
+ try {
228
+ writeHostPreferences(preferencesDirectory, 'codex', {
229
+ 'feature, refactoring': ['inherit-parent'],
230
+ });
231
+ const selection = selectWithPreferences(preferencesDirectory, {
232
+ availableModelIds: [],
233
+ parentFallback: {
234
+ isAllowed: true,
235
+ hasMaterialCapabilityLoss: false,
236
+ },
237
+ });
238
+ assert.equal(selection.canDelegate, true);
239
+ assert.equal(selection.selectionSource, 'parent-inheritance');
240
+ assert.deepEqual(selection.nativeSpawnArguments, {});
241
+ assert.equal(selection.omitNativeModelArgument, true);
242
+ } finally {
243
+ rmSync(preferencesDirectory, { recursive: true, force: true });
244
+ }
245
+ });
246
+
247
+ test('material capability loss requires a user choice before inheritance', () => {
248
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-parent-loss-'));
249
+ try {
250
+ writeHostPreferences(preferencesDirectory, 'codex', {
251
+ 'hardest tasks': ['inherit-parent'],
252
+ });
253
+ const selection = selectWithPreferences(preferencesDirectory, {
254
+ role: 'hardest tasks',
255
+ availableModelIds: [],
256
+ parentFallback: {
257
+ isAllowed: true,
258
+ hasMaterialCapabilityLoss: true,
259
+ },
260
+ });
261
+ assert.equal(selection.canDelegate, false);
262
+ assert.equal(selection.requiresUserChoice, true);
263
+ assert.equal(selection.failure, 'parent-capability-loss');
264
+ assert.deepEqual(selection.nativeSpawnArguments, {});
265
+ } finally {
266
+ rmSync(preferencesDirectory, { recursive: true, force: true });
267
+ }
268
+ });
269
+
270
+ test('inventory and confirmed alternatives must belong to the selected host', () => {
271
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-inventory-'));
272
+ try {
273
+ assert.throws(
274
+ () => selectWithPreferences(preferencesDirectory, {
275
+ host: 'codex',
276
+ inventoryHost: 'claude',
277
+ }),
278
+ /inventory host must match selected host/,
279
+ );
280
+ assert.throws(
281
+ () => selectWithPreferences(preferencesDirectory, {
282
+ availableModelIds: ['confirmed-current-model'],
283
+ confirmedSuitableModelIds: ['missing-model'],
284
+ }),
285
+ /confirmed suitable models must be available/,
286
+ );
287
+ } finally {
288
+ rmSync(preferencesDirectory, { recursive: true, force: true });
289
+ }
290
+ });
291
+
292
+ test('an empty model set materializes allowed parent inheritance', () => {
293
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-empty-parent-'));
294
+ try {
295
+ const selection = selectWithPreferences(preferencesDirectory, {
296
+ availableModelIds: [],
297
+ parentFallback: {
298
+ isAllowed: true,
299
+ hasMaterialCapabilityLoss: false,
300
+ },
301
+ });
302
+ assert.equal(selection.canDelegate, true);
303
+ assert.equal(selection.selectionSource, 'parent-inheritance');
304
+ assert.deepEqual(selection.panel.selectedModelIds, [null]);
305
+ assert.equal(selection.omitNativeModelArgument, true);
306
+ assert.deepEqual(selection.nativeSpawnArguments, {});
307
+ } finally {
308
+ rmSync(preferencesDirectory, { recursive: true, force: true });
309
+ }
310
+ });
311
+
312
+ test('delegation index and fallback flags require exact boundary types', () => {
313
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-boundary-'));
314
+ try {
315
+ for (const invalidIndex of [undefined, Number.NaN, 0.5, -1]) {
316
+ assert.throws(
317
+ () => selectWithPreferences(preferencesDirectory, {
318
+ delegationIndex: invalidIndex,
319
+ }),
320
+ /delegation index must be a non-negative integer/,
321
+ );
322
+ }
323
+ for (const invalidRole of ['toString', ['feature, refactoring']]) {
324
+ assert.throws(
325
+ () => selectWithPreferences(preferencesDirectory, {
326
+ role: invalidRole,
327
+ }),
328
+ /role must name a portable pstack role/,
329
+ );
330
+ }
331
+ assert.throws(
332
+ () => selectWithPreferences(preferencesDirectory, {
333
+ parentFallback: {
334
+ isAllowed: 'yes',
335
+ hasMaterialCapabilityLoss: false,
336
+ },
337
+ }),
338
+ /parent fallback flags must be booleans/,
339
+ );
340
+ assert.throws(
341
+ () => selectWithPreferences(preferencesDirectory, {
342
+ parentFallback: {
343
+ isAllowed: true,
344
+ },
345
+ }),
346
+ /parent fallback flags must be booleans/,
347
+ );
348
+ } finally {
349
+ rmSync(preferencesDirectory, { recursive: true, force: true });
350
+ }
351
+ });
352
+
353
+ test('an optional-diversity review panel reports repeated models', () => {
354
+ const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-review-panel-'));
355
+ try {
356
+ writeHostPreferences(preferencesDirectory, 'codex', {
357
+ 'how critics': ['gpt-5.6-sol'],
358
+ });
359
+ const selection = selectWithPreferences(preferencesDirectory, {
360
+ role: 'how critics',
361
+ delegationIndex: 2,
362
+ panel: {
363
+ agentCount: 3,
364
+ requiresDistinctModels: false,
365
+ },
366
+ availableModelIds: ['gpt-5.6-sol'],
367
+ });
368
+ assert.equal(selection.canDelegate, true);
369
+ assert.deepEqual(selection.panel.selectedModelIds, [
370
+ 'gpt-5.6-sol',
371
+ 'gpt-5.6-sol',
372
+ 'gpt-5.6-sol',
373
+ ]);
374
+ assert.equal(selection.panel.isCrossModelDiverse, false);
375
+ } finally {
376
+ rmSync(preferencesDirectory, { recursive: true, force: true });
377
+ }
378
+ });
@@ -0,0 +1,20 @@
1
+ """Shared names for mutation and shell tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ BASH_TOOL_NAME: str = "Bash"
6
+ POWERSHELL_TOOL_NAME: str = "PowerShell"
7
+ WRITE_TOOL_NAME: str = "Write"
8
+ EDIT_TOOL_NAME: str = "Edit"
9
+ MULTI_EDIT_TOOL_NAME: str = "MultiEdit"
10
+ APPLY_PATCH_TOOL_NAME: str = "apply_patch"
11
+
12
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES: frozenset[str] = frozenset(
13
+ {BASH_TOOL_NAME, POWERSHELL_TOOL_NAME}
14
+ )
15
+ ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES: frozenset[str] = frozenset(
16
+ {WRITE_TOOL_NAME, EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME}
17
+ )
18
+ ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES: frozenset[str] = frozenset(
19
+ ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES | {APPLY_PATCH_TOOL_NAME}
20
+ )
@@ -14,7 +14,7 @@ the CLI mode calls. A save-path finding matches the CLI mode's finding for
14
14
  the same validator; only Mypy and the process-per-check overhead are gone.
15
15
  """
16
16
 
17
- from collections.abc import Callable, Sequence
17
+ from collections.abc import Callable, Collection, Sequence
18
18
  from dataclasses import dataclass
19
19
  from pathlib import Path
20
20
 
@@ -125,92 +125,61 @@ class _FastSaveValidatorSpecification:
125
125
  no_matching_files_message: str
126
126
 
127
127
 
128
- def _all_fast_save_validator_specifications() -> (
129
- "tuple[_FastSaveValidatorSpecification, ...]"
130
- ):
131
- """Return the save-path roster: every non-Mypy, non-Ruff file-scoped check."""
128
+ def _build_specification(
129
+ display_name: str,
130
+ checks: str,
131
+ applies_to_file: FilePredicate,
132
+ collect_violation_lines: FileViolationLineCollector,
133
+ no_matching_files_message: str,
134
+ ) -> _FastSaveValidatorSpecification:
135
+ return _FastSaveValidatorSpecification(
136
+ display_name,
137
+ checks,
138
+ applies_to_file,
139
+ collect_violation_lines,
140
+ no_matching_files_message,
141
+ )
142
+
143
+
144
+ def _python_specifications() -> tuple[_FastSaveValidatorSpecification, ...]:
145
+ return (
146
+ _build_specification("Python Style", "1,2,3,4", _is_python_file, _violation_lines_from(validate_python_style_file), NO_PYTHON_FILES_MESSAGE),
147
+ _build_specification("Abbreviations", "5", _is_python_file, _violation_lines_from(validate_abbreviation_file), NO_PYTHON_FILES_MESSAGE),
148
+ _build_specification("Magic Values", "7", _is_python_file, _violation_lines_from(validate_magic_literal_file), NO_PYTHON_FILES_MESSAGE),
149
+ _build_specification("Security", "27,28,29", _is_python_file, _violation_lines_from(validate_security_file), NO_PYTHON_FILES_MESSAGE),
150
+ _build_specification("Code Quality", "30,31,32", _is_python_file, _violation_lines_from(validate_code_quality_file), NO_PYTHON_FILES_MESSAGE),
151
+ _build_specification("Python Anti-patterns", "33,34,35", _is_python_file, _violation_lines_from(validate_python_antipattern_file), NO_PYTHON_FILES_MESSAGE),
152
+ _build_specification("Type Safety", "39,40", _is_python_file, _violation_lines_from(validate_type_safety_file), NO_PYTHON_FILES_MESSAGE),
153
+ )
154
+
155
+
156
+ def _test_specifications() -> tuple[_FastSaveValidatorSpecification, ...]:
157
+ return (
158
+ _build_specification("Test Safety", "11,21", _is_test_python_file, _collect_test_safety_violation_lines, NO_TEST_FILES_MESSAGE),
159
+ _build_specification("Useless Tests", "12", _is_test_python_file, _violation_lines_from(validate_useless_test_file), NO_TEST_FILES_MESSAGE),
160
+ )
161
+
162
+
163
+ def _non_python_specifications() -> tuple[_FastSaveValidatorSpecification, ...]:
132
164
  return (
133
- # Python naming, style, and constants.
134
- _FastSaveValidatorSpecification(
135
- "Python Style",
136
- "1,2,3,4",
137
- _is_python_file,
138
- _violation_lines_from(validate_python_style_file),
139
- NO_PYTHON_FILES_MESSAGE,
140
- ),
141
- _FastSaveValidatorSpecification(
142
- "Abbreviations",
143
- "5",
144
- _is_python_file,
145
- _violation_lines_from(validate_abbreviation_file),
146
- NO_PYTHON_FILES_MESSAGE,
147
- ),
148
- _FastSaveValidatorSpecification(
149
- "Magic Values",
150
- "7",
151
- _is_python_file,
152
- _violation_lines_from(validate_magic_literal_file),
153
- NO_PYTHON_FILES_MESSAGE,
154
- ),
155
- _FastSaveValidatorSpecification(
156
- "Security",
157
- "27,28,29",
158
- _is_python_file,
159
- _violation_lines_from(validate_security_file),
160
- NO_PYTHON_FILES_MESSAGE,
161
- ),
162
- _FastSaveValidatorSpecification(
163
- "Code Quality",
164
- "30,31,32",
165
- _is_python_file,
166
- _violation_lines_from(validate_code_quality_file),
167
- NO_PYTHON_FILES_MESSAGE,
168
- ),
169
- # Python anti-patterns and typing.
170
- _FastSaveValidatorSpecification(
171
- "Python Anti-patterns",
172
- "33,34,35",
173
- _is_python_file,
174
- _violation_lines_from(validate_python_antipattern_file),
175
- NO_PYTHON_FILES_MESSAGE,
176
- ),
177
- _FastSaveValidatorSpecification(
178
- "Type Safety",
179
- "39,40",
180
- _is_python_file,
181
- _violation_lines_from(validate_type_safety_file),
182
- NO_PYTHON_FILES_MESSAGE,
183
- ),
184
- # Scoped to a test_*.py-shaped file.
185
- _FastSaveValidatorSpecification(
186
- "Test Safety",
187
- "11,21",
188
- _is_test_python_file,
189
- _collect_test_safety_violation_lines,
190
- NO_TEST_FILES_MESSAGE,
191
- ),
192
- _FastSaveValidatorSpecification(
193
- "Useless Tests",
194
- "12",
195
- _is_test_python_file,
196
- _violation_lines_from(validate_useless_test_file),
197
- NO_TEST_FILES_MESSAGE,
198
- ),
199
- # Non-Python or multi-language files.
200
- _FastSaveValidatorSpecification(
201
- "React",
202
- "17",
203
- _is_react_file,
204
- _collect_react_violation_lines,
205
- NO_REACT_FILES_MESSAGE,
206
- ),
207
- _FastSaveValidatorSpecification(
208
- "PR References",
209
- "6",
210
- _is_code_reference_file,
211
- _violation_lines_from(validate_pr_reference_file),
212
- NO_CODE_REFERENCE_FILES_MESSAGE,
213
- ),
165
+ _build_specification("React", "17", _is_react_file, _collect_react_violation_lines, NO_REACT_FILES_MESSAGE),
166
+ _build_specification("PR References", "6", _is_code_reference_file, _violation_lines_from(validate_pr_reference_file), NO_CODE_REFERENCE_FILES_MESSAGE),
167
+ )
168
+
169
+
170
+ def _all_fast_save_validator_specifications(
171
+ excluded_validator_names: Collection[str] = (),
172
+ ) -> tuple[_FastSaveValidatorSpecification, ...]:
173
+ """Return the save-path roster: every non-Mypy, non-Ruff file-scoped check."""
174
+ all_specifications = (
175
+ *_python_specifications(),
176
+ *_test_specifications(),
177
+ *_non_python_specifications(),
178
+ )
179
+ return tuple(
180
+ each_specification
181
+ for each_specification in all_specifications
182
+ if each_specification.display_name not in excluded_validator_names
214
183
  )
215
184
 
216
185
 
@@ -250,11 +219,14 @@ def _run_specification(
250
219
  return _fast_save_outcome(specification, False, violation_report)
251
220
 
252
221
 
253
- def run_fast_save_validators(files: "list[Path]") -> "list[ValidatorResult]":
222
+ def run_fast_save_validators(
223
+ files: "list[Path]", excluded_validator_names: Collection[str] = ()
224
+ ) -> "list[ValidatorResult]":
254
225
  """Run every save-path validator in-process, skipping Mypy and Ruff.
255
226
 
256
227
  Args:
257
- files: The files under validation -- one reconstructed file in gate mode.
228
+ files: Files under validation. Gate mode passes one reconstructed file.
229
+ excluded_validator_names: Display names omitted from this run.
258
230
 
259
231
  Returns:
260
232
  One result per in-process validator, in roster order. The caller adds
@@ -262,5 +234,7 @@ def run_fast_save_validators(files: "list[Path]") -> "list[ValidatorResult]":
262
234
  """
263
235
  return [
264
236
  _run_specification(each_specification, files)
265
- for each_specification in _all_fast_save_validator_specifications()
237
+ for each_specification in _all_fast_save_validator_specifications(
238
+ excluded_validator_names
239
+ )
266
240
  ]
@@ -19,7 +19,7 @@ from dataclasses import dataclass
19
19
  from functools import lru_cache
20
20
  from datetime import datetime
21
21
  from pathlib import Path
22
- from typing import Callable, Dict, List, Optional, Tuple
22
+ from typing import Callable, Collection, Dict, List, Optional, Tuple
23
23
 
24
24
  from ._path_setup import hooks_directory_on_path # noqa: F401
25
25
  from .config.baseline_identity_constants import (
@@ -51,7 +51,7 @@ from blocking.codex_apply_patch import (
51
51
  parse_codex_apply_patch,
52
52
  payload_patch_command,
53
53
  )
54
- from hooks_constants.pre_tool_use_dispatcher_constants import (
54
+ from hooks_constants.tool_names import (
55
55
  APPLY_PATCH_TOOL_NAME,
56
56
  )
57
57
  from hooks_constants.hook_block_logger import log_hook_block
@@ -885,6 +885,8 @@ def validate_proposed_file(
885
885
  file_path: str,
886
886
  proposed_content: str,
887
887
  config_source_path: Optional[Path] = None,
888
+ include_ruff: bool = True,
889
+ excluded_validator_names: Collection[str] = (),
888
890
  ) -> List[ValidatorResult]:
889
891
  """Validate *proposed_content* as if written to *file_path*.
890
892
 
@@ -901,10 +903,12 @@ def validate_proposed_file(
901
903
  proposed_content: The reconstructed post-edit content of that file.
902
904
  config_source_path: Path Ruff resolves its config from; defaults to
903
905
  *file_path* when the caller passes nothing.
906
+ include_ruff: Whether to run Ruff after the in-process validators.
907
+ excluded_validator_names: Display names omitted from the in-process run.
904
908
 
905
909
  Returns:
906
- One ValidatorResult per save-path check, plus Ruff. Mypy never runs
907
- here; CLI/full mode is the only caller that still runs it.
910
+ One ValidatorResult per in-process check, plus Ruff when requested.
911
+ Mypy never runs here; CLI/full mode is the only caller that still runs it.
908
912
  """
909
913
  with tempfile.TemporaryDirectory() as temporary_directory:
910
914
  temporary_file = _temporary_path_preserving_directory_signal(
@@ -916,10 +920,14 @@ def validate_proposed_file(
916
920
  if config_source_path is not None
917
921
  else Path(file_path)
918
922
  )
919
- return [
920
- *run_fast_save_validators([temporary_file]),
921
- run_ruff_checks([temporary_file], resolved_config_source_path),
922
- ]
923
+ all_results = run_fast_save_validators(
924
+ [temporary_file], excluded_validator_names
925
+ )
926
+ if include_ruff:
927
+ all_results.append(
928
+ run_ruff_checks([temporary_file], resolved_config_source_path)
929
+ )
930
+ return all_results
923
931
 
924
932
 
925
933
  def _validator_summaries(results: List[ValidatorResult]) -> str:
@@ -0,0 +1,25 @@
1
+ """Prove the fast-save validator seam can omit native or duplicate checks."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .fast_save_validators import run_fast_save_validators
6
+
7
+ _CLEAN_SOURCE = (
8
+ "def add_two_numbers(first_number: int, second_number: int) -> int:\n"
9
+ " return first_number + second_number\n"
10
+ )
11
+ _OVERLAPPING_VALIDATOR_NAMES = frozenset(
12
+ {"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
13
+ )
14
+
15
+
16
+ def test_fast_save_validators_excludes_requested_outcomes(tmp_path: Path) -> None:
17
+ target_path = tmp_path / "probe.py"
18
+ target_path.write_text(_CLEAN_SOURCE, encoding="utf-8")
19
+ all_results = run_fast_save_validators(
20
+ [target_path], _OVERLAPPING_VALIDATOR_NAMES
21
+ )
22
+
23
+ all_result_names = {each_result.name for each_result in all_results}
24
+ assert all_result_names.isdisjoint(_OVERLAPPING_VALIDATOR_NAMES)
25
+ assert "Abbreviations" in all_result_names