claude-dev-env 2.4.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 -49
- 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 +2 -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 +0 -1
- 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/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- 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 +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- 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/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -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/ruff_integration_constants.py +16 -0
- 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 +1 -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/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- 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 -44
- 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/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- 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_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/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -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/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/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- 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/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -1,280 +1,280 @@
|
|
|
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
|
+
"""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()
|
|
@@ -542,3 +542,28 @@ def test_main_allows_deletion_push_even_when_code_review_gate_would_block(
|
|
|
542
542
|
exit_code = pre_push.main()
|
|
543
543
|
|
|
544
544
|
assert exit_code == 0
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def test_main_allows_when_code_review_enforcement_flag_is_off(
|
|
548
|
+
tmp_path: Path,
|
|
549
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
550
|
+
) -> None:
|
|
551
|
+
"""Native pre-push honors the real gate with enforcement off.
|
|
552
|
+
|
|
553
|
+
Points at the production push gate (not a reason stub). With the shipped
|
|
554
|
+
default `CODE_REVIEW_ENFORCEMENT_ENABLED = False`, the gate returns no
|
|
555
|
+
deny reason and the backstop allows the push — one path covering flag,
|
|
556
|
+
real gate load, and pre-push exit together.
|
|
557
|
+
"""
|
|
558
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
559
|
+
real_gate_path = (
|
|
560
|
+
Path(__file__).resolve().parent.parent
|
|
561
|
+
/ "blocking"
|
|
562
|
+
/ "code_review_push_gate.py"
|
|
563
|
+
)
|
|
564
|
+
assert real_gate_path.is_file()
|
|
565
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(real_gate_path))
|
|
566
|
+
|
|
567
|
+
exit_code = pre_push.main()
|
|
568
|
+
|
|
569
|
+
assert exit_code == 0
|
package/hooks/hooks.json
CHANGED
|
@@ -102,6 +102,16 @@
|
|
|
102
102
|
}
|
|
103
103
|
]
|
|
104
104
|
},
|
|
105
|
+
{
|
|
106
|
+
"matcher": "ScheduleWakeup|CronCreate",
|
|
107
|
+
"hooks": [
|
|
108
|
+
{
|
|
109
|
+
"type": "command",
|
|
110
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/orchestrator_refresh_reschedule_gate.py",
|
|
111
|
+
"timeout": 15
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
105
115
|
{
|
|
106
116
|
"matcher": "Skill",
|
|
107
117
|
"hooks": [
|
|
@@ -64,6 +64,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
64
64
|
| `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
|
|
65
65
|
| `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
|
|
66
66
|
| `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
|
|
67
|
+
| `ruff_integration_constants.py` | ``NO_COLOR`` / ``FORCE_COLOR`` environment variable names for plain ruff diagnostics |
|
|
67
68
|
| `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
|
|
68
69
|
| `sensitive_file_protector_constants.py` | Sensitive filename patterns, the committed-template suffixes that earn an exemption, the write/edit tool names, and the deny decision and message template for `sensitive_file_protector` |
|
|
69
70
|
| `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
|
|
@@ -90,6 +91,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
90
91
|
- Most files in this package are pure constants modules — no side effects, no I/O.
|
|
91
92
|
- Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
|
|
92
93
|
- Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
|
|
93
|
-
- `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they
|
|
94
|
+
- `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they hold logic shared across hook surfaces.
|
|
94
95
|
- `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` (its path overridable via `CLAUDE_LOCAL_IDENTITY_PATH`) to resolve private NAS values (with committed placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`).
|
|
95
96
|
|