claude-dev-env 1.58.0 → 1.60.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 +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +439 -0
- package/hooks/blocking/code_rules_enforcer.py +190 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +58 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/file-global-constants.md +7 -1
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +67 -19
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +234 -42
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/rebase/SKILL.md +2 -4
- package/skills/update/SKILL.md +37 -5
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Segment-splitting and command-name constants for the destructive command blocker compound rm guard."""
|
|
2
|
+
|
|
3
|
+
ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset({"&&", "||", ";", "|&", "|", "&", "\n", "\r"})
|
|
4
|
+
ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
5
|
+
{
|
|
6
|
+
"timeout",
|
|
7
|
+
"nohup",
|
|
8
|
+
"nice",
|
|
9
|
+
"ionice",
|
|
10
|
+
"stdbuf",
|
|
11
|
+
"time",
|
|
12
|
+
"setsid",
|
|
13
|
+
"chrt",
|
|
14
|
+
"taskset",
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
ALL_LAUNCHERS_REQUIRING_A_POSITIONAL_VALUE: frozenset[str] = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"timeout",
|
|
20
|
+
"chrt",
|
|
21
|
+
"taskset",
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
ALL_INTERPRETER_AND_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"sh",
|
|
27
|
+
"bash",
|
|
28
|
+
"zsh",
|
|
29
|
+
"dash",
|
|
30
|
+
"ksh",
|
|
31
|
+
"tcsh",
|
|
32
|
+
"csh",
|
|
33
|
+
"fish",
|
|
34
|
+
"pwsh",
|
|
35
|
+
"powershell",
|
|
36
|
+
"cmd",
|
|
37
|
+
"eval",
|
|
38
|
+
"exec",
|
|
39
|
+
"source",
|
|
40
|
+
"sudo",
|
|
41
|
+
"su",
|
|
42
|
+
"env",
|
|
43
|
+
"xargs",
|
|
44
|
+
"awk",
|
|
45
|
+
"gawk",
|
|
46
|
+
"mawk",
|
|
47
|
+
"nawk",
|
|
48
|
+
"make",
|
|
49
|
+
"tclsh",
|
|
50
|
+
"expect",
|
|
51
|
+
"lua",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
ALL_REMOTE_AND_PROGRAM_STRING_EXECUTORS: frozenset[str] = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"ssh",
|
|
57
|
+
"python",
|
|
58
|
+
"python2",
|
|
59
|
+
"python3",
|
|
60
|
+
"perl",
|
|
61
|
+
"ruby",
|
|
62
|
+
"node",
|
|
63
|
+
"deno",
|
|
64
|
+
"bun",
|
|
65
|
+
"php",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
ALL_STRING_ARGUMENT_EXECUTION_FLAGS: frozenset[str] = frozenset({"-c", "-e"})
|
|
69
|
+
ALL_BENIGN_COMPOUND_SEGMENT_COMMANDS: frozenset[str] = frozenset(
|
|
70
|
+
{
|
|
71
|
+
"echo",
|
|
72
|
+
"printf",
|
|
73
|
+
"gh",
|
|
74
|
+
"head",
|
|
75
|
+
"tail",
|
|
76
|
+
"cat",
|
|
77
|
+
"ls",
|
|
78
|
+
"grep",
|
|
79
|
+
"wc",
|
|
80
|
+
"sort",
|
|
81
|
+
"uniq",
|
|
82
|
+
"true",
|
|
83
|
+
"git",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
OUTPUT_REDIRECTION_OPERATOR_PATTERN: str = r"(?:\d+|&)?>>?\|?(?!&[\d-])"
|
|
87
|
+
ALL_FILE_WRITING_OUTPUT_FLAGS_BY_BENIGN_PROGRAM: dict[str, frozenset[str]] = {
|
|
88
|
+
"sort": frozenset({"-o", "--output"}),
|
|
89
|
+
}
|
|
90
|
+
ALL_GIT_CONFIG_READ_ONLY_FLAGS: frozenset[str] = frozenset(
|
|
91
|
+
{"--get", "--get-all", "--get-regexp", "--list", "-l", "--get-urlmatch"}
|
|
92
|
+
)
|
|
93
|
+
ALL_GIT_REMOTE_READ_ONLY_VERBS: frozenset[str] = frozenset({"show", "get-url"})
|
|
94
|
+
ALL_GIT_FETCH_FORCE_FLAGS: frozenset[str] = frozenset({"-f", "--force"})
|
|
95
|
+
ALL_GH_HTTP_WRITE_METHOD_FLAGS: frozenset[str] = frozenset({"-X", "--method"})
|
|
96
|
+
ALL_GH_HTTP_WRITE_METHODS: frozenset[str] = frozenset({"POST", "PUT", "PATCH", "DELETE"})
|
|
97
|
+
GH_HTTP_READ_ONLY_METHOD: str = "GET"
|
|
98
|
+
GH_SHORT_METHOD_FLAG_PREFIX: str = "-X"
|
|
99
|
+
GH_LONG_METHOD_FLAG_EQUALS_PREFIX: str = "--method="
|
|
100
|
+
ALL_GH_API_REQUEST_BODY_FIELD_FLAGS: frozenset[str] = frozenset(
|
|
101
|
+
{"-f", "--raw-field", "-F", "--field", "--input"}
|
|
102
|
+
)
|
|
103
|
+
ALL_GH_API_GLUED_REQUEST_BODY_FIELD_FLAG_PREFIXES: tuple[str, ...] = (
|
|
104
|
+
"-f",
|
|
105
|
+
"-F",
|
|
106
|
+
"--raw-field=",
|
|
107
|
+
"--field=",
|
|
108
|
+
"--input=",
|
|
109
|
+
)
|
|
110
|
+
ALL_READ_ONLY_GIT_SUBCOMMANDS: frozenset[str] = frozenset(
|
|
111
|
+
{
|
|
112
|
+
"status",
|
|
113
|
+
"log",
|
|
114
|
+
"show",
|
|
115
|
+
"diff",
|
|
116
|
+
"rev-parse",
|
|
117
|
+
"rev-list",
|
|
118
|
+
"describe",
|
|
119
|
+
"config",
|
|
120
|
+
"remote",
|
|
121
|
+
"fetch",
|
|
122
|
+
"ls-files",
|
|
123
|
+
"ls-remote",
|
|
124
|
+
"ls-tree",
|
|
125
|
+
"cat-file",
|
|
126
|
+
"blame",
|
|
127
|
+
"shortlog",
|
|
128
|
+
"name-rev",
|
|
129
|
+
"for-each-ref",
|
|
130
|
+
"symbolic-ref",
|
|
131
|
+
"merge-base",
|
|
132
|
+
"count-objects",
|
|
133
|
+
"version",
|
|
134
|
+
"help",
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
ALL_READ_ONLY_GH_SUBCOMMANDS: frozenset[str] = frozenset(
|
|
138
|
+
{
|
|
139
|
+
"view",
|
|
140
|
+
"list",
|
|
141
|
+
"status",
|
|
142
|
+
"checks",
|
|
143
|
+
"diff",
|
|
144
|
+
"search",
|
|
145
|
+
"browse",
|
|
146
|
+
"api",
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
ALL_READ_ONLY_SUBCOMMANDS_BY_DISPATCHING_PROGRAM: dict[str, frozenset[str]] = {
|
|
150
|
+
"git": ALL_READ_ONLY_GIT_SUBCOMMANDS,
|
|
151
|
+
"gh": ALL_READ_ONLY_GH_SUBCOMMANDS,
|
|
152
|
+
}
|
|
153
|
+
ALL_READ_ONLY_SUBCOMMAND_POSITION_DEPTHS_BY_DISPATCHING_PROGRAM: dict[str, int] = {
|
|
154
|
+
"git": 1,
|
|
155
|
+
"gh": 2,
|
|
156
|
+
}
|
|
157
|
+
LAUNCHER_POSITIONAL_VALUE_SHAPE_PATTERN: str = (
|
|
158
|
+
r"^(?:0x[0-9A-Fa-f]+"
|
|
159
|
+
r"|[0-9]+(?:[.,][0-9]+)?[smhd]?"
|
|
160
|
+
r"|[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*)$"
|
|
161
|
+
)
|
|
162
|
+
ALL_LAUNCHER_OPTIONS_TAKING_SEPARATE_VALUE: frozenset[str] = frozenset(
|
|
163
|
+
{
|
|
164
|
+
"-s",
|
|
165
|
+
"--signal",
|
|
166
|
+
"-k",
|
|
167
|
+
"--kill-after",
|
|
168
|
+
"-n",
|
|
169
|
+
"-o",
|
|
170
|
+
"--output",
|
|
171
|
+
"-e",
|
|
172
|
+
"--error",
|
|
173
|
+
"-i",
|
|
174
|
+
"--input",
|
|
175
|
+
"--classdata",
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
ALL_SUBSHELL_GROUPING_CHARACTERS: str = "({"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Constants for the duplicate-function-body scan in ``code_rules_enforcer``.
|
|
2
|
+
|
|
3
|
+
The blocking scan flags a top-level function whose body is structurally identical
|
|
4
|
+
to a top-level function already defined in a sibling ``.py`` module in the same
|
|
5
|
+
directory. This catches the Reuse-before-create / DRY violation where a helper is
|
|
6
|
+
copy-pasted across several modules instead of imported from one shared home.
|
|
7
|
+
|
|
8
|
+
The ``CROSS_SKILL_*`` and ``SKILL*`` constants feed the non-blocking companion
|
|
9
|
+
advisory: a helper copied between two skills' ``scripts`` directories, where a
|
|
10
|
+
shared module would break independent install. That advisory names the source
|
|
11
|
+
skill on stderr rather than denying the write.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
MINIMUM_DUPLICATE_BODY_STATEMENTS: int = 3
|
|
15
|
+
MAX_DUPLICATE_BODY_ISSUES: int = 25
|
|
16
|
+
DUNDER_INIT_FILENAME: str = "__init__.py"
|
|
17
|
+
PYTHON_SOURCE_SUFFIX: str = ".py"
|
|
18
|
+
DUPLICATE_BODY_GUIDANCE: str = (
|
|
19
|
+
"this function body is identical to one in a sibling module; "
|
|
20
|
+
"extract a single shared helper (for example in hooks_constants/) and "
|
|
21
|
+
"import it from both modules instead of copying it (Reuse before create / DRY)"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
SKILLS_DIRECTORY_NAME: str = "skills"
|
|
25
|
+
SKILL_SCRIPTS_DIRECTORY_NAME: str = "scripts"
|
|
26
|
+
MAX_CROSS_SKILL_ADVISORY_ISSUES: int = 25
|
|
27
|
+
CROSS_SKILL_ADVISORY_PREFIX: str = "[CODE_RULES advisory]"
|
|
28
|
+
CROSS_SKILL_DUPLICATE_GUIDANCE: str = (
|
|
29
|
+
"two skill folders install on their own, so this copy is a defensible "
|
|
30
|
+
"skill-isolation tradeoff; a shared module would couple the skills and "
|
|
31
|
+
"break independent install. Confirm the copy is intentional, or for a "
|
|
32
|
+
"large or behavior-bearing body raise the choice through AskUserQuestion "
|
|
33
|
+
"(see the no-cross-skill-duplicate-helpers rule)"
|
|
34
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Configuration constants for the hook_prose_detector_consistency PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
WRITE_TOOL_NAME: str = "Write"
|
|
4
|
+
EDIT_TOOL_NAME: str = "Edit"
|
|
5
|
+
|
|
6
|
+
HOOK_MODULE_PATH_SEGMENT: str = "/hooks/"
|
|
7
|
+
PYTHON_FILE_SUFFIX: str = ".py"
|
|
8
|
+
CONSTANTS_MODULE_SUFFIX: str = "_constants.py"
|
|
9
|
+
TEST_MODULE_PREFIX: str = "test_"
|
|
10
|
+
|
|
11
|
+
PATH_SEPARATOR_CLASS_PATTERN: str = (
|
|
12
|
+
r"\[[^\]/]*\\\\[^\]/]*\]|\[[^\]]*\\\\?/[^\]]*\]|\[[^\]]*/\\\\?[^\]]*\]"
|
|
13
|
+
)
|
|
14
|
+
OVERSTATED_OUTPUT_KEY_PHRASE_PATTERN: str = r"output[- ]key\s+segment"
|
|
15
|
+
|
|
16
|
+
CORRECTIVE_MESSAGE: str = (
|
|
17
|
+
"BLOCKED [hook-prose-detector-consistency]: A hook module's user-facing prose "
|
|
18
|
+
"(its docstring lead narrative or CORRECTIVE_MESSAGE) claims the hook blocks an "
|
|
19
|
+
"'output-key segment', but the module's detector keys off a path separator only "
|
|
20
|
+
"(it matches a token next to `\\` or `/`). A quoted structured-output key alone "
|
|
21
|
+
"never triggers a block, so the prose overstates the contract: an author whose "
|
|
22
|
+
"only per-iteration token is an output key would never see this message, and an "
|
|
23
|
+
"author who does see it is told an output key caused a block it cannot cause.\n\n"
|
|
24
|
+
"Describe only the trigger the detector implements: a per-iteration path segment "
|
|
25
|
+
"next to a path separator. Drop 'or output-key segment' (or restate it as 'a "
|
|
26
|
+
"per-iteration path segment') so the message and docstring match what the regex "
|
|
27
|
+
"catches.\n\n"
|
|
28
|
+
"Invariant: a hook's docstring and corrective message describe exactly the shapes "
|
|
29
|
+
"its detector flags -- no broader trigger surface than the regex enforces."
|
|
30
|
+
)
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
GIT_DASH_C_COMMIT_PATTERN: str = r"git\s+-C\s+[\"']?[^\"';&|]+?[\"']?\s+commit\b"
|
|
10
10
|
GIT_COMMAND_TIMEOUT_SECONDS: int = 5
|
|
11
|
-
GATE_TIMEOUT_SECONDS: int =
|
|
11
|
+
GATE_TIMEOUT_SECONDS: int = 120
|
|
12
12
|
GATE_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts" / "code_rules_gate.py"
|
|
13
13
|
ALL_STAGED_PYTHON_FILES_COMMAND: tuple[str, ...] = (
|
|
14
14
|
"git",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Configuration constants for the workflow_substitution_slot_blocker PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
WRITE_TOOL_NAME: str = "Write"
|
|
4
|
+
EDIT_TOOL_NAME: str = "Edit"
|
|
5
|
+
MULTI_EDIT_TOOL_NAME: str = "MultiEdit"
|
|
6
|
+
|
|
7
|
+
WORKFLOW_FILE_SUFFIX: str = ".workflow.js"
|
|
8
|
+
|
|
9
|
+
CORRECTIVE_MESSAGE: str = (
|
|
10
|
+
"BLOCKED [workflow-substitution-slot]: A bare per-iteration index token "
|
|
11
|
+
"(for example `cand_i`) appears as a per-iteration path segment inside a "
|
|
12
|
+
".workflow.js agent-prompt block that loops over an index. A bare `_i` "
|
|
13
|
+
"token reads as a fixed literal, so an agent can create one literal "
|
|
14
|
+
"directory and overwrite it across every iteration -- collapsing an "
|
|
15
|
+
"N-iteration gate into one.\n\n"
|
|
16
|
+
"Mark the index as a substitution slot with the angle-bracket convention "
|
|
17
|
+
"this template already uses for per-call values (`<plate.svg>`, `<glow_hex>`): "
|
|
18
|
+
"write `cand_<i>` instead of `cand_i`, or spell out 'replace <i> with the "
|
|
19
|
+
"iteration index 0, 1, 2' in the step text.\n\n"
|
|
20
|
+
"Convention: every per-call substitution slot in a .workflow.js template is "
|
|
21
|
+
"marked with angle brackets, so an agent fills in a fresh value per call."
|
|
22
|
+
)
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Docstring Prose Matches Implementation
|
|
2
|
+
|
|
3
|
+
**When this applies:** Any Write or Edit to a public function, method, class, or module whose docstring prose makes an enumerable claim about behavior — a list of inputs the code handles, the conditions it treats as a match, the cases it skips, or the order of its steps.
|
|
4
|
+
|
|
5
|
+
## Rule
|
|
6
|
+
|
|
7
|
+
When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
|
|
8
|
+
|
|
9
|
+
The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"` — has no signature to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose. It carries documented-but-pending hook coverage; the audit lane below is the enforcement until a deterministic gate check exists.
|
|
10
|
+
|
|
11
|
+
## What to check before you write the docstring
|
|
12
|
+
|
|
13
|
+
Read the body and the docstring side by side:
|
|
14
|
+
|
|
15
|
+
- **Read-source / match-source unions.** A body that computes `read_names = a | b | c` (or any union of "what counts") names each union member in the prose enumeration. A union member the code applies but the prose omits is a gap.
|
|
16
|
+
- **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
|
|
17
|
+
- **Step order.** A docstring that says `A then B then C` matches the call order in the body.
|
|
18
|
+
- **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
|
|
19
|
+
|
|
20
|
+
When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
|
|
21
|
+
|
|
22
|
+
## Worked example
|
|
23
|
+
|
|
24
|
+
A `@dataclass` dead-field check builds its set of "field counts as read" sources by union:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
read_names = (
|
|
28
|
+
attribute_read_names
|
|
29
|
+
| dynamic_literal_names
|
|
30
|
+
| _match_pattern_attribute_names(tree)
|
|
31
|
+
| _exported_names(tree)
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A docstring that enumerates "attribute read, augmented-assignment target, class-pattern keyword, literal `getattr`/`attrgetter`" but omits the `__all__` source (`_exported_names`) is drifted: a field whose name appears in `__all__` is treated as read, and the prose hides that. The fix adds the missing source to the enumeration so the list matches the union.
|
|
36
|
+
|
|
37
|
+
## Enforcement (audit lane)
|
|
38
|
+
|
|
39
|
+
This drift class is sub-bucket **O6** in `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` (free-form `Note:` / `Returns:` / responsibility-list claims). The audit teammate lists every prose enumeration in a changed docstring and verifies each item against the body, and lists every union member / suppressor / step in the body and verifies each appears in the prose. A union member or suppressor in the body that the prose omits is an O6 finding.
|
|
40
|
+
|
|
41
|
+
## Why
|
|
42
|
+
|
|
43
|
+
A docstring enumeration earns its place by being trustworthy. A complete list lets a reader reason about the function without scanning the body; a list missing one item is worse than no list, because it asserts completeness it does not have. Naming this standard makes the gap a first-class finding at write time and at audit, rather than a surprise a reader hits months later.
|
|
@@ -20,7 +20,13 @@ Test-file detection uses the following anchored patterns against the full relati
|
|
|
20
20
|
|
|
21
21
|
## `config/` files are exempt
|
|
22
22
|
|
|
23
|
-
Constants placed in `config/` satisfy the constants-location rule; the use-count
|
|
23
|
+
Constants placed in `config/` satisfy the constants-location rule; the use-count rule applies only to production code outside `config/`.
|
|
24
|
+
|
|
25
|
+
## Dead constant in a dedicated constants module (cross-module)
|
|
26
|
+
|
|
27
|
+
The use-count rule above governs a file-global constant in production code outside `config/` by counting same-file references. A dedicated constants module — a file whose name ends in `_constants.py`, or any module under a `config/` directory — exports its constants to importer modules elsewhere, so a same-file count proves nothing. A separate hook, `check_dead_module_constants` (dispatched from `code_rules_enforcer`), governs these modules: it flags an `UPPER_SNAKE` constant defined in the written module whose name appears in no `.py` module anywhere under the enclosing package tree — not imported, not read, not listed in an `__all__` literal, not named in a string annotation. That is the dead exported constant CODE_RULES §9.8 targets, caught at Write/Edit time.
|
|
28
|
+
|
|
29
|
+
The scan resolves the enclosing package tree from the written file: for a constants module inside a package subdirectory, the tree is the package's parent (so an importer one directory up is in scope); for a `config/` module, the tree is the parent of the `config` directory. A module that declares its own `__all__` is skipped — the author's explicit export surface is taken as the liveness contract. A reference from a test module under the tree keeps a constant live. Test modules and migration modules are themselves exempt from the check.
|
|
24
30
|
|
|
25
31
|
## Examples
|
|
26
32
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths: **/hooks/**/*.py
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Hook Prose Matches Its Detector
|
|
6
|
+
|
|
7
|
+
**When this applies:** Any Write or Edit to a hook module (`.py` under `hooks/`) or its `*_constants.py` companion.
|
|
8
|
+
|
|
9
|
+
**Hook enforcement:** `hook-prose-detector-consistency` (PreToolUse on Write|Edit) blocks a hook whose user-facing prose claims a trigger its detector never fires on. See `hooks.json` for registration.
|
|
10
|
+
|
|
11
|
+
## Rule
|
|
12
|
+
|
|
13
|
+
A hook's docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags — no broader trigger surface than the regex enforces. An author reads the corrective message to learn what they did wrong; an author reads the docstring to learn what the hook guards. When either claims a trigger the detector cannot fire on, both audiences are misled: an author whose only token is that shape never sees the block, and an author who does see the block is told the wrong cause.
|
|
14
|
+
|
|
15
|
+
## The path-shape blocker case
|
|
16
|
+
|
|
17
|
+
A path-shape blocker detects a per-iteration token only when the token sits next to a path separator (its detection regex keys off a `[\\/]`-style character class). Such a hook must not claim it blocks an "output-key segment": a quoted structured-output key alone, with no looped path, is never flagged. The `*_constants.py` companion holds the corrective message and not the detector, so the phrase "output-key segment" describing a blocked trigger is itself the violation there, regardless of which file holds the regex.
|
|
18
|
+
|
|
19
|
+
| Prohibited claim | Why it overstates | Correct phrasing |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| "appears as a path or output-key segment" | the detector keys off a path separator only | "appears as a per-iteration path segment" |
|
|
22
|
+
| docstring: "blocks a bare token like `cand_i`" | a bare prose token next to no separator is not flagged | "blocks a per-iteration path like `${work}\cand_i\plate.svg`" |
|
|
23
|
+
|
|
24
|
+
## The test
|
|
25
|
+
|
|
26
|
+
After writing a hook, ask: **would a token that matches every word of this message actually trip the detector?** When the message names a shape the regex skips, rewrite the message to name only what the regex catches.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths: "**/skills/*/scripts/**/*.py"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Cross-Skill Duplicate Helpers
|
|
6
|
+
|
|
7
|
+
**When this applies:** Any Write or Edit to a `.py` file under a skill's `scripts/` directory (`**/skills/<skill-name>/scripts/**/*.py`) that copies a top-level helper from another skill's `scripts/` directory.
|
|
8
|
+
|
|
9
|
+
## The two duplication cases differ
|
|
10
|
+
|
|
11
|
+
CODE_RULES "Reuse before create" / DRY says one helper lives in one home and both call sites import it. That rule is blocking **within one skill** — two `.py` modules in the same skill's `scripts/` directory that carry the same top-level function body fail the `code_rules_duplicate_body` gate, and the fix is a shared module both import.
|
|
12
|
+
|
|
13
|
+
Across **two skill folders** the same copy is a different call. Each skill folder installs on its own, so a shared module would couple two skills the install model keeps separate: deleting or reinstalling one skill would break a helper the other depends on. A small launch helper copied into each skill (for example a Chrome-open helper that reads the registry and runs `chrome.exe`) is a defensible skill-isolation tradeoff, not a regression.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
Before you copy a top-level helper from one skill's `scripts/` directory into another:
|
|
18
|
+
|
|
19
|
+
- **Same skill, two modules** — extract one shared module in that skill and import it from both. The `code_rules_duplicate_body` gate blocks the copy.
|
|
20
|
+
- **Two skill folders, a small self-contained helper** — copy it; the skill-isolation tradeoff stands. A non-blocking `[CODE_RULES advisory]` names the source skill at Write time so the copy is a deliberate choice on record, not an oversight.
|
|
21
|
+
- **Two skill folders, a large or behavior-bearing body** — when the copied body is large, holds business logic, or would drift in a way that changes behavior, raise the choice through `AskUserQuestion`: copy and accept drift, or stand up a shared dependency both skills declare (for example a published package both `requirements` files name, or a `_shared` module the install step writes into each skill). A shared dependency that survives independent install is the only shared-home path that does not break the install model.
|
|
22
|
+
|
|
23
|
+
## What the advisory tells you
|
|
24
|
+
|
|
25
|
+
The `advise_cross_skill_duplicate_helper` check in `code_rules_duplicate_body` prints to stderr (never blocks) when a top-level function in the file being written has the same normalized body as a top-level function in another skill's `scripts/` directory. The message names the source skill and function so a reviewer can confirm the copy was intentional. It fires only across skill folders; within one skill the blocking gate already covers the copy.
|
|
26
|
+
|
|
27
|
+
## Why this is a rule, not a wider gate
|
|
28
|
+
|
|
29
|
+
Extending the blocking duplicate-body gate to span skill folders would deny the exact skill-isolation copy that keeps skills independently installable — a false positive on a sanctioned pattern. The boundary between "same skill, block" and "two skills, signal" is a judgment the writer makes with the source skill named in front of them. The rule states the judgment; the `[CODE_RULES advisory]` surfaces the signal; neither blocks the defensible copy.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# No Inline Destructive-Command Literals in Bash
|
|
2
|
+
|
|
3
|
+
The `destructive_command_blocker` PreToolUse hook matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text anywhere in a Bash-tool command, with no quote-awareness — so a destructive literal carried only as DATA (a commit message, a PR/issue/review-comment body, an echoed string, a `python -c`/`node -e`/`awk` argument, a heredoc) trips the confirmation prompt even though the shell never executes it. In a background or auto-mode run no human can answer that prompt, so the call stalls.
|
|
4
|
+
|
|
5
|
+
Keep destructive literals out of the Bash command string:
|
|
6
|
+
|
|
7
|
+
- Commit messages and PR/issue/review-comment bodies that describe destructive-command behavior go in a file passed by path — `git commit -F <file>`, `gh ... --body-file <file>` (see [`gh-body-file`](gh-body-file.md)) — never `git commit -m` / `gh ... -b`.
|
|
8
|
+
- To exercise or verify `destructive_command_blocker` (or any hook) behavior, run the committed test suite (`python -m pytest <test_file>`), which passes the command strings as in-language data, not as a shell command — never an inline `python -c` harness.
|
|
9
|
+
- Genuine cleanup targets the OS temp dir or `$CLAUDE_JOB_DIR/tmp` (auto-allowed as ephemeral), never a repository or worktree path.
|
|
10
|
+
|
|
11
|
+
The `destructive_command_blocker` hook is the enforcement surface; this rule is how to keep a non-executing mention from tripping it.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Workflow Substitution Slot Rule
|
|
2
|
+
|
|
3
|
+
In a `.workflow.js` agent-prompt template, every per-call or per-iteration value an agent must fill in is marked with the angle-bracket convention — `<plate.svg>`, `<object.svg>`, `<glow_hex>`, `cand_<i>`. A bare token such as `cand_i` reads as a fixed literal, so an agent can create one literal directory named `cand_i` and overwrite it across every iteration of a loop, collapsing an N-iteration gate into a single run.
|
|
4
|
+
|
|
5
|
+
When a loop builds a per-iteration path or output key, write the index as a slot — `cand_<i>` — or spell out `replace <i> with the iteration index 0, 1, 2` in the step text. Every per-call value in a `.workflow.js` template carries angle brackets so an agent fills in a fresh value per call.
|
|
6
|
+
|
|
7
|
+
`workflow_substitution_slot_blocker.py` (PreToolUse on Write/Edit) blocks a `.workflow.js` write whose looped content carries a bare `<word>_<i|j|k>` token as a per-iteration path segment, and returns the corrective message.
|