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,116 @@
1
+ # minimal-edit-audit.ps1 - afterFileEdit minimal-editing advisory (Cursor).
2
+ #
3
+ # Audits the just-edited file for over-editing:
4
+ # * line-count - git diff --numstat thresholds (any language). Native git,
5
+ # no bash (Git's MSYS bash mangles Windows paths / lacks PATH
6
+ # when spawned from pwsh, so we compute this directly).
7
+ # * token metrics - audit-metrics.py (token-Levenshtein + cognitive
8
+ # complexity), Python files only, via a resolved interpreter.
9
+ # On WARN/FAIL it APPENDS a short advisory to the shared pending-feedback file;
10
+ # post-tool-use.ps1 delivers it as additional_context on the next tool turn.
11
+ #
12
+ # Advisory only: never blocks, never writes persistent state. afterFileEdit
13
+ # output isn't consumed and a non-zero exit shows as "hook failed", so we
14
+ # ALWAYS exit 0. Self-contained.
15
+ #
16
+ # Thresholds (env-overridable): MINIMAL_EDIT_FAIL_LINES (400), MINIMAL_EDIT_WARN_LINES (100).
17
+ # Disable: HOOKS_ENFORCE=0 or MINIMAL_EDITING_ENFORCE=0
18
+
19
+ $ErrorActionPreference = 'SilentlyContinue'
20
+ . "$PSScriptRoot\hook-common.ps1"
21
+
22
+ if ($env:HOOKS_ENFORCE -eq '0' -or $env:MINIMAL_EDITING_ENFORCE -eq '0') { exit 0 }
23
+
24
+ $obj = Read-HookStdinJson
25
+ if (-not $obj) { exit 0 }
26
+
27
+ # audit root = project from JSON (cwd, then workspace_roots), else CURSOR_PROJECT_DIR / HOME
28
+ $root = ''
29
+ $cands = @()
30
+ if ($obj.PSObject.Properties['cwd'] -and $obj.cwd) { $cands += [string]$obj.cwd }
31
+ if ($obj.PSObject.Properties['workspace_roots']) { foreach ($w in $obj.workspace_roots) { $cands += [string]$w } }
32
+ foreach ($c in $cands) { $f = ConvertTo-FwdPath $c; if ($f -and (Test-Path -LiteralPath $f)) { $root = $f.TrimEnd('/'); break } }
33
+ if (-not $root) { $root = (& { if ($env:CURSOR_PROJECT_DIR) { $env:CURSOR_PROJECT_DIR } else { $HOME } }).Replace('\', '/').TrimEnd('/') }
34
+
35
+ # edited file -> repo-relative forward-slash path
36
+ $fp = ''
37
+ foreach ($k in 'file_path', 'path', 'filename', 'absolute_path', 'abs_path') {
38
+ if ($obj.PSObject.Properties[$k] -and $obj.$k) { $fp = [string]$obj.$k; break }
39
+ }
40
+ if (-not $fp) { exit 0 }
41
+ $rel = ConvertTo-FwdPath $fp
42
+ if ($rel.StartsWith($root + '/', [System.StringComparison]::OrdinalIgnoreCase)) { $rel = $rel.Substring($root.Length + 1) }
43
+ if (Test-IsCursorConfigPath $fp) { exit 0 }
44
+ if (Test-IsCursorConfigPath $rel) { exit 0 }
45
+
46
+ # git repo?
47
+ & git -C $root rev-parse --git-dir 2>$null | Out-Null
48
+ if ($LASTEXITCODE -ne 0) { exit 0 }
49
+
50
+ # --- line-count audit (any language) via native git ----------------------
51
+ $failLines = if ($env:MINIMAL_EDIT_FAIL_LINES) { [int]$env:MINIMAL_EDIT_FAIL_LINES } else { 400 }
52
+ $warnLines = if ($env:MINIMAL_EDIT_WARN_LINES) { [int]$env:MINIMAL_EDIT_WARN_LINES } else { 100 }
53
+ $ins = 0; $del = 0
54
+ foreach ($line in (& git -C $root diff HEAD --numstat -- $rel 2>$null)) {
55
+ $parts = $line -split "`t"
56
+ if ($parts.Count -lt 3 -or $parts[0] -eq '-') { continue } # skip header/binary
57
+ $ins += [int]$parts[0]; $del += [int]$parts[1]
58
+ }
59
+ $changed = $ins + $del
60
+
61
+ $grade = 'OK'; $hint = ''
62
+ if ($changed -gt $failLines) { $grade = 'FAIL'; $hint = "$changed lines changed (limit $failLines) - likely over-editing; trim or split" }
63
+ elseif ($changed -gt $warnLines) { $grade = 'WARN'; $hint = "$changed lines changed - justify each hunk or split the task" }
64
+
65
+ # --- token metrics (.py only) via a resolved interpreter -----------------
66
+ $auditMetrics = Join-Path $HOME '.cursor\skills\minimal-editing\scripts\audit-metrics.py'
67
+ if ((Test-Path $auditMetrics) -and ($rel -match '\.py$')) {
68
+ # Windows often ships the interpreter as `py` or `python3`, not `python`.
69
+ # Without this resolution the call errored silently and .py metrics never ran.
70
+ $py = Get-Command python, python3, py -ErrorAction SilentlyContinue | Select-Object -First 1
71
+ if ($py) {
72
+ $mout = & $py.Source $auditMetrics --root $root --format json --path $rel 2>$null
73
+ $mo = $null; try { $mo = ($mout -join "`n") | ConvertFrom-Json } catch { }
74
+ if ($mo -and $mo.grade) {
75
+ if ($mo.grade -eq 'FAIL') { $grade = 'FAIL' }
76
+ elseif ($mo.grade -eq 'WARN' -and $grade -eq 'OK') { $grade = 'WARN' }
77
+ }
78
+ }
79
+ }
80
+
81
+ if ($grade -eq 'OK') { exit 0 }
82
+
83
+ # --- compose advisory + append to the shared pending file ----------------
84
+ $hintTxt = if ($hint) { " - $hint" } else { '' }
85
+ if ($grade -eq 'FAIL') {
86
+ $actions = @"
87
+ - Trim every hunk that isn't required by the task.
88
+ - Prefer narrow, targeted edits over rewriting blocks.
89
+ - If the change is genuinely large, split it into smaller logical commits.
90
+ "@
91
+ } else {
92
+ $actions = ' Advisory only - trim unrelated hunks if any; otherwise proceed.'
93
+ }
94
+
95
+ $msg = @"
96
+ Minimal-edit audit $grade - $rel
97
+
98
+ IMPORTANT: Try to preserve the original code and the logic of the original code as much as possible.
99
+
100
+ grade: $grade$hintTxt
101
+
102
+ $actions
103
+
104
+ (Disable for this session: HOOKS_ENFORCE=0)
105
+ "@
106
+
107
+ $cid = Get-SafeConversationId $obj
108
+ $pending = Join-Path (Get-HooksPendingDir) "feedback-$cid.txt"
109
+ try {
110
+ New-Item -ItemType Directory -Path (Split-Path $pending) -Force | Out-Null
111
+ $prefix = ''
112
+ if ((Test-Path $pending) -and ((Get-Item $pending).Length -gt 0)) { $prefix = "`n`n---`n`n" }
113
+ Add-Content -Path $pending -Value ($prefix + $msg) -NoNewline
114
+ } catch { }
115
+
116
+ exit 0
@@ -0,0 +1,98 @@
1
+ # permission-gate.ps1 - beforeShellExecution for Cursor.
2
+ #
3
+ # Single responsibility: deny a small, explicit list of dangerous commands.
4
+ # This is a *permission* gate, not a *quality* gate. The model handles
5
+ # quality; the harness handles blast radius.
6
+ #
7
+ # Behavior:
8
+ # - Exit 0 always.
9
+ # - Print Cursor-canonical {"permission": "allow"|"deny", ...} JSON.
10
+ # - On internal failure: fail OPEN (allow), never block the user.
11
+ #
12
+ # Disable: PERM_GATE_ENFORCE=0
13
+
14
+ $ErrorActionPreference = 'SilentlyContinue'
15
+ . "$PSScriptRoot\hook-common.ps1"
16
+
17
+ if ($env:PERM_GATE_ENFORCE -eq '0') {
18
+ Write-HookJson @{ permission = 'allow' }
19
+ exit 0
20
+ }
21
+
22
+ # Without BOM-safe decode the JSON never parses, the raw-text fallback below
23
+ # matches deny patterns anywhere in the envelope (false positives), and the
24
+ # deny message leaks conversation id / transcript path / user email into the UI.
25
+ $inputText = Read-HookStdin
26
+
27
+ $cmd = ''
28
+ if ($inputText) {
29
+ try {
30
+ $obj = $inputText | ConvertFrom-Json
31
+ if ($obj -and $obj.PSObject.Properties['command']) {
32
+ $cmd = [string]$obj.command
33
+ }
34
+ } catch {
35
+ $cmd = ''
36
+ }
37
+ # Belt-and-braces: if stdin was not the documented JSON shape, still gate
38
+ # on the raw text rather than waving everything through.
39
+ if (-not $cmd) { $cmd = $inputText }
40
+ }
41
+
42
+ if (-not $cmd) {
43
+ Write-HookJson @{ permission = 'allow' }
44
+ exit 0
45
+ }
46
+
47
+ function Test-Deny {
48
+ param([string]$Pattern, [string]$Reason)
49
+ if ($cmd -match $Pattern) { Deny $Reason }
50
+ }
51
+
52
+ function Deny {
53
+ param([string]$Reason)
54
+ # Truncate the echo: the command can be a multi-hundred-char one-liner and
55
+ # the UI message only needs enough to identify it.
56
+ $shown = if ($cmd.Length -gt 400) { $cmd.Substring(0, 400) + '...' } else { $cmd }
57
+ $userMsg = "BLOCKED by permission-gate: $Reason`n`nCommand: $shown`n`nIf this is genuinely intended, run it yourself in your terminal."
58
+ Write-HookJson @{
59
+ permission = 'deny'
60
+ user_message = $userMsg
61
+ agent_message = "$userMsg Do not retry verbatim. Ask the user to run it manually if it is truly intended."
62
+ }
63
+ exit 0
64
+ }
65
+
66
+ # --- POSIX-flavored ---------------------------------------------------------
67
+ # Anchored to start OR a command separator so `cd /tmp && rm -rf /` is caught,
68
+ # while `git rm`, `npm run rm-cache`, `echo "rm -rf /"` stay allowed.
69
+ Test-Deny '(?:^|[;&|]\s*)(?:sudo\s+)?rm\s+-[a-zA-Z]*([rR][fF]|[fF][rR])[a-zA-Z]*\s+/' 'destructive rm -rf on absolute path (use relative paths or be more specific)'
70
+ Test-Deny ':\(\)\{\s*:\|:&\s*\};:|bash\s+-c\s+["'']*:\s*\(\)\{' 'reverse shell / fork-bomb pattern'
71
+ Test-Deny 'curl\s.*\|\s*(sudo\s*)?(bash|sh|zsh|dash|ash)' 'curl piped to shell'
72
+ Test-Deny 'wget\s.*\|\s*(sudo\s*)?(bash|sh|zsh|dash|ash)' 'wget piped to shell'
73
+ Test-Deny 'git\s+push\s+.*--force(-with-lease)?(\s|$)' 'git push --force'
74
+ Test-Deny 'git\s+push\s+(-f|--force)(\s|$)' 'git push -f / --force'
75
+ Test-Deny 'git\s+reset\s+--hard' 'git reset --hard (data loss)'
76
+ Test-Deny 'git\s+clean\s+-[a-zA-Z]*f' 'git clean -f (untracked data loss)'
77
+ Test-Deny 'dd\s.*of=/dev/(sd|nvme|hd|xvd)' 'dd to block device'
78
+ Test-Deny 'mkfs(\.[a-z0-9]+)?\s+/dev/' 'mkfs on device'
79
+ Test-Deny 'chmod\s+-R\s+777\s+/' 'chmod -R 777 on root'
80
+ Test-Deny 'chown\s+-R\s+[^\s]+\s+/' 'chown -R on root'
81
+ Test-Deny '^(npm|pnpm|yarn)\s+publish(\s|$)' 'package publish (use ship-hook, not direct publish)'
82
+
83
+ # --- Windows equivalents (the agent shell here IS PowerShell) ---------------
84
+ # iwr/irm | iex is the moral twin of curl|sh.
85
+ Test-Deny '\b(iwr|irm|curl|wget|Invoke-WebRequest|Invoke-RestMethod)\b[^|]*\|\s*(iex\b|Invoke-Expression)' 'web download piped to Invoke-Expression'
86
+ # Disk-level destruction, twin of mkfs / dd-to-device.
87
+ Test-Deny '\b(Format-Volume|Clear-Disk)\b' 'disk format / clear (destructive)'
88
+ # Recursive+forced delete of a bare drive root, user-profile root, or
89
+ # C:\Users / C:\Windows. Twin of rm -rf /. Composed checks instead of one
90
+ # unreadable regex; subfolder deletes (e.g. C:\Temp\x) stay allowed.
91
+ $rmVerb = '(?:^|[;&|]\s*)(?:Remove-Item|rm|ri|del|erase|rd|rmdir)\s'
92
+ $rootPath = '(?:^|[\s"''])(?:[A-Za-z]:[\\/]{0,2}|[A-Za-z]:[\\/](?:Users|Windows)[\\/]?|\$(?:env:USERPROFILE|HOME)[\\/]?)["'']?\s*(?:$|[;&|-])'
93
+ if (($cmd -match $rmVerb) -and ($cmd -match $rootPath) -and ($cmd -match '(?:-Recurse\b|/s\b)') -and ($cmd -match '(?:-Force\b|/q\b)')) {
94
+ Deny 'recursive forced delete of a drive root / Users / Windows / profile root'
95
+ }
96
+
97
+ Write-HookJson @{ permission = 'allow' }
98
+ exit 0
@@ -0,0 +1,46 @@
1
+ # post-tool-use.ps1 - postToolUse for Cursor.
2
+ #
3
+ # Two responsibilities, both message-bus work, keyed by conversation_id so
4
+ # concurrent sessions never receive each other's prompts:
5
+ #
6
+ # 1. Fold completed subagents' session-edits markers into this
7
+ # conversation's marker (postToolUse does NOT fire for the Task tool -
8
+ # verified - so this per-tool-boundary fold is how delegated edits reach
9
+ # the parent's stop-hook final review). When a fold happens, prime the
10
+ # parent to audit the subagent's diff now.
11
+ # 2. Drain this conversation's stashed self-review / advisory messages into
12
+ # Cursor's additional_context channel. One-shot delivery.
13
+ #
14
+ # We do not parse, score, or filter. We do not run any audit. We do not
15
+ # block. The model that already produced the edit will, on its next
16
+ # turn, do the self-review.
17
+
18
+ $ErrorActionPreference = 'SilentlyContinue'
19
+ . "$PSScriptRoot\hook-common.ps1"
20
+
21
+ $obj = Read-HookStdinJson
22
+ $cid = Get-SafeConversationId $obj
23
+
24
+ $foldNote = ''
25
+ if (Merge-SubagentEditMarkers $obj $cid) {
26
+ $foldNote = "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."
27
+ }
28
+
29
+ $pendingFile = Join-Path (Get-HooksPendingDir) "feedback-$cid.txt"
30
+
31
+ $msg = ''
32
+ if (Test-Path $pendingFile) {
33
+ if ((Get-Item $pendingFile).Length -gt 0) {
34
+ $msg = Get-Content $pendingFile -Raw
35
+ }
36
+ # One-shot: clear before emitting so a hook error doesn't replay forever.
37
+ Remove-Item $pendingFile -Force -ErrorAction SilentlyContinue
38
+ }
39
+
40
+ if ($foldNote) {
41
+ if ($msg) { $msg = "$foldNote`n`n---`n`n$msg" } else { $msg = $foldNote }
42
+ }
43
+ if (-not $msg) { exit 0 }
44
+
45
+ Write-HookJson @{ additional_context = $msg }
46
+ exit 0
@@ -0,0 +1,83 @@
1
+ # self-review-trigger.ps1 - afterFileEdit for Cursor.
2
+ #
3
+ # Single responsibility: when the model just edited a file, hand the
4
+ # edit context to the NEXT model turn as additional_context. The model
5
+ # is the auditor; the harness is just the message bus.
6
+ #
7
+ # This is intentionally minimal:
8
+ # - We do NOT parse the diff ourselves.
9
+ # - We do NOT spawn a sub-agent.
10
+ # - We do NOT write to .stuck-files/.
11
+ # - We do NOT block.
12
+ #
13
+ # We DO:
14
+ # - Capture the edited file path.
15
+ # - Stash a self-review prompt that primes the model's next turn.
16
+ # - Exit 0 always.
17
+ #
18
+ # Cursor's afterFileEdit doesn't consume its own output. To actually
19
+ # surface the message, post-tool-use.ps1 re-emits it on the next tool
20
+ # boundary. See hooks.json.
21
+
22
+ $ErrorActionPreference = 'SilentlyContinue'
23
+ . "$PSScriptRoot\hook-common.ps1"
24
+
25
+ $inputText = Read-HookStdin
26
+
27
+ $filePath = ''
28
+ $cid = ''
29
+ if ($inputText) {
30
+ try {
31
+ $obj = $inputText | ConvertFrom-Json
32
+ if ($obj) {
33
+ if ($obj.PSObject.Properties['file_path']) { $filePath = [string]$obj.file_path }
34
+ elseif ($obj.PSObject.Properties['path']) { $filePath = [string]$obj.path }
35
+ elseif ($obj.PSObject.Properties['filePath']) { $filePath = [string]$obj.filePath }
36
+ $cid = Get-SafeConversationId $obj
37
+ }
38
+ } catch {
39
+ $filePath = ''
40
+ }
41
+ }
42
+
43
+ # Empty path (JSON parse failed, or no file_path field) -> nothing to record.
44
+ # Without this guard the .cursor regex below doesn't match '' and we append a
45
+ # blank line to the session-edits marker on every such fire (it accumulates fast).
46
+ if (-not $filePath) { exit 0 }
47
+ if (Test-IsCursorConfigPath $filePath) { exit 0 }
48
+
49
+ # State is keyed by conversation_id and lives under $HOME, never the project:
50
+ # no repo litter, works in workspace-less sessions (CURSOR_PROJECT_DIR/
51
+ # workspace_roots are empty there), and concurrent sessions cannot drain each
52
+ # other's prompts.
53
+ $pendingDir = Get-HooksPendingDir
54
+
55
+ # Record this edit for the end-of-implementation review. The stop hook
56
+ # (final-review.ps1) drains this marker to fire one final review pass over
57
+ # everything changed this agent loop. Append = running list of edits.
58
+ try {
59
+ $mk = Join-Path $pendingDir "session-edits-$cid.txt"
60
+ New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
61
+ Add-Content -Path $mk -Value $filePath
62
+ } catch { }
63
+
64
+ $doctrineFile = Join-Path $HOME '.agents\hooks\self-review.md'
65
+ if (-not (Test-Path $doctrineFile)) { exit 0 }
66
+ $doctrine = Get-Content $doctrineFile -Raw
67
+
68
+ $msg = "SELF-REVIEW TRIGGER - you just edited: $filePath`n`n$doctrine"
69
+
70
+ $pendingFile = Join-Path $pendingDir "feedback-$cid.txt"
71
+
72
+ try {
73
+ New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
74
+ $prefix = ''
75
+ if ((Test-Path $pendingFile) -and ((Get-Item $pendingFile).Length -gt 0)) {
76
+ $prefix = "`n`n---`n`n"
77
+ }
78
+ Add-Content -Path $pendingFile -Value ($prefix + $msg) -NoNewline
79
+ } catch {
80
+ # Silently fail open
81
+ }
82
+
83
+ 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,89 @@
1
+ # subagent-stop-review.ps1 - subagentStop for Cursor.
2
+ #
3
+ # Counterpart of final-review.ps1 for delegated work. afterFileEdit DOES fire
4
+ # inside subagents (verified: a poteto subagent run left ~58 entries in
5
+ # session-edits-<subagent-cid>.txt), but subagents get no `stop` event, so
6
+ # that marker is never drained and the four-axis review never fires for
7
+ # delegated implementations. This hook closes the loop: when a subagent
8
+ # finishes and ITS conversation has a session-edits marker, return ONE
9
+ # followup_message so the subagent audits its own implementation before the
10
+ # result goes back to the parent.
11
+ #
12
+ # Same bounding pattern as final-review.ps1:
13
+ # - marker-gated: no edits in the subagent run -> no review, no noise,
14
+ # - reviewed-<cid>.flag one-shot brake: the stop AFTER the review pass
15
+ # clears flag + marker and ends the loop (one review per implementation;
16
+ # resumed subagents with a second implementation get a second review),
17
+ # - loop_limit in hooks.json caps runaway follow-ups harness-side,
18
+ # - only on status == 'completed' when a status field is present.
19
+ #
20
+ # If subagentStop's stdin carries a conversation_id that doesn't match the
21
+ # id afterFileEdit used, the marker lookup misses and this emits {} - the
22
+ # marker fold in post-tool-use.ps1 / final-review.ps1 still routes the
23
+ # subagent's edits into the parent's stop review as the backstop.
24
+ #
25
+ # Always emits valid JSON ({} = no follow-up). Review body reuses
26
+ # final-review.md (embedded fallback if missing).
27
+ # Disable: HOOKS_ENFORCE=0 or SUBAGENT_REVIEW_ENFORCE=0.
28
+
29
+ $ErrorActionPreference = 'SilentlyContinue'
30
+ . "$PSScriptRoot\hook-common.ps1"
31
+
32
+ function Emit-None { '{}'; exit 0 }
33
+
34
+ if ($env:HOOKS_ENFORCE -eq '0' -or $env:SUBAGENT_REVIEW_ENFORCE -eq '0') { Emit-None }
35
+
36
+ $obj = Read-HookStdinJson
37
+ if (-not $obj) { Emit-None }
38
+
39
+ $status = ''
40
+ if ($obj.PSObject.Properties['status']) { $status = [string]$obj.status }
41
+ $cid = Get-SafeConversationId $obj
42
+
43
+ $pendingDir = Get-HooksPendingDir
44
+ $marker = Join-Path $pendingDir "session-edits-$cid.txt"
45
+ $flag = Join-Path $pendingDir "reviewed-$cid.flag"
46
+
47
+ # One-shot brake: the previous subagentStop for this id emitted the review.
48
+ if (Test-Path $flag) {
49
+ Remove-Item $flag, $marker -Force -ErrorAction SilentlyContinue
50
+ Emit-None
51
+ }
52
+
53
+ # Review only a clean completion; otherwise clear the marker and stop.
54
+ if ($status -and $status -ne 'completed') {
55
+ Remove-Item $marker -Force -ErrorAction SilentlyContinue
56
+ Emit-None
57
+ }
58
+
59
+ # No edits this run -> nothing to review.
60
+ if (-not (Test-Path $marker)) { Emit-None }
61
+ $edited = @(Get-Content $marker -ErrorAction SilentlyContinue |
62
+ Where-Object { $_ -and $_.Trim() } | Select-Object -Unique)
63
+ Remove-Item $marker -Force -ErrorAction SilentlyContinue
64
+ if ($edited.Count -eq 0) { Emit-None }
65
+
66
+ $body = ''
67
+ $promptFile = Join-Path $HOME '.agents\hooks\final-review.md'
68
+ if (Test-Path $promptFile) { $body = Get-Content -Raw $promptFile }
69
+ if (-not $body) {
70
+ $body = @'
71
+ 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
+ '@
80
+ }
81
+
82
+ $fileList = ($edited | Select-Object -First 30) -join "`n "
83
+ $msg = "SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.`n`nFiles you changed this run:`n $fileList`n`n$body"
84
+
85
+ # Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
86
+ New-Item -ItemType File -Path $flag -Force -ErrorAction SilentlyContinue | Out-Null
87
+
88
+ Write-HookJson @{ followup_message = $msg }
89
+ exit 0
@@ -0,0 +1,64 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "sessionStart": [
5
+ {
6
+ "command": "pwsh.exe -NoProfile -File ~/.cursor/inject-doctrine.ps1",
7
+ "timeout": 5,
8
+ "_comment": "5s: inject the agent doctrine + user rules at session start. inject-doctrine.ps1 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": "pwsh.exe -NoProfile -File ~/.agents/hooks/self-review-trigger.ps1",
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.ps1/final-review.ps1."
17
+ },
18
+ {
19
+ "command": "pwsh.exe -NoProfile -File ~/.agents/hooks/minimal-edit-audit.ps1",
20
+ "timeout": 15,
21
+ "matcher": "^(Write|StrReplace|EditNotebook)$",
22
+ "_comment": "15s: minimal-editing advisory on the edited file (native git --numstat line-count + audit-metrics.py token metrics on .py, from ~/.cursor/skills/minimal-editing/). Appends findings to the conversation's pending file; never blocks. Disable: HOOKS_ENFORCE=0 or MINIMAL_EDITING_ENFORCE=0."
23
+ },
24
+ {
25
+ "command": "pwsh.exe -NoProfile -File ~/.agents/hooks/anti-slop-audit.ps1",
26
+ "timeout": 15,
27
+ "matcher": "^(Write|StrReplace|EditNotebook)$",
28
+ "_comment": "15s: AI-slop advisory, companion to minimal-edit-audit. Native 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": "pwsh.exe -NoProfile -File ~/.agents/hooks/post-tool-use.ps1",
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": "pwsh.exe -NoProfile -File ~/.agents/hooks/subagent-stop-review.ps1",
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.ps1: 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": "pwsh.exe -NoProfile -File ~/.agents/hooks/permission-gate.ps1",
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": "pwsh.exe -NoProfile -File ~/.agents/hooks/final-review.ps1",
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 counts follow-ups per CONVERSATION (docs), so 5 = harness-side runaway cap, not the per-implementation bound (2 would silently kill reviews after the 2nd implementation in a long chat). Disable: HOOKS_ENFORCE=0 or FINAL_REVIEW_ENFORCE=0."
61
+ }
62
+ ]
63
+ }
64
+ }
@@ -0,0 +1,58 @@
1
+ # inject-doctrine.ps1 - Cursor sessionStart injection.
2
+ #
3
+ # Emits {"additional_context": "<doctrine + USER-RULES>"} as PURE-ASCII JSON.
4
+ #
5
+ # Why pure ASCII: the doctrine contains multi-byte UTF-8 characters (em dash,
6
+ # section sign, <=, arrows). Written as UTF-8, their continuation bytes
7
+ # (0x80-0x9F) get decoded by Cursor's JSON reader as C1 control characters ->
8
+ # "Bad control character in string literal in JSON at position N". Escaping every
9
+ # non-ASCII char to \uXXXX makes the output byte-identical under EVERY encoding,
10
+ # so it cannot be mangled; JSON.parse turns § back into the real char. We
11
+ # also write the bytes straight to stdout to bypass [Console]::OutputEncoding.
12
+ #
13
+ # Fail open: missing files or any error -> "{}" (valid, empty). Never block or
14
+ # crash session start.
15
+
16
+ $ErrorActionPreference = 'SilentlyContinue'
17
+
18
+ # Drain stdin (Cursor sends session metadata) so the pipe never blocks.
19
+ $null = [Console]::In.ReadToEnd()
20
+
21
+ function Write-StdoutAscii([string]$s) {
22
+ # Write exact ASCII bytes to stdout, immune to whatever [Console]::OutputEncoding is.
23
+ $bytes = [System.Text.Encoding]::ASCII.GetBytes($s)
24
+ $stdout = [Console]::OpenStandardOutput()
25
+ $stdout.Write($bytes, 0, $bytes.Length)
26
+ $stdout.Flush()
27
+ }
28
+
29
+ try {
30
+ $paths = @(
31
+ (Join-Path $PSScriptRoot 'doctrine.md'),
32
+ (Join-Path $PSScriptRoot 'USER-RULES.md')
33
+ )
34
+
35
+ $parts = foreach ($p in $paths) {
36
+ if (Test-Path -LiteralPath $p) { Get-Content -Raw -LiteralPath $p }
37
+ }
38
+ $context = ($parts -join "`n`n").Trim()
39
+
40
+ if (-not $context) { Write-StdoutAscii '{}'; exit 0 }
41
+
42
+ $json = @{ additional_context = $context } | ConvertTo-Json -Compress
43
+
44
+ # Escape every non-ASCII (and any stray control) char to \uXXXX -> pure ASCII.
45
+ # ConvertTo-Json's structural chars and \n / \" escapes are ASCII and pass through.
46
+ $sb = [System.Text.StringBuilder]::new($json.Length + 64)
47
+ foreach ($ch in $json.ToCharArray()) {
48
+ $code = [int][char]$ch
49
+ if ($code -lt 32 -or $code -gt 126) { [void]$sb.AppendFormat('\u{0:x4}', $code) }
50
+ else { [void]$sb.Append($ch) }
51
+ }
52
+ Write-StdoutAscii $sb.ToString()
53
+ }
54
+ catch {
55
+ Write-StdoutAscii '{}'
56
+ }
57
+
58
+ exit 0