claude-dev-env 8.88.7 → 8.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.agents/skills/pr-visual/README.md +29 -0
  2. package/.agents/skills/pr-visual/SKILL.md +62 -0
  3. package/.agents/skills/pr-visual/assets/icons.symbols +27 -0
  4. package/.agents/skills/pr-visual/assets/viewer.html +147 -0
  5. package/.agents/skills/pr-visual/fixtures/bar.scene.json +193 -0
  6. package/.agents/skills/pr-visual/fixtures/flow.scene.json +201 -0
  7. package/.agents/skills/pr-visual/fixtures/head/metrics.json +16 -0
  8. package/.agents/skills/pr-visual/fixtures/integrated.scene.json +239 -0
  9. package/.agents/skills/pr-visual/fixtures/line.scene.json +193 -0
  10. package/.agents/skills/pr-visual/fixtures/scatter.scene.json +193 -0
  11. package/.agents/skills/pr-visual/fixtures/snapshot.json +21 -0
  12. package/.agents/skills/pr-visual/references/evidence.md +9 -0
  13. package/.agents/skills/pr-visual/references/formats.md +30 -0
  14. package/.agents/skills/pr-visual/references/scene-contract.md +48 -0
  15. package/.agents/skills/pr-visual/references/visual-contract.md +23 -0
  16. package/.agents/skills/pr-visual/scripts/collect.py +111 -0
  17. package/.agents/skills/pr-visual/scripts/metrics.py +99 -0
  18. package/.agents/skills/pr-visual/scripts/render.py +133 -0
  19. package/.agents/skills/pr-visual/scripts/test_collect.py +58 -0
  20. package/.agents/skills/pr-visual/scripts/test_metrics.py +47 -0
  21. package/.agents/skills/pr-visual/scripts/test_render.py +77 -0
  22. package/.agents/skills/pr-visual/scripts/verify.py +218 -0
  23. package/.agents/skills/pr-visual/scripts/visual_config/config/constants.py +104 -0
  24. package/bin/codex-skill-load-block.mjs +0 -25
  25. package/bin/codex-skill-load-block.test.mjs +5 -17
  26. package/bin/codex-trimmed-sol.test.mjs +0 -1
  27. package/bin/cursor-native-hooks.test.mjs +3 -3
  28. package/bin/ever-shipped-skills.mjs +1 -0
  29. package/bin/install-constants.mjs +3 -0
  30. package/bin/install-deepstack-plugin.mjs +42 -25
  31. package/bin/install-deepstack-plugin.test.mjs +37 -11
  32. package/bin/install-pstack-plugin.mjs +58 -11
  33. package/bin/install-pstack-plugin.test.mjs +54 -21
  34. package/bin/install.agents-home.test.mjs +0 -1
  35. package/bin/install.codex-rules.test.mjs +0 -2
  36. package/bin/install.cursor-rules.test.mjs +96 -21
  37. package/bin/install.mjs +48 -44
  38. package/bin/install.pr-visual.test.mjs +40 -0
  39. package/bin/install.prompt-context.test.mjs +3 -4
  40. package/bin/install.prune.test.mjs +0 -1
  41. package/bin/install.question-presentation.test.mjs +0 -4
  42. package/bin/install.settings-defaults.test.mjs +0 -1
  43. package/bin/install.shared-settings.test.mjs +0 -1
  44. package/bin/install.test.mjs +14 -44
  45. package/bin/install.transaction.test.mjs +0 -8
  46. package/bin/install.uninstall-transaction.test.mjs +0 -2
  47. package/bin/install.update-keeps-hooks.test.mjs +0 -1
  48. package/docs/BDD_SCENARIO_QUALITY.md +1 -1
  49. package/docs/BDD_TEST_LAYOUT.md +1 -1
  50. package/docs/CODE_RULES.md +1 -1
  51. package/docs/rule-guides/deepstack-wording.md +3 -5
  52. package/docs/rule-guides/session-title.md +5 -4
  53. package/hooks/advisory/owned_pr_review_loop.py +365 -0
  54. package/hooks/advisory/test_owned_pr_review_loop.py +369 -0
  55. package/hooks/blocking/session_title_turn_gate.py +287 -0
  56. package/hooks/blocking/test_session_title_format_gate.py +5 -0
  57. package/hooks/blocking/test_session_title_turn_gate.py +264 -0
  58. package/hooks/features/bash-dispatchers.md +3 -0
  59. package/hooks/features/conduct-gates.md +3 -0
  60. package/hooks/features/session-context.md +0 -4
  61. package/hooks/hooks.json +15 -5
  62. package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
  63. package/hooks/hooks_constants/owned_pr_review_loop_constants.py +120 -0
  64. package/hooks/hooks_constants/session_title_constants.py +41 -3
  65. package/hooks/lifecycle/nested_project_hooks.py +2 -2
  66. package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
  67. package/hooks/session/compaction_user_messages.py +2 -2
  68. package/package.json +2 -2
  69. package/rules/deepstack-wording.md +3 -3
  70. package/scripts/ci/install-playtest.mjs +58 -314
  71. package/scripts/ci/install-playtest.test.mjs +24 -6
  72. package/scripts/policy_lint/adapter_configuration.py +1 -0
  73. package/scripts/policy_lint/config/constants.py +1 -0
  74. package/scripts/test_deepstack_review_criterion.py +2 -3
  75. package/scripts/test_deepstack_wording_rule.py +6 -4
  76. package/scripts/tests/test_adapter_configuration.py +8 -0
  77. package/.agents/skills/deepen/SKILL.md +0 -26
  78. package/commands/deepen.md +0 -14
  79. package/docs/decisions/deepstack-gate.md +0 -32
  80. package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
  81. package/hooks/session/deepen_default_rewrite.py +0 -128
  82. package/hooks/session/deepen_first_prompt.py +0 -383
  83. package/hooks/session/deepen_role_gate.py +0 -171
  84. package/hooks/session/deepen_turn_marker.py +0 -75
  85. package/hooks/session/fixtures/deepstack_replay.json +0 -15
  86. package/hooks/session/grade_deepstack_replay.py +0 -78
  87. package/hooks/session/test_deepen_default_rewrite.py +0 -90
  88. package/hooks/session/test_deepen_first_prompt.py +0 -452
  89. package/hooks/session/test_deepen_role_gate.py +0 -83
  90. package/hooks/session/test_deepen_turn_marker.py +0 -59
  91. package/hooks/session/test_grade_deepstack_replay.py +0 -47
@@ -1,187 +0,0 @@
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
- "ALL_EXAMPLE_NOUN_STOPWORDS",
30
- "ALL_FOUR_QUESTION_STEMS",
31
- "EXAMPLE_NOUN_PATTERN",
32
- "HARD_CONSTRAINTS_LABEL",
33
- "REFUSAL_DROPPED_HARD_CONSTRAINT",
34
- "REFUSAL_EXAMPLE_NOUN_IN_SCOPE",
35
- "REFUSAL_MISSING_ROLE_LINES",
36
- "REFUSAL_RESTATES_FOUR_QUESTIONS",
37
- "ROLE_BLOCKER_LABEL",
38
- "ROLE_BLOCKER_NONE",
39
- "ROLE_EXAMPLES_LABEL",
40
- "BULLET_PREFIX",
41
- "QUESTION_LINE_SEPARATOR",
42
- "SCORE_ACCEPT",
43
- "SCORE_REFUSE",
44
- "ROLE_SCOPE_LABEL",
45
- "DEEPEN_SLASH_COMMAND",
46
- "DEEPEN_TIMEOUT_SECONDS",
47
- "DEEPEN_TURN_MARKER_SUFFIX",
48
- "DEEPEN_SESSION_MARKER_PREFIX",
49
- "ALL_FIRST_PROMPT_EVENT_NAMES",
50
- "CURSOR_BEFORE_SUBMIT_PROMPT_EVENT_NAME",
51
- "HOOK_EVENT_NAME_KEY",
52
- "HOOK_SPECIFIC_OUTPUT_KEY",
53
- "JSONL_ROLE_KEY",
54
- "JSONL_TYPE_KEY",
55
- "NONINTERACTIVE_CODEX_ENV_VAR",
56
- "NONINTERACTIVE_PRINT_ENV_VAR",
57
- "PROMPT_FIELD_NAME",
58
- "SESSION_ID_FIELD_NAME",
59
- "SKIP_DEEPEN_ENV_VAR",
60
- "SKIP_DEEPEN_TRUE_VALUE",
61
- "TRANSCRIPT_PATH_FIELD_NAME",
62
- "USER_PROMPT_SUBMIT_EVENT_NAME",
63
- "USER_ROLE_NAME",
64
- "USER_TYPE_NAME",
65
- "UTF8_ENCODING",
66
- ]
67
-
68
- DEEPEN_MARKER: str = "CDE_DEEPEN:v1"
69
- DEEPEN_AGENT_MARKER: str = "CDE_DEEPEN_AGENT"
70
- DEEPEN_SLASH_COMMAND: str = "/deepen"
71
- DEEPEN_TURN_MARKER_SUFFIX: str = ".cde-deepen-turn1"
72
- DEEPEN_SESSION_MARKER_PREFIX: str = "cde-deepen-"
73
- DEEPEN_TIMEOUT_SECONDS: int = 10
74
- DEEPEN_COMMAND_TIMEOUT_MARGIN_SECONDS: int = 2
75
- DEEPEN_COMMAND_TIMEOUT_SECONDS: int = DEEPEN_TIMEOUT_SECONDS - DEEPEN_COMMAND_TIMEOUT_MARGIN_SECONDS
76
- USER_PROMPT_SUBMIT_EVENT_NAME: str = "UserPromptSubmit"
77
- CURSOR_BEFORE_SUBMIT_PROMPT_EVENT_NAME: str = "beforeSubmitPrompt"
78
- ALL_FIRST_PROMPT_EVENT_NAMES: frozenset[str] = frozenset(
79
- {USER_PROMPT_SUBMIT_EVENT_NAME, CURSOR_BEFORE_SUBMIT_PROMPT_EVENT_NAME}
80
- )
81
- PROMPT_FIELD_NAME: str = "prompt"
82
- TRANSCRIPT_PATH_FIELD_NAME: str = "transcript_path"
83
- SESSION_ID_FIELD_NAME: str = "session_id"
84
- HOOK_SPECIFIC_OUTPUT_KEY: str = "hookSpecificOutput"
85
- HOOK_EVENT_NAME_KEY: str = "hookEventName"
86
- ADDITIONAL_CONTEXT_KEY: str = "additionalContext"
87
- SKIP_DEEPEN_ENV_VAR: str = "CDE_SKIP_DEEPEN"
88
- SKIP_DEEPEN_TRUE_VALUE: str = "1"
89
- DEEPEN_REWRITE_ENV_VAR: str = "CDE_DEEPEN_REWRITE"
90
- DEEPEN_COMMAND_ENV_VAR: str = "CDE_DEEPEN_COMMAND"
91
- DEEPEN_OUTPUT_ENV_VAR: str = "CDE_DEEPEN_OUTPUT"
92
- DEEPEN_OUTPUT_REWRITE: str = "rewrite"
93
- DEEPEN_OUTPUT_JSON: str = "json"
94
- NONINTERACTIVE_PRINT_ENV_VAR: str = "CLAUDE_CODE_NONINTERACTIVE"
95
- NONINTERACTIVE_CODEX_ENV_VAR: str = "CODEX_EXEC"
96
- CLAUDE_HOST_ENV_VAR: str = "CLAUDECODE"
97
- CLAUDE_HOST_TRUE_VALUE: str = "1"
98
- ALL_NONINTERACTIVE_ENV_TRUE_VALUES: frozenset[str] = frozenset({"1", "true", "yes"})
99
- UTF8_ENCODING: str = "utf-8"
100
- JSONL_TYPE_KEY: str = "type"
101
- JSONL_ROLE_KEY: str = "role"
102
- USER_TYPE_NAME: str = "user"
103
- USER_ROLE_NAME: str = "user"
104
-
105
- ALL_TRIVIAL_PROMPTS: frozenset[str] = frozenset(
106
- {
107
- "yes",
108
- "y",
109
- "no",
110
- "n",
111
- "ok",
112
- "okay",
113
- "thanks",
114
- "thank you",
115
- "thx",
116
- "status",
117
- "hello",
118
- "hi",
119
- "hey",
120
- "k",
121
- "sure",
122
- "continue",
123
- "go",
124
- "?",
125
- }
126
- )
127
-
128
- ALL_LOOP_PROMPT_PREFIXES: tuple[str, ...] = (
129
- "/loop",
130
- "<scheduled-task",
131
- "scheduled task firing",
132
- )
133
-
134
- ALL_HELPER_REPORT_MARKERS: tuple[str, ...] = (
135
- "<task-notification",
136
- "background subagent reporting",
137
- "a message another session sends",
138
- )
139
-
140
- ALL_SPAWN_REWRITE_PREFIXES: tuple[str, ...] = (
141
- CLAUDE_SUBAGENT_PROMPT_PREFIX,
142
- CODEX_SUBAGENT_PROMPT_PREFIX,
143
- )
144
-
145
- ROLE_SCOPE_LABEL: str = "SCOPE (role):"
146
- ROLE_EXAMPLES_LABEL: str = "EXAMPLES (illustration only):"
147
- ROLE_BLOCKER_LABEL: str = "ZOOM-IN BLOCKER:"
148
- ROLE_BLOCKER_NONE: str = "none"
149
- HARD_CONSTRAINTS_LABEL: str = "HARD CONSTRAINTS"
150
- REFUSAL_RESTATES_FOUR_QUESTIONS: str = "restates-four-questions"
151
- REFUSAL_MISSING_ROLE_LINES: str = "missing-role-lines"
152
- REFUSAL_EXAMPLE_NOUN_IN_SCOPE: str = "example-noun-in-scope"
153
- REFUSAL_DROPPED_HARD_CONSTRAINT: str = "dropped-hard-constraint"
154
- BULLET_PREFIX: str = "- "
155
- QUESTION_LINE_SEPARATOR: str = "\n"
156
- SCORE_ACCEPT: str = "accept"
157
- SCORE_REFUSE: str = "refuse"
158
- EXAMPLE_NOUN_PATTERN: str = r"[A-Za-z][A-Za-z0-9_+-]{3,}"
159
- ALL_FOUR_QUESTION_STEMS: tuple[str, ...] = (
160
- "What a top person in that field would do that this prompt omitted.",
161
- "The raw source to start from.",
162
- "The methods and tools they would use.",
163
- "What besides the agent or Jon would show the result is right.",
164
- )
165
- ALL_EXAMPLE_NOUN_STOPWORDS: frozenset[str] = frozenset(
166
- {
167
- "none",
168
- "the",
169
- "and",
170
- "for",
171
- "with",
172
- "from",
173
- "that",
174
- "this",
175
- "only",
176
- "illustration",
177
- "example",
178
- "into",
179
- "when",
180
- "your",
181
- "their",
182
- "about",
183
- "such",
184
- "like",
185
- "say",
186
- }
187
- )
@@ -1,128 +0,0 @@
1
- """Role-shaped default rewrite for a first task when no rewrite command is set.
2
-
3
- ``refusal_reason`` in ``deepen_role_gate.py`` refuses a rewrite that pastes the
4
- four question stems back at the worker, and one without the SCOPE, EXAMPLES,
5
- and ZOOM-IN BLOCKER lines. The default rewrite is built to carry those lines,
6
- so a first task deepens on every host with nothing configured: the scope is
7
- the task's own sentences with the marked examples taken out, the examples are
8
- the parts the task marks as examples, and the blocker asks for confirmation
9
- when the scope names one of those examples.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import re
15
-
16
- from hooks_constants.deepen_default_rewrite_constants import (
17
- ALL_EXAMPLE_END_MARKERS,
18
- ALL_EXAMPLE_SENTENCE_MARKERS,
19
- DEFAULT_BLOCKER_TEMPLATE,
20
- DEFAULT_CLOSING_LINE,
21
- DEFAULT_SCOPE_MAX_CHARACTERS,
22
- DEFAULT_TASK_HEADING,
23
- EXAMPLE_LEAD_PUNCTUATION,
24
- EXAMPLE_LIST_SEPARATOR,
25
- EXAMPLE_MARKER_ALTERNATION_SEPARATOR,
26
- EXAMPLE_MARKER_PATTERN_TEMPLATE,
27
- SCOPE_CLIPPED_SUFFIX,
28
- SENTENCE_BREAK_PATTERN,
29
- WORD_SEPARATOR,
30
- )
31
- from hooks_constants.deepen_first_prompt_constants import (
32
- DEEPEN_MARKER,
33
- HARD_CONSTRAINTS_LABEL,
34
- REFUSAL_EXAMPLE_NOUN_IN_SCOPE,
35
- ROLE_BLOCKER_LABEL,
36
- ROLE_BLOCKER_NONE,
37
- ROLE_EXAMPLES_LABEL,
38
- ROLE_SCOPE_LABEL,
39
- )
40
- from deepen_role_gate import refusal_reason
41
-
42
-
43
- def _body_before_constraints(task_text: str) -> str:
44
- all_body_lines: list[str] = []
45
- for each_line in task_text.splitlines():
46
- if each_line.strip().upper().startswith(HARD_CONSTRAINTS_LABEL):
47
- break
48
- all_body_lines.append(each_line)
49
- return WORD_SEPARATOR.join(WORD_SEPARATOR.join(all_body_lines).split())
50
-
51
-
52
- def _sentences(body_text: str) -> list[str]:
53
- return [each.strip() for each in re.split(SENTENCE_BREAK_PATTERN, body_text) if each.strip()]
54
-
55
-
56
- def _example_marker_pattern() -> re.Pattern[str]:
57
- alternatives = EXAMPLE_MARKER_ALTERNATION_SEPARATOR.join(re.escape(each_marker) for each_marker in ALL_EXAMPLE_SENTENCE_MARKERS)
58
- return re.compile(EXAMPLE_MARKER_PATTERN_TEMPLATE.format(alternatives=alternatives), re.IGNORECASE)
59
-
60
-
61
- def _split_example(sentence: str) -> tuple[str, list[str]]:
62
- """Return the sentence without its marked examples, and the example texts.
63
-
64
- A marker counts only as a whole phrase, so "for examples of" and "such
65
- assumptions" stay in the scope. The earliest marker splits first, and the
66
- text after the example is split again for later markers.
67
- """
68
- marker_match = _example_marker_pattern().search(sentence)
69
- if marker_match is None:
70
- return sentence, []
71
- head = sentence[: marker_match.start()].rstrip(EXAMPLE_LEAD_PUNCTUATION + "(")
72
- tail = sentence[marker_match.end() :].lstrip(EXAMPLE_LEAD_PUNCTUATION)
73
- end_index = min(
74
- (tail.find(each_end) for each_end in ALL_EXAMPLE_END_MARKERS if tail.find(each_end) >= 0),
75
- default=len(tail),
76
- )
77
- example = tail[:end_index].rstrip(".!? ")
78
- remainder_scope, all_remainder_examples = _split_example(
79
- tail[end_index:].lstrip(EXAMPLE_LEAD_PUNCTUATION + ")")
80
- )
81
- all_examples = ([example] if example else []) + all_remainder_examples
82
- scope_part = f"{head} {remainder_scope}".strip() if head else ""
83
- return scope_part, all_examples
84
-
85
-
86
- def _clipped(text: str) -> str:
87
- if len(text) <= DEFAULT_SCOPE_MAX_CHARACTERS:
88
- return text
89
- cut = text[:DEFAULT_SCOPE_MAX_CHARACTERS]
90
- kept = cut[: cut.rfind(" ")] if " " in cut else cut
91
- return f"{kept} {SCOPE_CLIPPED_SUFFIX}"
92
-
93
-
94
- def _scope_and_examples(task_text: str) -> tuple[str, str]:
95
- all_splits = [_split_example(each) for each in _sentences(_body_before_constraints(task_text))]
96
- all_examples = [each_example for _, all_sentence_examples in all_splits for each_example in all_sentence_examples]
97
- all_scope_parts = [each_part for each_part, _ in all_splits if each_part]
98
- scope_text = WORD_SEPARATOR.join(all_scope_parts) or task_text.strip()
99
- examples_text = EXAMPLE_LIST_SEPARATOR.join(all_examples) if all_examples else ROLE_BLOCKER_NONE
100
- return _clipped(scope_text), examples_text
101
-
102
-
103
- def _rewrite_with_blocker(scope: str, examples: str, blocker: str, task_text: str) -> str:
104
- return (
105
- f"{DEEPEN_MARKER}\n\n"
106
- f"{ROLE_SCOPE_LABEL} {scope}\n"
107
- f"{ROLE_EXAMPLES_LABEL} {examples}\n"
108
- f"{ROLE_BLOCKER_LABEL} {blocker}\n\n"
109
- f"{DEFAULT_TASK_HEADING}\n{task_text}\n\n"
110
- f"{DEFAULT_CLOSING_LINE}"
111
- )
112
-
113
-
114
- def role_default_rewrite(task_text: str) -> str:
115
- """Return the default rewrite: role lines from the task, the task itself, and one closing order.
116
-
117
- Args:
118
- task_text: The first task with any leading /deepen removed.
119
-
120
- Returns:
121
- A rewrite that opens with the deepen marker and carries the three role lines.
122
- """
123
- scope, examples = _scope_and_examples(task_text)
124
- rewrite = _rewrite_with_blocker(scope, examples, ROLE_BLOCKER_NONE, task_text)
125
- if refusal_reason(task_text, rewrite) != REFUSAL_EXAMPLE_NOUN_IN_SCOPE:
126
- return rewrite
127
- blocker = DEFAULT_BLOCKER_TEMPLATE.format(examples=examples)
128
- return _rewrite_with_blocker(scope, examples, blocker, task_text)
@@ -1,383 +0,0 @@
1
- #!/usr/bin/env python3
2
- """UserPromptSubmit hook: deepen the first interactive task before work starts.
3
-
4
- Cursor runs it on ``beforeSubmitPrompt`` too, and passes that event name to
5
- the copy it imports from the Claude Code settings, so both names count as a
6
- first prompt. When both copies run, the turn marker lets one inject.
7
-
8
- Skip checks stay local. One bounded deepen call runs only for a first task.
9
- Claude settings and Codex cannot replace the prompt string, so this hook emits
10
- additionalContext. Claude desktop and CLI also ship a prompt.submit mod that
11
- rewrites the visible text; this hook stays silent when that mod is on outside
12
- a cloud session.
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import json
18
- import os
19
- import shlex
20
- import subprocess
21
- import sys
22
- from dataclasses import dataclass
23
- from enum import StrEnum
24
- from pathlib import Path
25
-
26
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
27
- if _hooks_dir not in sys.path:
28
- sys.path.insert(0, _hooks_dir)
29
-
30
- from hooks_constants.cloud_session import is_cloud_session
31
- from hooks_constants.deepen_first_prompt_constants import (
32
- ADDITIONAL_CONTEXT_KEY,
33
- ALL_FIRST_PROMPT_EVENT_NAMES,
34
- ALL_HELPER_REPORT_MARKERS,
35
- ALL_LOOP_PROMPT_PREFIXES,
36
- ALL_NONINTERACTIVE_ENV_TRUE_VALUES,
37
- ALL_SPAWN_REWRITE_PREFIXES,
38
- ALL_TRIVIAL_PROMPTS,
39
- CLAUDE_HOST_ENV_VAR,
40
- CLAUDE_HOST_TRUE_VALUE,
41
- DEEPEN_AGENT_MARKER,
42
- DEEPEN_COMMAND_ENV_VAR,
43
- DEEPEN_COMMAND_TIMEOUT_SECONDS,
44
- DEEPEN_FIRST_PROMPT_PLUGIN_NAME,
45
- DEEPEN_MARKER,
46
- DEEPEN_OUTPUT_ENV_VAR,
47
- DEEPEN_OUTPUT_JSON,
48
- DEEPEN_OUTPUT_REWRITE,
49
- DEEPEN_REWRITE_ENV_VAR,
50
- DEEPEN_SLASH_COMMAND,
51
- HOOK_EVENT_NAME_KEY,
52
- HOOK_SPECIFIC_OUTPUT_KEY,
53
- JSONL_ROLE_KEY,
54
- JSONL_TYPE_KEY,
55
- NONINTERACTIVE_CODEX_ENV_VAR,
56
- NONINTERACTIVE_PRINT_ENV_VAR,
57
- PROMPT_FIELD_NAME,
58
- SKIP_DEEPEN_ENV_VAR,
59
- SKIP_DEEPEN_TRUE_VALUE,
60
- TRANSCRIPT_PATH_FIELD_NAME,
61
- USER_PROMPT_SUBMIT_EVENT_NAME,
62
- USER_ROLE_NAME,
63
- USER_TYPE_NAME,
64
- UTF8_ENCODING,
65
- )
66
- from hooks_constants.mod_handoff import is_mod_plugin_enabled
67
- from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
68
- from hooks_constants.subprocess_window import hidden_window_creation_flags
69
- from deepen_default_rewrite import role_default_rewrite
70
- from deepen_role_gate import refusal_reason
71
- from deepen_turn_marker import claim_turn_marker, turn_marker_exists
72
-
73
-
74
- class SkipReason(StrEnum):
75
- """Why the first-task gate left a prompt unchanged."""
76
-
77
- LATER_TURN = "later-turn"
78
- MARKER_PRESENT = "marker-present"
79
- SPAWN_REWRITE = "spawn-rewrite"
80
- SKIP_FLAG = "skip-flag"
81
- NONINTERACTIVE = "noninteractive"
82
- TRIVIAL = "trivial"
83
- SLASH_COMMAND = "slash-command"
84
- DEEPEN_AGENT = "deepen-agent"
85
- LOOP_OR_HELPER = "loop-or-helper"
86
-
87
-
88
- @dataclass(frozen=True)
89
- class Skip:
90
- """Leave the submitted prompt text unchanged."""
91
-
92
- reason: SkipReason
93
-
94
-
95
- @dataclass(frozen=True)
96
- class Deepen:
97
- """Replace the first task with a deeper rewrite."""
98
-
99
- rewritten_prompt: str
100
- additional_context: str
101
-
102
-
103
- @dataclass(frozen=True)
104
- class Refuse:
105
- """Leave the submitted prompt unchanged because the candidate rewrite failed."""
106
-
107
- reason: str
108
-
109
-
110
- DeepenDecision = Skip | Deepen | Refuse
111
-
112
-
113
- def submitted_prompt_text(all_hook_fields: dict[str, object]) -> str:
114
- """Return the submitted prompt text, or empty when the payload has none."""
115
- prompt = all_hook_fields.get(PROMPT_FIELD_NAME)
116
- if isinstance(prompt, str):
117
- return prompt
118
- return ""
119
-
120
-
121
- def _env_flag_is_true(variable_name: str) -> bool:
122
- return os.environ.get(variable_name, "").strip().lower() in ALL_NONINTERACTIVE_ENV_TRUE_VALUES
123
-
124
-
125
- def _normalized_prompt(prompt: str) -> str:
126
- return prompt.strip().lower()
127
-
128
-
129
- def _first_token(prompt: str) -> str:
130
- stripped = prompt.strip()
131
- if not stripped:
132
- return ""
133
- return stripped.split(None, 1)[0]
134
-
135
-
136
- def _parsed_transcript_entry(transcript_line: str) -> dict[str, object] | None:
137
- stripped_line = transcript_line.strip()
138
- if not stripped_line:
139
- return None
140
- try:
141
- parsed_entry = json.loads(stripped_line)
142
- except json.JSONDecodeError:
143
- return None
144
- return parsed_entry if isinstance(parsed_entry, dict) else None
145
-
146
-
147
- def _is_user_entry(all_entry_fields: dict[str, object] | None) -> bool:
148
- if all_entry_fields is None:
149
- return False
150
- if all_entry_fields.get(JSONL_TYPE_KEY) == USER_TYPE_NAME:
151
- return True
152
- if all_entry_fields.get(JSONL_ROLE_KEY) == USER_ROLE_NAME:
153
- return True
154
- message = all_entry_fields.get("message")
155
- return isinstance(message, dict) and message.get(JSONL_ROLE_KEY) == USER_ROLE_NAME
156
-
157
-
158
- def _transcript_has_prior_user_turn(transcript_path: object) -> bool:
159
- if not isinstance(transcript_path, str) or not transcript_path:
160
- return False
161
- try:
162
- with Path(transcript_path).open(encoding=UTF8_ENCODING) as transcript:
163
- return any(_is_user_entry(_parsed_transcript_entry(each_line)) for each_line in transcript)
164
- except OSError:
165
- return False
166
-
167
-
168
- def _environment_skip_reason() -> SkipReason | None:
169
- if os.environ.get(SKIP_DEEPEN_ENV_VAR, "").strip() == SKIP_DEEPEN_TRUE_VALUE:
170
- return SkipReason.SKIP_FLAG
171
- if _env_flag_is_true(NONINTERACTIVE_PRINT_ENV_VAR) or _env_flag_is_true(
172
- NONINTERACTIVE_CODEX_ENV_VAR
173
- ):
174
- return SkipReason.NONINTERACTIVE
175
- return None
176
-
177
-
178
- def _marker_skip_reason(prompt: str) -> SkipReason | None:
179
- if DEEPEN_MARKER in prompt:
180
- return SkipReason.MARKER_PRESENT
181
- if DEEPEN_AGENT_MARKER in prompt:
182
- return SkipReason.DEEPEN_AGENT
183
- if any(prompt.startswith(each_prefix) for each_prefix in ALL_SPAWN_REWRITE_PREFIXES):
184
- return SkipReason.SPAWN_REWRITE
185
- if any(prompt.lstrip().lower().startswith(each_prefix) for each_prefix in ALL_LOOP_PROMPT_PREFIXES):
186
- return SkipReason.LOOP_OR_HELPER
187
- if any(each_marker in prompt.lower() for each_marker in ALL_HELPER_REPORT_MARKERS):
188
- return SkipReason.LOOP_OR_HELPER
189
- return None
190
-
191
-
192
- def _shape_skip_reason(prompt: str) -> SkipReason | None:
193
- first_token = _first_token(prompt)
194
- if first_token.startswith("/") and first_token.lower() != DEEPEN_SLASH_COMMAND:
195
- return SkipReason.SLASH_COMMAND
196
- if first_token.lower() == DEEPEN_SLASH_COMMAND and prompt.strip().lower() == DEEPEN_SLASH_COMMAND:
197
- return SkipReason.SLASH_COMMAND
198
- if _normalized_prompt(prompt) in ALL_TRIVIAL_PROMPTS or not prompt.strip():
199
- return SkipReason.TRIVIAL
200
- return None
201
-
202
-
203
- def skip_reason_for(all_hook_fields: dict[str, object]) -> SkipReason | None:
204
- """Return the first skip that matches, or None when the gate should deepen."""
205
- event_name = all_hook_fields.get("hook_event_name")
206
- if event_name not in ALL_FIRST_PROMPT_EVENT_NAMES and event_name not in (None, ""):
207
- return SkipReason.LATER_TURN
208
- prompt = submitted_prompt_text(all_hook_fields)
209
- reason = _environment_skip_reason() or _marker_skip_reason(prompt) or _shape_skip_reason(prompt)
210
- if reason is not None:
211
- return reason
212
- if turn_marker_exists(all_hook_fields) or _transcript_has_prior_user_turn(
213
- all_hook_fields.get(TRANSCRIPT_PATH_FIELD_NAME)
214
- ):
215
- return SkipReason.LATER_TURN
216
- return None
217
-
218
-
219
- def task_text_for_deepen(prompt: str) -> str:
220
- """Return the task body, stripping a leading /deepen command when present."""
221
- stripped = prompt.strip()
222
- first_token = _first_token(stripped)
223
- if first_token.lower() != DEEPEN_SLASH_COMMAND:
224
- return stripped
225
- remainder = stripped[len(first_token) :].strip()
226
- return remainder or stripped
227
-
228
-
229
- def _with_deepen_marker(rewritten_body: str) -> str:
230
- if DEEPEN_MARKER in rewritten_body:
231
- return rewritten_body
232
- return f"{DEEPEN_MARKER}\n\n{rewritten_body}"
233
-
234
-
235
- def _command_rewrite(task_text: str) -> str | None:
236
- command = os.environ.get(DEEPEN_COMMAND_ENV_VAR, "").strip()
237
- if not command:
238
- return None
239
- try:
240
- completed = subprocess.run(
241
- shlex.split(command),
242
- input=task_text,
243
- capture_output=True,
244
- text=True,
245
- timeout=DEEPEN_COMMAND_TIMEOUT_SECONDS,
246
- check=False,
247
- creationflags=hidden_window_creation_flags(),
248
- )
249
- except (OSError, subprocess.TimeoutExpired):
250
- return None
251
- command_text = completed.stdout.strip()
252
- if completed.returncode != 0 or not command_text:
253
- return None
254
- return _with_deepen_marker(command_text)
255
-
256
-
257
- def _default_rewrite(task_text: str) -> str:
258
- """Return the role-shaped default rewrite the gate accepts."""
259
- return role_default_rewrite(task_text)
260
-
261
-
262
- def expand_first_task(prompt: str) -> str:
263
- """Rewrite a first task so the worker does the omitted expert job."""
264
- stub_rewrite = os.environ.get(DEEPEN_REWRITE_ENV_VAR, "").strip()
265
- task_text = task_text_for_deepen(prompt)
266
- if stub_rewrite:
267
- return _with_deepen_marker(stub_rewrite.replace("{prompt}", task_text))
268
- command_rewrite = _command_rewrite(task_text)
269
- if command_rewrite is not None:
270
- return command_rewrite
271
- return _default_rewrite(task_text)
272
-
273
-
274
- def additional_context_for(rewritten_prompt: str) -> str:
275
- """Return the context a command hook can inject beside the original prompt."""
276
- return (
277
- f"{DEEPEN_MARKER}\n"
278
- "Treat the submitted prompt as this rewritten task and do only that job.\n\n"
279
- f"{rewritten_prompt}"
280
- )
281
-
282
-
283
- def decide_first_prompt(all_hook_fields: dict[str, object]) -> DeepenDecision:
284
- """Skip, refuse, or deepen one UserPromptSubmit payload."""
285
- reason = skip_reason_for(all_hook_fields)
286
- if reason is not None:
287
- return Skip(reason)
288
- prompt = submitted_prompt_text(all_hook_fields)
289
- rewritten_prompt = expand_first_task(prompt)
290
- refusal = refusal_reason(task_text_for_deepen(prompt), rewritten_prompt)
291
- if refusal is not None:
292
- return Refuse(refusal)
293
- return Deepen(
294
- rewritten_prompt=rewritten_prompt,
295
- additional_context=additional_context_for(rewritten_prompt),
296
- )
297
-
298
-
299
- def _print_skip_json(reason: SkipReason) -> None:
300
- sys.stdout.write(json.dumps({"decision": "skip", "reason": reason.value}) + "\n")
301
-
302
-
303
- def _print_refuse_json(reason: str) -> None:
304
- sys.stdout.write(json.dumps({"decision": "refuse", "reason": reason}) + "\n")
305
-
306
-
307
- def _print_deepen_json(deepen: Deepen) -> None:
308
- sys.stdout.write(
309
- json.dumps(
310
- {
311
- "decision": "deepen",
312
- "rewrittenPrompt": deepen.rewritten_prompt,
313
- "additionalContext": deepen.additional_context,
314
- }
315
- )
316
- + "\n"
317
- )
318
-
319
-
320
- def _print_additional_context(deepen: Deepen) -> None:
321
- sys.stdout.write(
322
- json.dumps(
323
- {
324
- HOOK_SPECIFIC_OUTPUT_KEY: {
325
- HOOK_EVENT_NAME_KEY: USER_PROMPT_SUBMIT_EVENT_NAME,
326
- ADDITIONAL_CONTEXT_KEY: deepen.additional_context,
327
- }
328
- }
329
- )
330
- + "\n"
331
- )
332
-
333
-
334
- def should_leave_rewrite_to_mod() -> bool:
335
- """Return True when the Claude prompt.submit mod will rewrite the visible text.
336
-
337
- Claude Code sets ``CLAUDECODE=1`` for every command hook. Codex reads the same
338
- Claude settings file but runs no Claude mod, so a Codex run keeps the envelope.
339
- """
340
- is_claude_host = os.environ.get(CLAUDE_HOST_ENV_VAR, "").strip() == CLAUDE_HOST_TRUE_VALUE
341
- return (
342
- is_claude_host
343
- and is_mod_plugin_enabled(DEEPEN_FIRST_PROMPT_PLUGIN_NAME)
344
- and not is_cloud_session()
345
- )
346
-
347
-
348
- def emit_decision(decision: DeepenDecision, all_hook_fields: dict[str, object]) -> None:
349
- """Write the host encoding for one decision."""
350
- output_mode = os.environ.get(DEEPEN_OUTPUT_ENV_VAR, "").strip().lower()
351
- if isinstance(decision, Skip):
352
- if output_mode == DEEPEN_OUTPUT_JSON:
353
- _print_skip_json(decision.reason)
354
- return
355
- if isinstance(decision, Refuse):
356
- if output_mode == DEEPEN_OUTPUT_JSON:
357
- _print_refuse_json(decision.reason)
358
- return
359
- if not claim_turn_marker(all_hook_fields):
360
- return
361
- if output_mode == DEEPEN_OUTPUT_JSON:
362
- _print_deepen_json(decision)
363
- return
364
- if output_mode == DEEPEN_OUTPUT_REWRITE:
365
- sys.stdout.write(decision.rewritten_prompt)
366
- if not decision.rewritten_prompt.endswith("\n"):
367
- sys.stdout.write("\n")
368
- return
369
- if should_leave_rewrite_to_mod():
370
- return
371
- _print_additional_context(decision)
372
-
373
-
374
- def main() -> None:
375
- """Read one UserPromptSubmit payload and emit a skip or a deepened rewrite."""
376
- hook_payload = read_hook_input_dictionary_from_stdin()
377
- if hook_payload is None:
378
- return
379
- emit_decision(decide_first_prompt(hook_payload), hook_payload)
380
-
381
-
382
- if __name__ == "__main__":
383
- main()