claude-dev-env 7.6.1 → 7.7.1
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/e-code-review/reference/fix.md +4 -2
- package/.agents/skills/e-code-review/reference/loop.md +4 -2
- 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 +424 -26
- package/bin/subagent-model-policy.test.mjs +436 -0
- package/docs/codex-compatibility.md +19 -5
- package/docs/subagent-model-routing.md +69 -0
- package/hooks/hooks.json +10 -0
- package/hooks/routing/subagent_model_routing.mjs +81 -0
- package/hooks/routing/subagent_model_routing.test.mjs +255 -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 {
|
|
@@ -88,6 +111,38 @@ test('Codex preferences select every available model in a diverse panel', () =>
|
|
|
88
111
|
}
|
|
89
112
|
});
|
|
90
113
|
|
|
114
|
+
test('selector exclusions remove Sol Medium before its Luna remap enters a panel', () => {
|
|
115
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-sol-exclusion-'));
|
|
116
|
+
try {
|
|
117
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
118
|
+
'how critics': [
|
|
119
|
+
{ model: ' Sol ', effort: 'Medium' },
|
|
120
|
+
{ model: 'gpt-5.6-sol', effort: 'medium' },
|
|
121
|
+
{ model: 'gpt-5.6-luna', effort: 'high' },
|
|
122
|
+
{ model: 'gpt-6-astra', effort: 'medium' },
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
126
|
+
role: 'how critics',
|
|
127
|
+
panel: { agentCount: 2, requiresDistinctModels: true },
|
|
128
|
+
availableModelIds: ['gpt-5.6-sol', 'gpt-5.6-luna', 'gpt-6-astra'],
|
|
129
|
+
confirmedSuitableModelIds: ['gpt-5.6-sol'],
|
|
130
|
+
});
|
|
131
|
+
assert.equal(selection.canDelegate, true);
|
|
132
|
+
assert.deepEqual(selection.panel.selectedModelPairs, [
|
|
133
|
+
{ model: 'gpt-5.6-luna', effort: 'high' },
|
|
134
|
+
{ model: 'gpt-6-astra', effort: 'medium' },
|
|
135
|
+
]);
|
|
136
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
137
|
+
model: 'gpt-5.6-luna',
|
|
138
|
+
reasoning_effort: 'high',
|
|
139
|
+
});
|
|
140
|
+
assert.deepEqual(selection.routingDiagnostics, []);
|
|
141
|
+
} finally {
|
|
142
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
91
146
|
test('a foreign host reads only its own saved preferences', () => {
|
|
92
147
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-host-'));
|
|
93
148
|
try {
|
|
@@ -111,21 +166,23 @@ test('a foreign host reads only its own saved preferences', () => {
|
|
|
111
166
|
}
|
|
112
167
|
});
|
|
113
168
|
|
|
114
|
-
test('an unavailable preferred model
|
|
169
|
+
test('an unavailable preferred model stops before a confirmed host alternative', () => {
|
|
115
170
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-alternative-'));
|
|
116
171
|
try {
|
|
117
172
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
118
|
-
'feature, refactoring': ['gpt-5.6-
|
|
173
|
+
'feature, refactoring': ['gpt-5.6-terra'],
|
|
174
|
+
});
|
|
175
|
+
const policyPath = writePolicyWithModels(preferencesDirectory, {
|
|
176
|
+
current: 'confirmed-current-model',
|
|
119
177
|
});
|
|
120
178
|
const selection = selectWithPreferences(preferencesDirectory, {
|
|
121
179
|
availableModelIds: ['confirmed-current-model'],
|
|
122
180
|
confirmedSuitableModelIds: ['confirmed-current-model'],
|
|
181
|
+
policyPath,
|
|
123
182
|
});
|
|
124
|
-
assert.equal(selection.canDelegate,
|
|
125
|
-
assert.
|
|
126
|
-
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
127
|
-
model: 'confirmed-current-model',
|
|
128
|
-
});
|
|
183
|
+
assert.equal(selection.canDelegate, false);
|
|
184
|
+
assert.match(selection.failure, /model-routing-failed: replacement model is unavailable/);
|
|
185
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
129
186
|
} finally {
|
|
130
187
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
131
188
|
}
|
|
@@ -143,7 +200,10 @@ test('the arena cross-judge pool selects one model other than the parent', () =>
|
|
|
143
200
|
availableModelIds: ['gpt-5.6-sol', 'gpt-6-astra'],
|
|
144
201
|
});
|
|
145
202
|
assert.equal(selection.panel.agentCount, 1);
|
|
146
|
-
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
203
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
204
|
+
model: 'gpt-6-astra',
|
|
205
|
+
reasoning_effort: 'medium',
|
|
206
|
+
});
|
|
147
207
|
} finally {
|
|
148
208
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
149
209
|
}
|
|
@@ -154,6 +214,9 @@ test('the arena cross-judge prefers a real alternative over parent inheritance',
|
|
|
154
214
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
155
215
|
'arena cross-judge pool': ['auto', 'confirmed-other-model'],
|
|
156
216
|
});
|
|
217
|
+
const policyPath = writePolicyWithModels(preferencesDirectory, {
|
|
218
|
+
other: 'confirmed-other-model',
|
|
219
|
+
});
|
|
157
220
|
const selection = selectWithPreferences(preferencesDirectory, {
|
|
158
221
|
role: 'arena cross-judge pool',
|
|
159
222
|
parentModelId: 'current-parent-model',
|
|
@@ -162,9 +225,11 @@ test('the arena cross-judge prefers a real alternative over parent inheritance',
|
|
|
162
225
|
isAllowed: true,
|
|
163
226
|
hasMaterialCapabilityLoss: false,
|
|
164
227
|
},
|
|
228
|
+
policyPath,
|
|
165
229
|
});
|
|
166
230
|
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
167
231
|
model: 'confirmed-other-model',
|
|
232
|
+
reasoning_effort: 'medium',
|
|
168
233
|
});
|
|
169
234
|
assert.equal(selection.omitNativeModelArgument, false);
|
|
170
235
|
} finally {
|
|
@@ -196,13 +261,16 @@ test('a same-model swarm reports its actual panel composition', () => {
|
|
|
196
261
|
'gpt-5.6-luna',
|
|
197
262
|
]);
|
|
198
263
|
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
199
|
-
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
264
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
265
|
+
model: 'gpt-5.6-luna',
|
|
266
|
+
reasoning_effort: 'high',
|
|
267
|
+
});
|
|
200
268
|
} finally {
|
|
201
269
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
202
270
|
}
|
|
203
271
|
});
|
|
204
272
|
|
|
205
|
-
test('a diversity-required panel fails without enough distinct
|
|
273
|
+
test('a diversity-required panel fails without enough distinct models', () => {
|
|
206
274
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-diversity-'));
|
|
207
275
|
try {
|
|
208
276
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
@@ -214,7 +282,7 @@ test('a diversity-required panel fails without enough distinct real models', ()
|
|
|
214
282
|
});
|
|
215
283
|
assert.equal(selection.canDelegate, false);
|
|
216
284
|
assert.equal(selection.requiresUserChoice, true);
|
|
217
|
-
assert.
|
|
285
|
+
assert.match(selection.failure, /model-routing-failed:/);
|
|
218
286
|
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
219
287
|
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
220
288
|
} finally {
|
|
@@ -244,6 +312,63 @@ test('parent inheritance omits the native model argument', () => {
|
|
|
244
312
|
}
|
|
245
313
|
});
|
|
246
314
|
|
|
315
|
+
test('parent inheritance rejects an explicit preference effort', () => {
|
|
316
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-parent-effort-'));
|
|
317
|
+
try {
|
|
318
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
319
|
+
'feature, refactoring': [{ model: 'auto', effort: 'medium' }],
|
|
320
|
+
});
|
|
321
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
322
|
+
availableModelIds: [],
|
|
323
|
+
parentFallback: {
|
|
324
|
+
isAllowed: true,
|
|
325
|
+
hasMaterialCapabilityLoss: false,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
assert.equal(selection.canDelegate, false);
|
|
329
|
+
assert.equal(selection.failure, 'model-routing-failed: parent inheritance cannot set an effort');
|
|
330
|
+
assert.deepEqual(selection.panel.selectedModelIds, []);
|
|
331
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
332
|
+
} finally {
|
|
333
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('selector rejects policy-defined inheritance aliases in model inventories', () => {
|
|
338
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-inventory-alias-'));
|
|
339
|
+
try {
|
|
340
|
+
assert.throws(
|
|
341
|
+
() => selectWithPreferences(preferencesDirectory, {
|
|
342
|
+
availableModelIds: ['auto'],
|
|
343
|
+
confirmedSuitableModelIds: ['auto'],
|
|
344
|
+
parentFallback: {
|
|
345
|
+
isAllowed: true,
|
|
346
|
+
hasMaterialCapabilityLoss: false,
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
/confirmed suitable model ids must contain native model ids/,
|
|
350
|
+
);
|
|
351
|
+
const policy = JSON.parse(readFileSync(defaultSubagentModelPolicyPath(), 'utf8'));
|
|
352
|
+
policy.inheritanceAliases = ['custom-parent'];
|
|
353
|
+
const policyPath = join(preferencesDirectory, 'subagent-model-policy.json');
|
|
354
|
+
writeFileSync(policyPath, JSON.stringify(policy));
|
|
355
|
+
assert.throws(
|
|
356
|
+
() => selectWithPreferences(preferencesDirectory, {
|
|
357
|
+
availableModelIds: ['custom-parent'],
|
|
358
|
+
confirmedSuitableModelIds: [],
|
|
359
|
+
parentFallback: {
|
|
360
|
+
isAllowed: true,
|
|
361
|
+
hasMaterialCapabilityLoss: false,
|
|
362
|
+
},
|
|
363
|
+
policyPath,
|
|
364
|
+
}),
|
|
365
|
+
/available model ids must contain native model ids/,
|
|
366
|
+
);
|
|
367
|
+
} finally {
|
|
368
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
247
372
|
test('material capability loss requires a user choice before inheritance', () => {
|
|
248
373
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-parent-loss-'));
|
|
249
374
|
try {
|
|
@@ -354,7 +479,7 @@ test('an optional-diversity review panel reports repeated models', () => {
|
|
|
354
479
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-review-panel-'));
|
|
355
480
|
try {
|
|
356
481
|
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
357
|
-
'how critics': ['gpt-5.6-
|
|
482
|
+
'how critics': ['gpt-5.6-luna'],
|
|
358
483
|
});
|
|
359
484
|
const selection = selectWithPreferences(preferencesDirectory, {
|
|
360
485
|
role: 'how critics',
|
|
@@ -363,16 +488,289 @@ test('an optional-diversity review panel reports repeated models', () => {
|
|
|
363
488
|
agentCount: 3,
|
|
364
489
|
requiresDistinctModels: false,
|
|
365
490
|
},
|
|
366
|
-
availableModelIds: ['gpt-5.6-
|
|
491
|
+
availableModelIds: ['gpt-5.6-luna'],
|
|
367
492
|
});
|
|
368
493
|
assert.equal(selection.canDelegate, true);
|
|
369
494
|
assert.deepEqual(selection.panel.selectedModelIds, [
|
|
370
|
-
'gpt-5.6-
|
|
371
|
-
'gpt-5.6-
|
|
372
|
-
'gpt-5.6-
|
|
495
|
+
'gpt-5.6-luna',
|
|
496
|
+
'gpt-5.6-luna',
|
|
497
|
+
'gpt-5.6-luna',
|
|
373
498
|
]);
|
|
374
499
|
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
375
500
|
} finally {
|
|
376
501
|
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
377
502
|
}
|
|
378
|
-
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('a model-only preference still goes through policy validation', () => {
|
|
506
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-model-only-'));
|
|
507
|
+
try {
|
|
508
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
509
|
+
'feature, refactoring': ['unknown-model'],
|
|
510
|
+
});
|
|
511
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
512
|
+
availableModelIds: ['unknown-model'],
|
|
513
|
+
});
|
|
514
|
+
assert.equal(selection.canDelegate, false);
|
|
515
|
+
assert.match(selection.failure, /model-routing-failed: model is unknown/);
|
|
516
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
517
|
+
} finally {
|
|
518
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('a model-only Terra preference uses the host effort before routing', () => {
|
|
523
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-terra-default-'));
|
|
524
|
+
try {
|
|
525
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
526
|
+
'feature, refactoring': ['Terra'],
|
|
527
|
+
});
|
|
528
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
529
|
+
requestedEffort: 'medium',
|
|
530
|
+
availableModelIds: ['gpt-5.6-terra', 'gpt-5.6-luna'],
|
|
531
|
+
});
|
|
532
|
+
assert.equal(selection.canDelegate, true);
|
|
533
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
534
|
+
model: 'gpt-5.6-luna',
|
|
535
|
+
reasoning_effort: 'xhigh',
|
|
536
|
+
});
|
|
537
|
+
} finally {
|
|
538
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('a profile default effort supplies a missing model-only effort', () => {
|
|
543
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-profile-default-'));
|
|
544
|
+
try {
|
|
545
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
546
|
+
'feature, refactoring': ['Terra'],
|
|
547
|
+
}, 'medium');
|
|
548
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
549
|
+
requestedEffort: undefined,
|
|
550
|
+
availableModelIds: ['gpt-5.6-terra', 'gpt-5.6-luna'],
|
|
551
|
+
});
|
|
552
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
553
|
+
model: 'gpt-5.6-luna',
|
|
554
|
+
reasoning_effort: 'xhigh',
|
|
555
|
+
});
|
|
556
|
+
} finally {
|
|
557
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('a model-only worker Astra preference uses the host effort before routing', () => {
|
|
562
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-astra-default-'));
|
|
563
|
+
try {
|
|
564
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
565
|
+
'feature, refactoring': ['Astra'],
|
|
566
|
+
});
|
|
567
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
568
|
+
requestedEffort: 'high',
|
|
569
|
+
availableModelIds: ['gpt-6-astra'],
|
|
570
|
+
});
|
|
571
|
+
assert.equal(selection.canDelegate, true);
|
|
572
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
573
|
+
model: 'gpt-6-astra',
|
|
574
|
+
reasoning_effort: 'low',
|
|
575
|
+
});
|
|
576
|
+
} finally {
|
|
577
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('a model-only preference holds when the host supplies no usable effort', () => {
|
|
582
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-missing-effort-'));
|
|
583
|
+
try {
|
|
584
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
585
|
+
'feature, refactoring': ['Terra'],
|
|
586
|
+
});
|
|
587
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
588
|
+
requestedEffort: undefined,
|
|
589
|
+
availableModelIds: ['gpt-5.6-terra'],
|
|
590
|
+
});
|
|
591
|
+
assert.equal(selection.canDelegate, false);
|
|
592
|
+
assert.match(selection.failure, /effort is required for this model/);
|
|
593
|
+
} finally {
|
|
594
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test('a model-only Sol preference holds when no Sol effort remains approved', () => {
|
|
599
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-sol-missing-effort-'));
|
|
600
|
+
try {
|
|
601
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
602
|
+
'feature, refactoring': ['Sol'],
|
|
603
|
+
});
|
|
604
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
605
|
+
requestedEffort: undefined,
|
|
606
|
+
availableModelIds: ['gpt-5.6-sol', 'gpt-5.6-luna'],
|
|
607
|
+
});
|
|
608
|
+
assert.equal(selection.canDelegate, false);
|
|
609
|
+
assert.match(selection.failure, /effort is required for this model/);
|
|
610
|
+
} finally {
|
|
611
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('non-Codex inheritance keeps its ordered parent fallback', () => {
|
|
616
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-parent-'));
|
|
617
|
+
try {
|
|
618
|
+
writeHostPreferences(preferencesDirectory, 'claude', {
|
|
619
|
+
'feature, refactoring': ['auto', 'opus'],
|
|
620
|
+
});
|
|
621
|
+
const selection = selectPstackDelegation({
|
|
622
|
+
host: 'claude',
|
|
623
|
+
inventoryHost: 'claude',
|
|
624
|
+
role: 'feature, refactoring',
|
|
625
|
+
delegationIndex: 0,
|
|
626
|
+
availableModelIds: ['opus'],
|
|
627
|
+
confirmedSuitableModelIds: [],
|
|
628
|
+
parentFallback: {
|
|
629
|
+
isAllowed: true,
|
|
630
|
+
hasMaterialCapabilityLoss: false,
|
|
631
|
+
},
|
|
632
|
+
preferencesDirectory,
|
|
633
|
+
});
|
|
634
|
+
assert.equal(selection.canDelegate, true);
|
|
635
|
+
assert.equal(selection.omitNativeModelArgument, true);
|
|
636
|
+
assert.equal(selection.selectionSource, 'parent-inheritance');
|
|
637
|
+
assert.deepEqual(selection.panel.selectedModelIds, [null]);
|
|
638
|
+
} finally {
|
|
639
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test('selector routes preference pairs before panel dedupe', () => {
|
|
644
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-'));
|
|
645
|
+
try {
|
|
646
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
647
|
+
'feature, refactoring': [
|
|
648
|
+
{ model: 'gpt-5.6-terra', effort: 'medium' },
|
|
649
|
+
{ model: 'gpt-5.6-luna', effort: 'medium' },
|
|
650
|
+
],
|
|
651
|
+
});
|
|
652
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
653
|
+
panel: { agentCount: 2, requiresDistinctModels: false },
|
|
654
|
+
availableModelIds: ['gpt-5.6-terra', 'gpt-5.6-luna'],
|
|
655
|
+
});
|
|
656
|
+
assert.equal(selection.canDelegate, true);
|
|
657
|
+
assert.deepEqual(selection.panel.selectedModelIds, [
|
|
658
|
+
'gpt-5.6-luna',
|
|
659
|
+
'gpt-5.6-luna',
|
|
660
|
+
]);
|
|
661
|
+
assert.deepEqual(selection.panel.selectedModelPairs, [
|
|
662
|
+
{ model: 'gpt-5.6-luna', effort: 'xhigh' },
|
|
663
|
+
{ model: 'gpt-5.6-luna', effort: 'xhigh' },
|
|
664
|
+
]);
|
|
665
|
+
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
666
|
+
assert.deepEqual(selection.nativeSpawnArguments, {
|
|
667
|
+
model: 'gpt-5.6-luna',
|
|
668
|
+
reasoning_effort: 'xhigh',
|
|
669
|
+
});
|
|
670
|
+
} finally {
|
|
671
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test('panel diversity uses selected models after routing', () => {
|
|
676
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-panel-'));
|
|
677
|
+
try {
|
|
678
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
679
|
+
'arena runners': [
|
|
680
|
+
{ model: 'gpt-5.6-terra', effort: 'medium' },
|
|
681
|
+
{ model: 'gpt-5.6-sol', effort: 'medium' },
|
|
682
|
+
{ model: 'gpt-6-astra', effort: 'medium' },
|
|
683
|
+
],
|
|
684
|
+
});
|
|
685
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
686
|
+
role: 'arena runners',
|
|
687
|
+
availableModelIds: [
|
|
688
|
+
'gpt-5.6-terra',
|
|
689
|
+
'gpt-5.6-sol',
|
|
690
|
+
'gpt-6-astra',
|
|
691
|
+
'gpt-5.6-luna',
|
|
692
|
+
],
|
|
693
|
+
});
|
|
694
|
+
assert.equal(selection.canDelegate, false);
|
|
695
|
+
assert.equal(selection.failure, 'distinct-models-unavailable');
|
|
696
|
+
assert.deepEqual(selection.panel.selectedModelIds, [
|
|
697
|
+
'gpt-5.6-luna',
|
|
698
|
+
'gpt-6-astra',
|
|
699
|
+
]);
|
|
700
|
+
assert.equal(selection.panel.isCrossModelDiverse, false);
|
|
701
|
+
assert.deepEqual(selection.routingDiagnostics, []);
|
|
702
|
+
} finally {
|
|
703
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('selector keeps other Sol remaps as contenders', () => {
|
|
708
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-sol-remap-'));
|
|
709
|
+
try {
|
|
710
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
711
|
+
'feature, refactoring': [{ model: 'gpt-5.6-sol', effort: 'high' }],
|
|
712
|
+
});
|
|
713
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
714
|
+
availableModelIds: ['gpt-5.6-sol', 'gpt-6-astra'],
|
|
715
|
+
});
|
|
716
|
+
assert.equal(selection.canDelegate, true);
|
|
717
|
+
assert.deepEqual(selection.panel.selectedModelIds, ['gpt-6-astra']);
|
|
718
|
+
assert.deepEqual(selection.panel.selectedModelPairs, [
|
|
719
|
+
{ model: 'gpt-6-astra', effort: 'low' },
|
|
720
|
+
]);
|
|
721
|
+
assert.deepEqual(selection.routingDiagnostics, []);
|
|
722
|
+
} finally {
|
|
723
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test('unavailable routed models hold selection', () => {
|
|
728
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-unavailable-'));
|
|
729
|
+
try {
|
|
730
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
731
|
+
'feature, refactoring': [{ model: 'gpt-5.6-terra', effort: 'medium' }],
|
|
732
|
+
});
|
|
733
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
734
|
+
availableModelIds: ['gpt-5.6-terra'],
|
|
735
|
+
});
|
|
736
|
+
assert.equal(selection.canDelegate, false);
|
|
737
|
+
assert.equal(selection.requiresUserChoice, true);
|
|
738
|
+
assert.match(selection.failure, /model-routing-failed: replacement model is unavailable/);
|
|
739
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
740
|
+
} finally {
|
|
741
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test('malformed routing policy holds selection with a diagnostic', () => {
|
|
746
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-policy-'));
|
|
747
|
+
try {
|
|
748
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
749
|
+
'feature, refactoring': [{ model: 'gpt-5.6-terra', effort: 'medium' }],
|
|
750
|
+
});
|
|
751
|
+
const policyPath = join(preferencesDirectory, 'subagent-model-policy.json');
|
|
752
|
+
writeFileSync(policyPath, '{"schemaVersion":1}\n');
|
|
753
|
+
const selection = selectWithPreferences(preferencesDirectory, { policyPath });
|
|
754
|
+
assert.equal(selection.canDelegate, false);
|
|
755
|
+
assert.match(selection.failure, /model-routing-failed: models must be an object/);
|
|
756
|
+
assert.deepEqual(selection.nativeSpawnArguments, {});
|
|
757
|
+
} finally {
|
|
758
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('an explicit missing policy path holds selection', () => {
|
|
763
|
+
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-missing-policy-'));
|
|
764
|
+
try {
|
|
765
|
+
writeHostPreferences(preferencesDirectory, 'codex', {
|
|
766
|
+
'feature, refactoring': [{ model: 'Terra', effort: 'medium' }],
|
|
767
|
+
});
|
|
768
|
+
const selection = selectWithPreferences(preferencesDirectory, {
|
|
769
|
+
policyPath: join(preferencesDirectory, 'missing-policy.json'),
|
|
770
|
+
});
|
|
771
|
+
assert.equal(selection.canDelegate, false);
|
|
772
|
+
assert.match(selection.failure, /policy file is missing/);
|
|
773
|
+
} finally {
|
|
774
|
+
rmSync(preferencesDirectory, { recursive: true, force: true });
|
|
775
|
+
}
|
|
776
|
+
});
|