cursordoctrine 0.4.1 → 0.4.3
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 +2 -2
- package/README.md +17 -5
- package/bin/cli.mjs +78 -12
- package/linux/hooks/anchor-set-nudge.sh +13 -13
- package/linux/hooks/final-review.sh +12 -3
- package/linux/hooks/intent-anchor.sh +185 -0
- package/linux/hooks/subagent-stop-review.sh +7 -3
- package/linux/hooks.json +6 -1
- package/package.json +2 -2
- package/windows/hooks/anchor-set-nudge.ps1 +13 -13
- package/windows/hooks/final-review.ps1 +14 -4
- package/windows/hooks/intent-anchor.ps1 +167 -0
- package/windows/hooks/subagent-stop-review.ps1 +8 -4
- package/windows/hooks.json +6 -1
package/INSTALL.md
CHANGED
|
@@ -109,7 +109,7 @@ Also validate the config: `~/.cursor/hooks.json` must parse as JSON.
|
|
|
109
109
|
|
|
110
110
|
1. Restart Cursor (hooks.json is read at startup).
|
|
111
111
|
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).
|
|
112
|
-
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 (if it's the first edit of
|
|
112
|
+
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 (if it's the first edit of that turn) a `PRE-COMPILE NUDGE` reminder to write its Anchor Set to `.scope.json`. The nudge re-fires on the first edit of each new turn.
|
|
113
113
|
4. Ask the agent to run `git push --force` (in a throwaway repo). The permission gate must block it.
|
|
114
114
|
5. Finish a small implementation and stop. A single `FINAL REVIEW` follow-up should fire — exactly once.
|
|
115
115
|
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.)
|
|
@@ -120,4 +120,4 @@ Also validate the config: `~/.cursor/hooks.json` must parse as JSON.
|
|
|
120
120
|
|
|
121
121
|
Tell the user what was installed, which checks passed, and anything that failed with the exact error. Do not silently work around a failing check.
|
|
122
122
|
|
|
123
|
-
Kill switches if something misbehaves: `HOOKS_ENFORCE=0` (everything advisory off), `PERM_GATE_ENFORCE=0`, `ANCHOR_NUDGE_ENFORCE=0` (pre-compile nudge off), `MINIMAL_EDITING_ENFORCE=0` (deprecated in 0.3.0), `SEMANTIC_DENSITY_ENFORCE=0`, `SCOPE_GATE_ENFORCE=0`, `ANTI_SLOP_ENFORCE=0`, `FINAL_REVIEW_ENFORCE=0`, `SUBAGENT_REVIEW_ENFORCE=0`.
|
|
123
|
+
Kill switches if something misbehaves: `HOOKS_ENFORCE=0` (everything advisory off), `PERM_GATE_ENFORCE=0`, `ANCHOR_NUDGE_ENFORCE=0` (pre-compile nudge off), `INTENT_ANCHOR_ENFORCE=0` (thin-intent re-injection off), `MINIMAL_EDITING_ENFORCE=0` (deprecated in 0.3.0), `SEMANTIC_DENSITY_ENFORCE=0`, `SCOPE_GATE_ENFORCE=0`, `ANTI_SLOP_ENFORCE=0`, `FINAL_REVIEW_ENFORCE=0`, `SUBAGENT_REVIEW_ENFORCE=0`.
|
package/README.md
CHANGED
|
@@ -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*, and write that contract to `.scope.json`. On the first edit of each
|
|
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*, and write that contract to `.scope.json`. On the first edit of each agent turn a `anchor-set-nudge` advisory fires to catch intent dilution at token ~50 instead of waiting for the stop-hook review at token ~5000.
|
|
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, anti-slop, and the pre-compile nudge 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,18 +90,29 @@ 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 edit of each
|
|
93
|
+
On the **first edit of each agent turn**, `anchor-set-nudge` drops a reminder into the feedback bus: *did you write your Anchor Set to `.scope.json`?* One nudge per turn — the latch is armed on that first edit and cleared **unconditionally** by the stop hook on every turn boundary, so the next turn re-earns its nudge. A long chat with N turns gets up to N nudges, and the latch 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)
|
|
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. This is the failure mode the nudge alone can't fix (a reminder that the contract exists ≠ the contract being in context).
|
|
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):
|
|
102
|
+
|
|
103
|
+
1. **Re-inject the contract.** 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's work — **before** edits pile up and dilute it. This runs unconditionally (no transcript needed); it's the core anti-dilution move.
|
|
104
|
+
2. **Re-compile on prompt change.** Hashes the current `<user_query>` and compares to the previous turn's hash (`last-query-<cid>.hash`, which persists across turns). If the request moved, it demands the agent **update** `.scope.json` to match — the scope tracks the request. If no `.scope.json` exists, it demands one be written. (Needs `transcript_path` in the payload; if absent this part degrades to silent but the re-injection still runs.)
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
97
108
|
## The five flows
|
|
98
109
|
|
|
99
110
|
| Flow | Event | What happens |
|
|
100
111
|
|---|---|---|
|
|
101
112
|
| Session | `sessionStart` | `inject-doctrine` reads doctrine + user rules + declared-editing + **pre-compile** and emits them as `additional_context`. |
|
|
102
|
-
| Every turn | `postToolUse` |
|
|
113
|
+
| 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. |
|
|
103
114
|
| Shell | `beforeShellExecution` | `permission-gate` checks the command against a deny list. Allow by default, deny by list, fail open. |
|
|
104
|
-
| Edit | `afterFileEdit` + `stop` | **Proactive:** `anchor-set-nudge` fires once per
|
|
115
|
+
| Edit | `afterFileEdit` + `stop` | **Proactive:** `anchor-set-nudge` fires once per turn (on its first edit) to push the Anchor Set. **Reactive:** `self-review-trigger` stashes the review prompt per edit; `minimal-edit-audit` (deprecated), `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 six-axis pass. |
|
|
105
116
|
| 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`. |
|
|
106
117
|
|
|
107
118
|
## Layout
|
|
@@ -137,6 +148,7 @@ All hooks fail open and always exit 0. Nothing here can block your session.
|
|
|
137
148
|
| `HOOKS_ENFORCE=0` | on | turns off all advisory hooks at once |
|
|
138
149
|
| `PERM_GATE_ENFORCE=0` | on | disables the permission gate |
|
|
139
150
|
| `ANCHOR_NUDGE_ENFORCE=0` | on | disables the pre-compile nudge (first-edit Anchor Set reminder) |
|
|
151
|
+
| `INTENT_ANCHOR_ENFORCE=0` | on | disables the thin-intent re-injection (per-turn `.scope.json` echo into `additional_context`) |
|
|
140
152
|
| `MINIMAL_EDITING_ENFORCE=0` | on | disables the over-edit advisory (deprecated in 0.3.0) |
|
|
141
153
|
| `SCOPE_GATE_ENFORCE=0` | on | disables the declared-scope advisory (opt-in: only fires when `.scope.json` exists) |
|
|
142
154
|
| `SEMANTIC_DENSITY_ENFORCE=0` | on | disables the semantic-opacity advisory |
|
|
@@ -150,7 +162,7 @@ All hooks fail open and always exit 0. Nothing here can block your session.
|
|
|
150
162
|
|
|
151
163
|
- **State lives under `$HOME`**, in `~/.cursor/.hooks-pending/`, keyed by conversation id. No repo litter. Concurrent sessions can't drain each other's prompts. Stale state older than 7 days gets swept on every stop.
|
|
152
164
|
- **`afterFileEdit` output isn't consumed by Cursor**, so edit hooks write to a pending file and `post-tool-use` re-emits it at the next tool boundary. That's the whole message bus.
|
|
153
|
-
- **One review per implementation.** The stop hook arms a per-conversation flag before emitting its follow-up, so a crash can't re-fire it and a long chat still gets a review after each implementation. The `anchor-set-nudge`
|
|
165
|
+
- **One review per implementation.** The stop hook arms a per-conversation flag before emitting its follow-up, so a crash can't re-fire it and a long chat still gets a review after each implementation. The `anchor-set-nudge` latch is separate and simpler: it's cleared **unconditionally** on every stop, so the nudge re-fires on the first edit of each new turn and can never get stranded silenced mid-session.
|
|
154
166
|
- **The `.scope.json` contract is opt-in.** No `.scope.json` in the repo root → `scope-gate-audit` stays silent and the system falls back to the `declared-editing` ladder plus the final-review footprint check. Writing the file is how the agent opts into a machine-checked scope.
|
|
155
167
|
- **Subagents are first-class.** `afterFileEdit` fires inside subagents keyed by the subagent's conversation id. The harness normalizes agent edits (incl. `StrReplace`) to tool type `Write`, and `postToolUse` never fires for the `Task` tool — verified by payload capture. Matchers cover `Write|StrReplace|EditNotebook` defensively. `subagentStop` reviews the subagent in its own context. The parent folds orphaned subagent markers (from the `subagents/` transcript directory) into its own at every tool boundary and at stop.
|
|
156
168
|
|
package/bin/cli.mjs
CHANGED
|
@@ -263,19 +263,84 @@ function verify() {
|
|
|
263
263
|
return true;
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
check('anchor-set nudge fires
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
266
|
+
check('anchor-set nudge fires once per turn, stop re-arms it', () => {
|
|
267
|
+
// anchor-set-nudge appends to feedback-<cid>.txt (the shared bus) rather
|
|
268
|
+
// than emitting JSON directly; drain it the same way post-tool-use does.
|
|
269
|
+
const drainedOf = (cidv) => runHook(hook('post-tool-use'), { conversation_id: cidv });
|
|
270
|
+
|
|
271
|
+
// --- Turn 1 -------------------------------------------------------------
|
|
272
|
+
// First edit -> nudge stashes into the feedback bus and arms the latch.
|
|
273
|
+
runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'x.py') });
|
|
274
|
+
if (!drainedOf('npxv3').includes('PRE-COMPILE NUDGE')) {
|
|
275
|
+
return { ok: false, detail: 'nudge did not reach the feedback bus on first edit' };
|
|
275
276
|
}
|
|
276
|
-
// Second edit ->
|
|
277
|
-
|
|
278
|
-
if (
|
|
277
|
+
// Second edit same turn -> latch armed, nudge must stay silent.
|
|
278
|
+
runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'y.py') });
|
|
279
|
+
if (drainedOf('npxv3').includes('PRE-COMPILE NUDGE')) {
|
|
280
|
+
return { ok: false, detail: 'nudge re-fired on second edit (latch not gating)' };
|
|
281
|
+
}
|
|
282
|
+
// End of turn: final-review clears the latch unconditionally. Drive a
|
|
283
|
+
// review-less stop (no session-edits marker) so it hits the clear path and
|
|
284
|
+
// exits {}, same as a turn that produced no reviewable edits.
|
|
285
|
+
const stopOut = runHook(hook('final-review'), { conversation_id: 'npxv3', status: 'completed' });
|
|
286
|
+
if (stopOut !== '{}' && stopOut.replace(/\s/g, '') !== '{}') {
|
|
287
|
+
// A review fired (fine - the earlier edit left a session-edits marker via
|
|
288
|
+
// self-review-trigger if one ran). What matters is the latch got cleared;
|
|
289
|
+
// we verify that with the next-turn re-fire below.
|
|
290
|
+
}
|
|
291
|
+
// --- Turn 2 -------------------------------------------------------------
|
|
292
|
+
// First edit of the NEXT turn -> latch was cleared at the stop boundary, so
|
|
293
|
+
// the nudge MUST re-fire. This is the regression that 0.4.0 shipped broken:
|
|
294
|
+
// the latch only cleared on the fragile second-stop path, so it stranded
|
|
295
|
+
// and the nudge went permanently silent mid-session.
|
|
296
|
+
runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'z.py') });
|
|
297
|
+
if (!drainedOf('npxv3').includes('PRE-COMPILE NUDGE')) {
|
|
298
|
+
return { ok: false, detail: 'nudge did NOT re-fire on the next turn (latch stranded at the stop boundary)' };
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
check('intent-anchor re-injects .scope.json every turn; latch re-arms at stop', () => {
|
|
304
|
+
// intent-anchor appends to feedback-<cid>.txt (the shared bus); drain via
|
|
305
|
+
// post-tool-use the same way the harness delivers additional_context.
|
|
306
|
+
const drainedOf = (cidv) => runHook(hook('post-tool-use'), { conversation_id: cidv });
|
|
307
|
+
const anchorCid = 'npxv4';
|
|
308
|
+
const scopePath = join(HOME, '.scope.json');
|
|
309
|
+
|
|
310
|
+
const cleanup = () => { try { rmSync(scopePath, { force: true }); } catch {} };
|
|
311
|
+
cleanup();
|
|
312
|
+
|
|
313
|
+
// --- Case A: no .scope.json -> demand one be written --------------
|
|
314
|
+
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: HOME });
|
|
315
|
+
let d = drainedOf(anchorCid);
|
|
316
|
+
if (!d.includes('INTENT ANCHOR') || !d.includes('NOT compiled your Anchor Set')) {
|
|
317
|
+
cleanup(); return { ok: false, detail: 'no-scope branch did not demand compilation' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- Stop clears the latch (the regression path from 0.4.1) -------
|
|
321
|
+
runHook(hook('final-review'), { conversation_id: anchorCid, status: 'completed' });
|
|
322
|
+
|
|
323
|
+
// --- Case B: scope exists -> re-inject contract every turn --------
|
|
324
|
+
writeFileSync(scopePath, JSON.stringify({
|
|
325
|
+
intent: 'fix grid symmetry and color tokens',
|
|
326
|
+
files: ['src/grid.tsx'],
|
|
327
|
+
acceptance: 'grid renders symmetric; tokens match palette',
|
|
328
|
+
}));
|
|
329
|
+
// Turn 2 (no transcript in sandbox -> query unavailable -> re-inject branch).
|
|
330
|
+
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: HOME });
|
|
331
|
+
d = drainedOf(anchorCid);
|
|
332
|
+
if (!d.includes('fix grid symmetry and color tokens') || !d.includes('INTENT ANCHOR')) {
|
|
333
|
+
cleanup(); return { ok: false, detail: 'contract not re-injected on turn 2' };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// --- Stop clears the latch again; turn 3 must re-inject too -------
|
|
337
|
+
runHook(hook('final-review'), { conversation_id: anchorCid, status: 'completed' });
|
|
338
|
+
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: HOME });
|
|
339
|
+
d = drainedOf(anchorCid);
|
|
340
|
+
if (!d.includes('fix grid symmetry and color tokens')) {
|
|
341
|
+
cleanup(); return { ok: false, detail: 'contract not re-injected on turn 3 (latch stranded at stop)' };
|
|
342
|
+
}
|
|
343
|
+
cleanup();
|
|
279
344
|
return true;
|
|
280
345
|
});
|
|
281
346
|
|
|
@@ -389,6 +454,7 @@ Kill switches (environment variables, all hooks fail open)
|
|
|
389
454
|
HOOKS_ENFORCE=0 everything advisory off
|
|
390
455
|
PERM_GATE_ENFORCE=0 permission gate off
|
|
391
456
|
ANCHOR_NUDGE_ENFORCE=0 pre-compile nudge off (first-edit Anchor Set reminder)
|
|
457
|
+
INTENT_ANCHOR_ENFORCE=0 thin-intent re-injection off (per-turn .scope.json echo)
|
|
392
458
|
MINIMAL_EDITING_ENFORCE=0 over-edit advisory off (deprecated in 0.3.0)
|
|
393
459
|
SEMANTIC_DENSITY_ENFORCE=0 semantic-opacity advisory off
|
|
394
460
|
SCOPE_GATE_ENFORCE=0 declared-scope advisory off
|
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
# anchor-set-nudge.sh - afterFileEdit "pre-compile" nudge (Cursor, Linux).
|
|
3
3
|
#
|
|
4
4
|
# Proactive counterpart to the reactive audits. On the FIRST file edit of an
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# from the first edit.
|
|
5
|
+
# agent turn, remind the agent to compile its Anchor Set (pre-compile.md) and
|
|
6
|
+
# write .scope.json BEFORE piling on more code. The reactive stack (self-review,
|
|
7
|
+
# anti-slop, final-review) only fires AFTER code exists; this nudge catches
|
|
8
|
+
# intent dilution at token ~50, not at the ~5000 of the stop-hook axis 0. A
|
|
9
|
+
# clean final review of the wrong feature is still the wrong feature - the
|
|
10
|
+
# Anchor Set exists so the right feature is on the rails from the first edit.
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# final-review.sh / subagent-stop-review.sh
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
12
|
+
# Fires ONCE PER TURN (per conversation): gated by an anchor-declared-<cid>.flag
|
|
13
|
+
# in the pending dir, armed here on first edit and cleared UNCONDITIONALLY by
|
|
14
|
+
# final-review.sh / subagent-stop-review.sh on every stop. So a long
|
|
15
|
+
# conversation with N turns gets up to N nudges - every new turn re-earns the
|
|
16
|
+
# reminder on its first edit. The clear is unconditional (not gated on the
|
|
17
|
+
# reviewed-flag path) so the latch can never get stranded silenced mid-session,
|
|
18
|
+
# which would silently stop reminding the agent to write .scope.json.
|
|
19
19
|
#
|
|
20
20
|
# Advisory only: never blocks, never reads the diff, ALWAYS exits 0. Appends to
|
|
21
21
|
# the shared feedback-<cid>.txt bus; post-tool-use.sh delivers it next turn.
|
|
@@ -37,15 +37,24 @@ pending_dir="$(hooks_pending_dir)"
|
|
|
37
37
|
marker="$pending_dir/session-edits-$cid.txt"
|
|
38
38
|
flag="$pending_dir/reviewed-$cid.flag"
|
|
39
39
|
anchor_flag="$pending_dir/anchor-declared-$cid.flag"
|
|
40
|
+
intent_latch="$pending_dir/intent-injected-$cid.flag"
|
|
40
41
|
|
|
41
42
|
# Sweep state from sessions that died before their stop hook ran.
|
|
42
43
|
find "$pending_dir" -maxdepth 1 -type f -mtime +7 -delete 2>/dev/null
|
|
43
44
|
|
|
45
|
+
# Unconditionally clear the per-turn latches so the next turn re-fires. Every
|
|
46
|
+
# stop is a turn boundary; clearing here (not only inside the reviewed-flag
|
|
47
|
+
# block below) guarantees these re-fire on the first edit/tool of the NEXT
|
|
48
|
+
# turn and can never get stranded silenced mid-session:
|
|
49
|
+
# - anchor-declared-<cid>.flag (anchor-set-nudge, first-edit reminder)
|
|
50
|
+
# - intent-injected-<cid>.flag (intent-anchor, first-tool re-injection)
|
|
51
|
+
# last-query-<cid>.hash is NOT cleared here - it persists turn-to-turn so
|
|
52
|
+
# intent-anchor can detect prompt changes; the 7-day sweep above reaps it.
|
|
53
|
+
rm -f "$anchor_flag" "$intent_latch" 2>/dev/null
|
|
54
|
+
|
|
44
55
|
# One-shot brake: the previous stop for this conversation emitted the review.
|
|
45
|
-
# Also clear anchor-declared-<cid>.flag so the pre-compile nudge re-fires for
|
|
46
|
-
# the NEXT implementation (one nudge per body of work, not per session).
|
|
47
56
|
if [ -f "$flag" ]; then
|
|
48
|
-
rm -f "$flag" "$marker"
|
|
57
|
+
rm -f "$flag" "$marker" 2>/dev/null
|
|
49
58
|
emit_none
|
|
50
59
|
fi
|
|
51
60
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# intent-anchor.sh - postToolUse "thin intent compilation" anchor (Cursor, Linux).
|
|
3
|
+
#
|
|
4
|
+
# Counteracts Salience Dilution: the failure mode where the agent's original
|
|
5
|
+
# intent erodes as the conversation fills with code, logs and errors, until the
|
|
6
|
+
# token of the original request is a rounding error against the recent history
|
|
7
|
+
# and the agent drifts ("forgets" symmetry, colors, the .scope.json it wrote at
|
|
8
|
+
# prompt 1). Two jobs, both on the FIRST tool boundary of each turn (per-turn
|
|
9
|
+
# latch intent-injected-<cid>.flag, armed here, cleared at every stop):
|
|
10
|
+
#
|
|
11
|
+
# 1. RE-INJECT .scope.json (the core anti-dilution move): read the contract
|
|
12
|
+
# (intent + files + acceptance) and stash it in the feedback bus so
|
|
13
|
+
# post-tool-use.sh delivers it as additional_context at the next tool
|
|
14
|
+
# boundary. This puts the contract back in the model's attentional focus
|
|
15
|
+
# at the START of each turn's work, before edits pile up and dilute the
|
|
16
|
+
# original intent. Works UNCONDITIONALLY - no transcript needed.
|
|
17
|
+
#
|
|
18
|
+
# 2. RE-COMPILE ON PROMPT CHANGE: hash the current <user_query> (via
|
|
19
|
+
# extract_last_user_query, which reads the transcript) and compare to
|
|
20
|
+
# last-query-<cid>.hash. If they differ, demand the agent UPDATE
|
|
21
|
+
# .scope.json to match the new request. If no .scope.json exists, demand
|
|
22
|
+
# one be written. The scope tracks the request - when the request moves,
|
|
23
|
+
# the scope moves with it. This part needs transcript_path in the payload;
|
|
24
|
+
# if it is absent the hook degrades to silent on change-detection but the
|
|
25
|
+
# re-injection above still runs.
|
|
26
|
+
#
|
|
27
|
+
# Why postToolUse, not afterFileEdit: afterFileEdit only fires AFTER an edit
|
|
28
|
+
# exists, and Cursor has no preToolUse for file edits. postToolUse fires after
|
|
29
|
+
# EVERY tool (Read/Glob/Bash/Write/...), so its first fire of a turn is the
|
|
30
|
+
# earliest moment the agent has begun working - typically right after the first
|
|
31
|
+
# Read/Glob, before any edit. Best available injection point for "before files".
|
|
32
|
+
#
|
|
33
|
+
# Once per turn: latch armed on first fire, cleared UNCONDITIONALLY at every
|
|
34
|
+
# stop (final-review.sh). Cannot strand silenced mid-session. Registered first
|
|
35
|
+
# in the postToolUse array so it appends to the feedback bus before
|
|
36
|
+
# post-tool-use.sh drains it (same-tool delivery; if reordered, delivery slips
|
|
37
|
+
# one tool - still correct).
|
|
38
|
+
#
|
|
39
|
+
# Advisory only: never blocks, never reads the diff, ALWAYS exits 0. Appends to
|
|
40
|
+
# the shared feedback-<cid>.txt bus. Disable: HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0.
|
|
41
|
+
|
|
42
|
+
set +e
|
|
43
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
44
|
+
|
|
45
|
+
[ "${HOOKS_ENFORCE:-}" = "0" ] && exit 0
|
|
46
|
+
[ "${INTENT_ANCHOR_ENFORCE:-}" = "0" ] && exit 0
|
|
47
|
+
|
|
48
|
+
input="$(read_hook_stdin)"
|
|
49
|
+
[ -n "$input" ] || exit 0
|
|
50
|
+
|
|
51
|
+
cid="$(safe_conversation_id "$input")"
|
|
52
|
+
pending_dir="$(hooks_pending_dir)"
|
|
53
|
+
latch="$pending_dir/intent-injected-$cid.flag"
|
|
54
|
+
hash_file="$pending_dir/last-query-$cid.hash"
|
|
55
|
+
|
|
56
|
+
# Already injected this turn -> quiet. Latch cleared at every stop.
|
|
57
|
+
[ -f "$latch" ] && exit 0
|
|
58
|
+
|
|
59
|
+
# --- current request (best-effort; absent in sandboxed runs) -----------------
|
|
60
|
+
current_query="$(extract_last_user_query "$input")"
|
|
61
|
+
has_query=0
|
|
62
|
+
[ -n "$current_query" ] && has_query=1
|
|
63
|
+
|
|
64
|
+
current_hash=""
|
|
65
|
+
prompt_changed=0
|
|
66
|
+
if [ "$has_query" = "1" ]; then
|
|
67
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
68
|
+
current_hash="$(printf '%s' "$current_query" | sha256sum | awk '{print $1}')"
|
|
69
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
70
|
+
current_hash="$(printf '%s' "$current_query" | shasum -a 256 | awk '{print $1}')"
|
|
71
|
+
fi
|
|
72
|
+
prev_hash=""
|
|
73
|
+
[ -f "$hash_file" ] && prev_hash="$(cat "$hash_file" 2>/dev/null)"
|
|
74
|
+
[ "$current_hash" != "$prev_hash" ] && prompt_changed=1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# --- repo root (same resolution as scope-gate-audit.sh) ----------------------
|
|
78
|
+
root=""
|
|
79
|
+
while IFS= read -r cand; do
|
|
80
|
+
[ -n "$cand" ] && [ -d "$cand" ] && { root="${cand%/}"; break; }
|
|
81
|
+
done <<EOF
|
|
82
|
+
$(json_get "$input" cwd)
|
|
83
|
+
$(json_get_array "$input" workspace_roots)
|
|
84
|
+
EOF
|
|
85
|
+
[ -n "$root" ] || root="${CURSOR_PROJECT_DIR:-$HOME}"
|
|
86
|
+
root="${root%/}"
|
|
87
|
+
|
|
88
|
+
# --- read the existing contract (if any) -------------------------------------
|
|
89
|
+
scope_exists=0
|
|
90
|
+
scope_intent=""
|
|
91
|
+
scope_acceptance=""
|
|
92
|
+
scope_files=""
|
|
93
|
+
scope_path="$root/.scope.json"
|
|
94
|
+
if [ -f "$scope_path" ]; then
|
|
95
|
+
# Prefer jq; fall back to python3 (mirrors hook-common.sh degrade policy).
|
|
96
|
+
if have_jq; then
|
|
97
|
+
scope_intent="$(jq -r '.intent // empty' "$scope_path" 2>/dev/null)"
|
|
98
|
+
scope_acceptance="$(jq -r '.acceptance // empty' "$scope_path" 2>/dev/null)"
|
|
99
|
+
scope_files="$(jq -r '(.files // []) | join(", ")' "$scope_path" 2>/dev/null)"
|
|
100
|
+
scope_exists=1
|
|
101
|
+
elif have_py; then
|
|
102
|
+
read -r scope_intent scope_acceptance scope_files <<EOF
|
|
103
|
+
$(python3 -c '
|
|
104
|
+
import json, sys
|
|
105
|
+
try:
|
|
106
|
+
d = json.load(open(sys.argv[1]))
|
|
107
|
+
print(d.get("intent","") or "")
|
|
108
|
+
print(d.get("acceptance","") or "")
|
|
109
|
+
print(", ".join(d.get("files",[]) or []))
|
|
110
|
+
except Exception:
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
' "$scope_path" 2>/dev/null)
|
|
113
|
+
EOF
|
|
114
|
+
[ $? -eq 0 ] && scope_exists=1 || scope_exists=0
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# --- compose the anchor message ---------------------------------------------
|
|
119
|
+
if [ "$has_query" = "1" ]; then
|
|
120
|
+
query_line="$current_query"
|
|
121
|
+
else
|
|
122
|
+
query_line="(current request unavailable - no transcript in this event)"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ "$scope_exists" != "1" ]; then
|
|
126
|
+
msg="INTENT ANCHOR (pre-compile) - no .scope.json found in $root.
|
|
127
|
+
|
|
128
|
+
Current request:
|
|
129
|
+
$query_line
|
|
130
|
+
|
|
131
|
+
You have NOT compiled your Anchor Set. Before editing files, write .scope.json
|
|
132
|
+
in the repo root:
|
|
133
|
+
intent: one operational sentence (what is strictly necessary)
|
|
134
|
+
files: the exact files you will touch
|
|
135
|
+
acceptance: the one deterministic check that decides done
|
|
136
|
+
|
|
137
|
+
Compile it now, then proceed. The scope tracks the request - it is how you stay
|
|
138
|
+
on the rails when the conversation gets long."
|
|
139
|
+
elif [ "$prompt_changed" = "1" ]; then
|
|
140
|
+
msg="INTENT ANCHOR (pre-compile) - your request changed; .scope.json may be stale.
|
|
141
|
+
|
|
142
|
+
Current request:
|
|
143
|
+
$query_line
|
|
144
|
+
|
|
145
|
+
Your existing contract (.scope.json):
|
|
146
|
+
intent: $scope_intent
|
|
147
|
+
files: $scope_files
|
|
148
|
+
acceptance: $scope_acceptance
|
|
149
|
+
|
|
150
|
+
If the current request differs from the intent above, UPDATE .scope.json now
|
|
151
|
+
to match what was just asked. When the request moves, the scope moves with it -
|
|
152
|
+
do not edit against a contract written for a different request."
|
|
153
|
+
else
|
|
154
|
+
# Same prompt continuing (or query unavailable) -> re-inject the contract.
|
|
155
|
+
if [ "$has_query" = "1" ]; then
|
|
156
|
+
drift_note="Every edit this turn must advance intent and stay inside files. acceptance is the bar for done."
|
|
157
|
+
else
|
|
158
|
+
drift_note="(request unavailable to diff against - re-injecting the contract as-is.)"
|
|
159
|
+
fi
|
|
160
|
+
msg="INTENT ANCHOR (re-injected this turn from .scope.json) - your contract. Do not drift from it.
|
|
161
|
+
|
|
162
|
+
intent: $scope_intent
|
|
163
|
+
files: $scope_files
|
|
164
|
+
acceptance: $scope_acceptance
|
|
165
|
+
|
|
166
|
+
$drift_note If a constraint above conflicts with what you are about to do, stop
|
|
167
|
+
and reconcile - the contract outranks momentum."
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# --- stash to the feedback bus (drained by post-tool-use.sh) -----------------
|
|
171
|
+
pending="$pending_dir/feedback-$cid.txt"
|
|
172
|
+
mkdir -p "$pending_dir" 2>/dev/null
|
|
173
|
+
if [ -s "$pending" ]; then
|
|
174
|
+
printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
|
|
175
|
+
else
|
|
176
|
+
printf '%s' "$msg" >> "$pending" 2>/dev/null
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# --- arm the latch; record the query hash for next-turn change detection -----
|
|
180
|
+
touch "$latch" 2>/dev/null
|
|
181
|
+
if [ -n "$current_hash" ]; then
|
|
182
|
+
printf '%s' "$current_hash" > "$hash_file" 2>/dev/null
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
exit 0
|
|
@@ -45,12 +45,16 @@ pending_dir="$(hooks_pending_dir)"
|
|
|
45
45
|
marker="$pending_dir/session-edits-$cid.txt"
|
|
46
46
|
flag="$pending_dir/reviewed-$cid.flag"
|
|
47
47
|
anchor_flag="$pending_dir/anchor-declared-$cid.flag"
|
|
48
|
+
intent_latch="$pending_dir/intent-injected-$cid.flag"
|
|
49
|
+
|
|
50
|
+
# Unconditionally clear the per-turn latches so the next subagent run re-fires.
|
|
51
|
+
# Clearing here (not only inside the reviewed-flag block below) can never strand
|
|
52
|
+
# them silenced. last-query-<cid>.hash is kept (cross-turn prompt-change detect).
|
|
53
|
+
rm -f "$anchor_flag" "$intent_latch" 2>/dev/null
|
|
48
54
|
|
|
49
55
|
# One-shot brake: the previous subagentStop for this id emitted the review.
|
|
50
|
-
# Also clear anchor-declared-<cid>.flag so the pre-compile nudge re-fires for
|
|
51
|
-
# the next subagent implementation (one nudge per body of work).
|
|
52
56
|
if [ -f "$flag" ]; then
|
|
53
|
-
rm -f "$flag" "$marker"
|
|
57
|
+
rm -f "$flag" "$marker" 2>/dev/null
|
|
54
58
|
emit_none
|
|
55
59
|
fi
|
|
56
60
|
|
package/linux/hooks.json
CHANGED
|
@@ -43,10 +43,15 @@
|
|
|
43
43
|
"command": "bash ~/.agents/hooks/anchor-set-nudge.sh",
|
|
44
44
|
"timeout": 5,
|
|
45
45
|
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
46
|
-
"_comment": "5s: PROACTIVE pre-compile nudge. On the FIRST edit of each
|
|
46
|
+
"_comment": "5s: PROACTIVE pre-compile nudge. On the FIRST edit of each agent turn (per conversation), remind the agent to compile its Anchor Set (pre-compile.md) into .scope.json BEFORE piling on more code. The reactive audits (self-review / anti-slop / final-review axis 0) only fire after code exists; this catches intent dilution at token ~50 instead of ~5000. One-shot per turn: gated by anchor-declared-<cid>.flag, armed here on first edit and cleared UNCONDITIONALLY by final-review.sh / subagent-stop-review.sh on every stop - so it re-fires on the first edit of the next turn and can never get stranded silenced. Advisory only; never blocks. Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0."
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
49
|
"postToolUse": [
|
|
50
|
+
{
|
|
51
|
+
"command": "bash ~/.agents/hooks/intent-anchor.sh",
|
|
52
|
+
"timeout": 5,
|
|
53
|
+
"_comment": "5s: THIN INTENT COMPILATION (anti Salience Dilution). Registered FIRST so it appends to the feedback bus before post-tool-use.sh drains it (same-tool delivery). On the FIRST tool boundary of each turn (per-turn latch intent-injected-<cid>.flag, cleared unconditionally at every stop), (1) re-injects the existing .scope.json (intent/files/acceptance) into additional_context so the contract is back in the model's attentional focus before edits pile up - UNCONDITIONAL, no transcript needed; (2) if the current <user_query> hash differs from last-query-<cid>.hash, demands the agent UPDATE .scope.json to match the new request (scope tracks the request). No .scope.json -> demand one be written. Needs transcript_path for change-detection; degrades to silent there but re-injection still runs. Advisory only; never blocks. Disable: HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0."
|
|
54
|
+
},
|
|
50
55
|
{
|
|
51
56
|
"command": "bash ~/.agents/hooks/post-tool-use.sh",
|
|
52
57
|
"timeout": 5,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Thin self-review hooks for Cursor — the model is the auditor.
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "Thin self-review hooks for Cursor — the model is the auditor. Proactive intent compilation (pre-compile Anchor Set + per-turn .scope.json re-injection against Salience Dilution), intent-trace final review (Tier 0), unified 13-item anti-slop checklist, operational slop detection.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cursordoctrine": "bin/cli.mjs"
|
|
7
7
|
},
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# anchor-set-nudge.ps1 - afterFileEdit "pre-compile" nudge (Cursor).
|
|
2
2
|
#
|
|
3
3
|
# Proactive counterpart to the reactive audits. On the FIRST file edit of an
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# from the first edit.
|
|
4
|
+
# agent turn, remind the agent to compile its Anchor Set (pre-compile.md) and
|
|
5
|
+
# write .scope.json BEFORE piling on more code. The reactive stack (self-review,
|
|
6
|
+
# anti-slop, final-review) only fires AFTER code exists; this nudge catches
|
|
7
|
+
# intent dilution at token ~50, not at the ~5000 of the stop-hook axis 0. A
|
|
8
|
+
# clean final review of the wrong feature is still the wrong feature - the
|
|
9
|
+
# Anchor Set exists so the right feature is on the rails from the first edit.
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# final-review.ps1 / subagent-stop-review.ps1
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
11
|
+
# Fires ONCE PER TURN (per conversation): gated by an anchor-declared-<cid>.flag
|
|
12
|
+
# in the pending dir, armed here on first edit and cleared UNCONDITIONALLY by
|
|
13
|
+
# final-review.ps1 / subagent-stop-review.ps1 on every stop. So a long
|
|
14
|
+
# conversation with N turns gets up to N nudges - every new turn re-earns the
|
|
15
|
+
# reminder on its first edit. The clear is unconditional (not gated on the
|
|
16
|
+
# reviewed-flag path) so the latch can never get stranded silenced mid-session,
|
|
17
|
+
# which would silently stop reminding the agent to write .scope.json.
|
|
18
18
|
#
|
|
19
19
|
# Advisory only: never blocks, never reads the diff, ALWAYS exits 0. Appends to
|
|
20
20
|
# the shared feedback-<cid>.txt bus; post-tool-use.ps1 delivers it next turn.
|
|
@@ -40,7 +40,8 @@ $cid = Get-SafeConversationId $obj
|
|
|
40
40
|
$pendingDir = Get-HooksPendingDir
|
|
41
41
|
$marker = Join-Path $pendingDir "session-edits-$cid.txt"
|
|
42
42
|
$flag = Join-Path $pendingDir "reviewed-$cid.flag"
|
|
43
|
-
$anchorFlag
|
|
43
|
+
$anchorFlag = Join-Path $pendingDir "anchor-declared-$cid.flag"
|
|
44
|
+
$intentLatch = Join-Path $pendingDir "intent-injected-$cid.flag"
|
|
44
45
|
|
|
45
46
|
# Sweep state from sessions that died before their stop hook ran. Cheap (one
|
|
46
47
|
# directory listing on an event that fires once per agent loop).
|
|
@@ -48,12 +49,21 @@ Get-ChildItem $pendingDir -File -ErrorAction SilentlyContinue |
|
|
|
48
49
|
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } |
|
|
49
50
|
Remove-Item -Force -ErrorAction SilentlyContinue
|
|
50
51
|
|
|
52
|
+
# Unconditionally clear the per-turn latches so the next turn re-fires. Every
|
|
53
|
+
# stop is a turn boundary; clearing here (not only inside the reviewed-flag
|
|
54
|
+
# block below) guarantees these re-fire on the first edit/tool of the NEXT
|
|
55
|
+
# turn and can never get stranded silenced mid-session:
|
|
56
|
+
# - anchor-declared-<cid>.flag (anchor-set-nudge, first-edit reminder)
|
|
57
|
+
# - intent-injected-<cid>.flag (intent-anchor, first-tool re-injection)
|
|
58
|
+
# last-query-<cid>.hash is NOT cleared here - it must persist turn-to-turn so
|
|
59
|
+
# intent-anchor can detect prompt changes; the 7-day sweep above reaps it when
|
|
60
|
+
# a conversation truly dies.
|
|
61
|
+
Remove-Item $anchorFlag, $intentLatch -Force -ErrorAction SilentlyContinue
|
|
62
|
+
|
|
51
63
|
# One-shot brake: the previous stop for this conversation emitted the review.
|
|
52
64
|
# Clear the flag (and whatever the review pass itself edited) and end the loop.
|
|
53
|
-
# Also clear anchor-declared-<cid>.flag so the pre-compile nudge re-fires for
|
|
54
|
-
# the NEXT implementation (one nudge per body of work, not per session).
|
|
55
65
|
if (Test-Path $flag) {
|
|
56
|
-
Remove-Item $flag, $marker
|
|
66
|
+
Remove-Item $flag, $marker -Force -ErrorAction SilentlyContinue
|
|
57
67
|
Emit-None
|
|
58
68
|
}
|
|
59
69
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# intent-anchor.ps1 - postToolUse "thin intent compilation" anchor (Cursor).
|
|
2
|
+
#
|
|
3
|
+
# Counteracts Salience Dilution: the failure mode where the agent's original
|
|
4
|
+
# intent erodes as the conversation fills with code, logs and errors, until the
|
|
5
|
+
# token of the original request is a rounding error against the recent history
|
|
6
|
+
# and the agent drifts ("forgets" symmetry, colors, the .scope.json it wrote at
|
|
7
|
+
# prompt 1). Two jobs, both on the FIRST tool boundary of each turn (per-turn
|
|
8
|
+
# latch intent-injected-<cid>.flag, armed here, cleared at every stop):
|
|
9
|
+
#
|
|
10
|
+
# 1. RE-INJECT .scope.json (the core anti-dilution move): read the contract
|
|
11
|
+
# (intent + files + acceptance) and stash it in the feedback bus so
|
|
12
|
+
# post-tool-use.ps1 delivers it as additional_context at the next tool
|
|
13
|
+
# boundary. This puts the contract back in the model's attentional focus
|
|
14
|
+
# at the START of each turn's work, before edits pile up and dilute the
|
|
15
|
+
# original intent. Works UNCONDITIONALLY - no transcript needed.
|
|
16
|
+
#
|
|
17
|
+
# 2. RE-COMPILE ON PROMPT CHANGE: hash the current <user_query> (via
|
|
18
|
+
# Get-LastUserQuery, which reads the transcript) and compare to
|
|
19
|
+
# last-query-<cid>.hash. If they differ, demand the agent UPDATE
|
|
20
|
+
# .scope.json to match the new request. If no .scope.json exists, demand
|
|
21
|
+
# one be written. The scope tracks the request - when the request moves,
|
|
22
|
+
# the scope moves with it. This part needs transcript_path in the payload;
|
|
23
|
+
# if it is absent the hook degrades to silent on change-detection but the
|
|
24
|
+
# re-injection above still runs.
|
|
25
|
+
#
|
|
26
|
+
# Why postToolUse, not afterFileEdit: afterFileEdit only fires AFTER an edit
|
|
27
|
+
# exists, and Cursor has no preToolUse for file edits. postToolUse fires after
|
|
28
|
+
# EVERY tool (Read/Glob/Bash/Write/...), so its first fire of a turn is the
|
|
29
|
+
# earliest moment the agent has begun working - typically right after the first
|
|
30
|
+
# Read/Glob, before any edit. Best available injection point for "before files".
|
|
31
|
+
#
|
|
32
|
+
# Once per turn: latch armed on first fire, cleared UNCONDITIONALLY at every
|
|
33
|
+
# stop (final-review.ps1). Cannot strand silenced mid-session (that was the
|
|
34
|
+
# 0.4.0 bug). Registered first in the postToolUse array so it appends to the
|
|
35
|
+
# feedback bus before post-tool-use.ps1 drains it (same-tool delivery; if an
|
|
36
|
+
# updated install orders it after, delivery slips one tool - still correct).
|
|
37
|
+
#
|
|
38
|
+
# Advisory only: never blocks, never reads the diff, ALWAYS exits 0. Appends to
|
|
39
|
+
# the shared feedback-<cid>.txt bus. Disable: HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0.
|
|
40
|
+
|
|
41
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
42
|
+
. "$PSScriptRoot\hook-common.ps1"
|
|
43
|
+
|
|
44
|
+
if ($env:HOOKS_ENFORCE -eq '0' -or $env:INTENT_ANCHOR_ENFORCE -eq '0') { exit 0 }
|
|
45
|
+
|
|
46
|
+
$obj = Read-HookStdinJson
|
|
47
|
+
if (-not $obj) { exit 0 }
|
|
48
|
+
|
|
49
|
+
$cid = Get-SafeConversationId $obj
|
|
50
|
+
$pendingDir = Get-HooksPendingDir
|
|
51
|
+
$latch = Join-Path $pendingDir "intent-injected-$cid.flag"
|
|
52
|
+
$hashFile = Join-Path $pendingDir "last-query-$cid.hash"
|
|
53
|
+
|
|
54
|
+
# Already injected this turn -> quiet. Latch cleared at every stop.
|
|
55
|
+
if (Test-Path $latch) { exit 0 }
|
|
56
|
+
|
|
57
|
+
# --- current request (best-effort; absent in sandboxed runs) -----------------
|
|
58
|
+
$currentQuery = Get-LastUserQuery $obj
|
|
59
|
+
$hasQuery = -not [string]::IsNullOrWhiteSpace($currentQuery)
|
|
60
|
+
|
|
61
|
+
$currentHash = ''
|
|
62
|
+
$promptChanged = $false
|
|
63
|
+
if ($hasQuery) {
|
|
64
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($currentQuery)
|
|
65
|
+
$hasher = [System.Security.Cryptography.SHA256]::Create()
|
|
66
|
+
$currentHash = -join ($hasher.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') })
|
|
67
|
+
$prevHash = ''
|
|
68
|
+
if (Test-Path $hashFile) { $prevHash = (Get-Content $hashFile -Raw -ErrorAction SilentlyContinue).Trim() }
|
|
69
|
+
$promptChanged = ($currentHash -ne $prevHash)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# --- repo root (same resolution as scope-gate-audit.ps1) ---------------------
|
|
73
|
+
$root = ''
|
|
74
|
+
$cands = @()
|
|
75
|
+
if ($obj.PSObject.Properties['cwd'] -and $obj.cwd) { $cands += [string]$obj.cwd }
|
|
76
|
+
if ($obj.PSObject.Properties['workspace_roots']) { foreach ($w in $obj.workspace_roots) { $cands += [string]$w } }
|
|
77
|
+
foreach ($c in $cands) { $f = ConvertTo-FwdPath $c; if ($f -and (Test-Path -LiteralPath $f)) { $root = $f.TrimEnd('/'); break } }
|
|
78
|
+
if (-not $root) { $root = (& { if ($env:CURSOR_PROJECT_DIR) { $env:CURSOR_PROJECT_DIR } else { $HOME } }).Replace('\', '/').TrimEnd('/') }
|
|
79
|
+
|
|
80
|
+
# --- read the existing contract (if any) -------------------------------------
|
|
81
|
+
$scopeExists = $false
|
|
82
|
+
$scopeIntent = ''
|
|
83
|
+
$scopeAcceptance = ''
|
|
84
|
+
$scopeFiles = ''
|
|
85
|
+
$scopePath = Join-Path $root '.scope.json'
|
|
86
|
+
if (Test-Path -LiteralPath $scopePath) {
|
|
87
|
+
try {
|
|
88
|
+
$sj = Get-Content -LiteralPath $scopePath -Raw | ConvertFrom-Json
|
|
89
|
+
if ($sj.intent) { $scopeIntent = [string]$sj.intent }
|
|
90
|
+
if ($sj.acceptance) { $scopeAcceptance = [string]$sj.acceptance }
|
|
91
|
+
if ($sj.files) { $scopeFiles = ($sj.files -join ', ') }
|
|
92
|
+
$scopeExists = $true
|
|
93
|
+
} catch { $scopeExists = $false } # malformed JSON -> treat as missing
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# --- compose the anchor message ---------------------------------------------
|
|
97
|
+
# Re-injection (req 2) is unconditional whenever a contract exists.
|
|
98
|
+
# Recompile-demand (req 1) fires when there is no contract, or the prompt moved.
|
|
99
|
+
$queryLine = if ($hasQuery) { $currentQuery } else { '(current request unavailable - no transcript in this event)' }
|
|
100
|
+
|
|
101
|
+
if (-not $scopeExists) {
|
|
102
|
+
$msg = @"
|
|
103
|
+
INTENT ANCHOR (pre-compile) - no .scope.json found in $root.
|
|
104
|
+
|
|
105
|
+
Current request:
|
|
106
|
+
$queryLine
|
|
107
|
+
|
|
108
|
+
You have NOT compiled your Anchor Set. Before editing files, write .scope.json
|
|
109
|
+
in the repo root:
|
|
110
|
+
intent: one operational sentence (what is strictly necessary)
|
|
111
|
+
files: the exact files you will touch
|
|
112
|
+
acceptance: the one deterministic check that decides done
|
|
113
|
+
|
|
114
|
+
Compile it now, then proceed. The scope tracks the request - it is how you stay
|
|
115
|
+
on the rails when the conversation gets long.
|
|
116
|
+
"@
|
|
117
|
+
} elseif ($promptChanged) {
|
|
118
|
+
$msg = @"
|
|
119
|
+
INTENT ANCHOR (pre-compile) - your request changed; .scope.json may be stale.
|
|
120
|
+
|
|
121
|
+
Current request:
|
|
122
|
+
$queryLine
|
|
123
|
+
|
|
124
|
+
Your existing contract (.scope.json):
|
|
125
|
+
intent: $scopeIntent
|
|
126
|
+
files: $scopeFiles
|
|
127
|
+
acceptance: $scopeAcceptance
|
|
128
|
+
|
|
129
|
+
If the current request differs from the intent above, UPDATE .scope.json now
|
|
130
|
+
to match what was just asked. When the request moves, the scope moves with it -
|
|
131
|
+
do not edit against a contract written for a different request.
|
|
132
|
+
"@
|
|
133
|
+
} else {
|
|
134
|
+
# Same prompt continuing (or query unavailable) -> re-inject the contract.
|
|
135
|
+
$driftNote = if ($hasQuery) {
|
|
136
|
+
"Every edit this turn must advance intent and stay inside files. acceptance is the bar for done."
|
|
137
|
+
} else {
|
|
138
|
+
"(request unavailable to diff against - re-injecting the contract as-is.)"
|
|
139
|
+
}
|
|
140
|
+
$msg = @"
|
|
141
|
+
INTENT ANCHOR (re-injected this turn from .scope.json) - your contract. Do not drift from it.
|
|
142
|
+
|
|
143
|
+
intent: $scopeIntent
|
|
144
|
+
files: $scopeFiles
|
|
145
|
+
acceptance: $scopeAcceptance
|
|
146
|
+
|
|
147
|
+
$driftNote If a constraint above conflicts with what you are about to do, stop
|
|
148
|
+
and reconcile - the contract outranks momentum.
|
|
149
|
+
"@
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# --- stash to the feedback bus (drained by post-tool-use.ps1) ----------------
|
|
153
|
+
$pending = Join-Path $pendingDir "feedback-$cid.txt"
|
|
154
|
+
try {
|
|
155
|
+
New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
|
|
156
|
+
$prefix = ''
|
|
157
|
+
if ((Test-Path $pending) -and ((Get-Item $pending).Length -gt 0)) { $prefix = "`n`n---`n`n" }
|
|
158
|
+
Add-Content -Path $pending -Value ($prefix + $msg) -NoNewline
|
|
159
|
+
} catch { }
|
|
160
|
+
|
|
161
|
+
# --- arm the latch; record the query hash for next-turn change detection -----
|
|
162
|
+
New-Item -ItemType File -Path $latch -Force -ErrorAction SilentlyContinue | Out-Null
|
|
163
|
+
if ($currentHash) {
|
|
164
|
+
try { Set-Content -Path $hashFile -Value $currentHash -NoNewline -ErrorAction SilentlyContinue } catch { }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
exit 0
|
|
@@ -43,13 +43,17 @@ $cid = Get-SafeConversationId $obj
|
|
|
43
43
|
$pendingDir = Get-HooksPendingDir
|
|
44
44
|
$marker = Join-Path $pendingDir "session-edits-$cid.txt"
|
|
45
45
|
$flag = Join-Path $pendingDir "reviewed-$cid.flag"
|
|
46
|
-
$anchorFlag
|
|
46
|
+
$anchorFlag = Join-Path $pendingDir "anchor-declared-$cid.flag"
|
|
47
|
+
$intentLatch = Join-Path $pendingDir "intent-injected-$cid.flag"
|
|
48
|
+
|
|
49
|
+
# Unconditionally clear the per-turn latches so the next subagent run re-fires.
|
|
50
|
+
# Clearing here (not only inside the reviewed-flag block below) can never strand
|
|
51
|
+
# them silenced. last-query-<cid>.hash is kept (cross-turn prompt-change detect).
|
|
52
|
+
Remove-Item $anchorFlag, $intentLatch -Force -ErrorAction SilentlyContinue
|
|
47
53
|
|
|
48
54
|
# One-shot brake: the previous subagentStop for this id emitted the review.
|
|
49
|
-
# Also clear anchor-declared-<cid>.flag so the pre-compile nudge re-fires for
|
|
50
|
-
# the next subagent implementation (one nudge per body of work).
|
|
51
55
|
if (Test-Path $flag) {
|
|
52
|
-
Remove-Item $flag, $marker
|
|
56
|
+
Remove-Item $flag, $marker -Force -ErrorAction SilentlyContinue
|
|
53
57
|
Emit-None
|
|
54
58
|
}
|
|
55
59
|
|
package/windows/hooks.json
CHANGED
|
@@ -43,10 +43,15 @@
|
|
|
43
43
|
"command": "pwsh.exe -NoProfile -File ~/.agents/hooks/anchor-set-nudge.ps1",
|
|
44
44
|
"timeout": 5,
|
|
45
45
|
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
46
|
-
"_comment": "5s: PROACTIVE pre-compile nudge. On the FIRST edit of each
|
|
46
|
+
"_comment": "5s: PROACTIVE pre-compile nudge. On the FIRST edit of each agent turn (per conversation), remind the agent to compile its Anchor Set (pre-compile.md) into .scope.json BEFORE piling on more code. The reactive audits (self-review / anti-slop / final-review axis 0) only fire after code exists; this catches intent dilution at token ~50 instead of ~5000. One-shot per turn: gated by anchor-declared-<cid>.flag, armed here on first edit and cleared UNCONDITIONALLY by final-review.ps1 / subagent-stop-review.ps1 on every stop - so it re-fires on the first edit of the next turn and can never get stranded silenced. Advisory only; never blocks. Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0."
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
49
|
"postToolUse": [
|
|
50
|
+
{
|
|
51
|
+
"command": "pwsh.exe -NoProfile -File ~/.agents/hooks/intent-anchor.ps1",
|
|
52
|
+
"timeout": 5,
|
|
53
|
+
"_comment": "5s: THIN INTENT COMPILATION (anti Salience Dilution). Registered FIRST so it appends to the feedback bus before post-tool-use.ps1 drains it (same-tool delivery). On the FIRST tool boundary of each turn (per-turn latch intent-injected-<cid>.flag, cleared unconditionally at every stop), (1) re-injects the existing .scope.json (intent/files/acceptance) into additional_context so the contract is back in the model's attentional focus before edits pile up - UNCONDITIONAL, no transcript needed; (2) if the current <user_query> hash differs from last-query-<cid>.hash, demands the agent UPDATE .scope.json to match the new request (scope tracks the request). No .scope.json -> demand one be written. Needs transcript_path for change-detection; degrades to silent there but re-injection still runs. Advisory only; never blocks. Disable: HOOKS_ENFORCE=0 or INTENT_ANCHOR_ENFORCE=0."
|
|
54
|
+
},
|
|
50
55
|
{
|
|
51
56
|
"command": "pwsh.exe -NoProfile -File ~/.agents/hooks/post-tool-use.ps1",
|
|
52
57
|
"timeout": 5,
|