cursordoctrine 0.3.0 → 0.3.2
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/README.md +64 -42
- package/bin/cli.mjs +1 -1
- package/linux/declared-editing.md +30 -0
- package/linux/hooks/final-review.md +22 -1
- package/linux/hooks/final-review.sh +8 -2
- package/linux/hooks/subagent-stop-review.sh +103 -103
- package/linux/inject-doctrine.sh +1 -1
- package/package.json +1 -1
- package/windows/declared-editing.md +30 -0
- package/windows/hooks/final-review.md +22 -1
- package/windows/hooks/final-review.ps1 +8 -2
- package/windows/inject-doctrine.ps1 +59 -58
package/README.md
CHANGED
|
@@ -1,65 +1,81 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/node-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white" />
|
|
3
|
+
<img src="https://img.shields.io/npm/v/cursordoctrine?style=flat-square&color=blue" />
|
|
4
|
+
<img src="https://img.shields.io/badge/license-MIT-brightgreen?style=flat-square" />
|
|
5
|
+
<img src="https://img.shields.io/badge/built%20for-Cursor-6c47ff?style=flat-square" />
|
|
6
|
+
</div>
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
<br />
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
<h1>cursordoctrine</h1>
|
|
12
|
+
<p><strong>Thin self-review hooks for Cursor.</strong></p>
|
|
13
|
+
<p>Five hook events, one message bus.<br />The model audits its own work. Cursor carries context and gates blast radius.</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<br />
|
|
17
|
+
|
|
18
|
+
---
|
|
4
19
|
|
|
5
20
|
## What this is
|
|
6
21
|
|
|
7
|
-
|
|
22
|
+
Cursor hooks that make the agent review its own edits without bolting a static-analysis pipeline onto every keystroke. No regex army, no scoring engine. Three jobs:
|
|
8
23
|
|
|
9
|
-
1. **Inject the doctrine** at session start
|
|
10
|
-
2. **Hand the model its own edits back
|
|
11
|
-
3. **Gate blast radius
|
|
24
|
+
1. **Inject the doctrine** at session start — every chat starts with the same short governing text (`doctrine.md`, `USER-RULES.md`, and `declared-editing.md`, the YAGNI ultra ladder that stops over-building before a line gets written).
|
|
25
|
+
2. **Hand the model its own edits back** — after each agent edit, a self-review prompt goes into a pending file (plus minimal-edit, semantic-density, and anti-slop advisories when they trip). Next turn the model reads its diff, fixes real bugs, stays quiet otherwise.
|
|
26
|
+
3. **Gate blast radius** — one permission gate denies a short explicit list of dangerous commands (`rm -rf /`, `curl | sh`, force-push, `npm publish`, ...). Everything else passes.
|
|
12
27
|
|
|
13
|
-
When an implementation finishes,
|
|
28
|
+
When an implementation finishes, the stop hook runs one final review over everything that changed, then stops. Five axes. The first is **intent trace**: the hook pulls your last user message from the transcript and prepends it to the review so the model has to tie every diff hunk to a concrete request. Anything it can't trace is a hallucinated requirement and gets reverted. That's the only check that catches "clean code, wrong feature" — linters and later axes miss it.
|
|
14
29
|
|
|
15
|
-
|
|
30
|
+
Subagents get the same treatment. If a delegated run edited files, it reviews its own work before the result goes back to the parent. Those edits fold into the parent's final review. Every bound is enforced twice: in the script and in `hooks.json`.
|
|
16
31
|
|
|
17
|
-
|
|
32
|
+
Cursor only. Installs into `~/.cursor` and `~/.agents/hooks`. Doesn't touch your projects.
|
|
18
33
|
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Node 18+:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx cursordoctrine@latest install # copies the hook pack into ~/.agents/hooks + ~/.cursor, merges hooks.json
|
|
40
|
+
npx cursordoctrine verify # smoke-tests every hook with fake payloads, no restart needed
|
|
19
41
|
```
|
|
20
|
-
windows/ PowerShell hooks (pwsh) — install on Windows machines
|
|
21
|
-
hooks.json hook wiring for ~/.cursor/hooks.json
|
|
22
|
-
inject-doctrine.ps1, doctrine.md, USER-RULES.md
|
|
23
|
-
hooks/ the eight scripts + the three prompt files
|
|
24
|
-
linux/ bash hooks — install on Linux machines and SSH remotes
|
|
25
|
-
hooks.json, inject-doctrine.sh, doctrine.md, USER-RULES.md
|
|
26
|
-
hooks/ same hooks, ported to bash (jq preferred, python3 fallback)
|
|
27
|
-
skills/ Cursor agent skills shipped with the package
|
|
28
|
-
anti-slop/ SKILL.md + the duplication scanner (final review runs it)
|
|
29
|
-
bin/ the npm CLI (npx cursordoctrine install / verify / uninstall)
|
|
30
|
-
INSTALL.md a ready-to-paste prompt that tells a Cursor agent to
|
|
31
|
-
install the right folder and verify every hook
|
|
32
|
-
assets/ the architecture diagram above
|
|
33
|
-
```
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
Restart Cursor after install — `hooks.json` is read at startup. `install` is idempotent; re-run to update. Entries you added to `~/.cursor/hooks.json` yourself are kept. `npx cursordoctrine uninstall` removes the pack the same way.
|
|
44
|
+
|
|
45
|
+
No Node? Open `INSTALL.md`, paste it into a Cursor agent chat on the target machine, and let the agent copy files and run the checklist. Copy commands are in the same file if you prefer doing it by hand.
|
|
46
|
+
|
|
47
|
+
Prerequisites: `git` everywhere; `pwsh` on Windows; `bash` plus `jq` or `python3` on Linux.
|
|
48
|
+
|
|
49
|
+
The anti-slop skill (`skills/anti-slop/` — SKILL.md and the duplication scanner) installs to `~/.cursor/skills/anti-slop/`. The hook checklist (`~/.agents/hooks/anti-slop.md`, 13 items) is the canonical slop detector for per-edit advisories and final-review axis 4. Final review runs the scanner from the skill path first when it's there.
|
|
36
50
|
|
|
37
51
|
## The five flows
|
|
38
52
|
|
|
39
53
|
| Flow | Event | What happens |
|
|
40
54
|
|---|---|---|
|
|
41
|
-
| Session | `sessionStart` | `inject-doctrine` reads
|
|
55
|
+
| Session | `sessionStart` | `inject-doctrine` reads doctrine + user rules + declared-editing and emits them as `additional_context`. |
|
|
42
56
|
| Every turn | `postToolUse` | Folds completed subagents' edit markers into this conversation's marker, then drains the conversation's pending feedback file into `additional_context`. One-shot, keyed by conversation id. |
|
|
43
57
|
| Shell | `beforeShellExecution` | `permission-gate` checks the command against a deny list. Allow by default, deny by list, fail open. |
|
|
44
58
|
| Edit | `afterFileEdit` + `stop` | `self-review-trigger` stashes the review prompt per edit; `minimal-edit-audit` (deprecated in 0.3.0), `semantic-density-audit`, and `anti-slop-audit` append advisories when thresholds trip (new deps / premature abstraction / redundant comments / **semantic opacity**: low-density identifiers like `DataManager`, `process()`, `utils.ts` / Tier 3 operational slop: retry-without-backoff, await-in-loop, telemetry spam); `final-review` fires one end-of-implementation pass. |
|
|
45
59
|
| Subagent | `subagentStop` | `subagent-stop-review` fires one in-subagent final review when a delegated run edited files, before the result returns to the parent. Marker-gated and flag-braked like `final-review`. |
|
|
46
60
|
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
The fast path is npm (Node 18+):
|
|
61
|
+
## Layout
|
|
50
62
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
```
|
|
64
|
+
windows/ PowerShell hooks (pwsh) — install on Windows machines
|
|
65
|
+
hooks.json hook wiring for ~/.cursor/hooks.json
|
|
66
|
+
inject-doctrine.ps1, doctrine.md, USER-RULES.md, declared-editing.md
|
|
67
|
+
hooks/ the eight scripts + the three prompt files
|
|
68
|
+
linux/ bash hooks — install on Linux machines and SSH remotes
|
|
69
|
+
hooks.json, inject-doctrine.sh, doctrine.md, USER-RULES.md, declared-editing.md
|
|
70
|
+
hooks/ same hooks, ported to bash (jq preferred, python3 fallback)
|
|
71
|
+
skills/ Cursor agent skills shipped with the package
|
|
72
|
+
anti-slop/ SKILL.md + the duplication scanner (final review runs it)
|
|
73
|
+
bin/ the npm CLI (npx cursordoctrine install / verify / uninstall)
|
|
74
|
+
INSTALL.md ready-to-paste prompt that tells a Cursor agent to
|
|
75
|
+
install the right folder and verify every hook
|
|
54
76
|
```
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
No Node? Open `INSTALL.md`, paste its contents into a Cursor agent chat on the target machine, and let the agent copy the files and run the verification checklist. Or do it by hand — the copy commands are in the same file.
|
|
59
|
-
|
|
60
|
-
Prerequisites: `git` everywhere; `pwsh` on Windows; `bash` plus `jq` or `python3` on Linux.
|
|
61
|
-
|
|
62
|
-
The anti-slop skill (`skills/anti-slop/` — SKILL.md and the duplication scanner) installs to `~/.cursor/skills/anti-slop/`. The hook checklist (`~/.agents/hooks/anti-slop.md`, 13 items) is the canonical slop detector for both per-edit advisories and final-review axis 4. The final review runs the scanner from the skill path first when available.
|
|
78
|
+
Both folders do the same thing. Windows runs everything through `pwsh.exe`. Linux runs bash, which is what you want on a remote over SSH (check your `~/.ssh/config` host — hooks live on the remote's `$HOME`, not your laptop).
|
|
63
79
|
|
|
64
80
|
## Tuning and kill switches
|
|
65
81
|
|
|
@@ -79,9 +95,15 @@ All hooks fail open and always exit 0. Nothing here can block your session.
|
|
|
79
95
|
|
|
80
96
|
## Design notes
|
|
81
97
|
|
|
82
|
-
- **State lives under `$HOME`**, in `~/.cursor/.hooks-pending/`, keyed by conversation id. No repo litter
|
|
83
|
-
- **`afterFileEdit` output isn't consumed by Cursor**, so
|
|
98
|
+
- **State lives under `$HOME`**, in `~/.cursor/.hooks-pending/`, keyed by conversation id. No repo litter. Concurrent sessions can't drain each other's prompts. Stale state older than 7 days gets swept on every stop.
|
|
99
|
+
- **`afterFileEdit` output isn't consumed by Cursor**, so edit hooks write to a pending file and `post-tool-use` re-emits it at the next tool boundary. That's the whole message bus.
|
|
84
100
|
- **One review per implementation.** The stop hook arms a per-conversation flag before emitting its follow-up, so a crash can't re-fire it and a long chat still gets a review after each implementation.
|
|
85
|
-
- **Subagents are first-class.** `afterFileEdit` fires inside subagents keyed by the
|
|
101
|
+
- **Subagents are first-class.** `afterFileEdit` fires inside subagents keyed by the subagent's conversation id. The harness normalizes agent edits (incl. `StrReplace`) to tool type `Write`, and `postToolUse` never fires for the `Task` tool — verified by payload capture. Matchers cover `Write|StrReplace|EditNotebook` defensively. `subagentStop` reviews the subagent in its own context. The parent folds orphaned subagent markers (from the `subagents/` transcript directory) into its own at every tool boundary and at stop.
|
|
102
|
+
|
|
103
|
+
Self-contained. No build. Open `hooks.json` and read it — that's the whole system in one file.
|
|
104
|
+
|
|
105
|
+
Built with [Cursor](https://cursor.com).
|
|
106
|
+
|
|
107
|
+
## License
|
|
86
108
|
|
|
87
|
-
|
|
109
|
+
MIT. See [LICENSE](LICENSE).
|
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'];
|
|
43
|
+
const doctrineFiles = [injectName, 'doctrine.md', 'USER-RULES.md', 'declared-editing.md'];
|
|
44
44
|
|
|
45
45
|
function payloadHookFiles() {
|
|
46
46
|
return readdirSync(join(payload, 'hooks'));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Declared-editing — YAGNI ultra
|
|
2
|
+
|
|
3
|
+
ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure.
|
|
4
|
+
|
|
5
|
+
Before writing any code, stop at the first rung that holds:
|
|
6
|
+
|
|
7
|
+
1. Does this need to exist at all? (YAGNI) If no — say so, don't build it.
|
|
8
|
+
2. Does the stdlib already do this? Use it.
|
|
9
|
+
3. Does a native platform feature cover it? Use it.
|
|
10
|
+
4. Does an already-installed dependency solve it? Use it.
|
|
11
|
+
5. Can this be one line? Make it one line.
|
|
12
|
+
6. Only then: write the minimum code that works.
|
|
13
|
+
|
|
14
|
+
Ultra means:
|
|
15
|
+
|
|
16
|
+
- Deletion before addition. If you can remove code to solve the problem, remove it.
|
|
17
|
+
- Ship the one-liner and challenge the rest of the requirement in the same breath.
|
|
18
|
+
- A hand-rolled abstraction is a bug farm with a hit rate. Say so.
|
|
19
|
+
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
|
20
|
+
|
|
21
|
+
Mark intentional simplifications with a `declared:` comment naming the ceiling
|
|
22
|
+
and the upgrade path: `// declared: O(n^2) scan, fine <10k rows; index at 50k`.
|
|
23
|
+
|
|
24
|
+
Not lazy about: input validation at trust boundaries, error handling that
|
|
25
|
+
prevents data loss, security, accessibility, anything explicitly requested.
|
|
26
|
+
Non-trivial logic leaves ONE runnable check behind (an assert or one small
|
|
27
|
+
test, no framework, no fixtures). Trivial one-liners need none.
|
|
28
|
+
|
|
29
|
+
Output format when you skipped building something:
|
|
30
|
+
-> skipped: [X], add when [Y]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
FINAL REVIEW — you just finished an implementation. Before you treat it as done,
|
|
2
|
-
audit EVERYTHING you changed this session across the
|
|
2
|
+
audit EVERYTHING you changed this session across the six axes below and FIX what
|
|
3
3
|
fails. Do NOT revert the behaviour the user asked for. If an axis is already
|
|
4
4
|
clean, say so in one line — do not manufacture work.
|
|
5
5
|
|
|
@@ -65,3 +65,24 @@ Step C — session footprint (also in the header above):
|
|
|
65
65
|
file or trim. Unjustified files are slop.
|
|
66
66
|
|
|
67
67
|
Fix with edits now; re-run the scan (if Step A ran) and the tests; then stop.
|
|
68
|
+
|
|
69
|
+
## 5. Wiring completeness
|
|
70
|
+
For every user-visible behavior you added or changed (button, form submit, API
|
|
71
|
+
call, route, state transition, scheduled job), trace its execution path end to
|
|
72
|
+
end and confirm it reaches a REAL EFFECT (persist, mutate, call, render, notify).
|
|
73
|
+
A dead end is slop even if the code is clean. Hunt for the vibe-coding failure
|
|
74
|
+
mode where a layer EXISTS but is not WIRED:
|
|
75
|
+
|
|
76
|
+
- `handleSubmit()` that does not persist / does not call the API.
|
|
77
|
+
- An endpoint that no route or caller invokes.
|
|
78
|
+
- A DB write / table that nothing reads or writes.
|
|
79
|
+
- A component that renders but is never mounted / routed to.
|
|
80
|
+
- A hook / store / context that is declared but never consumed.
|
|
81
|
+
- A `TODO` / empty body / stubbed `console.log` standing in for the effect.
|
|
82
|
+
|
|
83
|
+
The bar is: a senior can follow the path click -> handler -> call -> store ->
|
|
84
|
+
render (or the equivalent slice) without hitting a gap. If a step is missing or
|
|
85
|
+
faked, either wire it now or remove the dead half so the diff does not ship
|
|
86
|
+
scaffolding that looks complete but does nothing. Stubs you intend to wire later
|
|
87
|
+
must be marked with a `TODO(wire):` comment naming what is missing; unmarked
|
|
88
|
+
dead ends are failures.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# final-review.sh - stop hook (Cursor, Linux).
|
|
3
3
|
#
|
|
4
|
-
# ONE comprehensive end-of-implementation review across
|
|
5
|
-
# intent, correctness, reliability, coverage,
|
|
4
|
+
# ONE comprehensive end-of-implementation review across six axes:
|
|
5
|
+
# intent, correctness, reliability, coverage, anti-slop, and wiring completeness. When the agent finishes
|
|
6
6
|
# an implementation that touched files, Cursor auto-submits this hook's
|
|
7
7
|
# `followup_message` as the next user turn, so the model re-audits everything
|
|
8
8
|
# it changed this session and FIXES what fails.
|
|
@@ -81,6 +81,12 @@ if [ -z "$body" ]; then
|
|
|
81
81
|
run `python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all` first.
|
|
82
82
|
Consolidate clones; drop premature abstraction, unneeded deps, operational
|
|
83
83
|
slop (retries, await-in-loop, log spam), unjustified files.
|
|
84
|
+
5. Wiring completeness - for every user-visible behavior you added/changed
|
|
85
|
+
(button, submit, API call, route, state transition), trace its execution
|
|
86
|
+
path to a REAL EFFECT (persist, mutate, call, render). A dead end is slop:
|
|
87
|
+
handleSubmit that does not persist, an endpoint no caller invokes, a store
|
|
88
|
+
never consumed, a stub/TODO/console.log standing in for the effect. Wire it
|
|
89
|
+
now or remove the dead half; mark later-stubs with TODO(wire):.
|
|
84
90
|
Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one line.'
|
|
85
91
|
fi
|
|
86
92
|
body="$(expand_agent_paths "$body")"
|
|
@@ -1,103 +1,103 @@
|
|
|
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 five-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 - if ~/.cursor/skills/anti-slop/scripts/scan_slop.py exists, run
|
|
77
|
-
`python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all`; otherwise
|
|
78
|
-
apply ~/.agents/hooks/anti-slop.md to the session diff.
|
|
79
|
-
If an axis is clean, say so in one line. Then stop.'
|
|
80
|
-
fi
|
|
81
|
-
body="$(expand_agent_paths "$body")"
|
|
82
|
-
|
|
83
|
-
file_list=""
|
|
84
|
-
while IFS= read -r p; do
|
|
85
|
-
[ -n "$p" ] || continue
|
|
86
|
-
rp="$(resolve_agent_path "$p")"
|
|
87
|
-
file_list="${file_list} ${rp}"$'\n'
|
|
88
|
-
done <<EOF
|
|
89
|
-
$edited
|
|
90
|
-
EOF
|
|
91
|
-
file_list="$(printf '%s' "$file_list" | head -n 30)"
|
|
92
|
-
msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.
|
|
93
|
-
|
|
94
|
-
Files you changed this run:
|
|
95
|
-
$file_list
|
|
96
|
-
|
|
97
|
-
$body"
|
|
98
|
-
|
|
99
|
-
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
100
|
-
touch "$flag" 2>/dev/null
|
|
101
|
-
|
|
102
|
-
emit_json followup_message "$msg"
|
|
103
|
-
exit 0
|
|
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 five-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 - if ~/.cursor/skills/anti-slop/scripts/scan_slop.py exists, run
|
|
77
|
+
`python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all`; otherwise
|
|
78
|
+
apply ~/.agents/hooks/anti-slop.md to the session diff.
|
|
79
|
+
If an axis is clean, say so in one line. Then stop.'
|
|
80
|
+
fi
|
|
81
|
+
body="$(expand_agent_paths "$body")"
|
|
82
|
+
|
|
83
|
+
file_list=""
|
|
84
|
+
while IFS= read -r p; do
|
|
85
|
+
[ -n "$p" ] || continue
|
|
86
|
+
rp="$(resolve_agent_path "$p")"
|
|
87
|
+
file_list="${file_list} ${rp}"$'\n'
|
|
88
|
+
done <<EOF
|
|
89
|
+
$edited
|
|
90
|
+
EOF
|
|
91
|
+
file_list="$(printf '%s' "$file_list" | head -n 30)"
|
|
92
|
+
msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.
|
|
93
|
+
|
|
94
|
+
Files you changed this run:
|
|
95
|
+
$file_list
|
|
96
|
+
|
|
97
|
+
$body"
|
|
98
|
+
|
|
99
|
+
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
100
|
+
touch "$flag" 2>/dev/null
|
|
101
|
+
|
|
102
|
+
emit_json followup_message "$msg"
|
|
103
|
+
exit 0
|
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"; do
|
|
19
|
+
for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md" "$HOME/.cursor/declared-editing.md"; do
|
|
20
20
|
if [ -f "$p" ]; then
|
|
21
21
|
part="$(cat "$p")"
|
|
22
22
|
if [ -n "$context" ]; then context="$context
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Thin self-review hooks for Cursor — the model is the auditor. Intent-trace final review (Tier 0), unified 13-item anti-slop checklist, operational slop detection.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cursordoctrine": "bin/cli.mjs"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Declared-editing — YAGNI ultra
|
|
2
|
+
|
|
3
|
+
ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure.
|
|
4
|
+
|
|
5
|
+
Before writing any code, stop at the first rung that holds:
|
|
6
|
+
|
|
7
|
+
1. Does this need to exist at all? (YAGNI) If no — say so, don't build it.
|
|
8
|
+
2. Does the stdlib already do this? Use it.
|
|
9
|
+
3. Does a native platform feature cover it? Use it.
|
|
10
|
+
4. Does an already-installed dependency solve it? Use it.
|
|
11
|
+
5. Can this be one line? Make it one line.
|
|
12
|
+
6. Only then: write the minimum code that works.
|
|
13
|
+
|
|
14
|
+
Ultra means:
|
|
15
|
+
|
|
16
|
+
- Deletion before addition. If you can remove code to solve the problem, remove it.
|
|
17
|
+
- Ship the one-liner and challenge the rest of the requirement in the same breath.
|
|
18
|
+
- A hand-rolled abstraction is a bug farm with a hit rate. Say so.
|
|
19
|
+
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
|
20
|
+
|
|
21
|
+
Mark intentional simplifications with a `declared:` comment naming the ceiling
|
|
22
|
+
and the upgrade path: `// declared: O(n^2) scan, fine <10k rows; index at 50k`.
|
|
23
|
+
|
|
24
|
+
Not lazy about: input validation at trust boundaries, error handling that
|
|
25
|
+
prevents data loss, security, accessibility, anything explicitly requested.
|
|
26
|
+
Non-trivial logic leaves ONE runnable check behind (an assert or one small
|
|
27
|
+
test, no framework, no fixtures). Trivial one-liners need none.
|
|
28
|
+
|
|
29
|
+
Output format when you skipped building something:
|
|
30
|
+
-> skipped: [X], add when [Y]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
FINAL REVIEW — you just finished an implementation. Before you treat it as done,
|
|
2
|
-
audit EVERYTHING you changed this session across the
|
|
2
|
+
audit EVERYTHING you changed this session across the six axes below and FIX what
|
|
3
3
|
fails. Do NOT revert the behaviour the user asked for. If an axis is already
|
|
4
4
|
clean, say so in one line — do not manufacture work.
|
|
5
5
|
|
|
@@ -65,3 +65,24 @@ Step C — session footprint (also in the header above):
|
|
|
65
65
|
file or trim. Unjustified files are slop.
|
|
66
66
|
|
|
67
67
|
Fix with edits now; re-run the scan (if Step A ran) and the tests; then stop.
|
|
68
|
+
|
|
69
|
+
## 5. Wiring completeness
|
|
70
|
+
For every user-visible behavior you added or changed (button, form submit, API
|
|
71
|
+
call, route, state transition, scheduled job), trace its execution path end to
|
|
72
|
+
end and confirm it reaches a REAL EFFECT (persist, mutate, call, render, notify).
|
|
73
|
+
A dead end is slop even if the code is clean. Hunt for the vibe-coding failure
|
|
74
|
+
mode where a layer EXISTS but is not WIRED:
|
|
75
|
+
|
|
76
|
+
- `handleSubmit()` that does not persist / does not call the API.
|
|
77
|
+
- An endpoint that no route or caller invokes.
|
|
78
|
+
- A DB write / table that nothing reads or writes.
|
|
79
|
+
- A component that renders but is never mounted / routed to.
|
|
80
|
+
- A hook / store / context that is declared but never consumed.
|
|
81
|
+
- A `TODO` / empty body / stubbed `console.log` standing in for the effect.
|
|
82
|
+
|
|
83
|
+
The bar is: a senior can follow the path click -> handler -> call -> store ->
|
|
84
|
+
render (or the equivalent slice) without hitting a gap. If a step is missing or
|
|
85
|
+
faked, either wire it now or remove the dead half so the diff does not ship
|
|
86
|
+
scaffolding that looks complete but does nothing. Stubs you intend to wire later
|
|
87
|
+
must be marked with a `TODO(wire):` comment naming what is missing; unmarked
|
|
88
|
+
dead ends are failures.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# final-review.ps1 - stop hook (Cursor).
|
|
2
2
|
#
|
|
3
|
-
# ONE comprehensive end-of-implementation review across
|
|
4
|
-
# intent, correctness, reliability, coverage,
|
|
3
|
+
# ONE comprehensive end-of-implementation review across six axes:
|
|
4
|
+
# intent, correctness, reliability, coverage, anti-slop, and wiring completeness. When the agent finishes an
|
|
5
5
|
# implementation that touched files, Cursor auto-submits this hook's
|
|
6
6
|
# `followup_message` as the next user turn, so the model re-audits everything it
|
|
7
7
|
# changed this session and FIXES what fails - the model-as-auditor pattern over
|
|
@@ -91,6 +91,12 @@ FINAL REVIEW - audit everything you changed this session and FIX what fails
|
|
|
91
91
|
run `python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all` first.
|
|
92
92
|
Consolidate clones; drop premature abstraction, unneeded deps, operational
|
|
93
93
|
slop (retries, await-in-loop, log spam), unjustified files.
|
|
94
|
+
5. Wiring completeness - for every user-visible behavior you added/changed
|
|
95
|
+
(button, submit, API call, route, state transition), trace its execution
|
|
96
|
+
path to a REAL EFFECT (persist, mutate, call, render). A dead end is slop:
|
|
97
|
+
handleSubmit that does not persist, an endpoint no caller invokes, a store
|
|
98
|
+
never consumed, a stub/TODO/console.log standing in for the effect. Wire it
|
|
99
|
+
now or remove the dead half; mark later-stubs with TODO(wire):.
|
|
94
100
|
Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one line.
|
|
95
101
|
'@
|
|
96
102
|
}
|
|
@@ -1,58 +1,59 @@
|
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
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
|
+
(Join-Path $PSScriptRoot 'declared-editing.md')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
$parts = foreach ($p in $paths) {
|
|
37
|
+
if (Test-Path -LiteralPath $p) { Get-Content -Raw -LiteralPath $p }
|
|
38
|
+
}
|
|
39
|
+
$context = ($parts -join "`n`n").Trim()
|
|
40
|
+
|
|
41
|
+
if (-not $context) { Write-StdoutAscii '{}'; exit 0 }
|
|
42
|
+
|
|
43
|
+
$json = @{ additional_context = $context } | ConvertTo-Json -Compress
|
|
44
|
+
|
|
45
|
+
# Escape every non-ASCII (and any stray control) char to \uXXXX -> pure ASCII.
|
|
46
|
+
# ConvertTo-Json's structural chars and \n / \" escapes are ASCII and pass through.
|
|
47
|
+
$sb = [System.Text.StringBuilder]::new($json.Length + 64)
|
|
48
|
+
foreach ($ch in $json.ToCharArray()) {
|
|
49
|
+
$code = [int][char]$ch
|
|
50
|
+
if ($code -lt 32 -or $code -gt 126) { [void]$sb.AppendFormat('\u{0:x4}', $code) }
|
|
51
|
+
else { [void]$sb.Append($ch) }
|
|
52
|
+
}
|
|
53
|
+
Write-StdoutAscii $sb.ToString()
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
Write-StdoutAscii '{}'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
exit 0
|