cursordoctrine 0.2.2 → 0.3.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/INSTALL.md +1 -1
- package/README.md +4 -3
- package/bin/cli.mjs +7 -6
- package/linux/hooks/minimal-edit-audit.sh +8 -0
- package/linux/hooks/semantic-density-audit.sh +151 -0
- package/linux/hooks/subagent-stop-review.sh +103 -103
- package/linux/hooks.json +7 -1
- package/package.json +5 -2
- package/skills/anti-slop/SKILL.md +9 -5
- package/skills/anti-slop/scripts/density_scan.py +78 -0
- package/skills/anti-slop/scripts/low_density.py +405 -0
- package/skills/anti-slop/scripts/scan_slop.py +29 -8
- package/windows/hooks/anti-slop-audit.ps1 +226 -226
- package/windows/hooks/final-review.md +67 -67
- package/windows/hooks/minimal-edit-audit.ps1 +124 -116
- package/windows/hooks/semantic-density-audit.ps1 +137 -0
- package/windows/hooks.json +70 -64
package/INSTALL.md
CHANGED
|
@@ -110,4 +110,4 @@ Also validate the config: `~/.cursor/hooks.json` must parse as JSON.
|
|
|
110
110
|
|
|
111
111
|
Tell the user what was installed, which checks passed, and anything that failed with the exact error. Do not silently work around a failing check.
|
|
112
112
|
|
|
113
|
-
Kill switches if something misbehaves: `HOOKS_ENFORCE=0` (everything advisory off), `PERM_GATE_ENFORCE=0`, `MINIMAL_EDITING_ENFORCE=0`, `ANTI_SLOP_ENFORCE=0`, `FINAL_REVIEW_ENFORCE=0`, `SUBAGENT_REVIEW_ENFORCE=0`.
|
|
113
|
+
Kill switches if something misbehaves: `HOOKS_ENFORCE=0` (everything advisory off), `PERM_GATE_ENFORCE=0`, `MINIMAL_EDITING_ENFORCE=0` (deprecated in 0.3.0), `SEMANTIC_DENSITY_ENFORCE=0`, `ANTI_SLOP_ENFORCE=0`, `FINAL_REVIEW_ENFORCE=0`, `SUBAGENT_REVIEW_ENFORCE=0`.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Thin self-review hooks for Cursor. Five hook events, one message bus. The model
|
|
|
7
7
|
A small set of Cursor hooks that make the agent review its own work without bolting a static-analysis pipeline onto every keystroke. There is no regex army and no scoring engine. The hooks do three jobs:
|
|
8
8
|
|
|
9
9
|
1. **Inject the doctrine** at session start, so every chat begins with the same short governing text (`doctrine.md` + `USER-RULES.md`).
|
|
10
|
-
2. **Hand the model its own edits back.** After each agent edit, a self-review prompt (plus minimal-edit and anti-slop advisories when they trip) is stashed and delivered on the next turn. The model reads its own diff, fixes real bugs, and stays quiet otherwise.
|
|
10
|
+
2. **Hand the model its own edits back.** After each agent edit, a self-review prompt (plus minimal-edit, semantic-density, and anti-slop advisories when they trip) is stashed and delivered on the next turn. The model reads its own diff, fixes real bugs, and stays quiet otherwise.
|
|
11
11
|
3. **Gate blast radius.** One permission gate denies a short, explicit list of dangerous commands (`rm -rf /`, `curl | sh`, force-push, `npm publish`, ...). Everything else is allowed.
|
|
12
12
|
|
|
13
13
|
When an implementation finishes, a stop hook fires exactly one final review pass over everything that changed — then stops. The review runs across five axes, the first of which is **intent trace**: the hook extracts your last user message from the transcript and prepends it to the review so the model must trace every diff hunk back to a concrete request. Anything untraceable is a hallucinated requirement and gets reverted — this is the only detector that catches "clean code, wrong feature," which no later axis and no linter can see. Delegated work gets the same treatment: a subagent that edited files reviews its own implementation before its result returns to the parent, and its edits are folded into the parent's final review. Every bound is enforced twice: in the script and in `hooks.json`.
|
|
@@ -41,7 +41,7 @@ The two folders are functionally identical. Windows runs everything through `pws
|
|
|
41
41
|
| Session | `sessionStart` | `inject-doctrine` reads the doctrine + user rules and emits them as `additional_context`. |
|
|
42
42
|
| Every turn | `postToolUse` | Folds completed subagents' edit markers into this conversation's marker, then drains the conversation's pending feedback file into `additional_context`. One-shot, keyed by conversation id. |
|
|
43
43
|
| Shell | `beforeShellExecution` | `permission-gate` checks the command against a deny list. Allow by default, deny by list, fail open. |
|
|
44
|
-
| Edit | `afterFileEdit` + `stop` | `self-review-trigger` stashes the review prompt per edit; `minimal-edit-audit` and `anti-slop-audit` append advisories when thresholds trip (new deps / premature abstraction / redundant comments / Tier 3 operational slop: retry-without-backoff, await-in-loop, telemetry spam); `final-review` fires one end-of-implementation pass. |
|
|
44
|
+
| Edit | `afterFileEdit` + `stop` | `self-review-trigger` stashes the review prompt per edit; `minimal-edit-audit` (deprecated in 0.3.0), `semantic-density-audit`, and `anti-slop-audit` append advisories when thresholds trip (new deps / premature abstraction / redundant comments / **semantic opacity**: low-density identifiers like `DataManager`, `process()`, `utils.ts` / Tier 3 operational slop: retry-without-backoff, await-in-loop, telemetry spam); `final-review` fires one end-of-implementation pass. |
|
|
45
45
|
| Subagent | `subagentStop` | `subagent-stop-review` fires one in-subagent final review when a delegated run edited files, before the result returns to the parent. Marker-gated and flag-braked like `final-review`. |
|
|
46
46
|
|
|
47
47
|
## Install
|
|
@@ -69,7 +69,8 @@ All hooks fail open and always exit 0. Nothing here can block your session.
|
|
|
69
69
|
|---|---|---|
|
|
70
70
|
| `HOOKS_ENFORCE=0` | on | turns off all advisory hooks at once |
|
|
71
71
|
| `PERM_GATE_ENFORCE=0` | on | disables the permission gate |
|
|
72
|
-
| `MINIMAL_EDITING_ENFORCE=0` | on | disables the over-edit advisory |
|
|
72
|
+
| `MINIMAL_EDITING_ENFORCE=0` | on | disables the over-edit advisory (deprecated in 0.3.0) |
|
|
73
|
+
| `SEMANTIC_DENSITY_ENFORCE=0` | on | disables the semantic-opacity advisory |
|
|
73
74
|
| `ANTI_SLOP_ENFORCE=0` | on | disables the slop advisory |
|
|
74
75
|
| `FINAL_REVIEW_ENFORCE=0` | on | disables the final review pass |
|
|
75
76
|
| `SUBAGENT_REVIEW_ENFORCE=0` | on | disables the in-subagent review pass |
|
package/bin/cli.mjs
CHANGED
|
@@ -370,12 +370,13 @@ Examples
|
|
|
370
370
|
npx cursordoctrine uninstall
|
|
371
371
|
|
|
372
372
|
Kill switches (environment variables, all hooks fail open)
|
|
373
|
-
HOOKS_ENFORCE=0
|
|
374
|
-
PERM_GATE_ENFORCE=0
|
|
375
|
-
MINIMAL_EDITING_ENFORCE=0
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
373
|
+
HOOKS_ENFORCE=0 everything advisory off
|
|
374
|
+
PERM_GATE_ENFORCE=0 permission gate off
|
|
375
|
+
MINIMAL_EDITING_ENFORCE=0 over-edit advisory off (deprecated in 0.3.0)
|
|
376
|
+
SEMANTIC_DENSITY_ENFORCE=0 semantic-opacity advisory off
|
|
377
|
+
ANTI_SLOP_ENFORCE=0 slop advisory off
|
|
378
|
+
FINAL_REVIEW_ENFORCE=0 final review off
|
|
379
|
+
SUBAGENT_REVIEW_ENFORCE=0 in-subagent review off
|
|
379
380
|
|
|
380
381
|
Docs https://github.com/kleosr/cursordoctrine`);
|
|
381
382
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# minimal-edit-audit.sh - afterFileEdit minimal-editing advisory (Cursor, Linux).
|
|
3
3
|
#
|
|
4
|
+
# DEPRECATED in 0.3.0 (superseded by semantic-density-audit.sh + the
|
|
5
|
+
# declared-editing discipline; removal slated for 0.4.0). The line-count
|
|
6
|
+
# heuristic here is the size-based gate the cursordoctrine audit identified
|
|
7
|
+
# as the antipattern: it penalizes legitimate large declared changes and
|
|
8
|
+
# misses small quiet drifts. Retained for compatibility with existing installs;
|
|
9
|
+
# new installs register semantic-density-audit.sh alongside it. To opt out of
|
|
10
|
+
# the deprecated hook without uninstalling: MINIMAL_EDITING_ENFORCE=0.
|
|
11
|
+
#
|
|
4
12
|
# Audits the just-edited file for over-editing:
|
|
5
13
|
# * line-count - git diff --numstat thresholds (any language).
|
|
6
14
|
# * token metrics - audit-metrics.py (token-Levenshtein + cognitive
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# semantic-density-audit.sh - afterFileEdit "semantic opacity" advisory (Cursor, Linux).
|
|
3
|
+
#
|
|
4
|
+
# Guards the naming layer the other audit hooks do not see. minimal-edit-audit
|
|
5
|
+
# watches diff SIZE; anti-slop-audit watches generated-code PATTERNS; this hook
|
|
6
|
+
# watches whether the identifiers the agent JUST introduced actually communicate
|
|
7
|
+
# intent. DataManager, process(), utils.ts, CoreEngine - names that exist but
|
|
8
|
+
# say nothing.
|
|
9
|
+
#
|
|
10
|
+
# Mechanism: extract ADDED lines from `git diff HEAD -- <rel>` (with the
|
|
11
|
+
# untracked-file fallback anti-slop-audit uses), pipe them to density_scan.py
|
|
12
|
+
# (a thin wrapper over the shared low_density module), read back one JSON
|
|
13
|
+
# object of findings, append a short advisory to the shared pending-feedback
|
|
14
|
+
# file. One denylist, shared with scan_slop.py's semantic_density bucket -
|
|
15
|
+
# zero drift between the per-edit advisory and the audit-of-record.
|
|
16
|
+
#
|
|
17
|
+
# FAIL findings (DataManager / Utils / placeholder names) always fire. WARN
|
|
18
|
+
# findings (defensible DDD with a domain noun - PostgresUserRepository) only
|
|
19
|
+
# fire when at least one FAIL is also present, so the hook stays quiet on
|
|
20
|
+
# legitimate code and loud on the real slop.
|
|
21
|
+
#
|
|
22
|
+
# Advisory only: never blocks, never persists state, ALWAYS exits 0.
|
|
23
|
+
# Disable: HOOKS_ENFORCE=0 or SEMANTIC_DENSITY_ENFORCE=0
|
|
24
|
+
|
|
25
|
+
set +e
|
|
26
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
27
|
+
|
|
28
|
+
[ "${HOOKS_ENFORCE:-}" = "0" ] && exit 0
|
|
29
|
+
[ "${SEMANTIC_DENSITY_ENFORCE:-}" = "0" ] && exit 0
|
|
30
|
+
|
|
31
|
+
input="$(read_hook_stdin)"
|
|
32
|
+
[ -n "$input" ] || exit 0
|
|
33
|
+
|
|
34
|
+
# audit root: project from JSON (cwd, then workspace_roots), else CURSOR_PROJECT_DIR / HOME
|
|
35
|
+
root=""
|
|
36
|
+
while IFS= read -r cand; do
|
|
37
|
+
[ -n "$cand" ] && [ -d "$cand" ] && { root="${cand%/}"; break; }
|
|
38
|
+
done <<EOF
|
|
39
|
+
$(json_get "$input" cwd)
|
|
40
|
+
$(json_get_array "$input" workspace_roots)
|
|
41
|
+
EOF
|
|
42
|
+
[ -n "$root" ] || root="${CURSOR_PROJECT_DIR:-$HOME}"
|
|
43
|
+
root="${root%/}"
|
|
44
|
+
|
|
45
|
+
# edited file -> repo-relative path
|
|
46
|
+
fp=""
|
|
47
|
+
for k in file_path path filename absolute_path abs_path; do
|
|
48
|
+
fp="$(json_get "$input" "$k")"
|
|
49
|
+
[ -n "$fp" ] && break
|
|
50
|
+
done
|
|
51
|
+
[ -n "$fp" ] || exit 0
|
|
52
|
+
rel="$fp"
|
|
53
|
+
case "$rel" in "$root"/*) rel="${rel#"$root"/}" ;; esac
|
|
54
|
+
if is_cursor_config_path "$fp" || is_cursor_config_path "$rel"; then exit 0; fi
|
|
55
|
+
|
|
56
|
+
# git repo?
|
|
57
|
+
git -C "$root" rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
58
|
+
|
|
59
|
+
# --- collect ADDED lines for this file (working tree vs HEAD) --------------
|
|
60
|
+
added="$(git -C "$root" diff HEAD -- "$rel" 2>/dev/null |
|
|
61
|
+
grep -E '^\+' | grep -vE '^\+\+\+' | cut -c2- | head -n 1500)"
|
|
62
|
+
if [ -z "$added" ]; then
|
|
63
|
+
# untracked / brand-new file: whole file is "added"
|
|
64
|
+
if ! git -C "$root" ls-files --error-unmatch -- "$rel" >/dev/null 2>&1; then
|
|
65
|
+
[ -f "$root/$rel" ] && added="$(head -n 1500 "$root/$rel")"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
[ -n "$added" ] || exit 0
|
|
69
|
+
|
|
70
|
+
# --- resolve Python + run density_scan.py ---------------------------------
|
|
71
|
+
# Linux ships python3; fall back to python for older distros.
|
|
72
|
+
py=""
|
|
73
|
+
for c in python3 python; do
|
|
74
|
+
if command -v "$c" >/dev/null 2>&1; then py="$c"; break; fi
|
|
75
|
+
done
|
|
76
|
+
[ -n "$py" ] || exit 0 # no Python -> fail open, scanner unavailable
|
|
77
|
+
|
|
78
|
+
scanner="$HOME/.cursor/skills/anti-slop/scripts/density_scan.py"
|
|
79
|
+
[ -f "$scanner" ] || exit 0 # skill not installed -> silent
|
|
80
|
+
|
|
81
|
+
# Pipe added lines to the scanner, read JSON back.
|
|
82
|
+
mout="$(printf '%s\n' "$added" | "$py" "$scanner" --rel "$rel" 2>/dev/null)"
|
|
83
|
+
[ -n "$mout" ] || exit 0
|
|
84
|
+
|
|
85
|
+
# --- parse JSON findings with python (the hook already requires python) ----
|
|
86
|
+
# jq would be ideal but the installer notes python3 as the fallback; reuse it.
|
|
87
|
+
parse_json() {
|
|
88
|
+
"$py" - "$@" <<'PYEOF' 2>/dev/null
|
|
89
|
+
import json, sys
|
|
90
|
+
try:
|
|
91
|
+
p = json.loads(sys.stdin.read())
|
|
92
|
+
except Exception:
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
fails = [f for f in p.get("findings", []) if f.get("severity") == "fail"]
|
|
95
|
+
warns = [f for f in p.get("findings", []) if f.get("severity") == "warn"]
|
|
96
|
+
if not fails and not warns:
|
|
97
|
+
sys.exit(2)
|
|
98
|
+
# WARNs only fire alongside a FAIL (defensible DDD stays quiet on clean code).
|
|
99
|
+
flagged = fails + (warns if fails else [])
|
|
100
|
+
lines = []
|
|
101
|
+
for f in (flagged)[:12]:
|
|
102
|
+
tag = f.get("severity", "").upper()
|
|
103
|
+
ln = f.get("line", 0)
|
|
104
|
+
where = f"line {ln}" if ln and ln > 0 else "file name"
|
|
105
|
+
reason = "; ".join(f.get("reasons", []))
|
|
106
|
+
if len(reason) > 110:
|
|
107
|
+
reason = reason[:107] + "..."
|
|
108
|
+
lines.append(f" [{tag}] {f.get('kind','?')} '{f.get('name','?')}' ({where}): {reason}")
|
|
109
|
+
print("\n".join(lines))
|
|
110
|
+
print(f"__COUNTS__{len(fails)}__{len(warns)}")
|
|
111
|
+
PYEOF
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
parsed="$(printf '%s' "$mout" | parse_json)"
|
|
115
|
+
rc=$?
|
|
116
|
+
[ "$rc" -eq 0 ] || exit 0 # parse failed or no findings -> silent
|
|
117
|
+
|
|
118
|
+
# Split the parsed output: findings lines + the __COUNTS__N__N sentinel.
|
|
119
|
+
counts_line="$(printf '%s\n' "$parsed" | grep '__COUNTS__' | tail -1)"
|
|
120
|
+
findings_block="$(printf '%s\n' "$parsed" | grep -v '__COUNTS__')"
|
|
121
|
+
fail_n="$(printf '%s' "$counts_line" | sed -E 's/.*__COUNTS__([0-9]+)__.*/\1/')"
|
|
122
|
+
warn_n="$(printf '%s' "$counts_line" | sed -E 's/.*__[0-9]+__([0-9]+)/\1/')"
|
|
123
|
+
|
|
124
|
+
# --- compose advisory ------------------------------------------------------
|
|
125
|
+
summary="Semantic-density audit - $rel - ${fail_n} FAIL, ${warn_n} WARN"
|
|
126
|
+
|
|
127
|
+
advice=' High-density names are predictable from the name alone (InvoiceEmailSender,
|
|
128
|
+
PostgresUserRepository, GenerateMonthlyReport). Low-density names name a
|
|
129
|
+
category, not a thing (Manager, Utils, process, handleThing). Rename so the
|
|
130
|
+
identifier states its concrete responsibility. WARNs with a domain noun are
|
|
131
|
+
defensible DDD and can be left if intentional.'
|
|
132
|
+
|
|
133
|
+
msg="${summary}
|
|
134
|
+
|
|
135
|
+
${findings_block}
|
|
136
|
+
|
|
137
|
+
${advice}
|
|
138
|
+
|
|
139
|
+
(Advisory; disable: SEMANTIC_DENSITY_ENFORCE=0)"
|
|
140
|
+
|
|
141
|
+
# --- append to the shared pending file --------------------------------------
|
|
142
|
+
cid="$(safe_conversation_id "$input")"
|
|
143
|
+
pending="$(hooks_pending_dir)/feedback-${cid}.txt"
|
|
144
|
+
mkdir -p "$(dirname "$pending")" 2>/dev/null
|
|
145
|
+
if [ -s "$pending" ]; then
|
|
146
|
+
printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
|
|
147
|
+
else
|
|
148
|
+
printf '%s' "$msg" >> "$pending" 2>/dev/null
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
exit 0
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# subagent-stop-review.sh - subagentStop for Cursor (Linux).
|
|
3
|
-
#
|
|
4
|
-
# Counterpart of final-review.sh for delegated work. afterFileEdit DOES fire
|
|
5
|
-
# inside subagents (verified: a subagent run left its edits in
|
|
6
|
-
# session-edits-<subagent-cid>.txt), but subagents get no `stop` event, so
|
|
7
|
-
# that marker is never drained and the five-axis review never fires for
|
|
8
|
-
# delegated implementations. This hook closes the loop: when a subagent
|
|
9
|
-
# finishes and ITS conversation has a session-edits marker, return ONE
|
|
10
|
-
# followup_message so the subagent audits its own implementation before the
|
|
11
|
-
# result goes back to the parent.
|
|
12
|
-
#
|
|
13
|
-
# Same bounding pattern as final-review.sh:
|
|
14
|
-
# - marker-gated: no edits in the subagent run -> no review, no noise,
|
|
15
|
-
# - reviewed-<cid>.flag one-shot brake: the stop AFTER the review pass
|
|
16
|
-
# clears flag + marker and ends the loop (one review per implementation;
|
|
17
|
-
# resumed subagents with a second implementation get a second review),
|
|
18
|
-
# - loop_limit in hooks.json caps runaway follow-ups harness-side,
|
|
19
|
-
# - only on status == 'completed' when a status field is present.
|
|
20
|
-
#
|
|
21
|
-
# If subagentStop's stdin carries a conversation_id that doesn't match the
|
|
22
|
-
# id afterFileEdit used, the marker lookup misses and this emits {} - the
|
|
23
|
-
# marker fold in post-tool-use.sh / final-review.sh still routes the
|
|
24
|
-
# subagent's edits into the parent's stop review as the backstop.
|
|
25
|
-
#
|
|
26
|
-
# Always emits valid JSON ({} = no follow-up). Review body reuses
|
|
27
|
-
# final-review.md (embedded fallback if missing).
|
|
28
|
-
# Disable: HOOKS_ENFORCE=0 or SUBAGENT_REVIEW_ENFORCE=0.
|
|
29
|
-
|
|
30
|
-
set +e
|
|
31
|
-
. "$(dirname "$0")/hook-common.sh"
|
|
32
|
-
|
|
33
|
-
emit_none() { printf '{}'; exit 0; }
|
|
34
|
-
|
|
35
|
-
[ "${HOOKS_ENFORCE:-}" = "0" ] && emit_none
|
|
36
|
-
[ "${SUBAGENT_REVIEW_ENFORCE:-}" = "0" ] && emit_none
|
|
37
|
-
|
|
38
|
-
input="$(read_hook_stdin)"
|
|
39
|
-
[ -n "$input" ] || emit_none
|
|
40
|
-
|
|
41
|
-
status="$(json_get "$input" status)"
|
|
42
|
-
cid="$(safe_conversation_id "$input")"
|
|
43
|
-
|
|
44
|
-
pending_dir="$(hooks_pending_dir)"
|
|
45
|
-
marker="$pending_dir/session-edits-$cid.txt"
|
|
46
|
-
flag="$pending_dir/reviewed-$cid.flag"
|
|
47
|
-
|
|
48
|
-
# One-shot brake: the previous subagentStop for this id emitted the review.
|
|
49
|
-
if [ -f "$flag" ]; then
|
|
50
|
-
rm -f "$flag" "$marker" 2>/dev/null
|
|
51
|
-
emit_none
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
# Review only a clean completion; otherwise clear the marker and stop.
|
|
55
|
-
if [ -n "$status" ] && [ "$status" != "completed" ]; then
|
|
56
|
-
rm -f "$marker" 2>/dev/null
|
|
57
|
-
emit_none
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# No edits this run -> nothing to review.
|
|
61
|
-
[ -f "$marker" ] || emit_none
|
|
62
|
-
edited="$(grep -vE '^[[:space:]]*$' "$marker" 2>/dev/null | sort -u)"
|
|
63
|
-
rm -f "$marker" 2>/dev/null
|
|
64
|
-
[ -n "$edited" ] || emit_none
|
|
65
|
-
|
|
66
|
-
# Compose the follow-up review prompt (md preferred, embedded fallback).
|
|
67
|
-
prompt_file="$HOME/.agents/hooks/final-review.md"
|
|
68
|
-
body=""
|
|
69
|
-
[ -f "$prompt_file" ] && body="$(cat "$prompt_file")"
|
|
70
|
-
if [ -z "$body" ]; then
|
|
71
|
-
body='Audit everything you changed in this run and FIX what fails (do NOT revert the
|
|
72
|
-
behaviour the task asked for):
|
|
73
|
-
1. Correctness - logic, edge cases (null/empty/zero/boundary), language traps, security.
|
|
74
|
-
2. Reliability - error paths handled, no swallowed errors, resources released.
|
|
75
|
-
3. Coverage - behaviour-bearing changes have real tests; RUN the suite if present.
|
|
76
|
-
4. Anti-slop - if ~/.cursor/skills/anti-slop/scripts/scan_slop.py exists, run
|
|
77
|
-
`python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all`; otherwise
|
|
78
|
-
apply ~/.agents/hooks/anti-slop.md to the session diff.
|
|
79
|
-
If an axis is clean, say so in one line. Then stop.'
|
|
80
|
-
fi
|
|
81
|
-
body="$(expand_agent_paths "$body")"
|
|
82
|
-
|
|
83
|
-
file_list=""
|
|
84
|
-
while IFS= read -r p; do
|
|
85
|
-
[ -n "$p" ] || continue
|
|
86
|
-
rp="$(resolve_agent_path "$p")"
|
|
87
|
-
file_list="${file_list} ${rp}"$'\n'
|
|
88
|
-
done <<EOF
|
|
89
|
-
$edited
|
|
90
|
-
EOF
|
|
91
|
-
file_list="$(printf '%s' "$file_list" | head -n 30)"
|
|
92
|
-
msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.
|
|
93
|
-
|
|
94
|
-
Files you changed this run:
|
|
95
|
-
$file_list
|
|
96
|
-
|
|
97
|
-
$body"
|
|
98
|
-
|
|
99
|
-
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
100
|
-
touch "$flag" 2>/dev/null
|
|
101
|
-
|
|
102
|
-
emit_json followup_message "$msg"
|
|
103
|
-
exit 0
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# subagent-stop-review.sh - subagentStop for Cursor (Linux).
|
|
3
|
+
#
|
|
4
|
+
# Counterpart of final-review.sh for delegated work. afterFileEdit DOES fire
|
|
5
|
+
# inside subagents (verified: a subagent run left its edits in
|
|
6
|
+
# session-edits-<subagent-cid>.txt), but subagents get no `stop` event, so
|
|
7
|
+
# that marker is never drained and the five-axis review never fires for
|
|
8
|
+
# delegated implementations. This hook closes the loop: when a subagent
|
|
9
|
+
# finishes and ITS conversation has a session-edits marker, return ONE
|
|
10
|
+
# followup_message so the subagent audits its own implementation before the
|
|
11
|
+
# result goes back to the parent.
|
|
12
|
+
#
|
|
13
|
+
# Same bounding pattern as final-review.sh:
|
|
14
|
+
# - marker-gated: no edits in the subagent run -> no review, no noise,
|
|
15
|
+
# - reviewed-<cid>.flag one-shot brake: the stop AFTER the review pass
|
|
16
|
+
# clears flag + marker and ends the loop (one review per implementation;
|
|
17
|
+
# resumed subagents with a second implementation get a second review),
|
|
18
|
+
# - loop_limit in hooks.json caps runaway follow-ups harness-side,
|
|
19
|
+
# - only on status == 'completed' when a status field is present.
|
|
20
|
+
#
|
|
21
|
+
# If subagentStop's stdin carries a conversation_id that doesn't match the
|
|
22
|
+
# id afterFileEdit used, the marker lookup misses and this emits {} - the
|
|
23
|
+
# marker fold in post-tool-use.sh / final-review.sh still routes the
|
|
24
|
+
# subagent's edits into the parent's stop review as the backstop.
|
|
25
|
+
#
|
|
26
|
+
# Always emits valid JSON ({} = no follow-up). Review body reuses
|
|
27
|
+
# final-review.md (embedded fallback if missing).
|
|
28
|
+
# Disable: HOOKS_ENFORCE=0 or SUBAGENT_REVIEW_ENFORCE=0.
|
|
29
|
+
|
|
30
|
+
set +e
|
|
31
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
32
|
+
|
|
33
|
+
emit_none() { printf '{}'; exit 0; }
|
|
34
|
+
|
|
35
|
+
[ "${HOOKS_ENFORCE:-}" = "0" ] && emit_none
|
|
36
|
+
[ "${SUBAGENT_REVIEW_ENFORCE:-}" = "0" ] && emit_none
|
|
37
|
+
|
|
38
|
+
input="$(read_hook_stdin)"
|
|
39
|
+
[ -n "$input" ] || emit_none
|
|
40
|
+
|
|
41
|
+
status="$(json_get "$input" status)"
|
|
42
|
+
cid="$(safe_conversation_id "$input")"
|
|
43
|
+
|
|
44
|
+
pending_dir="$(hooks_pending_dir)"
|
|
45
|
+
marker="$pending_dir/session-edits-$cid.txt"
|
|
46
|
+
flag="$pending_dir/reviewed-$cid.flag"
|
|
47
|
+
|
|
48
|
+
# One-shot brake: the previous subagentStop for this id emitted the review.
|
|
49
|
+
if [ -f "$flag" ]; then
|
|
50
|
+
rm -f "$flag" "$marker" 2>/dev/null
|
|
51
|
+
emit_none
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Review only a clean completion; otherwise clear the marker and stop.
|
|
55
|
+
if [ -n "$status" ] && [ "$status" != "completed" ]; then
|
|
56
|
+
rm -f "$marker" 2>/dev/null
|
|
57
|
+
emit_none
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# No edits this run -> nothing to review.
|
|
61
|
+
[ -f "$marker" ] || emit_none
|
|
62
|
+
edited="$(grep -vE '^[[:space:]]*$' "$marker" 2>/dev/null | sort -u)"
|
|
63
|
+
rm -f "$marker" 2>/dev/null
|
|
64
|
+
[ -n "$edited" ] || emit_none
|
|
65
|
+
|
|
66
|
+
# Compose the follow-up review prompt (md preferred, embedded fallback).
|
|
67
|
+
prompt_file="$HOME/.agents/hooks/final-review.md"
|
|
68
|
+
body=""
|
|
69
|
+
[ -f "$prompt_file" ] && body="$(cat "$prompt_file")"
|
|
70
|
+
if [ -z "$body" ]; then
|
|
71
|
+
body='Audit everything you changed in this run and FIX what fails (do NOT revert the
|
|
72
|
+
behaviour the task asked for):
|
|
73
|
+
1. Correctness - logic, edge cases (null/empty/zero/boundary), language traps, security.
|
|
74
|
+
2. Reliability - error paths handled, no swallowed errors, resources released.
|
|
75
|
+
3. Coverage - behaviour-bearing changes have real tests; RUN the suite if present.
|
|
76
|
+
4. Anti-slop - if ~/.cursor/skills/anti-slop/scripts/scan_slop.py exists, run
|
|
77
|
+
`python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all`; otherwise
|
|
78
|
+
apply ~/.agents/hooks/anti-slop.md to the session diff.
|
|
79
|
+
If an axis is clean, say so in one line. Then stop.'
|
|
80
|
+
fi
|
|
81
|
+
body="$(expand_agent_paths "$body")"
|
|
82
|
+
|
|
83
|
+
file_list=""
|
|
84
|
+
while IFS= read -r p; do
|
|
85
|
+
[ -n "$p" ] || continue
|
|
86
|
+
rp="$(resolve_agent_path "$p")"
|
|
87
|
+
file_list="${file_list} ${rp}"$'\n'
|
|
88
|
+
done <<EOF
|
|
89
|
+
$edited
|
|
90
|
+
EOF
|
|
91
|
+
file_list="$(printf '%s' "$file_list" | head -n 30)"
|
|
92
|
+
msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.
|
|
93
|
+
|
|
94
|
+
Files you changed this run:
|
|
95
|
+
$file_list
|
|
96
|
+
|
|
97
|
+
$body"
|
|
98
|
+
|
|
99
|
+
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
100
|
+
touch "$flag" 2>/dev/null
|
|
101
|
+
|
|
102
|
+
emit_json followup_message "$msg"
|
|
103
|
+
exit 0
|
package/linux/hooks.json
CHANGED
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
"command": "bash ~/.agents/hooks/minimal-edit-audit.sh",
|
|
20
20
|
"timeout": 15,
|
|
21
21
|
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
22
|
-
"_comment": "15s: minimal-editing advisory on the edited file (git --numstat line-count
|
|
22
|
+
"_comment": "15s (DEPRECATED in 0.3.0, superseded by semantic-density-audit + declared-editing; retained for compat, removal slated for 0.4.0): minimal-editing advisory on the edited file (git --numstat line-count). Size-based gate; the intent-declared gate supersedes it. Appends findings to the conversation's pending file; never blocks. Disable: HOOKS_ENFORCE=0 or MINIMAL_EDITING_ENFORCE=0."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"command": "bash ~/.agents/hooks/semantic-density-audit.sh",
|
|
26
|
+
"timeout": 15,
|
|
27
|
+
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
28
|
+
"_comment": "15s: semantic-opacity advisory on the edited file. Extracts added lines from git diff, pipes to density_scan.py (shared low_density module), flags identifiers that communicate no intent (DataManager, process(), utils.ts, CoreEngine). FAIL = bare low-density token or generic-suffix class without domain noun; WARN = defensible DDD with domain noun (PostgresUserRepository) - only fires alongside a FAIL so clean code stays quiet. One denylist shared with scan_slop.py's semantic_density bucket. Appends to pending; never blocks. Disable: HOOKS_ENFORCE=0 or SEMANTIC_DENSITY_ENFORCE=0."
|
|
23
29
|
},
|
|
24
30
|
{
|
|
25
31
|
"command": "bash ~/.agents/hooks/anti-slop-audit.sh",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Thin self-review hooks for Cursor — the model is the auditor. Intent-trace final review (Tier 0), unified 13-item anti-slop checklist, operational slop detection.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cursordoctrine": "bin/cli.mjs"
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
"windows/",
|
|
15
15
|
"linux/",
|
|
16
16
|
"skills/",
|
|
17
|
-
"INSTALL.md"
|
|
17
|
+
"INSTALL.md",
|
|
18
|
+
"!**/__pycache__",
|
|
19
|
+
"!**/*.pyc",
|
|
20
|
+
"!**/*.pyo"
|
|
18
21
|
],
|
|
19
22
|
"repository": {
|
|
20
23
|
"type": "git",
|
|
@@ -20,7 +20,7 @@ description: >-
|
|
|
20
20
|
duplicate utilities.
|
|
21
21
|
metadata:
|
|
22
22
|
layer: active-cleanup
|
|
23
|
-
pairs-with: minimal-editing,
|
|
23
|
+
pairs-with: declared-editing (supersedes minimal-editing), semantic-density-audit
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
# Anti-Slop
|
|
@@ -160,6 +160,7 @@ Walk every row. Rows tagged *(scanner)* are seeded mechanically by
|
|
|
160
160
|
| **Duplicated logic** | new code mirrors something already in the repo | Delete the copy; call the existing function. Grep before you keep it. |
|
|
161
161
|
| **Clone proliferation / DRY / Knowledge duplication** | `--all` reports the same function name in ≥2 files, or identical bodies under different names (`isRecord` / `isObject` / `isPlainObject`) | Keep ONE canonical definition; re-point imports; delete the copies. One source of truth per concept. |
|
|
162
162
|
| **Utility explosion / Helper Hell / Fingerprints** | a swarm of tiny `is*` / `assert*` / `safe*` one-liners; fingerprints (`isRecord`, `safeParse`, `sleep`, `retry`, `assertNever`) | Inline single-use micro-helpers; consolidate genuinely shared ones into one module. |
|
|
163
|
+
| **Semantic opacity / low-density names** *(scanner)* | identifiers that exist but communicate no intent: `DataManager`, `CoreEngine`, `process()`, `handleThing`, `utils.ts`, `x1`, `tempFix`, `finalFinal`. FAIL = bare low-density token or generic-suffix class with no domain noun; WARN = defensible DDD with a domain noun (`PostgresUserRepository`). Shared denylist lives in `low_density.py` and fires identically in `scan_slop.py --all` and the per-edit `semantic-density-audit` hook. | Rename to state the concrete responsibility: `DataManager` → `InvoiceRepository` or `PersistUserSessions`; `process` → `GenerateMonthlyReport`; `utils.ts` → `invoice_totals.ts`. Leave WARNs that are intentional DDD. |
|
|
163
164
|
| **Ignored conventions** | style / naming / structure / error-handling differs from the file's neighbours | Rewrite to match the surrounding code. |
|
|
164
165
|
| **Accidental complexity** | indirection / generics / config a junior can't read in 30s | Flatten to the simplest form that works. |
|
|
165
166
|
| **Superficial tests / Test theater** | the test asserts "it runs", mirrors the implementation, or cannot fail; literal tautologies (`expect(true).toBe(true)`, `assert True`) *(scanner)*; snapshot-everything, mocks of mocks, assertion poverty | Rewrite to assert real outcomes and the edge cases; delete tautological tests. |
|
|
@@ -268,7 +269,10 @@ Diff: {before} → {after} lines. Tests: {pass | n/a}
|
|
|
268
269
|
| Hook checklist | `~/.agents/hooks/anti-slop.md` (13 items; per-edit + final-review axis 4) |
|
|
269
270
|
|
|
270
271
|
The scanner is stdlib-only and needs Python 3.9+. Pairs with the **anti-slop
|
|
271
|
-
audit hook** (`anti-slop-audit.ps1` / `.sh`, advisory per edit), the
|
|
272
|
-
hook** (`
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
audit hook** (`anti-slop-audit.ps1` / `.sh`, advisory per edit), the
|
|
273
|
+
**semantic-density-audit hook** (`semantic-density-audit.ps1` / `.sh`, flags
|
|
274
|
+
low-density identifiers per edit — shares `low_density.py` with this scanner's
|
|
275
|
+
`semantic_density` bucket), the **stop hook** (`final-review.ps1` / `.sh`,
|
|
276
|
+
five-axis session review incl. intent trace), and **declared-editing**
|
|
277
|
+
(supersedes the deprecated `minimal-editing` size gate). This skill is the
|
|
278
|
+
active "delete it now" layer those only nudge toward.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""density_scan.py - per-edit semantic-density hook wrapper.
|
|
3
|
+
|
|
4
|
+
The afterFileEdit hook (semantic-density-audit.{ps1,sh}) extracts the ADDED
|
|
5
|
+
lines for the just-edited file from `git diff HEAD` and pipes them here on
|
|
6
|
+
stdin. This wrapper scores them with the shared low_density module and emits
|
|
7
|
+
one JSON object the hook can read. One job, one contract:
|
|
8
|
+
|
|
9
|
+
stdin: added lines (one per line, leading '+' already stripped)
|
|
10
|
+
argv: --rel <repo-relative path of the edited file>
|
|
11
|
+
stdout: {"rel": ..., "findings": [...], "count": N}
|
|
12
|
+
where each finding = {name, line, kind, severity, reasons}
|
|
13
|
+
exit: 0 always (advisory; the hook never blocks)
|
|
14
|
+
|
|
15
|
+
Why a separate wrapper instead of `scan_slop.py --added-json -`? scan_slop's
|
|
16
|
+
per-file signal loop is the right granularity for a whole-codebase audit, but
|
|
17
|
+
the hook needs ONE call that ingests pre-extracted added lines and returns
|
|
18
|
+
density findings only - no dep/abstraction/residue detection (that is
|
|
19
|
+
anti-slop-audit's job, already running in the same afterFileEdit slot). This
|
|
20
|
+
keeps the two hooks non-overlapping and the density path fast (<50ms typical).
|
|
21
|
+
|
|
22
|
+
Stdlib only; Python 3.9+. REPORTS only - never edits.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
# Resolve sibling low_density.py + scan_slop.py the same way low_density does.
|
|
33
|
+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
34
|
+
if _SCRIPT_DIR not in sys.path:
|
|
35
|
+
sys.path.insert(0, _SCRIPT_DIR)
|
|
36
|
+
|
|
37
|
+
import low_density # noqa: E402 (path set up above)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main() -> int:
|
|
41
|
+
ap = argparse.ArgumentParser(
|
|
42
|
+
description="Per-edit semantic-density scorer (hook wrapper).")
|
|
43
|
+
ap.add_argument("--rel", required=True,
|
|
44
|
+
help="repo-relative path of the edited file (used for "
|
|
45
|
+
"language detection and filename scoring)")
|
|
46
|
+
ap.add_argument("--max-lines", type=int, default=2000,
|
|
47
|
+
help="cap on stdin lines read (runtime bound, matches the "
|
|
48
|
+
"hook's own 1500-line git cap with headroom)")
|
|
49
|
+
args = ap.parse_args()
|
|
50
|
+
|
|
51
|
+
rel = args.rel.replace("\\", "/").lstrip("/")
|
|
52
|
+
|
|
53
|
+
# Read added lines from stdin. The hook already stripped leading '+' and
|
|
54
|
+
# '+++' headers and applied its own cap; we apply a defensive second cap.
|
|
55
|
+
added: list[str] = []
|
|
56
|
+
try:
|
|
57
|
+
for i, line in enumerate(sys.stdin):
|
|
58
|
+
if i >= args.max_lines:
|
|
59
|
+
break
|
|
60
|
+
added.append(line.rstrip("\n"))
|
|
61
|
+
except (KeyboardInterrupt, IOError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
findings: list[dict[str, Any]] = low_density.score_identifiers(added, rel)
|
|
65
|
+
|
|
66
|
+
payload = {
|
|
67
|
+
"rel": rel,
|
|
68
|
+
"findings": findings,
|
|
69
|
+
"count": len(findings),
|
|
70
|
+
"fail_count": sum(1 for f in findings if f.get("severity") == "fail"),
|
|
71
|
+
"warn_count": sum(1 for f in findings if f.get("severity") == "warn"),
|
|
72
|
+
}
|
|
73
|
+
print(json.dumps(payload))
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
sys.exit(main())
|