claude-dev-env 8.82.1 → 8.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.agents/skills/deepen/SKILL.md +42 -0
  2. package/bin/codex-skill-load-block.mjs +32 -0
  3. package/bin/codex-skill-load-block.test.mjs +27 -0
  4. package/bin/codex-trimmed-sol.test.mjs +1 -0
  5. package/bin/ever-shipped-skills.mjs +1 -0
  6. package/bin/install-pstack-plugin.mjs +12 -0
  7. package/bin/install-pstack-plugin.test.mjs +23 -0
  8. package/bin/install.agents-home.test.mjs +1 -0
  9. package/bin/install.codex-rules.test.mjs +2 -0
  10. package/bin/install.cursor-rules.test.mjs +38 -1
  11. package/bin/install.mjs +34 -2
  12. package/bin/install.prompt-context.test.mjs +3 -1
  13. package/bin/install.prune.test.mjs +1 -0
  14. package/bin/install.question-presentation.test.mjs +4 -0
  15. package/bin/install.settings-defaults.test.mjs +1 -0
  16. package/bin/install.shared-settings.test.mjs +1 -0
  17. package/bin/install.test.mjs +34 -1
  18. package/bin/install.transaction.test.mjs +8 -0
  19. package/bin/install.uninstall-transaction.test.mjs +2 -0
  20. package/bin/install.update-keeps-hooks.test.mjs +1 -0
  21. package/commands/deepen.md +19 -0
  22. package/hooks/features/lifecycle-cleanup.md +1 -1
  23. package/hooks/features/session-context.md +3 -0
  24. package/hooks/hooks.json +10 -0
  25. package/hooks/hooks_constants/deepen_first_prompt_constants.py +119 -0
  26. package/hooks/hooks_constants/mod_handoff_constants.py +2 -0
  27. package/hooks/hooks_constants/nested_project_hooks_constants.py +2 -0
  28. package/hooks/lifecycle/nested_project_hooks.py +17 -8
  29. package/hooks/lifecycle/test_nested_project_hooks.py +50 -0
  30. package/hooks/session/deepen_first_prompt.py +390 -0
  31. package/hooks/session/test_deepen_first_prompt.py +274 -0
  32. package/package.json +1 -1
  33. package/scripts/ci/install-playtest.mjs +329 -2
  34. package/scripts/ci/install-playtest.test.mjs +6 -0
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env python3
2
+ """UserPromptSubmit hook: deepen the first interactive task before work starts.
3
+
4
+ Skip checks stay local. One bounded deepen call runs only for a first task.
5
+ Claude settings and Codex cannot replace the prompt string, so this hook emits
6
+ additionalContext. Claude desktop and CLI also ship a prompt.submit mod that
7
+ rewrites the visible text; this hook stays silent when that mod is on outside
8
+ a cloud session.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import shlex
16
+ import subprocess
17
+ import sys
18
+ from dataclasses import dataclass
19
+ from enum import StrEnum
20
+ from pathlib import Path
21
+
22
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
23
+ if _hooks_dir not in sys.path:
24
+ sys.path.insert(0, _hooks_dir)
25
+
26
+ from hooks_constants.cloud_session import is_cloud_session
27
+ from hooks_constants.deepen_first_prompt_constants import (
28
+ ADDITIONAL_CONTEXT_KEY,
29
+ ALL_HELPER_REPORT_MARKERS,
30
+ ALL_LOOP_PROMPT_PREFIXES,
31
+ ALL_NONINTERACTIVE_ENV_TRUE_VALUES,
32
+ ALL_SPAWN_REWRITE_PREFIXES,
33
+ ALL_TRIVIAL_PROMPTS,
34
+ CLAUDE_HOST_ENV_VAR,
35
+ CLAUDE_HOST_TRUE_VALUE,
36
+ DEEPEN_AGENT_MARKER,
37
+ DEEPEN_COMMAND_ENV_VAR,
38
+ DEEPEN_COMMAND_TIMEOUT_SECONDS,
39
+ DEEPEN_FIRST_PROMPT_PLUGIN_NAME,
40
+ DEEPEN_MARKER,
41
+ DEEPEN_OUTPUT_ENV_VAR,
42
+ DEEPEN_OUTPUT_JSON,
43
+ DEEPEN_OUTPUT_REWRITE,
44
+ DEEPEN_REWRITE_ENV_VAR,
45
+ DEEPEN_SLASH_COMMAND,
46
+ DEEPEN_TURN_MARKER_SUFFIX,
47
+ HOOK_EVENT_NAME_KEY,
48
+ HOOK_SPECIFIC_OUTPUT_KEY,
49
+ JSONL_ROLE_KEY,
50
+ JSONL_TYPE_KEY,
51
+ NONINTERACTIVE_CODEX_ENV_VAR,
52
+ NONINTERACTIVE_PRINT_ENV_VAR,
53
+ PROMPT_FIELD_NAME,
54
+ SESSION_ID_FIELD_NAME,
55
+ SKIP_DEEPEN_ENV_VAR,
56
+ SKIP_DEEPEN_TRUE_VALUE,
57
+ TRANSCRIPT_PATH_FIELD_NAME,
58
+ USER_PROMPT_SUBMIT_EVENT_NAME,
59
+ USER_ROLE_NAME,
60
+ USER_TYPE_NAME,
61
+ UTF8_ENCODING,
62
+ )
63
+ from hooks_constants.mod_handoff import is_mod_plugin_enabled
64
+ from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
65
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
66
+
67
+
68
+ class SkipReason(StrEnum):
69
+ """Why the first-task gate left a prompt unchanged."""
70
+
71
+ LATER_TURN = "later-turn"
72
+ MARKER_PRESENT = "marker-present"
73
+ SPAWN_REWRITE = "spawn-rewrite"
74
+ SKIP_FLAG = "skip-flag"
75
+ NONINTERACTIVE = "noninteractive"
76
+ TRIVIAL = "trivial"
77
+ SLASH_COMMAND = "slash-command"
78
+ DEEPEN_AGENT = "deepen-agent"
79
+ LOOP_OR_HELPER = "loop-or-helper"
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class Skip:
84
+ """Leave the submitted prompt text unchanged."""
85
+
86
+ reason: SkipReason
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class Deepen:
91
+ """Replace the first task with a deeper rewrite."""
92
+
93
+ rewritten_prompt: str
94
+ additional_context: str
95
+
96
+
97
+ DeepenDecision = Skip | Deepen
98
+
99
+
100
+ def submitted_prompt_text(all_hook_fields: dict[str, object]) -> str:
101
+ """Return the submitted prompt text, or empty when the payload has none."""
102
+ prompt = all_hook_fields.get(PROMPT_FIELD_NAME)
103
+ if isinstance(prompt, str):
104
+ return prompt
105
+ return ""
106
+
107
+
108
+ def _env_flag_is_true(variable_name: str) -> bool:
109
+ return os.environ.get(variable_name, "").strip().lower() in ALL_NONINTERACTIVE_ENV_TRUE_VALUES
110
+
111
+
112
+ def _normalized_prompt(prompt: str) -> str:
113
+ return prompt.strip().lower()
114
+
115
+
116
+ def _first_token(prompt: str) -> str:
117
+ stripped = prompt.strip()
118
+ if not stripped:
119
+ return ""
120
+ return stripped.split(None, 1)[0]
121
+
122
+
123
+ def _parsed_transcript_entry(transcript_line: str) -> dict[str, object] | None:
124
+ stripped_line = transcript_line.strip()
125
+ if not stripped_line:
126
+ return None
127
+ try:
128
+ parsed_entry = json.loads(stripped_line)
129
+ except json.JSONDecodeError:
130
+ return None
131
+ return parsed_entry if isinstance(parsed_entry, dict) else None
132
+
133
+
134
+ def _is_user_entry(all_entry_fields: dict[str, object] | None) -> bool:
135
+ if all_entry_fields is None:
136
+ return False
137
+ if all_entry_fields.get(JSONL_TYPE_KEY) == USER_TYPE_NAME:
138
+ return True
139
+ message = all_entry_fields.get("message")
140
+ return isinstance(message, dict) and message.get(JSONL_ROLE_KEY) == USER_ROLE_NAME
141
+
142
+
143
+ def _transcript_has_prior_user_turn(transcript_path: object) -> bool:
144
+ if not isinstance(transcript_path, str) or not transcript_path:
145
+ return False
146
+ try:
147
+ with Path(transcript_path).open(encoding=UTF8_ENCODING) as transcript:
148
+ return any(_is_user_entry(_parsed_transcript_entry(each_line)) for each_line in transcript)
149
+ except OSError:
150
+ return False
151
+
152
+
153
+ def _turn_marker_path(all_hook_fields: dict[str, object]) -> Path | None:
154
+ transcript_path = all_hook_fields.get(TRANSCRIPT_PATH_FIELD_NAME)
155
+ if isinstance(transcript_path, str) and transcript_path:
156
+ return Path(transcript_path + DEEPEN_TURN_MARKER_SUFFIX)
157
+ session_id = all_hook_fields.get(SESSION_ID_FIELD_NAME)
158
+ if isinstance(session_id, str) and session_id:
159
+ return Path(os.environ.get("TMPDIR", "/tmp")) / f"cde-deepen-{session_id}"
160
+ return None
161
+
162
+
163
+ def _turn_marker_exists(all_hook_fields: dict[str, object]) -> bool:
164
+ marker_path = _turn_marker_path(all_hook_fields)
165
+ return marker_path is not None and marker_path.is_file()
166
+
167
+
168
+ def write_turn_marker(all_hook_fields: dict[str, object]) -> None:
169
+ """Record that this session already deepened turn one."""
170
+ marker_path = _turn_marker_path(all_hook_fields)
171
+ if marker_path is None:
172
+ return
173
+ try:
174
+ marker_path.write_text(DEEPEN_MARKER, encoding=UTF8_ENCODING)
175
+ except OSError:
176
+ return
177
+
178
+
179
+ def _environment_skip_reason() -> SkipReason | None:
180
+ if os.environ.get(SKIP_DEEPEN_ENV_VAR, "").strip() == SKIP_DEEPEN_TRUE_VALUE:
181
+ return SkipReason.SKIP_FLAG
182
+ if _env_flag_is_true(NONINTERACTIVE_PRINT_ENV_VAR) or _env_flag_is_true(
183
+ NONINTERACTIVE_CODEX_ENV_VAR
184
+ ):
185
+ return SkipReason.NONINTERACTIVE
186
+ return None
187
+
188
+
189
+ def _marker_skip_reason(prompt: str) -> SkipReason | None:
190
+ if DEEPEN_MARKER in prompt:
191
+ return SkipReason.MARKER_PRESENT
192
+ if DEEPEN_AGENT_MARKER in prompt:
193
+ return SkipReason.DEEPEN_AGENT
194
+ if any(prompt.startswith(each_prefix) for each_prefix in ALL_SPAWN_REWRITE_PREFIXES):
195
+ return SkipReason.SPAWN_REWRITE
196
+ if any(prompt.lstrip().lower().startswith(each_prefix) for each_prefix in ALL_LOOP_PROMPT_PREFIXES):
197
+ return SkipReason.LOOP_OR_HELPER
198
+ if any(each_marker in prompt.lower() for each_marker in ALL_HELPER_REPORT_MARKERS):
199
+ return SkipReason.LOOP_OR_HELPER
200
+ return None
201
+
202
+
203
+ def _shape_skip_reason(prompt: str) -> SkipReason | None:
204
+ first_token = _first_token(prompt)
205
+ if first_token.startswith("/") and first_token.lower() != DEEPEN_SLASH_COMMAND:
206
+ return SkipReason.SLASH_COMMAND
207
+ if first_token.lower() == DEEPEN_SLASH_COMMAND and prompt.strip().lower() == DEEPEN_SLASH_COMMAND:
208
+ return SkipReason.SLASH_COMMAND
209
+ if _normalized_prompt(prompt) in ALL_TRIVIAL_PROMPTS or not prompt.strip():
210
+ return SkipReason.TRIVIAL
211
+ return None
212
+
213
+
214
+ def skip_reason_for(all_hook_fields: dict[str, object]) -> SkipReason | None:
215
+ """Return the first skip that matches, or None when the gate should deepen."""
216
+ event_name = all_hook_fields.get("hook_event_name")
217
+ if event_name not in (USER_PROMPT_SUBMIT_EVENT_NAME, None, ""):
218
+ return SkipReason.LATER_TURN
219
+ prompt = submitted_prompt_text(all_hook_fields)
220
+ reason = _environment_skip_reason() or _marker_skip_reason(prompt) or _shape_skip_reason(prompt)
221
+ if reason is not None:
222
+ return reason
223
+ if _turn_marker_exists(all_hook_fields) or _transcript_has_prior_user_turn(
224
+ all_hook_fields.get(TRANSCRIPT_PATH_FIELD_NAME)
225
+ ):
226
+ return SkipReason.LATER_TURN
227
+ return None
228
+
229
+
230
+ def task_text_for_deepen(prompt: str) -> str:
231
+ """Return the task body, stripping a leading /deepen command when present."""
232
+ stripped = prompt.strip()
233
+ first_token = _first_token(stripped)
234
+ if first_token.lower() != DEEPEN_SLASH_COMMAND:
235
+ return stripped
236
+ remainder = stripped[len(first_token) :].strip()
237
+ return remainder or stripped
238
+
239
+
240
+ def _with_deepen_marker(rewritten_body: str) -> str:
241
+ if DEEPEN_MARKER in rewritten_body:
242
+ return rewritten_body
243
+ return f"{DEEPEN_MARKER}\n\n{rewritten_body}"
244
+
245
+
246
+ def _command_rewrite(task_text: str) -> str | None:
247
+ command = os.environ.get(DEEPEN_COMMAND_ENV_VAR, "").strip()
248
+ if not command:
249
+ return None
250
+ try:
251
+ completed = subprocess.run(
252
+ shlex.split(command),
253
+ input=task_text,
254
+ capture_output=True,
255
+ text=True,
256
+ timeout=DEEPEN_COMMAND_TIMEOUT_SECONDS,
257
+ check=False,
258
+ creationflags=hidden_window_creation_flags(),
259
+ )
260
+ except (OSError, subprocess.TimeoutExpired):
261
+ return None
262
+ command_text = completed.stdout.strip()
263
+ if completed.returncode != 0 or not command_text:
264
+ return None
265
+ return _with_deepen_marker(command_text)
266
+
267
+
268
+ def _default_rewrite(task_text: str) -> str:
269
+ return (
270
+ f"{DEEPEN_MARKER}\n\n"
271
+ "Do this task as a leading practitioner in its field would.\n\n"
272
+ f"{task_text}\n\n"
273
+ "Cover four things the original prompt left out:\n"
274
+ "1. What a top person in that field would do that this prompt omitted.\n"
275
+ "2. The raw source to start from.\n"
276
+ "3. The methods and tools they would use.\n"
277
+ "4. What besides the agent or Jon would show the result is right.\n"
278
+ "Then do exactly that job."
279
+ )
280
+
281
+
282
+ def expand_first_task(prompt: str) -> str:
283
+ """Rewrite a first task so the worker does the omitted expert job."""
284
+ stub_rewrite = os.environ.get(DEEPEN_REWRITE_ENV_VAR, "").strip()
285
+ task_text = task_text_for_deepen(prompt)
286
+ if stub_rewrite:
287
+ return _with_deepen_marker(stub_rewrite.replace("{prompt}", task_text))
288
+ command_rewrite = _command_rewrite(task_text)
289
+ if command_rewrite is not None:
290
+ return command_rewrite
291
+ return _default_rewrite(task_text)
292
+
293
+
294
+ def additional_context_for(rewritten_prompt: str) -> str:
295
+ """Return the context a command hook can inject beside the original prompt."""
296
+ return (
297
+ f"{DEEPEN_MARKER}\n"
298
+ "Treat the submitted prompt as this rewritten task and do only that job.\n\n"
299
+ f"{rewritten_prompt}"
300
+ )
301
+
302
+
303
+ def decide_first_prompt(all_hook_fields: dict[str, object]) -> DeepenDecision:
304
+ """Skip or deepen one UserPromptSubmit payload."""
305
+ reason = skip_reason_for(all_hook_fields)
306
+ if reason is not None:
307
+ return Skip(reason)
308
+ rewritten_prompt = expand_first_task(submitted_prompt_text(all_hook_fields))
309
+ return Deepen(
310
+ rewritten_prompt=rewritten_prompt,
311
+ additional_context=additional_context_for(rewritten_prompt),
312
+ )
313
+
314
+
315
+ def _print_skip_json(reason: SkipReason) -> None:
316
+ sys.stdout.write(json.dumps({"decision": "skip", "reason": reason.value}) + "\n")
317
+
318
+
319
+ def _print_deepen_json(deepen: Deepen) -> None:
320
+ sys.stdout.write(
321
+ json.dumps(
322
+ {
323
+ "decision": "deepen",
324
+ "rewrittenPrompt": deepen.rewritten_prompt,
325
+ "additionalContext": deepen.additional_context,
326
+ }
327
+ )
328
+ + "\n"
329
+ )
330
+
331
+
332
+ def _print_additional_context(deepen: Deepen) -> None:
333
+ sys.stdout.write(
334
+ json.dumps(
335
+ {
336
+ HOOK_SPECIFIC_OUTPUT_KEY: {
337
+ HOOK_EVENT_NAME_KEY: USER_PROMPT_SUBMIT_EVENT_NAME,
338
+ ADDITIONAL_CONTEXT_KEY: deepen.additional_context,
339
+ }
340
+ }
341
+ )
342
+ + "\n"
343
+ )
344
+
345
+
346
+ def should_leave_rewrite_to_mod() -> bool:
347
+ """Return True when the Claude prompt.submit mod will rewrite the visible text.
348
+
349
+ Claude Code sets ``CLAUDECODE=1`` for every command hook. Codex reads the same
350
+ Claude settings file but runs no Claude mod, so a Codex run keeps the envelope.
351
+ """
352
+ is_claude_host = os.environ.get(CLAUDE_HOST_ENV_VAR, "").strip() == CLAUDE_HOST_TRUE_VALUE
353
+ return (
354
+ is_claude_host
355
+ and is_mod_plugin_enabled(DEEPEN_FIRST_PROMPT_PLUGIN_NAME)
356
+ and not is_cloud_session()
357
+ )
358
+
359
+
360
+ def emit_decision(decision: DeepenDecision, all_hook_fields: dict[str, object]) -> None:
361
+ """Write the host encoding for one decision."""
362
+ output_mode = os.environ.get(DEEPEN_OUTPUT_ENV_VAR, "").strip().lower()
363
+ if isinstance(decision, Skip):
364
+ if output_mode == DEEPEN_OUTPUT_JSON:
365
+ _print_skip_json(decision.reason)
366
+ return
367
+ write_turn_marker(all_hook_fields)
368
+ if output_mode == DEEPEN_OUTPUT_JSON:
369
+ _print_deepen_json(decision)
370
+ return
371
+ if output_mode == DEEPEN_OUTPUT_REWRITE:
372
+ sys.stdout.write(decision.rewritten_prompt)
373
+ if not decision.rewritten_prompt.endswith("\n"):
374
+ sys.stdout.write("\n")
375
+ return
376
+ if should_leave_rewrite_to_mod():
377
+ return
378
+ _print_additional_context(decision)
379
+
380
+
381
+ def main() -> None:
382
+ """Read one UserPromptSubmit payload and emit a skip or a deepened rewrite."""
383
+ hook_payload = read_hook_input_dictionary_from_stdin()
384
+ if hook_payload is None:
385
+ return
386
+ emit_decision(decide_first_prompt(hook_payload), hook_payload)
387
+
388
+
389
+ if __name__ == "__main__":
390
+ main()
@@ -0,0 +1,274 @@
1
+ """Behavior tests for first-task deepening."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from io import StringIO
9
+ from pathlib import Path
10
+ from unittest.mock import patch
11
+
12
+ import pytest
13
+
14
+ _SESSION_DIR = Path(__file__).resolve().parent
15
+ _HOOKS_ROOT = _SESSION_DIR.parent
16
+ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
17
+ if each_sys_path_entry not in sys.path:
18
+ sys.path.insert(0, each_sys_path_entry)
19
+
20
+ import deepen_first_prompt as deepen
21
+
22
+ from hooks_constants.deepen_first_prompt_constants import (
23
+ DEEPEN_AGENT_MARKER,
24
+ DEEPEN_MARKER,
25
+ DEEPEN_OUTPUT_JSON,
26
+ DEEPEN_OUTPUT_REWRITE,
27
+ DEEPEN_REWRITE_ENV_VAR,
28
+ DEEPEN_TURN_MARKER_SUFFIX,
29
+ SKIP_DEEPEN_ENV_VAR,
30
+ USER_PROMPT_SUBMIT_EVENT_NAME,
31
+ )
32
+ from hooks_constants.skill_loaded_reminder_constants import CLAUDE_SUBAGENT_PROMPT_PREFIX
33
+
34
+
35
+ def _payload(prompt: str, transcript_path: str | None = None) -> dict[str, object]:
36
+ fields: dict[str, object] = {
37
+ "hook_event_name": USER_PROMPT_SUBMIT_EVENT_NAME,
38
+ "prompt": prompt,
39
+ "session_id": "session-one",
40
+ }
41
+ if transcript_path is not None:
42
+ fields["transcript_path"] = transcript_path
43
+ return fields
44
+
45
+
46
+ def _run_main(payload: dict[str, object]) -> str:
47
+ captured_stdout = StringIO()
48
+ with patch("sys.stdin", StringIO(json.dumps(payload))), patch("sys.stdout", captured_stdout):
49
+ deepen.main()
50
+ return captured_stdout.getvalue()
51
+
52
+
53
+ def _user_transcript_line() -> str:
54
+ return json.dumps({"type": "user", "message": {"role": "user", "content": "earlier task"}})
55
+
56
+
57
+ class TestSkipReasonFor:
58
+ def test_should_skip_a_thanks_prompt(self) -> None:
59
+ assert deepen.skip_reason_for(_payload("thanks")) is deepen.SkipReason.TRIVIAL
60
+
61
+ def test_should_skip_a_yes_prompt(self) -> None:
62
+ assert deepen.skip_reason_for(_payload("Yes")) is deepen.SkipReason.TRIVIAL
63
+
64
+ def test_should_skip_status(self) -> None:
65
+ assert deepen.skip_reason_for(_payload("status")) is deepen.SkipReason.TRIVIAL
66
+
67
+ def test_should_skip_a_slash_command_other_than_deepen(self) -> None:
68
+ assert deepen.skip_reason_for(_payload("/sr-loop")) is deepen.SkipReason.SLASH_COMMAND
69
+
70
+ def test_should_skip_bare_deepen_with_no_task(self) -> None:
71
+ assert deepen.skip_reason_for(_payload("/deepen")) is deepen.SkipReason.SLASH_COMMAND
72
+
73
+ def test_should_allow_deepen_with_a_task(self) -> None:
74
+ assert deepen.skip_reason_for(_payload("/deepen fix the login page")) is None
75
+
76
+ def test_should_skip_a_loop_iteration(self) -> None:
77
+ assert (
78
+ deepen.skip_reason_for(_payload("/loop until the tests pass"))
79
+ is deepen.SkipReason.LOOP_OR_HELPER
80
+ )
81
+
82
+ def test_should_skip_a_helper_report(self) -> None:
83
+ assert (
84
+ deepen.skip_reason_for(_payload("<task-notification> helper finished"))
85
+ is deepen.SkipReason.LOOP_OR_HELPER
86
+ )
87
+
88
+ def test_should_skip_a_spawn_rewrite_prefix(self) -> None:
89
+ prompt = CLAUDE_SUBAGENT_PROMPT_PREFIX + "\n\nReply leaf."
90
+ assert deepen.skip_reason_for(_payload(prompt)) is deepen.SkipReason.SPAWN_REWRITE
91
+
92
+ def test_should_skip_when_the_prompt_already_carries_the_marker(self) -> None:
93
+ assert (
94
+ deepen.skip_reason_for(_payload(f"{DEEPEN_MARKER}\n\nDo the job."))
95
+ is deepen.SkipReason.MARKER_PRESENT
96
+ )
97
+
98
+ def test_should_skip_the_deepen_agent_prompt(self) -> None:
99
+ prompt = f"{DEEPEN_AGENT_MARKER}\nAnswer the four deepening questions."
100
+ assert deepen.skip_reason_for(_payload(prompt)) is deepen.SkipReason.DEEPEN_AGENT
101
+
102
+ def test_should_skip_when_the_skip_flag_is_set(
103
+ self, monkeypatch: pytest.MonkeyPatch
104
+ ) -> None:
105
+ monkeypatch.setenv(SKIP_DEEPEN_ENV_VAR, "1")
106
+ assert (
107
+ deepen.skip_reason_for(_payload("Add dark mode to settings"))
108
+ is deepen.SkipReason.SKIP_FLAG
109
+ )
110
+
111
+ def test_should_skip_a_print_mode_session(self, monkeypatch: pytest.MonkeyPatch) -> None:
112
+ monkeypatch.setenv("CLAUDE_CODE_NONINTERACTIVE", "true")
113
+ assert (
114
+ deepen.skip_reason_for(_payload("Add dark mode to settings"))
115
+ is deepen.SkipReason.NONINTERACTIVE
116
+ )
117
+
118
+ def test_should_skip_codex_exec(self, monkeypatch: pytest.MonkeyPatch) -> None:
119
+ monkeypatch.setenv("CODEX_EXEC", "1")
120
+ assert (
121
+ deepen.skip_reason_for(_payload("Add dark mode to settings"))
122
+ is deepen.SkipReason.NONINTERACTIVE
123
+ )
124
+
125
+ def test_should_skip_turn_two_when_the_transcript_has_a_user_line(
126
+ self, tmp_path: Path
127
+ ) -> None:
128
+ transcript_path = tmp_path / "session.jsonl"
129
+ transcript_path.write_text(_user_transcript_line() + "\n", encoding="utf-8")
130
+ assert (
131
+ deepen.skip_reason_for(_payload("Add dark mode", str(transcript_path)))
132
+ is deepen.SkipReason.LATER_TURN
133
+ )
134
+
135
+ def test_should_deepen_a_first_task(self) -> None:
136
+ assert deepen.skip_reason_for(_payload("Add dark mode to the settings page")) is None
137
+
138
+
139
+ class TestDecideFirstPrompt:
140
+ def test_should_emit_additional_context_with_the_four_answers_for_a_first_task(
141
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
142
+ ) -> None:
143
+ monkeypatch.setenv(
144
+ DEEPEN_REWRITE_ENV_VAR,
145
+ (
146
+ "Omitted: ship contrast tokens with the theme.\n"
147
+ "Source: src/settings/theme.ts\n"
148
+ "Methods: existing token file and a visual check.\n"
149
+ "Proof: screenshot of the settings page in dark mode."
150
+ ),
151
+ )
152
+ transcript_path = tmp_path / "fresh.jsonl"
153
+ decision = deepen.decide_first_prompt(
154
+ _payload("Add dark mode to the settings page", str(transcript_path))
155
+ )
156
+ assert isinstance(decision, deepen.Deepen)
157
+ assert DEEPEN_MARKER in decision.rewritten_prompt
158
+ assert "Omitted: ship contrast tokens" in decision.rewritten_prompt
159
+ assert "src/settings/theme.ts" in decision.rewritten_prompt
160
+ assert "visual check" in decision.rewritten_prompt
161
+ assert "screenshot of the settings page" in decision.additional_context
162
+ assert decision.additional_context.startswith(DEEPEN_MARKER)
163
+
164
+ def test_should_keep_a_skip_silent_on_the_command_hook(
165
+ self, monkeypatch: pytest.MonkeyPatch
166
+ ) -> None:
167
+ monkeypatch.delenv("CDE_DEEPEN_OUTPUT", raising=False)
168
+ assert _run_main(_payload("thanks")) == ""
169
+
170
+ def test_should_write_a_turn_marker_so_turn_two_stays_silent(
171
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
172
+ ) -> None:
173
+ monkeypatch.setenv(DEEPEN_REWRITE_ENV_VAR, "Deeper task body")
174
+ transcript_path = tmp_path / "fresh.jsonl"
175
+ first_stdout = _run_main(_payload("Add dark mode to the settings page", str(transcript_path)))
176
+ assert DEEPEN_MARKER in first_stdout
177
+ marker_path = Path(str(transcript_path) + DEEPEN_TURN_MARKER_SUFFIX)
178
+ assert marker_path.is_file()
179
+ second_stdout = _run_main(_payload("and also the billing page", str(transcript_path)))
180
+ assert second_stdout == ""
181
+
182
+ def test_should_print_rewrite_text_for_the_claude_mod(
183
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
184
+ ) -> None:
185
+ monkeypatch.setenv(DEEPEN_REWRITE_ENV_VAR, "Deeper task body")
186
+ monkeypatch.setenv("CDE_DEEPEN_OUTPUT", DEEPEN_OUTPUT_REWRITE)
187
+ transcript_path = tmp_path / "fresh.jsonl"
188
+ printed = _run_main(_payload("Add dark mode to the settings page", str(transcript_path)))
189
+ assert printed.startswith(DEEPEN_MARKER)
190
+ assert "Deeper task body" in printed
191
+ assert "hookSpecificOutput" not in printed
192
+
193
+ def test_should_print_json_decisions_for_the_playtest(
194
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
195
+ ) -> None:
196
+ monkeypatch.setenv(DEEPEN_REWRITE_ENV_VAR, "Deeper task body")
197
+ monkeypatch.setenv("CDE_DEEPEN_OUTPUT", DEEPEN_OUTPUT_JSON)
198
+ transcript_path = tmp_path / "fresh.jsonl"
199
+ deepen_payload = json.loads(
200
+ _run_main(_payload("Add dark mode to the settings page", str(transcript_path)))
201
+ )
202
+ assert deepen_payload["decision"] == "deepen"
203
+ skip_payload = json.loads(_run_main(_payload("thanks")))
204
+ assert skip_payload == {"decision": "skip", "reason": "trivial"}
205
+
206
+ def test_should_stay_silent_when_the_claude_mod_is_on(
207
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
208
+ ) -> None:
209
+ config_directory = tmp_path / "claude-config"
210
+ config_directory.mkdir()
211
+ (config_directory / "settings.json").write_text(
212
+ json.dumps({"enabledPlugins": {"deepen-first-prompt@claude-dev-env": True}}),
213
+ encoding="utf-8",
214
+ )
215
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(config_directory))
216
+ monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
217
+ monkeypatch.setenv("CLAUDECODE", "1")
218
+ monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
219
+ monkeypatch.setenv(DEEPEN_REWRITE_ENV_VAR, "Deeper task body")
220
+ transcript_path = tmp_path / "fresh.jsonl"
221
+ assert _run_main(_payload("Add dark mode to the settings page", str(transcript_path))) == ""
222
+
223
+ def test_should_emit_context_for_codex_when_the_claude_mod_is_on(
224
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
225
+ ) -> None:
226
+ config_directory = tmp_path / "claude-config"
227
+ config_directory.mkdir()
228
+ (config_directory / "settings.json").write_text(
229
+ json.dumps({"enabledPlugins": {"deepen-first-prompt@claude-dev-env": True}}),
230
+ encoding="utf-8",
231
+ )
232
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(config_directory))
233
+ monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
234
+ monkeypatch.delenv("CLAUDECODE", raising=False)
235
+ monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
236
+ monkeypatch.setenv(DEEPEN_REWRITE_ENV_VAR, "Deeper task body")
237
+ transcript_path = tmp_path / "codex-fresh.jsonl"
238
+ printed = _run_main(_payload("Add dark mode to the settings page", str(transcript_path)))
239
+ envelope = json.loads(printed)
240
+ assert envelope["hookSpecificOutput"]["hookEventName"] == USER_PROMPT_SUBMIT_EVENT_NAME
241
+ assert "Deeper task body" in envelope["hookSpecificOutput"]["additionalContext"]
242
+
243
+ def test_should_still_emit_context_in_a_cloud_session_when_the_mod_is_on(
244
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
245
+ ) -> None:
246
+ config_directory = tmp_path / "claude-config"
247
+ config_directory.mkdir()
248
+ (config_directory / "settings.json").write_text(
249
+ json.dumps({"enabledPlugins": {"deepen-first-prompt@claude-dev-env": True}}),
250
+ encoding="utf-8",
251
+ )
252
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(config_directory))
253
+ monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
254
+ monkeypatch.setenv("CLAUDE_CODE_REMOTE", "true")
255
+ monkeypatch.setenv(DEEPEN_REWRITE_ENV_VAR, "Deeper task body")
256
+ transcript_path = tmp_path / "fresh.jsonl"
257
+ printed = _run_main(_payload("Add dark mode to the settings page", str(transcript_path)))
258
+ envelope = json.loads(printed)
259
+ assert envelope["hookSpecificOutput"]["hookEventName"] == USER_PROMPT_SUBMIT_EVENT_NAME
260
+ assert "Deeper task body" in envelope["hookSpecificOutput"]["additionalContext"]
261
+
262
+
263
+ def test_should_strip_the_deepen_slash_before_rewriting() -> None:
264
+ assert deepen.task_text_for_deepen("/deepen fix the login page") == "fix the login page"
265
+
266
+
267
+ def test_should_name_the_four_omitted_answers_in_the_default_rewrite() -> None:
268
+ rewritten = deepen.expand_first_task("Add dark mode to the settings page")
269
+ assert rewritten.startswith(DEEPEN_MARKER)
270
+ assert "Add dark mode to the settings page" in rewritten
271
+ assert "What a top person in that field would do that this prompt omitted." in rewritten
272
+ assert "The raw source to start from." in rewritten
273
+ assert "The methods and tools they would use." in rewritten
274
+ assert "What besides the agent or Jon would show the result is right." in rewritten
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.82.1",
3
+ "version": "8.83.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {