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