claude-dev-env 8.84.0 → 8.85.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.
@@ -35,11 +35,12 @@ test('reinstall drops the retired style reminder and keeps one skill reminder ho
35
35
  mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/example/.claude', 'python');
36
36
 
37
37
  const userPromptHooks = settings.hooks.UserPromptSubmit.flatMap(eachGroup => eachGroup.hooks);
38
- assert.equal(userPromptHooks.length, 4);
38
+ assert.equal(userPromptHooks.length, 5);
39
39
  assert.equal(userPromptHooks[0].command, userCommand);
40
40
  assert.match(userPromptHooks[1].command, /session\/skill_loaded_reminder\.py$/);
41
41
  assert.match(userPromptHooks[2].command, /session\/deepen_first_prompt\.py$/);
42
42
  assert.match(userPromptHooks[3].command, /lifecycle\/nested_project_hooks\.py$/);
43
+ assert.match(userPromptHooks[4].command, /blocking\/chat_reply_gate\.py$/);
43
44
  assert.ok(userPromptHooks.every(eachHook => !eachHook.command.includes('style_reminder_prompt')));
44
45
 
45
46
  const firstMerge = JSON.stringify(settings);
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """Hook that shows the user one short chat reply per message.
3
+
4
+ ``reply_length_gate.py`` checks only the chat tools that post to Slack.
5
+ A reply typed straight into the chat is no tool call, so this hook runs
6
+ the same checks on it::
7
+
8
+ UserPromptSubmit -> additionalContext states the rules
9
+ MessageDisplay, batch before the last -> hold the text, show nothing yet
10
+ MessageDisplay, last batch, passes -> show the whole message
11
+ MessageDisplay, last batch, fails -> show a draft from a small model that passes
12
+
13
+ A Stop hook would see the reply only after it is on screen, so each block
14
+ would add a second reply under the first. MessageDisplay changes the text
15
+ before it renders. The transcript keeps the original, so the next
16
+ UserPromptSubmit tells the agent the text the user saw.
17
+
18
+ The draft comes from ``claude -p`` on DEFAULT_DRAFTER_MODEL with no setting
19
+ sources and no tools. The draft must pass ``reply_violation``; after
20
+ MAXIMUM_DRAFT_ATTEMPTS failed drafts, the original shows.
21
+
22
+ The checks come from ``reply_length_gate.reply_violation``: at most
23
+ MAXIMUM_SENTENCE_COUNT sentences of at most MAXIMUM_WORDS_PER_SENTENCE
24
+ words, no mid-sentence colon, no em dash, linked pull requests, a source
25
+ for each causal claim, and no banned word.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import shutil
33
+ import subprocess
34
+ import sys
35
+ import tempfile
36
+ import uuid
37
+ from pathlib import Path
38
+
39
+ hooks_root_directory = str(Path(__file__).resolve().parent.parent)
40
+ if hooks_root_directory not in sys.path:
41
+ sys.path.insert(0, hooks_root_directory)
42
+ blocking_directory = str(Path(__file__).resolve().parent)
43
+ if blocking_directory not in sys.path:
44
+ sys.path.insert(0, blocking_directory)
45
+
46
+ from reply_length_gate import reply_violation
47
+ from hooks_constants.chat_reply_gate_constants import (
48
+ CLAUDE_SESSION_ID_ENV_VAR,
49
+ DEFAULT_DRAFTER_MODEL,
50
+ DELTA_KEY,
51
+ DISPLAY_CONTENT_KEY,
52
+ DRAFTER_GUARD_ENV_VAR,
53
+ DRAFTER_MODEL_ENV_VAR,
54
+ DRAFTER_REQUEST_TEMPLATE,
55
+ DRAFTER_ERROR_KEY,
56
+ DRAFTER_EXECUTABLE_NAME,
57
+ DRAFTER_RESULT_KEY,
58
+ DRAFTER_RULES,
59
+ DRAFTER_TIMEOUT_SECONDS,
60
+ FINAL_KEY,
61
+ HELD_TEXT_FILE_PREFIX,
62
+ HOOK_EVENT_NAME_INPUT_KEY,
63
+ MAXIMUM_DRAFT_ATTEMPTS,
64
+ MESSAGE_DISPLAY_EVENT_NAME,
65
+ MESSAGE_ID_KEY,
66
+ REPLY_LIMIT_REMINDER,
67
+ SESSION_END_EVENT_NAME,
68
+ SESSION_ID_KEY,
69
+ SHOWN_DRAFT_FILE_PREFIX,
70
+ SHOWN_DRAFT_PREFIX,
71
+ STATE_FILE_ENCODING,
72
+ STATE_FILE_SUFFIX,
73
+ USER_PROMPT_SUBMIT_EVENT_NAME,
74
+ )
75
+ from hooks_constants.hook_block_logger import log_hook_block
76
+ from hooks_constants.hook_specific_output_keys import (
77
+ HOOK_EVENT_NAME_KEY,
78
+ HOOK_SPECIFIC_OUTPUT_KEY,
79
+ )
80
+ from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
81
+ from hooks_constants.reply_length_gate_constants import (
82
+ ADDITIONAL_CONTEXT_KEY,
83
+ ALLOW_EXIT_CODE,
84
+ )
85
+ from hooks_constants.session_file_id import session_file_id
86
+
87
+
88
+ def state_file_path(file_prefix: str, state_key: object) -> Path:
89
+ """Return the temp file that holds one message's held text or one session's shown draft."""
90
+ file_name = f"{file_prefix}{session_file_id(state_key)}{STATE_FILE_SUFFIX}"
91
+ return Path(tempfile.gettempdir()) / file_name
92
+
93
+
94
+ def read_state_text(state_path: Path) -> str:
95
+ """Return the stored text, or an empty string when the file is absent or unreadable."""
96
+ try:
97
+ return state_path.read_text(encoding=STATE_FILE_ENCODING)
98
+ except OSError:
99
+ return ""
100
+
101
+
102
+ def write_state_text(state_path: Path, state_text: str) -> None:
103
+ """Store the text; a failed write leaves the shown text unchanged."""
104
+ try:
105
+ state_path.write_text(state_text, encoding=STATE_FILE_ENCODING)
106
+ except OSError:
107
+ return
108
+
109
+
110
+ def drafter_command() -> list[str]:
111
+ """Return the headless claude command that drafts with no settings and no tools."""
112
+ return [
113
+ shutil.which(DRAFTER_EXECUTABLE_NAME) or DRAFTER_EXECUTABLE_NAME,
114
+ "-p",
115
+ "--model",
116
+ os.environ.get(DRAFTER_MODEL_ENV_VAR, DEFAULT_DRAFTER_MODEL),
117
+ "--setting-sources",
118
+ "",
119
+ "--tools",
120
+ "",
121
+ "--no-session-persistence",
122
+ "--session-id",
123
+ str(uuid.uuid4()),
124
+ "--output-format",
125
+ "json",
126
+ "--system-prompt",
127
+ DRAFTER_RULES,
128
+ ]
129
+
130
+
131
+ def drafter_environment() -> dict[str, str]:
132
+ """Return the drafter's environment, marked so this hook exits inside it."""
133
+ environment = dict(os.environ)
134
+ environment[DRAFTER_GUARD_ENV_VAR] = "1"
135
+ environment.pop(CLAUDE_SESSION_ID_ENV_VAR, None)
136
+ return environment
137
+
138
+
139
+ def ask_drafter(reply_text: str, violation: str) -> str | None:
140
+ """Return one draft from the small model, or None when the call fails."""
141
+ try:
142
+ completed = subprocess.run(
143
+ drafter_command(),
144
+ input=DRAFTER_REQUEST_TEMPLATE.format(violation=violation, reply_text=reply_text),
145
+ capture_output=True,
146
+ text=True,
147
+ timeout=DRAFTER_TIMEOUT_SECONDS,
148
+ env=drafter_environment(),
149
+ check=False,
150
+ )
151
+ drafter_output = json.loads(completed.stdout)
152
+ draft_text = drafter_output.get(DRAFTER_RESULT_KEY)
153
+ except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError, AttributeError):
154
+ return None
155
+ if drafter_output.get(DRAFTER_ERROR_KEY) is True:
156
+ return None
157
+ if not isinstance(draft_text, str) or not draft_text.strip():
158
+ return None
159
+ return draft_text.strip()
160
+
161
+
162
+ def compliant_draft(reply_text: str, violation: str) -> str | None:
163
+ """Return a draft that passes ``reply_violation``, or None after MAXIMUM_DRAFT_ATTEMPTS."""
164
+ current_violation = violation
165
+ for _ in range(MAXIMUM_DRAFT_ATTEMPTS):
166
+ draft_text = ask_drafter(reply_text, current_violation)
167
+ if draft_text is None:
168
+ continue
169
+ draft_violation = reply_violation(draft_text, None, None)
170
+ if draft_violation is None:
171
+ return draft_text
172
+ current_violation = draft_violation
173
+ return None
174
+
175
+
176
+ def shown_text(all_hook_input: dict[str, object]) -> str | None:
177
+ """Return the text to show for this batch, or None to show the batch as it is."""
178
+ message_id = all_hook_input.get(MESSAGE_ID_KEY)
179
+ if not isinstance(message_id, str) or not message_id:
180
+ return None
181
+ held_path = state_file_path(HELD_TEXT_FILE_PREFIX, message_id)
182
+ held_text = read_state_text(held_path)
183
+ message_text = held_text + str(all_hook_input.get(DELTA_KEY) or "")
184
+ if not all_hook_input.get(FINAL_KEY):
185
+ write_state_text(held_path, message_text)
186
+ return ""
187
+ held_path.unlink(missing_ok=True)
188
+ shown_draft_path = state_file_path(SHOWN_DRAFT_FILE_PREFIX, all_hook_input.get(SESSION_ID_KEY))
189
+ violation = reply_violation(message_text, None, None)
190
+ draft_text = None if violation is None else compliant_draft(message_text, violation)
191
+ if violation is None or draft_text is None:
192
+ shown_draft_path.unlink(missing_ok=True)
193
+ return message_text if held_text else None
194
+ write_state_text(shown_draft_path, draft_text)
195
+ log_hook_block(
196
+ Path(__file__).name,
197
+ MESSAGE_DISPLAY_EVENT_NAME,
198
+ violation,
199
+ offending_input_preview=message_text,
200
+ )
201
+ return draft_text
202
+
203
+
204
+ def write_hook_output(event_name: str, output_key: str, output_text: str) -> None:
205
+ """Print one hookSpecificOutput field for the event."""
206
+ hook_event_output = {HOOK_EVENT_NAME_KEY: event_name, output_key: output_text}
207
+ sys.stdout.write(json.dumps({HOOK_SPECIFIC_OUTPUT_KEY: hook_event_output}))
208
+
209
+
210
+ def reply_limit_reminder(session_id: object) -> str:
211
+ """Return the reply rules, plus the draft the user saw in place of the last reply."""
212
+ shown_draft_path = state_file_path(SHOWN_DRAFT_FILE_PREFIX, session_id)
213
+ shown_draft = read_state_text(shown_draft_path)
214
+ if not shown_draft:
215
+ return REPLY_LIMIT_REMINDER
216
+ shown_draft_path.unlink(missing_ok=True)
217
+ return REPLY_LIMIT_REMINDER + SHOWN_DRAFT_PREFIX + shown_draft
218
+
219
+
220
+ def main() -> int:
221
+ if os.environ.get(DRAFTER_GUARD_ENV_VAR):
222
+ return ALLOW_EXIT_CODE
223
+ hook_input = read_hook_input_dictionary_from_stdin()
224
+ if hook_input is None:
225
+ return ALLOW_EXIT_CODE
226
+ event_name = hook_input.get(HOOK_EVENT_NAME_INPUT_KEY)
227
+ if event_name == USER_PROMPT_SUBMIT_EVENT_NAME:
228
+ write_hook_output(
229
+ USER_PROMPT_SUBMIT_EVENT_NAME,
230
+ ADDITIONAL_CONTEXT_KEY,
231
+ reply_limit_reminder(hook_input.get(SESSION_ID_KEY)),
232
+ )
233
+ return ALLOW_EXIT_CODE
234
+ if event_name == SESSION_END_EVENT_NAME:
235
+ state_file_path(SHOWN_DRAFT_FILE_PREFIX, hook_input.get(SESSION_ID_KEY)).unlink(missing_ok=True)
236
+ return ALLOW_EXIT_CODE
237
+ if event_name != MESSAGE_DISPLAY_EVENT_NAME:
238
+ return ALLOW_EXIT_CODE
239
+ display_text = shown_text(hook_input)
240
+ if display_text is not None:
241
+ write_hook_output(MESSAGE_DISPLAY_EVENT_NAME, DISPLAY_CONTENT_KEY, display_text)
242
+ return ALLOW_EXIT_CODE
243
+
244
+
245
+ if __name__ == "__main__":
246
+ sys.exit(main())
@@ -33,11 +33,10 @@ from inventory_intent_records.config.intent_records_constants import (
33
33
  )
34
34
 
35
35
  from hooks_constants.session_edit_stage_gate_constants import (
36
- SESSION_ID_UNSAFE_CHARACTERS_PATTERN,
37
36
  STATE_FILE_ATOMIC_WRITE_SUFFIX,
38
- STATE_FILE_DEFAULT_SESSION_ID,
39
37
  STATE_FILE_JSON_INDENT_SPACES,
40
38
  )
39
+ from hooks_constants.session_file_id import session_file_id
41
40
 
42
41
 
43
42
  def _intent_file_path(session_id: str) -> Path:
@@ -50,9 +49,7 @@ def _intent_file_path(session_id: str) -> Path:
50
49
  Returns:
51
50
  The absolute path of this session's intent-records file.
52
51
  """
53
- sanitized_session_id = SESSION_ID_UNSAFE_CHARACTERS_PATTERN.sub("", session_id)
54
- effective_session_id = sanitized_session_id or STATE_FILE_DEFAULT_SESSION_ID
55
- file_name = f"{INTENT_FILE_PREFIX}{effective_session_id}{INTENT_FILE_SUFFIX}"
52
+ file_name = f"{INTENT_FILE_PREFIX}{session_file_id(session_id)}{INTENT_FILE_SUFFIX}"
56
53
  return Path(tempfile.gettempdir()) / file_name
57
54
 
58
55
 
@@ -21,10 +21,20 @@ from hooks_constants.pull_request_proof_constants import (
21
21
  ALL_VISIBLE_FILE_SUFFIXES,
22
22
  CHANGED_FILES_SEPARATOR,
23
23
  COMMAND_MARKER,
24
+ ALL_BASE_FLAGS,
24
25
  ALL_DEFAULT_BRANCH_REFERENCES,
25
26
  ALL_HEAD_FLAGS,
27
+ ALL_REPOSITORY_FLAGS,
28
+ BASE_FLAG_ASSIGNMENT_PREFIX,
29
+ COMPARE_ENDPOINT_TEMPLATE,
30
+ COMPARE_FILE_LIST_LIMIT,
31
+ COMPARE_FILE_NAMES_QUERY,
32
+ DEFAULT_BRANCH_QUERY,
33
+ GH_TIMEOUT_SECONDS,
26
34
  HEAD_FLAG_ASSIGNMENT_PREFIX,
27
35
  REMOTE_BRANCH_PREFIX,
36
+ REPOSITORY_ENDPOINT_TEMPLATE,
37
+ REPOSITORY_FLAG_ASSIGNMENT_PREFIX,
28
38
  UNREADABLE_CHANGES_REASON,
29
39
  EVAL_HEADING_PATTERN,
30
40
  EXISTING_WORK_HEADING_PATTERN,
@@ -174,28 +184,55 @@ def _shell_create_calls(command: str, working_directory: str) -> list[CreateCall
174
184
  ]
175
185
 
176
186
 
177
- def _head_flag_value(all_create_arguments: list[str]) -> str | None:
187
+ def _flag_value(all_create_arguments: list[str], all_flags: frozenset[str], assignment_prefix: str) -> str | None:
178
188
  for each_position, each_argument in enumerate(all_create_arguments):
179
- if each_argument in ALL_HEAD_FLAGS and each_position + 1 < len(all_create_arguments):
180
- return all_create_arguments[each_position + 1]
181
- if each_argument.startswith(HEAD_FLAG_ASSIGNMENT_PREFIX):
182
- return each_argument[len(HEAD_FLAG_ASSIGNMENT_PREFIX) :]
189
+ if each_argument in all_flags and each_position + 1 < len(all_create_arguments):
190
+ return unquoted_token(all_create_arguments[each_position + 1])
191
+ if each_argument.startswith(assignment_prefix):
192
+ return unquoted_token(each_argument[len(assignment_prefix) :])
183
193
  return None
184
194
 
185
195
 
186
- def _named_head_branch(all_payload_fields: Mapping[str, object]) -> str | None:
187
- create_fields = create_tool_fields(all_payload_fields)
188
- head_branch = None if create_fields is None else create_fields.get("head")
189
- if isinstance(head_branch, str):
190
- return head_branch
196
+ def _shell_flag_value(
197
+ all_payload_fields: Mapping[str, object], all_flags: frozenset[str], assignment_prefix: str
198
+ ) -> str | None:
191
199
  command = _shell_command(all_payload_fields)
192
200
  if command is None:
193
201
  return None
194
- all_head_flag_values = [
195
- _head_flag_value(all_create_arguments)
202
+ all_flag_values = [
203
+ _flag_value(all_create_arguments, all_flags, assignment_prefix)
196
204
  for all_create_arguments in _all_create_argument_lists(command)
197
205
  ]
198
- return next((each_value for each_value in all_head_flag_values if each_value), None)
206
+ return next((each_value for each_value in all_flag_values if each_value), None)
207
+
208
+
209
+ def _named_create_field(
210
+ all_payload_fields: Mapping[str, object], field_name: str, all_flags: frozenset[str], assignment_prefix: str
211
+ ) -> str | None:
212
+ create_fields = create_tool_fields(all_payload_fields)
213
+ field_value = None if create_fields is None else create_fields.get(field_name)
214
+ if isinstance(field_value, str) and field_value:
215
+ return field_value
216
+ return _shell_flag_value(all_payload_fields, all_flags, assignment_prefix)
217
+
218
+
219
+ def _named_head_branch(all_payload_fields: Mapping[str, object]) -> str | None:
220
+ return _named_create_field(all_payload_fields, "head", ALL_HEAD_FLAGS, HEAD_FLAG_ASSIGNMENT_PREFIX)
221
+
222
+
223
+ def _named_base_branch(all_payload_fields: Mapping[str, object]) -> str | None:
224
+ return _named_create_field(all_payload_fields, "base", ALL_BASE_FLAGS, BASE_FLAG_ASSIGNMENT_PREFIX)
225
+
226
+
227
+ def _named_repository(all_payload_fields: Mapping[str, object]) -> str | None:
228
+ create_fields = create_tool_fields(all_payload_fields)
229
+ if create_fields is not None:
230
+ owner = create_fields.get("owner")
231
+ repository_name = create_fields.get("repo")
232
+ if isinstance(owner, str) and owner and isinstance(repository_name, str) and repository_name:
233
+ return f"{owner}/{repository_name}"
234
+ return None
235
+ return _shell_flag_value(all_payload_fields, ALL_REPOSITORY_FLAGS, REPOSITORY_FLAG_ASSIGNMENT_PREFIX)
199
236
 
200
237
 
201
238
  def _create_calls(all_payload_fields: Mapping[str, object]) -> list[CreateCall]:
@@ -485,9 +522,83 @@ def visible_changed_files(working_directory: str, head_branch: object) -> list[s
485
522
  all_changed_paths = _git_lines(working_directory, "diff", "--name-only", merge_base, head_reference)
486
523
  if all_changed_paths is None:
487
524
  return None
525
+ return _visible_paths(all_changed_paths)
526
+
527
+
528
+ def _visible_paths(all_changed_paths: list[str]) -> list[str]:
488
529
  return [each_path for each_path in all_changed_paths if each_path.lower().endswith(ALL_VISIBLE_FILE_SUFFIXES)]
489
530
 
490
531
 
532
+ def _gh_api_lines(endpoint: str, query: str) -> list[str] | None:
533
+ try:
534
+ completed = subprocess.run(
535
+ ["gh", "api", endpoint, "--jq", query],
536
+ capture_output=True,
537
+ text=True,
538
+ timeout=GH_TIMEOUT_SECONDS,
539
+ check=False,
540
+ )
541
+ except (OSError, subprocess.SubprocessError):
542
+ return None
543
+ return completed.stdout.splitlines() if completed.returncode == 0 else None
544
+
545
+
546
+ def _compare_base_branch(repository: str, base_branch: str | None) -> str | None:
547
+ if base_branch:
548
+ return base_branch
549
+ all_default_branch_lines = _gh_api_lines(
550
+ REPOSITORY_ENDPOINT_TEMPLATE.format(repository=repository), DEFAULT_BRANCH_QUERY
551
+ )
552
+ if not all_default_branch_lines:
553
+ return None
554
+ return all_default_branch_lines[0]
555
+
556
+
557
+ def _visible_compare_paths(repository: str, base_branch: str, head_branch: str) -> list[str] | None:
558
+ all_changed_paths = _gh_api_lines(
559
+ COMPARE_ENDPOINT_TEMPLATE.format(
560
+ repository=repository, base_branch=base_branch, head_branch=head_branch
561
+ ),
562
+ COMPARE_FILE_NAMES_QUERY,
563
+ )
564
+ if all_changed_paths is None:
565
+ return None
566
+ all_visible_paths = _visible_paths(all_changed_paths)
567
+ if all_visible_paths or len(all_changed_paths) < COMPARE_FILE_LIST_LIMIT:
568
+ return all_visible_paths
569
+ return None
570
+
571
+
572
+ def remote_visible_changed_files(
573
+ repository: str | None, base_branch: str | None, head_branch: str | None
574
+ ) -> list[str] | None:
575
+ """Return the changed files people see, read from the GitHub compare API, or None when it cannot tell.
576
+
577
+ ::
578
+
579
+ compare lists tool.py -> []
580
+ compare lists web/page.html -> ["web/page.html"]
581
+ compare returns 300 paths and none are visible -> None
582
+ repository or head missing -> None
583
+
584
+ The base is the named base branch, else the repository's default branch.
585
+ A page of 300 paths is the compare API's file cap. A visible file on that
586
+ page is returned. A full page with none is None, because a later file can
587
+ still be one people see.
588
+
589
+ Args:
590
+ repository: The owner/name the create call names, or None.
591
+ base_branch: The base branch the create call names, or None.
592
+ head_branch: The head branch the create call names, or None.
593
+ """
594
+ if not repository or not head_branch:
595
+ return None
596
+ resolved_base_branch = _compare_base_branch(repository, base_branch)
597
+ if resolved_base_branch is None:
598
+ return None
599
+ return _visible_compare_paths(repository, resolved_base_branch, head_branch)
600
+
601
+
491
602
  def missing_look_reason(all_payload_fields: Mapping[str, object]) -> str | None:
492
603
  """Return a deny reason when a pull request changing visible files shows no picture, else None.
493
604
 
@@ -496,7 +607,9 @@ def missing_look_reason(all_payload_fields: Mapping[str, object]) -> str | None:
496
607
  branch changes page.html, proof has ![after](https://x/after.png) -> None
497
608
  branch changes page.html, proof quotes only command output -> MISSING_LOOK_REASON
498
609
  branch changes only .py files -> None
499
- git cannot read the branch, proof shows no picture -> UNREADABLE_CHANGES_REASON
610
+ git cannot read the branch, compare API lists only .py files -> None
611
+ git cannot read the branch, compare API returns 300 .py files -> UNREADABLE_CHANGES_REASON
612
+ neither git nor the compare API can read it, no picture -> UNREADABLE_CHANGES_REASON
500
613
 
501
614
  Args:
502
615
  all_payload_fields: The parsed PreToolUse input.
@@ -505,9 +618,12 @@ def missing_look_reason(all_payload_fields: Mapping[str, object]) -> str | None:
505
618
  if not all_bodies:
506
619
  return None
507
620
  working_directory = all_payload_fields.get("cwd")
508
- all_visible_files = visible_changed_files(
509
- str(working_directory or ""), _named_head_branch(all_payload_fields)
510
- )
621
+ head_branch = _named_head_branch(all_payload_fields)
622
+ all_visible_files = visible_changed_files(str(working_directory or ""), head_branch)
623
+ if all_visible_files is None:
624
+ all_visible_files = remote_visible_changed_files(
625
+ _named_repository(all_payload_fields), _named_base_branch(all_payload_fields), head_branch
626
+ )
511
627
  if all_visible_files == []:
512
628
  return None
513
629
  for each_body in all_bodies: