@devrik-tools/claude-gates 0.1.2 → 0.2.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/.claude-plugin/marketplace.json +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- package/registry.json +793 -686
|
@@ -1,203 +1,263 @@
|
|
|
1
|
-
// risk-level — denies an implementation delegation that does not declare its risk
|
|
2
|
-
// LEVEL (QUESTION|MICRO|STANDARD|HIGH-RISK), and denies a declared level that
|
|
3
|
-
// contradicts a real high-impact signal in the prompt itself (anything but HIGH-RISK).
|
|
4
|
-
//
|
|
5
|
-
// ── What this does and does not decide ──────────────────────────────────────────────
|
|
6
|
-
// Classifying a task correctly between QUESTION/MICRO/STANDARD stays the delegator's
|
|
7
|
-
// judgment — this gate cannot tell whether STANDARD was the "right" level for a task
|
|
8
|
-
// with no detectable high-impact signal. The only deterministic thing it can and does
|
|
9
|
-
// impose: the declaration EXISTS, and it does not contradict a real, non-quoted signal
|
|
10
|
-
// of high risk. The level vocabulary itself (the four tokens) is a fixed base, not a
|
|
11
|
-
// project param — only which signals count as high-impact, and which subagents are
|
|
12
|
-
// exempt, are configurable.
|
|
13
|
-
//
|
|
14
|
-
// ── Same two-stage intent pattern as intent-flow ────────────────────────────────────
|
|
15
|
-
// Stage 1: cheap lexical prefilter (implementation verb + high-impact signal present
|
|
16
|
-
// anywhere). Stage 2: strip quoted/templated text, require near co-occurrence between
|
|
17
|
-
// the verb and the signal, discard a verb whose object is a documentary deliverable.
|
|
18
|
-
// Over-declaring HIGH-RISK is never penalized — this gate only catches under-declaring.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
1
|
+
// risk-level — denies an implementation delegation that does not declare its risk
|
|
2
|
+
// LEVEL (QUESTION|MICRO|STANDARD|HIGH-RISK), and denies a declared level that
|
|
3
|
+
// contradicts a real high-impact signal in the prompt itself (anything but HIGH-RISK).
|
|
4
|
+
//
|
|
5
|
+
// ── What this does and does not decide ──────────────────────────────────────────────
|
|
6
|
+
// Classifying a task correctly between QUESTION/MICRO/STANDARD stays the delegator's
|
|
7
|
+
// judgment — this gate cannot tell whether STANDARD was the "right" level for a task
|
|
8
|
+
// with no detectable high-impact signal. The only deterministic thing it can and does
|
|
9
|
+
// impose: the declaration EXISTS, and it does not contradict a real, non-quoted signal
|
|
10
|
+
// of high risk. The level vocabulary itself (the four tokens) is a fixed base, not a
|
|
11
|
+
// project param — only which signals count as high-impact, and which subagents are
|
|
12
|
+
// exempt, are configurable.
|
|
13
|
+
//
|
|
14
|
+
// ── Same two-stage intent pattern as intent-flow ────────────────────────────────────
|
|
15
|
+
// Stage 1: cheap lexical prefilter (implementation verb + high-impact signal present
|
|
16
|
+
// anywhere). Stage 2: strip quoted/templated text, require near co-occurrence between
|
|
17
|
+
// the verb and the signal, discard a verb whose object is a documentary deliverable.
|
|
18
|
+
// Over-declaring HIGH-RISK is never penalized — this gate only catches under-declaring.
|
|
19
|
+
//
|
|
20
|
+
// ── Which declaration governs ────────────────────────────────────────────────────────
|
|
21
|
+
// A prompt can mention the LEVEL token more than once (a decoy/reference to a previous
|
|
22
|
+
// task, then the real declaration for THIS task). Taking the first match lets an early,
|
|
23
|
+
// irrelevant mention govern instead of the operative one. This gate instead takes the
|
|
24
|
+
// LAST declaration as operative (a delegator revising a decoy note downward writes the
|
|
25
|
+
// real value last); when multiple declarations disagree, it denies and asks for a single
|
|
26
|
+
// unambiguous LEVEL rather than guessing which one is real.
|
|
27
|
+
//
|
|
28
|
+
// ── readOnlySubagents is a declared label, not a verified capability ────────────────
|
|
29
|
+
// This hook cannot check what tools a named subagent actually has — the whitelist
|
|
30
|
+
// exemption is void whenever the prompt itself carries a mutation-risk signal (money/
|
|
31
|
+
// auth/data/write/deploy): the signal in the text outranks the label on the call.
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
runGate,
|
|
35
|
+
deny,
|
|
36
|
+
toolInGroups,
|
|
37
|
+
delegationPromptOf,
|
|
38
|
+
} from '../../lib/hook-io.mjs';
|
|
39
|
+
import { MUTATION_RISK_SIGNAL } from '../../lib/signals.mjs';
|
|
40
|
+
|
|
41
|
+
const GATE_ID = 'risk-level';
|
|
42
|
+
const CONFIG_KEY = 'requireDeclaredRiskLevel';
|
|
43
|
+
|
|
44
|
+
const DEFAULT_READ_ONLY_SUBAGENTS = ['explore', 'claude-code-guide', 'plan'];
|
|
45
|
+
const DEFAULT_HIGH_IMPACT_PATTERNS = [
|
|
46
|
+
'dinero|pago|cobro|money|payment',
|
|
47
|
+
'auth|autenticaci[oó]n|authentication|permiso|permission|credencial|credential|token|' +
|
|
48
|
+
'sesi[oó]n|session|seguridad|security|PII',
|
|
49
|
+
'borrar|delete|drop|migraci[oó]n|migration|schema',
|
|
50
|
+
'contrato|contract|irreversible|producci[oó]n|production|API p[uú]blica|public api',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const CO_OCCURRENCE_WINDOW = 100;
|
|
54
|
+
const VERB_OBJECT_WINDOW = 60;
|
|
55
|
+
// How much of an over-length prompt to quote back in a denial message.
|
|
56
|
+
const PROMPT_EXCERPT_LENGTH = 80;
|
|
57
|
+
|
|
58
|
+
function withUnicodeWordBoundary(alternatives) {
|
|
59
|
+
return new RegExp(
|
|
60
|
+
`(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
|
|
61
|
+
'iu',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A prompt-level signal that a whitelisted read-only subagent name should NOT be
|
|
66
|
+
// trusted to exempt this call: the name is a declared label, never a verified
|
|
67
|
+
// capability this hook can check, and real mutation risk in the text must win over it.
|
|
68
|
+
// Centralized in lib/signals.mjs (ES+EN) — see its header for the class this covers.
|
|
69
|
+
const MUTATION_RISK_SIGNAL_PATTERN = MUTATION_RISK_SIGNAL;
|
|
70
|
+
|
|
71
|
+
const IMPLEMENTATION_VERBS = withUnicodeWordBoundary(
|
|
72
|
+
'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
|
|
73
|
+
'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|escrib(í|e) c[oó]digo|escribir c[oó]digo|' +
|
|
74
|
+
'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
|
|
75
|
+
'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
|
|
76
|
+
'escrib(í|e)|escribir|implement|build|fix|migrate|modify|refactor|remove|rewrite|deploy|write',
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const QUERY_VERBS = withUnicodeWordBoundary(
|
|
80
|
+
'explica|explic(á|ar)|qu[eé]|c[oó]mo|muestra|mostr(á|ar)|analiza|analiz(á|ar)|investiga|' +
|
|
81
|
+
'investig(á|ar)|revisa|revis(á|ar)|audita|audit(á|ar)|diagnostica|diagnostic(á|ar)|' +
|
|
82
|
+
'explain|what|how|show|analyze|investigate|review|audit|diagnose',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const DOCUMENTARY_DELIVERABLE_PATTERN = withUnicodeWordBoundary(
|
|
86
|
+
'documento|documentaci[oó]n|reporte|informe|diagrama|readme|wiki|changelog|' +
|
|
87
|
+
'p[aá]gina de documentaci[oó]n|archivo html|p[aá]gina html|markdown|document|documentation|report',
|
|
88
|
+
);
|
|
89
|
+
const DOCUMENTARY_EXTENSION_PATTERN = /\.(md|html?|adoc)\b/iu;
|
|
90
|
+
|
|
91
|
+
/** Declared LEVEL: one of the four fixed tokens, near the word "level"/"classification"
|
|
92
|
+
* (in Spanish or English) so a stray mention elsewhere in the prompt is not mistaken
|
|
93
|
+
* for a declaration. */
|
|
94
|
+
const DECLARED_LEVEL_PATTERN =
|
|
95
|
+
/(nivel|level|clasificaci[oó]n|classification)[^\n]{0,25}?\b(QUESTION|MICRO|STANDARD|HIGH-RISK)\b/iu;
|
|
96
|
+
|
|
97
|
+
function isReadOnlySubagentName(toolInput, readOnlySubagents) {
|
|
98
|
+
const type = String(
|
|
99
|
+
toolInput.subagent_type ?? toolInput.subagentType ?? '',
|
|
100
|
+
).toLowerCase();
|
|
101
|
+
return new Set(readOnlySubagents.map((name) => name.toLowerCase())).has(type);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A whitelisted subagent name exempts a call ONLY when the prompt carries no
|
|
105
|
+
* mutation-risk signal. The name is a declared label, never a verified capability this
|
|
106
|
+
* hook can check — a real risk signal in the text must win over it. */
|
|
107
|
+
function isReadOnlySubagent(toolInput, prompt, readOnlySubagents) {
|
|
108
|
+
if (!isReadOnlySubagentName(toolInput, readOnlySubagents)) return false;
|
|
109
|
+
return !MUTATION_RISK_SIGNAL_PATTERN.test(prompt);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isImplementationRequest(prompt) {
|
|
113
|
+
return IMPLEMENTATION_VERBS.test(prompt);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isExemptQuery(prompt) {
|
|
117
|
+
if (isImplementationRequest(prompt)) return false;
|
|
118
|
+
return QUERY_VERBS.test(prompt);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stripQuoted(text) {
|
|
122
|
+
return text
|
|
123
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
124
|
+
.replace(/"[^"\n]{0,300}"/g, ' ')
|
|
125
|
+
.replace(/'[^'\n]{0,300}'/g, ' ');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function allMatches(pattern, text) {
|
|
129
|
+
const flags = pattern.flags.includes('g')
|
|
130
|
+
? pattern.flags
|
|
131
|
+
: `${pattern.flags}g`;
|
|
132
|
+
return [...text.matchAll(new RegExp(pattern.source, flags))];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function verbProducesDocument(text, verbMatch) {
|
|
136
|
+
const from = verbMatch.index;
|
|
137
|
+
const to = from + verbMatch[0].length + VERB_OBJECT_WINDOW;
|
|
138
|
+
const objectOfVerb = text.slice(from, to);
|
|
139
|
+
return (
|
|
140
|
+
DOCUMENTARY_DELIVERABLE_PATTERN.test(objectOfVerb) ||
|
|
141
|
+
DOCUMENTARY_EXTENSION_PATTERN.test(objectOfVerb)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Returns the matched high-impact signal text if real (non-quoted) intent survives
|
|
146
|
+
* stage 2, else null. Returns null immediately when stage 1 finds no candidate. */
|
|
147
|
+
function realHighImpactSignal(prompt, highImpactPattern) {
|
|
148
|
+
const isCandidate =
|
|
149
|
+
IMPLEMENTATION_VERBS.test(prompt) && highImpactPattern.test(prompt);
|
|
150
|
+
if (!isCandidate) return null;
|
|
151
|
+
|
|
152
|
+
const text = stripQuoted(prompt);
|
|
153
|
+
const verbs = allMatches(IMPLEMENTATION_VERBS, text);
|
|
154
|
+
const signals = allMatches(highImpactPattern, text);
|
|
155
|
+
|
|
156
|
+
for (const verb of verbs) {
|
|
157
|
+
if (verbProducesDocument(text, verb)) continue;
|
|
158
|
+
for (const signal of signals) {
|
|
159
|
+
if (Math.abs(signal.index - verb.index) <= CO_OCCURRENCE_WINDOW)
|
|
160
|
+
return signal[0];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every LEVEL declaration in the prompt, in order of appearance. */
|
|
167
|
+
function declaredLevels(prompt) {
|
|
168
|
+
const withGlobal = new RegExp(
|
|
169
|
+
DECLARED_LEVEL_PATTERN.source,
|
|
170
|
+
`${DECLARED_LEVEL_PATTERN.flags}g`,
|
|
171
|
+
);
|
|
172
|
+
return [...prompt.matchAll(withGlobal)].map((match) => match[2].toUpperCase());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The operative LEVEL: the LAST declaration in the prompt (a delegator who corrects an
|
|
176
|
+
* earlier decoy/reference mention writes the real value last). Returns `{ level }` when
|
|
177
|
+
* every declaration agrees or there is exactly one; returns `{ ambiguous: true }` when
|
|
178
|
+
* two or more DIFFERENT levels are declared — the gate cannot know which one governs,
|
|
179
|
+
* so it asks for a single unambiguous LEVEL rather than silently picking one. */
|
|
180
|
+
function operativeLevel(prompt) {
|
|
181
|
+
const levels = declaredLevels(prompt);
|
|
182
|
+
if (levels.length === 0) return { level: null };
|
|
183
|
+
const distinct = new Set(levels);
|
|
184
|
+
if (distinct.size > 1) return { ambiguous: true, levels };
|
|
185
|
+
return { level: levels[levels.length - 1] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function denyNoLevelDeclared(prompt) {
|
|
189
|
+
const excerpt = prompt.slice(0, PROMPT_EXCERPT_LENGTH);
|
|
190
|
+
const ellipsis = prompt.length > PROMPT_EXCERPT_LENGTH ? '…' : '';
|
|
191
|
+
deny(
|
|
192
|
+
GATE_ID,
|
|
193
|
+
`This implementation delegation ("${excerpt}${ellipsis}") does not declare its risk LEVEL. Add a ` +
|
|
194
|
+
'line such as "LEVEL: STANDARD" (or QUESTION/MICRO/HIGH-RISK, whichever fits) before relaunching ' +
|
|
195
|
+
'this delegation.',
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function denyAmbiguousLevel(levels) {
|
|
200
|
+
deny(
|
|
201
|
+
GATE_ID,
|
|
202
|
+
`This delegation declares multiple different risk levels (${[...new Set(levels)].join(', ')}) — ` +
|
|
203
|
+
'it is not clear which one governs this task. Declare a single, unambiguous LEVEL for this ' +
|
|
204
|
+
'delegation (remove any decoy/reference mention of a different level) before relaunching.',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function denyLevelContradictsSignal(level, signal) {
|
|
209
|
+
deny(
|
|
210
|
+
GATE_ID,
|
|
211
|
+
`This delegation declares LEVEL: ${level}, but the request touches the risk signal "${signal}" near ` +
|
|
212
|
+
'an implementation verb (not quoted, not the topic of a documentary deliverable) — that requires ' +
|
|
213
|
+
`LEVEL: HIGH-RISK, not ${level}. Raise the declaration to HIGH-RISK before relaunching.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildHighImpactPattern(highImpactPatterns) {
|
|
218
|
+
return new RegExp(
|
|
219
|
+
(highImpactPatterns ?? []).map((source) => `(?:${source})`).join('|'),
|
|
220
|
+
'iu',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
runGate(
|
|
225
|
+
{
|
|
226
|
+
id: GATE_ID,
|
|
227
|
+
configKey: CONFIG_KEY,
|
|
228
|
+
enabledByDefault: false,
|
|
229
|
+
defaultParams: {
|
|
230
|
+
highImpactPatterns: DEFAULT_HIGH_IMPACT_PATTERNS,
|
|
231
|
+
readOnlySubagents: DEFAULT_READ_ONLY_SUBAGENTS,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
({ toolName, toolInput, parameters }) => {
|
|
235
|
+
if (!toolInGroups(toolName, ['delegation'])) return;
|
|
236
|
+
|
|
237
|
+
const prompt = delegationPromptOf(toolInput);
|
|
238
|
+
if (!prompt.trim()) return;
|
|
239
|
+
if (isReadOnlySubagent(toolInput, prompt, parameters.readOnlySubagents))
|
|
240
|
+
return;
|
|
241
|
+
if (isExemptQuery(prompt)) return;
|
|
242
|
+
if (!isImplementationRequest(prompt)) return;
|
|
243
|
+
|
|
244
|
+
const highImpactPattern = buildHighImpactPattern(
|
|
245
|
+
parameters.highImpactPatterns,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const { level, ambiguous, levels } = operativeLevel(prompt);
|
|
249
|
+
const signal = realHighImpactSignal(prompt, highImpactPattern);
|
|
250
|
+
|
|
251
|
+
if (ambiguous) {
|
|
252
|
+
denyAmbiguousLevel(levels);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!level) {
|
|
256
|
+
denyNoLevelDeclared(prompt.trim());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (signal && level !== 'HIGH-RISK') {
|
|
260
|
+
denyLevelContradictsSignal(level, signal);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Edge-case audit for risk-level. Each test demonstrates a confirmed BUG or an OK.
|
|
2
|
+
// Run: node --test risk-level.edge.test.mjs
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
12
|
+
|
|
13
|
+
function runGate(payload, { config } = {}) {
|
|
14
|
+
const project = mkdtempSync(join(tmpdir(), 'risk-level-edge-'));
|
|
15
|
+
mkdirSync(join(project, '.git'));
|
|
16
|
+
if (config) {
|
|
17
|
+
mkdirSync(join(project, '.ai'));
|
|
18
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
19
|
+
}
|
|
20
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
21
|
+
input: JSON.stringify(payload),
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
cwd: project,
|
|
24
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
25
|
+
});
|
|
26
|
+
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isDeny(result) {
|
|
30
|
+
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ENABLED = { config: { gates: { requireDeclaredRiskLevel: true } } };
|
|
34
|
+
|
|
35
|
+
const SENSITIVE_NO_LEVEL =
|
|
36
|
+
'Implementa el cobro del pago con la nueva pasarela de dinero para el checkout.';
|
|
37
|
+
|
|
38
|
+
test('FIXED: a delegation tool name outside the native list is now caught via toolInGroups (MCP delegation signal)', () => {
|
|
39
|
+
const result = runGate(
|
|
40
|
+
{ tool_name: 'mcp__orchestrator__spawn_agent', tool_input: { prompt: SENSITIVE_NO_LEVEL } },
|
|
41
|
+
ENABLED,
|
|
42
|
+
);
|
|
43
|
+
assert.ok(isDeny(result), 'gate now denies the no-level prompt under the MCP tool name too');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('FIXED: prompt carried in a field other than prompt/description/task is now read via delegationPromptOf', () => {
|
|
47
|
+
const result = runGate(
|
|
48
|
+
{ tool_name: 'Agent', tool_input: { instructions: SENSITIVE_NO_LEVEL } },
|
|
49
|
+
ENABLED,
|
|
50
|
+
);
|
|
51
|
+
assert.ok(isDeny(result), 'gate now sees the prompt under "instructions" and denies it for missing LEVEL');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('FIXED: a mutation-risk signal in the prompt overrides a whitelisted read-only subagent name', () => {
|
|
55
|
+
const result = runGate(
|
|
56
|
+
{ tool_name: 'Agent', tool_input: { prompt: SENSITIVE_NO_LEVEL, subagent_type: 'plan' } },
|
|
57
|
+
ENABLED,
|
|
58
|
+
);
|
|
59
|
+
assert.ok(
|
|
60
|
+
isDeny(result),
|
|
61
|
+
'gate no longer exempts a real money-mutation delegation solely because subagent_type says "plan"',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('FIXED: the LAST level declaration governs, not the first (a decoy early mention no longer wins)', () => {
|
|
66
|
+
// Previously DECLARED_LEVEL_PATTERN.exec(prompt) (no /g) took only the first match, so
|
|
67
|
+
// a decoy HIGH-RISK mention near the top let a real MICRO-declared money mutation
|
|
68
|
+
// through. operativeLevel() now takes the LAST declaration as operative: MICRO governs
|
|
69
|
+
// here and the real money-mutation signal contradicts it, so the gate denies.
|
|
70
|
+
const prompt =
|
|
71
|
+
'NIVEL: HIGH-RISK (nota: eso era para la tarea anterior, ignorar). Para esta tarea: NIVEL: MICRO. ' +
|
|
72
|
+
'Implementa el cobro del pago con la nueva pasarela de dinero para el checkout.';
|
|
73
|
+
const result = runGate({ tool_name: 'Agent', tool_input: { prompt } }, ENABLED);
|
|
74
|
+
assert.ok(
|
|
75
|
+
isDeny(result),
|
|
76
|
+
'gate now denies: the operative (last) declaration is MICRO, which contradicts the real money-mutation signal',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('FIXED: two DIFFERENT levels that both look operative are denied as ambiguous, not silently resolved', () => {
|
|
81
|
+
const prompt =
|
|
82
|
+
'NIVEL: MICRO. Mas adelante, NIVEL: HIGH-RISK. Arregla el boton que no cambia de color en la pagina.';
|
|
83
|
+
const result = runGate({ tool_name: 'Agent', tool_input: { prompt } }, ENABLED);
|
|
84
|
+
assert.ok(
|
|
85
|
+
isDeny(result),
|
|
86
|
+
'gate denies and asks for a single unambiguous LEVEL when multiple different levels are declared',
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('OK: repeating the SAME level twice is not treated as ambiguous', () => {
|
|
91
|
+
const result = runGate(
|
|
92
|
+
{
|
|
93
|
+
tool_name: 'Agent',
|
|
94
|
+
tool_input: {
|
|
95
|
+
prompt:
|
|
96
|
+
'NIVEL: STANDARD. Como se declaro, NIVEL: STANDARD. Arregla el boton que no cambia de color.',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
ENABLED,
|
|
100
|
+
);
|
|
101
|
+
assert.equal(result, null);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('OK: same sensitive prompt without a read-only subagent name and no level is denied', () => {
|
|
105
|
+
const result = runGate({ tool_name: 'Agent', tool_input: { prompt: SENSITIVE_NO_LEVEL } }, ENABLED);
|
|
106
|
+
assert.ok(isDeny(result));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('OK: Task and invoke_subagent tool names are both covered', () => {
|
|
110
|
+
assert.ok(isDeny(runGate({ tool_name: 'Task', tool_input: { prompt: SENSITIVE_NO_LEVEL } }, ENABLED)));
|
|
111
|
+
assert.ok(
|
|
112
|
+
isDeny(
|
|
113
|
+
runGate({ tool_name: 'invoke_subagent', tool_input: { prompt: SENSITIVE_NO_LEVEL } }, ENABLED),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
});
|