claude-dev-env 7.6.1 → 7.7.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.md +48 -71
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/advisor/reference/advisor-block.md +1 -1
- package/_shared/advisor/reference/astra-rung.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +122 -23
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +76 -9
- package/_shared/advisor/scripts/tests/test_advisor_route_constants.py +51 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +52 -2
- package/bin/install-constants.mjs +3 -0
- package/bin/install-transaction.mjs +41 -44
- package/bin/install.cursor-rules.test.mjs +311 -3
- package/bin/install.mjs +156 -95
- package/bin/install.transaction.test.mjs +61 -1
- package/bin/pstack-model-preferences.codex.json +1 -0
- package/bin/pstack.mjs +28 -6
- package/bin/pstack.test.mjs +48 -0
- package/bin/select-pstack-models.test.mjs +375 -26
- package/bin/subagent-model-policy.test.mjs +399 -0
- package/docs/codex-compatibility.md +19 -5
- package/docs/subagent-model-routing.md +50 -0
- package/hooks/hooks.json +10 -0
- package/hooks/routing/subagent_model_routing.mjs +81 -0
- package/hooks/routing/subagent_model_routing.test.mjs +218 -0
- package/package.json +1 -1
- package/rules/pstack-models.md +22 -0
- package/rules/subagent-model-policy.json +85 -0
- package/scripts/resolve_advisor_model_route.mjs +52 -0
- package/scripts/select_pstack_models.mjs +203 -38
- package/scripts/subagent_model_policy.mjs +620 -0
- package/scripts/tests/resolve_advisor_model_route.test.mjs +51 -0
- package/scripts/tests/test_codex_compat_materializer.py +15 -0
package/bin/pstack.test.mjs
CHANGED
|
@@ -46,6 +46,26 @@ for (const host of ['claude', 'codex', 'cursor']) {
|
|
|
46
46
|
const release = join(f.store, 'releases', result.release);
|
|
47
47
|
const releaseManifest = JSON.parse(readFileSync(join(release, 'release.json'), 'utf8'));
|
|
48
48
|
assert.equal(releaseManifest.skills.find(skill => skill.component === 'pstack').name, 'pstack:poteto-mode');
|
|
49
|
+
assert.equal(
|
|
50
|
+
releaseManifest.policyPath,
|
|
51
|
+
join(f.options.project, '.agents', 'rules', 'subagent-model-policy.json'),
|
|
52
|
+
);
|
|
53
|
+
assert.equal(
|
|
54
|
+
readFileSync(join(release, 'compat', 'subagent_model_policy.mjs'), 'utf8'),
|
|
55
|
+
readFileSync(join(packageRoot, 'scripts', 'subagent_model_policy.mjs'), 'utf8'),
|
|
56
|
+
);
|
|
57
|
+
assert.equal(
|
|
58
|
+
readFileSync(join(release, 'compat', 'subagent-model-policy.json'), 'utf8'),
|
|
59
|
+
readFileSync(join(packageRoot, 'rules', 'subagent-model-policy.json'), 'utf8'),
|
|
60
|
+
);
|
|
61
|
+
assert.equal(
|
|
62
|
+
readFileSync(join(release, 'compat', 'resolve_advisor_model_route.mjs'), 'utf8'),
|
|
63
|
+
readFileSync(join(packageRoot, 'scripts', 'resolve_advisor_model_route.mjs'), 'utf8'),
|
|
64
|
+
);
|
|
65
|
+
assert.equal(
|
|
66
|
+
readFileSync(join(f.options.project, '.agents', 'rules', 'subagent-model-policy.json'), 'utf8'),
|
|
67
|
+
readFileSync(join(packageRoot, 'rules', 'subagent-model-policy.json'), 'utf8'),
|
|
68
|
+
);
|
|
49
69
|
const expected = ['cde-create-skill', 'cursor-team-kit-control-cli', 'cursor-team-kit-control-ui', 'cursor-team-kit-deslop', 'pstack'];
|
|
50
70
|
for (const home of ['.claude', '.agents']) {
|
|
51
71
|
const skillsHome = join(f.options.project, home, 'skills');
|
|
@@ -85,6 +105,34 @@ test('unchanged install avoids another upstream fetch', t => {
|
|
|
85
105
|
assert.deepEqual(second.links, first.links);
|
|
86
106
|
});
|
|
87
107
|
|
|
108
|
+
test('policy edits survive a pstack update', t => {
|
|
109
|
+
const f = fixture(t);
|
|
110
|
+
installPstack(f.options, f.dependencies);
|
|
111
|
+
const policyPath = join(f.options.project, '.agents', 'rules', 'subagent-model-policy.json');
|
|
112
|
+
const editedPolicy = '{"schemaVersion":1,"edited":true}\n';
|
|
113
|
+
writeFileSync(policyPath, editedPolicy);
|
|
114
|
+
installPstack({ ...f.options, lock: { ...baseLock, commit: 'b'.repeat(40) } }, f.dependencies);
|
|
115
|
+
assert.equal(readFileSync(policyPath, 'utf8'), editedPolicy);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a release cannot redirect the installed policy path', t => {
|
|
119
|
+
const f = fixture(t);
|
|
120
|
+
const first = installPstack(f.options, f.dependencies);
|
|
121
|
+
const releaseRoot = join(f.store, 'releases', first.release);
|
|
122
|
+
const releaseManifestPath = join(releaseRoot, 'release.json');
|
|
123
|
+
const releaseManifest = JSON.parse(readFileSync(releaseManifestPath, 'utf8'));
|
|
124
|
+
const outsidePolicyPath = join(f.temporary, 'outside-policy.json');
|
|
125
|
+
releaseManifest.policyPath = outsidePolicyPath;
|
|
126
|
+
writeFileSync(releaseManifestPath, JSON.stringify(releaseManifest));
|
|
127
|
+
|
|
128
|
+
assert.throws(
|
|
129
|
+
() => installPstack({ ...f.options, strict: true }, f.dependencies),
|
|
130
|
+
/Invalid release policy path/,
|
|
131
|
+
);
|
|
132
|
+
assert.equal(existsSync(outsidePolicyPath), false);
|
|
133
|
+
assert.equal(verifyInstallation(f.options).release, first.release);
|
|
134
|
+
});
|
|
135
|
+
|
|
88
136
|
test('changed, added, and removed skills converge while old releases remain readable', t => {
|
|
89
137
|
const f = fixture(t);
|
|
90
138
|
put(join(f.checkout, 'pstack', 'skills', 'old', 'SKILL.md'), '---\nname: old\ndescription: Old skill.\n---\nOld body.\n');
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { defaultSubagentModelPolicyPath } from '../scripts/subagent_model_policy.mjs';
|
|
6
7
|
import {
|
|
7
8
|
allPstackRoleRequirements,
|
|
8
9
|
selectPstackDelegation,
|
|
@@ -29,14 +30,27 @@ const ALL_PSTACK_ROLES = [
|
|
|
29
30
|
'interrogate reviewers',
|
|
30
31
|
];
|
|
31
32
|
|
|
32
|
-
function writeHostPreferences(preferencesDirectory, host, modelsByRole) {
|
|
33
|
+
function writeHostPreferences(preferencesDirectory, host, modelsByRole, defaultEffort) {
|
|
33
34
|
mkdirSync(preferencesDirectory, { recursive: true });
|
|
35
|
+
const preferences = { host, modelsByRole };
|
|
36
|
+
if (defaultEffort !== undefined) preferences.defaultEffort = defaultEffort;
|
|
34
37
|
writeFileSync(
|
|
35
38
|
join(preferencesDirectory, 'pstack-model-preferences.' + host + '.json'),
|
|
36
|
-
JSON.stringify(
|
|
39
|
+
JSON.stringify(preferences),
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function writePolicyWithModels(preferencesDirectory, modelEntries) {
|
|
44
|
+
const policy = JSON.parse(readFileSync(defaultSubagentModelPolicyPath(), 'utf8'));
|
|
45
|
+
for (const [name, id] of Object.entries(modelEntries)) {
|
|
46
|
+
policy.models[name] = { id, aliases: [] };
|
|
47
|
+
policy.approvedPairs.push({ model: name, effort: 'medium' });
|
|
48
|
+
}
|
|
49
|
+
const policyPath = join(preferencesDirectory, 'subagent-model-policy.json');
|
|
50
|
+
writeFileSync(policyPath, JSON.stringify(policy));
|
|
51
|
+
return policyPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
function selectWithPreferences(preferencesDirectory, overrides) {
|
|
41
55
|
return selectPstackDelegation({
|
|
42
56
|
host: 'codex',
|
|
@@ -49,6 +63,7 @@ function selectWithPreferences(preferencesDirectory, overrides) {
|
|
|
49
63
|
isAllowed: false,
|
|
50
64
|
hasMaterialCapabilityLoss: false,
|
|
51
65
|
},
|
|
66
|
+
requestedEffort: 'medium',
|
|
52
67
|
preferencesDirectory,
|
|
53
68
|
...overrides,
|
|
54
69
|
});
|
|
@@ -60,7 +75,7 @@ test('portable policy represents every pstack role without model ids', () => {
|
|
|
60
75
|
assert.doesNotMatch(serializedRequirements, /gpt-|claude-|grok-/i);
|
|
61
76
|
});
|
|
62
77
|
|
|
63
|
-
test('Codex preferences
|
|
78
|
+
test('Codex preferences skip an excluded pair in a panel', () => {
|
|
64
79
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-codex-'));
|
|
65
80
|
try {
|
|
66
81
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
@@ -69,18 +84,26 @@ test('Codex preferences select every available model in a diverse panel', () =>
|
|
|
69
84
|
const selection = selectWithPreferences(preferencesDirectory, {
|
|
70
85
|
role: 'how critics',
|
|
71
86
|
delegationIndex: 1,
|
|
72
|
-
availableModelIds: [
|
|
87
|
+
availableModelIds: [
|
|
88
|
+
'gpt-5.6-sol',
|
|
89
|
+
'gpt-6-astra',
|
|
90
|
+
'gpt-5.6-terra',
|
|
91
|
+
'gpt-5.6-luna',
|
|
92
|
+
],
|
|
73
93
|
});
|
|
74
94
|
assert.equal(selection.canDelegate, true);
|
|
75
95
|
assert.equal(selection.selectedHost, 'codex');
|
|
76
96
|
assert.equal(selection.selectionSource, 'host-preference');
|
|
77
97
|
assert.deepEqual(selection.panel.selectedModelIds, [
|
|
78
|
-
'gpt-5.6-
|
|
79
|
-
'gpt-5.6-sol',
|
|
98
|
+
'gpt-5.6-luna',
|
|
80
99
|
'gpt-6-astra',
|
|
100
|
+
'gpt-5.6-luna',
|
|
81
101
|
]);
|
|
82
|
-
assert.equal(selection.panel.isCrossModelDiverse,
|
|
83
|
-
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
102
|
+
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
103
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
104
|
+
model: 'gpt-6-astra',
|
|
105
|
+
reasoning_effort: 'medium',
|
|
106
|
+
});
|
|
84
107
|
assert.equal(selection.omitNativeModelArgument, false);
|
|
85
108
|
assert.equal(selection.availabilityValidated, true);
|
|
86
109
|
} finally {
|
|
@@ -111,21 +134,23 @@ test('a foreign host reads only its own saved preferences', () => {
|
|
|
111
134
|
}
|
|
112
135
|
});
|
|
113
136
|
|
|
114
|
-
test('an unavailable preferred model
|
|
137
|
+
test('an unavailable preferred model stops before a confirmed host alternative', () => {
|
|
115
138
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-alternative-'));
|
|
116
139
|
try {
|
|
117
140
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
118
|
-
'feature, refactoring': ['gpt-5.6-
|
|
141
|
+
'feature, refactoring': ['gpt-5.6-terra'],
|
|
142
|
+
});
|
|
143
|
+
const policyPath = writePolicyWithModels(preferencesDirectory, {
|
|
144
|
+
current: 'confirmed-current-model',
|
|
119
145
|
});
|
|
120
146
|
const selection = selectWithPreferences(preferencesDirectory, {
|
|
121
147
|
availableModelIds: ['confirmed-current-model'],
|
|
122
148
|
confirmedSuitableModelIds: ['confirmed-current-model'],
|
|
149
|
+
policyPath,
|
|
123
150
|
});
|
|
124
|
-
assert.equal(selection.canDelegate,
|
|
125
|
-
assert.
|
|
126
|
-
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
127
|
-
model: 'confirmed-current-model',
|
|
128
|
-
});
|
|
151
|
+
assert.equal(selection.canDelegate, false);
|
|
152
|
+
assert.match(selection.failure, /model-routing-failed: replacement model is unavailable/);
|
|
153
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
129
154
|
} finally {
|
|
130
155
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
131
156
|
}
|
|
@@ -143,7 +168,10 @@ test('the arena cross-judge pool selects one model other than the parent', () =>
|
|
|
143
168
|
availableModelIds: ['gpt-5.6-sol', 'gpt-6-astra'],
|
|
144
169
|
});
|
|
145
170
|
assert.equal(selection.panel.agentCount, 1);
|
|
146
|
-
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
171
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
172
|
+
model: 'gpt-6-astra',
|
|
173
|
+
reasoning_effort: 'medium',
|
|
174
|
+
});
|
|
147
175
|
} finally {
|
|
148
176
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
149
177
|
}
|
|
@@ -154,6 +182,9 @@ test('the arena cross-judge prefers a real alternative over parent inheritance',
|
|
|
154
182
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
155
183
|
'arena cross-judge pool': ['auto', 'confirmed-other-model'],
|
|
156
184
|
});
|
|
185
|
+
const policyPath = writePolicyWithModels(preferencesDirectory, {
|
|
186
|
+
other: 'confirmed-other-model',
|
|
187
|
+
});
|
|
157
188
|
const selection = selectWithPreferences(preferencesDirectory, {
|
|
158
189
|
role: 'arena cross-judge pool',
|
|
159
190
|
parentModelId: 'current-parent-model',
|
|
@@ -162,9 +193,11 @@ test('the arena cross-judge prefers a real alternative over parent inheritance',
|
|
|
162
193
|
isAllowed: true,
|
|
163
194
|
hasMaterialCapabilityLoss: false,
|
|
164
195
|
},
|
|
196
|
+
policyPath,
|
|
165
197
|
});
|
|
166
198
|
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
167
199
|
model: 'confirmed-other-model',
|
|
200
|
+
reasoning_effort: 'medium',
|
|
168
201
|
});
|
|
169
202
|
assert.equal(selection.omitNativeModelArgument, false);
|
|
170
203
|
} finally {
|
|
@@ -196,13 +229,16 @@ test('a same-model swarm reports its actual panel composition', () => {
|
|
|
196
229
|
'gpt-5.6-luna',
|
|
197
230
|
]);
|
|
198
231
|
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
199
|
-
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
232
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
233
|
+
model: 'gpt-5.6-luna',
|
|
234
|
+
reasoning_effort: 'high',
|
|
235
|
+
});
|
|
200
236
|
} finally {
|
|
201
237
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
202
238
|
}
|
|
203
239
|
});
|
|
204
240
|
|
|
205
|
-
test('a diversity-required panel fails without enough distinct
|
|
241
|
+
test('a diversity-required panel fails without enough distinct models', () => {
|
|
206
242
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-diversity-'));
|
|
207
243
|
try {
|
|
208
244
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
@@ -214,7 +250,7 @@ test('a diversity-required panel fails without enough distinct real models', ()
|
|
|
214
250
|
});
|
|
215
251
|
assert.equal(selection.canDelegate, false);
|
|
216
252
|
assert.equal(selection.requiresUserChoice, true);
|
|
217
|
-
assert.
|
|
253
|
+
assert.match(selection.failure, /model-routing-failed:/);
|
|
218
254
|
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
219
255
|
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
220
256
|
} finally {
|
|
@@ -244,6 +280,63 @@ test('parent inheritance omits the native model argument', () => {
|
|
|
244
280
|
}
|
|
245
281
|
});
|
|
246
282
|
|
|
283
|
+
test('parent inheritance rejects an explicit preference effort', () => {
|
|
284
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-parent-effort-'));
|
|
285
|
+
try {
|
|
286
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
287
|
+
'feature, refactoring': [{ model: 'auto', effort: 'medium' }],
|
|
288
|
+
});
|
|
289
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
290
|
+
availableModelIds: [],
|
|
291
|
+
parentFallback: {
|
|
292
|
+
isAllowed: true,
|
|
293
|
+
hasMaterialCapabilityLoss: false,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
assert.equal(selection.canDelegate, false);
|
|
297
|
+
assert.equal(selection.failure, 'model-routing-failed: parent inheritance cannot set an effort');
|
|
298
|
+
assert.deepEqual(selection.panel.selectedModelIds, []);
|
|
299
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
300
|
+
} finally {
|
|
301
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('selector rejects policy-defined inheritance aliases in model inventories', () => {
|
|
306
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-inventory-alias-'));
|
|
307
|
+
try {
|
|
308
|
+
assert.throws(
|
|
309
|
+
() => selectWithPreferences(preferencesDirectory, {
|
|
310
|
+
availableModelIds: ['auto'],
|
|
311
|
+
confirmedSuitableModelIds: ['auto'],
|
|
312
|
+
parentFallback: {
|
|
313
|
+
isAllowed: true,
|
|
314
|
+
hasMaterialCapabilityLoss: false,
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
/confirmed suitable model ids must contain native model ids/,
|
|
318
|
+
);
|
|
319
|
+
const policy = JSON.parse(readFileSync(defaultSubagentModelPolicyPath(), 'utf8'));
|
|
320
|
+
policy.inheritanceAliases = ['custom-parent'];
|
|
321
|
+
const policyPath = join(preferencesDirectory, 'subagent-model-policy.json');
|
|
322
|
+
writeFileSync(policyPath, JSON.stringify(policy));
|
|
323
|
+
assert.throws(
|
|
324
|
+
() => selectWithPreferences(preferencesDirectory, {
|
|
325
|
+
availableModelIds: ['custom-parent'],
|
|
326
|
+
confirmedSuitableModelIds: [],
|
|
327
|
+
parentFallback: {
|
|
328
|
+
isAllowed: true,
|
|
329
|
+
hasMaterialCapabilityLoss: false,
|
|
330
|
+
},
|
|
331
|
+
policyPath,
|
|
332
|
+
}),
|
|
333
|
+
/available model ids must contain native model ids/,
|
|
334
|
+
);
|
|
335
|
+
} finally {
|
|
336
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
247
340
|
test('material capability loss requires a user choice before inheritance', () => {
|
|
248
341
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-parent-loss-'));
|
|
249
342
|
try {
|
|
@@ -354,7 +447,7 @@ test('an optional-diversity review panel reports repeated models', () => {
|
|
|
354
447
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-review-panel-'));
|
|
355
448
|
try {
|
|
356
449
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
357
|
-
'how critics': ['gpt-5.6-
|
|
450
|
+
'how critics': ['gpt-5.6-luna'],
|
|
358
451
|
});
|
|
359
452
|
const selection = selectWithPreferences(preferencesDirectory, {
|
|
360
453
|
role: 'how critics',
|
|
@@ -363,16 +456,272 @@ test('an optional-diversity review panel reports repeated models', () => {
|
|
|
363
456
|
agentCount: 3,
|
|
364
457
|
requiresDistinctModels: false,
|
|
365
458
|
},
|
|
366
|
-
availableModelIds: ['gpt-5.6-
|
|
459
|
+
availableModelIds: ['gpt-5.6-luna'],
|
|
367
460
|
});
|
|
368
461
|
assert.equal(selection.canDelegate, true);
|
|
369
462
|
assert.deepEqual(selection.panel.selectedModelIds, [
|
|
370
|
-
'gpt-5.6-
|
|
371
|
-
'gpt-5.6-
|
|
372
|
-
'gpt-5.6-
|
|
463
|
+
'gpt-5.6-luna',
|
|
464
|
+
'gpt-5.6-luna',
|
|
465
|
+
'gpt-5.6-luna',
|
|
373
466
|
]);
|
|
374
467
|
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
375
468
|
} finally {
|
|
376
469
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
377
470
|
}
|
|
378
|
-
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('a model-only preference still goes through policy validation', () => {
|
|
474
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-model-only-'));
|
|
475
|
+
try {
|
|
476
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
477
|
+
'feature, refactoring': ['unknown-model'],
|
|
478
|
+
});
|
|
479
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
480
|
+
availableModelIds: ['unknown-model'],
|
|
481
|
+
});
|
|
482
|
+
assert.equal(selection.canDelegate, false);
|
|
483
|
+
assert.match(selection.failure, /model-routing-failed: model is unknown/);
|
|
484
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
485
|
+
} finally {
|
|
486
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('a model-only Terra preference uses the host effort before routing', () => {
|
|
491
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-terra-default-'));
|
|
492
|
+
try {
|
|
493
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
494
|
+
'feature, refactoring': ['Terra'],
|
|
495
|
+
});
|
|
496
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
497
|
+
requestedEffort: 'medium',
|
|
498
|
+
availableModelIds: ['gpt-5.6-terra', 'gpt-5.6-luna'],
|
|
499
|
+
});
|
|
500
|
+
assert.equal(selection.canDelegate, true);
|
|
501
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
502
|
+
model: 'gpt-5.6-luna',
|
|
503
|
+
reasoning_effort: 'xhigh',
|
|
504
|
+
});
|
|
505
|
+
} finally {
|
|
506
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test('a profile default effort supplies a missing model-only effort', () => {
|
|
511
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-profile-default-'));
|
|
512
|
+
try {
|
|
513
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
514
|
+
'feature, refactoring': ['Terra'],
|
|
515
|
+
}, 'medium');
|
|
516
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
517
|
+
requestedEffort: undefined,
|
|
518
|
+
availableModelIds: ['gpt-5.6-terra', 'gpt-5.6-luna'],
|
|
519
|
+
});
|
|
520
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
521
|
+
model: 'gpt-5.6-luna',
|
|
522
|
+
reasoning_effort: 'xhigh',
|
|
523
|
+
});
|
|
524
|
+
} finally {
|
|
525
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('a model-only worker Astra preference uses the host effort before routing', () => {
|
|
530
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-astra-default-'));
|
|
531
|
+
try {
|
|
532
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
533
|
+
'feature, refactoring': ['Astra'],
|
|
534
|
+
});
|
|
535
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
536
|
+
requestedEffort: 'high',
|
|
537
|
+
availableModelIds: ['gpt-6-astra'],
|
|
538
|
+
});
|
|
539
|
+
assert.equal(selection.canDelegate, true);
|
|
540
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
541
|
+
model: 'gpt-6-astra',
|
|
542
|
+
reasoning_effort: 'low',
|
|
543
|
+
});
|
|
544
|
+
} finally {
|
|
545
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('a model-only preference holds when the host supplies no usable effort', () => {
|
|
550
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-missing-effort-'));
|
|
551
|
+
try {
|
|
552
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
553
|
+
'feature, refactoring': ['Terra'],
|
|
554
|
+
});
|
|
555
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
556
|
+
requestedEffort: undefined,
|
|
557
|
+
availableModelIds: ['gpt-5.6-terra'],
|
|
558
|
+
});
|
|
559
|
+
assert.equal(selection.canDelegate, false);
|
|
560
|
+
assert.match(selection.failure, /effort is required for this model/);
|
|
561
|
+
} finally {
|
|
562
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('non-Codex inheritance keeps its ordered parent fallback', () => {
|
|
567
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-parent-'));
|
|
568
|
+
try {
|
|
569
|
+
writeHostPreferences(preferencesDirectory, 'claude', {
|
|
570
|
+
'feature, refactoring': ['auto', 'opus'],
|
|
571
|
+
});
|
|
572
|
+
const selection = selectPstackDelegation({
|
|
573
|
+
host: 'claude',
|
|
574
|
+
inventoryHost: 'claude',
|
|
575
|
+
role: 'feature, refactoring',
|
|
576
|
+
delegationIndex: 0,
|
|
577
|
+
availableModelIds: ['opus'],
|
|
578
|
+
confirmedSuitableModelIds: [],
|
|
579
|
+
parentFallback: {
|
|
580
|
+
isAllowed: true,
|
|
581
|
+
hasMaterialCapabilityLoss: false,
|
|
582
|
+
},
|
|
583
|
+
preferencesDirectory,
|
|
584
|
+
});
|
|
585
|
+
assert.equal(selection.canDelegate, true);
|
|
586
|
+
assert.equal(selection.omitNativeModelArgument, true);
|
|
587
|
+
assert.equal(selection.selectionSource, 'parent-inheritance');
|
|
588
|
+
assert.deepEqual(selection.panel.selectedModelIds, [null]);
|
|
589
|
+
} finally {
|
|
590
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('selector routes preference pairs before panel dedupe', () => {
|
|
595
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-'));
|
|
596
|
+
try {
|
|
597
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
598
|
+
'feature, refactoring': [
|
|
599
|
+
{ model: 'gpt-5.6-terra', effort: 'medium' },
|
|
600
|
+
{ model: 'gpt-5.6-luna', effort: 'medium' },
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
604
|
+
panel: { agentCount: 2, requiresDistinctModels: false },
|
|
605
|
+
availableModelIds: ['gpt-5.6-terra', 'gpt-5.6-luna'],
|
|
606
|
+
});
|
|
607
|
+
assert.equal(selection.canDelegate, true);
|
|
608
|
+
assert.deepEqual(selection.panel.selectedModelIds, [
|
|
609
|
+
'gpt-5.6-luna',
|
|
610
|
+
'gpt-5.6-luna',
|
|
611
|
+
]);
|
|
612
|
+
assert.deepEqual(selection.panel.selectedModelPairs, [
|
|
613
|
+
{ model: 'gpt-5.6-luna', effort: 'xhigh' },
|
|
614
|
+
{ model: 'gpt-5.6-luna', effort: 'xhigh' },
|
|
615
|
+
]);
|
|
616
|
+
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
617
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
618
|
+
model: 'gpt-5.6-luna',
|
|
619
|
+
reasoning_effort: 'xhigh',
|
|
620
|
+
});
|
|
621
|
+
} finally {
|
|
622
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test('panel diversity uses selected models after routing', () => {
|
|
627
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-panel-'));
|
|
628
|
+
try {
|
|
629
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
630
|
+
'arena runners': [
|
|
631
|
+
{ model: 'gpt-5.6-terra', effort: 'medium' },
|
|
632
|
+
{ model: 'gpt-5.6-sol', effort: 'medium' },
|
|
633
|
+
{ model: 'gpt-6-astra', effort: 'medium' },
|
|
634
|
+
],
|
|
635
|
+
});
|
|
636
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
637
|
+
role: 'arena runners',
|
|
638
|
+
availableModelIds: [
|
|
639
|
+
'gpt-5.6-terra',
|
|
640
|
+
'gpt-5.6-sol',
|
|
641
|
+
'gpt-6-astra',
|
|
642
|
+
'gpt-5.6-luna',
|
|
643
|
+
],
|
|
644
|
+
});
|
|
645
|
+
assert.equal(selection.canDelegate, false);
|
|
646
|
+
assert.equal(selection.failure, 'distinct-models-unavailable');
|
|
647
|
+
assert.deepEqual(selection.panel.selectedModelIds, [
|
|
648
|
+
'gpt-5.6-luna',
|
|
649
|
+
'gpt-6-astra',
|
|
650
|
+
]);
|
|
651
|
+
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
652
|
+
assert.deepEqual(selection.routingDiagnostics, []);
|
|
653
|
+
} finally {
|
|
654
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('selector keeps other Sol remaps as contenders', () => {
|
|
659
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-sol-remap-'));
|
|
660
|
+
try {
|
|
661
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
662
|
+
'feature, refactoring': [{ model: 'gpt-5.6-sol', effort: 'high' }],
|
|
663
|
+
});
|
|
664
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
665
|
+
availableModelIds: ['gpt-5.6-sol', 'gpt-6-astra'],
|
|
666
|
+
});
|
|
667
|
+
assert.equal(selection.canDelegate, true);
|
|
668
|
+
assert.deepEqual(selection.panel.selectedModelIds, ['gpt-6-astra']);
|
|
669
|
+
assert.deepEqual(selection.panel.selectedModelPairs, [
|
|
670
|
+
{ model: 'gpt-6-astra', effort: 'low' },
|
|
671
|
+
]);
|
|
672
|
+
assert.deepEqual(selection.routingDiagnostics, []);
|
|
673
|
+
} finally {
|
|
674
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('unavailable routed models hold selection', () => {
|
|
679
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-unavailable-'));
|
|
680
|
+
try {
|
|
681
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
682
|
+
'feature, refactoring': [{ model: 'gpt-5.6-terra', effort: 'medium' }],
|
|
683
|
+
});
|
|
684
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
685
|
+
availableModelIds: ['gpt-5.6-terra'],
|
|
686
|
+
});
|
|
687
|
+
assert.equal(selection.canDelegate, false);
|
|
688
|
+
assert.equal(selection.requiresUserChoice, true);
|
|
689
|
+
assert.match(selection.failure, /model-routing-failed: replacement model is unavailable/);
|
|
690
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
691
|
+
} finally {
|
|
692
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('malformed routing policy holds selection with a diagnostic', () => {
|
|
697
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-policy-'));
|
|
698
|
+
try {
|
|
699
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
700
|
+
'feature, refactoring': [{ model: 'gpt-5.6-terra', effort: 'medium' }],
|
|
701
|
+
});
|
|
702
|
+
const policyPath = join(preferencesDirectory, 'subagent-model-policy.json');
|
|
703
|
+
writeFileSync(policyPath, '{"schemaVersion":1}\n');
|
|
704
|
+
const selection = selectWithPreferences(preferencesDirectory, { policyPath });
|
|
705
|
+
assert.equal(selection.canDelegate, false);
|
|
706
|
+
assert.match(selection.failure, /model-routing-failed: models must be an object/);
|
|
707
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
708
|
+
} finally {
|
|
709
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test('an explicit missing policy path holds selection', () => {
|
|
714
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-missing-policy-'));
|
|
715
|
+
try {
|
|
716
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
717
|
+
'feature, refactoring': [{ model: 'Terra', effort: 'medium' }],
|
|
718
|
+
});
|
|
719
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
720
|
+
policyPath: join(preferencesDirectory, 'missing-policy.json'),
|
|
721
|
+
});
|
|
722
|
+
assert.equal(selection.canDelegate, false);
|
|
723
|
+
assert.match(selection.failure, /policy file is missing/);
|
|
724
|
+
} finally {
|
|
725
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
726
|
+
}
|
|
727
|
+
});
|