cursordoctrine 0.1.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 +113 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/cli.mjs +413 -0
- package/linux/USER-RULES.md +12 -0
- package/linux/doctrine.md +172 -0
- package/linux/hooks/anti-slop-audit.sh +163 -0
- package/linux/hooks/anti-slop.md +56 -0
- package/linux/hooks/final-review.md +52 -0
- package/linux/hooks/final-review.sh +99 -0
- package/linux/hooks/hook-common.sh +120 -0
- package/linux/hooks/minimal-edit-audit.sh +112 -0
- package/linux/hooks/permission-gate.sh +75 -0
- package/linux/hooks/post-tool-use.sh +53 -0
- package/linux/hooks/self-review-trigger.sh +56 -0
- package/linux/hooks/self-review.md +48 -0
- package/linux/hooks/subagent-stop-review.sh +93 -0
- package/linux/hooks.json +64 -0
- package/linux/inject-doctrine.sh +31 -0
- package/package.json +40 -0
- package/skills/anti-slop/SKILL.md +267 -0
- package/skills/anti-slop/scripts/scan_slop.py +986 -0
- package/windows/USER-RULES.md +12 -0
- package/windows/doctrine.md +172 -0
- package/windows/hooks/anti-slop-audit.ps1 +182 -0
- package/windows/hooks/anti-slop.md +56 -0
- package/windows/hooks/final-review.md +52 -0
- package/windows/hooks/final-review.ps1 +105 -0
- package/windows/hooks/hook-common.ps1 +84 -0
- package/windows/hooks/minimal-edit-audit.ps1 +116 -0
- package/windows/hooks/permission-gate.ps1 +98 -0
- package/windows/hooks/post-tool-use.ps1 +46 -0
- package/windows/hooks/self-review-trigger.ps1 +83 -0
- package/windows/hooks/self-review.md +48 -0
- package/windows/hooks/subagent-stop-review.ps1 +89 -0
- package/windows/hooks.json +64 -0
- package/windows/inject-doctrine.ps1 +58 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# minimal-edit-audit.sh - afterFileEdit minimal-editing advisory (Cursor, Linux).
|
|
3
|
+
#
|
|
4
|
+
# Audits the just-edited file for over-editing:
|
|
5
|
+
# * line-count - git diff --numstat thresholds (any language).
|
|
6
|
+
# * token metrics - audit-metrics.py (token-Levenshtein + cognitive
|
|
7
|
+
# complexity), Python files only, if the script exists.
|
|
8
|
+
# On WARN/FAIL it APPENDS a short advisory to the shared pending-feedback file;
|
|
9
|
+
# post-tool-use.sh delivers it as additional_context on the next tool turn.
|
|
10
|
+
#
|
|
11
|
+
# Advisory only: never blocks, never writes persistent state. afterFileEdit
|
|
12
|
+
# output isn't consumed and a non-zero exit shows as "hook failed", so we
|
|
13
|
+
# ALWAYS exit 0.
|
|
14
|
+
#
|
|
15
|
+
# Thresholds (env-overridable): MINIMAL_EDIT_FAIL_LINES (400), MINIMAL_EDIT_WARN_LINES (100).
|
|
16
|
+
# Disable: HOOKS_ENFORCE=0 or MINIMAL_EDITING_ENFORCE=0
|
|
17
|
+
|
|
18
|
+
set +e
|
|
19
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
20
|
+
|
|
21
|
+
[ "${HOOKS_ENFORCE:-}" = "0" ] && exit 0
|
|
22
|
+
[ "${MINIMAL_EDITING_ENFORCE:-}" = "0" ] && exit 0
|
|
23
|
+
|
|
24
|
+
input="$(read_hook_stdin)"
|
|
25
|
+
[ -n "$input" ] || exit 0
|
|
26
|
+
|
|
27
|
+
# audit root = project from JSON (cwd, then workspace_roots), else CURSOR_PROJECT_DIR / HOME
|
|
28
|
+
root=""
|
|
29
|
+
while IFS= read -r cand; do
|
|
30
|
+
[ -n "$cand" ] && [ -d "$cand" ] && { root="${cand%/}"; break; }
|
|
31
|
+
done <<EOF
|
|
32
|
+
$(json_get "$input" cwd)
|
|
33
|
+
$(json_get_array "$input" workspace_roots)
|
|
34
|
+
EOF
|
|
35
|
+
[ -n "$root" ] || root="${CURSOR_PROJECT_DIR:-$HOME}"
|
|
36
|
+
root="${root%/}"
|
|
37
|
+
|
|
38
|
+
# edited file -> repo-relative path
|
|
39
|
+
fp=""
|
|
40
|
+
for k in file_path path filename absolute_path abs_path; do
|
|
41
|
+
fp="$(json_get "$input" "$k")"
|
|
42
|
+
[ -n "$fp" ] && break
|
|
43
|
+
done
|
|
44
|
+
[ -n "$fp" ] || exit 0
|
|
45
|
+
rel="$fp"
|
|
46
|
+
case "$rel" in "$root"/*) rel="${rel#"$root"/}" ;; esac
|
|
47
|
+
if is_cursor_config_path "$fp" || is_cursor_config_path "$rel"; then exit 0; fi
|
|
48
|
+
|
|
49
|
+
# git repo?
|
|
50
|
+
git -C "$root" rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
51
|
+
|
|
52
|
+
# --- line-count audit (any language) --------------------------------------
|
|
53
|
+
fail_lines="${MINIMAL_EDIT_FAIL_LINES:-400}"
|
|
54
|
+
warn_lines="${MINIMAL_EDIT_WARN_LINES:-100}"
|
|
55
|
+
changed="$(git -C "$root" diff HEAD --numstat -- "$rel" 2>/dev/null |
|
|
56
|
+
awk '$1 != "-" && $2 != "-" { n += $1 + $2 } END { print n + 0 }')"
|
|
57
|
+
|
|
58
|
+
grade="OK"; hint=""
|
|
59
|
+
if [ "$changed" -gt "$fail_lines" ]; then
|
|
60
|
+
grade="FAIL"; hint="$changed lines changed (limit $fail_lines) - likely over-editing; trim or split"
|
|
61
|
+
elif [ "$changed" -gt "$warn_lines" ]; then
|
|
62
|
+
grade="WARN"; hint="$changed lines changed - justify each hunk or split the task"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# --- token metrics (.py only) ---------------------------------------------
|
|
66
|
+
audit_metrics="$HOME/.cursor/skills/minimal-editing/scripts/audit-metrics.py"
|
|
67
|
+
if [ -f "$audit_metrics" ] && have_py; then
|
|
68
|
+
case "$rel" in
|
|
69
|
+
*.py)
|
|
70
|
+
mgrade="$(python3 "$audit_metrics" --root "$root" --format json --path "$rel" 2>/dev/null |
|
|
71
|
+
{ if have_jq; then jq -r '.grade // empty'; else python3 -c 'import json,sys
|
|
72
|
+
try: print(json.load(sys.stdin).get("grade",""))
|
|
73
|
+
except Exception: pass'; fi; })"
|
|
74
|
+
if [ "$mgrade" = "FAIL" ]; then grade="FAIL"
|
|
75
|
+
elif [ "$mgrade" = "WARN" ] && [ "$grade" = "OK" ]; then grade="WARN"; fi
|
|
76
|
+
;;
|
|
77
|
+
esac
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
[ "$grade" = "OK" ] && exit 0
|
|
81
|
+
|
|
82
|
+
# --- compose advisory + append to the shared pending file ------------------
|
|
83
|
+
hint_txt=""
|
|
84
|
+
[ -n "$hint" ] && hint_txt=" - $hint"
|
|
85
|
+
if [ "$grade" = "FAIL" ]; then
|
|
86
|
+
actions=" - Trim every hunk that isn't required by the task.
|
|
87
|
+
- Prefer narrow, targeted edits over rewriting blocks.
|
|
88
|
+
- If the change is genuinely large, split it into smaller logical commits."
|
|
89
|
+
else
|
|
90
|
+
actions=" Advisory only - trim unrelated hunks if any; otherwise proceed."
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
msg="Minimal-edit audit $grade - $rel
|
|
94
|
+
|
|
95
|
+
IMPORTANT: Try to preserve the original code and the logic of the original code as much as possible.
|
|
96
|
+
|
|
97
|
+
grade: $grade$hint_txt
|
|
98
|
+
|
|
99
|
+
$actions
|
|
100
|
+
|
|
101
|
+
(Disable for this session: HOOKS_ENFORCE=0)"
|
|
102
|
+
|
|
103
|
+
cid="$(safe_conversation_id "$input")"
|
|
104
|
+
pending="$(hooks_pending_dir)/feedback-$cid.txt"
|
|
105
|
+
mkdir -p "$(dirname "$pending")" 2>/dev/null
|
|
106
|
+
if [ -s "$pending" ]; then
|
|
107
|
+
printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
|
|
108
|
+
else
|
|
109
|
+
printf '%s' "$msg" >> "$pending" 2>/dev/null
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
exit 0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# permission-gate.sh - beforeShellExecution for Cursor (Linux).
|
|
3
|
+
#
|
|
4
|
+
# Single responsibility: deny a small, explicit list of dangerous commands.
|
|
5
|
+
# This is a *permission* gate, not a *quality* gate. The model handles
|
|
6
|
+
# quality; the harness handles blast radius.
|
|
7
|
+
#
|
|
8
|
+
# Behavior:
|
|
9
|
+
# - Exit 0 always.
|
|
10
|
+
# - Print Cursor-canonical {"permission": "allow"|"deny", ...} JSON.
|
|
11
|
+
# - On internal failure: fail OPEN (allow), never block the user.
|
|
12
|
+
#
|
|
13
|
+
# Disable: PERM_GATE_ENFORCE=0
|
|
14
|
+
|
|
15
|
+
set +e
|
|
16
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
17
|
+
|
|
18
|
+
allow() { printf '{"permission":"allow"}'; exit 0; }
|
|
19
|
+
|
|
20
|
+
[ "${PERM_GATE_ENFORCE:-}" = "0" ] && allow
|
|
21
|
+
|
|
22
|
+
input="$(read_hook_stdin)"
|
|
23
|
+
cmd="$(json_get "$input" command)"
|
|
24
|
+
# Belt-and-braces: if stdin was not the documented JSON shape, still gate
|
|
25
|
+
# on the raw text rather than waving everything through.
|
|
26
|
+
[ -n "$cmd" ] || cmd="$input"
|
|
27
|
+
[ -n "$cmd" ] || allow
|
|
28
|
+
|
|
29
|
+
deny() {
|
|
30
|
+
local reason="$1" shown="$cmd"
|
|
31
|
+
# Truncate the echo: the UI message only needs enough to identify it.
|
|
32
|
+
[ "${#shown}" -gt 400 ] && shown="${shown:0:400}..."
|
|
33
|
+
local user_msg="BLOCKED by permission-gate: $reason
|
|
34
|
+
|
|
35
|
+
Command: $shown
|
|
36
|
+
|
|
37
|
+
If this is genuinely intended, run it yourself in your terminal."
|
|
38
|
+
if have_jq; then
|
|
39
|
+
jq -cna --arg u "$user_msg" \
|
|
40
|
+
'{permission:"deny", user_message:$u, agent_message:($u + " Do not retry verbatim. Ask the user to run it manually if it is truly intended.")}'
|
|
41
|
+
elif have_py; then
|
|
42
|
+
U="$user_msg" python3 -c '
|
|
43
|
+
import json, os
|
|
44
|
+
u = os.environ["U"]
|
|
45
|
+
print(json.dumps({"permission": "deny", "user_message": u,
|
|
46
|
+
"agent_message": u + " Do not retry verbatim. Ask the user to run it manually if it is truly intended."},
|
|
47
|
+
ensure_ascii=True, separators=(",", ":")))'
|
|
48
|
+
else
|
|
49
|
+
printf '{"permission":"deny"}'
|
|
50
|
+
fi
|
|
51
|
+
exit 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# test_deny <ERE pattern> <reason>
|
|
55
|
+
test_deny() {
|
|
56
|
+
if printf '%s' "$cmd" | grep -qE "$1"; then deny "$2"; fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Anchored to start OR a command separator so `cd /tmp && rm -rf /` is caught,
|
|
60
|
+
# while `git rm`, `npm run rm-cache`, `echo "rm -rf /"` stay allowed.
|
|
61
|
+
test_deny '(^|[;&|][[:space:]]*)(sudo[[:space:]]+)?rm[[:space:]]+-[a-zA-Z]*([rR][fF]|[fF][rR])[a-zA-Z]*[[:space:]]+/' 'destructive rm -rf on absolute path (use relative paths or be more specific)'
|
|
62
|
+
test_deny ':\(\)\{[[:space:]]*:\|:&[[:space:]]*\};:' 'fork-bomb pattern'
|
|
63
|
+
test_deny 'curl[[:space:]].*\|[[:space:]]*(sudo[[:space:]]*)?(bash|sh|zsh|dash|ash)' 'curl piped to shell'
|
|
64
|
+
test_deny 'wget[[:space:]].*\|[[:space:]]*(sudo[[:space:]]*)?(bash|sh|zsh|dash|ash)' 'wget piped to shell'
|
|
65
|
+
test_deny 'git[[:space:]]+push[[:space:]]+.*--force(-with-lease)?([[:space:]"'"'"']|$)' 'git push --force'
|
|
66
|
+
test_deny 'git[[:space:]]+push[[:space:]]+(-f|--force)([[:space:]"'"'"']|$)' 'git push -f / --force'
|
|
67
|
+
test_deny 'git[[:space:]]+reset[[:space:]]+--hard' 'git reset --hard (data loss)'
|
|
68
|
+
test_deny 'git[[:space:]]+clean[[:space:]]+-[a-zA-Z]*f' 'git clean -f (untracked data loss)'
|
|
69
|
+
test_deny 'dd[[:space:]].*of=/dev/(sd|nvme|hd|xvd)' 'dd to block device'
|
|
70
|
+
test_deny 'mkfs(\.[a-z0-9]+)?[[:space:]]+/dev/' 'mkfs on device'
|
|
71
|
+
test_deny 'chmod[[:space:]]+-R[[:space:]]+777[[:space:]]+/' 'chmod -R 777 on root'
|
|
72
|
+
test_deny 'chown[[:space:]]+-R[[:space:]]+[^[:space:]]+[[:space:]]+/' 'chown -R on root'
|
|
73
|
+
test_deny '^(npm|pnpm|yarn)[[:space:]]+publish([[:space:]]|$)' 'package publish (use ship-hook, not direct publish)'
|
|
74
|
+
|
|
75
|
+
allow
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# post-tool-use.sh - postToolUse for Cursor (Linux).
|
|
3
|
+
#
|
|
4
|
+
# Two responsibilities, both message-bus work, keyed by conversation_id so
|
|
5
|
+
# concurrent sessions never receive each other's prompts:
|
|
6
|
+
#
|
|
7
|
+
# 1. Fold completed subagents' session-edits markers into this
|
|
8
|
+
# conversation's marker (postToolUse does NOT fire for the Task tool -
|
|
9
|
+
# verified - so this per-tool-boundary fold is how delegated edits reach
|
|
10
|
+
# the parent's stop-hook final review). When a fold happens, prime the
|
|
11
|
+
# parent to audit the subagent's diff now.
|
|
12
|
+
# 2. Drain this conversation's stashed self-review / advisory messages into
|
|
13
|
+
# Cursor's additional_context channel. One-shot delivery.
|
|
14
|
+
#
|
|
15
|
+
# We do not parse, score, or filter. We do not run any audit. We do not
|
|
16
|
+
# block. The model that already produced the edit will, on its next
|
|
17
|
+
# turn, do the self-review.
|
|
18
|
+
|
|
19
|
+
set +e
|
|
20
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
21
|
+
|
|
22
|
+
input="$(read_hook_stdin)"
|
|
23
|
+
cid="$(safe_conversation_id "$input")"
|
|
24
|
+
|
|
25
|
+
fold_note=""
|
|
26
|
+
if merge_subagent_edit_markers "$input" "$cid"; then
|
|
27
|
+
fold_note="SUBAGENT WORK DETECTED - a subagent of this conversation edited files (its edits fired hooks in ITS context, not yours). YOU are the auditor of its work: audit its diff (git status / git diff on the files it touched) against ~/.agents/hooks/self-review.md. Fix real bugs; stay silent otherwise. Its files are folded into this conversation's end-of-implementation review."
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
pending_file="$(hooks_pending_dir)/feedback-$cid.txt"
|
|
31
|
+
|
|
32
|
+
msg=""
|
|
33
|
+
if [ -f "$pending_file" ]; then
|
|
34
|
+
[ -s "$pending_file" ] && msg="$(cat "$pending_file" 2>/dev/null)"
|
|
35
|
+
# One-shot: clear before emitting so a hook error doesn't replay forever.
|
|
36
|
+
rm -f "$pending_file" 2>/dev/null
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ -n "$fold_note" ]; then
|
|
40
|
+
if [ -n "$msg" ]; then
|
|
41
|
+
msg="$fold_note
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
$msg"
|
|
46
|
+
else
|
|
47
|
+
msg="$fold_note"
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
[ -n "$msg" ] || exit 0
|
|
51
|
+
|
|
52
|
+
emit_json additional_context "$msg"
|
|
53
|
+
exit 0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# self-review-trigger.sh - afterFileEdit for Cursor (Linux).
|
|
3
|
+
#
|
|
4
|
+
# Single responsibility: when the model just edited a file, hand the
|
|
5
|
+
# edit context to the NEXT model turn as additional_context. The model
|
|
6
|
+
# is the auditor; the harness is just the message bus.
|
|
7
|
+
#
|
|
8
|
+
# We DO:
|
|
9
|
+
# - Capture the edited file path.
|
|
10
|
+
# - Record it in the session-edits marker (drained by final-review.sh).
|
|
11
|
+
# - Stash a self-review prompt that primes the model's next turn.
|
|
12
|
+
# - Exit 0 always.
|
|
13
|
+
#
|
|
14
|
+
# Cursor's afterFileEdit doesn't consume its own output. To actually
|
|
15
|
+
# surface the message, post-tool-use.sh re-emits it on the next tool
|
|
16
|
+
# boundary. See hooks.json.
|
|
17
|
+
|
|
18
|
+
set +e
|
|
19
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
20
|
+
|
|
21
|
+
input="$(read_hook_stdin)"
|
|
22
|
+
|
|
23
|
+
file_path="$(json_get "$input" file_path)"
|
|
24
|
+
[ -n "$file_path" ] || file_path="$(json_get "$input" path)"
|
|
25
|
+
[ -n "$file_path" ] || file_path="$(json_get "$input" filePath)"
|
|
26
|
+
cid="$(safe_conversation_id "$input")"
|
|
27
|
+
|
|
28
|
+
# Empty path (JSON parse failed, or no file_path field) -> nothing to record.
|
|
29
|
+
[ -n "$file_path" ] || exit 0
|
|
30
|
+
if is_cursor_config_path "$file_path"; then exit 0; fi
|
|
31
|
+
|
|
32
|
+
# State is keyed by conversation_id and lives under $HOME, never the project:
|
|
33
|
+
# no repo litter, works in workspace-less sessions, and concurrent sessions
|
|
34
|
+
# cannot drain each other's prompts.
|
|
35
|
+
pending_dir="$(hooks_pending_dir)"
|
|
36
|
+
mkdir -p "$pending_dir" 2>/dev/null
|
|
37
|
+
|
|
38
|
+
# Record this edit for the end-of-implementation review (final-review.sh).
|
|
39
|
+
printf '%s\n' "$file_path" >> "$pending_dir/session-edits-$cid.txt" 2>/dev/null
|
|
40
|
+
|
|
41
|
+
doctrine_file="$HOME/.agents/hooks/self-review.md"
|
|
42
|
+
[ -f "$doctrine_file" ] || exit 0
|
|
43
|
+
doctrine="$(cat "$doctrine_file")"
|
|
44
|
+
|
|
45
|
+
msg="SELF-REVIEW TRIGGER - you just edited: $file_path
|
|
46
|
+
|
|
47
|
+
$doctrine"
|
|
48
|
+
|
|
49
|
+
pending_file="$pending_dir/feedback-$cid.txt"
|
|
50
|
+
if [ -s "$pending_file" ]; then
|
|
51
|
+
printf '\n\n---\n\n%s' "$msg" >> "$pending_file" 2>/dev/null
|
|
52
|
+
else
|
|
53
|
+
printf '%s' "$msg" >> "$pending_file" 2>/dev/null
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
exit 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
You are the auditor of your own edit. The user's `Edit` tool just changed a
|
|
2
|
+
file. Your job, on this turn, is to:
|
|
3
|
+
|
|
4
|
+
1. Read the file that was just changed.
|
|
5
|
+
2. Read the diff (provided in the prior tool output).
|
|
6
|
+
3. Decide: does this edit introduce any of the following?
|
|
7
|
+
- **Security**: hardcoded secret (AWS key, private key, API token,
|
|
8
|
+
password in source), `eval(`, `exec(`, `pickle.loads`, `verify=False`,
|
|
9
|
+
`child_process` with user input, `dangerouslySetInnerHTML` with
|
|
10
|
+
untrusted data, SQL string concat.
|
|
11
|
+
- **Correctness**: assignment-in-condition (`if (x = 5)`), `==`/`!=`
|
|
12
|
+
with `null`/`None`/`NaN` in a comparison, `forEach` with `await`,
|
|
13
|
+
async `useEffect` with side-effects missing cleanup, `==` instead
|
|
14
|
+
of `===` in JS, mutable default args in Python, shadowed imports,
|
|
15
|
+
dead relative imports.
|
|
16
|
+
- **Safety**: `rm -rf /`, `curl ... | sh`, force-push, `git reset --hard`
|
|
17
|
+
without a backup, `npm publish` without version bump, secret
|
|
18
|
+
committed to a public file.
|
|
19
|
+
- **Logic bugs that the user would actually care about**: a function
|
|
20
|
+
that returns the wrong thing, an off-by-one, a missing `return`, a
|
|
21
|
+
wrong import path.
|
|
22
|
+
4. If you find a real bug, **fix it with `Edit`**, then say nothing.
|
|
23
|
+
Do not report it. Do not explain it. The user will see the fix
|
|
24
|
+
in the next message; the bug is gone.
|
|
25
|
+
5. If the edit is clean, respond with the single word: `clean`.
|
|
26
|
+
|
|
27
|
+
Hard constraints:
|
|
28
|
+
|
|
29
|
+
- **Never revert or re-do work the user asked for.** The user's intent
|
|
30
|
+
is the source of truth. You are a *post-hoc* auditor, not a rewriter.
|
|
31
|
+
- **Never change style, naming, formatting, or "improvements" the user
|
|
32
|
+
did not ask for.** If the user added a one-liner with bad formatting,
|
|
33
|
+
leave it. Self-review is for *bugs*, not taste.
|
|
34
|
+
- **Never re-read the whole repo.** Only the file you just edited, and
|
|
35
|
+
the diff. Context is finite.
|
|
36
|
+
- **Never run shell commands in this turn.** Your only allowed tool is
|
|
37
|
+
`Read` and `Edit`. (And `Edit` only if you are fixing a real bug.)
|
|
38
|
+
- **If you are uncertain whether something is a bug, leave it.** False
|
|
39
|
+
positives waste user time. The bar is "this would fail a careful
|
|
40
|
+
code review at Anthropic / Stripe / Vercel." Cosmetic things, missing
|
|
41
|
+
type hints, and "you could write this more idiomatically" are NOT bugs.
|
|
42
|
+
- **One pass, no recursion.** If you fix one bug and find another, fix
|
|
43
|
+
that too — but stop after at most 2 edits. Beyond that you are
|
|
44
|
+
thrashing.
|
|
45
|
+
|
|
46
|
+
This is the entire self-review prompt. It is the same prompt, every
|
|
47
|
+
edit, forever. The model is the auditor. There is no regex, no AST
|
|
48
|
+
parse, no Python — the model itself does the work.
|
|
@@ -0,0 +1,93 @@
|
|
|
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 four-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 - no duplicate helpers, premature abstraction, unneeded deps,
|
|
77
|
+
redundant comments, dead code.
|
|
78
|
+
If an axis is clean, say so in one line. Then stop.'
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
file_list="$(printf '%s\n' "$edited" | head -n 30 | sed 's/^/ /')"
|
|
82
|
+
msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.
|
|
83
|
+
|
|
84
|
+
Files you changed this run:
|
|
85
|
+
$file_list
|
|
86
|
+
|
|
87
|
+
$body"
|
|
88
|
+
|
|
89
|
+
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
90
|
+
touch "$flag" 2>/dev/null
|
|
91
|
+
|
|
92
|
+
emit_json followup_message "$msg"
|
|
93
|
+
exit 0
|
package/linux/hooks.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"command": "bash ~/.cursor/inject-doctrine.sh",
|
|
7
|
+
"timeout": 5,
|
|
8
|
+
"_comment": "5s: inject the agent doctrine + user rules at session start. inject-doctrine.sh reads ~/.cursor/doctrine.md + USER-RULES.md and emits them as {\"additional_context\": ...} JSON (sessionStart does NOT consume raw stdout)."
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"afterFileEdit": [
|
|
12
|
+
{
|
|
13
|
+
"command": "bash ~/.agents/hooks/self-review-trigger.sh",
|
|
14
|
+
"timeout": 5,
|
|
15
|
+
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
16
|
+
"_comment": "5s: record the edit in ~/.cursor/.hooks-pending/session-edits-<conversation_id>.txt and stash the self-review prompt in feedback-<conversation_id>.txt. The harness normalizes agent file edits (incl. StrReplace) to tool type 'Write' in this event - verified via payload capture - so ^Write$ matches them all; the alternation is defensive against future harness versions reporting raw tool names. Anchored so TabWrite (every user tab-completion) stays excluded. The model is the auditor. NOTE: fires inside subagent contexts too, keyed by the SUBAGENT's conversation_id - see subagentStop + the marker fold in post-tool-use.sh/final-review.sh."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"command": "bash ~/.agents/hooks/minimal-edit-audit.sh",
|
|
20
|
+
"timeout": 15,
|
|
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."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"command": "bash ~/.agents/hooks/anti-slop-audit.sh",
|
|
26
|
+
"timeout": 15,
|
|
27
|
+
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
28
|
+
"_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."
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"postToolUse": [
|
|
32
|
+
{
|
|
33
|
+
"command": "bash ~/.agents/hooks/post-tool-use.sh",
|
|
34
|
+
"timeout": 5,
|
|
35
|
+
"_comment": "5s: (1) fold completed subagents' session-edits markers into this conversation's marker - subagent edits fire afterFileEdit under the SUBAGENT's conversation_id, and postToolUse does NOT fire for the Task tool (verified by payload logging), so per-tool-boundary folding is how delegated edits reach the parent's stop-hook review - and prime the parent to audit the subagent diff; (2) drain this conversation's pending-feedback file (self-review + advisories) into additional_context. One-shot. No matcher: fires after EVERY tool so pending is delivered promptly."
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"subagentStop": [
|
|
39
|
+
{
|
|
40
|
+
"command": "bash ~/.agents/hooks/subagent-stop-review.sh",
|
|
41
|
+
"timeout": 5,
|
|
42
|
+
"matcher": "^(generalPurpose|poteto-agent|best-of-n-runner|impeccable-manual-edit-applier)$",
|
|
43
|
+
"loop_limit": 3,
|
|
44
|
+
"_comment": "5s: ONE in-subagent final review per implementation before the result returns to the parent. Matcher = editing-capable subagent types only. Marker-gated like final-review.sh: afterFileEdit fires inside subagents keyed by the subagent's conversation_id, so session-edits-<subagent-cid>.txt exists exactly when the run edited files; reviewed-<cid>.flag is the per-implementation brake, loop_limit 3 is the harness-side runaway cap (1 would suppress the review after a resumed subagent's second implementation). Disable: HOOKS_ENFORCE=0 or SUBAGENT_REVIEW_ENFORCE=0."
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"beforeShellExecution": [
|
|
48
|
+
{
|
|
49
|
+
"command": "bash ~/.agents/hooks/permission-gate.sh",
|
|
50
|
+
"timeout": 5,
|
|
51
|
+
"failClosed": false,
|
|
52
|
+
"_comment": "5s: deny a small explicit list of dangerous commands. Default-allow, deny-by-list."
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"stop": [
|
|
56
|
+
{
|
|
57
|
+
"command": "bash ~/.agents/hooks/final-review.sh",
|
|
58
|
+
"timeout": 5,
|
|
59
|
+
"loop_limit": 5,
|
|
60
|
+
"_comment": "5s: ONE comprehensive end-of-implementation review across correctness + reliability + coverage + anti-slop. If the agent edited files this loop (session-edits-<conversation_id> marker), returns {followup_message} so Cursor auto-submits ONE review pass. Bounded by the script's per-conversation reviewed-flag (one review per implementation); loop_limit is the harness-side runaway cap. Disable: HOOKS_ENFORCE=0 or FINAL_REVIEW_ENFORCE=0."
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# inject-doctrine.sh - Cursor sessionStart injection (Linux).
|
|
3
|
+
#
|
|
4
|
+
# Emits {"additional_context": "<doctrine + USER-RULES>"} as compact,
|
|
5
|
+
# ASCII-escaped JSON (jq -a / python ensure_ascii), so multi-byte characters
|
|
6
|
+
# in the doctrine can never be mangled by encoding layers.
|
|
7
|
+
#
|
|
8
|
+
# Fail open: missing files or any error -> "{}" (valid, empty). Never block
|
|
9
|
+
# or crash session start.
|
|
10
|
+
|
|
11
|
+
set +e
|
|
12
|
+
. "$HOME/.agents/hooks/hook-common.sh" 2>/dev/null || {
|
|
13
|
+
cat >/dev/null; printf '{}'; exit 0; }
|
|
14
|
+
|
|
15
|
+
# Drain stdin (Cursor sends session metadata) so the pipe never blocks.
|
|
16
|
+
cat >/dev/null
|
|
17
|
+
|
|
18
|
+
context=""
|
|
19
|
+
for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md"; do
|
|
20
|
+
if [ -f "$p" ]; then
|
|
21
|
+
part="$(cat "$p")"
|
|
22
|
+
if [ -n "$context" ]; then context="$context
|
|
23
|
+
|
|
24
|
+
$part"; else context="$part"; fi
|
|
25
|
+
fi
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
if [ -z "$context" ]; then printf '{}'; exit 0; fi
|
|
29
|
+
|
|
30
|
+
emit_json additional_context "$context"
|
|
31
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cursordoctrine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin self-review hooks for Cursor — the model is the auditor. One command installs the doctrine, the hook pack, and the anti-slop skill.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cursordoctrine": "bin/cli.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"windows/",
|
|
15
|
+
"linux/",
|
|
16
|
+
"skills/",
|
|
17
|
+
"INSTALL.md"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/kleosr/cursordoctrine.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/kleosr/cursordoctrine#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/kleosr/cursordoctrine/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"cursor",
|
|
29
|
+
"cursor-hooks",
|
|
30
|
+
"hooks",
|
|
31
|
+
"agent",
|
|
32
|
+
"ai-agents",
|
|
33
|
+
"self-review",
|
|
34
|
+
"anti-slop",
|
|
35
|
+
"code-review",
|
|
36
|
+
"cli"
|
|
37
|
+
],
|
|
38
|
+
"author": "Mario Pulice (kleosr)",
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|