cursordoctrine 0.6.3 → 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.
|
@@ -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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 -
|
|
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,
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.6.
|
|
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"
|
|
@@ -83,11 +83,36 @@ function Redact-SecretsFromIntent([string]$text) {
|
|
|
83
83
|
return $text
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
#
|
|
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,
|
|
89
|
-
#
|
|
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,
|
|
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
|
|
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
|