claude-dev-env 8.88.7 → 8.90.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 (91) hide show
  1. package/.agents/skills/pr-visual/README.md +29 -0
  2. package/.agents/skills/pr-visual/SKILL.md +62 -0
  3. package/.agents/skills/pr-visual/assets/icons.symbols +27 -0
  4. package/.agents/skills/pr-visual/assets/viewer.html +147 -0
  5. package/.agents/skills/pr-visual/fixtures/bar.scene.json +193 -0
  6. package/.agents/skills/pr-visual/fixtures/flow.scene.json +201 -0
  7. package/.agents/skills/pr-visual/fixtures/head/metrics.json +16 -0
  8. package/.agents/skills/pr-visual/fixtures/integrated.scene.json +239 -0
  9. package/.agents/skills/pr-visual/fixtures/line.scene.json +193 -0
  10. package/.agents/skills/pr-visual/fixtures/scatter.scene.json +193 -0
  11. package/.agents/skills/pr-visual/fixtures/snapshot.json +21 -0
  12. package/.agents/skills/pr-visual/references/evidence.md +9 -0
  13. package/.agents/skills/pr-visual/references/formats.md +30 -0
  14. package/.agents/skills/pr-visual/references/scene-contract.md +48 -0
  15. package/.agents/skills/pr-visual/references/visual-contract.md +23 -0
  16. package/.agents/skills/pr-visual/scripts/collect.py +111 -0
  17. package/.agents/skills/pr-visual/scripts/metrics.py +99 -0
  18. package/.agents/skills/pr-visual/scripts/render.py +133 -0
  19. package/.agents/skills/pr-visual/scripts/test_collect.py +58 -0
  20. package/.agents/skills/pr-visual/scripts/test_metrics.py +47 -0
  21. package/.agents/skills/pr-visual/scripts/test_render.py +77 -0
  22. package/.agents/skills/pr-visual/scripts/verify.py +218 -0
  23. package/.agents/skills/pr-visual/scripts/visual_config/config/constants.py +104 -0
  24. package/bin/codex-skill-load-block.mjs +0 -25
  25. package/bin/codex-skill-load-block.test.mjs +5 -17
  26. package/bin/codex-trimmed-sol.test.mjs +0 -1
  27. package/bin/cursor-native-hooks.test.mjs +3 -3
  28. package/bin/ever-shipped-skills.mjs +1 -0
  29. package/bin/install-constants.mjs +3 -0
  30. package/bin/install-deepstack-plugin.mjs +42 -25
  31. package/bin/install-deepstack-plugin.test.mjs +37 -11
  32. package/bin/install-pstack-plugin.mjs +58 -11
  33. package/bin/install-pstack-plugin.test.mjs +54 -21
  34. package/bin/install.agents-home.test.mjs +0 -1
  35. package/bin/install.codex-rules.test.mjs +0 -2
  36. package/bin/install.cursor-rules.test.mjs +96 -21
  37. package/bin/install.mjs +48 -44
  38. package/bin/install.pr-visual.test.mjs +40 -0
  39. package/bin/install.prompt-context.test.mjs +3 -4
  40. package/bin/install.prune.test.mjs +0 -1
  41. package/bin/install.question-presentation.test.mjs +0 -4
  42. package/bin/install.settings-defaults.test.mjs +0 -1
  43. package/bin/install.shared-settings.test.mjs +0 -1
  44. package/bin/install.test.mjs +14 -44
  45. package/bin/install.transaction.test.mjs +0 -8
  46. package/bin/install.uninstall-transaction.test.mjs +0 -2
  47. package/bin/install.update-keeps-hooks.test.mjs +0 -1
  48. package/docs/BDD_SCENARIO_QUALITY.md +1 -1
  49. package/docs/BDD_TEST_LAYOUT.md +1 -1
  50. package/docs/CODE_RULES.md +1 -1
  51. package/docs/rule-guides/deepstack-wording.md +3 -5
  52. package/docs/rule-guides/session-title.md +5 -4
  53. package/hooks/advisory/owned_pr_review_loop.py +365 -0
  54. package/hooks/advisory/test_owned_pr_review_loop.py +369 -0
  55. package/hooks/blocking/session_title_turn_gate.py +287 -0
  56. package/hooks/blocking/test_session_title_format_gate.py +5 -0
  57. package/hooks/blocking/test_session_title_turn_gate.py +264 -0
  58. package/hooks/features/bash-dispatchers.md +3 -0
  59. package/hooks/features/conduct-gates.md +3 -0
  60. package/hooks/features/session-context.md +0 -4
  61. package/hooks/hooks.json +15 -5
  62. package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
  63. package/hooks/hooks_constants/owned_pr_review_loop_constants.py +120 -0
  64. package/hooks/hooks_constants/session_title_constants.py +41 -3
  65. package/hooks/lifecycle/nested_project_hooks.py +2 -2
  66. package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
  67. package/hooks/session/compaction_user_messages.py +2 -2
  68. package/package.json +2 -2
  69. package/rules/deepstack-wording.md +3 -3
  70. package/scripts/ci/install-playtest.mjs +58 -314
  71. package/scripts/ci/install-playtest.test.mjs +24 -6
  72. package/scripts/policy_lint/adapter_configuration.py +1 -0
  73. package/scripts/policy_lint/config/constants.py +1 -0
  74. package/scripts/test_deepstack_review_criterion.py +2 -3
  75. package/scripts/test_deepstack_wording_rule.py +6 -4
  76. package/scripts/tests/test_adapter_configuration.py +8 -0
  77. package/.agents/skills/deepen/SKILL.md +0 -26
  78. package/commands/deepen.md +0 -14
  79. package/docs/decisions/deepstack-gate.md +0 -32
  80. package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
  81. package/hooks/session/deepen_default_rewrite.py +0 -128
  82. package/hooks/session/deepen_first_prompt.py +0 -383
  83. package/hooks/session/deepen_role_gate.py +0 -171
  84. package/hooks/session/deepen_turn_marker.py +0 -75
  85. package/hooks/session/fixtures/deepstack_replay.json +0 -15
  86. package/hooks/session/grade_deepstack_replay.py +0 -78
  87. package/hooks/session/test_deepen_default_rewrite.py +0 -90
  88. package/hooks/session/test_deepen_first_prompt.py +0 -452
  89. package/hooks/session/test_deepen_role_gate.py +0 -83
  90. package/hooks/session/test_deepen_turn_marker.py +0 -59
  91. package/hooks/session/test_grade_deepstack_replay.py +0 -47
@@ -62,7 +62,6 @@ function runInstaller(homeDirectory, extraArguments) {
62
62
  CDE_INSTALL_DEEPSTACK: '0',
63
63
  CDE_INSTALL_USAGE_WRAPUP: '0',
64
64
  CDE_INSTALL_SUBAGENT_MODELS: '0',
65
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
66
65
  HOME: homeDirectory,
67
66
  USERPROFILE: homeDirectory,
68
67
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -96,7 +95,6 @@ function scanTextWithProductionPiiScanner(scannedText) {
96
95
  CDE_INSTALL_DEEPSTACK: '0',
97
96
  CDE_INSTALL_USAGE_WRAPUP: '0',
98
97
  CDE_INSTALL_SUBAGENT_MODELS: '0',
99
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
100
98
  PYTHONPATH: BLOCKING_HOOKS_DIRECTORY,
101
99
  CLAUDE_LOCAL_IDENTITY_PATH: join(
102
100
  temporaryDirectory,
@@ -36,7 +36,6 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
36
36
  CDE_INSTALL_DEEPSTACK: '0',
37
37
  CDE_INSTALL_USAGE_WRAPUP: '0',
38
38
  CDE_INSTALL_SUBAGENT_MODELS: '0',
39
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
40
39
  HOME: homeDirectory,
41
40
  USERPROFILE: homeDirectory,
42
41
  CODEX_HOME: join(homeDirectory, '.codex'),
@@ -135,14 +134,20 @@ test('seeds one editable policy and one native Codex routing hook', () => {
135
134
  );
136
135
  assert.equal(firstSpawnPromptGroups.length, 1);
137
136
  assert.match(firstSpawnPromptGroups[0].hooks[0].command, /skill_loaded_reminder\.py/);
138
- assert.deepEqual(Object.keys(firstCodexHooks.hooks), ['PreToolUse', 'UserPromptSubmit', 'Stop']);
137
+ assert.deepEqual(Object.keys(firstCodexHooks.hooks), ['PreToolUse', 'UserPromptSubmit', 'PostToolUse', 'Stop']);
138
+ const allCodexPostToolUseCommands = firstCodexHooks.hooks.PostToolUse.flatMap(
139
+ group => group.hooks.map(hook => hook.command),
140
+ );
141
+ assert.equal(allCodexPostToolUseCommands.length, 1, 'Codex gets the owned PR review loop and no other PostToolUse hook');
142
+ assert.match(allCodexPostToolUseCommands[0], /advisory\/owned_pr_review_loop\.py/);
139
143
  const allCodexStopCommands = firstCodexHooks.hooks.Stop.flatMap(group => group.hooks.map(hook => hook.command));
140
144
  assert.equal(allCodexStopCommands.length, 1, 'Codex gets the open tasks Stop gate and no other Stop hook');
141
145
  assert.match(allCodexStopCommands[0], /open_tasks_stop_gate\.py/);
142
- assert.ok(
143
- firstCodexHooks.hooks.UserPromptSubmit.some(group =>
146
+ assert.equal(
147
+ (firstCodexHooks.hooks.UserPromptSubmit ?? []).some(group =>
144
148
  group.hooks.some(hook => String(hook.command).includes('deepen_first_prompt.py')),
145
149
  ),
150
+ false,
146
151
  );
147
152
  assert.equal(
148
153
  firstCodexHooks.hooks.PreToolUse.some(group => group.matcher === 'Write|Edit|MultiEdit|apply_patch'),
@@ -285,6 +290,7 @@ test('Codex reinstall removes omitted package hooks and preserves user configura
285
290
  const suffixCommand = `python "${join(codexHome, 'hooks', 'blocking', 'pre_tool_use_dispatcher.py.backup')}"`;
286
291
  hooksByEvent.PreToolUse.push({ matcher: 'suffix', hooks: [{ type: 'command', command: suffixCommand }] });
287
292
  hooksByEvent.CustomEvent = { metadata: 'keep-event' };
293
+ hooksByEvent.PostToolUse = [{ matcher: 'Write', hooks: [{ type: 'command', command: 'python user-post-tool-use.py' }] }];
288
294
  const expectedUserHooks = JSON.parse(JSON.stringify(hooksByEvent));
289
295
  for (const eachGroup of Object.values(expectedUserHooks).flat()) {
290
296
  if (eachGroup.hooks && eachGroup.metadata) eachGroup.hooks.shift();
@@ -301,15 +307,20 @@ test('Codex reinstall removes omitted package hooks and preserves user configura
301
307
  ['Agent|Task', 'multi_agent_v1__spawn_agent'].includes(eachGroup.matcher));
302
308
  assert.equal(allSpawnGroups.length, 2);
303
309
  migratedHooks.hooks.PreToolUse = migratedHooks.hooks.PreToolUse.filter(eachGroup => !allSpawnGroups.includes(eachGroup));
304
- const allPackagePromptCommands = migratedHooks.hooks.UserPromptSubmit
310
+ const allPackagePromptCommands = (migratedHooks.hooks.UserPromptSubmit ?? [])
305
311
  .flatMap(eachGroup => eachGroup.hooks)
306
312
  .map(eachHook => eachHook.command);
307
- assert.ok(allPackagePromptCommands.some(eachCommand => eachCommand.includes('deepen_first_prompt.py')));
313
+ assert.equal(allPackagePromptCommands.some(eachCommand => eachCommand.includes('deepen_first_prompt.py')), false);
308
314
  delete migratedHooks.hooks.UserPromptSubmit;
309
315
  const allPackageStopGroups = migratedHooks.hooks.Stop.filter(eachGroup =>
310
316
  eachGroup.hooks.some(eachHook => eachHook.command.includes('open_tasks_stop_gate.py')));
311
317
  assert.equal(allPackageStopGroups.length, 1);
312
318
  migratedHooks.hooks.Stop = migratedHooks.hooks.Stop.filter(eachGroup => !allPackageStopGroups.includes(eachGroup));
319
+ const allPackagePostToolUseGroups = migratedHooks.hooks.PostToolUse.filter(eachGroup =>
320
+ eachGroup.hooks.some(eachHook => eachHook.command.includes('owned_pr_review_loop.py')));
321
+ assert.equal(allPackagePostToolUseGroups.length, 1);
322
+ assert.equal(allPackagePostToolUseGroups[0].hooks.length, 1);
323
+ migratedHooks.hooks.PostToolUse = migratedHooks.hooks.PostToolUse.filter(eachGroup => !allPackagePostToolUseGroups.includes(eachGroup));
313
324
  assert.deepEqual(migratedHooks, { enabled: false, metadata: 'keep-file', hooks: expectedUserHooks });
314
325
  assert.equal(readFileSync(configPath, 'utf8'), disabledConfig);
315
326
  const firstInstall = readFileSync(codexHooksPath, 'utf8');
@@ -356,27 +367,91 @@ test('uninstall removes the routing hook and keeps a Codex user hook', () => {
356
367
  }
357
368
  });
358
369
 
359
- test('should ship the deepen skill to Cursor and the deepen block to Codex, and remove both on uninstall', () => {
360
- const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-deepen-surfaces-'));
370
+ const RETIRED_DEEPEN_CODEX_BLOCK = '<!-- claude-dev-env deepen: start -->\nDeepen the first interactive user task.\n<!-- claude-dev-env deepen: end -->\n';
371
+
372
+ function allSettingsHookCommands(settingsPath) {
373
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
374
+ return Object.values(settings.hooks ?? {})
375
+ .flatMap(allGroups => allGroups)
376
+ .flatMap(group => group.hooks ?? [])
377
+ .map(hook => String(hook.command));
378
+ }
379
+
380
+ function assertNoDeepenPieceInstalled(homeDirectory, cursorSkillPath, codexGuidancePath, settingsPath) {
381
+ assert.equal(existsSync(cursorSkillPath), false, 'the Cursor deepen skill is gone');
382
+ assert.equal(existsSync(join(homeDirectory, '.claude', 'skills', 'deepen')), false, 'the Claude deepen skill is gone');
383
+ assert.equal(existsSync(join(homeDirectory, '.claude', 'commands', 'deepen.md')), false, 'the /deepen command is gone');
384
+ const codexGuidance = existsSync(codexGuidancePath) ? readFileSync(codexGuidancePath, 'utf8') : '';
385
+ assert.doesNotMatch(codexGuidance, /claude-dev-env deepen/);
386
+ assert.equal(
387
+ allSettingsHookCommands(settingsPath).some(command => command.includes('deepen_first_prompt.py')),
388
+ false,
389
+ 'settings.json runs no deepen hook',
390
+ );
391
+ const cursorHooksPath = join(homeDirectory, DEFAULT_CURSOR_DIRECTORY_NAME, 'hooks.json');
392
+ const cursorHooksText = existsSync(cursorHooksPath) ? readFileSync(cursorHooksPath, 'utf8') : '';
393
+ assert.equal(cursorHooksText.includes('deepen_first_prompt.py'), false, 'Cursor runs no deepen hook');
394
+ const installedHookPath = join(homeDirectory, '.claude', 'hooks', 'session', 'deepen_first_prompt.py');
395
+ if (existsSync(installedHookPath)) {
396
+ assert.match(readFileSync(installedHookPath, 'utf8'), /claude-dev-env retired hook stand-in/);
397
+ }
398
+ }
399
+
400
+ test('a full install over an old deepen install removes every deepen piece, and a repeat install keeps them out', () => {
401
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-deepen-retired-'));
361
402
  try {
362
- const sourceSkillPath = join(PACKAGE_DIRECTORY, '.agents', 'skills', 'deepen', 'SKILL.md');
403
+ const claudeDirectory = join(homeDirectory, '.claude');
363
404
  const cursorSkillPath = join(homeDirectory, DEFAULT_CURSOR_DIRECTORY_NAME, 'skills', 'deepen', 'SKILL.md');
364
405
  const codexGuidancePath = join(homeDirectory, '.codex', 'AGENTS.md');
365
- const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
406
+ const manifestPath = join(claudeDirectory, '.claude-dev-env-manifest.json');
407
+ const settingsPath = join(claudeDirectory, 'settings.json');
408
+ const retiredPluginVersion = join(claudeDirectory, 'plugins', 'cache', 'claude-dev-env', 'deepen-first-prompt', '0.1.0');
409
+ const fakeClaudeLogPath = join(homeDirectory, 'fake-claude.log');
410
+ const orphanedMarkerPath = join(retiredPluginVersion, '.orphaned_at');
411
+ const isWindows = process.platform === 'win32';
412
+ const fakeClaudePath = join(homeDirectory, isWindows ? 'fake-claude.cmd' : 'fake-claude.sh');
413
+ writeFileSync(
414
+ fakeClaudePath,
415
+ isWindows
416
+ ? `@echo off\r\n>>"${fakeClaudeLogPath}" echo %*\r\ntype nul > "${orphanedMarkerPath}"\r\n`
417
+ : `#!/bin/sh\necho "$@" >> '${fakeClaudeLogPath}'\ntouch '${orphanedMarkerPath}'\n`,
418
+ { mode: 0o755 },
419
+ );
420
+ const environment = { CDE_CLAUDE_EXECUTABLE: fakeClaudePath };
366
421
 
367
- runInstaller(homeDirectory, []);
422
+ runInstaller(homeDirectory, [], environment);
368
423
 
369
- assert.equal(readFileSync(cursorSkillPath, 'utf8'), readFileSync(sourceSkillPath, 'utf8'));
370
- assert.equal(JSON.parse(readFileSync(manifestPath, 'utf8')).files.includes(cursorSkillPath), true);
371
- const codexGuidance = readFileSync(codexGuidancePath, 'utf8');
372
- assert.match(codexGuidance, /<!-- claude-dev-env deepen: start -->/);
373
- assert.match(codexGuidance, /CDE_DEEPEN:v1/);
424
+ mkdirSync(dirname(cursorSkillPath), { recursive: true });
425
+ writeFileSync(cursorSkillPath, '# deepen\n');
426
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
427
+ manifest.files.push(cursorSkillPath);
428
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
429
+ writeFileSync(codexGuidancePath, `${readFileSync(codexGuidancePath, 'utf8')}${RETIRED_DEEPEN_CODEX_BLOCK}`);
430
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
431
+ const reminderCommand = settings.hooks.UserPromptSubmit
432
+ .flatMap(group => group.hooks)
433
+ .map(hook => hook.command)
434
+ .find(command => command.includes('skill_loaded_reminder.py'));
435
+ settings.hooks.UserPromptSubmit.push({
436
+ matcher: '',
437
+ hooks: [{ type: 'command', command: reminderCommand.replace('skill_loaded_reminder.py', 'deepen_first_prompt.py'), timeout: 10 }],
438
+ });
439
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
440
+ const cursorHooksPath = join(homeDirectory, DEFAULT_CURSOR_DIRECTORY_NAME, 'hooks.json');
441
+ const cursorHooks = JSON.parse(readFileSync(cursorHooksPath, 'utf8'));
442
+ cursorHooks.hooks.beforeSubmitPrompt = [{ command: "python3 '/old/hooks/session/deepen_first_prompt.py'", timeout: 10 }];
443
+ writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2));
444
+ mkdirSync(retiredPluginVersion, { recursive: true });
374
445
 
375
- runInstaller(homeDirectory, ['--uninstall']);
446
+ runInstaller(homeDirectory, [], environment);
447
+
448
+ assertNoDeepenPieceInstalled(homeDirectory, cursorSkillPath, codexGuidancePath, settingsPath);
449
+ assert.match(readFileSync(fakeClaudeLogPath, 'utf8'), /^plugin uninstall deepen-first-prompt@claude-dev-env\r?$/m);
450
+ assert.equal(existsSync(retiredPluginVersion), false, 'the retired plugin cache is gone');
451
+
452
+ runInstaller(homeDirectory, [], environment);
376
453
 
377
- assert.equal(existsSync(cursorSkillPath), false);
378
- const remainingGuidance = existsSync(codexGuidancePath) ? readFileSync(codexGuidancePath, 'utf8') : '';
379
- assert.doesNotMatch(remainingGuidance, /claude-dev-env deepen/);
454
+ assertNoDeepenPieceInstalled(homeDirectory, cursorSkillPath, codexGuidancePath, settingsPath);
380
455
  } finally {
381
456
  rmSync(homeDirectory, { recursive: true, force: true });
382
457
  }
@@ -568,7 +643,7 @@ test('a full install writes only native Cursor hooks: AGENTS.md on sessionStart
568
643
  assert.equal(gateCommands.length, 1);
569
644
  assert.deepEqual(cursorHooks.hooks.stop.filter(entry => entry.command === userStopEntry.command), [userStopEntry]);
570
645
  assert.equal(execSync(gateCommands[0], { input: '{"conversation_id": "c-1", "status": "completed"}', encoding: 'utf8' }), '');
571
- assert.deepEqual(cursorHooks.hooks.beforeSubmitPrompt, [{ command: "python3 '/old/hooks/session/deepen_first_prompt.py'", timeout: 10 }]);
646
+ assert.equal(cursorHooks.hooks.beforeSubmitPrompt, undefined);
572
647
 
573
648
  runInstaller(homeDirectory, ['--uninstall']);
574
649
  const remainingHooks = JSON.parse(readFileSync(cursorHooksPath, 'utf8'));
package/bin/install.mjs CHANGED
@@ -52,19 +52,22 @@ import {
52
52
  PYTHON_PROBE_TIMEOUT_MILLISECONDS,
53
53
  ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS,
54
54
  ALL_CODEX_NATIVE_STOP_HOOK_SCRIPT_NAMES,
55
+ ALL_CODEX_NATIVE_POST_TOOL_USE_HOOK_SCRIPT_NAMES,
55
56
  } from './install-constants.mjs';
56
57
  import {
58
+ ALL_RETIRED_PLUGIN_SPECS,
57
59
  PSTACK_PLUGIN_SPEC,
58
- DEEPEN_FIRST_PROMPT_PLUGIN_SPEC,
59
60
  SUBAGENT_MODELS_PLUGIN_SPEC,
60
61
  USAGE_WRAPUP_PLUGIN_SPEC,
61
62
  installMarketplacePlugins,
63
+ removeRetiredMarketplacePlugins,
62
64
  shouldInstallMarketplacePlugin,
63
65
  } from './install-pstack-plugin.mjs';
64
66
  import {
65
67
  DEEPSTACK_PLUGIN_SPEC,
66
68
  installDeepStackForHosts,
67
69
  removeDeepStackCursorHooks,
70
+ removeRetiredCursorFirstTaskHook,
68
71
  } from './install-deepstack-plugin.mjs';
69
72
  import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
70
73
  import {
@@ -85,7 +88,6 @@ import {
85
88
  removeCodexPackageGuidance,
86
89
  removeCodexQuestionGuidance,
87
90
  writeCodexAgentsGuidance,
88
- writeCodexDeepenGuidance,
89
91
  writeCodexPackageGuidance,
90
92
  writeCodexQuestionGuidance,
91
93
  } from './codex-skill-load-block.mjs';
@@ -247,8 +249,8 @@ export const CORE_SKILLS = [
247
249
  'issue-tracker',
248
250
  'task-build',
249
251
  'eli5',
250
- 'deepen',
251
252
  'efficient-review',
253
+ 'pr-visual',
252
254
  ];
253
255
 
254
256
  export function collectPackageSourceConflicts(packageDirectory) {
@@ -1298,6 +1300,7 @@ export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
1298
1300
  'blocking/artifact_look_gate.py',
1299
1301
  'blocking/session_title_stop_gate.py',
1300
1302
  'session/issue_tracker_session_starter.py',
1303
+ 'session/deepen_first_prompt.py',
1301
1304
  ]);
1302
1305
 
1303
1306
  export const MOVED_HOOK_RELATIVE_PATHS = new Set([
@@ -1997,7 +2000,7 @@ export function pruneRetiredHookEntriesFromSettings(
1997
2000
  return removedCount;
1998
2001
  }
1999
2002
 
2000
- const RETIRED_HOOK_STAND_IN_MARKER = 'claude-dev-env retired hook stand-in';
2003
+ export const RETIRED_HOOK_STAND_IN_MARKER = 'claude-dev-env retired hook stand-in';
2001
2004
 
2002
2005
  const RETIRED_HOOK_STAND_IN_SOURCE = `"""${RETIRED_HOOK_STAND_IN_MARKER}.
2003
2006
 
@@ -2245,15 +2248,17 @@ function mergeHooksAtPath(
2245
2248
  return groupCount;
2246
2249
  }
2247
2250
 
2248
- function isCodexNativeStopHook(hook) {
2249
- const scriptName = String(hook?.command ?? '').replace(/\\/g, '/').split('/').pop();
2250
- return ALL_CODEX_NATIVE_STOP_HOOK_SCRIPT_NAMES.has(scriptName);
2251
+ function hookScriptName(hook) {
2252
+ return String(hook?.command ?? '').replace(/\\/g, '/').split('/').pop();
2251
2253
  }
2252
2254
 
2253
- function selectCodexNativeStopGroups(stopGroups) {
2254
- if (!Array.isArray(stopGroups)) return [];
2255
- return stopGroups
2256
- .map(group => ({ ...group, hooks: (group?.hooks ?? []).filter(isCodexNativeStopHook) }))
2255
+ function selectCodexNativeGroupsByScriptName(groups, allScriptNames) {
2256
+ if (!Array.isArray(groups)) return [];
2257
+ return groups
2258
+ .map(group => ({
2259
+ ...group,
2260
+ hooks: (group?.hooks ?? []).filter(hook => allScriptNames.has(hookScriptName(hook))),
2261
+ }))
2257
2262
  .filter(group => group.hooks.length > 0);
2258
2263
  }
2259
2264
 
@@ -2267,7 +2272,14 @@ function selectCodexNativeHooksConfig(hooksConfig) {
2267
2272
  ? preToolUseGroups.filter(group => ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS.has(group?.matcher))
2268
2273
  : [],
2269
2274
  UserPromptSubmit: Array.isArray(userPromptSubmitGroups) ? userPromptSubmitGroups : [],
2270
- Stop: selectCodexNativeStopGroups(hooksConfig.hooks?.Stop),
2275
+ PostToolUse: selectCodexNativeGroupsByScriptName(
2276
+ hooksConfig.hooks?.PostToolUse,
2277
+ ALL_CODEX_NATIVE_POST_TOOL_USE_HOOK_SCRIPT_NAMES,
2278
+ ),
2279
+ Stop: selectCodexNativeGroupsByScriptName(
2280
+ hooksConfig.hooks?.Stop,
2281
+ ALL_CODEX_NATIVE_STOP_HOOK_SCRIPT_NAMES,
2282
+ ),
2271
2283
  },
2272
2284
  };
2273
2285
  }
@@ -2561,10 +2573,12 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
2561
2573
  }
2562
2574
  const cursorInstallDirectory = INSTALL_ROOT_RESOLUTION.cursorInstallDirectory;
2563
2575
  const retiredCursorSyncManifestPath = join(cursorInstallDirectory, '.sync-manifest.json');
2564
- const isRetiredCursorRuleFile = priorFile => resolve(priorFile) === retiredCursorSyncManifestPath
2565
- || isInsideDirectory(resolve(priorFile), INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory);
2576
+ const retiredCursorSkillsDirectory = join(cursorInstallDirectory, MANAGED_SKILLS_DIRECTORY_NAME);
2577
+ const isRetiredCursorFile = priorFile => resolve(priorFile) === retiredCursorSyncManifestPath
2578
+ || isInsideDirectory(resolve(priorFile), INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory)
2579
+ || isInsideDirectory(resolve(priorFile), retiredCursorSkillsDirectory);
2566
2580
  const retiredCursorRulesOutcome = pruneStaleInstalledFiles(
2567
- priorInstalledFiles?.filter(isRetiredCursorRuleFile) ?? null,
2581
+ priorInstalledFiles?.filter(isRetiredCursorFile) ?? null,
2568
2582
  currentInstalledFiles,
2569
2583
  cursorInstallDirectory,
2570
2584
  join(backupRoot, 'cursor'),
@@ -3005,23 +3019,6 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
3005
3019
  }
3006
3020
  summary.skills = { created: skillsCreated, updated: skillsUpdated, pruned: 0, paths: skillPaths };
3007
3021
  allInstalledFiles.push(...skillPaths);
3008
- if (copiedSkillNames.has('deepen')) {
3009
- for (const sourceRoot of allSourceRoots) {
3010
- const deepenSourceDirectory = join(
3011
- resolvePackageManagedDirectory(sourceRoot, MANAGED_SKILLS_DIRECTORY_NAME),
3012
- 'deepen',
3013
- );
3014
- if (!existsSync(deepenSourceDirectory)) continue;
3015
- const cursorDeepenDirectory = join(
3016
- INSTALL_ROOT_RESOLUTION.cursorInstallDirectory,
3017
- 'skills',
3018
- 'deepen',
3019
- );
3020
- const cursorSkillStats = copyTree(deepenSourceDirectory, cursorDeepenDirectory);
3021
- allInstalledFiles.push(...cursorSkillStats.paths);
3022
- break;
3023
- }
3024
- }
3025
3022
  if (!selectedGroups) {
3026
3023
  summary.retiredSessionContinuity = {
3027
3024
  changedPaths: removeRetiredSessionContinuityHooks(),
@@ -3078,12 +3075,6 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
3078
3075
  claudeRoot: CLAUDE_HOME,
3079
3076
  codexHome: INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
3080
3077
  cursorDirectory: INSTALL_ROOT_RESOLUTION.cursorInstallDirectory,
3081
- firstTaskHookPath: join(
3082
- INSTALL_ROOT_RESOLUTION.hooksInstallDirectory,
3083
- 'session',
3084
- 'deepen_first_prompt.py',
3085
- ),
3086
- pythonCommand,
3087
3078
  formatPathArgument: commandArgumentFromPath,
3088
3079
  });
3089
3080
  summary.deepStackPlugin = deepStackPlugin;
@@ -3096,10 +3087,27 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
3096
3087
  allActionNeededLines.push(`DeepStack (cursor): failed — ${cursorOutcome.warning}`);
3097
3088
  }
3098
3089
  }
3090
+ if (!selectedGroups) {
3091
+ const retiredCursorHooksPath = removeRetiredCursorFirstTaskHook(
3092
+ INSTALL_ROOT_RESOLUTION.cursorInstallDirectory,
3093
+ );
3094
+ if (retiredCursorHooksPath) {
3095
+ console.log(` Retired first-task hook removed from ${retiredCursorHooksPath}`);
3096
+ }
3097
+ const retiredPlugins = removeRetiredMarketplacePlugins(ALL_RETIRED_PLUGIN_SPECS, {
3098
+ claudeRoot: CLAUDE_HOME,
3099
+ });
3100
+ summary.retiredPlugins = retiredPlugins;
3101
+ for (const eachIdentifier of retiredPlugins.removedIdentifiers) {
3102
+ console.log(` Retired plugin uninstalled: ${eachIdentifier}`);
3103
+ }
3104
+ for (const eachFailure of retiredPlugins.failedRemovals) {
3105
+ console.warn(` Warning: retired plugin ${eachFailure.pluginIdentifier} was not uninstalled: ${eachFailure.detail}`);
3106
+ }
3107
+ }
3099
3108
  const allRepositoryPluginSpecs = [
3100
3109
  USAGE_WRAPUP_PLUGIN_SPEC,
3101
3110
  SUBAGENT_MODELS_PLUGIN_SPEC,
3102
- DEEPEN_FIRST_PROMPT_PLUGIN_SPEC,
3103
3111
  ].filter(eachSpec => shouldInstallMarketplacePlugin(eachSpec));
3104
3112
  if (!selectedGroups && allRepositoryPluginSpecs.length > 0) {
3105
3113
  const repositoryPlugins = installMarketplacePlugins(allRepositoryPluginSpecs, {
@@ -3123,10 +3131,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
3123
3131
  readFileSync(join(PACKAGE_ROOT, 'rules', 'question-presentation.md'), 'utf8'),
3124
3132
  );
3125
3133
  if (questionGuidancePath) allCodexGuidancePaths.push(questionGuidancePath);
3126
- const deepenGuidancePath = writeCodexDeepenGuidance(
3127
- INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
3128
- );
3129
- if (deepenGuidancePath) allCodexGuidancePaths.push(deepenGuidancePath);
3134
+ removeCodexDeepenGuidance(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
3130
3135
  const solPromptPath = installCodexSolPrompt(
3131
3136
  INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
3132
3137
  join(PACKAGE_ROOT, SOL_PROMPT_DIRECTORY_NAME, SOL_PROMPT_FILE_NAME),
@@ -3676,7 +3681,6 @@ Usage:
3676
3681
  npx ${PACKAGE_NAME} --no-deepstack Full install without the DeepStack plugin (also CDE_INSTALL_DEEPSTACK=0)
3677
3682
  npx ${PACKAGE_NAME} --no-usage-wrapup Full install without the usage-wrapup plugin (also CDE_INSTALL_USAGE_WRAPUP=0)
3678
3683
  npx ${PACKAGE_NAME} --no-subagent-models Full install without the subagent-models plugin (also CDE_INSTALL_SUBAGENT_MODELS=0)
3679
- npx ${PACKAGE_NAME} --no-deepen-first-prompt Full install without the deepen-first-prompt plugin (also CDE_INSTALL_DEEPEN_FIRST_PROMPT=0)
3680
3684
  npx ${PACKAGE_NAME} --uninstall Remove installed files from the selected root
3681
3685
  npx ${PACKAGE_NAME} --help Show this help
3682
3686
 
@@ -0,0 +1,40 @@
1
+ import { strict as assert } from 'node:assert';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { test } from 'node:test';
8
+
9
+ const PACKAGE_DIRECTORY = dirname(dirname(fileURLToPath(import.meta.url)));
10
+ const SKILL_PATH_SEGMENTS = ['.agents', 'skills', 'pr-visual'];
11
+ const REQUIRED_FILES = ['SKILL.md', 'scripts/collect.py', 'scripts/render.py', 'scripts/metrics.py', 'scripts/verify.py', 'scripts/visual_config/config/constants.py', 'assets/viewer.html', 'assets/icons.symbols', 'fixtures/bar.scene.json', 'fixtures/integrated.scene.json', 'references/formats.md'];
12
+ const INSTALL_MODES = [[], ['--only', 'core']];
13
+ const NPM_EXECUTABLE = process.platform === 'win32' ? 'npm.cmd' : 'npm';
14
+
15
+ test('packed full and core installs ship the runnable PR visual skill to the shared agents home', () => {
16
+ const sandbox = mkdtempSync(join(tmpdir(), 'cde-pr-visual-'));
17
+ try {
18
+ const packed = JSON.parse(execFileSync(NPM_EXECUTABLE, ['pack', '--json', '--ignore-scripts', '--offline', '--cache', join(sandbox, 'npm-cache'), '--pack-destination', sandbox], { cwd: PACKAGE_DIRECTORY, encoding: 'utf8', shell: process.platform === 'win32' }));
19
+ execFileSync('tar', ['-xzf', join(sandbox, packed[0].filename), '-C', sandbox]);
20
+ const packaged = join(sandbox, 'package');
21
+ for (const [eachIndex, eachArguments] of INSTALL_MODES.entries()) {
22
+ const homeDirectory = join(sandbox, `home-${eachIndex}`);
23
+ execFileSync(process.execPath, [join(packaged, 'bin/install.mjs'), ...eachArguments], {
24
+ encoding: 'utf8',
25
+ env: { ...process.env, CLAUDE_CONFIG_DIR: undefined, LLM_SETTINGS_PROFILES_ROOT: undefined, HOME: homeDirectory, USERPROFILE: homeDirectory, CODEX_HOME: join(homeDirectory, '.codex'), GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'), CDE_INSTALL_PSTACK: '0', CDE_INSTALL_DEEPSTACK: '0', CDE_INSTALL_USAGE_WRAPUP: '0', CDE_INSTALL_SUBAGENT_MODELS: '0', CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0' },
26
+ });
27
+ const installed = join(homeDirectory, ...SKILL_PATH_SEGMENTS);
28
+ for (const eachFile of REQUIRED_FILES) assert.deepEqual(readFileSync(join(installed, eachFile)), readFileSync(join(packaged, ...SKILL_PATH_SEGMENTS, eachFile)));
29
+ assert.equal(realpathSync(join(homeDirectory, '.claude/skills/pr-visual')), realpathSync(installed));
30
+ const manifest = JSON.parse(readFileSync(join(homeDirectory, '.claude/.claude-dev-env-manifest.json'), 'utf8'));
31
+ assert.ok(manifest.skills.includes('pr-visual'));
32
+ const visual = join(sandbox, `visual-${eachIndex}.html`);
33
+ execFileSync(process.platform === 'win32' ? 'python' : 'python3', [join(installed, 'scripts/render.py'), join(installed, 'fixtures/integrated.scene.json'), '--out', visual]);
34
+ assert.ok(existsSync(visual));
35
+ assert.match(readFileSync(visual, 'utf8'), /Duration \(ms\)/);
36
+ }
37
+ } finally {
38
+ rmSync(sandbox, { recursive: true, force: true });
39
+ }
40
+ });
@@ -35,12 +35,11 @@ test('reinstall drops the retired style reminder and keeps one skill reminder ho
35
35
  mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/example/.claude', 'python');
36
36
 
37
37
  const userPromptHooks = settings.hooks.UserPromptSubmit.flatMap(eachGroup => eachGroup.hooks);
38
- assert.equal(userPromptHooks.length, 5);
38
+ assert.equal(userPromptHooks.length, 4);
39
39
  assert.equal(userPromptHooks[0].command, userCommand);
40
40
  assert.match(userPromptHooks[1].command, /session\/skill_loaded_reminder\.py$/);
41
- assert.match(userPromptHooks[2].command, /session\/deepen_first_prompt\.py$/);
42
- assert.match(userPromptHooks[3].command, /lifecycle\/nested_project_hooks\.py$/);
43
- assert.match(userPromptHooks[4].command, /blocking\/chat_reply_gate\.py$/);
41
+ assert.match(userPromptHooks[2].command, /lifecycle\/nested_project_hooks\.py$/);
42
+ assert.match(userPromptHooks[3].command, /blocking\/chat_reply_gate\.py$/);
44
43
  assert.ok(userPromptHooks.every(eachHook => !eachHook.command.includes('style_reminder_prompt')));
45
44
 
46
45
  const firstMerge = JSON.stringify(settings);
@@ -266,7 +266,6 @@ function resolveInstallerInvocation(homeDirectory) {
266
266
  CDE_INSTALL_DEEPSTACK: '0',
267
267
  CDE_INSTALL_USAGE_WRAPUP: '0',
268
268
  CDE_INSTALL_SUBAGENT_MODELS: '0',
269
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
270
269
  HOME: homeDirectory,
271
270
  USERPROFILE: homeDirectory,
272
271
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -28,7 +28,6 @@ function installInScratchHome(homeDirectory, argumentsList = [], extraEnvironmen
28
28
  CDE_INSTALL_DEEPSTACK: '0',
29
29
  CDE_INSTALL_USAGE_WRAPUP: '0',
30
30
  CDE_INSTALL_SUBAGENT_MODELS: '0',
31
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
32
31
  HOME: homeDirectory,
33
32
  USERPROFILE: homeDirectory,
34
33
  CODEX_HOME: join(homeDirectory, '.codex'),
@@ -186,7 +185,6 @@ test('a successful pstack install omits the retired skill block and gains the qu
186
185
  CDE_INSTALL_DEEPSTACK: '0',
187
186
  CDE_INSTALL_USAGE_WRAPUP: '0',
188
187
  CDE_INSTALL_SUBAGENT_MODELS: '0',
189
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
190
188
  CDE_CODEX_EXECUTABLE: commandPath,
191
189
  CDE_CLAUDE_EXECUTABLE: commandPath,
192
190
  });
@@ -216,7 +214,6 @@ test('enabling pstack after a pstack-off install preserves question guidance', (
216
214
  CDE_INSTALL_DEEPSTACK: '0',
217
215
  CDE_INSTALL_USAGE_WRAPUP: '0',
218
216
  CDE_INSTALL_SUBAGENT_MODELS: '0',
219
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
220
217
  CDE_CODEX_EXECUTABLE: commandPath,
221
218
  CDE_CLAUDE_EXECUTABLE: commandPath,
222
219
  });
@@ -247,7 +244,6 @@ test('a failed pstack transition restores question-only guidance and removes its
247
244
  CDE_INSTALL_DEEPSTACK: '0',
248
245
  CDE_INSTALL_USAGE_WRAPUP: '0',
249
246
  CDE_INSTALL_SUBAGENT_MODELS: '0',
250
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
251
247
  CDE_CODEX_EXECUTABLE: commandPath,
252
248
  CDE_CLAUDE_EXECUTABLE: commandPath,
253
249
  CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
@@ -70,7 +70,6 @@ function runInstallerInSandbox(sandboxHome, installerArguments = []) {
70
70
  CDE_INSTALL_DEEPSTACK: '0',
71
71
  CDE_INSTALL_USAGE_WRAPUP: '0',
72
72
  CDE_INSTALL_SUBAGENT_MODELS: '0',
73
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
74
73
  HOME: sandboxHome,
75
74
  USERPROFILE: sandboxHome,
76
75
  GIT_CONFIG_GLOBAL: gitConfigGlobal,
@@ -34,7 +34,6 @@ function installEnvironment(homeDirectory) {
34
34
  CDE_INSTALL_DEEPSTACK: '0',
35
35
  CDE_INSTALL_USAGE_WRAPUP: '0',
36
36
  CDE_INSTALL_SUBAGENT_MODELS: '0',
37
- CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
38
37
  };
39
38
  }
40
39