claude-dev-env 2.7.1 → 2.8.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 (85) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/process-tree/CLAUDE.md +41 -0
  6. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  8. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  9. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  10. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  11. package/agents/code-quality-agent.md +6 -5
  12. package/agents/deep-research.md +7 -24
  13. package/agents/docs-agent.md +1 -27
  14. package/agents/issue-tracker.md +1 -7
  15. package/agents/skill-writer-agent.md +1 -2
  16. package/agents/test_agent_frontmatter.py +309 -12
  17. package/hooks/blocking/CLAUDE.md +2 -0
  18. package/hooks/blocking/fable_spawn_gate.py +187 -0
  19. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  20. package/hooks/blocking/plain_language_blocker.py +287 -15
  21. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  22. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  23. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  24. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  25. package/hooks/git-hooks/CLAUDE.md +2 -2
  26. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  27. package/hooks/git-hooks/pre_push.py +343 -54
  28. package/hooks/git-hooks/test_pre_push.py +852 -6
  29. package/hooks/hooks.json +9 -19
  30. package/hooks/hooks_constants/CLAUDE.md +2 -0
  31. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  32. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  33. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  34. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  36. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  37. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  39. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  40. package/package.json +1 -1
  41. package/rules/CLAUDE.md +1 -0
  42. package/rules/ask-user-question-required.md +26 -0
  43. package/rules/claims-as-quotes.md +65 -0
  44. package/scripts/CLAUDE.md +1 -1
  45. package/scripts/_code_review_test_support.py +6 -0
  46. package/scripts/check.ps1 +18 -5
  47. package/scripts/claude_chain_runner.py +203 -31
  48. package/scripts/codec_forwarding_test_support.py +2 -0
  49. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  50. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  51. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  52. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
  53. package/scripts/grok_headless_runner.py +16 -83
  54. package/scripts/spawn_grok_batch.py +38 -1
  55. package/scripts/test_claude_chain_runner.py +358 -0
  56. package/scripts/test_grok_headless_runner.py +18 -63
  57. package/scripts/test_invoke_code_review.py +298 -0
  58. package/scripts/test_resolve_worker_spawn.py +6 -0
  59. package/scripts/test_spawn_grok_batch.py +101 -0
  60. package/scripts/tests/CLAUDE.md +1 -0
  61. package/scripts/tests/test_grok_worker_constants.py +59 -0
  62. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  63. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  64. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  65. package/skills/autoconverge/workflow/converge.mjs +66 -33
  66. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  67. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  68. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  69. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  70. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  71. package/skills/e-code-review/SKILL.md +9 -8
  72. package/skills/e-code-review/reference/fix.md +29 -7
  73. package/skills/e-code-review/reference/loop.md +230 -14
  74. package/skills/e-code-review/reference/low.md +33 -15
  75. package/skills/e-code-review/reference/medium.md +55 -21
  76. package/skills/e-code-review/reference/xhigh.md +30 -12
  77. package/skills/fresh-branch/CLAUDE.md +5 -5
  78. package/skills/fresh-branch/SKILL.md +14 -6
  79. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  80. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  81. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  82. package/skills/grok-spawn/SKILL.md +5 -0
  83. package/skills/orchestrator/SKILL.md +4 -1
  84. package/skills/orchestrator-refresh/SKILL.md +5 -1
  85. package/skills/team-advisor/SKILL.md +4 -1
@@ -2,8 +2,9 @@
2
2
 
3
3
  Covers the shared prose scanner (fenced code, inline code, blockquotes, URLs,
4
4
  file paths), the word-boundary guard, multi-word phrase matching, case
5
- insensitivity, the term -> replacement block message, and both registered
6
- PreToolUse surfaces (AskUserQuestion and Write|Edit on .md targets).
5
+ insensitivity, the term -> replacement block message, both registered
6
+ PreToolUse surfaces (AskUserQuestion and Write|Edit on .md targets), and the
7
+ lean-question-block check that keeps chat detail out of an AskUserQuestion call.
7
8
  """
8
9
 
9
10
  import importlib.util
@@ -39,6 +40,9 @@ hook_module = _load_hook_module()
39
40
  find_banned_terms = hook_module.find_banned_terms
40
41
  strip_non_prose_regions = hook_module.strip_non_prose_regions
41
42
  build_block_reason = hook_module.build_block_reason
43
+ find_question_block_violations = hook_module.find_question_block_violations
44
+ build_lean_block_reason = hook_module.build_lean_block_reason
45
+ evaluate = hook_module.evaluate
42
46
 
43
47
  from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
44
48
 
@@ -296,3 +300,274 @@ def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
296
300
  logged_record = json.loads(all_records[0])
297
301
  assert logged_record["hook"] == "plain_language_blocker.py"
298
302
  assert logged_record["event"] == "PreToolUse"
303
+
304
+
305
+ LEAN_QUESTION = "Which gate should run first?"
306
+ LEAN_DESCRIPTION = "Runs on every write."
307
+ QUESTION_AT_THE_WORD_CAP = (
308
+ "Should we keep the gate on the write path where it reads the whole file "
309
+ "each time, or move it to the commit path where it reads only the lines "
310
+ "that a change has already staged for the current review?"
311
+ )
312
+ QUESTION_OVER_THE_WORD_CAP = (
313
+ "Should we keep the gate on the write path where it reads the whole file "
314
+ "each time it runs, or move the gate to the commit path where it reads "
315
+ "only the staged lines and skips the files that no one on the team ever "
316
+ "touched?"
317
+ )
318
+ DESCRIPTION_AT_THE_WORD_CAP = (
319
+ "Runs the gate at commit time so it reads only the lines a change staged."
320
+ )
321
+ DESCRIPTION_OVER_THE_WORD_CAP = (
322
+ "Runs the gate at commit time so it reads only the lines the change "
323
+ "actually staged for review."
324
+ )
325
+ QUESTION_WITH_INLINE_CODE_AT_THE_WORD_CAP = (
326
+ "Should we keep the gate on the write path where it reads the whole file "
327
+ "each time, or move it to the commit path where it reads only what "
328
+ "`git diff --cached --name-only HEAD` lists for the current review before "
329
+ "the team sees it?"
330
+ )
331
+ QUESTION_WITH_INLINE_CODE_OVER_THE_WORD_CAP = (
332
+ "Should we keep the gate on the write path where it reads the whole file "
333
+ "each time, or move it to the commit path where it reads only what "
334
+ "`git diff --cached --name-only HEAD` lists for the current review before "
335
+ "the whole team sees it?"
336
+ )
337
+ DESCRIPTION_WITH_INLINE_CODE_AT_THE_WORD_CAP = (
338
+ "Runs the gate at commit time so it reads only what "
339
+ "`git diff --cached --name-only HEAD` lists for review."
340
+ )
341
+ DESCRIPTION_WITH_INLINE_CODE_OVER_THE_WORD_CAP = (
342
+ "Runs the gate at commit time so it reads only what "
343
+ "`git diff --cached --name-only HEAD` lists for the review."
344
+ )
345
+
346
+
347
+ def _ask_payload(question_text: str, all_descriptions: list[str]) -> dict[str, object]:
348
+ return {
349
+ "tool_name": "AskUserQuestion",
350
+ "tool_input": {
351
+ "questions": [
352
+ {
353
+ "question": question_text,
354
+ "header": "Gate",
355
+ "options": [
356
+ {"label": f"Gate {each_index}", "description": each_description}
357
+ for each_index, each_description in enumerate(all_descriptions)
358
+ ],
359
+ }
360
+ ]
361
+ },
362
+ }
363
+
364
+
365
+ def test_question_carrying_a_list_marker_plan_is_denied() -> None:
366
+ plan_question = f"{LEAN_QUESTION}\n- Split the file\n- Wire the gate\n- Run the suite"
367
+
368
+ deny_reason = evaluate(_ask_payload(plan_question, [LEAN_DESCRIPTION]))
369
+
370
+ assert deny_reason is not None
371
+ assert "a bullet or numbered list marker" in deny_reason
372
+
373
+
374
+ def test_question_carrying_a_fenced_block_is_denied() -> None:
375
+ fenced_question = f"{LEAN_QUESTION}\n```\nrun_gate()\n```"
376
+
377
+ deny_reason = evaluate(_ask_payload(fenced_question, [LEAN_DESCRIPTION]))
378
+
379
+ assert deny_reason is not None
380
+ assert "a fenced code block" in deny_reason
381
+
382
+
383
+ def test_question_carrying_a_second_paragraph_is_denied() -> None:
384
+ two_paragraph_question = f"{LEAN_QUESTION}\n\nThe write gate reads every file."
385
+
386
+ deny_reason = evaluate(_ask_payload(two_paragraph_question, [LEAN_DESCRIPTION]))
387
+
388
+ assert deny_reason is not None
389
+ assert "more than one paragraph" in deny_reason
390
+
391
+
392
+ def test_question_carrying_a_one_line_fence_is_denied() -> None:
393
+ one_line_fence_question = f"{LEAN_QUESTION}\n```print(1)```"
394
+
395
+ deny_reason = evaluate(_ask_payload(one_line_fence_question, [LEAN_DESCRIPTION]))
396
+
397
+ assert deny_reason is not None
398
+ assert "a fenced code block" in deny_reason
399
+
400
+
401
+ def test_question_carrying_a_carriage_return_paragraph_break_is_denied() -> None:
402
+ crlf_question = f"{LEAN_QUESTION}\r\n\r\nThe write gate reads every file."
403
+
404
+ deny_reason = evaluate(_ask_payload(crlf_question, [LEAN_DESCRIPTION]))
405
+
406
+ assert deny_reason is not None
407
+ assert "more than one paragraph" in deny_reason
408
+
409
+
410
+ def test_question_carrying_a_heading_is_denied() -> None:
411
+ headed_question = f"{LEAN_QUESTION}\n## The write gate\nIt reads every file."
412
+
413
+ deny_reason = evaluate(_ask_payload(headed_question, [LEAN_DESCRIPTION]))
414
+
415
+ assert deny_reason is not None
416
+ assert "a heading" in deny_reason
417
+
418
+
419
+ def test_option_description_carrying_a_table_row_is_denied() -> None:
420
+ deny_reason = evaluate(_ask_payload(LEAN_QUESTION, ["| gate | 12 ms |"]))
421
+
422
+ assert deny_reason is not None
423
+ assert "a table row" in deny_reason
424
+
425
+
426
+ def test_question_over_the_word_cap_is_denied() -> None:
427
+ deny_reason = evaluate(_ask_payload(QUESTION_OVER_THE_WORD_CAP, [LEAN_DESCRIPTION]))
428
+
429
+ assert deny_reason is not None
430
+ assert "46 words, over the 40-word cap" in deny_reason
431
+
432
+
433
+ def test_question_over_the_sentence_cap_is_denied() -> None:
434
+ three_sentence_question = (
435
+ "Which gate runs first? The write gate reads every file. "
436
+ "The commit gate reads staged lines only."
437
+ )
438
+
439
+ deny_reason = evaluate(_ask_payload(three_sentence_question, [LEAN_DESCRIPTION]))
440
+
441
+ assert deny_reason is not None
442
+ assert "3 sentences, over the 2-sentence cap" in deny_reason
443
+
444
+
445
+ def test_option_description_over_the_word_cap_is_denied() -> None:
446
+ deny_reason = evaluate(_ask_payload(LEAN_QUESTION, [DESCRIPTION_OVER_THE_WORD_CAP]))
447
+
448
+ assert deny_reason is not None
449
+ assert "18 words, over the 15-word cap" in deny_reason
450
+
451
+
452
+ def test_option_description_over_the_sentence_cap_is_denied() -> None:
453
+ deny_reason = evaluate(_ask_payload(LEAN_QUESTION, ["Runs on write. Reads every file."]))
454
+
455
+ assert deny_reason is not None
456
+ assert "2 sentences, over the 1-sentence cap" in deny_reason
457
+
458
+
459
+ def test_lean_question_with_three_short_options_is_allowed() -> None:
460
+ all_descriptions = [
461
+ LEAN_DESCRIPTION,
462
+ "Runs at commit time.",
463
+ "Runs in both places.",
464
+ ]
465
+
466
+ assert evaluate(_ask_payload(LEAN_QUESTION, all_descriptions)) is None
467
+
468
+
469
+ def test_two_sentence_question_at_the_sentence_cap_is_allowed() -> None:
470
+ two_sentence_question = f"{LEAN_QUESTION} Both gates read the same lines."
471
+
472
+ assert evaluate(_ask_payload(two_sentence_question, [LEAN_DESCRIPTION])) is None
473
+
474
+
475
+ def test_question_at_the_word_cap_is_allowed() -> None:
476
+ assert evaluate(_ask_payload(QUESTION_AT_THE_WORD_CAP, [LEAN_DESCRIPTION])) is None
477
+
478
+
479
+ def test_option_description_at_the_word_cap_is_allowed() -> None:
480
+ assert evaluate(_ask_payload(LEAN_QUESTION, [DESCRIPTION_AT_THE_WORD_CAP])) is None
481
+
482
+
483
+ def test_question_with_an_inline_code_span_is_allowed() -> None:
484
+ question_text = QUESTION_WITH_INLINE_CODE_AT_THE_WORD_CAP
485
+
486
+ assert len(question_text.split()) > 40
487
+ assert evaluate(_ask_payload(question_text, [LEAN_DESCRIPTION])) is None
488
+
489
+
490
+ def test_inline_code_span_counts_as_one_word() -> None:
491
+ deny_reason = evaluate(
492
+ _ask_payload(QUESTION_WITH_INLINE_CODE_OVER_THE_WORD_CAP, [LEAN_DESCRIPTION])
493
+ )
494
+
495
+ assert deny_reason is not None
496
+ assert "41 words, over the 40-word cap" in deny_reason
497
+
498
+
499
+ def test_option_description_with_an_inline_code_span_is_allowed() -> None:
500
+ option_description = DESCRIPTION_WITH_INLINE_CODE_AT_THE_WORD_CAP
501
+
502
+ assert len(option_description.split()) > 15
503
+ assert evaluate(_ask_payload(LEAN_QUESTION, [option_description])) is None
504
+
505
+
506
+ def test_inline_code_span_counts_as_one_word_in_an_option_description() -> None:
507
+ deny_reason = evaluate(
508
+ _ask_payload(LEAN_QUESTION, [DESCRIPTION_WITH_INLINE_CODE_OVER_THE_WORD_CAP])
509
+ )
510
+
511
+ assert deny_reason is not None
512
+ assert "16 words, over the 15-word cap" in deny_reason
513
+
514
+
515
+ def test_markdown_write_keeps_its_bullets_and_tables(tmp_path: Path) -> None:
516
+ bulleted_prose = "- Split the file\n- Wire the gate\n\n| gate | 12 ms |\n"
517
+ target_path = str(tmp_path / "notes.md")
518
+ heavy_payload = {
519
+ "tool_name": "Write",
520
+ "tool_input": {
521
+ "file_path": target_path,
522
+ "content": f"{bulleted_prose}Then utilize the cache.",
523
+ },
524
+ }
525
+ clean_payload = {
526
+ "tool_name": "Write",
527
+ "tool_input": {"file_path": target_path, "content": bulleted_prose},
528
+ }
529
+
530
+ assert evaluate(heavy_payload) is not None
531
+ assert evaluate(clean_payload) is None
532
+
533
+
534
+ def test_find_question_block_violations_reports_one_line_per_repeated_fault() -> None:
535
+ tool_input = {
536
+ "questions": [
537
+ {
538
+ "question": LEAN_QUESTION,
539
+ "options": [
540
+ {"label": "Write", "description": DESCRIPTION_OVER_THE_WORD_CAP},
541
+ {"label": "Commit", "description": DESCRIPTION_OVER_THE_WORD_CAP},
542
+ ],
543
+ }
544
+ ]
545
+ }
546
+
547
+ all_violations = find_question_block_violations(tool_input)
548
+
549
+ assert all_violations == [
550
+ "an option description runs 18 words, over the 15-word cap"
551
+ ]
552
+
553
+
554
+ def test_lean_block_reason_names_the_fault_and_the_fix() -> None:
555
+ reason = build_lean_block_reason(["the question carries a table row"])
556
+
557
+ assert "the question carries a table row" in reason
558
+ assert "in chat text" in reason
559
+
560
+
561
+ def test_each_denial_carries_its_own_user_notice() -> None:
562
+ plan_question = f"{LEAN_QUESTION}\n- Split the file\n- Wire the gate"
563
+ lean_denial = _run_hook_with_payload(_ask_payload(plan_question, [LEAN_DESCRIPTION]))
564
+ heavy_denial = _run_hook_with_payload(
565
+ _ask_payload("Should we utilize the cache?", [LEAN_DESCRIPTION])
566
+ )
567
+
568
+ lean_notice = json.loads(lean_denial.stdout)["systemMessage"]
569
+ heavy_notice = json.loads(heavy_denial.stdout)["systemMessage"]
570
+
571
+ assert "question" in lean_notice.lower()
572
+ assert "word" in heavy_notice.lower()
573
+ assert lean_notice != heavy_notice
@@ -656,8 +656,11 @@ def test_dispatcher_edit_applies_both_groups() -> None:
656
656
  assert "blocking/stale_comment_reference_blocker.py" in all_edit_script_paths, (
657
657
  "stale_comment_reference_blocker belongs in the Edit applicable set"
658
658
  )
659
- assert len(all_edit_entries) == 22, (
660
- f"expected 22 Edit entries, got {len(all_edit_entries)}"
659
+ assert "advisory/refactor_guard.py" in all_edit_script_paths, (
660
+ "refactor_guard is Edit-scoped and hosted, so it belongs in the Edit applicable set"
661
+ )
662
+ assert len(all_edit_entries) == 24, (
663
+ f"expected 24 Edit entries, got {len(all_edit_entries)}"
661
664
  )
662
665
 
663
666
 
@@ -844,6 +847,35 @@ def test_runpy_hosted_hook_sees_its_own_argv_not_the_dispatchers(tmp_path: Path)
844
847
  )
845
848
 
846
849
 
850
+ def test_folded_write_edit_hooks_have_no_standalone_hooks_json_entry() -> None:
851
+ """A hook the dispatcher hosts must not also run as its own process.
852
+
853
+ Each folded hook spawns an interpreter of its own when hooks.json still
854
+ registers it, so the fold only pays off once the standalone entry is gone.
855
+ """
856
+ hooks_json_text = (_HOOKS_ROOT / "hooks.json").read_text(encoding="utf-8")
857
+ hooks_configuration = json.loads(hooks_json_text)
858
+ all_pre_tool_use_commands = [
859
+ each_hook["command"]
860
+ for each_group in hooks_configuration["hooks"]["PreToolUse"]
861
+ for each_hook in each_group["hooks"]
862
+ ]
863
+ folded_script_relative_paths = (
864
+ "advisory/refactor_guard.py",
865
+ "advisory/migration_safety_advisor.py",
866
+ )
867
+ for each_script_path in folded_script_relative_paths:
868
+ matching_commands = [
869
+ each_command
870
+ for each_command in all_pre_tool_use_commands
871
+ if each_script_path in each_command
872
+ ]
873
+ assert not matching_commands, (
874
+ f"{each_script_path} is hosted by the dispatcher, so its standalone "
875
+ f"hooks.json entry must be gone. Found: {matching_commands!r}"
876
+ )
877
+
878
+
847
879
  def test_hosted_hook_set_covers_all_write_edit_blocking_hooks() -> None:
848
880
  """The hosted hook set covers all previously-registered Write/Edit blocking hooks.
849
881
 
@@ -7,8 +7,8 @@ Native git hooks that run outside the Claude Code lifecycle — invoked directly
7
7
  | File | Git hook | What it does |
8
8
  |---|---|---|
9
9
  | `pre_commit.py` | `pre-commit` | Runs the CODE_RULES gate (`precommit_code_rules_gate.py`) over staged changes; exits 1 when any staged file has a blocking violation |
10
- | `pre_push.py` | `pre-push` | Blocks a push that would land a non-`main` local branch onto remote `main` (or `master`), then runs the CODE_RULES gate over the commits about to be pushed |
11
- | `pre_push_base_reference.py` | — | Resolves the gate base for `pre_push.py`: reads the pushed remote name from git's arguments, then names that remote's default branch from its symbolic ref or its candidate default branches |
10
+ | `pre_push.py` | `pre-push` | Blocks a push that would land a non-`main` local branch onto remote `main` (or `master`), then runs the CODE_RULES gate. An existing branch's gate base is the merge base with the remote default branch; the gate process still diffs that base against checkout HEAD, so the surface matches the pushed tip only when HEAD is that tip. |
11
+ | `pre_push_base_reference.py` | — | Resolves a usable gate base for `pre_push.py`: reads the pushed remote name from git's arguments, then turns a symbolic default-branch head into a reference that git can resolve |
12
12
  | `post_commit.py` | `post-commit` | Runs after a commit lands; performs any post-commit bookkeeping |
13
13
  | `gate_utils.py` | — | Shared helpers: resolves the gate script path, checks that the path is a safe regular file |
14
14
  | `test_config.py` | — | Test configuration helpers |
@@ -47,7 +47,35 @@ NO_PARSEABLE_STDIN_LINES_MESSAGE: str = (
47
47
  "claude-dev-env pre-push: no parseable stdin lines; aborting"
48
48
  )
49
49
  NO_PARSEABLE_STDIN_LINES_SENTINEL: str = "__no_parseable_stdin_lines__"
50
+ UNRESOLVABLE_MERGE_BASE_SENTINEL: str = "__unresolvable_merge_base__"
51
+ UNRESOLVABLE_MERGE_BASE_MESSAGE: str = (
52
+ "claude-dev-env pre-push: git found no merge base between the pushed object "
53
+ "and the default branch, so the gate scope is unknown and "
54
+ "enforcement is skipped. That happens when the pushed branch and the "
55
+ "default branch hold unrelated histories, or when a default-branch ref "
56
+ "resolved and merge-base still could not name a shared commit "
57
+ "(for example a shallow clone missing the connecting history)."
58
+ )
50
59
  LOCAL_BRANCH_REFERENCE_PREFIX: str = "refs/heads/"
60
+ ORIGIN_HEAD_SYMBOLIC_REFERENCE: str = "refs/remotes/origin/HEAD"
61
+ ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX: str = "refs/remotes/origin/"
62
+ ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES: tuple[str, ...] = (
63
+ "refs/remotes/origin/main",
64
+ "refs/remotes/origin/master",
65
+ )
66
+ ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX: tuple[str, ...] = (
67
+ "git",
68
+ "symbolic-ref",
69
+ "--quiet",
70
+ )
71
+ ALL_GIT_VERIFY_REFERENCE_COMMAND_PREFIX: tuple[str, ...] = (
72
+ "git",
73
+ "rev-parse",
74
+ "--verify",
75
+ "--quiet",
76
+ )
77
+ ALL_GIT_MERGE_BASE_COMMAND_PREFIX: tuple[str, ...] = ("git", "merge-base")
78
+ GIT_REFERENCE_QUERY_TIMEOUT_SECONDS: int = 15
51
79
  CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_REVIEW_PUSH_GATE_PATH"
52
80
  CODE_REVIEW_PUSH_GATE_MODULE_NAME: str = "code_review_push_gate"
53
81
  CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME: str = "code_review_push_gate.py"