@jesdi/skills 0.0.14 → 0.0.15
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/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-manifest.json +4 -4
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"
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-13T13:58:48.949Z",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"name": "backlog",
|
|
@@ -22,9 +22,9 @@
|
|
|
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",
|