claude-dev-env 2.34.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.
- package/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
- package/bin/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/audit-hooks-cli.mjs +54 -0
- package/scripts/audit-hooks.mjs +397 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +328 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +352 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +66 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +387 -0
- package/scripts/policy_lint/registry.py +162 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- package/scripts/select_pstack_models.mjs +304 -0
- package/scripts/tests/test-audit-hooks.mjs +379 -0
- package/scripts/tests/test_cde_lint.py +374 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +252 -0
- 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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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(
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|