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,159 +1,160 @@
|
|
|
1
|
-
"""Constants for the verified-commit gate hook family.
|
|
2
|
-
|
|
3
|
-
Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
|
|
4
|
-
and ``verifier_verdict_minter.py`` so every tunable lives in one place.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
GIT_TIMEOUT_SECONDS = 30
|
|
10
|
-
ROOT_KEY_HEX_LENGTH = 16
|
|
11
|
-
VERDICT_JSON_INDENT = 2
|
|
12
|
-
CLAUDE_HOME_DIRECTORY_NAME = ".claude"
|
|
13
|
-
VERDICT_DIRECTORY_NAME = "verification"
|
|
14
|
-
VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
|
|
15
|
-
VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
|
|
16
|
-
RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
|
|
17
|
-
VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
|
|
18
|
-
VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
19
|
-
VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
|
|
20
|
-
VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
|
|
21
|
-
VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
|
|
22
|
-
rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
|
|
23
|
-
)
|
|
24
|
-
PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
|
|
25
|
-
r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
|
|
26
|
-
r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
|
|
27
|
-
)
|
|
28
|
-
ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
|
|
29
|
-
ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
|
|
30
|
-
HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
|
|
31
|
-
BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
|
|
32
|
-
CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
|
|
33
|
-
CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
|
|
34
|
-
CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
|
|
35
|
-
HEX_DIGITS_PER_BYTE = 2
|
|
36
|
-
FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
37
|
-
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
38
|
-
r"|Out-File|Set-Content|Add-Content|\btee\b|>"
|
|
39
|
-
)
|
|
40
|
-
NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
41
|
-
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
42
|
-
r"|Out-File|Set-Content|Add-Content|\btee\b"
|
|
43
|
-
)
|
|
44
|
-
WRITE_CALL_REGION_PATTERN = (
|
|
45
|
-
r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
46
|
-
r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
|
|
47
|
-
)
|
|
48
|
-
VERDICT_KEY_ALL_PASS = "all_pass"
|
|
49
|
-
VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
|
|
50
|
-
VERDICT_KEY_FINDINGS = "findings"
|
|
51
|
-
VERDICT_FINDING_CHECK_KEY = "check"
|
|
52
|
-
LOGS_DIRECTORY_NAME = "logs"
|
|
53
|
-
VERIFIER_VERDICTS_JSONL_FILENAME = "verifier-verdicts.jsonl"
|
|
54
|
-
LEDGER_KEY_TIMESTAMP = "timestamp"
|
|
55
|
-
LEDGER_KEY_REPO_ROOT = "repo_root"
|
|
56
|
-
LEDGER_KEY_BRANCH = "branch"
|
|
57
|
-
LEDGER_KEY_MANIFEST_HASH = "manifest_hash"
|
|
58
|
-
LEDGER_KEY_ALL_PASS = "all_pass"
|
|
59
|
-
LEDGER_KEY_FINDING_COUNT = "finding_count"
|
|
60
|
-
LEDGER_KEY_FINDING_CHECK_NAMES = "finding_check_names"
|
|
61
|
-
LEDGER_KEY_MINTED_FROM_AGENT_ID = "minted_from_agent_id"
|
|
62
|
-
VERDICT_FILE_GLOB = "*.json"
|
|
63
|
-
SUBAGENTS_DIRECTORY_NAME = "subagents"
|
|
64
|
-
AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
|
|
65
|
-
AGENT_META_SIDECAR_SUFFIX = ".meta.json"
|
|
66
|
-
AGENT_META_TYPE_KEY = "agentType"
|
|
67
|
-
TRANSCRIPT_ENTRY_TYPE_KEY = "type"
|
|
68
|
-
TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
|
|
69
|
-
TRANSCRIPT_MESSAGE_KEY = "message"
|
|
70
|
-
TRANSCRIPT_CONTENT_KEY = "content"
|
|
71
|
-
TRANSCRIPT_CONTENT_TYPE_KEY = "type"
|
|
72
|
-
TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
|
|
73
|
-
TRANSCRIPT_TEXT_KEY = "text"
|
|
74
|
-
VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
|
|
75
|
-
MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
|
|
76
|
-
MANIFEST_HASH_FOR_BRANCH_CLI_FLAG = "--manifest-hash-for-branch"
|
|
77
|
-
WORKTREE_LIST_PATH_PREFIX = "worktree "
|
|
78
|
-
WORKTREE_LIST_BRANCH_PREFIX = "branch "
|
|
79
|
-
BRANCH_REFERENCE_PREFIX = "refs/heads/"
|
|
80
|
-
EMPTY_SURFACE_GUARD_MESSAGE = (
|
|
81
|
-
"ERROR: The work tree at {repo_root} has no changed or untracked files "
|
|
82
|
-
"versus origin/main (empty change surface). This work tree holds nothing "
|
|
83
|
-
"to verify — you are pointed at the wrong work tree. Run `git worktree list` "
|
|
84
|
-
"to see all checked-out work trees and target the one on the branch under review."
|
|
85
|
-
)
|
|
86
|
-
BRANCH_WORKTREE_ABSENT_MESSAGE = (
|
|
87
|
-
"ERROR: No work tree has branch '{branch}' checked out. "
|
|
88
|
-
"Check it out in a work tree first: "
|
|
89
|
-
"`git worktree add <path> <branch>` or `git checkout <branch>`."
|
|
90
|
-
)
|
|
91
|
-
DOCS_ONLY_EXTENSIONS = frozenset(
|
|
92
|
-
{".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
|
|
93
|
-
)
|
|
94
|
-
PYTHON_EXTENSION = ".py"
|
|
95
|
-
TEST_FILE_PREFIX = "test_"
|
|
96
|
-
TEST_FILE_SUFFIX = "_test.py"
|
|
97
|
-
CONFTEST_FILE_NAME = "conftest.py"
|
|
98
|
-
MINIMUM_STATUS_FIELD_COUNT = 2
|
|
99
|
-
ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
|
|
100
|
-
DETACHED_HEAD_LABEL = "HEAD"
|
|
101
|
-
BRANCH_REMOTE_CONFIG_KEY_TEMPLATE = "branch.{branch_name}.remote"
|
|
102
|
-
ALL_TOOLING_STATE_PREFIXES = (
|
|
103
|
-
".claude/verification/",
|
|
104
|
-
".claude/worktrees/",
|
|
105
|
-
".claude/daemon/",
|
|
106
|
-
".claude/teams/",
|
|
107
|
-
".claude/sessions/",
|
|
108
|
-
".cursor/worktrees/",
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
)
|
|
1
|
+
"""Constants for the verified-commit gate hook family.
|
|
2
|
+
|
|
3
|
+
Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
|
|
4
|
+
and ``verifier_verdict_minter.py`` so every tunable lives in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
GIT_TIMEOUT_SECONDS = 30
|
|
10
|
+
ROOT_KEY_HEX_LENGTH = 16
|
|
11
|
+
VERDICT_JSON_INDENT = 2
|
|
12
|
+
CLAUDE_HOME_DIRECTORY_NAME = ".claude"
|
|
13
|
+
VERDICT_DIRECTORY_NAME = "verification"
|
|
14
|
+
VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
|
|
15
|
+
VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
|
|
16
|
+
RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
|
|
17
|
+
VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
|
|
18
|
+
VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
19
|
+
VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
|
|
20
|
+
VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
|
|
21
|
+
VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
|
|
22
|
+
rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
|
|
23
|
+
)
|
|
24
|
+
PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
|
|
25
|
+
r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
|
|
26
|
+
r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
|
|
27
|
+
)
|
|
28
|
+
ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
|
|
29
|
+
ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
|
|
30
|
+
HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
|
|
31
|
+
BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
|
|
32
|
+
CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
|
|
33
|
+
CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
|
|
34
|
+
CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
|
|
35
|
+
HEX_DIGITS_PER_BYTE = 2
|
|
36
|
+
FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
37
|
+
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
38
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b|>"
|
|
39
|
+
)
|
|
40
|
+
NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
41
|
+
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
42
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b"
|
|
43
|
+
)
|
|
44
|
+
WRITE_CALL_REGION_PATTERN = (
|
|
45
|
+
r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
46
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
|
|
47
|
+
)
|
|
48
|
+
VERDICT_KEY_ALL_PASS = "all_pass"
|
|
49
|
+
VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
|
|
50
|
+
VERDICT_KEY_FINDINGS = "findings"
|
|
51
|
+
VERDICT_FINDING_CHECK_KEY = "check"
|
|
52
|
+
LOGS_DIRECTORY_NAME = "logs"
|
|
53
|
+
VERIFIER_VERDICTS_JSONL_FILENAME = "verifier-verdicts.jsonl"
|
|
54
|
+
LEDGER_KEY_TIMESTAMP = "timestamp"
|
|
55
|
+
LEDGER_KEY_REPO_ROOT = "repo_root"
|
|
56
|
+
LEDGER_KEY_BRANCH = "branch"
|
|
57
|
+
LEDGER_KEY_MANIFEST_HASH = "manifest_hash"
|
|
58
|
+
LEDGER_KEY_ALL_PASS = "all_pass"
|
|
59
|
+
LEDGER_KEY_FINDING_COUNT = "finding_count"
|
|
60
|
+
LEDGER_KEY_FINDING_CHECK_NAMES = "finding_check_names"
|
|
61
|
+
LEDGER_KEY_MINTED_FROM_AGENT_ID = "minted_from_agent_id"
|
|
62
|
+
VERDICT_FILE_GLOB = "*.json"
|
|
63
|
+
SUBAGENTS_DIRECTORY_NAME = "subagents"
|
|
64
|
+
AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
|
|
65
|
+
AGENT_META_SIDECAR_SUFFIX = ".meta.json"
|
|
66
|
+
AGENT_META_TYPE_KEY = "agentType"
|
|
67
|
+
TRANSCRIPT_ENTRY_TYPE_KEY = "type"
|
|
68
|
+
TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
|
|
69
|
+
TRANSCRIPT_MESSAGE_KEY = "message"
|
|
70
|
+
TRANSCRIPT_CONTENT_KEY = "content"
|
|
71
|
+
TRANSCRIPT_CONTENT_TYPE_KEY = "type"
|
|
72
|
+
TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
|
|
73
|
+
TRANSCRIPT_TEXT_KEY = "text"
|
|
74
|
+
VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
|
|
75
|
+
MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
|
|
76
|
+
MANIFEST_HASH_FOR_BRANCH_CLI_FLAG = "--manifest-hash-for-branch"
|
|
77
|
+
WORKTREE_LIST_PATH_PREFIX = "worktree "
|
|
78
|
+
WORKTREE_LIST_BRANCH_PREFIX = "branch "
|
|
79
|
+
BRANCH_REFERENCE_PREFIX = "refs/heads/"
|
|
80
|
+
EMPTY_SURFACE_GUARD_MESSAGE = (
|
|
81
|
+
"ERROR: The work tree at {repo_root} has no changed or untracked files "
|
|
82
|
+
"versus origin/main (empty change surface). This work tree holds nothing "
|
|
83
|
+
"to verify — you are pointed at the wrong work tree. Run `git worktree list` "
|
|
84
|
+
"to see all checked-out work trees and target the one on the branch under review."
|
|
85
|
+
)
|
|
86
|
+
BRANCH_WORKTREE_ABSENT_MESSAGE = (
|
|
87
|
+
"ERROR: No work tree has branch '{branch}' checked out. "
|
|
88
|
+
"Check it out in a work tree first: "
|
|
89
|
+
"`git worktree add <path> <branch>` or `git checkout <branch>`."
|
|
90
|
+
)
|
|
91
|
+
DOCS_ONLY_EXTENSIONS = frozenset(
|
|
92
|
+
{".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
|
|
93
|
+
)
|
|
94
|
+
PYTHON_EXTENSION = ".py"
|
|
95
|
+
TEST_FILE_PREFIX = "test_"
|
|
96
|
+
TEST_FILE_SUFFIX = "_test.py"
|
|
97
|
+
CONFTEST_FILE_NAME = "conftest.py"
|
|
98
|
+
MINIMUM_STATUS_FIELD_COUNT = 2
|
|
99
|
+
ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
|
|
100
|
+
DETACHED_HEAD_LABEL = "HEAD"
|
|
101
|
+
BRANCH_REMOTE_CONFIG_KEY_TEMPLATE = "branch.{branch_name}.remote"
|
|
102
|
+
ALL_TOOLING_STATE_PREFIXES = (
|
|
103
|
+
".claude/verification/",
|
|
104
|
+
".claude/worktrees/",
|
|
105
|
+
".claude/daemon/",
|
|
106
|
+
".claude/teams/",
|
|
107
|
+
".claude/sessions/",
|
|
108
|
+
".cursor/worktrees/",
|
|
109
|
+
".claude-plugin-data/",
|
|
110
|
+
)
|
|
111
|
+
GATED_GIT_SUBCOMMANDS = frozenset({"commit", "push"})
|
|
112
|
+
ALL_GIT_BINARY_NAMES = frozenset({"git", "git.exe"})
|
|
113
|
+
VALUE_TAKING_GIT_OPTIONS = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace"})
|
|
114
|
+
REPO_DIRECTORY_OPTION = "-C"
|
|
115
|
+
WORK_TREE_OPTION = "--work-tree"
|
|
116
|
+
DIRECTORY_CHANGE_VERBS = frozenset({"cd", "pushd", "set-location", "sl"})
|
|
117
|
+
DIRECTORY_CHANGE_PATH_OPTIONS = frozenset({"-path", "-literalpath"})
|
|
118
|
+
DIRECTORY_CHANGE_OPTION_TERMINATOR = "--"
|
|
119
|
+
DIRECTORY_CHANGE_PATTERN_PREFIX = r"(?:^|(?<=[\s;&|(]))(?:"
|
|
120
|
+
DIRECTORY_CHANGE_PATTERN_SUFFIX = r")(?=\s|$)"
|
|
121
|
+
DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN = r"(?:[ \t]+(?:%s)(?=\s|$))*"
|
|
122
|
+
DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?\S*"
|
|
123
|
+
CLAUDE_HOME_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
124
|
+
VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
125
|
+
COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
|
|
126
|
+
OPTION_WITH_VALUE_STEP = 2
|
|
127
|
+
ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
|
|
128
|
+
HASH_PREVIEW_LENGTH = 16
|
|
129
|
+
VERIFICATION_BYPASS_MARKER = "# verify-skip"
|
|
130
|
+
MINTING_AGENT_TYPE = "code-verifier"
|
|
131
|
+
VERDICT_DIRECTORY_GUARD_MESSAGE = (
|
|
132
|
+
"BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
|
|
133
|
+
"verdict directory (~/.claude/verification/) is denied. Only the "
|
|
134
|
+
"verifier_verdict_minter.py SubagentStop hook writes verdict files; a "
|
|
135
|
+
"shell write here would forge a passing verdict and defeat the "
|
|
136
|
+
"verified-commit gate. Spawn the code-verifier agent to earn a verdict "
|
|
137
|
+
"instead of writing one."
|
|
138
|
+
)
|
|
139
|
+
CORRECTIVE_MESSAGE = (
|
|
140
|
+
"BLOCKED: [VERIFIED_COMMIT_GATE] This branch surface has no passing "
|
|
141
|
+
"verification verdict. Clear commit-committability gates first "
|
|
142
|
+
"(CODE_RULES on changed lines and merge conflicts vs base — the "
|
|
143
|
+
"code_verifier_spawn_preflight_gate enforces these on the spawn). If a "
|
|
144
|
+
"prior commit failed on CODE_RULES or staging, fix those before "
|
|
145
|
+
"re-verifying. Then spawn the code-verifier agent (Agent or Task tool, "
|
|
146
|
+
"subagent_type 'code-verifier') with the task texts, the diff scope, "
|
|
147
|
+
"and recorded baselines; when it finishes with a clean verdict the "
|
|
148
|
+
"SubagentStop hook mints the verdict and this command will pass. Any "
|
|
149
|
+
"file change after verification invalidates the verdict, so verify "
|
|
150
|
+
"last only after commit gates are green. The verdict binds to the exact "
|
|
151
|
+
"surface content (every changed and untracked file's bytes), not to a "
|
|
152
|
+
"work-tree path, so a commit from a different work tree passes only "
|
|
153
|
+
"when its surface is byte-identical; a work tree carrying any extra or "
|
|
154
|
+
"later edit has a surface no verdict covers. To land this change, run "
|
|
155
|
+
"this command from the work tree that holds the verified surface, or "
|
|
156
|
+
"verify this surface here. Exempt automatically: docs/image files, "
|
|
157
|
+
"pytest test files, and "
|
|
158
|
+
"Python files whose docstring- and comment-stripped AST is unchanged "
|
|
159
|
+
"(comment-only edits in non-Python files are not exempt)."
|
|
160
|
+
)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse gate: deny orchestrator-refresh re-arm when the run is not active.
|
|
3
|
+
|
|
4
|
+
Fires on ``ScheduleWakeup`` and ``CronCreate`` when the prompt targets
|
|
5
|
+
``/orchestrator-refresh``. Uses the same status-file gate as
|
|
6
|
+
``skills/orchestrator/scripts/status_gate.py`` so idle loops
|
|
7
|
+
cannot be re-armed on the Claude host without an active status file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from typing import Mapping
|
|
20
|
+
|
|
21
|
+
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
22
|
+
if _hooks_dir not in sys.path:
|
|
23
|
+
sys.path.insert(0, _hooks_dir)
|
|
24
|
+
|
|
25
|
+
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
26
|
+
from hooks_constants.orchestrator_refresh_reschedule_gate_constants import ( # noqa: E402
|
|
27
|
+
ALL_HOME_SKILLS_RELATIVE_PARTS,
|
|
28
|
+
ALL_SCHEDULE_TOOL_NAMES,
|
|
29
|
+
ALL_SKILL_SCRIPTS_RELATIVE_PARTS,
|
|
30
|
+
CALLING_HOOK_NAME,
|
|
31
|
+
CRON_CREATE_TOOL_NAME,
|
|
32
|
+
CWD_FIELD_NAME,
|
|
33
|
+
DENY_REASON_TEMPLATE,
|
|
34
|
+
HOOK_EVENT_NAME,
|
|
35
|
+
ORCHESTRATOR_REFRESH_PROMPT_TOKEN,
|
|
36
|
+
PLUGIN_ROOT_ENV_VAR,
|
|
37
|
+
PROMPT_FIELD_NAME,
|
|
38
|
+
REASON_CRON_CREATE_FORBIDDEN,
|
|
39
|
+
RUN_SLUG_PROMPT_FLAG,
|
|
40
|
+
RUN_SLUG_PROMPT_VALUE_PATTERN,
|
|
41
|
+
TOOL_INPUT_FIELD_NAME,
|
|
42
|
+
TOOL_NAME_FIELD_NAME,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _candidate_skill_script_directories() -> list[Path]:
|
|
47
|
+
"""Return ordered paths that may contain status_gate.py.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Absolute candidate directories in preference order.
|
|
51
|
+
"""
|
|
52
|
+
all_candidates: list[Path] = []
|
|
53
|
+
plugin_root = os.environ.get(PLUGIN_ROOT_ENV_VAR)
|
|
54
|
+
if plugin_root:
|
|
55
|
+
all_candidates.append(
|
|
56
|
+
Path(plugin_root).joinpath(*ALL_SKILL_SCRIPTS_RELATIVE_PARTS)
|
|
57
|
+
)
|
|
58
|
+
package_hooks_parent = Path(__file__).resolve().parents[2]
|
|
59
|
+
all_candidates.append(
|
|
60
|
+
package_hooks_parent.joinpath(*ALL_SKILL_SCRIPTS_RELATIVE_PARTS)
|
|
61
|
+
)
|
|
62
|
+
all_candidates.append(Path.home().joinpath(*ALL_HOME_SKILLS_RELATIVE_PARTS))
|
|
63
|
+
return all_candidates
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _load_status_gate_module() -> ModuleType | None:
|
|
67
|
+
"""Import status_gate from the skill scripts package.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The loaded module, or None when no candidate path is usable.
|
|
71
|
+
"""
|
|
72
|
+
for each_directory in _candidate_skill_script_directories():
|
|
73
|
+
module_path = each_directory / "status_gate.py"
|
|
74
|
+
if not module_path.is_file():
|
|
75
|
+
continue
|
|
76
|
+
scripts_directory = str(each_directory)
|
|
77
|
+
if scripts_directory not in sys.path:
|
|
78
|
+
sys.path.insert(0, scripts_directory)
|
|
79
|
+
spec = importlib.util.spec_from_file_location(
|
|
80
|
+
"orchestrator_status_gate_for_reschedule_hook",
|
|
81
|
+
module_path,
|
|
82
|
+
)
|
|
83
|
+
if spec is None or spec.loader is None:
|
|
84
|
+
continue
|
|
85
|
+
status_gate_module = importlib.util.module_from_spec(spec)
|
|
86
|
+
sys.modules[spec.name] = status_gate_module
|
|
87
|
+
spec.loader.exec_module(status_gate_module)
|
|
88
|
+
return status_gate_module
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _prompt_targets_orchestrator_refresh(
|
|
93
|
+
all_tool_input: Mapping[str, object],
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""Return True when the schedule prompt re-enters orchestrator-refresh.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
all_tool_input: PreToolUse tool_input mapping.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True when the prompt field contains the refresh skill token.
|
|
102
|
+
"""
|
|
103
|
+
prompt_value = all_tool_input.get(PROMPT_FIELD_NAME, "")
|
|
104
|
+
if not isinstance(prompt_value, str):
|
|
105
|
+
return False
|
|
106
|
+
stripped_prompt = prompt_value.lstrip()
|
|
107
|
+
if not stripped_prompt.startswith(ORCHESTRATOR_REFRESH_PROMPT_TOKEN):
|
|
108
|
+
return False
|
|
109
|
+
if len(stripped_prompt) == len(ORCHESTRATOR_REFRESH_PROMPT_TOKEN):
|
|
110
|
+
return True
|
|
111
|
+
return stripped_prompt[len(ORCHESTRATOR_REFRESH_PROMPT_TOKEN)] in (" ", "\n", "\t")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _should_evaluate(all_payload_by_field: Mapping[str, object]) -> bool:
|
|
115
|
+
"""Return True for schedule tools re-arming orchestrator-refresh.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
all_payload_by_field: Full PreToolUse payload.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True when this gate must run.
|
|
122
|
+
"""
|
|
123
|
+
tool_name = all_payload_by_field.get(TOOL_NAME_FIELD_NAME, "")
|
|
124
|
+
if tool_name not in ALL_SCHEDULE_TOOL_NAMES:
|
|
125
|
+
return False
|
|
126
|
+
tool_input = all_payload_by_field.get(TOOL_INPUT_FIELD_NAME, {})
|
|
127
|
+
if not isinstance(tool_input, dict):
|
|
128
|
+
return False
|
|
129
|
+
return _prompt_targets_orchestrator_refresh(tool_input)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _run_slug_from_prompt(prompt_value: str) -> str:
|
|
133
|
+
"""Extract ``--run-slug`` from a ScheduleWakeup prompt when present.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
prompt_value: The schedule tool prompt text.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The slug string, or empty when the flag is absent.
|
|
140
|
+
"""
|
|
141
|
+
run_slug_from_prompt_pattern = re.compile(
|
|
142
|
+
re.escape(RUN_SLUG_PROMPT_FLAG) + RUN_SLUG_PROMPT_VALUE_PATTERN
|
|
143
|
+
)
|
|
144
|
+
matched = run_slug_from_prompt_pattern.search(prompt_value)
|
|
145
|
+
if matched is None:
|
|
146
|
+
return ""
|
|
147
|
+
return matched.group(1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _resolve_status_file_path(
|
|
151
|
+
status_gate_module: ModuleType,
|
|
152
|
+
all_payload_by_field: Mapping[str, object],
|
|
153
|
+
) -> Path:
|
|
154
|
+
"""Resolve the run status file for this schedule attempt without chdir.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
status_gate_module: Loaded status_gate module.
|
|
158
|
+
all_payload_by_field: PreToolUse payload (may carry cwd and prompt).
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Absolute status file path.
|
|
162
|
+
"""
|
|
163
|
+
base_directory: Path | None = None
|
|
164
|
+
working_directory_value = all_payload_by_field.get(CWD_FIELD_NAME)
|
|
165
|
+
if isinstance(working_directory_value, str) and working_directory_value:
|
|
166
|
+
base_directory = Path(working_directory_value)
|
|
167
|
+
run_slug = ""
|
|
168
|
+
tool_input = all_payload_by_field.get(TOOL_INPUT_FIELD_NAME, {})
|
|
169
|
+
if isinstance(tool_input, dict):
|
|
170
|
+
prompt_value = tool_input.get(PROMPT_FIELD_NAME, "")
|
|
171
|
+
if isinstance(prompt_value, str):
|
|
172
|
+
run_slug = _run_slug_from_prompt(prompt_value)
|
|
173
|
+
return status_gate_module.resolve_status_file_path(
|
|
174
|
+
None, base_directory, run_slug
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def build_denial_response(reason_code: str) -> dict[str, dict[str, str]]:
|
|
179
|
+
"""Build the PreToolUse deny payload.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
reason_code: Machine reason from decide_should_reschedule.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Hook-specific deny JSON structure.
|
|
186
|
+
"""
|
|
187
|
+
denial_reason = DENY_REASON_TEMPLATE.format(reason=reason_code)
|
|
188
|
+
return {
|
|
189
|
+
"hookSpecificOutput": {
|
|
190
|
+
"hookEventName": HOOK_EVENT_NAME,
|
|
191
|
+
"permissionDecision": "deny",
|
|
192
|
+
"permissionDecisionReason": denial_reason,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _emit_denial(
|
|
198
|
+
reason_code: str,
|
|
199
|
+
all_payload_by_field: Mapping[str, object],
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Log and print a deny decision for the current schedule attempt.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
reason_code: Machine reason string.
|
|
205
|
+
all_payload_by_field: PreToolUse payload for logging context.
|
|
206
|
+
"""
|
|
207
|
+
denial = build_denial_response(reason_code)
|
|
208
|
+
hook_specific_output = denial["hookSpecificOutput"]
|
|
209
|
+
block_reason = hook_specific_output["permissionDecisionReason"]
|
|
210
|
+
log_hook_block(
|
|
211
|
+
calling_hook_name=CALLING_HOOK_NAME,
|
|
212
|
+
hook_event=HOOK_EVENT_NAME,
|
|
213
|
+
block_reason=block_reason,
|
|
214
|
+
tool_name=str(all_payload_by_field.get(TOOL_NAME_FIELD_NAME, "")),
|
|
215
|
+
offending_input_preview=str(
|
|
216
|
+
all_payload_by_field.get(TOOL_INPUT_FIELD_NAME, {})
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
print(json.dumps(denial))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main() -> None:
|
|
223
|
+
"""Read PreToolUse stdin and deny idle orchestrator-refresh re-arms."""
|
|
224
|
+
try:
|
|
225
|
+
payload_by_field = json.load(sys.stdin)
|
|
226
|
+
except json.JSONDecodeError:
|
|
227
|
+
sys.exit(0)
|
|
228
|
+
|
|
229
|
+
if not isinstance(payload_by_field, dict):
|
|
230
|
+
sys.exit(0)
|
|
231
|
+
if not _should_evaluate(payload_by_field):
|
|
232
|
+
sys.exit(0)
|
|
233
|
+
|
|
234
|
+
tool_name = payload_by_field.get(TOOL_NAME_FIELD_NAME, "")
|
|
235
|
+
if tool_name == CRON_CREATE_TOOL_NAME:
|
|
236
|
+
_emit_denial(REASON_CRON_CREATE_FORBIDDEN, payload_by_field)
|
|
237
|
+
sys.exit(0)
|
|
238
|
+
|
|
239
|
+
status_gate_module = _load_status_gate_module()
|
|
240
|
+
if status_gate_module is None:
|
|
241
|
+
_emit_denial("status_gate_unavailable", payload_by_field)
|
|
242
|
+
sys.exit(0)
|
|
243
|
+
|
|
244
|
+
status_file_path = _resolve_status_file_path(status_gate_module, payload_by_field)
|
|
245
|
+
is_reschedule_allowed, reason_code = status_gate_module.decide_should_reschedule(
|
|
246
|
+
status_file_path
|
|
247
|
+
)
|
|
248
|
+
if is_reschedule_allowed:
|
|
249
|
+
sys.exit(0)
|
|
250
|
+
|
|
251
|
+
_emit_denial(reason_code, payload_by_field)
|
|
252
|
+
sys.exit(0)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
main()
|