claude-dev-env 8.72.0 → 8.72.2

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/bin/install.mjs +1 -0
  2. package/bin/install.test.mjs +1 -0
  3. package/docs/rule-guides/asd-ste100-language.md +10 -0
  4. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +9 -1
  5. package/hooks/blocking/headless_claude_broker_gate.py +14 -1
  6. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +2 -0
  7. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +46 -0
  8. package/hooks/blocking/test_headless_claude_broker_gate.py +12 -0
  9. package/hooks/conftest.py +18 -0
  10. package/hooks/features/session-context.md +1 -3
  11. package/hooks/hooks.json +0 -5
  12. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  13. package/hooks/hooks_constants/headless_claude_broker_gate_constants.py +7 -1
  14. package/hooks/hooks_constants/mod_handoff.py +91 -0
  15. package/hooks/hooks_constants/mod_handoff_constants.py +38 -0
  16. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -1
  17. package/hooks/hooks_constants/test_mod_handoff.py +100 -0
  18. package/hooks/session/advisor_rules_prompt.py +5 -1
  19. package/hooks/session/orchestrator_auto_starter.py +5 -1
  20. package/hooks/session/task_tool_prompt.py +5 -1
  21. package/hooks/session/test_advisor_rules_prompt.py +11 -0
  22. package/hooks/session/test_orchestrator_auto_starter.py +22 -0
  23. package/hooks/session/test_task_tool_prompt.py +15 -0
  24. package/hooks/session/test_working_style_prompt.py +15 -0
  25. package/hooks/session/working_style_prompt.py +5 -1
  26. package/package.json +1 -1
  27. package/rules/asd-ste100-language.md +1 -1
  28. package/scripts/dev_env_scripts_constants/review_closure_constants.py +24 -0
  29. package/scripts/review_closure_model.py +48 -12
  30. package/scripts/test_files/review_closure/codex_comments.json +1266 -0
  31. package/scripts/tests/test_review_closure_model.py +130 -0
  32. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +0 -22
  33. package/hooks/session/issue_tracker_session_starter.py +0 -136
  34. package/hooks/session/test_issue_tracker_session_starter.py +0 -106
@@ -35,6 +35,7 @@ def _run_main() -> str:
35
35
  @pytest.fixture
36
36
  def config_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
37
37
  monkeypatch.setenv(CLAUDE_CONFIG_DIR_ENV_VAR, str(tmp_path))
38
+ monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
38
39
  monkeypatch.delenv(ADVISOR_DISABLE_ENV_VAR, raising=False)
39
40
  return tmp_path
40
41
 
@@ -56,6 +57,16 @@ class TestAdvisorRulesPrompt:
56
57
  _write_settings(config_directory, {"permissions": {}})
57
58
  assert _run_main() == ""
58
59
 
60
+ def test_should_emit_nothing_while_the_session_prompts_mod_is_on(
61
+ self, config_directory: Path, monkeypatch: pytest.MonkeyPatch
62
+ ) -> None:
63
+ monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(config_directory))
64
+ _write_settings(
65
+ config_directory,
66
+ {"advisorModel": "fable", "enabledPlugins": {"session-prompts@mods-marketplace": True}},
67
+ )
68
+ assert _run_main() == ""
69
+
59
70
  def test_should_emit_nothing_when_advisor_model_is_blank(self, config_directory: Path) -> None:
60
71
  _write_settings(config_directory, {"advisorModel": " "})
61
72
  assert _run_main() == ""
@@ -99,3 +99,25 @@ def test_main_stdin_disabled_prints_nothing() -> None:
99
99
  )
100
100
  assert completed.returncode == 0
101
101
  assert completed.stdout.strip() == ""
102
+
103
+
104
+ def test_main_prints_nothing_while_the_session_prompts_mod_is_on(tmp_path: Path) -> None:
105
+ (tmp_path / "settings.json").write_text(
106
+ json.dumps({"enabledPlugins": {"session-prompts@mods-marketplace": True}}), encoding="utf-8"
107
+ )
108
+ completed = subprocess.run(
109
+ [sys.executable, str(STARTER_SCRIPT)],
110
+ input=json.dumps({"source": "startup"}),
111
+ capture_output=True,
112
+ text=True,
113
+ check=False,
114
+ env={
115
+ **os.environ,
116
+ ORCHESTRATOR_AUTO_STARTER_ENABLED_ENV_VAR: "1",
117
+ "CLAUDE_CONFIG_DIR": str(tmp_path),
118
+ "CLAUDE_CODE_REMOTE": "",
119
+ "CLAUDE_PROJECT_DIR": str(tmp_path),
120
+ },
121
+ )
122
+ assert completed.returncode == 0
123
+ assert completed.stdout.strip() == ""
@@ -7,6 +7,8 @@ from io import StringIO
7
7
  from pathlib import Path
8
8
  from unittest.mock import patch
9
9
 
10
+ import pytest
11
+
10
12
  _SESSION_DIR = Path(__file__).resolve().parent
11
13
  _HOOKS_ROOT = _SESSION_DIR.parent
12
14
  for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
@@ -63,3 +65,16 @@ class TestSessionDirective:
63
65
 
64
66
  def test_build_session_directive_returns_the_shared_constant(self) -> None:
65
67
  assert prompt.build_session_directive() == TASK_TOOL_DIRECTIVE
68
+
69
+
70
+ class TestModHandoff:
71
+ def test_should_stay_silent_while_the_session_prompts_mod_is_on(
72
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
73
+ ) -> None:
74
+ (tmp_path / "settings.json").write_text(
75
+ json.dumps({"enabledPlugins": {"session-prompts@mods-marketplace": True}}), encoding="utf-8"
76
+ )
77
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path))
78
+ monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
79
+ monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
80
+ assert _run_main() == ""
@@ -6,6 +6,8 @@ from io import StringIO
6
6
  from pathlib import Path
7
7
  from unittest.mock import patch
8
8
 
9
+ import pytest
10
+
9
11
  _SESSION_DIR = Path(__file__).resolve().parent
10
12
  _HOOKS_ROOT = _SESSION_DIR.parent
11
13
  for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
@@ -60,3 +62,16 @@ class TestWorkingStylePrompt:
60
62
 
61
63
  def test_build_session_directive_returns_the_shared_constant(self) -> None:
62
64
  assert starter.build_session_directive() == WORKING_STYLE_PROMPT
65
+
66
+
67
+ class TestModHandoff:
68
+ def test_should_stay_silent_while_the_session_prompts_mod_is_on(
69
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
70
+ ) -> None:
71
+ (tmp_path / "settings.json").write_text(
72
+ json.dumps({"enabledPlugins": {"session-prompts@mods-marketplace": True}}), encoding="utf-8"
73
+ )
74
+ monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path))
75
+ monkeypatch.delenv("CLAUDE_CODE_REMOTE", raising=False)
76
+ monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
77
+ assert _run_main() == ""
@@ -17,6 +17,8 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
17
17
  if _hooks_dir not in sys.path:
18
18
  sys.path.insert(0, _hooks_dir)
19
19
 
20
+ from hooks_constants.mod_handoff import is_mod_plugin_enabled
21
+ from hooks_constants.mod_handoff_constants import SESSION_PROMPTS_PLUGIN_NAME
20
22
  from hooks_constants.working_style_prompt_constants import ( # noqa: E402
21
23
  WORKING_STYLE_PROMPT,
22
24
  )
@@ -28,7 +30,9 @@ def build_session_directive() -> str:
28
30
 
29
31
 
30
32
  def main() -> None:
31
- """Emit the working-style prompt as SessionStart additionalContext."""
33
+ """Emit the working-style prompt as SessionStart additionalContext, unless the session-prompts mod is on."""
34
+ if is_mod_plugin_enabled(SESSION_PROMPTS_PLUGIN_NAME):
35
+ return
32
36
  print(
33
37
  json.dumps(
34
38
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.72.0",
3
+ "version": "8.72.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  **When:** Writing chat, tool narration, or repository prose.
4
4
 
5
- Use the sole general language rule. Write short, complete sentences on one topic. Use active voice; lead with conditions; give one action per step. Use plain, precise words and stable terms. Expand abbreviations and contractions first; name unclear pronouns. Be inclusive; punctuate clearly. Preserve exact labels, identifiers, formulas, titles, and interface text. Send a result, blocker, or question. Mark injury or death `WARNING`, equipment damage `CAUTION`; state condition then result. Aim for 20 words per step, 25 per description. Treat checks as aids; have a human verify accuracy, terms, safety, confidentiality, and meaning.
5
+ Use the sole general language rule. Write short, complete sentences on one topic. Use active voice; lead with conditions; one action per step. Use plain, precise words, stable terms. Expand abbreviations and contractions; name unclear pronouns. Be inclusive; punctuate clearly. Preserve exact labels, identifiers, formulas, titles, interface text. Say each fact once. Send a result, blocker, or question. Mark injury or death `WARNING`, equipment damage `CAUTION`; state condition then result. Aim for 20 words per step, 25 per description. Treat checks as aids; have a human verify accuracy, terms, safety, confidentiality, meaning.
6
6
 
7
7
  **Enforcement:** none.
8
8
 
@@ -155,6 +155,30 @@ summary, a Graphite verdict mirror whose findings arrive as review threads, a
155
155
  Qodo description of the change, a Qodo placeholder posted while it reviews, and
156
156
  a Qodo review that found nothing."""
157
157
 
158
+ CODEX_REVIEW_BOT_LOGIN = "chatgpt-codex-connector[bot]"
159
+ ALL_REVIEW_REQUEST_COMMANDS = ("@codex review", "@codex security review")
160
+ ALL_CODEX_NOTICE_DETAILS = (
161
+ "<details> <summary>\u2139\ufe0f About Codex in GitHub</summary>\n<br/>\n\n[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you\n- Open a pull request for review\n- Mark a draft as ready\n- Comment \"@codex review\" or \"@codex security review\".\n\nCodex reacts with \U0001f440 while any review is running, comments if it has suggestions, and reacts with \U0001f44d once all reviews finish with no findings.\n\n</details>",
162
+ "<details> <summary>\u2139\ufe0f About Codex in GitHub</summary>\n<br/>\n\n[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you\n- Open a pull request for review\n- Mark a draft as ready\n- Comment \"@codex review\".\n\nIf Codex has suggestions, it will comment; otherwise it will react with \U0001f44d.\n\n\n\n\nCodex can also answer questions or update the PR. Try commenting \"@codex address that feedback\".\n \n</details>",
163
+ )
164
+ CODEX_SUMMARY_PREFIX = (
165
+ "<!-- codex-pull-request-review-summary -->\n\n"
166
+ "## Codex Review Summary\n\n"
167
+ "This comment shows the latest Codex review activity on this pull request.\n\n"
168
+ "| Review | Status | Commit | Review trigger |\n"
169
+ "| --- | --- | --- | --- |\n"
170
+ )
171
+ CODEX_SUMMARY_ROW_PATTERN = (
172
+ r"\| (?:\U0001f4dd \*\*Code Review\*\*|\U0001f6e1\ufe0f \*\*Security Review\*\*) "
173
+ r"\| \u2705 \*\*Completed\*\* <relative-time datetime=\"[^\"\n]+\">"
174
+ r"[^<\n]+</relative-time> \| `[a-f0-9]{7,40}` \| Manual request \|"
175
+ )
176
+ CODEX_CLEAN_REVIEW_PATTERN = (
177
+ r"Codex Review: Didn't find any major issues\."
178
+ r"(?: Nice work!| Chef's kiss\.| You're on a roll\.| Swish!)?"
179
+ r"\n\n\*\*Reviewed commit:\*\* `[a-f0-9]{7,40}`"
180
+ )
181
+
158
182
  SHORT_SHA_LENGTH: int = 7
159
183
  """How much of a commit identifier the verdict line prints."""
160
184
 
@@ -29,6 +29,7 @@ nobody.
29
29
 
30
30
  from __future__ import annotations
31
31
 
32
+ import re
32
33
  from collections.abc import Iterable, Mapping, Sequence
33
34
  from dataclasses import dataclass
34
35
  from datetime import datetime
@@ -38,6 +39,8 @@ from dev_env_scripts_constants.review_closure_constants import (
38
39
  ALL_COMMENT_AUTHOR_KEYS,
39
40
  ALL_COMMENT_LIST_KEYS,
40
41
  ALL_NOTICE_COMMENT_MARKERS,
42
+ ALL_CODEX_NOTICE_DETAILS,
43
+ ALL_REVIEW_REQUEST_COMMANDS,
41
44
  ALL_OUTDATED_KEYS,
42
45
  ALL_RESOLVED_KEYS,
43
46
  APPROVALS_CHECK_NAME,
@@ -48,6 +51,10 @@ from dev_env_scripts_constants.review_closure_constants import (
48
51
  CHECK_RUN_NAME_KEY,
49
52
  CLOSED_DETAIL,
50
53
  CLOSED_VERDICT_LABEL,
54
+ CODEX_CLEAN_REVIEW_PATTERN,
55
+ CODEX_REVIEW_BOT_LOGIN,
56
+ CODEX_SUMMARY_PREFIX,
57
+ CODEX_SUMMARY_ROW_PATTERN,
51
58
  COMMENT_BODY_KEY,
52
59
  COMMENT_IDENTIFIER_KEY,
53
60
  COMMENT_IDS_KEY,
@@ -187,21 +194,21 @@ def latest_driver_comment_time(
187
194
  all_comments: Iterable[TopLevelComment],
188
195
  all_driver_logins: frozenset[str],
189
196
  ) -> datetime | None:
190
- """Find when the driving agent last posted a top-level comment.
197
+ """Find when the driving agent last posted a substantive top-level comment.
191
198
 
192
199
  Args:
193
200
  all_comments: The top-level comments on the pull request.
194
201
  all_driver_logins: The logins that count as the driving agent.
195
202
 
196
203
  Returns:
197
- The creation time of the driving agent's latest top-level comment, or
204
+ The creation time of the driving agent's latest substantive comment, or
198
205
  None when it posted none.
199
206
  """
200
207
  return max(
201
208
  (
202
209
  each_comment.created_at
203
210
  for each_comment in all_comments
204
- if each_comment.author_login in all_driver_logins
211
+ if each_comment.author_login in all_driver_logins and not each_comment.is_notice
205
212
  ),
206
213
  default=None,
207
214
  )
@@ -241,10 +248,10 @@ def top_level_findings(
241
248
  """Decide which top-level comments still wait on the driving agent.
242
249
 
243
250
  The driving agent answers a top-level comment by posting a top-level
244
- comment of its own. Its latest one answers every comment last touched
251
+ substantive comment of its own. Its latest one answers every comment last touched
245
252
  before it. A person's comment edited after it waits again. A bot's edit
246
253
  leaves its comment answered, since a bot posts each new finding as a
247
- review thread or a new comment.
254
+ review thread or a new comment. Requests and notices answer no findings.
248
255
 
249
256
  Args:
250
257
  all_comments: The top-level comments on the pull request.
@@ -424,14 +431,47 @@ def comment_records_by_id(
424
431
  }
425
432
 
426
433
 
434
+ def _is_codex_notice(body: str) -> bool:
435
+ """Match the verified summary or clean format, including its metadata."""
436
+ normalized = body.strip()
437
+ for each_details in ALL_CODEX_NOTICE_DETAILS:
438
+ if not normalized.endswith(each_details):
439
+ continue
440
+ content = normalized[:-len(each_details)].rstrip()
441
+ if content.startswith(CODEX_SUMMARY_PREFIX):
442
+ all_rows = content[len(CODEX_SUMMARY_PREFIX):].splitlines()
443
+ return bool(all_rows) and all(
444
+ re.fullmatch(CODEX_SUMMARY_ROW_PATTERN, each_row) for each_row in all_rows
445
+ )
446
+ return re.fullmatch(CODEX_CLEAN_REVIEW_PATTERN, content) is not None
447
+ return False
448
+
449
+
450
+ def _is_notice_comment(
451
+ all_comment_fields: Mapping[str, object], body: str, all_extra_markers: Sequence[str]
452
+ ) -> bool:
453
+ """Separate exact review requests and trusted notices from findings."""
454
+ is_command = body.strip().casefold() in ALL_REVIEW_REQUEST_COMMANDS
455
+ if is_command:
456
+ return is_command
457
+ if not _is_bot_author(all_comment_fields):
458
+ return False
459
+ return any(
460
+ each_marker in body for each_marker in (*ALL_NOTICE_COMMENT_MARKERS, *all_extra_markers)
461
+ ) or (
462
+ _comment_author_login(all_comment_fields) == CODEX_REVIEW_BOT_LOGIN and _is_codex_notice(body)
463
+ )
464
+
465
+
427
466
  def parse_top_level_comment(
428
467
  all_comment_fields: Mapping[str, object],
429
468
  all_extra_notice_markers: Sequence[str] = (),
430
469
  ) -> TopLevelComment:
431
470
  """Read one top-level comment from the REST answer.
432
471
 
433
- A bot comment whose body carries a notice marker is a notice. A person's
434
- comment carrying the same text still counts, since anyone can paste it.
472
+ Command-only review requests and verified bot notices carry no findings.
473
+ A person's comment pasting bot notice text still counts. Notice metadata
474
+ and format must match before a Codex bot comment is exempted.
435
475
 
436
476
  Args:
437
477
  all_comment_fields: The comment as the issue comments route reports it.
@@ -450,11 +490,7 @@ def parse_top_level_comment(
450
490
  identifier=all_comment_fields.get(COMMENT_IDENTIFIER_KEY),
451
491
  author_login=_comment_author_login(all_comment_fields),
452
492
  is_bot=is_bot,
453
- is_notice=is_bot
454
- and any(
455
- each_marker in body
456
- for each_marker in (*ALL_NOTICE_COMMENT_MARKERS, *all_extra_notice_markers)
457
- ),
493
+ is_notice=_is_notice_comment(all_comment_fields, body, all_extra_notice_markers),
458
494
  created_at=datetime.fromisoformat(str(all_comment_fields.get(CREATED_AT_KEY))),
459
495
  updated_at=datetime.fromisoformat(str(all_comment_fields.get(UPDATED_AT_KEY))),
460
496
  url=str(all_comment_fields.get(HTML_URL_KEY) or ""),