@jesdi/skills 0.0.14 → 0.0.16
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/README.md +1 -1
- package/package.json +1 -1
- package/skills/crap-gate/SKILL.md +38 -11
- package/skills/crap-gate/crap.py +13 -1
- package/skills/crap-gate/run.sh +87 -0
- package/skills/crap-gate/test_launcher.py +116 -0
- package/skills/deep-quality-review/SKILL.md +26 -11
- package/skills/review-diff/SKILL.md +122 -0
- package/skills/review-diff/correctness.md +29 -0
- package/skills/review-diff/spec.md +24 -0
- package/skills-manifest.json +12 -6
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ which ones are part of the standard setup, where they come from, and (under
|
|
|
58
58
|
upstream with the [skills.sh](https://skills.sh/) CLI:
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
npx skills add mattpocock/skills # grill-me, grill-with-docs, grilling, improve-codebase-architecture, to-questionnaire, tdd, code-review, codebase-design, diagnosing-bugs, resolving-merge-conflicts
|
|
61
|
+
npx skills add mattpocock/skills # grill-me, grill-with-docs, grilling, improve-codebase-architecture, to-questionnaire, tdd, code-review, codebase-design, diagnosing-bugs, resolving-merge-conflicts, handoff, teach, wait-what
|
|
62
62
|
npx skills add JuliusBrussee/caveman # caveman suite
|
|
63
63
|
npx skills add vercel-labs/skills # find-skills
|
|
64
64
|
npx skills add vercel-labs/agent-skills # vercel-react-best-practices
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crap-gate
|
|
3
3
|
description: >-
|
|
4
4
|
Score every function a branch changed with CRAP (cyclomatic complexity ×
|
|
5
|
-
untested fraction), fail on existing functions
|
|
5
|
+
untested fraction), fail on existing functions above 15 and new functions above 9,
|
|
6
6
|
and drive a fixer + independent reviewer loop until the gate passes. Use
|
|
7
7
|
when implementation is done and before pushing, when the user says "run the
|
|
8
8
|
crap gate", "check CRAP", "is this change tested enough", or when a push
|
|
@@ -21,16 +21,19 @@ is the human approval gate at push time.
|
|
|
21
21
|
|
|
22
22
|
## Running the engine
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
Use the bundled launcher on macOS or Linux (x86_64/ARM64; Windows via WSL).
|
|
25
|
+
It downloads checksum-verified uv 0.12.13 and provisions private Python 3.13.7
|
|
26
|
+
on first use, then reuses them across repositories. No global Python, uv,
|
|
27
|
+
virtualenv activation, or pip installation is needed. The engine uses only
|
|
28
|
+
Python's standard library and supports Python source syntax through 3.13.
|
|
29
|
+
Run from anywhere inside the repo — it walks up to `.crap-gate.json`:
|
|
27
30
|
|
|
28
31
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
sh "<skill-dir>/run.sh" # human table, exit 1 on violations
|
|
33
|
+
sh "<skill-dir>/run.sh" --json # same data for a subagent prompt
|
|
34
|
+
sh "<skill-dir>/run.sh" --no-run # reuse the last coverage reports
|
|
35
|
+
sh "<skill-dir>/run.sh" --all # whole-repo baseline (exploration only)
|
|
36
|
+
sh "<skill-dir>/run.sh" --base main --threshold-new 7 # local what-ifs
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
`<skill-dir>` is wherever this skill is installed (`.agents/skills/crap-gate`,
|
|
@@ -40,13 +43,37 @@ wrap it (`make crap`); prefer the wrapper when one exists.
|
|
|
40
43
|
When you point a wrapper at an agent symlink, that symlink only exists after
|
|
41
44
|
`skills-cli sync` if the matching agent is declared for the skill in
|
|
42
45
|
`.my-skills.json` — `claude` → `.claude/skills/crap-gate`, `opencode` →
|
|
43
|
-
`.agents/skills/crap-gate`. A wrapper that runs `.agents/skills/crap-gate/
|
|
46
|
+
`.agents/skills/crap-gate`. A wrapper that runs `.agents/skills/crap-gate/run.sh`
|
|
44
47
|
therefore needs `"agents": ["opencode"]` (or `["claude", "opencode"]`) on the
|
|
45
48
|
`crap-gate` entry, or a fresh checkout/worktree will lack that path after `sync`.
|
|
46
|
-
The store copy `.my-skills/crap-gate/
|
|
49
|
+
The store copy `.my-skills/crap-gate/run.sh` is always materialized regardless
|
|
47
50
|
of declared agents, so wrapping against it avoids the agent/path coupling
|
|
48
51
|
entirely.
|
|
49
52
|
|
|
53
|
+
Existing `make crap` / `make crap-gate` wrappers must replace `python3 .../crap.py`
|
|
54
|
+
with `sh .../run.sh` (quote the path). Prefer the store path, for example:
|
|
55
|
+
|
|
56
|
+
```makefile
|
|
57
|
+
crap crap-gate:
|
|
58
|
+
sh "$(CURDIR)/.my-skills/crap-gate/run.sh"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The launcher needs standard shell utilities, `curl`, `tar`, and `sha256sum`
|
|
62
|
+
or `shasum`. Its cache defaults to `${XDG_CACHE_HOME:-$HOME/.cache}/crap-gate`;
|
|
63
|
+
set `CRAP_GATE_CACHE_DIR` to an absolute path to relocate it. To pre-provision,
|
|
64
|
+
run the launcher with `--help` while online. `CRAP_GATE_OFFLINE=1` then requires
|
|
65
|
+
that populated cache and prevents runtime downloads. Runtime setup messages go
|
|
66
|
+
to stderr; `--json` returns a structured error on setup failure, with exit 2.
|
|
67
|
+
Do not bypass the gate or repair global Python when setup fails; resolve the
|
|
68
|
+
reported download/cache problem and rerun the launcher.
|
|
69
|
+
|
|
70
|
+
TypeScript targets still need `node` and the project's own `typescript` package.
|
|
71
|
+
Coverage commands still need the project's test dependencies; configure them
|
|
72
|
+
with its normal runner (for example, `cd backend && pipenv run pytest ...`).
|
|
73
|
+
The launcher preserves PATH and the caller's environment for these commands;
|
|
74
|
+
its private Python is only for the gate engine. It does not install project
|
|
75
|
+
dependencies or edit project environments.
|
|
76
|
+
|
|
50
77
|
By default the engine runs each target's coverage command only when that
|
|
51
78
|
target has changed files, so a backend-only change never runs the frontend
|
|
52
79
|
suite. Coverage command output goes to `.crap/<target>.log`.
|
package/skills/crap-gate/crap.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""crap-gate engine: CRAP score for every function a branch changed.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
sh run.sh [--config PATH] [--base REF] [--threshold-existing N]
|
|
5
5
|
[--threshold-new N] [--run | --no-run] [--all] [--json]
|
|
6
6
|
|
|
7
7
|
Exit 0 = no violations, 1 = violations, 2 = configuration/tool error.
|
|
@@ -11,12 +11,22 @@ project's own `typescript` package.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
|
+
import json
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
16
17
|
import tempfile
|
|
17
18
|
import time
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
21
|
+
# Direct invocation remains useful for development, but should fail clearly
|
|
22
|
+
# before importing an analyzer that requires newer AST nodes.
|
|
23
|
+
if sys.version_info < (3, 10):
|
|
24
|
+
message = "Use the bundled run.sh launcher; direct execution requires Python >= 3.10"
|
|
25
|
+
print(f"crap: {message}", file=sys.stderr)
|
|
26
|
+
if "--json" in sys.argv:
|
|
27
|
+
print(json.dumps({"error": {"kind": "tooling", "message": message}}))
|
|
28
|
+
sys.exit(2)
|
|
29
|
+
|
|
20
30
|
import crap_git
|
|
21
31
|
import crap_python
|
|
22
32
|
import crap_typescript
|
|
@@ -201,6 +211,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
201
211
|
result = run(cfg, run_mode=args.run_mode, all_functions=args.all)
|
|
202
212
|
except ToolError as exc:
|
|
203
213
|
print(f"crap: {exc}", file=sys.stderr)
|
|
214
|
+
if args.json:
|
|
215
|
+
print(json.dumps({"error": {"kind": "tooling", "message": str(exc)}}))
|
|
204
216
|
return 2
|
|
205
217
|
sys.stdout.write(render_json(result) if args.json else render_text(result))
|
|
206
218
|
return 1 if result.summary["fail"] else 0
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Keep bootstrap dependencies to POSIX tools, curl, tar, and SHA-256.
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
json=false
|
|
6
|
+
for arg in "$@"; do
|
|
7
|
+
[ "$arg" != --json ] || json=true
|
|
8
|
+
done
|
|
9
|
+
fail() {
|
|
10
|
+
printf 'crap: %s\n' "$1" >&2
|
|
11
|
+
if "$json"; then
|
|
12
|
+
printf '%s\n' '{"error":{"kind":"tooling","message":"Runtime setup failed; see stderr. Run the launcher again after resolving the setup error."}}'
|
|
13
|
+
fi
|
|
14
|
+
exit 2
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Resolve file symlinks as well as symlinked skill directories.
|
|
18
|
+
entry=$0
|
|
19
|
+
while [ -L "$entry" ]; do
|
|
20
|
+
parent=$(CDPATH= cd -- "$(dirname -- "$entry")" && pwd)
|
|
21
|
+
entry=$(readlink "$entry")
|
|
22
|
+
case "$entry" in /*) ;; *) entry=$parent/$entry ;; esac
|
|
23
|
+
done
|
|
24
|
+
skill_dir=$(CDPATH= cd -- "$(dirname -- "$entry")" && pwd)
|
|
25
|
+
cache=${CRAP_GATE_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/crap-gate}
|
|
26
|
+
case "$cache" in /*) ;; *) fail 'CRAP_GATE_CACHE_DIR must be an absolute path' ;; esac
|
|
27
|
+
uv_version=0.12.13
|
|
28
|
+
python_version=3.13.7
|
|
29
|
+
case "$(uname -s):$(uname -m)" in
|
|
30
|
+
Darwin:arm64|Darwin:aarch64)
|
|
31
|
+
target=aarch64-apple-darwin
|
|
32
|
+
digest=7e6ddb9316acc00f2296c82ff4d99977870ee34b2f0ddcae9444d714db9364ed ;;
|
|
33
|
+
Darwin:x86_64)
|
|
34
|
+
target=x86_64-apple-darwin
|
|
35
|
+
digest=5e287ef61cb6a9b61b3a83fef124fd143e400468a7dac794230147a810e17119 ;;
|
|
36
|
+
Linux:aarch64|Linux:arm64)
|
|
37
|
+
target=aarch64-unknown-linux-musl
|
|
38
|
+
digest=f44bc1037a17889fe562fffd2002d4ed108e499fbe68b4f022af244dc7b8244f ;;
|
|
39
|
+
Linux:x86_64)
|
|
40
|
+
target=x86_64-unknown-linux-musl
|
|
41
|
+
digest=4e2bfd0c9007b1032a50e539e965fd0a6037d87ad93ae1580d220a92d4c94098 ;;
|
|
42
|
+
*) fail 'Unsupported platform: use macOS or Linux on x86_64 or ARM64 (Windows: WSL)' ;;
|
|
43
|
+
esac
|
|
44
|
+
uv_dir=$cache/uv/$uv_version/$target
|
|
45
|
+
uv=$uv_dir/uv
|
|
46
|
+
if [ ! -x "$uv" ]; then
|
|
47
|
+
[ "${CRAP_GATE_OFFLINE:-0}" != 1 ] || fail 'uv is not cached; provision this cache online first'
|
|
48
|
+
mkdir -p "$uv_dir" || fail 'Cannot create runtime cache'
|
|
49
|
+
stage=$(mktemp -d "$uv_dir/.download.XXXXXX") || fail 'Cannot create download directory'
|
|
50
|
+
trap 'rm -rf "$stage"' EXIT
|
|
51
|
+
trap 'exit 2' HUP INT TERM
|
|
52
|
+
printf 'crap: downloading uv %s\n' "$uv_version" >&2
|
|
53
|
+
curl --fail --location --silent --show-error --connect-timeout 15 --max-time 300 \
|
|
54
|
+
"https://github.com/astral-sh/uv/releases/download/$uv_version/uv-$target.tar.gz" \
|
|
55
|
+
-o "$stage/uv.tar.gz" || fail 'Cannot download uv; check network access and retry'
|
|
56
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
57
|
+
actual=$(sha256sum "$stage/uv.tar.gz")
|
|
58
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
59
|
+
actual=$(shasum -a 256 "$stage/uv.tar.gz")
|
|
60
|
+
else
|
|
61
|
+
fail 'SHA-256 verification requires sha256sum or shasum'
|
|
62
|
+
fi
|
|
63
|
+
[ "${actual%% *}" = "$digest" ] || fail 'uv archive checksum mismatch'
|
|
64
|
+
tar -xzf "$stage/uv.tar.gz" -C "$stage" || fail 'Cannot extract uv'
|
|
65
|
+
# Atomic publication: concurrent first runs never see a partial executable.
|
|
66
|
+
mv -f "$stage/uv-$target/uv" "$uv" || fail 'Cannot install cached uv'
|
|
67
|
+
rm -rf "$stage"
|
|
68
|
+
trap - EXIT HUP INT TERM
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Limit uv settings to this subprocess. Do not activate a venv or change PATH:
|
|
72
|
+
# coverage commands must inherit the caller's project environment unchanged.
|
|
73
|
+
managed_uv() (
|
|
74
|
+
unset UV_NO_MANAGED_PYTHON UV_PYTHON UV_PYTHON_DOWNLOADS UV_PYTHON_BIN_DIR
|
|
75
|
+
export UV_PYTHON_INSTALL_DIR="$cache/python"
|
|
76
|
+
export UV_CACHE_DIR="$cache/uv-cache"
|
|
77
|
+
export UV_MANAGED_PYTHON=true
|
|
78
|
+
"$uv" --no-config "$@"
|
|
79
|
+
)
|
|
80
|
+
python=$(managed_uv python find --no-project --system --managed-python --no-python-downloads "$python_version" 2>/dev/null) || {
|
|
81
|
+
[ "${CRAP_GATE_OFFLINE:-0}" != 1 ] || fail 'Python is not cached; provision this cache online first'
|
|
82
|
+
managed_uv python install --no-bin "$python_version" >&2 || fail 'Cannot provision Python; check network access and retry'
|
|
83
|
+
python=$(managed_uv python find --no-project --system --managed-python --no-python-downloads "$python_version") || fail 'Cannot locate managed Python'
|
|
84
|
+
}
|
|
85
|
+
# Ignore Python-specific ambient settings for the engine, but preserve them for
|
|
86
|
+
# child coverage processes. -I would also remove the sibling module directory.
|
|
87
|
+
exec "$python" -E -s "$skill_dir/crap.py" "$@"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Launcher regressions. Real-runtime cases use a pre-provisioned cache in CI."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from test_cli import BASE_SRC, repo # noqa: F401 — shared git repository fixture
|
|
11
|
+
|
|
12
|
+
LAUNCHER = Path(__file__).with_name("run.sh").resolve()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def launch(env, *args, cwd=None, script=LAUNCHER):
|
|
16
|
+
return subprocess.run(
|
|
17
|
+
["/bin/sh", str(script), *args], env=env, cwd=cwd,
|
|
18
|
+
text=True, capture_output=True, timeout=60,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_empty_offline_cache_is_json_tool_error(tmp_path):
|
|
23
|
+
env = dict(os.environ, CRAP_GATE_CACHE_DIR=str(tmp_path / "cache"), CRAP_GATE_OFFLINE="1")
|
|
24
|
+
proc = launch(env, "--json")
|
|
25
|
+
assert proc.returncode == 2
|
|
26
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
27
|
+
assert "not cached" in proc.stderr
|
|
28
|
+
assert not (tmp_path / "cache").exists()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_bad_download_is_rejected_before_execution(tmp_path):
|
|
32
|
+
bin_dir = tmp_path / "bin"
|
|
33
|
+
bin_dir.mkdir()
|
|
34
|
+
curl = bin_dir / "curl"
|
|
35
|
+
curl.write_text('#!/bin/sh\nwhile [ "$1" != -o ]; do shift; done\nprintf corrupt > "$2"\n')
|
|
36
|
+
curl.chmod(0o755)
|
|
37
|
+
env = dict(os.environ, PATH=f"{bin_dir}:{os.environ['PATH']}",
|
|
38
|
+
CRAP_GATE_CACHE_DIR=str(tmp_path / "cache"), CRAP_GATE_OFFLINE="0")
|
|
39
|
+
proc = launch(env, "--json")
|
|
40
|
+
assert proc.returncode == 2
|
|
41
|
+
assert "checksum mismatch" in proc.stderr
|
|
42
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
43
|
+
assert not list((tmp_path / "cache").rglob("uv.tar.gz"))
|
|
44
|
+
assert not list((tmp_path / "cache").rglob(".download.*"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def runtime_env():
|
|
49
|
+
cache = os.environ.get("CRAP_GATE_TEST_CACHE")
|
|
50
|
+
if not cache:
|
|
51
|
+
pytest.skip("Set CRAP_GATE_TEST_CACHE to a cache provisioned by run.sh --help")
|
|
52
|
+
return dict(os.environ, CRAP_GATE_CACHE_DIR=cache, CRAP_GATE_OFFLINE="1")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("python_on_path", ["absent", "incompatible"])
|
|
56
|
+
def test_runtime_ignores_host_python_and_preserves_coverage_environment(
|
|
57
|
+
repo, tmp_path, runtime_env, python_on_path,
|
|
58
|
+
):
|
|
59
|
+
bin_dir = tmp_path / "host bin"
|
|
60
|
+
bin_dir.mkdir()
|
|
61
|
+
for name in ("uname", "dirname", "git", "sh", "env"):
|
|
62
|
+
(bin_dir / name).symlink_to(shutil.which(name))
|
|
63
|
+
if python_on_path == "incompatible":
|
|
64
|
+
for name in ("python", "python3"):
|
|
65
|
+
executable = bin_dir / name
|
|
66
|
+
executable.write_text("#!/bin/sh\necho wrong-python >&2\nexit 99\n")
|
|
67
|
+
executable.chmod(0o755)
|
|
68
|
+
runtime_env.update(PATH=str(bin_dir), VIRTUAL_ENV=str(tmp_path / "wrong venv"),
|
|
69
|
+
PYTHONHOME="/nonexistent-python", PYTHONPATH="/nonexistent-modules")
|
|
70
|
+
# Both files must be ignored by runtime discovery.
|
|
71
|
+
(repo / ".python-version").write_text("3.9.6\n")
|
|
72
|
+
(repo / "uv.toml").write_text("deliberately invalid configuration\n")
|
|
73
|
+
(repo / "pkg/m.py").write_text(BASE_SRC + "\ndef added():\n return 1\n")
|
|
74
|
+
(repo / ".crap").mkdir()
|
|
75
|
+
cfg = json.loads((repo / ".crap-gate.json").read_text())
|
|
76
|
+
cfg["targets"][0]["coverage"]["command"] = (
|
|
77
|
+
"env > coverage-env.txt; printf '%s' '{\"files\":{}}' > .crap/coverage.json"
|
|
78
|
+
)
|
|
79
|
+
(repo / ".crap-gate.json").write_text(json.dumps(cfg))
|
|
80
|
+
proc = launch(runtime_env, "--json", cwd=repo)
|
|
81
|
+
assert proc.returncode == 0, proc.stderr
|
|
82
|
+
assert json.loads(proc.stdout)["functions"][0]["name"] == "added"
|
|
83
|
+
inherited = dict(line.split("=", 1) for line in (repo / "coverage-env.txt").read_text().splitlines())
|
|
84
|
+
for name in ("PATH", "VIRTUAL_ENV", "PYTHONHOME", "PYTHONPATH"):
|
|
85
|
+
assert inherited[name] == runtime_env[name]
|
|
86
|
+
assert "UV_PYTHON_INSTALL_DIR" not in inherited or inherited["UV_PYTHON_INSTALL_DIR"] == os.environ.get("UV_PYTHON_INSTALL_DIR")
|
|
87
|
+
assert not (repo / ".venv").exists()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_symlink_spaces_and_exit_codes(repo, tmp_path, runtime_env):
|
|
91
|
+
link = tmp_path / "launcher link"
|
|
92
|
+
link.symlink_to(LAUNCHER)
|
|
93
|
+
(repo / "pkg/m.py").write_text(BASE_SRC + "\ndef unnew(a, b):\n return a if b else (b if a else 0)\n")
|
|
94
|
+
(repo / ".crap").mkdir()
|
|
95
|
+
(repo / ".crap/coverage.json").write_text('{"files":{}}')
|
|
96
|
+
proc = launch(runtime_env, "--json", "--no-run", cwd=repo, script=link)
|
|
97
|
+
assert proc.returncode == 1, proc.stderr
|
|
98
|
+
assert json.loads(proc.stdout)["summary"]["fail"] == 1
|
|
99
|
+
proc = launch(runtime_env, "--json", "--base", "missing-ref", cwd=repo, script=link)
|
|
100
|
+
assert proc.returncode == 2
|
|
101
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_offline_cache_missing_python_is_tool_error(tmp_path, runtime_env):
|
|
105
|
+
original = Path(runtime_env["CRAP_GATE_CACHE_DIR"])
|
|
106
|
+
cache = tmp_path / "uv only cache"
|
|
107
|
+
for executable in (original / "uv").rglob("uv"):
|
|
108
|
+
if executable.is_file():
|
|
109
|
+
target = cache / executable.relative_to(original)
|
|
110
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
target.symlink_to(executable)
|
|
112
|
+
runtime_env["CRAP_GATE_CACHE_DIR"] = str(cache)
|
|
113
|
+
proc = launch(runtime_env, "--json")
|
|
114
|
+
assert proc.returncode == 2
|
|
115
|
+
assert "Python is not cached" in proc.stderr
|
|
116
|
+
assert json.loads(proc.stdout)["error"]["kind"] == "tooling"
|
|
@@ -34,23 +34,33 @@ size, orchestration.
|
|
|
34
34
|
causes it (an invariant enforced by an optional parameter is a structure
|
|
35
35
|
finding). Everything else belongs in a different review.
|
|
36
36
|
|
|
37
|
+
**Repo conventions come first.** Read the repo's AGENTS.md (and CLAUDE.md,
|
|
38
|
+
CONTRIBUTING.md, or coding-standards file) before the diff. A breach of a
|
|
39
|
+
documented rule is a finding; cite the file and the rule. A documented rule
|
|
40
|
+
overrides a smell: when the repo endorses a pattern the table would flag,
|
|
41
|
+
don't flag it.
|
|
42
|
+
|
|
37
43
|
## The smells
|
|
38
44
|
|
|
39
45
|
Run every meaningful change through this table, top to bottom. Each row is one
|
|
40
46
|
concept; the *Ask* is the question that exposes it, the *Remedy* is what to
|
|
41
|
-
push for.
|
|
47
|
+
push for. Names in parentheses are the matching Fowler smells (*Refactoring*,
|
|
48
|
+
ch. 3), folded into the row that already covers them.
|
|
42
49
|
|
|
43
50
|
| # | Smell | Ask | Remedy |
|
|
44
51
|
|---|-------|-----|--------|
|
|
45
|
-
| 1 | **Duplicated concept** — one idea expressed twice: near-duplicate helper, parallel model, copy-pasted branch, a flag *and* an optional both meaning "off", the same predicate reimplemented instead of reused | What single concept are these copies of? Where is its canonical home? | Name the concept once; make the variants its data or parameters; delete the copies. Never accept a second copy because the first one exists. |
|
|
52
|
+
| 1 | **Duplicated concept** — one idea expressed twice: near-duplicate helper, parallel model, copy-pasted branch (Duplicated Code), a flag *and* an optional both meaning "off", the same predicate reimplemented instead of reused, the same `switch`/`if`-cascade on the same type in two places (Repeated Switches) | What single concept are these copies of? Where is its canonical home? | Name the concept once; make the variants its data or parameters (one map or one polymorphic dispatch both sites share); delete the copies. Never accept a second copy because the first one exists. |
|
|
46
53
|
| 2 | **Complexity rearranged, not deleted** — the diff moves code around but the reader still holds the same number of branches, modes, and helpers | Which reframing of the model or ownership makes these branches disappear? | Change the model so the special case becomes the default flow. Prefer the version that feels inevitable in hindsight. |
|
|
47
54
|
| 3 | **Special case in a shared flow** — new `if`, one-off boolean, nullable mode, or feature check bolted into an unrelated path | Whose decision is this, and why is it made here? | Move it behind its own abstraction (policy, resolver, dispatcher, module); keep the shared path linear. |
|
|
48
|
-
| 4 | **Wrong layer / boundary leak** — feature logic in a shared module, implementation details through an API, callers re-deriving what the owner already knows | Which module owns this concept? | Move the logic to the owner; callers receive results, not raw inputs to recompute. |
|
|
49
|
-
| 5 | **
|
|
50
|
-
| 6 | **
|
|
51
|
-
| 7 | **
|
|
52
|
-
| 8 | **
|
|
53
|
-
| 9 | **
|
|
55
|
+
| 4 | **Wrong layer / boundary leak** — feature logic in a shared module, implementation details through an API, callers re-deriving what the owner already knows, a function that works on another module's data more than its own (Feature Envy), callers walking `a.b().c().d()` to reach what they need (Message Chains) | Which module owns this concept? | Move the logic to the owner; callers receive results, not raw inputs to recompute. Hide a navigation walk behind one method on the first object. |
|
|
56
|
+
| 5 | **Change doesn't follow module lines** — one logical change forces small edits scattered across many files (Shotgun Surgery), or one module is edited for several unrelated reasons (Divergent Change) | If this rule changes again, how many files move? Does this module have one reason to change? | Gather what changes together into one module; split a module that changes for unrelated reasons. |
|
|
57
|
+
| 6 | **Indirection without clarity** — thin wrapper, pass-through helper or class that only delegates (Middle Man), "generic" mechanism hiding a simple data shape, parameters, hooks or abstractions for needs nobody has yet (Speculative Generality) | What does the reader learn from this layer? Who needs this flexibility today? | Delete it and keep the direct call. Boring and direct beats clever. |
|
|
58
|
+
| 7 | **Refused inheritance** — a subclass or implementer that ignores, stubs out, or overrides most of what it inherits (Refused Bequest) | Is this really a kind of its parent? | Drop the inheritance; compose, or implement a narrower interface. |
|
|
59
|
+
| 8 | **Weak contract** — `any`, `unknown`, casts, optional params, silent fallbacks that paper over an unclear invariant; a string or number standing in for a domain concept (Primitive Obsession); the same few fields or params always travelling together (Data Clumps) | What invariant is this hiding? Which type wants to be born? | Make the boundary explicit in the type, a small domain type or one bundled type, so the control flow simplifies. |
|
|
60
|
+
| 9 | **Mysterious name** — a function, variable, or type whose name hides what it does or holds, or says something it no longer does | Could a reader predict the body from the name? | Rename it. If no honest name comes, the concept is murky: fix the design, not the name. |
|
|
61
|
+
| 10 | **File sprawl** — the diff pushes a file past a healthy size boundary (~1k lines) | Should this be decomposed *before* adding to it? | Split by concept first; waive only with a compelling structural reason. |
|
|
62
|
+
| 11 | **Needless sequencing / non-atomic update** — independent work serialized, related updates that can leave state half-applied | Is this actually dependent? Can it be observed half-done? | Parallelize independent work; group related updates. Not micro-optimization — brittleness. |
|
|
63
|
+
| 12 | **Short-horizon change** — "temporary", TODO-later, quick patch, deferred cleanup, a shape chosen because it is the smallest diff | What does this look like when its reason is gone? What does removing it cost? | Do the durable fix now, or structure the stopgap so removal is a pure deletion (one branch, one file), never a refactor of live callers. |
|
|
54
64
|
|
|
55
65
|
Short-horizon change is the long-term rule and it applies to every other smell: prefer
|
|
56
66
|
consolidating now over a third copy later; prefer the simplification that
|
|
@@ -88,9 +98,11 @@ Write the review in this shape:
|
|
|
88
98
|
|
|
89
99
|
Working code is not the bar. `REQUEST CHANGES` when any of Duplicated
|
|
90
100
|
concept, Complexity rearranged, Special case in a shared flow, Wrong layer,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
Change doesn't follow module lines, Indirection without clarity, Refused
|
|
102
|
+
inheritance, Weak contract, or Short-horizon change is present and
|
|
103
|
+
unjustified, when a documented repo rule is broken, or when a visible
|
|
104
|
+
code-judo move was skipped. Mysterious name, File sprawl and Needless
|
|
105
|
+
sequencing block when the cleaner structure is obvious.
|
|
94
106
|
|
|
95
107
|
## Author pushback
|
|
96
108
|
|
|
@@ -102,6 +114,9 @@ Needless sequencing block when the cleaner structure is obvious.
|
|
|
102
114
|
| "The existing code already does it this way" | Existing mess licenses fixing the concept, not adding a copy of it. |
|
|
103
115
|
| "A restructure is out of scope" | If the restructure deletes the complexity this change adds, it is the scope. |
|
|
104
116
|
|
|
117
|
+
The Fowler smell list is an idea from mattpocock/skills `code-review`; the
|
|
118
|
+
wording here is ours.
|
|
119
|
+
|
|
105
120
|
## Review Tone
|
|
106
121
|
|
|
107
122
|
Be direct, serious, and demanding about quality.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review-diff
|
|
3
|
+
description: >-
|
|
4
|
+
Review a branch before merge: first a gate (the repo's check command plus
|
|
5
|
+
crap-gate), then three independent reviewers in parallel — spec (did we build
|
|
6
|
+
what was asked, no more, no less), correctness via Codex (races, idempotency,
|
|
7
|
+
money, time, authz, indexes, N+1, migrations, error paths, test quality) and
|
|
8
|
+
structure (deep-quality-review). `--quick` runs only spec and correctness,
|
|
9
|
+
shortened, for a live interview. Use when the user says "/review-diff",
|
|
10
|
+
"review this branch", "review this slice", "review before merge", or before
|
|
11
|
+
closing out a spec task.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# /review-diff
|
|
15
|
+
|
|
16
|
+
One gate, then three reviewers that never see each other's output. Each
|
|
17
|
+
answers one question; their reports stay separate, because a strong answer
|
|
18
|
+
on one axis must not hide a failure on another.
|
|
19
|
+
|
|
20
|
+
| Reviewer | Question | Runs as |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Spec | Did we build what the spec asked, no more, no less? | subagent, `spec.md` |
|
|
23
|
+
| Correctness | Can this lose money or corrupt data? | `codex exec`, `correctness.md` |
|
|
24
|
+
| Structure | Is the code getting messier? | subagent, `deep-quality-review` |
|
|
25
|
+
|
|
26
|
+
`spec.md` and `correctness.md` sit next to this file (`<skill-dir>`).
|
|
27
|
+
|
|
28
|
+
## Arguments
|
|
29
|
+
|
|
30
|
+
- A range (`A..B`) or a base ref. Default: the merge-base with the repo's
|
|
31
|
+
default branch (`origin/main`, else `main`). Review `git diff <base>...HEAD`.
|
|
32
|
+
- A spec path, optional.
|
|
33
|
+
- `--quick`: interview mode. Gate, then spec + correctness only; each report
|
|
34
|
+
keeps its top 3 findings in under 150 words. Default: all three, full
|
|
35
|
+
reports.
|
|
36
|
+
|
|
37
|
+
Before anything else, confirm the range resolves and the diff is non-empty. A
|
|
38
|
+
bad ref fails here, not inside three reviewers.
|
|
39
|
+
|
|
40
|
+
## Step 0 — gate (sequential, a precondition, not a reviewer)
|
|
41
|
+
|
|
42
|
+
1. Run the repo's check command: `make check` if the Makefile has it,
|
|
43
|
+
otherwise the one AGENTS.md / CONTRIBUTING.md / package.json documents. None
|
|
44
|
+
documented → say so and continue.
|
|
45
|
+
2. Run crap-gate if the repo has `.crap-gate.json`: prefer its wrapper
|
|
46
|
+
(`make crap-gate` / `make crap`), else `sh <crap-gate-dir>/run.sh`, where
|
|
47
|
+
the dir is the first of `.my-skills/crap-gate`, `.agents/skills/crap-gate`,
|
|
48
|
+
`.claude/skills/crap-gate`, `~/.my-skills/crap-gate` that exists. No
|
|
49
|
+
config → "crap-gate: not configured, skipped". Config but skill missing →
|
|
50
|
+
"crap-gate: skill not installed, skipped".
|
|
51
|
+
|
|
52
|
+
Either red → **stop**. Report the failing command and the first failures, and
|
|
53
|
+
no review: reviewing red code wastes the reviewers, and "is there a test for
|
|
54
|
+
this scenario" means nothing while tests fail.
|
|
55
|
+
|
|
56
|
+
## Steps 1–3 — reviewers, in parallel
|
|
57
|
+
|
|
58
|
+
Launch all of them in one go; don't let one wait for another. Give each only
|
|
59
|
+
the range, the commit list (`git log --oneline <range>`) and its brief. Never
|
|
60
|
+
paste one reviewer's output into another's prompt.
|
|
61
|
+
|
|
62
|
+
- **Spec.** Find the spec: the path argument, then a spec under `specs/`
|
|
63
|
+
matching the branch or feature (skip templates), then issue refs in the
|
|
64
|
+
commits (`#123`, fetched with `gh issue view`). None → skip: "Spec: no spec
|
|
65
|
+
found, skipped". Otherwise spawn a subagent: "Read and follow
|
|
66
|
+
`<skill-dir>/spec.md`. Range: <range>. Commits: <list>. Spec: <path or fetched text>."
|
|
67
|
+
- **Correctness.** If `codex` is on PATH, run it (in the background while
|
|
68
|
+
the others run):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
out=$(mktemp) && codex exec -s read-only --ephemeral -o "$out" \
|
|
72
|
+
"Read and follow <skill-dir>/correctness.md. Range: <range>. Commits: <list>.<quick>" \
|
|
73
|
+
>/dev/null 2>&1; cat "$out"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
No Codex → spawn a subagent with the same instruction and note "Correctness:
|
|
77
|
+
Codex unavailable, ran on <current model>". The second model is the point;
|
|
78
|
+
never drop the note.
|
|
79
|
+
- **Structure** (skipped in `--quick`). Spawn a subagent: "Use the
|
|
80
|
+
`deep-quality-review` skill on range <range>." Skill not installed → skip:
|
|
81
|
+
"Structure: deep-quality-review not installed, skipped".
|
|
82
|
+
|
|
83
|
+
In `--quick`, append to each brief: "Quick mode: top 3 findings, under 150
|
|
84
|
+
words." No subagents in this harness → run them one after another, each in a
|
|
85
|
+
fresh context if you can, and say so.
|
|
86
|
+
|
|
87
|
+
## Output
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Gate: `make check` green · crap-gate green
|
|
91
|
+
|
|
92
|
+
## Spec
|
|
93
|
+
<report, verbatim or lightly cleaned>
|
|
94
|
+
|
|
95
|
+
## Correctness (Codex)
|
|
96
|
+
<report>
|
|
97
|
+
|
|
98
|
+
## Structure
|
|
99
|
+
<report>
|
|
100
|
+
|
|
101
|
+
Spec: 3 findings — worst: <one line>
|
|
102
|
+
Correctness: 1 finding — worst: <one line>
|
|
103
|
+
Structure: REQUEST CHANGES, 2 findings — worst: <one line>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A skipped step keeps its place with its one-line note (in the gate line, or
|
|
107
|
+
as the section body and summary line). `--quick` has no Structure section and
|
|
108
|
+
no Structure summary line. A clean reviewer's summary line is `0 findings`.
|
|
109
|
+
|
|
110
|
+
Do not merge, dedupe or re-rank findings across reviewers, and do not pick an
|
|
111
|
+
overall winner. A finding two reviewers both raise stays in both reports; that
|
|
112
|
+
is signal.
|
|
113
|
+
|
|
114
|
+
## Dependencies
|
|
115
|
+
|
|
116
|
+
Needs the `deep-quality-review` and `crap-gate` skills (install them with the
|
|
117
|
+
skills CLI next to this one) and, for the second model, the `codex` CLI. Any
|
|
118
|
+
of them missing skips that step with a one-line note; it never fails the
|
|
119
|
+
review.
|
|
120
|
+
|
|
121
|
+
The spec axis is an idea from mattpocock/skills `code-review`; the wording
|
|
122
|
+
here is ours.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Correctness reviewer
|
|
2
|
+
|
|
3
|
+
You check one thing: **can this diff lose money, corrupt data, or let the
|
|
4
|
+
wrong person act?** Not style, not structure, not spec coverage. Assume lint,
|
|
5
|
+
types and tests already pass.
|
|
6
|
+
|
|
7
|
+
Read `git diff <range>` in full, then only the surrounding code a check needs
|
|
8
|
+
(the model behind a query, the existing indexes, the caller of a handler).
|
|
9
|
+
|
|
10
|
+
| Check | Flag |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Concurrency | Read-then-write without a lock, conditional `UPDATE … WHERE`, or DB constraint: overselling, double-spend, lost updates. Transactions that commit half the work. |
|
|
13
|
+
| Idempotency | Webhooks, payment callbacks, retries and queue consumers that do the work twice when the same message arrives twice. |
|
|
14
|
+
| Money | Floats for amounts (use integer cents or decimal), rounding in more than one place, currency dropped. |
|
|
15
|
+
| Time | Naive datetimes, local time stored, "today" computed without the business timezone. Store UTC. |
|
|
16
|
+
| Authorization | A check done only in the client, or missing on the server: can user A read or change user B's rows by changing an id? |
|
|
17
|
+
| Foreign keys | A new FK column that is filtered or joined on without an index. |
|
|
18
|
+
| N+1 | One query per loop iteration where a join or `IN (…)` would do. |
|
|
19
|
+
| Migrations | Not reversible, or unsafe on a live table (a NOT NULL column without a default, a rewrite under lock). |
|
|
20
|
+
| Failure paths | A payment-provider, checkout or stock-decrement call whose failure (timeout, decline, provider error) is unhandled or silently swallowed. |
|
|
21
|
+
| Test quality | Tautological tests (asserting a mock's return value, or a value the test just set with no code in between) and tests on internals instead of the public seam. Say what they should assert. |
|
|
22
|
+
|
|
23
|
+
Every finding: `file:line`, the concrete failure scenario (inputs or
|
|
24
|
+
interleaving → wrong result), and the fix. "Consider reviewing" is not a
|
|
25
|
+
finding. Report only what you can point at; mark anything unconfirmed
|
|
26
|
+
"(unverified)".
|
|
27
|
+
|
|
28
|
+
Output: findings, worst first, then one line naming the checks with nothing
|
|
29
|
+
to report ("Clean: money, time, …"). Under 400 words.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Spec reviewer
|
|
2
|
+
|
|
3
|
+
You check one thing: **did this diff build what the spec asked, no more, no
|
|
4
|
+
less?** Not style, not structure, not bugs outside the spec.
|
|
5
|
+
|
|
6
|
+
1. Read the spec in full, then `git diff <range>` in full (not a summary),
|
|
7
|
+
then the tests the diff adds or touches.
|
|
8
|
+
2. Walk the spec requirement by requirement and report:
|
|
9
|
+
- **Missing / partial**: asked for, not built or only half built.
|
|
10
|
+
- **Scope creep**: built, not asked for (endpoints, flags, fields, UI,
|
|
11
|
+
options). Say what to delete.
|
|
12
|
+
- **Built wrong**: looks implemented, but behaves differently from the
|
|
13
|
+
spec line.
|
|
14
|
+
- **Untested scenario**: each Given/When/Then scenario with no test that
|
|
15
|
+
exercises it through the public seam (the API, the UI, the use case), not
|
|
16
|
+
through an internal helper. A test that only passes because a layer
|
|
17
|
+
above the one the scenario names stops the input (e.g. API validation
|
|
18
|
+
hiding an untested DB constraint) does not count.
|
|
19
|
+
3. Every finding quotes the spec line it rests on, then `file:line` and the
|
|
20
|
+
concrete fix. No spec line → not a finding.
|
|
21
|
+
|
|
22
|
+
Output: findings grouped under those four headings. Leave out empty groups
|
|
23
|
+
and end with one line naming them ("Clean: scope creep, built wrong"). Don't
|
|
24
|
+
list what was fine. Under 400 words.
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-23T16:36:43.907Z",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"name": "backlog",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "crap-gate",
|
|
25
|
-
"version": "0.1.
|
|
26
|
-
"hash": "sha256-
|
|
27
|
-
"description": "Score every function a branch changed with CRAP (cyclomatic complexity × untested fraction), fail on existing functions
|
|
25
|
+
"version": "0.1.3",
|
|
26
|
+
"hash": "sha256-39041927a985061599a39bb287232b83655ecbac61b7de52486f485991a99d30",
|
|
27
|
+
"description": "Score every function a branch changed with CRAP (cyclomatic complexity × untested fraction), fail on existing functions above 15 and new functions above 9, and drive a fixer + independent reviewer loop until the gate passes. Use when implementation is done and before pushing, when the user says \"run the crap gate\", \"check CRAP\", \"is this change tested enough\", or when a push gate reports CRAP violations."
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"name": "deep-quality-review",
|
|
31
|
-
"version": "0.1.
|
|
32
|
-
"hash": "sha256-
|
|
31
|
+
"version": "0.1.2",
|
|
32
|
+
"hash": "sha256-134c1a194e9ec53f244e6500e68cf963b384759d8b5f5c934784665b6ef4b8f6",
|
|
33
33
|
"description": "Extremely strict maintainability review of a branch's changes: duplicated concepts, complexity that got rearranged instead of deleted, spaghetti branching, boundary leaks, weak type contracts, file sprawl, and \"temporary\" fixes that will outlive their reason. Use when the user asks for a \"deep code quality review\", \"thermonuclear review\", \"harsh maintainability review\", \"code judo\", \"is this making the codebase messier\", or before merging a change that touches shared code. Not a correctness, security, or test-coverage review."
|
|
34
34
|
},
|
|
35
35
|
{
|
|
@@ -56,6 +56,12 @@
|
|
|
56
56
|
"hash": "sha256-211e0e116ff6efbc4589ea7264835855b9495ba535f242733af913903fe7707d",
|
|
57
57
|
"description": "Build a throwaway prototype to answer a design question, delivered as one self-contained HTML file. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like."
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
"name": "review-diff",
|
|
61
|
+
"version": "0.1.0",
|
|
62
|
+
"hash": "sha256-f1efdffdad0abf536777a3afcc733b2612ed51abad0b1a04d24f280bf2f223d3",
|
|
63
|
+
"description": "Review a branch before merge: first a gate (the repo's check command plus crap-gate), then three independent reviewers in parallel — spec (did we build what was asked, no more, no less), correctness via Codex (races, idempotency, money, time, authz, indexes, N+1, migrations, error paths, test quality) and structure (deep-quality-review). `--quick` runs only spec and correctness, shortened, for a live interview. Use when the user says \"/review-diff\", \"review this branch\", \"review this slice\", \"review before merge\", or before closing out a spec task."
|
|
64
|
+
},
|
|
59
65
|
{
|
|
60
66
|
"name": "to-spec",
|
|
61
67
|
"version": "0.1.0",
|