claude-dev-env 2.5.0 → 2.7.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 +20 -57
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/agents/CLAUDE.md +1 -1
- package/agents/code-verifier.md +36 -7
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/codex-capability-map.json +13 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/code-review-enforcement.md +31 -6
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +236 -27
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +0 -12
- package/hooks/hooks_constants/CLAUDE.md +5 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/package.json +4 -2
- package/rules/CLAUDE.md +17 -23
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +10 -2
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_invoke_code_review.py +143 -0
- package/scripts/test_invoke_code_review_chain.py +1 -1
- package/scripts/test_invoke_code_review_contract.py +1 -1
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/autoconverge/workflow/converge.mjs +1 -1
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/condensing-instructions/SKILL.md +42 -51
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/orchestrator/SKILL.md +23 -9
- package/skills/orchestrator-refresh/SKILL.md +20 -1
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/session-log/SKILL.md +1 -1
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/state-what-is.md +0 -25
- package/rules/tdd.md +0 -7
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"capabilities": [
|
|
4
|
+
{"source": "TaskList", "aliases": ["task_list", "tasks", "Task.list"], "target": "update_plan", "transformation": "snapshot", "status": "fallback", "confidence": "medium", "manual_review": true, "required_fields": {"plan": "plan"}},
|
|
5
|
+
{"source": "TaskCreate", "aliases": ["task_create", "tasks.create", "create_task"], "target": "update_plan", "transformation": "append/replace-plan-item", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"name": "string", "status": "status"}},
|
|
6
|
+
{"source": "TaskUpdate", "aliases": ["task_update", "tasks.update", "update_task"], "target": "update_plan", "transformation": "status mutation", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"name": "string", "status": "status"}},
|
|
7
|
+
{"source": "Task", "aliases": ["Agent", "task", "agent", "spawn_agent", "spawn", "spawnAgent"], "target": "multi_agent_v1__spawn_agent", "transformation": "spawn request", "status": "mapped", "confidence": "medium", "manual_review": true, "required_fields": {"prompt": "string"}},
|
|
8
|
+
{"source": "SendMessage", "aliases": ["send_message", "agent.send", "send_input", "message", "sendMessage"], "target": "multi_agent_v1__send_input", "transformation": "agent input", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"message": "string"}},
|
|
9
|
+
{"source": "TaskOutput", "aliases": ["task_output", "wait", "TaskWait"], "target": "multi_agent_v1__wait_agent", "transformation": "wait request", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"task_id": "string"}},
|
|
10
|
+
{"source": "TaskStop", "aliases": ["task_stop", "stop_task", "close_agent", "stop", "stopAgent"], "target": "multi_agent_v1__close_agent", "transformation": "close request", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"task_id": "string"}},
|
|
11
|
+
{"source": "ScheduleWakeup", "aliases": ["schedule_wakeup", "wake_up", "cron_wakeup"], "target": null, "transformation": "explicit unsupported result; future automation surface requires confirmation", "status": "unsupported", "confidence": "high", "manual_review": true}
|
|
12
|
+
]
|
|
13
|
+
}
|
package/docs/CODE_RULES.md
CHANGED
|
@@ -60,6 +60,8 @@ Advisory only, never blocking: soft advisory at >= 400 lines, strong nudge at >=
|
|
|
60
60
|
Never: ABC for single impl, DI frameworks, factory for single type. Always: functions when no state, concrete classes, simple imports.
|
|
61
61
|
Parameters follow YAGNI: add an optional parameter when a caller varies the value; when every call site passes the same value, make it required or inline the constant. Remove parameters no caller passes and no body reads.
|
|
62
62
|
|
|
63
|
+
**Complexity budget — state it BEFORE implementation:** files (target 1-2, max 3), lines (~50-300). Checkpoints: Is this the MINIMUM? Can it be fewer files? Functions instead of classes?
|
|
64
|
+
|
|
63
65
|
## 7.5 SOLID PRINCIPLES
|
|
64
66
|
|
|
65
67
|
**SRP always applies** — one reason to change per function/class/module. **OCP, LSP, ISP, DIP apply only where two or more concrete implementations already share a contract**; with a single concretion §7 wins (concrete classes, direct imports, YAGNI — introduce the abstraction at the commit that adds the second concretion). Misapplication signals: interface/ABC with exactly one implementation, SRP-splitting a cohesive class by size alone, abstract factories for one product, DI containers where every injected type has one concretion.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Codex compatibility entry point
|
|
2
|
+
|
|
3
|
+
`codex-compat` is an optional, explicit bridge from this package's Claude-oriented source tree to Codex-compatible records. The existing `claude-dev-env` installer is unchanged and does not invoke it.
|
|
4
|
+
|
|
5
|
+
## Materialization
|
|
6
|
+
|
|
7
|
+
Run `codex-compat materialize --source-root <claude-root> --target-root <codex-root>`. The command defaults to a dry run; add `--apply` to publish files. Use `--python <command>` or `CODEX_COMPAT_PYTHON` to select Python. If no usable interpreter is found, the command reports that condition. The launcher passes an argv array, never a shell command.
|
|
8
|
+
|
|
9
|
+
The Python materializer maps Claude `_shared/`, `agents/`, `hooks/`, `rules/`, and `scripts/` into the target according to the package's compatibility materialization rules. Claude agent frontmatter is converted to Codex TOML metadata. Unsupported Claude metadata is reported, rather than silently treated as equivalent.
|
|
10
|
+
|
|
11
|
+
Rules, hooks, and scripts that have no safe Codex runtime equivalent remain inert or source-only. They are preserved for inspection and are not executed as translated target tools. The capability bridge likewise emits declarative records only; it never invokes the translated surface.
|
|
12
|
+
|
|
13
|
+
Materialization uses a compatibility manifest to identify generated files. Dry runs report the plan without writing. Apply mode uses safe link/copy fallback where linking is unavailable, writes atomically, removes only stale managed files, and rolls back managed changes on failure. A failed rollback reports that reconciliation is required.
|
|
14
|
+
|
|
15
|
+
The manifest hash decides who owns a target file. A file whose hash still matches the manifest is one the tool wrote, so a later run refreshes it in place. A file whose hash differs is one you edited, so the run preserves it and reports a conflict. A file the manifest does not record at all is adopted only when its bytes already match the plan, which is what an interrupted run leaves behind; any other unrecorded file is preserved, and the error names the file to move or delete.
|
|
16
|
+
|
|
17
|
+
A missing or unreadable source root is an error, and the run changes nothing. An existing source root holding no agents makes every managed file stale, so the run refuses to delete them and exits non-zero; add `--allow-prune-all` to remove them on purpose.
|
|
18
|
+
|
|
19
|
+
## Capability bridge
|
|
20
|
+
|
|
21
|
+
Run `codex-compat bridge --surface <name> --payload '<json-object>'`. The bridge exposes the Python translation logic directly. `TaskCreate` and `TaskUpdate` map to `update_plan`; spawn, message, wait, and stop map to multi-agent surfaces. `ScheduleWakeup` is explicitly unsupported and requires manual review.
|
|
22
|
+
|
|
23
|
+
## Roots and safety
|
|
24
|
+
|
|
25
|
+
Both roots are caller-supplied. The tool never writes to `.agents` or `CODEX_HOME` automatically; pass those locations explicitly when desired. No personal paths or secrets are embedded in the package.
|
|
@@ -1,23 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Running Commands on the NAS
|
|
2
2
|
|
|
3
|
-
Full detail behind the always-on `rules/nas-ssh-invocation.md` kernel.
|
|
3
|
+
Full detail behind the always-on `rules/nas-ssh-invocation.md` kernel.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This file covers how to run a command there at all. What the NAS runs, where the automations are
|
|
6
|
+
deployed, and how to verify a change against them belong with the project that owns those
|
|
7
|
+
automations, not here.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## The form to use
|
|
9
|
+
## The call
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
|
|
12
|
+
python <runner-path> <command-script.sh> <private-key-path>
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Two arguments, both paths. The first is a file holding a bash script; the second is the private key.
|
|
16
|
+
|
|
17
|
+
The runner is `nas_ssh_key.py`. It ships with the automation project it serves, so read its path from
|
|
18
|
+
that project's own reference rather than assuming a location.
|
|
19
|
+
|
|
20
|
+
The host, ssh port and ssh user are constants inside the runner, so no command you write names them.
|
|
21
|
+
The same three values are recorded under the `nas` key in `~/.claude/local-identity.json`. Read them
|
|
22
|
+
from there when something needs them, and keep them out of anything committed or posted.
|
|
23
|
+
|
|
24
|
+
Write the script file with the Write tool. Keeping the commands in a file has a second benefit: a
|
|
25
|
+
destructive word such as `rm -rf` inside the file never appears in a Bash tool command string, so the
|
|
26
|
+
`destructive_command_blocker` hook stays out of the way.
|
|
27
|
+
|
|
28
|
+
The script runs through `bash -s` in a single shell. Variables, `cd`, and `source` all carry from one
|
|
29
|
+
line to the next. Standard output, standard error and the exit code all come back.
|
|
30
|
+
|
|
31
|
+
## Why not ssh, scp or sftp
|
|
32
|
+
|
|
33
|
+
The runner loads the key with paramiko and signs inside the Python process. The command-line clients
|
|
34
|
+
check the key file's permissions first and refuse to load a key whose permissions they do not like.
|
|
35
|
+
Git Bash's `ssh` then falls back to an interactive password prompt, which hangs a run with nobody to
|
|
36
|
+
answer it.
|
|
37
|
+
|
|
38
|
+
To move a file onto the NAS, write it from inside the script:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cat > /tmp/thing.conf <<'EOF'
|
|
42
|
+
contents here
|
|
43
|
+
EOF
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The quoted `'EOF'` stops the shell expanding anything in the body.
|
|
47
|
+
|
|
48
|
+
## The key
|
|
49
|
+
|
|
50
|
+
Use the ops key under `~/.claude/keys`. It is readable as-is and the NAS accepts it.
|
|
16
51
|
|
|
17
|
-
|
|
52
|
+
Keys under `~/.ssh` are for other jobs and none of them works here. The default one carries a
|
|
53
|
+
passphrase, so paramiko cannot load it unattended; the rest are for other hosts.
|
|
18
54
|
|
|
19
|
-
|
|
55
|
+
## Writing a script that touches the automations
|
|
56
|
+
|
|
57
|
+
Do not reach for the system interpreter. It carries neither pytest nor the libraries the automations
|
|
58
|
+
import, so anything checked against it proves nothing.
|
|
59
|
+
|
|
60
|
+
Each automation project records its own runtime — the interpreter, the virtual environment, the
|
|
61
|
+
import root, and the deploy path. Read that project's reference and activate what it names before
|
|
62
|
+
running anything.
|
|
63
|
+
|
|
64
|
+
## Verifying a change before it ships
|
|
65
|
+
|
|
66
|
+
The NAS is a Linux box and this machine is Windows, so two classes of difference show up only there:
|
|
67
|
+
syntax newer than the NAS interpreter accepts, and tests that assert Windows paths.
|
|
68
|
+
|
|
69
|
+
**Always separate a real break from a platform-only one.** Run the same test file at the branch head
|
|
70
|
+
and at the commit the work started from. Identical failures at both mean the environment, not the
|
|
71
|
+
change. Report both numbers rather than the head alone.
|
|
72
|
+
|
|
73
|
+
## The /tmp limit
|
|
74
|
+
|
|
75
|
+
`/tmp` is a small tmpfs. A full clone of a repository that carries binary assets fills it, the clone
|
|
76
|
+
dies partway through, and the disk stays full for whatever runs next.
|
|
77
|
+
|
|
78
|
+
Fetch shallow and sparse instead:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
d=$(mktemp -d /tmp/work.XXXXXX)
|
|
82
|
+
cd "$d"
|
|
83
|
+
git init --quiet
|
|
84
|
+
git remote add origin <repository-url>
|
|
85
|
+
git sparse-checkout init --cone
|
|
86
|
+
git sparse-checkout set <subdirectory>
|
|
87
|
+
git fetch --quiet --depth 1 origin <sha>
|
|
88
|
+
git checkout --quiet FETCH_HEAD
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
One subdirectory at one commit lands tens of megabytes rather than the whole history. Clean up at the
|
|
92
|
+
end and print `df -h /tmp` so the next run knows the state:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
find /tmp -maxdepth 1 -name 'work.*' -prune -exec rm -rf {} +
|
|
96
|
+
```
|
|
20
97
|
|
|
21
|
-
##
|
|
98
|
+
## When something fails
|
|
22
99
|
|
|
23
|
-
|
|
100
|
+
| What you see | What it means |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `PermissionError: [Errno 13]` on the key | The key file is not readable by this account. |
|
|
103
|
+
| `PasswordRequiredException: Private key file is encrypted` | That key has a passphrase and cannot be used unattended. |
|
|
104
|
+
| `Authentication (publickey) failed` | The transport works and the NAS does not accept that key. |
|
|
105
|
+
| `Load key ...: Permission denied` from `ssh.exe` | A command-line client is being used. Use the runner. |
|
|
106
|
+
| `No space left on device` | `/tmp` is full. Clean it, then fetch shallow and sparse. |
|
|
107
|
+
| `No module named pytest` | The system interpreter is being used. Activate the project's environment first. |
|
|
@@ -11,14 +11,35 @@ The gates follow the same shape as the `verified_commit` gate family.
|
|
|
11
11
|
|
|
12
12
|
## Opt-in (default off)
|
|
13
13
|
|
|
14
|
-
Enforcement is **off by default**.
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
Enforcement is **off by default**. Turn it on by setting the environment
|
|
15
|
+
variable `CLAUDE_CODE_REVIEW_ENFORCEMENT` to `1`, `true`, `yes`, or `on`
|
|
16
|
+
(case and surrounding spaces are ignored). Any other value, and an unset
|
|
17
|
+
variable, leave enforcement off.
|
|
18
|
+
|
|
19
|
+
Set the variable in the machine's own user environment so every gate reads it,
|
|
20
|
+
including the native git pre-push backstop, which git runs in the shell's
|
|
21
|
+
environment. An `env` block in `settings.json` reaches the three Claude Code
|
|
22
|
+
hook gates alone. Each gate process reads the variable as it starts, so a
|
|
23
|
+
Claude Code session already running keeps its current setting until it
|
|
24
|
+
restarts, while the git pre-push backstop picks up the current shell
|
|
25
|
+
environment on each push.
|
|
26
|
+
|
|
27
|
+
A user-environment setting arms the gates machine-wide. The backstop installs
|
|
28
|
+
through the shared `core.hooksPath`, so it runs for every repository on the
|
|
29
|
+
machine, and the gates carry no per-repository allowlist. Each repository then
|
|
30
|
+
needs its own clean stamp before a push lands there.
|
|
31
|
+
|
|
32
|
+
The variable feeds the master flag `CODE_REVIEW_ENFORCEMENT_ENABLED` in
|
|
33
|
+
`hooks/blocking/config/code_review_enforcement_constants.py`, which every gate
|
|
34
|
+
reads at start-up. When it is on, the push gate, the PR-create gate, the native
|
|
35
|
+
pre-push backstop (via the shared deny decision), and the stamp-directory write
|
|
36
|
+
blocker all enforce. When it is off, every gate allows the action and the
|
|
20
37
|
write-blocker allows stamp-directory access.
|
|
21
38
|
|
|
39
|
+
`npx claude-dev-env` copies the shipped `hooks/` tree over `~/.claude/` on each
|
|
40
|
+
install, so the environment setting survives an install and an edit to the
|
|
41
|
+
constant does not.
|
|
42
|
+
|
|
22
43
|
## How a stamp works
|
|
23
44
|
|
|
24
45
|
A stamp is a small JSON file that records one fact: a clean `/code-review` pass
|
|
@@ -88,6 +109,10 @@ anchor a forgery-proof mint. The stamp reaches the same posture the
|
|
|
88
109
|
create-PR gate.
|
|
89
110
|
- **`git push --no-verify`.** This flag tells git to skip the native pre-push
|
|
90
111
|
hook, so the native backstop does not run.
|
|
112
|
+
- **A push that clears the enable variable.** The backstop reads
|
|
113
|
+
`CLAUDE_CODE_REVIEW_ENFORCEMENT` from the shell that runs the push, so
|
|
114
|
+
`CLAUDE_CODE_REVIEW_ENFORCEMENT=0 git push` reads as enforcement off and the
|
|
115
|
+
backstop allows the push with no flag and no bypass marker.
|
|
91
116
|
- **A rebuilt store.** A script that re-implements the stamp store in memory
|
|
92
117
|
and writes a matching file can mint a stamp the gates accept.
|
|
93
118
|
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -82,6 +82,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
82
82
|
| `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
|
|
83
83
|
| `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
|
|
84
84
|
| `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
|
|
85
|
+
| `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 120 reader-visible words, more than 6 bullet lines, more than 2 lines carrying over 20 words each, or instruction lines telling the user to act with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged, replies under 60 words always pass, and a reply opening with `Long form:` opts out entirely |
|
|
85
86
|
| `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
|
|
86
87
|
| `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
|
|
87
88
|
| `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
|
|
@@ -108,10 +109,12 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
108
109
|
| `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
|
|
109
110
|
| `session_edit_stage_gate.py` | PreToolUse (Bash) | A `git commit` that would drop files edited this session because they are tracked but left unstaged |
|
|
110
111
|
| `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
|
|
112
|
+
| `shell_substitution_blocker.py` | PreToolUse (Bash) | A command carrying `$(...)`, a live backtick, or `<(...)`/`>(...)` process substitution, which the allowlist matcher cannot descend into |
|
|
111
113
|
| `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
|
|
112
114
|
| `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
|
|
113
115
|
| `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
|
|
114
116
|
| `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
|
|
117
|
+
| `unscoped_search_blocker.py` | PreToolUse (Bash/PowerShell) | A `find` or recursive listing that walks from the filesystem root, a drive root, bare home, or a network share root |
|
|
115
118
|
| `verdict_directory_write_blocker.py` | PreToolUse (Bash/PowerShell) | Shell writes into `~/.claude/verification/` |
|
|
116
119
|
| `verified_commit_gate.py` | PreToolUse (Bash/PowerShell) | `git commit`/`git push` without a passing verifier verdict |
|
|
117
120
|
| `verified_commit_message_accuracy_blocker.py` | PreToolUse | Commit messages that misstate what the diff has |
|
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
"""Single source of truth for the code-review enforcement gate family.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
the
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
CLAUDE_CODE_REVIEW_ENFORCEMENT=1 -> CODE_REVIEW_ENFORCEMENT_ENABLED True
|
|
6
|
+
(unset) -> CODE_REVIEW_ENFORCEMENT_ENABLED False
|
|
7
|
+
effort_meets_threshold("xhigh", PUSH_REQUIRED_EFFORT) -> True
|
|
8
|
+
effort_meets_threshold("high", PR_CREATE_REQUIRED_EFFORT) -> False
|
|
9
|
+
|
|
10
|
+
Every gate reads its thresholds and messages from here, so the effort a push
|
|
11
|
+
needs and the effort a pull request needs never drift apart. The master flag
|
|
12
|
+
tracks the environment, so a machine opts in through its own environment and
|
|
13
|
+
the choice outlives a reinstall that rewrites this shipped file.
|
|
12
14
|
"""
|
|
13
15
|
|
|
14
16
|
from __future__ import annotations
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
CODE_REVIEW_ENFORCEMENT_ENV_VAR = "CLAUDE_CODE_REVIEW_ENFORCEMENT"
|
|
21
|
+
ALL_ENFORCEMENT_ENABLED_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def code_review_enforcement_enabled_in_environment() -> bool:
|
|
25
|
+
"""Read whether this machine turns the code-review gates on.
|
|
26
|
+
|
|
27
|
+
::
|
|
28
|
+
|
|
29
|
+
CLAUDE_CODE_REVIEW_ENFORCEMENT=1 -> True
|
|
30
|
+
CLAUDE_CODE_REVIEW_ENFORCEMENT=" On " -> True
|
|
31
|
+
CLAUDE_CODE_REVIEW_ENFORCEMENT=0 -> False
|
|
32
|
+
(variable unset) -> False
|
|
33
|
+
|
|
34
|
+
A machine opts in through its own environment, so an install that rewrites
|
|
35
|
+
this shipped file leaves the choice standing. Any value outside the enabled
|
|
36
|
+
set, and an unset variable, read as off, so the gate family stays quiet
|
|
37
|
+
until someone asks for it.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True when the variable holds an enabled value, False otherwise.
|
|
41
|
+
"""
|
|
42
|
+
raw_environment_setting = os.environ.get(CODE_REVIEW_ENFORCEMENT_ENV_VAR, "")
|
|
43
|
+
return raw_environment_setting.strip().lower() in ALL_ENFORCEMENT_ENABLED_ENV_VALUES
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
CODE_REVIEW_ENFORCEMENT_ENABLED = code_review_enforcement_enabled_in_environment()
|
|
17
47
|
STAMP_DIRECTORY_NAME = "code-review-stamps"
|
|
18
48
|
ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = ("low", "medium", "high", "xhigh", "max")
|
|
19
49
|
PUSH_REQUIRED_EFFORT = "low"
|
|
@@ -9,6 +9,8 @@ import importlib.util
|
|
|
9
9
|
import pathlib
|
|
10
10
|
import re
|
|
11
11
|
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
12
14
|
_CONFIG_DIR = pathlib.Path(__file__).parent
|
|
13
15
|
|
|
14
16
|
SAMPLE_ROOT_KEY_HEX_LENGTH = 16
|
|
@@ -23,7 +25,10 @@ _constants_module = importlib.util.module_from_spec(_constants_spec)
|
|
|
23
25
|
_constants_spec.loader.exec_module(_constants_module)
|
|
24
26
|
|
|
25
27
|
effort_meets_threshold = _constants_module.effort_meets_threshold
|
|
26
|
-
|
|
28
|
+
CODE_REVIEW_ENFORCEMENT_ENV_VAR = _constants_module.CODE_REVIEW_ENFORCEMENT_ENV_VAR
|
|
29
|
+
code_review_enforcement_enabled_in_environment = (
|
|
30
|
+
_constants_module.code_review_enforcement_enabled_in_environment
|
|
31
|
+
)
|
|
27
32
|
PUSH_REQUIRED_EFFORT = _constants_module.PUSH_REQUIRED_EFFORT
|
|
28
33
|
PR_CREATE_REQUIRED_EFFORT = _constants_module.PR_CREATE_REQUIRED_EFFORT
|
|
29
34
|
GATED_PUSH_SUBCOMMANDS = _constants_module.GATED_PUSH_SUBCOMMANDS
|
|
@@ -109,5 +114,53 @@ def test_guard_message_directs_users_to_the_sanctioned_minter_flag() -> None:
|
|
|
109
114
|
assert SANCTIONED_STAMP_MINTER_FLAG in STAMP_DIRECTORY_GUARD_MESSAGE
|
|
110
115
|
|
|
111
116
|
|
|
112
|
-
def test_enforcement_defaults_to_off() -> None:
|
|
113
|
-
|
|
117
|
+
def test_enforcement_defaults_to_off(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
118
|
+
monkeypatch.delenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, raising=False)
|
|
119
|
+
assert code_review_enforcement_enabled_in_environment() is False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize("enabling_value", ["1", "true", "TRUE", " yes ", "on"])
|
|
123
|
+
def test_enforcement_turns_on_for_each_enabling_environment_value(
|
|
124
|
+
monkeypatch: pytest.MonkeyPatch, enabling_value: str
|
|
125
|
+
) -> None:
|
|
126
|
+
monkeypatch.setenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, enabling_value)
|
|
127
|
+
assert code_review_enforcement_enabled_in_environment() is True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.parametrize("non_enabling_value", ["0", "false", "off", "", "maybe"])
|
|
131
|
+
def test_enforcement_stays_off_for_a_non_enabling_environment_value(
|
|
132
|
+
monkeypatch: pytest.MonkeyPatch, non_enabling_value: str
|
|
133
|
+
) -> None:
|
|
134
|
+
monkeypatch.setenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, non_enabling_value)
|
|
135
|
+
assert code_review_enforcement_enabled_in_environment() is False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _module_flag_after_a_fresh_import(
|
|
139
|
+
monkeypatch: pytest.MonkeyPatch, environment_value: str | None
|
|
140
|
+
) -> bool:
|
|
141
|
+
if environment_value is None:
|
|
142
|
+
monkeypatch.delenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, raising=False)
|
|
143
|
+
else:
|
|
144
|
+
monkeypatch.setenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, environment_value)
|
|
145
|
+
fresh_spec = importlib.util.spec_from_file_location(
|
|
146
|
+
"code_review_enforcement_constants_fresh_import",
|
|
147
|
+
_CONFIG_DIR / "code_review_enforcement_constants.py",
|
|
148
|
+
)
|
|
149
|
+
assert fresh_spec is not None
|
|
150
|
+
assert fresh_spec.loader is not None
|
|
151
|
+
fresh_module = importlib.util.module_from_spec(fresh_spec)
|
|
152
|
+
fresh_spec.loader.exec_module(fresh_module)
|
|
153
|
+
flag_value: bool = fresh_module.CODE_REVIEW_ENFORCEMENT_ENABLED
|
|
154
|
+
return flag_value
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_module_flag_turns_on_when_the_environment_asks_for_enforcement(
|
|
158
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
159
|
+
) -> None:
|
|
160
|
+
assert _module_flag_after_a_fresh_import(monkeypatch, "true") is True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_module_flag_stays_off_when_the_environment_is_unset(
|
|
164
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
165
|
+
) -> None:
|
|
166
|
+
assert _module_flag_after_a_fresh_import(monkeypatch, None) is False
|