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.
Files changed (37) hide show
  1. package/INSTALL.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +86 -0
  4. package/bin/cli.mjs +413 -0
  5. package/linux/USER-RULES.md +12 -0
  6. package/linux/doctrine.md +172 -0
  7. package/linux/hooks/anti-slop-audit.sh +163 -0
  8. package/linux/hooks/anti-slop.md +56 -0
  9. package/linux/hooks/final-review.md +52 -0
  10. package/linux/hooks/final-review.sh +99 -0
  11. package/linux/hooks/hook-common.sh +120 -0
  12. package/linux/hooks/minimal-edit-audit.sh +112 -0
  13. package/linux/hooks/permission-gate.sh +75 -0
  14. package/linux/hooks/post-tool-use.sh +53 -0
  15. package/linux/hooks/self-review-trigger.sh +56 -0
  16. package/linux/hooks/self-review.md +48 -0
  17. package/linux/hooks/subagent-stop-review.sh +93 -0
  18. package/linux/hooks.json +64 -0
  19. package/linux/inject-doctrine.sh +31 -0
  20. package/package.json +40 -0
  21. package/skills/anti-slop/SKILL.md +267 -0
  22. package/skills/anti-slop/scripts/scan_slop.py +986 -0
  23. package/windows/USER-RULES.md +12 -0
  24. package/windows/doctrine.md +172 -0
  25. package/windows/hooks/anti-slop-audit.ps1 +182 -0
  26. package/windows/hooks/anti-slop.md +56 -0
  27. package/windows/hooks/final-review.md +52 -0
  28. package/windows/hooks/final-review.ps1 +105 -0
  29. package/windows/hooks/hook-common.ps1 +84 -0
  30. package/windows/hooks/minimal-edit-audit.ps1 +116 -0
  31. package/windows/hooks/permission-gate.ps1 +98 -0
  32. package/windows/hooks/post-tool-use.ps1 +46 -0
  33. package/windows/hooks/self-review-trigger.ps1 +83 -0
  34. package/windows/hooks/self-review.md +48 -0
  35. package/windows/hooks/subagent-stop-review.ps1 +89 -0
  36. package/windows/hooks.json +64 -0
  37. 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
@@ -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
+ }