claude-dev-env 8.89.0 → 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 (86) 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-deepstack-plugin.mjs +42 -25
  30. package/bin/install-deepstack-plugin.test.mjs +37 -11
  31. package/bin/install-pstack-plugin.mjs +58 -11
  32. package/bin/install-pstack-plugin.test.mjs +54 -21
  33. package/bin/install.agents-home.test.mjs +0 -1
  34. package/bin/install.codex-rules.test.mjs +0 -2
  35. package/bin/install.cursor-rules.test.mjs +90 -26
  36. package/bin/install.mjs +30 -36
  37. package/bin/install.pr-visual.test.mjs +40 -0
  38. package/bin/install.prompt-context.test.mjs +3 -4
  39. package/bin/install.prune.test.mjs +0 -1
  40. package/bin/install.question-presentation.test.mjs +0 -4
  41. package/bin/install.settings-defaults.test.mjs +0 -1
  42. package/bin/install.shared-settings.test.mjs +0 -1
  43. package/bin/install.test.mjs +14 -44
  44. package/bin/install.transaction.test.mjs +0 -8
  45. package/bin/install.uninstall-transaction.test.mjs +0 -2
  46. package/bin/install.update-keeps-hooks.test.mjs +0 -1
  47. package/docs/BDD_SCENARIO_QUALITY.md +1 -1
  48. package/docs/BDD_TEST_LAYOUT.md +1 -1
  49. package/docs/CODE_RULES.md +1 -1
  50. package/docs/rule-guides/deepstack-wording.md +3 -5
  51. package/docs/rule-guides/session-title.md +5 -4
  52. package/hooks/blocking/session_title_turn_gate.py +287 -0
  53. package/hooks/blocking/test_session_title_format_gate.py +5 -0
  54. package/hooks/blocking/test_session_title_turn_gate.py +264 -0
  55. package/hooks/features/conduct-gates.md +3 -0
  56. package/hooks/features/session-context.md +0 -4
  57. package/hooks/hooks.json +5 -5
  58. package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
  59. package/hooks/hooks_constants/session_title_constants.py +41 -3
  60. package/hooks/lifecycle/nested_project_hooks.py +2 -2
  61. package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
  62. package/hooks/session/compaction_user_messages.py +2 -2
  63. package/package.json +2 -2
  64. package/rules/deepstack-wording.md +3 -3
  65. package/scripts/ci/install-playtest.mjs +58 -314
  66. package/scripts/ci/install-playtest.test.mjs +24 -6
  67. package/scripts/policy_lint/adapter_configuration.py +1 -0
  68. package/scripts/policy_lint/config/constants.py +1 -0
  69. package/scripts/test_deepstack_review_criterion.py +2 -3
  70. package/scripts/test_deepstack_wording_rule.py +6 -4
  71. package/scripts/tests/test_adapter_configuration.py +8 -0
  72. package/.agents/skills/deepen/SKILL.md +0 -26
  73. package/commands/deepen.md +0 -14
  74. package/docs/decisions/deepstack-gate.md +0 -32
  75. package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
  76. package/hooks/session/deepen_default_rewrite.py +0 -128
  77. package/hooks/session/deepen_first_prompt.py +0 -383
  78. package/hooks/session/deepen_role_gate.py +0 -171
  79. package/hooks/session/deepen_turn_marker.py +0 -75
  80. package/hooks/session/fixtures/deepstack_replay.json +0 -15
  81. package/hooks/session/grade_deepstack_replay.py +0 -78
  82. package/hooks/session/test_deepen_default_rewrite.py +0 -90
  83. package/hooks/session/test_deepen_first_prompt.py +0 -452
  84. package/hooks/session/test_deepen_role_gate.py +0 -83
  85. package/hooks/session/test_deepen_turn_marker.py +0 -59
  86. package/hooks/session/test_grade_deepstack_replay.py +0 -47
@@ -5,7 +5,8 @@
5
5
  * The driver installs this package into a scratch home, reads the hook roster
6
6
  * the install wrote into `settings.json`, starts one SessionStart hook and one
7
7
  * PreToolUse hook from that installed tree against fixture payloads, and reads
8
- * the output envelope each one returns.
8
+ * the output envelope each one returns. A last stage checks that the install
9
+ * left no deepen piece behind.
9
10
  *
10
11
  * Every stage writes the envelope it read to the evidence directory and prints
11
12
  * one line carrying the stage name, the artifact path and the artifact's
@@ -31,6 +32,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from
31
32
  import { tmpdir } from 'node:os';
32
33
  import { join } from 'node:path';
33
34
  import { pathToFileURL } from 'node:url';
35
+ import { RETIRED_HOOK_STAND_IN_MARKER } from '../../bin/install.mjs';
34
36
  import {
35
37
  createScratchHome,
36
38
  removeScratchHome,
@@ -54,59 +56,16 @@ export const PRE_TOOL_USE_EVENT_NAME = 'PreToolUse';
54
56
  export const SESSION_START_HOOK_SCRIPT_NAME = 'working_style_prompt.py';
55
57
  export const BASH_HOOK_SCRIPT_NAME = 'bash_pre_tool_use_dispatcher.py';
56
58
  export const BASH_MATCHER = 'Bash';
57
- export const USER_PROMPT_SUBMIT_EVENT_NAME = 'UserPromptSubmit';
58
- export const DEEPEN_HOOK_SCRIPT_NAME = 'deepen_first_prompt.py';
59
- export const NESTED_HOOK_SCRIPT_NAME = 'nested_project_hooks.py';
60
- export const DEEPEN_MARKER = 'CDE_DEEPEN:v1';
61
- export const DEEPEN_AGENT_MARKER = 'CDE_DEEPEN_AGENT';
62
- export const DEEPEN_STAGE_NAME = 'deepen_first_prompt';
63
- export const DEEPEN_SKIP_STAGE_NAME = 'deepen_skips';
64
- export const DEEPEN_REWRITE_STAGE_NAME = 'deepen_mod_text';
65
- export const DEEPEN_NESTED_STAGE_NAME = 'deepen_nested_home';
66
- export const DEEPEN_CLOUD_GAP_STAGE_NAME = 'deepen_cloud_gaps';
67
- export const DEEPEN_STUB_REWRITE = [
68
- 'SCOPE (role): Ship a contrast change the screen can show',
69
- 'EXAMPLES (illustration only): none',
70
- 'ZOOM-IN BLOCKER: none',
71
- 'Omitted: ship contrast tokens with the theme.',
72
- 'Source: src/settings/theme.ts',
73
- 'Methods: existing token file and a visual check.',
74
- 'Proof: screenshot of the settings page in dark mode.',
75
- ].join('\n');
76
- export const CLAUDE_SUBAGENT_PROMPT_PREFIX = (
77
- 'Before any other work, invoke the poteto-mode skill with the Skill tool. '
78
- + 'Every prompt you write for a subagent opens with the same instruction.'
79
- );
80
- export const CLOUD_GAP_EVIDENCE = {
81
- cursorCloud: {
82
- doc: 'https://cursor.com/docs/hooks',
83
- quotes: [
84
- 'User-level hooks (`~/.cursor/hooks.json`) are not available in cloud agents. Cloud agent VMs don\'t have access to your local home directory configuration.',
85
- 'sessionStart: Deferred while cloud agents can still start in a read-only environment. Hooks don\'t load there, so a cloud sessionStart would fire too late (after the first write) rather than at true session start.',
86
- 'beforeSubmitPrompt output is continue and user_message only.',
87
- ],
88
- path: 'the deepen skill under ~/.cursor/skills',
89
- },
90
- codexCloud: {
91
- doc: 'https://learn.chatgpt.com/docs/hooks',
92
- quotes: [
93
- 'Command/shell, prompt, and agent handlers; hooks from local configuration, plugins, or local directories; environment-scoped hooks; and SessionEnd MCP hooks are not supported with cloud orchestration, even when tools execute locally.',
94
- ],
95
- },
96
- claudeUserPromptSubmit: {
97
- doc: 'https://code.claude.com/docs/en/hooks',
98
- quotes: [
99
- 'UserPromptSubmit: can\'t replace the prompt; it only injects additionalContext alongside it',
100
- ],
101
- },
102
- claudePromptSubmitMod: {
103
- doc: 'https://code.claude.com/docs/en/plugins/mods/events',
104
- quotes: [
105
- 'Rewrite the prompt. The transcript and your prompt history show the new text. next({ ...e, text: newText })',
106
- 'A cloud session: Yes, for a plugin that reaches the cloud session. This package still keeps the Python hook as the cloud fallback because mods that replace settings hooks stay off when CLAUDE_CODE_REMOTE is true.',
107
- ],
108
- },
109
- };
59
+ export const RETIRED_DEEPEN_STAGE_NAME = 'retired_deepen_absent';
60
+ export const RETIRED_DEEPEN_HOOK_SCRIPT_NAME = 'deepen_first_prompt.py';
61
+ export const RETIRED_DEEPEN_CODEX_BLOCK_START = '<!-- claude-dev-env deepen: start -->';
62
+ export const ALL_RETIRED_DEEPEN_RELATIVE_PATHS = Object.freeze([
63
+ join('.claude', 'skills', 'deepen'),
64
+ join('.agents', 'skills', 'deepen'),
65
+ join('.claude', 'commands', 'deepen.md'),
66
+ join('.claude', 'hooks', 'session', RETIRED_DEEPEN_HOOK_SCRIPT_NAME),
67
+ join('.cursor', 'skills', 'deepen'),
68
+ ]);
110
69
 
111
70
  export const SESSION_START_PAYLOAD = {
112
71
  hook_event_name: SESSION_START_EVENT_NAME,
@@ -280,273 +239,68 @@ function runSessionStartStage(settingsPath, scratchHome, evidenceDirectory) {
280
239
  if (typeof envelope.additionalContext !== 'string' || envelope.additionalContext.length === 0) {
281
240
  throw new Error('The SessionStart envelope carries no additionalContext');
282
241
  }
283
- if (envelope.additionalContext.includes(DEEPEN_MARKER)) {
284
- throw new Error('The SessionStart envelope repeated first-task deepen text');
285
- }
286
242
  const evidencePath = writeEvidence(evidenceDirectory, SESSION_START_STAGE_NAME, JSON.stringify(envelope));
287
243
  reportStage(SESSION_START_STAGE_NAME, evidencePath);
288
244
  }
289
245
 
290
- function userPromptPayload(prompt, transcriptPath) {
291
- const payload = {
292
- hook_event_name: USER_PROMPT_SUBMIT_EVENT_NAME,
293
- prompt,
294
- session_id: 'playtest-session',
295
- };
296
- if (transcriptPath) payload.transcript_path = transcriptPath;
297
- return payload;
298
- }
299
-
300
- function deepenHookEnvironment(extraEnvironment = {}) {
301
- return {
302
- CDE_DEEPEN_REWRITE: DEEPEN_STUB_REWRITE,
303
- ...extraEnvironment,
304
- };
305
- }
306
-
307
- function parseJsonDecision(stdout) {
308
- const trimmed = stdout.trim();
309
- if (!trimmed) return { decision: 'silent' };
310
- return JSON.parse(trimmed);
246
+ function readTextWhenPresent(filePath) {
247
+ return existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
311
248
  }
312
249
 
313
- function runDeepenFirstTaskStage(settingsPath, scratchHome, evidenceDirectory) {
314
- const command = selectHookCommand(
315
- hookCommandsForEvent(settingsPath, USER_PROMPT_SUBMIT_EVENT_NAME),
316
- DEEPEN_HOOK_SCRIPT_NAME,
317
- );
318
- const transcriptPath = join(evidenceDirectory, 'first-task.jsonl');
319
- const extraEnvironment = deepenHookEnvironment();
320
- const envelopeResult = runInstalledHook(
321
- command,
322
- scratchHome,
323
- userPromptPayload('Add dark mode to the settings page', transcriptPath),
324
- extraEnvironment,
325
- );
326
- const envelope = readEnvelope(envelopeResult.stdout, USER_PROMPT_SUBMIT_EVENT_NAME);
327
- if (!String(envelope.additionalContext || '').includes(DEEPEN_MARKER)) {
328
- throw new Error('The UserPromptSubmit envelope carries no deepen marker');
329
- }
330
- if (!String(envelope.additionalContext).includes('src/settings/theme.ts')) {
331
- throw new Error('The UserPromptSubmit envelope omitted the stub source');
332
- }
333
- const evidencePath = writeEvidence(
334
- evidenceDirectory,
335
- DEEPEN_STAGE_NAME,
336
- JSON.stringify(envelope),
337
- );
338
- reportStage(DEEPEN_STAGE_NAME, evidencePath);
339
- return { command, transcriptPath };
250
+ /**
251
+ * Whether a path holds the inert stand-in the installer leaves for a stale registration.
252
+ *
253
+ * @param {string} absolutePath Path to read.
254
+ * @returns {boolean} True when the file carries the stand-in marker.
255
+ */
256
+ function isRetiredHookStandIn(absolutePath) {
257
+ return absolutePath.endsWith('.py') && readTextWhenPresent(absolutePath).includes(RETIRED_HOOK_STAND_IN_MARKER);
340
258
  }
341
259
 
342
- function runDeepenRewriteStage(command, scratchHome, evidenceDirectory) {
343
- const transcriptPath = join(evidenceDirectory, 'mod-text.jsonl');
344
- const hookResult = runInstalledHook(
345
- command,
346
- scratchHome,
347
- userPromptPayload('Add dark mode to the settings page', transcriptPath),
348
- deepenHookEnvironment({ CDE_DEEPEN_OUTPUT: 'rewrite' }),
349
- );
350
- const printed = hookResult.stdout;
351
- if (!printed.startsWith(DEEPEN_MARKER)) {
352
- throw new Error(`The rewrite output did not start with ${DEEPEN_MARKER}`);
353
- }
354
- if (printed.includes('hookSpecificOutput')) {
355
- throw new Error('The Claude mod rewrite printed a command-hook envelope');
260
+ /**
261
+ * Check that the install left no deepen piece behind.
262
+ *
263
+ * The deepen skill, the /deepen command, the first-task hook and its
264
+ * registrations in Claude, Codex and Cursor, and the Codex AGENTS.md block
265
+ * were all retired. Any one of them in the scratch home fails the stage.
266
+ *
267
+ * @param {string} scratchHome Scratch home the install wrote into.
268
+ * @param {string} evidenceDirectory Directory the stage writes its evidence to.
269
+ * @returns {void}
270
+ * @throws {Error} The install left a deepen piece.
271
+ */
272
+ function runRetiredDeepenStage(scratchHome, evidenceDirectory) {
273
+ const allLeftoverPaths = ALL_RETIRED_DEEPEN_RELATIVE_PATHS
274
+ .map((relativePath) => join(scratchHome, relativePath))
275
+ .filter((absolutePath) => existsSync(absolutePath))
276
+ .filter((absolutePath) => !isRetiredHookStandIn(absolutePath));
277
+ if (allLeftoverPaths.length > 0) {
278
+ throw new Error(`The install left retired deepen paths: ${allLeftoverPaths.join(', ')}`);
356
279
  }
357
- const evidencePath = writeEvidence(
358
- evidenceDirectory,
359
- DEEPEN_REWRITE_STAGE_NAME,
360
- JSON.stringify({ stdout: printed }),
361
- );
362
- reportStage(DEEPEN_REWRITE_STAGE_NAME, evidencePath);
363
- }
364
-
365
- function runDeepenSkipStage(command, scratchHome, evidenceDirectory, firstTranscriptPath) {
366
- const jsonEnvironment = deepenHookEnvironment({ CDE_DEEPEN_OUTPUT: 'json' });
367
- const cases = [
368
- {
369
- name: 'thanks',
370
- payload: userPromptPayload('thanks', join(evidenceDirectory, 'thanks.jsonl')),
371
- extraEnvironment: jsonEnvironment,
372
- expected: { decision: 'skip', reason: 'trivial' },
373
- },
374
- {
375
- name: 'slash',
376
- payload: userPromptPayload('/sr-loop', join(evidenceDirectory, 'slash.jsonl')),
377
- extraEnvironment: jsonEnvironment,
378
- expected: { decision: 'skip', reason: 'slash-command' },
379
- },
380
- {
381
- name: 'spawn',
382
- payload: userPromptPayload(
383
- `${CLAUDE_SUBAGENT_PROMPT_PREFIX}\n\nReply leaf.`,
384
- join(evidenceDirectory, 'spawn.jsonl'),
385
- ),
386
- extraEnvironment: jsonEnvironment,
387
- expected: { decision: 'skip', reason: 'spawn-rewrite' },
388
- },
389
- {
390
- name: 'skip-flag',
391
- payload: userPromptPayload('Add dark mode to the settings page', join(evidenceDirectory, 'skip-flag.jsonl')),
392
- extraEnvironment: deepenHookEnvironment({ CDE_DEEPEN_OUTPUT: 'json', CDE_SKIP_DEEPEN: '1' }),
393
- expected: { decision: 'skip', reason: 'skip-flag' },
394
- },
395
- {
396
- name: 'later-turn',
397
- payload: userPromptPayload('and also the billing page', firstTranscriptPath),
398
- extraEnvironment: jsonEnvironment,
399
- expected: { decision: 'skip', reason: 'later-turn' },
400
- },
401
- {
402
- name: 'marker',
403
- payload: userPromptPayload(`${DEEPEN_MARKER}\n\nDo the job.`, join(evidenceDirectory, 'marker.jsonl')),
404
- extraEnvironment: jsonEnvironment,
405
- expected: { decision: 'skip', reason: 'marker-present' },
406
- },
407
- {
408
- name: 'loop',
409
- payload: userPromptPayload('/loop until tests pass', join(evidenceDirectory, 'loop.jsonl')),
410
- extraEnvironment: jsonEnvironment,
411
- expected: { decision: 'skip', reason: 'loop-or-helper' },
412
- },
413
- {
414
- name: 'deepen-agent',
415
- payload: userPromptPayload(
416
- `${DEEPEN_AGENT_MARKER}\nAnswer the four deepening questions.`,
417
- join(evidenceDirectory, 'agent.jsonl'),
418
- ),
419
- extraEnvironment: jsonEnvironment,
420
- expected: { decision: 'skip', reason: 'deepen-agent' },
421
- },
280
+ const allHookConfigurationPaths = [
281
+ join(scratchHome, SETTINGS_RELATIVE_PATH),
282
+ join(scratchHome, '.codex', 'hooks.json'),
283
+ join(scratchHome, '.cursor', 'hooks.json'),
422
284
  ];
423
- const results = [];
424
- for (const eachCase of cases) {
425
- const hookResult = runInstalledHook(
426
- command,
427
- scratchHome,
428
- eachCase.payload,
429
- eachCase.extraEnvironment,
430
- );
431
- const decision = parseJsonDecision(hookResult.stdout);
432
- if (decision.decision !== eachCase.expected.decision || decision.reason !== eachCase.expected.reason) {
433
- throw new Error(
434
- `Skip ${eachCase.name} returned ${JSON.stringify(decision)} expected ${JSON.stringify(eachCase.expected)}`,
435
- );
436
- }
437
- results.push({ name: eachCase.name, decision });
438
- }
439
- const silentThanks = runInstalledHook(
440
- command,
441
- scratchHome,
442
- userPromptPayload('thanks', join(evidenceDirectory, 'thanks-silent.jsonl')),
443
- deepenHookEnvironment(),
444
- );
445
- if (silentThanks.stdout.trim() !== '') {
446
- throw new Error('A skipped prompt printed command-hook output');
447
- }
448
- const evidencePath = writeEvidence(
449
- evidenceDirectory,
450
- DEEPEN_SKIP_STAGE_NAME,
451
- JSON.stringify({ results, silentThanks: silentThanks.stdout }),
452
- );
453
- reportStage(DEEPEN_SKIP_STAGE_NAME, evidencePath);
454
- }
455
-
456
- function runDeepenNestedStage(settingsPath, scratchHome, evidenceDirectory) {
457
- const nestedCommand = selectHookCommand(
458
- hookCommandsForEvent(settingsPath, USER_PROMPT_SUBMIT_EVENT_NAME),
459
- NESTED_HOOK_SCRIPT_NAME,
460
- );
461
- const deepenCommand = selectHookCommand(
462
- hookCommandsForEvent(settingsPath, USER_PROMPT_SUBMIT_EVENT_NAME),
463
- DEEPEN_HOOK_SCRIPT_NAME,
464
- );
465
- const sessionDirectory = join(evidenceDirectory, 'nested-session');
466
- const checkoutDirectory = join(sessionDirectory, 'alpha');
467
- mkdirSync(join(checkoutDirectory, '.git'), { recursive: true });
468
- mkdirSync(join(checkoutDirectory, '.claude'), { recursive: true });
469
- writeFileSync(
470
- join(checkoutDirectory, '.claude', 'settings.json'),
471
- `${JSON.stringify({
472
- hooks: {
473
- UserPromptSubmit: [
474
- {
475
- matcher: '',
476
- hooks: [{ type: 'command', command: deepenCommand, timeout: 10 }],
477
- },
478
- ],
479
- },
480
- }, null, 2)}\n`,
481
- );
482
- const hookResult = spawnSync(nestedCommand, {
483
- shell: true,
484
- input: JSON.stringify(userPromptPayload(
485
- 'Add dark mode to the settings page',
486
- join(evidenceDirectory, 'nested.jsonl'),
487
- )),
488
- encoding: 'utf8',
489
- cwd: sessionDirectory,
490
- timeout: HOOK_TIMEOUT_MILLISECONDS,
491
- env: {
492
- ...scratchHomeEnvironment(scratchHome),
493
- ...deepenHookEnvironment(),
494
- CLAUDE_PROJECT_DIR: sessionDirectory,
495
- },
496
- });
497
- const envelope = readEnvelope(hookResult.stdout, USER_PROMPT_SUBMIT_EVENT_NAME);
498
- if (!String(envelope.additionalContext || '').includes(DEEPEN_MARKER)) {
499
- throw new Error('The nested home-folder session carried no deepen marker');
500
- }
501
- const evidencePath = writeEvidence(
502
- evidenceDirectory,
503
- DEEPEN_NESTED_STAGE_NAME,
504
- JSON.stringify(envelope),
505
- );
506
- reportStage(DEEPEN_NESTED_STAGE_NAME, evidencePath);
507
- }
508
-
509
- function runDeepenInstallSurfaceStage(scratchHome, evidenceDirectory) {
510
- const settingsPath = join(scratchHome, SETTINGS_RELATIVE_PATH);
511
- const userPromptCommands = hookCommandsForEvent(settingsPath, USER_PROMPT_SUBMIT_EVENT_NAME);
512
- if (!userPromptCommands.some((each) => each.command.includes(DEEPEN_HOOK_SCRIPT_NAME))) {
513
- throw new Error('The installed Claude settings declare no deepen_first_prompt.py');
514
- }
515
- const codexHooksPath = join(scratchHome, '.codex', 'hooks.json');
516
- const codexHooks = JSON.parse(readFileSync(codexHooksPath, 'utf8'));
517
- const codexUserPrompt = JSON.stringify(codexHooks.hooks?.UserPromptSubmit ?? []);
518
- if (!codexUserPrompt.includes(DEEPEN_HOOK_SCRIPT_NAME)) {
519
- throw new Error('The installed Codex hooks declare no UserPromptSubmit deepen_first_prompt.py');
520
- }
521
- const cursorSkillPath = join(scratchHome, '.cursor', 'skills', 'deepen', 'SKILL.md');
522
- if (!existsSync(cursorSkillPath)) {
523
- throw new Error(`The install wrote no ${cursorSkillPath}`);
285
+ const allRegisteringPaths = allHookConfigurationPaths
286
+ .filter((configurationPath) => readTextWhenPresent(configurationPath).includes(RETIRED_DEEPEN_HOOK_SCRIPT_NAME));
287
+ if (allRegisteringPaths.length > 0) {
288
+ throw new Error(`The install registered ${RETIRED_DEEPEN_HOOK_SCRIPT_NAME} in ${allRegisteringPaths.join(', ')}`);
524
289
  }
525
290
  const codexAgentsPath = join(scratchHome, '.codex', 'AGENTS.md');
526
- const codexAgentsText = existsSync(codexAgentsPath) ? readFileSync(codexAgentsPath, 'utf8') : '';
527
- if (!codexAgentsText.includes('<!-- claude-dev-env deepen: start -->')) {
528
- throw new Error(`The install wrote no deepen block into ${codexAgentsPath}`);
291
+ if (readTextWhenPresent(codexAgentsPath).includes(RETIRED_DEEPEN_CODEX_BLOCK_START)) {
292
+ throw new Error(`The install wrote the deepen block into ${codexAgentsPath}`);
529
293
  }
530
294
  const evidencePath = writeEvidence(
531
295
  evidenceDirectory,
532
- 'deepen_install_surfaces',
296
+ RETIRED_DEEPEN_STAGE_NAME,
533
297
  JSON.stringify({
534
- claudeUserPromptSubmit: userPromptCommands.map((each) => each.command),
535
- codexUserPromptSubmit: codexHooks.hooks.UserPromptSubmit,
536
- cursorSkillPath,
298
+ checkedPaths: ALL_RETIRED_DEEPEN_RELATIVE_PATHS,
299
+ checkedHookConfigurations: allHookConfigurationPaths,
537
300
  codexAgentsPath,
538
301
  }),
539
302
  );
540
- reportStage('deepen_install_surfaces', evidencePath);
541
- }
542
-
543
- function runDeepenCloudGapStage(evidenceDirectory) {
544
- const evidencePath = writeEvidence(
545
- evidenceDirectory,
546
- DEEPEN_CLOUD_GAP_STAGE_NAME,
547
- JSON.stringify(CLOUD_GAP_EVIDENCE),
548
- );
549
- reportStage(DEEPEN_CLOUD_GAP_STAGE_NAME, evidencePath);
303
+ reportStage(RETIRED_DEEPEN_STAGE_NAME, evidencePath);
550
304
  }
551
305
 
552
306
  function runBlockingStage(settingsPath, scratchHome, evidenceDirectory) {
@@ -592,18 +346,8 @@ export function runPlaytest(argv) {
592
346
  runSessionStartStage(settingsPath, scratchHome, evidenceDirectory);
593
347
  stageName = BLOCKING_STAGE_NAME;
594
348
  runBlockingStage(settingsPath, scratchHome, evidenceDirectory);
595
- stageName = 'deepen_install_surfaces';
596
- runDeepenInstallSurfaceStage(scratchHome, evidenceDirectory);
597
- stageName = DEEPEN_STAGE_NAME;
598
- const deepen = runDeepenFirstTaskStage(settingsPath, scratchHome, evidenceDirectory);
599
- stageName = DEEPEN_REWRITE_STAGE_NAME;
600
- runDeepenRewriteStage(deepen.command, scratchHome, evidenceDirectory);
601
- stageName = DEEPEN_SKIP_STAGE_NAME;
602
- runDeepenSkipStage(deepen.command, scratchHome, evidenceDirectory, deepen.transcriptPath);
603
- stageName = DEEPEN_NESTED_STAGE_NAME;
604
- runDeepenNestedStage(settingsPath, scratchHome, evidenceDirectory);
605
- stageName = DEEPEN_CLOUD_GAP_STAGE_NAME;
606
- runDeepenCloudGapStage(evidenceDirectory);
349
+ stageName = RETIRED_DEEPEN_STAGE_NAME;
350
+ runRetiredDeepenStage(scratchHome, evidenceDirectory);
607
351
  } catch (error) {
608
352
  reportFailure(stageName, error instanceof Error ? error.message : String(error));
609
353
  return 1;
@@ -7,6 +7,7 @@ import test from 'node:test';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
9
  import { parseArguments, readEnvelope, selectHookCommand } from './install-playtest.mjs';
10
+ import { RETIRED_HOOK_STAND_IN_MARKER } from '../../bin/install.mjs';
10
11
  import { SANDBOX_GIT_CONFIG_BODY, SANDBOX_GIT_CONFIG_NAME } from './scratch-home-install.mjs';
11
12
 
12
13
  const CI_DIRECTORY = dirname(fileURLToPath(import.meta.url));
@@ -30,12 +31,7 @@ test('the driver reads an envelope from every stage of a fresh install, and repo
30
31
  assert.match(greenRun.stdout, /\[PASS\] playtest install .* sha256=[0-9a-f]{64}/);
31
32
  assert.match(greenRun.stdout, /\[PASS\] playtest session_start_hook .* sha256=[0-9a-f]{64}/);
32
33
  assert.match(greenRun.stdout, /\[PASS\] playtest blocking_hook .* sha256=[0-9a-f]{64}/);
33
- assert.match(greenRun.stdout, /\[PASS\] playtest deepen_install_surfaces .* sha256=[0-9a-f]{64}/);
34
- assert.match(greenRun.stdout, /\[PASS\] playtest deepen_first_prompt .* sha256=[0-9a-f]{64}/);
35
- assert.match(greenRun.stdout, /\[PASS\] playtest deepen_mod_text .* sha256=[0-9a-f]{64}/);
36
- assert.match(greenRun.stdout, /\[PASS\] playtest deepen_skips .* sha256=[0-9a-f]{64}/);
37
- assert.match(greenRun.stdout, /\[PASS\] playtest deepen_nested_home .* sha256=[0-9a-f]{64}/);
38
- assert.match(greenRun.stdout, /\[PASS\] playtest deepen_cloud_gaps .* sha256=[0-9a-f]{64}/);
34
+ assert.match(greenRun.stdout, /\[PASS\] playtest retired_deepen_absent .* sha256=[0-9a-f]{64}/);
39
35
  const blockingEnvelope = JSON.parse(readFileSync(join(evidenceDirectory, 'blocking_hook.json'), 'utf8'));
40
36
  assert.equal(Object.hasOwn(blockingEnvelope, 'permissionDecision'), false);
41
37
  assert.ok(
@@ -90,3 +86,25 @@ test('readEnvelope refuses output that names another event or no envelope at all
90
86
  /names PreToolUse rather than SessionStart/,
91
87
  );
92
88
  });
89
+
90
+ test('the retired deepen stage accepts the installer stand-in and fails on a restored deepen hook', () => {
91
+ const scratchHome = mkdtempSync(join(tmpdir(), 'cde-playtest-home-'));
92
+ const evidenceDirectory = mkdtempSync(join(tmpdir(), 'cde-playtest-evidence-'));
93
+ const retiredHookPath = join(scratchHome, '.claude', 'hooks', 'session', 'deepen_first_prompt.py');
94
+ try {
95
+ const installRun = runDriver(['--home', scratchHome, '--evidence', evidenceDirectory]);
96
+ assert.equal(installRun.status, 0, `${installRun.stdout}\n${installRun.stderr}`);
97
+
98
+ writeFileSync(retiredHookPath, `"""${RETIRED_HOOK_STAND_IN_MARKER}."""\n`);
99
+ const standInRun = runDriver(['--home', scratchHome, '--evidence', evidenceDirectory, '--skip-install']);
100
+ assert.match(standInRun.stdout, /\[PASS\] playtest retired_deepen_absent/);
101
+
102
+ writeFileSync(retiredHookPath, SILENT_HOOK_SOURCE);
103
+ const restoredRun = runDriver(['--home', scratchHome, '--evidence', evidenceDirectory, '--skip-install']);
104
+ assert.equal(restoredRun.status, 1, `${restoredRun.stdout}\n${restoredRun.stderr}`);
105
+ assert.match(restoredRun.stdout, /\[FAIL\] playtest retired_deepen_absent The install left retired deepen paths/);
106
+ } finally {
107
+ rmSync(scratchHome, { recursive: true, force: true });
108
+ rmSync(evidenceDirectory, { recursive: true, force: true });
109
+ }
110
+ });
@@ -140,6 +140,7 @@ def _names_exempt_registration_path(registered_string: str) -> bool:
140
140
  hooks/blocking/merge_over_review_offer_gate.py -> exempt
141
141
  hooks/blocking/final_reply_gate.py -> exempt
142
142
  hooks/blocking/open_tasks_stop_gate.py -> exempt
143
+ hooks/blocking/session_title_turn_gate.py -> exempt
143
144
  hooks/blocking/some_new_blocker.py -> flagged
144
145
 
145
146
  The Bash PreToolUse dispatcher sits under ``blocking/`` for layout reasons
@@ -79,6 +79,7 @@ ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
79
79
  "blocking/merge_over_review_offer_gate.py",
80
80
  "blocking/final_reply_gate.py",
81
81
  "blocking/open_tasks_stop_gate.py",
82
+ "blocking/session_title_turn_gate.py",
82
83
  )
83
84
  ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
84
85
  ALL_TEST_FILE_PREFIXES = ("test_",)
@@ -7,9 +7,8 @@ from pathlib import Path
7
7
  _RULES = Path(__file__).resolve().parent.parent / "docs" / "CODE_RULES.md"
8
8
 
9
9
 
10
- def test_new_instruction_criterion_cites_the_gate() -> None:
10
+ def test_new_instruction_criterion_names_the_manifest() -> None:
11
11
  text = _RULES.read_text(encoding="utf-8")
12
12
  assert "## 11.7 A NEW INSTRUCTION NAMES THE TRIGGER THAT COVERS IT" in text
13
- assert "hooks/session/deepen_first_prompt.py" in text
14
13
  assert "rules-archived/ARCHIVE-MANIFEST.md" in text
15
- assert "DeepStack" in text
14
+ assert "deepen_first_prompt.py" not in text
@@ -1,4 +1,4 @@
1
- """The wording rule cites the first-task gate."""
1
+ """The wording rule keeps the three DeepStack labels and cites no retired gate."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -7,13 +7,15 @@ from pathlib import Path
7
7
  _PACKAGE_ROOT = Path(__file__).resolve().parent.parent
8
8
 
9
9
 
10
- def test_wording_rule_cites_the_gate() -> None:
10
+ def test_wording_rule_keeps_the_labels_and_drops_the_retired_gate() -> None:
11
11
  rule_text = (_PACKAGE_ROOT / "rules" / "deepstack-wording.md").read_text(encoding="utf-8")
12
12
  guide_text = (_PACKAGE_ROOT / "docs" / "rule-guides" / "deepstack-wording.md").read_text(
13
13
  encoding="utf-8"
14
14
  )
15
- assert "hooks/session/deepen_first_prompt.py" in rule_text
16
- assert "hooks/session/deepen_first_prompt.py" in guide_text
15
+ for each_text in (rule_text, guide_text):
16
+ assert "deepen_first_prompt.py" not in each_text
17
+ assert "CDE_DEEPEN" not in each_text
18
+ assert "**Enforcement:** none." in rule_text
17
19
  assert "SCOPE (role):" in guide_text
18
20
  assert "EXAMPLES (illustration only):" in guide_text
19
21
  assert "ZOOM-IN BLOCKER:" in guide_text
@@ -138,6 +138,14 @@ def test_new_session_title_gate_registration_is_clean(tmp_path: Path) -> None:
138
138
  assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
139
139
 
140
140
 
141
+ def test_new_session_title_turn_gate_registration_is_clean(tmp_path: Path) -> None:
142
+ current_document = _hook_document(
143
+ ["hooks/blocking/session_title_turn_gate.py"],
144
+ '{"hooks": {}}',
145
+ )
146
+ assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
147
+
148
+
141
149
  def test_new_artifact_page_gate_registration_is_clean(tmp_path: Path) -> None:
142
150
  current_document = _hook_document(
143
151
  ["hooks/blocking/artifact_dark_mode_gate.py"],
@@ -1,26 +0,0 @@
1
- ---
2
- name: deepen
3
- description: >-
4
- Deepen a first user task before work starts. Triggers: /deepen, deepen this
5
- task, first-task deepening, Cursor Cloud launch prompt. DeepStack intake
6
- holds the contract.
7
- ---
8
-
9
- # deepen
10
-
11
- On a first task, `/deepen` plus a task, or a routine wake that has work, read DeepStack intake and follow it: [intake](https://github.com/jl-cmd-projects/DeepStack/blob/main/plugins/deepstack/skills/deepstack-mode/references/intake.md).
12
-
13
- Skip when the prompt already carries `CDE_DEEPEN:v1`, when the text is a thanks or yes/no or status line, when it is a slash command other than `/deepen`, when it is a `/loop` or helper report, or when it is a spawn rewrite that already starts with the poteto-mode prefix.
14
-
15
- A prompt that is only `/deepen` with no task is skipped. `/deepen` plus a task deepens the remainder.
16
-
17
- The deepening pass writes the worker prompt and stops. The worker runs that prompt.
18
-
19
- When you spawn a helper to fill the four answers, that helper's prompt starts with `CDE_DEEPEN_AGENT` so the first-task hook leaves it alone.
20
-
21
- ## Hosts
22
-
23
- - Claude desktop and CLI: a `prompt.submit` mod rewrites the visible text. The Python `UserPromptSubmit` hook is silent when that mod is on outside a cloud session.
24
- - Claude cloud and Codex local: the Python hook injects `additionalContext`. Command hooks cannot replace the prompt string.
25
- - Cursor Cloud: run DeepStack intake on the launch prompt. The host adapter for the three lines and the refusal lives with that host. This package keeps the gate.
26
- - Codex cloud orchestration skips command hooks. Local Codex only.
@@ -1,14 +0,0 @@
1
- ---
2
- description: Deepen a task into the standing job before work starts
3
- argument-hint: [task to deepen]
4
- ---
5
-
6
- Deepen this task before any work starts: `$ARGUMENTS`
7
-
8
- When `$ARGUMENTS` is empty, ask the user to paste the task and stop.
9
-
10
- When `$ARGUMENTS` holds a task, follow DeepStack intake at https://github.com/jl-cmd-projects/DeepStack/blob/main/plugins/deepstack/skills/deepstack-mode/references/intake.md.
11
-
12
- Write `SCOPE (role):`, `EXAMPLES (illustration only):`, and `ZOOM-IN BLOCKER:` before any plan. Put `CDE_DEEPEN:v1` on line 1 of the worker prompt and carry the scope line there. The deepening pass writes that prompt and stops.
13
-
14
- Skip this command when the prompt already carries `CDE_DEEPEN:v1`.
@@ -1,32 +0,0 @@
1
- # DeepStack gate
2
-
3
- The first-task hook is the control. A rewrite ships only when `refusal_reason` in `hooks/session/deepen_role_gate.py` returns no reason. DeepStack, the wording rule, and the review rule cite this function. They do not restate it.
4
-
5
- ## What the unchanged hook did
6
-
7
- Recorded before this gate edited `deepen_first_prompt.py`, with `CDE_DEEPEN_OUTPUT=json` on stdin.
8
-
9
- The paste path, a first task and no rewrite stub, printed `decision` `deepen`. The rewritten prompt restated the four question stems and ended by telling the same agent to do the job.
10
-
11
- The filled four-answer stub printed `decision` `deepen`. The rewritten prompt carried the four answer lines and no role lines.
12
-
13
- ## What the gate refuses
14
-
15
- | Reason | When |
16
- |---|---|
17
- | `restates-four-questions` | The rewrite contains every canonical question stem. The old paste path was this case. |
18
- | `missing-role-lines` | `SCOPE (role):`, `EXAMPLES (illustration only):`, or `ZOOM-IN BLOCKER:` is missing or blank. |
19
- | `example-noun-in-scope` | The scope line contains an example noun and `ZOOM-IN BLOCKER:` is `none`. |
20
- | `dropped-hard-constraint` | A bullet under `HARD CONSTRAINTS` in the task is absent as its own line in the rewrite. |
21
-
22
- Json mode prints `{"decision": "refuse", "reason": "..."}`. Rewrite mode and the command-hook path print nothing, so the prompt-submit mod leaves the submitted text in place.
23
-
24
- ## The default rewrite
25
-
26
- With no rewrite stub and no `CDE_DEEPEN_COMMAND`, `hooks/session/deepen_default_rewrite.py` builds the rewrite. It takes the scope from the task's own sentences, moves clauses the task marks as examples (`for example`, `for instance`, `e.g.`, `such as`) to the examples line, clips a scope over 240 characters with a pointer to the full task, copies the task with its hard constraints, and ends with one order to do the job. When the scope still names an example, the blocker line asks the worker to confirm before narrowing to it. The gate checks this rewrite like any other, so a first task deepens on every host with nothing configured.
27
-
28
- ## Proof shape
29
-
30
- Run the hook on stdin with `CDE_DEEPEN_OUTPUT=json`. A stub that pastes the four question stems refuses, and the default rewrite deepens. A role rewrite that leaves the example noun out deepens. Removing `refusal_reason` from `decide_first_prompt` lets the paste path deepen again, and `test_should_refuse_the_paste_path_as_the_known_bad` fails. The wrong twin puts the example noun in the scope line with `ZOOM-IN BLOCKER: none` and is refused.
31
-
32
- `hooks/session/grade_deepstack_replay.py` scores a fixture whose task names one failure as an example. It calls `refusal_reason` and does not rewrite.