claude-dev-env 1.51.0 → 1.52.1
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.
- package/hooks/blocking/code_rules_enforcer.py +367 -42
- package/hooks/blocking/tdd_enforcer.py +211 -19
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +519 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +1 -1
- package/hooks/blocking/test_tdd_enforcer.py +399 -0
- package/hooks/hooks.json +0 -15
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +5 -0
- package/package.json +1 -1
- package/skills/pr-converge/SKILL.md +34 -2
- package/skills/pr-converge/reference/fix-protocol.md +9 -0
- package/skills/pr-converge/reference/ground-rules.md +7 -0
- package/skills/pr-converge/reference/per-tick.md +93 -6
- package/skills/update/SKILL.md +143 -0
|
@@ -17,7 +17,9 @@ concern focused. The separate ``tdd_enforcer.py`` hook accepts any
|
|
|
17
17
|
"""
|
|
18
18
|
import json
|
|
19
19
|
import sys
|
|
20
|
+
from collections import Counter
|
|
20
21
|
from pathlib import Path
|
|
22
|
+
from typing import TextIO
|
|
21
23
|
|
|
22
24
|
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
23
25
|
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
@@ -120,6 +122,11 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
120
122
|
ALL_CODE_EXTENSIONS,
|
|
121
123
|
ALL_JAVASCRIPT_EXTENSIONS,
|
|
122
124
|
ALL_PYTHON_EXTENSIONS,
|
|
125
|
+
PRECHECK_USAGE_EXIT_CODE,
|
|
126
|
+
PRECHECK_USAGE_MESSAGE,
|
|
127
|
+
)
|
|
128
|
+
from hooks_constants.setup_project_paths_constants import ( # noqa: E402
|
|
129
|
+
UTF8_BYTE_ORDER_MARK,
|
|
123
130
|
)
|
|
124
131
|
|
|
125
132
|
|
|
@@ -310,72 +317,390 @@ def prior_and_post_edit_content(
|
|
|
310
317
|
return existing_content, existing_content.replace(old_string, new_string, 1)
|
|
311
318
|
|
|
312
319
|
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
input_data = json.load(sys.stdin)
|
|
316
|
-
except json.JSONDecodeError:
|
|
317
|
-
sys.exit(0)
|
|
320
|
+
def _is_validated_target(file_path: str) -> bool:
|
|
321
|
+
"""Return whether the path is subject to code-rules validation.
|
|
318
322
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
file_path = tool_input.get("file_path", "")
|
|
323
|
+
Args:
|
|
324
|
+
file_path: The destination path of the write, edit, or pre-check target.
|
|
322
325
|
|
|
326
|
+
Returns:
|
|
327
|
+
True when the path is non-empty, outside hook infrastructure, and
|
|
328
|
+
carries a code extension; False for every exempt path.
|
|
329
|
+
"""
|
|
323
330
|
if not file_path:
|
|
324
|
-
|
|
325
|
-
|
|
331
|
+
return False
|
|
326
332
|
if is_hook_infrastructure(file_path):
|
|
327
|
-
|
|
333
|
+
return False
|
|
334
|
+
return get_file_extension(file_path) in ALL_CODE_EXTENSIONS
|
|
328
335
|
|
|
329
|
-
extension = get_file_extension(file_path)
|
|
330
|
-
if extension not in ALL_CODE_EXTENSIONS:
|
|
331
|
-
sys.exit(0)
|
|
332
336
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
def _without_line_prefix(violation_text: str) -> str:
|
|
338
|
+
"""Return the violation message body with its ``Line <n>: `` locator removed.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
violation_text: A violation message, optionally carrying a leading
|
|
342
|
+
``Line <n>: `` locator.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
The message body shared by fragment-scoped and full-file scans of the
|
|
346
|
+
same violation, regardless of which line numbering produced it.
|
|
347
|
+
"""
|
|
348
|
+
locator, separator, message_body = violation_text.partition(": ")
|
|
349
|
+
if separator and locator.startswith("Line ") and locator[len("Line "):].isdigit():
|
|
350
|
+
return message_body
|
|
351
|
+
return violation_text
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _forecast_full_file_violations(
|
|
355
|
+
full_file_content_after_edit: str,
|
|
356
|
+
file_path: str,
|
|
357
|
+
prior_full_file_content: str,
|
|
358
|
+
all_blocking_issues: list[str],
|
|
359
|
+
) -> list[str]:
|
|
360
|
+
"""Return full-file violations absent from the fragment-scoped blocking list.
|
|
361
|
+
|
|
362
|
+
Runs a complete, un-scoped scan of the whole post-edit file so fragment-scoped
|
|
363
|
+
checks see every line, then drops the violations already present in
|
|
364
|
+
``all_blocking_issues``. Matching is line-number-agnostic with
|
|
365
|
+
per-occurrence accounting: each blocking entry consumes exactly one
|
|
366
|
+
full-file entry carrying the same message body, so a violation the fragment
|
|
367
|
+
itself introduces stays out of the forecast even though the two scans
|
|
368
|
+
number its line differently, while a second same-message violation
|
|
369
|
+
elsewhere in the file still surfaces. The remainder are the violations that
|
|
370
|
+
survive elsewhere in the file and will block a future edit.
|
|
371
|
+
|
|
372
|
+
Body matching relies on an invariant of the check suite: every check that
|
|
373
|
+
embeds a secondary source position in its message body (function length,
|
|
374
|
+
banned-noun binding spans, test isolation) scans the whole post-edit file
|
|
375
|
+
in both passes, so those embedded positions are identical across scans;
|
|
376
|
+
checks that scan only the fragment carry their position solely in the
|
|
377
|
+
strippable ``Line <n>: `` locator. A fragment-scoped check that embeds a
|
|
378
|
+
position in its body would defeat the dedup and re-list its own violation.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
full_file_content_after_edit: The whole post-edit file content.
|
|
382
|
+
file_path: The destination path used for classification.
|
|
383
|
+
prior_full_file_content: The whole file content before the edit applied,
|
|
384
|
+
used so the comment diff reflects the real prior state.
|
|
385
|
+
all_blocking_issues: The fragment-scoped issues that already decide the
|
|
386
|
+
deny.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
The full-file violations not already in ``all_blocking_issues``.
|
|
390
|
+
"""
|
|
391
|
+
all_full_file_issues = validate_content(
|
|
392
|
+
full_file_content_after_edit, file_path, prior_full_file_content
|
|
393
|
+
)
|
|
394
|
+
remaining_blocking_counts = Counter(
|
|
395
|
+
_without_line_prefix(each_issue) for each_issue in all_blocking_issues
|
|
396
|
+
)
|
|
397
|
+
forecast_issues: list[str] = []
|
|
398
|
+
for each_issue in all_full_file_issues:
|
|
399
|
+
message_body = _without_line_prefix(each_issue)
|
|
400
|
+
if remaining_blocking_counts[message_body] > 0:
|
|
401
|
+
remaining_blocking_counts[message_body] -= 1
|
|
402
|
+
continue
|
|
403
|
+
forecast_issues.append(each_issue)
|
|
404
|
+
return forecast_issues
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _precheck_hint() -> str:
|
|
408
|
+
"""Return the discoverability hint pointing at the script's pre-check mode."""
|
|
409
|
+
script_path = str(Path(__file__).resolve())
|
|
410
|
+
return (
|
|
411
|
+
"; Pre-check a complete candidate before retrying: "
|
|
412
|
+
f'"{sys.executable}" "{script_path}" --check <candidate> --as <target>'
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _run_precheck(
|
|
417
|
+
candidate_path: str,
|
|
418
|
+
target_path: str,
|
|
419
|
+
violation_stream: TextIO,
|
|
420
|
+
error_stream: TextIO,
|
|
421
|
+
) -> int:
|
|
422
|
+
"""Validate a complete candidate file as if it lived at its destination.
|
|
423
|
+
|
|
424
|
+
Reads the candidate's full content and runs the complete verdict (no diff
|
|
425
|
+
scoping) using ``target_path`` for every path-based classification, so a
|
|
426
|
+
candidate staged in a temporary directory is judged as if written to its real
|
|
427
|
+
destination. Every leading byte-order mark on the candidate is stripped so
|
|
428
|
+
the verdict matches the one the decoded tool-payload content receives — a
|
|
429
|
+
BOM would otherwise fail AST parsing and silently skip every AST-based
|
|
430
|
+
check.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
candidate_path: The path of the candidate file to validate.
|
|
434
|
+
target_path: The destination path used for extension dispatch and every
|
|
435
|
+
exemption decision.
|
|
436
|
+
violation_stream: The stream each violation line is written to.
|
|
437
|
+
error_stream: The stream the unreadable-candidate error line is written
|
|
438
|
+
to.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Exit code 1 when any violation exists or the candidate cannot be read,
|
|
442
|
+
and 0 when the candidate is clean or the target is exempt.
|
|
443
|
+
"""
|
|
444
|
+
if not _is_validated_target(target_path):
|
|
445
|
+
return 0
|
|
446
|
+
candidate_content = _read_existing_file_content(candidate_path)
|
|
447
|
+
if candidate_content is None:
|
|
448
|
+
error_stream.write(f"error: cannot read candidate file: {candidate_path}\n")
|
|
449
|
+
return 1
|
|
450
|
+
candidate_content = candidate_content.lstrip(UTF8_BYTE_ORDER_MARK)
|
|
451
|
+
old_content = _read_existing_file_content(target_path) or ""
|
|
452
|
+
all_issues = validate_content(candidate_content, target_path, old_content)
|
|
453
|
+
for each_issue in all_issues:
|
|
454
|
+
violation_stream.write(f"{each_issue}\n")
|
|
455
|
+
return 1 if all_issues else 0
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _precheck_arguments(all_arguments: list[str]) -> tuple[str, str] | None:
|
|
459
|
+
"""Parse a strict pre-check argument vector into a candidate and target pair.
|
|
460
|
+
|
|
461
|
+
Accepts exactly the two documented shapes — ``--check <candidate>`` or
|
|
462
|
+
``--check <candidate> --as <target>`` — with the target defaulting to the
|
|
463
|
+
candidate when ``--as`` is absent. Any unrecognized or extra token, a
|
|
464
|
+
reordered flag, or a missing path value is rejected rather than silently
|
|
465
|
+
ignored, so a malformed invocation can never look like a clean verdict.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
all_arguments: The argument vector following the script name, expected
|
|
469
|
+
to lead with ``--check``.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
A ``(candidate_path, target_path)`` pair for one of the two supported
|
|
473
|
+
shapes; otherwise None — the vector does not lead with ``--check``,
|
|
474
|
+
omits a path value, places a flag-shaped token where a path belongs,
|
|
475
|
+
or carries an unrecognized or extra token.
|
|
476
|
+
"""
|
|
477
|
+
if not all_arguments or all_arguments[0] != "--check":
|
|
478
|
+
return None
|
|
479
|
+
tokens_after_check = all_arguments[1:]
|
|
480
|
+
if not tokens_after_check or tokens_after_check[0].startswith("--"):
|
|
481
|
+
return None
|
|
482
|
+
candidate_path = tokens_after_check[0]
|
|
483
|
+
tokens_after_candidate = tokens_after_check[1:]
|
|
484
|
+
if not tokens_after_candidate:
|
|
485
|
+
return candidate_path, candidate_path
|
|
486
|
+
if tokens_after_candidate[0] != "--as":
|
|
487
|
+
return None
|
|
488
|
+
target_tokens = tokens_after_candidate[1:]
|
|
489
|
+
if len(target_tokens) != 1 or target_tokens[0].startswith("--"):
|
|
490
|
+
return None
|
|
491
|
+
return candidate_path, target_tokens[0]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _run_precheck_command(
|
|
495
|
+
all_arguments: list[str],
|
|
496
|
+
violation_stream: TextIO,
|
|
497
|
+
error_stream: TextIO,
|
|
498
|
+
) -> int:
|
|
499
|
+
"""Run the pre-check CLI mode for an argument vector carrying ``--check``.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
all_arguments: The argument vector following the script name.
|
|
503
|
+
violation_stream: The stream each violation line is written to.
|
|
504
|
+
error_stream: The stream usage and candidate errors are written to.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
The usage-error exit code for a malformed flag sequence, otherwise the
|
|
508
|
+
``_run_precheck`` verdict for the parsed candidate and target.
|
|
509
|
+
"""
|
|
510
|
+
precheck_paths = _precheck_arguments(all_arguments)
|
|
511
|
+
if precheck_paths is None:
|
|
512
|
+
error_stream.write(PRECHECK_USAGE_MESSAGE)
|
|
513
|
+
return PRECHECK_USAGE_EXIT_CODE
|
|
514
|
+
candidate_path, target_path = precheck_paths
|
|
515
|
+
return _run_precheck(candidate_path, target_path, violation_stream, error_stream)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _contents_for_validation(
|
|
519
|
+
tool_name: str,
|
|
520
|
+
new_string: str,
|
|
521
|
+
old_string: str,
|
|
522
|
+
written_content: str,
|
|
523
|
+
file_path: str,
|
|
524
|
+
) -> tuple[str, str, str | None, str] | None:
|
|
525
|
+
"""Resolve the content views the verdict needs for the given tool payload.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
tool_name: The tool named in the PreToolUse payload.
|
|
529
|
+
new_string: The Edit payload's replacement fragment.
|
|
530
|
+
old_string: The Edit payload's fragment to replace.
|
|
531
|
+
written_content: The Write payload's whole file body.
|
|
532
|
+
file_path: The destination path of the write or edit.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
A ``(content, old_content, full_file_content_after_edit,
|
|
536
|
+
prior_full_file_content)`` tuple, or None when no validatable view
|
|
537
|
+
exists — an unreadable edit target, or a write over an existing file.
|
|
538
|
+
"""
|
|
336
539
|
if tool_name == "Edit":
|
|
337
|
-
content = tool_input.get("new_string", "")
|
|
338
|
-
old_content = tool_input.get("old_string", "")
|
|
339
540
|
prior_content, full_file_content_after_edit = prior_and_post_edit_content(
|
|
340
|
-
file_path,
|
|
541
|
+
file_path, old_string, new_string,
|
|
341
542
|
)
|
|
342
|
-
prior_full_file_content = prior_content or ""
|
|
343
543
|
if full_file_content_after_edit is None:
|
|
344
544
|
full_file_content_after_edit = _read_existing_file_content(file_path)
|
|
345
545
|
if full_file_content_after_edit is None:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
546
|
+
return None
|
|
547
|
+
return new_string, old_string, full_file_content_after_edit, prior_content or ""
|
|
548
|
+
content = written_content or new_string
|
|
549
|
+
old_content = _read_existing_file_content(file_path) or ""
|
|
550
|
+
if old_content:
|
|
551
|
+
return None
|
|
552
|
+
return content, old_content, None, ""
|
|
350
553
|
|
|
351
|
-
if old_content:
|
|
352
|
-
sys.exit(0)
|
|
353
554
|
|
|
354
|
-
|
|
355
|
-
|
|
555
|
+
def _deny_reason_for_issues(
|
|
556
|
+
all_blocking_issues: list[str],
|
|
557
|
+
tool_name: str,
|
|
558
|
+
file_path: str,
|
|
559
|
+
full_file_content_after_edit: str | None,
|
|
560
|
+
prior_full_file_content: str,
|
|
561
|
+
) -> str:
|
|
562
|
+
"""Compose the deny reason: blocking list, optional forecast, pre-check hint.
|
|
356
563
|
|
|
357
|
-
|
|
564
|
+
Args:
|
|
565
|
+
all_blocking_issues: The blocking violations that decide the deny.
|
|
566
|
+
tool_name: The tool named in the PreToolUse payload.
|
|
567
|
+
file_path: The destination path used for forecast classification.
|
|
568
|
+
full_file_content_after_edit: The whole post-edit file content when the
|
|
569
|
+
edit reconstructs one, used to run the full-file forecast.
|
|
570
|
+
prior_full_file_content: The whole file content before the edit applied.
|
|
571
|
+
Empty when the edit's old_string is absent and no reliable prior
|
|
572
|
+
exists; the forecast is skipped in that case so a comment diff
|
|
573
|
+
against an empty prior cannot mislabel pre-existing comments as
|
|
574
|
+
future blockers.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
The complete ``permissionDecisionReason`` text.
|
|
578
|
+
"""
|
|
579
|
+
issue_list = "; ".join(all_blocking_issues[:10])
|
|
580
|
+
deny_reason = (
|
|
581
|
+
f"BLOCKED: [CODE_RULES] {len(all_blocking_issues)} violation(s): {issue_list}"
|
|
582
|
+
)
|
|
583
|
+
has_reconstructed_prior = bool(prior_full_file_content)
|
|
584
|
+
if (
|
|
585
|
+
tool_name == "Edit"
|
|
586
|
+
and full_file_content_after_edit is not None
|
|
587
|
+
and has_reconstructed_prior
|
|
588
|
+
):
|
|
589
|
+
forecast_issues = _forecast_full_file_violations(
|
|
590
|
+
full_file_content_after_edit,
|
|
591
|
+
file_path=file_path,
|
|
592
|
+
prior_full_file_content=prior_full_file_content,
|
|
593
|
+
all_blocking_issues=all_blocking_issues,
|
|
594
|
+
)
|
|
595
|
+
if forecast_issues:
|
|
596
|
+
forecast_list = "; ".join(forecast_issues[:10])
|
|
597
|
+
deny_reason += (
|
|
598
|
+
f"; FULL-FILE FORECAST — {len(forecast_issues)} additional "
|
|
599
|
+
"violation(s) elsewhere in this file will block future edits "
|
|
600
|
+
f"(full-file line numbers): {forecast_list}"
|
|
601
|
+
)
|
|
602
|
+
return deny_reason + _precheck_hint()
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _report_blocking_violations(
|
|
606
|
+
content: str,
|
|
607
|
+
tool_name: str,
|
|
608
|
+
file_path: str,
|
|
609
|
+
old_content: str,
|
|
610
|
+
full_file_content_after_edit: str | None,
|
|
611
|
+
prior_full_file_content: str,
|
|
612
|
+
deny_stream: TextIO,
|
|
613
|
+
) -> None:
|
|
614
|
+
"""Run the verdict and write a deny payload when blocking violations fire.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
content: The fragment or whole-file body under validation.
|
|
618
|
+
tool_name: The tool named in the PreToolUse payload.
|
|
619
|
+
file_path: The destination path of the write or edit.
|
|
620
|
+
old_content: The fragment the edit replaces, or empty for a write.
|
|
621
|
+
full_file_content_after_edit: The reconstructed post-edit file body,
|
|
622
|
+
or None when the payload is not an Edit.
|
|
623
|
+
prior_full_file_content: The on-disk content before the edit.
|
|
624
|
+
deny_stream: The stream the JSON deny payload is written to.
|
|
625
|
+
"""
|
|
626
|
+
all_blocking_issues = validate_content(
|
|
358
627
|
content,
|
|
359
628
|
file_path,
|
|
360
629
|
old_content,
|
|
361
630
|
full_file_content_after_edit,
|
|
362
631
|
prior_full_file_content,
|
|
363
632
|
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
633
|
+
if not all_blocking_issues:
|
|
634
|
+
return
|
|
635
|
+
deny_payload = {
|
|
636
|
+
"hookSpecificOutput": {
|
|
637
|
+
"hookEventName": "PreToolUse",
|
|
638
|
+
"permissionDecision": "deny",
|
|
639
|
+
"permissionDecisionReason": _deny_reason_for_issues(
|
|
640
|
+
all_blocking_issues,
|
|
641
|
+
tool_name,
|
|
642
|
+
file_path,
|
|
643
|
+
full_file_content_after_edit,
|
|
644
|
+
prior_full_file_content,
|
|
645
|
+
),
|
|
373
646
|
}
|
|
374
|
-
|
|
375
|
-
|
|
647
|
+
}
|
|
648
|
+
deny_stream.write(json.dumps(deny_payload) + "\n")
|
|
649
|
+
deny_stream.flush()
|
|
650
|
+
|
|
376
651
|
|
|
652
|
+
def main(all_arguments: list[str]) -> None:
|
|
653
|
+
"""Run the enforcer for the given argument vector.
|
|
654
|
+
|
|
655
|
+
Dispatches to the pre-check CLI mode when the vector carries ``--check``;
|
|
656
|
+
otherwise reads a PreToolUse payload from stdin and emits a deny payload
|
|
657
|
+
on stdout when the content violates a blocking rule.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
all_arguments: The argument vector following the script name.
|
|
661
|
+
"""
|
|
662
|
+
if "--check" in all_arguments:
|
|
663
|
+
sys.exit(_run_precheck_command(all_arguments, sys.stdout, sys.stderr))
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
pretooluse_payload = json.load(sys.stdin)
|
|
667
|
+
except json.JSONDecodeError:
|
|
668
|
+
sys.exit(0)
|
|
669
|
+
|
|
670
|
+
tool_name = pretooluse_payload.get("tool_name", "")
|
|
671
|
+
tool_input = pretooluse_payload.get("tool_input", {})
|
|
672
|
+
file_path = tool_input.get("file_path", "")
|
|
673
|
+
|
|
674
|
+
if not _is_validated_target(file_path):
|
|
675
|
+
sys.exit(0)
|
|
676
|
+
|
|
677
|
+
validation_contents = _contents_for_validation(
|
|
678
|
+
tool_name,
|
|
679
|
+
tool_input.get("new_string", ""),
|
|
680
|
+
tool_input.get("old_string", ""),
|
|
681
|
+
tool_input.get("content", ""),
|
|
682
|
+
file_path,
|
|
683
|
+
)
|
|
684
|
+
if validation_contents is None:
|
|
685
|
+
sys.exit(0)
|
|
686
|
+
content, old_content, full_file_content_after_edit, prior_full_file_content = (
|
|
687
|
+
validation_contents
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
if not content:
|
|
691
|
+
sys.exit(0)
|
|
692
|
+
|
|
693
|
+
_report_blocking_violations(
|
|
694
|
+
content,
|
|
695
|
+
tool_name,
|
|
696
|
+
file_path,
|
|
697
|
+
old_content,
|
|
698
|
+
full_file_content_after_edit,
|
|
699
|
+
prior_full_file_content,
|
|
700
|
+
sys.stdout,
|
|
701
|
+
)
|
|
377
702
|
sys.exit(0)
|
|
378
703
|
|
|
379
704
|
|
|
380
705
|
if __name__ == "__main__":
|
|
381
|
-
main()
|
|
706
|
+
main(sys.argv[1:])
|