cursordoctrine 0.4.4 → 0.5.1
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 +16 -13
- package/README.md +12 -13
- package/bin/cli.mjs +57 -76
- package/linux/hooks/anti-slop-audit.sh +5 -4
- package/linux/hooks/final-review.sh +14 -13
- package/linux/hooks/intent-anchor.sh +81 -61
- package/linux/hooks/semantic-density-audit.sh +4 -5
- package/linux/hooks/subagent-stop-review.sh +11 -9
- package/linux/hooks.json +2 -14
- package/package.json +2 -2
- package/skills/anti-slop/SKILL.md +4 -5
- package/windows/doctrine.md +22 -26
- package/windows/hooks/anti-slop-audit.ps1 +4 -4
- package/windows/hooks/final-review.md +6 -14
- package/windows/hooks/final-review.ps1 +16 -16
- package/windows/hooks/intent-anchor.ps1 +66 -62
- package/windows/hooks/self-review.md +7 -14
- package/windows/hooks/semantic-density-audit.ps1 +4 -5
- package/windows/hooks/subagent-stop-review.ps1 +11 -9
- package/windows/hooks.json +2 -14
- package/linux/hooks/anchor-set-nudge.sh +0 -77
- package/linux/hooks/minimal-edit-audit.sh +0 -120
- package/windows/hooks/anchor-set-nudge.ps1 +0 -76
- package/windows/hooks/minimal-edit-audit.ps1 +0 -124
package/INSTALL.md
CHANGED
|
@@ -76,13 +76,14 @@ echo '{"conversation_id":"t1","status":"completed"}' | bash ~/.agents/hooks/fina
|
|
|
76
76
|
echo '{"conversation_id":"t2","file_path":"/tmp/x.py"}' | bash ~/.agents/hooks/self-review-trigger.sh
|
|
77
77
|
echo '{"conversation_id":"t2","status":"completed"}' | bash ~/.agents/hooks/subagent-stop-review.sh # expect {"followup_message": "SUBAGENT FINAL REVIEW ..."} once, then {}
|
|
78
78
|
echo '{"conversation_id":"t2"}' | bash ~/.agents/hooks/post-tool-use.sh # drain t2's leftover feedback file
|
|
79
|
-
# anchor
|
|
80
|
-
|
|
81
|
-
echo '{"conversation_id":"t3"}'
|
|
82
|
-
|
|
83
|
-
echo '{"conversation_id":"t3"}' | bash ~/.agents/hooks/post-tool-use.sh
|
|
84
|
-
echo '{}' | bash ~/.cursor/inject-doctrine.sh
|
|
85
|
-
python3 ~/.cursor/skills/anti-slop/scripts/scan_slop.py --help
|
|
79
|
+
# intent-anchor: writes .scope.json from the current prompt and re-injects it.
|
|
80
|
+
# Needs a repo root in cwd (it will NOT write to $HOME - that's the guard).
|
|
81
|
+
echo '{"conversation_id":"t3","cwd":"/tmp","file_path":"/tmp/x.py"}' | bash ~/.agents/hooks/intent-anchor.sh
|
|
82
|
+
cat /tmp/.scope.json # expect a JSON scaffold with intent placeholder
|
|
83
|
+
echo '{"conversation_id":"t3"}' | bash ~/.agents/hooks/post-tool-use.sh # expect additional_context with the scaffold
|
|
84
|
+
echo '{}' | bash ~/.cursor/inject-doctrine.sh # expect {"additional_context": ...}
|
|
85
|
+
python3 ~/.cursor/skills/anti-slop/scripts/scan_slop.py --help # expect usage text (final review's scanner)
|
|
86
|
+
rm -f /tmp/.scope.json # cleanup the test scaffold
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
If the scanner check fails, the final review still works — it falls back to the
|
|
@@ -96,11 +97,13 @@ Windows (same payloads, swap `bash ~/...sh` for `pwsh.exe -NoProfile -File $HOME
|
|
|
96
97
|
echo '{"command":"git push --force"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\permission-gate.ps1
|
|
97
98
|
echo '{"conversation_id":"t2","file_path":"C:\tmp\x.py"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\self-review-trigger.ps1
|
|
98
99
|
echo '{"conversation_id":"t2","status":"completed"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\subagent-stop-review.ps1 # SUBAGENT FINAL REVIEW once, then {}
|
|
99
|
-
# anchor
|
|
100
|
-
|
|
101
|
-
echo '{"conversation_id":"t3"}'
|
|
102
|
-
|
|
100
|
+
# intent-anchor: writes .scope.json from the current prompt and re-injects it.
|
|
101
|
+
# Needs a repo root in cwd (it will NOT write to $HOME - that's the guard).
|
|
102
|
+
echo '{"conversation_id":"t3","cwd":"C:\tmp","file_path":"C:\tmp\x.py"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\intent-anchor.ps1
|
|
103
|
+
Get-Content C:\tmp\.scope.json # expect a JSON scaffold with intent placeholder
|
|
104
|
+
echo '{"conversation_id":"t3"}' | pwsh.exe -NoProfile -File $HOME\.agents\hooks\post-tool-use.ps1 # additional_context with the scaffold
|
|
103
105
|
python $HOME\.cursor\skills\anti-slop\scripts\scan_slop.py --help
|
|
106
|
+
Remove-Item C:\tmp\.scope.json -Force # cleanup the test scaffold
|
|
104
107
|
```
|
|
105
108
|
|
|
106
109
|
Also validate the config: `~/.cursor/hooks.json` must parse as JSON.
|
|
@@ -109,7 +112,7 @@ Also validate the config: `~/.cursor/hooks.json` must parse as JSON.
|
|
|
109
112
|
|
|
110
113
|
1. Restart Cursor (hooks.json is read at startup).
|
|
111
114
|
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
|
|
115
|
+
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 `intent-anchor` should have written `.scope.json` to the repo root (intent from your prompt, `<TODO>` placeholders for files/acceptance). The scaffold regenerates when your prompt changes and is re-injected every turn.
|
|
113
116
|
4. Ask the agent to run `git push --force` (in a throwaway repo). The permission gate must block it.
|
|
114
117
|
5. Finish a small implementation and stop. A single `FINAL REVIEW` follow-up should fire — exactly once.
|
|
115
118
|
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 +123,4 @@ Also validate the config: `~/.cursor/hooks.json` must parse as JSON.
|
|
|
120
123
|
|
|
121
124
|
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
125
|
|
|
123
|
-
Kill switches if something misbehaves: `HOOKS_ENFORCE=0` (everything advisory off), `PERM_GATE_ENFORCE=0`, `
|
|
126
|
+
Kill switches if something misbehaves: `HOOKS_ENFORCE=0` (everything advisory off), `PERM_GATE_ENFORCE=0`, `INTENT_ANCHOR_ENFORCE=0` (thin-intent `.scope.json` scaffold/re-injection off), `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,9 +21,9 @@
|
|
|
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
|
|
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*. `intent-anchor` materializes that as `.scope.json` on the first tool of each turn (auto-created, regenerated when the prompt changes) and re-injects it into context every turn, so the contract stays in focus against Salience Dilution.
|
|
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
|
-
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
|
|
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, and anti-slop 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.
|
|
28
28
|
|
|
29
29
|
When an implementation finishes, the stop hook runs one final review over everything that changed, then stops. Six axes. The first is **intent trace**: the hook pulls your last user message from the transcript and prepends it to the review so the model has to tie every diff hunk to a concrete request. Anything it can't trace is a hallucinated requirement and gets reverted. That's the only check that catches "clean code, wrong feature" — linters and later axes miss it.
|
|
@@ -90,19 +90,20 @@ 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
|
|
93
|
+
On the **first tool of each agent turn**, `intent-anchor` materializes the Anchor Set: it writes `.scope.json` to the repo root (regenerating it when the prompt changed) and re-injects it into context. One scaffold per prompt, re-injection per turn. The latch is armed on first fire and cleared **unconditionally** by the stop hook on every turn boundary, so the next turn re-fires and 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
97
|
### Keeping the contract alive: `intent-anchor` (anti-Salience-Dilution)
|
|
98
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.
|
|
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. Re-injecting the contract every turn is what defeats this: the requirements are re-stated in front of the model before each edit, not just written once and hoped-for.
|
|
100
100
|
|
|
101
|
-
`intent-anchor` (`postToolUse`, registered first so it runs before `post-tool-use` drains the bus) does
|
|
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
102
|
|
|
103
|
-
1. **Materialize the contract
|
|
104
|
-
2. **Re-inject the contract.** Reads `.scope.json` and stashes `intent` + `files` + `acceptance` into the feedback bus, which `post-tool-use` delivers as `additional_context`.
|
|
105
|
-
|
|
103
|
+
1. **Materialize the contract per prompt.** When the current `<user_query>` differs from the contract on disk (no contract yet, or its recorded `_intent_hash` doesn't match), the hook **writes a fresh `.scope.json` to the repo root**: `intent` locked from the prompt, `files`/`acceptance` as `<TODO>` placeholders the agent fills in. Every new prompt → a fresh contract the agent works from. Same prompt → no rewrite.
|
|
104
|
+
2. **Re-inject the contract every turn.** 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, before edits pile up and dilute it.
|
|
105
|
+
|
|
106
|
+
> **The hook writes `.scope.json` deliberately.** This is the intended behavior: the contract must exist *before* the agent edits, and it must track the request. The agent fills the `<TODO>` placeholders (`files`, `acceptance`) on the first turn; the hook regenerates the scaffold when the prompt changes. Two real bugs in the earlier 0.4.4 build are fixed here: (a) **never writes to `$HOME`** — if the repo `cwd` can't be resolved the hook stays silent rather than drop a ghost file (bail instead of fallback); (b) **regenerates on prompt CHANGE, not on every turn** — staleness is tracked via `_intent_hash` stored in the file itself, so a same-prompt turn re-injects without rewriting.
|
|
106
107
|
|
|
107
108
|
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.
|
|
108
109
|
|
|
@@ -113,7 +114,7 @@ Crucially, `intent-anchor` carries the **semantic** contract (`intent`/`acceptan
|
|
|
113
114
|
| Session | `sessionStart` | `inject-doctrine` reads doctrine + user rules + declared-editing + **pre-compile** and emits them as `additional_context`. |
|
|
114
115
|
| 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. |
|
|
115
116
|
| Shell | `beforeShellExecution` | `permission-gate` checks the command against a deny list. Allow by default, deny by list, fail open. |
|
|
116
|
-
| Edit | `afterFileEdit` + `stop` | **Proactive:** `anchor
|
|
117
|
+
| Edit | `afterFileEdit` + `stop` | **Proactive:** `intent-anchor` (`postToolUse`) scaffolds `.scope.json` per prompt and re-injects it each turn. **Reactive:** `self-review-trigger` stashes the review prompt per edit; `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. |
|
|
117
118
|
| 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`. |
|
|
118
119
|
|
|
119
120
|
## Layout
|
|
@@ -148,9 +149,7 @@ All hooks fail open and always exit 0. Nothing here can block your session.
|
|
|
148
149
|
|---|---|---|
|
|
149
150
|
| `HOOKS_ENFORCE=0` | on | turns off all advisory hooks at once |
|
|
150
151
|
| `PERM_GATE_ENFORCE=0` | on | disables the permission gate |
|
|
151
|
-
| `
|
|
152
|
-
| `INTENT_ANCHOR_ENFORCE=0` | on | disables the thin-intent re-injection (per-turn `.scope.json` echo into `additional_context`) |
|
|
153
|
-
| `MINIMAL_EDITING_ENFORCE=0` | on | disables the over-edit advisory (deprecated in 0.3.0) |
|
|
152
|
+
| `INTENT_ANCHOR_ENFORCE=0` | on | disables the thin-intent `.scope.json` scaffold + re-injection |
|
|
154
153
|
| `SCOPE_GATE_ENFORCE=0` | on | disables the declared-scope advisory (opt-in: only fires when `.scope.json` exists) |
|
|
155
154
|
| `SEMANTIC_DENSITY_ENFORCE=0` | on | disables the semantic-opacity advisory |
|
|
156
155
|
| `ANTI_SLOP_ENFORCE=0` | on | disables the slop advisory |
|
|
@@ -163,7 +162,7 @@ All hooks fail open and always exit 0. Nothing here can block your session.
|
|
|
163
162
|
|
|
164
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.
|
|
165
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.
|
|
166
|
-
- **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
|
|
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 `intent-anchor` per-turn latch is separate and simpler: it's cleared **unconditionally** on every stop, so the scaffold/re-inject re-fires on the first tool of each new turn and can never get stranded silenced mid-session.
|
|
167
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.
|
|
168
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.
|
|
169
168
|
|
package/bin/cli.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
readFileSync,
|
|
18
18
|
readdirSync,
|
|
19
19
|
rmSync,
|
|
20
|
+
statSync,
|
|
20
21
|
writeFileSync,
|
|
21
22
|
} from 'node:fs';
|
|
22
23
|
import { join, resolve, dirname } from 'node:path';
|
|
@@ -283,102 +284,84 @@ function verify() {
|
|
|
283
284
|
return true;
|
|
284
285
|
});
|
|
285
286
|
|
|
286
|
-
check('anchor
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
// First edit -> nudge stashes into the feedback bus and arms the latch.
|
|
293
|
-
runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'x.py') });
|
|
294
|
-
if (!drainedOf('npxv3').includes('PRE-COMPILE NUDGE')) {
|
|
295
|
-
return { ok: false, detail: 'nudge did not reach the feedback bus on first edit' };
|
|
296
|
-
}
|
|
297
|
-
// Second edit same turn -> latch armed, nudge must stay silent.
|
|
298
|
-
runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'y.py') });
|
|
299
|
-
if (drainedOf('npxv3').includes('PRE-COMPILE NUDGE')) {
|
|
300
|
-
return { ok: false, detail: 'nudge re-fired on second edit (latch not gating)' };
|
|
301
|
-
}
|
|
302
|
-
// End of turn: final-review clears the latch unconditionally. Drive a
|
|
303
|
-
// review-less stop (no session-edits marker) so it hits the clear path and
|
|
304
|
-
// exits {}, same as a turn that produced no reviewable edits.
|
|
305
|
-
const stopOut = runHook(hook('final-review'), { conversation_id: 'npxv3', status: 'completed' });
|
|
306
|
-
if (stopOut !== '{}' && stopOut.replace(/\s/g, '') !== '{}') {
|
|
307
|
-
// A review fired (fine - the earlier edit left a session-edits marker via
|
|
308
|
-
// self-review-trigger if one ran). What matters is the latch got cleared;
|
|
309
|
-
// we verify that with the next-turn re-fire below.
|
|
310
|
-
}
|
|
311
|
-
// --- Turn 2 -------------------------------------------------------------
|
|
312
|
-
// First edit of the NEXT turn -> latch was cleared at the stop boundary, so
|
|
313
|
-
// the nudge MUST re-fire. This is the regression that 0.4.0 shipped broken:
|
|
314
|
-
// the latch only cleared on the fragile second-stop path, so it stranded
|
|
315
|
-
// and the nudge went permanently silent mid-session.
|
|
316
|
-
runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'z.py') });
|
|
317
|
-
if (!drainedOf('npxv3').includes('PRE-COMPILE NUDGE')) {
|
|
318
|
-
return { ok: false, detail: 'nudge did NOT re-fire on the next turn (latch stranded at the stop boundary)' };
|
|
319
|
-
}
|
|
320
|
-
return true;
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
check('intent-anchor scaffolds .scope.json and re-injects every turn', () => {
|
|
287
|
+
check('intent-anchor scaffolds .scope.json per-prompt, never to $HOME', () => {
|
|
288
|
+
// The user-requested behavior: every NEW prompt -> a fresh .scope.json
|
|
289
|
+
// in the repo root, intent locked from the query. Same prompt -> re-inject
|
|
290
|
+
// without rewriting. Never writes to $HOME (the 0.4.4 ghost-file bug).
|
|
291
|
+
// We drive it with a synthetic transcript so Get-LastUserQuery can read
|
|
292
|
+
// the <user_query>; the hook resolves cwd -> HOME (the repo root here).
|
|
324
293
|
const drainedOf = (cidv) => runHook(hook('post-tool-use'), { conversation_id: cidv });
|
|
325
294
|
const anchorCid = 'npxv4';
|
|
326
|
-
const
|
|
295
|
+
const repoDir = HOME; // verify() runs under a pinned HOME; treat it as the repo
|
|
296
|
+
const scopePath = join(repoDir, '.scope.json');
|
|
327
297
|
const transcriptPath = join(HOME, '.cursor', '.hooks-pending', 'verify-transcript-npxv4.jsonl');
|
|
328
|
-
const
|
|
298
|
+
const q1 = 'fix grid symmetry and color tokens';
|
|
299
|
+
const q2 = 'now add dark mode support';
|
|
329
300
|
|
|
330
301
|
const cleanup = () => {
|
|
331
302
|
try { rmSync(scopePath, { force: true }); } catch {}
|
|
332
303
|
try { rmSync(transcriptPath, { force: true }); } catch {}
|
|
333
304
|
};
|
|
334
305
|
cleanup();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const anchorPayload = { conversation_id: anchorCid, cwd: HOME, transcript_path: transcriptPath };
|
|
344
|
-
|
|
345
|
-
// --- Case A: no .scope.json -> hook writes scaffold on disk ----------
|
|
346
|
-
runHook(hook('intent-anchor'), anchorPayload);
|
|
306
|
+
const writeTranscript = (q) =>
|
|
307
|
+
writeFileSync(transcriptPath,
|
|
308
|
+
JSON.stringify({ role: 'user', message: { content: `<user_query>${q}</user_query>` } }) + '\n',
|
|
309
|
+
'utf8');
|
|
310
|
+
|
|
311
|
+
// --- Case A: no .scope.json + prompt q1 -> WRITE scaffold with q1 as intent -
|
|
312
|
+
writeTranscript(q1);
|
|
313
|
+
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: repoDir, transcript_path: transcriptPath });
|
|
347
314
|
let d = drainedOf(anchorCid);
|
|
348
315
|
if (!existsSync(scopePath)) {
|
|
349
|
-
cleanup(); return { ok: false, detail: '
|
|
316
|
+
cleanup(); return { ok: false, detail: 'scaffold was NOT written on first prompt' };
|
|
350
317
|
}
|
|
351
318
|
let scope;
|
|
352
|
-
try { scope = JSON.parse(readFileSync(scopePath, 'utf8')); }
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
cleanup(); return { ok: false, detail: `scaffold intent mismatch: ${scope.intent}` };
|
|
319
|
+
try { scope = JSON.parse(readFileSync(scopePath, 'utf8')); }
|
|
320
|
+
catch { cleanup(); return { ok: false, detail: '.scope.json is not valid JSON' }; }
|
|
321
|
+
if (scope.intent !== q1) {
|
|
322
|
+
cleanup(); return { ok: false, detail: `intent mismatch (want "${q1}"): ${scope.intent}` };
|
|
357
323
|
}
|
|
358
|
-
if (!d.includes('
|
|
359
|
-
cleanup(); return { ok: false, detail: '
|
|
324
|
+
if (!d.includes('scope regenerated')) {
|
|
325
|
+
cleanup(); return { ok: false, detail: 'regenerated branch did not fire' };
|
|
360
326
|
}
|
|
361
327
|
|
|
362
|
-
// --- Stop clears the latch
|
|
328
|
+
// --- Stop clears the latch -> next turn can act again --------------------
|
|
363
329
|
runHook(hook('final-review'), { conversation_id: anchorCid, status: 'completed' });
|
|
364
330
|
|
|
365
|
-
// --- Case B:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
files: ['src/grid.tsx'],
|
|
369
|
-
acceptance: 'grid renders symmetric; tokens match palette',
|
|
370
|
-
}));
|
|
371
|
-
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: HOME, transcript_path: transcriptPath });
|
|
331
|
+
// --- Case B: same prompt q1 again -> re-INJECT, do NOT rewrite ----------
|
|
332
|
+
const sizeBefore = statSync(scopePath).size;
|
|
333
|
+
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: repoDir, transcript_path: transcriptPath });
|
|
372
334
|
d = drainedOf(anchorCid);
|
|
373
|
-
if (!d.includes('
|
|
374
|
-
cleanup(); return { ok: false, detail: '
|
|
335
|
+
if (!d.includes('re-injected this turn')) {
|
|
336
|
+
cleanup(); return { ok: false, detail: 'same-prompt turn did not re-inject' };
|
|
337
|
+
}
|
|
338
|
+
// _intent_hash matches -> file must not have been regenerated. Intent still q1.
|
|
339
|
+
try { scope = JSON.parse(readFileSync(scopePath, 'utf8')); }
|
|
340
|
+
catch { cleanup(); return { ok: false, detail: '.scope.json corrupted on same-prompt turn' }; }
|
|
341
|
+
if (scope.intent !== q1) {
|
|
342
|
+
cleanup(); return { ok: false, detail: `same-prompt turn rewrote intent (should stay "${q1}"): ${scope.intent}` };
|
|
375
343
|
}
|
|
376
344
|
|
|
345
|
+
// --- Stop; new prompt q2 -> REGENERATE with q2 as intent ----------------
|
|
377
346
|
runHook(hook('final-review'), { conversation_id: anchorCid, status: 'completed' });
|
|
378
|
-
|
|
347
|
+
writeTranscript(q2);
|
|
348
|
+
runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: repoDir, transcript_path: transcriptPath });
|
|
379
349
|
d = drainedOf(anchorCid);
|
|
380
|
-
|
|
381
|
-
|
|
350
|
+
try { scope = JSON.parse(readFileSync(scopePath, 'utf8')); }
|
|
351
|
+
catch { cleanup(); return { ok: false, detail: '.scope.json corrupted on prompt-change turn' }; }
|
|
352
|
+
if (scope.intent !== q2) {
|
|
353
|
+
cleanup(); return { ok: false, detail: `prompt-change did not regenerate intent (want "${q2}"): ${scope.intent}` };
|
|
354
|
+
}
|
|
355
|
+
if (!d.includes('scope regenerated')) {
|
|
356
|
+
cleanup(); return { ok: false, detail: 'prompt-change turn did not report regeneration' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- The anti-ghost-file guard: no .scope.json should ever exist at $HOME
|
|
360
|
+
// level OUTSIDE the resolved repo. Here repo == HOME so this is moot,
|
|
361
|
+
// but assert the file has the _generated_by marker (proof the hook wrote
|
|
362
|
+
// it, and that it carries _intent_hash for self-contained staleness).
|
|
363
|
+
if (!scope._generated_by || !scope._intent_hash) {
|
|
364
|
+
cleanup(); return { ok: false, detail: 'scaffold missing _generated_by / _intent_hash fields' };
|
|
382
365
|
}
|
|
383
366
|
cleanup();
|
|
384
367
|
return true;
|
|
@@ -493,9 +476,7 @@ Examples
|
|
|
493
476
|
Kill switches (environment variables, all hooks fail open)
|
|
494
477
|
HOOKS_ENFORCE=0 everything advisory off
|
|
495
478
|
PERM_GATE_ENFORCE=0 permission gate off
|
|
496
|
-
|
|
497
|
-
INTENT_ANCHOR_ENFORCE=0 thin-intent re-injection off (per-turn .scope.json echo)
|
|
498
|
-
MINIMAL_EDITING_ENFORCE=0 over-edit advisory off (deprecated in 0.3.0)
|
|
479
|
+
INTENT_ANCHOR_ENFORCE=0 thin-intent scaffold/re-injection off
|
|
499
480
|
SEMANTIC_DENSITY_ENFORCE=0 semantic-opacity advisory off
|
|
500
481
|
SCOPE_GATE_ENFORCE=0 declared-scope advisory off
|
|
501
482
|
ANTI_SLOP_ENFORCE=0 slop advisory off
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# anti-slop-audit.sh - afterFileEdit "AI slop" advisory (Cursor, Linux).
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# analysis can cheaply and precisely flag, plus a self-review checklist for
|
|
7
|
-
# the parts it cannot.
|
|
4
|
+
# Guards the parts of the slop taxonomy static analysis can cheaply and
|
|
5
|
+
# precisely flag, plus a self-review checklist for the parts it cannot.
|
|
8
6
|
#
|
|
9
7
|
# Statically flagged (high-precision, deliberately low false-positive):
|
|
10
8
|
# * new dependency added to a manifest
|
|
@@ -174,6 +172,9 @@ msg="Anti-slop audit - $rel
|
|
|
174
172
|
${flag_block}${checklist}
|
|
175
173
|
|
|
176
174
|
(Advisory; the bug pass is the self-review trigger. Disable: ANTI_SLOP_ENFORCE=0)"
|
|
175
|
+
# Expand ~ so the model gets literal absolute paths (followups should be
|
|
176
|
+
# copy-pasteable; bash expands ~ but the agent may not be in a bash context).
|
|
177
|
+
msg="$(expand_agent_paths "$msg")"
|
|
177
178
|
|
|
178
179
|
# --- append to the shared pending file --------------------------------------
|
|
179
180
|
cid="$(safe_conversation_id "$input")"
|
|
@@ -36,21 +36,18 @@ cid="$(safe_conversation_id "$input")"
|
|
|
36
36
|
pending_dir="$(hooks_pending_dir)"
|
|
37
37
|
marker="$pending_dir/session-edits-$cid.txt"
|
|
38
38
|
flag="$pending_dir/reviewed-$cid.flag"
|
|
39
|
-
anchor_flag="$pending_dir/anchor-declared-$cid.flag"
|
|
40
39
|
intent_latch="$pending_dir/intent-injected-$cid.flag"
|
|
41
40
|
|
|
42
41
|
# Sweep state from sessions that died before their stop hook ran.
|
|
43
42
|
find "$pending_dir" -maxdepth 1 -type f -mtime +7 -delete 2>/dev/null
|
|
44
43
|
|
|
45
|
-
# Unconditionally clear the per-turn
|
|
46
|
-
# stop is a turn boundary; clearing here (not only inside the
|
|
47
|
-
# block below) guarantees
|
|
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)
|
|
44
|
+
# Unconditionally clear the intent-anchor per-turn latch so the next turn
|
|
45
|
+
# re-fires. Every stop is a turn boundary; clearing here (not only inside the
|
|
46
|
+
# reviewed-flag block below) guarantees it re-fires on the first tool of the
|
|
47
|
+
# NEXT turn and can never get stranded silenced mid-session.
|
|
51
48
|
# last-query-<cid>.hash is NOT cleared here - it persists turn-to-turn so
|
|
52
49
|
# intent-anchor can detect prompt changes; the 7-day sweep above reaps it.
|
|
53
|
-
rm -f "$
|
|
50
|
+
rm -f "$intent_latch" 2>/dev/null
|
|
54
51
|
|
|
55
52
|
# One-shot brake: the previous stop for this conversation emitted the review.
|
|
56
53
|
if [ -f "$flag" ]; then
|
|
@@ -83,16 +80,17 @@ body=""
|
|
|
83
80
|
if [ -z "$body" ]; then
|
|
84
81
|
body='FINAL REVIEW - audit everything you changed this session and FIX what fails
|
|
85
82
|
(do NOT revert the behaviour the user asked for):
|
|
83
|
+
0. Intent trace - tie every diff hunk back to the ORIGINAL REQUEST above.
|
|
84
|
+
Anything untraceable is a hallucinated requirement: revert it. Runs FIRST.
|
|
86
85
|
1. Correctness - logic, edge cases (null/empty/zero/boundary), language traps, security.
|
|
87
86
|
2. Reliability - error paths handled (no empty catch), timeouts/retries, resources
|
|
88
87
|
released on every path, no races, input validated at the boundary.
|
|
89
88
|
3. Coverage - behaviour-bearing changes have real tests; RUN the suite if present;
|
|
90
89
|
no tautological tests.
|
|
91
|
-
4. Anti-slop -
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
slop (retries, await-in-loop, log spam), unjustified files.
|
|
90
|
+
4. Anti-slop - if the anti-slop scanner exists, run `python <scanner> --all` first;
|
|
91
|
+
then read ~/.agents/hooks/anti-slop.md (the single source of truth) and apply all
|
|
92
|
+
13 items to the session diff. Consolidate clones; drop premature abstraction,
|
|
93
|
+
unneeded deps, operational slop, unjustified files. Do NOT re-list the items here.
|
|
96
94
|
5. Wiring completeness - for every user-visible behavior you added/changed
|
|
97
95
|
(button, submit, API call, route, state transition), trace its execution
|
|
98
96
|
path to a REAL EFFECT (persist, mutate, call, render). A dead end is slop:
|
|
@@ -101,6 +99,9 @@ if [ -z "$body" ]; then
|
|
|
101
99
|
now or remove the dead half; mark later-stubs with TODO(wire):.
|
|
102
100
|
Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one line.'
|
|
103
101
|
fi
|
|
102
|
+
# Expand ~ in the body AND the fallback, so the model gets literal absolute
|
|
103
|
+
# paths it can paste at the shell (bash expands ~, but followups should emit
|
|
104
|
+
# literals agents can copy-paste without re-interpreting).
|
|
104
105
|
body="$(expand_agent_paths "$body")"
|
|
105
106
|
|
|
106
107
|
# Regla R1 (re-entry): if this review pass is a re-audit after a failed gate or
|