claude-dev-env 2.28.0 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/AGENTS.md +0 -31
- package/.agents/agents/clean-coder.md +1 -1
- package/.agents/agents/test_agent_frontmatter.py +3 -1
- package/.agents/skills/AGENTS.md +0 -70
- package/.agents/skills/_shared/AGENTS.md +0 -44
- package/.agents/skills/_shared/advisor/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -57
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -34
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -23
- package/.agents/skills/autoconverge/AGENTS.md +0 -35
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -15
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -25
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -15
- package/.agents/skills/everything-search/AGENTS.md +0 -16
- package/.agents/skills/fresh-branch/AGENTS.md +0 -13
- package/.agents/skills/grok-spawn/AGENTS.md +0 -27
- package/.agents/skills/orchestrator/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/SKILL.md +44 -53
- package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/reference/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +70 -0
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +62 -0
- package/.agents/skills/orchestrator/reference/host-detect.md +15 -0
- package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +62 -0
- package/.agents/skills/orchestrator-refresh/SKILL.md +16 -34
- package/.agents/skills/rebase/AGENTS.md +0 -31
- package/.agents/skills/session-log/AGENTS.md +0 -31
- package/.agents/skills/session-tidy/AGENTS.md +0 -35
- package/.agents/skills/skill-builder/AGENTS.md +0 -48
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -24
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -13
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -18
- package/.agents/skills/task-build/AGENTS.md +0 -28
- package/.agents/skills/update/AGENTS.md +0 -37
- package/.agents/skills-archived/AGENTS.md +0 -44
- package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -33
- package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -10
- package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -15
- package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -12
- package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -13
- package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -20
- package/.agents/skills-archived/bugteam/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -19
- package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -23
- package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/AGENTS.md +0 -45
- package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -14
- package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/test_skill_scaffold.py +13 -3
- package/.agents/skills-archived/copilot-review/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/AGENTS.md +0 -31
- package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -25
- package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -27
- package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -22
- package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -45
- package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -15
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/.agents/skills-archived/recall/AGENTS.md +0 -29
- package/.agents/skills-archived/remember/AGENTS.md +0 -30
- package/AGENTS.md +0 -112
- package/_shared/AGENTS.md +0 -16
- package/_shared/advisor/AGENTS.md +0 -21
- package/_shared/advisor/advisor-protocol.md +4 -1
- package/_shared/advisor/reference/consult-format.md +4 -2
- package/_shared/pr-loop/AGENTS.md +0 -27
- package/_shared/pr-loop/scripts/AGENTS.md +0 -49
- package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -41
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -43
- package/_shared/process-tree/AGENTS.md +0 -40
- package/audit-rubrics/AGENTS.md +0 -42
- package/audit-rubrics/category_rubrics/AGENTS.md +0 -36
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
- package/audit-rubrics/prompts/AGENTS.md +0 -36
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/bin/AGENTS.md +0 -118
- package/bin/install.test.mjs +1 -1
- package/commands/AGENTS.md +0 -14
- package/docs/AGENTS.md +0 -31
- package/docs/CODE_RULES.md +3 -3
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/references/AGENTS.md +0 -16
- package/docs/references/prose-style-enforcement.md +5 -9
- package/hooks/AGENTS.md +0 -27
- package/hooks/advisory/AGENTS.md +0 -15
- package/hooks/blocking/AGENTS.md +0 -122
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -27
- package/hooks/blocking/code_rules_docstrings.py +10 -2138
- package/hooks/blocking/code_rules_enforcer.py +0 -121
- package/hooks/blocking/code_rules_imports_logging.py +1 -236
- package/hooks/blocking/code_rules_shared.py +23 -0
- package/hooks/blocking/code_rules_test_layout.py +8 -8
- package/hooks/blocking/config/AGENTS.md +0 -9
- package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
- package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -25
- package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -25
- package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -23
- package/hooks/blocking/precommit_code_rules_gate.py +8 -43
- package/hooks/blocking/state_description_blocker.py +1 -7
- package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -29
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
- package/hooks/blocking/test_code_rules_shared.py +12 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
- package/hooks/blocking/test_state_description_blocker.py +6 -4
- package/hooks/blocking/test_stop_dispatcher.py +5 -29
- package/hooks/git-hooks/AGENTS.md +0 -31
- package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -20
- package/hooks/hooks_constants/AGENTS.md +0 -104
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/messages.py +0 -2
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_messages.py +5 -3
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
- package/hooks/lifecycle/AGENTS.md +0 -17
- package/hooks/observability/AGENTS.md +0 -19
- package/hooks/session/AGENTS.md +0 -37
- package/hooks/validation/AGENTS.md +0 -19
- package/hooks/validators/AGENTS.md +0 -51
- package/hooks/workflow/AGENTS.md +0 -15
- package/output-styles/AGENTS.md +0 -14
- package/package.json +1 -1
- package/rules/AGENTS.md +0 -58
- package/rules/claims-as-quotes.md +0 -10
- package/rules/code-standards.md +6 -6
- package/rules/explore-thoroughly.md +0 -1
- package/rules/failure-blast-radius.md +0 -8
- package/rules/falsify-before-green.md +0 -8
- package/rules/file-global-constants.md +2 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +1 -9
- package/rules/hedging-claims.md +2 -6
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/measurement-denominators.md +0 -9
- package/rules/research-mode.md +0 -6
- package/rules/verify-before-asking.md +0 -5
- package/rules/verify-runtime-state.md +0 -5
- package/scripts/AGENTS.md +0 -55
- package/scripts/codex_compat_materializer.py +0 -12
- package/scripts/dev_env_scripts_constants/AGENTS.md +0 -20
- package/scripts/sync_to_cursor/AGENTS.md +0 -22
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/AGENTS.md +0 -34
- package/scripts/tests/test_engine.py +0 -1
- package/scripts/tests/test_rules.py +0 -1
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/system-prompts/AGENTS.md +0 -24
- package/system-prompts/software-engineer.xml +3 -3
- package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
- package/hooks/blocking/code_rules_dead_config_field.py +0 -568
- package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
- package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
- package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
- package/hooks/blocking/code_rules_mock_completeness.py +0 -295
- package/hooks/blocking/code_rules_scope_binding.py +0 -151
- package/hooks/blocking/code_rules_unused_imports.py +0 -197
- package/hooks/blocking/hedging_language_blocker.py +0 -221
- package/hooks/blocking/intent_only_ending_blocker.py +0 -148
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
- package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
- package/hooks/blocking/test_hedging_language_blocker.py +0 -261
- package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
- package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
- package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
- package/rules/conservative-action.md +0 -17
- package/rules/context7.md +0 -8
- package/rules/few-words.md +0 -3
- package/rules/parallel-tools.md +0 -23
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
"""Unused module-level import check and its import-range helpers."""
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
8
|
-
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
9
|
-
if _blocking_directory not in sys.path:
|
|
10
|
-
sys.path.insert(0, _blocking_directory)
|
|
11
|
-
if _hooks_directory not in sys.path:
|
|
12
|
-
sys.path.insert(0, _hooks_directory)
|
|
13
|
-
|
|
14
|
-
from code_rules_scope_binding import ( # noqa: E402
|
|
15
|
-
_attribute_root_name_if_loaded,
|
|
16
|
-
_collect_string_annotation_names,
|
|
17
|
-
_load_name_is_shadowed,
|
|
18
|
-
)
|
|
19
|
-
from code_rules_shared import ( # noqa: E402
|
|
20
|
-
_build_parent_map,
|
|
21
|
-
is_migration_file,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
from hooks_constants.unused_module_import_constants import ( # noqa: E402
|
|
25
|
-
MAX_UNUSED_IMPORT_ISSUES,
|
|
26
|
-
UNUSED_IMPORT_GUIDANCE,
|
|
27
|
-
line_suppresses_unused_import_via_noqa,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _import_alias_pairs(
|
|
32
|
-
import_node: ast.Import | ast.ImportFrom,
|
|
33
|
-
) -> list[tuple[str, int, int | None]]:
|
|
34
|
-
"""Return (binding_name, alias_line, from_keyword_line) for each name introduced.
|
|
35
|
-
|
|
36
|
-
The from-keyword line is None for plain `import X` statements; for
|
|
37
|
-
`from X import (...)` it carries the line of the `from` keyword so
|
|
38
|
-
callers can honor a `# noqa` placed on the opening line of a
|
|
39
|
-
multi-line import block.
|
|
40
|
-
"""
|
|
41
|
-
bindings: list[tuple[str, int, int | None]] = []
|
|
42
|
-
from_keyword_line = import_node.lineno if isinstance(import_node, ast.ImportFrom) else None
|
|
43
|
-
for each_alias in import_node.names:
|
|
44
|
-
if each_alias.name == "*":
|
|
45
|
-
continue
|
|
46
|
-
binding_name = each_alias.asname if each_alias.asname else each_alias.name.split(".")[0]
|
|
47
|
-
alias_line = each_alias.lineno or import_node.lineno
|
|
48
|
-
bindings.append((binding_name, alias_line, from_keyword_line))
|
|
49
|
-
return bindings
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _import_statement_line_ranges(tree: ast.Module) -> list[tuple[int, int]]:
|
|
53
|
-
ranges: list[tuple[int, int]] = []
|
|
54
|
-
for each_node in tree.body:
|
|
55
|
-
if isinstance(each_node, (ast.Import, ast.ImportFrom)):
|
|
56
|
-
start_line = each_node.lineno
|
|
57
|
-
end_line = each_node.end_lineno or each_node.lineno
|
|
58
|
-
ranges.append((start_line, end_line))
|
|
59
|
-
return ranges
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _line_number_falls_in_import_ranges(
|
|
63
|
-
line_number: int,
|
|
64
|
-
all_import_line_ranges: list[tuple[int, int]],
|
|
65
|
-
) -> bool:
|
|
66
|
-
for each_start, each_end in all_import_line_ranges:
|
|
67
|
-
if each_start <= line_number <= each_end:
|
|
68
|
-
return True
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _collect_load_names_outside_import_ranges(
|
|
73
|
-
tree: ast.Module,
|
|
74
|
-
all_import_line_ranges: list[tuple[int, int]],
|
|
75
|
-
) -> set[str]:
|
|
76
|
-
parent_by_node_id = _build_parent_map(tree)
|
|
77
|
-
referenced_names: set[str] = set()
|
|
78
|
-
for each_node in ast.walk(tree):
|
|
79
|
-
if isinstance(each_node, ast.Name) and isinstance(each_node.ctx, ast.Load):
|
|
80
|
-
line_number = each_node.lineno
|
|
81
|
-
if line_number is None or _line_number_falls_in_import_ranges(
|
|
82
|
-
line_number,
|
|
83
|
-
all_import_line_ranges,
|
|
84
|
-
):
|
|
85
|
-
continue
|
|
86
|
-
if _load_name_is_shadowed(each_node, each_node.id, parent_by_node_id):
|
|
87
|
-
continue
|
|
88
|
-
referenced_names.add(each_node.id)
|
|
89
|
-
elif isinstance(each_node, ast.Attribute) and isinstance(
|
|
90
|
-
each_node.ctx, ast.Load
|
|
91
|
-
):
|
|
92
|
-
line_number = each_node.lineno
|
|
93
|
-
if line_number is None or _line_number_falls_in_import_ranges(
|
|
94
|
-
line_number,
|
|
95
|
-
all_import_line_ranges,
|
|
96
|
-
):
|
|
97
|
-
continue
|
|
98
|
-
root_name = _attribute_root_name_if_loaded(each_node)
|
|
99
|
-
if root_name is not None and not _load_name_is_shadowed(
|
|
100
|
-
root_name,
|
|
101
|
-
root_name.id,
|
|
102
|
-
parent_by_node_id,
|
|
103
|
-
):
|
|
104
|
-
referenced_names.add(root_name.id)
|
|
105
|
-
referenced_names.update(_collect_string_annotation_names(tree))
|
|
106
|
-
return referenced_names
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _module_declares_dunder_all(tree: ast.Module) -> bool:
|
|
110
|
-
"""Return True when the module body assigns or annotates ``__all__``."""
|
|
111
|
-
return any(
|
|
112
|
-
(
|
|
113
|
-
isinstance(each_node, ast.Assign)
|
|
114
|
-
and any(
|
|
115
|
-
isinstance(each_target, ast.Name) and each_target.id == "__all__"
|
|
116
|
-
for each_target in each_node.targets
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
or (
|
|
120
|
-
isinstance(each_node, ast.AnnAssign)
|
|
121
|
-
and isinstance(each_node.target, ast.Name)
|
|
122
|
-
and each_node.target.id == "__all__"
|
|
123
|
-
)
|
|
124
|
-
for each_node in tree.body
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def check_unused_module_level_imports(
|
|
129
|
-
content: str,
|
|
130
|
-
file_path: str,
|
|
131
|
-
full_file_content: str | None = None,
|
|
132
|
-
) -> list[str]:
|
|
133
|
-
"""Flag module-level imports that are never referenced in the rest of the file.
|
|
134
|
-
|
|
135
|
-
References are detected from AST ``Name`` / ``Attribute`` loads outside import
|
|
136
|
-
statements so mentions in comments or string literals do not count. Files
|
|
137
|
-
declaring ``__all__`` (including annotated assignments) are skipped. A
|
|
138
|
-
``if TYPE_CHECKING:`` block does not exempt the file: its guarded imports
|
|
139
|
-
are nested inside the ``If`` node and are never scanned as top-level
|
|
140
|
-
bindings, while a dead top-level runtime import in the same file is still
|
|
141
|
-
flagged. Suppression honors bare ``# noqa`` or an explicit ``F401`` code in
|
|
142
|
-
the noqa list only.
|
|
143
|
-
|
|
144
|
-
When ``full_file_content`` is provided, ``content`` is treated as an Edit
|
|
145
|
-
fragment containing the imports being added or replaced, while the
|
|
146
|
-
``__all__`` gate detection and reference scanning run
|
|
147
|
-
against ``full_file_content`` (the post-edit file as it will look once the
|
|
148
|
-
Edit applies). This prevents false-positive flags on imports added in the
|
|
149
|
-
same Edit as their consumers.
|
|
150
|
-
"""
|
|
151
|
-
if is_migration_file(file_path):
|
|
152
|
-
return []
|
|
153
|
-
try:
|
|
154
|
-
fragment_tree = ast.parse(content)
|
|
155
|
-
except SyntaxError:
|
|
156
|
-
return []
|
|
157
|
-
reference_source = full_file_content if full_file_content is not None else content
|
|
158
|
-
try:
|
|
159
|
-
reference_tree = ast.parse(reference_source)
|
|
160
|
-
except SyntaxError:
|
|
161
|
-
return []
|
|
162
|
-
if _module_declares_dunder_all(reference_tree):
|
|
163
|
-
return []
|
|
164
|
-
fragment_lines = content.splitlines()
|
|
165
|
-
reference_import_ranges = _import_statement_line_ranges(reference_tree)
|
|
166
|
-
referenced_names = _collect_load_names_outside_import_ranges(
|
|
167
|
-
reference_tree,
|
|
168
|
-
reference_import_ranges,
|
|
169
|
-
)
|
|
170
|
-
import_bindings: list[tuple[str, int, int | None]] = []
|
|
171
|
-
for each_node in fragment_tree.body:
|
|
172
|
-
if isinstance(each_node, (ast.Import, ast.ImportFrom)):
|
|
173
|
-
if isinstance(each_node, ast.ImportFrom) and each_node.module == "__future__":
|
|
174
|
-
continue
|
|
175
|
-
for each_binding in _import_alias_pairs(each_node):
|
|
176
|
-
import_bindings.append(each_binding)
|
|
177
|
-
issues: list[str] = []
|
|
178
|
-
for each_name, each_line_number, each_from_keyword_line in import_bindings:
|
|
179
|
-
if 1 <= each_line_number <= len(fragment_lines):
|
|
180
|
-
if line_suppresses_unused_import_via_noqa(fragment_lines[each_line_number - 1]):
|
|
181
|
-
continue
|
|
182
|
-
if each_from_keyword_line is not None and 1 <= each_from_keyword_line <= len(
|
|
183
|
-
fragment_lines
|
|
184
|
-
):
|
|
185
|
-
if line_suppresses_unused_import_via_noqa(
|
|
186
|
-
fragment_lines[each_from_keyword_line - 1]
|
|
187
|
-
):
|
|
188
|
-
continue
|
|
189
|
-
if each_name in referenced_names:
|
|
190
|
-
continue
|
|
191
|
-
issues.append(
|
|
192
|
-
f"Line {each_line_number}: unused module-level import {each_name!r}"
|
|
193
|
-
f" — {UNUSED_IMPORT_GUIDANCE}"
|
|
194
|
-
)
|
|
195
|
-
if len(issues) >= MAX_UNUSED_IMPORT_ISSUES:
|
|
196
|
-
break
|
|
197
|
-
return issues
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Stop hook for bare hedging words that mark unverified claims.
|
|
3
|
-
|
|
4
|
-
Words like "likely" or "probably" stand in for evidence. When prose-style
|
|
5
|
-
enforcement is on, a hedge word blocks the response unless the same sentence
|
|
6
|
-
carries an explicit uncertainty label (unverified, I don't know, no source for
|
|
7
|
-
this claim). A label in another sentence does not exempt a bare hedge.
|
|
8
|
-
|
|
9
|
-
::
|
|
10
|
-
|
|
11
|
-
pass: This claim is unverified; the deploy is probably blocked.
|
|
12
|
-
flag: This claim is unverified. The deploy is probably blocked.
|
|
13
|
-
|
|
14
|
-
When enforcement is off, surviving bare hedges emit privacy-safe advisory
|
|
15
|
-
candidates (OP-07B). See rules/hedging-claims.md and
|
|
16
|
-
docs/references/prose-style-enforcement.md.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
import json
|
|
20
|
-
import os
|
|
21
|
-
import re
|
|
22
|
-
import sys
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
26
|
-
if _hooks_dir not in sys.path:
|
|
27
|
-
sys.path.insert(0, _hooks_dir)
|
|
28
|
-
|
|
29
|
-
from blocking.config.prose_style_enforcement_constants import ( # noqa: E402
|
|
30
|
-
prose_style_enforcement_enabled_in_environment,
|
|
31
|
-
)
|
|
32
|
-
from hooks_constants.hedging_uncertainty_constants import ( # noqa: E402
|
|
33
|
-
ALL_EXPLICIT_UNCERTAINTY_LABEL_PATTERNS,
|
|
34
|
-
BLOCK_REASON_PREFIX,
|
|
35
|
-
HEDGING_TERM_LIST_SEPARATOR,
|
|
36
|
-
POSITIVE_CORRECTIVE_GUIDANCE,
|
|
37
|
-
SENTENCE_BOUNDARY_PATTERN,
|
|
38
|
-
)
|
|
39
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
40
|
-
from hooks_constants.messages import USER_FACING_NOTICE # noqa: E402
|
|
41
|
-
from hooks_constants.prose_matcher_precision_constants import ( # noqa: E402
|
|
42
|
-
ADVISORY_CONTEXT_SNIPPET_MAX_CHARS,
|
|
43
|
-
MATCHER_ID_HEDGING_WORD,
|
|
44
|
-
MAXIMUM_ADVISORY_EMITS_PER_CALL,
|
|
45
|
-
)
|
|
46
|
-
from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
|
|
47
|
-
from observability.prose_matcher_advisory import emit_advisory_candidate # noqa: E402
|
|
48
|
-
|
|
49
|
-
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
50
|
-
|
|
51
|
-
RESEARCH_MODE_SKILL_SEARCH_PATHS = [
|
|
52
|
-
os.path.join(PLUGIN_ROOT, ".agents", "skills", "research-mode", "SKILL.md"),
|
|
53
|
-
os.path.join(PLUGIN_ROOT, "skills", "research-mode", "SKILL.md"),
|
|
54
|
-
os.path.join(os.path.expanduser("~"), ".claude", "skills", "research-mode", "SKILL.md"),
|
|
55
|
-
os.path.join(os.path.expanduser("~"), ".claude", "plugins", "marketplaces", "claude-deep-research", "skills", "research-mode", "SKILL.md"),
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
HEDGING_WORDS = [
|
|
59
|
-
r"\blikely\b",
|
|
60
|
-
r"\bunlikely\b",
|
|
61
|
-
r"\bprobably\b",
|
|
62
|
-
r"\bprobable\b",
|
|
63
|
-
r"\bpresumably\b",
|
|
64
|
-
r"\bperhaps\b",
|
|
65
|
-
r"\bpossibly\b",
|
|
66
|
-
r"\bseemingly\b",
|
|
67
|
-
r"\bapparently\b",
|
|
68
|
-
r"\barguably\b",
|
|
69
|
-
r"\bsupposedly\b",
|
|
70
|
-
r"\bostensibly\b",
|
|
71
|
-
r"\bconceivably\b",
|
|
72
|
-
r"\bplausibly\b",
|
|
73
|
-
]
|
|
74
|
-
|
|
75
|
-
HEDGING_PHRASES = [
|
|
76
|
-
r"\bmight be\b",
|
|
77
|
-
r"\bcould be\b",
|
|
78
|
-
r"\bseems to be\b",
|
|
79
|
-
r"\bappears to be\b",
|
|
80
|
-
r"\bin all likelihood\b",
|
|
81
|
-
r"\bmore likely than not\b",
|
|
82
|
-
r"\bit.s possible that\b",
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
ALL_HEDGING_PATTERNS = [
|
|
86
|
-
re.compile(pattern, re.IGNORECASE) for pattern in HEDGING_WORDS + HEDGING_PHRASES
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _sentence_carries_explicit_uncertainty(sentence: str) -> bool:
|
|
91
|
-
"""Return True when the sentence labels the claim as unknown or unverified."""
|
|
92
|
-
for each_label_pattern in ALL_EXPLICIT_UNCERTAINTY_LABEL_PATTERNS:
|
|
93
|
-
if each_label_pattern.search(sentence):
|
|
94
|
-
return True
|
|
95
|
-
return False
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _hedging_terms_in_prose_slice(prose_slice: str) -> list[str]:
|
|
99
|
-
"""Return unique hedge terms found in one prose slice (sentence or full text)."""
|
|
100
|
-
matched_terms: list[str] = []
|
|
101
|
-
for each_pattern in ALL_HEDGING_PATTERNS:
|
|
102
|
-
for each_match in each_pattern.findall(prose_slice):
|
|
103
|
-
normalized_term = each_match.strip().lower()
|
|
104
|
-
if normalized_term not in matched_terms:
|
|
105
|
-
matched_terms.append(normalized_term)
|
|
106
|
-
return matched_terms
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def find_hedging_words(text: str) -> list[str]:
|
|
110
|
-
"""Return all hedging words/phrases in the stripped prose (any sentence)."""
|
|
111
|
-
prose_text = strip_code_and_quotes(text)
|
|
112
|
-
return _hedging_terms_in_prose_slice(prose_text)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def find_blocking_hedging_terms(text: str) -> list[str]:
|
|
116
|
-
"""Return hedge terms that lack an explicit uncertainty label in-sentence.
|
|
117
|
-
|
|
118
|
-
::
|
|
119
|
-
|
|
120
|
-
find_blocking_hedging_terms("Unverified: the port is probably down.")
|
|
121
|
-
-> []
|
|
122
|
-
find_blocking_hedging_terms("Unverified. The port is probably down.")
|
|
123
|
-
-> ["probably"]
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
text: Full assistant message, including code and quotes.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
Unique bare hedge terms that still warrant a block or advisory emit.
|
|
130
|
-
"""
|
|
131
|
-
prose_text = strip_code_and_quotes(text)
|
|
132
|
-
if not prose_text.strip():
|
|
133
|
-
return []
|
|
134
|
-
|
|
135
|
-
all_sentences = SENTENCE_BOUNDARY_PATTERN.split(prose_text)
|
|
136
|
-
if len(all_sentences) <= 1:
|
|
137
|
-
all_sentences = [prose_text]
|
|
138
|
-
|
|
139
|
-
matched_terms: list[str] = []
|
|
140
|
-
for each_sentence in all_sentences:
|
|
141
|
-
if not each_sentence.strip():
|
|
142
|
-
continue
|
|
143
|
-
if _sentence_carries_explicit_uncertainty(each_sentence):
|
|
144
|
-
continue
|
|
145
|
-
for each_term in _hedging_terms_in_prose_slice(each_sentence):
|
|
146
|
-
if each_term not in matched_terms:
|
|
147
|
-
matched_terms.append(each_term)
|
|
148
|
-
return matched_terms
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def main() -> None:
|
|
152
|
-
try:
|
|
153
|
-
hook_input = json.load(sys.stdin)
|
|
154
|
-
except json.JSONDecodeError:
|
|
155
|
-
sys.exit(0)
|
|
156
|
-
|
|
157
|
-
if hook_input.get("stop_hook_active", False):
|
|
158
|
-
sys.exit(0)
|
|
159
|
-
|
|
160
|
-
assistant_message = hook_input.get("last_assistant_message", "")
|
|
161
|
-
|
|
162
|
-
if not assistant_message:
|
|
163
|
-
sys.exit(0)
|
|
164
|
-
|
|
165
|
-
found_hedging_terms = find_blocking_hedging_terms(assistant_message)
|
|
166
|
-
|
|
167
|
-
if not found_hedging_terms:
|
|
168
|
-
sys.exit(0)
|
|
169
|
-
|
|
170
|
-
if not prose_style_enforcement_enabled_in_environment():
|
|
171
|
-
try:
|
|
172
|
-
for each_term in found_hedging_terms[:MAXIMUM_ADVISORY_EMITS_PER_CALL]:
|
|
173
|
-
emit_advisory_candidate(
|
|
174
|
-
MATCHER_ID_HEDGING_WORD,
|
|
175
|
-
"Stop",
|
|
176
|
-
f"{each_term}:{assistant_message[:ADVISORY_CONTEXT_SNIPPET_MAX_CHARS]}",
|
|
177
|
-
)
|
|
178
|
-
except (ImportError, OSError, TypeError, ValueError):
|
|
179
|
-
pass
|
|
180
|
-
sys.exit(0)
|
|
181
|
-
|
|
182
|
-
formatted_term_list = HEDGING_TERM_LIST_SEPARATOR.join(
|
|
183
|
-
f'"{term}"' for term in found_hedging_terms
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
resolved_skill_path: str | None = None
|
|
187
|
-
for each_skill_path in RESEARCH_MODE_SKILL_SEARCH_PATHS:
|
|
188
|
-
if os.path.exists(each_skill_path):
|
|
189
|
-
resolved_skill_path = each_skill_path
|
|
190
|
-
break
|
|
191
|
-
|
|
192
|
-
if resolved_skill_path is not None:
|
|
193
|
-
skill_reference = f"under the research-mode constraints defined in:\n\n{resolved_skill_path}"
|
|
194
|
-
else:
|
|
195
|
-
skill_reference = (
|
|
196
|
-
"under research-mode constraints "
|
|
197
|
-
"(no research-mode skill installed; verify with sources or prompt the user via AskUserQuestion with potential options + context)"
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
block_reason = (
|
|
201
|
-
f"{BLOCK_REASON_PREFIX}{formatted_term_list}. "
|
|
202
|
-
f"Rewrite {skill_reference}\n\n"
|
|
203
|
-
f"{POSITIVE_CORRECTIVE_GUIDANCE}"
|
|
204
|
-
)
|
|
205
|
-
block_response = {
|
|
206
|
-
"decision": "block",
|
|
207
|
-
"reason": block_reason,
|
|
208
|
-
"systemMessage": USER_FACING_NOTICE,
|
|
209
|
-
"suppressOutput": True,
|
|
210
|
-
}
|
|
211
|
-
log_hook_block(
|
|
212
|
-
calling_hook_name="hedging_language_blocker.py",
|
|
213
|
-
hook_event="Stop",
|
|
214
|
-
block_reason=block_reason,
|
|
215
|
-
)
|
|
216
|
-
print(json.dumps(block_response))
|
|
217
|
-
sys.exit(0)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if __name__ == "__main__":
|
|
221
|
-
main()
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Stop hook that blocks a turn ending with a promise of future work.
|
|
3
|
-
|
|
4
|
-
When a turn ends with a forward-looking statement of intent ("I'll now run the
|
|
5
|
-
tests", "Let me implement the fix"), complete the work with tool calls during
|
|
6
|
-
the turn. Route work requiring user-only input through AskUserQuestion. The
|
|
7
|
-
rule name is long-horizon-autonomy.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import re
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
16
|
-
if _hooks_dir not in sys.path:
|
|
17
|
-
sys.path.insert(0, _hooks_dir)
|
|
18
|
-
|
|
19
|
-
from blocking.config.prose_style_enforcement_constants import ( # noqa: E402
|
|
20
|
-
prose_style_enforcement_enabled_in_environment,
|
|
21
|
-
)
|
|
22
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
23
|
-
from hooks_constants.messages import USER_FACING_INTENT_ENDING_NOTICE # noqa: E402
|
|
24
|
-
from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def extract_final_paragraph(text: str) -> str:
|
|
28
|
-
"""Return the last non-empty paragraph of the prose after stripping code and quotes.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
text: The raw assistant message to extract the closing paragraph from.
|
|
32
|
-
"""
|
|
33
|
-
paragraph_split_pattern = re.compile(r"\n\s*\n")
|
|
34
|
-
prose_text = strip_code_and_quotes(text)
|
|
35
|
-
candidate_paragraphs = [
|
|
36
|
-
each_paragraph.strip()
|
|
37
|
-
for each_paragraph in paragraph_split_pattern.split(prose_text)
|
|
38
|
-
if each_paragraph.strip()
|
|
39
|
-
]
|
|
40
|
-
if not candidate_paragraphs:
|
|
41
|
-
return ""
|
|
42
|
-
return candidate_paragraphs[-1]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def extract_first_sentence(paragraph: str) -> str:
|
|
46
|
-
"""Return the first sentence of a paragraph.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
paragraph: The paragraph whose leading sentence is needed.
|
|
50
|
-
"""
|
|
51
|
-
sentence_boundary_pattern = re.compile(r"(?<=[.!?])\s+")
|
|
52
|
-
sentences = sentence_boundary_pattern.split(paragraph.strip(), maxsplit=1)
|
|
53
|
-
if not sentences:
|
|
54
|
-
return ""
|
|
55
|
-
return sentences[0].strip()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def find_intent_only_ending(text: str) -> bool:
|
|
59
|
-
"""Return whether the final paragraph ends the turn on a promise about undone work.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
text: The raw assistant message to evaluate.
|
|
63
|
-
"""
|
|
64
|
-
future_intent_opener_pattern = re.compile(
|
|
65
|
-
r"^(?:(?:now|next|then|okay|alright)\s*,?\s*)?"
|
|
66
|
-
r"(?:i['’]ll(?:\s+now|\s+go\s+ahead\s+and|\s+proceed\s+to)?"
|
|
67
|
-
r"|i\s+will"
|
|
68
|
-
r"|i['’]m\s+going\s+to"
|
|
69
|
-
r"|i\s+am\s+going\s+to"
|
|
70
|
-
r"|i['’]m\s+about\s+to"
|
|
71
|
-
r"|i\s+plan\s+to"
|
|
72
|
-
r"|let\s+me"
|
|
73
|
-
r"|let['’]s"
|
|
74
|
-
r"|going\s+to)\b",
|
|
75
|
-
re.IGNORECASE,
|
|
76
|
-
)
|
|
77
|
-
undone_work_verb_pattern = re.compile(
|
|
78
|
-
r"\b(?:run|start|implement|create|write|add|fix|update|check|test|wire"
|
|
79
|
-
r"|build|deploy|push|git\s+commit|commit\s+the\s+changes"
|
|
80
|
-
r"|commit\s+the\s+fix|investigate|set\s+up|refactor|generate"
|
|
81
|
-
r"|install|continue|look\s+into)\b",
|
|
82
|
-
re.IGNORECASE,
|
|
83
|
-
)
|
|
84
|
-
next_steps_lead_in_pattern = re.compile(r"^next\s+steps?:", re.IGNORECASE)
|
|
85
|
-
second_person_subject_pattern = re.compile(
|
|
86
|
-
r"\b(?:you|your|you['’]?re|user['’]?s?)\b",
|
|
87
|
-
re.IGNORECASE,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
final_paragraph = extract_final_paragraph(text)
|
|
91
|
-
if not final_paragraph:
|
|
92
|
-
return False
|
|
93
|
-
|
|
94
|
-
if next_steps_lead_in_pattern.match(final_paragraph):
|
|
95
|
-
return not second_person_subject_pattern.search(final_paragraph)
|
|
96
|
-
|
|
97
|
-
first_sentence = extract_first_sentence(final_paragraph)
|
|
98
|
-
if not future_intent_opener_pattern.match(first_sentence):
|
|
99
|
-
return False
|
|
100
|
-
|
|
101
|
-
return bool(undone_work_verb_pattern.search(first_sentence))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def main() -> None:
|
|
105
|
-
"""Read the stop-hook payload and block turns that promise future work."""
|
|
106
|
-
try:
|
|
107
|
-
hook_input = json.load(sys.stdin)
|
|
108
|
-
except json.JSONDecodeError:
|
|
109
|
-
sys.exit(0)
|
|
110
|
-
|
|
111
|
-
if hook_input.get("stop_hook_active", False):
|
|
112
|
-
sys.exit(0)
|
|
113
|
-
|
|
114
|
-
if not prose_style_enforcement_enabled_in_environment():
|
|
115
|
-
sys.exit(0)
|
|
116
|
-
|
|
117
|
-
assistant_message = hook_input.get("last_assistant_message", "")
|
|
118
|
-
|
|
119
|
-
if not assistant_message:
|
|
120
|
-
sys.exit(0)
|
|
121
|
-
|
|
122
|
-
if not find_intent_only_ending(assistant_message):
|
|
123
|
-
sys.exit(0)
|
|
124
|
-
|
|
125
|
-
block_reason = (
|
|
126
|
-
"LONG-HORIZON-AUTONOMY GUARDRAIL: This turn ends with a promise of future "
|
|
127
|
-
"work. Complete that work with tool calls during this turn. Route questions "
|
|
128
|
-
"requiring user-only input through AskUserQuestion, then end the turn cleanly.\n\n"
|
|
129
|
-
"Re-output the complete response with the work performed, per the "
|
|
130
|
-
"long-horizon-autonomy rule."
|
|
131
|
-
)
|
|
132
|
-
block_response = {
|
|
133
|
-
"decision": "block",
|
|
134
|
-
"reason": block_reason,
|
|
135
|
-
"systemMessage": USER_FACING_INTENT_ENDING_NOTICE,
|
|
136
|
-
"suppressOutput": True,
|
|
137
|
-
}
|
|
138
|
-
log_hook_block(
|
|
139
|
-
calling_hook_name="intent_only_ending_blocker.py",
|
|
140
|
-
hook_event="Stop",
|
|
141
|
-
block_reason=block_reason,
|
|
142
|
-
)
|
|
143
|
-
print(json.dumps(block_response))
|
|
144
|
-
sys.exit(0)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if __name__ == "__main__":
|
|
148
|
-
main()
|