claude-dev-env 7.6.0 → 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 (35) hide show
  1. package/.agents/skills/session-continuity/README.md +19 -9
  2. package/AGENTS.md +48 -71
  3. package/_shared/advisor/advisor-protocol.md +2 -2
  4. package/_shared/advisor/reference/advisor-block.md +1 -1
  5. package/_shared/advisor/reference/astra-rung.md +2 -2
  6. package/_shared/advisor/scripts/codex_astra_advisor.py +122 -23
  7. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +76 -9
  8. package/_shared/advisor/scripts/tests/test_advisor_route_constants.py +51 -0
  9. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +52 -2
  10. package/bin/install-constants.mjs +3 -0
  11. package/bin/install-session-continuity.mjs +73 -30
  12. package/bin/install-session-continuity.test.mjs +52 -1
  13. package/bin/install-transaction.mjs +41 -44
  14. package/bin/install.cursor-rules.test.mjs +311 -3
  15. package/bin/install.mjs +226 -95
  16. package/bin/install.test.mjs +57 -0
  17. package/bin/install.transaction.test.mjs +61 -1
  18. package/bin/pstack-model-preferences.codex.json +1 -0
  19. package/bin/pstack.mjs +28 -6
  20. package/bin/pstack.test.mjs +48 -0
  21. package/bin/select-pstack-models.test.mjs +375 -26
  22. package/bin/subagent-model-policy.test.mjs +399 -0
  23. package/docs/codex-compatibility.md +19 -5
  24. package/docs/subagent-model-routing.md +50 -0
  25. package/hooks/hooks.json +10 -0
  26. package/hooks/routing/subagent_model_routing.mjs +81 -0
  27. package/hooks/routing/subagent_model_routing.test.mjs +218 -0
  28. package/package.json +1 -1
  29. package/rules/pstack-models.md +22 -0
  30. package/rules/subagent-model-policy.json +85 -0
  31. package/scripts/resolve_advisor_model_route.mjs +52 -0
  32. package/scripts/select_pstack_models.mjs +203 -38
  33. package/scripts/subagent_model_policy.mjs +620 -0
  34. package/scripts/tests/resolve_advisor_model_route.test.mjs +51 -0
  35. package/scripts/tests/test_codex_compat_materializer.py +15 -0
@@ -1,8 +1,13 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import {
5
+ defaultSubagentModelPolicyPath,
6
+ isSelectorPairExcluded,
7
+ loadSubagentModelPolicy,
8
+ resolveSubagentModelRoute,
9
+ } from './subagent_model_policy.mjs';
4
10
 
5
- const inheritanceAliases = new Set(['inherit-parent', 'auto']);
6
11
  const hostNamePattern = /^[a-z0-9][a-z0-9_-]*$/;
7
12
  const defaultSingleDelegationPanel = Object.freeze({
8
13
  agentCount: 1,
@@ -42,10 +47,20 @@ export function selectPstackDelegation(input) {
42
47
  validateSelectionInput(input);
43
48
  const panel = resolvePanel(input);
44
49
  validateDelegationIndex(input.delegationIndex, panel.agentCount);
45
- const modelsByRole = readHostPreferences(input);
46
- const allCandidates = collectCandidates(input, modelsByRole[input.role] ?? []);
50
+ const hostPreferences = readHostPreferences(input);
51
+ const routingInput = (input.requestedEffort === undefined || input.requestedEffort === null)
52
+ && hostPreferences.defaultEffort !== undefined
53
+ ? { ...input, requestedEffort: hostPreferences.defaultEffort }
54
+ : input;
55
+ const selectionPolicy = loadSelectionPolicy(input);
56
+ if (selectionPolicy.policy) validateModelIdInventories(input, selectionPolicy.policy);
57
+ const { allCandidates, routingFailures } = collectCandidates(
58
+ routingInput,
59
+ hostPreferences.modelsByRole[routingInput.role] ?? [],
60
+ selectionPolicy,
61
+ );
47
62
  const allSelectedCandidates = selectPanelCandidates(allCandidates, panel);
48
- return buildSelection(input, panel, allSelectedCandidates);
63
+ return buildSelection(routingInput, panel, allSelectedCandidates, routingFailures);
49
64
  }
50
65
 
51
66
  function validateSelectionInput(input) {
@@ -56,7 +71,10 @@ function validateSelectionInput(input) {
56
71
  validateDelegationIndex(input.delegationIndex);
57
72
  requireStringArray(input.availableModelIds, 'available model ids');
58
73
  requireStringArray(input.confirmedSuitableModelIds, 'confirmed suitable model ids');
59
- validateRealModelIds(input.availableModelIds, 'available model ids');
74
+ if (input.requestedEffort !== undefined && input.requestedEffort !== null
75
+ && (typeof input.requestedEffort !== 'string' || input.requestedEffort.trim() === '')) {
76
+ throw new Error('requested effort must be a non-empty string');
77
+ }
60
78
  validateParentFallback(input.parentFallback);
61
79
  validateConfirmedModels(input);
62
80
  if (typeof input.role !== 'string' || !Object.hasOwn(allPstackRoleRequirements, input.role)) {
@@ -90,12 +108,6 @@ function requireStringArray(allEntries, label) {
90
108
  }
91
109
  }
92
110
 
93
- function validateRealModelIds(allModelIds, label) {
94
- if (allModelIds.some(eachModelId => inheritanceAliases.has(eachModelId))) {
95
- throw new Error(label + ' cannot contain inheritance aliases');
96
- }
97
- }
98
-
99
111
  function validateParentFallback(parentFallback) {
100
112
  const hasBooleanFlags = parentFallback
101
113
  && typeof parentFallback === 'object'
@@ -107,7 +119,6 @@ function validateParentFallback(parentFallback) {
107
119
  }
108
120
 
109
121
  function validateConfirmedModels(input) {
110
- validateRealModelIds(input.confirmedSuitableModelIds, 'confirmed suitable model ids');
111
122
  const allAvailableModelIds = new Set(input.availableModelIds);
112
123
  if (input.confirmedSuitableModelIds.some(
113
124
  eachModelId => !allAvailableModelIds.has(eachModelId),
@@ -116,6 +127,18 @@ function validateConfirmedModels(input) {
116
127
  }
117
128
  }
118
129
 
130
+ function validateModelIdInventories(input, policy) {
131
+ const inventories = [
132
+ ['confirmed suitable model ids', input.confirmedSuitableModelIds],
133
+ ['available model ids', input.availableModelIds],
134
+ ];
135
+ for (const [label, modelIds] of inventories) {
136
+ if (modelIds.some(eachModelId => policy.inheritanceAliases.has(eachModelId.trim().toLowerCase()))) {
137
+ throw new Error(label + ' must contain native model ids');
138
+ }
139
+ }
140
+ }
141
+
119
142
  function resolvePanel(input) {
120
143
  const panel = input.panel
121
144
  ?? defaultPanelByPstackRole[input.role]
@@ -142,50 +165,182 @@ function readHostPreferences(input) {
142
165
  'pstack-model-preferences.' + input.host + '.json',
143
166
  );
144
167
  if (!existsSync(preferencesPath)) {
145
- return {};
168
+ return { modelsByRole: {}, defaultEffort: undefined };
146
169
  }
147
170
  const preferences = JSON.parse(readFileSync(preferencesPath, 'utf8'));
148
171
  if (preferences.host !== input.host) {
149
172
  throw new Error('preference host must match selected host');
150
173
  }
151
- return preferences.modelsByRole ?? {};
174
+ return {
175
+ modelsByRole: preferences.modelsByRole ?? {},
176
+ defaultEffort: preferences.defaultEffort,
177
+ };
152
178
  }
153
179
 
154
- function collectCandidates(input, allPreferredModelIds) {
155
- requireStringArray(allPreferredModelIds, 'role preferences');
156
- const allAvailableModelIds = new Set(input.availableModelIds);
157
- const allPreferredCandidates = allPreferredModelIds
158
- .map(eachModelId => preferenceCandidate(eachModelId, allAvailableModelIds, input))
180
+ function loadSelectionPolicy(input) {
181
+ try {
182
+ return { policy: loadSubagentModelPolicy(resolvePolicyPath(input)), error: null };
183
+ } catch (error) {
184
+ return {
185
+ policy: null,
186
+ error: error instanceof Error ? error.message : String(error),
187
+ };
188
+ }
189
+ }
190
+
191
+ function collectCandidates(input, allPreferredModelEntries, selectionPolicy) {
192
+ validatePreferenceEntries(allPreferredModelEntries, 'role preferences');
193
+ if (!selectionPolicy.policy) {
194
+ return { allCandidates: [], routingFailures: [selectionPolicy.error] };
195
+ }
196
+ const routingFailures = [];
197
+ const allPreferredCandidates = allPreferredModelEntries
198
+ .map(eachEntry => preferenceCandidate(
199
+ eachEntry,
200
+ input,
201
+ 'host-preference',
202
+ routingFailures,
203
+ selectionPolicy.policy,
204
+ ))
205
+ .filter(Boolean);
206
+ const allAlternativeCandidates = input.confirmedSuitableModelIds
207
+ .map(eachModelId => preferenceCandidate(
208
+ eachModelId,
209
+ input,
210
+ 'confirmed-host-alternative',
211
+ routingFailures,
212
+ selectionPolicy.policy,
213
+ ))
159
214
  .filter(Boolean);
160
- const allAlternativeCandidates = input.confirmedSuitableModelIds.map(
161
- eachModelId => modelCandidate(eachModelId, 'confirmed-host-alternative'),
162
- );
163
215
  const allCandidates = deduplicateCandidates([
164
216
  ...allPreferredCandidates,
165
217
  ...allAlternativeCandidates,
166
218
  ]);
167
- const allCandidatesWithFallback = allCandidates.length === 0
219
+ const allCandidatesWithFallback = allCandidates.length === 0 && routingFailures.length === 0
168
220
  && input.parentFallback.isAllowed
169
221
  ? [parentCandidate()]
170
222
  : allCandidates;
171
- return orderCrossJudgeCandidates(input, allCandidatesWithFallback);
223
+ return {
224
+ allCandidates: orderCrossJudgeCandidates(input, allCandidatesWithFallback),
225
+ routingFailures,
226
+ };
172
227
  }
173
228
 
174
- function preferenceCandidate(modelId, allAvailableModelIds, input) {
175
- if (inheritanceAliases.has(modelId)) {
229
+ function preferenceCandidate(
230
+ preferenceEntry,
231
+ input,
232
+ source,
233
+ routingFailures,
234
+ policy,
235
+ ) {
236
+ const resolvedEntry = resolvePreferenceEntry(preferenceEntry, input, policy);
237
+ if (resolvedEntry.error) {
238
+ routingFailures.push(resolvedEntry.error);
239
+ return null;
240
+ }
241
+ const { modelId, effort } = resolvedEntry;
242
+ if (isSelectorPairExcluded(modelId, effort, policy)) return null;
243
+ if (policy.inheritanceAliases.has(modelId.trim().toLowerCase())) {
244
+ if (effort !== null) {
245
+ routingFailures.push('parent inheritance cannot set an effort');
246
+ return null;
247
+ }
176
248
  return input.parentFallback.isAllowed ? parentCandidate() : null;
177
249
  }
178
- return allAvailableModelIds.has(modelId)
179
- ? modelCandidate(modelId, 'host-preference')
180
- : null;
250
+ if (input.host !== 'codex') {
251
+ return input.availableModelIds.includes(modelId)
252
+ ? modelCandidate(modelId, source)
253
+ : null;
254
+ }
255
+ const routeRequest = { model: modelId, agent_type: input.routingRole ?? 'worker' };
256
+ if (effort !== null) routeRequest.reasoning_effort = effort;
257
+ const route = resolveSubagentModelRoute(
258
+ routeRequest,
259
+ {
260
+ policyPath: resolvePolicyPath(input),
261
+ policy,
262
+ availableModelIds: input.availableModelIds,
263
+ allowMissingEffort: effort === null,
264
+ trustedSessionMetadata: input.trustedSessionMetadata,
265
+ },
266
+ );
267
+ if (route.status === 'blocked') {
268
+ routingFailures.push(route.diagnostic);
269
+ return null;
270
+ }
271
+ if (isSelectorPairExcluded(route.selected.model, route.selected.effort, policy)) return null;
272
+ if (route.status === 'inherited') return input.parentFallback.isAllowed ? parentCandidate() : null;
273
+ return modelCandidate(route.selected.model, source, route);
181
274
  }
182
275
 
183
- function modelCandidate(modelId, source) {
184
- return { kind: 'model', modelId, source };
276
+ function modelCandidate(modelId, source, route = null) {
277
+ return {
278
+ kind: 'model',
279
+ modelId,
280
+ source,
281
+ requestedPair: route?.requested ?? null,
282
+ selectedPair: route?.selected ?? null,
283
+ effort: route?.selected?.effort ?? null,
284
+ };
185
285
  }
186
286
 
187
287
  function parentCandidate() {
188
- return { kind: 'parent', modelId: null, source: 'parent-inheritance' };
288
+ return {
289
+ kind: 'parent',
290
+ modelId: null,
291
+ source: 'parent-inheritance',
292
+ requestedPair: null,
293
+ selectedPair: null,
294
+ effort: null,
295
+ };
296
+ }
297
+
298
+ function resolvePreferenceEntry(preferenceEntry, input, policy = null) {
299
+ const preferenceModel = typeof preferenceEntry === 'string'
300
+ ? preferenceEntry
301
+ : preferenceEntry?.modelId ?? preferenceEntry?.model;
302
+ const isInheritanceAlias = typeof preferenceModel === 'string'
303
+ && policy?.inheritanceAliases.has(preferenceModel.trim().toLowerCase());
304
+ const fallbackEffort = isInheritanceAlias ? null : input.requestedEffort ?? null;
305
+ if (typeof preferenceEntry === 'string' && preferenceEntry !== '') {
306
+ return { modelId: preferenceEntry, effort: fallbackEffort, error: null };
307
+ }
308
+ if (!preferenceEntry || typeof preferenceEntry !== 'object' || Array.isArray(preferenceEntry)) {
309
+ return { modelId: null, effort: null, error: 'role preferences must contain model ids or pair objects' };
310
+ }
311
+ const modelId = preferenceEntry.modelId ?? preferenceEntry.model;
312
+ const effortFields = ['reasoning_effort', 'effort'].filter(
313
+ field => Object.hasOwn(preferenceEntry, field),
314
+ );
315
+ if (effortFields.length > 1) {
316
+ return { modelId: null, effort: null, error: 'role preference effort fields are ambiguous' };
317
+ }
318
+ const effort = preferenceEntry[effortFields[0]] ?? fallbackEffort;
319
+ if (typeof modelId !== 'string' || modelId === '') {
320
+ return { modelId: null, effort: null, error: 'role preference model must be a non-empty string' };
321
+ }
322
+ return { modelId, effort, error: null };
323
+ }
324
+
325
+ function validatePreferenceEntries(allEntries, label) {
326
+ if (!Array.isArray(allEntries)) throw new Error(label + ' must be an array');
327
+ allEntries.forEach(eachEntry => {
328
+ const resolvedEntry = resolvePreferenceEntry(eachEntry, {});
329
+ if (resolvedEntry.error) throw new Error(resolvedEntry.error);
330
+ });
331
+ }
332
+
333
+ function resolvePolicyPath(input) {
334
+ if (typeof input.policyPath === 'string' && input.policyPath.trim() !== '') {
335
+ return input.policyPath;
336
+ }
337
+ const allPolicyPaths = [
338
+ input.preferencesDirectory
339
+ ? join(input.preferencesDirectory, 'subagent-model-policy.json')
340
+ : null,
341
+ defaultSubagentModelPolicyPath(),
342
+ ].filter(Boolean);
343
+ return allPolicyPaths.find(eachPath => existsSync(eachPath)) ?? allPolicyPaths[0];
189
344
  }
190
345
 
191
346
  function deduplicateCandidates(allCandidates) {
@@ -230,9 +385,9 @@ function selectPanelCandidates(allCandidates, panel) {
230
385
  );
231
386
  }
232
387
 
233
- function buildSelection(input, requestedPanel, allSelectedCandidates) {
388
+ function buildSelection(input, requestedPanel, allSelectedCandidates, routingFailures) {
234
389
  const panel = reportPanel(requestedPanel, allSelectedCandidates);
235
- const failure = selectionFailure(input, panel, allSelectedCandidates);
390
+ const failure = selectionFailure(input, panel, allSelectedCandidates, routingFailures);
236
391
  const selectedCandidate = allSelectedCandidates[input.delegationIndex];
237
392
  return {
238
393
  selectedHost: input.host,
@@ -244,6 +399,7 @@ function buildSelection(input, requestedPanel, allSelectedCandidates) {
244
399
  failure,
245
400
  selectionSource: selectedCandidate?.source ?? null,
246
401
  panel,
402
+ routingDiagnostics: routingFailures,
247
403
  nativeSpawnArguments: nativeSpawnArguments(selectedCandidate, failure),
248
404
  omitNativeModelArgument: selectedCandidate?.kind === 'parent',
249
405
  };
@@ -253,18 +409,23 @@ function reportPanel(requestedPanel, allSelectedCandidates) {
253
409
  const allSelectedModelIds = allSelectedCandidates.map(
254
410
  eachCandidate => eachCandidate.modelId,
255
411
  );
256
- const distinctRealModelCount = new Set(allSelectedModelIds.filter(Boolean)).size;
412
+ const distinctModelCount = new Set(allSelectedModelIds.filter(Boolean)).size;
257
413
  return {
258
414
  agentCount: requestedPanel.agentCount,
259
415
  requiresDistinctModels: requestedPanel.requiresDistinctModels,
260
416
  selectedModelIds: allSelectedModelIds,
417
+ requestedModelPairs: allSelectedCandidates.map(eachCandidate => eachCandidate.requestedPair),
418
+ selectedModelPairs: allSelectedCandidates.map(eachCandidate => eachCandidate.selectedPair),
261
419
  selectionSources: allSelectedCandidates.map(eachCandidate => eachCandidate.source),
262
420
  isCrossModelDiverse: requestedPanel.agentCount > 1
263
- && distinctRealModelCount === requestedPanel.agentCount,
421
+ && distinctModelCount === requestedPanel.agentCount,
264
422
  };
265
423
  }
266
424
 
267
- function selectionFailure(input, panel, allSelectedCandidates) {
425
+ function selectionFailure(input, panel, allSelectedCandidates, routingFailures) {
426
+ if (routingFailures.length > 0) {
427
+ return 'model-routing-failed: ' + routingFailures[0];
428
+ }
268
429
  if (panel.requiresDistinctModels && !panel.isCrossModelDiverse) {
269
430
  return 'distinct-models-unavailable';
270
431
  }
@@ -282,7 +443,11 @@ function nativeSpawnArguments(selectedCandidate, failure) {
282
443
  if (failure || !selectedCandidate || selectedCandidate.kind === 'parent') {
283
444
  return {};
284
445
  }
285
- return { model: selectedCandidate.modelId };
446
+ const spawnArguments = { model: selectedCandidate.modelId };
447
+ if (selectedCandidate.effort !== null) {
448
+ spawnArguments.reasoning_effort = selectedCandidate.effort;
449
+ }
450
+ return spawnArguments;
286
451
  }
287
452
 
288
453
  async function readSelectionInput() {