claude-dev-env 8.51.4 → 8.52.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.
@@ -8,10 +8,11 @@ import {
8
8
  PSTACK_PLUGIN_HOSTS,
9
9
  PSTACK_PLUGIN_IDENTIFIER,
10
10
  PSTACK_PLUGIN_SPEC,
11
+ SUBAGENT_MODELS_PLUGIN_SPEC,
11
12
  USAGE_WRAPUP_PLUGIN_SPEC,
12
13
  hostCommandInvocation,
13
- installMarketplacePlugin,
14
- marketplacePluginPlan,
14
+ installMarketplacePlugins,
15
+ marketplacePluginsPlan,
15
16
  removeOrphanedPluginVersions,
16
17
  shouldInstallMarketplacePlugin,
17
18
  } from './install-pstack-plugin.mjs';
@@ -36,7 +37,7 @@ const ROOTS = Object.freeze({
36
37
  const WINDOWS_ENVIRONMENT = Object.freeze({ ComSpec: 'C:\\Windows\\system32\\cmd.exe' });
37
38
 
38
39
  test('the Claude plan carries the two documented plugin commands', () => {
39
- const plan = marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'claude');
40
+ const plan = marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'claude');
40
41
  assert.equal(plan.executable, 'claude');
41
42
  assert.deepEqual(plan.commands, [
42
43
  ['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
@@ -45,7 +46,7 @@ test('the Claude plan carries the two documented plugin commands', () => {
45
46
  });
46
47
 
47
48
  test('the Codex plan adds the plugin with the Codex verb', () => {
48
- const plan = marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'codex');
49
+ const plan = marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'codex');
49
50
  assert.equal(plan.executable, 'codex');
50
51
  assert.deepEqual(plan.commands, [
51
52
  ['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
@@ -54,13 +55,13 @@ test('the Codex plan adds the plugin with the Codex verb', () => {
54
55
  });
55
56
 
56
57
  test('an unsupported host names the hosts this installer knows', () => {
57
- assert.throws(() => marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'cursor'), /cursor/);
58
+ assert.throws(() => marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'cursor'), /cursor/);
58
59
  assert.deepEqual(PSTACK_PLUGIN_HOSTS, ['claude', 'codex']);
59
60
  });
60
61
 
61
62
  test('each host installs into the managed root this run wrote', () => {
62
63
  const runner = recordingRunner();
63
- const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
64
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
64
65
  assert.equal(outcome.status, 'installed');
65
66
  assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
66
67
  ['claude', 'plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
@@ -77,11 +78,11 @@ test('each host installs into the managed root this run wrote', () => {
77
78
  });
78
79
 
79
80
  test('each host removes the installed plugin and marketplace before it installs', () => {
80
- assert.deepEqual(marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'claude').removalCommands, [
81
+ assert.deepEqual(marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'claude').removalCommands, [
81
82
  ['plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
82
83
  ['plugin', 'marketplace', 'remove', 'pstack-claude'],
83
84
  ]);
84
- assert.deepEqual(marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'codex').removalCommands, [
85
+ assert.deepEqual(marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'codex').removalCommands, [
85
86
  ['plugin', 'remove', PSTACK_PLUGIN_IDENTIFIER],
86
87
  ['plugin', 'marketplace', 'remove', 'pstack-claude'],
87
88
  ]);
@@ -92,7 +93,7 @@ test('a removal that finds nothing installed still lets the install run', () =>
92
93
  commandArguments.includes('uninstall') || commandArguments.includes('remove')
93
94
  ? { status: 1, stderr: 'not installed' }
94
95
  : { status: 0, stderr: '' }));
95
- const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, { ...ROOTS, hosts: ['claude'] }, runner);
96
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
96
97
  assert.equal(outcome.status, 'installed');
97
98
  assert.deepEqual(runner.calls.at(-1).commandArguments, ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER]);
98
99
  });
@@ -101,7 +102,7 @@ test('an absent host command skips that host and leaves the other installed', ()
101
102
  const runner = recordingRunner((executable) => (executable === 'codex'
102
103
  ? { status: null, error: Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' }) }
103
104
  : { status: 0, stderr: '' }));
104
- const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
105
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
105
106
  assert.equal(outcome.hosts.find(host => host.host === 'claude').status, 'installed');
106
107
  const codex = outcome.hosts.find(host => host.host === 'codex');
107
108
  assert.equal(codex.status, 'skipped');
@@ -114,7 +115,7 @@ test('a failing command reports that host and still installs the next one', () =
114
115
  executable === 'claude' && commandArguments[2] === 'add'
115
116
  ? { status: 1, stderr: 'marketplace unreachable' }
116
117
  : { status: 0, stderr: '' }));
117
- const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
118
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
118
119
  assert.equal(outcome.status, 'failed');
119
120
  const claude = outcome.hosts.find(host => host.host === 'claude');
120
121
  assert.equal(claude.status, 'failed');
@@ -126,8 +127,8 @@ test('a failing command reports that host and still installs the next one', () =
126
127
 
127
128
  test('an executable override replaces the host command for that host only', () => {
128
129
  const runner = recordingRunner();
129
- installMarketplacePlugin(
130
- PSTACK_PLUGIN_SPEC,
130
+ installMarketplacePlugins(
131
+ [PSTACK_PLUGIN_SPEC],
131
132
  { ...ROOTS, environment: { CDE_CODEX_EXECUTABLE: '/opt/codex/bin/codex' } },
132
133
  runner,
133
134
  );
@@ -146,7 +147,7 @@ test('the opt-out flag and variable both turn the step off', () => {
146
147
 
147
148
  test('a selected host list installs only that host', () => {
148
149
  const runner = recordingRunner();
149
- const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, { ...ROOTS, hosts: ['codex'] }, runner);
150
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['codex'] }, runner);
150
151
  assert.deepEqual(outcome.hosts.map(host => host.host), ['codex']);
151
152
  assert.deepEqual([...new Set(runner.calls.map(call => call.executable))], ['codex']);
152
153
  });
@@ -199,7 +200,7 @@ test("cmd.exe's command-not-found exit code reads as an absent host, not a failu
199
200
  stderr: "'codex' is not recognized as an internal or external command,\noperable program or batch file.",
200
201
  }
201
202
  : { status: 0, stderr: '' }));
202
- const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
203
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
203
204
  const codex = outcome.hosts.find(host => host.host === 'codex');
204
205
  assert.equal(codex.status, 'skipped');
205
206
  assert.equal(outcome.status, 'installed');
@@ -208,13 +209,13 @@ test("cmd.exe's command-not-found exit code reads as an absent host, not a failu
208
209
 
209
210
  test('a non-zero exit that is not command-not-found still reads as a failure', () => {
210
211
  const runner = recordingRunner(() => ({ status: 9009, stderr: 'the marketplace rejected the catalog' }));
211
- const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, { ...ROOTS, hosts: ['claude'] }, runner);
212
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
212
213
  assert.equal(outcome.hosts[0].status, 'failed');
213
214
  assert.match(outcome.hosts[0].warning, /rejected the catalog/);
214
215
  });
215
216
 
216
217
  test('the usage-wrapup plan adds this repository marketplace and installs the plugin on Claude', () => {
217
- const plan = marketplacePluginPlan(USAGE_WRAPUP_PLUGIN_SPEC, 'claude');
218
+ const plan = marketplacePluginsPlan([USAGE_WRAPUP_PLUGIN_SPEC], 'claude');
218
219
  assert.equal(plan.executable, 'claude');
219
220
  assert.equal(plan.homeVariable, 'CLAUDE_CONFIG_DIR');
220
221
  assert.deepEqual(plan.commands, [
@@ -225,12 +226,12 @@ test('the usage-wrapup plan adds this repository marketplace and installs the pl
225
226
 
226
227
  test('usage-wrapup is a Claude-only plugin and refuses a Codex plan', () => {
227
228
  assert.deepEqual(USAGE_WRAPUP_PLUGIN_SPEC.hosts, ['claude']);
228
- assert.throws(() => marketplacePluginPlan(USAGE_WRAPUP_PLUGIN_SPEC, 'codex'), /usage-wrapup plugin host codex/);
229
+ assert.throws(() => marketplacePluginsPlan([USAGE_WRAPUP_PLUGIN_SPEC], 'codex'), /usage-wrapup plugin host codex/);
229
230
  });
230
231
 
231
232
  test('installing usage-wrapup runs only the Claude commands, inside the managed Claude root', () => {
232
233
  const runner = recordingRunner();
233
- const outcome = installMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ROOTS, runner);
234
+ const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC], ROOTS, runner);
234
235
  assert.equal(outcome.status, 'installed');
235
236
  assert.deepEqual(outcome.hosts.map(host => host.host), ['claude']);
236
237
  assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
@@ -249,7 +250,7 @@ test('an absent claude command skips usage-wrapup and names the plugin in the wa
249
250
  status: null,
250
251
  error: Object.assign(new Error('spawn claude ENOENT'), { code: 'ENOENT' }),
251
252
  }));
252
- const outcome = installMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ROOTS, runner);
253
+ const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC], ROOTS, runner);
253
254
  assert.equal(outcome.status, 'skipped');
254
255
  assert.match(outcome.warning, /usage-wrapup was not installed for claude/);
255
256
  });
@@ -268,16 +269,60 @@ test('the usage-wrapup opt-out flag and variable turn off only usage-wrapup', ()
268
269
  );
269
270
  });
270
271
 
271
- test('the repository marketplace lists usage-wrapup from its package folder in this repository', () => {
272
- const marketplace = JSON.parse(readFileSync(
273
- new URL('../../../.claude-plugin/marketplace.json', import.meta.url),
274
- 'utf8',
275
- ));
276
- const [pluginName, marketplaceName] = USAGE_WRAPUP_PLUGIN_SPEC.pluginIdentifier.split('@');
277
- assert.equal(marketplace.name, marketplaceName);
278
- const entry = marketplace.plugins.find(plugin => plugin.name === pluginName);
279
- assert.ok(entry, `the marketplace lists ${pluginName}`);
280
- assert.equal(entry.source, './packages/usage-wrapup');
272
+ for (const [spec, packageFolder] of [
273
+ [USAGE_WRAPUP_PLUGIN_SPEC, './packages/usage-wrapup'],
274
+ [SUBAGENT_MODELS_PLUGIN_SPEC, './packages/subagent-models'],
275
+ ]) {
276
+ test(`the repository marketplace lists ${spec.name} from its package folder in this repository`, () => {
277
+ const marketplace = JSON.parse(readFileSync(
278
+ new URL('../../../.claude-plugin/marketplace.json', import.meta.url),
279
+ 'utf8',
280
+ ));
281
+ const [pluginName, marketplaceName] = spec.pluginIdentifier.split('@');
282
+ assert.equal(marketplace.name, marketplaceName);
283
+ const entry = marketplace.plugins.find(plugin => plugin.name === pluginName);
284
+ assert.ok(entry, `the marketplace lists ${pluginName}`);
285
+ assert.equal(entry.source, packageFolder);
286
+ assert.ok(spec.sparsePaths.includes(packageFolder.slice(2)), `the sparse checkout holds ${packageFolder}`);
287
+ });
288
+ }
289
+
290
+ test('should remove and add the shared marketplace once when installing usage-wrapup and subagent-models together', () => {
291
+ const runner = recordingRunner();
292
+ const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC, SUBAGENT_MODELS_PLUGIN_SPEC], ROOTS, runner);
293
+ assert.equal(outcome.status, 'installed');
294
+ assert.deepEqual(outcome.hosts.map(host => host.host), ['claude']);
295
+ assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
296
+ ['claude', 'plugin', 'uninstall', 'usage-wrapup@claude-dev-env'],
297
+ ['claude', 'plugin', 'uninstall', 'subagent-models@claude-dev-env'],
298
+ ['claude', 'plugin', 'marketplace', 'remove', 'claude-dev-env'],
299
+ [
300
+ 'claude', 'plugin', 'marketplace', 'add', 'jl-cmd/claude-dev-env',
301
+ '--sparse', '.claude-plugin', 'packages/usage-wrapup', 'packages/subagent-models',
302
+ ],
303
+ ['claude', 'plugin', 'install', 'usage-wrapup@claude-dev-env'],
304
+ ['claude', 'plugin', 'install', 'subagent-models@claude-dev-env'],
305
+ ]);
306
+ });
307
+
308
+ test('should refuse one plan for plugins from two marketplaces', () => {
309
+ assert.throws(
310
+ () => marketplacePluginsPlan([PSTACK_PLUGIN_SPEC, SUBAGENT_MODELS_PLUGIN_SPEC], 'claude'),
311
+ /subagent-models comes from jl-cmd\/claude-dev-env/,
312
+ );
313
+ });
314
+
315
+ test('should turn off only subagent-models with its opt-out flag or variable', () => {
316
+ assert.equal(shouldInstallMarketplacePlugin(SUBAGENT_MODELS_PLUGIN_SPEC, [], {}), true);
317
+ assert.equal(shouldInstallMarketplacePlugin(SUBAGENT_MODELS_PLUGIN_SPEC, ['--no-subagent-models'], {}), false);
318
+ assert.equal(
319
+ shouldInstallMarketplacePlugin(SUBAGENT_MODELS_PLUGIN_SPEC, [], { CDE_INSTALL_SUBAGENT_MODELS: '0' }),
320
+ false,
321
+ );
322
+ assert.equal(
323
+ shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ['--no-subagent-models'], { CDE_INSTALL_SUBAGENT_MODELS: '0' }),
324
+ true,
325
+ );
281
326
  });
282
327
 
283
328
  test('a clean install deletes the orphaned pstack versions and keeps the installed one', () => {
@@ -286,8 +331,8 @@ test('a clean install deletes the orphaned pstack versions and keeps the install
286
331
  mkdirSync(join(versionsDirectory, '0.9.55'), { recursive: true });
287
332
  writeFileSync(join(versionsDirectory, '0.9.55', '.orphaned_at'), '1');
288
333
  mkdirSync(join(versionsDirectory, '0.9.64'), { recursive: true });
289
- const outcome = installMarketplacePlugin(
290
- PSTACK_PLUGIN_SPEC,
334
+ const outcome = installMarketplacePlugins(
335
+ [PSTACK_PLUGIN_SPEC],
291
336
  { claudeRoot, hosts: ['claude'], environment: {} },
292
337
  recordingRunner(),
293
338
  );
@@ -52,6 +52,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
52
52
  ...process.env,
53
53
  CDE_INSTALL_PSTACK: '0',
54
54
  CDE_INSTALL_USAGE_WRAPUP: '0',
55
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
55
56
  HOME: homeDirectory,
56
57
  USERPROFILE: homeDirectory,
57
58
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -58,6 +58,7 @@ function runInstaller(homeDirectory, extraArguments) {
58
58
  ...process.env,
59
59
  CDE_INSTALL_PSTACK: '0',
60
60
  CDE_INSTALL_USAGE_WRAPUP: '0',
61
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
61
62
  HOME: homeDirectory,
62
63
  USERPROFILE: homeDirectory,
63
64
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -87,6 +88,7 @@ function scanTextWithProductionPiiScanner(scannedText) {
87
88
  ...process.env,
88
89
  CDE_INSTALL_PSTACK: '0',
89
90
  CDE_INSTALL_USAGE_WRAPUP: '0',
91
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
90
92
  PYTHONPATH: BLOCKING_HOOKS_DIRECTORY,
91
93
  CLAUDE_LOCAL_IDENTITY_PATH: join(
92
94
  temporaryDirectory,
@@ -33,6 +33,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
33
33
  ...process.env,
34
34
  CDE_INSTALL_PSTACK: '0',
35
35
  CDE_INSTALL_USAGE_WRAPUP: '0',
36
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
36
37
  HOME: homeDirectory,
37
38
  USERPROFILE: homeDirectory,
38
39
  CODEX_HOME: join(homeDirectory, '.codex'),
package/bin/install.mjs CHANGED
@@ -49,8 +49,9 @@ import {
49
49
  } from './install-constants.mjs';
50
50
  import {
51
51
  PSTACK_PLUGIN_SPEC,
52
+ SUBAGENT_MODELS_PLUGIN_SPEC,
52
53
  USAGE_WRAPUP_PLUGIN_SPEC,
53
- installMarketplacePlugin,
54
+ installMarketplacePlugins,
54
55
  shouldInstallMarketplacePlugin,
55
56
  } from './install-pstack-plugin.mjs';
56
57
  import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
@@ -407,17 +408,19 @@ export function pythonFileAndPrefixArguments(pythonCommand) {
407
408
  * @returns {{status: string, hosts: object[], warning: string|null}} The outcome.
408
409
  */
409
410
  export function installPstackPluginForHosts() {
410
- return installMarketplacePlugin(PSTACK_PLUGIN_SPEC, {
411
+ return installMarketplacePlugins([PSTACK_PLUGIN_SPEC], {
411
412
  claudeRoot: CLAUDE_HOME,
412
413
  codexHome: INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
413
414
  });
414
415
  }
415
416
 
416
- function printPluginHostOutcomes(spec, pluginOutcome) {
417
- for (const eachHost of pluginOutcome.hosts) {
418
- console.log(eachHost.warning
419
- ? ` ${spec.label} (${eachHost.host}): ${eachHost.status} — ${eachHost.warning}`
420
- : ` ${spec.label} (${eachHost.host}): ${eachHost.status}`);
417
+ function printPluginHostOutcomes(allSpecs, pluginOutcome) {
418
+ for (const eachSpec of allSpecs) {
419
+ for (const eachHost of pluginOutcome.hosts) {
420
+ console.log(eachHost.warning
421
+ ? ` ${eachSpec.label} (${eachHost.host}): ${eachHost.status} — ${eachHost.warning}`
422
+ : ` ${eachSpec.label} (${eachHost.host}): ${eachHost.status}`);
423
+ }
421
424
  }
422
425
  }
423
426
 
@@ -2890,14 +2893,16 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2890
2893
  console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
2891
2894
  }
2892
2895
  }
2893
- printPluginHostOutcomes(PSTACK_PLUGIN_SPEC, pstackPlugin);
2896
+ printPluginHostOutcomes([PSTACK_PLUGIN_SPEC], pstackPlugin);
2894
2897
  }
2895
- if (!selectedGroups && shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC)) {
2896
- const usageWrapupPlugin = installMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, {
2898
+ const allRepositoryPluginSpecs = [USAGE_WRAPUP_PLUGIN_SPEC, SUBAGENT_MODELS_PLUGIN_SPEC]
2899
+ .filter(eachSpec => shouldInstallMarketplacePlugin(eachSpec));
2900
+ if (!selectedGroups && allRepositoryPluginSpecs.length > 0) {
2901
+ const repositoryPlugins = installMarketplacePlugins(allRepositoryPluginSpecs, {
2897
2902
  claudeRoot: CLAUDE_HOME,
2898
2903
  });
2899
- summary.usageWrapupPlugin = usageWrapupPlugin;
2900
- printPluginHostOutcomes(USAGE_WRAPUP_PLUGIN_SPEC, usageWrapupPlugin);
2904
+ summary.repositoryPlugins = repositoryPlugins;
2905
+ printPluginHostOutcomes(allRepositoryPluginSpecs, repositoryPlugins);
2901
2906
  }
2902
2907
  if (!selectedGroups) {
2903
2908
  const packageGuidancePath = writeCodexPackageGuidance(
@@ -3412,6 +3417,7 @@ Usage:
3412
3417
  npx ${PACKAGE_NAME} --profiles A,B Install into each selected profile (one ownership manifest per target)
3413
3418
  npx ${PACKAGE_NAME} --no-pstack Full install without the pstack plugin (also CDE_INSTALL_PSTACK=0)
3414
3419
  npx ${PACKAGE_NAME} --no-usage-wrapup Full install without the usage-wrapup plugin (also CDE_INSTALL_USAGE_WRAPUP=0)
3420
+ npx ${PACKAGE_NAME} --no-subagent-models Full install without the subagent-models plugin (also CDE_INSTALL_SUBAGENT_MODELS=0)
3415
3421
  npx ${PACKAGE_NAME} --uninstall Remove installed files from the selected root
3416
3422
  npx ${PACKAGE_NAME} --help Show this help
3417
3423
 
@@ -261,6 +261,7 @@ function resolveInstallerInvocation(homeDirectory) {
261
261
  ...process.env,
262
262
  CDE_INSTALL_PSTACK: '0',
263
263
  CDE_INSTALL_USAGE_WRAPUP: '0',
264
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
264
265
  HOME: homeDirectory,
265
266
  USERPROFILE: homeDirectory,
266
267
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -24,6 +24,7 @@ function installInScratchHome(homeDirectory, argumentsList = [], extraEnvironmen
24
24
  ...process.env,
25
25
  CDE_INSTALL_PSTACK: '0',
26
26
  CDE_INSTALL_USAGE_WRAPUP: '0',
27
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
27
28
  HOME: homeDirectory,
28
29
  USERPROFILE: homeDirectory,
29
30
  CODEX_HOME: join(homeDirectory, '.codex'),
@@ -179,6 +180,7 @@ test('a successful pstack install retains its skill block and gains the question
179
180
  const installation = installInScratchHome(homeDirectory, [], {
180
181
  CDE_INSTALL_PSTACK: '1',
181
182
  CDE_INSTALL_USAGE_WRAPUP: '0',
183
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
182
184
  CDE_CODEX_EXECUTABLE: commandPath,
183
185
  CDE_CLAUDE_EXECUTABLE: commandPath,
184
186
  });
@@ -206,6 +208,7 @@ test('enabling pstack after a pstack-off install preserves question guidance', (
206
208
  const secondInstallation = installInScratchHome(homeDirectory, [], {
207
209
  CDE_INSTALL_PSTACK: '1',
208
210
  CDE_INSTALL_USAGE_WRAPUP: '0',
211
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
209
212
  CDE_CODEX_EXECUTABLE: commandPath,
210
213
  CDE_CLAUDE_EXECUTABLE: commandPath,
211
214
  });
@@ -234,6 +237,7 @@ test('a failed pstack transition restores question-only guidance and removes its
234
237
  const failedInstallation = installInScratchHome(homeDirectory, [], {
235
238
  CDE_INSTALL_PSTACK: '1',
236
239
  CDE_INSTALL_USAGE_WRAPUP: '0',
240
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
237
241
  CDE_CODEX_EXECUTABLE: commandPath,
238
242
  CDE_CLAUDE_EXECUTABLE: commandPath,
239
243
  CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
@@ -58,6 +58,7 @@ function runInstallerInSandbox(sandboxHome, installerArguments = []) {
58
58
  ...process.env,
59
59
  CDE_INSTALL_PSTACK: '0',
60
60
  CDE_INSTALL_USAGE_WRAPUP: '0',
61
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
61
62
  HOME: sandboxHome,
62
63
  USERPROFILE: sandboxHome,
63
64
  GIT_CONFIG_GLOBAL: gitConfigGlobal,
@@ -31,6 +31,7 @@ function installEnvironment(homeDirectory) {
31
31
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
32
32
  CDE_INSTALL_PSTACK: '0',
33
33
  CDE_INSTALL_USAGE_WRAPUP: '0',
34
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
34
35
  };
35
36
  }
36
37
 
@@ -3026,6 +3026,7 @@ test('a failed update preserves an existing skills lookup pointer', () => {
3026
3026
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
3027
3027
  CDE_INSTALL_PSTACK: '0',
3028
3028
  CDE_INSTALL_USAGE_WRAPUP: '0',
3029
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
3029
3030
  };
3030
3031
  const argumentsForCoreInstall = [PSTACK_TEST_INSTALLER_PATH, '--only', 'core'];
3031
3032
  const firstRun = spawnSync(process.execPath, argumentsForCoreInstall, {
@@ -3077,6 +3078,7 @@ function runPstackInstaller(homeDirectory, extraArguments, environmentOverrides
3077
3078
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
3078
3079
  CDE_INSTALL_PSTACK: '1',
3079
3080
  CDE_INSTALL_USAGE_WRAPUP: '0',
3081
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
3080
3082
  ...environmentOverrides,
3081
3083
  },
3082
3084
  });
@@ -3391,6 +3393,60 @@ test('the help output names the usage-wrapup opt-out beside the pstack one', ()
3391
3393
  assert.match(helpRun.stdout, /--no-pstack .*\n.*--no-usage-wrapup .*CDE_INSTALL_USAGE_WRAPUP=0/);
3392
3394
  });
3393
3395
 
3396
+ const REPOSITORY_PLUGIN_CLAUDE_COMMANDS = Object.freeze([
3397
+ 'claude plugin uninstall usage-wrapup@claude-dev-env',
3398
+ 'claude plugin uninstall subagent-models@claude-dev-env',
3399
+ 'claude plugin marketplace remove claude-dev-env',
3400
+ 'claude plugin marketplace add jl-cmd/claude-dev-env --sparse .claude-plugin packages/usage-wrapup packages/subagent-models',
3401
+ 'claude plugin install usage-wrapup@claude-dev-env',
3402
+ 'claude plugin install subagent-models@claude-dev-env',
3403
+ ]);
3404
+
3405
+ test('should install subagent-models beside usage-wrapup with one marketplace add on a full install', t => {
3406
+ const sandbox = pstackPluginSandbox(t);
3407
+
3408
+ const installerOutput = runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], {
3409
+ ...sandbox.environment,
3410
+ CDE_INSTALL_USAGE_WRAPUP: '1',
3411
+ CDE_INSTALL_SUBAGENT_MODELS: '1',
3412
+ });
3413
+
3414
+ assert.deepEqual(usageWrapupCommands(sandbox.recordedCommands()), REPOSITORY_PLUGIN_CLAUDE_COMMANDS);
3415
+ assert.match(installerOutput, /Usage-wrapup \(claude\): installed/);
3416
+ assert.match(installerOutput, /Subagent-models \(claude\): installed/);
3417
+ });
3418
+
3419
+ for (const [caseName, extraArguments, environmentOverrides] of [
3420
+ ['--no-subagent-models', ['--no-subagent-models'], { CDE_INSTALL_SUBAGENT_MODELS: '1' }],
3421
+ ['CDE_INSTALL_SUBAGENT_MODELS=0', [], { CDE_INSTALL_SUBAGENT_MODELS: '0' }],
3422
+ ['an --only run', ['--only', 'core'], { CDE_INSTALL_SUBAGENT_MODELS: '1' }],
3423
+ ]) {
3424
+ test(`should skip subagent-models on ${caseName}`, t => {
3425
+ const sandbox = pstackPluginSandbox(t);
3426
+
3427
+ const installerOutput = runPstackInstaller(sandbox.homeDirectory, extraArguments, {
3428
+ ...sandbox.environment,
3429
+ ...environmentOverrides,
3430
+ });
3431
+
3432
+ assert.equal(
3433
+ sandbox.recordedCommands().some(eachCommand => eachCommand.includes('subagent-models')),
3434
+ false,
3435
+ );
3436
+ assert.doesNotMatch(installerOutput, /Subagent-models \(/);
3437
+ });
3438
+ }
3439
+
3440
+ test('should name the subagent-models opt-out in the help output', () => {
3441
+ const helpRun = spawnSync(process.execPath, [PSTACK_TEST_INSTALLER_PATH, '--help'], {
3442
+ encoding: 'utf8',
3443
+ env: process.env,
3444
+ });
3445
+
3446
+ assert.equal(helpRun.status, 0, helpRun.stderr);
3447
+ assert.match(helpRun.stdout, /--no-subagent-models .*CDE_INSTALL_SUBAGENT_MODELS=0/);
3448
+ });
3449
+
3394
3450
  function continuityCommandCount(configurationPath) {
3395
3451
  if (!existsSync(configurationPath)) return 0;
3396
3452
  const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
@@ -87,6 +87,7 @@ test('capture and restore recover settings, manifest, files, and hooksPath', ()
87
87
  ...process.env,
88
88
  CDE_INSTALL_PSTACK: '0',
89
89
  CDE_INSTALL_USAGE_WRAPUP: '0',
90
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
90
91
  HOME: box.root,
91
92
  USERPROFILE: box.root,
92
93
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -149,6 +150,7 @@ test('capture and restore recover an additional host settings file', () => {
149
150
  ...process.env,
150
151
  CDE_INSTALL_PSTACK: '0',
151
152
  CDE_INSTALL_USAGE_WRAPUP: '0',
153
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
152
154
  HOME: box.root,
153
155
  USERPROFILE: box.root,
154
156
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -184,6 +186,7 @@ test('runWithInstallTransaction restores prior state on injected fault', () => {
184
186
  ...process.env,
185
187
  CDE_INSTALL_PSTACK: '0',
186
188
  CDE_INSTALL_USAGE_WRAPUP: '0',
189
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
187
190
  HOME: box.root,
188
191
  USERPROFILE: box.root,
189
192
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -238,6 +241,7 @@ test('runWithInstallTransaction commits and discards journal on success', () =>
238
241
  ...process.env,
239
242
  CDE_INSTALL_PSTACK: '0',
240
243
  CDE_INSTALL_USAGE_WRAPUP: '0',
244
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
241
245
  HOME: box.root,
242
246
  USERPROFILE: box.root,
243
247
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -281,6 +285,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
281
285
  ...process.env,
282
286
  CDE_INSTALL_PSTACK: '0',
283
287
  CDE_INSTALL_USAGE_WRAPUP: '0',
288
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
284
289
  HOME: homeDirectory,
285
290
  USERPROFILE: homeDirectory,
286
291
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -316,6 +321,7 @@ test('a later install fault removes newly seeded Codex pstack files', () => {
316
321
  environment: {
317
322
  CDE_INSTALL_PSTACK: '1',
318
323
  CDE_INSTALL_USAGE_WRAPUP: '0',
324
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
319
325
  CDE_CODEX_EXECUTABLE: codexCommandPath,
320
326
  },
321
327
  });
@@ -459,6 +465,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
459
465
  ...process.env,
460
466
  CDE_INSTALL_PSTACK: '0',
461
467
  CDE_INSTALL_USAGE_WRAPUP: '0',
468
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
462
469
  HOME: homeDirectory,
463
470
  USERPROFILE: homeDirectory,
464
471
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -476,6 +483,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
476
483
  ...process.env,
477
484
  CDE_INSTALL_PSTACK: '0',
478
485
  CDE_INSTALL_USAGE_WRAPUP: '0',
486
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
479
487
  HOME: homeDirectory,
480
488
  USERPROFILE: homeDirectory,
481
489
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -73,6 +73,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
73
73
  ...process.env,
74
74
  CDE_INSTALL_PSTACK: '0',
75
75
  CDE_INSTALL_USAGE_WRAPUP: '0',
76
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
76
77
  HOME: homeDirectory,
77
78
  USERPROFILE: homeDirectory,
78
79
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -307,6 +308,7 @@ test('uninstall fault after_git_config restores prior core.hooksPath', () => {
307
308
  ...process.env,
308
309
  CDE_INSTALL_PSTACK: '0',
309
310
  CDE_INSTALL_USAGE_WRAPUP: '0',
311
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
310
312
  HOME: homeDirectory,
311
313
  USERPROFILE: homeDirectory,
312
314
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -24,6 +24,7 @@ function installerEnvironment(homeDirectory) {
24
24
  ...process.env,
25
25
  CDE_INSTALL_PSTACK: '0',
26
26
  CDE_INSTALL_USAGE_WRAPUP: '0',
27
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
27
28
  HOME: homeDirectory,
28
29
  USERPROFILE: homeDirectory,
29
30
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.51.4",
3
+ "version": "8.52.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import dataclasses
6
6
  import errno
7
+ import importlib
7
8
  import json
8
9
  import os
9
10
  import shutil
@@ -63,6 +64,16 @@ from dev_env_scripts_constants.codex_account_constants import (
63
64
  WEEKLY_WINDOW_MINUTES,
64
65
  )
65
66
  from dev_env_scripts_constants.shared_tree_constants import CLAUDE_CONFIG_DIR_ENV_VAR
67
+ from shared_tree_paths import resolve_shared_process_tree_scripts_directory
68
+
69
+ _shared_process_tree_scripts_directory = resolve_shared_process_tree_scripts_directory(
70
+ __file__,
71
+ all_environment=os.environ,
72
+ )
73
+ if str(_shared_process_tree_scripts_directory) not in sys.path:
74
+ sys.path.insert(0, str(_shared_process_tree_scripts_directory))
75
+
76
+ _process_tree_kill = importlib.import_module("process_tree_kill")
66
77
 
67
78
 
68
79
  def _read_list(path: Path) -> object:
@@ -492,21 +503,26 @@ def _run_captured_subprocess(all_argv: Sequence[str], **options: object) -> subp
492
503
  errors = str(options.get("errors") or "replace")
493
504
  stdin_bytes = options.get("input")
494
505
  with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file:
495
- completion = subprocess.run(
506
+ with subprocess.Popen(
496
507
  _resolve_command(all_argv),
497
- input=stdin_bytes,
508
+ stdin=subprocess.PIPE if stdin_bytes is not None else None,
498
509
  stdout=stdout_file,
499
510
  stderr=stderr_file,
500
511
  env=options.get("env"),
501
512
  cwd=options.get("cwd"),
502
- timeout=options.get("timeout"),
503
- check=False,
504
- )
513
+ start_new_session=_process_tree_kill.should_start_new_session(),
514
+ ) as process:
515
+ try:
516
+ process.communicate(input=stdin_bytes, timeout=options.get("timeout"))
517
+ except (subprocess.TimeoutExpired, KeyboardInterrupt):
518
+ _process_tree_kill.terminate_process_tree(process)
519
+ process.wait()
520
+ raise
505
521
  stdout_file.seek(0)
506
522
  stderr_file.seek(0)
507
523
  stdout = stdout_file.read().decode(encoding, errors).replace("\r\n", "\n").replace("\r", "\n")
508
524
  stderr = stderr_file.read().decode(encoding, errors).replace("\r\n", "\n").replace("\r", "\n")
509
- return subprocess.CompletedProcess(list(all_argv), completion.returncode, stdout, stderr)
525
+ return subprocess.CompletedProcess(list(all_argv), process.returncode, stdout, stderr)
510
526
 
511
527
 
512
528
  subprocess_runner: SubprocessRunner = _run_captured_subprocess