cursordoctrine 0.5.4 → 0.5.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/bin/cli.mjs +19 -0
- package/linux/hooks/final-review.md +13 -0
- package/linux/hooks/intent-anchor.sh +36 -18
- package/package.json +1 -1
- package/windows/hooks/final-review.md +13 -0
- package/windows/hooks/intent-anchor.ps1 +40 -24
package/bin/cli.mjs
CHANGED
|
@@ -334,6 +334,25 @@ function verify() {
|
|
|
334
334
|
JSON.stringify({ role: 'user', message: { content: `<user_query>${q}</user_query>` } }) + '\n',
|
|
335
335
|
'utf8');
|
|
336
336
|
|
|
337
|
+
// --- Case 0: no .scope.json + NO transcript -> WRITE scaffold anyway -------
|
|
338
|
+
// This is the 0.5.3 regression: creation was gated on $hasQuery, so when
|
|
339
|
+
// Cursor didn't surface transcript_path in the first postToolUse fire, the
|
|
340
|
+
// scope never appeared. Now creation is unconditional on a real root.
|
|
341
|
+
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: repoDir });
|
|
342
|
+
let d0 = drainedOf(anchorCid);
|
|
343
|
+
if (!existsSync(scopePath)) {
|
|
344
|
+
cleanup(); return { ok: false, detail: 'scaffold NOT created without transcript (0.5.3 regression)' };
|
|
345
|
+
}
|
|
346
|
+
let scope0;
|
|
347
|
+
try { scope0 = JSON.parse(readFileSync(scopePath, 'utf8')); }
|
|
348
|
+
catch { cleanup(); return { ok: false, detail: '.scope.json (no-transcript) is not valid JSON' }; }
|
|
349
|
+
if (!scope0.intent || !scope0.intent.includes('TODO')) {
|
|
350
|
+
cleanup(); return { ok: false, detail: `no-transcript scaffold should have intent <TODO>, got: ${scope0.intent}` };
|
|
351
|
+
}
|
|
352
|
+
// Clear the latch + scope so Case A starts fresh (with a real query this time).
|
|
353
|
+
runHook(hook('final-review'), { conversation_id: anchorCid, status: 'completed' });
|
|
354
|
+
cleanup();
|
|
355
|
+
|
|
337
356
|
// --- Case A: no .scope.json + prompt q1 -> WRITE scaffold with q1 as intent -
|
|
338
357
|
writeTranscript(q1);
|
|
339
358
|
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: repoDir, transcript_path: transcriptPath });
|
|
@@ -122,5 +122,18 @@ Determinism / purity:
|
|
|
122
122
|
- In-place mutation of shared state (arr.push, obj.prop =) when a caller holds
|
|
123
123
|
a reference -> return new structures ([...arr, x], .map/.filter).
|
|
124
124
|
|
|
125
|
+
Logic & structure:
|
|
126
|
+
- Arrow code: >2 levels of nested if/for -> flatten with guard clauses
|
|
127
|
+
(early returns). Code reads top-to-bottom, no deep indent.
|
|
128
|
+
- Switch/if-else bloat: a switch or 5+ if/else branches -> Map/dispatch
|
|
129
|
+
(Record<State, fn>) or the Command pattern.
|
|
130
|
+
- Mixed abstraction (SLAP): a function mixing DB calls + string validation +
|
|
131
|
+
date formatting -> one level of abstraction per function; extract helpers.
|
|
132
|
+
- Primitive obsession: a primitive with business rules (email, userId, chainId)
|
|
133
|
+
passed as a bare string/number across functions -> a named type/value object.
|
|
134
|
+
- Imperative transforms: a `for` loop building an array when the language has
|
|
135
|
+
.map/.filter/.reduce -> use the declarative form; reserve `for` for cases
|
|
136
|
+
map/reduce cannot express.
|
|
137
|
+
|
|
125
138
|
You do NOT need to run a tool for these — read the diff and apply the named fix.
|
|
126
139
|
If none apply, say so in one line.
|
|
@@ -58,6 +58,16 @@ pending_dir="$(hooks_pending_dir)"
|
|
|
58
58
|
latch="$pending_dir/intent-injected-$cid.flag"
|
|
59
59
|
hash_file="$pending_dir/last-query-$cid.hash"
|
|
60
60
|
|
|
61
|
+
# Stale-latch defense: if a previous session died mid-turn without hitting
|
|
62
|
+
# stop (Cursor crash, force-quit), the latch can persist and silence this hook
|
|
63
|
+
# for the whole next session -> scope never gets created. If the latch is older
|
|
64
|
+
# than 2 hours, treat it as orphaned and clear it. Normal clears happen at
|
|
65
|
+
# every stop (final-review.sh); this is the backstop for abnormal terminations.
|
|
66
|
+
if [ -f "$latch" ]; then
|
|
67
|
+
age_hours=$(( ($(date +%s) - $(stat -c %Y "$latch" 2>/dev/null || stat -f %m "$latch" 2>/dev/null || echo 0)) / 3600 ))
|
|
68
|
+
[ "$age_hours" -ge 2 ] && rm -f "$latch" 2>/dev/null
|
|
69
|
+
fi
|
|
70
|
+
|
|
61
71
|
# Already injected this turn -> quiet. Latch cleared at every stop.
|
|
62
72
|
[ -f "$latch" ] && exit 0
|
|
63
73
|
|
|
@@ -133,30 +143,38 @@ EOF
|
|
|
133
143
|
fi
|
|
134
144
|
fi
|
|
135
145
|
|
|
136
|
-
# --- auto-create / regenerate .scope.json
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
146
|
+
# --- auto-create / regenerate .scope.json -----------------------------------
|
|
147
|
+
# CREATION does NOT require the query: if there's a root and no scope yet,
|
|
148
|
+
# scaffold it NOW with intent=<TODO> (the agent fills it from the chat it's
|
|
149
|
+
# already responding to). This was the 0.5.3 bug - creation was gated on
|
|
150
|
+
# $hasQuery, so when Cursor didn't surface transcript_path in the first
|
|
151
|
+
# postToolUse fire, the scope never got created.
|
|
152
|
+
# REGENERATION does require the query: we can only detect a prompt change if
|
|
153
|
+
# we can hash the current request. Without a query we leave an existing scope
|
|
154
|
+
# alone (re-inject it) rather than blank it.
|
|
142
155
|
regenerated=0
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
should_create=0
|
|
157
|
+
should_regen=0
|
|
158
|
+
[ "$scope_exists" != "1" ] && should_create=1
|
|
159
|
+
if [ "$has_query" = "1" ] && [ "$scope_exists" = "1" ] && [ "$scope_stale" = "1" ]; then
|
|
160
|
+
should_regen=1
|
|
148
161
|
fi
|
|
149
162
|
|
|
150
|
-
if [ "$
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
if [ "$should_create" = "1" ] || [ "$should_regen" = "1" ]; then
|
|
164
|
+
# intent from the query when available, else a TODO for the agent to fill.
|
|
165
|
+
if [ "$has_query" = "1" ]; then
|
|
166
|
+
intent_val="$current_query"
|
|
167
|
+
else
|
|
168
|
+
intent_val="<TODO: state the operational objective - what is strictly necessary>"
|
|
169
|
+
fi
|
|
170
|
+
# jq preferred; python3 fallback. Write intent, empty files[], TODO
|
|
171
|
+
# acceptance, and record _intent_hash so staleness is self-contained.
|
|
154
172
|
if have_jq; then
|
|
155
|
-
jq -n --arg intent "$
|
|
173
|
+
jq -n --arg intent "$intent_val" --arg hash "$current_hash" \
|
|
156
174
|
'{intent:$intent, files:[], acceptance:"<TODO: the one deterministic check that decides done>", allow_growth:false, _intent_hash:$hash, _generated_by:"intent-anchor hook"}' \
|
|
157
175
|
> "$scope_path" 2>/dev/null && regenerated=1
|
|
158
176
|
elif have_py; then
|
|
159
|
-
if I_FILE="$scope_path" I_INTENT="$
|
|
177
|
+
if I_FILE="$scope_path" I_INTENT="$intent_val" I_HASH="$current_hash" python3 -c '
|
|
160
178
|
import json, os
|
|
161
179
|
obj = {
|
|
162
180
|
"intent": os.environ["I_INTENT"],
|
|
@@ -173,7 +191,7 @@ with open(os.environ["I_FILE"], "w", encoding="utf-8") as f:
|
|
|
173
191
|
fi
|
|
174
192
|
fi
|
|
175
193
|
if [ "$regenerated" = "1" ]; then
|
|
176
|
-
scope_intent="$
|
|
194
|
+
scope_intent="$intent_val"
|
|
177
195
|
scope_acceptance="<TODO: the one deterministic check that decides done>"
|
|
178
196
|
scope_files="(auto-tracked - the scope hook records every file you edit)"
|
|
179
197
|
scope_exists=1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.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"
|
|
@@ -122,5 +122,18 @@ Determinism / purity:
|
|
|
122
122
|
- In-place mutation of shared state (arr.push, obj.prop =) when a caller holds
|
|
123
123
|
a reference -> return new structures ([...arr, x], .map/.filter).
|
|
124
124
|
|
|
125
|
+
Logic & structure:
|
|
126
|
+
- Arrow code: >2 levels of nested if/for -> flatten with guard clauses
|
|
127
|
+
(early returns). Code reads top-to-bottom, no deep indent.
|
|
128
|
+
- Switch/if-else bloat: a switch or 5+ if/else branches -> Map/dispatch
|
|
129
|
+
(Record<State, fn>) or the Command pattern.
|
|
130
|
+
- Mixed abstraction (SLAP): a function mixing DB calls + string validation +
|
|
131
|
+
date formatting -> one level of abstraction per function; extract helpers.
|
|
132
|
+
- Primitive obsession: a primitive with business rules (email, userId, chainId)
|
|
133
|
+
passed as a bare string/number across functions -> a named type/value object.
|
|
134
|
+
- Imperative transforms: a `for` loop building an array when the language has
|
|
135
|
+
.map/.filter/.reduce -> use the declarative form; reserve `for` for cases
|
|
136
|
+
map/reduce cannot express.
|
|
137
|
+
|
|
125
138
|
You do NOT need to run a tool for these — read the diff and apply the named fix.
|
|
126
139
|
If none apply, say so in one line.
|
|
@@ -14,17 +14,20 @@
|
|
|
14
14
|
# at the START of each turn's work, before edits pile up and dilute the
|
|
15
15
|
# original intent. Works UNCONDITIONALLY - no transcript needed.
|
|
16
16
|
#
|
|
17
|
-
# 2. AUTO-CREATE
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
17
|
+
# 2. AUTO-CREATE .scope.json (UNCONDITIONAL on a real root): if no valid
|
|
18
|
+
# contract exists in the repo root, WRITE one now - intent locked from the
|
|
19
|
+
# query when available, otherwise `intent: <TODO>` for the agent to fill.
|
|
20
|
+
# Creation does NOT require transcript_path; only regeneration does. This
|
|
21
|
+
# was the 0.5.3 bug: creation was gated on $hasQuery, so when Cursor didn't
|
|
22
|
+
# surface the transcript on the first postToolUse fire, the scope never
|
|
23
|
+
# appeared and the agent had no contract to work from.
|
|
24
|
+
# 3. REGENERATE on prompt CHANGE: when the current <user_query> hash differs
|
|
25
|
+
# from the contract's _intent_hash, overwrite the scaffold with the new
|
|
26
|
+
# intent + empty files + TODO acceptance. Requires $hasQuery (you can only
|
|
27
|
+
# detect a change if you can read the request). Fixed vs the broken 0.4.4
|
|
28
|
+
# build: never writes to $HOME (bails if no real root resolves -> no
|
|
29
|
+
# ghost files).
|
|
30
|
+
# 4. RE-INJECT on same-prompt turns: when the query is unchanged (contract
|
|
28
31
|
# already current), the hook re-injects the existing contract into the
|
|
29
32
|
# feedback bus so it stays in the model's attentional focus each turn.
|
|
30
33
|
#
|
|
@@ -56,6 +59,16 @@ $pendingDir = Get-HooksPendingDir
|
|
|
56
59
|
$latch = Join-Path $pendingDir "intent-injected-$cid.flag"
|
|
57
60
|
$hashFile = Join-Path $pendingDir "last-query-$cid.hash"
|
|
58
61
|
|
|
62
|
+
# Stale-latch defense: if a previous session died mid-turn without hitting
|
|
63
|
+
# stop (Cursor crash, force-quit), the latch can persist and silence this hook
|
|
64
|
+
# for the whole next session -> scope never gets created. If the latch is older
|
|
65
|
+
# than 2 hours, treat it as orphaned and clear it. Normal clears happen at
|
|
66
|
+
# every stop (final-review.ps1); this is the backstop for abnormal terminations.
|
|
67
|
+
if (Test-Path $latch) {
|
|
68
|
+
$age = (Get-Date) - (Get-Item $latch).LastWriteTime
|
|
69
|
+
if ($age.TotalHours -ge 2) { Remove-Item $latch -Force -ErrorAction SilentlyContinue }
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
# Already injected this turn -> quiet. Latch cleared at every stop.
|
|
60
73
|
if (Test-Path $latch) { exit 0 }
|
|
61
74
|
|
|
@@ -115,21 +128,24 @@ if (Test-Path -LiteralPath $scopePath) {
|
|
|
115
128
|
} catch { $scopeExists = $false } # malformed JSON -> treat as missing
|
|
116
129
|
}
|
|
117
130
|
|
|
118
|
-
# --- auto-create / regenerate .scope.json
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
131
|
+
# --- auto-create / regenerate .scope.json -----------------------------------
|
|
132
|
+
# CREATION does NOT require the query: if there's a root and no scope yet,
|
|
133
|
+
# scaffold it NOW with intent=<TODO> (the agent fills it from the chat it's
|
|
134
|
+
# already responding to). This was the 0.5.3 bug - creation was gated on
|
|
135
|
+
# $hasQuery, so when Cursor didn't surface transcript_path in the first
|
|
136
|
+
# postToolUse fire, the scope never got created. The agent never had a
|
|
137
|
+
# contract to work from.
|
|
138
|
+
# REGENERATION does require the query: we can only detect a prompt change if
|
|
139
|
+
# we can hash the current request. Without a query we leave an existing scope
|
|
140
|
+
# alone (re-inject it) rather than blank it.
|
|
127
141
|
$regenerated = $false
|
|
128
|
-
$
|
|
129
|
-
|
|
142
|
+
$shouldCreate = -not $scopeExists
|
|
143
|
+
$shouldRegen = $hasQuery -and $scopeExists -and $scopeStale
|
|
144
|
+
if ($shouldCreate -or $shouldRegen) {
|
|
130
145
|
try {
|
|
146
|
+
$intentVal = if ($hasQuery) { $currentQuery } else { '<TODO: state the operational objective - what is strictly necessary>' }
|
|
131
147
|
$scaffold = [ordered]@{
|
|
132
|
-
intent = $
|
|
148
|
+
intent = $intentVal
|
|
133
149
|
files = @()
|
|
134
150
|
acceptance = '<TODO: the one deterministic check that decides done>'
|
|
135
151
|
allow_growth = $false
|
|
@@ -138,7 +154,7 @@ if ($shouldWrite) {
|
|
|
138
154
|
}
|
|
139
155
|
$json = $scaffold | ConvertTo-Json -Depth 5
|
|
140
156
|
[System.IO.File]::WriteAllText($scopePath, $json, [System.Text.UTF8Encoding]::new($false))
|
|
141
|
-
$scopeIntent = $
|
|
157
|
+
$scopeIntent = $intentVal
|
|
142
158
|
$scopeAcceptance = '<TODO: the one deterministic check that decides done>'
|
|
143
159
|
$scopeFiles = '(auto-tracked - the scope hook records every file you edit)'
|
|
144
160
|
$scopeExists = $true
|