cursordoctrine 0.5.3 → 0.5.4
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/linux/hooks/intent-anchor.sh +18 -11
- package/linux/hooks/scope-gate-audit.sh +45 -110
- package/linux/hooks.json +1 -1
- package/package.json +1 -1
- package/windows/hooks/intent-anchor.ps1 +17 -13
- package/windows/hooks/scope-gate-audit.ps1 +54 -93
- package/windows/hooks.json +1 -1
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
# 2. AUTO-CREATE / REGENERATE .scope.json: when the current <user_query>
|
|
19
19
|
# differs from the contract on disk (no contract yet, OR _intent_hash
|
|
20
20
|
# mismatch), the hook WRITES a scaffold to the REPO ROOT: intent locked
|
|
21
|
-
# from the prompt, files
|
|
22
|
-
#
|
|
23
|
-
# a
|
|
24
|
-
#
|
|
25
|
-
#
|
|
21
|
+
# from the prompt, files as an EMPTY array (scope-gate-audit.sh fills it
|
|
22
|
+
# mechanically as the agent edits - the agent never maintains files[] by
|
|
23
|
+
# hand), acceptance as a TODO the agent sets. This is the user-requested
|
|
24
|
+
# behavior: every new prompt -> a fresh .scope.json the agent works from.
|
|
25
|
+
# Fixed vs the broken 0.4.4 build: never writes to $HOME (bails if no real
|
|
26
|
+
# root resolves -> no ghost files), regenerates on prompt CHANGE not just
|
|
27
|
+
# on absence.
|
|
26
28
|
# 3. RE-INJECT on same-prompt turns: when the query is unchanged (contract
|
|
27
29
|
# already current), the hook re-injects the existing contract into the
|
|
28
30
|
# feedback bus so it stays in the model's attentional focus each turn.
|
|
@@ -151,14 +153,14 @@ if [ "$should_write" = "1" ]; then
|
|
|
151
153
|
# is self-contained in the file (survives cross-session hash sweeps).
|
|
152
154
|
if have_jq; then
|
|
153
155
|
jq -n --arg intent "$current_query" --arg hash "$current_hash" \
|
|
154
|
-
'{intent:$intent, files:[
|
|
156
|
+
'{intent:$intent, files:[], acceptance:"<TODO: the one deterministic check that decides done>", allow_growth:false, _intent_hash:$hash, _generated_by:"intent-anchor hook"}' \
|
|
155
157
|
> "$scope_path" 2>/dev/null && regenerated=1
|
|
156
158
|
elif have_py; then
|
|
157
159
|
if I_FILE="$scope_path" I_INTENT="$current_query" I_HASH="$current_hash" python3 -c '
|
|
158
160
|
import json, os
|
|
159
161
|
obj = {
|
|
160
162
|
"intent": os.environ["I_INTENT"],
|
|
161
|
-
"files": [
|
|
163
|
+
"files": [],
|
|
162
164
|
"acceptance": "<TODO: the one deterministic check that decides done>",
|
|
163
165
|
"allow_growth": False,
|
|
164
166
|
"_intent_hash": os.environ["I_HASH"],
|
|
@@ -173,12 +175,16 @@ with open(os.environ["I_FILE"], "w", encoding="utf-8") as f:
|
|
|
173
175
|
if [ "$regenerated" = "1" ]; then
|
|
174
176
|
scope_intent="$current_query"
|
|
175
177
|
scope_acceptance="<TODO: the one deterministic check that decides done>"
|
|
176
|
-
scope_files="
|
|
178
|
+
scope_files="(auto-tracked - the scope hook records every file you edit)"
|
|
177
179
|
scope_exists=1
|
|
178
180
|
scope_stale=0
|
|
179
181
|
fi
|
|
180
182
|
fi
|
|
181
183
|
|
|
184
|
+
# files[] is auto-tracked and starts empty; show something readable until the
|
|
185
|
+
# scope hook has recorded the first edit.
|
|
186
|
+
[ -n "$scope_files" ] || scope_files="(none yet - auto-tracked as you edit)"
|
|
187
|
+
|
|
182
188
|
# --- compose the anchor message ---------------------------------------------
|
|
183
189
|
# Three states: regenerated this turn (new prompt), no contract (and no query
|
|
184
190
|
# to scaffold from), or re-injecting an existing current contract.
|
|
@@ -196,9 +202,10 @@ if [ "$regenerated" = "1" ]; then
|
|
|
196
202
|
acceptance: $scope_acceptance
|
|
197
203
|
|
|
198
204
|
The hook wrote a fresh scaffold to $scope_path from your current request. intent
|
|
199
|
-
is locked from what you just asked.
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
is locked from what you just asked. files[] is AUTO-TRACKED - the scope hook
|
|
206
|
+
records every file you edit, so do not maintain it by hand. Set acceptance to
|
|
207
|
+
the one deterministic check that decides done, THEN proceed. This contract will
|
|
208
|
+
be re-injected every turn until your request changes again."
|
|
202
209
|
elif [ "$scope_exists" != "1" ]; then
|
|
203
210
|
msg="INTENT ANCHOR (pre-compile) - no .scope.json found in $root, and the current
|
|
204
211
|
request was unavailable to scaffold from.
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# scope-gate-audit.sh - afterFileEdit "
|
|
2
|
+
# scope-gate-audit.sh - afterFileEdit "scope auto-record" (Cursor, Linux).
|
|
3
3
|
#
|
|
4
|
-
# Compuerta 1
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# Compuerta 1, mechanical edition: keep .scope.json's files[] in sync with what
|
|
5
|
+
# the agent ACTUALLY edits, with ZERO reliance on the model remembering to fill
|
|
6
|
+
# it. intent-anchor.sh writes the scaffold (intent locked from the prompt,
|
|
7
|
+
# files: [], acceptance: TODO); THIS hook appends every edited file to files[]
|
|
8
|
+
# as the edit happens. Net effect: the contract's files[] is always an accurate
|
|
9
|
+
# ledger of the session footprint, which final-review audits against intent.
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
11
|
+
# This REPLACES the old declared-scope VIOLATION advisory. When every edit is
|
|
12
|
+
# auto-recorded, an edit can never be "out of declared scope" - there is nothing
|
|
13
|
+
# to violate. The gate became a recorder. acceptance stays the model's to fill.
|
|
12
14
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# Advisory only: never blocks, never persists state, ALWAYS exits 0.
|
|
15
|
+
# Opt-in: silent if .scope.json does not exist in the repo root. Rewrites ONLY
|
|
16
|
+
# files[]; every other field is preserved. jq preferred, python3 fallback; if
|
|
17
|
+
# neither is present we fail open (no JSON tool = no record). ALWAYS exits 0.
|
|
17
18
|
# Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0
|
|
18
19
|
|
|
19
20
|
set +e
|
|
@@ -45,111 +46,45 @@ done
|
|
|
45
46
|
[ -n "$fp" ] || exit 0
|
|
46
47
|
rel="$fp"
|
|
47
48
|
case "$rel" in "$root"/*) rel="${rel#"$root"/}" ;; esac
|
|
49
|
+
rel="${rel#/}"
|
|
48
50
|
if is_cursor_config_path "$fp" || is_cursor_config_path "$rel"; then exit 0; fi
|
|
51
|
+
# Never record the contract file into itself.
|
|
52
|
+
[ "$rel" = ".scope.json" ] && exit 0
|
|
49
53
|
|
|
50
|
-
# --- opt-in gate: no .scope.json =
|
|
54
|
+
# --- opt-in gate: no .scope.json = nothing to maintain ---------------------
|
|
51
55
|
scope_file="$root/.scope.json"
|
|
52
56
|
[ -f "$scope_file" ] || exit 0
|
|
53
57
|
|
|
54
|
-
# ---
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
# --- auto-record $rel into files[] (jq preferred, python3 fallback) --------
|
|
59
|
+
# Clean the existing list (drop the scaffold placeholder + blanks), then add
|
|
60
|
+
# this edit if absent. Write only when the resulting files[] actually changed,
|
|
61
|
+
# so repeat edits of the same file do not churn the contract.
|
|
62
|
+
jq_prog='((.files // []) | map(select(type=="string" and . != "" and (startswith("<TODO")|not)))) as $c'
|
|
63
|
+
|
|
64
|
+
if have_jq; then
|
|
65
|
+
old_files="$(jq -c '.files // []' "$scope_file" 2>/dev/null)"
|
|
66
|
+
new_files="$(jq -c --arg rel "$rel" "$jq_prog | (if (\$c | index(\$rel)) then \$c else \$c + [\$rel] end)" "$scope_file" 2>/dev/null)"
|
|
67
|
+
[ -n "$new_files" ] || exit 0
|
|
68
|
+
[ "$new_files" = "$old_files" ] && exit 0
|
|
69
|
+
updated="$(jq --arg rel "$rel" "$jq_prog | .files = (if (\$c | index(\$rel)) then \$c else \$c + [\$rel] end)" "$scope_file" 2>/dev/null)"
|
|
70
|
+
[ -n "$updated" ] && printf '%s\n' "$updated" > "$scope_file"
|
|
71
|
+
elif have_py; then
|
|
72
|
+
I_FILE="$scope_file" I_REL="$rel" python3 -c '
|
|
73
|
+
import json, os, sys
|
|
74
|
+
path = os.environ["I_FILE"]; rel = os.environ["I_REL"]
|
|
71
75
|
try:
|
|
72
|
-
|
|
76
|
+
d = json.load(open(path, encoding="utf-8"))
|
|
73
77
|
except Exception:
|
|
74
|
-
sys.exit(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
print(f"__ACCEPT__{acceptance}")
|
|
85
|
-
sys.exit(0)
|
|
86
|
-
PYEOF
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
parsed="$(printf '%s' "$mout" | parse_result)"
|
|
90
|
-
rc=$?
|
|
91
|
-
[ "$rc" -eq 0 ] || exit 0 # 2=skipped, 3=in-scope, 1=parse-fail -> all silent
|
|
92
|
-
|
|
93
|
-
allow_growth="$(printf '%s\n' "$parsed" | grep '__AG__' | sed 's/__AG__//')"
|
|
94
|
-
intent="$(printf '%s\n' "$parsed" | grep '__INTENT__' | sed 's/__INTENT__//')"
|
|
95
|
-
acceptance="$(printf '%s\n' "$parsed" | grep '__ACCEPT__' | sed 's/__ACCEPT__//')"
|
|
96
|
-
|
|
97
|
-
# Read declared files for the message (best-effort)
|
|
98
|
-
declared_files="$(printf '%s' "$scope_file" | "$py" -c "
|
|
99
|
-
import json, sys
|
|
100
|
-
try:
|
|
101
|
-
d = json.load(open(sys.argv[1]))
|
|
102
|
-
print(', '.join(d.get('files', [])))
|
|
103
|
-
except Exception:
|
|
104
|
-
pass
|
|
105
|
-
" "$scope_file" 2>/dev/null)"
|
|
106
|
-
|
|
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
|
-
|
|
117
|
-
if [ "$allow_growth" = "1" ]; then
|
|
118
|
-
summary="Scope note - $rel is new vs your declared scope (growth allowed)"
|
|
119
|
-
body=" You touched a file outside your initial declared set. Since allow_growth is
|
|
120
|
-
true, this is not a violation, but justify it: add $rel to .scope.json or
|
|
121
|
-
explain why the scope grew.
|
|
122
|
-
|
|
123
|
-
Your success contract (acceptance): $acceptance_line
|
|
124
|
-
Does growing into $rel still serve that?"
|
|
125
|
-
else
|
|
126
|
-
summary="[SCOPE VIOLATION] $rel is NOT in your declared scope"
|
|
127
|
-
body=" Your contract (.scope.json):
|
|
128
|
-
intent: $intent
|
|
129
|
-
files: $declared_files
|
|
130
|
-
acceptance: $acceptance_line
|
|
131
|
-
|
|
132
|
-
You declared these files and touched one outside the set. Either:
|
|
133
|
-
1. Add $rel to .scope.json with a one-line justification, OR
|
|
134
|
-
2. Revert the change - it is out of scope for the declared intent.
|
|
135
|
-
|
|
136
|
-
Declared-editing: declare BEFORE you expand. Don't sneak edits past the gate."
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
msg="${summary}
|
|
140
|
-
|
|
141
|
-
${body}
|
|
142
|
-
|
|
143
|
-
(Advisory; disable: SCOPE_GATE_ENFORCE=0)"
|
|
144
|
-
|
|
145
|
-
# --- append to the shared pending file --------------------------------------
|
|
146
|
-
cid="$(safe_conversation_id "$input")"
|
|
147
|
-
pending="$(hooks_pending_dir)/feedback-${cid}.txt"
|
|
148
|
-
mkdir -p "$(dirname "$pending")" 2>/dev/null
|
|
149
|
-
if [ -s "$pending" ]; then
|
|
150
|
-
printf '\n\n---\n\n%s' "$msg" >> "$pending" 2>/dev/null
|
|
151
|
-
else
|
|
152
|
-
printf '%s' "$msg" >> "$pending" 2>/dev/null
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
files = d.get("files", []) or []
|
|
80
|
+
clean = [f for f in files if isinstance(f, str) and f and not f.startswith("<TODO")]
|
|
81
|
+
new = clean if rel in clean else clean + [rel]
|
|
82
|
+
if new == files:
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
d["files"] = new
|
|
85
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
86
|
+
json.dump(d, f, ensure_ascii=False, indent=2)
|
|
87
|
+
' 2>/dev/null
|
|
153
88
|
fi
|
|
154
89
|
|
|
155
90
|
exit 0
|
package/linux/hooks.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"command": "bash ~/.agents/hooks/scope-gate-audit.sh",
|
|
26
26
|
"timeout": 10,
|
|
27
27
|
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
28
|
-
"_comment": "10s (Compuerta 1):
|
|
28
|
+
"_comment": "10s (Compuerta 1, mechanical): scope auto-record. OPT-IN: only active when .scope.json exists in the repo root. intent-anchor scaffolds files:[]; this hook APPENDS every edited file to files[] (drops the scaffold placeholder, dedups, preserves all other fields) via jq with a python3 fallback, so files[] is always an accurate ledger of the session footprint that final-review audits against intent. No model effort required - the agent never maintains files[] by hand. Replaces the old [SCOPE VIOLATION] advisory (with auto-record an edit can never be out-of-declared-scope). No JSON tool / no .scope.json = silent. Never blocks, always exits 0. Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0."
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"command": "bash ~/.agents/hooks/anti-slop-audit.sh",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Thin self-review hooks for Cursor — the model is the auditor. Pruned + deduplicated: intent-anchor (auto-scaffolded .scope.json per prompt + per-turn re-injection against Salience Dilution), intent-trace final review, unified anti-slop checklist as single source of truth.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cursordoctrine": "bin/cli.mjs"
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
# 2. AUTO-CREATE / REGENERATE .scope.json: when the current <user_query>
|
|
18
18
|
# differs from the contract on disk (no contract yet, OR _intent_hash
|
|
19
19
|
# mismatch), the hook WRITES a scaffold to the REPO ROOT: intent locked
|
|
20
|
-
# from the prompt, files
|
|
21
|
-
#
|
|
22
|
-
# a
|
|
23
|
-
#
|
|
24
|
-
#
|
|
20
|
+
# from the prompt, files as an EMPTY array (scope-gate-audit.ps1 fills it
|
|
21
|
+
# mechanically as the agent edits - the agent never maintains files[] by
|
|
22
|
+
# hand), acceptance as a TODO the agent sets. This is the user-requested
|
|
23
|
+
# behavior: every new prompt -> a fresh .scope.json the agent works from.
|
|
24
|
+
# Fixed vs the broken 0.4.4 build: never writes to $HOME (bails if no real
|
|
25
|
+
# root resolves -> no ghost files), regenerates on prompt CHANGE not just
|
|
26
|
+
# on absence.
|
|
25
27
|
# 3. RE-INJECT on same-prompt turns: when the query is unchanged (contract
|
|
26
28
|
# already current), the hook re-injects the existing contract into the
|
|
27
29
|
# feedback bus so it stays in the model's attentional focus each turn.
|
|
@@ -101,7 +103,8 @@ if (Test-Path -LiteralPath $scopePath) {
|
|
|
101
103
|
$sj = Get-Content -LiteralPath $scopePath -Raw | ConvertFrom-Json
|
|
102
104
|
if ($sj.intent) { $scopeIntent = [string]$sj.intent }
|
|
103
105
|
if ($sj.acceptance) { $scopeAcceptance = [string]$sj.acceptance }
|
|
104
|
-
if ($sj.files) { $scopeFiles = ($sj.files -join ', ') }
|
|
106
|
+
if ($sj.files) { $scopeFiles = (@($sj.files) -join ', ') }
|
|
107
|
+
if ([string]::IsNullOrWhiteSpace($scopeFiles)) { $scopeFiles = '(none yet - auto-tracked as you edit)' }
|
|
105
108
|
$scopeExists = $true
|
|
106
109
|
# The contract is "stale" if its recorded intent hash != current query
|
|
107
110
|
# hash. We persist the query hash inside .scope.json under _intent_hash
|
|
@@ -116,8 +119,8 @@ if (Test-Path -LiteralPath $scopePath) {
|
|
|
116
119
|
# The user wants: every NEW prompt -> a fresh .scope.json the agent works from.
|
|
117
120
|
# So we WRITE the scaffold when (a) there is no valid contract, OR (b) the
|
|
118
121
|
# contract on disk is stale (its _intent_hash != current query hash). Intent is
|
|
119
|
-
# locked from the current <user_query>; files
|
|
120
|
-
# the agent
|
|
122
|
+
# locked from the current <user_query>; files starts EMPTY (scope-gate-audit
|
|
123
|
+
# auto-records edits into it); acceptance is a TODO the agent sets. Fixed vs 0.4.4:
|
|
121
124
|
# - NEVER writes to $HOME (bail above if no real root) -> no ghost files.
|
|
122
125
|
# - Regenerates on prompt CHANGE, not just on absence -> "each prompt, new file".
|
|
123
126
|
# - Records _intent_hash so staleness is self-contained in the file.
|
|
@@ -127,7 +130,7 @@ if ($shouldWrite) {
|
|
|
127
130
|
try {
|
|
128
131
|
$scaffold = [ordered]@{
|
|
129
132
|
intent = $currentQuery
|
|
130
|
-
files = @(
|
|
133
|
+
files = @()
|
|
131
134
|
acceptance = '<TODO: the one deterministic check that decides done>'
|
|
132
135
|
allow_growth = $false
|
|
133
136
|
_intent_hash = $currentHash
|
|
@@ -137,7 +140,7 @@ if ($shouldWrite) {
|
|
|
137
140
|
[System.IO.File]::WriteAllText($scopePath, $json, [System.Text.UTF8Encoding]::new($false))
|
|
138
141
|
$scopeIntent = $currentQuery
|
|
139
142
|
$scopeAcceptance = '<TODO: the one deterministic check that decides done>'
|
|
140
|
-
$scopeFiles = '
|
|
143
|
+
$scopeFiles = '(auto-tracked - the scope hook records every file you edit)'
|
|
141
144
|
$scopeExists = $true
|
|
142
145
|
$scopeStale = $false
|
|
143
146
|
$regenerated = $true
|
|
@@ -158,9 +161,10 @@ INTENT ANCHOR (scope regenerated) - .scope.json written for this prompt.
|
|
|
158
161
|
acceptance: $scopeAcceptance
|
|
159
162
|
|
|
160
163
|
The hook wrote a fresh scaffold to $scopePath from your current request. intent
|
|
161
|
-
is locked from what you just asked.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
is locked from what you just asked. files[] is AUTO-TRACKED - the scope hook
|
|
165
|
+
records every file you edit, so do not maintain it by hand. Set acceptance to
|
|
166
|
+
the one deterministic check that decides done, THEN proceed. This contract will
|
|
167
|
+
be re-injected every turn until your request changes again.
|
|
164
168
|
"@
|
|
165
169
|
} elseif (-not $scopeExists) {
|
|
166
170
|
$msg = @"
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# scope-gate-audit.ps1 - afterFileEdit "
|
|
1
|
+
# scope-gate-audit.ps1 - afterFileEdit "scope auto-record" (Cursor).
|
|
2
2
|
#
|
|
3
|
-
# Compuerta 1
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
3
|
+
# Compuerta 1, mechanical edition: keep .scope.json's files[] in sync with what
|
|
4
|
+
# the agent ACTUALLY edits, with ZERO reliance on the model remembering to fill
|
|
5
|
+
# it. intent-anchor.ps1 writes the scaffold (intent locked from the prompt,
|
|
6
|
+
# files: [], acceptance: TODO); THIS hook appends every edited file to files[]
|
|
7
|
+
# as the edit happens. Net effect: the contract's files[] is always an accurate
|
|
8
|
+
# ledger of the session footprint, which final-review audits against intent
|
|
9
|
+
# (the "you touched 8 files for a 1-line request - justify" axis).
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# This REPLACES the old declared-scope VIOLATION advisory. When every edit is
|
|
12
|
+
# auto-recorded, an edit can never be "out of declared scope" - there is nothing
|
|
13
|
+
# to violate. The gate became a recorder. acceptance stays the model's to fill:
|
|
14
|
+
# a deterministic success check cannot be derived mechanically.
|
|
14
15
|
#
|
|
15
|
-
#
|
|
16
|
-
# .
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# Advisory only: never blocks, never persists state, ALWAYS exits 0.
|
|
16
|
+
# Opt-in: silent if .scope.json does not exist in the repo root (no scaffold yet
|
|
17
|
+
# = nothing to maintain). Rewrites ONLY files[]; every other field (intent,
|
|
18
|
+
# acceptance, allow_growth, _intent_hash, _generated_by, ...) is preserved.
|
|
19
|
+
# Never blocks, never needs Python, ALWAYS exits 0.
|
|
20
20
|
# Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0
|
|
21
21
|
|
|
22
22
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
@@ -43,94 +43,55 @@ foreach ($k in 'file_path', 'path', 'filename', 'absolute_path', 'abs_path') {
|
|
|
43
43
|
if (-not $fp) { exit 0 }
|
|
44
44
|
$rel = ConvertTo-FwdPath $fp
|
|
45
45
|
if ($rel.StartsWith($root + '/', [System.StringComparison] 'OrdinalIgnoreCase')) { $rel = $rel.Substring($root.Length + 1) }
|
|
46
|
+
$rel = $rel.TrimStart('/')
|
|
46
47
|
if (Test-IsCursorConfigPath $fp) { exit 0 }
|
|
47
48
|
if (Test-IsCursorConfigPath $rel) { exit 0 }
|
|
49
|
+
# Never record the contract file into itself.
|
|
50
|
+
if ($rel -ieq '.scope.json') { exit 0 }
|
|
48
51
|
|
|
49
|
-
# --- opt-in gate: no .scope.json =
|
|
52
|
+
# --- opt-in gate: no .scope.json = nothing to maintain ---------------------
|
|
50
53
|
$scopeFile = "$root/.scope.json"
|
|
51
54
|
if (-not (Test-Path -LiteralPath $scopeFile)) { exit 0 }
|
|
52
55
|
|
|
53
|
-
# ---
|
|
54
|
-
$
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
$
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
$inScope = $false
|
|
73
|
-
try { $inScope = [bool]$payload.in_scope } catch { }
|
|
74
|
-
if ($inScope) { exit 0 }
|
|
75
|
-
|
|
76
|
-
# --- violation: compose advisory -------------------------------------------
|
|
77
|
-
$allowGrowth = $false
|
|
78
|
-
if ($payload.PSObject.Properties['allow_growth'] -and $payload.allow_growth) { $allowGrowth = $true }
|
|
79
|
-
$intent = ''
|
|
80
|
-
if ($payload.PSObject.Properties['intent']) { $intent = [string]$payload.intent }
|
|
81
|
-
$acceptance = ''
|
|
82
|
-
if ($payload.PSObject.Properties['acceptance']) { $acceptance = [string]$payload.acceptance }
|
|
83
|
-
|
|
84
|
-
# Read the declared files list for the message (best-effort; skip on failure)
|
|
85
|
-
$declaredFiles = ''
|
|
86
|
-
try {
|
|
87
|
-
$scopeJson = Get-Content -LiteralPath $scopeFile -Raw | ConvertFrom-Json
|
|
88
|
-
if ($scopeJson.files) { $declaredFiles = ($scopeJson.files -join ', ') }
|
|
89
|
-
} catch { }
|
|
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
|
-
|
|
96
|
-
if ($allowGrowth) {
|
|
97
|
-
# Growth is allowed: informational, not a violation
|
|
98
|
-
$summary = "Scope note - $rel is new vs your declared scope (growth allowed)"
|
|
99
|
-
$body = @"
|
|
100
|
-
You touched a file outside your initial declared set. Since allow_growth is
|
|
101
|
-
true, this is not a violation, but justify it: add $rel to .scope.json or
|
|
102
|
-
explain why the scope grew.
|
|
103
|
-
|
|
104
|
-
Your success contract (acceptance): $acceptanceLine
|
|
105
|
-
Does growing into $rel still serve that?
|
|
106
|
-
"@
|
|
107
|
-
} else {
|
|
108
|
-
# Hard violation: edited outside the declared contract
|
|
109
|
-
$summary = "[SCOPE VIOLATION] $rel is NOT in your declared scope"
|
|
110
|
-
$body = @"
|
|
111
|
-
Your contract (.scope.json):
|
|
112
|
-
intent: $intent
|
|
113
|
-
files: $declaredFiles
|
|
114
|
-
acceptance: $acceptanceLine
|
|
115
|
-
|
|
116
|
-
You declared these files and touched one outside the set. Either:
|
|
117
|
-
1. Add $rel to .scope.json with a one-line justification, OR
|
|
118
|
-
2. Revert the change - it is out of scope for the declared intent.
|
|
56
|
+
# --- load the contract; bail quietly on malformed JSON ---------------------
|
|
57
|
+
$sj = $null
|
|
58
|
+
try { $sj = Get-Content -LiteralPath $scopeFile -Raw | ConvertFrom-Json } catch { exit 0 }
|
|
59
|
+
if (-not $sj) { exit 0 }
|
|
60
|
+
|
|
61
|
+
# --- compute the new files[] -----------------------------------------------
|
|
62
|
+
# Start from existing files, drop the scaffold placeholder and blanks, then add
|
|
63
|
+
# this edit if it is not already recorded (case-insensitive, slash-normalized).
|
|
64
|
+
$existing = @()
|
|
65
|
+
if ($sj.PSObject.Properties['files'] -and $sj.files) { $existing = @($sj.files) }
|
|
66
|
+
|
|
67
|
+
$kept = @()
|
|
68
|
+
foreach ($e in $existing) {
|
|
69
|
+
if (-not $e) { continue }
|
|
70
|
+
$s = [string]$e
|
|
71
|
+
if ($s -match '^\s*<TODO') { continue } # drop the scaffold placeholder
|
|
72
|
+
if ([string]::IsNullOrWhiteSpace($s)) { continue }
|
|
73
|
+
$kept += $s
|
|
74
|
+
}
|
|
119
75
|
|
|
120
|
-
|
|
121
|
-
|
|
76
|
+
$already = $false
|
|
77
|
+
foreach ($f in $kept) {
|
|
78
|
+
if (([string]$f).Replace('\', '/').TrimStart('/') -ieq $rel) { $already = $true; break }
|
|
122
79
|
}
|
|
80
|
+
if (-not $already) { $kept += $rel }
|
|
123
81
|
|
|
124
|
-
|
|
82
|
+
# Only rewrite when files[] actually changed (avoid churning the file on every
|
|
83
|
+
# repeat edit of the same path).
|
|
84
|
+
$before = ($existing | ForEach-Object { [string]$_ }) -join '|'
|
|
85
|
+
$after = ($kept | ForEach-Object { [string]$_ }) -join '|'
|
|
86
|
+
if ($before -eq $after) { exit 0 }
|
|
125
87
|
|
|
126
|
-
# ---
|
|
127
|
-
$cid = Get-SafeConversationId $obj
|
|
128
|
-
$pending = Join-Path (Get-HooksPendingDir) "feedback-$cid.txt"
|
|
88
|
+
# --- write back, preserving every other field and its order ----------------
|
|
129
89
|
try {
|
|
130
|
-
|
|
131
|
-
$
|
|
132
|
-
|
|
133
|
-
|
|
90
|
+
$ordered = [ordered]@{}
|
|
91
|
+
foreach ($p in $sj.PSObject.Properties) { $ordered[$p.Name] = $p.Value }
|
|
92
|
+
$ordered['files'] = @($kept) # force array form under pwsh 7
|
|
93
|
+
$json = $ordered | ConvertTo-Json -Depth 8
|
|
94
|
+
[System.IO.File]::WriteAllText($scopeFile, $json, [System.Text.UTF8Encoding]::new($false))
|
|
134
95
|
} catch { }
|
|
135
96
|
|
|
136
97
|
exit 0
|
package/windows/hooks.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"command": "pwsh.exe -NoProfile -File ~/.agents/hooks/scope-gate-audit.ps1",
|
|
26
26
|
"timeout": 10,
|
|
27
27
|
"matcher": "^(Write|StrReplace|EditNotebook)$",
|
|
28
|
-
"_comment": "10s (Compuerta 1):
|
|
28
|
+
"_comment": "10s (Compuerta 1, mechanical): scope auto-record. OPT-IN: only active when .scope.json exists in the repo root. intent-anchor scaffolds files:[]; this hook APPENDS every edited file to files[] (drops the scaffold placeholder, dedups, preserves all other fields), so files[] is always an accurate ledger of the session footprint that final-review audits against intent. No model effort required - the agent never maintains files[] by hand. Replaces the old [SCOPE VIOLATION] advisory (with auto-record an edit can never be out-of-declared-scope). No JSON tool / no .scope.json = silent. Never blocks, always exits 0. Disable: HOOKS_ENFORCE=0 or SCOPE_GATE_ENFORCE=0."
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"command": "pwsh.exe -NoProfile -File ~/.agents/hooks/anti-slop-audit.ps1",
|