claude-dev-env 8.25.0 → 8.26.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 (31) hide show
  1. package/docs/CODE_RULES.md +2 -2
  2. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -82
  3. package/hooks/blocking/code_rules_comments.py +14 -23
  4. package/hooks/blocking/code_rules_enforcer.py +0 -6
  5. package/hooks/blocking/code_rules_imports_logging.py +1 -120
  6. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +9 -12
  7. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +0 -10
  8. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -16
  9. package/hooks/hooks.json +10 -0
  10. package/hooks/hooks_constants/blocking_check_limits.py +0 -13
  11. package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -8
  12. package/hooks/hooks_constants/local_identity.py +7 -120
  13. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +0 -6
  14. package/hooks/hooks_constants/test_local_identity.py +7 -43
  15. package/hooks/hooks_constants/thread_spawn_pace_hook_constants.py +45 -0
  16. package/hooks/hooks_constants/usage_pace_constants.py +49 -0
  17. package/hooks/routing/conftest.py +10 -0
  18. package/hooks/routing/test_thread_spawn_pace_hook.py +139 -0
  19. package/hooks/routing/test_usage_pace.py +142 -0
  20. package/hooks/routing/thread_spawn_pace_hook.py +220 -0
  21. package/hooks/routing/usage_pace.py +345 -0
  22. package/package.json +1 -1
  23. package/rules/shell-invocation.md +1 -1
  24. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +0 -4
  25. package/scripts/repository_checks/config/constants.py +0 -5
  26. package/scripts/test_resolve_worker_spawn.py +0 -28
  27. package/docs/nas-ssh-invocation.md +0 -107
  28. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +0 -266
  29. package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +0 -87
  30. package/hooks/blocking/test_code_rules_logging_printf_tokens.py +0 -134
  31. package/rules/nas-ssh-invocation.md +0 -29
@@ -13,7 +13,7 @@ Do not add code comments. Preserve existing comments. Docstrings remain allowed.
13
13
  When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
14
14
  Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
15
15
 
16
- A keep marker is the one comment that may be added and kept: a comment that opens with `STEALTH: Keep`, or with a prefix the repository lists under `comment_keep_markers` in `.claude/policy-lint.json`.
16
+ A keep marker is the one comment that may be added and kept: a comment that opens with a prefix the repository lists under `comment_keep_markers` in `.claude/policy-lint.json`.
17
17
 
18
18
  ---
19
19
 
@@ -31,7 +31,7 @@ A keep marker is the one comment that may be added and kept: a comment that open
31
31
 
32
32
  The staged policy lint reports each of these through `code_rules_enforcer.py` and names the specific breach; exact patterns and exemption lists live in that module:
33
33
 
34
- no new comments · imports at top · logging format args (`log_*("...", arg)`) · no `%s`/`%d` printf tokens in a `str.format`-logger message (`log_*` imported from `automation_logging`; `str.format` drops the args — use `{}`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`; `/migrations/`; Workflow registries: path contains any of these substrings — `/workflow/`, `_tab.py`, `/states.py`, or `/modules.py`, each matching independently as a substring, so `pkg/states.py` qualifies while a top-level `states.py` follows the standard `config/` rule; test files — path or filename matches `test_`, `_test.`, `.spec.`, `conftest`, or `/tests/`) · no hardcoded user home paths · guarded `sys.path.insert` · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`, a parameter typed bare `object` whose body reads `param.attribute`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost) · JavaScript/TypeScript boolean declarations (`const`/`let`/`var` bound to a boolean literal or negation) and `@param {boolean}` JSDoc names prefixed `is`/`has`/`should`/`can`/`was`/`did` (camelCase forms) · banned identifiers as `.mjs`/`.js` declaration names (`result`, `data`, `ctx`, `msg`, …), scoped to changed lines · in test files, banned identifiers fire on changed lines, and pytest-collectable `test_*` functions need a return annotation · unused module-level imports and unsorted import blocks are ruff's job (F401, isort I001), not this hook's · a `hooks/blocking/` command classifier anchors its multi-word command regex to the command start (`^`/`\A`) or tokenizes the first word (`shlex.split`), never matching a command as a bare substring
34
+ no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`; `/migrations/`; Workflow registries: path contains any of these substrings — `/workflow/`, `_tab.py`, `/states.py`, or `/modules.py`, each matching independently as a substring, so `pkg/states.py` qualifies while a top-level `states.py` follows the standard `config/` rule; test files — path or filename matches `test_`, `_test.`, `.spec.`, `conftest`, or `/tests/`) · no hardcoded user home paths · guarded `sys.path.insert` · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`, a parameter typed bare `object` whose body reads `param.attribute`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost) · JavaScript/TypeScript boolean declarations (`const`/`let`/`var` bound to a boolean literal or negation) and `@param {boolean}` JSDoc names prefixed `is`/`has`/`should`/`can`/`was`/`did` (camelCase forms) · banned identifiers as `.mjs`/`.js` declaration names (`result`, `data`, `ctx`, `msg`, …), scoped to changed lines · in test files, banned identifiers fire on changed lines, and pytest-collectable `test_*` functions need a return annotation · unused module-level imports and unsorted import blocks belong to ruff (F401, isort I001) · a `hooks/blocking/` command classifier anchors its multi-word command regex to the command start (`^`/`\A`) or tokenizes the first word (`shlex.split`), never matching a command as a bare substring
35
35
 
36
36
  Test files follow the comment policy above; other test-specific exemptions are listed here. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks.
37
37
 
@@ -1,4 +1,4 @@
1
- """Boolean naming-prefix and ignored must-check-return checks."""
1
+ """Boolean naming-prefix checks."""
2
2
 
3
3
  import ast
4
4
  import sys
@@ -21,12 +21,8 @@ from code_rules_shared import ( # noqa: E402
21
21
  is_workflow_registry_file,
22
22
  )
23
23
 
24
- from hooks_constants.blocking_check_limits import ( # noqa: E402
25
- MAX_IGNORED_MUST_CHECK_RETURN_ISSUES,
26
- )
27
24
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
28
25
  ALL_BOOLEAN_NAME_PREFIXES,
29
- ALL_MUST_CHECK_RETURN_FUNCTION_NAMES,
30
26
  ALL_SELF_AND_CLS_PARAMETER_NAMES,
31
27
  UPPER_SNAKE_CONSTANT_PATTERN,
32
28
  )
@@ -271,80 +267,3 @@ def _called_terminal_name(call_node: ast.Call) -> str | None:
271
267
  if isinstance(callee, ast.Attribute):
272
268
  return callee.attr
273
269
  return None
274
-
275
-
276
- def check_ignored_must_check_return(
277
- content: str,
278
- file_path: str,
279
- all_changed_lines: set[int] | None = None,
280
- defer_scope_to_caller: bool = False,
281
- ) -> list[str]:
282
- """Flag bare-expression calls whose discarded return is the only failure signal.
283
-
284
- Functions in ``ALL_MUST_CHECK_RETURN_FUNCTION_NAMES`` report success or failure
285
- solely through their return value. A bare-statement call discards that value,
286
- so the caller silently proceeds on failure. Bare ``ast.Expr`` calls are flagged,
287
- including a bare ``await``-wrapped call (``await find_and_click(...)`` as a
288
- statement); an assigned or branched-on call is exempt.
289
-
290
- The caller passes the reconstructed full file as *content* so ``ast.parse``
291
- sees a complete module rather than an Edit's ``new_string`` fragment, which is
292
- rarely valid standalone Python (a bare ``await find_and_click(...)`` line is a
293
- SyntaxError on its own). Findings are then scoped to *all_changed_lines* so an
294
- Edit blocks on the discarded return it just introduced while a pre-existing
295
- violation on an untouched line does not block the edit.
296
-
297
- Args:
298
- content: The source text to inspect — the reconstructed full file on an
299
- Edit so the parse succeeds.
300
- file_path: The path the source will be written to, used for exemptions.
301
- all_changed_lines: Post-edit line numbers the current edit touched, or
302
- None to treat the whole file as in scope. When provided, a violation
303
- blocks only when the bare call's line intersects the changed lines.
304
- defer_scope_to_caller: When True, return every violation so the
305
- commit/push gate's ``split_violations_by_scope`` can scope by added
306
- line.
307
-
308
- Returns:
309
- One issue per discarded must-check return, scoped to the changed lines
310
- unless *defer_scope_to_caller* is True or *all_changed_lines* is None. When
311
- *defer_scope_to_caller* is True every violation is returned uncapped so the
312
- gate can scope by added line and apply its own ceiling; otherwise the
313
- terminal result is capped at the module limit.
314
- """
315
- if is_test_file(file_path):
316
- return []
317
- try:
318
- tree = ast.parse(content)
319
- except SyntaxError:
320
- return []
321
- all_violations_in_walk_order: list[tuple[range, str]] = []
322
- for each_node in ast.walk(tree):
323
- if not isinstance(each_node, ast.Expr):
324
- continue
325
- expression_value = each_node.value
326
- call_node = (
327
- expression_value.value
328
- if isinstance(expression_value, ast.Await)
329
- else expression_value
330
- )
331
- if not isinstance(call_node, ast.Call):
332
- continue
333
- called_name = _called_terminal_name(call_node)
334
- if called_name is None or called_name not in ALL_MUST_CHECK_RETURN_FUNCTION_NAMES:
335
- continue
336
- end_line_number = each_node.end_lineno or each_node.lineno
337
- line_span = range(each_node.lineno, end_line_number + 1)
338
- message = (
339
- f"Line {each_node.lineno}: return value of {called_name}() is discarded - "
340
- "assign and check it (the boolean/outcome is the only failure signal)"
341
- )
342
- all_violations_in_walk_order.append((line_span, message))
343
- scoped_issues = _scope_violations_to_changed_lines(
344
- all_violations_in_walk_order,
345
- all_changed_lines,
346
- defer_scope_to_caller,
347
- )
348
- if defer_scope_to_caller:
349
- return scoped_issues
350
- return scoped_issues[:MAX_IGNORED_MUST_CHECK_RETURN_ISSUES]
@@ -36,7 +36,6 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
36
36
  REPOSITORY_LINT_SETTINGS_RELATIVE_PATH,
37
37
  REPOSITORY_ROOT_MARKER_NAME,
38
38
  SETTINGS_TEXT_ENCODING,
39
- STEALTH_KEEP_COMMENT_MARKER,
40
39
  )
41
40
  _javascript_comment_scanner = importlib.import_module("javascript_comment_scanner")
42
41
  extract_javascript_comment_occurrences = (
@@ -180,26 +179,22 @@ def comment_keep_markers(file_path: str) -> tuple[str, ...]:
180
179
  ::
181
180
 
182
181
  repo/.claude/policy-lint.json {"comment_keep_markers": ["VENDOR: Keep"]}
183
- repo/app/main.py ("STEALTH: Keep", "VENDOR: Keep")
184
- no settings file ("STEALTH: Keep",)
182
+ repo/app/main.py ("VENDOR: Keep",)
183
+ no settings file ()
185
184
 
186
185
  The repository that holds the file names its own markers. A missing or
187
- unreadable settings file adds none.
186
+ unreadable settings file names none.
188
187
 
189
188
  Args:
190
189
  file_path: The path of the file under check.
191
190
 
192
191
  Returns:
193
- The package's built-in marker, then the markers the file's repository
194
- adds.
192
+ The markers the file's repository names.
195
193
  """
196
194
  for each_directory in Path(file_path).resolve().parents:
197
195
  if (each_directory / REPOSITORY_ROOT_MARKER_NAME).exists():
198
- return (
199
- STEALTH_KEEP_COMMENT_MARKER,
200
- *_repository_keep_markers(each_directory),
201
- )
202
- return (STEALTH_KEEP_COMMENT_MARKER,)
196
+ return _repository_keep_markers(each_directory)
197
+ return ()
203
198
 
204
199
 
205
200
  def _is_keep_occurrence(comment_text: str, all_keep_markers: tuple[str, ...]) -> bool:
@@ -230,10 +225,9 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
230
225
  """Check for comment additions or removals between old and new content.
231
226
 
232
227
  Inline and standalone comments are blocking findings when added, except
233
- keep-marker occurrences (``STEALTH: Keep`` and any marker the file's
234
- repository names in ``.claude/policy-lint.json``), which are excluded
235
- entirely so the marker can be added and can persist across edits to its
236
- line.
228
+ keep-marker occurrences (a marker the file's repository names in
229
+ ``.claude/policy-lint.json``), which are excluded entirely so the marker
230
+ can be added and can persist across edits to its line.
237
231
  Existing comments can be removed when the touched code no longer needs them.
238
232
 
239
233
  When the file is Python and either *old_content* or *new_content* cannot
@@ -431,14 +425,11 @@ def _is_exempt_python_comment(comment_token: tokenize.TokenInfo) -> bool:
431
425
  token-anchored directive body never legitimately carries a ``#``
432
426
  (noqa codes, pylint symbols, and pragma directives contain none), so
433
427
  any inner ``#`` reliably marks chained prose. Free-form markers
434
- (``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``, ``STEALTH: Keep``)
435
- accept any trailing prose:
436
- ``# type:`` participates in the documented justification
437
- convention enforced by ``check_type_escape_hatches`` (which
438
- requires a trailing reason), the TODO-family markers carry
439
- annotation text by convention, and ``STEALTH: Keep`` is the
440
- acknowledged-bypass marker the CDP runtime-usage scanner reads
441
- from the flagged line together with its reason.
428
+ (``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``) accept any
429
+ trailing prose: ``# type:`` participates in the documented
430
+ justification convention enforced by ``check_type_escape_hatches``
431
+ (which requires a trailing reason), and the TODO-family markers carry
432
+ annotation text by convention.
442
433
  """
443
434
  comment_string = comment_token.string
444
435
  if comment_string.startswith("#!") and comment_token.start == (1, 0):
@@ -46,7 +46,6 @@ from code_rules_blast_radius import ( # noqa: E402
46
46
  )
47
47
  from code_rules_boolean_mustcheck import ( # noqa: E402
48
48
  check_boolean_naming,
49
- check_ignored_must_check_return,
50
49
  )
51
50
  from code_rules_command_dispatch import ( # noqa: E402
52
51
  check_unanchored_command_dispatch,
@@ -86,7 +85,6 @@ from code_rules_imports_logging import ( # noqa: E402
86
85
  check_library_print,
87
86
  check_logging_adjacent_string_literals,
88
87
  check_logging_fstrings,
89
- check_logging_printf_tokens,
90
88
  check_naive_datetime_construction,
91
89
  check_windows_api_none,
92
90
  )
@@ -329,7 +327,6 @@ def _python_comment_and_logging_issues(context: _ValidationContext) -> list[str]
329
327
  all_issues.extend(check_comment_changes(old_content, content, file_path))
330
328
  all_issues.extend(check_imports_at_top(content))
331
329
  all_issues.extend(check_logging_fstrings(content))
332
- all_issues.extend(check_logging_printf_tokens(content, file_path))
333
330
  all_issues.extend(check_logging_adjacent_string_literals(content, file_path))
334
331
  all_issues.extend(check_windows_api_none(content))
335
332
  all_issues.extend(check_naive_datetime_construction(content, file_path))
@@ -450,9 +447,6 @@ def _python_boolean_and_test_assertion_issues(context: _ValidationContext) -> li
450
447
  )
451
448
  changed, defer = context.all_changed_lines, context.defer_scope_to_caller
452
449
  all_issues = check_boolean_naming(effective_content, file_path, changed, defer)
453
- all_issues.extend(
454
- check_ignored_must_check_return(effective_content, file_path, changed, defer)
455
- )
456
450
  all_issues.extend(check_skip_decorators_in_tests(content, file_path))
457
451
  all_issues.extend(
458
452
  check_tests_use_isolated_filesystem_paths(
@@ -1,4 +1,4 @@
1
- """Import and logging convention checks: check_imports_at_top, check_logging_fstrings, check_logging_printf_tokens, check_logging_adjacent_string_literals, check_windows_api_none, check_naive_datetime_construction, check_e2e_test_naming, check_js_resume_task_enumeration_coverage, check_js_returns_object_schemaless_branch, check_js_sibling_return_object_key_drift, check_js_bare_flag_return_directive, and check_library_print."""
1
+ """Import and logging convention checks: check_imports_at_top, check_logging_fstrings, check_logging_adjacent_string_literals, check_windows_api_none, check_naive_datetime_construction, check_e2e_test_naming, check_js_resume_task_enumeration_coverage, check_js_returns_object_schemaless_branch, check_js_sibling_return_object_key_drift, check_js_bare_flag_return_directive, and check_library_print."""
2
2
 
3
3
  import ast
4
4
  import re
@@ -25,7 +25,6 @@ from code_rules_shared import ( # noqa: E402
25
25
 
26
26
  from hooks_constants.blocking_check_limits import ( # noqa: E402
27
27
  ALL_ALWAYS_NAIVE_DATETIME_CONSTRUCTORS,
28
- ALL_FORMAT_LOGGER_FUNCTION_NAMES,
29
28
  DATETIME_CLASS_ATTRIBUTE_NAME,
30
29
  FROMTIMESTAMP_POSITIONAL_TIMEZONE_ARGUMENT_COUNT,
31
30
  MAX_E2E_TEST_NAMING_ISSUES,
@@ -35,7 +34,6 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
35
34
  MAX_JS_SIBLING_RETURN_OBJECT_KEY_DRIFT_ISSUES,
36
35
  MAX_LOGGING_ADJACENT_LITERAL_ISSUES,
37
36
  MAX_LOGGING_FSTRING_ISSUES,
38
- MAX_LOGGING_PRINTF_TOKEN_ISSUES,
39
37
  MAX_NAIVE_DATETIME_ISSUES,
40
38
  MAX_WINDOWS_API_NONE_ISSUES,
41
39
  MINIMUM_RESUME_TASK_ENUMERATION_ITEMS,
@@ -87,9 +85,7 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
87
85
  JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
88
86
  LOGGING_FSTRING_PATTERN,
89
87
  LOGGING_HELPER_FUNCTION_NAME_PATTERN,
90
- LOGGING_PRINTF_TOKEN_PATTERN,
91
88
  LOGGING_RECEIVER_NAME_PATTERN,
92
- MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT,
93
89
  NOT_INSIDE_TYPE_CHECKING_BLOCK,
94
90
  RESUME_TASK_ENUMERATION_PATTERN,
95
91
  RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
@@ -280,121 +276,6 @@ def check_logging_fstrings(content: str) -> list[str]:
280
276
  return issues
281
277
 
282
278
 
283
- def _format_logger_names_imported(tree: ast.Module) -> set[str]:
284
- """Return the local names bound to automation_logging log helpers.
285
-
286
- Scans every ``from ... import ...`` statement for an import whose module
287
- path contains ``automation_logging`` and collects the local binding of each
288
- imported ``log_*`` helper (the alias when ``as`` is present, otherwise the
289
- imported name). Only these names identify a str.format-logger call; a
290
- ``log_*`` helper from any other module is not collected, because a
291
- ``%``-style logger formats its tokens correctly.
292
-
293
- Args:
294
- tree: The parsed module to scan for logger imports.
295
-
296
- Returns:
297
- The set of local names bound to automation_logging log helpers.
298
- """
299
- bound_names: set[str] = set()
300
- for each_node in ast.walk(tree):
301
- if not isinstance(each_node, ast.ImportFrom):
302
- continue
303
- if each_node.module is None or "automation_logging" not in each_node.module:
304
- continue
305
- for each_alias in each_node.names:
306
- if each_alias.name in ALL_FORMAT_LOGGER_FUNCTION_NAMES:
307
- bound_names.add(each_alias.asname or each_alias.name)
308
- return bound_names
309
-
310
-
311
- def _printf_token_log_call_line(
312
- node: ast.AST, all_format_logger_names: set[str]
313
- ) -> int | None:
314
- """Return the line of a format-logger call carrying a printf token, else None.
315
-
316
- Args:
317
- node: The AST node to inspect.
318
- all_format_logger_names: Local names bound to automation_logging log
319
- helpers.
320
-
321
- Returns:
322
- The 1-based line number when ``node`` is a bare-name call to a format
323
- logger that has at least one format argument after the message and
324
- whose first string-literal argument carries a printf token; otherwise
325
- None. A call with only the message and no format arguments never runs
326
- ``.format(*args)``, so its token prints intact and is not flagged.
327
- """
328
- if not isinstance(node, ast.Call):
329
- return None
330
- function_reference = node.func
331
- if (
332
- not isinstance(function_reference, ast.Name)
333
- or function_reference.id not in all_format_logger_names
334
- ):
335
- return None
336
- if len(node.args) < MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT:
337
- return None
338
- message_argument = node.args[0]
339
- if not isinstance(message_argument, ast.Constant) or not isinstance(
340
- message_argument.value, str
341
- ):
342
- return None
343
- if not LOGGING_PRINTF_TOKEN_PATTERN.search(message_argument.value):
344
- return None
345
- return node.lineno
346
-
347
-
348
- def check_logging_printf_tokens(content: str, file_path: str) -> list[str]:
349
- """Flag printf tokens in a str.format-logger (automation_logging) message.
350
-
351
- The ``shared_utils.automation_logging`` helpers (``log_info``, ``log_error``,
352
- ...) format with ``str.format`` (``{}`` placeholders) only when format
353
- arguments follow the message (``message.format(*args) if args else
354
- message``), so a printf-style token such as ``%s`` in the message literal is
355
- never substituted: the format arguments are dropped and the literal token
356
- prints. The check fires only in a file that imports one of those helpers
357
- from an ``automation_logging`` module, and only for a bare-name call to such
358
- a helper that passes at least one format argument after the message and
359
- whose first argument is a string literal carrying a token. A call with only
360
- the message and no format arguments never runs ``.format``, so its token
361
- prints intact and is left alone. An attribute call (``logger.info``) or a
362
- ``log_*`` helper from any other module formats ``%``-tokens correctly and is
363
- left alone. Test files are exempt so a test may exercise the malformed shape.
364
-
365
- Args:
366
- content: The Python source under validation.
367
- file_path: The destination path, used to skip test files and non-Python.
368
-
369
- Returns:
370
- One issue line per offending call, capped at the configured maximum.
371
- """
372
- if is_test_file(file_path):
373
- return []
374
- if get_file_extension(file_path) not in ALL_PYTHON_EXTENSIONS:
375
- return []
376
- try:
377
- tree = ast.parse(content)
378
- except SyntaxError:
379
- return []
380
- all_format_logger_names = _format_logger_names_imported(tree)
381
- if not all_format_logger_names:
382
- return []
383
- issues: list[str] = []
384
- for each_node in ast.walk(tree):
385
- offending_line = _printf_token_log_call_line(each_node, all_format_logger_names)
386
- if offending_line is None:
387
- continue
388
- issues.append(
389
- f"Line {offending_line}: printf token in a str.format logger "
390
- "message - the automation_logging helpers format with str.format; "
391
- "use {} placeholders (the %-arguments are silently dropped)"
392
- )
393
- if len(issues) >= MAX_LOGGING_PRINTF_TOKEN_ISSUES:
394
- break
395
- return issues
396
-
397
-
398
279
  def _logging_call_receiver_name(receiver_node: ast.expr) -> str | None:
399
280
  """Return the trailing identifier of a logging-call receiver expression, else None.
400
281
 
@@ -134,18 +134,15 @@ def test_check_comment_changes_allows_comment_removal_without_advisory(
134
134
  assert captured.err == ""
135
135
 
136
136
 
137
- def test_check_comment_changes_does_not_flag_added_stealth_keep_marker() -> None:
138
- old_content = "x = 1\n"
139
- new_content = "x = 1 # STEALTH: Keep -- reason\n"
140
- issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
141
- assert issues == []
142
-
143
-
144
- def test_check_comment_changes_does_not_flag_stealth_keep_retained_on_touched_line() -> None:
145
- old_content = "x = 1 # STEALTH: Keep -- reason\n"
146
- new_content = "x = 2 # STEALTH: Keep -- reason\n"
147
- issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
148
- assert issues == []
137
+ def test_check_comment_changes_flags_stealth_keep_in_a_repository_that_names_no_marker(
138
+ tmp_path: Path,
139
+ ) -> None:
140
+ file_path = _repository_file(tmp_path, None)
141
+ issues = code_rules_enforcer.check_comment_changes(
142
+ "x = 1\n", "x = 1 # STEALTH: Keep -- reason\n", file_path
143
+ )
144
+ assert len(issues) == 1
145
+ assert "comment added" in issues[0]
149
146
 
150
147
 
151
148
  @pytest.mark.parametrize(
@@ -156,16 +156,6 @@ def test_should_exempt_fixme_with_trailing_prose() -> None:
156
156
  assert _is_exempt_python_comment(token) is True
157
157
 
158
158
 
159
- def test_should_exempt_stealth_keep_with_trailing_reason() -> None:
160
- token = _build_comment_token("# STEALTH: Keep bypass reviewed in incident 4021")
161
- assert _is_exempt_python_comment(token) is True
162
-
163
-
164
- def test_should_flag_stealth_marker_missing_required_space_after_colon() -> None:
165
- token = _build_comment_token("# STEALTH:Keep bypass reviewed in incident 4021")
166
- assert _is_exempt_python_comment(token) is False
167
-
168
-
169
159
  def test_should_exempt_shebang_at_line_one_column_zero() -> None:
170
160
  token = tokenize.TokenInfo(
171
161
  type=tokenize.COMMENT,
@@ -49,7 +49,6 @@ ALL_SCOPE_AWARE_RULE_NAMES = frozenset(
49
49
  "check_docstring_runon_sentence",
50
50
  "check_docstring_prose_wall_without_illustration",
51
51
  "check_boolean_naming",
52
- "check_ignored_must_check_return",
53
52
  "check_tests_use_isolated_filesystem_paths",
54
53
  "check_return_annotations",
55
54
  "check_function_length",
@@ -70,7 +69,6 @@ NARROW_EDIT_ACCEPTED_RULE_NAMES = frozenset(
70
69
  "check_join_separator_string_magic",
71
70
  "check_banned_noun_word_boundary",
72
71
  "check_boolean_naming",
73
- "check_ignored_must_check_return",
74
72
  }
75
73
  )
76
74
 
@@ -80,7 +78,6 @@ EXPECTED_RULE_TEXT_BY_NAME = {
80
78
  "check_join_separator_string_magic": "string separator",
81
79
  "check_banned_noun_word_boundary": "Identifier",
82
80
  "check_boolean_naming": "Boolean",
83
- "check_ignored_must_check_return": "return value",
84
81
  }
85
82
 
86
83
 
@@ -139,19 +136,6 @@ ALL_REQUIRED_RULE_FIXTURES = (
139
136
  expected_marker="ready",
140
137
  rule_name="check_boolean_naming",
141
138
  ),
142
- NarrowEditFixture(
143
- old_fragment=(
144
- "def submit_form() -> None:\n"
145
- " if find_and_click('#submit'):\n"
146
- " return\n"
147
- ),
148
- new_fragment=(
149
- "def submit_form() -> None:\n"
150
- " find_and_click('#submit')\n"
151
- ),
152
- expected_marker="find_and_click",
153
- rule_name="check_ignored_must_check_return",
154
- ),
155
139
  )
156
140
 
157
141
 
package/hooks/hooks.json CHANGED
@@ -72,6 +72,16 @@
72
72
  }
73
73
  ]
74
74
  },
75
+ {
76
+ "matcher": "mcp__hearthbot__start_thread_session",
77
+ "hooks": [
78
+ {
79
+ "type": "command",
80
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/thread_spawn_pace_hook.py",
81
+ "timeout": 30
82
+ }
83
+ ]
84
+ },
75
85
  {
76
86
  "matcher": "*",
77
87
  "hooks": [
@@ -22,12 +22,10 @@ MAX_DOCSTRING_FORMAT_ISSUES: int = 5
22
22
  MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES: int = 5
23
23
  MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES: int = 5
24
24
  MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH: int = 2
25
- MAX_IGNORED_MUST_CHECK_RETURN_ISSUES: int = 5
26
25
  MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
27
26
  MAX_THIN_WRAPPER_ISSUES: int = 1
28
27
  MAX_ZERO_PAYLOAD_ALIAS_ISSUES: int = 3
29
28
  MAX_LOGGING_FSTRING_ISSUES: int = 3
30
- MAX_LOGGING_PRINTF_TOKEN_ISSUES: int = 3
31
29
  MAX_LOGGING_ADJACENT_LITERAL_ISSUES: int = 3
32
30
  MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES: int = 3
33
31
  MAX_WINDOWS_API_NONE_ISSUES: int = 3
@@ -204,17 +202,6 @@ DOCSTRING_REFERENCE_MARKER_WINDOW: int = 2
204
202
  ALL_GENERIC_CHECK_NAME_TOKENS: frozenset[str] = frozenset(
205
203
  {"check", "checks", "test", "tests", "in", "for", "and", "the"}
206
204
  )
207
- ALL_FORMAT_LOGGER_FUNCTION_NAMES: frozenset[str] = frozenset(
208
- {
209
- "log_debug",
210
- "log_info",
211
- "log_ok",
212
- "log_error",
213
- "log_warning",
214
- "log_batch",
215
- "log_background",
216
- }
217
- )
218
205
  DOCSTRING_RUNON_SENTENCE_WORD_LIMIT: int = 30
219
206
  MAX_DOCSTRING_RUNON_SENTENCE_ISSUES: int = 5
220
207
  ALL_DOCSTRING_RUNON_JOINER_MARKERS: tuple[str, ...] = ("—", " -- ", ";")
@@ -72,8 +72,6 @@ ALL_POLARITY_ANTONYM_TOKEN_PAIRS: tuple[tuple[str, str], ...] = (
72
72
  )
73
73
  POLARITY_TOKEN_BOUNDARY_PATTERN: str = r"(?:^|_)%s(?:_|$)"
74
74
 
75
- ALL_MUST_CHECK_RETURN_FUNCTION_NAMES: frozenset[str] = frozenset({"find_and_click", "write_outcome"})
76
-
77
75
  DOCSTRING_ARG_ENTRY_PATTERN: re.Pattern[str] = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*[:(]")
78
76
  DOCSTRING_PLURAL_FAMILY_STOP_PATTERN: re.Pattern[str] = re.compile(
79
77
  r"\bthe\s+([a-z][a-z]+)\s+stops\b"
@@ -135,7 +133,6 @@ ALL_TOKEN_ANCHORED_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
135
133
  "pragma:",
136
134
  )
137
135
  ALL_TOKEN_ANCHORED_DIRECTIVE_BOUNDARY_CHARACTERS: frozenset[str] = frozenset({":"})
138
- STEALTH_KEEP_COMMENT_MARKER: str = "STEALTH: Keep"
139
136
  REPOSITORY_LINT_SETTINGS_RELATIVE_PATH: str = ".claude/policy-lint.json"
140
137
  COMMENT_KEEP_MARKERS_KEY: str = "comment_keep_markers"
141
138
  REPOSITORY_ROOT_MARKER_NAME: str = ".git"
@@ -146,7 +143,6 @@ ALL_FREE_FORM_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
146
143
  "FIXME",
147
144
  "HACK",
148
145
  "XXX",
149
- STEALTH_KEEP_COMMENT_MARKER,
150
146
  )
151
147
  CHAINED_INLINE_COMMENT_PATTERN = re.compile(r"#")
152
148
  ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES: tuple[str, ...] = (
@@ -211,14 +207,10 @@ LOGGING_FSTRING_PATTERN = re.compile(
211
207
  r'|(?:logger|logging|log)\.(?:debug|info|warning|error|critical|exception))'
212
208
  r'\s*\(\s*(?:[rR][fF]|[fF][rR]?)["\']'
213
209
  )
214
- LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
215
- r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
216
- )
217
210
  ADJACENT_STRING_LITERAL_PATTERN: re.Pattern[str] = re.compile(
218
211
  r'(?<!["\'])[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')'
219
212
  r'\s*[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')(?!["\'])'
220
213
  )
221
- MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
222
214
  ALL_LOGGING_CALL_METHOD_NAMES: frozenset[str] = frozenset(
223
215
  {"debug", "info", "warning", "error", "critical", "exception"}
224
216
  )