cursordoctrine 0.3.1 → 0.3.3

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` (deprecated in 0.3.0), `SEMANTIC_DENSITY_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`, `SCOPE_GATE_ENFORCE=0`, `ANTI_SLOP_ENFORCE=0`, `FINAL_REVIEW_ENFORCE=0`, `SUBAGENT_REVIEW_ENFORCE=0`.
package/README.md CHANGED
@@ -1,65 +1,81 @@
1
- # cursordoctrine
1
+ <div align="center">
2
+ <img src="https://img.shields.io/badge/node-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white" />
3
+ <img src="https://img.shields.io/npm/v/cursordoctrine?style=flat-square&color=blue" />
4
+ <img src="https://img.shields.io/badge/license-MIT-brightgreen?style=flat-square" />
5
+ <img src="https://img.shields.io/badge/built%20for-Cursor-6c47ff?style=flat-square" />
6
+ </div>
2
7
 
3
- Thin self-review hooks for Cursor. Five hook events, one message bus. The model is the auditor Cursor only carries context and gates blast radius.
8
+ <br />
9
+
10
+ <div align="center">
11
+ <h1>cursordoctrine</h1>
12
+ <p><strong>Thin self-review hooks for Cursor.</strong></p>
13
+ <p>Five hook events, one message bus.<br />The model audits its own work. Cursor carries context and gates blast radius.</p>
14
+ </div>
15
+
16
+ <br />
17
+
18
+ ---
4
19
 
5
20
  ## What this is
6
21
 
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:
22
+ Cursor hooks that make the agent review its own edits without bolting a static-analysis pipeline onto every keystroke. No regex army, no scoring engine. Three jobs:
8
23
 
9
- 1. **Inject the doctrine** at session start, so every chat begins with the same short governing text (`doctrine.md` + `USER-RULES.md` + `declared-editing.md` the YAGNI ultra ladder that prevents over-building before a single line is written).
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
- 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.
24
+ 1. **Inject the doctrine** at session start every chat starts with the same short governing text (`doctrine.md`, `USER-RULES.md`, and `declared-editing.md`, the YAGNI ultra ladder that stops over-building before a line gets written).
25
+ 2. **Hand the model its own edits back** after each agent edit, a self-review prompt goes into a pending file (plus minimal-edit, semantic-density, and anti-slop advisories when they trip). Next turn the model reads its diff, fixes real bugs, stays quiet otherwise.
26
+ 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 passes.
12
27
 
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`.
28
+ When an implementation finishes, the stop hook runs one final review over everything that changed, then stops. Five axes. The first is **intent trace**: the hook pulls your last user message from the transcript and prepends it to the review so the model has to tie every diff hunk to a concrete request. Anything it can't trace is a hallucinated requirement and gets reverted. That's the only check that catches "clean code, wrong feature" linters and later axes miss it.
14
29
 
15
- This setup is for Cursor only. It installs into `~/.cursor` and `~/.agents/hooks` and touches nothing in your projects.
30
+ Subagents get the same treatment. If a delegated run edited files, it reviews its own work before the result goes back to the parent. Those edits fold into the parent's final review. Every bound is enforced twice: in the script and in `hooks.json`.
16
31
 
17
- ## Layout
32
+ Cursor only. Installs into `~/.cursor` and `~/.agents/hooks`. Doesn't touch your projects.
18
33
 
34
+ ## Install
35
+
36
+ Node 18+:
37
+
38
+ ```bash
39
+ npx cursordoctrine@latest install # copies the hook pack into ~/.agents/hooks + ~/.cursor, merges hooks.json
40
+ npx cursordoctrine verify # smoke-tests every hook with fake payloads, no restart needed
19
41
  ```
20
- windows/ PowerShell hooks (pwsh) — install on Windows machines
21
- hooks.json hook wiring for ~/.cursor/hooks.json
22
- inject-doctrine.ps1, doctrine.md, USER-RULES.md
23
- hooks/ the eight scripts + the three prompt files
24
- linux/ bash hooks — install on Linux machines and SSH remotes
25
- hooks.json, inject-doctrine.sh, doctrine.md, USER-RULES.md
26
- hooks/ same hooks, ported to bash (jq preferred, python3 fallback)
27
- skills/ Cursor agent skills shipped with the package
28
- anti-slop/ SKILL.md + the duplication scanner (final review runs it)
29
- bin/ the npm CLI (npx cursordoctrine install / verify / uninstall)
30
- INSTALL.md a ready-to-paste prompt that tells a Cursor agent to
31
- install the right folder and verify every hook
32
- assets/ the architecture diagram above
33
- ```
34
42
 
35
- The two folders are functionally identical. Windows runs everything through `pwsh.exe`; Linux runs bash, which is what you want on a remote you reach over SSH (see your `~/.ssh/config` host the hooks live on the remote's `$HOME`, not on your laptop).
43
+ Restart Cursor after install `hooks.json` is read at startup. `install` is idempotent; re-run to update. Entries you added to `~/.cursor/hooks.json` yourself are kept. `npx cursordoctrine uninstall` removes the pack the same way.
44
+
45
+ No Node? Open `INSTALL.md`, paste it into a Cursor agent chat on the target machine, and let the agent copy files and run the checklist. Copy commands are in the same file if you prefer doing it by hand.
46
+
47
+ Prerequisites: `git` everywhere; `pwsh` on Windows; `bash` plus `jq` or `python3` on Linux.
48
+
49
+ The anti-slop skill (`skills/anti-slop/` — SKILL.md and the duplication scanner) installs to `~/.cursor/skills/anti-slop/`. The hook checklist (`~/.agents/hooks/anti-slop.md`, 13 items) is the canonical slop detector for per-edit advisories and final-review axis 4. Final review runs the scanner from the skill path first when it's there.
36
50
 
37
51
  ## The five flows
38
52
 
39
53
  | Flow | Event | What happens |
40
54
  |---|---|---|
41
- | Session | `sessionStart` | `inject-doctrine` reads the doctrine + user rules and emits them as `additional_context`. |
55
+ | Session | `sessionStart` | `inject-doctrine` reads doctrine + user rules + declared-editing and emits them as `additional_context`. |
42
56
  | 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
57
  | Shell | `beforeShellExecution` | `permission-gate` checks the command against a deny list. Allow by default, deny by list, fail open. |
44
58
  | 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
59
  | 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
60
 
47
- ## Install
48
-
49
- The fast path is npm (Node 18+):
61
+ ## Layout
50
62
 
51
- ```bash
52
- npx cursordoctrine@latest install # copies the hook pack into ~/.agents/hooks + ~/.cursor, merges hooks.json
53
- npx cursordoctrine verify # smoke-tests every hook with fake payloads, no restart needed
63
+ ```
64
+ windows/ PowerShell hooks (pwsh) install on Windows machines
65
+ hooks.json hook wiring for ~/.cursor/hooks.json
66
+ inject-doctrine.ps1, doctrine.md, USER-RULES.md, declared-editing.md
67
+ hooks/ the eight scripts + the three prompt files
68
+ linux/ bash hooks — install on Linux machines and SSH remotes
69
+ hooks.json, inject-doctrine.sh, doctrine.md, USER-RULES.md, declared-editing.md
70
+ hooks/ same hooks, ported to bash (jq preferred, python3 fallback)
71
+ skills/ Cursor agent skills shipped with the package
72
+ anti-slop/ SKILL.md + the duplication scanner (final review runs it)
73
+ bin/ the npm CLI (npx cursordoctrine install / verify / uninstall)
74
+ INSTALL.md ready-to-paste prompt that tells a Cursor agent to
75
+ install the right folder and verify every hook
54
76
  ```
55
77
 
56
- Then restart Cursor `hooks.json` is read at startup. `install` is idempotent: re-run it to update, and entries you added to `~/.cursor/hooks.json` yourself are preserved. `npx cursordoctrine uninstall` removes the pack the same way.
57
-
58
- No Node? Open `INSTALL.md`, paste its contents into a Cursor agent chat on the target machine, and let the agent copy the files and run the verification checklist. Or do it by hand — the copy commands are in the same file.
59
-
60
- Prerequisites: `git` everywhere; `pwsh` on Windows; `bash` plus `jq` or `python3` on Linux.
61
-
62
- The anti-slop skill (`skills/anti-slop/` — SKILL.md and the duplication scanner) installs to `~/.cursor/skills/anti-slop/`. The hook checklist (`~/.agents/hooks/anti-slop.md`, 13 items) is the canonical slop detector for both per-edit advisories and final-review axis 4. The final review runs the scanner from the skill path first when available.
78
+ Both folders do the same thing. Windows runs everything through `pwsh.exe`. Linux runs bash, which is what you want on a remote over SSH (check your `~/.ssh/config` host hooks live on the remote's `$HOME`, not your laptop).
63
79
 
64
80
  ## Tuning and kill switches
65
81
 
@@ -70,6 +86,7 @@ All hooks fail open and always exit 0. Nothing here can block your session.
70
86
  | `HOOKS_ENFORCE=0` | on | turns off all advisory hooks at once |
71
87
  | `PERM_GATE_ENFORCE=0` | on | disables the permission gate |
72
88
  | `MINIMAL_EDITING_ENFORCE=0` | on | disables the over-edit advisory (deprecated in 0.3.0) |
89
+ | `SCOPE_GATE_ENFORCE=0` | on | disables the declared-scope advisory (opt-in: only fires when `.scope.json` exists) |
73
90
  | `SEMANTIC_DENSITY_ENFORCE=0` | on | disables the semantic-opacity advisory |
74
91
  | `ANTI_SLOP_ENFORCE=0` | on | disables the slop advisory |
75
92
  | `FINAL_REVIEW_ENFORCE=0` | on | disables the final review pass |
@@ -79,9 +96,15 @@ All hooks fail open and always exit 0. Nothing here can block your session.
79
96
 
80
97
  ## Design notes
81
98
 
82
- - **State lives under `$HOME`**, in `~/.cursor/.hooks-pending/`, keyed by conversation id. No repo litter, and concurrent sessions can't drain each other's prompts. Stale state older than 7 days is swept on every stop.
83
- - **`afterFileEdit` output isn't consumed by Cursor**, so the edit hooks write to a pending file and `post-tool-use` re-emits it at the next tool boundary. That's the whole message bus.
99
+ - **State lives under `$HOME`**, in `~/.cursor/.hooks-pending/`, keyed by conversation id. No repo litter. Concurrent sessions can't drain each other's prompts. Stale state older than 7 days gets swept on every stop.
100
+ - **`afterFileEdit` output isn't consumed by Cursor**, so edit hooks write to a pending file and `post-tool-use` re-emits it at the next tool boundary. That's the whole message bus.
84
101
  - **One review per implementation.** The stop hook arms a per-conversation flag before emitting its follow-up, so a crash can't re-fire it and a long chat still gets a review after each implementation.
85
- - **Subagents are first-class.** `afterFileEdit` fires inside subagents keyed by the *subagent's* conversation id, the harness normalizes agent edits (incl. `StrReplace`) to tool type `Write`, and `postToolUse` never fires for the `Task` tool — all verified by payload capture. So the matchers cover `Write|StrReplace|EditNotebook` defensively, `subagentStop` reviews the subagent in its own context, and the parent folds orphaned subagent markers (found via the `subagents/` transcript directory) into its own at every tool boundary and at stop.
102
+ - **Subagents are first-class.** `afterFileEdit` fires inside subagents keyed by the subagent's conversation id. The harness normalizes agent edits (incl. `StrReplace`) to tool type `Write`, and `postToolUse` never fires for the `Task` tool — verified by payload capture. Matchers cover `Write|StrReplace|EditNotebook` defensively. `subagentStop` reviews the subagent in its own context. The parent folds orphaned subagent markers (from the `subagents/` transcript directory) into its own at every tool boundary and at stop.
103
+
104
+ Self-contained. No build. Open `hooks.json` and read it — that's the whole system in one file.
105
+
106
+ Built with [Cursor](https://cursor.com).
107
+
108
+ ## License
86
109
 
87
- Self-contained. No build. Open `hooks.json` and read it — it's the whole system in one file.
110
+ MIT. See [LICENSE](LICENSE).
package/bin/cli.mjs CHANGED
@@ -374,6 +374,7 @@ Kill switches (environment variables, all hooks fail open)
374
374
  PERM_GATE_ENFORCE=0 permission gate off
375
375
  MINIMAL_EDITING_ENFORCE=0 over-edit advisory off (deprecated in 0.3.0)
376
376
  SEMANTIC_DENSITY_ENFORCE=0 semantic-opacity advisory off
377
+ SCOPE_GATE_ENFORCE=0 declared-scope advisory off
377
378
  ANTI_SLOP_ENFORCE=0 slop advisory off
378
379
  FINAL_REVIEW_ENFORCE=0 final review off
379
380
  SUBAGENT_REVIEW_ENFORCE=0 in-subagent review off
@@ -1,5 +1,5 @@
1
1
  FINAL REVIEW — you just finished an implementation. Before you treat it as done,
2
- audit EVERYTHING you changed this session across the five axes below and FIX what
2
+ audit EVERYTHING you changed this session across the six axes below and FIX what
3
3
  fails. Do NOT revert the behaviour the user asked for. If an axis is already
4
4
  clean, say so in one line — do not manufacture work.
5
5
 
@@ -64,4 +64,36 @@ Step C — session footprint (also in the header above):
64
64
  If "Session footprint" shows >5 files or the request was simple, justify each
65
65
  file or trim. Unjustified files are slop.
66
66
 
67
+ Step D — declared scope (closing gate for Compuerta 1):
68
+ If `.scope.json` exists in the repo root, run the session's full diff against
69
+ the declared contract. In your shell:
70
+ for f in $(git diff --name-only HEAD); do
71
+ python ~/.cursor/skills/anti-slop/scripts/scope_match.py --path "$f" --patterns-file .scope.json
72
+ done
73
+ Any file reporting `"in_scope": false` is a scope violation you must justify
74
+ (add to .scope.json with a one-line reason) or revert. If `.scope.json` does
75
+ not exist, this step is skipped — the declared-editing ladder and the
76
+ per-edit scope-gate-audit hook are the opt-in discipline.
77
+
67
78
  Fix with edits now; re-run the scan (if Step A ran) and the tests; then stop.
79
+
80
+ ## 5. Wiring completeness
81
+ For every user-visible behavior you added or changed (button, form submit, API
82
+ call, route, state transition, scheduled job), trace its execution path end to
83
+ end and confirm it reaches a REAL EFFECT (persist, mutate, call, render, notify).
84
+ A dead end is slop even if the code is clean. Hunt for the vibe-coding failure
85
+ mode where a layer EXISTS but is not WIRED:
86
+
87
+ - `handleSubmit()` that does not persist / does not call the API.
88
+ - An endpoint that no route or caller invokes.
89
+ - A DB write / table that nothing reads or writes.
90
+ - A component that renders but is never mounted / routed to.
91
+ - A hook / store / context that is declared but never consumed.
92
+ - A `TODO` / empty body / stubbed `console.log` standing in for the effect.
93
+
94
+ The bar is: a senior can follow the path click -> handler -> call -> store ->
95
+ render (or the equivalent slice) without hitting a gap. If a step is missing or
96
+ faked, either wire it now or remove the dead half so the diff does not ship
97
+ scaffolding that looks complete but does nothing. Stubs you intend to wire later
98
+ must be marked with a `TODO(wire):` comment naming what is missing; unmarked
99
+ dead ends are failures.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # final-review.sh - stop hook (Cursor, Linux).
3
3
  #
4
- # ONE comprehensive end-of-implementation review across five axes:
5
- # intent, correctness, reliability, coverage, and anti-slop. When the agent finishes
4
+ # ONE comprehensive end-of-implementation review across six axes:
5
+ # intent, correctness, reliability, coverage, anti-slop, and wiring completeness. When the agent finishes
6
6
  # an implementation that touched files, Cursor auto-submits this hook's
7
7
  # `followup_message` as the next user turn, so the model re-audits everything
8
8
  # it changed this session and FIXES what fails.
@@ -81,6 +81,12 @@ if [ -z "$body" ]; then
81
81
  run `python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all` first.
82
82
  Consolidate clones; drop premature abstraction, unneeded deps, operational
83
83
  slop (retries, await-in-loop, log spam), unjustified files.
84
+ 5. Wiring completeness - for every user-visible behavior you added/changed
85
+ (button, submit, API call, route, state transition), trace its execution
86
+ path to a REAL EFFECT (persist, mutate, call, render). A dead end is slop:
87
+ handleSubmit that does not persist, an endpoint no caller invokes, a store
88
+ never consumed, a stub/TODO/console.log standing in for the effect. Wire it
89
+ now or remove the dead half; mark later-stubs with TODO(wire):.
84
90
  Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one line.'
85
91
  fi
86
92
  body="$(expand_agent_paths "$body")"
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # scope-gate-audit.sh - afterFileEdit "declared scope" advisory (Cursor, Linux).
3
+ #
4
+ # Compuerta 1 of the anti-slop system: the declared-scope gate. When the agent
5
+ # writes a .scope.json contract (intent + files[] + acceptance), this hook
6
+ # checks every edited file against it. Editing OUTSIDE the declared set is the
7
+ # textbook scope-creep / gold-plating signal. Advisory only (no preToolUse for
8
+ # file edits on Cursor); the violation is flagged on the next turn.
9
+ #
10
+ # Opt-in: if .scope.json does not exist in the repo root, this hook is silent.
11
+ # No contract = no gate (fallback to declared-editing ladder + final-review).
12
+ #
13
+ # Mechanism: resolve edited file -> repo-relative, run scope_match.py against
14
+ # .scope.json's files[], append advisory to feedback-<cid>.txt on violation.
15
+ #
16
+ # Advisory only: never blocks, never persists state, ALWAYS exits 0.
17
+ # Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0
18
+
19
+ set +e
20
+ . "$(dirname "$0")/hook-common.sh"
21
+
22
+ [ "${HOOKS_ENFORCE:-}" = "0" ] && exit 0
23
+ [ "${SCOPE_GATE_ENFORCE:-}" = "0" ] && exit 0
24
+
25
+ input="$(read_hook_stdin)"
26
+ [ -n "$input" ] || exit 0
27
+
28
+ # audit root: project from JSON (cwd, then workspace_roots), else CURSOR_PROJECT_DIR / HOME
29
+ root=""
30
+ while IFS= read -r cand; do
31
+ [ -n "$cand" ] && [ -d "$cand" ] && { root="${cand%/}"; break; }
32
+ done <<EOF
33
+ $(json_get "$input" cwd)
34
+ $(json_get_array "$input" workspace_roots)
35
+ EOF
36
+ [ -n "$root" ] || root="${CURSOR_PROJECT_DIR:-$HOME}"
37
+ root="${root%/}"
38
+
39
+ # edited file -> repo-relative path
40
+ fp=""
41
+ for k in file_path path filename absolute_path abs_path; do
42
+ fp="$(json_get "$input" "$k")"
43
+ [ -n "$fp" ] && break
44
+ done
45
+ [ -n "$fp" ] || exit 0
46
+ rel="$fp"
47
+ case "$rel" in "$root"/*) rel="${rel#"$root"/}" ;; esac
48
+ if is_cursor_config_path "$fp" || is_cursor_config_path "$rel"; then exit 0; fi
49
+
50
+ # --- opt-in gate: no .scope.json = no gate ---------------------------------
51
+ scope_file="$root/.scope.json"
52
+ [ -f "$scope_file" ] || exit 0
53
+
54
+ # --- resolve Python + run scope_match.py ---------------------------------
55
+ py=""
56
+ for c in python3 python; do
57
+ if command -v "$c" >/dev/null 2>&1; then py="$c"; break; fi
58
+ done
59
+ [ -n "$py" ] || exit 0 # no Python -> fail open
60
+
61
+ matcher="$HOME/.cursor/skills/anti-slop/scripts/scope_match.py"
62
+ [ -f "$matcher" ] || exit 0 # skill not installed -> silent
63
+
64
+ mout="$("$py" "$matcher" --path "$rel" --patterns-file "$scope_file" 2>/dev/null)"
65
+ [ -n "$mout" ] || exit 0
66
+
67
+ # --- parse the JSON result (reuse the Python we already resolved) ----------
68
+ parse_result() {
69
+ "$py" - "$@" <<'PYEOF' 2>/dev/null
70
+ import json, sys
71
+ try:
72
+ p = json.loads(sys.stdin.read())
73
+ except Exception:
74
+ sys.exit(1)
75
+ if p.get("skipped"):
76
+ sys.exit(2) # no valid contract -> fail-open
77
+ if p.get("in_scope"):
78
+ sys.exit(3) # in scope -> clean
79
+ allow_growth = "1" if p.get("allow_growth") else "0"
80
+ intent = p.get("intent", "")
81
+ print(f"__AG__{allow_growth}")
82
+ print(f"__INTENT__{intent}")
83
+ sys.exit(0)
84
+ PYEOF
85
+ }
86
+
87
+ parsed="$(printf '%s' "$mout" | parse_result)"
88
+ rc=$?
89
+ [ "$rc" -eq 0 ] || exit 0 # 2=skipped, 3=in-scope, 1=parse-fail -> all silent
90
+
91
+ allow_growth="$(printf '%s\n' "$parsed" | grep '__AG__' | sed 's/__AG__//')"
92
+ intent="$(printf '%s\n' "$parsed" | grep '__INTENT__' | sed 's/__INTENT__//')"
93
+
94
+ # Read declared files for the message (best-effort)
95
+ declared_files="$(printf '%s' "$scope_file" | "$py" -c "
96
+ import json, sys
97
+ try:
98
+ d = json.load(open(sys.argv[1]))
99
+ print(', '.join(d.get('files', [])))
100
+ except Exception:
101
+ pass
102
+ " "$scope_file" 2>/dev/null)"
103
+
104
+ # --- compose advisory ------------------------------------------------------
105
+ if [ "$allow_growth" = "1" ]; then
106
+ summary="Scope note - $rel is new vs your declared scope (growth allowed)"
107
+ body=" You touched a file outside your initial declared set. Since allow_growth is
108
+ true, this is not a violation, but justify it: add $rel to .scope.json or
109
+ explain why the scope grew."
110
+ else
111
+ summary="[SCOPE VIOLATION] $rel is NOT in your declared scope"
112
+ body=" Your contract (.scope.json):
113
+ intent: $intent
114
+ files: $declared_files
115
+
116
+ You declared these files and touched one outside the set. Either:
117
+ 1. Add $rel to .scope.json with a one-line justification, OR
118
+ 2. Revert the change - it is out of scope for the declared intent.
119
+
120
+ Declared-editing: declare BEFORE you expand. Don't sneak edits past the gate."
121
+ fi
122
+
123
+ msg="${summary}
124
+
125
+ ${body}
126
+
127
+ (Advisory; disable: SCOPE_GATE_ENFORCE=0)"
128
+
129
+ # --- append to the shared pending file --------------------------------------
130
+ cid="$(safe_conversation_id "$input")"
131
+ pending="$(hooks_pending_dir)/feedback-${cid}.txt"
132
+ mkdir -p "$(dirname "$pending")" 2>/dev/null
133
+ if [ -s "$pending" ]; then
134
+ printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
135
+ else
136
+ printf '%s' "$msg" >> "$pending" 2>/dev/null
137
+ fi
138
+
139
+ exit 0
package/linux/hooks.json CHANGED
@@ -27,6 +27,12 @@
27
27
  "matcher": "^(Write|StrReplace|EditNotebook)$",
28
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."
29
29
  },
30
+ {
31
+ "command": "bash ~/.agents/hooks/scope-gate-audit.sh",
32
+ "timeout": 10,
33
+ "matcher": "^(Write|StrReplace|EditNotebook)$",
34
+ "_comment": "10s (Compuerta 1): declared-scope advisory. OPT-IN: only active when .scope.json exists in the repo root. The agent declares intent + files[] + acceptance; this hook checks every edit against the declared set via scope_match.py (exact, * glob, ** recursive, bare-dir). Out-of-scope edit = [SCOPE VIOLATION] advisory to pending. No .scope.json = silent (fallback to declared-editing ladder + final-review footprint check). Never blocks (Cursor has no preToolUse for file edits). Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0."
35
+ },
30
36
  {
31
37
  "command": "bash ~/.agents/hooks/anti-slop-audit.sh",
32
38
  "timeout": 15,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursordoctrine",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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"
@@ -272,7 +272,11 @@ The scanner is stdlib-only and needs Python 3.9+. Pairs with the **anti-slop
272
272
  audit hook** (`anti-slop-audit.ps1` / `.sh`, advisory per edit), the
273
273
  **semantic-density-audit hook** (`semantic-density-audit.ps1` / `.sh`, flags
274
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
275
+ `semantic_density` bucket), the **scope-gate-audit hook**
276
+ (`scope-gate-audit.ps1` / `.sh`, Compuerta 1 opt-in declared-scope gate
277
+ that flags edits outside `.scope.json`; shares `scope_match.py` with the
278
+ final-review Step D closing gate), the **stop hook** (`final-review.ps1` / `.sh`,
279
+ six-axis session review incl. intent trace and wiring completeness), and
280
+ **declared-editing** (YAGNI ultra ladder injected at session start;
281
+ supersedes the deprecated `minimal-editing` size gate). This skill is the
278
282
  active "delete it now" layer those only nudge toward.
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """scope_match.py - declared-scope glob matcher (shared helper).
3
+
4
+ One job: given a repo-relative path and a list of declared-scope patterns
5
+ (from .scope.json `files`), return whether the path is in scope. Shared
6
+ between the per-edit scope-gate-audit hook (afterFileEdit) and final-review's
7
+ declared-scope check (Step C), so the two never disagree on what counts as
8
+ "in scope".
9
+
10
+ Pattern support:
11
+ - exact path: src/components/LoginButton.tsx
12
+ - glob *: src/styles/*.css (single segment, no /)
13
+ - glob **: src/**/test_*.py (recursive across dirs)
14
+ - bare dir: src/components (matches everything under it)
15
+
16
+ Stdlib only; Python 3.9+. REPORTS only - never edits.
17
+
18
+ CLI:
19
+ scope_match.py --path src/auth/session.ts --patterns-file .scope.json
20
+ -> prints JSON {"in_scope": false, "matched_by": null} and exits 0
21
+ -> if .scope.json is missing or unparseable, prints {"in_scope": true,
22
+ "skipped": "no .scope.json"} (fail-open: no contract = no gate)
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import os
29
+ import re
30
+ import sys
31
+
32
+ # Sentinel chars for anchoring; built once, used in the regex below.
33
+ _ANCHOR_START = "^"
34
+ _ANCHOR_END = "$"
35
+
36
+
37
+ def _pattern_to_regex(pattern: str) -> re.Pattern:
38
+ """Convert a glob pattern to a compiled regex matching the WHOLE path.
39
+
40
+ Standard glob semantics (NOT fnmatch's):
41
+ ** matches any chars INCLUDING / (recursive)
42
+ * matches any chars EXCEPT / (single segment)
43
+ ? matches a single char EXCEPT /
44
+ A bare directory pattern (basename has no dot, e.g. 'src/components')
45
+ matches the dir AND everything beneath it.
46
+ """
47
+ bare = pattern.rstrip("/")
48
+ base = os.path.basename(bare)
49
+ is_dir = ("." not in base)
50
+
51
+ out: list[str] = []
52
+ i = 0
53
+ while i < len(pattern):
54
+ c = pattern[i]
55
+ if c == "*" and i + 1 < len(pattern) and pattern[i + 1] == "*":
56
+ out.append(".*") # ** crosses /
57
+ i += 2
58
+ elif c == "*":
59
+ out.append("[^/]*") # * stays within a segment
60
+ i += 1
61
+ elif c == "?":
62
+ out.append("[^/]")
63
+ i += 1
64
+ else:
65
+ out.append(re.escape(c))
66
+ i += 1
67
+ body = "".join(out)
68
+
69
+ if is_dir:
70
+ body = body + "(/.*)?"
71
+
72
+ return re.compile(_ANCHOR_START + body + _ANCHOR_END)
73
+
74
+
75
+ def in_scope(path: str, patterns: list) -> tuple:
76
+ """Return (matched_bool, matched_by_pattern_or_None)."""
77
+ norm = path.replace("\\", "/").lstrip("/")
78
+ for p in patterns:
79
+ p = p.strip().replace("\\", "/")
80
+ if not p:
81
+ continue
82
+ if _pattern_to_regex(p).match(norm):
83
+ return True, p
84
+ return False, None
85
+
86
+
87
+ def load_scope(scope_path: str):
88
+ """Load and validate .scope.json. Returns the dict, or None if missing/
89
+ unparseable (fail-open: no contract = no gate fires)."""
90
+ if not os.path.isfile(scope_path):
91
+ return None
92
+ try:
93
+ with open(scope_path, "r", encoding="utf-8") as f:
94
+ data = json.load(f)
95
+ if not isinstance(data, dict):
96
+ return None
97
+ if not isinstance(data.get("files", []), list):
98
+ return None
99
+ return data
100
+ except (json.JSONDecodeError, OSError):
101
+ return None
102
+
103
+
104
+ def main() -> int:
105
+ ap = argparse.ArgumentParser(
106
+ description="Declared-scope glob matcher (shared helper).")
107
+ ap.add_argument("--path", required=True,
108
+ help="repo-relative path to check")
109
+ ap.add_argument("--patterns-file",
110
+ help="path to .scope.json (default: .scope.json in cwd)")
111
+ ap.add_argument("--patterns",
112
+ help="comma-separated patterns (overrides --patterns-file)")
113
+ args = ap.parse_args()
114
+
115
+ if args.patterns:
116
+ patterns = [p.strip() for p in args.patterns.split(",") if p.strip()]
117
+ matched, by = in_scope(args.path, patterns)
118
+ result = {"in_scope": matched, "matched_by": by}
119
+ else:
120
+ scope_path = args.patterns_file or os.path.join(os.getcwd(), ".scope.json")
121
+ scope = load_scope(scope_path)
122
+ if scope is None:
123
+ print(json.dumps({"in_scope": True, "skipped": "no valid .scope.json"}))
124
+ return 0
125
+ patterns = [str(f) for f in scope.get("files", [])]
126
+ matched, by = in_scope(args.path, patterns)
127
+ result = {
128
+ "in_scope": matched,
129
+ "matched_by": by,
130
+ "allow_growth": bool(scope.get("allow_growth", False)),
131
+ "intent": scope.get("intent", ""),
132
+ }
133
+
134
+ print(json.dumps(result))
135
+ return 0
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
@@ -1,5 +1,5 @@
1
1
  FINAL REVIEW — you just finished an implementation. Before you treat it as done,
2
- audit EVERYTHING you changed this session across the five axes below and FIX what
2
+ audit EVERYTHING you changed this session across the six axes below and FIX what
3
3
  fails. Do NOT revert the behaviour the user asked for. If an axis is already
4
4
  clean, say so in one line — do not manufacture work.
5
5
 
@@ -64,4 +64,36 @@ Step C — session footprint (also in the header above):
64
64
  If "Session footprint" shows >5 files or the request was simple, justify each
65
65
  file or trim. Unjustified files are slop.
66
66
 
67
+ Step D — declared scope (closing gate for Compuerta 1):
68
+ If `.scope.json` exists in the repo root, run the session's full diff against
69
+ the declared contract. In your shell:
70
+ for f in $(git diff --name-only HEAD); do
71
+ python ~/.cursor/skills/anti-slop/scripts/scope_match.py --path "$f" --patterns-file .scope.json
72
+ done
73
+ Any file reporting `"in_scope": false` is a scope violation you must justify
74
+ (add to .scope.json with a one-line reason) or revert. If `.scope.json` does
75
+ not exist, this step is skipped — the declared-editing ladder and the
76
+ per-edit scope-gate-audit hook are the opt-in discipline.
77
+
67
78
  Fix with edits now; re-run the scan (if Step A ran) and the tests; then stop.
79
+
80
+ ## 5. Wiring completeness
81
+ For every user-visible behavior you added or changed (button, form submit, API
82
+ call, route, state transition, scheduled job), trace its execution path end to
83
+ end and confirm it reaches a REAL EFFECT (persist, mutate, call, render, notify).
84
+ A dead end is slop even if the code is clean. Hunt for the vibe-coding failure
85
+ mode where a layer EXISTS but is not WIRED:
86
+
87
+ - `handleSubmit()` that does not persist / does not call the API.
88
+ - An endpoint that no route or caller invokes.
89
+ - A DB write / table that nothing reads or writes.
90
+ - A component that renders but is never mounted / routed to.
91
+ - A hook / store / context that is declared but never consumed.
92
+ - A `TODO` / empty body / stubbed `console.log` standing in for the effect.
93
+
94
+ The bar is: a senior can follow the path click -> handler -> call -> store ->
95
+ render (or the equivalent slice) without hitting a gap. If a step is missing or
96
+ faked, either wire it now or remove the dead half so the diff does not ship
97
+ scaffolding that looks complete but does nothing. Stubs you intend to wire later
98
+ must be marked with a `TODO(wire):` comment naming what is missing; unmarked
99
+ dead ends are failures.
@@ -1,7 +1,7 @@
1
1
  # final-review.ps1 - stop hook (Cursor).
2
2
  #
3
- # ONE comprehensive end-of-implementation review across five axes:
4
- # intent, correctness, reliability, coverage, and anti-slop. When the agent finishes an
3
+ # ONE comprehensive end-of-implementation review across six axes:
4
+ # intent, correctness, reliability, coverage, anti-slop, and wiring completeness. When the agent finishes an
5
5
  # implementation that touched files, Cursor auto-submits this hook's
6
6
  # `followup_message` as the next user turn, so the model re-audits everything it
7
7
  # changed this session and FIXES what fails - the model-as-auditor pattern over
@@ -91,6 +91,12 @@ FINAL REVIEW - audit everything you changed this session and FIX what fails
91
91
  run `python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all` first.
92
92
  Consolidate clones; drop premature abstraction, unneeded deps, operational
93
93
  slop (retries, await-in-loop, log spam), unjustified files.
94
+ 5. Wiring completeness - for every user-visible behavior you added/changed
95
+ (button, submit, API call, route, state transition), trace its execution
96
+ path to a REAL EFFECT (persist, mutate, call, render). A dead end is slop:
97
+ handleSubmit that does not persist, an endpoint no caller invokes, a store
98
+ never consumed, a stub/TODO/console.log standing in for the effect. Wire it
99
+ now or remove the dead half; mark later-stubs with TODO(wire):.
94
100
  Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one line.
95
101
  '@
96
102
  }
@@ -0,0 +1,125 @@
1
+ # scope-gate-audit.ps1 - afterFileEdit "declared scope" advisory (Cursor).
2
+ #
3
+ # Compuerta 1 of the anti-slop system: the declared-scope gate. When the agent
4
+ # writes a .scope.json contract (intent + files[] + acceptance), this hook
5
+ # checks every edited file against it. Editing OUTSIDE the declared set is the
6
+ # textbook scope-creep / gold-plating signal - the agent is doing work it did
7
+ # not declare. Advisory only on Cursor (no preToolUse for file edits), but the
8
+ # violation is flagged on the next turn and the model must justify or revert.
9
+ #
10
+ # Opt-in: if .scope.json does not exist in the repo root, this hook is silent.
11
+ # Declared-editing discipline is something the agent opts into by writing the
12
+ # contract. No contract = no gate (fallback to declared-editing ladder + the
13
+ # footprint check in final-review).
14
+ #
15
+ # Mechanism: resolve edited file -> repo-relative, run scope_match.py against
16
+ # .scope.json's files[], append advisory to feedback-<cid>.txt on violation.
17
+ # Identical pattern to semantic-density-audit and anti-slop-audit.
18
+ #
19
+ # Advisory only: never blocks, never persists state, ALWAYS exits 0.
20
+ # Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0
21
+
22
+ $ErrorActionPreference = 'SilentlyContinue'
23
+ . "$PSScriptRoot\hook-common.ps1"
24
+
25
+ if ($env:HOOKS_ENFORCE -eq '0' -or $env:SCOPE_GATE_ENFORCE -eq '0') { exit 0 }
26
+
27
+ $obj = Read-HookStdinJson
28
+ if (-not $obj) { exit 0 }
29
+
30
+ # audit root: project from JSON (cwd, then workspace_roots), else CURSOR_PROJECT_DIR / HOME
31
+ $root = ''
32
+ $cands = @()
33
+ if ($obj.PSObject.Properties['cwd'] -and $obj.cwd) { $cands += [string]$obj.cwd }
34
+ if ($obj.PSObject.Properties['workspace_roots']) { foreach ($w in $obj.workspace_roots) { $cands += [string]$w } }
35
+ foreach ($c in $cands) { $f = ConvertTo-FwdPath $c; if ($f -and (Test-Path -LiteralPath $f)) { $root = $f.TrimEnd('/'); break } }
36
+ if (-not $root) { $root = (& { if ($env:CURSOR_PROJECT_DIR) { $env:CURSOR_PROJECT_DIR } else { $HOME } }).Replace('\', '/').TrimEnd('/') }
37
+
38
+ # edited file -> repo-relative forward-slash path
39
+ $fp = ''
40
+ foreach ($k in 'file_path', 'path', 'filename', 'absolute_path', 'abs_path') {
41
+ if ($obj.PSObject.Properties[$k] -and $obj.$k) { $fp = [string]$obj.$k; break }
42
+ }
43
+ if (-not $fp) { exit 0 }
44
+ $rel = ConvertTo-FwdPath $fp
45
+ if ($rel.StartsWith($root + '/', [System.StringComparison] 'OrdinalIgnoreCase')) { $rel = $rel.Substring($root.Length + 1) }
46
+ if (Test-IsCursorConfigPath $fp) { exit 0 }
47
+ if (Test-IsCursorConfigPath $rel) { exit 0 }
48
+
49
+ # --- opt-in gate: no .scope.json = no gate ---------------------------------
50
+ $scopeFile = "$root/.scope.json"
51
+ if (-not (Test-Path -LiteralPath $scopeFile)) { exit 0 }
52
+
53
+ # --- resolve Python + run scope_match.py -----------------------------------
54
+ $py = Get-Command python, python3, py -ErrorAction SilentlyContinue | Select-Object -First 1
55
+ if (-not $py) { exit 0 } # no Python -> fail open
56
+
57
+ $matcher = Join-Path $HOME '.cursor\skills\anti-slop\scripts\scope_match.py'
58
+ if (-not (Test-Path $matcher)) { exit 0 } # skill not installed -> silent
59
+
60
+ $mout = & $py.Source $matcher --path $rel --patterns-file $scopeFile 2>$null
61
+ if (-not $mout) { exit 0 }
62
+
63
+ $payload = $null
64
+ try { $payload = ($mout -join "`n") | ConvertFrom-Json } catch { }
65
+ if (-not $payload) { exit 0 }
66
+
67
+ # fail-open: if scope_match reported skipped (no valid contract), stay silent
68
+ $hasSkipped = $false
69
+ try { if ($payload.PSObject.Properties['skipped']) { $hasSkipped = $true } } catch { }
70
+ if ($hasSkipped) { exit 0 }
71
+
72
+ $inScope = $false
73
+ try { $inScope = [bool]$payload.in_scope } catch { }
74
+ if ($inScope) { exit 0 }
75
+
76
+ # --- violation: compose advisory -------------------------------------------
77
+ $allowGrowth = $false
78
+ if ($payload.PSObject.Properties['allow_growth'] -and $payload.allow_growth) { $allowGrowth = $true }
79
+ $intent = ''
80
+ if ($payload.PSObject.Properties['intent']) { $intent = [string]$payload.intent }
81
+
82
+ # Read the declared files list for the message (best-effort; skip on failure)
83
+ $declaredFiles = ''
84
+ try {
85
+ $scopeJson = Get-Content -LiteralPath $scopeFile -Raw | ConvertFrom-Json
86
+ if ($scopeJson.files) { $declaredFiles = ($scopeJson.files -join ', ') }
87
+ } catch { }
88
+
89
+ if ($allowGrowth) {
90
+ # Growth is allowed: informational, not a violation
91
+ $summary = "Scope note - $rel is new vs your declared scope (growth allowed)"
92
+ $body = @"
93
+ You touched a file outside your initial declared set. Since allow_growth is
94
+ true, this is not a violation, but justify it: add $rel to .scope.json or
95
+ explain why the scope grew.
96
+ "@
97
+ } else {
98
+ # Hard violation: edited outside the declared contract
99
+ $summary = "[SCOPE VIOLATION] $rel is NOT in your declared scope"
100
+ $body = @"
101
+ Your contract (.scope.json):
102
+ intent: $intent
103
+ files: $declaredFiles
104
+
105
+ You declared these files and touched one outside the set. Either:
106
+ 1. Add $rel to .scope.json with a one-line justification, OR
107
+ 2. Revert the change - it is out of scope for the declared intent.
108
+
109
+ Declared-editing: declare BEFORE you expand. Don't sneak edits past the gate.
110
+ "@
111
+ }
112
+
113
+ $msg = "$summary`n`n$body`n`n(Advisory; disable: SCOPE_GATE_ENFORCE=0)"
114
+
115
+ # --- append to the shared pending file --------------------------------------
116
+ $cid = Get-SafeConversationId $obj
117
+ $pending = Join-Path (Get-HooksPendingDir) "feedback-$cid.txt"
118
+ try {
119
+ New-Item -ItemType Directory -Path (Split-Path $pending) -Force | Out-Null
120
+ $prefix = ''
121
+ if ((Test-Path $pending) -and ((Get-Item $pending).Length -gt 0)) { $prefix = "`n`n---`n`n" }
122
+ Add-Content -Path $pending -Value ($prefix + $msg) -NoNewline
123
+ } catch { }
124
+
125
+ exit 0
@@ -27,6 +27,12 @@
27
27
  "matcher": "^(Write|StrReplace|EditNotebook)$",
28
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."
29
29
  },
30
+ {
31
+ "command": "pwsh.exe -NoProfile -File ~/.agents/hooks/scope-gate-audit.ps1",
32
+ "timeout": 10,
33
+ "matcher": "^(Write|StrReplace|EditNotebook)$",
34
+ "_comment": "10s (Compuerta 1): declared-scope advisory. OPT-IN: only active when .scope.json exists in the repo root. The agent declares intent + files[] + acceptance; this hook checks every edit against the declared set via scope_match.py (exact, * glob, ** recursive, bare-dir). Out-of-scope edit = [SCOPE VIOLATION] advisory to pending. No .scope.json = silent (fallback to declared-editing ladder + final-review footprint check). Never blocks (Cursor has no preToolUse for file edits). Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0."
35
+ },
30
36
  {
31
37
  "command": "pwsh.exe -NoProfile -File ~/.agents/hooks/anti-slop-audit.ps1",
32
38
  "timeout": 15,