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 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-anchor: writes .scope.json from the current prompt and re-injects it.
80
- # Needs a repo root in cwd (it will NOT write to $HOME - that's the guard).
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
- cat /tmp/.scope.json # expect a JSON scaffold with intent placeholder
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-anchor: writes .scope.json from the current prompt and re-injects it.
101
- # Needs a repo root in cwd (it will NOT write to $HOME - that's the guard).
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
- Get-Content C:\tmp\.scope.json # expect a JSON scaffold with intent placeholder
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. Have the agent make a small edit to a tracked file. On the next turn it should receive a `SELF-REVIEW TRIGGER` message, and `intent-anchor` should have written `.scope.json` to the repo root (intent from your prompt, `<TODO>` placeholders for files/acceptance). The scaffold regenerates when your prompt changes and is re-injected every turn.
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>Five 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>
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-anchor` materializes that as `.scope.json` on the first tool of each turn (auto-created, regenerated when the prompt changes) and re-injects it into context every turn, so the contract stays in focus against Salience Dilution.
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
- On the **first tool of each agent turn**, `intent-anchor` materializes the Anchor Set: it writes `.scope.json` to the repo root (regenerating it when the prompt changed) and re-injects it into context. One scaffold per prompt, re-injection per turn. The 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.
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
- ### Keeping the contract alive: `intent-anchor` (anti-Salience-Dilution)
97
+ ### Contract first, then kept alive: `intent-precompile` + `intent-anchor`
98
98
 
99
- Writing `.scope.json` once is not enough. As a conversation fills with code, logs and errors, the token of the original request shrinks to a rounding error against the recent history — *Salience Dilution* — and the agent stops checking the contract it wrote at prompt 1. It forgets symmetry, colors, the acceptance bar. Re-injecting the contract every turn is what defeats this: the requirements are re-stated in front of the model before each edit, not just written once and hoped-for.
99
+ The contract has to exist *before* the agent edits and stay in focus *as* it edits. Two hooks, two jobs:
100
100
 
101
- `intent-anchor` (`postToolUse`, registered first so it runs before `post-tool-use` drains the bus) does two things on the **first tool boundary of every turn** (per-turn latch, cleared unconditionally at each stop):
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
- 1. **Materialize the contract per prompt.** When the current `<user_query>` differs from the contract on disk (no contract yet, or its recorded `_intent_hash` doesn't match), the hook **writes a fresh `.scope.json` to the repo root**: `intent` locked from the prompt, `files`/`acceptance` as `<TODO>` placeholders the agent fills in. Every new prompt a fresh contract the agent works from. Same prompt no rewrite.
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 hook writes `.scope.json` deliberately.** This is the intended behavior: the contract must exist *before* the agent edits, and it must track the request. The agent fills the `<TODO>` placeholders (`files`, `acceptance`) on the first turn; the hook regenerates the scaffold when the prompt changes. Two real bugs in the earlier 0.4.4 build are fixed here: (a) **never writes to `$HOME`** — if the repo `cwd` can't be resolved the hook stays silent rather than drop a ghost file (bail instead of fallback); (b) **regenerates on prompt CHANGE, not on every turn** — staleness is tracked via `_intent_hash` stored in the file itself, so a same-prompt turn re-injects without rewriting.
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 five flows
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. If the prompt changed since last turn, it demands the contract be updated. Then `post-tool-use` folds subagent markers and drains the feedback file. |
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-anchor` (`postToolUse`) scaffolds `.scope.json` per prompt and 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. |
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
@@ -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 auto-creates `.scope.json` in the repo root on your first tool of
10
- each turn, and re-injects it into your context every turn. Treat it as your
11
- operating contract, not optional:
12
- - On a fresh scaffold, FILL the `intent` and `acceptance` TODOs from the user's
13
- request before editing source. `files[]` is auto-tracked - do not maintain it.
14
- - When the user's request changes, the scaffold regenerates with a new intent -
15
- refill it for the new ask.
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 (best-effort; absent in sandboxed runs) -----------------
78
- current_query="$(extract_last_user_query "$input")"
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
- if command -v sha256sum >/dev/null 2>&1; then
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
- # jq preferred; python3 fallback. Write intent, empty files[], TODO acceptance,
200
- # trace provenance, and _intent_hash so staleness is self-contained.
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:"<TODO: the one deterministic check that decides done>", allow_growth:false, trace:{query:$tq, ts:$ts}, _intent_hash:$hash, _generated_by:"intent-anchor hook"}' \
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": "<TODO: the one deterministic check that decides done>",
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="<TODO: the one deterministic check that decides done>"
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. Set acceptance to
284
- the one deterministic check that decides done, THEN proceed. This contract will
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",
@@ -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 `intent-anchor` hook creates and maintains `.scope.json` in the repo root for
57
- you, automatically, on the first tool of every turn:
58
- - `intent` is seeded from your verbatim request and REFRESHED when the request
59
- changesa new prompt regenerates the contract and resets `files[]`, so it
60
- never carries over between features;
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
+ requestso 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 verbatim seed with your Step 0 restatement: the
69
- normalized, meaning-preserving sentence. This is what final-review axis 0
70
- traces each diff hunk against, so a clean `intent` makes the audit sharp.
71
- - **`acceptance`** → set the single deterministic check that decides done, which
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 set this: the deterministic check that decides done>",
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.4",
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"
@@ -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 auto-creates `.scope.json` in the repo root on your first tool of
10
- each turn, and re-injects it into your context every turn. Treat it as your
11
- operating contract, not optional:
12
- - On a fresh scaffold, FILL the `intent` and `acceptance` TODOs from the user's
13
- request before editing source. `files[]` is auto-tracked - do not maintain it.
14
- - When the user's request changes, the scaffold regenerates with a new intent -
15
- refill it for the new ask.
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 (13 items). Fallback if missing:
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 (best-effort; absent in sandboxed runs) -----------------
80
- $currentQuery = Get-LastUserQuery $obj
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
- $bytes = [System.Text.Encoding]::UTF8.GetBytes($currentQuery)
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 = '<TODO: the one deterministic check that decides done>'
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 = '<TODO: the one deterministic check that decides done>'
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. Set acceptance to
239
- the one deterministic check that decides done, THEN proceed. This contract will
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
@@ -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",
@@ -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 `intent-anchor` hook creates and maintains `.scope.json` in the repo root for
57
- you, automatically, on the first tool of every turn:
58
- - `intent` is seeded from your verbatim request and REFRESHED when the request
59
- changesa new prompt regenerates the contract and resets `files[]`, so it
60
- never carries over between features;
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
+ requestso 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 verbatim seed with your Step 0 restatement: the
69
- normalized, meaning-preserving sentence. This is what final-review axis 0
70
- traces each diff hunk against, so a clean `intent` makes the audit sharp.
71
- - **`acceptance`** → set the single deterministic check that decides done, which
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 set this: the deterministic check that decides done>",
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