claude-dev-env 8.92.2 → 8.92.3

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.
@@ -53,7 +53,10 @@ try:
53
53
  ALL_PRESET_REPOSITORY_DIGESTS,
54
54
  ALL_SHELL_COMMAND_INPUT_KEYS,
55
55
  ALL_SHELL_TOOL_NAMES,
56
+ ALL_PYTHON_SCRIPTLESS_OPTIONS,
56
57
  ALREADY_EXISTS_MARKER,
58
+ BACKSLASH,
59
+ DOUBLED_BACKSLASH,
57
60
  GH_API_POST_METHOD,
58
61
  GH_API_PULLS_ENDPOINT_PATTERN,
59
62
  GH_API_SUBCOMMAND,
@@ -68,14 +71,28 @@ try:
68
71
  MCP_TOOL_NAME_PREFIX,
69
72
  NAME_DIGEST_TEXT_ENCODING,
70
73
  PULL_REQUEST_URL_PATTERN,
74
+ PYTHON_OPTION_AND_VALUE_WORD_COUNT,
75
+ PYTHON_OPTION_PREFIX,
76
+ PYTHON_SHORT_OPTION_LENGTH,
71
77
  REVIEW_LOOP_CONTEXT_TEMPLATE,
72
78
  SHELL_ARGUMENT_SEPARATOR,
73
79
  )
74
80
  from hooks_constants.post_tool_use_context import (
75
81
  write_post_tool_use_context_to_stdout,
76
82
  )
83
+ from hooks_constants.pr_lifecycle_skill_gate_constants import PYTHON_OPTIONS_WITH_VALUE
77
84
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
78
- from hooks_constants.shell_command_segments import command_tokens, split_into_segments
85
+ from hooks_constants.pull_request_proof_constants import (
86
+ ALL_PYTHON_PROGRAM_NAMES,
87
+ PULL_REQUEST_SCRIPT_CREATE_WORD,
88
+ PULL_REQUEST_SCRIPT_NAME,
89
+ )
90
+ from hooks_constants.pytest_invocation import unquoted_token
91
+ from hooks_constants.shell_command_segments import (
92
+ command_tokens,
93
+ split_into_segments,
94
+ token_basename,
95
+ )
79
96
  from hooks_constants.shell_command_wrappers import (
80
97
  all_wrapped_command_texts,
81
98
  segment_program_and_arguments,
@@ -127,8 +144,34 @@ def _is_gh_api_pulls_post(all_arguments: list[str]) -> bool:
127
144
  return sends_body
128
145
 
129
146
 
147
+ def _python_script_index(all_arguments: list[str]) -> int | None:
148
+ index = 0
149
+ while index < len(all_arguments) and all_arguments[index].startswith(PYTHON_OPTION_PREFIX):
150
+ if all_arguments[index][:PYTHON_SHORT_OPTION_LENGTH] in ALL_PYTHON_SCRIPTLESS_OPTIONS:
151
+ return None
152
+ is_option_with_value = all_arguments[index] in PYTHON_OPTIONS_WITH_VALUE
153
+ index += PYTHON_OPTION_AND_VALUE_WORD_COUNT if is_option_with_value else 1
154
+ return index if index < len(all_arguments) else None
155
+
156
+
157
+ def _is_pull_request_script_create(program_name: str, all_arguments: list[str]) -> bool:
158
+ all_script_arguments = all_arguments
159
+ if program_name != PULL_REQUEST_SCRIPT_NAME:
160
+ script_index = (
161
+ _python_script_index(all_arguments) if program_name in ALL_PYTHON_PROGRAM_NAMES else None
162
+ )
163
+ if script_index is None:
164
+ return False
165
+ if token_basename(unquoted_token(all_arguments[script_index])) != PULL_REQUEST_SCRIPT_NAME:
166
+ return False
167
+ all_script_arguments = all_arguments[script_index + 1 :]
168
+ return all_script_arguments[:1] == [PULL_REQUEST_SCRIPT_CREATE_WORD]
169
+
170
+
130
171
  def _segment_opens_pull_request(all_segment_tokens: list[str]) -> bool:
131
172
  program_name, all_arguments = segment_program_and_arguments(all_segment_tokens)
173
+ if _is_pull_request_script_create(program_name, all_arguments):
174
+ return True
132
175
  return program_name == GH_PROGRAM_NAME and (
133
176
  _is_gh_pr_create(all_arguments) or _is_gh_api_pulls_post(all_arguments)
134
177
  )
@@ -137,11 +180,18 @@ def _segment_opens_pull_request(all_segment_tokens: list[str]) -> bool:
137
180
  def command_creates_pull_request(command: str) -> bool:
138
181
  """Return True when any segment of the shell command opens a pull request.
139
182
 
140
- Three shapes count::
183
+ Four shapes count::
141
184
 
142
185
  gh pr create --fill
143
186
  gh pr new --fill
144
187
  gh api repos/acme/widgets/pulls --input pr.json
188
+ python ~/.agents/skills/pull-request/scripts/pull_request.py create --repo acme/widgets
189
+
190
+ The publisher counts when ``python``, ``python3``, or ``py`` runs a
191
+ script named ``pull_request.py`` with ``create``, or the script runs
192
+ alone. Interpreter options such as ``-X utf8`` before the script are
193
+ skipped, and an unquoted Windows path such as
194
+ ``C:\\tools\\pull_request.py`` keeps its separators.
145
195
 
146
196
  A ``gh api`` call counts only when it targets the pulls collection. An
147
197
  explicit method (``-X POST``, ``-XPOST``, ``--method=POST``) decides
@@ -157,7 +207,11 @@ def command_creates_pull_request(command: str) -> bool:
157
207
  return any(
158
208
  _segment_opens_pull_request(each_segment)
159
209
  for each_command_text in all_wrapped_command_texts(command)
160
- for each_segment in split_into_segments(command_tokens(each_command_text))
210
+ for each_command_tokens in (
211
+ command_tokens(each_command_text),
212
+ command_tokens(each_command_text.replace(BACKSLASH, DOUBLED_BACKSLASH)),
213
+ )
214
+ for each_segment in split_into_segments(each_command_tokens)
161
215
  )
162
216
 
163
217
 
@@ -85,6 +85,13 @@ def _context_of(stdout: str) -> str:
85
85
  "gh api --method=POST repos/acme/widgets/pulls",
86
86
  "gh api -XPOST repos/acme/widgets/pulls",
87
87
  "gh api repos/acme/widgets/pulls -Ftitle=t -fhead=x",
88
+ 'python "C:/Users/me/.agents/skills/pull-request/scripts/pull_request.py" create --repo acme/widgets',
89
+ "python3 ~/.agents/skills/pull-request/scripts/pull_request.py create --title t",
90
+ "py pull_request.py create --fill",
91
+ "git push && python pull_request.py create --repo acme/widgets 2>&1 | tail -5",
92
+ "python -X utf8 ~/.agents/skills/pull-request/scripts/pull_request.py create --fill",
93
+ "python -I -W ignore pull_request.py create",
94
+ "python C:\\Users\\me\\.agents\\skills\\pull-request\\scripts\\pull_request.py create",
88
95
  ],
89
96
  )
90
97
  def test_should_detect_gh_pr_create(command: str) -> None:
@@ -103,6 +110,12 @@ def test_should_detect_gh_pr_create(command: str) -> None:
103
110
  "gh api repos/acme/widgets/issues --input issue.json",
104
111
  "gh api --method GET repos/acme/widgets/pulls -f state=open",
105
112
  "gh api -X GET repos/acme/widgets/pulls -f state=open",
113
+ "python pull_request.py edit --repo acme/widgets --number 42",
114
+ "python other_script.py create",
115
+ "python pull_request.py",
116
+ "python -m pull_request.py create",
117
+ "python -c pull_request.py create",
118
+ "python C:\\Users\\me\\scripts\\my_pull_request.py create",
106
119
  ],
107
120
  )
108
121
  def test_should_not_detect_other_commands(command: str) -> None:
@@ -325,6 +338,16 @@ def test_main_should_emit_the_loop_after_a_gh_api_pulls_post() -> None:
325
338
  assert "jl-cmd-projects/claude-dev-env#42" in _context_of(stdout)
326
339
 
327
340
 
341
+ def test_main_should_emit_the_loop_after_the_pull_request_publisher_creates() -> None:
342
+ publisher_command = (
343
+ 'python "C:/Users/me/.agents/skills/pull-request/scripts/pull_request.py" create'
344
+ " --repo jl-cmd-projects/claude-dev-env --base main --head fix/x --title t"
345
+ " --body-file body.md 2>&1 | tail -5"
346
+ )
347
+ stdout = _run_hook(_shell_payload(publisher_command, f"{OWNED_PR_URL}\n"))
348
+ assert "jl-cmd-projects/claude-dev-env#42" in _context_of(stdout)
349
+
350
+
328
351
  def test_main_should_emit_the_loop_for_a_codex_shell_call() -> None:
329
352
  stdout = _run_hook(
330
353
  _shell_payload(
@@ -23,7 +23,7 @@ This family rewrites a Git Bash command when path conversion would change a revi
23
23
  - `blocking/cloud_graphql_gate.py` runs inside that dispatcher on `PreToolUse`, matcher `Bash`, timeout `60` seconds, for the Bash and PowerShell tools, when `CLAUDE_CODE_REMOTE` is `true`.
24
24
  - `blocking/bash_post_call_dispatcher.py` runs on `PostToolUse`, matcher `Bash|PowerShell`, timeout `60` seconds in `hooks.json`.
25
25
  - `advisory/pr_done_reminder.py` runs inside that dispatcher on `PostToolUse`, matcher `Bash|PowerShell`, timeout `60` seconds. `ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES` selects `Bash` and `PowerShell`.
26
- - `advisory/owned_pr_review_loop.py` runs on `PostToolUse`, matcher `Bash|PowerShell|exec_command|shell_command|mcp__.*__(create_pull_request|github_create_pull_request)`, timeout `10` seconds in `hooks.json`. It wakes on `gh pr create`, `gh pr new`, a POST to `gh api repos/<owner>/<repo>/pulls`, and an MCP pull request creation. The installer also registers it on Codex `PostToolUse` through `ALL_CODEX_NATIVE_POST_TOOL_USE_HOOK_SCRIPT_NAMES`.
26
+ - `advisory/owned_pr_review_loop.py` runs on `PostToolUse`, matcher `Bash|PowerShell|exec_command|shell_command|mcp__.*__(create_pull_request|github_create_pull_request)`, timeout `10` seconds in `hooks.json`. It wakes on `gh pr create`, `gh pr new`, a POST to `gh api repos/<owner>/<repo>/pulls`, a `pull_request.py create` call to the pull request publisher, and an MCP pull request creation. The installer also registers it on Codex `PostToolUse` through `ALL_CODEX_NATIVE_POST_TOOL_USE_HOOK_SCRIPT_NAMES`.
27
27
  - `blocking/owned_pr_merge_stop_gate.py` runs on `Stop`, timeout `60` seconds in `hooks.json`. The installer also registers it on Codex `Stop` through `ALL_CODEX_NATIVE_STOP_HOOK_SCRIPT_NAMES`.
28
28
 
29
29
  ## Proving it
@@ -39,6 +39,12 @@ __all__ = [
39
39
  "GH_SHORT_OPTION_LENGTH",
40
40
  "ALL_GH_API_SHORT_OPTIONS",
41
41
  "GH_OPTION_VALUE_SEPARATOR",
42
+ "PYTHON_OPTION_PREFIX",
43
+ "PYTHON_SHORT_OPTION_LENGTH",
44
+ "PYTHON_OPTION_AND_VALUE_WORD_COUNT",
45
+ "ALL_PYTHON_SCRIPTLESS_OPTIONS",
46
+ "BACKSLASH",
47
+ "DOUBLED_BACKSLASH",
42
48
  "MCP_TOOL_NAME_PREFIX",
43
49
  "ALL_MCP_CREATE_PULL_REQUEST_TOOL_SUFFIXES",
44
50
  "MCP_OWNER_INPUT_KEY",
@@ -85,6 +91,12 @@ GH_LONG_OPTION_PREFIX: str = "--"
85
91
  GH_SHORT_OPTION_LENGTH: int = 2
86
92
  ALL_GH_API_SHORT_OPTIONS: frozenset[str] = frozenset({"-f", "-F", "-X"})
87
93
  GH_OPTION_VALUE_SEPARATOR: str = "="
94
+ PYTHON_OPTION_PREFIX: str = "-"
95
+ PYTHON_SHORT_OPTION_LENGTH: int = 2
96
+ PYTHON_OPTION_AND_VALUE_WORD_COUNT: int = 2
97
+ ALL_PYTHON_SCRIPTLESS_OPTIONS: frozenset[str] = frozenset({"-m", "-c"})
98
+ BACKSLASH: str = "\\"
99
+ DOUBLED_BACKSLASH: str = "\\\\"
88
100
 
89
101
  MCP_TOOL_NAME_PREFIX: str = "mcp__"
90
102
  ALL_MCP_CREATE_PULL_REQUEST_TOOL_SUFFIXES: tuple[str, ...] = (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.92.2",
3
+ "version": "8.92.3",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {