cursordoctrine 0.3.3 → 0.4.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 CHANGED
@@ -40,7 +40,7 @@ const pendingDir = join(cursorDst, '.hooks-pending');
40
40
  const hooksJsonDst = join(cursorDst, 'hooks.json');
41
41
 
42
42
  const injectName = platform === 'windows' ? 'inject-doctrine.ps1' : 'inject-doctrine.sh';
43
- const doctrineFiles = [injectName, 'doctrine.md', 'USER-RULES.md', 'declared-editing.md'];
43
+ const doctrineFiles = [injectName, 'doctrine.md', 'USER-RULES.md', 'declared-editing.md', 'pre-compile.md'];
44
44
 
45
45
  function payloadHookFiles() {
46
46
  return readdirSync(join(payload, 'hooks'));
@@ -263,6 +263,22 @@ function verify() {
263
263
  return true;
264
264
  });
265
265
 
266
+ check('anchor-set nudge fires on first edit, then goes quiet', () => {
267
+ // First edit of the implementation -> the pre-compile nudge stashes its
268
+ // advisory in the pending feedback bus and arms the one-shot flag.
269
+ const first = runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'x.py') });
270
+ if (!first.includes('additional_context') || !first.includes('PRE-COMPILE NUDGE')) {
271
+ // anchor-set-nudge appends to feedback-<cid>.txt (the shared bus) rather
272
+ // than emitting JSON directly; drain it the same way post-tool-use does.
273
+ const drained = runHook(hook('post-tool-use'), { conversation_id: 'npxv3' });
274
+ if (!drained.includes('PRE-COMPILE NUDGE')) return { ok: false, detail: 'nudge did not reach the feedback bus on first edit' };
275
+ }
276
+ // Second edit -> flag is armed, nudge must stay silent.
277
+ const second = runHook(hook('anchor-set-nudge'), { conversation_id: 'npxv3', file_path: join(HOME, 'y.py') });
278
+ if (second.includes('PRE-COMPILE NUDGE')) return { ok: false, detail: 'nudge re-fired on second edit (flag not gating)' };
279
+ return true;
280
+ });
281
+
266
282
  check('doctrine injection emits additional_context', () =>
267
283
  runHook(join(cursorDst, injectName), {}).includes('additional_context'));
268
284
 
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # anchor-set-nudge.sh - afterFileEdit "pre-compile" nudge (Cursor, Linux).
3
+ #
4
+ # Proactive counterpart to the reactive audits. On the FIRST file edit of an
5
+ # implementation (per conversation), remind the agent to compile its Anchor Set
6
+ # (pre-compile.md) and write .scope.json BEFORE piling on more code. The
7
+ # reactive stack (self-review, anti-slop, final-review) only fires AFTER code
8
+ # exists; this nudge catches intent dilution at token ~50, not at the ~5000 of
9
+ # the stop-hook axis 0. A clean final review of the wrong feature is still the
10
+ # wrong feature - the Anchor Set exists so the right feature is on the rails
11
+ # from the first edit.
12
+ #
13
+ # One-shot PER IMPLEMENTATION, not per session: gated by an
14
+ # anchor-declared-<cid>.flag in the pending dir. That flag is cleared by
15
+ # final-review.sh / subagent-stop-review.sh at the SAME per-implementation
16
+ # boundary where they clear session-edits-<cid>.txt and reviewed-<cid>.flag
17
+ # (the stop after a review pass). So a long conversation with N implementations
18
+ # gets N nudges, not one - every new body of work re-earns the reminder.
19
+ #
20
+ # Advisory only: never blocks, never reads the diff, ALWAYS exits 0. Appends to
21
+ # the shared feedback-<cid>.txt bus; post-tool-use.sh delivers it next turn.
22
+ # Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0
23
+
24
+ set +e
25
+ . "$(dirname "$0")/hook-common.sh"
26
+
27
+ [ "${HOOKS_ENFORCE:-}" = "0" ] && exit 0
28
+ [ "${ANCHOR_NUDGE_ENFORCE:-}" = "0" ] && exit 0
29
+
30
+ input="$(read_hook_stdin)"
31
+ [ -n "$input" ] || exit 0
32
+
33
+ file_path=""
34
+ for k in file_path path filePath; do
35
+ file_path="$(json_get "$input" "$k")"
36
+ [ -n "$file_path" ] && break
37
+ done
38
+ [ -n "$file_path" ] || exit 0
39
+ is_cursor_config_path "$file_path" && exit 0
40
+
41
+ cid="$(safe_conversation_id "$input")"
42
+ pending_dir="$(hooks_pending_dir)"
43
+ flag="$pending_dir/anchor-declared-$cid.flag"
44
+
45
+ # Already nudged this implementation -> stay quiet. The flag is cleared at the
46
+ # per-implementation boundary in final-review.sh / subagent-stop-review.sh.
47
+ [ -f "$flag" ] && exit 0
48
+
49
+ msg="PRE-COMPILE NUDGE - first edit of this implementation: $file_path
50
+
51
+ Before you keep going, did you compile your Anchor Set (pre-compile.md)?
52
+ 1. OBJECTIVE - one operational sentence. What is strictly necessary.
53
+ 2. CONSTRAINTS - local negations (what you will NOT do).
54
+ 3. SCOPE - files to touch, files untouchable.
55
+ 4. SUCCESS - the one deterministic check that decides done.
56
+
57
+ If you have not already, write it to .scope.json now (intent / files /
58
+ acceptance / allow_growth). The scope-gate audits every edit against files[],
59
+ and final-review axis 0 traces every diff hunk back to intent. An Anchor Set
60
+ that lives only in your head is not an Anchor Set - the gate cannot audit it.
61
+
62
+ Skip this for trivial one-liners (typo, literal). Otherwise: compile, then code."
63
+
64
+ # Append to the shared pending file (same bus as the other advisories).
65
+ pending="$pending_dir/feedback-$cid.txt"
66
+ mkdir -p "$pending_dir" 2>/dev/null
67
+ if [ -s "$pending" ]; then
68
+ printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
69
+ else
70
+ printf '%s' "$msg" >> "$pending" 2>/dev/null
71
+ fi
72
+
73
+ # Arm the one-shot brake BEFORE returning, so a crash after the append can't
74
+ # re-nudge on the next edit. Mirrors the arming order in final-review.sh.
75
+ touch "$flag" 2>/dev/null
76
+
77
+ exit 0
@@ -36,13 +36,16 @@ 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"
39
40
 
40
41
  # Sweep state from sessions that died before their stop hook ran.
41
42
  find "$pending_dir" -maxdepth 1 -type f -mtime +7 -delete 2>/dev/null
42
43
 
43
44
  # 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).
44
47
  if [ -f "$flag" ]; then
45
- rm -f "$flag" "$marker" 2>/dev/null
48
+ rm -f "$flag" "$marker" "$anchor_flag" 2>/dev/null
46
49
  emit_none
47
50
  fi
48
51
 
@@ -91,6 +94,14 @@ Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one
91
94
  fi
92
95
  body="$(expand_agent_paths "$body")"
93
96
 
97
+ # Regla R1 (re-entry): if this review pass is a re-audit after a failed gate or
98
+ # axis, suppress History Propagation - the model must NOT build on its own prior
99
+ # wrong diff. Reset its prior to the Anchor Set, not to its previous attempt.
100
+ reentry_line="
101
+
102
+ 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, if you wrote one). 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.
103
+ "
104
+
94
105
  file_list=""
95
106
  while IFS= read -r p; do
96
107
  [ -n "$p" ] || continue
@@ -127,7 +138,7 @@ msg="FINAL REVIEW (end of implementation) - intent, correctness, reliability, co
127
138
  ${surface_block}${intent_block}Files you changed this session:
128
139
  $file_list
129
140
 
130
- $body"
141
+ ${body}${reentry_line}"
131
142
 
132
143
  # Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
133
144
  touch "$flag" 2>/dev/null
@@ -78,8 +78,10 @@ if p.get("in_scope"):
78
78
  sys.exit(3) # in scope -> clean
79
79
  allow_growth = "1" if p.get("allow_growth") else "0"
80
80
  intent = p.get("intent", "")
81
+ acceptance = p.get("acceptance", "")
81
82
  print(f"__AG__{allow_growth}")
82
83
  print(f"__INTENT__{intent}")
84
+ print(f"__ACCEPT__{acceptance}")
83
85
  sys.exit(0)
84
86
  PYEOF
85
87
  }
@@ -90,6 +92,7 @@ rc=$?
90
92
 
91
93
  allow_growth="$(printf '%s\n' "$parsed" | grep '__AG__' | sed 's/__AG__//')"
92
94
  intent="$(printf '%s\n' "$parsed" | grep '__INTENT__' | sed 's/__INTENT__//')"
95
+ acceptance="$(printf '%s\n' "$parsed" | grep '__ACCEPT__' | sed 's/__ACCEPT__//')"
93
96
 
94
97
  # Read declared files for the message (best-effort)
95
98
  declared_files="$(printf '%s' "$scope_file" | "$py" -c "
@@ -102,16 +105,29 @@ except Exception:
102
105
  " "$scope_file" 2>/dev/null)"
103
106
 
104
107
  # --- compose advisory ------------------------------------------------------
108
+ # acceptance line: only quote it when the agent declared one. A blank acceptance
109
+ # means the Anchor Set was incomplete - surface that gap, since the whole point
110
+ # of the pre-compile phase is to name the deterministic success check.
111
+ if [ -n "$acceptance" ]; then
112
+ acceptance_line="$acceptance"
113
+ else
114
+ acceptance_line="(not declared - your Anchor Set is missing the EXITO/acceptance field)"
115
+ fi
116
+
105
117
  if [ "$allow_growth" = "1" ]; then
106
118
  summary="Scope note - $rel is new vs your declared scope (growth allowed)"
107
119
  body=" You touched a file outside your initial declared set. Since allow_growth is
108
120
  true, this is not a violation, but justify it: add $rel to .scope.json or
109
- explain why the scope grew."
121
+ explain why the scope grew.
122
+
123
+ Your success contract (acceptance): $acceptance_line
124
+ Does growing into $rel still serve that?"
110
125
  else
111
126
  summary="[SCOPE VIOLATION] $rel is NOT in your declared scope"
112
127
  body=" Your contract (.scope.json):
113
128
  intent: $intent
114
129
  files: $declared_files
130
+ acceptance: $acceptance_line
115
131
 
116
132
  You declared these files and touched one outside the set. Either:
117
133
  1. Add $rel to .scope.json with a one-line justification, OR
@@ -44,10 +44,13 @@ cid="$(safe_conversation_id "$input")"
44
44
  pending_dir="$(hooks_pending_dir)"
45
45
  marker="$pending_dir/session-edits-$cid.txt"
46
46
  flag="$pending_dir/reviewed-$cid.flag"
47
+ anchor_flag="$pending_dir/anchor-declared-$cid.flag"
47
48
 
48
49
  # 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).
49
52
  if [ -f "$flag" ]; then
50
- rm -f "$flag" "$marker" 2>/dev/null
53
+ rm -f "$flag" "$marker" "$anchor_flag" 2>/dev/null
51
54
  emit_none
52
55
  fi
53
56
 
@@ -80,6 +83,14 @@ If an axis is clean, say so in one line. Then stop.'
80
83
  fi
81
84
  body="$(expand_agent_paths "$body")"
82
85
 
86
+ # Regla R1 (re-entry): same suppression as final-review.sh. A subagent that
87
+ # failed an axis must not build on its own prior wrong diff - reset its prior
88
+ # to the Anchor Set, not to its previous attempt.
89
+ reentry_line="
90
+
91
+ RE-ENTRY RULE (Regla R1): if an axis failed, forget the approach that produced it. Re-read your original task and your Anchor Set (.scope.json, if you wrote one). Fix ONLY what is failing. Do not refactor in this pass.
92
+ "
93
+
83
94
  file_list=""
84
95
  while IFS= read -r p; do
85
96
  [ -n "$p" ] || continue
@@ -94,7 +105,7 @@ msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Be
94
105
  Files you changed this run:
95
106
  $file_list
96
107
 
97
- $body"
108
+ ${body}${reentry_line}"
98
109
 
99
110
  # Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
100
111
  touch "$flag" 2>/dev/null
package/linux/hooks.json CHANGED
@@ -38,6 +38,12 @@
38
38
  "timeout": 15,
39
39
  "matcher": "^(Write|StrReplace|EditNotebook)$",
40
40
  "_comment": "15s: AI-slop advisory, companion to minimal-edit-audit. git diff flags new deps / premature abstractions (Factory/Repository/Mediator/CQRS/DDD) / redundant comments, and injects the anti-slop.md self-review checklist on substantial edits (>= ANTI_SLOP_CHECKLIST_LINES, default 40). Appends to the conversation's pending file; never blocks. Disable: HOOKS_ENFORCE=0 or ANTI_SLOP_ENFORCE=0."
41
+ },
42
+ {
43
+ "command": "bash ~/.agents/hooks/anchor-set-nudge.sh",
44
+ "timeout": 5,
45
+ "matcher": "^(Write|StrReplace|EditNotebook)$",
46
+ "_comment": "5s: PROACTIVE pre-compile nudge. On the FIRST edit of each implementation (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 implementation: gated by anchor-declared-<cid>.flag, which final-review.sh / subagent-stop-review.sh clear at the same per-implementation boundary as reviewed-<cid>.flag. Advisory only; never blocks. Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0."
41
47
  }
42
48
  ],
43
49
  "postToolUse": [
@@ -16,7 +16,7 @@ set +e
16
16
  cat >/dev/null
17
17
 
18
18
  context=""
19
- for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md" "$HOME/.cursor/declared-editing.md"; do
19
+ for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md" "$HOME/.cursor/declared-editing.md" "$HOME/.cursor/pre-compile.md"; do
20
20
  if [ -f "$p" ]; then
21
21
  part="$(cat "$p")"
22
22
  if [ -n "$context" ]; then context="$context
@@ -0,0 +1,72 @@
1
+ # Pre-compile — Thin Intent Compilation
2
+
3
+ ACTIVE EVERY IMPLEMENTATION TURN. Before writing or modifying a single line of
4
+ code, emit your Anchor Set. Compiling intent first is what stops the dilution
5
+ that no later axis can fully undo — a clean final review of the wrong feature
6
+ is still the wrong feature.
7
+
8
+ This is the proactive phase. The anti-slop checklist, the self-review trigger
9
+ and the final review are reactive — they audit after the fact. You compile the
10
+ intent BEFORE the first token of code so they have the right thing to audit.
11
+
12
+ ## The Anchor Set
13
+
14
+ Answer these four, terse, in your first response. One phrase each, not prose:
15
+
16
+ 1. OBJECTIVE — one operational sentence. What is *strictly* necessary. Not
17
+ "improve X" — "make X return Y when Z".
18
+ 2. CONSTRAINTS (local negations) — what you will NOT do. "NO schema migration.
19
+ NO new dependency. NO refactor of the surrounding function." Negations bind
20
+ harder than the objective: a constraint that the task contradicts is a bug
21
+ in your reading of the task, and you ask before you override it.
22
+ 3. SCOPE —
23
+ - FILES TO TOUCH: exact list, derived from the objective, nothing speculative.
24
+ - FILES UNTOUCHABLE: anything the system marked off-limits (.cursor state,
25
+ lockfiles you weren't asked to touch, files outside the request's blast
26
+ radius).
27
+ 4. DETERMINISTIC SUCCESS — the one command, test, or observable check that
28
+ will decide whether this is done. "Tests pass" is not deterministic; the
29
+ specific failing test going green is. If you cannot name one, you do not
30
+ yet understand the task — ask.
31
+
32
+ ## Materialize it: .scope.json
33
+
34
+ Write the Anchor Set to `.scope.json` in the repo root before editing source.
35
+ This is the machine-checkable form — the scope-gate hook audits every edit
36
+ against `files[]`, and the final-review axis 0 traces every diff hunk back to
37
+ `intent`. An Anchor Set that lives only in your head is not an Anchor Set.
38
+
39
+ ```json
40
+ {
41
+ "intent": "<OBJECTIVE>",
42
+ "files": ["<FILES TO TOUCH, repo-relative, glob-friendly>"],
43
+ "acceptance": "<DETERMINISTIC SUCCESS>",
44
+ "allow_growth": false
45
+ }
46
+ ```
47
+
48
+ `allow_growth: false` is the default — the gate fires on any edit outside
49
+ `files[]`. Set it true only if you expect the work to discover new files
50
+ (a refactor, a migration) and you will justify each one as it appears.
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.
55
+
56
+ ## Regla R3 — Authority
57
+
58
+ If, during execution, you read logs or code that contradict these anchors,
59
+ **the anchors win.** Prior history in this session is auditor material, not
60
+ authority. An earlier wrong assumption of yours does not override the Anchor
61
+ Set you declared at the start.
62
+
63
+ ## Regla R1 — On re-entry (when the loop hands you back a failure)
64
+
65
+ If the harness returns a gate failure or a failed axis: forget the approach
66
+ that produced it. Re-read your OBJECTIVE and your Anchor Set, not your prior
67
+ diff. Fix ONLY what is failing. Do not refactor in the same pass — that is
68
+ History Propagation, the failure mode the Anchor Set exists to prevent.
69
+
70
+ ---
71
+
72
+ End of pre-compile. Now emit the Anchor Set, then do the work.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cursordoctrine",
3
- "version": "0.3.3",
4
- "description": "Thin self-review hooks for Cursor — the model is the auditor. Intent-trace final review (Tier 0), unified 13-item anti-slop checklist, operational slop detection.",
3
+ "version": "0.4.0",
4
+ "description": "Thin self-review hooks for Cursor — the model is the auditor. Pre-compile Anchor Set phase (proactive intent compilation), 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
  },
@@ -5,7 +5,16 @@ One job: given a repo-relative path and a list of declared-scope patterns
5
5
  (from .scope.json `files`), return whether the path is in scope. Shared
6
6
  between the per-edit scope-gate-audit hook (afterFileEdit) and final-review's
7
7
  declared-scope check (Step C), so the two never disagree on what counts as
8
- "in scope".
8
+ "in scope". It also surfaces the contract's `intent` and `acceptance` fields
9
+ so the calling hook can quote them back to the agent.
10
+
11
+ .scope.json schema (intent + files[] + acceptance + allow_growth):
12
+ {
13
+ "intent": "one operational sentence of objective",
14
+ "files": [ "repo-relative globs", ... ],
15
+ "acceptance": "the deterministic check that decides success",
16
+ "allow_growth": false
17
+ }
9
18
 
10
19
  Pattern support:
11
20
  - exact path: src/components/LoginButton.tsx
@@ -17,7 +26,8 @@ Stdlib only; Python 3.9+. REPORTS only - never edits.
17
26
 
18
27
  CLI:
19
28
  scope_match.py --path src/auth/session.ts --patterns-file .scope.json
20
- -> prints JSON {"in_scope": false, "matched_by": null} and exits 0
29
+ -> prints JSON {"in_scope": false, "matched_by": null, "acceptance": "..."}
30
+ and exits 0
21
31
  -> if .scope.json is missing or unparseable, prints {"in_scope": true,
22
32
  "skipped": "no .scope.json"} (fail-open: no contract = no gate)
23
33
  """
@@ -129,6 +139,7 @@ def main() -> int:
129
139
  "matched_by": by,
130
140
  "allow_growth": bool(scope.get("allow_growth", False)),
131
141
  "intent": scope.get("intent", ""),
142
+ "acceptance": scope.get("acceptance", ""),
132
143
  }
133
144
 
134
145
  print(json.dumps(result))
@@ -0,0 +1,76 @@
1
+ # anchor-set-nudge.ps1 - afterFileEdit "pre-compile" nudge (Cursor).
2
+ #
3
+ # Proactive counterpart to the reactive audits. On the FIRST file edit of an
4
+ # implementation (per conversation), remind the agent to compile its Anchor Set
5
+ # (pre-compile.md) and write .scope.json BEFORE piling on more code. The
6
+ # reactive stack (self-review, anti-slop, final-review) only fires AFTER code
7
+ # exists; this nudge catches intent dilution at token ~50, not at the ~5000 of
8
+ # the stop-hook axis 0. A clean final review of the wrong feature is still the
9
+ # wrong feature - the Anchor Set exists so the right feature is on the rails
10
+ # from the first edit.
11
+ #
12
+ # One-shot PER IMPLEMENTATION, not per session: gated by an
13
+ # anchor-declared-<cid>.flag in the pending dir. That flag is cleared by
14
+ # final-review.ps1 / subagent-stop-review.ps1 at the SAME per-implementation
15
+ # boundary where they clear session-edits-<cid>.txt and reviewed-<cid>.flag
16
+ # (the stop after a review pass). So a long conversation with N implementations
17
+ # gets N nudges, not one - every new body of work re-earns the reminder.
18
+ #
19
+ # Advisory only: never blocks, never reads the diff, ALWAYS exits 0. Appends to
20
+ # the shared feedback-<cid>.txt bus; post-tool-use.ps1 delivers it next turn.
21
+ # Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0
22
+
23
+ $ErrorActionPreference = 'SilentlyContinue'
24
+ . "$PSScriptRoot\hook-common.ps1"
25
+
26
+ if ($env:HOOKS_ENFORCE -eq '0' -or $env:ANCHOR_NUDGE_ENFORCE -eq '0') { exit 0 }
27
+
28
+ $obj = Read-HookStdinJson
29
+ if (-not $obj) { exit 0 }
30
+
31
+ $filePath = ''
32
+ if ($obj.PSObject.Properties['file_path']) { $filePath = [string]$obj.file_path }
33
+ elseif ($obj.PSObject.Properties['path']) { $filePath = [string]$obj.path }
34
+ elseif ($obj.PSObject.Properties['filePath']) { $filePath = [string]$obj.filePath }
35
+ if (-not $filePath) { exit 0 }
36
+ if (Test-IsCursorConfigPath $filePath) { exit 0 }
37
+
38
+ $cid = Get-SafeConversationId $obj
39
+ $pendingDir = Get-HooksPendingDir
40
+ $flag = Join-Path $pendingDir "anchor-declared-$cid.flag"
41
+
42
+ # Already nudged this implementation -> stay quiet. The flag is cleared at the
43
+ # per-implementation boundary in final-review.ps1 / subagent-stop-review.ps1.
44
+ if (Test-Path $flag) { exit 0 }
45
+
46
+ $msg = @"
47
+ PRE-COMPILE NUDGE - first edit of this implementation: $filePath
48
+
49
+ Before you keep going, did you compile your Anchor Set (pre-compile.md)?
50
+ 1. OBJECTIVE - one operational sentence. What is strictly necessary.
51
+ 2. CONSTRAINTS - local negations (what you will NOT do).
52
+ 3. SCOPE - files to touch, files untouchable.
53
+ 4. SUCCESS - the one deterministic check that decides done.
54
+
55
+ If you have not already, write it to .scope.json now (intent / files /
56
+ acceptance / allow_growth). The scope-gate audits every edit against files[],
57
+ and final-review axis 0 traces every diff hunk back to intent. An Anchor Set
58
+ that lives only in your head is not an Anchor Set - the gate cannot audit it.
59
+
60
+ Skip this for trivial one-liners (typo, literal). Otherwise: compile, then code.
61
+ "@
62
+
63
+ # Append to the shared pending file (same bus as the other advisories).
64
+ $pending = Join-Path $pendingDir "feedback-$cid.txt"
65
+ try {
66
+ New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
67
+ $prefix = ''
68
+ if ((Test-Path $pending) -and ((Get-Item $pending).Length -gt 0)) { $prefix = "`n`n---`n`n" }
69
+ Add-Content -Path $pending -Value ($prefix + $msg) -NoNewline
70
+ } catch { }
71
+
72
+ # Arm the one-shot brake BEFORE returning, so a crash after the append can't
73
+ # re-nudge on the next edit. Mirrors the arming order in final-review.ps1.
74
+ New-Item -ItemType File -Path $flag -Force -ErrorAction SilentlyContinue | Out-Null
75
+
76
+ exit 0
@@ -40,6 +40,7 @@ $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 = Join-Path $pendingDir "anchor-declared-$cid.flag"
43
44
 
44
45
  # Sweep state from sessions that died before their stop hook ran. Cheap (one
45
46
  # directory listing on an event that fires once per agent loop).
@@ -49,8 +50,10 @@ Get-ChildItem $pendingDir -File -ErrorAction SilentlyContinue |
49
50
 
50
51
  # One-shot brake: the previous stop for this conversation emitted the review.
51
52
  # 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).
52
55
  if (Test-Path $flag) {
53
- Remove-Item $flag, $marker -Force -ErrorAction SilentlyContinue
56
+ Remove-Item $flag, $marker, $anchorFlag -Force -ErrorAction SilentlyContinue
54
57
  Emit-None
55
58
  }
56
59
 
@@ -102,6 +105,11 @@ Fix now, re-run the scan + tests, then stop. If an axis is clean, say so in one
102
105
  }
103
106
  $body = Expand-AgentPaths $body
104
107
 
108
+ # Regla R1 (re-entry): if this review pass is a re-audit after a failed gate or
109
+ # axis, suppress History Propagation - the model must NOT build on its own prior
110
+ # wrong diff. Reset its prior to the Anchor Set, not to its previous attempt.
111
+ $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, if you wrote one). 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"
112
+
105
113
  $resolved = @($edited | ForEach-Object { Resolve-AgentPath $_ })
106
114
  $fileList = ($resolved | Select-Object -First 30) -join "`n "
107
115
 
@@ -121,7 +129,7 @@ if ($userQuery) {
121
129
  $uniqueFiles = @($edited | Select-Object -Unique).Count
122
130
  $surfaceBlock = "Session footprint: $uniqueFiles file(s) touched. If a simple request produced >5 files or >200 lines, justify each file's inclusion or trim.`n`n"
123
131
 
124
- $msg = "FINAL REVIEW (end of implementation) - intent, correctness, reliability, coverage, anti-slop.`n`n${surfaceBlock}${intentBlock}Files you changed this session:`n $fileList`n`n$body"
132
+ $msg = "FINAL REVIEW (end of implementation) - intent, correctness, reliability, coverage, anti-slop.`n`n${surfaceBlock}${intentBlock}Files you changed this session:`n $fileList`n`n$body${reentryLine}"
125
133
 
126
134
  # Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
127
135
  New-Item -ItemType File -Path $flag -Force -ErrorAction SilentlyContinue | Out-Null
@@ -1,98 +1,98 @@
1
- # permission-gate.ps1 - beforeShellExecution for Cursor.
2
- #
3
- # Single responsibility: deny a small, explicit list of dangerous commands.
4
- # This is a *permission* gate, not a *quality* gate. The model handles
5
- # quality; the harness handles blast radius.
6
- #
7
- # Behavior:
8
- # - Exit 0 always.
9
- # - Print Cursor-canonical {"permission": "allow"|"deny", ...} JSON.
10
- # - On internal failure: fail OPEN (allow), never block the user.
11
- #
12
- # Disable: PERM_GATE_ENFORCE=0
13
-
14
- $ErrorActionPreference = 'SilentlyContinue'
15
- . "$PSScriptRoot\hook-common.ps1"
16
-
17
- if ($env:PERM_GATE_ENFORCE -eq '0') {
18
- Write-HookJson @{ permission = 'allow' }
19
- exit 0
20
- }
21
-
22
- # Without BOM-safe decode the JSON never parses, the raw-text fallback below
23
- # matches deny patterns anywhere in the envelope (false positives), and the
24
- # deny message leaks conversation id / transcript path / user email into the UI.
25
- $inputText = Read-HookStdin
26
-
27
- $cmd = ''
28
- if ($inputText) {
29
- try {
30
- $obj = $inputText | ConvertFrom-Json
31
- if ($obj -and $obj.PSObject.Properties['command']) {
32
- $cmd = [string]$obj.command
33
- }
34
- } catch {
35
- $cmd = ''
36
- }
37
- # Belt-and-braces: if stdin was not the documented JSON shape, still gate
38
- # on the raw text rather than waving everything through.
39
- if (-not $cmd) { $cmd = $inputText }
40
- }
41
-
42
- if (-not $cmd) {
43
- Write-HookJson @{ permission = 'allow' }
44
- exit 0
45
- }
46
-
47
- function Test-Deny {
48
- param([string]$Pattern, [string]$Reason)
49
- if ($cmd -match $Pattern) { Deny $Reason }
50
- }
51
-
52
- function Deny {
53
- param([string]$Reason)
54
- # Truncate the echo: the command can be a multi-hundred-char one-liner and
55
- # the UI message only needs enough to identify it.
56
- $shown = if ($cmd.Length -gt 400) { $cmd.Substring(0, 400) + '...' } else { $cmd }
57
- $userMsg = "BLOCKED by permission-gate: $Reason`n`nCommand: $shown`n`nIf this is genuinely intended, run it yourself in your terminal."
58
- Write-HookJson @{
59
- permission = 'deny'
60
- user_message = $userMsg
61
- agent_message = "$userMsg Do not retry verbatim. Ask the user to run it manually if it is truly intended."
62
- }
63
- exit 0
64
- }
65
-
66
- # --- POSIX-flavored ---------------------------------------------------------
67
- # Anchored to start OR a command separator so `cd /tmp && rm -rf /` is caught,
68
- # while `git rm`, `npm run rm-cache`, `echo "rm -rf /"` stay allowed.
69
- Test-Deny '(?:^|[;&|]\s*)(?:sudo\s+)?rm\s+-[a-zA-Z]*([rR][fF]|[fF][rR])[a-zA-Z]*\s+/' 'destructive rm -rf on absolute path (use relative paths or be more specific)'
70
- Test-Deny ':\(\)\{\s*:\|:&\s*\};:|bash\s+-c\s+["'']*:\s*\(\)\{' 'reverse shell / fork-bomb pattern'
71
- Test-Deny 'curl\s.*\|\s*(sudo\s*)?(bash|sh|zsh|dash|ash)' 'curl piped to shell'
72
- Test-Deny 'wget\s.*\|\s*(sudo\s*)?(bash|sh|zsh|dash|ash)' 'wget piped to shell'
73
- Test-Deny 'git\s+push\s+.*--force(-with-lease)?(\s|$)' 'git push --force'
74
- Test-Deny 'git\s+push\s+(-f|--force)(\s|$)' 'git push -f / --force'
75
- Test-Deny 'git\s+reset\s+--hard' 'git reset --hard (data loss)'
76
- Test-Deny 'git\s+clean\s+-[a-zA-Z]*f' 'git clean -f (untracked data loss)'
77
- Test-Deny 'dd\s.*of=/dev/(sd|nvme|hd|xvd)' 'dd to block device'
78
- Test-Deny 'mkfs(\.[a-z0-9]+)?\s+/dev/' 'mkfs on device'
79
- Test-Deny 'chmod\s+-R\s+777\s+/' 'chmod -R 777 on root'
80
- Test-Deny 'chown\s+-R\s+[^\s]+\s+/' 'chown -R on root'
81
- Test-Deny '^(npm|pnpm|yarn)\s+publish(\s|$)' 'package publish (use ship-hook, not direct publish)'
82
-
83
- # --- Windows equivalents (the agent shell here IS PowerShell) ---------------
84
- # iwr/irm | iex is the moral twin of curl|sh.
85
- Test-Deny '\b(iwr|irm|curl|wget|Invoke-WebRequest|Invoke-RestMethod)\b[^|]*\|\s*(iex\b|Invoke-Expression)' 'web download piped to Invoke-Expression'
86
- # Disk-level destruction, twin of mkfs / dd-to-device.
87
- Test-Deny '\b(Format-Volume|Clear-Disk)\b' 'disk format / clear (destructive)'
88
- # Recursive+forced delete of a bare drive root, user-profile root, or
89
- # C:\Users / C:\Windows. Twin of rm -rf /. Composed checks instead of one
90
- # unreadable regex; subfolder deletes (e.g. C:\Temp\x) stay allowed.
91
- $rmVerb = '(?:^|[;&|]\s*)(?:Remove-Item|rm|ri|del|erase|rd|rmdir)\s'
92
- $rootPath = '(?:^|[\s"''])(?:[A-Za-z]:[\\/]{0,2}|[A-Za-z]:[\\/](?:Users|Windows)[\\/]?|\$(?:env:USERPROFILE|HOME)[\\/]?)["'']?\s*(?:$|[;&|-])'
93
- if (($cmd -match $rmVerb) -and ($cmd -match $rootPath) -and ($cmd -match '(?:-Recurse\b|/s\b)') -and ($cmd -match '(?:-Force\b|/q\b)')) {
94
- Deny 'recursive forced delete of a drive root / Users / Windows / profile root'
95
- }
96
-
97
- Write-HookJson @{ permission = 'allow' }
98
- exit 0
1
+ # permission-gate.ps1 - beforeShellExecution for Cursor.
2
+ #
3
+ # Single responsibility: deny a small, explicit list of dangerous commands.
4
+ # This is a *permission* gate, not a *quality* gate. The model handles
5
+ # quality; the harness handles blast radius.
6
+ #
7
+ # Behavior:
8
+ # - Exit 0 always.
9
+ # - Print Cursor-canonical {"permission": "allow"|"deny", ...} JSON.
10
+ # - On internal failure: fail OPEN (allow), never block the user.
11
+ #
12
+ # Disable: PERM_GATE_ENFORCE=0
13
+
14
+ $ErrorActionPreference = 'SilentlyContinue'
15
+ . "$PSScriptRoot\hook-common.ps1"
16
+
17
+ if ($env:PERM_GATE_ENFORCE -eq '0') {
18
+ Write-HookJson @{ permission = 'allow' }
19
+ exit 0
20
+ }
21
+
22
+ # Without BOM-safe decode the JSON never parses, the raw-text fallback below
23
+ # matches deny patterns anywhere in the envelope (false positives), and the
24
+ # deny message leaks conversation id / transcript path / user email into the UI.
25
+ $inputText = Read-HookStdin
26
+
27
+ $cmd = ''
28
+ if ($inputText) {
29
+ try {
30
+ $obj = $inputText | ConvertFrom-Json
31
+ if ($obj -and $obj.PSObject.Properties['command']) {
32
+ $cmd = [string]$obj.command
33
+ }
34
+ } catch {
35
+ $cmd = ''
36
+ }
37
+ # Belt-and-braces: if stdin was not the documented JSON shape, still gate
38
+ # on the raw text rather than waving everything through.
39
+ if (-not $cmd) { $cmd = $inputText }
40
+ }
41
+
42
+ if (-not $cmd) {
43
+ Write-HookJson @{ permission = 'allow' }
44
+ exit 0
45
+ }
46
+
47
+ function Test-Deny {
48
+ param([string]$Pattern, [string]$Reason)
49
+ if ($cmd -match $Pattern) { Deny $Reason }
50
+ }
51
+
52
+ function Deny {
53
+ param([string]$Reason)
54
+ # Truncate the echo: the command can be a multi-hundred-char one-liner and
55
+ # the UI message only needs enough to identify it.
56
+ $shown = if ($cmd.Length -gt 400) { $cmd.Substring(0, 400) + '...' } else { $cmd }
57
+ $userMsg = "BLOCKED by permission-gate: $Reason`n`nCommand: $shown`n`nIf this is genuinely intended, run it yourself in your terminal."
58
+ Write-HookJson @{
59
+ permission = 'deny'
60
+ user_message = $userMsg
61
+ agent_message = "$userMsg Do not retry verbatim. Ask the user to run it manually if it is truly intended."
62
+ }
63
+ exit 0
64
+ }
65
+
66
+ # --- POSIX-flavored ---------------------------------------------------------
67
+ # Anchored to start OR a command separator so `cd /tmp && rm -rf /` is caught,
68
+ # while `git rm`, `npm run rm-cache`, `echo "rm -rf /"` stay allowed.
69
+ Test-Deny '(?:^|[;&|]\s*)(?:sudo\s+)?rm\s+-[a-zA-Z]*([rR][fF]|[fF][rR])[a-zA-Z]*\s+/' 'destructive rm -rf on absolute path (use relative paths or be more specific)'
70
+ Test-Deny ':\(\)\{\s*:\|:&\s*\};:|bash\s+-c\s+["'']*:\s*\(\)\{' 'reverse shell / fork-bomb pattern'
71
+ Test-Deny 'curl\s.*\|\s*(sudo\s*)?(bash|sh|zsh|dash|ash)' 'curl piped to shell'
72
+ Test-Deny 'wget\s.*\|\s*(sudo\s*)?(bash|sh|zsh|dash|ash)' 'wget piped to shell'
73
+ Test-Deny 'git\s+push\s+.*--force(-with-lease)?(\s|$)' 'git push --force'
74
+ Test-Deny 'git\s+push\s+(-f|--force)(\s|$)' 'git push -f / --force'
75
+ Test-Deny 'git\s+reset\s+--hard' 'git reset --hard (data loss)'
76
+ Test-Deny 'git\s+clean\s+-[a-zA-Z]*f' 'git clean -f (untracked data loss)'
77
+ Test-Deny 'dd\s.*of=/dev/(sd|nvme|hd|xvd)' 'dd to block device'
78
+ Test-Deny 'mkfs(\.[a-z0-9]+)?\s+/dev/' 'mkfs on device'
79
+ Test-Deny 'chmod\s+-R\s+777\s+/' 'chmod -R 777 on root'
80
+ Test-Deny 'chown\s+-R\s+[^\s]+\s+/' 'chown -R on root'
81
+ Test-Deny '^(npm|pnpm|yarn)\s+publish(\s|$)' 'package publish (use ship-hook, not direct publish)'
82
+
83
+ # --- Windows equivalents (the agent shell here IS PowerShell) ---------------
84
+ # iwr/irm | iex is the moral twin of curl|sh.
85
+ Test-Deny '\b(iwr|irm|curl|wget|Invoke-WebRequest|Invoke-RestMethod)\b[^|]*\|\s*(iex\b|Invoke-Expression)' 'web download piped to Invoke-Expression'
86
+ # Disk-level destruction, twin of mkfs / dd-to-device.
87
+ Test-Deny '\b(Format-Volume|Clear-Disk)\b' 'disk format / clear (destructive)'
88
+ # Recursive+forced delete of a bare drive root, user-profile root, or
89
+ # C:\Users / C:\Windows. Twin of rm -rf /. Composed checks instead of one
90
+ # unreadable regex; subfolder deletes (e.g. C:\Temp\x) stay allowed.
91
+ $rmVerb = '(?:^|[;&|]\s*)(?:Remove-Item|rm|ri|del|erase|rd|rmdir)\s'
92
+ $rootPath = '(?:^|[\s"''])(?:[A-Za-z]:[\\/]{0,2}|[A-Za-z]:[\\/](?:Users|Windows)[\\/]?|\$(?:env:USERPROFILE|HOME)[\\/]?)["'']?\s*(?:$|[;&|-])'
93
+ if (($cmd -match $rmVerb) -and ($cmd -match $rootPath) -and ($cmd -match '(?:-Recurse\b|/s\b)') -and ($cmd -match '(?:-Force\b|/q\b)')) {
94
+ Deny 'recursive forced delete of a drive root / Users / Windows / profile root'
95
+ }
96
+
97
+ Write-HookJson @{ permission = 'allow' }
98
+ exit 0
@@ -78,6 +78,8 @@ $allowGrowth = $false
78
78
  if ($payload.PSObject.Properties['allow_growth'] -and $payload.allow_growth) { $allowGrowth = $true }
79
79
  $intent = ''
80
80
  if ($payload.PSObject.Properties['intent']) { $intent = [string]$payload.intent }
81
+ $acceptance = ''
82
+ if ($payload.PSObject.Properties['acceptance']) { $acceptance = [string]$payload.acceptance }
81
83
 
82
84
  # Read the declared files list for the message (best-effort; skip on failure)
83
85
  $declaredFiles = ''
@@ -86,6 +88,11 @@ try {
86
88
  if ($scopeJson.files) { $declaredFiles = ($scopeJson.files -join ', ') }
87
89
  } catch { }
88
90
 
91
+ # acceptance line: only quote it when the agent bothered to declare one. A blank
92
+ # acceptance means the Anchor Set was incomplete - surface that gap, since the
93
+ # whole point of the pre-compile phase is to name the deterministic success check.
94
+ $acceptanceLine = if ($acceptance) { $acceptance } else { '(not declared — your Anchor Set is missing the ÉXITO/acceptance field)' }
95
+
89
96
  if ($allowGrowth) {
90
97
  # Growth is allowed: informational, not a violation
91
98
  $summary = "Scope note - $rel is new vs your declared scope (growth allowed)"
@@ -93,6 +100,9 @@ if ($allowGrowth) {
93
100
  You touched a file outside your initial declared set. Since allow_growth is
94
101
  true, this is not a violation, but justify it: add $rel to .scope.json or
95
102
  explain why the scope grew.
103
+
104
+ Your success contract (acceptance): $acceptanceLine
105
+ Does growing into $rel still serve that?
96
106
  "@
97
107
  } else {
98
108
  # Hard violation: edited outside the declared contract
@@ -101,6 +111,7 @@ if ($allowGrowth) {
101
111
  Your contract (.scope.json):
102
112
  intent: $intent
103
113
  files: $declaredFiles
114
+ acceptance: $acceptanceLine
104
115
 
105
116
  You declared these files and touched one outside the set. Either:
106
117
  1. Add $rel to .scope.json with a one-line justification, OR
@@ -1,83 +1,83 @@
1
- # self-review-trigger.ps1 - afterFileEdit for Cursor.
2
- #
3
- # Single responsibility: when the model just edited a file, hand the
4
- # edit context to the NEXT model turn as additional_context. The model
5
- # is the auditor; the harness is just the message bus.
6
- #
7
- # This is intentionally minimal:
8
- # - We do NOT parse the diff ourselves.
9
- # - We do NOT spawn a sub-agent.
10
- # - We do NOT write to .stuck-files/.
11
- # - We do NOT block.
12
- #
13
- # We DO:
14
- # - Capture the edited file path.
15
- # - Stash a self-review prompt that primes the model's next turn.
16
- # - Exit 0 always.
17
- #
18
- # Cursor's afterFileEdit doesn't consume its own output. To actually
19
- # surface the message, post-tool-use.ps1 re-emits it on the next tool
20
- # boundary. See hooks.json.
21
-
22
- $ErrorActionPreference = 'SilentlyContinue'
23
- . "$PSScriptRoot\hook-common.ps1"
24
-
25
- $inputText = Read-HookStdin
26
-
27
- $filePath = ''
28
- $cid = ''
29
- if ($inputText) {
30
- try {
31
- $obj = $inputText | ConvertFrom-Json
32
- if ($obj) {
33
- if ($obj.PSObject.Properties['file_path']) { $filePath = [string]$obj.file_path }
34
- elseif ($obj.PSObject.Properties['path']) { $filePath = [string]$obj.path }
35
- elseif ($obj.PSObject.Properties['filePath']) { $filePath = [string]$obj.filePath }
36
- $cid = Get-SafeConversationId $obj
37
- }
38
- } catch {
39
- $filePath = ''
40
- }
41
- }
42
-
43
- # Empty path (JSON parse failed, or no file_path field) -> nothing to record.
44
- # Without this guard the .cursor regex below doesn't match '' and we append a
45
- # blank line to the session-edits marker on every such fire (it accumulates fast).
46
- if (-not $filePath) { exit 0 }
47
- if (Test-IsCursorConfigPath $filePath) { exit 0 }
48
-
49
- # State is keyed by conversation_id and lives under $HOME, never the project:
50
- # no repo litter, works in workspace-less sessions (CURSOR_PROJECT_DIR/
51
- # workspace_roots are empty there), and concurrent sessions cannot drain each
52
- # other's prompts.
53
- $pendingDir = Get-HooksPendingDir
54
-
55
- # Record this edit for the end-of-implementation review. The stop hook
56
- # (final-review.ps1) drains this marker to fire one final review pass over
57
- # everything changed this agent loop. Append = running list of edits.
58
- try {
59
- $mk = Join-Path $pendingDir "session-edits-$cid.txt"
60
- New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
61
- Add-Content -Path $mk -Value $filePath
62
- } catch { }
63
-
64
- $doctrineFile = Join-Path $HOME '.agents\hooks\self-review.md'
65
- if (-not (Test-Path $doctrineFile)) { exit 0 }
66
- $doctrine = Get-Content $doctrineFile -Raw
67
-
68
- $msg = "SELF-REVIEW TRIGGER - you just edited: $filePath`n`n$doctrine"
69
-
70
- $pendingFile = Join-Path $pendingDir "feedback-$cid.txt"
71
-
72
- try {
73
- New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
74
- $prefix = ''
75
- if ((Test-Path $pendingFile) -and ((Get-Item $pendingFile).Length -gt 0)) {
76
- $prefix = "`n`n---`n`n"
77
- }
78
- Add-Content -Path $pendingFile -Value ($prefix + $msg) -NoNewline
79
- } catch {
80
- # Silently fail open
81
- }
82
-
83
- exit 0
1
+ # self-review-trigger.ps1 - afterFileEdit for Cursor.
2
+ #
3
+ # Single responsibility: when the model just edited a file, hand the
4
+ # edit context to the NEXT model turn as additional_context. The model
5
+ # is the auditor; the harness is just the message bus.
6
+ #
7
+ # This is intentionally minimal:
8
+ # - We do NOT parse the diff ourselves.
9
+ # - We do NOT spawn a sub-agent.
10
+ # - We do NOT write to .stuck-files/.
11
+ # - We do NOT block.
12
+ #
13
+ # We DO:
14
+ # - Capture the edited file path.
15
+ # - Stash a self-review prompt that primes the model's next turn.
16
+ # - Exit 0 always.
17
+ #
18
+ # Cursor's afterFileEdit doesn't consume its own output. To actually
19
+ # surface the message, post-tool-use.ps1 re-emits it on the next tool
20
+ # boundary. See hooks.json.
21
+
22
+ $ErrorActionPreference = 'SilentlyContinue'
23
+ . "$PSScriptRoot\hook-common.ps1"
24
+
25
+ $inputText = Read-HookStdin
26
+
27
+ $filePath = ''
28
+ $cid = ''
29
+ if ($inputText) {
30
+ try {
31
+ $obj = $inputText | ConvertFrom-Json
32
+ if ($obj) {
33
+ if ($obj.PSObject.Properties['file_path']) { $filePath = [string]$obj.file_path }
34
+ elseif ($obj.PSObject.Properties['path']) { $filePath = [string]$obj.path }
35
+ elseif ($obj.PSObject.Properties['filePath']) { $filePath = [string]$obj.filePath }
36
+ $cid = Get-SafeConversationId $obj
37
+ }
38
+ } catch {
39
+ $filePath = ''
40
+ }
41
+ }
42
+
43
+ # Empty path (JSON parse failed, or no file_path field) -> nothing to record.
44
+ # Without this guard the .cursor regex below doesn't match '' and we append a
45
+ # blank line to the session-edits marker on every such fire (it accumulates fast).
46
+ if (-not $filePath) { exit 0 }
47
+ if (Test-IsCursorConfigPath $filePath) { exit 0 }
48
+
49
+ # State is keyed by conversation_id and lives under $HOME, never the project:
50
+ # no repo litter, works in workspace-less sessions (CURSOR_PROJECT_DIR/
51
+ # workspace_roots are empty there), and concurrent sessions cannot drain each
52
+ # other's prompts.
53
+ $pendingDir = Get-HooksPendingDir
54
+
55
+ # Record this edit for the end-of-implementation review. The stop hook
56
+ # (final-review.ps1) drains this marker to fire one final review pass over
57
+ # everything changed this agent loop. Append = running list of edits.
58
+ try {
59
+ $mk = Join-Path $pendingDir "session-edits-$cid.txt"
60
+ New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
61
+ Add-Content -Path $mk -Value $filePath
62
+ } catch { }
63
+
64
+ $doctrineFile = Join-Path $HOME '.agents\hooks\self-review.md'
65
+ if (-not (Test-Path $doctrineFile)) { exit 0 }
66
+ $doctrine = Get-Content $doctrineFile -Raw
67
+
68
+ $msg = "SELF-REVIEW TRIGGER - you just edited: $filePath`n`n$doctrine"
69
+
70
+ $pendingFile = Join-Path $pendingDir "feedback-$cid.txt"
71
+
72
+ try {
73
+ New-Item -ItemType Directory -Path $pendingDir -Force | Out-Null
74
+ $prefix = ''
75
+ if ((Test-Path $pendingFile) -and ((Get-Item $pendingFile).Length -gt 0)) {
76
+ $prefix = "`n`n---`n`n"
77
+ }
78
+ Add-Content -Path $pendingFile -Value ($prefix + $msg) -NoNewline
79
+ } catch {
80
+ # Silently fail open
81
+ }
82
+
83
+ exit 0
@@ -43,10 +43,13 @@ $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 = Join-Path $pendingDir "anchor-declared-$cid.flag"
46
47
 
47
48
  # 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).
48
51
  if (Test-Path $flag) {
49
- Remove-Item $flag, $marker -Force -ErrorAction SilentlyContinue
52
+ Remove-Item $flag, $marker, $anchorFlag -Force -ErrorAction SilentlyContinue
50
53
  Emit-None
51
54
  }
52
55
 
@@ -81,9 +84,14 @@ If an axis is clean, say so in one line. Then stop.
81
84
  }
82
85
  $body = Expand-AgentPaths $body
83
86
 
87
+ # Regla R1 (re-entry): same suppression as final-review.ps1. A subagent that
88
+ # failed an axis must not build on its own prior wrong diff - reset its prior
89
+ # to the Anchor Set, not to its previous attempt.
90
+ $reentryLine = "`n`nRE-ENTRY RULE (Regla R1): if an axis failed, forget the approach that produced it. Re-read your original task and your Anchor Set (.scope.json, if you wrote one). Fix ONLY what is failing. Do not refactor in this pass.`n"
91
+
84
92
  $resolved = @($edited | ForEach-Object { Resolve-AgentPath $_ })
85
93
  $fileList = ($resolved | Select-Object -First 30) -join "`n "
86
- $msg = "SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.`n`nFiles you changed this run:`n $fileList`n`n$body"
94
+ $msg = "SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.`n`nFiles you changed this run:`n $fileList`n`n$body${reentryLine}"
87
95
 
88
96
  # Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
89
97
  New-Item -ItemType File -Path $flag -Force -ErrorAction SilentlyContinue | Out-Null
@@ -38,6 +38,12 @@
38
38
  "timeout": 15,
39
39
  "matcher": "^(Write|StrReplace|EditNotebook)$",
40
40
  "_comment": "15s: AI-slop advisory, companion to minimal-edit-audit. Native git diff flags new deps / premature abstractions (Factory/Repository/Mediator/CQRS/DDD) / redundant comments, and injects the anti-slop.md self-review checklist on substantial edits (>= ANTI_SLOP_CHECKLIST_LINES, default 40). Appends to the conversation's pending file; never blocks. Disable: HOOKS_ENFORCE=0 or ANTI_SLOP_ENFORCE=0."
41
+ },
42
+ {
43
+ "command": "pwsh.exe -NoProfile -File ~/.agents/hooks/anchor-set-nudge.ps1",
44
+ "timeout": 5,
45
+ "matcher": "^(Write|StrReplace|EditNotebook)$",
46
+ "_comment": "5s: PROACTIVE pre-compile nudge. On the FIRST edit of each implementation (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 implementation: gated by anchor-declared-<cid>.flag, which final-review.ps1 / subagent-stop-review.ps1 clear at the same per-implementation boundary as reviewed-<cid>.flag. Advisory only; never blocks. Disable: HOOKS_ENFORCE=0 or ANCHOR_NUDGE_ENFORCE=0."
41
47
  }
42
48
  ],
43
49
  "postToolUse": [
@@ -30,7 +30,8 @@ try {
30
30
  $paths = @(
31
31
  (Join-Path $PSScriptRoot 'doctrine.md'),
32
32
  (Join-Path $PSScriptRoot 'USER-RULES.md'),
33
- (Join-Path $PSScriptRoot 'declared-editing.md')
33
+ (Join-Path $PSScriptRoot 'declared-editing.md'),
34
+ (Join-Path $PSScriptRoot 'pre-compile.md')
34
35
  )
35
36
 
36
37
  $parts = foreach ($p in $paths) {
@@ -0,0 +1,72 @@
1
+ # Pre-compile — Thin Intent Compilation
2
+
3
+ ACTIVE EVERY IMPLEMENTATION TURN. Before writing or modifying a single line of
4
+ code, emit your Anchor Set. Compiling intent first is what stops the dilution
5
+ that no later axis can fully undo — a clean final review of the wrong feature
6
+ is still the wrong feature.
7
+
8
+ This is the proactive phase. The anti-slop checklist, the self-review trigger
9
+ and the final review are reactive — they audit after the fact. You compile the
10
+ intent BEFORE the first token of code so they have the right thing to audit.
11
+
12
+ ## The Anchor Set
13
+
14
+ Answer these four, terse, in your first response. One phrase each, not prose:
15
+
16
+ 1. OBJECTIVE — one operational sentence. What is *strictly* necessary. Not
17
+ "improve X" — "make X return Y when Z".
18
+ 2. CONSTRAINTS (local negations) — what you will NOT do. "NO schema migration.
19
+ NO new dependency. NO refactor of the surrounding function." Negations bind
20
+ harder than the objective: a constraint that the task contradicts is a bug
21
+ in your reading of the task, and you ask before you override it.
22
+ 3. SCOPE —
23
+ - FILES TO TOUCH: exact list, derived from the objective, nothing speculative.
24
+ - FILES UNTOUCHABLE: anything the system marked off-limits (.cursor state,
25
+ lockfiles you weren't asked to touch, files outside the request's blast
26
+ radius).
27
+ 4. DETERMINISTIC SUCCESS — the one command, test, or observable check that
28
+ will decide whether this is done. "Tests pass" is not deterministic; the
29
+ specific failing test going green is. If you cannot name one, you do not
30
+ yet understand the task — ask.
31
+
32
+ ## Materialize it: .scope.json
33
+
34
+ Write the Anchor Set to `.scope.json` in the repo root before editing source.
35
+ This is the machine-checkable form — the scope-gate hook audits every edit
36
+ against `files[]`, and the final-review axis 0 traces every diff hunk back to
37
+ `intent`. An Anchor Set that lives only in your head is not an Anchor Set.
38
+
39
+ ```json
40
+ {
41
+ "intent": "<OBJECTIVE>",
42
+ "files": ["<FILES TO TOUCH, repo-relative, glob-friendly>"],
43
+ "acceptance": "<DETERMINISTIC SUCCESS>",
44
+ "allow_growth": false
45
+ }
46
+ ```
47
+
48
+ `allow_growth: false` is the default — the gate fires on any edit outside
49
+ `files[]`. Set it true only if you expect the work to discover new files
50
+ (a refactor, a migration) and you will justify each one as it appears.
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.
55
+
56
+ ## Regla R3 — Authority
57
+
58
+ If, during execution, you read logs or code that contradict these anchors,
59
+ **the anchors win.** Prior history in this session is auditor material, not
60
+ authority. An earlier wrong assumption of yours does not override the Anchor
61
+ Set you declared at the start.
62
+
63
+ ## Regla R1 — On re-entry (when the loop hands you back a failure)
64
+
65
+ If the harness returns a gate failure or a failed axis: forget the approach
66
+ that produced it. Re-read your OBJECTIVE and your Anchor Set, not your prior
67
+ diff. Fix ONLY what is failing. Do not refactor in the same pass — that is
68
+ History Propagation, the failure mode the Anchor Set exists to prevent.
69
+
70
+ ---
71
+
72
+ End of pre-compile. Now emit the Anchor Set, then do the work.