claude-dev-env 8.82.1 → 8.83.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) 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/docs/account-broker.md +1 -1
  23. package/docs/rule-guides/agent-board.md +27 -0
  24. package/docs/rule-guides/features-start-with-an-eval.md +4 -2
  25. package/docs/rule-guides/proof-before-pull-request.md +1 -1
  26. package/hooks/blocking/build_eval_before_eval_write_gate.py +102 -0
  27. package/hooks/blocking/pr_lifecycle_skill_gate.py +42 -16
  28. package/hooks/blocking/pull_request_proof.py +111 -2
  29. package/hooks/blocking/reply_length_gate.py +69 -13
  30. package/hooks/blocking/test_build_eval_before_eval_write_gate.py +162 -0
  31. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +25 -0
  32. package/hooks/blocking/test_pull_request_proof.py +115 -0
  33. package/hooks/blocking/test_reply_length_gate.py +98 -10
  34. package/hooks/context_budget/tests/test_model.py +65 -0
  35. package/hooks/features/conduct-gates.md +4 -0
  36. package/hooks/features/lifecycle-cleanup.md +1 -1
  37. package/hooks/features/session-context.md +3 -0
  38. package/hooks/hooks.json +20 -0
  39. package/hooks/hooks_constants/build_eval_before_eval_write_gate_constants.py +19 -0
  40. package/hooks/hooks_constants/deepen_first_prompt_constants.py +119 -0
  41. package/hooks/hooks_constants/mod_handoff_constants.py +2 -0
  42. package/hooks/hooks_constants/nested_project_hooks_constants.py +2 -0
  43. package/hooks/hooks_constants/pull_request_proof_constants.py +38 -0
  44. package/hooks/hooks_constants/reply_length_gate_constants.py +14 -0
  45. package/hooks/hooks_constants/test_reply_length_gate_constants.py +8 -0
  46. package/hooks/lifecycle/nested_project_hooks.py +17 -8
  47. package/hooks/lifecycle/test_nested_project_hooks.py +50 -0
  48. package/hooks/session/deepen_first_prompt.py +390 -0
  49. package/hooks/session/test_deepen_first_prompt.py +274 -0
  50. package/hooks/test_transcript_skill_scan.py +6 -2
  51. package/hooks/transcript_skill_scan.py +18 -7
  52. package/package.json +1 -1
  53. package/rules/agent-board.md +9 -0
  54. package/rules/index.md +1 -1
  55. package/scripts/account_broker.py +1 -0
  56. package/scripts/account_broker_support.py +71 -24
  57. package/scripts/ci/install-playtest.mjs +329 -2
  58. package/scripts/ci/install-playtest.test.mjs +6 -0
  59. package/scripts/dev_env_scripts_constants/account_broker_constants.py +11 -3
  60. package/scripts/dev_env_scripts_constants/test_account_broker_constants.py +15 -0
  61. package/scripts/policy_lint/adapter_configuration.py +5 -0
  62. package/scripts/policy_lint/config/constants.py +1 -0
  63. package/scripts/test_account_broker.py +19 -1
  64. package/scripts/test_account_broker_support.py +161 -3
  65. package/scripts/tests/test_adapter_configuration.py +8 -0
  66. package/scripts/tests/test_rule_load_scopes.py +1 -0
@@ -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")
@@ -24,6 +24,44 @@ MISSING_PROOF_REASON = (
24
24
  " prove the change, leave the pull request unopened and tell the user what you could"
25
25
  " not run. Read " + PROOF_GUIDE_PATH + " for the full rule."
26
26
  )
27
+ INLINE_COMMAND_PATTERN = r"(?<!`)`([^`\n]+)`(?!`)"
28
+ CODE_FENCE_MARKER = "```"
29
+ FENCED_COMMAND_PROMPT = "$ "
30
+ UNITTEST_MODULE_NAME = "unittest"
31
+ PATH_WITH_LINE_PATTERN = r"^\S+:\d+(?:[-:]\d+)?$"
32
+ SINGLE_FILE_NAME_PATTERN = r"^\S*[/\\]\S*$|^[\w.-]+\.[A-Za-z0-9]+$"
33
+ ALL_PASSTHROUGH_PROGRAM_NAMES = frozenset({"tee", "tail", "head", "grep", "cat"})
34
+ ALL_TEST_RUNNER_COMMAND_PREFIXES = frozenset(
35
+ {
36
+ ("jest",),
37
+ ("vitest",),
38
+ ("mocha",),
39
+ ("npm", "test"),
40
+ ("npm", "t"),
41
+ ("npm", "run", "test"),
42
+ ("yarn", "test"),
43
+ ("yarn", "run", "test"),
44
+ ("pnpm", "test"),
45
+ ("pnpm", "run", "test"),
46
+ ("bun", "test"),
47
+ ("npx", "jest"),
48
+ ("npx", "vitest"),
49
+ ("npx", "mocha"),
50
+ ("go", "test"),
51
+ ("cargo", "test"),
52
+ ("deno", "test"),
53
+ ("dotnet", "test"),
54
+ ("unittest",),
55
+ }
56
+ )
57
+ UNIT_TESTS_GUIDE_LINE = "Unit tests are welcome as extra evidence. The live run is the proof."
58
+ TEST_ONLY_PROOF_REASON = (
59
+ "Every command under this pull request's 'Proof in practice' heading runs a test"
60
+ " suite. The proof rule says: \"" + UNIT_TESTS_GUIDE_LINE + "\" Run the changed thing"
61
+ " the way its user runs it, once without the change and once with it. List each"
62
+ " command in backticks and quote the output lines that show the difference. Keep the"
63
+ " test run in the section as extra evidence. Read " + PROOF_GUIDE_PATH + " for the full rule."
64
+ )
27
65
  EXISTING_WORK_HEADING_PATTERN = r"^(#{1,6})[ \t]+existing work[ \t]*#*[ \t]*$"
28
66
  EXISTING_WORK_GUIDE_PATH = "~/.claude/docs/rule-guides/explore-thoroughly.md"
29
67
  MISSING_EXISTING_WORK_REASON = (
@@ -12,6 +12,10 @@ CARD_TEXT_SEPARATOR = "\n"
12
12
  ALL_DECISION_CARD_PROSE_KEYS = ("question", "context")
13
13
  DECISION_CARD_OPTIONS_KEY = "options"
14
14
  ALL_DECISION_OPTION_PROSE_KEYS = ("label", "consequence")
15
+ DECISION_CARD_CONTEXT_KEY = "context"
16
+ MAXIMUM_CARD_CONTEXT_SENTENCE_COUNT = 2
17
+ ALL_DECISION_CARD_LENGTH_KEYS = ("question", "context", "reason")
18
+ ALL_DECISION_OPTION_LENGTH_KEYS = ("consequence",)
15
19
  QUESTION_CARD_TOOL_NAME = "AskUserQuestion"
16
20
  QUESTION_CARD_QUESTIONS_KEY = "questions"
17
21
  ALL_QUESTION_CARD_PROSE_KEYS = ("question", "header")
@@ -41,6 +45,16 @@ WORD_SEPARATOR = " "
41
45
  SENTENCE_PREVIEW_SUFFIX = "..."
42
46
  RETRY_INSTRUCTION = " Cut the text and resend the call."
43
47
  TOO_MANY_SENTENCES_MESSAGE = "Reply too long: {sentence_count} sentences, limit {sentence_limit}."
48
+ CARD_CONTEXT_TOO_LONG_MESSAGE = (
49
+ "Card context too long: {sentence_count} sentences, limit {sentence_limit}."
50
+ )
51
+ CARD_CONTEXT_LIST_MESSAGE = "Card context holds a list of items, and a card cannot format a list."
52
+ CARD_LIST_ITEM_PATTERN = re.compile(r"^[ \t]*(?:[-*+]|\d+[.)])[ \t]+\S", re.MULTILINE)
53
+ CARD_FORMAT_FLOW_INSTRUCTION = (
54
+ " For a list or several items, publish an Artifact page with a tap button per item"
55
+ " and a Send button, with the capability that republishes its state so the watching"
56
+ " session wakes. Then send a short reply that links the page."
57
+ )
44
58
  UNLINKED_PULL_REQUEST_MESSAGE = 'Pull request "{reference}" has no link. Write it as [PR N](https://github.com/<owner>/<repo>/pull/N).'
45
59
  MID_SENTENCE_COLON_MESSAGE = (
46
60
  "A colon joins two clauses on one line. Write two sentences, or end the line with the colon"
@@ -51,3 +51,11 @@ def test_colon_pattern_matches_a_colon_with_prose_after_it_on_the_line() -> None
51
51
  assert constants.MID_SENTENCE_COLON_PATTERN.search("matters: one message")
52
52
  assert constants.MID_SENTENCE_COLON_PATTERN.search("at 9:47") is None
53
53
  assert constants.MID_SENTENCE_COLON_PATTERN.search("items:\n- one") is None
54
+
55
+
56
+ def test_gate_caps_card_context_at_two_sentences_and_checks_card_length_fields() -> None:
57
+ assert (
58
+ constants.MAXIMUM_CARD_CONTEXT_SENTENCE_COUNT,
59
+ constants.ALL_DECISION_CARD_LENGTH_KEYS,
60
+ constants.ALL_DECISION_OPTION_LENGTH_KEYS,
61
+ ) == (2, ("question", "context", "reason"), ("consequence",))
@@ -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