cursordoctrine 0.2.3 → 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 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 everything advisory off
374
- PERM_GATE_ENFORCE=0 permission gate off
375
- MINIMAL_EDITING_ENFORCE=0 over-edit advisory off
376
- ANTI_SLOP_ENFORCE=0 slop advisory off
377
- FINAL_REVIEW_ENFORCE=0 final review off
378
- SUBAGENT_REVIEW_ENFORCE=0 in-subagent review off
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 + audit-metrics.py token metrics on .py, from ~/.cursor/skills/minimal-editing/ if installed). Appends findings to the conversation's pending file; never blocks. Disable: HOOKS_ENFORCE=0 or MINIMAL_EDITING_ENFORCE=0."
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.2.3",
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, anti-slop-hook
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 **stop
272
- hook** (`final-review.ps1` / `.sh`, five-axis session review incl. intent
273
- trace), and **minimal-editing** (smallest-diff). This skill is the active
274
- "delete it now" layer those only nudge toward.
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())