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 +17 -1
- package/linux/hooks/anchor-set-nudge.sh +77 -0
- package/linux/hooks/final-review.sh +13 -2
- package/linux/hooks/scope-gate-audit.sh +17 -1
- package/linux/hooks/subagent-stop-review.sh +13 -2
- package/linux/hooks.json +6 -0
- package/linux/inject-doctrine.sh +1 -1
- package/linux/pre-compile.md +72 -0
- package/package.json +2 -2
- package/skills/anti-slop/scripts/scope_match.py +13 -2
- package/windows/hooks/anchor-set-nudge.ps1 +76 -0
- package/windows/hooks/final-review.ps1 +10 -2
- package/windows/hooks/permission-gate.ps1 +98 -98
- package/windows/hooks/scope-gate-audit.ps1 +11 -0
- package/windows/hooks/self-review-trigger.ps1 +83 -83
- package/windows/hooks/subagent-stop-review.ps1 +10 -2
- package/windows/hooks.json +6 -0
- package/windows/inject-doctrine.ps1 +2 -1
- package/windows/pre-compile.md +72 -0
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": [
|
package/linux/inject-doctrine.sh
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "Thin self-review hooks for Cursor — the model is the auditor.
|
|
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
|
|
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
|
package/windows/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. 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.
|