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.
- package/.agents/skills/deepen/SKILL.md +42 -0
- package/bin/codex-skill-load-block.mjs +32 -0
- package/bin/codex-skill-load-block.test.mjs +27 -0
- package/bin/codex-trimmed-sol.test.mjs +1 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-pstack-plugin.mjs +12 -0
- package/bin/install-pstack-plugin.test.mjs +23 -0
- package/bin/install.agents-home.test.mjs +1 -0
- package/bin/install.codex-rules.test.mjs +2 -0
- package/bin/install.cursor-rules.test.mjs +38 -1
- package/bin/install.mjs +34 -2
- package/bin/install.prompt-context.test.mjs +3 -1
- package/bin/install.prune.test.mjs +1 -0
- package/bin/install.question-presentation.test.mjs +4 -0
- package/bin/install.settings-defaults.test.mjs +1 -0
- package/bin/install.shared-settings.test.mjs +1 -0
- package/bin/install.test.mjs +34 -1
- package/bin/install.transaction.test.mjs +8 -0
- package/bin/install.uninstall-transaction.test.mjs +2 -0
- package/bin/install.update-keeps-hooks.test.mjs +1 -0
- package/commands/deepen.md +19 -0
- package/docs/account-broker.md +1 -1
- package/docs/rule-guides/agent-board.md +27 -0
- package/docs/rule-guides/features-start-with-an-eval.md +4 -2
- package/docs/rule-guides/proof-before-pull-request.md +1 -1
- package/hooks/blocking/build_eval_before_eval_write_gate.py +102 -0
- package/hooks/blocking/pr_lifecycle_skill_gate.py +42 -16
- package/hooks/blocking/pull_request_proof.py +111 -2
- package/hooks/blocking/reply_length_gate.py +69 -13
- package/hooks/blocking/test_build_eval_before_eval_write_gate.py +162 -0
- package/hooks/blocking/test_pr_lifecycle_skill_gate.py +25 -0
- package/hooks/blocking/test_pull_request_proof.py +115 -0
- package/hooks/blocking/test_reply_length_gate.py +98 -10
- package/hooks/context_budget/tests/test_model.py +65 -0
- package/hooks/features/conduct-gates.md +4 -0
- package/hooks/features/lifecycle-cleanup.md +1 -1
- package/hooks/features/session-context.md +3 -0
- package/hooks/hooks.json +20 -0
- package/hooks/hooks_constants/build_eval_before_eval_write_gate_constants.py +19 -0
- package/hooks/hooks_constants/deepen_first_prompt_constants.py +119 -0
- package/hooks/hooks_constants/mod_handoff_constants.py +2 -0
- package/hooks/hooks_constants/nested_project_hooks_constants.py +2 -0
- package/hooks/hooks_constants/pull_request_proof_constants.py +38 -0
- package/hooks/hooks_constants/reply_length_gate_constants.py +14 -0
- package/hooks/hooks_constants/test_reply_length_gate_constants.py +8 -0
- package/hooks/lifecycle/nested_project_hooks.py +17 -8
- package/hooks/lifecycle/test_nested_project_hooks.py +50 -0
- package/hooks/session/deepen_first_prompt.py +390 -0
- package/hooks/session/test_deepen_first_prompt.py +274 -0
- package/hooks/test_transcript_skill_scan.py +6 -2
- package/hooks/transcript_skill_scan.py +18 -7
- package/package.json +1 -1
- package/rules/agent-board.md +9 -0
- package/rules/index.md +1 -1
- package/scripts/account_broker.py +1 -0
- package/scripts/account_broker_support.py +71 -24
- package/scripts/ci/install-playtest.mjs +329 -2
- package/scripts/ci/install-playtest.test.mjs +6 -0
- package/scripts/dev_env_scripts_constants/account_broker_constants.py +11 -3
- package/scripts/dev_env_scripts_constants/test_account_broker_constants.py +15 -0
- package/scripts/policy_lint/adapter_configuration.py +5 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/test_account_broker.py +19 -1
- package/scripts/test_account_broker_support.py +161 -3
- package/scripts/tests/test_adapter_configuration.py +8 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
345
|
+
def additional_context_verdict(
|
|
346
346
|
all_results: list[subprocess.CompletedProcess[str]],
|
|
347
|
+
event_name: str,
|
|
347
348
|
) -> HookVerdict:
|
|
348
|
-
"""Join nested
|
|
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:
|
|
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
|
|
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
|