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.
Files changed (33) hide show
  1. package/.agents/skills/e-code-review/reference/fix.md +4 -2
  2. package/.agents/skills/e-code-review/reference/loop.md +4 -2
  3. package/AGENTS.md +48 -71
  4. package/_shared/advisor/advisor-protocol.md +2 -2
  5. package/_shared/advisor/reference/advisor-block.md +1 -1
  6. package/_shared/advisor/reference/astra-rung.md +2 -2
  7. package/_shared/advisor/scripts/codex_astra_advisor.py +122 -23
  8. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +76 -9
  9. package/_shared/advisor/scripts/tests/test_advisor_route_constants.py +51 -0
  10. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +52 -2
  11. package/bin/install-constants.mjs +3 -0
  12. package/bin/install-transaction.mjs +41 -44
  13. package/bin/install.cursor-rules.test.mjs +311 -3
  14. package/bin/install.mjs +156 -95
  15. package/bin/install.transaction.test.mjs +61 -1
  16. package/bin/pstack-model-preferences.codex.json +1 -0
  17. package/bin/pstack.mjs +28 -6
  18. package/bin/pstack.test.mjs +48 -0
  19. package/bin/select-pstack-models.test.mjs +424 -26
  20. package/bin/subagent-model-policy.test.mjs +436 -0
  21. package/docs/codex-compatibility.md +19 -5
  22. package/docs/subagent-model-routing.md +69 -0
  23. package/hooks/hooks.json +10 -0
  24. package/hooks/routing/subagent_model_routing.mjs +81 -0
  25. package/hooks/routing/subagent_model_routing.test.mjs +255 -0
  26. package/package.json +1 -1
  27. package/rules/pstack-models.md +22 -0
  28. package/rules/subagent-model-policy.json +85 -0
  29. package/scripts/resolve_advisor_model_route.mjs +52 -0
  30. package/scripts/select_pstack_models.mjs +203 -38
  31. package/scripts/subagent_model_policy.mjs +620 -0
  32. package/scripts/tests/resolve_advisor_model_route.test.mjs +51 -0
  33. package/scripts/tests/test_codex_compat_materializer.py +15 -0
@@ -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({ host, modelsByRole }),
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 select every available model in a diverse panel', () => {
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: ['gpt-5.6-sol', 'gpt-6-astra', 'gpt-5.6-terra'],
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-terra',
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, true);
83
- assert.deepEqual(selection.nativeSpawnArguments, { model: 'gpt-5.6-sol' });
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 uses a confirmed host alternative', () => {
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-sol'],
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, true);
125
- assert.equal(selection.selectionSource, 'confirmed-host-alternative');
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, { model: 'gpt-6-astra' });
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, { model: 'gpt-5.6-luna' });
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 real models', () => {
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.equal(selection.failure, 'distinct-models-unavailable');
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-sol'],
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-sol'],
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-sol',
371
- 'gpt-5.6-sol',
372
- 'gpt-5.6-sol',
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
+ });