cursordoctrine 0.5.5 → 0.6.0
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 +50 -0
- package/linux/hooks/final-review.sh +1 -1
- package/linux/hooks/intent-anchor.sh +62 -13
- package/linux/pre-compile.md +29 -17
- package/package.json +1 -1
- package/skills/anti-slop/scripts/scope_match.py +6 -2
- package/windows/hooks/final-review.ps1 +1 -1
- package/windows/hooks/intent-anchor.ps1 +50 -12
- package/windows/pre-compile.md +29 -17
package/bin/cli.mjs
CHANGED
|
@@ -408,6 +408,56 @@ function verify() {
|
|
|
408
408
|
if (!scope._generated_by || !scope._intent_hash) {
|
|
409
409
|
cleanup(); return { ok: false, detail: 'scaffold missing _generated_by / _intent_hash fields' };
|
|
410
410
|
}
|
|
411
|
+
|
|
412
|
+
// --- Case C: MODEL-written scope (no _intent_hash) + a NEW prompt --------
|
|
413
|
+
// Reproduces the shipped bug (chiquipuesto/WAVE): the model wrote .scope.json
|
|
414
|
+
// per the legacy 4-field schema, so the hook could never detect staleness and
|
|
415
|
+
// scope-gate kept appending files across features. A new prompt (fresh cid =>
|
|
416
|
+
// promptChanged) must now REGENERATE and RESET files[].
|
|
417
|
+
const mkT = (p, q) => writeFileSync(p,
|
|
418
|
+
JSON.stringify({ role: 'user', message: { content: `<user_query>${q}</user_query>` } }) + '\n', 'utf8');
|
|
419
|
+
const cidC = 'npxv5';
|
|
420
|
+
const transcriptC = join(HOME, '.cursor', '.hooks-pending', 'verify-transcript-npxv5.jsonl');
|
|
421
|
+
const failC = (detail) => { try { rmSync(transcriptC, { force: true }); } catch {} cleanup(); return { ok: false, detail }; };
|
|
422
|
+
try { rmSync(scopePath, { force: true }); } catch {}
|
|
423
|
+
writeFileSync(scopePath, JSON.stringify({ intent: q1, files: ['a.ts', 'b.ts'], acceptance: 'keep', allow_growth: false }, null, 2), 'utf8');
|
|
424
|
+
mkT(transcriptC, q2);
|
|
425
|
+
runHook(hook('intent-anchor'), { conversation_id: cidC, cwd: repoDir, transcript_path: transcriptC });
|
|
426
|
+
drainedOf(cidC);
|
|
427
|
+
let sc;
|
|
428
|
+
try { sc = JSON.parse(readFileSync(scopePath, 'utf8')); } catch { return failC('model-written scope corrupted after new prompt'); }
|
|
429
|
+
if (sc.intent !== q2) return failC(`carryover not regenerated (want "${q2}"): ${sc.intent}`);
|
|
430
|
+
if (!Array.isArray(sc.files) || sc.files.length !== 0) return failC(`files[] not reset on regen: ${JSON.stringify(sc.files)}`);
|
|
431
|
+
if (!sc._intent_hash) return failC('regen did not install _intent_hash');
|
|
432
|
+
if (!sc.trace || sc.trace.query !== q2) return failC('regen did not record trace.query');
|
|
433
|
+
runHook(hook('final-review'), { conversation_id: cidC, status: 'completed' });
|
|
434
|
+
rmSync(transcriptC, { force: true });
|
|
435
|
+
|
|
436
|
+
// --- Case D: MODEL-written scope (no _intent_hash) + the SAME prompt ------
|
|
437
|
+
// The model wrote the contract for THIS request this session. The hook must
|
|
438
|
+
// HEAL in place (backfill _intent_hash + trace) WITHOUT resetting files[] or
|
|
439
|
+
// acceptance, so the NEXT prompt change is detectable by hash.
|
|
440
|
+
const cidD = 'npxv6';
|
|
441
|
+
const transcriptD = join(HOME, '.cursor', '.hooks-pending', 'verify-transcript-npxv6.jsonl');
|
|
442
|
+
const failD = (detail) => { try { rmSync(transcriptD, { force: true }); } catch {} cleanup(); return { ok: false, detail }; };
|
|
443
|
+
try { rmSync(scopePath, { force: true }); } catch {}
|
|
444
|
+
mkT(transcriptD, q1);
|
|
445
|
+
// prime last-query-<cidD>.hash with q1 (a hook-written scaffold), clear the
|
|
446
|
+
// latch, then overwrite with a model-style scope for the SAME prompt.
|
|
447
|
+
runHook(hook('intent-anchor'), { conversation_id: cidD, cwd: repoDir, transcript_path: transcriptD });
|
|
448
|
+
runHook(hook('final-review'), { conversation_id: cidD, status: 'completed' });
|
|
449
|
+
writeFileSync(scopePath, JSON.stringify({ intent: q1, files: ['a.ts', 'b.ts'], acceptance: 'keep me', allow_growth: false }, null, 2), 'utf8');
|
|
450
|
+
runHook(hook('intent-anchor'), { conversation_id: cidD, cwd: repoDir, transcript_path: transcriptD });
|
|
451
|
+
drainedOf(cidD);
|
|
452
|
+
let sd;
|
|
453
|
+
try { sd = JSON.parse(readFileSync(scopePath, 'utf8')); } catch { return failD('healed scope corrupted'); }
|
|
454
|
+
if (!sd._intent_hash) return failD('heal did not backfill _intent_hash');
|
|
455
|
+
if (!sd.trace) return failD('heal did not add trace');
|
|
456
|
+
if (!Array.isArray(sd.files) || sd.files.join(',') !== 'a.ts,b.ts') return failD(`heal reset files[] (should preserve): ${JSON.stringify(sd.files)}`);
|
|
457
|
+
if (sd.acceptance !== 'keep me') return failD(`heal clobbered acceptance: ${sd.acceptance}`);
|
|
458
|
+
runHook(hook('final-review'), { conversation_id: cidD, status: 'completed' });
|
|
459
|
+
rmSync(transcriptD, { force: true });
|
|
460
|
+
|
|
411
461
|
cleanup();
|
|
412
462
|
return true;
|
|
413
463
|
});
|
|
@@ -110,7 +110,7 @@ body="$(expand_agent_paths "$body")"
|
|
|
110
110
|
# wrong diff. Reset its prior to the Anchor Set, not to its previous attempt.
|
|
111
111
|
reentry_line="
|
|
112
112
|
|
|
113
|
-
RE-ENTRY RULE (Regla R1): if a gate or axis failed, forget the approach that produced it. Re-read your ORIGINAL REQUEST above and your Anchor Set (.scope.json,
|
|
113
|
+
RE-ENTRY RULE (Regla R1): if a gate or axis failed, forget the approach that produced it. Re-read your ORIGINAL REQUEST above and your Anchor Set (.scope.json, maintained by the intent-anchor hook). Fix ONLY what is failing. Do not refactor in this pass - that is History Propagation, the exact failure mode the Anchor Set exists to prevent.
|
|
114
114
|
"
|
|
115
115
|
|
|
116
116
|
file_list=""
|
|
@@ -111,7 +111,9 @@ scope_exists=0
|
|
|
111
111
|
scope_intent=""
|
|
112
112
|
scope_acceptance=""
|
|
113
113
|
scope_files=""
|
|
114
|
-
scope_stale=0
|
|
114
|
+
scope_stale=0 # 1 when the on-disk contract belongs to a DIFFERENT prompt -> regenerate (resets files[])
|
|
115
|
+
needs_heal=0 # 1 when a model-written contract matches THIS prompt but lacks _intent_hash -> backfill in place
|
|
116
|
+
on_disk_hash=""
|
|
115
117
|
scope_path="$root/.scope.json"
|
|
116
118
|
if [ -f "$scope_path" ]; then
|
|
117
119
|
# Prefer jq; fall back to python3 (mirrors hook-common.sh degrade policy).
|
|
@@ -137,21 +139,33 @@ except Exception:
|
|
|
137
139
|
EOF
|
|
138
140
|
[ $? -eq 0 ] && scope_exists=1 || scope_exists=0
|
|
139
141
|
fi
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
# Staleness, hash-agnostic so it survives MODEL-written contracts:
|
|
143
|
+
# - hook-written (has _intent_hash): stale when that hash != current query hash.
|
|
144
|
+
# - model-written (no _intent_hash - the legacy pre-compile.md schema): fall back to
|
|
145
|
+
# $prompt_changed (current query hash != the per-conversation last-query hash). Prompt
|
|
146
|
+
# changed (or a new session) => regenerate and RESET files[] (the "arrastre entre
|
|
147
|
+
# features" fix). Same prompt this session => heal in place (backfill bookkeeping, keep
|
|
148
|
+
# files[]/acceptance) so the NEXT prompt is detected by hash.
|
|
149
|
+
if [ "$scope_exists" = "1" ] && [ "$has_query" = "1" ]; then
|
|
150
|
+
if [ -n "$on_disk_hash" ]; then
|
|
151
|
+
[ "$on_disk_hash" != "$current_hash" ] && scope_stale=1
|
|
152
|
+
elif [ "$prompt_changed" = "1" ]; then
|
|
153
|
+
scope_stale=1
|
|
154
|
+
else
|
|
155
|
+
needs_heal=1
|
|
156
|
+
fi
|
|
143
157
|
fi
|
|
144
158
|
fi
|
|
145
159
|
|
|
146
|
-
# --- auto-create / regenerate .scope.json
|
|
160
|
+
# --- auto-create / regenerate / heal .scope.json ----------------------------
|
|
147
161
|
# CREATION does NOT require the query: if there's a root and no scope yet,
|
|
148
162
|
# scaffold it NOW with intent=<TODO> (the agent fills it from the chat it's
|
|
149
163
|
# already responding to). This was the 0.5.3 bug - creation was gated on
|
|
150
164
|
# $hasQuery, so when Cursor didn't surface transcript_path in the first
|
|
151
165
|
# postToolUse fire, the scope never got created.
|
|
152
|
-
# REGENERATION
|
|
153
|
-
#
|
|
154
|
-
#
|
|
166
|
+
# REGENERATION requires the query: a prompt change is only detectable when we
|
|
167
|
+
# can read the request. A fresh scaffold resets files[] -> ".scope fresco por
|
|
168
|
+
# prompt, sin arrastre entre features."
|
|
155
169
|
regenerated=0
|
|
156
170
|
should_create=0
|
|
157
171
|
should_regen=0
|
|
@@ -159,28 +173,34 @@ should_regen=0
|
|
|
159
173
|
if [ "$has_query" = "1" ] && [ "$scope_exists" = "1" ] && [ "$scope_stale" = "1" ]; then
|
|
160
174
|
should_regen=1
|
|
161
175
|
fi
|
|
176
|
+
now_ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)"
|
|
162
177
|
|
|
163
178
|
if [ "$should_create" = "1" ] || [ "$should_regen" = "1" ]; then
|
|
164
179
|
# intent from the query when available, else a TODO for the agent to fill.
|
|
180
|
+
# trace.query records the verbatim originating request (provenance); empty
|
|
181
|
+
# when there is no transcript to read it from.
|
|
165
182
|
if [ "$has_query" = "1" ]; then
|
|
166
183
|
intent_val="$current_query"
|
|
184
|
+
trace_query="$current_query"
|
|
167
185
|
else
|
|
168
186
|
intent_val="<TODO: state the operational objective - what is strictly necessary>"
|
|
187
|
+
trace_query=""
|
|
169
188
|
fi
|
|
170
|
-
# jq preferred; python3 fallback. Write intent, empty files[], TODO
|
|
171
|
-
#
|
|
189
|
+
# jq preferred; python3 fallback. Write intent, empty files[], TODO acceptance,
|
|
190
|
+
# trace provenance, and _intent_hash so staleness is self-contained.
|
|
172
191
|
if have_jq; then
|
|
173
|
-
jq -n --arg intent "$intent_val" --arg hash "$current_hash" \
|
|
174
|
-
'{intent:$intent, files:[], acceptance:"<TODO: the one deterministic check that decides done>", allow_growth:false, _intent_hash:$hash, _generated_by:"intent-anchor hook"}' \
|
|
192
|
+
jq -n --arg intent "$intent_val" --arg hash "$current_hash" --arg tq "$trace_query" --arg ts "$now_ts" \
|
|
193
|
+
'{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"}' \
|
|
175
194
|
> "$scope_path" 2>/dev/null && regenerated=1
|
|
176
195
|
elif have_py; then
|
|
177
|
-
if I_FILE="$scope_path" I_INTENT="$intent_val" I_HASH="$current_hash" python3 -c '
|
|
196
|
+
if I_FILE="$scope_path" I_INTENT="$intent_val" I_HASH="$current_hash" I_TQ="$trace_query" I_TS="$now_ts" python3 -c '
|
|
178
197
|
import json, os
|
|
179
198
|
obj = {
|
|
180
199
|
"intent": os.environ["I_INTENT"],
|
|
181
200
|
"files": [],
|
|
182
201
|
"acceptance": "<TODO: the one deterministic check that decides done>",
|
|
183
202
|
"allow_growth": False,
|
|
203
|
+
"trace": {"query": os.environ["I_TQ"], "ts": os.environ["I_TS"]},
|
|
184
204
|
"_intent_hash": os.environ["I_HASH"],
|
|
185
205
|
"_generated_by": "intent-anchor hook",
|
|
186
206
|
}
|
|
@@ -199,6 +219,35 @@ with open(os.environ["I_FILE"], "w", encoding="utf-8") as f:
|
|
|
199
219
|
fi
|
|
200
220
|
fi
|
|
201
221
|
|
|
222
|
+
# HEAL a model-written contract that matches the current prompt but lacks the
|
|
223
|
+
# hook's bookkeeping: backfill _intent_hash + trace + _generated_by IN PLACE,
|
|
224
|
+
# preserving the model's files[] and acceptance. Without this a contract written
|
|
225
|
+
# per the legacy pre-compile.md schema (no _intent_hash) can never go stale, so
|
|
226
|
+
# the next prompt never regenerates - the carryover bug. Healing installs the
|
|
227
|
+
# hash so the next prompt change is detected by hash like any hook contract.
|
|
228
|
+
if [ "$needs_heal" = "1" ] && [ "$regenerated" != "1" ]; then
|
|
229
|
+
if have_jq; then
|
|
230
|
+
healed="$(jq --arg hash "$current_hash" --arg tq "$current_query" --arg ts "$now_ts" \
|
|
231
|
+
'._intent_hash = $hash | .trace //= {query:$tq, ts:$ts} | ._generated_by //= "intent-anchor hook (healed)"' \
|
|
232
|
+
"$scope_path" 2>/dev/null)"
|
|
233
|
+
[ -n "$healed" ] && printf '%s\n' "$healed" > "$scope_path"
|
|
234
|
+
elif have_py; then
|
|
235
|
+
I_FILE="$scope_path" I_HASH="$current_hash" I_TQ="$current_query" I_TS="$now_ts" python3 -c '
|
|
236
|
+
import json, os, sys
|
|
237
|
+
path = os.environ["I_FILE"]
|
|
238
|
+
try:
|
|
239
|
+
d = json.load(open(path, encoding="utf-8"))
|
|
240
|
+
except Exception:
|
|
241
|
+
sys.exit(0)
|
|
242
|
+
d["_intent_hash"] = os.environ["I_HASH"]
|
|
243
|
+
d.setdefault("trace", {"query": os.environ["I_TQ"], "ts": os.environ["I_TS"]})
|
|
244
|
+
d.setdefault("_generated_by", "intent-anchor hook (healed)")
|
|
245
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
246
|
+
json.dump(d, f, ensure_ascii=False, indent=2)
|
|
247
|
+
' 2>/dev/null
|
|
248
|
+
fi
|
|
249
|
+
fi
|
|
250
|
+
|
|
202
251
|
# files[] is auto-tracked and starts empty; show something readable until the
|
|
203
252
|
# scope hook has recorded the first edit.
|
|
204
253
|
[ -n "$scope_files" ] || scope_files="(none yet - auto-tracked as you edit)"
|
package/linux/pre-compile.md
CHANGED
|
@@ -29,29 +29,41 @@ Answer these four, terse, in your first response. One phrase each, not prose:
|
|
|
29
29
|
specific failing test going green is. If you cannot name one, you do not
|
|
30
30
|
yet understand the task — ask.
|
|
31
31
|
|
|
32
|
-
## Materialize it: .scope.json
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
## Materialize it: .scope.json (the hook owns this file)
|
|
33
|
+
|
|
34
|
+
The `intent-anchor` hook creates and maintains `.scope.json` in the repo root for
|
|
35
|
+
you, automatically, on the first tool of every turn:
|
|
36
|
+
- `intent` is locked from your current request and REFRESHED when the request
|
|
37
|
+
changes — a new prompt regenerates the contract and resets `files[]`, so it
|
|
38
|
+
never carries over between features;
|
|
39
|
+
- `files[]` is auto-recorded — the scope hook appends every file you edit, so
|
|
40
|
+
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`.
|
|
38
50
|
|
|
39
51
|
```json
|
|
40
52
|
{
|
|
41
|
-
"intent":
|
|
42
|
-
"files":
|
|
43
|
-
"acceptance":
|
|
44
|
-
"allow_growth": false
|
|
53
|
+
"intent": "<locked from your request by the hook>",
|
|
54
|
+
"files": ["<auto-recorded by the hook as you edit>"],
|
|
55
|
+
"acceptance": "<YOU set this: the deterministic check that decides done>",
|
|
56
|
+
"allow_growth": false,
|
|
57
|
+
"trace": { "query": "<originating request>", "ts": "<when>" },
|
|
58
|
+
"_intent_hash": "<hook bookkeeping>",
|
|
59
|
+
"_generated_by":"intent-anchor hook"
|
|
45
60
|
}
|
|
46
61
|
```
|
|
47
62
|
|
|
48
|
-
`allow_growth: false` is the default
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
No need to write `.scope.json` for trivial one-liners (a typo, a literal).
|
|
53
|
-
The declared-editing ladder's rung 1 ("does this need to exist?") governs when
|
|
54
|
-
the Anchor Set itself is overkill. When in doubt, write it.
|
|
63
|
+
`allow_growth: false` is the default. If the contract has not appeared yet (the
|
|
64
|
+
hook scaffolds on a tool boundary), just proceed — you do not need to hand-write
|
|
65
|
+
it. The declared-editing ladder's rung 1 ("does this need to exist?") still governs
|
|
66
|
+
trivial one-liners.
|
|
55
67
|
|
|
56
68
|
## Regla R3 — Authority
|
|
57
69
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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"
|
|
@@ -8,12 +8,16 @@ declared-scope check (Step C), so the two never disagree on what counts as
|
|
|
8
8
|
"in scope". It also surfaces the contract's `intent` and `acceptance` fields
|
|
9
9
|
so the calling hook can quote them back to the agent.
|
|
10
10
|
|
|
11
|
-
.scope.json schema (intent
|
|
11
|
+
.scope.json schema (the intent-anchor hook owns this file; this matcher reads
|
|
12
|
+
only intent/files/acceptance/allow_growth and ignores the hook bookkeeping):
|
|
12
13
|
{
|
|
13
14
|
"intent": "one operational sentence of objective",
|
|
14
15
|
"files": [ "repo-relative globs", ... ],
|
|
15
16
|
"acceptance": "the deterministic check that decides success",
|
|
16
|
-
"allow_growth": false
|
|
17
|
+
"allow_growth": false,
|
|
18
|
+
"trace": { "query": "originating request", "ts": "ISO-8601" },
|
|
19
|
+
"_intent_hash": "hook bookkeeping: sha256 of the request, drives per-prompt regen",
|
|
20
|
+
"_generated_by":"intent-anchor hook"
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
Pattern support:
|
|
@@ -119,7 +119,7 @@ $body = Expand-AgentPaths $body
|
|
|
119
119
|
# Regla R1 (re-entry): if this review pass is a re-audit after a failed gate or
|
|
120
120
|
# axis, suppress History Propagation - the model must NOT build on its own prior
|
|
121
121
|
# wrong diff. Reset its prior to the Anchor Set, not to its previous attempt.
|
|
122
|
-
$reentryLine = "`n`nRE-ENTRY RULE (Regla R1): if a gate or axis failed, forget the approach that produced it. Re-read your ORIGINAL REQUEST above and your Anchor Set (.scope.json,
|
|
122
|
+
$reentryLine = "`n`nRE-ENTRY RULE (Regla R1): if a gate or axis failed, forget the approach that produced it. Re-read your ORIGINAL REQUEST above and your Anchor Set (.scope.json, maintained by the intent-anchor hook). Fix ONLY what is failing. Do not refactor in this pass - that is History Propagation, the exact failure mode the Anchor Set exists to prevent.`n"
|
|
123
123
|
|
|
124
124
|
$resolved = @($edited | ForEach-Object { Resolve-AgentPath $_ })
|
|
125
125
|
$fileList = ($resolved | Select-Object -First 30) -join "`n "
|
|
@@ -109,7 +109,9 @@ $scopeExists = $false
|
|
|
109
109
|
$scopeIntent = ''
|
|
110
110
|
$scopeAcceptance = ''
|
|
111
111
|
$scopeFiles = ''
|
|
112
|
-
$scopeStale = $false
|
|
112
|
+
$scopeStale = $false # true when the on-disk contract belongs to a DIFFERENT prompt -> regenerate (resets files[])
|
|
113
|
+
$needsHeal = $false # true when a model-written contract matches THIS prompt but lacks _intent_hash -> backfill in place
|
|
114
|
+
$scopeHasHash = $false
|
|
113
115
|
$scopePath = Join-Path $root '.scope.json'
|
|
114
116
|
if (Test-Path -LiteralPath $scopePath) {
|
|
115
117
|
try {
|
|
@@ -119,40 +121,56 @@ if (Test-Path -LiteralPath $scopePath) {
|
|
|
119
121
|
if ($sj.files) { $scopeFiles = (@($sj.files) -join ', ') }
|
|
120
122
|
if ([string]::IsNullOrWhiteSpace($scopeFiles)) { $scopeFiles = '(none yet - auto-tracked as you edit)' }
|
|
121
123
|
$scopeExists = $true
|
|
122
|
-
|
|
123
|
-
# hash
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
$scopeHasHash = ($sj.PSObject.Properties['_intent_hash'] -and -not [string]::IsNullOrWhiteSpace([string]$sj._intent_hash))
|
|
125
|
+
# Staleness, hash-agnostic so it survives MODEL-written contracts:
|
|
126
|
+
# - hook-written (has _intent_hash): stale when that hash != current query hash.
|
|
127
|
+
# - model-written (no _intent_hash - the legacy pre-compile.md schema): we cannot
|
|
128
|
+
# hash-compare, so fall back to $promptChanged (current query hash != the per-
|
|
129
|
+
# conversation last-query hash). Prompt changed (or a new session) => stale ->
|
|
130
|
+
# regenerate and RESET files[]; this is the "arrastre entre features" fix (a model-
|
|
131
|
+
# written scope could never go stale, so it never refreshed and scope-gate kept
|
|
132
|
+
# appending files across unrelated features). Same prompt this session => the model
|
|
133
|
+
# wrote it for THIS request; heal in place (backfill the bookkeeping, keep its
|
|
134
|
+
# files[]/acceptance) so the NEXT prompt is detected by hash like any hook contract.
|
|
135
|
+
if ($hasQuery) {
|
|
136
|
+
if ($scopeHasHash) {
|
|
137
|
+
$scopeStale = ([string]$sj._intent_hash -ne $currentHash)
|
|
138
|
+
} elseif ($promptChanged) {
|
|
139
|
+
$scopeStale = $true
|
|
140
|
+
} else {
|
|
141
|
+
$needsHeal = $true
|
|
142
|
+
}
|
|
127
143
|
}
|
|
128
144
|
} catch { $scopeExists = $false } # malformed JSON -> treat as missing
|
|
129
145
|
}
|
|
130
146
|
|
|
131
|
-
# --- auto-create / regenerate .scope.json
|
|
147
|
+
# --- auto-create / regenerate / heal .scope.json ----------------------------
|
|
132
148
|
# CREATION does NOT require the query: if there's a root and no scope yet,
|
|
133
149
|
# scaffold it NOW with intent=<TODO> (the agent fills it from the chat it's
|
|
134
150
|
# already responding to). This was the 0.5.3 bug - creation was gated on
|
|
135
151
|
# $hasQuery, so when Cursor didn't surface transcript_path in the first
|
|
136
152
|
# postToolUse fire, the scope never got created. The agent never had a
|
|
137
153
|
# contract to work from.
|
|
138
|
-
# REGENERATION
|
|
139
|
-
#
|
|
140
|
-
#
|
|
154
|
+
# REGENERATION requires the query: a prompt change is only detectable when we
|
|
155
|
+
# can read the request. A fresh scaffold resets files[] -> ".scope fresco por
|
|
156
|
+
# prompt, sin arrastre entre features."
|
|
141
157
|
$regenerated = $false
|
|
142
158
|
$shouldCreate = -not $scopeExists
|
|
143
159
|
$shouldRegen = $hasQuery -and $scopeExists -and $scopeStale
|
|
144
160
|
if ($shouldCreate -or $shouldRegen) {
|
|
145
161
|
try {
|
|
146
|
-
$intentVal
|
|
162
|
+
$intentVal = if ($hasQuery) { $currentQuery } else { '<TODO: state the operational objective - what is strictly necessary>' }
|
|
163
|
+
$traceQuery = if ($hasQuery) { $currentQuery } else { '' }
|
|
147
164
|
$scaffold = [ordered]@{
|
|
148
165
|
intent = $intentVal
|
|
149
166
|
files = @()
|
|
150
167
|
acceptance = '<TODO: the one deterministic check that decides done>'
|
|
151
168
|
allow_growth = $false
|
|
169
|
+
trace = [ordered]@{ query = $traceQuery; ts = (Get-Date).ToString('o') }
|
|
152
170
|
_intent_hash = $currentHash
|
|
153
171
|
_generated_by = 'intent-anchor hook'
|
|
154
172
|
}
|
|
155
|
-
$json = $scaffold | ConvertTo-Json -Depth
|
|
173
|
+
$json = $scaffold | ConvertTo-Json -Depth 8
|
|
156
174
|
[System.IO.File]::WriteAllText($scopePath, $json, [System.Text.UTF8Encoding]::new($false))
|
|
157
175
|
$scopeIntent = $intentVal
|
|
158
176
|
$scopeAcceptance = '<TODO: the one deterministic check that decides done>'
|
|
@@ -163,6 +181,26 @@ if ($shouldCreate -or $shouldRegen) {
|
|
|
163
181
|
} catch { } # write failed (perms / locked) -> fall through to demand msg
|
|
164
182
|
}
|
|
165
183
|
|
|
184
|
+
# HEAL a model-written contract that matches the current prompt but lacks the
|
|
185
|
+
# hook's bookkeeping: backfill _intent_hash + trace + _generated_by IN PLACE,
|
|
186
|
+
# preserving the model's files[] and acceptance. Without this, a contract written
|
|
187
|
+
# per pre-compile.md (no _intent_hash) can never go stale, so the next prompt
|
|
188
|
+
# never regenerates - the carryover bug. Healing installs the hash so the next
|
|
189
|
+
# prompt change is detected by hash like any hook-written contract.
|
|
190
|
+
if ($needsHeal -and -not $regenerated) {
|
|
191
|
+
try {
|
|
192
|
+
$ordered = [ordered]@{}
|
|
193
|
+
foreach ($p in $sj.PSObject.Properties) { $ordered[$p.Name] = $p.Value }
|
|
194
|
+
if (-not $ordered.Contains('trace')) {
|
|
195
|
+
$ordered['trace'] = [ordered]@{ query = $currentQuery; ts = (Get-Date).ToString('o') }
|
|
196
|
+
}
|
|
197
|
+
$ordered['_intent_hash'] = $currentHash
|
|
198
|
+
if (-not $ordered.Contains('_generated_by')) { $ordered['_generated_by'] = 'intent-anchor hook (healed)' }
|
|
199
|
+
$json = $ordered | ConvertTo-Json -Depth 8
|
|
200
|
+
[System.IO.File]::WriteAllText($scopePath, $json, [System.Text.UTF8Encoding]::new($false))
|
|
201
|
+
} catch { }
|
|
202
|
+
}
|
|
203
|
+
|
|
166
204
|
# --- compose the anchor message ---------------------------------------------
|
|
167
205
|
# Three states: regenerated this turn (new prompt), no contract (and no query
|
|
168
206
|
# to scaffold from), or re-injecting an existing current contract.
|
package/windows/pre-compile.md
CHANGED
|
@@ -29,29 +29,41 @@ Answer these four, terse, in your first response. One phrase each, not prose:
|
|
|
29
29
|
specific failing test going green is. If you cannot name one, you do not
|
|
30
30
|
yet understand the task — ask.
|
|
31
31
|
|
|
32
|
-
## Materialize it: .scope.json
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
## Materialize it: .scope.json (the hook owns this file)
|
|
33
|
+
|
|
34
|
+
The `intent-anchor` hook creates and maintains `.scope.json` in the repo root for
|
|
35
|
+
you, automatically, on the first tool of every turn:
|
|
36
|
+
- `intent` is locked from your current request and REFRESHED when the request
|
|
37
|
+
changes — a new prompt regenerates the contract and resets `files[]`, so it
|
|
38
|
+
never carries over between features;
|
|
39
|
+
- `files[]` is auto-recorded — the scope hook appends every file you edit, so
|
|
40
|
+
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`.
|
|
38
50
|
|
|
39
51
|
```json
|
|
40
52
|
{
|
|
41
|
-
"intent":
|
|
42
|
-
"files":
|
|
43
|
-
"acceptance":
|
|
44
|
-
"allow_growth": false
|
|
53
|
+
"intent": "<locked from your request by the hook>",
|
|
54
|
+
"files": ["<auto-recorded by the hook as you edit>"],
|
|
55
|
+
"acceptance": "<YOU set this: the deterministic check that decides done>",
|
|
56
|
+
"allow_growth": false,
|
|
57
|
+
"trace": { "query": "<originating request>", "ts": "<when>" },
|
|
58
|
+
"_intent_hash": "<hook bookkeeping>",
|
|
59
|
+
"_generated_by":"intent-anchor hook"
|
|
45
60
|
}
|
|
46
61
|
```
|
|
47
62
|
|
|
48
|
-
`allow_growth: false` is the default
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
No need to write `.scope.json` for trivial one-liners (a typo, a literal).
|
|
53
|
-
The declared-editing ladder's rung 1 ("does this need to exist?") governs when
|
|
54
|
-
the Anchor Set itself is overkill. When in doubt, write it.
|
|
63
|
+
`allow_growth: false` is the default. If the contract has not appeared yet (the
|
|
64
|
+
hook scaffolds on a tool boundary), just proceed — you do not need to hand-write
|
|
65
|
+
it. The declared-editing ladder's rung 1 ("does this need to exist?") still governs
|
|
66
|
+
trivial one-liners.
|
|
55
67
|
|
|
56
68
|
## Regla R3 — Authority
|
|
57
69
|
|