cursordoctrine 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +13 -9
- package/README.md +12 -12
- package/linux/USER-RULES.md +9 -7
- package/linux/hooks/hook-common.sh +44 -0
- package/linux/hooks/intent-anchor.sh +32 -18
- package/linux/hooks/intent-precompile.sh +114 -0
- package/linux/hooks.json +7 -0
- package/linux/pre-compile.md +20 -13
- package/package.json +1 -1
- package/windows/USER-RULES.md +9 -7
- package/windows/hooks/anti-slop-audit.ps1 +88 -1
- package/windows/hooks/anti-slop.md +24 -0
- package/windows/hooks/hook-common.ps1 +39 -0
- package/windows/hooks/intent-anchor.ps1 +26 -11
- package/windows/hooks/intent-precompile.ps1 +104 -0
- package/windows/hooks.json +7 -0
- package/windows/pre-compile.md +20 -13
package/INSTALL.md
CHANGED
|
@@ -76,11 +76,13 @@ echo '{"conversation_id":"t1","status":"completed"}' | bash ~/.agents/hooks/fina
|
|
|
76
76
|
echo '{"conversation_id":"t2","file_path":"/tmp/x.py"}' | bash ~/.agents/hooks/self-review-trigger.sh
|
|
77
77
|
echo '{"conversation_id":"t2","status":"completed"}' | bash ~/.agents/hooks/subagent-stop-review.sh # expect {"followup_message": "SUBAGENT FINAL REVIEW ..."} once, then {}
|
|
78
78
|
echo '{"conversation_id":"t2"}' | bash ~/.agents/hooks/post-tool-use.sh # drain t2's leftover feedback file
|
|
79
|
-
# intent-
|
|
80
|
-
# Needs a repo root
|
|
79
|
+
# intent-precompile: writes .scope.json from the prompt BEFORE the first token.
|
|
80
|
+
# Needs a repo root (workspace_roots/cwd; it will NOT write to $HOME - that's the guard).
|
|
81
|
+
echo '{"conversation_id":"t3","cwd":"/tmp","prompt":"fix the dashboard bug"}' | bash ~/.agents/hooks/intent-precompile.sh
|
|
82
|
+
cat /tmp/.scope.json # expect intent="fix the dashboard bug", a seeded acceptance (no <TODO>)
|
|
83
|
+
# intent-anchor: re-injects the contract (and is the fallback creator).
|
|
81
84
|
echo '{"conversation_id":"t3","cwd":"/tmp","file_path":"/tmp/x.py"}' | bash ~/.agents/hooks/intent-anchor.sh
|
|
82
|
-
|
|
83
|
-
echo '{"conversation_id":"t3"}' | bash ~/.agents/hooks/post-tool-use.sh # expect additional_context with the scaffold
|
|
85
|
+
echo '{"conversation_id":"t3"}' | bash ~/.agents/hooks/post-tool-use.sh # expect additional_context with the contract
|
|
84
86
|
echo '{}' | bash ~/.cursor/inject-doctrine.sh # expect {"additional_context": ...}
|
|
85
87
|
python3 ~/.cursor/skills/anti-slop/scripts/scan_slop.py --help # expect usage text (final review's scanner)
|
|
86
88
|
rm -f /tmp/.scope.json # cleanup the test scaffold
|
|
@@ -97,11 +99,13 @@ Windows (same payloads, swap `bash ~/...sh` for `pwsh.exe -NoProfile -File $HOME
|
|
|
97
99
|
echo '{"command":"git push --force"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\permission-gate.ps1
|
|
98
100
|
echo '{"conversation_id":"t2","file_path":"C:\tmp\x.py"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\self-review-trigger.ps1
|
|
99
101
|
echo '{"conversation_id":"t2","status":"completed"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\subagent-stop-review.ps1 # SUBAGENT FINAL REVIEW once, then {}
|
|
100
|
-
# intent-
|
|
101
|
-
# Needs a repo root
|
|
102
|
+
# intent-precompile: writes .scope.json from the prompt BEFORE the first token.
|
|
103
|
+
# Needs a repo root (workspace_roots/cwd; it will NOT write to $HOME - that's the guard).
|
|
104
|
+
echo '{"conversation_id":"t3","cwd":"C:\tmp","prompt":"fix the dashboard bug"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\intent-precompile.ps1
|
|
105
|
+
Get-Content C:\tmp\.scope.json # expect intent="fix the dashboard bug", a seeded acceptance (no <TODO>)
|
|
106
|
+
# intent-anchor: re-injects the contract (and is the fallback creator).
|
|
102
107
|
echo '{"conversation_id":"t3","cwd":"C:\tmp","file_path":"C:\tmp\x.py"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\intent-anchor.ps1
|
|
103
|
-
|
|
104
|
-
echo '{"conversation_id":"t3"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\post-tool-use.ps1 # additional_context with the scaffold
|
|
108
|
+
echo '{"conversation_id":"t3"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\post-tool-use.ps1 # additional_context with the contract
|
|
105
109
|
python $HOME\.cursor\skills\anti-slop\scripts\scan_slop.py --help
|
|
106
110
|
Remove-Item C:\tmp\.scope.json -Force # cleanup the test scaffold
|
|
107
111
|
```
|
|
@@ -112,7 +116,7 @@ Also validate the config: `~/.cursor/hooks.json` must parse as JSON.
|
|
|
112
116
|
|
|
113
117
|
1. Restart Cursor (hooks.json is read at startup).
|
|
114
118
|
2. Open any project and start a new agent chat. The doctrine should be in context — ask the agent "what does your doctrine say about diffs?" and it should answer from §2; ask "what is the Anchor Set?" and it should answer from `pre-compile.md` (Objective / Constraints / Scope / Deterministic success).
|
|
115
|
-
3.
|
|
119
|
+
3. Send a prompt. `.scope.json` should appear in the repo root **immediately** (before the agent's first edit), written by `intent-precompile` with `intent` from your prompt and a seeded `acceptance` (not a bare `<TODO>`). Have the agent make a small edit: on the next turn it should receive a `SELF-REVIEW TRIGGER` message, and `intent-anchor` re-injects the contract. The contract regenerates when your prompt changes.
|
|
116
120
|
4. Ask the agent to run `git push --force` (in a throwaway repo). The permission gate must block it.
|
|
117
121
|
5. Finish a small implementation and stop. A single `FINAL REVIEW` follow-up should fire — exactly once.
|
|
118
122
|
6. Delegate a small edit to a subagent (e.g. ask the agent to "use a generalPurpose subagent to add a comment to <file>"). The subagent should receive one `SUBAGENT FINAL REVIEW` follow-up before returning, and the parent should see `SUBAGENT WORK DETECTED` at its next tool boundary. (`subagentStop` is only read at startup — if nothing fires, restart Cursor again.)
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<div align="center">
|
|
11
11
|
<h1>cursordoctrine</h1>
|
|
12
12
|
<p><strong>Self-review hooks for Cursor — proactive and reactive.</strong></p>
|
|
13
|
-
<p>
|
|
13
|
+
<p>Six hook events, one message bus.<br />The model compiles its intent, audits its own work, and stays on the rails. Cursor carries context and gates blast radius.</p>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
<br />
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
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. Four jobs:
|
|
23
23
|
|
|
24
|
-
1. **Compile intent before coding** (proactive) — at session start the agent gets the doctrine plus the **Anchor Set** discipline (`pre-compile.md`): before writing code it must emit *Objective / Constraints / Scope / Deterministic success*. `intent-
|
|
24
|
+
1. **Compile intent before coding** (proactive) — at session start the agent gets the doctrine plus the **Anchor Set** discipline (`pre-compile.md`): before writing code it must emit *Objective / Constraints / Scope / Deterministic success*. `intent-precompile` (`beforeSubmitPrompt`) materializes that as `.scope.json` the moment you hit send — **before the agent's first token** — with `intent` locked from the request and `acceptance` seeded (never a bare `<TODO>`), so the contract is the first artifact of the turn. `intent-anchor` then re-injects it into context every turn (regenerated when the prompt changes), so it stays in focus against Salience Dilution.
|
|
25
25
|
2. **Inject the doctrine** at session start — every chat starts with the same short governing text (`doctrine.md`, `USER-RULES.md`, `declared-editing.md` the YAGNI ultra ladder, and `pre-compile.md` the thin intent-compilation phase).
|
|
26
26
|
3. **Hand the model its own edits back** (reactive) — after each agent edit, a self-review prompt goes into a pending file (plus semantic-density, scope-gate, and anti-slop advisories when they trip). Next turn the model reads its diff, fixes real bugs, stays quiet otherwise.
|
|
27
27
|
4. **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.
|
|
@@ -90,31 +90,31 @@ Two machine-checkable consequences:
|
|
|
90
90
|
- **`scope-gate-audit`** (afterFileEdit, opt-in via `.scope.json` existing) audits every edit against `files[]` and quotes `intent` + `acceptance` back on a violation. Editing outside the declared set is the textbook scope-creep signal.
|
|
91
91
|
- **final-review axis 0** (intent trace) traces every diff hunk back to `intent`. Anything untraceable is a hallucinated requirement.
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
**Before the agent's first token**, `intent-precompile` (`beforeSubmitPrompt`) materializes the Anchor Set: the moment you hit send it writes `.scope.json` to the repo root with `intent` locked from the prompt (which is in the event payload directly) and `acceptance` seeded with a real default — so the contract is the first artifact of the turn. `intent-anchor` (`postToolUse`) then re-injects it into context on the first tool boundary. One contract per prompt, re-injection per turn. The intent-anchor latch is armed on first fire and cleared **unconditionally** by the stop hook on every turn boundary, so the next turn re-fires and can never get stranded silenced mid-session.
|
|
94
94
|
|
|
95
95
|
The Anchor Set is skipped for trivial one-liners (typo, literal) — the `declared-editing.md` ladder's rung 1 governs when it's overkill.
|
|
96
96
|
|
|
97
|
-
###
|
|
97
|
+
### Contract first, then kept alive: `intent-precompile` + `intent-anchor`
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
The contract has to exist *before* the agent edits and stay in focus *as* it edits. Two hooks, two jobs:
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
**`intent-precompile` (`beforeSubmitPrompt`) — write it first.** This event fires right after the user hits send, before the backend request, with the user's `prompt` in the payload directly — no `<user_query>` extraction, no transcript dependency, no contamination from auto-submitted review followups. When the prompt is new (its `_intent_hash` differs from the contract on disk) it writes a fresh `.scope.json`: `intent` locked from the prompt, `files: []`, `acceptance` seeded with a real default (never a bare `<TODO>` — a placeholder that looks owned and never gets filled was the old failure mode), `_intent_hash` for change detection. It also stashes the verbatim prompt so `intent-anchor` reads the *same* ground-truth text. Same prompt on disk → left intact, preserving the agent's refined `intent`/`acceptance`/`files`. Hook-generated submits are skipped.
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
2. **Re-inject the contract every turn.** Reads `.scope.json` and stashes `intent` + `files` + `acceptance` into the feedback bus, which `post-tool-use` delivers as `additional_context`. The contract is back in the model's attentional focus at the start of each turn, before edits pile up and dilute it.
|
|
103
|
+
**`intent-anchor` (`postToolUse`, registered first so it runs before `post-tool-use` drains the bus) — keep it alive.** As a conversation fills with code, logs and errors, the token of the original request shrinks to a rounding error against recent history — *Salience Dilution* — and the agent stops checking the contract. So on the first tool boundary of every turn (per-turn latch, cleared unconditionally at each stop) it reads `.scope.json` and stashes `intent` + `files` + `acceptance` into the feedback bus, which `post-tool-use` delivers as `additional_context` — the contract back in attentional focus before edits pile up. It also **falls back to creating the contract** if `beforeSubmitPrompt` didn't run (older Cursor), and re-injects a loud demand each turn until the agent sharpens the seeded `acceptance` to the one deterministic check.
|
|
105
104
|
|
|
106
|
-
> **The
|
|
105
|
+
> **The hooks write `.scope.json` deliberately.** The contract must exist before the agent edits and track the request. Safeguards: (a) **never writes to `$HOME`** — if the repo root can't be resolved the hook stays silent rather than drop a ghost file; (b) **regenerates on prompt CHANGE, not every turn** — staleness is tracked via `_intent_hash` in the file, and `intent-precompile`/`intent-anchor` share one hashing helper so a same-prompt turn re-injects without rewriting; (c) **`trace.query` stays verbatim** as the audit anchor even when the agent normalizes `intent`.
|
|
107
106
|
|
|
108
107
|
Crucially, `intent-anchor` carries the **semantic** contract (`intent`/`acceptance`) into context every turn — something the path-only `scope-gate-audit` can never do. That is what makes "the agent forgot about grid symmetry while editing the right file" catchable: the symmetry requirement is re-stated in front of the model before each edit, not just checked against a file list after.
|
|
109
108
|
|
|
110
|
-
## The
|
|
109
|
+
## The flows
|
|
111
110
|
|
|
112
111
|
| Flow | Event | What happens |
|
|
113
112
|
|---|---|---|
|
|
113
|
+
| Submit | `beforeSubmitPrompt` | **`intent-precompile`** writes `.scope.json` to the repo root from the prompt in the payload — **before the agent's first token** — with `intent` locked and `acceptance` seeded, and stashes the verbatim prompt for `intent-anchor`. Skips hook-generated auto-submits; hash-gated so the agent's refinements survive. |
|
|
114
114
|
| Session | `sessionStart` | `inject-doctrine` reads doctrine + user rules + declared-editing + **pre-compile** and emits them as `additional_context`. |
|
|
115
|
-
| Every turn | `postToolUse` | **`intent-anchor`** (registered first) re-injects `.scope.json` into `additional_context` at the first tool boundary of each turn — the anti-Salience-Dilution move that keeps `intent` + `acceptance` in the model's attentional focus before edits pile up
|
|
115
|
+
| Every turn | `postToolUse` | **`intent-anchor`** (registered first) re-injects `.scope.json` into `additional_context` at the first tool boundary of each turn — the anti-Salience-Dilution move that keeps `intent` + `acceptance` in the model's attentional focus before edits pile up, and demands the seeded `acceptance` be sharpened. Then `post-tool-use` folds subagent markers and drains the feedback file. |
|
|
116
116
|
| Shell | `beforeShellExecution` | `permission-gate` checks the command against a deny list. Allow by default, deny by list, fail open. |
|
|
117
|
-
| Edit | `afterFileEdit` + `stop` | **Proactive:** `intent-
|
|
117
|
+
| Edit | `afterFileEdit` + `stop` | **Proactive:** `intent-precompile` writes the contract per prompt; `intent-anchor` re-injects it each turn. **Reactive:** `self-review-trigger` stashes the review prompt per edit; `semantic-density-audit`, `scope-gate-audit` (opt-in, audits `.scope.json`), and `anti-slop-audit` append advisories when they trip; `final-review` fires one end-of-implementation seven-axis pass. |
|
|
118
118
|
| 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`. |
|
|
119
119
|
|
|
120
120
|
## Layout
|
package/linux/USER-RULES.md
CHANGED
|
@@ -6,13 +6,15 @@ the task itself is a behavior change. Refactors, renames, cleanup only when
|
|
|
6
6
|
asked. Leave generated files alone unless explicitly required.
|
|
7
7
|
|
|
8
8
|
## Intent contract (.scope.json)
|
|
9
|
-
The harness
|
|
10
|
-
|
|
11
|
-
operating contract, not optional:
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
The harness writes `.scope.json` to the repo root the moment you hit send -
|
|
10
|
+
BEFORE your first token - with `intent` locked from the request, and re-injects
|
|
11
|
+
it every turn. Treat it as your operating contract, not optional:
|
|
12
|
+
- It is the FIRST thing that exists each turn; govern by it from your first action.
|
|
13
|
+
As your first edits, refine `intent` to your normalized restatement and SHARPEN
|
|
14
|
+
the seeded `acceptance` to the one deterministic check (it is a draft, not a
|
|
15
|
+
blank `<TODO>`). `files[]` is auto-tracked - do not maintain it.
|
|
16
|
+
- When the user's request changes, the contract regenerates with the new intent -
|
|
17
|
+
refine it again for the new ask.
|
|
16
18
|
- If a hook surfaces the contract, defer to it: it outranks momentum. Edit
|
|
17
19
|
inside the declared scope; if you must grow it, justify it, don't sneak past.
|
|
18
20
|
|
|
@@ -79,6 +79,50 @@ safe_conversation_id() {
|
|
|
79
79
|
|
|
80
80
|
hooks_pending_dir() { printf '%s' "$HOME/.cursor/.hooks-pending"; }
|
|
81
81
|
|
|
82
|
+
# sha256_hex <text> -> SHA-256 hex. SHARED so intent-precompile (beforeSubmitPrompt)
|
|
83
|
+
# and intent-anchor (postToolUse) hash the SAME text the SAME way; otherwise they
|
|
84
|
+
# disagree on _intent_hash and postToolUse needlessly rewrites the prompt hook's
|
|
85
|
+
# scope. Mirrors the hashing intent-anchor.sh already does (sha256sum|shasum).
|
|
86
|
+
sha256_hex() {
|
|
87
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
88
|
+
printf '%s' "$1" | sha256sum | awk '{print $1}'
|
|
89
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
90
|
+
printf '%s' "$1" | shasum -a 256 | awk '{print $1}'
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Path where beforeSubmitPrompt stashes the verbatim user prompt for the turn.
|
|
95
|
+
# intent-anchor PREFERS this over transcript parsing: ground-truth request from
|
|
96
|
+
# the payload, present on the FIRST postToolUse, immune to <user_query>
|
|
97
|
+
# contamination.
|
|
98
|
+
current_prompt_path() { printf '%s' "$(hooks_pending_dir)/current-prompt-$1.txt"; }
|
|
99
|
+
|
|
100
|
+
# stashed_prompt <cid> -> the stashed prompt ('' if none). Only ever holds real
|
|
101
|
+
# human prompts - intent-precompile filters hook-generated submits. (The caller's
|
|
102
|
+
# command substitution strips the trailing newline.)
|
|
103
|
+
stashed_prompt() {
|
|
104
|
+
local p; p="$(current_prompt_path "$1")"
|
|
105
|
+
[ -f "$p" ] && cat "$p" 2>/dev/null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# default_acceptance -> the real default the hook seeds so .scope.json NEVER ships
|
|
109
|
+
# a bare "<TODO>" (the thing that looks broken and never gets filled). A verifiable
|
|
110
|
+
# bar the agent then sharpens to the single deterministic check. ONE place so both
|
|
111
|
+
# hooks emit the identical string.
|
|
112
|
+
default_acceptance() {
|
|
113
|
+
printf '%s' 'Every change traces to intent; the project typecheck/build and any *.selfcheck pass, and the described problem no longer reproduces. (Sharpen to the one deterministic check.)'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# redact_secrets <text> -> portable (sed) secret scrub for text we persist to
|
|
117
|
+
# .scope.json / the prompt stash. Mirrors the python redaction in
|
|
118
|
+
# extract_last_user_query so the beforeSubmitPrompt path is no leakier.
|
|
119
|
+
redact_secrets() {
|
|
120
|
+
printf '%s' "$1" | sed -E \
|
|
121
|
+
-e 's/\bnpm_[A-Za-z0-9]{10,}\b/[REDACTED_NPM_TOKEN]/g' \
|
|
122
|
+
-e 's/\b(sk-[A-Za-z0-9]{10,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,})\b/[REDACTED_TOKEN]/g' \
|
|
123
|
+
-e 's/([Aa][Pp][Ii][_-]?[Kk][Ee][Yy]|[Tt][Oo][Kk][Ee][Nn]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd])[[:space:]]*[:=][[:space:]]*[^[:space:]]+/\1=[REDACTED]/g'
|
|
124
|
+
}
|
|
125
|
+
|
|
82
126
|
# is_cursor_config_path <path> -> 0 if the path lives under a .cursor directory
|
|
83
127
|
is_cursor_config_path() {
|
|
84
128
|
case "$1" in
|
|
@@ -74,19 +74,20 @@ fi
|
|
|
74
74
|
# Already injected this turn -> quiet. Latch cleared at every stop.
|
|
75
75
|
[ -f "$latch" ] && exit 0
|
|
76
76
|
|
|
77
|
-
# --- current request
|
|
78
|
-
|
|
77
|
+
# --- current request ---------------------------------------------------------
|
|
78
|
+
# PREFER the prompt stashed by intent-precompile (beforeSubmitPrompt): ground-truth
|
|
79
|
+
# request from the payload, present on the FIRST postToolUse, immune to <user_query>
|
|
80
|
+
# contamination. Fall back to transcript parsing only when the prompt hook did not
|
|
81
|
+
# run. Both share sha256_hex so _intent_hash is consistent across the two hooks.
|
|
82
|
+
current_query="$(stashed_prompt "$cid")"
|
|
83
|
+
[ -n "$current_query" ] || current_query="$(extract_last_user_query "$input")"
|
|
79
84
|
has_query=0
|
|
80
85
|
[ -n "$current_query" ] && has_query=1
|
|
81
86
|
|
|
82
87
|
current_hash=""
|
|
83
88
|
prompt_changed=0
|
|
84
89
|
if [ "$has_query" = "1" ]; then
|
|
85
|
-
|
|
86
|
-
current_hash="$(printf '%s' "$current_query" | sha256sum | awk '{print $1}')"
|
|
87
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
88
|
-
current_hash="$(printf '%s' "$current_query" | shasum -a 256 | awk '{print $1}')"
|
|
89
|
-
fi
|
|
90
|
+
current_hash="$(sha256_hex "$current_query")"
|
|
90
91
|
prev_hash=""
|
|
91
92
|
[ -f "$hash_file" ] && prev_hash="$(cat "$hash_file" 2>/dev/null)"
|
|
92
93
|
[ "$current_hash" != "$prev_hash" ] && prompt_changed=1
|
|
@@ -196,19 +197,21 @@ if [ "$should_create" = "1" ] || [ "$should_regen" = "1" ]; then
|
|
|
196
197
|
# Both paths require $has_query, so intent is always locked from the request.
|
|
197
198
|
intent_val="$current_query"
|
|
198
199
|
trace_query="$current_query"
|
|
199
|
-
|
|
200
|
-
#
|
|
200
|
+
default_acc="$(default_acceptance)"
|
|
201
|
+
# jq preferred; python3 fallback. Write intent, empty files[], a real seeded
|
|
202
|
+
# acceptance (never a bare <TODO>), trace provenance, and _intent_hash so
|
|
203
|
+
# staleness is self-contained.
|
|
201
204
|
if have_jq; then
|
|
202
|
-
jq -n --arg intent "$intent_val" --arg hash "$current_hash" --arg tq "$trace_query" --arg ts "$now_ts" \
|
|
203
|
-
'{intent:$intent, files:[], acceptance
|
|
205
|
+
jq -n --arg intent "$intent_val" --arg hash "$current_hash" --arg tq "$trace_query" --arg ts "$now_ts" --arg acc "$default_acc" \
|
|
206
|
+
'{intent:$intent, files:[], acceptance:$acc, allow_growth:false, trace:{query:$tq, ts:$ts}, _intent_hash:$hash, _generated_by:"intent-anchor hook"}' \
|
|
204
207
|
> "$scope_path" 2>/dev/null && regenerated=1
|
|
205
208
|
elif have_py; then
|
|
206
|
-
if I_FILE="$scope_path" I_INTENT="$intent_val" I_HASH="$current_hash" I_TQ="$trace_query" I_TS="$now_ts" python3 -c '
|
|
209
|
+
if I_FILE="$scope_path" I_INTENT="$intent_val" I_HASH="$current_hash" I_TQ="$trace_query" I_TS="$now_ts" I_ACC="$default_acc" python3 -c '
|
|
207
210
|
import json, os
|
|
208
211
|
obj = {
|
|
209
212
|
"intent": os.environ["I_INTENT"],
|
|
210
213
|
"files": [],
|
|
211
|
-
"acceptance": "
|
|
214
|
+
"acceptance": os.environ["I_ACC"],
|
|
212
215
|
"allow_growth": False,
|
|
213
216
|
"trace": {"query": os.environ["I_TQ"], "ts": os.environ["I_TS"]},
|
|
214
217
|
"_intent_hash": os.environ["I_HASH"],
|
|
@@ -222,7 +225,7 @@ with open(os.environ["I_FILE"], "w", encoding="utf-8") as f:
|
|
|
222
225
|
fi
|
|
223
226
|
if [ "$regenerated" = "1" ]; then
|
|
224
227
|
scope_intent="$intent_val"
|
|
225
|
-
scope_acceptance="
|
|
228
|
+
scope_acceptance="$default_acc"
|
|
226
229
|
scope_files="(auto-tracked - the scope hook records every file you edit)"
|
|
227
230
|
scope_exists=1
|
|
228
231
|
scope_stale=0
|
|
@@ -271,6 +274,17 @@ else
|
|
|
271
274
|
query_line="(current request unavailable - no transcript in this event)"
|
|
272
275
|
fi
|
|
273
276
|
|
|
277
|
+
# acceptance is seeded with a real default (never a bare <TODO>), but the default
|
|
278
|
+
# is generic - the contract is not fully set until the agent sharpens it to the
|
|
279
|
+
# ONE deterministic check. Detect the unsharpened state -> loud demand each turn.
|
|
280
|
+
acceptance_demand=""
|
|
281
|
+
case "$scope_acceptance" in
|
|
282
|
+
"<TODO"*|*"Sharpen to the one deterministic check"*)
|
|
283
|
+
acceptance_demand="
|
|
284
|
+
|
|
285
|
+
>> acceptance is still the seeded default. Your FIRST action this turn is a targeted string-replace on .scope.json setting acceptance to the one deterministic check that decides done - then do the work." ;;
|
|
286
|
+
esac
|
|
287
|
+
|
|
274
288
|
if [ "$regenerated" = "1" ]; then
|
|
275
289
|
msg="INTENT ANCHOR (scope regenerated) - .scope.json written for this prompt.
|
|
276
290
|
|
|
@@ -280,9 +294,9 @@ if [ "$regenerated" = "1" ]; then
|
|
|
280
294
|
|
|
281
295
|
The hook wrote a fresh scaffold to $scope_path from your current request. intent
|
|
282
296
|
is locked from what you just asked. files[] is AUTO-TRACKED - the scope hook
|
|
283
|
-
records every file you edit, so do not maintain it by hand.
|
|
284
|
-
the one deterministic check
|
|
285
|
-
be re-injected every turn until your request changes again
|
|
297
|
+
records every file you edit, so do not maintain it by hand. acceptance is seeded
|
|
298
|
+
with a sensible default; sharpen it to the one deterministic check, THEN proceed.
|
|
299
|
+
This contract will be re-injected every turn until your request changes again.$acceptance_demand"
|
|
286
300
|
elif [ "$scope_exists" != "1" ] || [ "$scope_hollow" = "1" ]; then
|
|
287
301
|
if [ "$scope_hollow" = "1" ]; then
|
|
288
302
|
state="the .scope.json in $root is only a <TODO> placeholder (the hook could not read your request to fill it)"
|
|
@@ -316,7 +330,7 @@ else
|
|
|
316
330
|
acceptance: $scope_acceptance
|
|
317
331
|
|
|
318
332
|
$drift_note If a constraint above conflicts with what you are about to do, stop
|
|
319
|
-
and reconcile - the contract outranks momentum
|
|
333
|
+
and reconcile - the contract outranks momentum.$acceptance_demand"
|
|
320
334
|
fi
|
|
321
335
|
|
|
322
336
|
# --- stash to the feedback bus (drained by post-tool-use.sh) -----------------
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# intent-precompile.sh - beforeSubmitPrompt "contract first" writer (Cursor, Linux).
|
|
3
|
+
#
|
|
4
|
+
# THE FIX for "el .scope.json se crea casi al final": creation used to live on
|
|
5
|
+
# postToolUse (intent-anchor), which fires only AFTER the agent's first tool and
|
|
6
|
+
# depends on the transcript becoming readable to detect the prompt. Until then the
|
|
7
|
+
# PREVIOUS prompt's intent persisted and the agent worked under it - the scope only
|
|
8
|
+
# flipped to the right intent late in the turn.
|
|
9
|
+
#
|
|
10
|
+
# beforeSubmitPrompt fires "right after the user hits send, before the backend
|
|
11
|
+
# request" - BEFORE the agent's first token - and its payload carries the user's
|
|
12
|
+
# `prompt` DIRECTLY (no <user_query> extraction, no transcript dependency, no
|
|
13
|
+
# hook-followup contamination). So this hook writes .scope.json with the real
|
|
14
|
+
# intent up front, making the contract the FIRST artifact of the turn.
|
|
15
|
+
#
|
|
16
|
+
# Two deterministic jobs:
|
|
17
|
+
# 1. STASH the verbatim prompt to current-prompt-<cid>.txt. intent-anchor prefers
|
|
18
|
+
# it over transcript parsing, so both hooks hash the SAME text (sha256_hex)
|
|
19
|
+
# and never fight over _intent_hash.
|
|
20
|
+
# 2. WRITE / REGENERATE .scope.json from the prompt when it is new (hash differs)
|
|
21
|
+
# or missing. Same prompt on disk -> leave it (preserve the agent's refined
|
|
22
|
+
# intent / acceptance / files). acceptance is seeded with a real default
|
|
23
|
+
# (default_acceptance), never a bare <TODO>.
|
|
24
|
+
#
|
|
25
|
+
# Hook-generated submits (auto-submitted final-review / subagent-review followups)
|
|
26
|
+
# are SKIPPED: their prompt is review boilerplate, not the user's request.
|
|
27
|
+
#
|
|
28
|
+
# Never blocks submission; writes files as a side effect and exits 0. Disable:
|
|
29
|
+
# HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0 (shares the intent-anchor kill switch).
|
|
30
|
+
|
|
31
|
+
set +e
|
|
32
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
33
|
+
|
|
34
|
+
[ "${HOOKS_ENFORCE:-}" = "0" ] && exit 0
|
|
35
|
+
[ "${INTENT_ANCHOR_ENFORCE:-}" = "0" ] && exit 0
|
|
36
|
+
|
|
37
|
+
input="$(read_hook_stdin)"
|
|
38
|
+
[ -n "$input" ] || exit 0
|
|
39
|
+
|
|
40
|
+
# --- the prompt (direct from payload - the whole point of this event) --------
|
|
41
|
+
prompt="$(json_get "$input" prompt)"
|
|
42
|
+
# trim leading/trailing whitespace
|
|
43
|
+
prompt="$(printf '%s' "$prompt" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
44
|
+
[ -n "$prompt" ] || exit 0
|
|
45
|
+
|
|
46
|
+
# Auto-submitted hook followups are not the user's request -> leave the contract.
|
|
47
|
+
case "$prompt" in
|
|
48
|
+
"FINAL REVIEW (end of implementation)"*|"SUBAGENT FINAL REVIEW"*|"SELF-REVIEW"*|"INTENT ANCHOR"*) exit 0 ;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
prompt="$(redact_secrets "$prompt")"
|
|
52
|
+
|
|
53
|
+
cid="$(safe_conversation_id "$input")"
|
|
54
|
+
pending_dir="$(hooks_pending_dir)"
|
|
55
|
+
|
|
56
|
+
# --- repo root (workspace_roots / cwd; NO $HOME fallback - no ghost files) ----
|
|
57
|
+
root=""
|
|
58
|
+
while IFS= read -r cand; do
|
|
59
|
+
[ -n "$cand" ] && [ -d "$cand" ] && { root="${cand%/}"; break; }
|
|
60
|
+
done <<EOF
|
|
61
|
+
$(json_get "$input" cwd)
|
|
62
|
+
$(json_get_array "$input" workspace_roots)
|
|
63
|
+
EOF
|
|
64
|
+
if [ -z "$root" ] && [ -n "$CURSOR_PROJECT_DIR" ] && [ -d "$CURSOR_PROJECT_DIR" ]; then
|
|
65
|
+
root="${CURSOR_PROJECT_DIR%/}"
|
|
66
|
+
fi
|
|
67
|
+
[ -n "$root" ] || exit 0
|
|
68
|
+
|
|
69
|
+
# --- stash the prompt so intent-anchor reads the same ground-truth text -------
|
|
70
|
+
mkdir -p "$pending_dir" 2>/dev/null
|
|
71
|
+
printf '%s' "$prompt" > "$(current_prompt_path "$cid")" 2>/dev/null
|
|
72
|
+
|
|
73
|
+
# --- write / regenerate .scope.json (hash-gated) ------------------------------
|
|
74
|
+
current_hash="$(sha256_hex "$prompt")"
|
|
75
|
+
scope_path="$root/.scope.json"
|
|
76
|
+
on_disk_hash=""
|
|
77
|
+
if [ -f "$scope_path" ]; then
|
|
78
|
+
if have_jq; then
|
|
79
|
+
on_disk_hash="$(jq -r '._intent_hash // empty' "$scope_path" 2>/dev/null)"
|
|
80
|
+
elif have_py; then
|
|
81
|
+
on_disk_hash="$(python3 -c 'import json,sys
|
|
82
|
+
try: print(json.load(open(sys.argv[1])).get("_intent_hash","") or "")
|
|
83
|
+
except Exception: pass' "$scope_path" 2>/dev/null)"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Same prompt already locked (prior fire this turn, or the agent refined the
|
|
88
|
+
# contract) -> leave it. Only (re)write on a NEW or missing/garbage contract.
|
|
89
|
+
[ "$on_disk_hash" = "$current_hash" ] && [ -n "$current_hash" ] && exit 0
|
|
90
|
+
|
|
91
|
+
now_ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)"
|
|
92
|
+
default_acc="$(default_acceptance)"
|
|
93
|
+
if have_jq; then
|
|
94
|
+
jq -n --arg intent "$prompt" --arg hash "$current_hash" --arg ts "$now_ts" --arg acc "$default_acc" \
|
|
95
|
+
'{intent:$intent, files:[], acceptance:$acc, allow_growth:false, trace:{query:$intent, ts:$ts}, _intent_hash:$hash, _generated_by:"intent-precompile hook (beforeSubmitPrompt)"}' \
|
|
96
|
+
> "$scope_path" 2>/dev/null
|
|
97
|
+
elif have_py; then
|
|
98
|
+
I_FILE="$scope_path" I_INTENT="$prompt" I_HASH="$current_hash" I_TS="$now_ts" I_ACC="$default_acc" python3 -c '
|
|
99
|
+
import json, os
|
|
100
|
+
obj = {
|
|
101
|
+
"intent": os.environ["I_INTENT"],
|
|
102
|
+
"files": [],
|
|
103
|
+
"acceptance": os.environ["I_ACC"],
|
|
104
|
+
"allow_growth": False,
|
|
105
|
+
"trace": {"query": os.environ["I_INTENT"], "ts": os.environ["I_TS"]},
|
|
106
|
+
"_intent_hash": os.environ["I_HASH"],
|
|
107
|
+
"_generated_by": "intent-precompile hook (beforeSubmitPrompt)",
|
|
108
|
+
}
|
|
109
|
+
with open(os.environ["I_FILE"], "w", encoding="utf-8") as f:
|
|
110
|
+
json.dump(obj, f, ensure_ascii=False, indent=2)
|
|
111
|
+
' 2>/dev/null
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
exit 0
|
package/linux/hooks.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"hooks": {
|
|
4
|
+
"beforeSubmitPrompt": [
|
|
5
|
+
{
|
|
6
|
+
"command": "bash ~/.agents/hooks/intent-precompile.sh",
|
|
7
|
+
"timeout": 5,
|
|
8
|
+
"_comment": "5s: CONTRACT FIRST. Fires right after the user hits send, BEFORE the agent's first token, with the user's `prompt` in the payload directly. Writes/regenerates .scope.json (intent locked from the prompt, acceptance seeded with a real default - never a bare <TODO>) so the contract is the FIRST artifact of the turn instead of appearing late once postToolUse can read the transcript. Also stashes the verbatim prompt to current-prompt-<cid>.txt so intent-anchor (postToolUse) re-injects from the SAME ground-truth text - no <user_query> contamination, no _intent_hash fights. Skips hook-generated auto-submits (FINAL REVIEW / SUBAGENT / SELF-REVIEW). Hash-gated: same prompt on disk -> left intact (preserves the agent's refined intent/acceptance/files). Never blocks submission, always exits 0. Disable: HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0."
|
|
9
|
+
}
|
|
10
|
+
],
|
|
4
11
|
"sessionStart": [
|
|
5
12
|
{
|
|
6
13
|
"command": "bash ~/.cursor/inject-doctrine.sh",
|
package/linux/pre-compile.md
CHANGED
|
@@ -53,23 +53,30 @@ Answer these four, terse, in your first response. One phrase each, not prose:
|
|
|
53
53
|
|
|
54
54
|
## Materialize it: .scope.json (the hook owns this file)
|
|
55
55
|
|
|
56
|
-
The
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
The contract is written for you **before your first token**: the `intent-precompile`
|
|
57
|
+
hook fires on `beforeSubmitPrompt` (right after the user hits send) and writes
|
|
58
|
+
`.scope.json` to the repo root with the real `intent` already locked from the
|
|
59
|
+
request — so the contract is the FIRST artifact of the turn, and you govern by it
|
|
60
|
+
from the very first action. `intent-anchor` then re-injects it on every tool
|
|
61
|
+
boundary to keep it in focus.
|
|
62
|
+
- `intent` is locked from the request and REFRESHED when the request changes — a
|
|
63
|
+
new prompt regenerates the contract and resets `files[]`, so it never carries
|
|
64
|
+
over between features;
|
|
61
65
|
- `files[]` is auto-recorded — the scope hook appends every file you edit, so
|
|
62
66
|
you never maintain it by hand;
|
|
67
|
+
- `acceptance` is SEEDED with a real default (never a bare `<TODO>`); it is not a
|
|
68
|
+
blank you must fill, it is a draft you SHARPEN;
|
|
63
69
|
- `trace.query` is the VERBATIM request (the audit anchor), `_intent_hash` and
|
|
64
70
|
`_generated_by` are hook bookkeeping. Leave all three alone.
|
|
65
71
|
|
|
66
72
|
Your two targeted edits on the contract (each a string replace on ONE field, never
|
|
67
|
-
a whole-file rewrite):
|
|
68
|
-
- **`intent`** → replace the
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- **`acceptance`** →
|
|
72
|
-
the hook cannot derive.
|
|
73
|
+
a whole-file rewrite), done as your FIRST actions this turn before editing source:
|
|
74
|
+
- **`intent`** → replace the seed with your Step 0 restatement: the normalized,
|
|
75
|
+
meaning-preserving sentence. This is what final-review axis 0 traces each diff
|
|
76
|
+
hunk against, so a clean `intent` makes the audit sharp.
|
|
77
|
+
- **`acceptance`** → sharpen the seeded default to the single deterministic check
|
|
78
|
+
that decides done, which the hook cannot derive. The hook re-injects a loud
|
|
79
|
+
demand every turn until you do.
|
|
73
80
|
|
|
74
81
|
Do **NOT** touch `trace.query`, `_intent_hash`, or `_generated_by`, and do **NOT**
|
|
75
82
|
rewrite the whole file: `_intent_hash` is computed from the verbatim `trace.query`,
|
|
@@ -82,11 +89,11 @@ changed the meaning: `intent` and `trace.query` must agree.
|
|
|
82
89
|
{
|
|
83
90
|
"intent": "<YOU refine this: your normalized Step 0 restatement>",
|
|
84
91
|
"files": ["<auto-recorded by the hook as you edit>"],
|
|
85
|
-
"acceptance": "<YOU
|
|
92
|
+
"acceptance": "<seeded with a default; YOU sharpen to the deterministic check>",
|
|
86
93
|
"allow_growth": false,
|
|
87
94
|
"trace": { "query": "<VERBATIM request - the hook owns this, leave it>", "ts": "<when>" },
|
|
88
95
|
"_intent_hash": "<hook bookkeeping>",
|
|
89
|
-
"_generated_by":"intent-anchor hook"
|
|
96
|
+
"_generated_by":"intent-precompile / intent-anchor hook"
|
|
90
97
|
}
|
|
91
98
|
```
|
|
92
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Thin self-review hooks for Cursor — the model is the auditor. Pruned + deduplicated: intent-anchor (auto-scaffolded .scope.json per prompt + per-turn re-injection against Salience Dilution), intent-trace final review, unified anti-slop checklist as single source of truth.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cursordoctrine": "bin/cli.mjs"
|
package/windows/USER-RULES.md
CHANGED
|
@@ -6,13 +6,15 @@ the task itself is a behavior change. Refactors, renames, cleanup only when
|
|
|
6
6
|
asked. Leave generated files alone unless explicitly required.
|
|
7
7
|
|
|
8
8
|
## Intent contract (.scope.json)
|
|
9
|
-
The harness
|
|
10
|
-
|
|
11
|
-
operating contract, not optional:
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
The harness writes `.scope.json` to the repo root the moment you hit send -
|
|
10
|
+
BEFORE your first token - with `intent` locked from the request, and re-injects
|
|
11
|
+
it every turn. Treat it as your operating contract, not optional:
|
|
12
|
+
- It is the FIRST thing that exists each turn; govern by it from your first action.
|
|
13
|
+
As your first edits, refine `intent` to your normalized restatement and SHARPEN
|
|
14
|
+
the seeded `acceptance` to the one deterministic check (it is a draft, not a
|
|
15
|
+
blank `<TODO>`). `files[]` is auto-tracked - do not maintain it.
|
|
16
|
+
- When the user's request changes, the contract regenerates with the new intent -
|
|
17
|
+
refine it again for the new ask.
|
|
16
18
|
- If a hook surfaces the contract, defer to it: it outranks momentum. Edit
|
|
17
19
|
inside the declared scope; if you must grow it, justify it, don't sneak past.
|
|
18
20
|
|
|
@@ -172,6 +172,86 @@ if ($logCount -ge 6) {
|
|
|
172
172
|
$opsFlags.Add("- TELEMETRY SPAM: $logCount log/print statements added in this one edit. Debug-level telemetry that nobody reads is slop; consolidate or remove (kept only if this is a real logging entrypoint).")
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
# --- signal 5: AI vibe-coding structural slop (Arrow / Masking / Boolean /
|
|
176
|
+
# Switch Bloat) - high-precision regex/heuristics, deterministic, language-agnostic
|
|
177
|
+
# where possible. Each rule flags a SPECIFIC pattern that the model does because
|
|
178
|
+
# it cannot combine conditions or design types; the rule's "Stop" forces the
|
|
179
|
+
# canonical fix (guard clauses, fail-fast, named types, dictionary dispatch).
|
|
180
|
+
$vibeFlags = New-Object System.Collections.Generic.List[string]
|
|
181
|
+
|
|
182
|
+
# (5a) ARROW CODE: 3+ levels of nested if/for/while/switch in the ADDED lines.
|
|
183
|
+
# Measure indent depth of control-flow keywords; 3+ distinct nesting levels in
|
|
184
|
+
# one block = arrow code. The model nests because it cannot combine conditions.
|
|
185
|
+
# Stop: guard clauses (early returns) - read top-to-bottom, no deep indent.
|
|
186
|
+
$controlKw = '(?i)^\s*(if|for|while|switch|try|else|elif|else\s+if)\b'
|
|
187
|
+
$nestHits = 0
|
|
188
|
+
$maxNest = 0
|
|
189
|
+
foreach ($a in $added) {
|
|
190
|
+
if ($a -notmatch $controlKw) { continue }
|
|
191
|
+
$spaces = ($a -replace '\S.*$', '').Length
|
|
192
|
+
# Normalize: assume 2-space or tab indent. Floor to indent LEVELS.
|
|
193
|
+
$indents = [int][math]::Floor($spaces / 2)
|
|
194
|
+
if ($indents -ge 3) { # 3+ levels of nesting
|
|
195
|
+
$nestHits++
|
|
196
|
+
if ($indents -gt $maxNest) { $maxNest = $indents }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if ($nestHits -ge 2) {
|
|
200
|
+
$vibeFlags.Add("- ARROW CODE: $nestHits control-flow lines at >= 3 levels of nesting (max level: $maxNest). The model nests because it cannot combine conditions. Stop: GUARD CLAUSES (early returns). If a condition does not hold, return immediately; the body must read top-to-bottom without deep indent.")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# (5b) SYMPTOM MASKING: empty catch, catch-and-swallow, or value-nullish-coalesced
|
|
204
|
+
# instead of fixing the source of null. The model patches the symptom (catch it,
|
|
205
|
+
# default it) instead of fixing why the value is invalid.
|
|
206
|
+
# - `catch {}` / `catch (e) {}` with no body OR a bare return/log
|
|
207
|
+
# - `x ?? defaultValue` / `x || default` on what should be a fail-fast path
|
|
208
|
+
# - `try { ... } catch { return ... }` swallowing an error
|
|
209
|
+
# Stop: FAIL-FAST. If a function receives invalid state, throw an explicit Error;
|
|
210
|
+
# never catch an error only to hide it.
|
|
211
|
+
$swallowRe = '(?i)catch\s*(\([^)]*\))?\s*\{\s*(?://[^\n]*)?(?:\}|return\s|//\s*ignore|/\*.*\*/\s*\})'
|
|
212
|
+
# Note: no leading \b - the ?? operator is not a word boundary.
|
|
213
|
+
$coalesceRe = '\?\?\s*(?:null|undefined|0|''''|""|\[\]|\{\}|false|new\b)'
|
|
214
|
+
$maskingHits = 0
|
|
215
|
+
foreach ($a in $added) {
|
|
216
|
+
if ($a -match $swallowRe) { $maskingHits++; continue }
|
|
217
|
+
if ($a -match $coalesceRe) { $maskingHits++ }
|
|
218
|
+
}
|
|
219
|
+
if ($maskingHits -ge 1) {
|
|
220
|
+
$vibeFlags.Add("- SYMPTOM MASKING: $maskingHits instance(s) of empty/swallowed catch or nullish-coalesced fallback detected. The model patches the symptom (`x ?? default`, `catch {} return`) instead of fixing WHY the value is invalid. Stop: FAIL-FAST. If a function receives invalid state, throw an explicit Error; never catch only to hide, never default over a value that should never be null.")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# (5c) BOOLEAN TRAP: a function that takes a positional boolean to switch behavior.
|
|
224
|
+
# `processData(data, true)` is a classic - the model uses one function + a flag
|
|
225
|
+
# instead of two functions or a strategy. Stop: no boolean flags; split the
|
|
226
|
+
# function or use a strategy/dictionary dispatch.
|
|
227
|
+
$boolParamRe = '(?i)\bfunction\s+\w+\s*\([^)]*\b(is[A-Z]|should[A-Z]|with[A-Z]|use[A-Z]|enable[A-Z]|disable[A-Z]|force[A-Z]|skip[A-Z]|verbose|dryRun|async|strict|deep|quick)\b\s*[:=]\s*(?:boolean|bool|true|false)[^)]*\)'
|
|
228
|
+
$arrowBoolRe = '(?i)(?:const|let|var|func|def|fn)\s+\w+\s*\([^)]*\b(is[A-Z]|should[A-Z]|with[A-Z]|use[A-Z]|enable[A-Z]|disable[A-Z]|force[A-Z]|skip[A-Z]|verbose|dryRun|strict|deep|quick)\??\s*[:=]\s*(?:boolean|bool|true|false)\b[^)]*\)'
|
|
229
|
+
$boolHits = 0
|
|
230
|
+
foreach ($a in $added) {
|
|
231
|
+
if ($a -match $boolParamRe) { $boolHits++; continue }
|
|
232
|
+
if ($a -match $arrowBoolRe) { $boolHits++ }
|
|
233
|
+
}
|
|
234
|
+
if ($boolHits -ge 1) {
|
|
235
|
+
$vibeFlags.Add("- BOOLEAN TRAP: $boolHits function(s) take a positional boolean to switch behavior (is*/should*/with*/use*/enable*/force*/skip*/verbose/dryRun/strict/deep/quick). The model uses a flag instead of two functions. Stop: SPLIT the function or use a strategy/dictionary dispatch. If the behavior changes, it is a different function.")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# (5d) SWITCH BLOAT: a switch or if/else-if chain with > 5 cases. The model maps
|
|
239
|
+
# states via switch instead of a dictionary. Stop: replace with a Record<Map, Fn>
|
|
240
|
+
# / dictionary dispatch (Map<Estado, Funcion>), never a switch with > 5 arms.
|
|
241
|
+
$switchRe = '(?i)\bswitch\b'
|
|
242
|
+
$caseRe = '(?i)^\s*(?:case\s|default\s*:|when\s)'
|
|
243
|
+
$elseifRe = '(?i)^\s*(?:}\s*)?else\s+if\b'
|
|
244
|
+
$caseCount = 0
|
|
245
|
+
$inSwitch = $false
|
|
246
|
+
foreach ($a in $added) {
|
|
247
|
+
if ($a -match $switchRe) { $inSwitch = $true; continue }
|
|
248
|
+
if ($inSwitch -and $a -match $caseRe) { $caseCount++; continue }
|
|
249
|
+
if ($a -match $elseifRe) { $caseCount++ }
|
|
250
|
+
}
|
|
251
|
+
if ($caseCount -ge 6) {
|
|
252
|
+
$vibeFlags.Add("- SWITCH BLOAT: $caseCount case/branch arms detected in one switch or if/else-if chain. The model maps states via switch. Stop: DICTIONARY DISPATCH - a Record<Estado, Funcion> / Map<state, handler> lookup, never a switch with > 5 arms.")
|
|
253
|
+
}
|
|
254
|
+
|
|
175
255
|
# --- decide whether to fire ----------------------------------------------
|
|
176
256
|
$srcRe = '\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|kts|cs|cpp|cc|cxx|c|h|hpp|rb|php|swift|scala|m|mm|sh|ps1|lua|dart|ex|exs|vue|svelte)$'
|
|
177
257
|
$addedCode = 0
|
|
@@ -184,6 +264,7 @@ if ($depAdded) { $flags.Add("- DEPENDENCY: " + $base + " gained a d
|
|
|
184
264
|
if ($patterns.Count -gt 0) { $flags.Add("- PREMATURE ABSTRACTION: " + ($patterns -join ', ') + " - is there a real, present problem (2-3+ call sites that exist today) that needs it? If it is speculative, delete it and write the direct code.") }
|
|
185
265
|
if ($redundant.Count -gt 0) { $flags.Add("- REDUNDANT COMMENTS: " + ($redundant -join ' | ') + " - delete comments that restate the code; keep only WHY.") }
|
|
186
266
|
$flags.AddRange($opsFlags)
|
|
267
|
+
$flags.AddRange($vibeFlags)
|
|
187
268
|
|
|
188
269
|
if ($flags.Count -eq 0 -and -not $substantial) { exit 0 }
|
|
189
270
|
|
|
@@ -193,13 +274,19 @@ $checklist = ''
|
|
|
193
274
|
if (Test-Path -LiteralPath $checklistFile) { $checklist = Get-Content -Raw -LiteralPath $checklistFile }
|
|
194
275
|
if (-not $checklist) {
|
|
195
276
|
$checklist = @'
|
|
196
|
-
ANTI-SLOP — read ~/.agents/hooks/anti-slop.md (
|
|
277
|
+
ANTI-SLOP — read ~/.agents/hooks/anti-slop.md (17 items). Fallback if missing:
|
|
197
278
|
1–10: edge cases, duplication, conventions, deps, premature abstraction,
|
|
198
279
|
accidental complexity, tests (no tautologies), cargo cult, architecture,
|
|
199
280
|
redundant comments / prompt residue.
|
|
200
281
|
11: semantic contracts (behavior change without name/signature change).
|
|
201
282
|
12: operational slop (retry w/o backoff, await-in-loop, telemetry spam).
|
|
202
283
|
13: change surface (too many files for a simple request).
|
|
284
|
+
14: SLAP - one function one abstraction level.
|
|
285
|
+
15: phantom state - explicit lifecycle, no implicit call-order.
|
|
286
|
+
16: primitive obsession - value objects over loose primitives with rules.
|
|
287
|
+
17: loop-driven - prefer map/filter/reduce over imperative for on arrays.
|
|
288
|
+
Static vibe-coding signals also flagged inline when detected: arrow code,
|
|
289
|
+
symptom masking, boolean trap, switch bloat.
|
|
203
290
|
Fix guilty items now. Never revert what the user asked for.
|
|
204
291
|
'@
|
|
205
292
|
}
|
|
@@ -50,5 +50,29 @@ Do not explain. If clean, say nothing.
|
|
|
50
50
|
13. CHANGE SURFACE (Tier 5) — Did a simple request touch many files? Every
|
|
51
51
|
file in the diff must trace to the task. Trim unrelated hunks.
|
|
52
52
|
|
|
53
|
+
14. MIXED LEVELS OF ABSTRACTION (SLAP violation) — Does one function mix a DB
|
|
54
|
+
call, a string validation, and a date format? The model writes one blob
|
|
55
|
+
because it cannot see the layers. Stop: SLAP - one function, one level of
|
|
56
|
+
abstraction. Extract details to named helpers; the top function reads as a
|
|
57
|
+
recipe, the bottom functions do the work.
|
|
58
|
+
|
|
59
|
+
15. PHANTOM STATE (temporal coupling) — Must callers invoke init() before
|
|
60
|
+
process()? Does the function break unless something else ran first? The
|
|
61
|
+
model never validates lifecycle. Stop: make state explicit - a state
|
|
62
|
+
machine, or a guard at the top of every public method that throws if the
|
|
63
|
+
object is not in the required state. No implicit call-order contracts.
|
|
64
|
+
|
|
65
|
+
16. PRIMITIVE OBSESSION — Are you passing loose strings/numbers for things
|
|
66
|
+
that have rules (userId: string, email: string, amount: number)? The model
|
|
67
|
+
spreads primitives instead of value objects. Stop: if a primitive has
|
|
68
|
+
domain rules (validation, formatting, equality semantics), it is a named
|
|
69
|
+
type / value object, not a raw string. Stop at the boundary where it enters.
|
|
70
|
+
|
|
71
|
+
17. LOOP-DRIVEN LOGIC (imperative where declarative fits) — Are you writing
|
|
72
|
+
for loops to transform arrays when the language has .map / .filter /
|
|
73
|
+
.reduce / list comprehensions? The model defaults to imperative mutation.
|
|
74
|
+
Stop: prefer pure higher-order functions for data transformation; reserve
|
|
75
|
+
for-loops for genuine early-exit, index-based, or perf-critical paths.
|
|
76
|
+
|
|
53
77
|
Hard constraints: never revert what the USER asked for — slop is what got added
|
|
54
78
|
on top. At most a few targeted edits, then stop.
|
|
@@ -39,6 +39,45 @@ function Get-HooksPendingDir {
|
|
|
39
39
|
return Join-Path $HOME '.cursor\.hooks-pending'
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
# SHA-256 hex of a string. SHARED so intent-precompile (beforeSubmitPrompt) and
|
|
43
|
+
# intent-anchor (postToolUse) hash the SAME text the SAME way - otherwise the two
|
|
44
|
+
# disagree on the contract's _intent_hash and the postToolUse hook needlessly
|
|
45
|
+
# regenerates the scope the prompt hook just wrote.
|
|
46
|
+
function Get-Sha256Hex([string]$text) {
|
|
47
|
+
if ($null -eq $text) { $text = '' }
|
|
48
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
|
49
|
+
$hasher = [System.Security.Cryptography.SHA256]::Create()
|
|
50
|
+
return (-join ($hasher.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Path where the beforeSubmitPrompt hook stashes the verbatim user prompt for the
|
|
54
|
+
# turn, keyed by conversation. intent-anchor PREFERS this over transcript parsing:
|
|
55
|
+
# it is the ground-truth request captured directly from the payload (no <user_query>
|
|
56
|
+
# extraction, no hook-followup contamination, available on the FIRST postToolUse
|
|
57
|
+
# of the turn instead of whenever the transcript happens to become readable).
|
|
58
|
+
function Get-CurrentPromptPath([string]$cid) {
|
|
59
|
+
return Join-Path (Get-HooksPendingDir) "current-prompt-$cid.txt"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Read the stashed prompt for this conversation ('' if none). The stash only ever
|
|
63
|
+
# holds real human prompts - intent-precompile filters hook-generated submits.
|
|
64
|
+
function Get-StashedPrompt([string]$cid) {
|
|
65
|
+
$p = Get-CurrentPromptPath $cid
|
|
66
|
+
if (Test-Path -LiteralPath $p) {
|
|
67
|
+
$t = Get-Content -LiteralPath $p -Raw -ErrorAction SilentlyContinue
|
|
68
|
+
if ($t) { return $t.TrimEnd("`r", "`n") }
|
|
69
|
+
}
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Default acceptance the hook seeds so .scope.json NEVER ships a bare "<TODO>"
|
|
74
|
+
# placeholder (the thing that looks broken and never gets filled). It is a real,
|
|
75
|
+
# verifiable bar derived from intent; the agent sharpens it to the single
|
|
76
|
+
# deterministic check. Kept in ONE place so both hooks emit the identical string.
|
|
77
|
+
function Get-DefaultAcceptance {
|
|
78
|
+
return 'Every change traces to intent; the project typecheck/build and any *.selfcheck pass, and the described problem no longer reproduces. (Sharpen to the one deterministic check.)'
|
|
79
|
+
}
|
|
80
|
+
|
|
42
81
|
function Test-IsCursorConfigPath([string]$path) {
|
|
43
82
|
if (-not $path) { return $false }
|
|
44
83
|
return ($path -match '(^|[\\/])\.cursor([\\/]|$)')
|
|
@@ -76,16 +76,21 @@ if (Test-Path $latch) {
|
|
|
76
76
|
# Already injected this turn -> quiet. Latch cleared at every stop.
|
|
77
77
|
if (Test-Path $latch) { exit 0 }
|
|
78
78
|
|
|
79
|
-
# --- current request
|
|
80
|
-
|
|
79
|
+
# --- current request ---------------------------------------------------------
|
|
80
|
+
# PREFER the prompt stashed by intent-precompile (beforeSubmitPrompt): it is the
|
|
81
|
+
# verbatim request captured straight from the payload, available on the FIRST
|
|
82
|
+
# postToolUse of the turn and immune to <user_query> contamination. Fall back to
|
|
83
|
+
# transcript parsing only when the prompt hook did not run (older Cursor without
|
|
84
|
+
# beforeSubmitPrompt, or a sandboxed run). Both sources share Get-Sha256Hex so
|
|
85
|
+
# the contract's _intent_hash is consistent across the two hooks.
|
|
86
|
+
$currentQuery = Get-StashedPrompt $cid
|
|
87
|
+
if ([string]::IsNullOrWhiteSpace($currentQuery)) { $currentQuery = Get-LastUserQuery $obj }
|
|
81
88
|
$hasQuery = -not [string]::IsNullOrWhiteSpace($currentQuery)
|
|
82
89
|
|
|
83
90
|
$currentHash = ''
|
|
84
91
|
$promptChanged = $false
|
|
85
92
|
if ($hasQuery) {
|
|
86
|
-
$
|
|
87
|
-
$hasher = [System.Security.Cryptography.SHA256]::Create()
|
|
88
|
-
$currentHash = -join ($hasher.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') })
|
|
93
|
+
$currentHash = Get-Sha256Hex $currentQuery
|
|
89
94
|
$prevHash = ''
|
|
90
95
|
if (Test-Path $hashFile) { $prevHash = (Get-Content $hashFile -Raw -ErrorAction SilentlyContinue).Trim() }
|
|
91
96
|
$promptChanged = ($currentHash -ne $prevHash)
|
|
@@ -180,10 +185,11 @@ if ($shouldCreate -or $shouldRegen) {
|
|
|
180
185
|
try {
|
|
181
186
|
$intentVal = $currentQuery
|
|
182
187
|
$traceQuery = $currentQuery
|
|
188
|
+
$defaultAcceptance = Get-DefaultAcceptance
|
|
183
189
|
$scaffold = [ordered]@{
|
|
184
190
|
intent = $intentVal
|
|
185
191
|
files = @()
|
|
186
|
-
acceptance =
|
|
192
|
+
acceptance = $defaultAcceptance
|
|
187
193
|
allow_growth = $false
|
|
188
194
|
trace = [ordered]@{ query = $traceQuery; ts = (Get-Date).ToString('o') }
|
|
189
195
|
_intent_hash = $currentHash
|
|
@@ -192,7 +198,7 @@ if ($shouldCreate -or $shouldRegen) {
|
|
|
192
198
|
$json = $scaffold | ConvertTo-Json -Depth 8
|
|
193
199
|
[System.IO.File]::WriteAllText($scopePath, $json, [System.Text.UTF8Encoding]::new($false))
|
|
194
200
|
$scopeIntent = $intentVal
|
|
195
|
-
$scopeAcceptance =
|
|
201
|
+
$scopeAcceptance = $defaultAcceptance
|
|
196
202
|
$scopeFiles = '(auto-tracked - the scope hook records every file you edit)'
|
|
197
203
|
$scopeExists = $true
|
|
198
204
|
$scopeStale = $false
|
|
@@ -225,6 +231,15 @@ if ($needsHeal -and -not $regenerated) {
|
|
|
225
231
|
# to scaffold from), or re-injecting an existing current contract.
|
|
226
232
|
$queryLine = if ($hasQuery) { $currentQuery } else { '(current request unavailable - no transcript in this event)' }
|
|
227
233
|
|
|
234
|
+
# acceptance is seeded with a real default (never a bare <TODO>), but the default
|
|
235
|
+
# is generic - the contract is not "done being set up" until the agent sharpens it
|
|
236
|
+
# to the ONE deterministic check. Detect the unsharpened state and make it a loud,
|
|
237
|
+
# repeated demand each turn until the agent replaces it.
|
|
238
|
+
$acceptanceUnsharpened = ($scopeAcceptance -match '^\s*<TODO' -or $scopeAcceptance -eq (Get-DefaultAcceptance) -or $scopeAcceptance -match 'Sharpen to the one deterministic check')
|
|
239
|
+
$acceptanceDemand = if ($acceptanceUnsharpened) {
|
|
240
|
+
"`n`n>> acceptance is still the seeded default. Your FIRST action this turn is a targeted string-replace on .scope.json setting acceptance to the one deterministic check that decides done - then do the work."
|
|
241
|
+
} else { '' }
|
|
242
|
+
|
|
228
243
|
if ($regenerated) {
|
|
229
244
|
$msg = @"
|
|
230
245
|
INTENT ANCHOR (scope regenerated) - .scope.json written for this prompt.
|
|
@@ -235,9 +250,9 @@ INTENT ANCHOR (scope regenerated) - .scope.json written for this prompt.
|
|
|
235
250
|
|
|
236
251
|
The hook wrote a fresh scaffold to $scopePath from your current request. intent
|
|
237
252
|
is locked from what you just asked. files[] is AUTO-TRACKED - the scope hook
|
|
238
|
-
records every file you edit, so do not maintain it by hand.
|
|
239
|
-
the one deterministic check
|
|
240
|
-
be re-injected every turn until your request changes again
|
|
253
|
+
records every file you edit, so do not maintain it by hand. acceptance is seeded
|
|
254
|
+
with a sensible default; sharpen it to the one deterministic check, THEN proceed.
|
|
255
|
+
This contract will be re-injected every turn until your request changes again.$acceptanceDemand
|
|
241
256
|
"@
|
|
242
257
|
} elseif (-not $scopeExists -or $scopeHollow) {
|
|
243
258
|
$state = if ($scopeHollow) { "the .scope.json in $root is only a <TODO> placeholder (the hook could not read your request to fill it)" } else { "no .scope.json found in $root, and the current request was unavailable to scaffold from" }
|
|
@@ -271,7 +286,7 @@ INTENT ANCHOR (re-injected this turn from .scope.json) - your contract. Do not d
|
|
|
271
286
|
acceptance: $scopeAcceptance
|
|
272
287
|
|
|
273
288
|
$driftNote If a constraint above conflicts with what you are about to do, stop
|
|
274
|
-
and reconcile - the contract outranks momentum
|
|
289
|
+
and reconcile - the contract outranks momentum.$acceptanceDemand
|
|
275
290
|
"@
|
|
276
291
|
}
|
|
277
292
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# intent-precompile.ps1 - beforeSubmitPrompt "contract first" writer (Cursor).
|
|
2
|
+
#
|
|
3
|
+
# THE FIX for "el .scope.json se crea casi al final": creation used to live on
|
|
4
|
+
# postToolUse (intent-anchor), which fires only AFTER the agent's first tool and
|
|
5
|
+
# depends on the transcript becoming readable to detect the prompt. Until then the
|
|
6
|
+
# PREVIOUS prompt's intent persisted and the agent worked under it - the scope
|
|
7
|
+
# only flipped to the right intent late in the turn.
|
|
8
|
+
#
|
|
9
|
+
# beforeSubmitPrompt fires "right after the user hits send, before the backend
|
|
10
|
+
# request" - the earliest possible moment, BEFORE the agent's first token - and
|
|
11
|
+
# its payload carries the user's `prompt` DIRECTLY (no <user_query> extraction, no
|
|
12
|
+
# transcript dependency, no hook-followup contamination). So this hook writes
|
|
13
|
+
# .scope.json with the real intent up front, making the contract the FIRST artifact
|
|
14
|
+
# of the turn, which the agent then governs by.
|
|
15
|
+
#
|
|
16
|
+
# It does TWO things, both deterministic:
|
|
17
|
+
# 1. STASH the verbatim prompt to current-prompt-<cid>.txt. intent-anchor prefers
|
|
18
|
+
# this over transcript parsing, so both hooks hash the SAME text and never
|
|
19
|
+
# fight over _intent_hash.
|
|
20
|
+
# 2. WRITE / REGENERATE .scope.json from the prompt when it is new (hash differs)
|
|
21
|
+
# or missing. Same prompt as on disk -> leave it (preserve the agent's refined
|
|
22
|
+
# intent / acceptance / files within the turn). acceptance is seeded with a
|
|
23
|
+
# real default (Get-DefaultAcceptance), never a bare <TODO>.
|
|
24
|
+
#
|
|
25
|
+
# Hook-generated submits (final-review / subagent-review followups that Cursor
|
|
26
|
+
# auto-submits) are SKIPPED: their prompt is review boilerplate, not the user's
|
|
27
|
+
# request - stashing or regenerating from them is the contamination loop.
|
|
28
|
+
#
|
|
29
|
+
# Never blocks submission (no `continue:false`); writes files as a side effect and
|
|
30
|
+
# exits 0. Disable: HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0 (shares the
|
|
31
|
+
# intent-anchor kill switch - they are one subsystem).
|
|
32
|
+
|
|
33
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
34
|
+
. "$PSScriptRoot\hook-common.ps1"
|
|
35
|
+
|
|
36
|
+
if ($env:HOOKS_ENFORCE -eq '0' -or $env:INTENT_ANCHOR_ENFORCE -eq '0') { exit 0 }
|
|
37
|
+
|
|
38
|
+
$obj = Read-HookStdinJson
|
|
39
|
+
if (-not $obj) { exit 0 }
|
|
40
|
+
|
|
41
|
+
# --- the prompt (direct from payload - the whole point of this event) --------
|
|
42
|
+
$prompt = ''
|
|
43
|
+
if ($obj.PSObject.Properties['prompt']) { $prompt = [string]$obj.prompt }
|
|
44
|
+
$prompt = $prompt.Trim()
|
|
45
|
+
if ([string]::IsNullOrWhiteSpace($prompt)) { exit 0 }
|
|
46
|
+
|
|
47
|
+
# Auto-submitted hook followups (FINAL REVIEW / SUBAGENT / SELF-REVIEW / INTENT
|
|
48
|
+
# ANCHOR) are not the user's request. Leave the real contract intact.
|
|
49
|
+
if (Test-IsHookGeneratedQuery $prompt) { exit 0 }
|
|
50
|
+
|
|
51
|
+
$prompt = Redact-SecretsFromIntent $prompt
|
|
52
|
+
|
|
53
|
+
$cid = Get-SafeConversationId $obj
|
|
54
|
+
$pendingDir = Get-HooksPendingDir
|
|
55
|
+
|
|
56
|
+
# --- repo root (workspace_roots / cwd; NO $HOME fallback - no ghost files) ----
|
|
57
|
+
$root = ''
|
|
58
|
+
$cands = @()
|
|
59
|
+
if ($obj.PSObject.Properties['cwd'] -and $obj.cwd) { $cands += [string]$obj.cwd }
|
|
60
|
+
if ($obj.PSObject.Properties['workspace_roots']) { foreach ($w in $obj.workspace_roots) { $cands += [string]$w } }
|
|
61
|
+
foreach ($c in $cands) { $f = ConvertTo-FwdPath $c; if ($f -and (Test-Path -LiteralPath $f)) { $root = $f.TrimEnd('/'); break } }
|
|
62
|
+
if (-not $root -and $env:CURSOR_PROJECT_DIR) {
|
|
63
|
+
$cpd = $env:CURSOR_PROJECT_DIR.Replace('\', '/').TrimEnd('/')
|
|
64
|
+
if (Test-Path -LiteralPath $cpd) { $root = $cpd }
|
|
65
|
+
}
|
|
66
|
+
if (-not $root) { exit 0 }
|
|
67
|
+
|
|
68
|
+
# --- stash the prompt so intent-anchor reads the same ground-truth text -------
|
|
69
|
+
try {
|
|
70
|
+
New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
|
|
71
|
+
[System.IO.File]::WriteAllText((Get-CurrentPromptPath $cid), $prompt, [System.Text.UTF8Encoding]::new($false))
|
|
72
|
+
} catch { }
|
|
73
|
+
|
|
74
|
+
# --- write / regenerate .scope.json (hash-gated) ------------------------------
|
|
75
|
+
$currentHash = Get-Sha256Hex $prompt
|
|
76
|
+
$scopePath = Join-Path $root '.scope.json'
|
|
77
|
+
$onDiskHash = ''
|
|
78
|
+
if (Test-Path -LiteralPath $scopePath) {
|
|
79
|
+
try {
|
|
80
|
+
$sj = Get-Content -LiteralPath $scopePath -Raw | ConvertFrom-Json
|
|
81
|
+
if ($sj.PSObject.Properties['_intent_hash']) { $onDiskHash = [string]$sj._intent_hash }
|
|
82
|
+
} catch { $onDiskHash = '' } # malformed -> regenerate
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Same prompt already locked (by a prior fire this turn, or by the agent who may
|
|
86
|
+
# have refined intent/acceptance/files) -> leave it. Only (re)write on a NEW or
|
|
87
|
+
# missing/garbage contract.
|
|
88
|
+
if ($onDiskHash -eq $currentHash) { exit 0 }
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
$scaffold = [ordered]@{
|
|
92
|
+
intent = $prompt
|
|
93
|
+
files = @()
|
|
94
|
+
acceptance = (Get-DefaultAcceptance)
|
|
95
|
+
allow_growth = $false
|
|
96
|
+
trace = [ordered]@{ query = $prompt; ts = (Get-Date).ToString('o') }
|
|
97
|
+
_intent_hash = $currentHash
|
|
98
|
+
_generated_by = 'intent-precompile hook (beforeSubmitPrompt)'
|
|
99
|
+
}
|
|
100
|
+
$json = $scaffold | ConvertTo-Json -Depth 8
|
|
101
|
+
[System.IO.File]::WriteAllText($scopePath, $json, [System.Text.UTF8Encoding]::new($false))
|
|
102
|
+
} catch { }
|
|
103
|
+
|
|
104
|
+
exit 0
|
package/windows/hooks.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"hooks": {
|
|
4
|
+
"beforeSubmitPrompt": [
|
|
5
|
+
{
|
|
6
|
+
"command": "pwsh.exe -NoProfile -File ~/.agents/hooks/intent-precompile.ps1",
|
|
7
|
+
"timeout": 5,
|
|
8
|
+
"_comment": "5s: CONTRACT FIRST. Fires right after the user hits send, BEFORE the agent's first token, with the user's `prompt` in the payload directly. Writes/regenerates .scope.json (intent locked from the prompt, acceptance seeded with a real default - never a bare <TODO>) so the contract is the FIRST artifact of the turn instead of appearing late once postToolUse can read the transcript. Also stashes the verbatim prompt to current-prompt-<cid>.txt so intent-anchor (postToolUse) re-injects from the SAME ground-truth text - no <user_query> contamination, no _intent_hash fights. Skips hook-generated auto-submits (FINAL REVIEW / SUBAGENT / SELF-REVIEW). Hash-gated: same prompt on disk -> left intact (preserves the agent's refined intent/acceptance/files). Never blocks submission, always exits 0. Disable: HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0."
|
|
9
|
+
}
|
|
10
|
+
],
|
|
4
11
|
"sessionStart": [
|
|
5
12
|
{
|
|
6
13
|
"command": "pwsh.exe -NoProfile -File ~/.cursor/inject-doctrine.ps1",
|
package/windows/pre-compile.md
CHANGED
|
@@ -53,23 +53,30 @@ Answer these four, terse, in your first response. One phrase each, not prose:
|
|
|
53
53
|
|
|
54
54
|
## Materialize it: .scope.json (the hook owns this file)
|
|
55
55
|
|
|
56
|
-
The
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
The contract is written for you **before your first token**: the `intent-precompile`
|
|
57
|
+
hook fires on `beforeSubmitPrompt` (right after the user hits send) and writes
|
|
58
|
+
`.scope.json` to the repo root with the real `intent` already locked from the
|
|
59
|
+
request — so the contract is the FIRST artifact of the turn, and you govern by it
|
|
60
|
+
from the very first action. `intent-anchor` then re-injects it on every tool
|
|
61
|
+
boundary to keep it in focus.
|
|
62
|
+
- `intent` is locked from the request and REFRESHED when the request changes — a
|
|
63
|
+
new prompt regenerates the contract and resets `files[]`, so it never carries
|
|
64
|
+
over between features;
|
|
61
65
|
- `files[]` is auto-recorded — the scope hook appends every file you edit, so
|
|
62
66
|
you never maintain it by hand;
|
|
67
|
+
- `acceptance` is SEEDED with a real default (never a bare `<TODO>`); it is not a
|
|
68
|
+
blank you must fill, it is a draft you SHARPEN;
|
|
63
69
|
- `trace.query` is the VERBATIM request (the audit anchor), `_intent_hash` and
|
|
64
70
|
`_generated_by` are hook bookkeeping. Leave all three alone.
|
|
65
71
|
|
|
66
72
|
Your two targeted edits on the contract (each a string replace on ONE field, never
|
|
67
|
-
a whole-file rewrite):
|
|
68
|
-
- **`intent`** → replace the
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
- **`acceptance`** →
|
|
72
|
-
the hook cannot derive.
|
|
73
|
+
a whole-file rewrite), done as your FIRST actions this turn before editing source:
|
|
74
|
+
- **`intent`** → replace the seed with your Step 0 restatement: the normalized,
|
|
75
|
+
meaning-preserving sentence. This is what final-review axis 0 traces each diff
|
|
76
|
+
hunk against, so a clean `intent` makes the audit sharp.
|
|
77
|
+
- **`acceptance`** → sharpen the seeded default to the single deterministic check
|
|
78
|
+
that decides done, which the hook cannot derive. The hook re-injects a loud
|
|
79
|
+
demand every turn until you do.
|
|
73
80
|
|
|
74
81
|
Do **NOT** touch `trace.query`, `_intent_hash`, or `_generated_by`, and do **NOT**
|
|
75
82
|
rewrite the whole file: `_intent_hash` is computed from the verbatim `trace.query`,
|
|
@@ -82,11 +89,11 @@ changed the meaning: `intent` and `trace.query` must agree.
|
|
|
82
89
|
{
|
|
83
90
|
"intent": "<YOU refine this: your normalized Step 0 restatement>",
|
|
84
91
|
"files": ["<auto-recorded by the hook as you edit>"],
|
|
85
|
-
"acceptance": "<YOU
|
|
92
|
+
"acceptance": "<seeded with a default; YOU sharpen to the deterministic check>",
|
|
86
93
|
"allow_growth": false,
|
|
87
94
|
"trace": { "query": "<VERBATIM request - the hook owns this, leave it>", "ts": "<when>" },
|
|
88
95
|
"_intent_hash": "<hook bookkeeping>",
|
|
89
|
-
"_generated_by":"intent-anchor hook"
|
|
96
|
+
"_generated_by":"intent-precompile / intent-anchor hook"
|
|
90
97
|
}
|
|
91
98
|
```
|
|
92
99
|
|