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
@@ -0,0 +1,218 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { test } from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { buildSubagentModelRoutingResponse } from './subagent_model_routing.mjs';
7
+
8
+ const targetToolName = 'multi_agent_v1__spawn_agent';
9
+
10
+ function buildHookPayload(toolInput, metadata = {}) {
11
+ return { tool_name: targetToolName, tool_input: toolInput, ...metadata };
12
+ }
13
+
14
+ test('remaps Terra Medium to Luna Xhigh', () => {
15
+ const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
16
+ model: 'Terra',
17
+ reasoning_effort: 'Medium',
18
+ }));
19
+
20
+ assert.equal(hookResponse.hookSpecificOutput.updatedInput.model, 'gpt-5.6-luna');
21
+ assert.equal(
22
+ hookResponse.hookSpecificOutput.updatedInput.reasoning_effort,
23
+ 'xhigh',
24
+ );
25
+ });
26
+
27
+ test('emits only the allow response shape after a remap', () => {
28
+ const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
29
+ model: 'Terra',
30
+ reasoning_effort: 'medium',
31
+ }));
32
+
33
+ assert.deepEqual(Object.keys(hookResponse), ['hookSpecificOutput']);
34
+ assert.deepEqual(Object.keys(hookResponse.hookSpecificOutput), [
35
+ 'hookEventName',
36
+ 'permissionDecision',
37
+ 'updatedInput',
38
+ ]);
39
+ assert.deepEqual(hookResponse.hookSpecificOutput, {
40
+ hookEventName: 'PreToolUse',
41
+ permissionDecision: 'allow',
42
+ updatedInput: {
43
+ model: 'gpt-5.6-luna',
44
+ reasoning_effort: 'xhigh',
45
+ },
46
+ });
47
+ });
48
+
49
+ test('preserves every non-routing tool field', () => {
50
+ const toolInput = {
51
+ assignment: 'preserve',
52
+ message: 'do the work',
53
+ child_permissions: { shell: 'ask' },
54
+ output_contract: ['summary'],
55
+ model: 'Terra',
56
+ reasoning_effort: 'medium',
57
+ };
58
+
59
+ const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload(toolInput));
60
+
61
+ assert.deepEqual(hookResponse.hookSpecificOutput.updatedInput, {
62
+ assignment: 'preserve',
63
+ message: 'do the work',
64
+ child_permissions: { shell: 'ask' },
65
+ output_contract: ['summary'],
66
+ model: 'gpt-5.6-luna',
67
+ reasoning_effort: 'xhigh',
68
+ });
69
+ assert.deepEqual(toolInput, {
70
+ assignment: 'preserve',
71
+ message: 'do the work',
72
+ child_permissions: { shell: 'ask' },
73
+ output_contract: ['summary'],
74
+ model: 'Terra',
75
+ reasoning_effort: 'medium',
76
+ });
77
+ });
78
+
79
+ test('passes unrelated tool calls through', () => {
80
+ const hookResponse = buildSubagentModelRoutingResponse({
81
+ tool_name: 'Task',
82
+ tool_input: { model: 'Terra', reasoning_effort: 'medium' },
83
+ });
84
+
85
+ assert.deepEqual(hookResponse, {});
86
+ });
87
+
88
+ test('blocks malformed policy, unknown input, unavailable replacement, and untrusted advisor roles', () => {
89
+ const malformedPolicyResponse = buildSubagentModelRoutingResponse(
90
+ buildHookPayload({ model: 'Terra', reasoning_effort: 'medium' }),
91
+ { policy: {} },
92
+ );
93
+ const unknownInputResponse = buildSubagentModelRoutingResponse(
94
+ buildHookPayload({ model: 'Unknown', reasoning_effort: 'medium' }),
95
+ );
96
+ const unavailableReplacementResponse = buildSubagentModelRoutingResponse(
97
+ buildHookPayload({ model: 'Terra', reasoning_effort: 'medium' }),
98
+ { availableModelIds: ['gpt-5.6-terra'] },
99
+ );
100
+ const untrustedAdvisorResponse = buildSubagentModelRoutingResponse(
101
+ buildHookPayload({
102
+ agent_type: 'advisor',
103
+ model: 'Astra',
104
+ reasoning_effort: 'medium',
105
+ }),
106
+ );
107
+
108
+ const allBlockedResponses = [
109
+ [malformedPolicyResponse, /policy/],
110
+ [unknownInputResponse, /model is unknown/],
111
+ [unavailableReplacementResponse, /replacement model is unavailable/],
112
+ [untrustedAdvisorResponse, /advisor role is not trusted/],
113
+ ];
114
+ for (const [eachBlockedResponse, eachExpectedDiagnostic] of allBlockedResponses) {
115
+ assert.equal(eachBlockedResponse.hookSpecificOutput.permissionDecision, 'deny');
116
+ assert.equal(eachBlockedResponse.hookSpecificOutput.hookEventName, 'PreToolUse');
117
+ assert.equal(typeof eachBlockedResponse.hookSpecificOutput.permissionDecisionReason, 'string');
118
+ assert.match(
119
+ eachBlockedResponse.hookSpecificOutput.permissionDecisionReason,
120
+ eachExpectedDiagnostic,
121
+ );
122
+ }
123
+ });
124
+
125
+ test('allows a registered session advisor with trusted session metadata', () => {
126
+ const hookResponse = buildSubagentModelRoutingResponse(
127
+ buildHookPayload({
128
+ agent_type: 'session-advisor',
129
+ model: 'Astra',
130
+ reasoning_effort: 'high',
131
+ }),
132
+ {
133
+ trustedSessionMetadata: {
134
+ authorized: true,
135
+ registeredAgentType: 'session-advisor',
136
+ },
137
+ },
138
+ );
139
+
140
+ assert.deepEqual(hookResponse.hookSpecificOutput.updatedInput, {
141
+ agent_type: 'session-advisor',
142
+ model: 'gpt-6-astra',
143
+ reasoning_effort: 'high',
144
+ });
145
+ });
146
+
147
+ test('a session advisor name without trusted session metadata is blocked', () => {
148
+ const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
149
+ agent_type: 'session-advisor',
150
+ model: 'Astra',
151
+ reasoning_effort: 'high',
152
+ }));
153
+ assert.equal(hookResponse.hookSpecificOutput.permissionDecision, 'deny');
154
+ assert.equal(
155
+ hookResponse.hookSpecificOutput.permissionDecisionReason,
156
+ 'advisor role is not trusted',
157
+ );
158
+ });
159
+
160
+ test('payload advisor metadata cannot grant advisory authorization', () => {
161
+ const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload(
162
+ {
163
+ agent_type: 'advisor',
164
+ model: 'Astra',
165
+ reasoning_effort: 'high',
166
+ },
167
+ {
168
+ trustedSessionMetadata: {
169
+ authorized: true,
170
+ registeredAgentType: 'advisor',
171
+ },
172
+ },
173
+ ));
174
+ assert.equal(hookResponse.hookSpecificOutput.permissionDecision, 'deny');
175
+ assert.equal(
176
+ hookResponse.hookSpecificOutput.permissionDecisionReason,
177
+ 'advisor role is not trusted',
178
+ );
179
+ });
180
+
181
+ test('the standalone hook command blocks an advisor without host metadata', () => {
182
+ const output = execFileSync(
183
+ process.execPath,
184
+ [fileURLToPath(new URL('./subagent_model_routing.mjs', import.meta.url))],
185
+ {
186
+ encoding: 'utf8',
187
+ input: JSON.stringify(buildHookPayload({
188
+ agent_type: 'session-advisor',
189
+ model: 'Astra',
190
+ reasoning_effort: 'high',
191
+ })),
192
+ },
193
+ );
194
+ const hookResponse = JSON.parse(output);
195
+ assert.equal(hookResponse.hookSpecificOutput.permissionDecision, 'deny');
196
+ assert.equal(
197
+ hookResponse.hookSpecificOutput.permissionDecisionReason,
198
+ 'advisor role is not trusted',
199
+ );
200
+ });
201
+
202
+ test('keeps a second routing pass unchanged', () => {
203
+ const firstResponse = buildSubagentModelRoutingResponse(buildHookPayload({
204
+ model: 'Terra',
205
+ reasoning_effort: 'medium',
206
+ message: 'keep',
207
+ }));
208
+ const secondResponse = buildSubagentModelRoutingResponse(buildHookPayload(
209
+ firstResponse.hookSpecificOutput.updatedInput,
210
+ ));
211
+
212
+ assert.deepEqual(secondResponse, {});
213
+ assert.deepEqual(firstResponse.hookSpecificOutput.updatedInput, {
214
+ model: 'gpt-5.6-luna',
215
+ reasoning_effort: 'xhigh',
216
+ message: 'keep',
217
+ });
218
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,12 +21,21 @@ The file contains this shape:
21
21
  ```json
22
22
  {
23
23
  "host": "current-host",
24
+ "defaultEffort": "medium",
24
25
  "modelsByRole": {
25
26
  "feature, refactoring": ["confirmed-host-model"]
26
27
  }
27
28
  }
28
29
  ```
29
30
 
31
+ Automatic model and effort routing reads
32
+ `rules/subagent-model-policy.json` from the same agents home. Edit that file to
33
+ change the next routing decision. The installer creates it only when it is
34
+ absent. The native hook blocks advisor role claims because the current Codex hook
35
+ input has no host-provided role binding. The Python advisor helper passes trusted
36
+ session metadata to the shared resolver. The native advisor hook path stays
37
+ pending until the host supplies that binding.
38
+
30
39
  Offer only model IDs the active native tool accepts. Keep the user's order.
31
40
  Treat `inherit-parent` and `auto` as parent inheritance, not model IDs. A full
32
41
  install seeds the Claude file and the Codex file from the package when a host
@@ -55,6 +64,7 @@ $selectorInput = @'
55
64
  "agentCount": 3,
56
65
  "requiresDistinctModels": false
57
66
  },
67
+ "requestedEffort": "medium",
58
68
  "availableModelIds": ["confirmed-host-model"],
59
69
  "confirmedSuitableModelIds": [],
60
70
  "parentModelId": "current-parent-model",
@@ -71,12 +81,24 @@ $selection = $selectorInput | node "$HOME/.agents/scripts/select_pstack_models.m
71
81
  `confirmedSuitableModelIds` contains available alternatives whose suitability
72
82
  for this role the host metadata or user has confirmed. Do not rank or classify a
73
83
  model from its name.
84
+ Keep the host's default effort in the preference file as `defaultEffort`, or
85
+ pass it as trusted runtime data in `requestedEffort`. The selector applies that
86
+ effort to a model-only entry before it picks a candidate. It holds the entry
87
+ when neither source supplies an effort and the policy has no single approved
88
+ effort for that model and role.
74
89
 
75
90
  Use the returned `nativeSpawnArguments` in the native subagent call. An empty
76
91
  object with `omitNativeModelArgument: true` means parent inheritance, so omit
77
92
  the native `model` argument. Stop when `canDelegate` is false. Ask the user
78
93
  when `requiresUserChoice` is true.
79
94
 
95
+ When a preference entry includes `{ "model": "model-id", "effort": "medium" }`,
96
+ the selector routes the pair before it deduplicates candidates and checks panel
97
+ diversity. The selected pair appears in `panel.selectedModelPairs`, and the
98
+ native argument uses `reasoning_effort`. The policy's `selectorExclusions` list
99
+ removes matching routed pairs from panel contenders without blocking direct
100
+ resolver or spawn-hook use.
101
+
80
102
  Run the selector again before the next agent call. Pass the same panel contract
81
103
  and the new delegation index with the refreshed inventory. Report
82
104
  `panel.selectedModelIds` and `panel.isCrossModelDiverse` as returned. A panel
@@ -0,0 +1,85 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "models": {
4
+ "astra": {
5
+ "id": "gpt-6-astra",
6
+ "aliases": ["Astra"]
7
+ },
8
+ "sol": {
9
+ "id": "gpt-5.6-sol",
10
+ "aliases": ["Sol"]
11
+ },
12
+ "terra": {
13
+ "id": "gpt-5.6-terra",
14
+ "aliases": ["Terra"]
15
+ },
16
+ "luna": {
17
+ "id": "gpt-5.6-luna",
18
+ "aliases": ["Luna"]
19
+ }
20
+ },
21
+ "efforts": {
22
+ "values": ["low", "medium", "high", "xhigh", "max"],
23
+ "aliases": {
24
+ "light": "low"
25
+ }
26
+ },
27
+ "inheritanceAliases": ["auto", "inherit-parent"],
28
+ "roles": {
29
+ "default": "worker",
30
+ "worker": [
31
+ "worker",
32
+ "default",
33
+ "explorer",
34
+ "caveman",
35
+ "clean-coder",
36
+ "code-quality-agent",
37
+ "code-verifier",
38
+ "deep-research",
39
+ "docs-agent",
40
+ "git-commit-crafter",
41
+ "issue-tracker",
42
+ "plan-packet-validator",
43
+ "pr-description-writer",
44
+ "skill-writer-agent",
45
+ "theme-prompt-agent",
46
+ "theme-prompt-generator",
47
+ "translator"
48
+ ],
49
+ "advisor": ["advisor", "session-advisor", "team-advisor", "code-advisor"],
50
+ "trustedAdvisor": ["session-advisor", "team-advisor", "code-advisor"]
51
+ },
52
+ "approvedPairs": [
53
+ {"model": "luna", "effort": "high"},
54
+ {"model": "luna", "effort": "xhigh"},
55
+ {"model": "luna", "effort": "max"},
56
+ {"model": "sol", "effort": "medium"},
57
+ {"model": "astra", "effort": "low"},
58
+ {"model": "astra", "effort": "medium"},
59
+ {"model": "astra", "effort": "high", "roles": ["advisor"]}
60
+ ],
61
+ "selectorExclusions": [
62
+ {"model": "sol", "effort": "medium"}
63
+ ],
64
+ "advisorDefault": {"model": "astra", "effort": "medium"},
65
+ "replacements": [
66
+ {"requested": {"model": "luna", "effort": "low"}, "selected": {"model": "luna", "effort": "high"}},
67
+ {"requested": {"model": "luna", "effort": "medium"}, "selected": {"model": "luna", "effort": "high"}},
68
+ {"requested": {"model": "sol", "effort": "low"}, "selected": {"model": "luna", "effort": "xhigh"}},
69
+ {"requested": {"model": "terra", "effort": "low"}, "selected": {"model": "luna", "effort": "xhigh"}},
70
+ {"requested": {"model": "terra", "effort": "medium"}, "selected": {"model": "luna", "effort": "xhigh"}},
71
+ {"requested": {"model": "terra", "effort": "high"}, "selected": {"model": "luna", "effort": "xhigh"}},
72
+ {"requested": {"model": "terra", "effort": "xhigh"}, "selected": {"model": "luna", "effort": "max"}},
73
+ {"requested": {"model": "sol", "effort": "high"}, "selected": {"model": "astra", "effort": "low"}},
74
+ {"requested": {"model": "sol", "effort": "xhigh"}, "selected": {"model": "astra", "effort": "low"}},
75
+ {"requested": {"model": "sol", "effort": "max"}, "selected": {"model": "astra", "effort": "low"}},
76
+ {"requested": {"model": "terra", "effort": "max"}, "selected": {"model": "astra", "effort": "low"}}
77
+ ],
78
+ "roleReplacements": [
79
+ {"role": "advisor", "requested": {"model": "astra", "effort": "xhigh"}, "selected": {"model": "astra", "effort": "medium"}},
80
+ {"role": "advisor", "requested": {"model": "astra", "effort": "max"}, "selected": {"model": "astra", "effort": "medium"}},
81
+ {"role": "worker", "requested": {"model": "astra", "effort": "xhigh"}, "selected": {"model": "astra", "effort": "low"}},
82
+ {"role": "worker", "requested": {"model": "astra", "effort": "max"}, "selected": {"model": "astra", "effort": "low"}},
83
+ {"role": "worker", "requested": {"model": "astra", "effort": "high"}, "selected": {"model": "astra", "effort": "low"}}
84
+ ]
85
+ }
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { resolveSubagentModelRoute } from './subagent_model_policy.mjs';
4
+
5
+ function blockedRoute(diagnostic) {
6
+ return { status: 'blocked', diagnostic };
7
+ }
8
+
9
+ function isObject(objectCandidate) {
10
+ return objectCandidate !== null && typeof objectCandidate === 'object' && !Array.isArray(objectCandidate);
11
+ }
12
+
13
+ function validatePolicyPath(input) {
14
+ if (!Object.hasOwn(input, 'policyPath')) return null;
15
+ if (typeof input.policyPath !== 'string' || input.policyPath.trim() === '') {
16
+ return 'advisor route policyPath must be a non-empty string';
17
+ }
18
+ return null;
19
+ }
20
+
21
+ let input;
22
+ try {
23
+ input = JSON.parse(readFileSync(0, 'utf8'));
24
+ } catch {
25
+ const routingDecision = blockedRoute('advisor route input is not valid JSON');
26
+ process.stdout.write(JSON.stringify(routingDecision));
27
+ process.exitCode = 1;
28
+ }
29
+
30
+ if (input !== undefined) {
31
+ let routingDecision;
32
+ if (!isObject(input)) {
33
+ routingDecision = blockedRoute('advisor route input must be an object');
34
+ } else {
35
+ const routingInputIssue = validatePolicyPath(input);
36
+ routingDecision = routingInputIssue
37
+ ? blockedRoute(routingInputIssue)
38
+ : resolveSubagentModelRoute({
39
+ agent_type: 'session-advisor',
40
+ ...(input.model !== undefined && input.model !== null ? { model: input.model } : {}),
41
+ ...(input.reasoning_effort !== undefined ? { reasoning_effort: input.reasoning_effort } : {}),
42
+ }, {
43
+ policyPath: input.policyPath,
44
+ trustedSessionMetadata: {
45
+ authorized: true,
46
+ registeredAgentType: 'session-advisor',
47
+ },
48
+ });
49
+ }
50
+ process.stdout.write(JSON.stringify(routingDecision));
51
+ if (routingDecision.status === 'blocked') process.exitCode = 1;
52
+ }