claude-dev-env 8.82.0 → 8.83.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 (40) hide show
  1. package/.agents/skills/deepen/SKILL.md +42 -0
  2. package/bin/codex-skill-load-block.mjs +32 -0
  3. package/bin/codex-skill-load-block.test.mjs +27 -0
  4. package/bin/codex-trimmed-sol.test.mjs +1 -0
  5. package/bin/ever-shipped-skills.mjs +1 -0
  6. package/bin/install-pstack-plugin.mjs +12 -0
  7. package/bin/install-pstack-plugin.test.mjs +23 -0
  8. package/bin/install.agents-home.test.mjs +1 -0
  9. package/bin/install.codex-rules.test.mjs +2 -0
  10. package/bin/install.cursor-rules.test.mjs +38 -1
  11. package/bin/install.mjs +34 -2
  12. package/bin/install.prompt-context.test.mjs +3 -1
  13. package/bin/install.prune.test.mjs +1 -0
  14. package/bin/install.question-presentation.test.mjs +4 -0
  15. package/bin/install.settings-defaults.test.mjs +1 -0
  16. package/bin/install.shared-settings.test.mjs +1 -0
  17. package/bin/install.test.mjs +34 -1
  18. package/bin/install.transaction.test.mjs +8 -0
  19. package/bin/install.uninstall-transaction.test.mjs +2 -0
  20. package/bin/install.update-keeps-hooks.test.mjs +1 -0
  21. package/commands/deepen.md +19 -0
  22. package/hooks/features/lifecycle-cleanup.md +1 -1
  23. package/hooks/features/session-context.md +3 -0
  24. package/hooks/hooks.json +10 -0
  25. package/hooks/hooks_constants/deepen_first_prompt_constants.py +119 -0
  26. package/hooks/hooks_constants/mod_handoff_constants.py +2 -0
  27. package/hooks/hooks_constants/nested_project_hooks_constants.py +2 -0
  28. package/hooks/lifecycle/nested_project_hooks.py +17 -8
  29. package/hooks/lifecycle/test_nested_project_hooks.py +50 -0
  30. package/hooks/session/deepen_first_prompt.py +390 -0
  31. package/hooks/session/test_deepen_first_prompt.py +274 -0
  32. package/package.json +1 -1
  33. package/scripts/account_broker_support.py +5 -4
  34. package/scripts/ci/install-playtest.mjs +329 -2
  35. package/scripts/ci/install-playtest.test.mjs +6 -0
  36. package/scripts/claude_chain_usage.py +28 -2
  37. package/scripts/dev_env_scripts_constants/account_broker_constants.py +27 -1
  38. package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +6 -0
  39. package/scripts/test_account_broker_support.py +35 -3
  40. package/scripts/test_claude_chain_usage.py +26 -3
@@ -76,6 +76,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
76
76
  CDE_INSTALL_PSTACK: '0',
77
77
  CDE_INSTALL_USAGE_WRAPUP: '0',
78
78
  CDE_INSTALL_SUBAGENT_MODELS: '0',
79
+ CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
79
80
  HOME: homeDirectory,
80
81
  USERPROFILE: homeDirectory,
81
82
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -313,6 +314,7 @@ test('uninstall fault after_git_config restores prior core.hooksPath', () => {
313
314
  CDE_INSTALL_PSTACK: '0',
314
315
  CDE_INSTALL_USAGE_WRAPUP: '0',
315
316
  CDE_INSTALL_SUBAGENT_MODELS: '0',
317
+ CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
316
318
  HOME: homeDirectory,
317
319
  USERPROFILE: homeDirectory,
318
320
  GIT_CONFIG_GLOBAL: gitConfigPath,
@@ -27,6 +27,7 @@ function installerEnvironment(homeDirectory) {
27
27
  CDE_INSTALL_PSTACK: '0',
28
28
  CDE_INSTALL_USAGE_WRAPUP: '0',
29
29
  CDE_INSTALL_SUBAGENT_MODELS: '0',
30
+ CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
30
31
  HOME: homeDirectory,
31
32
  USERPROFILE: homeDirectory,
32
33
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Deepen a task into the omitted expert 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, answer four things the prompt left out:
11
+
12
+ 1. What a top person in that field would do that this prompt omitted.
13
+ 2. The raw source to start from.
14
+ 3. The methods and tools they would use.
15
+ 4. What besides the agent or Jon would show the result is right.
16
+
17
+ Rewrite the task so it asks for exactly that job. Put `CDE_DEEPEN:v1` at the top of the rewrite. Then do only that job.
18
+
19
+ Skip this command when the prompt already carries `CDE_DEEPEN:v1`.
@@ -13,7 +13,7 @@ This family runs nested checkout hooks, refreshes a worktree base, and clears st
13
13
 
14
14
  ## When it fires
15
15
 
16
- - `lifecycle/nested_project_hooks.py` runs on `PreToolUse` matcher `*` at `120` seconds and `SessionStart` matcher empty at `1800` seconds in `hooks.json`.
16
+ - `lifecycle/nested_project_hooks.py` runs on `PreToolUse` matcher `*` at `120` seconds, `UserPromptSubmit` matcher empty at `120` seconds, and `SessionStart` matcher empty at `1800` seconds in `hooks.json`.
17
17
  - `lifecycle/enter_worktree_origin_prefetch.py` runs on `PreToolUse`, matcher `EnterWorktree`, timeout `25` seconds in `hooks.json`.
18
18
  - `lifecycle/session_end_cleanup.py` runs on `SessionEnd`, matcher empty, timeout `3` seconds in `hooks.json`.
19
19
  - `session/plugin_data_dir_cleanup.py` and `session/session_env_cleanup.py` run on `SessionStart`, matcher empty, timeout `10` seconds each in `hooks.json`.
@@ -9,6 +9,7 @@ This family injects reminders and task guidance when an agent starts, resumes, s
9
9
  - `session/working_style_prompt.py` injects the session's working style at session start.
10
10
  - `session/advisor_rules_prompt.py` injects advisor guidance when the built-in advisor is enabled.
11
11
  - `session/orchestrator_auto_starter.py` injects an orchestrator directive when its environment flag is enabled.
12
+ - `session/deepen_first_prompt.py` rewrites a first interactive user task so the worker does the omitted expert job.
12
13
  - `session/artifact_template_pointer.py` routes an agent that starts an artifact page by scope. A status or progress report prefers the status flowchart from `docs/templates/artifact-page/status-flowchart.html`, unless the person asks for another shape. One flow or one comparison is an inline SVG flowchart with minimal text. A multi-part plan or evidence is an html-plan page from `docs/templates/html-plan/README.md`. When the request names the older artifact page template, it points at `docs/templates/artifact-page/README.md`, which names the tracker template and the diagram template and which ask each fits.
13
14
  - `advisory/auto_mode_denial_quick_fix.py` proposes one `autoMode.allow` entry and a PowerShell block that writes it after an auto mode denial. It never retries the denied call.
14
15
 
@@ -16,6 +17,7 @@ This family injects reminders and task guidance when an agent starts, resumes, s
16
17
 
17
18
  - `session/skill_loaded_reminder.py` runs on `PreToolUse` matcher `Agent|Task` at `10` seconds, `UserPromptSubmit` matcher empty at `10` seconds, `SessionStart` matcher `compact` at `10` seconds, and `SubagentStart` matcher `workflow-subagent` at `10` seconds in `hooks.json`.
18
19
  - `session/task_tool_prompt.py`, `session/working_style_prompt.py`, `session/advisor_rules_prompt.py`, and `session/orchestrator_auto_starter.py` run on `SessionStart`, matcher empty, timeout `10` seconds each in `hooks.json`.
20
+ - `session/deepen_first_prompt.py` runs on `UserPromptSubmit`, matcher empty, timeout `10` seconds in `hooks.json`.
19
21
  - `session/artifact_template_pointer.py` runs on `PreToolUse` matcher `Artifact|Skill` at `10` seconds in `hooks.json`. It speaks only on an `Artifact` call with action `quickstart` or a `Skill` call that loads `artifact-design`.
20
22
  - `advisory/auto_mode_denial_quick_fix.py` runs on `PermissionDenied`, matcher `*`, timeout `10` seconds in `hooks.json`.
21
23
 
@@ -30,6 +32,7 @@ Preconditions:
30
32
  - **Working style.** Input is a SessionStart event. Run `python -m pytest packages/claude-dev-env/hooks/session/test_working_style_prompt.py -q`. The adjacent test observes the fixed context text.
31
33
  - **Advisor guidance.** Input is SessionStart with `advisorModel` set. Run `python -m pytest packages/claude-dev-env/hooks/session/test_advisor_rules_prompt.py -q`. The adjacent test observes guidance only when the advisor setting is enabled.
32
34
  - **Orchestrator start.** Input is SessionStart with its opt-in flag set. Run `python -m pytest packages/claude-dev-env/hooks/session/test_orchestrator_auto_starter.py -q`. The adjacent test observes an orchestrator directive.
35
+ - **First-task deepen.** Input is a first UserPromptSubmit task. Run `python -m pytest packages/claude-dev-env/hooks/session/test_deepen_first_prompt.py -q`. The adjacent test observes a skip table and a rewrite that names the four omitted answers.
33
36
  - **Artifact template pointer.** Input is an `Artifact` quickstart call. Run `python -m pytest packages/claude-dev-env/hooks/session/test_artifact_template_pointer.py -q`. The adjacent test observes the status flowchart path first, the html-plan path ahead of the template path in `additionalContext`, and silence on a publish call or another skill.
34
37
  - **Denial quick fix.** Input is a `PermissionDenied` event with a bracketed rule label and a classifier verdict. Run `python -m pytest packages/claude-dev-env/hooks/advisory/test_auto_mode_denial_quick_fix.py -q`. The adjacent test observes the named rule, the allow entry, and the PowerShell block; a denial with no verdict gets a note and no block.
35
38
 
package/hooks/hooks.json CHANGED
@@ -273,6 +273,16 @@
273
273
  "type": "command",
274
274
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/skill_loaded_reminder.py",
275
275
  "timeout": 10
276
+ },
277
+ {
278
+ "type": "command",
279
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/deepen_first_prompt.py",
280
+ "timeout": 10
281
+ },
282
+ {
283
+ "type": "command",
284
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/nested_project_hooks.py",
285
+ "timeout": 120
276
286
  }
277
287
  ]
278
288
  }
@@ -0,0 +1,119 @@
1
+ """Named values for first-task deepening."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hooks_constants.mod_handoff_constants import DEEPEN_FIRST_PROMPT_PLUGIN_NAME
6
+ from hooks_constants.skill_loaded_reminder_constants import (
7
+ CLAUDE_SUBAGENT_PROMPT_PREFIX,
8
+ CODEX_SUBAGENT_PROMPT_PREFIX,
9
+ )
10
+
11
+ __all__ = [
12
+ "ADDITIONAL_CONTEXT_KEY",
13
+ "ALL_HELPER_REPORT_MARKERS",
14
+ "ALL_LOOP_PROMPT_PREFIXES",
15
+ "ALL_NONINTERACTIVE_ENV_TRUE_VALUES",
16
+ "ALL_SPAWN_REWRITE_PREFIXES",
17
+ "ALL_TRIVIAL_PROMPTS",
18
+ "CLAUDE_HOST_ENV_VAR",
19
+ "CLAUDE_HOST_TRUE_VALUE",
20
+ "DEEPEN_AGENT_MARKER",
21
+ "DEEPEN_COMMAND_ENV_VAR",
22
+ "DEEPEN_COMMAND_TIMEOUT_SECONDS",
23
+ "DEEPEN_FIRST_PROMPT_PLUGIN_NAME",
24
+ "DEEPEN_MARKER",
25
+ "DEEPEN_OUTPUT_ENV_VAR",
26
+ "DEEPEN_OUTPUT_JSON",
27
+ "DEEPEN_OUTPUT_REWRITE",
28
+ "DEEPEN_REWRITE_ENV_VAR",
29
+ "DEEPEN_SLASH_COMMAND",
30
+ "DEEPEN_TIMEOUT_SECONDS",
31
+ "DEEPEN_TURN_MARKER_SUFFIX",
32
+ "HOOK_EVENT_NAME_KEY",
33
+ "HOOK_SPECIFIC_OUTPUT_KEY",
34
+ "JSONL_ROLE_KEY",
35
+ "JSONL_TYPE_KEY",
36
+ "NONINTERACTIVE_CODEX_ENV_VAR",
37
+ "NONINTERACTIVE_PRINT_ENV_VAR",
38
+ "PROMPT_FIELD_NAME",
39
+ "SESSION_ID_FIELD_NAME",
40
+ "SKIP_DEEPEN_ENV_VAR",
41
+ "SKIP_DEEPEN_TRUE_VALUE",
42
+ "TRANSCRIPT_PATH_FIELD_NAME",
43
+ "USER_PROMPT_SUBMIT_EVENT_NAME",
44
+ "USER_ROLE_NAME",
45
+ "USER_TYPE_NAME",
46
+ "UTF8_ENCODING",
47
+ ]
48
+
49
+ DEEPEN_MARKER: str = "CDE_DEEPEN:v1"
50
+ DEEPEN_AGENT_MARKER: str = "CDE_DEEPEN_AGENT"
51
+ DEEPEN_SLASH_COMMAND: str = "/deepen"
52
+ DEEPEN_TURN_MARKER_SUFFIX: str = ".cde-deepen-turn1"
53
+ DEEPEN_TIMEOUT_SECONDS: int = 10
54
+ DEEPEN_COMMAND_TIMEOUT_MARGIN_SECONDS: int = 2
55
+ DEEPEN_COMMAND_TIMEOUT_SECONDS: int = DEEPEN_TIMEOUT_SECONDS - DEEPEN_COMMAND_TIMEOUT_MARGIN_SECONDS
56
+ USER_PROMPT_SUBMIT_EVENT_NAME: str = "UserPromptSubmit"
57
+ PROMPT_FIELD_NAME: str = "prompt"
58
+ TRANSCRIPT_PATH_FIELD_NAME: str = "transcript_path"
59
+ SESSION_ID_FIELD_NAME: str = "session_id"
60
+ HOOK_SPECIFIC_OUTPUT_KEY: str = "hookSpecificOutput"
61
+ HOOK_EVENT_NAME_KEY: str = "hookEventName"
62
+ ADDITIONAL_CONTEXT_KEY: str = "additionalContext"
63
+ SKIP_DEEPEN_ENV_VAR: str = "CDE_SKIP_DEEPEN"
64
+ SKIP_DEEPEN_TRUE_VALUE: str = "1"
65
+ DEEPEN_REWRITE_ENV_VAR: str = "CDE_DEEPEN_REWRITE"
66
+ DEEPEN_COMMAND_ENV_VAR: str = "CDE_DEEPEN_COMMAND"
67
+ DEEPEN_OUTPUT_ENV_VAR: str = "CDE_DEEPEN_OUTPUT"
68
+ DEEPEN_OUTPUT_REWRITE: str = "rewrite"
69
+ DEEPEN_OUTPUT_JSON: str = "json"
70
+ NONINTERACTIVE_PRINT_ENV_VAR: str = "CLAUDE_CODE_NONINTERACTIVE"
71
+ NONINTERACTIVE_CODEX_ENV_VAR: str = "CODEX_EXEC"
72
+ CLAUDE_HOST_ENV_VAR: str = "CLAUDECODE"
73
+ CLAUDE_HOST_TRUE_VALUE: str = "1"
74
+ ALL_NONINTERACTIVE_ENV_TRUE_VALUES: frozenset[str] = frozenset({"1", "true", "yes"})
75
+ UTF8_ENCODING: str = "utf-8"
76
+ JSONL_TYPE_KEY: str = "type"
77
+ JSONL_ROLE_KEY: str = "role"
78
+ USER_TYPE_NAME: str = "user"
79
+ USER_ROLE_NAME: str = "user"
80
+
81
+ ALL_TRIVIAL_PROMPTS: frozenset[str] = frozenset(
82
+ {
83
+ "yes",
84
+ "y",
85
+ "no",
86
+ "n",
87
+ "ok",
88
+ "okay",
89
+ "thanks",
90
+ "thank you",
91
+ "thx",
92
+ "status",
93
+ "hello",
94
+ "hi",
95
+ "hey",
96
+ "k",
97
+ "sure",
98
+ "continue",
99
+ "go",
100
+ "?",
101
+ }
102
+ )
103
+
104
+ ALL_LOOP_PROMPT_PREFIXES: tuple[str, ...] = (
105
+ "/loop",
106
+ "<scheduled-task",
107
+ "scheduled task firing",
108
+ )
109
+
110
+ ALL_HELPER_REPORT_MARKERS: tuple[str, ...] = (
111
+ "<task-notification",
112
+ "background subagent reporting",
113
+ "a message another session sends",
114
+ )
115
+
116
+ ALL_SPAWN_REWRITE_PREFIXES: tuple[str, ...] = (
117
+ CLAUDE_SUBAGENT_PROMPT_PREFIX,
118
+ CODEX_SUBAGENT_PROMPT_PREFIX,
119
+ )
@@ -14,6 +14,7 @@ __all__ = [
14
14
  "ENABLED_PLUGINS_SETTINGS_KEY",
15
15
  "PLUGIN_MARKETPLACE_SEPARATOR",
16
16
  "PROJECT_SETTINGS_DIRECTORY_NAME",
17
+ "DEEPEN_FIRST_PROMPT_PLUGIN_NAME",
17
18
  "SESSION_PROMPTS_PLUGIN_NAME",
18
19
  "SHELL_GUARDS_PLUGIN_NAME",
19
20
  "USER_SETTINGS_FILE_NAME",
@@ -23,6 +24,7 @@ ENABLED_PLUGINS_SETTINGS_KEY = "enabledPlugins"
23
24
  PLUGIN_MARKETPLACE_SEPARATOR = "@"
24
25
  SHELL_GUARDS_PLUGIN_NAME = "shell-guards"
25
26
  SESSION_PROMPTS_PLUGIN_NAME = "session-prompts"
27
+ DEEPEN_FIRST_PROMPT_PLUGIN_NAME = "deepen-first-prompt"
26
28
  CLAUDE_CONFIG_DIR_ENV_VAR = "CLAUDE_CONFIG_DIR"
27
29
  CLAUDE_PROJECT_DIR_ENV_VAR = "CLAUDE_PROJECT_DIR"
28
30
  DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude"
@@ -20,9 +20,11 @@ ALL_MATCH_EVERYTHING_MATCHERS: frozenset[str] = frozenset({"", "*"})
20
20
  HOOK_EVENT_NAME_KEY: str = "hook_event_name"
21
21
  PRE_TOOL_USE_EVENT: str = "PreToolUse"
22
22
  SESSION_START_EVENT: str = "SessionStart"
23
+ USER_PROMPT_SUBMIT_EVENT: str = "UserPromptSubmit"
23
24
  ALL_MATCH_TARGET_KEYS_BY_EVENT: dict[str, str] = {
24
25
  PRE_TOOL_USE_EVENT: "tool_name",
25
26
  SESSION_START_EVENT: "source",
27
+ USER_PROMPT_SUBMIT_EVENT: "prompt",
26
28
  }
27
29
 
28
30
  ALL_SHELL_PROGRAM_NAMES: tuple[str, ...] = ("bash", "sh")
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env python3
2
- """SessionStart and PreToolUse hook: run the hooks of checkouts nested one level down.
2
+ """SessionStart, UserPromptSubmit, and PreToolUse hook: run nested checkout hooks.
3
3
 
4
4
  Claude Code reads project hooks from ``<project>/.claude/settings.json`` of the
5
5
  directory a session starts in. A cloud session that holds several repository
6
6
  checkouts starts in their parent directory, which has no settings of its own,
7
- and ``--add-dir`` loads no hooks from the checkouts. Their start hooks and tool
8
- guards then never run. This hook runs them::
7
+ and ``--add-dir`` loads no hooks from the checkouts. Their start hooks, first
8
+ prompt deepeners, and tool guards then never run. This hook runs them::
9
9
 
10
10
  session directory (no .git, no .claude/settings.json)
11
11
  |-- checkout-a/.claude/settings.json hooks run with CLAUDE_PROJECT_DIR=checkout-a
@@ -15,7 +15,7 @@ Each child hook receives the same stdin payload, runs inside its checkout, and
15
15
  keeps its own matcher and timeout. A child ``exit 2`` blocks the tool call and a
16
16
  child ``deny`` or ``ask`` decision passes through; a child ``allow`` is dropped,
17
17
  so no child widens permissions for the session. The ``additionalContext`` of
18
- every child, at SessionStart and at PreToolUse, joins into one
18
+ every child, at SessionStart, UserPromptSubmit, and PreToolUse, joins into one
19
19
  ``additionalContext``.
20
20
 
21
21
  When the session directory is itself a checkout or holds its own settings,
@@ -342,13 +342,15 @@ def _non_empty_sections(all_sections: Iterable[str]) -> list[str]:
342
342
  return [each_section for each_section in all_sections if each_section]
343
343
 
344
344
 
345
- def session_start_verdict(
345
+ def additional_context_verdict(
346
346
  all_results: list[subprocess.CompletedProcess[str]],
347
+ event_name: str,
347
348
  ) -> HookVerdict:
348
- """Join nested SessionStart output into one additional-context block.
349
+ """Join nested hook output into one additional-context block.
349
350
 
350
351
  Args:
351
352
  all_results: The completed nested hooks, in run order.
353
+ event_name: The envelope event name Claude Code expects.
352
354
 
353
355
  Returns:
354
356
  A pass whose stdout carries every child's context, or an empty pass
@@ -364,13 +366,20 @@ def session_start_verdict(
364
366
  return HookVerdict(0, "", stderr_text)
365
367
  hook_output = {
366
368
  HOOK_SPECIFIC_OUTPUT_KEY: {
367
- HOOK_EVENT_NAME_OUTPUT_KEY: SESSION_START_EVENT,
369
+ HOOK_EVENT_NAME_OUTPUT_KEY: event_name,
368
370
  ADDITIONAL_CONTEXT_KEY: CONTEXT_SECTION_SEPARATOR.join(all_context_sections),
369
371
  }
370
372
  }
371
373
  return HookVerdict(0, json.dumps(hook_output), stderr_text)
372
374
 
373
375
 
376
+ def session_start_verdict(
377
+ all_results: list[subprocess.CompletedProcess[str]],
378
+ ) -> HookVerdict:
379
+ """Join nested SessionStart output into one additional-context block."""
380
+ return additional_context_verdict(all_results, SESSION_START_EVENT)
381
+
382
+
374
383
  def _completed_results(
375
384
  all_nested_hooks: list[NestedHook], payload_text: str
376
385
  ) -> list[subprocess.CompletedProcess[str]]:
@@ -402,7 +411,7 @@ def forward_nested_hooks(session_directory: Path, payload_text: str) -> HookVerd
402
411
  all_results = _completed_results(all_nested_hooks, payload_text)
403
412
  if event_name == PRE_TOOL_USE_EVENT:
404
413
  return pre_tool_use_verdict(all_results)
405
- return session_start_verdict(all_results)
414
+ return additional_context_verdict(all_results, event_name)
406
415
 
407
416
 
408
417
  def main() -> None:
@@ -62,6 +62,10 @@ def _session_start_payload() -> dict[str, object]:
62
62
  return {"hook_event_name": "SessionStart", "source": "startup"}
63
63
 
64
64
 
65
+ def _user_prompt_submit_payload(prompt: str) -> dict[str, object]:
66
+ return {"hook_event_name": "UserPromptSubmit", "prompt": prompt}
67
+
68
+
65
69
  def test_should_run_a_child_session_start_hook_inside_that_child(
66
70
  tmp_path: pathlib.Path,
67
71
  ) -> None:
@@ -352,6 +356,52 @@ def test_should_let_the_call_through_when_a_child_hook_fails_to_start(
352
356
  assert completed_process.returncode == 0
353
357
 
354
358
 
359
+ def test_should_carry_child_user_prompt_submit_output_as_additional_context(
360
+ tmp_path: pathlib.Path,
361
+ ) -> None:
362
+ context_output = {
363
+ "hookSpecificOutput": {
364
+ "hookEventName": "UserPromptSubmit",
365
+ "additionalContext": "CDE_DEEPEN:v1\nTreat the submitted prompt as this rewritten task.",
366
+ }
367
+ }
368
+ _make_checkout(
369
+ tmp_path,
370
+ "alpha",
371
+ {
372
+ "UserPromptSubmit": [
373
+ _command_group("", f"echo '{json.dumps(context_output)}'")
374
+ ]
375
+ },
376
+ )
377
+
378
+ completed_process = _run_hook(
379
+ tmp_path, _user_prompt_submit_payload("Add dark mode to the settings page")
380
+ )
381
+
382
+ assert completed_process.returncode == 0
383
+ hook_output = json.loads(completed_process.stdout)
384
+ assert hook_output["hookSpecificOutput"]["hookEventName"] == "UserPromptSubmit"
385
+ assert "CDE_DEEPEN:v1" in hook_output["hookSpecificOutput"]["additionalContext"]
386
+
387
+
388
+ def test_should_stay_quiet_on_an_event_it_does_not_forward(
389
+ tmp_path: pathlib.Path,
390
+ ) -> None:
391
+ _make_checkout(
392
+ tmp_path,
393
+ "alpha",
394
+ {"SessionStart": [_command_group("", "echo should not run")]},
395
+ )
396
+
397
+ completed_process = _run_hook(
398
+ tmp_path, {"hook_event_name": "SessionEnd", "prompt": "Add dark mode"}
399
+ )
400
+
401
+ assert completed_process.returncode == 0
402
+ assert completed_process.stdout.strip() == ""
403
+
404
+
355
405
  @pytest.mark.parametrize("unreadable_settings", ["{not json", "[]", '{"hooks": []}'])
356
406
  def test_should_skip_a_child_whose_settings_cannot_be_read(
357
407
  tmp_path: pathlib.Path, unreadable_settings: str