claude-dev-env 2.3.0 → 2.5.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/CLAUDE.md +53 -48
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +3 -4
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/skill-writer-agent.md +84 -0
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +2 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +5 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +107 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +9 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +198 -0
- package/hooks/blocking/code_review_push_gate.py +145 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -155
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
- package/hooks/blocking/test_code_review_push_gate.py +205 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +974 -903
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1014
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +128 -0
- package/hooks/hooks.json +26 -1
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +2 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +257 -25
- package/hooks/validators/run_all_validators.py +223 -19
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +160 -2
- package/hooks/validators/test_run_all_validators.py +140 -68
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -67
- package/rules/durable-post-artifacts.md +7 -0
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +23 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +626 -619
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -421
- package/skills/autoconverge/reference/convergence.md +26 -4
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
- package/skills/autoconverge/workflow/converge.mjs +191 -8
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/skills/team-advisor/SKILL.md +2 -2
- package/scripts/test_invoke_code_review.py +0 -672
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -81,14 +81,15 @@ def _resolved_option_value(
|
|
|
81
81
|
return value_after_option(all_following_tokens, option_index)
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def gated_invocation_directory(
|
|
84
|
+
def gated_invocation_directory(
|
|
85
|
+
all_following_tokens: list[str],
|
|
86
|
+
all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
|
|
87
|
+
) -> tuple[bool, str | None]:
|
|
85
88
|
"""Walk the quote-stripped tokens after a ``git`` word to its subcommand.
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
Whether the subcommand is gated, and its ``-C``/``--work-tree`` directory.
|
|
90
|
+
Args:
|
|
91
|
+
all_following_tokens: Quote-stripped tokens after the ``git`` word.
|
|
92
|
+
all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
|
|
92
93
|
"""
|
|
93
94
|
repo_directory: str | None = None
|
|
94
95
|
work_tree_directory: str | None = None
|
|
@@ -108,7 +109,7 @@ def gated_invocation_directory(all_following_tokens: list[str]) -> tuple[bool, s
|
|
|
108
109
|
if each_token.startswith("-"):
|
|
109
110
|
token_index += 1
|
|
110
111
|
continue
|
|
111
|
-
return each_token.lower() in
|
|
112
|
+
return each_token.lower() in all_gated_subcommands, repo_directory or work_tree_directory
|
|
112
113
|
return False, repo_directory or work_tree_directory
|
|
113
114
|
|
|
114
115
|
|
|
@@ -167,11 +168,16 @@ def _apply_directory_change(
|
|
|
167
168
|
|
|
168
169
|
|
|
169
170
|
def _target_directory_for_match(
|
|
170
|
-
command_text: str,
|
|
171
|
+
command_text: str,
|
|
172
|
+
git_word_match: re.Match[str],
|
|
173
|
+
active_directory: str,
|
|
174
|
+
all_gated_subcommands: frozenset[str],
|
|
171
175
|
) -> str | None:
|
|
172
176
|
"""Resolve the gated directory for one ``git`` word match, or None."""
|
|
173
177
|
all_following_tokens = _following_tokens(command_text, git_word_match.end())
|
|
174
|
-
is_gated, flagged_directory = gated_invocation_directory(
|
|
178
|
+
is_gated, flagged_directory = gated_invocation_directory(
|
|
179
|
+
all_following_tokens, all_gated_subcommands
|
|
180
|
+
)
|
|
175
181
|
if not is_gated:
|
|
176
182
|
return None
|
|
177
183
|
if flagged_directory is None:
|
|
@@ -179,18 +185,22 @@ def _target_directory_for_match(
|
|
|
179
185
|
return resolve_against(active_directory, flagged_directory)
|
|
180
186
|
|
|
181
187
|
|
|
182
|
-
def gated_repo_directories(
|
|
183
|
-
|
|
188
|
+
def gated_repo_directories(
|
|
189
|
+
command_text: str,
|
|
190
|
+
fallback_directory: str,
|
|
191
|
+
all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
|
|
192
|
+
) -> list[str]:
|
|
193
|
+
"""Collect the directories of every gated git call found in a command.
|
|
184
194
|
|
|
185
195
|
Args:
|
|
186
196
|
command_text: The raw command string from the tool payload.
|
|
187
|
-
fallback_directory: The session working directory,
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
fallback_directory: The session working directory, the active directory
|
|
198
|
+
until a directory-change verb or a ``-C`` flag overrides it.
|
|
199
|
+
all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
|
|
190
200
|
|
|
191
201
|
Returns:
|
|
192
|
-
One directory per detected
|
|
193
|
-
|
|
202
|
+
One directory per detected gated invocation, in order; empty when the
|
|
203
|
+
command carries no gated git verb.
|
|
194
204
|
"""
|
|
195
205
|
command_text = collapse_line_continuations(command_text)
|
|
196
206
|
active_directory = fallback_directory
|
|
@@ -199,7 +209,9 @@ def gated_repo_directories(command_text: str, fallback_directory: str) -> list[s
|
|
|
199
209
|
if each_match.group().lower().strip("\"'") in DIRECTORY_CHANGE_VERBS:
|
|
200
210
|
active_directory = _apply_directory_change(command_text, each_match, active_directory)
|
|
201
211
|
continue
|
|
202
|
-
target_directory = _target_directory_for_match(
|
|
212
|
+
target_directory = _target_directory_for_match(
|
|
213
|
+
command_text, each_match, active_directory, all_gated_subcommands
|
|
214
|
+
)
|
|
203
215
|
if target_directory is not None:
|
|
204
216
|
target_directories.append(target_directory)
|
|
205
217
|
return target_directories
|
|
@@ -59,3 +59,38 @@ def test_gated_repo_directories_gates_an_empty_work_tree_against_the_session() -
|
|
|
59
59
|
assert gated_repo_directories("git --work-tree= commit -m x", "/session") == [
|
|
60
60
|
os.path.join("/session", "")
|
|
61
61
|
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_default_gated_subcommands_gates_both_commit_and_push() -> None:
|
|
65
|
+
assert gated_repo_directories("git commit -m x", "/session") == ["/session"]
|
|
66
|
+
assert gated_repo_directories("git push", "/session") == ["/session"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_push_only_subcommands_gate_push_but_not_commit() -> None:
|
|
70
|
+
push_only = frozenset({"push"})
|
|
71
|
+
assert gated_invocation_directory(["push"], push_only)[0] is True
|
|
72
|
+
assert gated_invocation_directory(["commit", "-m", "x"], push_only)[0] is False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_commit_only_subcommands_gate_commit_but_not_push() -> None:
|
|
76
|
+
commit_only = frozenset({"commit"})
|
|
77
|
+
assert gated_invocation_directory(["commit", "-m", "x"], commit_only)[0] is True
|
|
78
|
+
assert gated_invocation_directory(["push"], commit_only)[0] is False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_gated_repo_directories_threads_push_only_subcommands() -> None:
|
|
82
|
+
push_only = frozenset({"push"})
|
|
83
|
+
assert gated_repo_directories("git push", "/session", push_only) == ["/session"]
|
|
84
|
+
assert gated_repo_directories("git commit -m x", "/session", push_only) == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_gated_repo_directories_accepts_the_all_gated_subcommands_keyword() -> None:
|
|
88
|
+
push_only = frozenset({"push"})
|
|
89
|
+
assert gated_repo_directories(
|
|
90
|
+
"git push", "/session", all_gated_subcommands=push_only
|
|
91
|
+
) == ["/session"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_gated_invocation_directory_accepts_the_all_gated_subcommands_keyword() -> None:
|
|
95
|
+
push_only = frozenset({"push"})
|
|
96
|
+
assert gated_invocation_directory(["push"], all_gated_subcommands=push_only)[0] is True
|
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: keep verified-commit exemption messages accurate.
|
|
3
|
-
|
|
4
|
-
The verified-commit gate exempts a branch surface only when every changed path
|
|
5
|
-
matches one of two narrow rules, implemented in ``is_verification_exempt_diff``:
|
|
6
|
-
|
|
7
|
-
1. file extension in ``DOCS_ONLY_EXTENSIONS`` (docs and image extensions), or
|
|
8
|
-
2. a ``.py`` file whose docstring/comment-stripped AST is unchanged.
|
|
9
|
-
|
|
10
|
-
A comment-only change to a non-Python, non-doc file (for example ``.sh``,
|
|
11
|
-
``.json``, ``.yaml``) is therefore NOT exempt: comments are ignored for
|
|
12
|
-
exemption purposes only inside Python files via the AST path. A corrective or
|
|
13
|
-
guard message that claims comment-only or docs-only surfaces are blanket exempt
|
|
14
|
-
overstates the rule and misleads users into expecting such a change to skip
|
|
15
|
-
verification.
|
|
16
|
-
|
|
17
|
-
This hook fires on Write/Edit of any verified-commit constants module and denies
|
|
18
|
-
content whose exemption-claim wording asserts a blanket comment-only or docs-only
|
|
19
|
-
exemption. It guards the message constants at authoring time, before the change
|
|
20
|
-
reaches review.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
import json
|
|
24
|
-
import os
|
|
25
|
-
import re
|
|
26
|
-
import sys
|
|
27
|
-
from pathlib import Path
|
|
28
|
-
|
|
29
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
30
|
-
if _hooks_dir not in sys.path:
|
|
31
|
-
sys.path.insert(0, _hooks_dir)
|
|
32
|
-
|
|
33
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def is_guarded_file(file_path: str) -> bool:
|
|
37
|
-
"""Return True for any verified-commit constants module carrying messages."""
|
|
38
|
-
all_guarded_file_names = frozenset({"verified_commit_constants.py"})
|
|
39
|
-
return os.path.basename(file_path) in all_guarded_file_names
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def join_adjacent_string_literals(written_text: str) -> str:
|
|
43
|
-
"""Collapse a closing quote, inter-literal whitespace, and an opening quote.
|
|
44
|
-
|
|
45
|
-
Write/Edit content carries the message constant as it appears in source: a
|
|
46
|
-
long sentence split across adjacent Python string literals. Between two
|
|
47
|
-
literals sit a closing quote, a newline, indentation, and an opening quote.
|
|
48
|
-
Replacing that run with a single space rejoins the prose so a phrase wrapped
|
|
49
|
-
across a literal boundary — for example ``exempt "`` then ``"automatically``
|
|
50
|
-
— reads as one continuous clause for matching.
|
|
51
|
-
"""
|
|
52
|
-
inter_literal_noise_pattern = re.compile(r"[\"']\s*[\"']")
|
|
53
|
-
return inter_literal_noise_pattern.sub(" ", written_text)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def claims_blanket_comment_exemption(written_text: str) -> bool:
|
|
57
|
-
"""Return True when the text claims comment surfaces are blanket-exempt.
|
|
58
|
-
|
|
59
|
-
Rejoins source-wrapped string literals first, then matches only a genuine
|
|
60
|
-
blanket form in which a comment surface is the grammatical subject of
|
|
61
|
-
"exempt automatically". A comment surface is the bare noun ``comments`` or
|
|
62
|
-
the ``comment-only`` category — never the ``comment`` stem inside an
|
|
63
|
-
unrelated word such as ``commentary`` or the ``comment-stripped`` AST
|
|
64
|
-
qualifier.
|
|
65
|
-
|
|
66
|
-
Two blanket shapes match. The direct shape names a comment surface as the
|
|
67
|
-
immediate subject of "(are|is) exempt automatically" across a bridge that
|
|
68
|
-
crosses no period, semicolon, or comma, so an intervening predicate (for
|
|
69
|
-
example "Comments are handled, and docs are exempt automatically") leaves
|
|
70
|
-
docs, not comments, as the exemption subject and does not match. The
|
|
71
|
-
enumerated shape names a comment item inside an "...-only surfaces are
|
|
72
|
-
exempt automatically" list (for example "Docs-, comment-, and test-only
|
|
73
|
-
surfaces are exempt automatically").
|
|
74
|
-
|
|
75
|
-
An accurate sentence whose true exemption subject is docs, or that qualifies
|
|
76
|
-
comments as the stripped input to the AST comparison, does not match. This
|
|
77
|
-
isolates the overstated form the verified-commit gate does not honor for
|
|
78
|
-
non-Python files.
|
|
79
|
-
"""
|
|
80
|
-
comment_surface = r"comments?-only|comments?\b(?!-)"
|
|
81
|
-
blanket_direct_claim = (
|
|
82
|
-
"(?:" + comment_surface + r")(?:[^.;,]*?\b)?(?:are|is)\s+exempt\s+automatically"
|
|
83
|
-
)
|
|
84
|
-
blanket_enumerated_claim = (
|
|
85
|
-
"(?:" + comment_surface + "|comment-)"
|
|
86
|
-
r"[^.;]*?-only\s+surfaces\s+are\s+exempt\s+automatically"
|
|
87
|
-
)
|
|
88
|
-
blanket_exemption_claim_pattern = re.compile(
|
|
89
|
-
"(?:" + blanket_direct_claim + ")|(?:" + blanket_enumerated_claim + ")",
|
|
90
|
-
re.IGNORECASE,
|
|
91
|
-
)
|
|
92
|
-
joined_text = join_adjacent_string_literals(written_text)
|
|
93
|
-
return bool(blanket_exemption_claim_pattern.search(joined_text))
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def build_corrective_message() -> str:
|
|
97
|
-
"""Return the deny reason explaining the real, narrower exemption rules."""
|
|
98
|
-
accurate_exemption_phrasing = (
|
|
99
|
-
"Docs and images are exempt by extension, and Python files whose "
|
|
100
|
-
"docstring- and comment-stripped AST is unchanged; a comment-only "
|
|
101
|
-
"change to a non-Python file still needs a verdict."
|
|
102
|
-
)
|
|
103
|
-
return (
|
|
104
|
-
"BLOCKED [verified-commit-message-accuracy]: this exemption message "
|
|
105
|
-
"claims comment-only surfaces are exempt automatically, but the "
|
|
106
|
-
"verified-commit gate exempts comments only inside Python files (via the "
|
|
107
|
-
"docstring/comment stripped AST path). A comment-only change to a "
|
|
108
|
-
"non-Python file is NOT exempt and still needs a verifier verdict, so "
|
|
109
|
-
"the blanket wording misleads users.\n\nDescribe the real exemption "
|
|
110
|
-
"rules instead, for example:\n " + accurate_exemption_phrasing
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def extract_written_text(all_written_fields: dict[str, str]) -> str:
|
|
115
|
-
"""Return the Write ``content`` and Edit ``new_string`` joined for scanning."""
|
|
116
|
-
return (
|
|
117
|
-
all_written_fields.get("content", "")
|
|
118
|
-
+ "\n"
|
|
119
|
-
+ all_written_fields.get("new_string", "")
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def main() -> None:
|
|
124
|
-
all_write_edit_tools = frozenset({"Write", "Edit"})
|
|
125
|
-
try:
|
|
126
|
-
hook_input = json.load(sys.stdin)
|
|
127
|
-
except json.JSONDecodeError:
|
|
128
|
-
sys.exit(0)
|
|
129
|
-
|
|
130
|
-
tool_name = hook_input.get("tool_name", "")
|
|
131
|
-
if tool_name not in all_write_edit_tools:
|
|
132
|
-
sys.exit(0)
|
|
133
|
-
|
|
134
|
-
tool_input = hook_input.get("tool_input", {})
|
|
135
|
-
file_path = tool_input.get("file_path", "")
|
|
136
|
-
if not file_path:
|
|
137
|
-
sys.exit(0)
|
|
138
|
-
|
|
139
|
-
if not is_guarded_file(file_path):
|
|
140
|
-
sys.exit(0)
|
|
141
|
-
|
|
142
|
-
written_text = extract_written_text(tool_input)
|
|
143
|
-
if not claims_blanket_comment_exemption(written_text):
|
|
144
|
-
sys.exit(0)
|
|
145
|
-
|
|
146
|
-
corrective_message = build_corrective_message()
|
|
147
|
-
deny_response = {
|
|
148
|
-
"hookSpecificOutput": {
|
|
149
|
-
"hookEventName": "PreToolUse",
|
|
150
|
-
"permissionDecision": "deny",
|
|
151
|
-
"permissionDecisionReason": corrective_message,
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
log_hook_block(
|
|
155
|
-
calling_hook_name="verified_commit_message_accuracy_blocker.py",
|
|
156
|
-
hook_event="PreToolUse",
|
|
157
|
-
block_reason=corrective_message,
|
|
158
|
-
tool_name=tool_name,
|
|
159
|
-
offending_input_preview=file_path,
|
|
160
|
-
)
|
|
161
|
-
print(json.dumps(deny_response))
|
|
162
|
-
sys.stdout.flush()
|
|
163
|
-
sys.exit(0)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if __name__ == "__main__":
|
|
167
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse hook: keep verified-commit exemption messages accurate.
|
|
3
|
+
|
|
4
|
+
The verified-commit gate exempts a branch surface only when every changed path
|
|
5
|
+
matches one of two narrow rules, implemented in ``is_verification_exempt_diff``:
|
|
6
|
+
|
|
7
|
+
1. file extension in ``DOCS_ONLY_EXTENSIONS`` (docs and image extensions), or
|
|
8
|
+
2. a ``.py`` file whose docstring/comment-stripped AST is unchanged.
|
|
9
|
+
|
|
10
|
+
A comment-only change to a non-Python, non-doc file (for example ``.sh``,
|
|
11
|
+
``.json``, ``.yaml``) is therefore NOT exempt: comments are ignored for
|
|
12
|
+
exemption purposes only inside Python files via the AST path. A corrective or
|
|
13
|
+
guard message that claims comment-only or docs-only surfaces are blanket exempt
|
|
14
|
+
overstates the rule and misleads users into expecting such a change to skip
|
|
15
|
+
verification.
|
|
16
|
+
|
|
17
|
+
This hook fires on Write/Edit of any verified-commit constants module and denies
|
|
18
|
+
content whose exemption-claim wording asserts a blanket comment-only or docs-only
|
|
19
|
+
exemption. It guards the message constants at authoring time, before the change
|
|
20
|
+
reaches review.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
30
|
+
if _hooks_dir not in sys.path:
|
|
31
|
+
sys.path.insert(0, _hooks_dir)
|
|
32
|
+
|
|
33
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_guarded_file(file_path: str) -> bool:
|
|
37
|
+
"""Return True for any verified-commit constants module carrying messages."""
|
|
38
|
+
all_guarded_file_names = frozenset({"verified_commit_constants.py"})
|
|
39
|
+
return os.path.basename(file_path) in all_guarded_file_names
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def join_adjacent_string_literals(written_text: str) -> str:
|
|
43
|
+
"""Collapse a closing quote, inter-literal whitespace, and an opening quote.
|
|
44
|
+
|
|
45
|
+
Write/Edit content carries the message constant as it appears in source: a
|
|
46
|
+
long sentence split across adjacent Python string literals. Between two
|
|
47
|
+
literals sit a closing quote, a newline, indentation, and an opening quote.
|
|
48
|
+
Replacing that run with a single space rejoins the prose so a phrase wrapped
|
|
49
|
+
across a literal boundary — for example ``exempt "`` then ``"automatically``
|
|
50
|
+
— reads as one continuous clause for matching.
|
|
51
|
+
"""
|
|
52
|
+
inter_literal_noise_pattern = re.compile(r"[\"']\s*[\"']")
|
|
53
|
+
return inter_literal_noise_pattern.sub(" ", written_text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def claims_blanket_comment_exemption(written_text: str) -> bool:
|
|
57
|
+
"""Return True when the text claims comment surfaces are blanket-exempt.
|
|
58
|
+
|
|
59
|
+
Rejoins source-wrapped string literals first, then matches only a genuine
|
|
60
|
+
blanket form in which a comment surface is the grammatical subject of
|
|
61
|
+
"exempt automatically". A comment surface is the bare noun ``comments`` or
|
|
62
|
+
the ``comment-only`` category — never the ``comment`` stem inside an
|
|
63
|
+
unrelated word such as ``commentary`` or the ``comment-stripped`` AST
|
|
64
|
+
qualifier.
|
|
65
|
+
|
|
66
|
+
Two blanket shapes match. The direct shape names a comment surface as the
|
|
67
|
+
immediate subject of "(are|is) exempt automatically" across a bridge that
|
|
68
|
+
crosses no period, semicolon, or comma, so an intervening predicate (for
|
|
69
|
+
example "Comments are handled, and docs are exempt automatically") leaves
|
|
70
|
+
docs, not comments, as the exemption subject and does not match. The
|
|
71
|
+
enumerated shape names a comment item inside an "...-only surfaces are
|
|
72
|
+
exempt automatically" list (for example "Docs-, comment-, and test-only
|
|
73
|
+
surfaces are exempt automatically").
|
|
74
|
+
|
|
75
|
+
An accurate sentence whose true exemption subject is docs, or that qualifies
|
|
76
|
+
comments as the stripped input to the AST comparison, does not match. This
|
|
77
|
+
isolates the overstated form the verified-commit gate does not honor for
|
|
78
|
+
non-Python files.
|
|
79
|
+
"""
|
|
80
|
+
comment_surface = r"comments?-only|comments?\b(?!-)"
|
|
81
|
+
blanket_direct_claim = (
|
|
82
|
+
"(?:" + comment_surface + r")(?:[^.;,]*?\b)?(?:are|is)\s+exempt\s+automatically"
|
|
83
|
+
)
|
|
84
|
+
blanket_enumerated_claim = (
|
|
85
|
+
"(?:" + comment_surface + "|comment-)"
|
|
86
|
+
r"[^.;]*?-only\s+surfaces\s+are\s+exempt\s+automatically"
|
|
87
|
+
)
|
|
88
|
+
blanket_exemption_claim_pattern = re.compile(
|
|
89
|
+
"(?:" + blanket_direct_claim + ")|(?:" + blanket_enumerated_claim + ")",
|
|
90
|
+
re.IGNORECASE,
|
|
91
|
+
)
|
|
92
|
+
joined_text = join_adjacent_string_literals(written_text)
|
|
93
|
+
return bool(blanket_exemption_claim_pattern.search(joined_text))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_corrective_message() -> str:
|
|
97
|
+
"""Return the deny reason explaining the real, narrower exemption rules."""
|
|
98
|
+
accurate_exemption_phrasing = (
|
|
99
|
+
"Docs and images are exempt by extension, and Python files whose "
|
|
100
|
+
"docstring- and comment-stripped AST is unchanged; a comment-only "
|
|
101
|
+
"change to a non-Python file still needs a verdict."
|
|
102
|
+
)
|
|
103
|
+
return (
|
|
104
|
+
"BLOCKED [verified-commit-message-accuracy]: this exemption message "
|
|
105
|
+
"claims comment-only surfaces are exempt automatically, but the "
|
|
106
|
+
"verified-commit gate exempts comments only inside Python files (via the "
|
|
107
|
+
"docstring/comment stripped AST path). A comment-only change to a "
|
|
108
|
+
"non-Python file is NOT exempt and still needs a verifier verdict, so "
|
|
109
|
+
"the blanket wording misleads users.\n\nDescribe the real exemption "
|
|
110
|
+
"rules instead, for example:\n " + accurate_exemption_phrasing
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extract_written_text(all_written_fields: dict[str, str]) -> str:
|
|
115
|
+
"""Return the Write ``content`` and Edit ``new_string`` joined for scanning."""
|
|
116
|
+
return (
|
|
117
|
+
all_written_fields.get("content", "")
|
|
118
|
+
+ "\n"
|
|
119
|
+
+ all_written_fields.get("new_string", "")
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main() -> None:
|
|
124
|
+
all_write_edit_tools = frozenset({"Write", "Edit"})
|
|
125
|
+
try:
|
|
126
|
+
hook_input = json.load(sys.stdin)
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
tool_name = hook_input.get("tool_name", "")
|
|
131
|
+
if tool_name not in all_write_edit_tools:
|
|
132
|
+
sys.exit(0)
|
|
133
|
+
|
|
134
|
+
tool_input = hook_input.get("tool_input", {})
|
|
135
|
+
file_path = tool_input.get("file_path", "")
|
|
136
|
+
if not file_path:
|
|
137
|
+
sys.exit(0)
|
|
138
|
+
|
|
139
|
+
if not is_guarded_file(file_path):
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
|
|
142
|
+
written_text = extract_written_text(tool_input)
|
|
143
|
+
if not claims_blanket_comment_exemption(written_text):
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
|
|
146
|
+
corrective_message = build_corrective_message()
|
|
147
|
+
deny_response = {
|
|
148
|
+
"hookSpecificOutput": {
|
|
149
|
+
"hookEventName": "PreToolUse",
|
|
150
|
+
"permissionDecision": "deny",
|
|
151
|
+
"permissionDecisionReason": corrective_message,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
log_hook_block(
|
|
155
|
+
calling_hook_name="verified_commit_message_accuracy_blocker.py",
|
|
156
|
+
hook_event="PreToolUse",
|
|
157
|
+
block_reason=corrective_message,
|
|
158
|
+
tool_name=tool_name,
|
|
159
|
+
offending_input_preview=file_path,
|
|
160
|
+
)
|
|
161
|
+
print(json.dumps(deny_response))
|
|
162
|
+
sys.stdout.flush()
|
|
163
|
+
sys.exit(0)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
main()
|