claude-dev-env 2.11.0 → 2.13.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/_shared/advisor/reference/sol-rung.md +5 -2
- package/_shared/advisor/reference/warm-up.md +1 -0
- package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/agents/CLAUDE.md +1 -2
- package/agents/pr-description-writer.md +11 -81
- package/bin/install.mjs +48 -9
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +2 -4
- package/commands/CLAUDE.md +1 -0
- package/commands/sr-loop.md +52 -0
- package/docs/CLAUDE.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -27
- package/hooks/blocking/config/CLAUDE.md +1 -13
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/CLAUDE.md +3 -3
- package/hooks/diagnostic/hook_log_extractor.py +2 -36
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
- package/hooks/git-hooks/pre_push.py +3 -85
- package/hooks/git-hooks/test_pre_push.py +0 -130
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/CLAUDE.md +0 -9
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/lifecycle/CLAUDE.md +1 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +0 -2
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/CLAUDE.md +0 -1
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/settings.json +1 -10
- package/skills/CLAUDE.md +8 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -1
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/comments/SKILL.md +65 -0
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/imagegen/SKILL.md +21 -0
- package/skills/imagegen/scripts/config/__init__.py +1 -0
- package/skills/imagegen/scripts/config/constants.py +28 -0
- package/skills/imagegen/scripts/imagegen.py +45 -0
- package/skills/imagegen/scripts/imagegen_core.py +528 -0
- package/skills/imagegen/scripts/test_imagegen.py +204 -0
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +4 -4
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/reviews/SKILL.md +85 -0
- package/skills/small-cl/SKILL.md +52 -0
- package/skills/usage-pause/SKILL.md +1 -1
- package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,167 +0,0 @@
|
|
|
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,280 +0,0 @@
|
|
|
1
|
-
"""SubagentStop hook: mint a commit-gate verdict when code-verifier finishes.
|
|
2
|
-
|
|
3
|
-
Only this hook writes verdict files — the main session is denied writes to
|
|
4
|
-
the verdict directory, so a session cannot fabricate a passing verdict. The
|
|
5
|
-
SubagentStop payload names the stopping subagent's own transcript
|
|
6
|
-
(``agent_transcript_path``), which sits beside a harness-written
|
|
7
|
-
``agent-<id>.meta.json`` sidecar naming the spawning ``agentType``. The hook
|
|
8
|
-
reads that type from the sidecar, so it resolves identically in interactive,
|
|
9
|
-
background, and worktree-switched sessions; only the harness writes the
|
|
10
|
-
sidecar, so the type it records cannot be forged. When that type is
|
|
11
|
-
``code-verifier``, the hook pulls the verdict block out of the agent's own
|
|
12
|
-
transcript (``agent_transcript_path``), recomputes the live change-surface
|
|
13
|
-
hash for the session repository, and writes the verdict bound to that hash.
|
|
14
|
-
The companion ``verified_commit_gate.py`` (PreToolUse) then allows ``git
|
|
15
|
-
commit`` / ``git push`` only while the work tree still matches the verified
|
|
16
|
-
state.
|
|
17
|
-
|
|
18
|
-
The verifier's final message must end with a fenced block::
|
|
19
|
-
|
|
20
|
-
```verdict
|
|
21
|
-
{"all_pass": true, "findings": []}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
A missing or unparseable block mints nothing, which leaves the gate closed.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
from __future__ import annotations
|
|
28
|
-
|
|
29
|
-
import json
|
|
30
|
-
import re
|
|
31
|
-
import sys
|
|
32
|
-
from pathlib import Path
|
|
33
|
-
|
|
34
|
-
blocking_directory = str(Path(__file__).resolve().parent)
|
|
35
|
-
if blocking_directory not in sys.path:
|
|
36
|
-
sys.path.insert(0, blocking_directory)
|
|
37
|
-
|
|
38
|
-
from verified_commit_config_bootstrap import ( # noqa: E402
|
|
39
|
-
register_verified_commit_constants,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
register_verified_commit_constants()
|
|
43
|
-
|
|
44
|
-
from config.verified_commit_constants import (
|
|
45
|
-
MINTING_AGENT_TYPE,
|
|
46
|
-
VERDICT_KEY_MANIFEST_SHA256,
|
|
47
|
-
)
|
|
48
|
-
from verification_verdict_store import (
|
|
49
|
-
branch_surface_manifest,
|
|
50
|
-
empty_surface_hash,
|
|
51
|
-
manifest_sha256,
|
|
52
|
-
resolve_merge_base,
|
|
53
|
-
resolve_repo_root,
|
|
54
|
-
write_verdict,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def assistant_text_blocks(transcript_path: str) -> list[str]:
|
|
59
|
-
"""Collect every assistant text block from a transcript JSONL file.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
transcript_path: Path to the subagent's transcript.
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
The text of each assistant content block, in transcript order;
|
|
66
|
-
empty when the file is missing or holds no assistant text.
|
|
67
|
-
"""
|
|
68
|
-
collected_blocks: list[str] = []
|
|
69
|
-
try:
|
|
70
|
-
transcript_lines = (
|
|
71
|
-
Path(transcript_path).read_text(encoding="utf-8", errors="replace").splitlines()
|
|
72
|
-
)
|
|
73
|
-
except OSError:
|
|
74
|
-
return collected_blocks
|
|
75
|
-
for each_line in transcript_lines:
|
|
76
|
-
try:
|
|
77
|
-
transcript_entry = json.loads(each_line)
|
|
78
|
-
except json.JSONDecodeError:
|
|
79
|
-
continue
|
|
80
|
-
if not isinstance(transcript_entry, dict):
|
|
81
|
-
continue
|
|
82
|
-
if transcript_entry.get("type") != "assistant":
|
|
83
|
-
continue
|
|
84
|
-
message_body = transcript_entry.get("message")
|
|
85
|
-
if not isinstance(message_body, dict):
|
|
86
|
-
continue
|
|
87
|
-
content_blocks = message_body.get("content")
|
|
88
|
-
if not isinstance(content_blocks, list):
|
|
89
|
-
continue
|
|
90
|
-
for each_block in content_blocks:
|
|
91
|
-
if isinstance(each_block, dict) and each_block.get("type") == "text":
|
|
92
|
-
block_text = each_block.get("text")
|
|
93
|
-
if isinstance(block_text, str):
|
|
94
|
-
collected_blocks.append(block_text)
|
|
95
|
-
return collected_blocks
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def last_verdict_in_blocks(all_text_blocks: list[str]) -> dict | None:
|
|
99
|
-
"""Extract the final verdict fence from assistant text blocks.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
all_text_blocks: Assistant text blocks in transcript order.
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
The parsed verdict mapping carrying a boolean ``all_pass`` and a
|
|
106
|
-
list ``findings``, or None when no block holds a wellformed fence.
|
|
107
|
-
"""
|
|
108
|
-
verdict_fence_pattern = re.compile(r"```verdict\s*\n(.*?)```", re.DOTALL)
|
|
109
|
-
fence_bodies: list[str] = []
|
|
110
|
-
for each_block in all_text_blocks:
|
|
111
|
-
fence_bodies.extend(verdict_fence_pattern.findall(each_block))
|
|
112
|
-
for each_fence_body in reversed(fence_bodies):
|
|
113
|
-
try:
|
|
114
|
-
verdict_record = json.loads(each_fence_body)
|
|
115
|
-
except json.JSONDecodeError:
|
|
116
|
-
continue
|
|
117
|
-
if not isinstance(verdict_record, dict):
|
|
118
|
-
continue
|
|
119
|
-
if not isinstance(verdict_record.get("all_pass"), bool):
|
|
120
|
-
continue
|
|
121
|
-
if not isinstance(verdict_record.get("findings"), list):
|
|
122
|
-
continue
|
|
123
|
-
return verdict_record
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
|
|
128
|
-
"""Read the spawning agentType from a subagent transcript's sidecar.
|
|
129
|
-
|
|
130
|
-
Each subagent transcript ``agent-<id>.jsonl`` sits beside a harness-written
|
|
131
|
-
``agent-<id>.meta.json`` naming the spawning ``agentType``. Reading the type
|
|
132
|
-
from this sidecar binds it to the stopping subagent itself, so it resolves
|
|
133
|
-
identically in interactive, background, and worktree-switched sessions and
|
|
134
|
-
needs no parent-transcript scan or flush retry. Only the harness writes this
|
|
135
|
-
sidecar, so a session cannot forge the type it reports.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
agent_transcript_path: The stopping subagent's own transcript path from
|
|
139
|
-
the SubagentStop payload.
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
The recorded ``agentType``, or None when the path is empty, the sidecar
|
|
143
|
-
is absent, unreadable, or unparseable, it does not hold a JSON object,
|
|
144
|
-
or it names no string ``agentType``.
|
|
145
|
-
"""
|
|
146
|
-
if not agent_transcript_path:
|
|
147
|
-
return None
|
|
148
|
-
transcript_file = Path(agent_transcript_path)
|
|
149
|
-
sidecar_file = transcript_file.with_name(f"{transcript_file.stem}.meta.json")
|
|
150
|
-
try:
|
|
151
|
-
sidecar_record = json.loads(sidecar_file.read_text(encoding="utf-8"))
|
|
152
|
-
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
153
|
-
return None
|
|
154
|
-
if not isinstance(sidecar_record, dict):
|
|
155
|
-
return None
|
|
156
|
-
recorded_type = sidecar_record.get("agentType")
|
|
157
|
-
return recorded_type if isinstance(recorded_type, str) else None
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def resolved_subagent_type(subagent_stop_payload: dict) -> str | None:
|
|
161
|
-
"""Recover the spawning agent type for a SubagentStop payload.
|
|
162
|
-
|
|
163
|
-
The stopping subagent's own transcript (``agent_transcript_path``) sits
|
|
164
|
-
beside a harness-written ``agent-<id>.meta.json`` sidecar naming its
|
|
165
|
-
``agentType``. The type comes solely from that sidecar, which only the
|
|
166
|
-
harness writes — the main session can neither write the sidecar nor forge
|
|
167
|
-
the agentType it records. A verdict fence in the transcript is never used to
|
|
168
|
-
infer the type, because the main session controls the prompt of any
|
|
169
|
-
Agent-tool subagent it spawns and could instruct one to print such a fence.
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
subagent_stop_payload: The SubagentStop hook payload.
|
|
173
|
-
|
|
174
|
-
Returns:
|
|
175
|
-
The agent type the sidecar records, or None when the
|
|
176
|
-
``agent_transcript_path`` is empty, the sidecar is absent, unreadable,
|
|
177
|
-
or unparseable, it does not hold a JSON object, or it names no string
|
|
178
|
-
``agentType``.
|
|
179
|
-
"""
|
|
180
|
-
return _agent_type_from_meta_sidecar(
|
|
181
|
-
subagent_stop_payload.get("agent_transcript_path", "")
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _attested_or_recomputed_hash(verdict_record: dict, repo_root: str) -> str | None:
|
|
186
|
-
"""Choose the surface hash the minted verdict binds to.
|
|
187
|
-
|
|
188
|
-
A code-verifier that verifies a work tree other than the stop event's cwd
|
|
189
|
-
attests the surface it checked by emitting ``manifest_sha256`` in its
|
|
190
|
-
verdict fence (computed against the verified work tree via the
|
|
191
|
-
``--manifest-hash`` CLI). Binding the minted verdict to that attested hash
|
|
192
|
-
keeps the verdict tied to the code actually verified rather than the
|
|
193
|
-
subagent's cwd, so a verdict earned for one work tree clears a commit in a
|
|
194
|
-
sibling work tree of the same surface. When the fence attests no hash, the
|
|
195
|
-
minter recomputes one from the cwd work tree, which is correct whenever the
|
|
196
|
-
verifier ran in the work tree it verified.
|
|
197
|
-
|
|
198
|
-
Returns None (mints nothing) in two empty-surface cases:
|
|
199
|
-
|
|
200
|
-
- The attested hash equals ``empty_surface_hash()`` — the verifier called
|
|
201
|
-
the store CLI on a wrong (empty) work tree and the hash it got back
|
|
202
|
-
represents nothing.
|
|
203
|
-
- The recompute branch produces an empty manifest — the cwd work tree also
|
|
204
|
-
has no changed or untracked files versus the merge base.
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
verdict_record: The parsed verdict fence from the verifier transcript.
|
|
208
|
-
repo_root: The work-tree root resolved from the stop event's cwd, used
|
|
209
|
-
for the recompute fallback.
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
The attested ``manifest_sha256`` when the fence carries a non-empty
|
|
213
|
-
string one that is not the empty-surface sentinel; the cwd work tree's
|
|
214
|
-
recomputed surface hash when the fence attests nothing and the surface
|
|
215
|
-
is non-empty; or None when the attested hash is the empty-surface
|
|
216
|
-
sentinel, the surface manifest is empty, or no upstream base resolves.
|
|
217
|
-
"""
|
|
218
|
-
attested_manifest_sha256 = verdict_record.get(VERDICT_KEY_MANIFEST_SHA256)
|
|
219
|
-
if isinstance(attested_manifest_sha256, str) and attested_manifest_sha256:
|
|
220
|
-
if attested_manifest_sha256 == empty_surface_hash():
|
|
221
|
-
return None
|
|
222
|
-
return attested_manifest_sha256
|
|
223
|
-
merge_base_sha = resolve_merge_base(repo_root)
|
|
224
|
-
if merge_base_sha is None:
|
|
225
|
-
return None
|
|
226
|
-
surface_manifest_text = branch_surface_manifest(repo_root, merge_base_sha)
|
|
227
|
-
if not surface_manifest_text:
|
|
228
|
-
return None
|
|
229
|
-
return manifest_sha256(surface_manifest_text)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def mint_for_payload(subagent_stop_payload: dict) -> Path | None:
|
|
233
|
-
"""Mint a verdict file for a code-verifier stop event.
|
|
234
|
-
|
|
235
|
-
Args:
|
|
236
|
-
subagent_stop_payload: The SubagentStop hook payload.
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
The verdict file path when minted; None when the payload is not a
|
|
240
|
-
code-verifier stop, the transcript holds no verdict, the cwd is not a
|
|
241
|
-
work tree, or — for a verdict that attests no ``manifest_sha256`` of
|
|
242
|
-
its own — that work tree has no upstream base to recompute the surface
|
|
243
|
-
hash from.
|
|
244
|
-
"""
|
|
245
|
-
if resolved_subagent_type(subagent_stop_payload) != MINTING_AGENT_TYPE:
|
|
246
|
-
return None
|
|
247
|
-
agent_transcript_path = subagent_stop_payload.get("agent_transcript_path", "")
|
|
248
|
-
if not agent_transcript_path:
|
|
249
|
-
return None
|
|
250
|
-
verdict_record = last_verdict_in_blocks(assistant_text_blocks(agent_transcript_path))
|
|
251
|
-
if verdict_record is None:
|
|
252
|
-
return None
|
|
253
|
-
repo_root = resolve_repo_root(subagent_stop_payload.get("cwd", "."))
|
|
254
|
-
if repo_root is None:
|
|
255
|
-
return None
|
|
256
|
-
bound_manifest_sha256 = _attested_or_recomputed_hash(verdict_record, repo_root)
|
|
257
|
-
if bound_manifest_sha256 is None:
|
|
258
|
-
return None
|
|
259
|
-
return write_verdict(
|
|
260
|
-
repo_root,
|
|
261
|
-
bound_manifest_sha256,
|
|
262
|
-
verdict_record["all_pass"],
|
|
263
|
-
verdict_record["findings"],
|
|
264
|
-
str(subagent_stop_payload.get("agent_id", "")),
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def main() -> None:
|
|
269
|
-
"""Read the SubagentStop payload and mint a verdict when one applies."""
|
|
270
|
-
try:
|
|
271
|
-
subagent_stop_payload = json.load(sys.stdin)
|
|
272
|
-
except json.JSONDecodeError:
|
|
273
|
-
return
|
|
274
|
-
if not isinstance(subagent_stop_payload, dict):
|
|
275
|
-
return
|
|
276
|
-
mint_for_payload(subagent_stop_payload)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if __name__ == "__main__":
|
|
280
|
-
main()
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"""Configuration constants for the code_verifier_spawn_preflight_gate hook.
|
|
2
|
-
|
|
3
|
-
The gate denies an ``Agent`` or ``Task`` spawn whose ``subagent_type`` is
|
|
4
|
-
``code-verifier`` when the branch carries a merge conflict against its base
|
|
5
|
-
ref, a CODE_RULES violation on a line added in the working tree since the merge
|
|
6
|
-
base (committed on the branch or uncommitted), or a CODE_RULES engine
|
|
7
|
-
import/load failure. It runs those pre-flight checks before the expensive
|
|
8
|
-
verification spawn and addresses its deny reason to the spawning agent so that
|
|
9
|
-
agent fixes the named issues and re-spawns. Environmental git failures stay
|
|
10
|
-
fail-open; engine import/load failure is fail-closed. Every literal the hook
|
|
11
|
-
body reads lives here; ``AGENT_TOOL_NAME`` comes from
|
|
12
|
-
``pr_converge_bugteam_enforcer_constants`` and joins ``TASK_TOOL_NAME`` in
|
|
13
|
-
``ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES`` so both spawn tool names share one source.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
from hooks_constants.pr_converge_bugteam_enforcer_constants import AGENT_TOOL_NAME
|
|
21
|
-
|
|
22
|
-
CODE_VERIFIER_SUBAGENT_TYPE: str = "code-verifier"
|
|
23
|
-
TASK_TOOL_NAME: str = "Task"
|
|
24
|
-
ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES: frozenset[str] = frozenset(
|
|
25
|
-
{AGENT_TOOL_NAME, TASK_TOOL_NAME}
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
ALL_MERGE_TREE_COMMAND_FLAGS: tuple[str, ...] = (
|
|
29
|
-
"merge-tree",
|
|
30
|
-
"--write-tree",
|
|
31
|
-
"--name-only",
|
|
32
|
-
)
|
|
33
|
-
MERGE_TREE_CONFLICT_EXIT_CODE: int = 1
|
|
34
|
-
MERGE_TREE_CLEAN_EXIT_CODE: int = 0
|
|
35
|
-
MERGE_TREE_TIMEOUT_SECONDS: int = 30
|
|
36
|
-
|
|
37
|
-
ALL_MERGE_HEAD_PROBE_FLAGS: tuple[str, ...] = ("rev-parse", "--verify", "--quiet", "MERGE_HEAD")
|
|
38
|
-
ALL_UNMERGED_PATHS_DIFF_FLAGS: tuple[str, ...] = ("diff", "--name-only", "--diff-filter=U")
|
|
39
|
-
|
|
40
|
-
ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
|
|
41
|
-
"-c",
|
|
42
|
-
"core.quotePath=false",
|
|
43
|
-
"diff",
|
|
44
|
-
"--name-only",
|
|
45
|
-
"--no-renames",
|
|
46
|
-
)
|
|
47
|
-
ALL_UNIFIED_ZERO_DIFF_FLAGS: tuple[str, ...] = ("diff", "--unified=0")
|
|
48
|
-
|
|
49
|
-
DENY_REASON_LEAD: str = (
|
|
50
|
-
"BLOCKED [code-verifier-spawn-preflight]: a code-verifier spawn "
|
|
51
|
-
"(Agent or Task tool, subagent_type code-verifier) is blocked because "
|
|
52
|
-
"the branch is not in a committable state. "
|
|
53
|
-
"Fix these, then re-spawn the code-verifier:"
|
|
54
|
-
)
|
|
55
|
-
MERGE_CONFLICT_SECTION_HEADER: str = "Merge conflicts vs {base_ref}:"
|
|
56
|
-
CODE_RULES_SECTION_HEADER: str = "CODE_RULES violations on changed lines:"
|
|
57
|
-
ENGINE_LOAD_FAILURE_SECTION: str = (
|
|
58
|
-
"CODE_RULES engine failed to load:\n"
|
|
59
|
-
" The CODE_RULES validate_content engine could not be imported or loaded "
|
|
60
|
-
"(package import/load failure). Repair the enforcer so load_validate_content "
|
|
61
|
-
"succeeds, then re-spawn code-verifier."
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
GATE_SCRIPTS_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts"
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"""Command-parse patterns for the convergence gate's repo binding.
|
|
2
|
-
|
|
3
|
-
The convergence gate keys its evidence lookup to the PR that the
|
|
4
|
-
``gh pr ready`` command names. These patterns pull an explicit owner/repo
|
|
5
|
-
out of the command — and, for a full PR URL, the PR number too — so the
|
|
6
|
-
gate binds to the repository the command targets rather than the
|
|
7
|
-
session working directory's repository when the two differ.
|
|
8
|
-
|
|
9
|
-
The owner/repo and URL patterns run only over the ``gh pr ready`` segment,
|
|
10
|
-
clipped at the first command separator, so a flag or URL that belongs to a
|
|
11
|
-
chained command does not bind the gate to the wrong PR::
|
|
12
|
-
|
|
13
|
-
gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo
|
|
14
|
-
^^^^^^^^^^^^^^^^ clipped here ^
|
|
15
|
-
-> ready segment scanned; the chained --repo is out of scope
|
|
16
|
-
|
|
17
|
-
gh pr ready 161 --repo sample-owner/target-repo
|
|
18
|
-
^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^
|
|
19
|
-
flag owner repo -> REPO_OVERRIDE_FLAG_PATTERN
|
|
20
|
-
gh pr ready https://github.com/sample-owner/target-repo/pull/161
|
|
21
|
-
^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^
|
|
22
|
-
owner repo number
|
|
23
|
-
-> PR_URL_OWNER_REPO_NUMBER_PATTERN
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
GH_PR_READY_ANCHOR_PATTERN: str = r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)"
|
|
27
|
-
|
|
28
|
-
COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n|(?<![>&])&(?![>&])"
|
|
29
|
-
|
|
30
|
-
BASH_LINE_CONTINUATION_PATTERN: str = r"\\\r?\n[ \t]*"
|
|
31
|
-
|
|
32
|
-
PR_URL_OWNER_REPO_NUMBER_PATTERN: str = (
|
|
33
|
-
r"https?://[^/]+/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
REPO_OVERRIDE_FLAG_PATTERN: str = (
|
|
37
|
-
r"(?:^|\s)(?:--repo(?:=|\s+)|-R(?:=|\s+)?)"
|
|
38
|
-
r"(?:https?://[^/\s]+/|git@[^:\s]+:|(?:[\w.-]+/)*)"
|
|
39
|
-
r"(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)"
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
ALL_GH_PR_VIEW_NUMBER_COMMAND: tuple[str, ...] = (
|
|
43
|
-
"gh",
|
|
44
|
-
"pr",
|
|
45
|
-
"view",
|
|
46
|
-
"--json",
|
|
47
|
-
"number",
|
|
48
|
-
"--jq",
|
|
49
|
-
".number",
|
|
50
|
-
)
|
|
51
|
-
GH_REPO_FLAG: str = "--repo"
|
|
52
|
-
REPO_SLUG_TEMPLATE: str = "{owner}/{repo}"
|
|
53
|
-
GIT_URL_SUFFIX: str = ".git"
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Configuration constants for the pr_converge_bugteam_enforcer hook pair.
|
|
2
|
-
|
|
3
|
-
The enforcer denies ``Agent({subagent_type: "clean-coder"})`` invocations that
|
|
4
|
-
substitute audit-shaped work for the formal ``Skill({skill: "bugteam"})`` call
|
|
5
|
-
during Step 5 BUGTEAM of the pr-converge loop. The tracker records every
|
|
6
|
-
formal ``Skill({skill: "bugteam"})`` invocation so the enforcer can confirm
|
|
7
|
-
the Skill fired this tick at the current HEAD before allowing follow-on
|
|
8
|
-
clean-coder spawns.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
AGENT_TOOL_NAME: str = "Agent"
|
|
14
|
-
SKILL_TOOL_NAME: str = "Skill"
|
|
15
|
-
|
|
16
|
-
CLEAN_CODER_SUBAGENT_TYPE: str = "clean-coder"
|
|
17
|
-
BUGTEAM_SKILL_NAME: str = "bugteam"
|
|
18
|
-
|
|
19
|
-
PR_CONVERGE_STATE_FILENAME: str = "pr-converge-state.json"
|
|
20
|
-
CLAUDE_JOB_DIR_ENV_VAR: str = "CLAUDE_JOB_DIR"
|
|
21
|
-
|
|
22
|
-
BUGTEAM_PHASE: str = "BUGTEAM"
|
|
23
|
-
|
|
24
|
-
STATE_FIELD_PHASE: str = "phase"
|
|
25
|
-
STATE_FIELD_CURRENT_HEAD: str = "current_head"
|
|
26
|
-
STATE_FIELD_TICK_COUNT: str = "tick_count"
|
|
27
|
-
STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD: str = "bugteam_skill_invoked_at_head"
|
|
28
|
-
STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK: str = "bugteam_skill_invoked_at_tick"
|
|
29
|
-
|
|
30
|
-
ALL_AUDIT_PROMPT_SUBSTRINGS: tuple[str, ...] = (
|
|
31
|
-
"audit",
|
|
32
|
-
"findings",
|
|
33
|
-
"bugteam",
|
|
34
|
-
"a-j categor",
|
|
35
|
-
"code-quality",
|
|
36
|
-
"verify_clean",
|
|
37
|
-
"converge",
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
ENFORCER_CORRECTIVE_MESSAGE: str = (
|
|
41
|
-
"BLOCKED [pr-converge-bugteam-enforcer]: Step 5 BUGTEAM advances ONLY after "
|
|
42
|
-
'`Skill({skill: "bugteam", args: "<PR URL>"})` fires this tick at the '
|
|
43
|
-
'current HEAD. Substituting an `Agent({subagent_type: "clean-coder"})` '
|
|
44
|
-
"audit call for the formal Skill invocation is a protocol violation — the "
|
|
45
|
-
"formal Skill writes the artifact `check_convergence.py`'s `bugteam_clean_at` "
|
|
46
|
-
"gate looks for, and a substituted audit silently bypasses that gate.\n\n"
|
|
47
|
-
"`qbug` is NOT an accepted substitute — `bugteam` is the only allowed skill "
|
|
48
|
-
"at this step.\n\n"
|
|
49
|
-
'Run `Skill({skill: "bugteam", args: "<PR URL>"})` first. Follow-on '
|
|
50
|
-
"clean-coder fix spawns are allowed once the formal Skill has registered at "
|
|
51
|
-
"the current HEAD and tick."
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
STATE_FILE_ATOMIC_WRITE_SUFFIX: str = ".tmp"
|
|
55
|
-
STATE_FILE_JSON_INDENT_SPACES: int = 2
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
"""Shared state-loading helpers for the pr_converge_bugteam_enforcer hook pair.
|
|
2
|
-
|
|
3
|
-
Both the enforcer (``pr_converge_bugteam_enforcer.py``) and the tracker
|
|
4
|
-
(``pr_converge_bugteam_skill_tracker.py``) read the per-job
|
|
5
|
-
``$CLAUDE_JOB_DIR/pr-converge-state.json`` file from the same per-job
|
|
6
|
-
directory. This module hosts the byte-identical ``load_state_dictionary``
|
|
7
|
-
and ``resolve_state_path`` helpers so each hook imports a single canonical
|
|
8
|
-
definition.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import os
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
from hooks_constants.pr_converge_bugteam_enforcer_constants import (
|
|
18
|
-
CLAUDE_JOB_DIR_ENV_VAR,
|
|
19
|
-
PR_CONVERGE_STATE_FILENAME,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def load_state_dictionary(state_path: Path) -> dict[str, object] | None:
|
|
24
|
-
"""Return the parsed pr-converge state, or None when absent or unparseable.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
state_path: Absolute path to ``pr-converge-state.json``.
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
The decoded state dictionary, or None when the file is missing,
|
|
31
|
-
malformed, empty, or not a JSON object at the root.
|
|
32
|
-
"""
|
|
33
|
-
if not state_path.is_file():
|
|
34
|
-
return None
|
|
35
|
-
try:
|
|
36
|
-
raw_text = state_path.read_text(encoding="utf-8")
|
|
37
|
-
except OSError:
|
|
38
|
-
return None
|
|
39
|
-
if not raw_text.strip():
|
|
40
|
-
return None
|
|
41
|
-
try:
|
|
42
|
-
parsed_state = json.loads(raw_text)
|
|
43
|
-
except json.JSONDecodeError:
|
|
44
|
-
return None
|
|
45
|
-
if not isinstance(parsed_state, dict):
|
|
46
|
-
return None
|
|
47
|
-
return parsed_state
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def resolve_state_path() -> Path | None:
|
|
51
|
-
"""Return the absolute path to the per-job ``pr-converge-state.json``.
|
|
52
|
-
|
|
53
|
-
Reads ``$CLAUDE_JOB_DIR`` from the environment and joins
|
|
54
|
-
``PR_CONVERGE_STATE_FILENAME`` onto it. The path is returned even when
|
|
55
|
-
the file does not yet exist on disk; callers are expected to call
|
|
56
|
-
``load_state_dictionary`` (or ``state_path.is_file()``) to check for
|
|
57
|
-
presence.
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
Absolute path to ``$CLAUDE_JOB_DIR/pr-converge-state.json``, or
|
|
61
|
-
``None`` when the ``CLAUDE_JOB_DIR`` environment variable is unset
|
|
62
|
-
or empty (no single-PR pr-converge job is currently active).
|
|
63
|
-
"""
|
|
64
|
-
job_directory = os.environ.get(CLAUDE_JOB_DIR_ENV_VAR, "")
|
|
65
|
-
if not job_directory:
|
|
66
|
-
return None
|
|
67
|
-
return Path(job_directory) / PR_CONVERGE_STATE_FILENAME
|