claude-dev-env 7.7.0 → 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/bin/select-pstack-models.test.mjs +49 -0
- package/bin/subagent-model-policy.test.mjs +56 -19
- package/docs/subagent-model-routing.md +21 -2
- package/hooks/routing/subagent_model_routing.test.mjs +65 -28
- package/package.json +1 -1
- package/rules/subagent-model-policy.json +1 -1
|
@@ -21,8 +21,10 @@ every rule below.
|
|
|
21
21
|
Before returning, the resumed agent runs the code-rules gate with the same bare
|
|
22
22
|
call every other surface in this skill uses:
|
|
23
23
|
|
|
24
|
-
1. Run
|
|
25
|
-
root>` with no file paths and no `--only-under` prefix.
|
|
24
|
+
1. Run `python ~/.claude/_shared/pr-loop/scripts/code_rules_gate.py --repo-root
|
|
25
|
+
<repo root>` with no file paths and no `--only-under` prefix. The `python`
|
|
26
|
+
prefix is required. On Windows a bare `.py` path opens the file in Cursor,
|
|
27
|
+
which then crashes on the gate flags.
|
|
26
28
|
2. If the gate reports violations **on lines this fix already owns** (the files
|
|
27
29
|
and added lines the fix changed), fix them and re-run the exact same command.
|
|
28
30
|
3. A violation on a path or added line outside this fix's own work is reported
|
|
@@ -48,8 +48,10 @@ no advisor call reopens that.
|
|
|
48
48
|
## Required checks
|
|
49
49
|
|
|
50
50
|
"Run required checks" means: run
|
|
51
|
-
|
|
52
|
-
with no file paths and no `--only-under` prefix.
|
|
51
|
+
`python ~/.claude/_shared/pr-loop/scripts/code_rules_gate.py --repo-root <repo root>`
|
|
52
|
+
with no file paths and no `--only-under` prefix. The `python` prefix is
|
|
53
|
+
required. On Windows a bare `.py` path opens the file in Cursor, which then
|
|
54
|
+
crashes on the gate flags. On a violation on lines this
|
|
53
55
|
round already owns, fix it and re-run the exact same command until clean. A
|
|
54
56
|
violation outside the review target is reported or skipped, not force-fixed into
|
|
55
57
|
unrelated files, and the skip is logged with path and reason.
|
|
@@ -111,6 +111,38 @@ test('Codex preferences skip an excluded pair in a panel', () => {
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
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
|
+
|
|
114
146
|
test('a foreign host reads only its own saved preferences', () => {
|
|
115
147
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-host-'));
|
|
116
148
|
try {
|
|
@@ -563,6 +595,23 @@ test('a model-only preference holds when the host supplies no usable effort', ()
|
|
|
563
595
|
}
|
|
564
596
|
});
|
|
565
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
|
+
|
|
566
615
|
test('non-Codex inheritance keeps its ordered parent fallback', () => {
|
|
567
616
|
const preferencesDirectory = mkdtempSync(join(tmpdir(), 'pstack-routing-parent-'));
|
|
568
617
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
4
|
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
@@ -35,6 +35,7 @@ const replacements = [
|
|
|
35
35
|
[['Luna', 'low'], ['gpt-5.6-luna', 'high']],
|
|
36
36
|
[['Luna', 'medium'], ['gpt-5.6-luna', 'high']],
|
|
37
37
|
[['Sol', 'low'], ['gpt-5.6-luna', 'xhigh']],
|
|
38
|
+
[['Sol', 'medium'], ['gpt-5.6-luna', 'max']],
|
|
38
39
|
[['Terra', 'low'], ['gpt-5.6-luna', 'xhigh']],
|
|
39
40
|
[['Terra', 'medium'], ['gpt-5.6-luna', 'xhigh']],
|
|
40
41
|
[['Terra', 'high'], ['gpt-5.6-luna', 'xhigh']],
|
|
@@ -59,7 +60,6 @@ test('every approved pair returns its literal native pair', () => {
|
|
|
59
60
|
[['Luna', 'high'], ['gpt-5.6-luna', 'high']],
|
|
60
61
|
[['Luna', 'xhigh'], ['gpt-5.6-luna', 'xhigh']],
|
|
61
62
|
[['Luna', 'max'], ['gpt-5.6-luna', 'max']],
|
|
62
|
-
[['Sol', 'medium'], ['gpt-5.6-sol', 'medium']],
|
|
63
63
|
[['Astra', 'low'], ['gpt-6-astra', 'low']],
|
|
64
64
|
[['Astra', 'medium'], ['gpt-6-astra', 'medium']],
|
|
65
65
|
];
|
|
@@ -67,6 +67,9 @@ test('every approved pair returns its literal native pair', () => {
|
|
|
67
67
|
const result = route({ model, reasoning_effort: effort });
|
|
68
68
|
assert.ok(['pass', 'remapped'].includes(result.status));
|
|
69
69
|
assert.deepEqual(result.selected, { model: expectedModel, effort: expectedEffort });
|
|
70
|
+
const nativeRoute = route({ model: expectedModel, reasoning_effort: expectedEffort });
|
|
71
|
+
assert.equal(nativeRoute.status, 'pass');
|
|
72
|
+
assert.deepEqual(nativeRoute.selected, { model: expectedModel, effort: expectedEffort });
|
|
70
73
|
}
|
|
71
74
|
const advisor = advisorRoute({
|
|
72
75
|
model: 'Astra',
|
|
@@ -77,12 +80,12 @@ test('every approved pair returns its literal native pair', () => {
|
|
|
77
80
|
assert.deepEqual(advisor.selected, { model: 'gpt-6-astra', effort: 'high' });
|
|
78
81
|
});
|
|
79
82
|
|
|
80
|
-
test('selector exclusions
|
|
83
|
+
test('selector exclusions keep Sol Medium out while direct routing remaps', () => {
|
|
81
84
|
const policy = loadSubagentModelPolicy();
|
|
82
85
|
assert.equal(policy.selectorExclusions.has('sol/medium'), true);
|
|
83
86
|
const result = route({ model: 'Sol', reasoning_effort: 'medium' });
|
|
84
|
-
assert.
|
|
85
|
-
assert.deepEqual(result.selected, { model: 'gpt-5.6-
|
|
87
|
+
assert.equal(result.status, 'remapped');
|
|
88
|
+
assert.deepEqual(result.selected, { model: 'gpt-5.6-luna', effort: 'max' });
|
|
86
89
|
|
|
87
90
|
const rawPolicy = JSON.parse(readFileSync(POLICY_SOURCE, 'utf8'));
|
|
88
91
|
rawPolicy.selectorExclusions = [{ model: 'missing', effort: 'medium' }];
|
|
@@ -174,6 +177,25 @@ test('the advisor bridge blocks malformed input', () => {
|
|
|
174
177
|
);
|
|
175
178
|
});
|
|
176
179
|
|
|
180
|
+
test('the advisor bridge routes Sol Medium to Luna Max', () => {
|
|
181
|
+
const bridgePath = join(PACKAGE_ROOT, 'scripts', 'resolve_advisor_model_route.mjs');
|
|
182
|
+
const bridgeExecution = spawnSync(
|
|
183
|
+
process.execPath,
|
|
184
|
+
[bridgePath],
|
|
185
|
+
{
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
input: JSON.stringify({ model: 'Sol', reasoning_effort: 'medium' }),
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
assert.equal(bridgeExecution.status, 0);
|
|
191
|
+
assert.equal(bridgeExecution.stderr, '');
|
|
192
|
+
const advisorDecision = JSON.parse(bridgeExecution.stdout);
|
|
193
|
+
assert.equal(advisorDecision.status, 'remapped');
|
|
194
|
+
assert.equal(advisorDecision.role, 'advisor');
|
|
195
|
+
assert.equal(advisorDecision.diagnostic, null);
|
|
196
|
+
assert.deepEqual(advisorDecision.selected, { model: 'gpt-5.6-luna', effort: 'max' });
|
|
197
|
+
});
|
|
198
|
+
|
|
177
199
|
test('missing worker settings preserve parent inheritance', () => {
|
|
178
200
|
const input = { message: 'keep', agent_type: 'worker' };
|
|
179
201
|
const result = routeSubagentToolInput(input);
|
|
@@ -181,6 +203,15 @@ test('missing worker settings preserve parent inheritance', () => {
|
|
|
181
203
|
assert.strictEqual(result.updatedInput, input);
|
|
182
204
|
});
|
|
183
205
|
|
|
206
|
+
test('Sol without an effort stays blocked after its only approved pair is removed', () => {
|
|
207
|
+
const result = route(
|
|
208
|
+
{ model: 'Sol' },
|
|
209
|
+
{ allowMissingEffort: true },
|
|
210
|
+
);
|
|
211
|
+
assert.equal(result.status, 'blocked');
|
|
212
|
+
assert.equal(result.diagnostic, 'effort is required for this model');
|
|
213
|
+
});
|
|
214
|
+
|
|
184
215
|
test('unknown input holds with a short diagnostic', () => {
|
|
185
216
|
assert.match(route({ model: 'Unknown', reasoning_effort: 'medium' }).diagnostic, /model is unknown/);
|
|
186
217
|
assert.match(route({ model: 'Terra', reasoning_effort: 'future' }).diagnostic, /effort is unknown/);
|
|
@@ -201,16 +232,16 @@ test('native values with case or whitespace differences are canonicalized', () =
|
|
|
201
232
|
});
|
|
202
233
|
assert.equal(result.status, 'remapped');
|
|
203
234
|
assert.deepEqual(result.updatedInput, {
|
|
204
|
-
model: 'gpt-5.6-
|
|
205
|
-
reasoning_effort: '
|
|
235
|
+
model: 'gpt-5.6-luna',
|
|
236
|
+
reasoning_effort: 'max',
|
|
206
237
|
message: 'keep',
|
|
207
238
|
});
|
|
208
239
|
});
|
|
209
240
|
|
|
210
241
|
test('unavailable replacements hold the spawn', () => {
|
|
211
242
|
const result = route(
|
|
212
|
-
{ model: '
|
|
213
|
-
{ availableModelIds: ['gpt-5.6-
|
|
243
|
+
{ model: 'Sol', reasoning_effort: 'medium' },
|
|
244
|
+
{ availableModelIds: ['gpt-5.6-sol'] },
|
|
214
245
|
);
|
|
215
246
|
assert.equal(result.status, 'blocked');
|
|
216
247
|
assert.match(result.diagnostic, /replacement model is unavailable/);
|
|
@@ -246,7 +277,7 @@ test('the tool route copies every non-routing field', () => {
|
|
|
246
277
|
|
|
247
278
|
test('a second route pass is idempotent', () => {
|
|
248
279
|
const first = routeSubagentToolInput(
|
|
249
|
-
{ model: '
|
|
280
|
+
{ model: 'Sol', reasoning_effort: 'medium', message: 'keep' },
|
|
250
281
|
{},
|
|
251
282
|
);
|
|
252
283
|
const second = routeSubagentToolInput(first.updatedInput);
|
|
@@ -261,17 +292,22 @@ test('a temporary policy copy changes the next route without source edits', () =
|
|
|
261
292
|
const temporaryPolicy = join(temporaryRoot, 'policy.json');
|
|
262
293
|
const sourceText = readFileSync(POLICY_SOURCE, 'utf8');
|
|
263
294
|
const policy = JSON.parse(sourceText);
|
|
295
|
+
writeFileSync(temporaryPolicy, sourceText);
|
|
296
|
+
assert.deepEqual(route(
|
|
297
|
+
{ model: 'Sol', reasoning_effort: 'medium' },
|
|
298
|
+
{ policyPath: temporaryPolicy },
|
|
299
|
+
).selected, { model: 'gpt-5.6-luna', effort: 'max' });
|
|
264
300
|
policy.replacements = policy.replacements.map(replacement => (
|
|
265
|
-
replacement.requested.model === '
|
|
266
|
-
? { ...replacement, selected: { model: 'luna', effort: '
|
|
301
|
+
replacement.requested.model === 'sol' && replacement.requested.effort === 'medium'
|
|
302
|
+
? { ...replacement, selected: { model: 'luna', effort: 'xhigh' } }
|
|
267
303
|
: replacement
|
|
268
304
|
));
|
|
269
305
|
writeFileSync(temporaryPolicy, JSON.stringify(policy));
|
|
270
|
-
const
|
|
271
|
-
{ model: '
|
|
306
|
+
const updatedRoute = route(
|
|
307
|
+
{ model: 'Sol', reasoning_effort: 'medium' },
|
|
272
308
|
{ policyPath: temporaryPolicy },
|
|
273
309
|
);
|
|
274
|
-
assert.deepEqual(
|
|
310
|
+
assert.deepEqual(updatedRoute.selected, { model: 'gpt-5.6-luna', effort: 'xhigh' });
|
|
275
311
|
assert.equal(readFileSync(POLICY_SOURCE, 'utf8'), sourceText);
|
|
276
312
|
} finally {
|
|
277
313
|
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
@@ -347,10 +383,11 @@ test('model names cannot collide after normalization', () => {
|
|
|
347
383
|
|
|
348
384
|
test('combined role routes must end at an approved terminal pair', () => {
|
|
349
385
|
const policy = JSON.parse(readFileSync(POLICY_SOURCE, 'utf8'));
|
|
350
|
-
policy.replacements.
|
|
351
|
-
requested
|
|
352
|
-
|
|
353
|
-
|
|
386
|
+
const solMediumReplacement = policy.replacements.find(replacement => (
|
|
387
|
+
replacement.requested.model === 'sol'
|
|
388
|
+
&& replacement.requested.effort === 'medium'
|
|
389
|
+
));
|
|
390
|
+
solMediumReplacement.selected = { model: 'astra', effort: 'low' };
|
|
354
391
|
policy.roleReplacements.push({
|
|
355
392
|
role: 'worker',
|
|
356
393
|
requested: { model: 'astra', effort: 'low' },
|
|
@@ -32,8 +32,11 @@ example, change the `terra` and `medium` entry in `replacements` to:
|
|
|
32
32
|
|
|
33
33
|
The next routing call reads the file again. The resolver source stays unchanged.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Selector exclusions keep a pair out of panels. They do not change direct route
|
|
36
|
+
decisions. The shipped policy keeps Sol Medium out of panels and routes direct
|
|
37
|
+
requests for Sol Medium to Luna Max through `replacements`. Sol Medium is absent
|
|
38
|
+
from `approvedPairs`. The selector drops the requested pair before routing it,
|
|
39
|
+
so its Luna replacement does not enter the panel through that preference.
|
|
37
40
|
|
|
38
41
|
```json
|
|
39
42
|
"selectorExclusions": [
|
|
@@ -41,9 +44,25 @@ does not block the shared resolver or the spawn hook. For example:
|
|
|
41
44
|
]
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
The matching replacement is:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"requested": {"model": "sol", "effort": "medium"},
|
|
52
|
+
"selected": {"model": "luna", "effort": "max"}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
44
56
|
Unknown values, invalid policy data, and unavailable replacement models stop the
|
|
45
57
|
spawn with a short diagnostic. Parent inheritance stays unchanged.
|
|
46
58
|
|
|
59
|
+
The direct spawn hook and advisor bridge use the same shared resolver. A Sol
|
|
60
|
+
Medium remap changes only the model and effort fields in the hook's tool input.
|
|
61
|
+
The hook returns an allow decision with the updated input and emits no remap
|
|
62
|
+
notice. Luna High, Xhigh, and Max and Astra Low and Medium keep their routes.
|
|
63
|
+
Trusted advisors also keep Astra High. A worker request for Sol without an
|
|
64
|
+
effort stops because Sol has no approved pair to supply a default effort.
|
|
65
|
+
|
|
47
66
|
The hook blocks advisor role claims because the current Codex hook input has no
|
|
48
67
|
host-provided role binding. The Python advisor bridge passes trusted session
|
|
49
68
|
metadata to the shared resolver. The native advisor hook path stays pending
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
3
|
import { test } from 'node:test';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -11,22 +11,22 @@ function buildHookPayload(toolInput, metadata = {}) {
|
|
|
11
11
|
return { tool_name: targetToolName, tool_input: toolInput, ...metadata };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
test('remaps
|
|
14
|
+
test('remaps Sol Medium to Luna Max', () => {
|
|
15
15
|
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
16
|
-
model: '
|
|
16
|
+
model: 'Sol',
|
|
17
17
|
reasoning_effort: 'Medium',
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
20
|
assert.equal(hookResponse.hookSpecificOutput.updatedInput.model, 'gpt-5.6-luna');
|
|
21
21
|
assert.equal(
|
|
22
22
|
hookResponse.hookSpecificOutput.updatedInput.reasoning_effort,
|
|
23
|
-
'
|
|
23
|
+
'max',
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
test('emits only the allow response shape after a remap', () => {
|
|
28
28
|
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
29
|
-
model: '
|
|
29
|
+
model: 'Sol',
|
|
30
30
|
reasoning_effort: 'medium',
|
|
31
31
|
}));
|
|
32
32
|
|
|
@@ -41,7 +41,7 @@ test('emits only the allow response shape after a remap', () => {
|
|
|
41
41
|
permissionDecision: 'allow',
|
|
42
42
|
updatedInput: {
|
|
43
43
|
model: 'gpt-5.6-luna',
|
|
44
|
-
reasoning_effort: '
|
|
44
|
+
reasoning_effort: 'max',
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
47
|
});
|
|
@@ -52,7 +52,7 @@ test('preserves every non-routing tool field', () => {
|
|
|
52
52
|
message: 'do the work',
|
|
53
53
|
child_permissions: { shell: 'ask' },
|
|
54
54
|
output_contract: ['summary'],
|
|
55
|
-
model: '
|
|
55
|
+
model: 'Sol',
|
|
56
56
|
reasoning_effort: 'medium',
|
|
57
57
|
};
|
|
58
58
|
|
|
@@ -64,14 +64,14 @@ test('preserves every non-routing tool field', () => {
|
|
|
64
64
|
child_permissions: { shell: 'ask' },
|
|
65
65
|
output_contract: ['summary'],
|
|
66
66
|
model: 'gpt-5.6-luna',
|
|
67
|
-
reasoning_effort: '
|
|
67
|
+
reasoning_effort: 'max',
|
|
68
68
|
});
|
|
69
69
|
assert.deepEqual(toolInput, {
|
|
70
70
|
assignment: 'preserve',
|
|
71
71
|
message: 'do the work',
|
|
72
72
|
child_permissions: { shell: 'ask' },
|
|
73
73
|
output_contract: ['summary'],
|
|
74
|
-
model: '
|
|
74
|
+
model: 'Sol',
|
|
75
75
|
reasoning_effort: 'medium',
|
|
76
76
|
});
|
|
77
77
|
});
|
|
@@ -94,8 +94,8 @@ test('blocks malformed policy, unknown input, unavailable replacement, and untru
|
|
|
94
94
|
buildHookPayload({ model: 'Unknown', reasoning_effort: 'medium' }),
|
|
95
95
|
);
|
|
96
96
|
const unavailableReplacementResponse = buildSubagentModelRoutingResponse(
|
|
97
|
-
buildHookPayload({ model: '
|
|
98
|
-
{ availableModelIds: ['gpt-5.6-
|
|
97
|
+
buildHookPayload({ model: 'Sol', reasoning_effort: 'medium' }),
|
|
98
|
+
{ availableModelIds: ['gpt-5.6-sol'] },
|
|
99
99
|
);
|
|
100
100
|
const untrustedAdvisorResponse = buildSubagentModelRoutingResponse(
|
|
101
101
|
buildHookPayload({
|
|
@@ -122,25 +122,62 @@ test('blocks malformed policy, unknown input, unavailable replacement, and untru
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
for (const [eachModel, eachEffort, eachSelectedModel, eachSelectedEffort] of [
|
|
126
|
+
['Sol', 'medium', 'gpt-5.6-luna', 'max'],
|
|
127
|
+
['Astra', 'high', 'gpt-6-astra', 'high'],
|
|
128
|
+
]) {
|
|
129
|
+
test(`allows a trusted session advisor requesting ${eachModel} ${eachEffort}`, () => {
|
|
130
|
+
const hookResponse = buildSubagentModelRoutingResponse(
|
|
131
|
+
buildHookPayload({
|
|
132
|
+
agent_type: 'session-advisor',
|
|
133
|
+
model: eachModel,
|
|
134
|
+
reasoning_effort: eachEffort,
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
trustedSessionMetadata: {
|
|
138
|
+
authorized: true,
|
|
139
|
+
registeredAgentType: 'session-advisor',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(hookResponse.hookSpecificOutput.updatedInput, {
|
|
128
145
|
agent_type: 'session-advisor',
|
|
129
|
-
model:
|
|
130
|
-
reasoning_effort:
|
|
131
|
-
})
|
|
146
|
+
model: eachSelectedModel,
|
|
147
|
+
reasoning_effort: eachSelectedEffort,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test('the standalone hook silently remaps native Sol Medium and preserves other fields', () => {
|
|
153
|
+
const hookExecution = spawnSync(
|
|
154
|
+
process.execPath,
|
|
155
|
+
[fileURLToPath(new URL('./subagent_model_routing.mjs', import.meta.url))],
|
|
132
156
|
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
input: JSON.stringify(buildHookPayload({
|
|
159
|
+
model: 'gpt-5.6-sol',
|
|
160
|
+
effort: 'medium',
|
|
161
|
+
message: 'keep',
|
|
162
|
+
child_permissions: { shell: 'ask' },
|
|
163
|
+
output_contract: ['summary'],
|
|
164
|
+
})),
|
|
137
165
|
},
|
|
138
166
|
);
|
|
139
|
-
|
|
140
|
-
assert.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
assert.equal(hookExecution.status, 0);
|
|
168
|
+
assert.equal(hookExecution.stderr, '');
|
|
169
|
+
assert.deepEqual(JSON.parse(hookExecution.stdout), {
|
|
170
|
+
hookSpecificOutput: {
|
|
171
|
+
hookEventName: 'PreToolUse',
|
|
172
|
+
permissionDecision: 'allow',
|
|
173
|
+
updatedInput: {
|
|
174
|
+
model: 'gpt-5.6-luna',
|
|
175
|
+
effort: 'max',
|
|
176
|
+
message: 'keep',
|
|
177
|
+
child_permissions: { shell: 'ask' },
|
|
178
|
+
output_contract: ['summary'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
144
181
|
});
|
|
145
182
|
});
|
|
146
183
|
|
|
@@ -201,7 +238,7 @@ test('the standalone hook command blocks an advisor without host metadata', () =
|
|
|
201
238
|
|
|
202
239
|
test('keeps a second routing pass unchanged', () => {
|
|
203
240
|
const firstResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
204
|
-
model: '
|
|
241
|
+
model: 'Sol',
|
|
205
242
|
reasoning_effort: 'medium',
|
|
206
243
|
message: 'keep',
|
|
207
244
|
}));
|
|
@@ -212,7 +249,7 @@ test('keeps a second routing pass unchanged', () => {
|
|
|
212
249
|
assert.deepEqual(secondResponse, {});
|
|
213
250
|
assert.deepEqual(firstResponse.hookSpecificOutput.updatedInput, {
|
|
214
251
|
model: 'gpt-5.6-luna',
|
|
215
|
-
reasoning_effort: '
|
|
252
|
+
reasoning_effort: 'max',
|
|
216
253
|
message: 'keep',
|
|
217
254
|
});
|
|
218
255
|
});
|
package/package.json
CHANGED
|
@@ -53,7 +53,6 @@
|
|
|
53
53
|
{"model": "luna", "effort": "high"},
|
|
54
54
|
{"model": "luna", "effort": "xhigh"},
|
|
55
55
|
{"model": "luna", "effort": "max"},
|
|
56
|
-
{"model": "sol", "effort": "medium"},
|
|
57
56
|
{"model": "astra", "effort": "low"},
|
|
58
57
|
{"model": "astra", "effort": "medium"},
|
|
59
58
|
{"model": "astra", "effort": "high", "roles": ["advisor"]}
|
|
@@ -66,6 +65,7 @@
|
|
|
66
65
|
{"requested": {"model": "luna", "effort": "low"}, "selected": {"model": "luna", "effort": "high"}},
|
|
67
66
|
{"requested": {"model": "luna", "effort": "medium"}, "selected": {"model": "luna", "effort": "high"}},
|
|
68
67
|
{"requested": {"model": "sol", "effort": "low"}, "selected": {"model": "luna", "effort": "xhigh"}},
|
|
68
|
+
{"requested": {"model": "sol", "effort": "medium"}, "selected": {"model": "luna", "effort": "max"}},
|
|
69
69
|
{"requested": {"model": "terra", "effort": "low"}, "selected": {"model": "luna", "effort": "xhigh"}},
|
|
70
70
|
{"requested": {"model": "terra", "effort": "medium"}, "selected": {"model": "luna", "effort": "xhigh"}},
|
|
71
71
|
{"requested": {"model": "terra", "effort": "high"}, "selected": {"model": "luna", "effort": "xhigh"}},
|