cursordoctrine 0.3.2 → 0.4.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 +1 -0
- package/bin/cli.mjs +18 -1
- package/linux/hooks/anchor-set-nudge.sh +77 -0
- package/linux/hooks/final-review.md +11 -0
- package/linux/hooks/final-review.sh +13 -2
- package/linux/hooks/scope-gate-audit.sh +155 -0
- package/linux/hooks/subagent-stop-review.sh +13 -2
- package/linux/hooks.json +12 -0
- package/linux/inject-doctrine.sh +1 -1
- package/linux/pre-compile.md +72 -0
- package/package.json +2 -2
- package/skills/anti-slop/SKILL.md +7 -3
- package/skills/anti-slop/scripts/scope_match.py +150 -0
- package/windows/hooks/anchor-set-nudge.ps1 +76 -0
- package/windows/hooks/final-review.md +11 -0
- package/windows/hooks/final-review.ps1 +10 -2
- package/windows/hooks/permission-gate.ps1 +98 -98
- package/windows/hooks/scope-gate-audit.ps1 +136 -0
- package/windows/hooks/self-review-trigger.ps1 +83 -83
- package/windows/hooks/subagent-stop-review.ps1 +10 -2
- package/windows/hooks.json +12 -0
- package/windows/inject-doctrine.ps1 +2 -1
- package/windows/pre-compile.md +72 -0
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
|
@@ -86,6 +86,7 @@ All hooks fail open and always exit 0. Nothing here can block your session.
|
|
|
86
86
|
| `HOOKS_ENFORCE=0` | on | turns off all advisory hooks at once |
|
|
87
87
|
| `PERM_GATE_ENFORCE=0` | on | disables the permission gate |
|
|
88
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) |
|
|
89
90
|
| `SEMANTIC_DENSITY_ENFORCE=0` | on | disables the semantic-opacity advisory |
|
|
90
91
|
| `ANTI_SLOP_ENFORCE=0` | on | disables the slop advisory |
|
|
91
92
|
| `FINAL_REVIEW_ENFORCE=0` | on | disables the final review pass |
|
package/bin/cli.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const pendingDir = join(cursorDst, '.hooks-pending');
|
|
|
40
40
|
const hooksJsonDst = join(cursorDst, 'hooks.json');
|
|
41
41
|
|
|
42
42
|
const injectName = platform === 'windows' ? 'inject-doctrine.ps1' : 'inject-doctrine.sh';
|
|
43
|
-
const doctrineFiles = [injectName, 'doctrine.md', 'USER-RULES.md', 'declared-editing.md'];
|
|
43
|
+
const doctrineFiles = [injectName, 'doctrine.md', 'USER-RULES.md', 'declared-editing.md', 'pre-compile.md'];
|
|
44
44
|
|
|
45
45
|
function payloadHookFiles() {
|
|
46
46
|
return readdirSync(join(payload, 'hooks'));
|
|
@@ -263,6 +263,22 @@ function verify() {
|
|
|
263
263
|
return true;
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
check('anchor-set nudge fires on first edit, then goes quiet', () => {
|
|
267
|
+
// First edit of the implementation -> the pre-compile nudge stashes its
|
|
268
|
+
// advisory in the pending feedback bus and arms the one-shot flag.
|
|
269
|
+
const first = runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'x.py') });
|
|
270
|
+
if (!first.includes('additional_context') || !first.includes('PRE-COMPILE NUDGE')) {
|
|
271
|
+
// anchor-set-nudge appends to feedback-<cid>.txt (the shared bus) rather
|
|
272
|
+
// than emitting JSON directly; drain it the same way post-tool-use does.
|
|
273
|
+
const drained = runHook(hook('post-tool-use'), { conversation_id: 'npxv3' });
|
|
274
|
+
if (!drained.includes('PRE-COMPILE NUDGE')) return { ok: false, detail: 'nudge did not reach the feedback bus on first edit' };
|
|
275
|
+
}
|
|
276
|
+
// Second edit -> flag is armed, nudge must stay silent.
|
|
277
|
+
const second = runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'y.py') });
|
|
278
|
+
if (second.includes('PRE-COMPILE NUDGE')) return { ok: false, detail: 'nudge re-fired on second edit (flag not gating)' };
|
|
279
|
+
return true;
|
|
280
|
+
});
|
|
281
|
+
|
|
266
282
|
check('doctrine injection emits additional_context', () =>
|
|
267
283
|
runHook(join(cursorDst, injectName), {}).includes('additional_context'));
|
|
268
284
|
|
|
@@ -374,6 +390,7 @@ Kill switches (environment variables, all hooks fail open)
|
|
|
374
390
|
PERM_GATE_ENFORCE=0 permission gate off
|
|
375
391
|
MINIMAL_EDITING_ENFORCE=0 over-edit advisory off (deprecated in 0.3.0)
|
|
376
392
|
SEMANTIC_DENSITY_ENFORCE=0 semantic-opacity advisory off
|
|
393
|
+
SCOPE_GATE_ENFORCE=0 declared-scope advisory off
|
|
377
394
|
ANTI_SLOP_ENFORCE=0 slop advisory off
|
|
378
395
|
FINAL_REVIEW_ENFORCE=0 final review off
|
|
379
396
|
SUBAGENT_REVIEW_ENFORCE=0 in-subagent review off
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# anchor-set-nudge.sh - afterFileEdit "pre-compile" nudge (Cursor, Linux).
|
|
3
|
+
#
|
|
4
|
+
# Proactive counterpart to the reactive audits. On the FIRST file edit of an
|
|
5
|
+
# implementation (per conversation), remind the agent to compile its Anchor Set
|
|
6
|
+
# (pre-compile.md) and write .scope.json BEFORE piling on more code. The
|
|
7
|
+
# reactive stack (self-review, anti-slop, final-review) only fires AFTER code
|
|
8
|
+
# exists; this nudge catches intent dilution at token ~50, not at the ~5000 of
|
|
9
|
+
# the stop-hook axis 0. A clean final review of the wrong feature is still the
|
|
10
|
+
# wrong feature - the Anchor Set exists so the right feature is on the rails
|
|
11
|
+
# from the first edit.
|
|
12
|
+
#
|
|
13
|
+
# One-shot PER IMPLEMENTATION, not per session: gated by an
|
|
14
|
+
# anchor-declared-<cid>.flag in the pending dir. That flag is cleared by
|
|
15
|
+
# final-review.sh / subagent-stop-review.sh at the SAME per-implementation
|
|
16
|
+
# boundary where they clear session-edits-<cid>.txt and reviewed-<cid>.flag
|
|
17
|
+
# (the stop after a review pass). So a long conversation with N implementations
|
|
18
|
+
# gets N nudges, not one - every new body of work re-earns the reminder.
|
|
19
|
+
#
|
|
20
|
+
# Advisory only: never blocks, never reads the diff, ALWAYS exits 0. Appends to
|
|
21
|
+
# the shared feedback-<cid>.txt bus; post-tool-use.sh delivers it next turn.
|
|
22
|
+
# Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0
|
|
23
|
+
|
|
24
|
+
set +e
|
|
25
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
26
|
+
|
|
27
|
+
[ "${HOOKS_ENFORCE:-}" = "0" ] && exit 0
|
|
28
|
+
[ "${ANCHOR_NUDGE_ENFORCE:-}" = "0" ] && exit 0
|
|
29
|
+
|
|
30
|
+
input="$(read_hook_stdin)"
|
|
31
|
+
[ -n "$input" ] || exit 0
|
|
32
|
+
|
|
33
|
+
file_path=""
|
|
34
|
+
for k in file_path path filePath; do
|
|
35
|
+
file_path="$(json_get "$input" "$k")"
|
|
36
|
+
[ -n "$file_path" ] && break
|
|
37
|
+
done
|
|
38
|
+
[ -n "$file_path" ] || exit 0
|
|
39
|
+
is_cursor_config_path "$file_path" && exit 0
|
|
40
|
+
|
|
41
|
+
cid="$(safe_conversation_id "$input")"
|
|
42
|
+
pending_dir="$(hooks_pending_dir)"
|
|
43
|
+
flag="$pending_dir/anchor-declared-$cid.flag"
|
|
44
|
+
|
|
45
|
+
# Already nudged this implementation -> stay quiet. The flag is cleared at the
|
|
46
|
+
# per-implementation boundary in final-review.sh / subagent-stop-review.sh.
|
|
47
|
+
[ -f "$flag" ] && exit 0
|
|
48
|
+
|
|
49
|
+
msg="PRE-COMPILE NUDGE - first edit of this implementation: $file_path
|
|
50
|
+
|
|
51
|
+
Before you keep going, did you compile your Anchor Set (pre-compile.md)?
|
|
52
|
+
1. OBJECTIVE - one operational sentence. What is strictly necessary.
|
|
53
|
+
2. CONSTRAINTS - local negations (what you will NOT do).
|
|
54
|
+
3. SCOPE - files to touch, files untouchable.
|
|
55
|
+
4. SUCCESS - the one deterministic check that decides done.
|
|
56
|
+
|
|
57
|
+
If you have not already, write it to .scope.json now (intent / files /
|
|
58
|
+
acceptance / allow_growth). The scope-gate audits every edit against files[],
|
|
59
|
+
and final-review axis 0 traces every diff hunk back to intent. An Anchor Set
|
|
60
|
+
that lives only in your head is not an Anchor Set - the gate cannot audit it.
|
|
61
|
+
|
|
62
|
+
Skip this for trivial one-liners (typo, literal). Otherwise: compile, then code."
|
|
63
|
+
|
|
64
|
+
# Append to the shared pending file (same bus as the other advisories).
|
|
65
|
+
pending="$pending_dir/feedback-$cid.txt"
|
|
66
|
+
mkdir -p "$pending_dir" 2>/dev/null
|
|
67
|
+
if [ -s "$pending" ]; then
|
|
68
|
+
printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
|
|
69
|
+
else
|
|
70
|
+
printf '%s' "$msg" >> "$pending" 2>/dev/null
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Arm the one-shot brake BEFORE returning, so a crash after the append can't
|
|
74
|
+
# re-nudge on the next edit. Mirrors the arming order in final-review.sh.
|
|
75
|
+
touch "$flag" 2>/dev/null
|
|
76
|
+
|
|
77
|
+
exit 0
|
|
@@ -64,6 +64,17 @@ 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.
|
|
68
79
|
|
|
69
80
|
## 5. Wiring completeness
|
|
@@ -36,13 +36,16 @@ cid="$(safe_conversation_id "$input")"
|
|
|
36
36
|
pending_dir="$(hooks_pending_dir)"
|
|
37
37
|
marker="$pending_dir/session-edits-$cid.txt"
|
|
38
38
|
flag="$pending_dir/reviewed-$cid.flag"
|
|
39
|
+
anchor_flag="$pending_dir/anchor-declared-$cid.flag"
|
|
39
40
|
|
|
40
41
|
# Sweep state from sessions that died before their stop hook ran.
|
|
41
42
|
find "$pending_dir" -maxdepth 1 -type f -mtime +7 -delete 2>/dev/null
|
|
42
43
|
|
|
43
44
|
# One-shot brake: the previous stop for this conversation emitted the review.
|
|
45
|
+
# Also clear anchor-declared-<cid>.flag so the pre-compile nudge re-fires for
|
|
46
|
+
# the NEXT implementation (one nudge per body of work, not per session).
|
|
44
47
|
if [ -f "$flag" ]; then
|
|
45
|
-
rm -f "$flag" "$marker" 2>/dev/null
|
|
48
|
+
rm -f "$flag" "$marker" "$anchor_flag" 2>/dev/null
|
|
46
49
|
emit_none
|
|
47
50
|
fi
|
|
48
51
|
|
|
@@ -91,6 +94,14 @@ Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one
|
|
|
91
94
|
fi
|
|
92
95
|
body="$(expand_agent_paths "$body")"
|
|
93
96
|
|
|
97
|
+
# Regla R1 (re-entry): if this review pass is a re-audit after a failed gate or
|
|
98
|
+
# axis, suppress History Propagation - the model must NOT build on its own prior
|
|
99
|
+
# wrong diff. Reset its prior to the Anchor Set, not to its previous attempt.
|
|
100
|
+
reentry_line="
|
|
101
|
+
|
|
102
|
+
RE-ENTRY RULE (Regla R1): if a gate or axis failed, forget the approach that produced it. Re-read your ORIGINAL REQUEST above and your Anchor Set (.scope.json, if you wrote one). Fix ONLY what is failing. Do not refactor in this pass - that is History Propagation, the exact failure mode the Anchor Set exists to prevent.
|
|
103
|
+
"
|
|
104
|
+
|
|
94
105
|
file_list=""
|
|
95
106
|
while IFS= read -r p; do
|
|
96
107
|
[ -n "$p" ] || continue
|
|
@@ -127,7 +138,7 @@ msg="FINAL REVIEW (end of implementation) - intent, correctness, reliability, co
|
|
|
127
138
|
${surface_block}${intent_block}Files you changed this session:
|
|
128
139
|
$file_list
|
|
129
140
|
|
|
130
|
-
$body"
|
|
141
|
+
${body}${reentry_line}"
|
|
131
142
|
|
|
132
143
|
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
133
144
|
touch "$flag" 2>/dev/null
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
acceptance = p.get("acceptance", "")
|
|
82
|
+
print(f"__AG__{allow_growth}")
|
|
83
|
+
print(f"__INTENT__{intent}")
|
|
84
|
+
print(f"__ACCEPT__{acceptance}")
|
|
85
|
+
sys.exit(0)
|
|
86
|
+
PYEOF
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
parsed="$(printf '%s' "$mout" | parse_result)"
|
|
90
|
+
rc=$?
|
|
91
|
+
[ "$rc" -eq 0 ] || exit 0 # 2=skipped, 3=in-scope, 1=parse-fail -> all silent
|
|
92
|
+
|
|
93
|
+
allow_growth="$(printf '%s\n' "$parsed" | grep '__AG__' | sed 's/__AG__//')"
|
|
94
|
+
intent="$(printf '%s\n' "$parsed" | grep '__INTENT__' | sed 's/__INTENT__//')"
|
|
95
|
+
acceptance="$(printf '%s\n' "$parsed" | grep '__ACCEPT__' | sed 's/__ACCEPT__//')"
|
|
96
|
+
|
|
97
|
+
# Read declared files for the message (best-effort)
|
|
98
|
+
declared_files="$(printf '%s' "$scope_file" | "$py" -c "
|
|
99
|
+
import json, sys
|
|
100
|
+
try:
|
|
101
|
+
d = json.load(open(sys.argv[1]))
|
|
102
|
+
print(', '.join(d.get('files', [])))
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
" "$scope_file" 2>/dev/null)"
|
|
106
|
+
|
|
107
|
+
# --- compose advisory ------------------------------------------------------
|
|
108
|
+
# acceptance line: only quote it when the agent declared one. A blank acceptance
|
|
109
|
+
# means the Anchor Set was incomplete - surface that gap, since the whole point
|
|
110
|
+
# of the pre-compile phase is to name the deterministic success check.
|
|
111
|
+
if [ -n "$acceptance" ]; then
|
|
112
|
+
acceptance_line="$acceptance"
|
|
113
|
+
else
|
|
114
|
+
acceptance_line="(not declared - your Anchor Set is missing the EXITO/acceptance field)"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
if [ "$allow_growth" = "1" ]; then
|
|
118
|
+
summary="Scope note - $rel is new vs your declared scope (growth allowed)"
|
|
119
|
+
body=" You touched a file outside your initial declared set. Since allow_growth is
|
|
120
|
+
true, this is not a violation, but justify it: add $rel to .scope.json or
|
|
121
|
+
explain why the scope grew.
|
|
122
|
+
|
|
123
|
+
Your success contract (acceptance): $acceptance_line
|
|
124
|
+
Does growing into $rel still serve that?"
|
|
125
|
+
else
|
|
126
|
+
summary="[SCOPE VIOLATION] $rel is NOT in your declared scope"
|
|
127
|
+
body=" Your contract (.scope.json):
|
|
128
|
+
intent: $intent
|
|
129
|
+
files: $declared_files
|
|
130
|
+
acceptance: $acceptance_line
|
|
131
|
+
|
|
132
|
+
You declared these files and touched one outside the set. Either:
|
|
133
|
+
1. Add $rel to .scope.json with a one-line justification, OR
|
|
134
|
+
2. Revert the change - it is out of scope for the declared intent.
|
|
135
|
+
|
|
136
|
+
Declared-editing: declare BEFORE you expand. Don't sneak edits past the gate."
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
msg="${summary}
|
|
140
|
+
|
|
141
|
+
${body}
|
|
142
|
+
|
|
143
|
+
(Advisory; disable: SCOPE_GATE_ENFORCE=0)"
|
|
144
|
+
|
|
145
|
+
# --- append to the shared pending file --------------------------------------
|
|
146
|
+
cid="$(safe_conversation_id "$input")"
|
|
147
|
+
pending="$(hooks_pending_dir)/feedback-${cid}.txt"
|
|
148
|
+
mkdir -p "$(dirname "$pending")" 2>/dev/null
|
|
149
|
+
if [ -s "$pending" ]; then
|
|
150
|
+
printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
|
|
151
|
+
else
|
|
152
|
+
printf '%s' "$msg" >> "$pending" 2>/dev/null
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
exit 0
|
|
@@ -44,10 +44,13 @@ cid="$(safe_conversation_id "$input")"
|
|
|
44
44
|
pending_dir="$(hooks_pending_dir)"
|
|
45
45
|
marker="$pending_dir/session-edits-$cid.txt"
|
|
46
46
|
flag="$pending_dir/reviewed-$cid.flag"
|
|
47
|
+
anchor_flag="$pending_dir/anchor-declared-$cid.flag"
|
|
47
48
|
|
|
48
49
|
# One-shot brake: the previous subagentStop for this id emitted the review.
|
|
50
|
+
# Also clear anchor-declared-<cid>.flag so the pre-compile nudge re-fires for
|
|
51
|
+
# the next subagent implementation (one nudge per body of work).
|
|
49
52
|
if [ -f "$flag" ]; then
|
|
50
|
-
rm -f "$flag" "$marker" 2>/dev/null
|
|
53
|
+
rm -f "$flag" "$marker" "$anchor_flag" 2>/dev/null
|
|
51
54
|
emit_none
|
|
52
55
|
fi
|
|
53
56
|
|
|
@@ -80,6 +83,14 @@ If an axis is clean, say so in one line. Then stop.'
|
|
|
80
83
|
fi
|
|
81
84
|
body="$(expand_agent_paths "$body")"
|
|
82
85
|
|
|
86
|
+
# Regla R1 (re-entry): same suppression as final-review.sh. A subagent that
|
|
87
|
+
# failed an axis must not build on its own prior wrong diff - reset its prior
|
|
88
|
+
# to the Anchor Set, not to its previous attempt.
|
|
89
|
+
reentry_line="
|
|
90
|
+
|
|
91
|
+
RE-ENTRY RULE (Regla R1): if an axis failed, forget the approach that produced it. Re-read your original task and your Anchor Set (.scope.json, if you wrote one). Fix ONLY what is failing. Do not refactor in this pass.
|
|
92
|
+
"
|
|
93
|
+
|
|
83
94
|
file_list=""
|
|
84
95
|
while IFS= read -r p; do
|
|
85
96
|
[ -n "$p" ] || continue
|
|
@@ -94,7 +105,7 @@ msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Be
|
|
|
94
105
|
Files you changed this run:
|
|
95
106
|
$file_list
|
|
96
107
|
|
|
97
|
-
$body"
|
|
108
|
+
${body}${reentry_line}"
|
|
98
109
|
|
|
99
110
|
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
100
111
|
touch "$flag" 2>/dev/null
|
package/linux/hooks.json
CHANGED
|
@@ -27,11 +27,23 @@
|
|
|
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,
|
|
33
39
|
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
34
40
|
"_comment": "15s: AI-slop advisory, companion to minimal-edit-audit. git diff flags new deps / premature abstractions (Factory/Repository/Mediator/CQRS/DDD) / redundant comments, and injects the anti-slop.md self-review checklist on substantial edits (>= ANTI_SLOP_CHECKLIST_LINES, default 40). Appends to the conversation's pending file; never blocks. Disable: HOOKS_ENFORCE=0 or ANTI_SLOP_ENFORCE=0."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"command": "bash ~/.agents/hooks/anchor-set-nudge.sh",
|
|
44
|
+
"timeout": 5,
|
|
45
|
+
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
46
|
+
"_comment": "5s: PROACTIVE pre-compile nudge. On the FIRST edit of each implementation (per conversation), remind the agent to compile its Anchor Set (pre-compile.md) into .scope.json BEFORE piling on more code. The reactive audits (self-review / anti-slop / final-review axis 0) only fire after code exists; this catches intent dilution at token ~50 instead of ~5000. One-shot per implementation: gated by anchor-declared-<cid>.flag, which final-review.sh / subagent-stop-review.sh clear at the same per-implementation boundary as reviewed-<cid>.flag. Advisory only; never blocks. Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0."
|
|
35
47
|
}
|
|
36
48
|
],
|
|
37
49
|
"postToolUse": [
|
package/linux/inject-doctrine.sh
CHANGED
|
@@ -16,7 +16,7 @@ set +e
|
|
|
16
16
|
cat >/dev/null
|
|
17
17
|
|
|
18
18
|
context=""
|
|
19
|
-
for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md" "$HOME/.cursor/declared-editing.md"; do
|
|
19
|
+
for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md" "$HOME/.cursor/declared-editing.md" "$HOME/.cursor/pre-compile.md"; do
|
|
20
20
|
if [ -f "$p" ]; then
|
|
21
21
|
part="$(cat "$p")"
|
|
22
22
|
if [ -n "$context" ]; then context="$context
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Pre-compile — Thin Intent Compilation
|
|
2
|
+
|
|
3
|
+
ACTIVE EVERY IMPLEMENTATION TURN. Before writing or modifying a single line of
|
|
4
|
+
code, emit your Anchor Set. Compiling intent first is what stops the dilution
|
|
5
|
+
that no later axis can fully undo — a clean final review of the wrong feature
|
|
6
|
+
is still the wrong feature.
|
|
7
|
+
|
|
8
|
+
This is the proactive phase. The anti-slop checklist, the self-review trigger
|
|
9
|
+
and the final review are reactive — they audit after the fact. You compile the
|
|
10
|
+
intent BEFORE the first token of code so they have the right thing to audit.
|
|
11
|
+
|
|
12
|
+
## The Anchor Set
|
|
13
|
+
|
|
14
|
+
Answer these four, terse, in your first response. One phrase each, not prose:
|
|
15
|
+
|
|
16
|
+
1. OBJECTIVE — one operational sentence. What is *strictly* necessary. Not
|
|
17
|
+
"improve X" — "make X return Y when Z".
|
|
18
|
+
2. CONSTRAINTS (local negations) — what you will NOT do. "NO schema migration.
|
|
19
|
+
NO new dependency. NO refactor of the surrounding function." Negations bind
|
|
20
|
+
harder than the objective: a constraint that the task contradicts is a bug
|
|
21
|
+
in your reading of the task, and you ask before you override it.
|
|
22
|
+
3. SCOPE —
|
|
23
|
+
- FILES TO TOUCH: exact list, derived from the objective, nothing speculative.
|
|
24
|
+
- FILES UNTOUCHABLE: anything the system marked off-limits (.cursor state,
|
|
25
|
+
lockfiles you weren't asked to touch, files outside the request's blast
|
|
26
|
+
radius).
|
|
27
|
+
4. DETERMINISTIC SUCCESS — the one command, test, or observable check that
|
|
28
|
+
will decide whether this is done. "Tests pass" is not deterministic; the
|
|
29
|
+
specific failing test going green is. If you cannot name one, you do not
|
|
30
|
+
yet understand the task — ask.
|
|
31
|
+
|
|
32
|
+
## Materialize it: .scope.json
|
|
33
|
+
|
|
34
|
+
Write the Anchor Set to `.scope.json` in the repo root before editing source.
|
|
35
|
+
This is the machine-checkable form — the scope-gate hook audits every edit
|
|
36
|
+
against `files[]`, and the final-review axis 0 traces every diff hunk back to
|
|
37
|
+
`intent`. An Anchor Set that lives only in your head is not an Anchor Set.
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"intent": "<OBJECTIVE>",
|
|
42
|
+
"files": ["<FILES TO TOUCH, repo-relative, glob-friendly>"],
|
|
43
|
+
"acceptance": "<DETERMINISTIC SUCCESS>",
|
|
44
|
+
"allow_growth": false
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`allow_growth: false` is the default — the gate fires on any edit outside
|
|
49
|
+
`files[]`. Set it true only if you expect the work to discover new files
|
|
50
|
+
(a refactor, a migration) and you will justify each one as it appears.
|
|
51
|
+
|
|
52
|
+
No need to write `.scope.json` for trivial one-liners (a typo, a literal).
|
|
53
|
+
The declared-editing ladder's rung 1 ("does this need to exist?") governs when
|
|
54
|
+
the Anchor Set itself is overkill. When in doubt, write it.
|
|
55
|
+
|
|
56
|
+
## Regla R3 — Authority
|
|
57
|
+
|
|
58
|
+
If, during execution, you read logs or code that contradict these anchors,
|
|
59
|
+
**the anchors win.** Prior history in this session is auditor material, not
|
|
60
|
+
authority. An earlier wrong assumption of yours does not override the Anchor
|
|
61
|
+
Set you declared at the start.
|
|
62
|
+
|
|
63
|
+
## Regla R1 — On re-entry (when the loop hands you back a failure)
|
|
64
|
+
|
|
65
|
+
If the harness returns a gate failure or a failed axis: forget the approach
|
|
66
|
+
that produced it. Re-read your OBJECTIVE and your Anchor Set, not your prior
|
|
67
|
+
diff. Fix ONLY what is failing. Do not refactor in the same pass — that is
|
|
68
|
+
History Propagation, the failure mode the Anchor Set exists to prevent.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
End of pre-compile. Now emit the Anchor Set, then do the work.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Thin self-review hooks for Cursor — the model is the auditor.
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Thin self-review hooks for Cursor — the model is the auditor. Pre-compile Anchor Set phase (proactive intent compilation), intent-trace final review (Tier 0), unified 13-item anti-slop checklist, operational slop detection.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cursordoctrine": "bin/cli.mjs"
|
|
7
7
|
},
|
|
@@ -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 **
|
|
276
|
-
|
|
277
|
-
|
|
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.
|