cursordoctrine 0.6.2 → 0.6.4

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/linux/doctrine.md CHANGED
@@ -69,6 +69,10 @@ what the task requires**.
69
69
 
70
70
  The natural order is:
71
71
 
72
+ 0. **Restate** the request in one normalized line (`Understood as: …`) before
73
+ you touch a tool — grammar fixed, ambiguity resolved, meaning preserved.
74
+ It is the user's catch-point for a misread, and it becomes the contract's
75
+ `intent`. Full spec in pre-compile.
72
76
  1. **Read** the relevant file(s) to understand context.
73
77
  2. **Edit** to make the change.
74
78
  3. **Read** the file again (the self-review trigger will remind you).
@@ -105,7 +105,7 @@ resolve_agent_path() {
105
105
  esac
106
106
  }
107
107
 
108
- # extract_last_user_query <json> -> text of the last <user_query> in this
108
+ # extract_last_user_query <json> -> text of the last *human* <user_query> in this
109
109
  # conversation's transcript, or '' if there is none. Capped at 2000 chars.
110
110
  #
111
111
  # This is the Tier 0 intent-trace primitive: the final-review hook prepends the
@@ -113,7 +113,14 @@ resolve_agent_path() {
113
113
  # to it. Anything untraceable is a hallucinated requirement.
114
114
  #
115
115
  # Walks the JSONL backward via tac (preferred) or a portable awk fallback; finds
116
- # the first (last) user record whose content carries a <user_query> tag.
116
+ # the first (last) user record whose content carries a <user_query> tag - SKIPPING
117
+ # hook-generated turns. final-review.sh / subagent-stop-review.sh emit a
118
+ # {followup_message} that Cursor replays as a user turn (and self-review /
119
+ # intent-anchor inject into additional_context); returning one of those would lock
120
+ # the review boilerplate into .scope.json as the intent (the contamination loop).
121
+ # Hook turns are detected by their fixed headers; if the transcript has been
122
+ # trimmed to just the hook turn, recover the real request from the embedded
123
+ # "ORIGINAL REQUEST ... --- <request> ---" block.
117
124
  extract_last_user_query() {
118
125
  local json="$1"
119
126
  local tp
@@ -134,6 +141,14 @@ extract_last_user_query() {
134
141
  if have_py; then
135
142
  printf '%s' "$reversed" | python3 -c '
136
143
  import json, re, sys
144
+ HOOK_HDR = re.compile(r"^\s*(FINAL REVIEW \(end of implementation\)|SUBAGENT FINAL REVIEW|SELF-REVIEW|INTENT ANCHOR)", re.M)
145
+ EMBEDDED = re.compile(r"ORIGINAL REQUEST[^\r\n]*\r?\n-{3,}\r?\n(.+?)\r?\n-{3,}", re.S)
146
+ def redact(q):
147
+ q = re.sub(r"\bnpm_[A-Za-z0-9]{10,}\b", "[REDACTED_NPM_TOKEN]", q)
148
+ q = re.sub(r"\b(sk-[A-Za-z0-9]{10,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,})\b", "[REDACTED_TOKEN]", q)
149
+ q = re.sub(r"(?i)(api[_-]?key|token|secret|password)\s*[:=]\s*\S+", r"\1=[REDACTED]", q)
150
+ return q
151
+ embedded_fallback = ""
137
152
  try:
138
153
  for line in sys.stdin:
139
154
  line = line.strip()
@@ -155,15 +170,26 @@ try:
155
170
  if isinstance(p, dict) and p.get("type") == "text" and p.get("text"):
156
171
  text += p["text"]
157
172
  m = re.search(r"<user_query>\s*(.+?)\s*</user_query>", text, re.S)
158
- if m:
159
- q = m.group(1).strip()
160
- if len(q) > 2000:
161
- q = q[:2000] + "..."
162
- q = re.sub(r"\bnpm_[A-Za-z0-9]{10,}\b", "[REDACTED_NPM_TOKEN]", q)
163
- q = re.sub(r"\b(sk-[A-Za-z0-9]{10,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,})\b", "[REDACTED_TOKEN]", q)
164
- q = re.sub(r"(?i)(api[_-]?key|token|secret|password)\s*[:=]\s*\S+", r"\1=[REDACTED]", q)
165
- print(q)
166
- break
173
+ if not m:
174
+ continue
175
+ q = m.group(1).strip()
176
+ # Hook-generated turn -> not the human words. Remember the embedded
177
+ # ORIGINAL REQUEST (latest such turn) and keep walking back.
178
+ if HOOK_HDR.search(q):
179
+ if not embedded_fallback:
180
+ em = EMBEDDED.search(q)
181
+ if em:
182
+ embedded_fallback = em.group(1).strip()
183
+ continue
184
+ if len(q) > 2000:
185
+ q = q[:2000] + "..."
186
+ print(redact(q))
187
+ break
188
+ else:
189
+ if embedded_fallback:
190
+ if len(embedded_fallback) > 2000:
191
+ embedded_fallback = embedded_fallback[:2000] + "..."
192
+ print(redact(embedded_fallback))
167
193
  except Exception:
168
194
  pass
169
195
  ' 2>/dev/null
@@ -172,9 +198,14 @@ except Exception:
172
198
 
173
199
  # No python3: best-effort grep for the common case where the user message
174
200
  # is the only place <user_query> appears in a line. Imperfect but bounded.
201
+ # Drop hook-generated turns (FINAL REVIEW / SUBAGENT / SELF-REVIEW / INTENT
202
+ # ANCHOR) so we never lock review boilerplate as the intent; take the first
203
+ # surviving human <user_query>.
175
204
  printf '%s' "$reversed" |
176
- grep -m1 -oE '<user_query>[^<]*</user_query>' 2>/dev/null |
205
+ grep -oE '<user_query>[^<]*</user_query>' 2>/dev/null |
177
206
  sed -E 's@</?user_query>@@g' |
207
+ grep -vE '^[[:space:]]*(FINAL REVIEW \(end of implementation\)|SUBAGENT FINAL REVIEW|SELF-REVIEW|INTENT ANCHOR)' 2>/dev/null |
208
+ head -n1 |
178
209
  sed -E 's/\bnpm_[A-Za-z0-9]{10,}\b/[REDACTED_NPM_TOKEN]/g' |
179
210
  head -c 2000
180
211
  }
@@ -150,12 +150,14 @@ EOF
150
150
  # changed (or a new session) => regenerate and RESET files[] (the "arrastre entre
151
151
  # features" fix). Same prompt this session => heal in place (backfill bookkeeping, keep
152
152
  # files[]/acceptance) so the NEXT prompt is detected by hash.
153
- # Hollow = no real intent on disk: empty, or still the hook's <TODO> placeholder.
154
- # A hollow contract is worse than none (it looks owned, so neither hook nor agent
155
- # fills it). Treat it as unusable: regenerate when the request is readable, else
156
- # hand the agent the pre-compile demand to author a real one.
153
+ # Hollow = no real intent on disk: empty, the hook's <TODO> placeholder, OR
154
+ # hook-generated review boilerplate that a stale extractor locked in (the
155
+ # contamination loop - "FINAL REVIEW (end of implementation)..."). A hollow
156
+ # contract is worse than none (it looks owned, so neither hook nor agent fills
157
+ # it). Treat it as unusable: regenerate when the request is readable, else hand
158
+ # the agent the pre-compile demand to author a real one.
157
159
  case "$scope_intent" in
158
- ""|"<TODO"*) scope_hollow=1 ;;
160
+ ""|"<TODO"*|"FINAL REVIEW (end of implementation)"*|"SUBAGENT FINAL REVIEW"*|"SELF-REVIEW"*|"INTENT ANCHOR"*) scope_hollow=1 ;;
159
161
  esac
160
162
  if [ "$scope_exists" = "1" ] && [ "$has_query" = "1" ]; then
161
163
  if [ "$scope_hollow" = "1" ]; then
@@ -9,12 +9,34 @@ This is the proactive phase. The anti-slop checklist, the self-review trigger
9
9
  and the final review are reactive — they audit after the fact. You compile the
10
10
  intent BEFORE the first token of code so they have the right thing to audit.
11
11
 
12
+ ## Step 0 — Restate the request (the user writes fast; you normalize)
13
+
14
+ Before the Anchor Set, in ONE line, play the request back as you understood it:
15
+
16
+ > **Understood as:** _one clean sentence — grammar fixed, pronouns resolved,
17
+ > implicit constraints made explicit, in the language of the request._
18
+
19
+ Mandatory every implementation turn. This line is the user's catch-point: a
20
+ misread surfaces HERE, before you write a line of code, instead of in review.
21
+ The restatement is **meaning-preserving** — you normalize phrasing, you do NOT
22
+ add scope, drop a constraint, or invent a requirement. If normalizing would
23
+ force a guess that changes what "correct" means, you do not bury the guess in
24
+ the restatement — you ask one sharp question (§5) and wait.
25
+
26
+ The user's verbatim words stay the ground truth: `.scope.json`'s `trace.query`
27
+ keeps them exactly as typed, and final-review traces every diff hunk back to
28
+ THAT, not to your paraphrase. Your normalized sentence becomes `intent` (and the
29
+ Anchor Set's OBJECTIVE below). The two must say the same thing in different
30
+ words — if you cannot make `intent` and `trace.query` agree, you have misread
31
+ the request, and that is the bug to fix first.
32
+
12
33
  ## The Anchor Set
13
34
 
14
35
  Answer these four, terse, in your first response. One phrase each, not prose:
15
36
 
16
- 1. OBJECTIVE — one operational sentence. What is *strictly* necessary. Not
17
- "improve X" — "make X return Y when Z".
37
+ 1. OBJECTIVE — your Step 0 restatement, tightened to the operational verb. One
38
+ sentence, what is *strictly* necessary. Not "improve X" — "make X return Y
39
+ when Z".
18
40
  2. CONSTRAINTS (local negations) — what you will NOT do. "NO schema migration.
19
41
  NO new dependency. NO refactor of the surrounding function." Negations bind
20
42
  harder than the objective: a constraint that the task contradicts is a bug
@@ -33,28 +55,36 @@ Answer these four, terse, in your first response. One phrase each, not prose:
33
55
 
34
56
  The `intent-anchor` hook creates and maintains `.scope.json` in the repo root for
35
57
  you, automatically, on the first tool of every turn:
36
- - `intent` is locked from your current request and REFRESHED when the request
58
+ - `intent` is seeded from your verbatim request and REFRESHED when the request
37
59
  changes — a new prompt regenerates the contract and resets `files[]`, so it
38
60
  never carries over between features;
39
61
  - `files[]` is auto-recorded — the scope hook appends every file you edit, so
40
62
  you never maintain it by hand;
41
- - `trace`, `_intent_hash`, `_generated_by` are hook bookkeeping (provenance and
42
- change detection). Leave them alone.
43
-
44
- Your one job on the contract: set **`acceptance`** the single deterministic check
45
- that decides done — which the hook cannot derive. Edit ONLY that field (a targeted
46
- string replace). Do **NOT** rewrite the whole file: overwriting it drops the hook's
47
- `_intent_hash`/`trace`, which disables per-prompt regeneration and brings back the
48
- cross-feature carryover. The scope-gate hook audits every edit against `files[]`,
49
- and final-review axis 0 traces every diff hunk back to `intent`.
63
+ - `trace.query` is the VERBATIM request (the audit anchor), `_intent_hash` and
64
+ `_generated_by` are hook bookkeeping. Leave all three alone.
65
+
66
+ 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
+
74
+ Do **NOT** touch `trace.query`, `_intent_hash`, or `_generated_by`, and do **NOT**
75
+ rewrite the whole file: `_intent_hash` is computed from the verbatim `trace.query`,
76
+ not from `intent`, so refining `intent` is safe — but dropping the hash/trace
77
+ disables per-prompt regeneration and brings back cross-feature carryover. Keeping
78
+ `trace.query` verbatim is what lets the audit catch a paraphrase that quietly
79
+ changed the meaning: `intent` and `trace.query` must agree.
50
80
 
51
81
  ```json
52
82
  {
53
- "intent": "<locked from your request by the hook>",
83
+ "intent": "<YOU refine this: your normalized Step 0 restatement>",
54
84
  "files": ["<auto-recorded by the hook as you edit>"],
55
85
  "acceptance": "<YOU set this: the deterministic check that decides done>",
56
86
  "allow_growth": false,
57
- "trace": { "query": "<originating request>", "ts": "<when>" },
87
+ "trace": { "query": "<VERBATIM request - the hook owns this, leave it>", "ts": "<when>" },
58
88
  "_intent_hash": "<hook bookkeeping>",
59
89
  "_generated_by":"intent-anchor hook"
60
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursordoctrine",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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"
@@ -67,6 +67,10 @@ what the task requires**.
67
67
 
68
68
  The natural order is:
69
69
 
70
+ 0. **Restate** the request in one normalized line (`Understood as: …`) before
71
+ you touch a tool — grammar fixed, ambiguity resolved, meaning preserved.
72
+ It is the user's catch-point for a misread, and it becomes the contract's
73
+ `intent`. Full spec in pre-compile.
70
74
  1. **Read** the relevant file(s) to understand context.
71
75
  2. **Edit** to make the change.
72
76
  3. **Read** the file again (the self-review trigger will remind you).
@@ -83,11 +83,36 @@ function Redact-SecretsFromIntent([string]$text) {
83
83
  return $text
84
84
  }
85
85
 
86
- # Extract the last user <user_query> from a Cursor transcript JSONL.
86
+ # A <user_query> turn can actually be a HOOK-GENERATED message replayed by Cursor
87
+ # as a user turn: final-review.ps1 and subagent-stop-review.ps1 emit a
88
+ # {followup_message} that Cursor auto-submits as the next user turn, and the
89
+ # self-review / intent-anchor injections get drained into additional_context.
90
+ # If Get-LastUserQuery returns one of those, intent-anchor locks the REVIEW
91
+ # BOILERPLATE into .scope.json as the "intent" (the contamination loop that put
92
+ # "FINAL REVIEW (end of implementation)..." in the contract). Detect them by the
93
+ # fixed headers the hooks emit and skip past them to the real human turn.
94
+ function Test-IsHookGeneratedQuery([string]$text) {
95
+ if (-not $text) { return $false }
96
+ return ($text -match '(?m)^\s*(FINAL REVIEW \(end of implementation\)|SUBAGENT FINAL REVIEW|SELF-REVIEW|INTENT ANCHOR)')
97
+ }
98
+
99
+ # The final-review / subagent-review followups embed the real human request as:
100
+ # ORIGINAL REQUEST (...):\n---\n<request>\n---
101
+ # When the transcript has been trimmed to just the hook turn, recover the human
102
+ # request from that block instead of returning the boilerplate.
103
+ function Get-EmbeddedOriginalRequest([string]$text) {
104
+ if (-not $text) { return '' }
105
+ if ($text -match '(?s)ORIGINAL REQUEST[^\r\n]*\r?\n-{3,}\r?\n(.+?)\r?\n-{3,}') {
106
+ return $Matches[1].Trim()
107
+ }
108
+ return ''
109
+ }
110
+
111
+ # Extract the last *human* user <user_query> from a Cursor transcript JSONL.
87
112
  # transcript is an array of {role, message} records; we walk backward from the
88
- # end, find the last user turn whose content has a <user_query> tag, and return
89
- # its text. Returns '' if there is no transcript or no user_query. Capped at
90
- # 2000 chars so the follow-up prompt stays bounded.
113
+ # end, skipping hook-generated turns (see above), and return the first real human
114
+ # turn's text. Returns '' if there is no transcript or no human user_query. Capped
115
+ # at 2000 chars so the follow-up prompt stays bounded.
91
116
  #
92
117
  # This is the Tier 0 intent-trace primitive: the final-review hook prepends the
93
118
  # extracted request to its followup so the model must trace every diff hunk back
@@ -97,6 +122,7 @@ function Get-LastUserQuery($obj) {
97
122
  if ($obj -and $obj.PSObject.Properties['transcript_path']) { $tp = [string]$obj.transcript_path }
98
123
  if (-not $tp -or -not (Test-Path -LiteralPath $tp)) { return '' }
99
124
  $lines = @(Get-Content -LiteralPath $tp -ErrorAction SilentlyContinue)
125
+ $embeddedFallback = ''
100
126
  for ($i = $lines.Count - 1; $i -ge 0; $i--) {
101
127
  $line = $lines[$i]
102
128
  if (-not $line -or $line -notmatch '"role"\s*:\s*"user"') { continue }
@@ -117,10 +143,22 @@ function Get-LastUserQuery($obj) {
117
143
  }
118
144
  if ($text -match '(?s)<user_query>\s*(.+?)\s*</user_query>') {
119
145
  $q = $Matches[1].Trim()
146
+ # Hook-generated turn -> not the human's words. Remember the embedded
147
+ # ORIGINAL REQUEST (from the most recent such turn) and keep walking.
148
+ if (Test-IsHookGeneratedQuery $q) {
149
+ if (-not $embeddedFallback) { $embeddedFallback = Get-EmbeddedOriginalRequest $q }
150
+ continue
151
+ }
120
152
  if ($q.Length -gt 2000) { $q = $q.Substring(0, 2000) + '...' }
121
153
  return (Redact-SecretsFromIntent $q)
122
154
  }
123
155
  }
156
+ # No real human turn survived in the transcript -> fall back to the request
157
+ # embedded in the latest hook followup, if we found one.
158
+ if ($embeddedFallback) {
159
+ if ($embeddedFallback.Length -gt 2000) { $embeddedFallback = $embeddedFallback.Substring(0, 2000) + '...' }
160
+ return (Redact-SecretsFromIntent $embeddedFallback)
161
+ }
124
162
  return ''
125
163
  }
126
164
 
@@ -127,12 +127,14 @@ if (Test-Path -LiteralPath $scopePath) {
127
127
  if ([string]::IsNullOrWhiteSpace($scopeFiles)) { $scopeFiles = '(none yet - auto-tracked as you edit)' }
128
128
  $scopeExists = $true
129
129
  $scopeHasHash = ($sj.PSObject.Properties['_intent_hash'] -and -not [string]::IsNullOrWhiteSpace([string]$sj._intent_hash))
130
- # Hollow = no real intent on disk: empty, or still the hook's <TODO> placeholder.
130
+ # Hollow = no real intent on disk: empty, the hook's <TODO> placeholder, OR
131
+ # hook-generated review boilerplate that a stale extractor locked in as the
132
+ # intent (the contamination loop - "FINAL REVIEW (end of implementation)...").
131
133
  # A hollow contract is worse than none (it looks owned, so neither hook nor agent
132
- # fills it; scope-gate appends files to it and final-review audits against <TODO>).
134
+ # fills it; scope-gate appends files to it and final-review audits against garbage).
133
135
  # Treat it as unusable: regenerate when we can read the request, else hand the agent
134
136
  # the pre-compile demand to write a real one.
135
- $scopeHollow = ([string]::IsNullOrWhiteSpace($scopeIntent) -or $scopeIntent -match '^\s*<TODO')
137
+ $scopeHollow = ([string]::IsNullOrWhiteSpace($scopeIntent) -or $scopeIntent -match '^\s*<TODO' -or (Test-IsHookGeneratedQuery $scopeIntent))
136
138
  # Staleness, hash-agnostic so it survives MODEL-written contracts:
137
139
  # - hook-written (has _intent_hash): stale when that hash != current query hash.
138
140
  # - model-written (no _intent_hash - the legacy pre-compile.md schema): we cannot
@@ -9,12 +9,34 @@ This is the proactive phase. The anti-slop checklist, the self-review trigger
9
9
  and the final review are reactive — they audit after the fact. You compile the
10
10
  intent BEFORE the first token of code so they have the right thing to audit.
11
11
 
12
+ ## Step 0 — Restate the request (the user writes fast; you normalize)
13
+
14
+ Before the Anchor Set, in ONE line, play the request back as you understood it:
15
+
16
+ > **Understood as:** _one clean sentence — grammar fixed, pronouns resolved,
17
+ > implicit constraints made explicit, in the language of the request._
18
+
19
+ Mandatory every implementation turn. This line is the user's catch-point: a
20
+ misread surfaces HERE, before you write a line of code, instead of in review.
21
+ The restatement is **meaning-preserving** — you normalize phrasing, you do NOT
22
+ add scope, drop a constraint, or invent a requirement. If normalizing would
23
+ force a guess that changes what "correct" means, you do not bury the guess in
24
+ the restatement — you ask one sharp question (§5) and wait.
25
+
26
+ The user's verbatim words stay the ground truth: `.scope.json`'s `trace.query`
27
+ keeps them exactly as typed, and final-review traces every diff hunk back to
28
+ THAT, not to your paraphrase. Your normalized sentence becomes `intent` (and the
29
+ Anchor Set's OBJECTIVE below). The two must say the same thing in different
30
+ words — if you cannot make `intent` and `trace.query` agree, you have misread
31
+ the request, and that is the bug to fix first.
32
+
12
33
  ## The Anchor Set
13
34
 
14
35
  Answer these four, terse, in your first response. One phrase each, not prose:
15
36
 
16
- 1. OBJECTIVE — one operational sentence. What is *strictly* necessary. Not
17
- "improve X" — "make X return Y when Z".
37
+ 1. OBJECTIVE — your Step 0 restatement, tightened to the operational verb. One
38
+ sentence, what is *strictly* necessary. Not "improve X" — "make X return Y
39
+ when Z".
18
40
  2. CONSTRAINTS (local negations) — what you will NOT do. "NO schema migration.
19
41
  NO new dependency. NO refactor of the surrounding function." Negations bind
20
42
  harder than the objective: a constraint that the task contradicts is a bug
@@ -33,28 +55,36 @@ Answer these four, terse, in your first response. One phrase each, not prose:
33
55
 
34
56
  The `intent-anchor` hook creates and maintains `.scope.json` in the repo root for
35
57
  you, automatically, on the first tool of every turn:
36
- - `intent` is locked from your current request and REFRESHED when the request
58
+ - `intent` is seeded from your verbatim request and REFRESHED when the request
37
59
  changes — a new prompt regenerates the contract and resets `files[]`, so it
38
60
  never carries over between features;
39
61
  - `files[]` is auto-recorded — the scope hook appends every file you edit, so
40
62
  you never maintain it by hand;
41
- - `trace`, `_intent_hash`, `_generated_by` are hook bookkeeping (provenance and
42
- change detection). Leave them alone.
43
-
44
- Your one job on the contract: set **`acceptance`** the single deterministic check
45
- that decides done — which the hook cannot derive. Edit ONLY that field (a targeted
46
- string replace). Do **NOT** rewrite the whole file: overwriting it drops the hook's
47
- `_intent_hash`/`trace`, which disables per-prompt regeneration and brings back the
48
- cross-feature carryover. The scope-gate hook audits every edit against `files[]`,
49
- and final-review axis 0 traces every diff hunk back to `intent`.
63
+ - `trace.query` is the VERBATIM request (the audit anchor), `_intent_hash` and
64
+ `_generated_by` are hook bookkeeping. Leave all three alone.
65
+
66
+ 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
+
74
+ Do **NOT** touch `trace.query`, `_intent_hash`, or `_generated_by`, and do **NOT**
75
+ rewrite the whole file: `_intent_hash` is computed from the verbatim `trace.query`,
76
+ not from `intent`, so refining `intent` is safe — but dropping the hash/trace
77
+ disables per-prompt regeneration and brings back cross-feature carryover. Keeping
78
+ `trace.query` verbatim is what lets the audit catch a paraphrase that quietly
79
+ changed the meaning: `intent` and `trace.query` must agree.
50
80
 
51
81
  ```json
52
82
  {
53
- "intent": "<locked from your request by the hook>",
83
+ "intent": "<YOU refine this: your normalized Step 0 restatement>",
54
84
  "files": ["<auto-recorded by the hook as you edit>"],
55
85
  "acceptance": "<YOU set this: the deterministic check that decides done>",
56
86
  "allow_growth": false,
57
- "trace": { "query": "<originating request>", "ts": "<when>" },
87
+ "trace": { "query": "<VERBATIM request - the hook owns this, leave it>", "ts": "<when>" },
58
88
  "_intent_hash": "<hook bookkeeping>",
59
89
  "_generated_by":"intent-anchor hook"
60
90
  }