claude-dev-env 8.89.0 → 8.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.agents/skills/pr-visual/README.md +29 -0
  2. package/.agents/skills/pr-visual/SKILL.md +62 -0
  3. package/.agents/skills/pr-visual/assets/icons.symbols +27 -0
  4. package/.agents/skills/pr-visual/assets/viewer.html +147 -0
  5. package/.agents/skills/pr-visual/fixtures/bar.scene.json +193 -0
  6. package/.agents/skills/pr-visual/fixtures/flow.scene.json +201 -0
  7. package/.agents/skills/pr-visual/fixtures/head/metrics.json +16 -0
  8. package/.agents/skills/pr-visual/fixtures/integrated.scene.json +239 -0
  9. package/.agents/skills/pr-visual/fixtures/line.scene.json +193 -0
  10. package/.agents/skills/pr-visual/fixtures/scatter.scene.json +193 -0
  11. package/.agents/skills/pr-visual/fixtures/snapshot.json +21 -0
  12. package/.agents/skills/pr-visual/references/evidence.md +9 -0
  13. package/.agents/skills/pr-visual/references/formats.md +30 -0
  14. package/.agents/skills/pr-visual/references/scene-contract.md +48 -0
  15. package/.agents/skills/pr-visual/references/visual-contract.md +23 -0
  16. package/.agents/skills/pr-visual/scripts/collect.py +111 -0
  17. package/.agents/skills/pr-visual/scripts/metrics.py +99 -0
  18. package/.agents/skills/pr-visual/scripts/render.py +133 -0
  19. package/.agents/skills/pr-visual/scripts/test_collect.py +58 -0
  20. package/.agents/skills/pr-visual/scripts/test_metrics.py +47 -0
  21. package/.agents/skills/pr-visual/scripts/test_render.py +77 -0
  22. package/.agents/skills/pr-visual/scripts/verify.py +218 -0
  23. package/.agents/skills/pr-visual/scripts/visual_config/config/constants.py +104 -0
  24. package/bin/codex-skill-load-block.mjs +0 -25
  25. package/bin/codex-skill-load-block.test.mjs +5 -17
  26. package/bin/codex-trimmed-sol.test.mjs +0 -1
  27. package/bin/cursor-native-hooks.test.mjs +3 -3
  28. package/bin/ever-shipped-skills.mjs +1 -0
  29. package/bin/install-deepstack-plugin.mjs +42 -25
  30. package/bin/install-deepstack-plugin.test.mjs +37 -11
  31. package/bin/install-pstack-plugin.mjs +58 -11
  32. package/bin/install-pstack-plugin.test.mjs +54 -21
  33. package/bin/install.agents-home.test.mjs +0 -1
  34. package/bin/install.codex-rules.test.mjs +0 -2
  35. package/bin/install.cursor-rules.test.mjs +90 -26
  36. package/bin/install.mjs +30 -36
  37. package/bin/install.pr-visual.test.mjs +40 -0
  38. package/bin/install.prompt-context.test.mjs +3 -4
  39. package/bin/install.prune.test.mjs +0 -1
  40. package/bin/install.question-presentation.test.mjs +0 -4
  41. package/bin/install.settings-defaults.test.mjs +0 -1
  42. package/bin/install.shared-settings.test.mjs +0 -1
  43. package/bin/install.test.mjs +14 -44
  44. package/bin/install.transaction.test.mjs +0 -8
  45. package/bin/install.uninstall-transaction.test.mjs +0 -2
  46. package/bin/install.update-keeps-hooks.test.mjs +0 -1
  47. package/docs/BDD_SCENARIO_QUALITY.md +1 -1
  48. package/docs/BDD_TEST_LAYOUT.md +1 -1
  49. package/docs/CODE_RULES.md +1 -1
  50. package/docs/rule-guides/deepstack-wording.md +3 -5
  51. package/docs/rule-guides/session-title.md +5 -4
  52. package/hooks/blocking/session_title_turn_gate.py +287 -0
  53. package/hooks/blocking/test_session_title_format_gate.py +5 -0
  54. package/hooks/blocking/test_session_title_turn_gate.py +264 -0
  55. package/hooks/features/conduct-gates.md +3 -0
  56. package/hooks/features/session-context.md +0 -4
  57. package/hooks/hooks.json +5 -5
  58. package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
  59. package/hooks/hooks_constants/session_title_constants.py +41 -3
  60. package/hooks/lifecycle/nested_project_hooks.py +2 -2
  61. package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
  62. package/hooks/session/compaction_user_messages.py +2 -2
  63. package/package.json +2 -2
  64. package/rules/deepstack-wording.md +3 -3
  65. package/scripts/ci/install-playtest.mjs +58 -314
  66. package/scripts/ci/install-playtest.test.mjs +24 -6
  67. package/scripts/policy_lint/adapter_configuration.py +1 -0
  68. package/scripts/policy_lint/config/constants.py +1 -0
  69. package/scripts/test_deepstack_review_criterion.py +2 -3
  70. package/scripts/test_deepstack_wording_rule.py +6 -4
  71. package/scripts/tests/test_adapter_configuration.py +8 -0
  72. package/.agents/skills/deepen/SKILL.md +0 -26
  73. package/commands/deepen.md +0 -14
  74. package/docs/decisions/deepstack-gate.md +0 -32
  75. package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
  76. package/hooks/session/deepen_default_rewrite.py +0 -128
  77. package/hooks/session/deepen_first_prompt.py +0 -383
  78. package/hooks/session/deepen_role_gate.py +0 -171
  79. package/hooks/session/deepen_turn_marker.py +0 -75
  80. package/hooks/session/fixtures/deepstack_replay.json +0 -15
  81. package/hooks/session/grade_deepstack_replay.py +0 -78
  82. package/hooks/session/test_deepen_default_rewrite.py +0 -90
  83. package/hooks/session/test_deepen_first_prompt.py +0 -452
  84. package/hooks/session/test_deepen_role_gate.py +0 -83
  85. package/hooks/session/test_deepen_turn_marker.py +0 -59
  86. package/hooks/session/test_grade_deepstack_replay.py +0 -47
@@ -0,0 +1,264 @@
1
+ import io
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ import session_title_turn_gate
8
+
9
+ REMOTE_TITLE_TOOL = "mcp__claude-code-remote__set_session_title"
10
+ DESKTOP_TITLE_TOOL = "mcp__ccd_session_mgmt__set_session_title"
11
+ TOOL_LISTING = {
12
+ "type": "attachment",
13
+ "attachment": {
14
+ "type": "prompt_snapshot",
15
+ "tools": [{"name": "Bash"}, {"name": REMOTE_TITLE_TOOL}],
16
+ },
17
+ }
18
+ DEFERRED_LISTING = {
19
+ "type": "attachment",
20
+ "attachment": {"type": "deferred_tools_delta", "addedNames": [DESKTOP_TITLE_TOOL]},
21
+ }
22
+ PROMPT = {"type": "user", "message": {"role": "user", "content": "Fix the gate"}}
23
+ WORK_CALL = {
24
+ "type": "assistant",
25
+ "message": {"content": [{"type": "tool_use", "id": "w1", "name": "Bash", "input": {}}]},
26
+ }
27
+ WORK_RESULT = {
28
+ "type": "user",
29
+ "message": {"content": [{"type": "tool_result", "tool_use_id": "w1", "content": "ok"}]},
30
+ }
31
+ REPLY = {"type": "assistant", "message": {"content": [{"type": "text", "text": "Done."}]}}
32
+
33
+
34
+ def title_call(tool_use_id: str, tool_name: str = REMOTE_TITLE_TOOL) -> dict[str, object]:
35
+ return {
36
+ "type": "assistant",
37
+ "message": {
38
+ "content": [
39
+ {
40
+ "type": "tool_use",
41
+ "id": tool_use_id,
42
+ "name": tool_name,
43
+ "input": {"title": "✅ Gate"},
44
+ }
45
+ ]
46
+ },
47
+ }
48
+
49
+
50
+ def title_result(tool_use_id: str, is_error: bool = False) -> dict[str, object]:
51
+ return {
52
+ "type": "user",
53
+ "message": {
54
+ "content": [
55
+ {
56
+ "type": "tool_result",
57
+ "tool_use_id": tool_use_id,
58
+ "content": "ok",
59
+ "is_error": is_error,
60
+ }
61
+ ]
62
+ },
63
+ }
64
+
65
+
66
+ def run_gate(
67
+ monkeypatch: pytest.MonkeyPatch,
68
+ capsys: pytest.CaptureFixture[str],
69
+ tmp_path: Path,
70
+ all_entries: list[dict[str, object]],
71
+ is_stop_hook_active: bool = False,
72
+ ) -> tuple[int, str]:
73
+ transcript_path = tmp_path / "transcript.jsonl"
74
+ transcript_path.write_text(
75
+ "\n".join(json.dumps(each) for each in all_entries) + "\n", encoding="utf-8"
76
+ )
77
+ hook_input = {
78
+ "hook_event_name": "Stop",
79
+ "stop_hook_active": is_stop_hook_active,
80
+ "transcript_path": str(transcript_path),
81
+ }
82
+ monkeypatch.setattr(
83
+ "sys.stdin", io.TextIOWrapper(io.BytesIO(json.dumps(hook_input).encode("utf-8")))
84
+ )
85
+ exit_code = session_title_turn_gate.main()
86
+ return exit_code, capsys.readouterr().out
87
+
88
+
89
+ @pytest.fixture(autouse=True)
90
+ def isolated_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
91
+ monkeypatch.setenv("HOME", str(tmp_path))
92
+ monkeypatch.setenv("USERPROFILE", str(tmp_path))
93
+
94
+
95
+ def test_should_hold_a_turn_that_set_no_title(
96
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
97
+ ) -> None:
98
+ exit_code, stdout_text = run_gate(
99
+ monkeypatch, capsys, tmp_path, [TOOL_LISTING, PROMPT, WORK_CALL, WORK_RESULT, REPLY]
100
+ )
101
+ block_output = json.loads(stdout_text)
102
+ assert exit_code == 0
103
+ assert block_output["decision"] == "block"
104
+ assert REMOTE_TITLE_TOOL in block_output["reason"]
105
+ assert all(
106
+ each_emoji in block_output["reason"]
107
+ for each_emoji in ("\U0001f6a9", "\U0001f6a7", "✅", "⏳")
108
+ )
109
+
110
+
111
+ def test_should_pass_a_turn_that_set_a_title(
112
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
113
+ ) -> None:
114
+ all_entries = [
115
+ TOOL_LISTING,
116
+ PROMPT,
117
+ WORK_CALL,
118
+ WORK_RESULT,
119
+ title_call("t1"),
120
+ title_result("t1"),
121
+ REPLY,
122
+ ]
123
+ assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
124
+
125
+
126
+ def test_should_hold_a_turn_whose_title_call_was_denied(
127
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
128
+ ) -> None:
129
+ all_entries = [TOOL_LISTING, PROMPT, title_call("t1"), title_result("t1", is_error=True), REPLY]
130
+ assert (
131
+ json.loads(run_gate(monkeypatch, capsys, tmp_path, all_entries)[1])["decision"] == "block"
132
+ )
133
+
134
+
135
+ def test_should_pass_a_held_turn_whose_title_call_failed(
136
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
137
+ ) -> None:
138
+ all_entries = [TOOL_LISTING, PROMPT, title_call("t1"), title_result("t1", is_error=True), REPLY]
139
+ assert run_gate(monkeypatch, capsys, tmp_path, all_entries, is_stop_hook_active=True) == (
140
+ 0,
141
+ "",
142
+ )
143
+
144
+
145
+ def test_should_hold_a_held_turn_with_no_title_call(
146
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
147
+ ) -> None:
148
+ all_entries = [TOOL_LISTING, PROMPT, WORK_CALL, WORK_RESULT, REPLY]
149
+ stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries, is_stop_hook_active=True)[1]
150
+ assert json.loads(stdout_text)["decision"] == "block"
151
+
152
+
153
+ def test_should_hold_when_the_title_was_set_only_in_an_earlier_turn(
154
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
155
+ ) -> None:
156
+ all_entries = [
157
+ PROMPT,
158
+ title_call("t1"),
159
+ title_result("t1"),
160
+ REPLY,
161
+ PROMPT,
162
+ WORK_CALL,
163
+ WORK_RESULT,
164
+ REPLY,
165
+ ]
166
+ block_output = json.loads(run_gate(monkeypatch, capsys, tmp_path, all_entries)[1])
167
+ assert block_output["decision"] == "block"
168
+
169
+
170
+ def test_should_start_a_turn_at_an_image_only_prompt(
171
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
172
+ ) -> None:
173
+ image_prompt = {
174
+ "type": "user",
175
+ "message": {"content": [{"type": "image", "source": {"type": "base64", "data": ""}}]},
176
+ }
177
+ all_entries = [
178
+ TOOL_LISTING,
179
+ PROMPT,
180
+ title_call("t1"),
181
+ title_result("t1"),
182
+ REPLY,
183
+ image_prompt,
184
+ REPLY,
185
+ ]
186
+ stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)[1]
187
+ assert json.loads(stdout_text)["decision"] == "block"
188
+
189
+
190
+ def test_should_hold_a_turn_whose_only_title_call_ran_on_a_sidechain(
191
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
192
+ ) -> None:
193
+ sidechain_call = {**title_call("s1"), "isSidechain": True}
194
+ sidechain_result = {**title_result("s1"), "isSidechain": True}
195
+ all_entries = [TOOL_LISTING, PROMPT, sidechain_call, sidechain_result, REPLY]
196
+ stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)[1]
197
+ assert json.loads(stdout_text)["decision"] == "block"
198
+
199
+
200
+ def test_should_keep_the_turn_across_a_stop_block_and_a_meta_entry(
201
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
202
+ ) -> None:
203
+ stop_block = {
204
+ "type": "attachment",
205
+ "attachment": {"type": "hook_blocking_error", "hookEvent": "Stop"},
206
+ }
207
+ meta_entry = {"type": "user", "isMeta": True, "message": {"content": "Stop hook feedback"}}
208
+ all_entries = [
209
+ TOOL_LISTING,
210
+ PROMPT,
211
+ title_call("t1"),
212
+ title_result("t1"),
213
+ REPLY,
214
+ stop_block,
215
+ meta_entry,
216
+ REPLY,
217
+ ]
218
+ assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
219
+
220
+
221
+ def test_should_name_a_deferred_title_tool_in_the_reason(
222
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
223
+ ) -> None:
224
+ block_output = json.loads(
225
+ run_gate(monkeypatch, capsys, tmp_path, [DEFERRED_LISTING, PROMPT, REPLY])[1]
226
+ )
227
+ assert DESKTOP_TITLE_TOOL in block_output["reason"]
228
+
229
+
230
+ @pytest.mark.parametrize(
231
+ "all_entries",
232
+ [
233
+ [PROMPT, WORK_CALL, WORK_RESULT, REPLY],
234
+ [
235
+ DEFERRED_LISTING,
236
+ {
237
+ "type": "attachment",
238
+ "attachment": {
239
+ "type": "deferred_tools_delta",
240
+ "removedNames": [DESKTOP_TITLE_TOOL],
241
+ },
242
+ },
243
+ PROMPT,
244
+ REPLY,
245
+ ],
246
+ ],
247
+ )
248
+ def test_should_pass_a_session_without_the_title_tool(
249
+ monkeypatch: pytest.MonkeyPatch,
250
+ capsys: pytest.CaptureFixture[str],
251
+ tmp_path: Path,
252
+ all_entries: list[dict[str, object]],
253
+ ) -> None:
254
+ assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
255
+
256
+
257
+ def test_should_pass_when_the_transcript_cannot_be_read(
258
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
259
+ ) -> None:
260
+ hook_input = {"hook_event_name": "Stop", "transcript_path": str(tmp_path / "missing.jsonl")}
261
+ monkeypatch.setattr(
262
+ "sys.stdin", io.TextIOWrapper(io.BytesIO(json.dumps(hook_input).encode("utf-8")))
263
+ )
264
+ assert (session_title_turn_gate.main(), capsys.readouterr().out) == (0, "")
@@ -12,6 +12,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
12
12
  - `blocking/edit_marker_gate.py` rejects strikethrough and edit-note markers in replacement chat text or cards.
13
13
  - `blocking/issue_close_handoff_gate.py` denies an issue comment or issue write that closes the issue and routes a found defect to another issue or epic.
14
14
  - `blocking/session_title_format_gate.py` denies a session title that breaks the `<emoji> <name>` shape or has a name longer than 25 characters.
15
+ - `blocking/session_title_turn_gate.py` holds the end of a turn in a session that has a `__set_session_title` tool until a title call in that turn comes back without an error. A held turn whose title call failed may end.
15
16
  - `blocking/step_note_gate.py` requires a status line before a tool call while its opt-in flag is on.
16
17
  - `blocking/verify_before_acting.py` blocks a completed mutating call when the agent's reasoning contains an unchecked claim.
17
18
  - `blocking/pr_lifecycle_skill_gate.py` denies a commit, push, pull request action, or merge until the `pr-lifecycle` skill appears in the transcript since the last compaction.
@@ -30,6 +31,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
30
31
  - `blocking/edit_marker_gate.py` runs on `PreToolUse`, matcher `mcp__.*__update_message`, timeout `10` seconds in `hooks.json`.
31
32
  - `blocking/issue_close_handoff_gate.py` runs on `PreToolUse`, matcher `mcp__.*__(add_issue_comment|issue_write|update_issue_comment)`, timeout `10` seconds in `hooks.json`.
32
33
  - `blocking/session_title_format_gate.py` runs on `PreToolUse`, matcher `mcp__.*__set_session_title`, timeout `10` seconds in `hooks.json`.
34
+ - `blocking/session_title_turn_gate.py` runs on `Stop`, timeout `10` seconds in `hooks.json`.
33
35
  - `blocking/step_note_gate.py` runs on `PreToolUse`, matcher `*`, timeout `15` seconds in `hooks.json`.
34
36
  - `blocking/verify_before_acting.py` runs on `PostToolUse`, matcher `Write|Edit|MultiEdit|NotebookEdit|Agent|Task|apply_patch|Bash|PowerShell|mcp__.*`, timeout `10` seconds in `hooks.json`.
35
37
  - `blocking/pr_lifecycle_skill_gate.py` runs on `PreToolUse`, matcher `Bash|PowerShell|mcp__.*__(create_pull_request|merge_pull_request|enable_pr_auto_merge|update_pull_request|actions_run_trigger)`, timeout `10` seconds in `hooks.json`.
@@ -52,6 +54,7 @@ Preconditions:
52
54
  - **Edit marker.** Input is an update message with strikethrough or an edit note. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_edit_marker_gate.py -q`. The adjacent test observes a denial; clean replacement text passes.
53
55
  - **Handoff close.** Input is an issue comment that says the issue is closed and that the found defect belongs to another issue. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_issue_close_handoff_gate.py -q`. The adjacent test observes a denial; a close with a plain cross-reference and a routing comment on an open issue both pass.
54
56
  - **Session title.** Input is a set session title call with no leading emoji or a name over 25 characters. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_session_title_format_gate.py -q`. The adjacent test observes a denial that names the broken rule; a well-formed title passes.
57
+ - **Title this turn.** Input is a transcript that lists the title tool and a turn with no title call. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_session_title_turn_gate.py -q`. The adjacent test observes a `block` decision that names the tool and the four status emoji; a turn whose title call came back without an error passes.
55
58
  - **Step note.** Input is a tool call after a transcript message without a status line while the flag is on. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_step_note_gate.py -q`. The adjacent test observes a block until a status line appears.
56
59
  - **Checked claim.** Input is a Write after hedged reasoning in the transcript. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_verify_before_acting.py -q`. The adjacent test observes a block and a logged blocked outcome.
57
60
  - **Lifecycle skill.** Input is a `git commit` or `gh pr create` command with no `pr-lifecycle` invocation in the transcript. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_pr_lifecycle_skill_gate.py -q`. The adjacent test observes a denial, then a silent allow once the skill or its slash command appears.
@@ -10,7 +10,6 @@ This family injects reminders and task guidance when an agent starts, resumes, s
10
10
  - `session/working_style_prompt.py` injects the session's working style at session start.
11
11
  - `session/advisor_rules_prompt.py` injects advisor guidance when the built-in advisor is enabled.
12
12
  - `session/orchestrator_auto_starter.py` injects an orchestrator directive when its environment flag is enabled.
13
- - `session/deepen_first_prompt.py` rewrites a first interactive user task so the worker does the omitted expert job.
14
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.
15
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.
16
15
 
@@ -19,8 +18,6 @@ This family injects reminders and task guidance when an agent starts, resumes, s
19
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`.
20
19
  - `session/compaction_recall_prompt.py` runs on `SessionStart` matcher `compact` at `10` seconds in `hooks.json`. The condensing hook is a `type: "agent"` hook on `PostToolUse` matcher `Bash`, with `if` set to `Bash(*compaction_user_messages.py*)`, model `haiku`, and a `180` second timeout. The hook's Read call obeys session permissions, so the package `settings.json` allows `Read(~/.claude/compaction-recall/**)`.
21
20
  - `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`.
22
- - `session/deepen_first_prompt.py` runs on `UserPromptSubmit`, matcher empty, timeout `10` seconds in `hooks.json`.
23
- - In Cursor, `session/deepen_first_prompt.py` also runs on `beforeSubmitPrompt`. Cursor passes that event name to the `UserPromptSubmit` copy it imports from the Claude Code settings too. When both copies run, `session/deepen_turn_marker.py` creates the turn marker only when it does not exist yet, so one copy injects. A later prompt skips when either the transcript marker or the session marker exists, or when the transcript already has a user line in Claude Code's or Cursor's shape.
24
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`.
25
22
  - `advisory/auto_mode_denial_quick_fix.py` runs on `PermissionDenied`, matcher `*`, timeout `10` seconds in `hooks.json`.
26
23
 
@@ -36,7 +33,6 @@ Preconditions:
36
33
  - **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.
37
34
  - **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.
38
35
  - **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.
39
- - **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, a refusal of the paste path, a rewrite that carries the SCOPE, EXAMPLES, and ZOOM-IN BLOCKER lines, and one injection when two copies see the same Cursor prompt. `python -m pytest packages/claude-dev-env/hooks/session/test_deepen_turn_marker.py -q` observes that only the first marker claim wins. With nothing configured, `deepen_default_rewrite.py` builds that role-shaped rewrite, and `test_deepen_default_rewrite.py` checks the gate accepts it.
40
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.
41
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.
42
38
 
package/hooks/hooks.json CHANGED
@@ -284,11 +284,6 @@
284
284
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/skill_loaded_reminder.py",
285
285
  "timeout": 10
286
286
  },
287
- {
288
- "type": "command",
289
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/deepen_first_prompt.py",
290
- "timeout": 10
291
- },
292
287
  {
293
288
  "type": "command",
294
289
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/nested_project_hooks.py",
@@ -389,6 +384,11 @@
389
384
  "type": "command",
390
385
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/open_tasks_stop_gate.py",
391
386
  "timeout": 10
387
+ },
388
+ {
389
+ "type": "command",
390
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/session_title_turn_gate.py",
391
+ "timeout": 10
392
392
  }
393
393
  ]
394
394
  }
@@ -14,7 +14,6 @@ __all__ = [
14
14
  "ENABLED_PLUGINS_SETTINGS_KEY",
15
15
  "PLUGIN_MARKETPLACE_SEPARATOR",
16
16
  "PROJECT_SETTINGS_DIRECTORY_NAME",
17
- "DEEPEN_FIRST_PROMPT_PLUGIN_NAME",
18
17
  "SESSION_PROMPTS_PLUGIN_NAME",
19
18
  "SHELL_GUARDS_PLUGIN_NAME",
20
19
  "USER_SETTINGS_FILE_NAME",
@@ -24,7 +23,6 @@ ENABLED_PLUGINS_SETTINGS_KEY = "enabledPlugins"
24
23
  PLUGIN_MARKETPLACE_SEPARATOR = "@"
25
24
  SHELL_GUARDS_PLUGIN_NAME = "shell-guards"
26
25
  SESSION_PROMPTS_PLUGIN_NAME = "session-prompts"
27
- DEEPEN_FIRST_PROMPT_PLUGIN_NAME = "deepen-first-prompt"
28
26
  CLAUDE_CONFIG_DIR_ENV_VAR = "CLAUDE_CONFIG_DIR"
29
27
  CLAUDE_PROJECT_DIR_ENV_VAR = "CLAUDE_PROJECT_DIR"
30
28
  DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude"
@@ -1,4 +1,4 @@
1
- """Constants for session_title_format_gate.py."""
1
+ """Constants for session_title_format_gate.py and session_title_turn_gate.py."""
2
2
 
3
3
  import re
4
4
 
@@ -10,14 +10,15 @@ FORMAT_GATE_EVENT_NAME = "PreToolUse"
10
10
  ALLOW_EXIT_CODE = 0
11
11
  BLOCK_EXIT_CODE = 2
12
12
 
13
- STATUS_PREFIX_PATTERN = re.compile("^(\U0001f6a9|\u2705|\u23f3)\ufe0f? (?=\\S)")
13
+ STATUS_PREFIX_PATTERN = re.compile("^(\U0001f6a9|\U0001f6a7|\u2705|\u23f3)\ufe0f? (?=\\S)")
14
14
  MAXIMUM_NAME_LENGTH = 25
15
15
  TRAILING_PUNCTUATION = ".!?,;:"
16
16
  IDENTIFIER_DIGITS_PATTERN = re.compile(r"\d{3,}")
17
17
 
18
18
  STATUS_PREFIX_MESSAGE = (
19
19
  "The title must start with one status emoji and one space:"
20
- " \U0001f6a9 when the user must act, \u2705 when the work is done,"
20
+ " \U0001f6a9 when the user must act, \U0001f6a7 when an outside dependency"
21
+ " blocks the work, \u2705 when the work is done,"
21
22
  " \u23f3 while work runs and nothing waits on the user."
22
23
  )
23
24
  SINGLE_LINE_MESSAGE = "The title must be one line."
@@ -31,3 +32,40 @@ FORMAT_RETRY_INSTRUCTION = (
31
32
  " the main change or outcome in concrete nouns, such as"
32
33
  " '\u23f3 Broker gate + replay rule'."
33
34
  )
35
+
36
+ TURN_GATE_EVENT_NAME = "Stop"
37
+ TRANSCRIPT_PATH_KEY = "transcript_path"
38
+ STOP_HOOK_ACTIVE_KEY = "stop_hook_active"
39
+ DECISION_KEY = "decision"
40
+ REASON_KEY = "reason"
41
+ BLOCK_DECISION = "block"
42
+ UTF8_ENCODING = "utf-8"
43
+ DECODE_ERRORS_POLICY = "replace"
44
+ ENTRY_TYPE_KEY = "type"
45
+ USER_ENTRY_TYPE = "user"
46
+ ASSISTANT_ENTRY_TYPE = "assistant"
47
+ ATTACHMENT_KEY = "attachment"
48
+ MESSAGE_KEY = "message"
49
+ CONTENT_KEY = "content"
50
+ TOOL_USE_BLOCK_TYPE = "tool_use"
51
+ TOOL_RESULT_BLOCK_TYPE = "tool_result"
52
+ BLOCK_ID_KEY = "id"
53
+ BLOCK_NAME_KEY = "name"
54
+ TOOL_USE_ID_KEY = "tool_use_id"
55
+ IS_ERROR_KEY = "is_error"
56
+ SIDECHAIN_FLAG_KEY = "isSidechain"
57
+ ALL_SKIPPED_PROMPT_FLAGS = ("isMeta", SIDECHAIN_FLAG_KEY, "isCompactSummary")
58
+ ALL_TOOL_LISTING_NAME_KEYS = ("addedNames", "readdedNames")
59
+ ALL_TOOL_LISTING_RECORD_KEYS = ("tools", "entries")
60
+ REMOVED_NAMES_KEY = "removedNames"
61
+ TITLE_TOOL_DISPLAY_SEPARATOR = " or "
62
+
63
+ TURN_BLOCK_REASON = (
64
+ "This turn set no session title. Call {tool_name} now with '<emoji> <name>':"
65
+ " \U0001f6a9 the user must act, \U0001f6a7 an outside dependency blocks the work,"
66
+ " \u2705 the work is done, \u23f3 work runs and nothing waits on the user."
67
+ " When the tool is deferred, load its schema with ToolSearch first."
68
+ " When it asks for a session id, get it from get_session with no session_id."
69
+ " After the call, end the turn with one short closing sentence."
70
+ " Leave the earlier reply as it stands and do not repeat it."
71
+ )
@@ -4,8 +4,8 @@
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, first
8
- prompt deepeners, and tool guards then never run. This hook runs them::
7
+ and ``--add-dir`` loads no hooks from the checkouts. Their start hooks, prompt
8
+ hooks, 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
@@ -362,7 +362,7 @@ def test_should_carry_child_user_prompt_submit_output_as_additional_context(
362
362
  context_output = {
363
363
  "hookSpecificOutput": {
364
364
  "hookEventName": "UserPromptSubmit",
365
- "additionalContext": "CDE_DEEPEN:v1\nTreat the submitted prompt as this rewritten task.",
365
+ "additionalContext": "CHILD_CONTEXT:v1\nTreat the submitted prompt as this rewritten task.",
366
366
  }
367
367
  }
368
368
  _make_checkout(
@@ -382,7 +382,7 @@ def test_should_carry_child_user_prompt_submit_output_as_additional_context(
382
382
  assert completed_process.returncode == 0
383
383
  hook_output = json.loads(completed_process.stdout)
384
384
  assert hook_output["hookSpecificOutput"]["hookEventName"] == "UserPromptSubmit"
385
- assert "CDE_DEEPEN:v1" in hook_output["hookSpecificOutput"]["additionalContext"]
385
+ assert "CHILD_CONTEXT:v1" in hook_output["hookSpecificOutput"]["additionalContext"]
386
386
 
387
387
 
388
388
  def test_should_stay_quiet_on_an_event_it_does_not_forward(
@@ -168,8 +168,8 @@ def typed_user_messages(all_transcript_lines: Iterable[str]) -> list[TypedMessag
168
168
  ::
169
169
 
170
170
  user "fix the login bug" -> kept
171
- user "<command-name>/deepen</command-name>
172
- <command-args>speed it up</command-args>" -> "/deepen speed it up"
171
+ user "<command-name>/deepstack-mode</command-name>
172
+ <command-args>speed it up</command-args>" -> "/deepstack-mode speed it up"
173
173
  user tool_result, isMeta skill body,
174
174
  isCompactSummary, isSidechain, origin peer -> left out
175
175
  user "<system-reminder>...", "<local-command-stdout>" -> left out
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.89.0",
3
+ "version": "8.90.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "cde": "bin/cde.mjs"
10
10
  },
11
11
  "scripts": {
12
- "test": "node --test --test-concurrency=1 \"bin/*.test.mjs\" \".agents/skills/**/*.test.mjs\" \"tests/fresh-session/**/*.test.mjs\" \"scripts/ci/*.test.mjs\""
12
+ "test": "node --test --test-concurrency=4 \"bin/*.test.mjs\" \".agents/skills/**/*.test.mjs\" \"tests/fresh-session/**/*.test.mjs\" \"scripts/ci/*.test.mjs\""
13
13
  },
14
14
  "files": [
15
15
  "bin/",
@@ -6,10 +6,10 @@ paths:
6
6
 
7
7
  # DeepStack wording
8
8
 
9
- **When:** Writing a deepened task, a worker prompt, or a line that names the three DeepStack lines.
9
+ **When:** Writing a worker prompt or a line that names the three DeepStack lines.
10
10
 
11
- Use these labels: `SCOPE (role):`, `EXAMPLES (illustration only):`, and `ZOOM-IN BLOCKER:`. Put `CDE_DEEPEN:v1` on line 1. Say "the user".
11
+ Use these labels: `SCOPE (role):`, `EXAMPLES (illustration only):`, and `ZOOM-IN BLOCKER:`. Say "the user".
12
12
 
13
- **Enforcement:** `hooks/session/deepen_first_prompt.py` is the gate. This rule cites that gate.
13
+ **Enforcement:** none.
14
14
 
15
15
  **Full text:** [guide](../docs/rule-guides/deepstack-wording.md). Read it when you write the three lines.