claude-dev-env 7.0.0 → 7.2.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.
- package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -615,7 +615,7 @@ def _resolved_codex_enforcer_command(config: MaterializerConfig) -> str:
|
|
|
615
615
|
|
|
616
616
|
def _source_codex_enforcer_hook(
|
|
617
617
|
all_source_manifest: dict[str, object], command: str
|
|
618
|
-
) -> dict[str, object]:
|
|
618
|
+
) -> dict[str, object] | None:
|
|
619
619
|
"""Read the source enforcer hook shape and resolve its target command."""
|
|
620
620
|
all_events = all_source_manifest.get("hooks")
|
|
621
621
|
if not isinstance(all_events, dict):
|
|
@@ -637,11 +637,7 @@ def _source_codex_enforcer_hook(
|
|
|
637
637
|
resolved_hook = dict(each_hook)
|
|
638
638
|
resolved_hook[codex_hook_command_field_name] = command
|
|
639
639
|
return resolved_hook
|
|
640
|
-
return
|
|
641
|
-
"type": "command",
|
|
642
|
-
codex_hook_command_field_name: command,
|
|
643
|
-
"timeout": codex_hook_timeout_seconds,
|
|
644
|
-
}
|
|
640
|
+
return None
|
|
645
641
|
|
|
646
642
|
|
|
647
643
|
def _hook_records(raw_hooks: object) -> list[dict[str, object]]:
|
|
@@ -668,6 +664,72 @@ def _is_code_rules_enforcer_hook(all_hook_record: dict[str, object]) -> bool:
|
|
|
668
664
|
)
|
|
669
665
|
|
|
670
666
|
|
|
667
|
+
def _is_managed_codex_enforcer_hook(
|
|
668
|
+
all_hook_record: dict[str, object], managed_command: str
|
|
669
|
+
) -> bool:
|
|
670
|
+
"""Report whether a hook record is the materializer's target command."""
|
|
671
|
+
command = all_hook_record.get(codex_hook_command_field_name)
|
|
672
|
+
if not isinstance(command, str):
|
|
673
|
+
return False
|
|
674
|
+
normalized_command = command.replace("\\", path_separator)
|
|
675
|
+
normalized_managed_command = managed_command.replace("\\", path_separator)
|
|
676
|
+
if os.name == "nt":
|
|
677
|
+
return normalized_command.casefold() == normalized_managed_command.casefold()
|
|
678
|
+
return normalized_command == normalized_managed_command
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _without_managed_codex_enforcer(
|
|
682
|
+
entry: object, managed_command: str
|
|
683
|
+
) -> object | None:
|
|
684
|
+
"""Remove the managed enforcer while preserving every other hook record."""
|
|
685
|
+
if not isinstance(entry, dict):
|
|
686
|
+
return entry
|
|
687
|
+
if entry.get("matcher") != codex_hook_matcher:
|
|
688
|
+
return entry
|
|
689
|
+
copied_entry = dict(entry)
|
|
690
|
+
all_hook_records = copied_entry.get(codex_hook_records_field_name)
|
|
691
|
+
if not isinstance(all_hook_records, list):
|
|
692
|
+
return copied_entry
|
|
693
|
+
all_retained_hooks = [
|
|
694
|
+
each_hook
|
|
695
|
+
for each_hook in all_hook_records
|
|
696
|
+
if not isinstance(each_hook, dict)
|
|
697
|
+
or not _is_managed_codex_enforcer_hook(each_hook, managed_command)
|
|
698
|
+
]
|
|
699
|
+
if all_retained_hooks == all_hook_records:
|
|
700
|
+
return copied_entry
|
|
701
|
+
if not all_retained_hooks:
|
|
702
|
+
return None
|
|
703
|
+
copied_entry[codex_hook_records_field_name] = all_retained_hooks
|
|
704
|
+
return copied_entry
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _remove_managed_codex_enforcer(
|
|
708
|
+
all_target_manifest: dict[str, object], managed_command: str
|
|
709
|
+
) -> dict[str, object]:
|
|
710
|
+
"""Remove the retired managed enforcer from the target manifest."""
|
|
711
|
+
all_events = all_target_manifest.get(codex_hook_records_field_name, {})
|
|
712
|
+
if not isinstance(all_events, dict):
|
|
713
|
+
raise MaterializerError("Codex hook manifest requires a hooks object")
|
|
714
|
+
all_pre_tool_use = all_events.get(codex_hook_event_name)
|
|
715
|
+
if all_pre_tool_use is None:
|
|
716
|
+
return _prune_retired_codex_hooks(all_target_manifest)
|
|
717
|
+
if not isinstance(all_pre_tool_use, list):
|
|
718
|
+
raise MaterializerError("Codex hook manifest requires a PreToolUse list")
|
|
719
|
+
all_retained_entries: list[object] = []
|
|
720
|
+
for each_entry in all_pre_tool_use:
|
|
721
|
+
maybe_retained_entry = _without_managed_codex_enforcer(
|
|
722
|
+
each_entry, managed_command
|
|
723
|
+
)
|
|
724
|
+
if maybe_retained_entry is not None:
|
|
725
|
+
all_retained_entries.append(maybe_retained_entry)
|
|
726
|
+
merged_events = dict(all_events)
|
|
727
|
+
merged_events[codex_hook_event_name] = all_retained_entries
|
|
728
|
+
merged_manifest = dict(all_target_manifest)
|
|
729
|
+
merged_manifest[codex_hook_records_field_name] = merged_events
|
|
730
|
+
return _prune_retired_codex_hooks(merged_manifest)
|
|
731
|
+
|
|
732
|
+
|
|
671
733
|
def _is_retired_codex_hook_command(command: object) -> bool:
|
|
672
734
|
"""Report whether a command names a known retired hook path."""
|
|
673
735
|
if not isinstance(command, str):
|
|
@@ -764,13 +826,20 @@ def _has_modified_codex_enforcer_hook(
|
|
|
764
826
|
current_hooks = _codex_enforcer_hooks(current_manifest)
|
|
765
827
|
if not current_hooks:
|
|
766
828
|
return False
|
|
767
|
-
|
|
829
|
+
planned_hooks = _codex_enforcer_hooks(planned_manifest)
|
|
830
|
+
if all(each_hook in current_hooks for each_hook in planned_hooks):
|
|
831
|
+
return False
|
|
832
|
+
return current_hooks != planned_hooks
|
|
768
833
|
|
|
769
834
|
|
|
770
835
|
def _merge_codex_hook_manifest(
|
|
771
|
-
all_target_manifest: dict[str, object],
|
|
836
|
+
all_target_manifest: dict[str, object],
|
|
837
|
+
all_focused_hook: dict[str, object] | None,
|
|
838
|
+
managed_command: str,
|
|
772
839
|
) -> dict[str, object]:
|
|
773
840
|
"""Preserve target hook order while merging one deterministic enforcer entry."""
|
|
841
|
+
if all_focused_hook is None:
|
|
842
|
+
return _remove_managed_codex_enforcer(all_target_manifest, managed_command)
|
|
774
843
|
all_events = all_target_manifest.get(codex_hook_records_field_name, {})
|
|
775
844
|
if not isinstance(all_events, dict):
|
|
776
845
|
raise MaterializerError("Codex hook manifest requires a hooks object")
|
|
@@ -797,13 +866,17 @@ def _merge_codex_hook_manifest(
|
|
|
797
866
|
if merged_apply_patch_entry is None:
|
|
798
867
|
merged_apply_patch_entry = copied_entry
|
|
799
868
|
merged_hooks = [
|
|
800
|
-
each_hook
|
|
869
|
+
each_hook
|
|
870
|
+
for each_hook in all_hook_records
|
|
871
|
+
if not _is_managed_codex_enforcer_hook(each_hook, managed_command)
|
|
801
872
|
]
|
|
802
873
|
merged_apply_patch_entry[codex_hook_records_field_name] = merged_hooks
|
|
803
874
|
merged_pre_tool_use.append(merged_apply_patch_entry)
|
|
804
875
|
else:
|
|
805
876
|
merged_hooks.extend(
|
|
806
|
-
each_hook
|
|
877
|
+
each_hook
|
|
878
|
+
for each_hook in all_hook_records
|
|
879
|
+
if not _is_managed_codex_enforcer_hook(each_hook, managed_command)
|
|
807
880
|
)
|
|
808
881
|
if merged_apply_patch_entry is None:
|
|
809
882
|
merged_apply_patch_entry = {
|
|
@@ -831,10 +904,11 @@ def _build_codex_hook_projection(config: MaterializerConfig) -> PlannedFile | No
|
|
|
831
904
|
if target_path.is_file()
|
|
832
905
|
else {"hooks": {}}
|
|
833
906
|
)
|
|
834
|
-
|
|
835
|
-
|
|
907
|
+
managed_command = _resolved_codex_enforcer_command(config)
|
|
908
|
+
focused_hook = _source_codex_enforcer_hook(source_manifest, managed_command)
|
|
909
|
+
projected_manifest = _merge_codex_hook_manifest(
|
|
910
|
+
target_manifest, focused_hook, managed_command
|
|
836
911
|
)
|
|
837
|
-
projected_manifest = _merge_codex_hook_manifest(target_manifest, focused_hook)
|
|
838
912
|
projected_content = json.dumps(projected_manifest, ensure_ascii=False, indent=manifest_indentation_width) + line_separator
|
|
839
913
|
return PlannedFile(
|
|
840
914
|
codex_hook_manifest_source_path,
|
|
@@ -849,6 +923,8 @@ def _build_codex_hook_dependency_projection(
|
|
|
849
923
|
config: MaterializerConfig,
|
|
850
924
|
) -> list[PlannedFile]:
|
|
851
925
|
"""Build the reviewed source files required by the target enforcer."""
|
|
926
|
+
if not _has_source_codex_enforcer(config):
|
|
927
|
+
return []
|
|
852
928
|
all_dependencies: list[PlannedFile] = []
|
|
853
929
|
for each_relative_path in codex_hook_dependency_manifest:
|
|
854
930
|
source_path = _validated_source_file(
|
|
@@ -871,6 +947,16 @@ def _build_codex_hook_dependency_projection(
|
|
|
871
947
|
return all_dependencies
|
|
872
948
|
|
|
873
949
|
|
|
950
|
+
def _has_source_codex_enforcer(config: MaterializerConfig) -> bool:
|
|
951
|
+
"""Report whether hooks.json registers the enforcer."""
|
|
952
|
+
source_path = _find_codex_hook_source(config)
|
|
953
|
+
if source_path is None:
|
|
954
|
+
return False
|
|
955
|
+
source_manifest = _read_json_object(source_path, "source Codex hook manifest")
|
|
956
|
+
managed_command = _resolved_codex_enforcer_command(config)
|
|
957
|
+
return _source_codex_enforcer_hook(source_manifest, managed_command) is not None
|
|
958
|
+
|
|
959
|
+
|
|
874
960
|
def discover_agents(config: MaterializerConfig) -> list[ClaudeAgent]:
|
|
875
961
|
"""Discover and parse Markdown agents below the source root.
|
|
876
962
|
|
|
@@ -16,6 +16,7 @@ from durable_post_lint_config.config.constants import (
|
|
|
16
16
|
ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS,
|
|
17
17
|
ALL_POST_ACTIONS,
|
|
18
18
|
ALL_PR_DESCRIPTION_ACTIONS,
|
|
19
|
+
ALL_RELEASE_BODY_MARKERS,
|
|
19
20
|
ALL_REQUIRED_PR_DESCRIPTION_HEADINGS,
|
|
20
21
|
ALL_TITLE_ACTIONS,
|
|
21
22
|
BODY_FILE_ENCODING,
|
|
@@ -35,6 +36,9 @@ from durable_post_lint_config.config.constants import (
|
|
|
35
36
|
MISSING_HEADING_MESSAGE_TEMPLATE,
|
|
36
37
|
PATH_ANCHOR_CHARACTER,
|
|
37
38
|
PATH_SEGMENT_START_CHARACTERS,
|
|
39
|
+
RELEASE_BRANCH_PREFIX,
|
|
40
|
+
REWRITTEN_RELEASE_BODY_CODE,
|
|
41
|
+
REWRITTEN_RELEASE_BODY_MESSAGE,
|
|
38
42
|
TITLE_NOT_ALLOWED_MESSAGE,
|
|
39
43
|
TITLE_REQUIRED_MESSAGE,
|
|
40
44
|
VOLATILE_PATH_CODE,
|
|
@@ -136,9 +140,25 @@ def _title_findings(title: str | None) -> list[DurablePostFinding]:
|
|
|
136
140
|
return [DurablePostFinding(code=INVALID_TITLE_CODE, message=INVALID_TITLE_MESSAGE)]
|
|
137
141
|
|
|
138
142
|
|
|
143
|
+
def _is_release_automation_branch(head_branch: str | None) -> bool:
|
|
144
|
+
return head_branch is not None and head_branch.startswith(RELEASE_BRANCH_PREFIX)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _release_body_findings(body_text: str) -> list[DurablePostFinding]:
|
|
148
|
+
if all(each_marker in body_text for each_marker in ALL_RELEASE_BODY_MARKERS):
|
|
149
|
+
return []
|
|
150
|
+
return [
|
|
151
|
+
DurablePostFinding(
|
|
152
|
+
code=REWRITTEN_RELEASE_BODY_CODE,
|
|
153
|
+
message=REWRITTEN_RELEASE_BODY_MESSAGE,
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
139
158
|
def _body_findings(
|
|
140
159
|
action: str,
|
|
141
160
|
body_text: str | None,
|
|
161
|
+
head_branch: str | None = None,
|
|
142
162
|
) -> list[DurablePostFinding]:
|
|
143
163
|
if body_text is None:
|
|
144
164
|
return []
|
|
@@ -150,7 +170,9 @@ def _body_findings(
|
|
|
150
170
|
message=BODY_MUST_NOT_BE_EMPTY_MESSAGE,
|
|
151
171
|
)
|
|
152
172
|
)
|
|
153
|
-
if
|
|
173
|
+
if _is_release_automation_branch(head_branch):
|
|
174
|
+
all_findings.extend(_release_body_findings(body_text))
|
|
175
|
+
elif action in ALL_PR_DESCRIPTION_ACTIONS:
|
|
154
176
|
all_findings.extend(_description_heading_findings(body_text))
|
|
155
177
|
if find_volatile_path_marker(body_text) is not None:
|
|
156
178
|
all_findings.append(
|
|
@@ -166,10 +188,14 @@ def lint_durable_post(
|
|
|
166
188
|
action: str,
|
|
167
189
|
title: str | None,
|
|
168
190
|
body_text: str | None,
|
|
191
|
+
head_branch: str | None = None,
|
|
169
192
|
) -> tuple[DurablePostFinding, ...]:
|
|
170
193
|
"""Return content findings for one locally valid GitHub post request."""
|
|
171
194
|
_validate_request_shape(action, title, body_text)
|
|
172
|
-
return (
|
|
195
|
+
return (
|
|
196
|
+
*_title_findings(title),
|
|
197
|
+
*_body_findings(action, body_text, head_branch),
|
|
198
|
+
)
|
|
173
199
|
|
|
174
200
|
|
|
175
201
|
def _parse_arguments(all_arguments: Sequence[str]) -> argparse.Namespace:
|
|
@@ -177,6 +203,7 @@ def _parse_arguments(all_arguments: Sequence[str]) -> argparse.Namespace:
|
|
|
177
203
|
parser.add_argument("--action", required=True)
|
|
178
204
|
parser.add_argument("--title")
|
|
179
205
|
parser.add_argument("--body-file", type=Path)
|
|
206
|
+
parser.add_argument("--head-branch")
|
|
180
207
|
return parser.parse_args(list(all_arguments))
|
|
181
208
|
|
|
182
209
|
|
|
@@ -193,6 +220,7 @@ def main(all_arguments: Sequence[str]) -> int:
|
|
|
193
220
|
action=arguments.action,
|
|
194
221
|
title=arguments.title,
|
|
195
222
|
body_text=body_text,
|
|
223
|
+
head_branch=arguments.head_branch,
|
|
196
224
|
)
|
|
197
225
|
except (DurablePostInputError, DurablePostUsageError) as error:
|
|
198
226
|
sys.stderr.write(f"{error}\n")
|
|
@@ -94,3 +94,13 @@ TITLE_NOT_ALLOWED_MESSAGE = "this action does not accept a title"
|
|
|
94
94
|
TITLE_REQUIRED_MESSAGE = "pr-create requires a title"
|
|
95
95
|
VOLATILE_PATH_CODE = "volatile-local-path"
|
|
96
96
|
VOLATILE_PATH_MESSAGE = "body contains a volatile local artifact path"
|
|
97
|
+
RELEASE_BRANCH_PREFIX = "release-please--branches--"
|
|
98
|
+
ALL_RELEASE_BODY_MARKERS: tuple[str, ...] = (
|
|
99
|
+
":robot: I have created a release",
|
|
100
|
+
"This PR was generated with [Release Please]",
|
|
101
|
+
)
|
|
102
|
+
REWRITTEN_RELEASE_BODY_CODE = "rewritten-release-body"
|
|
103
|
+
REWRITTEN_RELEASE_BODY_MESSAGE = (
|
|
104
|
+
"release automation reads this body back to recognize its own merge; "
|
|
105
|
+
"keep the generated text whole"
|
|
106
|
+
)
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from durable_post_lint_config.config.constants import (
|
|
4
4
|
ALL_CONVENTIONAL_TITLE_TYPES,
|
|
5
5
|
ALL_POST_ACTIONS,
|
|
6
|
+
ALL_RELEASE_BODY_MARKERS,
|
|
6
7
|
CONVENTIONAL_TITLE_PATTERN,
|
|
8
|
+
RELEASE_BRANCH_PREFIX,
|
|
7
9
|
)
|
|
8
10
|
|
|
9
11
|
|
|
@@ -23,3 +25,14 @@ def test_action_inventory_covers_pull_request_issue_and_tool_posts() -> None:
|
|
|
23
25
|
def test_title_pattern_uses_every_configured_type() -> None:
|
|
24
26
|
for each_title_type in ALL_CONVENTIONAL_TITLE_TYPES:
|
|
25
27
|
assert CONVENTIONAL_TITLE_PATTERN.fullmatch(f"{each_title_type}: one change")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def should_carry_both_markers_release_automation_writes() -> None:
|
|
31
|
+
assert ALL_RELEASE_BODY_MARKERS == (
|
|
32
|
+
":robot: I have created a release",
|
|
33
|
+
"This PR was generated with [Release Please]",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def should_name_the_branch_prefix_release_automation_pushes() -> None:
|
|
38
|
+
assert RELEASE_BRANCH_PREFIX == "release-please--branches--"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TextIO
|
|
10
|
+
|
|
11
|
+
from local_report_core import PublicationOutcome, publish_local_report
|
|
12
|
+
from local_verification.config import SUCCESS_EXIT_CODE
|
|
13
|
+
from pr_verification.config.constants import GIT_BARE_SUFFIX, GITHUB_API_URL
|
|
14
|
+
from pr_verification.github import GitHubAppAuthenticator, GitHubError
|
|
15
|
+
from pr_verification.model import RepositorySettings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(
|
|
19
|
+
all_arguments: Sequence[str],
|
|
20
|
+
*,
|
|
21
|
+
stdout: TextIO = sys.stdout,
|
|
22
|
+
stderr: TextIO = sys.stderr,
|
|
23
|
+
) -> int:
|
|
24
|
+
"""Publish one local verification report.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
all_arguments: Command line arguments.
|
|
28
|
+
stdout: Output stream for the publication outcome.
|
|
29
|
+
stderr: Output stream for startup and API errors.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The verifier process exit code.
|
|
33
|
+
"""
|
|
34
|
+
parser = _build_parser()
|
|
35
|
+
parsed_arguments = parser.parse_args(list(all_arguments))
|
|
36
|
+
try:
|
|
37
|
+
settings = _load_settings(parsed_arguments)
|
|
38
|
+
outcome = _publish_from_settings(settings, parsed_arguments)
|
|
39
|
+
except (OSError, TypeError, ValueError, GitHubError) as error:
|
|
40
|
+
stderr.write(f"{error}\n")
|
|
41
|
+
return 3
|
|
42
|
+
stdout.write(json.dumps(_outcome_mapping(outcome)) + "\n")
|
|
43
|
+
return SUCCESS_EXIT_CODE
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _publish_from_settings(
|
|
47
|
+
settings: _PublisherSettings, parsed_arguments: argparse.Namespace
|
|
48
|
+
) -> PublicationOutcome:
|
|
49
|
+
repository = RepositorySettings(
|
|
50
|
+
settings.repository, settings.repository + GIT_BARE_SUFFIX
|
|
51
|
+
)
|
|
52
|
+
authenticator = GitHubAppAuthenticator(
|
|
53
|
+
settings.api_url,
|
|
54
|
+
settings.app_id,
|
|
55
|
+
settings.installation_id,
|
|
56
|
+
settings.private_key_path,
|
|
57
|
+
)
|
|
58
|
+
github = authenticator.issue_repository_api(
|
|
59
|
+
repository, should_write_issue_labels=True
|
|
60
|
+
)
|
|
61
|
+
return publish_local_report(
|
|
62
|
+
github,
|
|
63
|
+
repository,
|
|
64
|
+
settings.pull_request_number,
|
|
65
|
+
parsed_arguments.local_repo,
|
|
66
|
+
parsed_arguments.manifest,
|
|
67
|
+
parsed_arguments.report,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _outcome_mapping(outcome: PublicationOutcome) -> dict[str, object]:
|
|
72
|
+
return {
|
|
73
|
+
"status": outcome.status.value,
|
|
74
|
+
"description": outcome.description,
|
|
75
|
+
"publishable": outcome.publishable,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class _PublisherSettings:
|
|
81
|
+
api_url: str
|
|
82
|
+
app_id: int
|
|
83
|
+
installation_id: int
|
|
84
|
+
private_key_path: Path
|
|
85
|
+
repository: str
|
|
86
|
+
pull_request_number: int
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
90
|
+
parser = argparse.ArgumentParser(prog="cde publish-local-report")
|
|
91
|
+
parser.add_argument("--settings", type=Path)
|
|
92
|
+
parser.add_argument("--api-url")
|
|
93
|
+
parser.add_argument("--app-id", type=int)
|
|
94
|
+
parser.add_argument("--installation-id", type=int)
|
|
95
|
+
parser.add_argument("--private-key-path", type=Path)
|
|
96
|
+
parser.add_argument("--repository")
|
|
97
|
+
parser.add_argument("--pull-number", type=int)
|
|
98
|
+
parser.add_argument("--local-repo", type=Path, required=True)
|
|
99
|
+
parser.add_argument("--manifest", type=Path, required=True)
|
|
100
|
+
parser.add_argument("--report", type=Path, required=True)
|
|
101
|
+
return parser
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _load_settings(parsed_arguments: argparse.Namespace) -> _PublisherSettings:
|
|
105
|
+
all_settings: Mapping[str, object] = {}
|
|
106
|
+
if parsed_arguments.settings is not None:
|
|
107
|
+
parsed_settings = json.loads(
|
|
108
|
+
parsed_arguments.settings.read_text(encoding="utf-8")
|
|
109
|
+
)
|
|
110
|
+
if not isinstance(parsed_settings, Mapping):
|
|
111
|
+
raise ValueError("Publisher settings must be an object")
|
|
112
|
+
all_settings = parsed_settings
|
|
113
|
+
return _PublisherSettings(
|
|
114
|
+
_required_setting(
|
|
115
|
+
parsed_arguments.api_url,
|
|
116
|
+
all_settings,
|
|
117
|
+
"api_url",
|
|
118
|
+
GITHUB_API_URL,
|
|
119
|
+
),
|
|
120
|
+
_required_integer_setting(parsed_arguments.app_id, all_settings, "app_id"),
|
|
121
|
+
_required_integer_setting(
|
|
122
|
+
parsed_arguments.installation_id, all_settings, "installation_id"
|
|
123
|
+
),
|
|
124
|
+
_required_path_setting(parsed_arguments.private_key_path, all_settings),
|
|
125
|
+
_required_repository_setting(parsed_arguments.repository, all_settings),
|
|
126
|
+
_required_integer_setting(
|
|
127
|
+
parsed_arguments.pull_number, all_settings, "pull_number"
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _required_setting(
|
|
133
|
+
explicit_setting: object,
|
|
134
|
+
all_settings: Mapping[str, object],
|
|
135
|
+
setting_name: str,
|
|
136
|
+
default_setting: str | None = None,
|
|
137
|
+
) -> str:
|
|
138
|
+
selected_setting = explicit_setting
|
|
139
|
+
if selected_setting is None:
|
|
140
|
+
selected_setting = all_settings.get(setting_name, default_setting)
|
|
141
|
+
if not isinstance(selected_setting, str) or not selected_setting:
|
|
142
|
+
raise TypeError(f"Missing publisher setting: {setting_name}")
|
|
143
|
+
return selected_setting
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _required_repository_setting(
|
|
147
|
+
explicit_repository: object, all_settings: Mapping[str, object]
|
|
148
|
+
) -> str:
|
|
149
|
+
repository_slug = _required_setting(explicit_repository, all_settings, "repository")
|
|
150
|
+
try:
|
|
151
|
+
repository_owner, repository_name = repository_slug.split("/")
|
|
152
|
+
except ValueError as error:
|
|
153
|
+
raise TypeError("Repository must use the owner/name form") from error
|
|
154
|
+
if not repository_owner or not repository_name:
|
|
155
|
+
raise TypeError("Repository must use the owner/name form")
|
|
156
|
+
return repository_slug
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _required_path_setting(
|
|
160
|
+
explicit_path: object, all_settings: Mapping[str, object]
|
|
161
|
+
) -> Path:
|
|
162
|
+
selected_path = explicit_path
|
|
163
|
+
if selected_path is None:
|
|
164
|
+
selected_path = all_settings.get("private_key_path")
|
|
165
|
+
if not isinstance(selected_path, (str, Path)) or not selected_path:
|
|
166
|
+
raise TypeError("Missing publisher setting: private_key_path")
|
|
167
|
+
return Path(selected_path)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _required_integer_setting(
|
|
171
|
+
explicit_setting: object,
|
|
172
|
+
all_settings: Mapping[str, object],
|
|
173
|
+
setting_name: str,
|
|
174
|
+
) -> int:
|
|
175
|
+
selected_setting = explicit_setting
|
|
176
|
+
if selected_setting is None:
|
|
177
|
+
selected_setting = all_settings.get(setting_name)
|
|
178
|
+
if isinstance(selected_setting, bool) or not isinstance(selected_setting, int):
|
|
179
|
+
raise TypeError(f"Missing publisher setting: {setting_name}")
|
|
180
|
+
return selected_setting
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
raise SystemExit(main(sys.argv[1:]))
|